diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index 8de288c..24f7c3f 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -198,8 +198,10 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr // 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)) { - $newBalance = (float) optional($contract->account)->fresh()->balance_amount; - if ($newBalance !== $oldBalance) { + // Guard against null account (e.g., if creation failed silently earlier) + $newAccount = $contract->account; // single relationship access + $newBalance = $newAccount ? (float) optional($newAccount->fresh())->balance_amount : $oldBalance; + if ($newAccount && $newBalance !== $oldBalance) { try { $currency = optional(\App\Models\PaymentSetting::query()->first())->default_currency ?? 'EUR'; $beforeStr = number_format($oldBalance, 2, ',', '.').' '.$currency; @@ -244,12 +246,9 @@ public function storeActivity(ClientCase $clientCase, Request $request) // Map contract_uuid to contract_id within the same client case, if provided $contractId = null; if (! empty($attributes['contract_uuid'])) { - $contract = $clientCase->contracts()->where('uuid', $attributes['contract_uuid'])->firstOrFail('id'); + $contract = $clientCase->contracts()->where('uuid', $attributes['contract_uuid'])->firstOrFail(['id']); if ($contract) { - // Prevent attaching a new activity specifically to an archived contract - if (! $contract->active) { - return back()->with('warning', __('contracts.activity_not_allowed_archived')); - } + // Archived contracts are now allowed: link activity regardless of active flag $contractId = $contract->id; } } @@ -1049,7 +1048,22 @@ 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() - ->with(['type', 'account', 'objects', 'segments:id,name']); + // 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']) + ->with([ + 'type:id,name', + // Use closure for account to avoid ambiguous column names with latestOfMany join + 'account' => function ($q) { + $q->select([ + 'accounts.id', + 'accounts.contract_id', + 'accounts.type_id', + 'accounts.initial_amount', + 'accounts.balance_amount', + ]); + }, + 'segments:id,name', + ]); $contractsQuery->orderByDesc('created_at'); @@ -1070,7 +1084,10 @@ public function show(ClientCase $clientCase) }); } - $contracts = $contractsQuery->get(); + // NOTE: If a case has an extremely large number of contracts this can still be heavy. + // Consider pagination or deferred (Inertia lazy) loading. For now, hard-cap to 500 to prevent + // pathological memory / header growth. Frontend can request more via future endpoint. + $contracts = $contractsQuery->limit(500)->get(); $contractRefMap = []; foreach ($contracts as $c) { @@ -1080,26 +1097,31 @@ public function show(ClientCase $clientCase) // Merge client case and contract documents into a single array and include contract reference when applicable $contractIds = $contracts->pluck('id'); $contractDocs = Document::query() + ->select(['id', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at']) ->where('documentable_type', Contract::class) ->when($contractIds->isNotEmpty(), fn ($q) => $q->whereIn('documentable_id', $contractIds)) ->orderByDesc('created_at') + ->limit(300) // cap to prevent excessive payload; add pagination later if needed ->get() ->map(function ($d) use ($contractRefMap) { $arr = method_exists($d, 'toArray') ? $d->toArray() : (array) $d; $arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null; - $arr['documentable_type'] = Contract::class; $arr['contract_uuid'] = optional(Contract::withTrashed()->find($d->documentable_id))->uuid; return $arr; }); - $caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) { - $arr = method_exists($d, 'toArray') ? $d->toArray() : (array) $d; - $arr['documentable_type'] = ClientCase::class; - $arr['client_case_uuid'] = $case->uuid; + $caseDocs = $case->documents() + ->select(['id', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at']) + ->orderByDesc('created_at') + ->limit(200) + ->get() + ->map(function ($d) use ($case) { + $arr = method_exists($d, 'toArray') ? $d->toArray() : (array) $d; + $arr['client_case_uuid'] = $case->uuid; - return $arr; - }); + return $arr; + }); $mergedDocs = $caseDocs ->concat($contractDocs) ->sortByDesc('created_at') diff --git a/package-lock.json b/package-lock.json index 39254bf..0a27895 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,7 +4,6 @@ "requires": true, "packages": { "": { - "name": "Teren-app", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.6.0", "@fortawesome/free-brands-svg-icons": "^6.6.0", @@ -24,6 +23,7 @@ "reka-ui": "^2.5.1", "tailwindcss-inner-border": "^0.2.0", "v-calendar": "^3.1.2", + "vue-currency-input": "^3.2.1", "vue-multiselect": "^3.1.0", "vue-search-input": "^1.1.16", "vue3-apexcharts": "^1.7.0", @@ -3931,6 +3931,15 @@ } } }, + "node_modules/vue-currency-input": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/vue-currency-input/-/vue-currency-input-3.2.1.tgz", + "integrity": "sha512-Osfxzdu5cdZSCS4Cm0vuk7LwNeSdHWGIWK8gtDBC1kU0UtAKz7iU/8dyJ0KDJKxbAYiKeovoQTRfYxCH82I0EA==", + "license": "MIT", + "peerDependencies": { + "vue": "^2.7 || ^3.0.0" + } + }, "node_modules/vue-multiselect": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/vue-multiselect/-/vue-multiselect-3.3.1.tgz", diff --git a/package.json b/package.json index f9dd9fe..8dc69fe 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "vue-multiselect": "^3.1.0", "vue-search-input": "^1.1.16", "vue3-apexcharts": "^1.7.0", - "vuedraggable": "^4.1.0" + "vuedraggable": "^4.1.0", + "vue-currency-input": "^3.2.1" } } diff --git a/resources/js/Components/CurrencyInput.vue b/resources/js/Components/CurrencyInput.vue new file mode 100644 index 0000000..59b6554 --- /dev/null +++ b/resources/js/Components/CurrencyInput.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/resources/js/Components/TextInput.vue b/resources/js/Components/TextInput.vue index cddcd13..cd731ad 100644 --- a/resources/js/Components/TextInput.vue +++ b/resources/js/Components/TextInput.vue @@ -1,28 +1,31 @@ - + diff --git a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue index 302757f..01033b7 100644 --- a/resources/js/Pages/Cases/Partials/ActivityDrawer.vue +++ b/resources/js/Pages/Cases/Partials/ActivityDrawer.vue @@ -5,6 +5,7 @@ import DialogModal from "@/Components/DialogModal.vue"; import InputLabel from "@/Components/InputLabel.vue"; import DatePickerField from "@/Components/DatePickerField.vue"; import TextInput from "@/Components/TextInput.vue"; +import CurrencyInput from "@/Components/CurrencyInput.vue"; import { useForm } from "@inertiajs/vue3"; import { FwbTextarea } from "flowbite-vue"; import { ref, watch } from "vue"; @@ -170,13 +171,12 @@ watch( />