Dev branch

This commit is contained in:
Simon Pocrnjič
2025-11-02 12:31:01 +01:00
parent 5f879c9436
commit 63e0958b66
241 changed files with 17686 additions and 7327 deletions
+98 -550
View File
@@ -7,6 +7,8 @@
use App\Models\ClientCase;
use App\Models\Contract;
use App\Models\Document;
use App\Services\Documents\DocumentStreamService;
use App\Services\ReferenceDataCache;
use App\Services\Sms\SmsService;
use Exception;
use Illuminate\Database\QueryException;
@@ -16,45 +18,45 @@
class ClientCaseContoller extends Controller
{
public function __construct(
protected ReferenceDataCache $referenceCache,
protected DocumentStreamService $documentStream
) {}
/**
* Display a listing of the resource.
*/
public function index(ClientCase $clientCase, Request $request)
{
$search = $request->input('search');
$query = $clientCase::query()
->with(['person.client', 'client.person'])
->where('active', 1)
->when($request->input('search'), function ($que, $search) {
$que->whereHas('person', function ($q) use ($search) {
$q->where('full_name', 'ilike', '%'.$search.'%');
});
->select('client_cases.*')
->when($search, function ($que) use ($search) {
$que->join('person', 'person.id', '=', 'client_cases.person_id')
->where('person.full_name', 'ilike', '%'.$search.'%')
->groupBy('client_cases.id');
})
->where('client_cases.active', 1)
// Use LEFT JOINs for aggregated data to avoid subqueries
->leftJoin('contracts', function ($join) {
$join->on('contracts.client_case_id', '=', 'client_cases.id')
->whereNull('contracts.deleted_at');
})
->leftJoin('contract_segment', function ($join) {
$join->on('contract_segment.contract_id', '=', 'contracts.id')
->where('contract_segment.active', true);
})
->leftJoin('accounts', 'accounts.contract_id', '=', 'contracts.id')
->groupBy('client_cases.id')
->addSelect([
// Count of active contracts (a contract is considered active if it has an active pivot in contract_segment)
'active_contracts_count' => \DB::query()
->from('contracts')
->selectRaw('COUNT(*)')
->whereColumn('contracts.client_case_id', 'client_cases.id')
->whereNull('contracts.deleted_at')
->whereExists(function ($q) {
$q->from('contract_segment')
->whereColumn('contract_segment.contract_id', 'contracts.id')
->where('contract_segment.active', true);
}),
\DB::raw('COUNT(DISTINCT CASE WHEN contract_segment.id IS NOT NULL THEN contracts.id END) as active_contracts_count'),
// Sum of balances for accounts of active contracts
'active_contracts_balance_sum' => \DB::query()
->from('contracts')
->join('accounts', 'accounts.contract_id', '=', 'contracts.id')
->selectRaw('COALESCE(SUM(accounts.balance_amount), 0)')
->whereColumn('contracts.client_case_id', 'client_cases.id')
->whereNull('contracts.deleted_at')
->whereExists(function ($q) {
$q->from('contract_segment')
->whereColumn('contract_segment.contract_id', 'contracts.id')
->where('contract_segment.active', true);
}),
\DB::raw('COALESCE(SUM(CASE WHEN contract_segment.id IS NOT NULL THEN accounts.balance_amount END), 0) as active_contracts_balance_sum'),
])
->orderByDesc('created_at');
->with(['person.client', 'client.person'])
->orderByDesc('client_cases.created_at');
return Inertia::render('Cases/Index', [
'client_cases' => $query
@@ -609,188 +611,7 @@ public function viewDocument(ClientCase $clientCase, Document $document, Request
abort(404);
}
// Optional: add authz checks here (e.g., policies)
$disk = $document->disk ?: 'public';
// Normalize relative path (handle legacy 'public/' or 'public\\' prefixes and backslashes on Windows)
$relPath = $document->path ?? '';
$relPath = str_replace('\\', '/', $relPath); // unify slashes
$relPath = ltrim($relPath, '/');
if (str_starts_with($relPath, 'public/')) {
$relPath = substr($relPath, 7);
}
// If a preview exists (e.g., PDF generated for doc/docx), stream that
$previewDisk = config('files.preview_disk', 'public');
if ($document->preview_path && Storage::disk($previewDisk)->exists($document->preview_path)) {
$stream = Storage::disk($previewDisk)->readStream($document->preview_path);
if ($stream === false) {
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->preview_mime ?: 'application/pdf',
'Content-Disposition' => 'inline; filename="'.addslashes(($document->original_name ?: $document->file_name).'.pdf').'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
// If it's a DOC/DOCX and no preview yet, queue generation and show 202 Accepted
$ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
if (in_array($ext, ['doc', 'docx'])) {
\App\Jobs\GenerateDocumentPreview::dispatch($document->id);
return response('Preview is being generated. Please try again shortly.', 202);
}
// Try multiple path candidates to account for legacy prefixes
$candidates = [];
$candidates[] = $relPath;
// also try raw original (normalized slashes, trimmed)
$raw = $document->path ? ltrim(str_replace('\\', '/', $document->path), '/') : null;
if ($raw && $raw !== $relPath) {
$candidates[] = $raw;
}
// if path accidentally contains 'storage/' prefix (public symlink), strip it
if (str_starts_with($relPath, 'storage/')) {
$candidates[] = substr($relPath, 8);
}
if ($raw && str_starts_with($raw, 'storage/')) {
$candidates[] = substr($raw, 8);
}
$existsOnDisk = false;
foreach ($candidates as $cand) {
if (Storage::disk($disk)->exists($cand)) {
$existsOnDisk = true;
$relPath = $cand;
break;
}
}
if (! $existsOnDisk) {
// Fallback: some legacy files may live directly under public/, attempt to stream from there
$publicFull = public_path($relPath);
$real = @realpath($publicFull);
$publicRoot = @realpath(public_path());
$realN = $real ? str_replace('\\', '/', $real) : null;
$rootN = $publicRoot ? str_replace('\\', '/', $publicRoot) : null;
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
logger()->info('Document view fallback: serving from public path', [
'document_id' => $document->id,
'path' => $realN,
]);
$fp = @fopen($real, 'rb');
if ($fp === false) {
abort(404);
}
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'inline; filename="'.addslashes((($document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME)).'.'.strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION)))).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
logger()->warning('Document view 404: file missing on disk and public fallback failed', [
'document_id' => $document->id,
'document_uuid' => $document->uuid,
'disk' => $disk,
'path' => $document->path,
'normalizedCandidates' => $candidates,
'public_candidate' => $publicFull,
]);
abort(404);
}
$stream = Storage::disk($disk)->readStream($relPath);
if ($stream === false) {
logger()->warning('Document view: readStream failed, attempting fallbacks', [
'document_id' => $document->id,
'disk' => $disk,
'relPath' => $relPath,
]);
$headers = [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'inline; filename="'.addslashes((($document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME)).'.'.strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION)))).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
];
// Fallback 1: get() the bytes directly
try {
$bytes = Storage::disk($disk)->get($relPath);
} catch (\Throwable $e) {
$bytes = null;
}
if (! is_null($bytes) && $bytes !== false) {
return response($bytes, 200, $headers);
}
// Fallback 2: open via absolute path (local driver)
$abs = null;
try {
if (method_exists(Storage::disk($disk), 'path')) {
$abs = Storage::disk($disk)->path($relPath);
}
} catch (\Throwable $e) {
$abs = null;
}
if ($abs && is_file($abs)) {
$fp = @fopen($abs, 'rb');
if ($fp !== false) {
logger()->info('Document view fallback: serving from absolute storage path', [
'document_id' => $document->id,
'abs' => str_replace('\\\\', '/', (string) realpath($abs)),
]);
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
// Fallback 3: serve from public path if available
$publicFull = public_path($relPath);
$real = @realpath($publicFull);
$publicRoot = @realpath(public_path());
$realN = $real ? str_replace('\\\\', '/', $real) : null;
$rootN = $publicRoot ? str_replace('\\\\', '/', $publicRoot) : null;
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
logger()->info('Document view fallback: serving from public path (post-readStream failure)', [
'document_id' => $document->id,
'path' => $realN,
]);
$fp = @fopen($real, 'rb');
if ($fp !== false) {
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
logger()->warning('Document view 404: all fallbacks failed after readStream failure', [
'document_id' => $document->id,
'disk' => $disk,
'relPath' => $relPath,
]);
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'inline; filename="'.addslashes((($document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME)).'.'.strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION)))).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
return $this->documentStream->stream($document, inline: true);
}
public function downloadDocument(ClientCase $clientCase, Document $document, Request $request)
@@ -814,163 +635,8 @@ public function downloadDocument(ClientCase $clientCase, Document $document, Req
]);
abort(404);
}
$disk = $document->disk ?: 'public';
// Normalize relative path for Windows and legacy prefixes
$relPath = $document->path ?? '';
$relPath = str_replace('\\', '/', $relPath);
$relPath = ltrim($relPath, '/');
if (str_starts_with($relPath, 'public/')) {
$relPath = substr($relPath, 7);
}
$candidates = [];
$candidates[] = $relPath;
$raw = $document->path ? ltrim(str_replace('\\', '/', $document->path), '/') : null;
if ($raw && $raw !== $relPath) {
$candidates[] = $raw;
}
if (str_starts_with($relPath, 'storage/')) {
$candidates[] = substr($relPath, 8);
}
if ($raw && str_starts_with($raw, 'storage/')) {
$candidates[] = substr($raw, 8);
}
$existsOnDisk = false;
foreach ($candidates as $cand) {
if (Storage::disk($disk)->exists($cand)) {
$existsOnDisk = true;
$relPath = $cand;
break;
}
}
if (! $existsOnDisk) {
// Fallback to public/ direct path if present
$publicFull = public_path($relPath);
$real = @realpath($publicFull);
$publicRoot = @realpath(public_path());
$realN = $real ? str_replace('\\', '/', $real) : null;
$rootN = $publicRoot ? str_replace('\\', '/', $publicRoot) : null;
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
logger()->info('Document download fallback: serving from public path', [
'document_id' => $document->id,
'path' => $realN,
]);
$nameBase = $document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME);
$ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
$name = $ext ? ($nameBase.'.'.$ext) : $nameBase;
$fp = @fopen($real, 'rb');
if ($fp === false) {
abort(404);
}
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.addslashes($name).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
logger()->warning('Document download 404: file missing on disk and public fallback failed', [
'document_id' => $document->id,
'document_uuid' => $document->uuid,
'disk' => $disk,
'path' => $document->path,
'normalizedCandidates' => $candidates,
'public_candidate' => $publicFull,
]);
abort(404);
}
$nameBase = $document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME);
$ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
$name = $ext ? ($nameBase.'.'.$ext) : $nameBase;
$stream = Storage::disk($disk)->readStream($relPath);
if ($stream === false) {
logger()->warning('Document download: readStream failed, attempting fallbacks', [
'document_id' => $document->id,
'disk' => $disk,
'relPath' => $relPath,
]);
$headers = [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.addslashes($name).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
];
// Fallback 1: get() the bytes directly
try {
$bytes = Storage::disk($disk)->get($relPath);
} catch (\Throwable $e) {
$bytes = null;
}
if (! is_null($bytes) && $bytes !== false) {
return response($bytes, 200, $headers);
}
// Fallback 2: open via absolute storage path
$abs = null;
try {
if (method_exists(Storage::disk($disk), 'path')) {
$abs = Storage::disk($disk)->path($relPath);
}
} catch (\Throwable $e) {
$abs = null;
}
if ($abs && is_file($abs)) {
$fp = @fopen($abs, 'rb');
if ($fp !== false) {
logger()->info('Document download fallback: serving from absolute storage path', [
'document_id' => $document->id,
'abs' => str_replace('\\\\', '/', (string) realpath($abs)),
]);
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
// Fallback 3: serve from public path if available
$publicFull = public_path($relPath);
$real = @realpath($publicFull);
$publicRoot = @realpath(public_path());
$realN = $real ? str_replace('\\\\', '/', $real) : null;
$rootN = $publicRoot ? str_replace('\\\\', '/', $publicRoot) : null;
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
logger()->info('Document download fallback: serving from public path (post-readStream failure)', [
'document_id' => $document->id,
'path' => $realN,
]);
$fp = @fopen($real, 'rb');
if ($fp !== false) {
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
logger()->warning('Document download 404: all fallbacks failed after readStream failure', [
'document_id' => $document->id,
'disk' => $disk,
'relPath' => $relPath,
]);
abort(404);
}
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="'.addslashes($name).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
return $this->documentStream->stream($document, inline: false);
}
/**
@@ -984,8 +650,7 @@ public function viewContractDocument(Contract $contract, Document $document, Req
abort(404);
}
// Reuse the existing logic by delegating to a small helper
return $this->streamDocumentForDisk($document, inline: true);
return $this->documentStream->stream($document, inline: true);
}
/**
@@ -998,138 +663,7 @@ public function downloadContractDocument(Contract $contract, Document $document,
abort(404);
}
return $this->streamDocumentForDisk($document, inline: false);
}
/**
* Internal helper to stream a document either inline or as attachment with all Windows/public fallbacks.
*/
protected function streamDocumentForDisk(Document $document, bool $inline = true)
{
$disk = $document->disk ?: 'public';
$relPath = $document->path ?? '';
$relPath = str_replace('\\', '/', $relPath);
$relPath = ltrim($relPath, '/');
if (str_starts_with($relPath, 'public/')) {
$relPath = substr($relPath, 7);
}
// Previews for DOC/DOCX
$ext = strtolower(pathinfo($document->original_name ?: $document->file_name, PATHINFO_EXTENSION));
$previewDisk = config('files.preview_disk', 'public');
if ($inline && in_array($ext, ['doc', 'docx'])) {
if ($document->preview_path && Storage::disk($previewDisk)->exists($document->preview_path)) {
$stream = Storage::disk($previewDisk)->readStream($document->preview_path);
if ($stream !== false) {
$previewNameBase = $document->name ?: pathinfo($document->original_name ?: $document->file_name, PATHINFO_FILENAME);
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, [
'Content-Type' => $document->preview_mime ?: 'application/pdf',
'Content-Disposition' => 'inline; filename="'.addslashes($previewNameBase.'.pdf').'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
]);
}
}
\App\Jobs\GenerateDocumentPreview::dispatch($document->id);
return response('Preview is being generated. Please try again shortly.', 202);
}
// Try storage candidates
$candidates = [$relPath];
$raw = $document->path ? ltrim(str_replace('\\', '/', $document->path), '/') : null;
if ($raw && $raw !== $relPath) {
$candidates[] = $raw;
}
if (str_starts_with($relPath, 'storage/')) {
$candidates[] = substr($relPath, 8);
}
if ($raw && str_starts_with($raw, 'storage/')) {
$candidates[] = substr($raw, 8);
}
$found = null;
foreach ($candidates as $cand) {
if (Storage::disk($disk)->exists($cand)) {
$found = $cand;
break;
}
}
$headers = [
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
'Content-Disposition' => ($inline ? 'inline' : 'attachment').'; filename="'.addslashes($document->original_name ?: $document->file_name).'"',
'Cache-Control' => 'private, max-age=0, no-cache',
'Pragma' => 'no-cache',
];
if (! $found) {
// public/ fallback
$publicFull = public_path($relPath);
$real = @realpath($publicFull);
$publicRoot = @realpath(public_path());
$realN = $real ? str_replace('\\\\', '/', $real) : null;
$rootN = $publicRoot ? str_replace('\\\\', '/', $publicRoot) : null;
if ($realN && $rootN && str_starts_with($realN, $rootN) && is_file($real)) {
$fp = @fopen($real, 'rb');
if ($fp !== false) {
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
abort(404);
}
$stream = Storage::disk($disk)->readStream($found);
if ($stream !== false) {
return response()->stream(function () use ($stream) {
fpassthru($stream);
}, 200, $headers);
}
// Fallbacks on readStream failure
try {
$bytes = Storage::disk($disk)->get($found);
if (! is_null($bytes) && $bytes !== false) {
return response($bytes, 200, $headers);
}
} catch (\Throwable $e) {
}
$abs = null;
try {
if (method_exists(Storage::disk($disk), 'path')) {
$abs = Storage::disk($disk)->path($found);
}
} catch (\Throwable $e) {
$abs = null;
}
if ($abs && is_file($abs)) {
$fp = @fopen($abs, 'rb');
if ($fp !== false) {
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
// public/ again as last try
$publicFull = public_path($found);
$real = @realpath($publicFull);
if ($real && is_file($real)) {
$fp = @fopen($real, 'rb');
if ($fp !== false) {
return response()->stream(function () use ($fp) {
fpassthru($fp);
}, 200, $headers);
}
}
abort(404);
return $this->documentStream->stream($document, inline: false);
}
/**
@@ -1142,8 +676,8 @@ public function show(ClientCase $clientCase)
])->where('active', 1)->findOrFail($clientCase->id);
$types = [
'address_types' => \App\Models\Person\AddressType::all(),
'phone_types' => \App\Models\Person\PhoneType::all(),
'address_types' => $this->referenceCache->getAddressTypes(),
'phone_types' => $this->referenceCache->getPhoneTypes(),
];
// $active = false;
@@ -1210,10 +744,10 @@ public function show(ClientCase $clientCase)
});
}
// 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();
// Use pagination for contracts to avoid loading too many at once
// Default to 50 per page, but allow frontend to request more
$perPage = request()->integer('contracts_per_page', 50);
$contracts = $contractsQuery->paginate($perPage, ['*'], 'contracts_page')->withQueryString();
// TEMP DEBUG: log what balances are being sent to Inertia (remove once issue resolved)
try {
@@ -1234,49 +768,19 @@ public function show(ClientCase $clientCase)
// swallow
}
// Prepare contract reference and UUID maps from paginated contracts
$contractItems = $contracts instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator
? $contracts->items()
: $contracts->all();
$contractRefMap = [];
foreach ($contracts as $c) {
$contractUuidMap = [];
foreach ($contractItems as $c) {
$contractRefMap[$c->id] = $c->reference;
$contractUuidMap[$c->id] = $c->uuid;
}
// Merge client case and contract documents into a single array and include contract reference when applicable
$contractIds = $contracts->pluck('id');
// Include 'uuid' so frontend can build document routes (was causing missing 'document' param error)
// IMPORTANT: If there are no contracts for this case we must NOT return all contract documents from other cases.
if ($contractIds->isEmpty()) {
$contractDocs = collect();
} else {
$contractDocs = Document::query()
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
->where('documentable_type', Contract::class)
->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['contract_uuid'] = optional(Contract::withTrashed()->find($d->documentable_id))->uuid;
return $arr;
});
}
$caseDocs = $case->documents()
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
->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;
});
$mergedDocs = $caseDocs
->concat($contractDocs)
->sortByDesc('created_at')
->values();
$contractIds = collect($contractItems)->pluck('id');
// Resolve current segment for display when filtered
$currentSegment = null;
@@ -1284,10 +788,55 @@ public function show(ClientCase $clientCase)
$currentSegment = \App\Models\Segment::query()->select('id', 'name')->find($segmentId);
}
// Load initial batch of documents (limit to reduce payload size)
$contractDocs = collect();
if ($contractIds->isNotEmpty()) {
// Build UUID map for all contracts (including trashed) to avoid N+1 queries
$allContractUuids = Contract::withTrashed()
->whereIn('id', $contractIds->all())
->pluck('uuid', 'id')
->toArray();
// Merge with contracts already loaded
$contractUuidMap = array_merge($contractUuidMap, $allContractUuids);
$contractDocs = Document::query()
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
->where('documentable_type', Contract::class)
->whereIn('documentable_id', $contractIds->all())
->orderByDesc('created_at')
->limit(50) // Initial batch - frontend can request more via separate endpoint if needed
->get()
->map(function ($d) use ($contractRefMap, $contractUuidMap) {
$arr = $d->toArray();
$arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null;
$arr['contract_uuid'] = $contractUuidMap[$d->documentable_id] ?? null;
return $arr;
});
}
$caseDocs = $case->documents()
->select(['id', 'uuid', 'documentable_id', 'documentable_type', 'name', 'file_name', 'original_name', 'extension', 'mime_type', 'size', 'created_at', 'is_public'])
->orderByDesc('created_at')
->limit(50) // Initial batch
->get()
->map(function ($d) use ($case) {
$arr = $d->toArray();
$arr['client_case_uuid'] = $case->uuid;
return $arr;
});
$mergedDocs = $caseDocs
->concat($contractDocs)
->sortByDesc('created_at')
->values();
return Inertia::render('Cases/Show', [
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts', 'client']))->firstOrFail(),
'client_case' => $case,
'contracts' => $contracts,
'contracts' => $contracts, // Now paginated
'documents' => $mergedDocs,
])->with([
// Active document templates for contracts (latest version per slug)
'contract_doc_templates' => \App\Models\DocumentTemplate::query()
->where('active', true)
@@ -1326,9 +875,8 @@ function ($p) {
});
}
),
'documents' => $mergedDocs,
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
'account_types' => \App\Models\AccountType::all(),
'contract_types' => $this->referenceCache->getContractTypes(),
'account_types' => $this->referenceCache->getAccountTypes(),
// Include decisions with auto-mail metadata and the linked email template entity_types for UI logic
'actions' => \App\Models\Action::query()
->with([