Emergency button for missing persons

This commit is contained in:
Simon Pocrnjič 2025-10-09 00:01:15 +02:00
parent c177264b0b
commit 86898eac1a
6 changed files with 194 additions and 27 deletions

View File

@ -228,11 +228,12 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
public function debugContractAccounts(ClientCase $clientCase, string $uuid, Request $request) public function debugContractAccounts(ClientCase $clientCase, string $uuid, Request $request)
{ {
abort_unless(config('app.debug'), 404); 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') $accounts = \DB::table('accounts')
->where('contract_id', $contract->id) ->where('contract_id', $contract->id)
->orderBy('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([ return response()->json([
'contract' => $contract, 'contract' => $contract,
'accounts' => $accounts, 'accounts' => $accounts,
@ -1108,7 +1109,7 @@ public function show(ClientCase $clientCase)
logger()->info('Show contracts balances', [ logger()->info('Show contracts balances', [
'case_id' => $case->id, 'case_id' => $case->id,
'contract_count' => $contracts->count(), 'contract_count' => $contracts->count(),
'contracts' => $contracts->map(fn($c) => [ 'contracts' => $contracts->map(fn ($c) => [
'id' => $c->id, 'id' => $c->id,
'uuid' => $c->uuid, 'uuid' => $c->uuid,
'reference' => $c->reference, 'reference' => $c->reference,
@ -1330,8 +1331,6 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
$hasReactivateRule = false; $hasReactivateRule = false;
} }
$executor = app(\App\Services\Archiving\ArchiveExecutor::class); $executor = app(\App\Services\Archiving\ArchiveExecutor::class);
$context = [ $context = [
'contract_id' => $contract->id, 'contract_id' => $contract->id,
@ -1475,4 +1474,70 @@ public function archiveContract(ClientCase $clientCase, string $uuid, Request $r
return back()->with('success', $message); 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,
]);
}
} }

View File

@ -158,4 +158,75 @@ public function update(Client $client, Request $request)
return to_route('client.show', $client); 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,
]);
}
} }

View File

@ -3,23 +3,27 @@
namespace App\Models\Person; namespace App\Models\Person;
use App\Traits\Uuid; use App\Traits\Uuid;
use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; 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\Sanctum\HasApiTokens;
use Laravel\Scout\Searchable; use Laravel\Scout\Searchable;
class Person extends Model class Person extends Model
{ {
use HasApiTokens; use HasApiTokens;
/** @use HasFactory<\Database\Factories\Person/PersonFactory> */ /** @use HasFactory<\Database\Factories\Person/PersonFactory> */
use HasFactory; use HasFactory;
use Uuid;
use Searchable; use Searchable;
use SoftDeletes;
use Uuid;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -27,6 +31,7 @@ class Person extends Model
* @var array<int, string> * @var array<int, string>
*/ */
protected $table = 'person'; protected $table = 'person';
protected $fillable = [ protected $fillable = [
'nu', 'nu',
'first_name', 'first_name',
@ -39,18 +44,19 @@ class Person extends Model
'description', 'description',
'group_id', 'group_id',
'type_id', 'type_id',
'user_id' 'user_id',
]; ];
protected $hidden = [ protected $hidden = [
'id', 'id',
'deleted', 'deleted',
'user_id' 'user_id',
]; ];
protected static function booted(){ protected static function booted()
{
static::creating(function (Person $person) { static::creating(function (Person $person) {
if(!isset($person->user_id)){ if (! isset($person->user_id)) {
$person->user_id = auth()->id(); $person->user_id = auth()->id();
} }
// Ensure a unique 6-character alphanumeric 'nu' is set globally on create // Ensure a unique 6-character alphanumeric 'nu' is set globally on create
@ -72,16 +78,15 @@ public function toSearchableArray(): array
'last_name' => '', 'last_name' => '',
'full_name' => '', 'full_name' => '',
'person_addresses.address' => '', 'person_addresses.address' => '',
'person_phones.nu' => '' 'person_phones.nu' => '',
]; ];
} }
public function phones(): HasMany public function phones(): HasMany
{ {
return $this->hasMany(\App\Models\Person\PersonPhone::class) return $this->hasMany(\App\Models\Person\PersonPhone::class)
->with(['type']) ->with(['type'])
->where('active','=',1) ->where('active', '=', 1)
->orderBy('id'); ->orderBy('id');
} }
@ -89,7 +94,7 @@ public function addresses(): HasMany
{ {
return $this->hasMany(\App\Models\Person\PersonAddress::class) return $this->hasMany(\App\Models\Person\PersonAddress::class)
->with(['type']) ->with(['type'])
->where('active','=',1) ->where('active', '=', 1)
->orderBy('id'); ->orderBy('id');
} }
@ -135,6 +140,7 @@ protected static function generateUniqueNu(): string
do { do {
$nu = Str::random(6); // [A-Za-z0-9] $nu = Str::random(6); // [A-Za-z0-9]
} while (static::where('nu', $nu)->exists()); } while (static::where('nu', $nu)->exists());
return $nu; return $nu;
} }
} }

View File

@ -90,6 +90,18 @@ const fmtCurrency = (v) => {
> >
{{ c.person?.full_name || "-" }} {{ c.person?.full_name || "-" }}
</Link> </Link>
<button
v-if="!c.person"
@click.prevent="
router.post(
route('clientCase.emergencyPerson', { client_case: c.uuid })
)
"
class="ml-2 inline-flex items-center rounded bg-red-50 px-2 py-0.5 text-xs font-semibold text-red-600 hover:bg-red-100 border border-red-200"
title="Emergency: recreate missing person"
>
Add Person
</button>
</td> </td>
<td class="py-2 pr-4">{{ c.client?.person?.full_name || "-" }}</td> <td class="py-2 pr-4">{{ c.client?.person?.full_name || "-" }}</td>

View File

@ -141,6 +141,17 @@ const fmtCurrency = (v) => {
> >
{{ client.person?.full_name || "-" }} {{ client.person?.full_name || "-" }}
</Link> </Link>
<div v-if="!client.person" class="mt-1">
<PrimaryButton
class="!py-0.5 !px-2 bg-red-500 hover:bg-red-600 text-xs"
@click.prevent="
router.post(
route('client.emergencyPerson', { uuid: client.uuid })
)
"
>Add Person</PrimaryButton
>
</div>
</td> </td>
<td class="py-2 pr-4 text-right"> <td class="py-2 pr-4 text-right">
{{ client.cases_with_active_contracts_count ?? 0 }} {{ client.cases_with_active_contracts_count ?? 0 }}

View File

@ -1,6 +1,5 @@
<?php <?php
use App\Charts\ExampleChart;
use App\Http\Controllers\AccountBookingController; use App\Http\Controllers\AccountBookingController;
use App\Http\Controllers\AccountPaymentController; use App\Http\Controllers\AccountPaymentController;
use App\Http\Controllers\ArchiveSettingController; use App\Http\Controllers\ArchiveSettingController;
@ -19,7 +18,6 @@
use App\Http\Controllers\SettingController; use App\Http\Controllers\SettingController;
use App\Http\Controllers\WorkflowController; use App\Http\Controllers\WorkflowController;
use App\Models\Person\Person; use App\Models\Person\Person;
use ArielMejiaDev\LarapexCharts\LarapexChart;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
@ -65,7 +63,9 @@
// Mail profiles (dynamic outgoing mail configuration) // 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', [\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::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::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'); 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::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
Route::post('clients', [ClientController::class, 'store'])->name('client.store'); Route::post('clients', [ClientController::class, 'store'])->name('client.store');
Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update'); 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 // client-case
Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase'); 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}/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/{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', [ClientCaseContoller::class, 'store'])->name('clientCase.store');
Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson');
// client-case / contract // client-case / contract
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store'); 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'); Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');