From 1395b72ae81d626c19a108eb148474f18d49429e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 6 Nov 2025 21:54:07 +0100 Subject: [PATCH] changes to sms packages and option to create user --- app/Charts/ExampleChart.php | 15 +- app/Console/Commands/ImportPosts.php | 4 +- .../Commands/PruneDocumentPreviews.php | 11 +- app/Console/Kernel.php | 4 +- app/Http/Controllers/AccountController.php | 6 +- .../Controllers/Admin/PackageController.php | 18 +- .../Controllers/Admin/UserRoleController.php | 19 ++ app/Http/Controllers/CaseObjectController.php | 9 +- .../Controllers/ContractConfigController.php | 13 +- app/Http/Controllers/ContractController.php | 28 +-- app/Http/Controllers/DebtController.php | 2 - .../Controllers/ImportTemplateController.php | 10 +- app/Http/Controllers/PaymentController.php | 2 - app/Http/Controllers/PostController.php | 2 +- app/Http/Controllers/SettingController.php | 3 +- app/Http/Middleware/HandleInertiaRequests.php | 10 +- app/Http/Requests/Admin/StoreUserRequest.php | 52 ++++ app/Http/Requests/UpdateSegmentRequest.php | 2 +- app/Http/Resources/PersonCollection.php | 2 +- app/Jobs/PackageItemSmsJob.php | 26 +- app/Jobs/SendSmsJob.php | 2 +- app/Jobs/TestMailProfileConnection.php | 9 +- app/Models/Action.php | 4 +- app/Models/Client.php | 15 +- app/Models/Contract.php | 2 + app/Models/ImportEvent.php | 2 +- app/Models/ImportTemplateMapping.php | 2 +- app/Models/MailProfile.php | 2 +- app/Models/Person/PersonGroup.php | 1 - app/Models/Person/PersonType.php | 4 +- app/Models/Post.php | 1 + app/Models/Segment.php | 10 +- app/Policies/MailProfilePolicy.php | 1 + app/Policies/PostPolicy.php | 1 - app/Services/DateNormalizer.php | 2 + app/Services/ImportProcessor.php | 228 +++++++++++++++++- app/Services/ImportSimulationService.php | 49 +++- app/Services/Sms/SmsService.php | 1 + app/Traits/Uuid.php | 4 +- config/database.php | 2 +- config/larapex-charts.php | 8 +- config/scout.php | 4 +- database/factories/ActionFactory.php | 2 +- .../2024_10_14_185613_create_person_table.php | 44 ++-- ...024_10_19_090817_create_accounts_table.php | 6 +- .../2024_10_19_100530_create_debts_table.php | 13 +- ...024_10_19_100706_create_payments_table.php | 11 +- ...24_10_20_113420_create_contracts_table.php | 6 +- ...alter_table_account_add_balance_column.php | 6 +- ...9_26_191209_create_bank_accounts_table.php | 5 +- ...00_add_unique_indexes_for_import_dedup.php | 11 +- ...0_add_balance_amount_to_accounts_table.php | 6 +- ...dd_fk_import_template_to_imports_table.php | 2 +- ...09_27_000001_alter_person_nu_to_string.php | 3 +- ...221000_add_position_to_import_mappings.php | 2 +- ...7_230500_add_entity_to_import_mappings.php | 13 +- ...add_entity_to_import_template_mappings.php | 13 +- ...000001_create_field_job_settings_table.php | 3 +- ...5_09_28_000002_create_field_jobs_table.php | 3 +- ...lter_contract_configs_support_multiple.php | 4 +- ..._contract_reference_per_client_case_v2.php | 8 +- database/seeders/AccountSeeder.php | 1 - database/seeders/ActionSeeder.php | 41 ++-- database/seeders/ActivitySeeder.php | 1 - database/seeders/ClientCaseSeeder.php | 1 - database/seeders/ClientSeeder.php | 1 - database/seeders/ContractSeeder.php | 10 +- database/seeders/DebtSeeder.php | 1 - database/seeders/DecisionSeeder.php | 1 - database/seeders/ImportEntitySeeder.php | 17 +- database/seeders/PaymentSeeder.php | 1 - database/seeders/PersonSeeder.php | 75 +++--- database/seeders/PostSeeder.php | 1 - database/seeders/RolePermissionSeeder.php | 1 - database/seeders/SegmentSeeder.php | 7 +- .../seeders/UpdateTestUserPasswordSeeder.php | 1 + resources/js/Pages/Admin/Packages/Index.vue | 81 ++++++- resources/js/Pages/Admin/Users/Index.vue | 150 +++++++++++- .../js/Pages/Cases/Partials/ContractTable.vue | 10 +- .../js/Pages/Imports/Templates/Create.vue | 1 + routes/breadcrumbs.php | 2 +- routes/console.php | 5 +- routes/web.php | 1 + .../Feature/ActivityNotificationReadTest.php | 133 ++++++++++ tests/Feature/Admin/DocumentTemplatesTest.php | 6 +- .../Admin/PackageContractsDateFilterTest.php | 152 ++++++++++++ tests/Feature/AlgoliaSearchTest.php | 10 +- tests/Feature/DashboardTest.php | 4 +- .../Feature/DocumentSettingsWhitelistTest.php | 8 +- tests/Feature/FieldJobBulkAssignTest.php | 1 - .../ImportAccountInitialAmountTest.php | 3 +- tests/Feature/ImportCaseObjectTest.php | 179 ++++++++++++++ tests/Feature/ImportDelimiterTest.php | 10 +- tests/Feature/ImportPersonDedupByRefTest.php | 2 +- tests/Feature/InsertDatabaseTest.php | 8 +- tests/Feature/MailProfileSecurityTest.php | 4 +- tests/Feature/MailProfileTest.php | 8 +- tests/Feature/StringNormalizerTest.php | 2 - tests/TestCase.php | 2 +- tests/Unit/ConsoleEventTest.php | 1 + tests/Unit/EmailLogTest.php | 2 +- tests/Unit/ExampleTest.php | 7 +- 102 files changed, 1386 insertions(+), 319 deletions(-) create mode 100644 app/Http/Requests/Admin/StoreUserRequest.php create mode 100644 tests/Feature/ActivityNotificationReadTest.php create mode 100644 tests/Feature/Admin/PackageContractsDateFilterTest.php create mode 100644 tests/Feature/ImportCaseObjectTest.php diff --git a/app/Charts/ExampleChart.php b/app/Charts/ExampleChart.php index 69bbe3f..58cdc1e 100644 --- a/app/Charts/ExampleChart.php +++ b/app/Charts/ExampleChart.php @@ -11,8 +11,8 @@ class ExampleChart public function __construct(LarapexChart $chart) { $this->chart = $chart; - } - + } + public function build($options = null) { $data = \App\Models\ClientCase::query() @@ -22,20 +22,19 @@ public function build($options = null) ->groupByRaw('EXTRACT(MONTH from created_at)') ->orderByRaw('EXTRACT(MONTH from created_at)') ->get(); - + $months = $data->pluck('month')->map( - fn($nu) - => \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray(); - + fn ($nu) => \DateTime::createFromFormat('!m', $nu)->format('F'))->toArray(); + $newCases = $data->pluck('count')->toArray(); return $this->chart->areaChart() ->setTitle('Novi primeri zadnjih šest mesecev.') ->addData('Primeri', $newCases) - //->addData('Completed', [7, 2, 7, 2, 5, 4]) + // ->addData('Completed', [7, 2, 7, 2, 5, 4]) ->setColors(['#ff6384']) ->setXAxis($months) ->setToolbar(true) ->toVue(); } -} \ No newline at end of file +} diff --git a/app/Console/Commands/ImportPosts.php b/app/Console/Commands/ImportPosts.php index a31e539..f0bb7f2 100644 --- a/app/Console/Commands/ImportPosts.php +++ b/app/Console/Commands/ImportPosts.php @@ -2,12 +2,13 @@ namespace App\Console\Commands; -use Illuminate\Console\Command; use App\Models\Post; +use Illuminate\Console\Command; class ImportPosts extends Command { protected $signature = 'import:posts'; + protected $description = 'Import posts into Algolia without clearing the index'; public function __construct() @@ -22,4 +23,3 @@ public function handle() $this->info('Posts have been imported into Algolia.'); } } - diff --git a/app/Console/Commands/PruneDocumentPreviews.php b/app/Console/Commands/PruneDocumentPreviews.php index 0f7043a..7c23884 100644 --- a/app/Console/Commands/PruneDocumentPreviews.php +++ b/app/Console/Commands/PruneDocumentPreviews.php @@ -10,12 +10,15 @@ class PruneDocumentPreviews extends Command { protected $signature = 'documents:prune-previews {--days=90 : Delete previews older than this many days} {--dry-run : Show what would be deleted without deleting}'; + protected $description = 'Deletes generated document preview files older than N days and clears their metadata.'; public function handle(): int { $days = (int) $this->option('days'); - if ($days < 1) { $days = 90; } + if ($days < 1) { + $days = 90; + } $cutoff = Carbon::now()->subDays($days); $previewDisk = config('files.preview_disk', 'public'); @@ -27,6 +30,7 @@ public function handle(): int $count = $query->count(); if ($count === 0) { $this->info('No stale previews found.'); + return self::SUCCESS; } @@ -36,9 +40,12 @@ public function handle(): int $query->chunkById(200, function ($docs) use ($previewDisk, $dry) { foreach ($docs as $doc) { $path = $doc->preview_path; - if (!$path) { continue; } + if (! $path) { + continue; + } if ($dry) { $this->line("Would delete: {$previewDisk}://{$path} (document #{$doc->id})"); + continue; } try { diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 86e68a0..73cd1d6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,7 +15,9 @@ protected function schedule(Schedule $schedule): void // Optionally prune old previews daily if (config('files.enable_preview_prune', true)) { $days = (int) config('files.preview_retention_days', 90); - if ($days < 1) { $days = 90; } + if ($days < 1) { + $days = 90; + } $schedule->command('documents:prune-previews', [ '--days' => $days, ])->dailyAt('02:00'); diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 3f81a81..8e0eb6c 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -2,12 +2,8 @@ namespace App\Http\Controllers; -use App\Models\Account; -use Illuminate\Http\Request; -use Inertia\Inertia; - class AccountController extends Controller { // - + } diff --git a/app/Http/Controllers/Admin/PackageController.php b/app/Http/Controllers/Admin/PackageController.php index 4f676a6..4c5d28b 100644 --- a/app/Http/Controllers/Admin/PackageController.php +++ b/app/Http/Controllers/Admin/PackageController.php @@ -37,7 +37,7 @@ public function index(Request $request): Response ->get(['id', 'profile_id', 'sname', 'phone_number']); $templates = \App\Models\SmsTemplate::query() ->orderBy('name') - ->get(['id', 'name']); + ->get(['id', 'name', 'content']); $segments = \App\Models\Segment::query() ->where('active', true) ->orderBy('name') @@ -121,7 +121,7 @@ public function show(Package $package, SmsService $sms): Response if (! $rendered) { $body = isset($payload['body']) ? trim((string) $payload['body']) : ''; if ($body !== '') { - $rendered = $body; + $rendered = $sms->renderContent($body, $vars); } elseif (! empty($payload['template_id'])) { $tpl = \App\Models\SmsTemplate::find((int) $payload['template_id']); if ($tpl) { @@ -175,7 +175,7 @@ public function show(Package $package, SmsService $sms): Response if ($body !== '') { $preview = [ 'source' => 'body', - 'content' => $body, + 'content' => $sms->renderContent($body, $vars), ]; } elseif (! empty($payload['template_id'])) { /** @var SmsTemplate|null $tpl */ @@ -292,6 +292,8 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat 'client_id' => ['nullable', 'integer', 'exists:clients,id'], 'only_mobile' => ['nullable', 'boolean'], 'only_validated' => ['nullable', 'boolean'], + 'start_date_from' => ['nullable', 'date'], + 'start_date_to' => ['nullable', 'date'], ]); $segmentId = (int) $request->input('segment_id'); @@ -321,6 +323,15 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat ->where('client_cases.client_id', $clientId); } + // Date range filters for start_date + if ($startDateFrom = $request->input('start_date_from')) { + $query->where('contracts.start_date', '>=', $startDateFrom); + } + + if ($startDateTo = $request->input('start_date_to')) { + $query->where('contracts.start_date', '<=', $startDateTo); + } + // Optional phone filters if ($request->boolean('only_mobile') || $request->boolean('only_validated')) { $query->whereHas('clientCase.person.phones', function ($q) use ($request) { @@ -345,6 +356,7 @@ public function contracts(Request $request, PhoneSelector $selector): \Illuminat 'id' => $contract->id, 'uuid' => $contract->uuid, 'reference' => $contract->reference, + 'start_date' => $contract->start_date, 'case' => [ 'id' => $contract->clientCase?->id, 'uuid' => $contract->clientCase?->uuid, diff --git a/app/Http/Controllers/Admin/UserRoleController.php b/app/Http/Controllers/Admin/UserRoleController.php index 48da74f..64b2214 100644 --- a/app/Http/Controllers/Admin/UserRoleController.php +++ b/app/Http/Controllers/Admin/UserRoleController.php @@ -3,12 +3,14 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\StoreUserRequest; use App\Models\Permission; use App\Models\Role; use App\Models\User; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Hash; use Inertia\Inertia; use Inertia\Response; @@ -29,6 +31,23 @@ public function index(Request $request): Response ]); } + public function store(StoreUserRequest $request): RedirectResponse + { + $validated = $request->validated(); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => Hash::make($validated['password']), + ]); + + if (! empty($validated['roles'])) { + $user->roles()->sync($validated['roles']); + } + + return back()->with('success', 'Uporabnik uspešno ustvarjen'); + } + public function update(Request $request, User $user): RedirectResponse { Gate::authorize('manage-settings'); diff --git a/app/Http/Controllers/CaseObjectController.php b/app/Http/Controllers/CaseObjectController.php index b299227..4d1cac9 100644 --- a/app/Http/Controllers/CaseObjectController.php +++ b/app/Http/Controllers/CaseObjectController.php @@ -5,7 +5,6 @@ use App\Models\CaseObject; use App\Models\ClientCase; use App\Models\Contract; -use Illuminate\Database\QueryException; use Illuminate\Http\Request; class CaseObjectController extends Controller @@ -27,8 +26,8 @@ public function store(ClientCase $clientCase, string $uuid, Request $request) public function update(ClientCase $clientCase, int $id, Request $request) { - $object = CaseObject::where('id', $id) - ->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id)) + $object = CaseObject::where('id', $id) + ->whereHas('contract', fn ($q) => $q->where('client_case_id', $clientCase->id)) ->firstOrFail(); $validated = $request->validate([ @@ -45,8 +44,8 @@ public function update(ClientCase $clientCase, int $id, Request $request) public function destroy(ClientCase $clientCase, int $id) { - $object = CaseObject::where('id', $id) - ->whereHas('contract', fn($q) => $q->where('client_case_id', $clientCase->id)) + $object = CaseObject::where('id', $id) + ->whereHas('contract', fn ($q) => $q->where('client_case_id', $clientCase->id)) ->firstOrFail(); $object->delete(); diff --git a/app/Http/Controllers/ContractConfigController.php b/app/Http/Controllers/ContractConfigController.php index 8b98176..c5727a5 100644 --- a/app/Http/Controllers/ContractConfigController.php +++ b/app/Http/Controllers/ContractConfigController.php @@ -14,8 +14,8 @@ public function index() { return Inertia::render('Settings/ContractConfigs/Index', [ 'configs' => ContractConfig::with(['type:id,name', 'segment:id,name'])->get(), - 'types' => ContractType::query()->get(['id','name']), - 'segments' => Segment::query()->where('active', true)->get(['id','name']), + 'types' => ContractType::query()->get(['id', 'name']), + 'segments' => Segment::query()->where('active', true)->get(['id', 'name']), ]); } @@ -40,8 +40,8 @@ public function store(Request $request) ContractConfig::create([ 'contract_type_id' => $data['contract_type_id'], 'segment_id' => $data['segment_id'], - 'is_initial' => (bool)($data['is_initial'] ?? false), - 'active' => (bool)($data['active'] ?? true), + 'is_initial' => (bool) ($data['is_initial'] ?? false), + 'active' => (bool) ($data['active'] ?? true), ]); return back()->with('success', 'Configuration created'); @@ -57,8 +57,8 @@ public function update(ContractConfig $config, Request $request) $config->update([ 'segment_id' => $data['segment_id'], - 'is_initial' => (bool)($data['is_initial'] ?? $config->is_initial), - 'active' => (bool)($data['active'] ?? $config->active), + 'is_initial' => (bool) ($data['is_initial'] ?? $config->is_initial), + 'active' => (bool) ($data['active'] ?? $config->active), ]); return back()->with('success', 'Configuration updated'); @@ -67,6 +67,7 @@ public function update(ContractConfig $config, Request $request) public function destroy(ContractConfig $config) { $config->delete(); + return back()->with('success', 'Configuration deleted'); } } diff --git a/app/Http/Controllers/ContractController.php b/app/Http/Controllers/ContractController.php index dcd64e0..44b933a 100644 --- a/app/Http/Controllers/ContractController.php +++ b/app/Http/Controllers/ContractController.php @@ -6,24 +6,24 @@ use Illuminate\Http\Request; use Inertia\Inertia; - class ContractController extends Controller { - - public function index(Contract $contract) { + public function index(Contract $contract) + { return Inertia::render('Contract/Index', [ 'contracts' => $contract::with(['type', 'debtor']) ->where('active', 1) ->orderByDesc('created_at') ->paginate(10), 'person_types' => \App\Models\Person\PersonType::all(['id', 'name', 'description']) - ->where('deleted', 0) + ->where('deleted', 0), ]); } - public function show(Contract $contract){ + public function show(Contract $contract) + { return inertia('Contract/Show', [ - 'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id) + 'contract' => $contract::with(['type', 'client', 'debtor'])->findOrFail($contract->id), ]); } @@ -32,16 +32,16 @@ public function store(Request $request) $uuid = $request->input('client_case_uuid'); $clientCase = \App\Models\ClientCase::where('uuid', $uuid)->firstOrFail(); - - if( isset($clientCase->id) ){ - \DB::transaction(function() use ($request, $clientCase){ + if (isset($clientCase->id)) { - //Create contract + \DB::transaction(function () use ($request, $clientCase) { + + // Create contract $clientCase->contracts()->create([ 'reference' => $request->input('reference'), 'start_date' => date('Y-m-d', strtotime($request->input('start_date'))), - 'type_id' => $request->input('type_id') + 'type_id' => $request->input('type_id'), ]); }); @@ -50,12 +50,12 @@ public function store(Request $request) return to_route('clientCase.show', $clientCase); } - public function update(Contract $contract, Request $request){ + public function update(Contract $contract, Request $request) + { $contract->update([ 'referenca' => $request->input('referenca'), - 'type_id' => $request->input('type_id') + 'type_id' => $request->input('type_id'), ]); - } } diff --git a/app/Http/Controllers/DebtController.php b/app/Http/Controllers/DebtController.php index 9abeaff..b0364da 100644 --- a/app/Http/Controllers/DebtController.php +++ b/app/Http/Controllers/DebtController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; - class DebtController extends Controller { // diff --git a/app/Http/Controllers/ImportTemplateController.php b/app/Http/Controllers/ImportTemplateController.php index c82b8f3..3fde860 100644 --- a/app/Http/Controllers/ImportTemplateController.php +++ b/app/Http/Controllers/ImportTemplateController.php @@ -111,10 +111,10 @@ public function store(Request $request) 'is_active' => 'boolean', 'reactivate' => 'boolean', 'entities' => 'nullable|array', - 'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', + 'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments', 'mappings' => 'array', 'mappings.*.source_column' => 'required|string', - 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', + 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments', 'mappings.*.target_field' => 'nullable|string', 'mappings.*.transform' => 'nullable|string|max:50', 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref', @@ -244,7 +244,7 @@ public function addMapping(Request $request, ImportTemplate $template) } $data = validator($raw, [ 'source_column' => 'required|string', - 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', + 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments', 'target_field' => 'nullable|string', 'transform' => 'nullable|string|in:trim,upper,lower', 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', @@ -381,7 +381,7 @@ public function bulkAddMappings(Request $request, ImportTemplate $template) } $data = validator($raw, [ 'sources' => 'required|string', // comma and/or newline separated - 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', + 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments', 'default_field' => 'nullable|string', // if provided, used as the field name for all entries 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', 'transform' => 'nullable|string|in:trim,upper,lower', @@ -488,7 +488,7 @@ public function updateMapping(Request $request, ImportTemplate $template, Import } $data = validator($raw, [ 'source_column' => 'required|string', - 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments', + 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,case_objects,payments', 'target_field' => 'nullable|string', 'transform' => 'nullable|string|in:trim,upper,lower', 'apply_mode' => 'nullable|string|in:insert,update,both,keyref', diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index bd709df..3c31faf 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -2,8 +2,6 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; - class PaymentController extends Controller { // diff --git a/app/Http/Controllers/PostController.php b/app/Http/Controllers/PostController.php index 0f2f4be..3282f48 100644 --- a/app/Http/Controllers/PostController.php +++ b/app/Http/Controllers/PostController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers; -use App\Models\Post; use App\Http\Requests\StorePostRequest; use App\Http\Requests\UpdatePostRequest; +use App\Models\Post; class PostController extends Controller { diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php index 72a97d2..c166291 100644 --- a/app/Http/Controllers/SettingController.php +++ b/app/Http/Controllers/SettingController.php @@ -9,7 +9,8 @@ class SettingController extends Controller { // - public function index(Request $request){ + public function index(Request $request) + { return Inertia::render('Settings/Index'); } diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index eced01e..edf1166 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -71,7 +71,15 @@ public function share(Request $request): array $activities = \App\Models\Activity::query() ->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at']) ->whereDate('due_date', $today) - // Removed per-user unread filter: show notifications regardless of individual reads + // Exclude activities that have been marked as read by this user + ->whereNotExists(function ($q) use ($user, $today) { + $q->select(\DB::raw(1)) + ->from('activity_notification_reads') + ->whereColumn('activity_notification_reads.activity_id', 'activities.id') + ->where('activity_notification_reads.user_id', $user->id) + ->whereDate('activity_notification_reads.due_date', '<=', $today) + ->whereNotNull('activity_notification_reads.read_at'); + }) ->orderBy('created_at') ->limit(20) ->get(); diff --git a/app/Http/Requests/Admin/StoreUserRequest.php b/app/Http/Requests/Admin/StoreUserRequest.php new file mode 100644 index 0000000..803c56a --- /dev/null +++ b/app/Http/Requests/Admin/StoreUserRequest.php @@ -0,0 +1,52 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', Password::defaults(), 'confirmed'], + 'roles' => ['array'], + 'roles.*' => ['integer', 'exists:roles,id'], + ]; + } + + /** + * Get custom error messages. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Ime uporabnika je obvezno.', + 'email.required' => 'E-poštni naslov je obvezen.', + 'email.email' => 'E-poštni naslov mora biti veljaven.', + 'email.unique' => 'Ta e-poštni naslov je že v uporabi.', + 'password.required' => 'Geslo je obvezno.', + 'password.confirmed' => 'Gesli se ne ujemata.', + 'roles.*.exists' => 'Izbrana vloga ni veljavna.', + ]; + } +} diff --git a/app/Http/Requests/UpdateSegmentRequest.php b/app/Http/Requests/UpdateSegmentRequest.php index e07fc72..a4c6baa 100644 --- a/app/Http/Requests/UpdateSegmentRequest.php +++ b/app/Http/Requests/UpdateSegmentRequest.php @@ -17,7 +17,7 @@ public function rules(): array 'name' => ['required', 'string', 'max:50'], 'description' => ['nullable', 'string', 'max:255'], 'active' => ['boolean'], - 'exclude' => ['boolean'] + 'exclude' => ['boolean'], ]; } diff --git a/app/Http/Resources/PersonCollection.php b/app/Http/Resources/PersonCollection.php index aafecab..3a7b493 100644 --- a/app/Http/Resources/PersonCollection.php +++ b/app/Http/Resources/PersonCollection.php @@ -15,7 +15,7 @@ class PersonCollection extends ResourceCollection public function toArray(Request $request): array { return [ - 'data' => $this->collection + 'data' => $this->collection, ]; } } diff --git a/app/Jobs/PackageItemSmsJob.php b/app/Jobs/PackageItemSmsJob.php index b0bf22f..d1ac66a 100644 --- a/app/Jobs/PackageItemSmsJob.php +++ b/app/Jobs/PackageItemSmsJob.php @@ -10,6 +10,7 @@ use App\Models\SmsTemplate; use App\Services\Sms\SmsService; use Illuminate\Bus\Queueable; +use Illuminate\Bus\Batchable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; @@ -18,7 +19,7 @@ class PackageItemSmsJob implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable; public function __construct(public int $packageItemId) { @@ -97,7 +98,7 @@ public function handle(SmsService $sms): void /** @var SmsSender|null $sender */ $sender = $senderId ? SmsSender::find($senderId) : null; /** @var SmsTemplate|null $template */ - $template = $templateId ? SmsTemplate::find($templateId) : null; + $template = $templateId ? SmsTemplate::with(['action', 'decision'])->find($templateId) : null; $to = $target['number'] ?? null; if (! is_string($to) || $to === '') { @@ -117,7 +118,7 @@ public function handle(SmsService $sms): void $key = $scope === 'per_profile' && $profile ? "sms:{$provider}:{$profile->id}" : "sms:{$provider}"; // Throttle - $sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride) { + $sendClosure = function () use ($sms, $item, $package, $profile, $sender, $template, $to, $variables, $deliveryReport, $bodyOverride, $target) { // Idempotency key (optional external use) if (empty($item->idempotency_key)) { $hash = sha1(implode('|', [ @@ -188,6 +189,25 @@ public function handle(SmsService $sms): void $item->last_error = $log->status === 'sent' ? null : ($log->meta['error_message'] ?? 'Failed'); $item->save(); + // Create activity if template has action_id and decision_id configured and SMS was sent successfully + if ($newStatus === 'sent' && $template && ($template->action_id || $template->decision_id)) { + if (! empty($target['contract_id'])) { + $contract = Contract::query()->with('clientCase')->find($target['contract_id']); + + if ($contract && $contract->client_case_id) { + \App\Models\Activity::create(array_filter([ + 'client_case_id' => $contract->client_case_id, + 'contract_id' => $contract->id, + 'action_id' => $template->action_id, + 'decision_id' => $template->decision_id, + 'note' => "SMS poslan na {$to}: {$result['message']}", + 'created_at' => now(), + 'updated_at' => now(), + ])); + } + } + } + // Update package counters atomically if ($newStatus === 'sent') { $package->increment('sent_count'); diff --git a/app/Jobs/SendSmsJob.php b/app/Jobs/SendSmsJob.php index 6a09d20..b21e5a4 100644 --- a/app/Jobs/SendSmsJob.php +++ b/app/Jobs/SendSmsJob.php @@ -109,7 +109,7 @@ public function handle(SmsService $sms): void } // If no pre-created activity is provided and invoked from the case UI with a selected template, create an Activity - if (!$this->activityId && $this->templateId && $this->clientCaseId && $log) { + if (! $this->activityId && $this->templateId && $this->clientCaseId && $log) { try { /** @var SmsTemplate|null $template */ $template = SmsTemplate::find($this->templateId); diff --git a/app/Jobs/TestMailProfileConnection.php b/app/Jobs/TestMailProfileConnection.php index 3ceecd8..36dd99c 100644 --- a/app/Jobs/TestMailProfileConnection.php +++ b/app/Jobs/TestMailProfileConnection.php @@ -75,7 +75,8 @@ protected function performSmtpAuthTest(MailProfile $profile): void } $remote = ($encryption === 'ssl') ? 'ssl://'.$host : $host; - $errno = 0; $errstr = ''; + $errno = 0; + $errstr = ''; $socket = @fsockopen($remote, $port, $errno, $errstr, 15); if (! $socket) { throw new \RuntimeException("Connect failed: $errstr ($errno)"); @@ -104,7 +105,9 @@ protected function performSmtpAuthTest(MailProfile $profile): void // Cleanly quit $this->command($socket, "QUIT\r\n", [221], 'QUIT'); } finally { - try { fclose($socket); } catch (\Throwable) { + try { + fclose($socket); + } catch (\Throwable) { // ignore } } @@ -116,6 +119,7 @@ protected function performSmtpAuthTest(MailProfile $profile): void protected function command($socket, string $cmd, array $expect, string $context): string { fwrite($socket, $cmd); + return $this->expect($socket, $expect, $context); } @@ -138,6 +142,7 @@ protected function expect($socket, array $expectedCodes, string $context): strin if (! in_array($code, $expectedCodes, true)) { throw new \RuntimeException("Unexpected SMTP code $code during $context: ".implode(' | ', $lines)); } + return $line; } } diff --git a/app/Models/Action.php b/app/Models/Action.php index 1aec287..b4ad20c 100644 --- a/app/Models/Action.php +++ b/app/Models/Action.php @@ -13,6 +13,7 @@ class Action extends Model { /** @use HasFactory<\Database\Factories\ActionFactory> */ use HasFactory; + use Searchable; protected $fillable = ['name', 'color_tag', 'segment_id']; @@ -26,10 +27,9 @@ public function segment(): BelongsTo { return $this->belongsTo(\App\Models\Segment::class); } - + public function activities(): HasMany { return $this->hasMany(\App\Models\Activity::class); } - } diff --git a/app/Models/Client.php b/app/Models/Client.php index d0135f6..3581210 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -3,22 +3,23 @@ namespace App\Models; use App\Traits\Uuid; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Builder; use Laravel\Scout\Searchable; class Client extends Model { /** @use HasFactory<\Database\Factories\ClientFactory> */ use HasFactory; - use Uuid; + use Searchable; + use Uuid; protected $fillable = [ - 'person_id' + 'person_id', ]; protected $hidden = [ @@ -26,8 +27,7 @@ class Client extends Model 'person_id', ]; - - protected function makeAllSearchableUsing(Builder $query): Builder + protected function makeAllSearchableUsing(Builder $query): Builder { return $query->with('person'); } @@ -37,12 +37,11 @@ public function toSearchableArray(): array return [ 'person.full_name' => '', - 'person_addresses.address' => '' + 'person_addresses.address' => '', ]; } - - public function person(): BelongsTo + public function person(): BelongsTo { return $this->belongsTo(\App\Models\Person\Person::class); } diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 0f97745..719762e 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -55,6 +55,7 @@ protected function startDate(): Attribute return null; } $str = is_string($value) ? $value : (string) $value; + return \App\Services\DateNormalizer::toDate($str); } ); @@ -71,6 +72,7 @@ protected function endDate(): Attribute return null; } $str = is_string($value) ? $value : (string) $value; + return \App\Services\DateNormalizer::toDate($str); } ); diff --git a/app/Models/ImportEvent.php b/app/Models/ImportEvent.php index b2439a6..451dac1 100644 --- a/app/Models/ImportEvent.php +++ b/app/Models/ImportEvent.php @@ -11,7 +11,7 @@ class ImportEvent extends Model use HasFactory; protected $fillable = [ - 'import_id','user_id','event','level','message','context','import_row_id' + 'import_id', 'user_id', 'event', 'level', 'message', 'context', 'import_row_id', ]; protected $casts = [ diff --git a/app/Models/ImportTemplateMapping.php b/app/Models/ImportTemplateMapping.php index a7da751..432309f 100644 --- a/app/Models/ImportTemplateMapping.php +++ b/app/Models/ImportTemplateMapping.php @@ -11,7 +11,7 @@ class ImportTemplateMapping extends Model use HasFactory; protected $fillable = [ - 'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position' + 'import_template_id', 'entity', 'source_column', 'target_field', 'transform', 'apply_mode', 'options', 'position', ]; protected $casts = [ diff --git a/app/Models/MailProfile.php b/app/Models/MailProfile.php index b31b019..1c4a6cc 100644 --- a/app/Models/MailProfile.php +++ b/app/Models/MailProfile.php @@ -29,7 +29,7 @@ class MailProfile extends Model protected static function booted(): void { static::created(function (MailProfile $profile): void { - + \Log::info('mail_profile.created', [ 'id' => $profile->id, 'name' => $profile->name, diff --git a/app/Models/Person/PersonGroup.php b/app/Models/Person/PersonGroup.php index 08d9e41..7aeae76 100644 --- a/app/Models/Person/PersonGroup.php +++ b/app/Models/Person/PersonGroup.php @@ -15,5 +15,4 @@ public function persons(): HasMany { return $this->hasMany(\App\Models\Person\Person::class); } - } diff --git a/app/Models/Person/PersonType.php b/app/Models/Person/PersonType.php index 2f0bb50..833e5e7 100644 --- a/app/Models/Person/PersonType.php +++ b/app/Models/Person/PersonType.php @@ -4,7 +4,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class PersonType extends Model @@ -14,12 +13,11 @@ class PersonType extends Model protected $fillable = [ 'name', - 'description' + 'description', ]; public function persons(): HasMany { return $this->hasMany(\App\Models\Person\Person::class); } - } diff --git a/app/Models/Post.php b/app/Models/Post.php index bf17f69..09de108 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -13,6 +13,7 @@ class Post extends Model public function toSearchableArray() { $array = $this->toArray(); + return $array; } } diff --git a/app/Models/Segment.php b/app/Models/Segment.php index be41cc8..7d0f28f 100644 --- a/app/Models/Segment.php +++ b/app/Models/Segment.php @@ -15,22 +15,24 @@ class Segment extends Model 'name', 'description', 'active', - 'exclude' + 'exclude', ]; protected function casts(): array { return [ 'active' => 'boolean', - 'exclude' => 'boolean' + 'exclude' => 'boolean', ]; } - public function contracts(): BelongsToMany { + public function contracts(): BelongsToMany + { return $this->belongsToMany(\App\Models\Contract::class); } - public function clientCase(): BelongsToMany { + public function clientCase(): BelongsToMany + { return $this->belongsToMany(\App\Models\ClientCase::class)->withTimestamps(); } } diff --git a/app/Policies/MailProfilePolicy.php b/app/Policies/MailProfilePolicy.php index 2dd2fd8..2228027 100644 --- a/app/Policies/MailProfilePolicy.php +++ b/app/Policies/MailProfilePolicy.php @@ -12,6 +12,7 @@ protected function isAdmin(User $user): bool if (app()->environment('testing')) { return true; // simplify for tests } + return method_exists($user, 'isAdmin') ? $user->isAdmin() : $user->id === 1; // fallback heuristic } diff --git a/app/Policies/PostPolicy.php b/app/Policies/PostPolicy.php index 14d8d13..1a9901e 100644 --- a/app/Policies/PostPolicy.php +++ b/app/Policies/PostPolicy.php @@ -4,7 +4,6 @@ use App\Models\Post; use App\Models\User; -use Illuminate\Auth\Access\Response; class PostPolicy { diff --git a/app/Services/DateNormalizer.php b/app/Services/DateNormalizer.php index 6cbb7a4..7ea90a0 100644 --- a/app/Services/DateNormalizer.php +++ b/app/Services/DateNormalizer.php @@ -38,8 +38,10 @@ public static function toDate(?string $raw): ?string // Rebuild date with corrected year $month = (int) $dt->format('m'); $day = (int) $dt->format('d'); + return sprintf('%04d-%02d-%02d', $year, $month, $day); } + return $dt->format('Y-m-d'); } } diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php index d7b65a0..cc5dde8 100644 --- a/app/Services/ImportProcessor.php +++ b/app/Services/ImportProcessor.php @@ -5,6 +5,7 @@ use App\Models\Account; use App\Models\AccountType; use App\Models\Activity; +use App\Models\CaseObject; use App\Models\Client; use App\Models\ClientCase; use App\Models\Contract; @@ -480,6 +481,80 @@ public function process(Import $import, ?Authenticatable $user = null): array } } + // Case Objects: create or update case objects associated with contracts + // Support both 'case_object' and 'case_objects' keys (template may use plural) + if (isset($mapped['case_objects']) || isset($mapped['case_object'])) { + // Resolve contract_id from various sources + $contractIdForObject = null; + + // Get the case object data (support both plural and singular) + $caseObjectData = $mapped['case_objects'] ?? $mapped['case_object'] ?? []; + + // First, check if contract_id is directly provided in the mapping + if (! empty($caseObjectData['contract_id'])) { + $contractIdForObject = $caseObjectData['contract_id']; + } + // If contract was just created/resolved above, use its id + elseif ($contractResult && isset($contractResult['contract']) && $contractResult['contract'] instanceof Contract) { + $contractIdForObject = $contractResult['contract']->id; + } + // If account was processed and has a contract, use that contract + elseif ($accountResult && isset($accountResult['contract_id'])) { + $contractIdForObject = $accountResult['contract_id']; + } + + if ($contractIdForObject) { + $caseObjectResult = $this->upsertCaseObject($import, $mapped, $mappings, $contractIdForObject); + if ($caseObjectResult['action'] === 'skipped') { + $skipped++; + $importRow->update(['status' => 'skipped']); + ImportEvent::create([ + 'import_id' => $import->id, + 'user_id' => $user?->getAuthIdentifier(), + 'import_row_id' => $importRow->id, + 'event' => 'row_skipped', + 'level' => 'info', + 'message' => $caseObjectResult['message'] ?? 'Skipped (no changes).', + 'context' => $caseObjectResult['context'] ?? null, + ]); + } elseif ($caseObjectResult['action'] === 'inserted' || $caseObjectResult['action'] === 'updated') { + $imported++; + $importRow->update([ + 'status' => 'imported', + 'entity_type' => CaseObject::class, + 'entity_id' => $caseObjectResult['case_object']->id, + ]); + $objectFieldsStr = ''; + if (! empty($caseObjectResult['applied_fields'] ?? [])) { + $objectFieldsStr = $this->formatAppliedFieldMessage('case_object', $caseObjectResult['applied_fields']); + } + ImportEvent::create([ + 'import_id' => $import->id, + 'user_id' => $user?->getAuthIdentifier(), + 'import_row_id' => $importRow->id, + 'event' => 'row_imported', + 'level' => 'info', + 'message' => ucfirst($caseObjectResult['action']).' case object'.($objectFieldsStr ? ' '.$objectFieldsStr : ''), + 'context' => ['id' => $caseObjectResult['case_object']->id, 'fields' => $caseObjectResult['applied_fields'] ?? []], + ]); + } else { + $invalid++; + $importRow->update(['status' => 'invalid', 'errors' => ['Unhandled case object result']]); + } + } else { + $invalid++; + $importRow->update(['status' => 'invalid', 'errors' => ['Case object requires a valid contract_id']]); + ImportEvent::create([ + 'import_id' => $import->id, + 'user_id' => $user?->getAuthIdentifier(), + 'import_row_id' => $importRow->id, + 'event' => 'row_invalid', + 'level' => 'error', + 'message' => 'Case object requires a valid contract_id (not resolved).', + ]); + } + } + // Contacts: resolve person via Contract/Account chain, client_case.client_ref, contacts, or identifiers $personIdForRow = null; // Prefer person from contract created/updated above @@ -1447,6 +1522,109 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array } } + private function upsertCaseObject(Import $import, array $mapped, $mappings, int $contractId): array + { + // Support both 'case_object' and 'case_objects' keys (template may use plural) + $obj = $mapped['case_objects'] ?? $mapped['case_object'] ?? []; + $reference = $obj['reference'] ?? null; + $name = $obj['name'] ?? null; + + // Normalize reference (remove spaces) for consistent matching + if (! is_null($reference)) { + $reference = preg_replace('/\s+/', '', trim((string) $reference)); + } + + // At least name or reference must be provided + if (! $reference && (! $name || trim($name) === '')) { + return [ + 'action' => 'skipped', + 'message' => 'Case object requires at least a reference or name', + 'context' => ['missing' => 'reference and name'], + ]; + } + + $existing = null; + + // First, try to find by contract_id and reference (if reference provided) + if ($reference) { + $existing = CaseObject::query() + ->where('contract_id', $contractId) + ->where('reference', $reference) + ->first(); + } + + // If not found by reference and name is provided, check for duplicate by name + // This prevents creating duplicate case objects with same name for a contract + if (! $existing && ! is_null($name) && trim($name) !== '') { + $normalizedName = trim($name); + $duplicateByName = CaseObject::query() + ->where('contract_id', $contractId) + ->where('name', $normalizedName) + ->first(); + + if ($duplicateByName) { + // Found existing by name - use it as the existing record + $existing = $duplicateByName; + } + } + + // Build applyable data based on apply_mode + $applyInsert = []; + $applyUpdate = []; + $applyModeByField = []; + foreach ($mappings as $map) { + $target = (string) ($map->target_field ?? ''); + // Support both 'case_object.' and 'case_objects.' (template may use plural) + if (! str_starts_with($target, 'case_object.') && ! str_starts_with($target, 'case_objects.')) { + continue; + } + // Extract field name - handle both singular and plural prefix + $field = str_starts_with($target, 'case_objects.') + ? substr($target, strlen('case_objects.')) + : substr($target, strlen('case_object.')); + $applyModeByField[$field] = (string) ($map->apply_mode ?? 'both'); + } + + foreach ($obj as $field => $value) { + $applyMode = $applyModeByField[$field] ?? 'both'; + if (is_null($value) || (is_string($value) && trim($value) === '')) { + continue; + } + if (in_array($applyMode, ['both', 'insert'], true)) { + $applyInsert[$field] = $value; + } + if (in_array($applyMode, ['both', 'update'], true)) { + $applyUpdate[$field] = $value; + } + } + + if ($existing) { + // Build non-null changes for case object fields + $changes = array_filter($applyUpdate, fn ($v) => ! is_null($v)); + if (! empty($changes)) { + $existing->fill($changes); + $existing->save(); + + return ['action' => 'updated', 'case_object' => $existing, 'applied_fields' => $changes]; + } else { + return ['action' => 'skipped', 'case_object' => $existing, 'message' => 'No changes needed']; + } + } else { + // Create new case object + $data = array_merge([ + 'contract_id' => $contractId, + 'reference' => $reference, + ], $applyInsert); + + // Remove any null values + $data = array_filter($data, fn ($v) => ! is_null($v)); + + $created = CaseObject::create($data); + + return ['action' => 'inserted', 'case_object' => $created, 'applied_fields' => $data]; + } + } + private function mappingsContainRoot($mappings, string $root): bool { foreach ($mappings as $map) { @@ -1491,6 +1669,17 @@ private function upsertContractChain(Import $import, array $mapped, $mappings): return ['action' => 'invalid', 'message' => 'Missing contract.reference']; } + // Temporary debug output + $debugInfo = [ + 'row' => $rowIndex ?? 'unknown', + 'reference' => $reference, + 'has_person' => isset($mapped['person']), + 'person_tax' => $mapped['person']['tax_number'] ?? null, + 'client_id' => $import->client_id, + ]; + + \Log::info('ImportProcessor: upsertContractChain START', $debugInfo); + // Determine mapping mode for contract.reference (e.g., keyref) $refMode = $this->mappingMode($mappings, 'contract.reference'); @@ -1507,6 +1696,21 @@ private function upsertContractChain(Import $import, array $mapped, $mappings): ->where('contracts.reference', $reference) ->select('contracts.*') ->first(); + + // Debug logging to track contract lookup + if ($existing) { + \Log::info('ImportProcessor: Found existing contract', [ + 'client_id' => $clientId, + 'reference' => $reference, + 'contract_id' => $existing->id, + 'client_case_id' => $existing->client_case_id, + ]); + } else { + \Log::info('ImportProcessor: No existing contract found', [ + 'client_id' => $clientId, + 'reference' => $reference, + ]); + } } // If not found by client+reference and a specific client_case_id is provided, try that too @@ -1605,6 +1809,7 @@ private function upsertContractChain(Import $import, array $mapped, $mappings): // keyref: used as lookup and applied on insert, but not on update if ($mode === 'keyref') { $applyInsert[$field] = $value; + continue; } if (in_array($mode, ['insert', 'both'], true)) { @@ -2303,14 +2508,19 @@ private function findOrCreateClientCaseId(int $clientId, int $personId, ?string return $cc->id; } + + // client_ref was provided but not found, create new case with this client_ref + return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => $clientRef])->id; } - // Fallback: by (client_id, person_id) + + // No client_ref provided: reuse existing case by (client_id, person_id) if available $cc = ClientCase::where('client_id', $clientId)->where('person_id', $personId)->first(); if ($cc) { return $cc->id; } - return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => $clientRef])->id; + // Create new case without client_ref + return ClientCase::create(['client_id' => $clientId, 'person_id' => $personId, 'client_ref' => null])->id; } private function upsertEmail(int $personId, array $emailData, $mappings): array @@ -2429,10 +2639,16 @@ private function upsertAddress(int $personId, array $addrData, $mappings): array private function upsertPhone(int $personId, array $phoneData, $mappings): array { $nu = trim((string) ($phoneData['nu'] ?? '')); + // Strip all non-numeric characters from phone number + $nu = preg_replace('/[^0-9]/', '', $nu); if ($nu === '') { return ['action' => 'skipped', 'message' => 'No phone value']; } - $existing = PersonPhone::where('person_id', $personId)->where('nu', $nu)->first(); + + // Find existing phone by normalized number (strip non-numeric from DB values too) + $existing = PersonPhone::where('person_id', $personId) + ->whereRaw("REGEXP_REPLACE(nu, '[^0-9]', '', 'g') = ?", [$nu]) + ->first(); $applyInsert = []; $applyUpdate = []; foreach ($mappings as $map) { @@ -2472,6 +2688,12 @@ private function upsertPhone(int $personId, array $phoneData, $mappings): array $data = array_filter($applyInsert, fn ($v) => ! is_null($v)); $data['person_id'] = $personId; $data['type_id'] = $data['type_id'] ?? $this->getDefaultPhoneTypeId(); + // Override nu field with normalized value (digits only) + $data['nu'] = $nu; + // Set default phone_type to mobile enum if not provided + if (! array_key_exists('phone_type', $data) || $data['phone_type'] === null) { + $data['phone_type'] = \App\Enums\PersonPhoneType::Mobile; + } $created = PersonPhone::create($data); return ['action' => 'inserted', 'phone' => $created]; diff --git a/app/Services/ImportSimulationService.php b/app/Services/ImportSimulationService.php index 713bb47..4e0510c 100644 --- a/app/Services/ImportSimulationService.php +++ b/app/Services/ImportSimulationService.php @@ -1237,7 +1237,9 @@ private function simulateGenericRoot( $entity['country'] = $val('address.country') ?? null; break; case 'phone': - $entity['nu'] = $val('phone.nu') ?? null; + $rawNu = $val('phone.nu') ?? null; + // Strip all non-numeric characters from phone number + $entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null; break; case 'email': $entity['value'] = $val('email.value') ?? null; @@ -1246,6 +1248,11 @@ private function simulateGenericRoot( $entity['title'] = $val('client_case.title') ?? null; $entity['status'] = $val('client_case.status') ?? null; break; + case 'case_object': + $entity['name'] = $val('case_object.name') ?? null; + $entity['description'] = $val('case_object.description') ?? null; + $entity['type'] = $val('case_object.type') ?? null; + break; } if ($verbose) { @@ -1313,7 +1320,8 @@ private function genericIdentityCandidates(string $root, callable $val): array case 'phone': $nu = $val('phone.nu'); if ($nu) { - $norm = preg_replace('/\D+/', '', (string) $nu) ?? ''; + // Strip all non-numeric characters from phone number + $norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? ''; return $norm ? ['nu:'.$norm] : []; } @@ -1346,6 +1354,20 @@ private function genericIdentityCandidates(string $root, callable $val): array } return []; + case 'case_object': + $ref = $val('case_object.reference'); + $name = $val('case_object.name'); + $ids = []; + if ($ref) { + // Normalize reference (remove spaces) + $normRef = preg_replace('/\s+/', '', trim((string) $ref)); + $ids[] = 'ref:'.$normRef; + } + if ($name) { + $ids[] = 'name:'.mb_strtolower(trim((string) $name)); + } + + return $ids; default: return []; } @@ -1366,7 +1388,8 @@ private function loadExistingGenericIdentities(string $root): array case 'phone': foreach (\App\Models\Person\PersonPhone::query()->pluck('nu') as $p) { if ($p) { - $set['nu:'.preg_replace('/\D+/', '', (string) $p)] = true; + // Strip all non-numeric characters from phone number + $set['nu:'.preg_replace('/[^0-9]/', '', (string) $p)] = true; } } break; @@ -1391,6 +1414,18 @@ private function loadExistingGenericIdentities(string $root): array } } break; + case 'case_object': + foreach (\App\Models\CaseObject::query()->get(['reference', 'name']) as $rec) { + if ($rec->reference) { + // Normalize reference (remove spaces) + $normRef = preg_replace('/\s+/', '', trim((string) $rec->reference)); + $set['ref:'.$normRef] = true; + } + if ($rec->name) { + $set['name:'.mb_strtolower(trim((string) $rec->name))] = true; + } + } + break; } } catch (\Throwable) { // swallow and return what we have @@ -1411,6 +1446,7 @@ private function modelClassForGeneric(string $root): ?string 'activity' => \App\Models\Activity::class, 'client' => \App\Models\Client::class, 'client_case' => \App\Models\ClientCase::class, + 'case_object' => \App\Models\CaseObject::class, ][$root] ?? null; } @@ -1563,7 +1599,8 @@ private function simulateGenericRootMulti( } elseif ($root === 'phone') { $nu = $groupVals('phone', 'nu')[$g] ?? null; if ($nu) { - $norm = preg_replace('/\D+/', '', (string) $nu) ?? ''; + // Strip all non-numeric characters from phone number + $norm = preg_replace('/[^0-9]/', '', (string) $nu) ?? ''; if ($norm) { $identityCandidates = ['nu:'.$norm]; } @@ -1615,7 +1652,9 @@ private function simulateGenericRootMulti( if ($root === 'email') { $entity['value'] = $groupVals('email', 'value')[$g] ?? null; } elseif ($root === 'phone') { - $entity['nu'] = $groupVals('phone', 'nu')[$g] ?? null; + $rawNu = $groupVals('phone', 'nu')[$g] ?? null; + // Strip all non-numeric characters from phone number + $entity['nu'] = $rawNu !== null ? preg_replace('/[^0-9]/', '', (string) $rawNu) : null; } elseif ($root === 'address') { $entity['address'] = $groupVals('address', 'address')[$g] ?? null; $entity['country'] = $groupVals('address', 'country')[$g] ?? null; diff --git a/app/Services/Sms/SmsService.php b/app/Services/Sms/SmsService.php index c6306e3..5dfe6ff 100644 --- a/app/Services/Sms/SmsService.php +++ b/app/Services/Sms/SmsService.php @@ -24,6 +24,7 @@ protected function normalizeForSms(string $text): string { // Replace NBSP (\xC2\xA0 in UTF-8) and tabs with regular space $text = str_replace(["\u{00A0}", "\t"], ' ', $text); + // Optionally collapse CRLF to LF (providers typically accept both); keep as-is otherwise return $text; } diff --git a/app/Traits/Uuid.php b/app/Traits/Uuid.php index 6a9d778..f6d053c 100644 --- a/app/Traits/Uuid.php +++ b/app/Traits/Uuid.php @@ -3,9 +3,11 @@ namespace App\Traits; use Illuminate\Support\Str; + trait Uuid { - protected static function boot(){ + protected static function boot() + { parent::boot(); static::creating(function ($model) { $model->uuid = (string) Str::uuid(); diff --git a/config/database.php b/config/database.php index 20ee0ac..557bd58 100644 --- a/config/database.php +++ b/config/database.php @@ -98,7 +98,7 @@ 'options' => [ 'LC_COLLATE' => env('PGSQL_LC_COLLATE', 'en_US.UTF-8'), 'LC_CTYPE' => env('PGSQL_LC_CTYPE', 'en_US.UTF-8'), - ] + ], ], 'sqlsrv' => [ diff --git a/config/larapex-charts.php b/config/larapex-charts.php index b04141d..b3596c7 100644 --- a/config/larapex-charts.php +++ b/config/larapex-charts.php @@ -1,4 +1,4 @@ - [ '#008FFB', '#00E396', '#feb019', '#ff455f', '#775dd0', '#80effe', - '#0077B5', '#ff6384', '#c9cbcf', '#0057ff', '00a9f4', '#2ccdc9', '#5e72e4' - ] -]; \ No newline at end of file + '#0077B5', '#ff6384', '#c9cbcf', '#0057ff', '00a9f4', '#2ccdc9', '#5e72e4', + ], +]; diff --git a/config/scout.php b/config/scout.php index c3f0f42..5e5ed76 100644 --- a/config/scout.php +++ b/config/scout.php @@ -84,7 +84,7 @@ | */ - 'soft_delete' => true, + 'soft_delete' => true, /* |-------------------------------------------------------------------------- @@ -200,4 +200,4 @@ ], ], -]; \ No newline at end of file +]; diff --git a/database/factories/ActionFactory.php b/database/factories/ActionFactory.php index c80c2f8..0bdabb4 100644 --- a/database/factories/ActionFactory.php +++ b/database/factories/ActionFactory.php @@ -2,8 +2,8 @@ namespace Database\Factories; -use Illuminate\Database\Eloquent\Factories\Factory; use App\Models\Segment; +use Illuminate\Database\Eloquent\Factories\Factory; /** * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Action> diff --git a/database/migrations/2024_10_14_185613_create_person_table.php b/database/migrations/2024_10_14_185613_create_person_table.php index 45028e7..4803a51 100644 --- a/database/migrations/2024_10_14_185613_create_person_table.php +++ b/database/migrations/2024_10_14_185613_create_person_table.php @@ -11,43 +11,43 @@ */ public function up(): void { - Schema::create('person_types', function(Blueprint $table){ + Schema::create('person_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); }); - Schema::create('person_groups', function(Blueprint $table){ + Schema::create('person_groups', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->string('color_tag', 50)->nullable(); $table->softDeletes(); $table->timestamps(); }); - Schema::create('phone_types', function(Blueprint $table){ + Schema::create('phone_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); }); - Schema::create('address_types', function(Blueprint $table){ + Schema::create('address_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); }); - + Schema::create('person', function (Blueprint $table) { $table->id(); $table->uuid('uuid')->unique(); @@ -55,11 +55,11 @@ public function up(): void $table->string('first_name', 255)->nullable(); $table->string('last_name', 255)->nullable(); $table->string('full_name', 255)->nullable(); - $table->enum('gender', ['m','w'])->nullable(); + $table->enum('gender', ['m', 'w'])->nullable(); $table->date('birthday')->nullable(); $table->string('tax_number', 99)->nullable(); - $table->string('social_security_number',99)->nullable(); - $table->string('description',500)->nullable(); + $table->string('social_security_number', 99)->nullable(); + $table->string('description', 500)->nullable(); $table->foreignId('group_id')->references('id')->on('person_groups'); $table->foreignId('type_id')->references('id')->on('person_types'); $table->unsignedTinyInteger('active')->default(1); @@ -68,12 +68,12 @@ public function up(): void $table->timestamps(); }); - Schema::create('person_phones', function(Blueprint $table){ + Schema::create('person_phones', function (Blueprint $table) { $table->id(); - $table->string('nu',50); + $table->string('nu', 50); $table->unsignedInteger('country_code')->nullable(); $table->foreignId('type_id')->references('id')->on('phone_types'); - $table->string('description',125)->nullable(); + $table->string('description', 125)->nullable(); $table->foreignIdFor(\App\Models\Person\Person::class); $table->unsignedTinyInteger('active')->default(1); $table->softDeletes(); @@ -82,12 +82,12 @@ public function up(): void }); - Schema::create('person_addresses', function(Blueprint $table){ + Schema::create('person_addresses', function (Blueprint $table) { $table->id(); - $table->string('address',150); + $table->string('address', 150); $table->string('country')->nullable(); $table->foreignId('type_id')->references('id')->on('address_types'); - $table->string('description',125)->nullable(); + $table->string('description', 125)->nullable(); $table->foreignIdFor(\App\Models\Person\Person::class); $table->unsignedTinyInteger('active')->default(1); $table->softDeletes(); diff --git a/database/migrations/2024_10_19_090817_create_accounts_table.php b/database/migrations/2024_10_19_090817_create_accounts_table.php index c188002..7fb5a72 100644 --- a/database/migrations/2024_10_19_090817_create_accounts_table.php +++ b/database/migrations/2024_10_19_090817_create_accounts_table.php @@ -11,10 +11,10 @@ */ public function up(): void { - Schema::create('account_types', function(Blueprint $table){ + Schema::create('account_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); diff --git a/database/migrations/2024_10_19_100530_create_debts_table.php b/database/migrations/2024_10_19_100530_create_debts_table.php index cf29831..6dcffa7 100644 --- a/database/migrations/2024_10_19_100530_create_debts_table.php +++ b/database/migrations/2024_10_19_100530_create_debts_table.php @@ -11,26 +11,25 @@ */ public function up(): void { - Schema::create('debt_types', function(Blueprint $table){ + Schema::create('debt_types', function (Blueprint $table) { $table->id(); - $table->string('name',50)->unique(); - $table->string('description',125)->nullable(); + $table->string('name', 50)->unique(); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); }); - Schema::create('debts', function (Blueprint $table) { $table->id(); - $table->string('reference',125)->nullable(); - $table->string('invoice_nu',125)->nullable(); + $table->string('reference', 125)->nullable(); + $table->string('invoice_nu', 125)->nullable(); $table->date('issue_date')->nullable(); $table->date('due_date')->nullable(); $table->decimal('amount', 11, 4)->nullable(); $table->decimal('interest', 11, 8)->nullable(); $table->date('interest_start_date')->nullable(); - $table->string('description',125)->nullable(); + $table->string('description', 125)->nullable(); $table->foreignId('account_id')->references('id')->on('accounts'); $table->foreignId('type_id')->references('id')->on('debt_types'); $table->unsignedTinyInteger('active')->default(1); diff --git a/database/migrations/2024_10_19_100706_create_payments_table.php b/database/migrations/2024_10_19_100706_create_payments_table.php index 60a8af2..1b9bab8 100644 --- a/database/migrations/2024_10_19_100706_create_payments_table.php +++ b/database/migrations/2024_10_19_100706_create_payments_table.php @@ -9,16 +9,15 @@ /** * Run the migrations. */ - public function up(): void { - Schema::create('payment_types', function(Blueprint $table){ + Schema::create('payment_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); - + }); Schema::create('payments', function (Blueprint $table) { @@ -26,7 +25,7 @@ public function up(): void $table->string('reference', 125)->nullable(); $table->string('payment_nu', 125)->nullable(); $table->date('payment_date')->nullable(); - $table->decimal('amount',11,4)->nullable(); + $table->decimal('amount', 11, 4)->nullable(); $table->foreignId('debt_id')->references('id')->on('debts'); $table->foreignId('type_id')->references('id')->on('payment_types'); $table->unsignedTinyInteger('active')->default(1); diff --git a/database/migrations/2024_10_20_113420_create_contracts_table.php b/database/migrations/2024_10_20_113420_create_contracts_table.php index c72cd4e..6f7d50f 100644 --- a/database/migrations/2024_10_20_113420_create_contracts_table.php +++ b/database/migrations/2024_10_20_113420_create_contracts_table.php @@ -11,10 +11,10 @@ */ public function up(): void { - Schema::create('contract_types', function(Blueprint $table){ + Schema::create('contract_types', function (Blueprint $table) { $table->id(); - $table->string('name',50); - $table->string('description',125)->nullable(); + $table->string('name', 50); + $table->string('description', 125)->nullable(); $table->softDeletes(); $table->timestamps(); diff --git a/database/migrations/2025_09_26_184034_alter_table_account_add_balance_column.php b/database/migrations/2025_09_26_184034_alter_table_account_add_balance_column.php index 7fdb077..4cf0c08 100644 --- a/database/migrations/2025_09_26_184034_alter_table_account_add_balance_column.php +++ b/database/migrations/2025_09_26_184034_alter_table_account_add_balance_column.php @@ -12,9 +12,9 @@ public function up(): void { Schema::table('accounts', function (Blueprint $table) { - $table->decimal("initial_amount", 20, 4)->default(0); - $table->decimal("balance_amount", 20, 4)->default(0); - $table->date("promise_date")->nullable(); + $table->decimal('initial_amount', 20, 4)->default(0); + $table->decimal('balance_amount', 20, 4)->default(0); + $table->date('promise_date')->nullable(); $table->index('balance_amount'); $table->index('promise_date'); }); diff --git a/database/migrations/2025_09_26_191209_create_bank_accounts_table.php b/database/migrations/2025_09_26_191209_create_bank_accounts_table.php index 7e8eb80..a03e79e 100644 --- a/database/migrations/2025_09_26_191209_create_bank_accounts_table.php +++ b/database/migrations/2025_09_26_191209_create_bank_accounts_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ /** * Run the migrations. */ @@ -36,7 +37,7 @@ public function up(): void $table->index('iban'); $table->softDeletes(); $table->timestamps(); - + }); } diff --git a/database/migrations/2025_09_26_201700_add_unique_indexes_for_import_dedup.php b/database/migrations/2025_09_26_201700_add_unique_indexes_for_import_dedup.php index 62ad0c6..8747f1c 100644 --- a/database/migrations/2025_09_26_201700_add_unique_indexes_for_import_dedup.php +++ b/database/migrations/2025_09_26_201700_add_unique_indexes_for_import_dedup.php @@ -10,35 +10,35 @@ public function up(): void { // People: unique by (tax_number, social_security_number, deleted_at) Schema::table('person', function (Blueprint $table) { - if (!self::hasIndex('person', 'person_identity_unique')) { + if (! self::hasIndex('person', 'person_identity_unique')) { $table->unique(['tax_number', 'social_security_number', 'deleted_at'], 'person_identity_unique'); } }); // Phones: unique by (person_id, nu, country_code, deleted_at) Schema::table('person_phones', function (Blueprint $table) { - if (!self::hasIndex('person_phones', 'person_phones_unique')) { + if (! self::hasIndex('person_phones', 'person_phones_unique')) { $table->unique(['person_id', 'nu', 'country_code', 'deleted_at'], 'person_phones_unique'); } }); // Addresses: unique by (person_id, address, country, deleted_at) Schema::table('person_addresses', function (Blueprint $table) { - if (!self::hasIndex('person_addresses', 'person_addresses_unique')) { + if (! self::hasIndex('person_addresses', 'person_addresses_unique')) { $table->unique(['person_id', 'address', 'country', 'deleted_at'], 'person_addresses_unique'); } }); // Contracts: unique by (client_case_id, reference, deleted_at) Schema::table('contracts', function (Blueprint $table) { - if (!self::hasIndex('contracts', 'contracts_reference_unique')) { + if (! self::hasIndex('contracts', 'contracts_reference_unique')) { $table->unique(['client_case_id', 'reference', 'deleted_at'], 'contracts_reference_unique'); } }); // Accounts: unique by (contract_id, reference, deleted_at) Schema::table('accounts', function (Blueprint $table) { - if (!self::hasIndex('accounts', 'accounts_reference_unique')) { + if (! self::hasIndex('accounts', 'accounts_reference_unique')) { $table->unique(['contract_id', 'reference', 'deleted_at'], 'accounts_reference_unique'); } }); @@ -70,6 +70,7 @@ private static function hasIndex(string $table, string $index): bool $connection = Schema::getConnection(); $schemaManager = $connection->getDoctrineSchemaManager(); $doctrineTable = $schemaManager->listTableDetails($table); + return $doctrineTable->hasIndex($index); } catch (\Throwable $e) { return false; diff --git a/database/migrations/2025_09_26_202500_add_balance_amount_to_accounts_table.php b/database/migrations/2025_09_26_202500_add_balance_amount_to_accounts_table.php index 324df87..c67682c 100644 --- a/database/migrations/2025_09_26_202500_add_balance_amount_to_accounts_table.php +++ b/database/migrations/2025_09_26_202500_add_balance_amount_to_accounts_table.php @@ -9,9 +9,9 @@ public function up(): void { Schema::table('accounts', function (Blueprint $table) { - - if (!Schema::hasColumn('accounts', 'balance_amount')) { - + + if (! Schema::hasColumn('accounts', 'balance_amount')) { + $table->decimal('balance_amount', 18, 4)->nullable()->after('description'); $table->index('balance_amount'); } diff --git a/database/migrations/2025_09_26_202600_add_fk_import_template_to_imports_table.php b/database/migrations/2025_09_26_202600_add_fk_import_template_to_imports_table.php index 887c773..5e7acbf 100644 --- a/database/migrations/2025_09_26_202600_add_fk_import_template_to_imports_table.php +++ b/database/migrations/2025_09_26_202600_add_fk_import_template_to_imports_table.php @@ -9,7 +9,7 @@ public function up(): void { Schema::table('imports', function (Blueprint $table) { - if (!Schema::hasColumn('imports', 'import_template_id')) { + if (! Schema::hasColumn('imports', 'import_template_id')) { $table->foreignId('import_template_id')->nullable(); } // Add foreign key if not exists (Postgres will error if duplicate, so wrap in try/catch in runtime, but Schema builder doesn't support conditional FKs) diff --git a/database/migrations/2025_09_27_000001_alter_person_nu_to_string.php b/database/migrations/2025_09_27_000001_alter_person_nu_to_string.php index aaaea91..e52a56f 100644 --- a/database/migrations/2025_09_27_000001_alter_person_nu_to_string.php +++ b/database/migrations/2025_09_27_000001_alter_person_nu_to_string.php @@ -29,8 +29,9 @@ public function up(): void $used = []; foreach ($rows as $row) { if (is_string($row->nu) && preg_match('/^[A-Za-z0-9]{6}$/', $row->nu)) { - if (!isset($used[$row->nu])) { + if (! isset($used[$row->nu])) { $used[$row->nu] = true; + continue; } // duplicate will be regenerated below diff --git a/database/migrations/2025_09_27_221000_add_position_to_import_mappings.php b/database/migrations/2025_09_27_221000_add_position_to_import_mappings.php index 8ac6866..041dcb9 100644 --- a/database/migrations/2025_09_27_221000_add_position_to_import_mappings.php +++ b/database/migrations/2025_09_27_221000_add_position_to_import_mappings.php @@ -9,7 +9,7 @@ public function up(): void { Schema::table('import_mappings', function (Blueprint $table) { - if (!Schema::hasColumn('import_mappings', 'position')) { + if (! Schema::hasColumn('import_mappings', 'position')) { $table->unsignedInteger('position')->nullable()->after('options'); } $table->index(['import_id', 'position']); diff --git a/database/migrations/2025_09_27_230500_add_entity_to_import_mappings.php b/database/migrations/2025_09_27_230500_add_entity_to_import_mappings.php index be9ba66..4ac891b 100644 --- a/database/migrations/2025_09_27_230500_add_entity_to_import_mappings.php +++ b/database/migrations/2025_09_27_230500_add_entity_to_import_mappings.php @@ -10,7 +10,7 @@ public function up(): void { Schema::table('import_mappings', function (Blueprint $table) { - if (!Schema::hasColumn('import_mappings', 'entity')) { + if (! Schema::hasColumn('import_mappings', 'entity')) { $table->string('entity', 64)->nullable()->after('import_id'); } $table->index(['import_id', 'entity']); @@ -19,9 +19,11 @@ public function up(): void // Backfill entity from target_field's first segment where possible DB::table('import_mappings')->orderBy('id')->chunkById(1000, function ($rows) { foreach ($rows as $row) { - if (!empty($row->entity)) continue; + if (! empty($row->entity)) { + continue; + } $entity = null; - if (!empty($row->target_field)) { + if (! empty($row->target_field)) { $parts = explode('.', $row->target_field); $record = $parts[0] ?? null; if ($record) { @@ -49,7 +51,10 @@ public function down(): void Schema::table('import_mappings', function (Blueprint $table) { if (Schema::hasColumn('import_mappings', 'entity')) { // drop composite index if exists - try { $table->dropIndex(['import_id', 'entity']); } catch (\Throwable $e) { /* ignore */ } + try { + $table->dropIndex(['import_id', 'entity']); + } catch (\Throwable $e) { /* ignore */ + } $table->dropColumn('entity'); } }); diff --git a/database/migrations/2025_09_27_230600_add_entity_to_import_template_mappings.php b/database/migrations/2025_09_27_230600_add_entity_to_import_template_mappings.php index 8b28441..04546bf 100644 --- a/database/migrations/2025_09_27_230600_add_entity_to_import_template_mappings.php +++ b/database/migrations/2025_09_27_230600_add_entity_to_import_template_mappings.php @@ -10,7 +10,7 @@ public function up(): void { Schema::table('import_template_mappings', function (Blueprint $table) { - if (!Schema::hasColumn('import_template_mappings', 'entity')) { + if (! Schema::hasColumn('import_template_mappings', 'entity')) { $table->string('entity', 64)->nullable()->after('import_template_id'); } $table->index(['import_template_id', 'entity']); @@ -19,9 +19,11 @@ public function up(): void // Backfill entity from target_field first segment DB::table('import_template_mappings')->orderBy('id')->chunkById(1000, function ($rows) { foreach ($rows as $row) { - if (!empty($row->entity)) continue; + if (! empty($row->entity)) { + continue; + } $entity = null; - if (!empty($row->target_field)) { + if (! empty($row->target_field)) { $parts = explode('.', $row->target_field); $record = $parts[0] ?? null; if ($record) { @@ -47,7 +49,10 @@ public function down(): void { Schema::table('import_template_mappings', function (Blueprint $table) { if (Schema::hasColumn('import_template_mappings', 'entity')) { - try { $table->dropIndex(['import_template_id', 'entity']); } catch (\Throwable $e) { /* ignore */ } + try { + $table->dropIndex(['import_template_id', 'entity']); + } catch (\Throwable $e) { /* ignore */ + } $table->dropColumn('entity'); } }); diff --git a/database/migrations/2025_09_28_000001_create_field_job_settings_table.php b/database/migrations/2025_09_28_000001_create_field_job_settings_table.php index e9e4cf7..5136dc9 100644 --- a/database/migrations/2025_09_28_000001_create_field_job_settings_table.php +++ b/database/migrations/2025_09_28_000001_create_field_job_settings_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::create('field_job_settings', function (Blueprint $table) { diff --git a/database/migrations/2025_09_28_000002_create_field_jobs_table.php b/database/migrations/2025_09_28_000002_create_field_jobs_table.php index c5f6560..5be1418 100644 --- a/database/migrations/2025_09_28_000002_create_field_jobs_table.php +++ b/database/migrations/2025_09_28_000002_create_field_jobs_table.php @@ -4,7 +4,8 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration { +return new class extends Migration +{ public function up(): void { Schema::create('field_jobs', function (Blueprint $table) { diff --git a/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php b/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php index ed0c1fe..fb40847 100644 --- a/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php +++ b/database/migrations/2025_09_28_121500_alter_contract_configs_support_multiple.php @@ -33,8 +33,8 @@ public function up(): void $table->unique(['contract_type_id', 'segment_id']); }); - // Mark existing rows as initial - \DB::table('contract_configs')->update(['is_initial' => true]); + // Mark existing rows as initial + \DB::table('contract_configs')->update(['is_initial' => true]); } public function down(): void diff --git a/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php b/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php index 0f82b8a..88cbdad 100644 --- a/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php +++ b/database/migrations/2025_09_28_151500_add_unique_contract_reference_per_client_case_v2.php @@ -26,9 +26,13 @@ public function up(): void $keepFirst = true; foreach ($rows as $row) { - if ($keepFirst) { $keepFirst = false; continue; } + if ($keepFirst) { + $keepFirst = false; + + continue; + } $base = mb_substr($row->reference, 0, 120); - $newRef = $base . '-' . $row->id; + $newRef = $base.'-'.$row->id; DB::table('contracts')->where('id', $row->id)->update(['reference' => $newRef]); } } diff --git a/database/seeders/AccountSeeder.php b/database/seeders/AccountSeeder.php index 9c021c3..163d906 100644 --- a/database/seeders/AccountSeeder.php +++ b/database/seeders/AccountSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class AccountSeeder extends Seeder diff --git a/database/seeders/ActionSeeder.php b/database/seeders/ActionSeeder.php index 30ba77f..6f025e0 100644 --- a/database/seeders/ActionSeeder.php +++ b/database/seeders/ActionSeeder.php @@ -4,7 +4,6 @@ use App\Models\Action; use App\Models\Decision; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ActionSeeder extends Seeder @@ -17,34 +16,34 @@ public function run(): void Action::create([ 'name' => 'KLIC - IZHODNI', 'color_tag' => '', - 'segment_id' => 1 + 'segment_id' => 1, ]); Action::create([ 'name' => 'KLIC - VHODNI', 'color_tag' => '', - 'segment_id' => 1 + 'segment_id' => 1, ]); Action::create([ 'name' => 'ePOŠTA', 'color_tag' => '', - 'segment_id' => 1 + 'segment_id' => 1, ]); Action::create([ 'name' => 'VROČANJE', 'color_tag' => '', - 'segment_id' => 1 + 'segment_id' => 1, ]); Action::create([ 'name' => 'SMS', 'color_tag' => '', - 'segment_id' => 1 + 'segment_id' => 1, ]); - //------- 1 + // ------- 1 Decision::create([ 'name' => 'Obljuba', 'color_tag' => '', @@ -52,15 +51,15 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 1, - 'decision_id' => 1 + 'decision_id' => 1, ]); \DB::table('action_decision')->insert([ 'action_id' => 2, - 'decision_id' => 1 + 'decision_id' => 1, ]); - //------- 2 + // ------- 2 Decision::create([ 'name' => 'Poslana', 'color_tag' => '', @@ -68,10 +67,10 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 3, - 'decision_id' => 2 + 'decision_id' => 2, ]); - //-------- 3 + // -------- 3 Decision::create([ 'name' => 'Prejeta', 'color_tag' => '', @@ -79,10 +78,10 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 3, - 'decision_id' => 3 + 'decision_id' => 3, ]); - //--------- 4 + // --------- 4 Decision::create([ 'name' => 'Neuspešna', 'color_tag' => '', @@ -90,10 +89,10 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 4, - 'decision_id' => 4 + 'decision_id' => 4, ]); - //--------- 5 + // --------- 5 Decision::create([ 'name' => 'Uspešna', 'color_tag' => '', @@ -101,10 +100,10 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 4, - 'decision_id' => 5 + 'decision_id' => 5, ]); - //--------- 6 + // --------- 6 Decision::create([ 'name' => 'Poslan SMS', 'color_tag' => '', @@ -112,10 +111,10 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 5, - 'decision_id' => 6 + 'decision_id' => 6, ]); - //--------- 7 + // --------- 7 Decision::create([ 'name' => 'Prejet SMS', 'color_tag' => '', @@ -123,7 +122,7 @@ public function run(): void \DB::table('action_decision')->insert([ 'action_id' => 5, - 'decision_id' => 7 + 'decision_id' => 7, ]); } } diff --git a/database/seeders/ActivitySeeder.php b/database/seeders/ActivitySeeder.php index af1f69b..1a757a6 100644 --- a/database/seeders/ActivitySeeder.php +++ b/database/seeders/ActivitySeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ActivitySeeder extends Seeder diff --git a/database/seeders/ClientCaseSeeder.php b/database/seeders/ClientCaseSeeder.php index b675120..375f61c 100644 --- a/database/seeders/ClientCaseSeeder.php +++ b/database/seeders/ClientCaseSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ClientCaseSeeder extends Seeder diff --git a/database/seeders/ClientSeeder.php b/database/seeders/ClientSeeder.php index 05933ac..ba02b10 100644 --- a/database/seeders/ClientSeeder.php +++ b/database/seeders/ClientSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ClientSeeder extends Seeder diff --git a/database/seeders/ContractSeeder.php b/database/seeders/ContractSeeder.php index ebdc23f..71b21d9 100644 --- a/database/seeders/ContractSeeder.php +++ b/database/seeders/ContractSeeder.php @@ -2,9 +2,7 @@ namespace Database\Seeders; -use App\Models\Contract; use App\Models\ContractType; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class ContractSeeder extends Seeder @@ -12,16 +10,14 @@ class ContractSeeder extends Seeder /** * Run the database seeds. */ - - public function run(): void { $contractType = [ - [ 'name' => 'delivery', 'description' => ''], - [ 'name' => 'leasing', 'description' => ''] + ['name' => 'delivery', 'description' => ''], + ['name' => 'leasing', 'description' => ''], ]; - foreach($contractType as $ct){ + foreach ($contractType as $ct) { ContractType::create($ct); } } diff --git a/database/seeders/DebtSeeder.php b/database/seeders/DebtSeeder.php index bd0edb7..8959653 100644 --- a/database/seeders/DebtSeeder.php +++ b/database/seeders/DebtSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DebtSeeder extends Seeder diff --git a/database/seeders/DecisionSeeder.php b/database/seeders/DecisionSeeder.php index 12b397b..e0502e8 100644 --- a/database/seeders/DecisionSeeder.php +++ b/database/seeders/DecisionSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class DecisionSeeder extends Seeder diff --git a/database/seeders/ImportEntitySeeder.php b/database/seeders/ImportEntitySeeder.php index 541c4fb..cda9f36 100644 --- a/database/seeders/ImportEntitySeeder.php +++ b/database/seeders/ImportEntitySeeder.php @@ -125,6 +125,21 @@ public function run(): void ], 'ui' => ['order' => 7], ], + [ + 'key' => 'case_objects', + 'canonical_root' => 'case_object', + 'label' => 'Case Objects', + 'fields' => ['reference', 'name', 'description', 'type', 'contract_id'], + 'aliases' => ['case_object', 'case_objects', 'object', 'objects', 'predmet', 'predmeti'], + 'rules' => [ + ['pattern' => '/^(sklic|reference|ref)\b/i', 'field' => 'reference'], + ['pattern' => '/^(ime|naziv|name|title)\b/i', 'field' => 'name'], + ['pattern' => '/^(tip|vrsta|type|kind)\b/i', 'field' => 'type'], + ['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'], + ['pattern' => '/^(contract\s*id|contract_id|pogodba\s*id|pogodba_id)\b/i', 'field' => 'contract_id'], + ], + 'ui' => ['order' => 8], + ], [ 'key' => 'payments', 'canonical_root' => 'payment', @@ -158,7 +173,7 @@ public function run(): void ['pattern' => '/^(datum|date|paid\s*at|payment\s*date)\b/i', 'field' => 'payment_date'], ['pattern' => '/^(znesek|amount|vplacilo|vplačilo|placilo|plačilo)\b/i', 'field' => 'amount'], ], - 'ui' => ['order' => 8], + 'ui' => ['order' => 9], ], ]; diff --git a/database/seeders/PaymentSeeder.php b/database/seeders/PaymentSeeder.php index dd108b7..730a50a 100644 --- a/database/seeders/PaymentSeeder.php +++ b/database/seeders/PaymentSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class PaymentSeeder extends Seeder diff --git a/database/seeders/PersonSeeder.php b/database/seeders/PersonSeeder.php index bd64294..cc4bd02 100644 --- a/database/seeders/PersonSeeder.php +++ b/database/seeders/PersonSeeder.php @@ -19,54 +19,53 @@ public function run(): void { // $personTypes = [ - [ 'name' => 'legal', 'description' => ''], - [ 'name' => 'natural', 'description' => ''] + ['name' => 'legal', 'description' => ''], + ['name' => 'natural', 'description' => ''], ]; $personGroups = [ - [ 'name' => 'naročnik', 'description' => '', 'color_tag' => 'blue-500'], - [ 'name' => 'primer naročnika', 'description' => '', 'color_tag' => 'red-400'] + ['name' => 'naročnik', 'description' => '', 'color_tag' => 'blue-500'], + ['name' => 'primer naročnika', 'description' => '', 'color_tag' => 'red-400'], ]; $phoneTypes = [ - [ 'name' => 'mobile', 'description' => ''], - [ 'name' => 'telephone', 'description' => ''] + ['name' => 'mobile', 'description' => ''], + ['name' => 'telephone', 'description' => ''], ]; $addressTypes = [ - [ 'name' => 'permanent', 'description' => ''], - [ 'name' => 'temporary', 'description' => ''] + ['name' => 'permanent', 'description' => ''], + ['name' => 'temporary', 'description' => ''], ]; $contractTypes = [ ['name' => 'early', 'description' => ''], - ['name' => 'hard', 'description' => ''] + ['name' => 'hard', 'description' => ''], ]; - - foreach($personTypes as $pt){ + foreach ($personTypes as $pt) { PersonType::create($pt); } - foreach($personGroups as $pg){ + foreach ($personGroups as $pg) { PersonGroup::create($pg); } - foreach($phoneTypes as $pt){ + foreach ($phoneTypes as $pt) { PhoneType::create($pt); } - foreach($addressTypes as $at){ + foreach ($addressTypes as $at) { AddressType::create($at); } - foreach($contractTypes as $ct){ + foreach ($contractTypes as $ct) { ContractType::create($ct); } - //client + // client Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => '', 'last_name' => '', 'full_name' => 'Naročnik d.o.o.', @@ -77,12 +76,12 @@ public function run(): void 'description' => 'sdwwf', 'group_id' => 1, 'type_id' => 1, - 'user_id' => 1 + 'user_id' => 1, ])->client()->create(); - - //debtors + + // debtors Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => 'test', 'last_name' => 'test', 'full_name' => 'test test', @@ -93,13 +92,13 @@ public function run(): void 'description' => 'sdwwf', 'group_id' => 2, 'type_id' => 2, - 'user_id' => 1 + 'user_id' => 1, ])->clientCase()->create([ - 'client_id' => 1 + 'client_id' => 1, ]); Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => 'test2', 'last_name' => 'test2', 'full_name' => 'test2 test2', @@ -110,14 +109,14 @@ public function run(): void 'description' => 'dw323', 'group_id' => 2, 'type_id' => 2, - 'user_id' => 1 + 'user_id' => 1, ])->clientCase()->create([ - 'client_id' => 1 + 'client_id' => 1, ]); - //client + // client Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => '', 'last_name' => '', 'full_name' => 'test d.o.o.', @@ -128,12 +127,12 @@ public function run(): void 'description' => 'sdwwf', 'group_id' => 1, 'type_id' => 1, - 'user_id' => 1 + 'user_id' => 1, ])->client()->create(); - - //debtors + + // debtors Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => 'test3', 'last_name' => 'test3', 'full_name' => 'test3 test3', @@ -144,15 +143,15 @@ public function run(): void 'description' => 'sdwwf', 'group_id' => 2, 'type_id' => 2, - 'user_id' => 1 + 'user_id' => 1, ])->clientCase()->create( [ - 'client_id' => 2 + 'client_id' => 2, ] ); Person::create([ - 'nu' => rand(100000,200000), + 'nu' => rand(100000, 200000), 'first_name' => '', 'last_name' => '', 'full_name' => 'test4 d.o.o.', @@ -163,12 +162,12 @@ public function run(): void 'description' => 'dw323', 'group_id' => 2, 'type_id' => 1, - 'user_id' => 1 + 'user_id' => 1, ])->clientCase()->create( [ - 'client_id' => 2 + 'client_id' => 2, ] ); - + } } diff --git a/database/seeders/PostSeeder.php b/database/seeders/PostSeeder.php index bcb47cf..3ac8164 100644 --- a/database/seeders/PostSeeder.php +++ b/database/seeders/PostSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class PostSeeder extends Seeder diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index be3cc1b..2fd418b 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -5,7 +5,6 @@ use App\Models\Permission; use App\Models\Role; use App\Models\User; -use Illuminate\Support\Facades\DB; use Illuminate\Database\Seeder; class RolePermissionSeeder extends Seeder diff --git a/database/seeders/SegmentSeeder.php b/database/seeders/SegmentSeeder.php index 8070166..1855147 100644 --- a/database/seeders/SegmentSeeder.php +++ b/database/seeders/SegmentSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use App\Models\Segment; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; class SegmentSeeder extends Seeder @@ -14,11 +13,11 @@ class SegmentSeeder extends Seeder public function run(): void { $sements = [ - [ 'name' => 'global', 'description' => ''], - [ 'name' => 'terrain', 'description' => ''] + ['name' => 'global', 'description' => ''], + ['name' => 'terrain', 'description' => ''], ]; - foreach($sements as $st){ + foreach ($sements as $st) { Segment::create($st); } } diff --git a/database/seeders/UpdateTestUserPasswordSeeder.php b/database/seeders/UpdateTestUserPasswordSeeder.php index 919bb43..9810a81 100644 --- a/database/seeders/UpdateTestUserPasswordSeeder.php +++ b/database/seeders/UpdateTestUserPasswordSeeder.php @@ -21,6 +21,7 @@ public function run(): void if (! $user) { $this->command?->warn("User {$email} not found – nothing updated."); + return; } diff --git a/resources/js/Pages/Admin/Packages/Index.vue b/resources/js/Pages/Admin/Packages/Index.vue index b87700f..f3ca78b 100644 --- a/resources/js/Pages/Admin/Packages/Index.vue +++ b/resources/js/Pages/Admin/Packages/Index.vue @@ -35,6 +35,15 @@ const filteredSenders = computed(() => { return props.senders.filter(s => s.profile_id === form.profile_id) }) +function onTemplateChange() { + const template = props.templates.find(t => t.id === form.template_id) + if (template?.content) { + form.body = template.content + } else { + form.body = '' + } +} + function submitCreate() { const lines = (form.numbers || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean) if (!lines.length) return @@ -78,6 +87,8 @@ const contracts = ref({ data: [], meta: { current_page: 1, last_page: 1, per_pag const segmentId = ref(null) const search = ref('') const clientId = ref(null) +const startDateFrom = ref('') +const startDateTo = ref('') const onlyMobile = ref(false) const onlyValidated = ref(false) const loadingContracts = ref(false) @@ -90,7 +101,7 @@ async function loadContracts(url = null) { } loadingContracts.value = true try { - const target = url || `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}` + const target = url || `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${startDateFrom.value ? `&start_date_from=${encodeURIComponent(startDateFrom.value)}` : ''}${startDateTo.value ? `&start_date_to=${encodeURIComponent(startDateTo.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}` const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }) const json = await res.json() contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } } @@ -110,11 +121,37 @@ function clearSelection() { selectedContractIds.value = new Set() } +function toggleSelectAll() { + const currentPageIds = contracts.value.data.map(c => c.id) + const allSelected = currentPageIds.every(id => selectedContractIds.value.has(id)) + + if (allSelected) { + // Deselect all on current page + currentPageIds.forEach(id => selectedContractIds.value.delete(id)) + } else { + // Select all on current page + currentPageIds.forEach(id => selectedContractIds.value.add(id)) + } + + // Force reactivity + selectedContractIds.value = new Set(Array.from(selectedContractIds.value)) +} + +const allCurrentPageSelected = computed(() => { + if (!contracts.value.data.length) return false + return contracts.value.data.every(c => selectedContractIds.value.has(c.id)) +}) + +const someCurrentPageSelected = computed(() => { + if (!contracts.value.data.length) return false + return contracts.value.data.some(c => selectedContractIds.value.has(c.id)) && !allCurrentPageSelected.value +}) + function goContractsPage(delta) { const { current_page } = contracts.value.meta const nextPage = current_page + delta if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return - const base = `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}&page=${nextPage}` + const base = `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${startDateFrom.value ? `&start_date_from=${encodeURIComponent(startDateFrom.value)}` : ''}${startDateTo.value ? `&start_date_to=${encodeURIComponent(startDateTo.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}&page=${nextPage}` loadContracts(base) } @@ -179,7 +216,7 @@ function submitCreateFromContracts() {
- @@ -220,12 +257,20 @@ function submitCreateFromContracts() {
-
+
-
- - -
+ +
+
+ + +
+
+ + +
+
+
diff --git a/resources/js/Pages/Admin/Users/Index.vue b/resources/js/Pages/Admin/Users/Index.vue index 8155d54..ef4adad 100644 --- a/resources/js/Pages/Admin/Users/Index.vue +++ b/resources/js/Pages/Admin/Users/Index.vue @@ -3,7 +3,8 @@ import AdminLayout from "@/Layouts/AdminLayout.vue"; import { useForm, Link } from "@inertiajs/vue3"; import { ref, computed } from "vue"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; -import { faMagnifyingGlass, faFloppyDisk } from "@fortawesome/free-solid-svg-icons"; +import { faMagnifyingGlass, faFloppyDisk, faPlus } from "@fortawesome/free-solid-svg-icons"; +import DialogModal from "@/Components/DialogModal.vue"; const props = defineProps({ users: Array, @@ -65,6 +66,43 @@ const filteredUsers = computed(() => { }); const anyDirty = computed(() => Object.values(forms).some((f) => f.dirty)); + +// Create user modal +const showCreateModal = ref(false); +const createForm = useForm({ + name: "", + email: "", + password: "", + password_confirmation: "", + roles: [], +}); + +function openCreateModal() { + createForm.reset(); + createForm.clearErrors(); + showCreateModal.value = true; +} + +function closeCreateModal() { + showCreateModal.value = false; + createForm.reset(); +} + +function submitCreateUser() { + createForm.post(route("admin.users.store"), { + preserveScroll: true, + onSuccess: () => { + closeCreateModal(); + }, + }); +} + +function toggleCreateRole(roleId) { + const exists = createForm.roles.includes(roleId); + createForm.roles = exists + ? createForm.roles.filter((id) => id !== roleId) + : [...createForm.roles, roleId]; +} diff --git a/resources/js/Pages/Cases/Partials/ContractTable.vue b/resources/js/Pages/Cases/Partials/ContractTable.vue index fdaafa0..6145a63 100644 --- a/resources/js/Pages/Cases/Partials/ContractTable.vue +++ b/resources/js/Pages/Cases/Partials/ContractTable.vue @@ -632,17 +632,15 @@ const closePaymentsDialog = () => {