diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index c1f73af..fe6ec73 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -252,11 +252,14 @@ public function storeActivity(ClientCase $clientCase, Request $request) 'action_id' => 'exists:\App\Models\Action,id', 'decision_id' => 'exists:\App\Models\Decision,id', 'contract_uuid' => 'nullable|uuid', + 'phone_view' => 'nullable|boolean', 'send_auto_mail' => 'sometimes|boolean', 'attachment_document_ids' => 'sometimes|array', 'attachment_document_ids.*' => 'integer', ]); + $isPhoneView = $attributes['phone_view'] ?? false; + // Map contract_uuid to contract_id within the same client case, if provided $contractId = null; if (! empty($attributes['contract_uuid'])) { @@ -279,10 +282,23 @@ public function storeActivity(ClientCase $clientCase, Request $request) 'decision_id' => $attributes['decision_id'], 'contract_id' => $contractId, ]); - /*foreach ($activity->decision->events as $e) { - $class = '\\App\\Events\\' . $e->name; - event(new $class($clientCase)); - }*/ + + if ($isPhoneView && $contractId) { + $fieldJob = $contract->fieldJobs() + ->whereNull('completed_at') + ->whereNull('cancelled_at') + ->where('assigned_user_id', \Auth::id()) + ->orderByDesc('id') + ->first(); + + if ($fieldJob) { + $fieldJob->update([ + 'added_activity' => true, + 'last_activity' => $row->created_at, + ]); + + } + } logger()->info('Activity successfully inserted', $attributes); @@ -297,8 +313,8 @@ public function storeActivity(ClientCase $clientCase, Request $request) ->values(); $validAttachmentIds = collect(); if ($attachmentIds->isNotEmpty() && $contractId) { - $validAttachmentIds = \App\Models\Document::query() - ->where('documentable_type', \App\Models\Contract::class) + $validAttachmentIds = Document::query() + ->where('documentable_type', Contract::class) ->where('documentable_id', $contractId) ->whereIn('id', $attachmentIds) ->pluck('id'); @@ -1458,178 +1474,115 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r { $contract = Contract::query()->where('uuid', $uuid)->firstOrFail(); if ($contract->client_case_id !== $clientCase->id) { + \Log::warning('Contract not found uuid: {uuid}', ['uuid' => $uuid]); abort(404); } - $reactivateRequested = (bool) $request->boolean('reactivate'); - // Determine applicable settings based on intent (archive vs reactivate) - if ($reactivateRequested) { - $latestReactivate = \App\Models\ArchiveSetting::query() - ->where('enabled', true) - ->where('reactivate', true) - ->whereIn('strategy', ['immediate', 'manual']) - ->orderByDesc('id') - ->first(); - if (! $latestReactivate) { - return back()->with('warning', __('contracts.reactivate_not_allowed')); - } - $settings = collect([$latestReactivate]); - $hasReactivateRule = true; - } else { - $settings = \App\Models\ArchiveSetting::query() - ->where('enabled', true) - ->whereIn('strategy', ['immediate', 'manual']) - ->where(function ($q) { // exclude reactivate-only rules from archive run - $q->whereNull('reactivate')->orWhere('reactivate', false); - }) - ->get(); - if ($settings->isEmpty()) { - return back()->with('warning', __('contracts.no_archive_settings')); - } - $hasReactivateRule = false; + + $attr = $request->validate([ + 'reactivate' => 'boolean', + ]); + + $reactivate = $attr['reactivate'] ?? false; + + $setting = \App\Models\ArchiveSetting::query() + ->where('enabled', true) + ->whereIn('strategy', ['immediate', 'manual']) + ->where('reactivate', $reactivate) + ->orderByDesc('id') + ->first(); + + if (! $setting->exists()) { + \Log::warning('No archive settings found!'); + + return back()->with('warning', 'No settings found'); } + // Service archive executor $executor = app(\App\Services\Archiving\ArchiveExecutor::class); + $result = null; + $context = [ 'contract_id' => $contract->id, 'client_case_id' => $clientCase->id, + 'account_id' => $contract->account->id ?? null, ]; - if ($contract->account) { - $context['account_id'] = $contract->account->id; + + try { + $result = $executor->executeSetting($setting, $context, \Auth::id()); + } catch (Exception $e) { + \Log::error('There was an error executing ArchiveExecutor::executeSetting {msg}', ['msg' => $e->getMessage()]); + + return back()->with('warning', 'Something went wrong!'); } - $overall = []; - $hadAnyEffect = false; - foreach ($settings as $setting) { - - $res = $executor->executeSetting($setting, $context, optional($request->user())->id); - foreach ($res as $table => $count) { - $overall[$table] = ($overall[$table] ?? 0) + $count; - if ($count > 0) { - $hadAnyEffect = true; - } - } - } - - if ($reactivateRequested && $hasReactivateRule) { - // Reactivation path: ensure contract becomes active and soft-delete cleared. - if ($contract->active == 0 || $contract->deleted_at) { - $contract->forceFill(['active' => 1, 'deleted_at' => null])->save(); - $overall['contracts_reactivated'] = ($overall['contracts_reactivated'] ?? 0) + 1; - $hadAnyEffect = true; - } - } else { - // Ensure the contract itself is archived even if rule conditions would have excluded it - if (! empty($contract->getAttributes()) && $contract->active) { - if (! array_key_exists('contracts', $overall)) { - $contract->update(['active' => 0]); - $overall['contracts'] = ($overall['contracts'] ?? 0) + 1; - } else { - $contract->refresh(); - } - $hadAnyEffect = true; - } - } - - // Create an Activity record logging this archive if an action or decision is tied to any setting - if ($hadAnyEffect) { - $activitySetting = $settings->first(fn ($s) => ! is_null($s->action_id) || ! is_null($s->decision_id)); - if ($activitySetting) { - try { - if ($reactivateRequested) { - $note = 'Ponovna aktivacija pogodba '.$contract->reference; - } else { - $noteKey = 'contracts.archived_activity_note'; - $note = __($noteKey, ['reference' => $contract->reference]); - if ($note === $noteKey) { - $note = \Illuminate\Support\Facades\Lang::get($noteKey, ['reference' => $contract->reference], 'sl'); - } - } + try { + \DB::transaction(function () use ($contract, $clientCase, $setting, $reactivate) { + // Create an Activity record logging this archive if an action or decision is tied to any setting + if ($setting->action_id && $setting->decision_id) { $activityData = [ 'client_case_id' => $clientCase->id, - 'action_id' => $activitySetting->action_id, - 'decision_id' => $activitySetting->decision_id, - 'note' => $note, - 'active' => 1, - 'user_id' => optional($request->user())->id, + 'action_id' => $setting->action_id, + 'decision_id' => $setting->decision_id, + 'note' => ($reactivate) + ? "Ponovno aktivirana pogodba $contract->reference" + : "Arhivirana pogodba $contract->reference", ]; - if ($reactivateRequested) { - // Attach the contract_id when reactivated as per requirement - $activityData['contract_id'] = $contract->id; + + try { + \App\Models\Activity::create($activityData); + } catch (Exception $e) { + \Log::warning('Activity could not be created!'); } - \App\Models\Activity::create($activityData); - } catch (\Throwable $e) { - logger()->warning('Failed to create archive/reactivate activity', [ - 'error' => $e->getMessage(), - 'contract_id' => $contract->id, - 'setting_id' => optional($activitySetting)->id, - 'reactivate' => $reactivateRequested, - ]); + } - } - } - // If any archive setting specifies a segment_id, move the contract to that segment (archive bucket) - $segmentSetting = $settings->first(fn ($s) => ! is_null($s->segment_id)); // for reactivation this is the single reactivation setting if segment specified - if ($segmentSetting && $segmentSetting->segment_id) { - try { - $segmentId = $segmentSetting->segment_id; - \DB::transaction(function () use ($contract, $segmentId, $clientCase) { - // Ensure the segment is attached to the client case (activate if previously inactive) - $casePivot = \DB::table('client_case_segment') - ->where('client_case_id', $clientCase->id) - ->where('segment_id', $segmentId) - ->first(); - if (! $casePivot) { - \DB::table('client_case_segment')->insert([ - 'client_case_id' => $clientCase->id, - 'segment_id' => $segmentId, + + // If any archive setting specifies a segment_id, move the contract to that segment (archive bucket) + if ($setting->segment_id) { + $segmentId = $setting->segment_id; + + $contract->segments() + ->allRelatedIds() + ->map(fn (int $val, int|string $key) => $contract->segments()->updateExistingPivot($val, [ + 'active' => false, + 'updated_at' => now(), + ]) + ); + + if ($contract->attachedSegments()->find($segmentId)->pluck('id')->isNotEmpty()) { + $contract->attachedSegments()->updateExistingPivot($segmentId, [ 'active' => true, - 'created_at' => now(), 'updated_at' => now(), ]); - } elseif (! $casePivot->active) { - \DB::table('client_case_segment') - ->where('id', $casePivot->id) - ->update(['active' => true, 'updated_at' => now()]); - } - - // Deactivate all current active contract segments - \DB::table('contract_segment') - ->where('contract_id', $contract->id) - ->where('active', true) - ->update(['active' => false, 'updated_at' => now()]); - - // Attach or activate the archive segment for this contract - $existing = \DB::table('contract_segment') - ->where('contract_id', $contract->id) - ->where('segment_id', $segmentId) - ->first(); - if ($existing) { - \DB::table('contract_segment') - ->where('id', $existing->id) - ->update(['active' => true, 'updated_at' => now()]); } else { - \DB::table('contract_segment')->insert([ - 'contract_id' => $contract->id, - 'segment_id' => $segmentId, - 'active' => true, - 'created_at' => now(), - 'updated_at' => now(), - ]); + $contract->segments()->attach( + $segmentId, + [ + 'active' => true, + 'created_at' => now(), + 'updated_at' => now(), + ] + ); } - }); - } catch (\Throwable $e) { - logger()->warning('Failed to move contract to archive segment', [ - 'error' => $e->getMessage(), - 'contract_id' => $contract->id, - 'segment_id' => $segmentSetting->segment_id, - 'setting_id' => $segmentSetting->id, - ]); - } + } + + $contract->fieldJobs() + ->whereNull('completed_at') + ->whereNull('cancelled_at') + ->update([ + 'cancelled_at' => date('Y-m-d'), + 'updated_at' => now(), + ]); + }); + } catch (Exception $e) { + \Log::warning('Something went wrong with inserting / updating archive setting partials!'); + + return back()->with('warning', 'Something went wrong!'); } - $message = $reactivateRequested ? __('contracts.reactivated') : __('contracts.archived'); - - return back()->with('success', $message); + return back()->with('success', $reactivate + ? __('contracts.reactivated') + : __('contracts.archived') + ); } /** diff --git a/app/Http/Controllers/PhoneViewController.php b/app/Http/Controllers/PhoneViewController.php index 062a095..14d3a13 100644 --- a/app/Http/Controllers/PhoneViewController.php +++ b/app/Http/Controllers/PhoneViewController.php @@ -76,169 +76,81 @@ public function completedToday(Request $request) public function showCase(\App\Models\ClientCase $clientCase, Request $request) { $userId = $request->user()->id; - $completedMode = (bool) $request->boolean('completed'); + $completedMode = $request->boolean('completed'); - // Eager load client case with person details - $case = \App\Models\ClientCase::query() - ->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])]) - ->findOrFail($clientCase->id); + // Eager load case with person details + $case = $clientCase->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'); - // Determine contracts of this case relevant to the current user - // - Normal mode: contracts assigned to me and still active (not completed/cancelled) - // - Completed mode (?completed=1): contracts where my field job was completed today - if ($completedMode) { - $start = now()->startOfDay(); - $end = now()->endOfDay(); - $contractIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereNull('cancelled_at') - ->whereBetween('completed_at', [$start, $end]) - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->pluck('contract_id') - ->unique() - ->values(); - } else { - $contractIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereNull('completed_at') - ->whereNull('cancelled_at') - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->pluck('contract_id') - ->unique() - ->values(); - } + // Query contracts based on field jobs + $contractsQuery = FieldJob::query() + ->where('assigned_user_id', $userId) + ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) + ->when($completedMode, + fn ($q) => $q->whereNull('cancelled_at')->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]), + fn ($q) => $q->whereNull('completed_at')->whereNull('cancelled_at') + ); + // Get contracts with relationships $contracts = \App\Models\Contract::query() ->where('client_case_id', $case->id) - ->whereIn('id', $contractIds) - ->with(['type:id,name', 'account']) + ->whereIn('id', $contractsQuery->pluck('contract_id')->unique()) + ->with(['type:id,name', 'account', 'latestObject']) ->orderByDesc('created_at') ->get(); - // Attach latest object (if any) to each contract as last_object for display - if ($contracts->isNotEmpty()) { - $byId = $contracts->keyBy('id'); - $latestObjects = \App\Models\CaseObject::query() - ->whereIn('contract_id', $byId->keys()) - ->whereNull('deleted_at') - ->select('id', 'reference', 'name', 'description', 'type', 'contract_id', 'created_at') - ->orderByDesc('created_at') - ->get() - ->groupBy('contract_id') - ->map(function ($group) { - return $group->first(); - }); - - foreach ($latestObjects as $cid => $obj) { - if (isset($byId[$cid])) { - $byId[$cid]->setAttribute('last_object', $obj); - } - } - } - - // Build merged documents: case documents + documents of assigned contracts - $contractRefMap = []; - foreach ($contracts as $c) { - $contractRefMap[$c->id] = $c->reference; - } - - $contractDocs = \App\Models\Document::query() - ->where('documentable_type', \App\Models\Contract::class) - ->whereIn('documentable_id', $contractIds) + // Build merged documents + $documents = $case->documents() ->orderByDesc('created_at') ->get() - ->map(function ($d) use ($contractRefMap) { - $arr = $d->toArray(); - $arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null; - $arr['documentable_type'] = \App\Models\Contract::class; - $arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid; - - return $arr; - }); - - $caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) { - $arr = $d->toArray(); - $arr['documentable_type'] = \App\Models\ClientCase::class; - $arr['client_case_uuid'] = $case->uuid; - - return $arr; - }); - - $documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values(); - - // Provide minimal types for PersonInfoGrid - $types = [ - 'address_types' => \App\Models\Person\AddressType::all(), - 'phone_types' => \App\Models\Person\PhoneType::all(), - ]; - - // Case activities (compact for phone): latest 20 with relations - $activities = $case->activities() - ->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name']) - ->orderByDesc('created_at') - ->limit(20) - ->get() - ->map(function ($a) { - $a->setAttribute('user_name', optional($a->user)->name); - - return $a; - }); - - // Determine segment filters from FieldJobSettings for this case/user context - $settingIds = FieldJob::query() - ->where('assigned_user_id', $userId) - ->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id)) - ->when( - $completedMode, - function ($q) { - $q->whereNull('cancelled_at') - ->whereBetween('completed_at', [now()->startOfDay(), now()->endOfDay()]); - }, - function ($q) { - $q->whereNull('completed_at')->whereNull('cancelled_at'); - } + ->map(fn ($d) => array_merge($d->toArray(), [ + 'documentable_type' => \App\Models\ClientCase::class, + 'client_case_uuid' => $case->uuid, + ])) + ->concat( + \App\Models\Document::query() + ->where('documentable_type', \App\Models\Contract::class) + ->whereIn('documentable_id', $contracts->pluck('id')) + ->with('documentable:id,uuid,reference') + ->orderByDesc('created_at') + ->get() + ->map(fn ($d) => array_merge($d->toArray(), [ + 'contract_reference' => $d->documentable?->reference, + 'contract_uuid' => $d->documentable?->uuid, + ])) ) - ->pluck('field_job_setting_id') - ->filter() - ->unique() + ->sortByDesc('created_at') ->values(); - $segmentIds = collect(); - if ($settingIds->isNotEmpty()) { - $segmentIds = \App\Models\FieldJobSetting::query() - ->whereIn('id', $settingIds) - ->pluck('segment_id') - ->filter() - ->unique() - ->values(); - } - - // Filter actions and their decisions by the derived segment ids (decisions.segment_id) - $actions = \App\Models\Action::query() - ->when($segmentIds->isNotEmpty(), function ($q) use ($segmentIds) { - // Filter actions by their segment_id matching the FieldJobSetting segment(s) - $q->whereIn('segment_id', $segmentIds); - }) - ->with([ - 'decisions' => function ($q) { - $q->select('decisions.id', 'decisions.name', 'decisions.color_tag', 'decisions.auto_mail', 'decisions.email_template_id'); - }, - 'decisions.emailTemplate' => function ($q) { - $q->select('id', 'name', 'entity_types', 'allow_attachments'); - }, - ]) - ->get(['id', 'name', 'color_tag', 'segment_id']); + // Get segment IDs for filtering actions + $segmentIds = \App\Models\FieldJobSetting::query() + ->whereIn('id', $contractsQuery->pluck('field_job_setting_id')->filter()->unique()) + ->pluck('segment_id') + ->filter() + ->unique(); return Inertia::render('Phone/Case/Index', [ - 'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(), + 'client' => $case->client->load('person.addresses', 'person.phones', 'person.emails', 'person.bankAccounts'), 'client_case' => $case, 'contracts' => $contracts, 'documents' => $documents, - 'types' => $types, + 'types' => [ + 'address_types' => \App\Models\Person\AddressType::all(), + 'phone_types' => \App\Models\Person\PhoneType::all(), + ], 'account_types' => \App\Models\AccountType::all(), - // Provide decisions (filtered by segment) with linked email template metadata (entity_types, allow_attachments) - 'actions' => $actions, - 'activities' => $activities, + 'actions' => \App\Models\Action::query() + ->when($segmentIds->isNotEmpty(), fn ($q) => $q->whereIn('segment_id', $segmentIds)) + ->with([ + 'decisions:id,name,color_tag,auto_mail,email_template_id', + 'decisions.emailTemplate:id,name,entity_types,allow_attachments', + ]) + ->get(['id', 'name', 'color_tag', 'segment_id']), + 'activities' => $case->activities() + ->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name']) + ->orderByDesc('created_at') + ->limit(20) + ->get() + ->map(fn ($a) => $a->setAttribute('user_name', $a->user?->name)), 'completed_mode' => $completedMode, ]); } diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 719762e..be4d12b 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -96,6 +96,11 @@ public function segments(): BelongsToMany ->wherePivot('active', true); } + public function attachedSegments(): BelongsToMany + { + return $this->belongsToMany(\App\Models\Segment::class); + } + public function account(): HasOne { // Use latestOfMany to always surface newest account snapshot if multiple exist. @@ -114,6 +119,18 @@ public function documents(): MorphMany return $this->morphMany(\App\Models\Document::class, 'documentable'); } + public function fieldJobs(): HasMany + { + return $this->hasMany(\App\Models\FieldJob::class); + } + + public function latestObject(): HasOne + { + return $this->hasOne(\App\Models\CaseObject::class) + ->whereNull('deleted_at') + ->latest(); + } + protected static function booted(): void { static::created(function (Contract $contract): void { diff --git a/app/Models/FieldJob.php b/app/Models/FieldJob.php index 179d44a..11dfa10 100644 --- a/app/Models/FieldJob.php +++ b/app/Models/FieldJob.php @@ -24,6 +24,8 @@ class FieldJob extends Model 'priority', 'notes', 'address_snapshot ', + 'last_activity', + 'added_activity' ]; protected $casts = [ @@ -31,6 +33,8 @@ class FieldJob extends Model 'completed_at' => 'datetime', 'cancelled_at' => 'datetime', 'priority' => 'boolean', + 'last_activity' => 'datetime', + 'added_activity' => 'boolean', 'address_snapshot ' => 'array', ]; @@ -90,7 +94,8 @@ public function user(): BelongsTo public function contract(): BelongsTo { - return $this->belongsTo(Contract::class, 'contract_id'); + return $this->belongsTo(Contract::class, 'contract_id') + ->where('active', true); } /** diff --git a/app/Services/Archiving/ArchiveExecutor.php b/app/Services/Archiving/ArchiveExecutor.php index 63de252..b3e1fd4 100644 --- a/app/Services/Archiving/ArchiveExecutor.php +++ b/app/Services/Archiving/ArchiveExecutor.php @@ -69,6 +69,8 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null, $entities = $flat; } + // dd($entities); + foreach ($entities as $entityDef) { $rawTable = $entityDef['table'] ?? null; if (! $rawTable) { @@ -97,7 +99,7 @@ public function executeSetting(ArchiveSetting $setting, ?array $context = null, // Process in batches to avoid locking large tables while (true) { $query = DB::table($table)->whereNull('deleted_at'); - if (Schema::hasColumn($table, 'active')) { + if (Schema::hasColumn($table, 'active') && ! $reactivate) { $query->where('active', 1); } // Apply context filters or chain derived filters diff --git a/composer.json b/composer.json index 9c4b92d..1f4b811 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,6 @@ "keywords": ["laravel", "framework"], "license": "MIT", "require": { - "tijsverkoyen/css-to-inline-styles": "^2.2", "php": "^8.2", "arielmejiadev/larapex-charts": "^2.1", "diglactic/laravel-breadcrumbs": "^10.0", @@ -19,7 +18,8 @@ "maatwebsite/excel": "^3.1", "meilisearch/meilisearch-php": "^1.11", "robertboes/inertia-breadcrumbs": "dev-laravel-12", - "tightenco/ziggy": "^2.0" + "tightenco/ziggy": "^2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index c640d1c..a8a9bdd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "51fd57123c1b9f51c24f28e04a692ec4", + "content-hash": "d29c47a4d6813ee8e80a7c8112c2f17e", "packages": [ { "name": "arielmejiadev/larapex-charts", @@ -242,6 +242,162 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "dasprid/enum", "version": "1.0.6", @@ -737,6 +893,67 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, { "name": "facade/ignition-contracts", "version": "1.0.2", @@ -2695,6 +2912,272 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "maatwebsite/excel", + "version": "3.1.67", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.0", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2025-08-26T09:13:16+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-12-10T09:58:31+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "meilisearch/meilisearch-php", "version": "v1.13.0", @@ -3475,6 +3958,112 @@ }, "time": "2024-10-02T11:20:13+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", + "reference": "fa8257a579ec623473eabfe49731de5967306c4c", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": ">=7.4.0 <8.5.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" + }, + "time": "2025-10-26T16:01:04+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -10335,6 +10924,6 @@ "platform": { "php": "^8.2" }, - "platform-dev": {}, + "platform-dev": [], "plugin-api-version": "2.6.0" } diff --git a/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php b/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php new file mode 100644 index 0000000..e4543f4 --- /dev/null +++ b/database/migrations/2025_12_20_171903_alter_field_jobs_table_add_column_last_activity.php @@ -0,0 +1,30 @@ +boolean('added_activity')->default(false); + $table->timestamp('last_activity')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('field_jobs', function (Blueprint $table) { + $table->dropColumn('last_activity'); + $table->dropColumn('added_activity'); + }); + } +}; diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue index 05fac8e..dacb612 100644 --- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue @@ -113,6 +113,7 @@ const store = async () => { form .transform((data) => ({ ...data, + phone_view: props.phoneMode, due_date: formatDateForSubmit(data.due_date), attachment_document_ids: templateAllowsAttachments.value && data.attach_documents diff --git a/resources/js/Pages/Phone/Index.vue b/resources/js/Pages/Phone/Index.vue index c2f9de1..8440a76 100644 --- a/resources/js/Pages/Phone/Index.vue +++ b/resources/js/Pages/Phone/Index.vue @@ -1,5 +1,6 @@