From 76f76f73b42ccfcc23bdc4c6e31cd1bf75f3b70c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 20:45:02 +0100 Subject: [PATCH 01/14] error handling importer --- app/Services/ImportProcessor.php | 47 +++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 6c6bc24..7b371b7 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -25,6 +25,7 @@ use App\Models\Person\PersonType; use App\Models\Person\PhoneType; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Database\QueryException; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Storage; @@ -1194,7 +1195,8 @@ public function process(Import $import, ?Authenticatable $user = null): array 'user_id' => $user?->getAuthIdentifier(), 'event' => 'processing_failed', 'level' => 'error', - 'message' => $e->getMessage(), + 'message' => $this->safeErrorMessage($e->getMessage()), + 'context' => $this->exceptionContext($e), ]); return ['ok' => false, 'status' => 'failed', 'error' => $e->getMessage()]; @@ -2482,6 +2484,49 @@ private function safeErrorMessage(string $msg): string return $msg; } + /** + * Extract structured exception details for logging. + */ + private function exceptionContext(\Throwable $e): array + { + $ctx = [ + 'exception' => get_class($e), + 'message' => $this->safeErrorMessage($e->getMessage()), + 'code' => $e->getCode(), + 'file' => $e->getFile().':'.$e->getLine(), + ]; + + if (method_exists($e, 'getPrevious') && $e->getPrevious()) { + $prev = $e->getPrevious(); + $ctx['previous'] = [ + 'exception' => get_class($prev), + 'message' => $this->safeErrorMessage($prev->getMessage()), + 'code' => $prev->getCode(), + 'file' => $prev->getFile().':'.$prev->getLine(), + ]; + } + + if ($e instanceof QueryException) { + $ctx['sql'] = $e->getSql(); + $ctx['bindings'] = $e->getBindings(); + $info = $e->errorInfo ?? null; + if (is_array($info)) { + $ctx['sqlstate'] = $info[0] ?? null; + $ctx['driver_error_code'] = $info[1] ?? null; + $ctx['driver_error_message'] = $info[2] ?? null; + } + } elseif (property_exists($e, 'errorInfo')) { + $info = $e->errorInfo; + if (is_array($info)) { + $ctx['sqlstate'] = $info[0] ?? null; + $ctx['driver_error_code'] = $info[1] ?? null; + $ctx['driver_error_message'] = $info[2] ?? null; + } + } + + return $ctx; + } + /** * Build a trimmed raw data preview (first 8 columns, truncated values) for logging. */ From a4db37adfab55d339097cd90797751ad043b7003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 20:49:11 +0100 Subject: [PATCH 02/14] Fix error reporting --- app/Services/ImportProcessor.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 7b371b7..7bc7eb3 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -1068,13 +1068,33 @@ public function process(Import $import, ?Authenticatable $user = null): array } } } catch (\Throwable $e) { + $rollbackFailed = false; + $rollbackError = null; if ($isPg) { // Roll back only this row's work try { DB::statement('ROLLBACK TO SAVEPOINT import_row_'.$rowNum); - } catch (\Throwable $ignored) { /* noop */ + } catch (\Throwable $ignored) { + $rollbackFailed = true; + $rollbackError = $ignored; } } + if ($rollbackFailed) { + // Abort the whole import if we cannot rollback to the row savepoint (transaction is poisoned) + ImportEvent::create([ + 'import_id' => $import->id, + 'user_id' => $user?->getAuthIdentifier(), + 'event' => 'row_rollback_failed', + 'level' => 'error', + 'message' => 'Rollback to savepoint failed; aborting import.', + 'context' => [ + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($rollbackError ?? $e), + ], + ]); + + throw $rollbackError ?? $e; + } // Ensure importRow exists for logging if failure happened before its creation if (! $importRow) { try { From b8c9b51f2915a658ef6707cac39896af0ceb0ad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 20:51:31 +0100 Subject: [PATCH 03/14] test --- app/Services/ImportProcessor.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 7bc7eb3..0194b02 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -217,7 +217,23 @@ public function process(Import $import, ?Authenticatable $user = null): array if ($isPg) { // Establish a savepoint so a failing row does not poison the whole transaction - DB::statement('SAVEPOINT import_row_'.$rowNum); + try { + DB::statement('SAVEPOINT import_row_'.$rowNum); + } catch (\Throwable $se) { + ImportEvent::create([ + 'import_id' => $import->id, + 'user_id' => $user?->getAuthIdentifier(), + 'event' => 'savepoint_failed', + 'level' => 'error', + 'message' => 'Failed to create savepoint; transaction already aborted.', + 'context' => [ + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($se), + ], + ]); + + throw $se; // abort import so root cause surfaces + } } // Scope variables per row so they aren't reused after exception From 6c45063e47c86e6cc0f5129d12f96f841a8461e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 21:12:53 +0100 Subject: [PATCH 04/14] fixed naslove --- app/Services/ImportProcessor.php | 38 +++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 0194b02..11be0cb 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -28,12 +28,14 @@ use Illuminate\Database\QueryException; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; class ImportProcessor { /** * Track contracts that already existed and were matched during history imports. + * * @var array */ private array $historyFoundContractIds = []; @@ -220,6 +222,11 @@ public function process(Import $import, ?Authenticatable $user = null): array try { DB::statement('SAVEPOINT import_row_'.$rowNum); } catch (\Throwable $se) { + Log::error('Import savepoint_failed', [ + 'import_id' => $import->id, + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($se), + ]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), @@ -1096,6 +1103,11 @@ public function process(Import $import, ?Authenticatable $user = null): array } } if ($rollbackFailed) { + Log::error('Import row_rollback_failed', [ + 'import_id' => $import->id, + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($rollbackError ?? $e), + ]); // Abort the whole import if we cannot rollback to the row savepoint (transaction is poisoned) ImportEvent::create([ 'import_id' => $import->id, @@ -1137,6 +1149,12 @@ public function process(Import $import, ?Authenticatable $user = null): array } $failedRows[] = $rowNum; $invalid++; + Log::error('Import row_exception', [ + 'import_id' => $import->id, + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($e), + 'raw_preview' => isset($rawAssoc) ? $this->buildRawDataPreview($rawAssoc) : [], + ]); try { ImportEvent::create([ 'import_id' => $import->id, @@ -1154,6 +1172,12 @@ public function process(Import $import, ?Authenticatable $user = null): array ], ]); } catch (\Throwable $evtErr) { + Log::error('Import row_exception_event_failed', [ + 'import_id' => $import->id, + 'row_number' => $rowNum, + 'exception' => $this->exceptionContext($evtErr), + 'original_exception' => $this->exceptionContext($e), + ]); // Swallow secondary failure to ensure loop continues } @@ -1226,6 +1250,10 @@ public function process(Import $import, ?Authenticatable $user = null): array // Mark failed and log after rollback (so no partial writes persist) $import->refresh(); $import->update(['status' => 'failed', 'failed_at' => now()]); + Log::error('Import processing_failed', [ + 'import_id' => $import->id, + 'exception' => $this->exceptionContext($e), + ]); ImportEvent::create([ 'import_id' => $import->id, 'user_id' => $user?->getAuthIdentifier(), @@ -2047,7 +2075,7 @@ private function upsertActivity(Import $import, array $mapped, $mappings, ?array } $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); - $activityModel = new Activity(); + $activityModel = new Activity; $activityModel->forceFill($data); if (array_key_exists('created_at', $data)) { // Preserve provided timestamps by disabling automatic timestamps for this save @@ -2271,6 +2299,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings, b if ($existing) { if ($historyImport) { $this->historyFoundContractIds[$existing->id] = true; + return ['action' => 'skipped_history', 'contract' => $existing, 'message' => 'Existing contract left unchanged (history import)']; } // 1) Prepare contract field changes (non-null) @@ -3090,8 +3119,8 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array if (mb_strlen($addressLine) < 3) { return ['action' => 'skipped', 'message' => 'Invalid address value']; } - // Allow only basic address characters to avoid noisy special chars - if (! preg_match('/^[A-Za-z0-9\\s\\.,\\-\\/\\#\\\'"\\(\\)&]+$/', $addressLine)) { + // Allow letters (incl. diacritics), numbers, and common separators + if (! preg_match('/^[\\p{L}0-9\\s\\.,\\-\\/\\#\\\'"\\(\\)&]+$/u', $addressLine)) { return ['action' => 'skipped', 'message' => 'Invalid address value']; } // Default country SLO if not provided @@ -3343,3 +3372,6 @@ protected function attemptContractReactivation(Contract $contract, ?Authenticata } } } + + + From 06fa443b3ee6b38c23b59b0e0fe5d93d615f9eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 21:22:17 +0100 Subject: [PATCH 05/14] trimming additional spaces example TEST 2 now to TEST 2 --- app/Services/ImportProcessor.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 11be0cb..92854f0 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3110,8 +3110,10 @@ private function upsertEmail(int $personId, array $emailData, $mappings): array private function upsertAddress(int $personId, array $addrData, $mappings): array { $addressLine = trim((string) ($addrData['address'] ?? '')); - // Normalize whitespace + // Normalize whitespace: collapse multiples and tighten around separators $addressLine = preg_replace('/\s+/', ' ', $addressLine); + $addressLine = preg_replace('/\s*([,;\/])\s*/', '$1 ', $addressLine); + $addressLine = trim($addressLine); // Skip common placeholders or missing values if ($addressLine === '' || $addressLine === '0' || strcasecmp($addressLine, '#N/A') === 0 || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) { return ['action' => 'skipped', 'message' => 'No address value']; @@ -3372,6 +3374,3 @@ protected function attemptContractReactivation(Contract $contract, ?Authenticata } } } - - - From 2140181a76b92e623d42bc21f4f7ed2db1f860a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 21:50:48 +0100 Subject: [PATCH 06/14] sdwsd --- app/Services/ImportProcessor.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 92854f0..922fc47 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -183,10 +183,7 @@ public function process(Import $import, ?Authenticatable $user = null): array } // Preflight: warn if any mapped source columns are not present in the header (exact match) $headerSet = []; - foreach ($header as $h) { - $headerSet[$h] = true; - } - $missingSources = []; + // Regex validation removed per request; rely on basic length/placeholder checks only foreach ($mappings as $map) { $src = (string) ($map->source_column ?? ''); if ($src !== '' && ! array_key_exists($src, $headerSet)) { From 94ad0c0772e473d410049aaec734c11998c694b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 22:14:43 +0100 Subject: [PATCH 07/14] test --- app/Services/ImportProcessor.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 922fc47..4966dac 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3118,9 +3118,10 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array if (mb_strlen($addressLine) < 3) { return ['action' => 'skipped', 'message' => 'Invalid address value']; } - // Allow letters (incl. diacritics), numbers, and common separators - if (! preg_match('/^[\\p{L}0-9\\s\\.,\\-\\/\\#\\\'"\\(\\)&]+$/u', $addressLine)) { - return ['action' => 'skipped', 'message' => 'Invalid address value']; + // If identical address already exists anywhere, skip to avoid constraint violation + $existingAny = PersonAddress::where('address', $addressLine)->first(); + if ($existingAny && $existingAny->person_id !== $personId) { + return ['action' => 'skipped', 'message' => 'Address already exists for another person']; } // Default country SLO if not provided if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') { From 5ddca3538912625e282120b7956058e1bc9b5d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 22:17:00 +0100 Subject: [PATCH 08/14] test --- app/Services/ImportProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 4966dac..edc690a 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3120,8 +3120,8 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array } // If identical address already exists anywhere, skip to avoid constraint violation $existingAny = PersonAddress::where('address', $addressLine)->first(); - if ($existingAny && $existingAny->person_id !== $personId) { - return ['action' => 'skipped', 'message' => 'Address already exists for another person']; + if ($existingAny) { + return ['action' => 'skipped', 'message' => 'Address already exists in database']; } // Default country SLO if not provided if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') { From 96473fd60be662ac579e2fe65f95fe289b373e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Wed, 17 Dec 2025 22:21:36 +0100 Subject: [PATCH 09/14] dwdwf --- app/Services/ImportProcessor.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index edc690a..de5ca07 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3170,9 +3170,18 @@ 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(); - $created = PersonAddress::create($data); + try { + $created = PersonAddress::create($data); - return ['action' => 'inserted', 'address' => $created]; + return ['action' => 'inserted', 'address' => $created]; + } catch (QueryException $e) { + // If unique constraint violation, skip instead of aborting + if ($e->getCode() === '23505' || str_contains($e->getMessage(), 'unique') || str_contains($e->getMessage(), 'duplicate')) { + return ['action' => 'skipped', 'message' => 'Address already exists (constraint violation)']; + } + + throw $e; + } } } From 622f53e401c9c5983a540e47ffb65cb741ae004c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 18 Dec 2025 18:25:15 +0100 Subject: [PATCH 10/14] removed something --- app/Services/ImportProcessor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index de5ca07..8bb6aad 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3112,17 +3112,17 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array $addressLine = preg_replace('/\s*([,;\/])\s*/', '$1 ', $addressLine); $addressLine = trim($addressLine); // Skip common placeholders or missing values - if ($addressLine === '' || $addressLine === '0' || strcasecmp($addressLine, '#N/A') === 0 || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) { + if ($addressLine === '' || $addressLine === '0' || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) { return ['action' => 'skipped', 'message' => 'No address value']; } if (mb_strlen($addressLine) < 3) { return ['action' => 'skipped', 'message' => 'Invalid address value']; } // If identical address already exists anywhere, skip to avoid constraint violation - $existingAny = PersonAddress::where('address', $addressLine)->first(); + /*$existingAny = PersonAddress::where('address', $addressLine)->first(); if ($existingAny) { return ['action' => 'skipped', 'message' => 'Address already exists in database']; - } + }*/ // Default country SLO if not provided if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') { $addrData['country'] = 'SLO'; From 5d4498ac5a166e98cae06c38b57e1b76de842722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 18 Dec 2025 19:40:27 +0100 Subject: [PATCH 11/14] fixed address import --- app/Services/ImportProcessor.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 8bb6aad..f0b8305 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3127,7 +3127,12 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array if (! isset($addrData['country']) || $addrData['country'] === null || $addrData['country'] === '') { $addrData['country'] = 'SLO'; } - $existing = PersonAddress::where('person_id', $personId)->where('address', $addressLine)->first(); + // Compare addresses with all spaces removed to handle whitespace variations + $addressLineNoSpaces = preg_replace('/\s+/', '', $addressLine); + $existing = PersonAddress::where('person_id', $personId) + ->whereRaw("REPLACE(address, ' ', '') = ?", [$addressLineNoSpaces]) + ->first(); + $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { @@ -3176,6 +3181,11 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array return ['action' => 'inserted', 'address' => $created]; } catch (QueryException $e) { // If unique constraint violation, skip instead of aborting + Log::warning('Address constraint violation during import', [ + 'person_id' => $personId, + 'address' => $addressLine, + 'error' => $e->getMessage(), + ]); if ($e->getCode() === '23505' || str_contains($e->getMessage(), 'unique') || str_contains($e->getMessage(), 'duplicate')) { return ['action' => 'skipped', 'message' => 'Address already exists (constraint violation)']; } From 39a597f6eb0c8a2ced9386f22f1df74fa1cc4cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 18 Dec 2025 20:22:14 +0100 Subject: [PATCH 12/14] added #N/A check --- app/Services/ImportProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index f0b8305..7d4f07c 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -3112,7 +3112,7 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array $addressLine = preg_replace('/\s*([,;\/])\s*/', '$1 ', $addressLine); $addressLine = trim($addressLine); // Skip common placeholders or missing values - if ($addressLine === '' || $addressLine === '0' || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) { + if ($addressLine === '' || $addressLine === '0' || strcasecmp($addressLine, '#N/A') === 0 || preg_match('/^(#?n\/?a|na|null|none)$/i', $addressLine)) { return ['action' => 'skipped', 'message' => 'No address value']; } if (mb_strlen($addressLine) < 3) { From 11206fb4f763d54815be8564669629f3e347dc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 18 Dec 2025 20:48:11 +0100 Subject: [PATCH 13/14] UTF8 fixed --- app/Services/ImportProcessor.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index 7d4f07c..f322245 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -1983,6 +1983,8 @@ private function upsertActivity(Import $import, array $mapped, $mappings, ?array } elseif (in_array($field, ['action_id', 'decision_id', 'user_id'], true)) { $normalized = is_null($value) ? null : (int) $value; } elseif (is_string($normalized)) { + // Clean invalid UTF-8 sequences from string fields + $normalized = mb_convert_encoding($normalized, 'UTF-8', 'UTF-8'); $normalized = trim($normalized); } if (in_array($applyMode, ['both', 'insert'], true)) { @@ -2539,8 +2541,9 @@ private function safeErrorMessage(string $msg): string } // Fallback strip invalid bytes $msg = @iconv('UTF-8', 'UTF-8//IGNORE', $msg) ?: $msg; - if (strlen($msg) > 500) { - $msg = substr($msg, 0, 497).'...'; + // Use mb_strlen and mb_substr for UTF-8 safety + if (mb_strlen($msg) > 500) { + $msg = mb_substr($msg, 0, 497).'...'; } return $msg; @@ -2629,9 +2632,9 @@ private function formatAppliedFieldMessage(string $root, array $fields): string } else { $disp = method_exists($v, '__toString') ? (string) $v : gettype($v); } - // Truncate very long values for log safety - if (strlen($disp) > 60) { - $disp = substr($disp, 0, 57).'...'; + // Truncate very long values for log safety (use mb_substr for UTF-8 safety) + if (mb_strlen($disp) > 60) { + $disp = mb_substr($disp, 0, 57).'...'; } $parts[] = $k.'='.$disp; } From adc2a64687f1882f422f4009fa586a9db0e18911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Sun, 21 Dec 2025 21:00:49 +0100 Subject: [PATCH 14/14] Fixed some field job problem where field operator could still see archived contracts --- app/Http/Controllers/ClientCaseContoller.php | 263 ++++---- app/Http/Controllers/PhoneViewController.php | 200 ++---- app/Models/Contract.php | 17 + app/Models/FieldJob.php | 7 +- app/Services/Archiving/ArchiveExecutor.php | 4 +- composer.json | 4 +- composer.lock | 593 +++++++++++++++++- ...ld_jobs_table_add_column_last_activity.php | 30 + .../Pages/Cases/Partials/ActivityDrawer.vue | 1 + resources/js/Pages/Phone/Index.vue | 119 +++- 10 files changed, 919 insertions(+), 319 deletions(-) create mode 100644 database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index c1f73af..fe6ec73 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -252,11 +252,14 @@ public function storeActivity(ClientCase $clientCase, Request $request) 'action_id' => 'exists:\App\Models\Action,id', 'decision_id' => 'exists:\App\Models\Decision,id', 'contract_uuid' => 'nullable|uuid', + 'phone_view' => 'nullable|boolean', 'send_auto_mail' => 'sometimes|boolean', 'attachment_document_ids' => 'sometimes|array', 'attachment_document_ids.*' => 'integer', ]); + $isPhoneView = $attributes['phone_view'] ?? false; + // Map contract_uuid to contract_id within the same client case, if provided $contractId = null; if (! empty($attributes['contract_uuid'])) { @@ -279,10 +282,23 @@ public function storeActivity(ClientCase $clientCase, Request $request) 'decision_id' => $attributes['decision_id'], 'contract_id' => $contractId, ]); - /*foreach ($activity->decision->events as $e) { - $class = '\\App\\Events\\' . $e->name; - event(new $class($clientCase)); - }*/ + + if ($isPhoneView && $contractId) { + $fieldJob = $contract->fieldJobs() + ->whereNull('completed_at') + ->whereNull('cancelled_at') + ->where('assigned_user_id', \Auth::id()) + ->orderByDesc('id') + ->first(); + + if ($fieldJob) { + $fieldJob->update([ + 'added_activity' => true, + 'last_activity' => $row->created_at, + ]); + + } + } logger()->info('Activity successfully inserted', $attributes); @@ -297,8 +313,8 @@ public function storeActivity(ClientCase $clientCase, Request $request) ->values(); $validAttachmentIds = collect(); if ($attachmentIds->isNotEmpty() && $contractId) { - $validAttachmentIds = \App\Models\Document::query() - ->where('documentable_type', \App\Models\Contract::class) + $validAttachmentIds = Document::query() + ->where('documentable_type', Contract::class) ->where('documentable_id', $contractId) ->whereIn('id', $attachmentIds) ->pluck('id'); @@ -1458,178 +1474,115 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r { $contract = Contract::query()->where('uuid', $uuid)->firstOrFail(); if ($contract->client_case_id !== $clientCase->id) { + \Log::warning('Contract not found uuid: {uuid}', ['uuid' => $uuid]); abort(404); } - $reactivateRequested = (bool) $request->boolean('reactivate'); - // Determine applicable settings based on intent (archive vs reactivate) - if ($reactivateRequested) { - $latestReactivate = \App\Models\ArchiveSetting::query() - ->where('enabled', true) - ->where('reactivate', true) - ->whereIn('strategy', ['immediate', 'manual']) - ->orderByDesc('id') - ->first(); - if (! $latestReactivate) { - return back()->with('warning', __('contracts.reactivate_not_allowed')); - } - $settings = collect([$latestReactivate]); - $hasReactivateRule = true; - } else { - $settings = \App\Models\ArchiveSetting::query() - ->where('enabled', true) - ->whereIn('strategy', ['immediate', 'manual']) - ->where(function ($q) { // exclude reactivate-only rules from archive run - $q->whereNull('reactivate')->orWhere('reactivate', false); - }) - ->get(); - if ($settings->isEmpty()) { - return back()->with('warning', __('contracts.no_archive_settings')); - } - $hasReactivateRule = false; + + $attr = $request->validate([ + 'reactivate' => 'boolean', + ]); + + $reactivate = $attr['reactivate'] ?? false; + + $setting = \App\Models\ArchiveSetting::query() + ->where('enabled', true) + ->whereIn('strategy', ['immediate', 'manual']) + ->where('reactivate', $reactivate) + ->orderByDesc('id') + ->first(); + + if (! $setting->exists()) { + \Log::warning('No archive settings found!'); + + return back()->with('warning', 'No settings found'); } + // Service archive executor $executor = app(\App\Services\Archiving\ArchiveExecutor::class); + $result = null; + $context = [ 'contract_id' => $contract->id, 'client_case_id' => $clientCase->id, + 'account_id' => $contract->account->id ?? null, ]; - if ($contract->account) { - $context['account_id'] = $contract->account->id; + + try { + $result = $executor->executeSetting($setting, $context, \Auth::id()); + } catch (Exception $e) { + \Log::error('There was an error executing ArchiveExecutor::executeSetting {msg}', ['msg' => $e->getMessage()]); + + return back()->with('warning', 'Something went wrong!'); } - $overall = []; - $hadAnyEffect = false; - foreach ($settings as $setting) { - - $res = $executor->executeSetting($setting, $context, optional($request->user())->id); - foreach ($res as $table => $count) { - $overall[$table] = ($overall[$table] ?? 0) + $count; - if ($count > 0) { - $hadAnyEffect = true; - } - } - } - - if ($reactivateRequested && $hasReactivateRule) { - // Reactivation path: ensure contract becomes active and soft-delete cleared. - if ($contract->active == 0 || $contract->deleted_at) { - $contract->forceFill(['active' => 1, 'deleted_at' => null])->save(); - $overall['contracts_reactivated'] = ($overall['contracts_reactivated'] ?? 0) + 1; - $hadAnyEffect = true; - } - } else { - // Ensure the contract itself is archived even if rule conditions would have excluded it - if (! empty($contract->getAttributes()) && $contract->active) { - if (! array_key_exists('contracts', $overall)) { - $contract->update(['active' => 0]); - $overall['contracts'] = ($overall['contracts'] ?? 0) + 1; - } else { - $contract->refresh(); - } - $hadAnyEffect = true; - } - } - - // Create an Activity record logging this archive if an action or decision is tied to any setting - if ($hadAnyEffect) { - $activitySetting = $settings->first(fn ($s) => ! is_null($s->action_id) || ! is_null($s->decision_id)); - if ($activitySetting) { - try { - if ($reactivateRequested) { - $note = 'Ponovna aktivacija pogodba '.$contract->reference; - } else { - $noteKey = 'contracts.archived_activity_note'; - $note = __($noteKey, ['reference' => $contract->reference]); - if ($note === $noteKey) { - $note = \Illuminate\Support\Facades\Lang::get($noteKey, ['reference' => $contract->reference], 'sl'); - } - } + try { + \DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) { + // Create an Activity record logging this archive if an action or decision is tied to any setting + if ($setting->action_id && $setting->decision_id) { $activityData = [ 'client_case_id' => $clientCase->id, - 'action_id' => $activitySetting->action_id, - 'decision_id' => $activitySetting->decision_id, - 'note' => $note, - 'active' => 1, - 'user_id' => optional($request->user())->id, + 'action_id' => $setting->action_id, + 'decision_id' => $setting->decision_id, + 'note' => ($reactivate) + ? "Ponovno aktivirana pogodba $contract->reference" + : "Arhivirana pogodba $contract->reference", ]; - if ($reactivateRequested) { - // Attach the contract_id when reactivated as per requirement - $activityData['contract_id'] = $contract->id; + + try { + \App\Models\Activity::create($activityData); + } catch (Exception $e) { + \Log::warning('Activity could not be created!'); } - \App\Models\Activity::create($activityData); - } catch (\Throwable $e) { - logger()->warning('Failed to create archive/reactivate activity', [ - 'error' => $e->getMessage(), - 'contract_id' => $contract->id, - 'setting_id' => optional($activitySetting)->id, - 'reactivate' => $reactivateRequested, - ]); + } - } - } - // If any archive setting specifies a segment_id, move the contract to that segment (archive bucket) - $segmentSetting = $settings->first(fn ($s) => ! is_null($s->segment_id)); // for reactivation this is the single reactivation setting if segment specified - if ($segmentSetting && $segmentSetting->segment_id) { - try { - $segmentId = $segmentSetting->segment_id; - \DB::transaction(function () use ($contract, $segmentId, $clientCase) { - // Ensure the segment is attached to the client case (activate if previously inactive) - $casePivot = \DB::table('client_case_segment') - ->where('client_case_id', $clientCase->id) - ->where('segment_id', $segmentId) - ->first(); - if (! $casePivot) { - \DB::table('client_case_segment')->insert([ - 'client_case_id' => $clientCase->id, - 'segment_id' => $segmentId, + + // If any archive setting specifies a segment_id, move the contract to that segment (archive bucket) + if ($setting->segment_id) { + $segmentId = $setting->segment_id; + + $contract->segments() + ->allRelatedIds() + ->map(fn (int $val, int|string $key) => $contract->segments()->updateExistingPivot($val, [ + 'active' => false, + 'updated_at' => now(), + ]) + ); + + if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) { + $contract->attachedSegments()->updateExistingPivot($segmentId, [ 'active' => true, - 'created_at' => now(), 'updated_at' => now(), ]); - } elseif (! $casePivot->active) { - \DB::table('client_case_segment') - ->where('id', $casePivot->id) - ->update(['active' => true, 'updated_at' => now()]); - } - - // Deactivate all current active contract segments - \DB::table('contract_segment') - ->where('contract_id', $contract->id) - ->where('active', true) - ->update(['active' => false, 'updated_at' => now()]); - - // Attach or activate the archive segment for this contract - $existing = \DB::table('contract_segment') - ->where('contract_id', $contract->id) - ->where('segment_id', $segmentId) - ->first(); - if ($existing) { - \DB::table('contract_segment') - ->where('id', $existing->id) - ->update(['active' => true, 'updated_at' => now()]); } else { - \DB::table('contract_segment')->insert([ - 'contract_id' => $contract->id, - 'segment_id' => $segmentId, - 'active' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]); + $contract->segments()->attach( + $segmentId, + [ + 'active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); } - }); - } catch (\Throwable $e) { - logger()->warning('Failed to move contract to archive segment', [ - 'error' => $e->getMessage(), - 'contract_id' => $contract->id, - 'segment_id' => $segmentSetting->segment_id, - 'setting_id' => $segmentSetting->id, - ]); - } + } + + $contract->fieldJobs() + ->whereNull('completed_at') + ->whereNull('cancelled_at') + ->update([ + 'cancelled_at' => date('Y-m-d'), + 'updated_at' => now(), + ]); + }); + } catch (Exception $e) { + \Log::warning('Something went wrong with inserting / updating archive setting partials!'); + + return back()->with('warning', 'Something went wrong!'); } - $message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived'); - - return back()->with('success', $message); + return back()->with('success', $reactivate + ? __('contracts.reactivated') + : __('contracts.archived') + ); } /** diff --git a/app/Http/Controllers/PhoneViewController.php b/app/Http/Controllers/PhoneViewController.php index 062a095..14d3a13 100644 --- a/app/Http/Controllers/PhoneViewController.php +++ b/app/Http/Controllers/PhoneViewController.php @@ -76,169 +76,81 @@ public function completedToday(Request $request) public function showCase(\App\Models\ClientCase $clientCase, Request $request) { $userId = $request->user()->id; - $completedMode = (bool) $request->boolean('completed'); + $completedMode = $request->boolean('completed'); - // Eager load client case with person details - $case = \App\Models\ClientCase::query() - ->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])]) - ->findOrFail($clientCase->id); + // Eager load case with person details + $case = $clientCase->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'); - // Determine contracts of this case relevant to the current user - // - Normal mode: contracts assigned to me and still active (not completed/cancelled) - // - Completed mode (?completed=1): contracts where my field job was completed today - if ($completedMode) { - $start = now()->startOfDay(); - $end = now()->endOfDay(); - $contractIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereNull('cancelled_at') - ->whereBetween('completed_at', [$start, $end]) - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->pluck('contract_id') - ->unique() - ->values(); - } else { - $contractIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereNull('completed_at') - ->whereNull('cancelled_at') - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->pluck('contract_id') - ->unique() - ->values(); - } + // Query contracts based on field jobs + $contractsQuery = FieldJob::query() + ->where('assigned_user_id', $userId) + ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) + ->when($completedMode, + fn ($q) => $q->whereNull('cancelled_at')->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]), + fn ($q) => $q->whereNull('completed_at')->whereNull('cancelled_at') + ); + // Get contracts with relationships $contracts = \App\Models\Contract::query() ->where('client_case_id', $case->id) - ->whereIn('id', $contractIds) - ->with(['type:id,name', 'account']) + ->whereIn('id', $contractsQuery->pluck('contract_id')->unique()) + ->with(['type:id,name', 'account', 'latestObject']) ->orderByDesc('created_at') ->get(); - // Attach latest object (if any) to each contract as last_object for display - if ($contracts->isNotEmpty()) { - $byId = $contracts->keyBy('id'); - $latestObjects = \App\Models\CaseObject::query() - ->whereIn('contract_id', $byId->keys()) - ->whereNull('deleted_at') - ->select('id', 'reference', 'name', 'description', 'type', 'contract_id', 'created_at') - ->orderByDesc('created_at') - ->get() - ->groupBy('contract_id') - ->map(function ($group) { - return $group->first(); - }); - - foreach ($latestObjects as $cid => $obj) { - if (isset($byId[$cid])) { - $byId[$cid]->setAttribute('last_object', $obj); - } - } - } - - // Build merged documents: case documents + documents of assigned contracts - $contractRefMap = []; - foreach ($contracts as $c) { - $contractRefMap[$c->id] = $c->reference; - } - - $contractDocs = \App\Models\Document::query() - ->where('documentable_type', \App\Models\Contract::class) - ->whereIn('documentable_id', $contractIds) + // Build merged documents + $documents = $case->documents() ->orderByDesc('created_at') ->get() - ->map(function ($d) use ($contractRefMap) { - $arr = $d->toArray(); - $arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null; - $arr['documentable_type'] = \App\Models\Contract::class; - $arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid; - - return $arr; - }); - - $caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) { - $arr = $d->toArray(); - $arr['documentable_type'] = \App\Models\ClientCase::class; - $arr['client_case_uuid'] = $case->uuid; - - return $arr; - }); - - $documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values(); - - // Provide minimal types for PersonInfoGrid - $types = [ - 'address_types' => \App\Models\Person\AddressType::all(), - 'phone_types' => \App\Models\Person\PhoneType::all(), - ]; - - // Case activities (compact for phone): latest 20 with relations - $activities = $case->activities() - ->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name']) - ->orderByDesc('created_at') - ->limit(20) - ->get() - ->map(function ($a) { - $a->setAttribute('user_name', optional($a->user)->name); - - return $a; - }); - - // Determine segment filters from FieldJobSettings for this case/user context - $settingIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->when( - $completedMode, - function ($q) { - $q->whereNull('cancelled_at') - ->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]); - }, - function ($q) { - $q->whereNull('completed_at')->whereNull('cancelled_at'); - } + ->map(fn ($d) => array_merge($d->toArray(), [ + 'documentable_type' => \App\Models\ClientCase::class, + 'client_case_uuid' => $case->uuid, + ])) + ->concat( + \App\Models\Document::query() + ->where('documentable_type', \App\Models\Contract::class) + ->whereIn('documentable_id', $contracts->pluck('id')) + ->with('documentable:id,uuid,reference') + ->orderByDesc('created_at') + ->get() + ->map(fn ($d) => array_merge($d->toArray(), [ + 'contract_reference' => $d->documentable?->reference, + 'contract_uuid' => $d->documentable?->uuid, + ])) ) - ->pluck('field_job_setting_id') - ->filter() - ->unique() + ->sortByDesc('created_at') ->values(); - $segmentIds = collect(); - if ($settingIds->isNotEmpty()) { - $segmentIds = \App\Models\FieldJobSetting::query() - ->whereIn('id', $settingIds) - ->pluck('segment_id') - ->filter() - ->unique() - ->values(); - } - - // Filter actions and their decisions by the derived segment ids (decisions.segment_id) - $actions = \App\Models\Action::query() - ->when($segmentIds->isNotEmpty(), function ($q) use ($segmentIds) { - // Filter actions by their segment_id matching the FieldJobSetting segment(s) - $q->whereIn('segment_id', $segmentIds); - }) - ->with([ - 'decisions' => function ($q) { - $q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id'); - }, - 'decisions.emailTemplate' => function ($q) { - $q->select('id', 'name', 'entity_types', 'allow_attachments'); - }, - ]) - ->get(['id', 'name', 'color_tag', 'segment_id']); + // Get segment IDs for filtering actions + $segmentIds = \App\Models\FieldJobSetting::query() + ->whereIn('id', $contractsQuery->pluck('field_job_setting_id')->filter()->unique()) + ->pluck('segment_id') + ->filter() + ->unique(); return Inertia::render('Phone/Case/Index', [ - 'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(), + 'client' => $case->client->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'), 'client_case' => $case, 'contracts' => $contracts, 'documents' => $documents, - 'types' => $types, + 'types' => [ + 'address_types' => \App\Models\Person\AddressType::all(), + 'phone_types' => \App\Models\Person\PhoneType::all(), + ], 'account_types' => \App\Models\AccountType::all(), - // Provide decisions (filtered by segment) with linked email template metadata (entity_types, allow_attachments) - 'actions' => $actions, - 'activities' => $activities, + 'actions' => \App\Models\Action::query() + ->when($segmentIds->isNotEmpty(), fn ($q) => $q->whereIn('segment_id', $segmentIds)) + ->with([ + 'decisions:id,name,color_tag,auto_mail,email_template_id', + 'decisions.emailTemplate:id,name,entity_types,allow_attachments', + ]) + ->get(['id', 'name', 'color_tag', 'segment_id']), + 'activities' => $case->activities() + ->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name']) + ->orderByDesc('created_at') + ->limit(20) + ->get() + ->map(fn ($a) => $a->setAttribute('user_name', $a->user?->name)), 'completed_mode' => $completedMode, ]); } diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 719762e..be4d12b 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -96,6 +96,11 @@ public function segments(): BelongsToMany ->wherePivot('active', true); } + public function attachedSegments(): BelongsToMany + { + return $this->belongsToMany(\App\Models\Segment::class); + } + public function account(): HasOne { // Use latestOfMany to always surface newest account snapshot if multiple exist. @@ -114,6 +119,18 @@ public function documents(): MorphMany return $this->morphMany(\App\Models\Document::class, 'documentable'); } + public function fieldJobs(): HasMany + { + return $this->hasMany(\App\Models\FieldJob::class); + } + + public function latestObject(): HasOne + { + return $this->hasOne(\App\Models\CaseObject::class) + ->whereNull('deleted_at') + ->latest(); + } + protected static function booted(): void { static::created(function (Contract $contract): void { diff --git a/app/Models/FieldJob.php b/app/Models/FieldJob.php index 179d44a..11dfa10 100644 --- a/app/Models/FieldJob.php +++ b/app/Models/FieldJob.php @@ -24,6 +24,8 @@ class FieldJob extends Model 'priority', 'notes', 'address_snapshot ', + 'last_activity', + 'added_activity' ]; protected $casts = [ @@ -31,6 +33,8 @@ class FieldJob extends Model 'completed_at' => 'datetime', 'cancelled_at' => 'datetime', 'priority' => 'boolean', + 'last_activity' => 'datetime', + 'added_activity' => 'boolean', 'address_snapshot ' => 'array', ]; @@ -90,7 +94,8 @@ public function user(): BelongsTo public function contract(): BelongsTo { - return $this->belongsTo(Contract::class, 'contract_id'); + return $this->belongsTo(Contract::class, 'contract_id') + ->where('active', true); } /** diff --git a/app/Services/Archiving/ArchiveExecutor.php b/app/Services/Archiving/ArchiveExecutor.php index 63de252..b3e1fd4 100644 --- a/app/Services/Archiving/ArchiveExecutor.php +++ b/app/Services/Archiving/ArchiveExecutor.php @@ -69,6 +69,8 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null, $entities = $flat; } + // dd($entities); + foreach ($entities as $entityDef) { $rawTable = $entityDef['table'] ?? null; if (! $rawTable) { @@ -97,7 +99,7 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null, // Process in batches to avoid locking large tables while (true) { $query = DB::table($table)->whereNull('deleted_at'); - if (Schema::hasColumn($table, 'active')) { + if (Schema::hasColumn($table, 'active') && ! $reactivate) { $query->where('active', 1); } // Apply context filters or chain derived filters diff --git a/composer.json b/composer.json index 9c4b92d..1f4b811 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "keywords": ["laravel", "framework"], "license": "MIT", "require": { - "tijsverkoyen/css-to-inline-styles": "^2.2", "php": "^8.2", "arielmejiadev/larapex-charts": "^2.1", "diglactic/laravel-breadcrumbs": "^10.0", @@ -19,7 +18,8 @@ "maatwebsite/excel": "^3.1", "meilisearch/meilisearch-php": "^1.11", "robertboes/inertia-breadcrumbs": "dev-laravel-12", - "tightenco/ziggy": "^2.0" + "tightenco/ziggy": "^2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index c640d1c..a8a9bdd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "51fd57123c1b9f51c24f28e04a692ec4", + "content-hash": "d29c47a4d6813ee8e80a7c8112c2f17e", "packages": [ { "name": "arielmejiadev/larapex-charts", @@ -242,6 +242,162 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "dasprid/enum", "version": "1.0.6", @@ -737,6 +893,67 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, { "name": "facade/ignition-contracts", "version": "1.0.2", @@ -2695,6 +2912,272 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.67", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.0", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2025-08-26T09:13:16+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-12-10T09:58:31+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "meilisearch/meilisearch-php", "version": "v1.13.0", @@ -3475,6 +3958,112 @@ }, "time": "2024-10-02T11:20:13+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" + }, + "time": "2025-10-26T16:01:04+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -10335,6 +10924,6 @@ "platform": { "php": "^8.2" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php b/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php new file mode 100644 index 0000000..e4543f4 --- /dev/null +++ b/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php @@ -0,0 +1,30 @@ +boolean('added_activity')->default(false); + $table->timestamp('last_activity')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('field_jobs', function (Blueprint $table) { + $table->dropColumn('last_activity'); + $table->dropColumn('added_activity'); + }); + } +}; diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue index 05fac8e..dacb612 100644 --- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue @@ -113,6 +113,7 @@ const store = async () => { form .transform((data) => ({ ...data, + phone_view: props.phoneMode, due_date: formatDateForSubmit(data.due_date), attachment_document_ids: templateAllowsAttachments.value && data.attach_documents diff --git a/resources/js/Pages/Phone/Index.vue b/resources/js/Pages/Phone/Index.vue index c2f9de1..8440a76 100644 --- a/resources/js/Pages/Phone/Index.vue +++ b/resources/js/Pages/Phone/Index.vue @@ -1,5 +1,6 @@