From f40c3d0f2e2335043815b3102eb808f60069cd83 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 8 Oct 2025 18:26:47 +0200 Subject: [PATCH] changes --- app/Http/Controllers/ClientCaseContoller.php | 112 +++++++++++++++--- app/Models/Account.php | 9 ++ app/Models/Contract.php | 1 + .../Pages/Cases/Partials/ContractDrawer.vue | 21 +++- .../js/Pages/Cases/Partials/ContractTable.vue | 5 + routes/web.php | 1 + 6 files changed, 134 insertions(+), 15 deletions(-) diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index 2a0d4d4..4a90271 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -172,34 +172,76 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr ]); $initial = $request->input('initial_amount'); - $balance = $request->input('balance_amount'); - $shouldUpsertAccount = (! is_null($initial)) || (! is_null($balance)) || $request->has('account_type_id'); + // Use has() to distinguish between an omitted field and an explicit 0 / null intent + $balanceFieldPresent = $request->has('balance_amount'); + $balance = $balanceFieldPresent ? $request->input('balance_amount') : null; + // Always allow updating existing account even if only balance set to 0 (or unchanged) so user can correct it. + $hasType = $request->has('account_type_id'); + $shouldUpsertAccount = ($contract->account()->exists()) || (! is_null($initial)) || $balanceFieldPresent || $hasType; if ($shouldUpsertAccount) { $accountData = []; // Track old balance before applying changes - $oldBalance = (float) optional($contract->account)->balance_amount; + $currentAccount = $contract->account; // newest (latestOfMany) + $oldBalance = (float) optional($currentAccount)->balance_amount; if (! is_null($initial)) { $accountData['initial_amount'] = $initial; } - if (! is_null($balance)) { - $accountData['balance_amount'] = $balance; + // If the balance field was present in the request payload we always apply it (allow setting to 0) + if ($balanceFieldPresent) { + // Allow explicitly setting to 0, fallback to 0 if null provided + $accountData['balance_amount'] = $balance ?? 0; } if ($request->has('account_type_id')) { $accountData['type_id'] = $request->input('account_type_id'); } - - if ($contract->account) { - $contract->account->update($accountData); + if ($currentAccount) { + $currentAccount->update($accountData); + if (array_key_exists('balance_amount', $accountData)) { + $currentAccount->forceFill(['balance_amount' => $accountData['balance_amount']])->save(); + $freshBal = (float) optional($currentAccount->fresh())->balance_amount; + if ((float) $freshBal !== (float) $accountData['balance_amount']) { + \DB::table('accounts') + ->where('id', $currentAccount->id) + ->update(['balance_amount' => $accountData['balance_amount'], 'updated_at' => now()]); + $freshBal = (float) optional($currentAccount->fresh())->balance_amount; + } + } else { + $freshBal = (float) optional($currentAccount->fresh())->balance_amount; + } } else { - // For create, ensure defaults exist if not provided $accountData = array_merge(['initial_amount' => 0, 'balance_amount' => 0], $accountData); - $contract->account()->create($accountData); + $created = $contract->account()->create($accountData); + $freshBal = (float) optional($created->fresh())->balance_amount; + } + // If multiple historical accounts exist, log them and optionally propagate update to all to keep consistent + $allAccounts = \DB::table('accounts')->where('contract_id', $contract->id)->orderBy('id')->get(['id','balance_amount','initial_amount']); + if ($allAccounts->count() > 1 && array_key_exists('balance_amount', $accountData)) { + // Propagate balance to all for consistency (comment out if not desired) + \DB::table('accounts')->where('contract_id', $contract->id)->update(['balance_amount' => $accountData['balance_amount'], 'updated_at' => now()]); + $freshBal = (float) \DB::table('accounts')->where('contract_id', $contract->id)->latest('id')->value('balance_amount'); + } + try { + $accountCount = $allAccounts->count(); + logger()->info('Contract account upsert', [ + 'contract_id' => $contract->id, + 'request_initial' => $initial, + 'request_balance_present' => $balanceFieldPresent, + 'request_balance' => $balance, + 'request_account_type_id' => $request->input('account_type_id'), + 'account_data_applied' => $accountData, + 'old_balance' => $oldBalance, + 'new_balance_after_update' => $freshBal, + 'accounts_for_contract' => $accountCount, + 'accounts_snapshot' => $allAccounts, + ]); + } catch (\Throwable $e) { + // ignore logging errors } // After update/create, if balance_amount changed (and not through a payment), log an activity with before/after if (array_key_exists('balance_amount', $accountData)) { // Guard against null account (e.g., if creation failed silently earlier) - $newAccount = $contract->account; // single relationship access + $newAccount = $contract->account; // refreshed latest $newBalance = $newAccount ? (float) optional($newAccount->fresh())->balance_amount : $oldBalance; if ($newAccount && $newBalance !== $oldBalance) { try { @@ -231,6 +273,24 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]); } + /** + * Debug endpoint: list all account rows for a contract (only in debug mode). + */ + 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']); + $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']); + return response()->json([ + 'contract' => $contract, + 'accounts' => $accounts, + 'count' => $accounts->count(), + ]); + } + public function storeActivity(ClientCase $clientCase, Request $request) { try { @@ -1048,8 +1108,8 @@ public function show(ClientCase $clientCase) // Prepare contracts and a reference map. // Only apply active/inactive filtering IF a segment filter is provided. $contractsQuery = $case->contracts() - // Only select lean columns to avoid oversize JSON / headers - ->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'active', 'type_id', 'client_case_id', 'created_at']) + // Only select lean columns to avoid oversize JSON / headers (include description for UI display) + ->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'active', 'type_id', 'client_case_id', 'created_at']) ->with([ 'type:id,name', // Use closure for account to avoid ambiguous column names with latestOfMany join @@ -1060,9 +1120,14 @@ public function show(ClientCase $clientCase) 'accounts.type_id', 'accounts.initial_amount', 'accounts.balance_amount', - ]); + 'accounts.promise_date', + 'accounts.created_at', + 'accounts.updated_at', // include updated_at so FE can detect changes & for debugging + ])->orderByDesc('accounts.id'); }, 'segments:id,name', + // Eager load objects so newly created objects appear without full reload logic issues + 'objects:id,contract_id,reference,name,description,type,created_at', ]); $contractsQuery->orderByDesc('created_at'); @@ -1089,6 +1154,25 @@ public function show(ClientCase $clientCase) // pathological memory / header growth. Frontend can request more via future endpoint. $contracts = $contractsQuery->limit(500)->get(); + // TEMP DEBUG: log what balances are being sent to Inertia (remove once issue resolved) + try { + logger()->info('Show contracts balances', [ + 'case_id' => $case->id, + 'contract_count' => $contracts->count(), + 'contracts' => $contracts->map(fn($c) => [ + 'id' => $c->id, + 'uuid' => $c->uuid, + 'reference' => $c->reference, + 'account_id' => optional($c->account)->id, + 'initial_amount' => optional($c->account)->initial_amount, + 'balance_amount' => optional($c->account)->balance_amount, + 'account_updated_at' => optional($c->account)->updated_at, + ])->toArray(), + ]); + } catch (\Throwable $e) { + // swallow + } + $contractRefMap = []; foreach ($contracts as $c) { $contractRefMap[$c->id] = $c->reference; diff --git a/app/Models/Account.php b/app/Models/Account.php index 8ebfc9b..633fe4d 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -22,6 +22,15 @@ class Account extends Model 'balance_amount', ]; + protected function casts(): array + { + return [ + 'initial_amount' => 'decimal:4', + 'balance_amount' => 'decimal:4', + 'promise_date' => 'date', + ]; + } + public function debtor(): BelongsTo { return $this->belongsTo(\App\Models\Person\Person::class, 'debtor_id'); diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 1f8b540..2fca9ad 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -55,6 +55,7 @@ public function segments(): BelongsToMany public function account(): HasOne { + // Use latestOfMany to always surface newest account snapshot if multiple exist. return $this->hasOne(\App\Models\Account::class) ->latestOfMany() ->with('type'); diff --git a/resources/js/Pages/Cases/Partials/ContractDrawer.vue b/resources/js/Pages/Cases/Partials/ContractDrawer.vue index 0d60283..36af1ed 100644 --- a/resources/js/Pages/Cases/Partials/ContractDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ContractDrawer.vue @@ -8,7 +8,7 @@ import SectionTitle from "@/Components/SectionTitle.vue"; import TextInput from "@/Components/TextInput.vue"; import CurrencyInput from "@/Components/CurrencyInput.vue"; import DatePickerField from "@/Components/DatePickerField.vue"; -import { useForm } from "@inertiajs/vue3"; +import { useForm, router } from "@inertiajs/vue3"; import { watch, nextTick, ref as vRef } from "vue"; const props = defineProps({ @@ -95,6 +95,10 @@ watch( const storeOrUpdate = () => { const isEdit = !!formContract.uuid; + // Debug: log payload being sent to verify balance_amount presence + try { + console.debug('Submitting contract form', JSON.parse(JSON.stringify(formContract))); + } catch (e) {} const options = { onBefore: () => { formContract.start_date = formContract.start_date; @@ -103,6 +107,21 @@ const storeOrUpdate = () => { close(); // keep state clean; reset to initial if (!isEdit) formContract.reset(); + // After edit ensure contracts list reflects updated balance + if (isEdit) { + try { + const params = {}; + try { + const url = new URL(window.location.href); + const seg = url.searchParams.get('segment'); + if (seg) params.segment = seg; + } catch (e) {} + router.visit(route('clientCase.show', { client_case: props.client_case.uuid, ...params }), { + preserveScroll: true, + replace: true, + }); + } catch (e) {} + } }, preserveScroll: true, }; diff --git a/resources/js/Pages/Cases/Partials/ContractTable.vue b/resources/js/Pages/Cases/Partials/ContractTable.vue index d089f63..4272c21 100644 --- a/resources/js/Pages/Cases/Partials/ContractTable.vue +++ b/resources/js/Pages/Cases/Partials/ContractTable.vue @@ -34,6 +34,11 @@ const props = defineProps({ all_segments: { type: Array, default: () => [] }, }); +// Debug: log incoming contract balances (remove after fix) +try { + console.debug('Contracts received (balances):', props.contracts.map(c => ({ ref: c.reference, bal: c?.account?.balance_amount }))); +} catch (e) {} + const emit = defineEmits(["edit", "delete", "add-activity"]); const formatDate = (d) => { diff --git a/routes/web.php b/routes/web.php index 069c526..d326dc0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -218,6 +218,7 @@ // 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'); + Route::get('client-cases/{client_case:uuid}/contract/{uuid}/debug-accounts', [ClientCaseContoller::class, 'debugContractAccounts'])->name('clientCase.contract.debugAccounts'); Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete'); // client-case / contract / objects Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store');