Compare commits
8 Commits
f646b6530a
...
previous
| Author | SHA1 | Date | |
|---|---|---|---|
| 229c100cc4 | |||
| 9a4897bf0c | |||
| b2a9350d0f | |||
| 27bdb942ab | |||
| 7eaab16e30 | |||
| 6a2dd860fa | |||
| ca8754cd94 | |||
| 8fdc0d6359 |
@@ -1585,6 +1585,156 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive multiple contracts in a batch operation
|
||||
*/
|
||||
public function archiveBatch(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'contracts' => 'required|array',
|
||||
'contracts.*' => 'required|uuid|exists:contracts,uuid',
|
||||
'reactivate' => 'boolean',
|
||||
]);
|
||||
|
||||
$reactivate = $validated['reactivate'] ?? false;
|
||||
|
||||
// Get archive setting
|
||||
$setting = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->whereIn('strategy', ['immediate', 'manual'])
|
||||
->where('reactivate', $reactivate)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
if (! $setting) {
|
||||
\Log::warning('No archive settings found for batch archive');
|
||||
return back()->with('flash', [
|
||||
'error' => 'No archive settings found',
|
||||
]);
|
||||
}
|
||||
|
||||
$executor = app(\App\Services\Archiving\ArchiveExecutor::class);
|
||||
$successCount = 0;
|
||||
$skippedCount = 0;
|
||||
$errors = [];
|
||||
|
||||
foreach ($validated['contracts'] as $contractUuid) {
|
||||
try {
|
||||
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
|
||||
|
||||
// Skip if contract is already archived (active = 0)
|
||||
if (!$contract->active) {
|
||||
$skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$clientCase = $contract->clientCase;
|
||||
|
||||
$context = [
|
||||
'contract_id' => $contract->id,
|
||||
'client_case_id' => $clientCase->id,
|
||||
'account_id' => $contract->account->id ?? null,
|
||||
];
|
||||
|
||||
// Execute archive setting
|
||||
$executor->executeSetting($setting, $context, \Auth::id());
|
||||
|
||||
// Transaction for segment updates and activity logging
|
||||
\DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) {
|
||||
// Create activity log
|
||||
if ($setting->action_id && $setting->decision_id) {
|
||||
$activityData = [
|
||||
'client_case_id' => $clientCase->id,
|
||||
'action_id' => $setting->action_id,
|
||||
'decision_id' => $setting->decision_id,
|
||||
'note' => ($reactivate)
|
||||
? "Ponovno aktivirana pogodba $contract->reference"
|
||||
: "Arhivirana pogodba $contract->reference",
|
||||
];
|
||||
|
||||
try {
|
||||
\App\Models\Activity::create($activityData);
|
||||
} catch (Exception $e) {
|
||||
\Log::warning('Activity could not be created during batch archive');
|
||||
}
|
||||
}
|
||||
|
||||
// Move to archive segment if specified
|
||||
if ($setting->segment_id) {
|
||||
$segmentId = $setting->segment_id;
|
||||
|
||||
// Deactivate all current segments
|
||||
$contract->segments()
|
||||
->allRelatedIds()
|
||||
->map(fn (int $val) => $contract->segments()->updateExistingPivot($val, [
|
||||
'active' => false,
|
||||
'updated_at' => now(),
|
||||
]));
|
||||
|
||||
// Activate archive segment
|
||||
if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) {
|
||||
$contract->attachedSegments()->updateExistingPivot($segmentId, [
|
||||
'active' => true,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
} else {
|
||||
$contract->segments()->attach($segmentId, [
|
||||
'active' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel pending field jobs
|
||||
$contract->fieldJobs()
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->update([
|
||||
'cancelled_at' => date('Y-m-d'),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Error archiving contract in batch', [
|
||||
'uuid' => $contractUuid,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$errors[] = [
|
||||
'uuid' => $contractUuid,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$message = "Archived $successCount contracts";
|
||||
if ($skippedCount > 0) {
|
||||
$message .= ", skipped $skippedCount already archived";
|
||||
}
|
||||
$message .= ", " . count($errors) . " failed";
|
||||
|
||||
return back()->with('flash', [
|
||||
'error' => $message,
|
||||
'details' => $errors,
|
||||
]);
|
||||
}
|
||||
|
||||
$message = $reactivate
|
||||
? "Successfully reactivated $successCount contracts"
|
||||
: "Successfully archived $successCount contracts";
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
$message .= " ($skippedCount already archived)";
|
||||
}
|
||||
|
||||
return back()->with('flash', [
|
||||
'success' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emergency: recreate a missing / soft-deleted person for a client case and re-link related data.
|
||||
*/
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Account extends Model
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\Person/AccountFactory> */
|
||||
use SoftDeletes;
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
|
||||
@@ -46,6 +46,7 @@ class Person extends Model
|
||||
'group_id',
|
||||
'type_id',
|
||||
'user_id',
|
||||
'employer'
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
use App\Models\Person\PersonPhone;
|
||||
use App\Models\Person\PersonType;
|
||||
use App\Models\Person\PhoneType;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -1632,7 +1633,7 @@ private function upsertAccount(Import $import, array $mapped, $mappings, bool $h
|
||||
|
||||
$existing = Account::query()
|
||||
->where('contract_id', $contractId)
|
||||
->where('reference', $reference)
|
||||
//->where('reference', $reference)
|
||||
->where('active', 1)
|
||||
->first();
|
||||
|
||||
@@ -1655,6 +1656,10 @@ private function upsertAccount(Import $import, array $mapped, $mappings, bool $h
|
||||
$value = $acc[$field] ?? null;
|
||||
if (in_array($field, ['balance_amount', 'initial_amount'], true) && is_string($value)) {
|
||||
$value = $this->normalizeDecimal($value);
|
||||
// Ensure the normalized value is numeric, otherwise default to 0
|
||||
if ($value === '' || $value === '-' || ! is_numeric($value)) {
|
||||
$value = 0;
|
||||
}
|
||||
}
|
||||
// Convert empty string to 0 for amount fields
|
||||
if (in_array($field, ['balance_amount', 'initial_amount'], true) && ($value === '' || $value === null)) {
|
||||
@@ -1688,8 +1693,12 @@ private function upsertAccount(Import $import, array $mapped, $mappings, bool $h
|
||||
if ($existing) {
|
||||
// Build non-null changes for account fields
|
||||
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
|
||||
// Track balance change
|
||||
$oldBalance = (float) ($existing->balance_amount ?? 0);
|
||||
// Track balance change - normalize in case DB has malformed data
|
||||
$rawBalance = $existing->balance_amount ?? 0;
|
||||
if (is_string($rawBalance) && $rawBalance !== '') {
|
||||
$rawBalance = $this->normalizeDecimal($rawBalance);
|
||||
}
|
||||
$oldBalance = is_numeric($rawBalance) ? (float) $rawBalance : 0;
|
||||
// Note: meta merging for contracts is handled in upsertContractChain, not here
|
||||
if (! empty($changes)) {
|
||||
$existing->fill($changes);
|
||||
@@ -1698,7 +1707,11 @@ private function upsertAccount(Import $import, array $mapped, $mappings, bool $h
|
||||
|
||||
// If balance_amount changed and this wasn't caused by a payment (we are in account upsert), log an activity with before/after
|
||||
if (array_key_exists('balance_amount', $changes)) {
|
||||
$newBalance = (float) ($existing->balance_amount ?? 0);
|
||||
$rawNewBalance = $existing->balance_amount ?? 0;
|
||||
if (is_string($rawNewBalance) && $rawNewBalance !== '') {
|
||||
$rawNewBalance = $this->normalizeDecimal($rawNewBalance);
|
||||
}
|
||||
$newBalance = is_numeric($rawNewBalance) ? (float) $rawNewBalance : 0;
|
||||
if ($newBalance !== $oldBalance) {
|
||||
try {
|
||||
$contractId = $existing->contract_id;
|
||||
@@ -2974,7 +2987,7 @@ private function findOrCreatePersonId(array $p): ?int
|
||||
// Create person if any fields present; ensure required foreign keys
|
||||
if (! empty($p)) {
|
||||
$data = [];
|
||||
foreach (['first_name', 'last_name', 'full_name', 'tax_number', 'social_security_number', 'birthday', 'gender', 'description', 'group_id', 'type_id'] as $k) {
|
||||
foreach (['first_name', 'last_name', 'full_name', 'tax_number', 'social_security_number', 'birthday', 'gender', 'description', 'group_id', 'type_id', 'employer'] as $k) {
|
||||
if (array_key_exists($k, $p)) {
|
||||
$data[$k] = $p[$k];
|
||||
}
|
||||
@@ -2987,6 +3000,16 @@ private function findOrCreatePersonId(array $p): ?int
|
||||
$data['full_name'] = trim($fn.' '.$ln);
|
||||
}
|
||||
}
|
||||
|
||||
// normalise birthday date
|
||||
if (!empty($data['birthday'])) {
|
||||
try {
|
||||
$data['birthday'] = date('Y-m-d', strtotime($data['birthday']));
|
||||
} catch (Exception $e) {
|
||||
Log::warning('ImportProcessor::findOrCreatePersonId ' . $e->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
// ensure required group/type ids
|
||||
$data['group_id'] = $data['group_id'] ?? $this->getDefaultPersonGroupId();
|
||||
$data['type_id'] = $data['type_id'] ?? $this->getDefaultPersonTypeId();
|
||||
@@ -3163,10 +3186,38 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array
|
||||
if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') {
|
||||
$addrData['country'] = 'SLO';
|
||||
}
|
||||
|
||||
if (!empty($addrData['city']) && empty($addrData['post_code'])) {
|
||||
if (preg_match('/^\d{3,}\s+/',trim($addrData['city']))) {
|
||||
$cleanStrCity = str($addrData['city'])->squish()->value();
|
||||
$splitCity = preg_split('/\s/', $cleanStrCity, 2);
|
||||
if (count($splitCity) >= 2) {
|
||||
$addrData['post_code'] = $splitCity[0];
|
||||
$addrData['city'] = $splitCity[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Compare addresses with all spaces removed to handle whitespace variations
|
||||
$addressLineNoSpaces = preg_replace('/\s+/', '', $addressLine);
|
||||
/*$addressLineNoSpaces = preg_replace('/\s+/', '', $addressLine);
|
||||
|
||||
|
||||
$existing = PersonAddress::where('person_id', $personId)
|
||||
->whereRaw("REPLACE(address, ' ', '') = ?", [$addressLineNoSpaces])
|
||||
->first();*/
|
||||
|
||||
// Build search query combining address, post_code and city
|
||||
$searchParts = [$addrData['address']];
|
||||
if (!empty($addrData['post_code'])) {
|
||||
$searchParts[] = $addrData['post_code'];
|
||||
}
|
||||
if (!empty($addrData['city'])) {
|
||||
$searchParts[] = $addrData['city'];
|
||||
}
|
||||
|
||||
$searchQuery = implode(' ', $searchParts);
|
||||
// Use fulltext search (GIN index optimized)
|
||||
$existing = PersonAddress::query()->where('person_id', $personId)
|
||||
->whereRaw("search_vector @@ plainto_tsquery('simple', ?)", [$searchQuery])
|
||||
->first();
|
||||
|
||||
$applyInsert = [];
|
||||
@@ -3211,6 +3262,11 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array
|
||||
$data['person_id'] = $personId;
|
||||
$data['country'] = $data['country'] ?? 'SLO';
|
||||
$data['type_id'] = $data['type_id'] ?? $this->getDefaultAddressTypeId();
|
||||
|
||||
if (!empty($addrData['post_code']) && $addrData['post_code'] !== '0' && !isset($applyUpdate['post_code'])) {
|
||||
$data['post_code'] = $addrData['post_code'];
|
||||
}
|
||||
|
||||
try {
|
||||
$created = PersonAddress::create($data);
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('person', function (Blueprint $table){
|
||||
$table->string('employer', 125)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('person', function (Blueprint $table){
|
||||
$table->dropColumn('employer');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Add a generated tsvector column for fulltext search
|
||||
DB::statement("
|
||||
ALTER TABLE person_addresses
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('simple',
|
||||
coalesce(address, '') || ' ' ||
|
||||
coalesce(post_code, '') || ' ' ||
|
||||
coalesce(city, '')
|
||||
)
|
||||
) STORED
|
||||
");
|
||||
|
||||
// Create GIN index on the tsvector column for fast fulltext search
|
||||
DB::statement('CREATE INDEX person_addresses_search_vector_idx ON person_addresses USING GIN(search_vector)');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('person_addresses', function (Blueprint $table) {
|
||||
$table->dropIndex('person_addresses_search_vector_idx');
|
||||
$table->dropColumn('search_vector');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,7 @@ public function run(): void
|
||||
'key' => 'person',
|
||||
'canonical_root' => 'person',
|
||||
'label' => 'Person',
|
||||
'fields' => ['first_name', 'last_name', 'full_name', 'gender', 'birthday', 'tax_number', 'social_security_number', 'description'],
|
||||
'fields' => ['first_name', 'last_name', 'full_name', 'gender', 'birthday', 'tax_number', 'social_security_number', 'description', 'employer'],
|
||||
'field_aliases' => [
|
||||
'dob' => 'birthday',
|
||||
'date_of_birth' => 'birthday',
|
||||
@@ -30,6 +30,7 @@ public function run(): void
|
||||
['pattern' => '/^(spol|gender)\b/i', 'field' => 'gender'],
|
||||
['pattern' => '/^(rojstvo|datum\s*rojstva|dob|birth|birthday|date\s*of\s*birth)\b/i', 'field' => 'birthday'],
|
||||
['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'],
|
||||
['pattern' => '/^(delodajalec|služba)\b/i', 'field' => 'employer']
|
||||
],
|
||||
'ui' => ['order' => 1],
|
||||
],
|
||||
|
||||
@@ -580,6 +580,19 @@ const openSmsDialog = (phone) => {
|
||||
// Load contracts for this case (for contract/account placeholders)
|
||||
loadContractsForCase();
|
||||
};
|
||||
// Format YYYY-MM-DD (or ISO date) to dd.mm.yyyy
|
||||
function formatDate(value) {
|
||||
if (!value) return "-";
|
||||
try {
|
||||
const iso = String(value).split("T")[0];
|
||||
const parts = iso.split("-");
|
||||
if (parts.length !== 3) return value;
|
||||
const [y, m, d] = parts;
|
||||
return `${d.padStart(2, "0")}.${m.padStart(2, "0")}.${y}`;
|
||||
} catch (e) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
const loadContractsForCase = async () => {
|
||||
try {
|
||||
const url = route("clientCase.contracts.list", { client_case: props.clientCaseUuid });
|
||||
@@ -640,43 +653,86 @@ const submitSms = () => {
|
||||
</div>
|
||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-2">
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-xs leading-5 md:text-sm text-gray-500">Nu.</p>
|
||||
<p class="text-xs leading-5 md:text-sm text-gray-500">Primer ref.</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">{{ person.nu }}</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Name.</p>
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Naziv</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.full_name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Tax NU.</p>
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Davčna</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.tax_number }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Social security NU.</p>
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Emšo</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.social_security_number }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1">
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Address</p>
|
||||
<div
|
||||
v-if="clientCaseUuid"
|
||||
class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-1"
|
||||
>
|
||||
<div class="col-span-full lg:col-span-1 rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Naslov</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ getMainAddress(person.addresses) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Phone</p>
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Telefon</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ getMainPhone(person.phones) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="md:col-span-full lg:col-span-1 rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Description</p>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Datum rojstva</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ formatDate(person.birthday) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-2 mt-1">
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Naslov</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ getMainAddress(person.addresses) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Telefon</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ getMainPhone(person.phones) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="clientCaseUuid"
|
||||
class="grid grid-rows-* grid-cols-1 lg:grid-cols-2 gap-2 mt-1"
|
||||
>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Delodajalec</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.employer }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Opis</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-2 mt-1">
|
||||
<div class="col-span-full rounded p-2 shadow">
|
||||
<p class="text-sm leading-5 md:text-sm text-gray-500">Opis</p>
|
||||
<p class="text-sm md:text-base leading-7 text-gray-900">
|
||||
{{ person.description }}
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import { Link, router } from "@inertiajs/vue3";
|
||||
import { Link, router, useForm, usePage } from "@inertiajs/vue3";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import axios from "axios";
|
||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
|
||||
|
||||
const props = defineProps({
|
||||
segment: Object,
|
||||
@@ -20,6 +21,7 @@ const selectedClient = ref(initialClient);
|
||||
|
||||
// Column definitions for the server-driven table
|
||||
const columns = [
|
||||
{ key: "select", label: "", sortable: false, width: "50px" },
|
||||
{ key: "reference", label: "Pogodba", sortable: true },
|
||||
{ key: "client_case", label: "Primer" },
|
||||
{ key: "address", label: "Naslov" },
|
||||
@@ -36,6 +38,13 @@ const exportColumns = ref(columns.map((col) => col.key));
|
||||
const exportError = ref("");
|
||||
const isExporting = ref(false);
|
||||
|
||||
const selectedRows = ref([]);
|
||||
const showConfirmDialog = ref(false);
|
||||
const archiveForm = useForm({
|
||||
contracts: [],
|
||||
reactivate: false,
|
||||
});
|
||||
|
||||
const contractsCurrentPage = computed(() => props.contracts?.current_page ?? 1);
|
||||
const contractsPerPage = computed(() => props.contracts?.per_page ?? 15);
|
||||
const totalContracts = computed(
|
||||
@@ -48,6 +57,11 @@ const exportDisabled = computed(
|
||||
() => exportColumns.value.length === 0 || isExporting.value
|
||||
);
|
||||
|
||||
const canManageSettings = computed(() => {
|
||||
const permissions = usePage().props?.auth?.user?.permissions || [];
|
||||
return permissions.includes("mass-archive");
|
||||
});
|
||||
|
||||
function toggleAllColumns(checked) {
|
||||
exportColumns.value = checked ? columns.map((col) => col.key) : [];
|
||||
}
|
||||
@@ -205,6 +219,67 @@ function extractFilenameFromHeaders(headers) {
|
||||
const asciiMatch = disposition.match(/filename="?([^";]+)"?/i);
|
||||
return asciiMatch?.[1] || null;
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
if (selectedRows.value.length === props.contracts.data.length) {
|
||||
selectedRows.value = [];
|
||||
} else {
|
||||
selectedRows.value = props.contracts.data.map((row) => row.uuid);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRowSelection(uuid) {
|
||||
const index = selectedRows.value.indexOf(uuid);
|
||||
if (index > -1) {
|
||||
selectedRows.value.splice(index, 1);
|
||||
} else {
|
||||
selectedRows.value.push(uuid);
|
||||
}
|
||||
}
|
||||
|
||||
function isRowSelected(uuid) {
|
||||
return selectedRows.value.includes(uuid);
|
||||
}
|
||||
|
||||
function isAllSelected() {
|
||||
return (
|
||||
props.contracts.data.length > 0 &&
|
||||
selectedRows.value.length === props.contracts.data.length
|
||||
);
|
||||
}
|
||||
|
||||
function isIndeterminate() {
|
||||
return (
|
||||
selectedRows.value.length > 0 &&
|
||||
selectedRows.value.length < props.contracts.data.length
|
||||
);
|
||||
}
|
||||
|
||||
function openArchiveModal() {
|
||||
if (!selectedRows.value.length) return;
|
||||
showConfirmDialog.value = true;
|
||||
}
|
||||
|
||||
function closeConfirmDialog() {
|
||||
showConfirmDialog.value = false;
|
||||
}
|
||||
|
||||
function submitArchive() {
|
||||
if (!selectedRows.value.length) return;
|
||||
|
||||
showConfirmDialog.value = false;
|
||||
|
||||
archiveForm.contracts = [...selectedRows.value];
|
||||
archiveForm.reactivate = false;
|
||||
|
||||
archiveForm.post(route("contracts.archive-batch"), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => {
|
||||
selectedRows.value = [];
|
||||
router.reload({ only: ["contracts"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -259,13 +334,51 @@ function extractFilenameFromHeaders(headers) {
|
||||
row-key="uuid"
|
||||
>
|
||||
<template #toolbar-extra>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
|
||||
@click="openExportDialog"
|
||||
>
|
||||
Izvozi v Excel
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-indigo-200 bg-white px-3 py-2 text-sm font-medium text-indigo-700 shadow-sm hover:bg-indigo-50"
|
||||
@click="openExportDialog"
|
||||
>
|
||||
Izvozi v Excel
|
||||
</button>
|
||||
<div
|
||||
v-if="canManageSettings && selectedRows.length"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span class="text-sm text-gray-600"
|
||||
>{{ selectedRows.length }} izbran{{
|
||||
selectedRows.length === 1 ? "a" : "ih"
|
||||
}}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-red-200 bg-white px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50"
|
||||
@click="openArchiveModal"
|
||||
>
|
||||
Arhiviraj izbrane
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-select>
|
||||
<input
|
||||
v-if="canManageSettings"
|
||||
type="checkbox"
|
||||
:checked="isAllSelected()"
|
||||
:indeterminate="isIndeterminate()"
|
||||
@change="toggleSelectAll"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
</template>
|
||||
<template #cell-select="{ row }">
|
||||
<input
|
||||
v-if="canManageSettings"
|
||||
type="checkbox"
|
||||
:checked="isRowSelected(row.uuid)"
|
||||
@change="toggleRowSelection(row.uuid)"
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
|
||||
/>
|
||||
</template>
|
||||
<!-- Primer (client_case) cell with link when available -->
|
||||
<template #cell-client_case="{ row }">
|
||||
@@ -316,6 +429,19 @@ function extractFilenameFromHeaders(headers) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
:show="showConfirmDialog"
|
||||
title="Arhiviraj pogodbe"
|
||||
:message="`Ali ste prepričani, da želite arhivirati ${selectedRows.length} pogodb${
|
||||
selectedRows.length === 1 ? 'o' : ''
|
||||
}? Arhivirane pogodbe bodo odstranjene iz aktivnih segmentov.`"
|
||||
confirm-text="Arhiviraj"
|
||||
cancel-text="Prekliči"
|
||||
:danger="true"
|
||||
@close="closeConfirmDialog"
|
||||
@confirm="submitArchive"
|
||||
/>
|
||||
|
||||
<DialogModal :show="exportDialogOpen" max-width="3xl" @close="closeExportDialog">
|
||||
<template #title>
|
||||
<div>
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
Route::get('client-cases/{client_case:uuid}', [ClientCaseContoller::class, 'show'])->name('clientCase.show');
|
||||
Route::post('client-cases/{client_case:uuid}/contracts/{uuid}/segment', [ClientCaseContoller::class, 'updateContractSegment'])->name('clientCase.contract.updateSegment');
|
||||
Route::post('client-cases/{client_case:uuid}/contracts/{uuid}/archive', [ClientCaseContoller::class, 'archiveContract'])->name('clientCase.contract.archive');
|
||||
Route::post('contracts/archive-batch', [ClientCaseContoller::class, 'archiveBatch'])->name('contracts.archive-batch')->middleware('permission:mass-archive');
|
||||
Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store');
|
||||
Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson');
|
||||
// client-case / contract
|
||||
|
||||
Reference in New Issue
Block a user