changes
This commit is contained in:
parent
ee1af56d03
commit
f40c3d0f2e
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user