From b0d2aa93ab7235092b87fb843637e9e0a869066a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Sun, 8 Mar 2026 21:42:39 +0100 Subject: [PATCH] Added call later, option to limit auto mail so for a client person email you can limit which decision activity will be send to that specific email and moved SMS packages from admin panel to default app view --- .../Controllers/Admin/PackageController.php | 7 +- app/Http/Controllers/CallLaterController.php | 53 ++++ app/Http/Controllers/ClientCaseContoller.php | 66 ++-- app/Http/Controllers/ClientController.php | 8 +- app/Http/Controllers/PersonController.php | 23 +- app/Models/Activity.php | 13 + app/Models/CallLater.php | 46 +++ app/Services/AutoMailDispatcher.php | 15 +- .../Handlers/CallLaterHandler.php | 27 ++ app/Services/DecisionEvents/Registry.php | 1 + ...6_add_call_back_at_to_activities_table.php | 28 ++ ..._03_08_163057_create_call_laters_table.php | 33 ++ database/seeders/EventSeeder.php | 5 + package-lock.json | 18 -- phpunit.xml | 3 + .../DocumentsTable/DocumentViewerDialog.vue | 210 ++++++++++++- .../Components/PersonInfo/EmailCreateForm.vue | 87 ++++- resources/js/Layouts/AdminLayout.vue | 8 - resources/js/Layouts/AppLayout.vue | 16 + resources/js/Pages/Admin/Index.vue | 6 - resources/js/Pages/CallLaters/Index.vue | 297 ++++++++++++++++++ .../Pages/Cases/Partials/ActivityDrawer.vue | 59 +++- .../js/Pages/{Admin => }/Packages/Create.vue | 24 +- .../js/Pages/{Admin => }/Packages/Index.vue | 14 +- .../js/Pages/{Admin => }/Packages/Show.vue | 14 +- .../Workflow/Partials/ActionTable.vue | 24 +- .../Workflow/Partials/DecisionTable.vue | 43 +-- routes/web.php | 27 +- .../Admin/PackageContractsDateFilterTest.php | 12 +- tests/Feature/AutoMailDecisionFilterTest.php | 46 +++ tests/Pest.php | 1 + tests/Pure/AutoMailDecisionFilterTest.php | 43 +++ 32 files changed, 1103 insertions(+), 174 deletions(-) create mode 100644 app/Http/Controllers/CallLaterController.php create mode 100644 app/Models/CallLater.php create mode 100644 app/Services/DecisionEvents/Handlers/CallLaterHandler.php create mode 100644 database/migrations/2026_03_08_163056_add_call_back_at_to_activities_table.php create mode 100644 database/migrations/2026_03_08_163057_create_call_laters_table.php create mode 100644 resources/js/Pages/CallLaters/Index.vue rename resources/js/Pages/{Admin => }/Packages/Create.vue (97%) rename resources/js/Pages/{Admin => }/Packages/Index.vue (94%) rename resources/js/Pages/{Admin => }/Packages/Show.vue (97%) create mode 100644 tests/Feature/AutoMailDecisionFilterTest.php create mode 100644 tests/Pure/AutoMailDecisionFilterTest.php diff --git a/app/Http/Controllers/Admin/PackageController.php b/app/Http/Controllers/Admin/PackageController.php index a3493fd..4db3f98 100644 --- a/app/Http/Controllers/Admin/PackageController.php +++ b/app/Http/Controllers/Admin/PackageController.php @@ -12,7 +12,6 @@ use App\Models\SmsTemplate; use App\Services\Contact\PhoneSelector; use App\Services\Sms\SmsService; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Bus; @@ -30,7 +29,7 @@ public function index(Request $request): Response ->latest('id') ->paginate($perPage); - return Inertia::render('Admin/Packages/Index', [ + return Inertia::render('Packages/Index', [ 'packages' => $packages, ]); } @@ -70,7 +69,7 @@ public function create(Request $request): Response }) ->values(); - return Inertia::render('Admin/Packages/Create', [ + return Inertia::render('Packages/Create', [ 'profiles' => $profiles, 'senders' => $senders, 'templates' => $templates, @@ -213,7 +212,7 @@ public function show(Package $package, SmsService $sms): Response } } - return Inertia::render('Admin/Packages/Show', [ + return Inertia::render('Packages/Show', [ 'package' => $package, 'items' => $items, 'preview' => $preview, diff --git a/app/Http/Controllers/CallLaterController.php b/app/Http/Controllers/CallLaterController.php new file mode 100644 index 0000000..fa67a1c --- /dev/null +++ b/app/Http/Controllers/CallLaterController.php @@ -0,0 +1,53 @@ +with([ + 'clientCase.person', + 'contract', + 'user', + 'activity', + ]) + ->whereNull('completed_at') + ->orderBy('call_back_at', 'asc'); + + if ($request->filled('date_from')) { + $query->whereDate('call_back_at', '>=', $request->date_from); + } + if ($request->filled('date_to')) { + $query->whereDate('call_back_at', '<=', $request->date_to); + } + if ($request->filled('search')) { + $term = '%'.$request->search.'%'; + $query->whereHas('clientCase.person', function ($q) use ($term) { + $q->where('first_name', 'ilike', $term) + ->orWhere('last_name', 'ilike', $term) + ->orWhere('full_name', 'ilike', $term) + ->orWhereRaw("CONCAT(first_name, ' ', last_name) ILIKE ?", [$term]); + }); + } + + $callLaters = $query->paginate(50)->withQueryString(); + + return Inertia::render('CallLaters/Index', [ + 'callLaters' => $callLaters, + 'filters' => $request->only(['date_from', 'date_to', 'search']), + ]); + } + + public function complete(CallLater $callLater): \Illuminate\Http\RedirectResponse + { + $callLater->update(['completed_at' => now()]); + + return back()->with('success', 'Klic označen kot opravljen.'); + } +} diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index 2de0160..9847f23 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -306,6 +306,7 @@ public function storeActivity(ClientCase $clientCase, Request $request) try { $attributes = $request->validate([ 'due_date' => 'nullable|date', + 'call_back_at' => 'nullable|date_format:Y-m-d H:i:s|after_or_equal:now', 'amount' => 'nullable|decimal:0,4', 'note' => 'nullable|string', 'action_id' => 'exists:\App\Models\Action,id', @@ -326,14 +327,14 @@ public function storeActivity(ClientCase $clientCase, Request $request) // Determine which contracts to process $contractIds = []; - if ($createForAll && !empty($contractUuids)) { + if ($createForAll && ! empty($contractUuids)) { // Get all contract IDs from the provided UUIDs $contracts = Contract::withTrashed() ->whereIn('uuid', $contractUuids) ->where('client_case_id', $clientCase->id) ->get(); $contractIds = $contracts->pluck('id')->toArray(); - } elseif (!empty($contractUuids) && isset($contractUuids[0])) { + } elseif (! empty($contractUuids) && isset($contractUuids[0])) { // Single contract mode $contract = Contract::withTrashed() ->where('uuid', $contractUuids[0]) @@ -342,7 +343,7 @@ public function storeActivity(ClientCase $clientCase, Request $request) if ($contract) { $contractIds = [$contract->id]; } - } elseif (!empty($attributes['contract_uuid'])) { + } elseif (! empty($attributes['contract_uuid'])) { // Legacy single contract_uuid support $contract = Contract::withTrashed() ->where('uuid', $attributes['contract_uuid']) @@ -360,7 +361,7 @@ public function storeActivity(ClientCase $clientCase, Request $request) $createdActivities = []; $sendFlag = (bool) ($attributes['send_auto_mail'] ?? true); - + // Disable auto mail if creating activities for multiple contracts if ($sendFlag && count($contractIds) > 1) { $sendFlag = false; @@ -371,6 +372,7 @@ public function storeActivity(ClientCase $clientCase, Request $request) // Create activity $row = $clientCase->activities()->create([ 'due_date' => $attributes['due_date'] ?? null, + 'call_back_at' => $attributes['call_back_at'] ?? null, 'amount' => $attributes['amount'] ?? null, 'note' => $attributes['note'] ?? null, 'action_id' => $attributes['action_id'], @@ -417,29 +419,29 @@ public function storeActivity(ClientCase $clientCase, Request $request) ->whereIn('id', $attachmentIds) ->pluck('id'); $validAttachmentIds = Document::query() - ->where('documentable_type', Contract::class) - ->where('documentable_id', $contractId) - ->whereIn('id', $attachmentIds) - ->pluck('id'); + ->where('documentable_type', Contract::class) + ->where('documentable_id', $contractId) + ->whereIn('id', $attachmentIds) + ->pluck('id'); + } + $result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [ + 'attachment_ids' => $validAttachmentIds->all(), + ]); + if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) { + // If template requires contract and user attempted to send, surface a validation message + logger()->warning('Email not queued: required contract is missing for the selected template.'); + } + if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) { + logger()->warning('Email not queued: no eligible client emails to receive auto mails.'); + } + } catch (\Throwable $e) { + // Do not fail activity creation due to mailing issues + logger()->warning('Auto mail dispatch failed: '.$e->getMessage()); } - $result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [ - 'attachment_ids' => $validAttachmentIds->all(), - ]); - if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) { - // If template requires contract and user attempted to send, surface a validation message - logger()->warning('Email not queued: required contract is missing for the selected template.'); - } - if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) { - logger()->warning('Email not queued: no eligible client emails to receive auto mails.'); - } - } catch (\Throwable $e) { - // Do not fail activity creation due to mailing issues - logger()->warning('Auto mail dispatch failed: '.$e->getMessage()); - } } $activityCount = count($createdActivities); - $successMessage = $activityCount > 1 + $successMessage = $activityCount > 1 ? "Successfully created {$activityCount} activities!" : 'Successfully created activity!'; @@ -867,6 +869,9 @@ public function show(ClientCase $clientCase) 'decisions.emailTemplate' => function ($q) { $q->select('id', 'name', 'entity_types', 'allow_attachments'); }, + 'decisions.events' => function ($q) { + $q->select('events.id', 'events.key', 'events.name'); + }, ]) ->get(['id', 'name', 'color_tag', 'segment_id']), 'types' => $types, @@ -888,6 +893,7 @@ public function show(ClientCase $clientCase) ->select(['id', 'name', 'content', 'allow_custom_body']) ->orderBy('name') ->get(), + 'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']), ]); } @@ -1101,6 +1107,7 @@ public function archiveBatch(Request $request) if (! $setting) { \Log::warning('No archive settings found for batch archive'); + return back()->with('flash', [ 'error' => 'No archive settings found', ]); @@ -1114,13 +1121,14 @@ public function archiveBatch(Request $request) foreach ($validated['contracts'] as $contractUuid) { try { $contract = Contract::where('uuid', $contractUuid)->firstOrFail(); - + // Skip if contract is already archived (active = 0) - if (!$contract->active) { + if (! $contract->active) { $skippedCount++; + continue; } - + $clientCase = $contract->clientCase; $context = [ @@ -1207,8 +1215,8 @@ public function archiveBatch(Request $request) if ($skippedCount > 0) { $message .= ", skipped $skippedCount already archived"; } - $message .= ", " . count($errors) . " failed"; - + $message .= ', '.count($errors).' failed'; + return back()->with('flash', [ 'error' => $message, 'details' => $errors, @@ -1218,7 +1226,7 @@ public function archiveBatch(Request $request) $message = $reactivate ? "Successfully reactivated $successCount contracts" : "Successfully archived $successCount contracts"; - + if ($skippedCount > 0) { $message .= " ($skippedCount already archived)"; } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 69a5e98..0723513 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -27,7 +27,7 @@ public function index(Client $client, Request $request) ->where('person.full_name', 'ilike', '%'.$search.'%') ->groupBy('clients.id'); }) - //->where('clients.active', 1) + // ->where('clients.active', 1) // Use LEFT JOINs for aggregated data to avoid subqueries ->leftJoin('client_cases', 'client_cases.client_id', '=', 'clients.id') ->leftJoin('contracts', function ($join) { @@ -71,6 +71,7 @@ public function show(Client $client, Request $request) return Inertia::render('Client/Show', [ 'client' => $data, + 'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']), 'client_cases' => $data->clientCases() ->select('client_cases.*') ->when($request->input('search'), function ($que, $search) { @@ -162,6 +163,7 @@ public function contracts(Client $client, Request $request) return Inertia::render('Client/Contracts', [ 'client' => $data, + 'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']), 'contracts' => $contractsQuery ->paginate($perPage, ['*'], 'contracts_page', $pageNumber) ->withQueryString(), @@ -175,7 +177,7 @@ public function exportContracts(ExportClientContractsRequest $request, Client $c { $data = $request->validated(); $columns = array_values(array_unique($data['columns'])); - + $from = $data['from'] ?? null; $to = $data['to'] ?? null; $search = $data['search'] ?? null; @@ -236,7 +238,7 @@ private function buildExportFilename(Client $client): string { $datePrefix = now()->format('dmy'); $clientName = $this->slugify($client->person?->full_name ?? 'stranka'); - + return sprintf('%s_%s-Pogodbe.xlsx', $datePrefix, $clientName); } diff --git a/app/Http/Controllers/PersonController.php b/app/Http/Controllers/PersonController.php index c2682e3..38b28a8 100644 --- a/app/Http/Controllers/PersonController.php +++ b/app/Http/Controllers/PersonController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers; -use App\Models\BankAccount; use App\Models\Person\Person; use Illuminate\Http\Request; @@ -27,9 +26,7 @@ public function update(Person $person, Request $request) $person->update($attributes); return back()->with('success', 'Person updated')->with('flash_method', 'PUT'); - - } public function createAddress(Person $person, Request $request) @@ -72,7 +69,7 @@ public function updateAddress(Person $person, int $address_id, Request $request) $address->update($attributes); return back()->with('success', 'Address updated')->with('flash_method', 'PUT'); - + } public function deleteAddress(Person $person, int $address_id, Request $request) @@ -80,7 +77,6 @@ public function deleteAddress(Person $person, int $address_id, Request $request) $address = $person->addresses()->findOrFail($address_id); $address->delete(); // soft delete - return back()->with('success', 'Address deleted')->with('flash_method', 'DELETE'); } @@ -142,8 +138,14 @@ public function createEmail(Person $person, Request $request) 'verified_at' => 'nullable|date', 'preferences' => 'nullable|array', 'meta' => 'nullable|array', + 'decision_ids' => 'nullable|array', + 'decision_ids.*' => 'integer|exists:decisions,id', ]); + $decisionIds = array_map('intval', $attributes['decision_ids'] ?? []); + unset($attributes['decision_ids']); + $attributes['preferences'] = array_merge($attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]); + // Dedup: avoid duplicate email per person by value $email = $person->emails()->firstOrCreate([ 'value' => $attributes['value'], @@ -164,10 +166,16 @@ public function updateEmail(Person $person, int $email_id, Request $request) 'verified_at' => 'nullable|date', 'preferences' => 'nullable|array', 'meta' => 'nullable|array', + 'decision_ids' => 'nullable|array', + 'decision_ids.*' => 'integer|exists:decisions,id', ]); $email = $person->emails()->findOrFail($email_id); + $decisionIds = array_map('intval', $attributes['decision_ids'] ?? []); + unset($attributes['decision_ids']); + $attributes['preferences'] = array_merge($email->preferences ?? [], $attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]); + $email->update($attributes); return back()->with('success', 'Email updated successfully')->with('flash_method', 'PUT'); @@ -204,10 +212,8 @@ public function createTrr(Person $person, Request $request) // Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided $trr = $person->bankAccounts()->create($attributes); - return back()->with('success', 'TRR added successfully')->with('flash_method', 'POST'); - } public function updateTrr(Person $person, int $trr_id, Request $request) @@ -238,8 +244,7 @@ public function deleteTrr(Person $person, int $trr_id, Request $request) $trr = $person->bankAccounts()->findOrFail($trr_id); $trr->delete(); - return back()->with('success', 'TRR deleted')->with('flash_method', 'DELETE'); - + } } diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 3a6afa1..d187be1 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -18,6 +18,7 @@ class Activity extends Model protected $fillable = [ 'due_date', + 'call_back_at', 'amount', 'note', 'action_id', @@ -27,6 +28,13 @@ class Activity extends Model 'client_case_id', ]; + protected function casts(): array + { + return [ + 'call_back_at' => 'datetime', + ]; + } + protected $hidden = [ 'action_id', 'decision_id', @@ -146,4 +154,9 @@ public function user(): BelongsTo { return $this->belongsTo(\App\Models\User::class); } + + public function callLaters(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(\App\Models\CallLater::class); + } } diff --git a/app/Models/CallLater.php b/app/Models/CallLater.php new file mode 100644 index 0000000..2640e83 --- /dev/null +++ b/app/Models/CallLater.php @@ -0,0 +1,46 @@ + 'datetime', + 'completed_at' => 'datetime', + ]; + } + + public function activity(): BelongsTo + { + return $this->belongsTo(Activity::class); + } + + public function clientCase(): BelongsTo + { + return $this->belongsTo(ClientCase::class); + } + + public function contract(): BelongsTo + { + return $this->belongsTo(Contract::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Services/AutoMailDispatcher.php b/app/Services/AutoMailDispatcher.php index c5efcde..5630cb0 100644 --- a/app/Services/AutoMailDispatcher.php +++ b/app/Services/AutoMailDispatcher.php @@ -59,10 +59,23 @@ public function maybeQueue(Activity $activity, bool $sendFlag = true, array $opt // Resolve eligible recipients: client's person emails with receive_auto_mails = true $recipients = []; if ($client && $client->person) { - $recipients = Email::query() + $emails = Email::query() ->where('person_id', $client->person->id) ->where('is_active', true) ->where('receive_auto_mails', true) + ->get(['value', 'preferences']); + + $recipients = $emails + ->filter(function (Email $email) use ($decision): bool { + $decisionIds = $email->preferences['decision_ids'] ?? []; + + // Empty list means "all decisions" — always receive + if (empty($decisionIds)) { + return true; + } + + return in_array((int) $decision->id, array_map('intval', $decisionIds), true); + }) ->pluck('value') ->map(fn ($v) => strtolower(trim((string) $v))) ->filter(fn ($v) => filter_var($v, FILTER_VALIDATE_EMAIL)) diff --git a/app/Services/DecisionEvents/Handlers/CallLaterHandler.php b/app/Services/DecisionEvents/Handlers/CallLaterHandler.php new file mode 100644 index 0000000..631e14e --- /dev/null +++ b/app/Services/DecisionEvents/Handlers/CallLaterHandler.php @@ -0,0 +1,27 @@ +activity; + + if (empty($activity->call_back_at)) { + return; + } + + CallLater::create([ + 'activity_id' => $activity->id, + 'client_case_id' => $activity->client_case_id, + 'contract_id' => $activity->contract_id, + 'user_id' => $activity->user_id, + 'call_back_at' => $activity->call_back_at, + ]); + } +} diff --git a/app/Services/DecisionEvents/Registry.php b/app/Services/DecisionEvents/Registry.php index a2cabff..cbd0d94 100644 --- a/app/Services/DecisionEvents/Registry.php +++ b/app/Services/DecisionEvents/Registry.php @@ -17,6 +17,7 @@ class Registry 'add_segment' => AddSegmentHandler::class, 'archive_contract' => \App\Services\DecisionEvents\Handlers\ArchiveContractHandler::class, 'end_field_job' => \App\Services\DecisionEvents\Handlers\EndFieldJobHandler::class, + 'add_call_later' => \App\Services\DecisionEvents\Handlers\CallLaterHandler::class, ]; public static function resolve(string $key): DecisionEventHandler diff --git a/database/migrations/2026_03_08_163056_add_call_back_at_to_activities_table.php b/database/migrations/2026_03_08_163056_add_call_back_at_to_activities_table.php new file mode 100644 index 0000000..9be016f --- /dev/null +++ b/database/migrations/2026_03_08_163056_add_call_back_at_to_activities_table.php @@ -0,0 +1,28 @@ +dateTime('call_back_at')->nullable()->after('due_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn('call_back_at'); + }); + } +}; diff --git a/database/migrations/2026_03_08_163057_create_call_laters_table.php b/database/migrations/2026_03_08_163057_create_call_laters_table.php new file mode 100644 index 0000000..f54e5b1 --- /dev/null +++ b/database/migrations/2026_03_08_163057_create_call_laters_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('activity_id')->constrained('activities')->cascadeOnDelete(); + $table->foreignId('client_case_id')->constrained('client_cases')->cascadeOnDelete(); + $table->foreignId('contract_id')->nullable()->constrained('contracts')->nullOnDelete(); + $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->dateTime('call_back_at'); + $table->dateTime('completed_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('call_laters'); + } +}; diff --git a/database/seeders/EventSeeder.php b/database/seeders/EventSeeder.php index ca508c4..54f6321 100644 --- a/database/seeders/EventSeeder.php +++ b/database/seeders/EventSeeder.php @@ -31,6 +31,11 @@ public function run(): void 'name' => 'End field job', 'description' => 'Dispatches a queued job to finalize field-related processing (implementation-specific).', ], + [ + 'key' => 'add_call_later', + 'name' => 'Klic kasneje', + 'description' => 'Ustvari zapis za povratni klic ob določenem datumu in uri.', + ], ]; foreach ($rows as $row) { diff --git a/package-lock.json b/package-lock.json index 67ca1eb..545a221 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6029,24 +6029,6 @@ "which": "bin/which" } }, - "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/phpunit.xml b/phpunit.xml index 17ab44b..e4f7055 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,6 +11,9 @@ tests/Feature + + tests/Pure + diff --git a/resources/js/Components/DocumentsTable/DocumentViewerDialog.vue b/resources/js/Components/DocumentsTable/DocumentViewerDialog.vue index 423a4df..4cacd6f 100644 --- a/resources/js/Components/DocumentsTable/DocumentViewerDialog.vue +++ b/resources/js/Components/DocumentsTable/DocumentViewerDialog.vue @@ -1,5 +1,5 @@ + + diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue index b826ace..9308423 100644 --- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue @@ -58,6 +58,8 @@ const form = useInertiaForm({ send_auto_mail: true, attach_documents: false, attachment_document_ids: [], + call_back_at_date: null, + call_back_at_time: null, }); watch( @@ -127,6 +129,20 @@ const store = async () => { const isMultipleContracts = contractUuids && contractUuids.length > 1; + const buildCallBackAt = (date, time) => { + if (!date) return null; + const t = time || '00:00'; + const [h, m] = t.split(':'); + const d = date instanceof Date ? date : new Date(date); + if (isNaN(d.getTime())) return null; + const y = d.getFullYear(); + const mo = String(d.getMonth() + 1).padStart(2, '0'); + const dy = String(d.getDate()).padStart(2, '0'); + const hh = String(Number(h || 0)).padStart(2, '0'); + const mm = String(Number(m || 0)).padStart(2, '0'); + return `${y}-${mo}-${dy} ${hh}:${mm}:00`; + }; + form .transform((data) => ({ ...data, @@ -138,11 +154,16 @@ const store = async () => { templateAllowsAttachments.value && data.attach_documents && !isMultipleContracts ? data.attachment_document_ids : [], + call_back_at: hasCallLaterEvent.value + ? buildCallBackAt(data.call_back_at_date, data.call_back_at_time) + : null, + call_back_at_date: undefined, + call_back_at_time: undefined, })) .post(route("clientCase.activity.store", props.client_case), { onSuccess: () => { close(); - form.reset("due_date", "amount", "note", "contract_uuids"); + form.reset("due_date", "amount", "note", "contract_uuids", "call_back_at_date", "call_back_at_time"); emit("saved"); }, }); @@ -156,6 +177,22 @@ const currentDecision = () => { decisions.value.find((d) => d.id === form.decision_id) || decisions.value[0] || null ); }; + +const hasCallLaterEvent = computed(() => { + const d = currentDecision(); + if (!d) return false; + return Array.isArray(d.events) && d.events.some((e) => e.key === 'add_call_later'); +}); + +watch( + () => hasCallLaterEvent.value, + (has) => { + if (!has) { + form.call_back_at_date = null; + form.call_back_at_time = null; + } + } +); const showSendAutoMail = () => { const d = currentDecision(); return !!(d && d.auto_mail && d.email_template_id); @@ -409,6 +446,26 @@ watch( /> +
+ +
+ + +
+

+ {{ form.errors.call_back_at }} +

+
+
-import AdminLayout from "@/Layouts/AdminLayout.vue"; +import AppLayout from "@/Layouts/AppLayout.vue"; import { Link, router, useForm } from "@inertiajs/vue3"; import { ref, computed, nextTick } from "vue"; import axios from "axios"; @@ -112,9 +112,9 @@ function submitCreate() { })), }; - router.post(route("admin.packages.store"), payload, { + router.post(route("packages.store"), payload, { onSuccess: () => { - router.visit(route("admin.packages.index")); + router.visit(route("packages.index")); }, }); } @@ -202,7 +202,7 @@ async function loadContracts(url = null) { if (onlyValidated.value) params.append("only_validated", "1"); params.append("per_page", perPage.value); - const target = url || `${route("admin.packages.contracts")}?${params.toString()}`; + const target = url || `${route("packages.contracts")}?${params.toString()}`; const { data: json } = await axios.get(target, { headers: { "X-Requested-With": "XMLHttpRequest" }, }); @@ -268,7 +268,7 @@ function goToPage(page) { params.append("per_page", perPage.value); params.append("page", page); - const url = `${route("admin.packages.contracts")}?${params.toString()}`; + const url = `${route("packages.contracts")}?${params.toString()}`; loadContracts(url); } @@ -312,9 +312,9 @@ function submitCreateFromContracts() { }; creatingFromContracts.value = true; - router.post(route("admin.packages.store-from-contracts"), payload, { + router.post(route("packages.store-from-contracts"), payload, { onSuccess: () => { - router.visit(route("admin.packages.index")); + router.visit(route("packages.index")); }, onError: (errors) => { const first = errors && Object.values(errors)[0]; @@ -337,11 +337,11 @@ const numbersCount = computed(() => { diff --git a/resources/js/Pages/Admin/Packages/Index.vue b/resources/js/Pages/Packages/Index.vue similarity index 94% rename from resources/js/Pages/Admin/Packages/Index.vue rename to resources/js/Pages/Packages/Index.vue index 1b086eb..d9357f4 100644 --- a/resources/js/Pages/Admin/Packages/Index.vue +++ b/resources/js/Pages/Packages/Index.vue @@ -1,5 +1,5 @@