This commit is contained in:
Simon 2025-10-08 18:26:47 +02:00
parent ee1af56d03
commit f40c3d0f2e
6 changed files with 134 additions and 15 deletions

View File

@ -172,34 +172,76 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
]); ]);
$initial = $request->input('initial_amount'); $initial = $request->input('initial_amount');
$balance = $request->input('balance_amount'); // Use has() to distinguish between an omitted field and an explicit 0 / null intent
$shouldUpsertAccount = (! is_null($initial)) || (! is_null($balance)) || $request->has('account_type_id'); $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) { if ($shouldUpsertAccount) {
$accountData = []; $accountData = [];
// Track old balance before applying changes // 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)) { if (! is_null($initial)) {
$accountData['initial_amount'] = $initial; $accountData['initial_amount'] = $initial;
} }
if (! is_null($balance)) { // If the balance field was present in the request payload we always apply it (allow setting to 0)
$accountData['balance_amount'] = $balance; if ($balanceFieldPresent) {
// Allow explicitly setting to 0, fallback to 0 if null provided
$accountData['balance_amount'] = $balance ?? 0;
} }
if ($request->has('account_type_id')) { if ($request->has('account_type_id')) {
$accountData['type_id'] = $request->input('account_type_id'); $accountData['type_id'] = $request->input('account_type_id');
} }
if ($currentAccount) {
if ($contract->account) { $currentAccount->update($accountData);
$contract->account->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 { } else {
// For create, ensure defaults exist if not provided
$accountData = array_merge(['initial_amount' => 0, 'balance_amount' => 0], $accountData); $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 // 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)) { if (array_key_exists('balance_amount', $accountData)) {
// Guard against null account (e.g., if creation failed silently earlier) // 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; $newBalance = $newAccount ? (float) optional($newAccount->fresh())->balance_amount : $oldBalance;
if ($newAccount && $newBalance !== $oldBalance) { if ($newAccount && $newBalance !== $oldBalance) {
try { try {
@ -231,6 +273,24 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]); 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) public function storeActivity(ClientCase $clientCase, Request $request)
{ {
try { try {
@ -1048,8 +1108,8 @@ public function show(ClientCase $clientCase)
// Prepare contracts and a reference map. // Prepare contracts and a reference map.
// Only apply active/inactive filtering IF a segment filter is provided. // Only apply active/inactive filtering IF a segment filter is provided.
$contractsQuery = $case->contracts() $contractsQuery = $case->contracts()
// Only select lean columns to avoid oversize JSON / headers // Only select lean columns to avoid oversize JSON / headers (include description for UI display)
->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'active', 'type_id', 'client_case_id', 'created_at']) ->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'active', 'type_id', 'client_case_id', 'created_at'])
->with([ ->with([
'type:id,name', 'type:id,name',
// Use closure for account to avoid ambiguous column names with latestOfMany join // 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.type_id',
'accounts.initial_amount', 'accounts.initial_amount',
'accounts.balance_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', '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'); $contractsQuery->orderByDesc('created_at');
@ -1089,6 +1154,25 @@ public function show(ClientCase $clientCase)
// pathological memory / header growth. Frontend can request more via future endpoint. // pathological memory / header growth. Frontend can request more via future endpoint.
$contracts = $contractsQuery->limit(500)->get(); $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 = []; $contractRefMap = [];
foreach ($contracts as $c) { foreach ($contracts as $c) {
$contractRefMap[$c->id] = $c->reference; $contractRefMap[$c->id] = $c->reference;

View File

@ -22,6 +22,15 @@ class Account extends Model
'balance_amount', 'balance_amount',
]; ];
protected function casts(): array
{
return [
'initial_amount' => 'decimal:4',
'balance_amount' => 'decimal:4',
'promise_date' => 'date',
];
}
public function debtor(): BelongsTo public function debtor(): BelongsTo
{ {
return $this->belongsTo(\App\Models\Person\Person::class, 'debtor_id'); return $this->belongsTo(\App\Models\Person\Person::class, 'debtor_id');

View File

@ -55,6 +55,7 @@ public function segments(): BelongsToMany
public function account(): HasOne public function account(): HasOne
{ {
// Use latestOfMany to always surface newest account snapshot if multiple exist.
return $this->hasOne(\App\Models\Account::class) return $this->hasOne(\App\Models\Account::class)
->latestOfMany() ->latestOfMany()
->with('type'); ->with('type');

View File

@ -8,7 +8,7 @@ import SectionTitle from "@/Components/SectionTitle.vue";
import TextInput from "@/Components/TextInput.vue"; import TextInput from "@/Components/TextInput.vue";
import CurrencyInput from "@/Components/CurrencyInput.vue"; import CurrencyInput from "@/Components/CurrencyInput.vue";
import DatePickerField from "@/Components/DatePickerField.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"; import { watch, nextTick, ref as vRef } from "vue";
const props = defineProps({ const props = defineProps({
@ -95,6 +95,10 @@ watch(
const storeOrUpdate = () => { const storeOrUpdate = () => {
const isEdit = !!formContract.uuid; 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 = { const options = {
onBefore: () => { onBefore: () => {
formContract.start_date = formContract.start_date; formContract.start_date = formContract.start_date;
@ -103,6 +107,21 @@ const storeOrUpdate = () => {
close(); close();
// keep state clean; reset to initial // keep state clean; reset to initial
if (!isEdit) formContract.reset(); 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, preserveScroll: true,
}; };

View File

@ -34,6 +34,11 @@ const props = defineProps({
all_segments: { type: Array, default: () => [] }, 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 emit = defineEmits(["edit", "delete", "add-activity"]);
const formatDate = (d) => { const formatDate = (d) => {

View File

@ -218,6 +218,7 @@
// 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');
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'); Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
// client-case / contract / objects // client-case / contract / objects
Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store'); Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store');