From 86898eac1ac416e33b8574d635b1eb262d5b54bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Pocrnji=C4=8D?= Date: Thu, 9 Oct 2025 00:01:15 +0200 Subject: [PATCH] Emergency button for missing persons --- app/Http/Controllers/ClientCaseContoller.php | 75 ++++++++++++++++++-- app/Http/Controllers/ClientController.php | 71 ++++++++++++++++++ app/Models/Person/Person.php | 32 +++++---- resources/js/Pages/Cases/Index.vue | 12 ++++ resources/js/Pages/Client/Index.vue | 11 +++ routes/web.php | 20 +++--- 6 files changed, 194 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index 1d1e563..db64cf4 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -228,11 +228,12 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr public function debugContractAccounts(ClientCase $clientCase, string $uuid, Request $request) { abort_unless(config('app.debug'), 404); - $contract = $clientCase->contracts()->where('uuid', $uuid)->firstOrFail(['id','uuid','reference']); + $contract = $clientCase->contracts()->where('uuid', $uuid)->firstOrFail(['id', 'uuid', 'reference']); $accounts = \DB::table('accounts') ->where('contract_id', $contract->id) ->orderBy('id') - ->get(['id','contract_id','initial_amount','balance_amount','type_id','created_at','updated_at']); + ->get(['id', 'contract_id', 'initial_amount', 'balance_amount', 'type_id', 'created_at', 'updated_at']); + return response()->json([ 'contract' => $contract, 'accounts' => $accounts, @@ -1108,7 +1109,7 @@ public function show(ClientCase $clientCase) logger()->info('Show contracts balances', [ 'case_id' => $case->id, 'contract_count' => $contracts->count(), - 'contracts' => $contracts->map(fn($c) => [ + 'contracts' => $contracts->map(fn ($c) => [ 'id' => $c->id, 'uuid' => $c->uuid, 'reference' => $c->reference, @@ -1330,8 +1331,6 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r $hasReactivateRule = false; } - - $executor = app(\App\Services\Archiving\ArchiveExecutor::class); $context = [ 'contract_id' => $contract->id, @@ -1475,4 +1474,70 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r return back()->with('success', $message); } + + /** + * Emergency: recreate a missing / soft-deleted person for a client case and re-link related data. + */ + public function emergencyCreatePerson(ClientCase $clientCase, Request $request) + { + $oldPersonId = $clientCase->person_id; + /** @var \App\Models\Person\Person|null $existing */ + $existing = \App\Models\Person\Person::withTrashed()->find($oldPersonId); + if ($existing && ! $existing->trashed()) { + return back()->with('flash', [ + 'type' => 'info', + 'message' => 'Person already exists – emergency creation not needed.', + ]); + } + + $data = $request->validate([ + 'full_name' => ['nullable', 'string', 'max:255'], + 'first_name' => ['nullable', 'string', 'max:255'], + 'last_name' => ['nullable', 'string', 'max:255'], + 'tax_number' => ['nullable', 'string', 'max:99'], + 'social_security_number' => ['nullable', 'string', 'max:99'], + 'description' => ['nullable', 'string', 'max:500'], + ]); + + $fullName = $data['full_name'] ?? trim(($data['first_name'] ?? '').' '.($data['last_name'] ?? '')); + if ($fullName === '') { + $fullName = 'Unknown Person'; + } + + $newPerson = null; + + \DB::transaction(function () use ($oldPersonId, $clientCase, $fullName, $data, &$newPerson) { + $newPerson = \App\Models\Person\Person::create([ + 'nu' => null, + 'first_name' => $data['first_name'] ?? null, + 'last_name' => $data['last_name'] ?? null, + 'full_name' => $fullName, + 'gender' => null, + 'birthday' => null, + 'tax_number' => $data['tax_number'] ?? null, + 'social_security_number' => $data['social_security_number'] ?? null, + 'description' => $data['description'] ?? 'Emergency recreated person (case)', + 'group_id' => 2, + 'type_id' => 1, + ]); + + // Re-point related data referencing old person + $tables = [ + 'emails', 'person_phones', 'person_addresses', 'bank_accounts', + ]; + foreach ($tables as $table) { + \DB::table($table)->where('person_id', $oldPersonId)->update(['person_id' => $newPerson->id]); + } + + // Update the client case + $clientCase->person_id = $newPerson->id; + $clientCase->save(); + }); + + return back()->with('flash', [ + 'type' => 'success', + 'message' => 'New person created and case re-linked.', + 'person_uuid' => $newPerson?->uuid, + ]); + } } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 45ce300..67bced2 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -158,4 +158,75 @@ public function update(Client $client, Request $request) return to_route('client.show', $client); } + + /** + * Emergency endpoint: if the linked person record is missing (hard deleted) or soft deleted, + * create a new minimal Person and re-point all related child records (emails, phones, addresses, bank accounts, + * client cases) from the old person_id to the new one, then update the client itself. + */ + public function emergencyCreatePerson(Client $client, Request $request) + { + $oldPersonId = $client->person_id; + + // If person exists and is not trashed, abort – nothing to do + /** @var \App\Models\Person\Person|null $existing */ + $existing = \App\Models\Person\Person::withTrashed()->find($oldPersonId); + if ($existing && ! $existing->trashed()) { + return redirect()->back()->with('flash', [ + 'type' => 'info', + 'message' => 'Person already exists – emergency creation not needed.', + ]); + } + + $data = $request->validate([ + 'full_name' => ['nullable', 'string', 'max:255'], + 'first_name' => ['nullable', 'string', 'max:255'], + 'last_name' => ['nullable', 'string', 'max:255'], + 'tax_number' => ['nullable', 'string', 'max:99'], + 'social_security_number' => ['nullable', 'string', 'max:99'], + 'description' => ['nullable', 'string', 'max:500'], + ]); + + // Provide sensible fallbacks. + $fullName = $data['full_name'] ?? trim(($data['first_name'] ?? '').' '.($data['last_name'] ?? '')); + if ($fullName === '') { + $fullName = 'Unknown Person'; + } + + $newPerson = null; + + \DB::transaction(function () use ($oldPersonId, $client, $fullName, $data, &$newPerson) { + $newPerson = \App\Models\Person\Person::create([ + 'nu' => null, // boot event will generate + 'first_name' => $data['first_name'] ?? null, + 'last_name' => $data['last_name'] ?? null, + 'full_name' => $fullName, + 'gender' => null, + 'birthday' => null, + 'tax_number' => $data['tax_number'] ?? null, + 'social_security_number' => $data['social_security_number'] ?? null, + 'description' => $data['description'] ?? 'Emergency recreated person', + 'group_id' => 1, + 'type_id' => 2, + ]); + + // Re-point related records referencing the old (missing) person id + $tables = [ + 'emails', 'person_phones', 'person_addresses', 'bank_accounts', 'client_cases', + ]; + foreach ($tables as $table) { + \DB::table($table)->where('person_id', $oldPersonId)->update(['person_id' => $newPerson->id]); + } + + // Finally update the client itself (only this one; avoid touching other potential clients) + $client->person_id = $newPerson->id; + $client->save(); + }); + + return redirect()->back()->with('flash', [ + 'type' => 'success', + 'message' => 'New person created and related records re-linked.', + 'person_uuid' => $newPerson?->uuid, + ]); + } } diff --git a/app/Models/Person/Person.php b/app/Models/Person/Person.php index 84797b2..d65c435 100644 --- a/app/Models/Person/Person.php +++ b/app/Models/Person/Person.php @@ -3,30 +3,35 @@ namespace App\Models\Person; use App\Traits\Uuid; -use Illuminate\Support\Str; +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\Relations\HasOne; -use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Str; use Laravel\Sanctum\HasApiTokens; use Laravel\Scout\Searchable; class Person extends Model { use HasApiTokens; + /** @use HasFactory<\Database\Factories\Person/PersonFactory> */ use HasFactory; - use Uuid; - use Searchable; - /** + use Searchable; + use SoftDeletes; + use Uuid; + + /** * The attributes that are mass assignable. * * @var array */ protected $table = 'person'; + protected $fillable = [ 'nu', 'first_name', @@ -39,18 +44,19 @@ class Person extends Model 'description', 'group_id', 'type_id', - 'user_id' + 'user_id', ]; protected $hidden = [ 'id', 'deleted', - 'user_id' + 'user_id', ]; - protected static function booted(){ + protected static function booted() + { static::creating(function (Person $person) { - if(!isset($person->user_id)){ + if (! isset($person->user_id)) { $person->user_id = auth()->id(); } // Ensure a unique 6-character alphanumeric 'nu' is set globally on create @@ -72,16 +78,15 @@ public function toSearchableArray(): array 'last_name' => '', 'full_name' => '', 'person_addresses.address' => '', - 'person_phones.nu' => '' + 'person_phones.nu' => '', ]; } - public function phones(): HasMany { return $this->hasMany(\App\Models\Person\PersonPhone::class) ->with(['type']) - ->where('active','=',1) + ->where('active', '=', 1) ->orderBy('id'); } @@ -89,7 +94,7 @@ public function addresses(): HasMany { return $this->hasMany(\App\Models\Person\PersonAddress::class) ->with(['type']) - ->where('active','=',1) + ->where('active', '=', 1) ->orderBy('id'); } @@ -135,6 +140,7 @@ protected static function generateUniqueNu(): string do { $nu = Str::random(6); // [A-Za-z0-9] } while (static::where('nu', $nu)->exists()); + return $nu; } } diff --git a/resources/js/Pages/Cases/Index.vue b/resources/js/Pages/Cases/Index.vue index 2cbbff4..10a3bf6 100644 --- a/resources/js/Pages/Cases/Index.vue +++ b/resources/js/Pages/Cases/Index.vue @@ -90,6 +90,18 @@ const fmtCurrency = (v) => { > {{ c.person?.full_name || "-" }} + {{ c.client?.person?.full_name || "-" }} diff --git a/resources/js/Pages/Client/Index.vue b/resources/js/Pages/Client/Index.vue index f07678f..d024bc8 100644 --- a/resources/js/Pages/Client/Index.vue +++ b/resources/js/Pages/Client/Index.vue @@ -141,6 +141,17 @@ const fmtCurrency = (v) => { > {{ client.person?.full_name || "-" }} +
+ Add Person +
{{ client.cases_with_active_contracts_count ?? 0 }} diff --git a/routes/web.php b/routes/web.php index d326dc0..16f1e8d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,5 @@ name('users.index'); Route::put('users/{user}', [\App\Http\Controllers\Admin\UserRoleController::class, 'update'])->name('users.update'); - // Permissions management - Route::get('permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'index'])->name('permissions.index'); - Route::get('permissions/create', [\App\Http\Controllers\Admin\PermissionController::class, 'create'])->name('permissions.create'); - Route::post('permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'store'])->name('permissions.store'); + // Permissions management + Route::get('permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'index'])->name('permissions.index'); + Route::get('permissions/create', [\App\Http\Controllers\Admin\PermissionController::class, 'create'])->name('permissions.create'); + Route::post('permissions', [\App\Http\Controllers\Admin\PermissionController::class, 'store'])->name('permissions.store'); // Document templates & global document settings Route::get('document-templates', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'index'])->name('document-templates.index'); Route::post('document-templates', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'store'])->name('document-templates.store'); Route::post('document-templates/{template}/toggle', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'toggleActive'])->name('document-templates.toggle'); Route::put('document-templates/{template}/settings', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'updateSettings'])->name('document-templates.settings.update'); - Route::get('document-templates/{template}', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'show'])->name('document-templates.show'); - Route::get('document-templates/{template}/edit', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'edit'])->name('document-templates.edit'); + Route::get('document-templates/{template}', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'show'])->name('document-templates.show'); + Route::get('document-templates/{template}/edit', [\App\Http\Controllers\Admin\DocumentTemplateController::class, 'edit'])->name('document-templates.edit'); Route::get('document-settings', [\App\Http\Controllers\Admin\DocumentSettingsController::class, 'edit'])->name('document-settings.index'); Route::put('document-settings', [\App\Http\Controllers\Admin\DocumentSettingsController::class, 'update'])->name('document-settings.update'); // Mail profiles (dynamic outgoing mail configuration) Route::get('mail-profiles', [\App\Http\Controllers\Admin\MailProfileController::class, 'index'])->name('mail-profiles.index'); - Route::get('mail-profiles.json', function() { return \App\Models\MailProfile::query()->get(); })->name('mail-profiles.json'); + Route::get('mail-profiles.json', function () { + return \App\Models\MailProfile::query()->get(); + })->name('mail-profiles.json'); Route::post('mail-profiles', [\App\Http\Controllers\Admin\MailProfileController::class, 'store'])->name('mail-profiles.store'); Route::put('mail-profiles/{mailProfile}', [\App\Http\Controllers\Admin\MailProfileController::class, 'update'])->name('mail-profiles.update'); Route::post('mail-profiles/{mailProfile}/toggle', [\App\Http\Controllers\Admin\MailProfileController::class, 'toggle'])->name('mail-profiles.toggle'); @@ -208,6 +208,7 @@ Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show'); Route::post('clients', [ClientController::class, 'store'])->name('client.store'); Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update'); + Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson'); // client-case Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase'); @@ -215,6 +216,7 @@ Route::post('client-cases/{client_case:uuid}/contracts/{uuid}/segment', [ClientCaseContoller::class, 'updateContractSegment'])->name('clientCase.contract.updateSegment'); Route::post('client-cases/{client_case:uuid}/contracts/{uuid}/archive', [ClientCaseContoller::class, 'archiveContract'])->name('clientCase.contract.archive'); Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store'); + Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson'); // client-case / contract Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store'); Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');