Changes to UI and other stuff
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class LZStringHelper
|
||||
{
|
||||
/**
|
||||
* Decompresses a string compressed with LZ-String's compressToEncodedURIComponent method.
|
||||
* This is a PHP port of the JavaScript LZ-String library.
|
||||
*
|
||||
* @param string $compressed
|
||||
* @return string|null
|
||||
*/
|
||||
public static function decompressFromEncodedURIComponent($compressed)
|
||||
{
|
||||
if ($compressed === null || $compressed === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Replace URL-safe characters back
|
||||
$compressed = str_replace(' ', '+', $compressed);
|
||||
|
||||
return self::decompress(strlen($compressed), 32, function ($index) use ($compressed) {
|
||||
return self::getBaseValue(self::$keyStrUriSafe, $compressed[$index]);
|
||||
});
|
||||
}
|
||||
|
||||
private static $keyStrUriSafe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$';
|
||||
|
||||
private static function getBaseValue($alphabet, $character)
|
||||
{
|
||||
$pos = strpos($alphabet, $character);
|
||||
|
||||
return $pos !== false ? $pos : -1;
|
||||
}
|
||||
|
||||
private static function decompress($length, $resetValue, $getNextValue)
|
||||
{
|
||||
$dictionary = [];
|
||||
$enlargeIn = 4;
|
||||
$dictSize = 4;
|
||||
$numBits = 3;
|
||||
$entry = '';
|
||||
$result = [];
|
||||
$data = ['val' => $getNextValue(0), 'position' => $resetValue, 'index' => 1];
|
||||
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$dictionary[$i] = chr($i);
|
||||
}
|
||||
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, 2);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$next = $bits;
|
||||
|
||||
switch ($next) {
|
||||
case 0:
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, 8);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$c = chr($bits);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, 16);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$c = chr($bits);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
return '';
|
||||
}
|
||||
|
||||
$dictionary[$dictSize++] = $c;
|
||||
$w = $c;
|
||||
$result[] = $c;
|
||||
|
||||
while (true) {
|
||||
if ($data['index'] > $length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, $numBits);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$c = $bits;
|
||||
|
||||
switch ($c) {
|
||||
case 0:
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, 8);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$dictionary[$dictSize++] = chr($bits);
|
||||
$c = $dictSize - 1;
|
||||
$enlargeIn--;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
$bits = 0;
|
||||
$maxpower = pow(2, 16);
|
||||
$power = 1;
|
||||
|
||||
while ($power != $maxpower) {
|
||||
$resb = $data['val'] & $data['position'];
|
||||
$data['position'] >>= 1;
|
||||
|
||||
if ($data['position'] == 0) {
|
||||
$data['position'] = $resetValue;
|
||||
$data['val'] = $getNextValue($data['index']++);
|
||||
}
|
||||
|
||||
$bits |= ($resb > 0 ? 1 : 0) * $power;
|
||||
$power <<= 1;
|
||||
}
|
||||
|
||||
$dictionary[$dictSize++] = chr($bits);
|
||||
$c = $dictSize - 1;
|
||||
$enlargeIn--;
|
||||
break;
|
||||
|
||||
case 2:
|
||||
return implode('', $result);
|
||||
}
|
||||
|
||||
if ($enlargeIn == 0) {
|
||||
$enlargeIn = pow(2, $numBits);
|
||||
$numBits++;
|
||||
}
|
||||
|
||||
if (isset($dictionary[$c])) {
|
||||
$entry = $dictionary[$c];
|
||||
} else {
|
||||
if ($c === $dictSize) {
|
||||
$entry = $w.$w[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = $entry;
|
||||
|
||||
$dictionary[$dictSize++] = $w.$entry[0];
|
||||
$enlargeIn--;
|
||||
|
||||
$w = $entry;
|
||||
|
||||
if ($enlargeIn == 0) {
|
||||
$enlargeIn = pow(2, $numBits);
|
||||
$numBits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ class ClientCaseContoller extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected ReferenceDataCache $referenceCache,
|
||||
protected DocumentStreamService $documentStream
|
||||
protected DocumentStreamService $documentStream,
|
||||
protected \App\Services\ClientCaseDataService $caseDataService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -29,7 +30,7 @@ public function __construct(
|
||||
public function index(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$search = $request->input('search');
|
||||
|
||||
|
||||
$query = $clientCase::query()
|
||||
->select('client_cases.*')
|
||||
->when($search, function ($que) use ($search) {
|
||||
@@ -60,7 +61,7 @@ public function index(ClientCase $clientCase, Request $request)
|
||||
|
||||
return Inertia::render('Cases/Index', [
|
||||
'client_cases' => $query
|
||||
->paginate($request->integer('perPage', 15), ['*'], 'client-cases-page')
|
||||
->paginate($request->integer('perPage', 15), ['*'], 'clientCasesPage')
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
@@ -348,7 +349,7 @@ public function deleteActivity(ClientCase $clientCase, \App\Models\Activity $act
|
||||
});
|
||||
|
||||
return back()->with('success', 'Activity deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteContract(ClientCase $clientCase, string $uuid, Request $request)
|
||||
{
|
||||
@@ -680,137 +681,36 @@ public function show(ClientCase $clientCase)
|
||||
'phone_types' => $this->referenceCache->getPhoneTypes(),
|
||||
];
|
||||
|
||||
// $active = false;
|
||||
|
||||
// Optional segment filter from query string
|
||||
$segmentId = request()->integer('segment');
|
||||
|
||||
// Determine latest archive (non-reactivate) setting for this context to infer archive segment and related tables
|
||||
$latestArchiveSetting = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('reactivate')->orWhere('reactivate', false);
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
$archiveSegmentId = optional($latestArchiveSetting)->segment_id; // may be null
|
||||
$relatedArchiveTables = [];
|
||||
if ($latestArchiveSetting) {
|
||||
$entities = (array) $latestArchiveSetting->entities;
|
||||
foreach ($entities as $edef) {
|
||||
if (isset($edef['related']) && is_array($edef['related'])) {
|
||||
foreach ($edef['related'] as $rel) {
|
||||
$relatedArchiveTables[] = $rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
$relatedArchiveTables = array_values(array_unique($relatedArchiveTables));
|
||||
}
|
||||
|
||||
// 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 (include description for UI display)
|
||||
->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'meta', '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',
|
||||
'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');
|
||||
|
||||
if (! empty($segmentId)) {
|
||||
// Filter to contracts that are in the provided segment and active on pivot
|
||||
$contractsQuery->whereExists(function ($q) use ($segmentId) {
|
||||
$q->from('contract_segment')
|
||||
->whereColumn('contract_segment.contract_id', 'contracts.id')
|
||||
->where('contract_segment.segment_id', $segmentId)
|
||||
->where('contract_segment.active', true);
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// Prepare contract reference map from paginated contracts
|
||||
$contractItems = $contracts instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
? $contracts->items()
|
||||
: $contracts->all();
|
||||
|
||||
|
||||
$contractRefMap = [];
|
||||
$contractUuidMap = [];
|
||||
foreach ($contractItems as $c) {
|
||||
$contractRefMap[$c->id] = $c->reference;
|
||||
$contractUuidMap[$c->id] = $c->uuid;
|
||||
}
|
||||
|
||||
$contractIds = collect($contractItems)->pluck('id');
|
||||
|
||||
// Resolve current segment for display when filtered
|
||||
$currentSegment = null;
|
||||
if (! empty($segmentId)) {
|
||||
$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()) {
|
||||
$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;
|
||||
});
|
||||
}
|
||||
// Get contracts using service
|
||||
$contractsPerPage = request()->integer('contracts_per_page', 10);
|
||||
$contracts = $this->caseDataService->getContracts($case, $segmentId, $contractsPerPage);
|
||||
$contractIds = collect($contracts->items())->pluck('id')->all();
|
||||
|
||||
$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();
|
||||
// Get activities using service
|
||||
$activitiesPerPage = request()->integer('activities_per_page', 15);
|
||||
$encodedFilters = request()->input('filter_activities');
|
||||
$activities = $this->caseDataService->getActivities($case, $segmentId, $encodedFilters, $contractIds, $activitiesPerPage);
|
||||
|
||||
// Get documents using service
|
||||
$contractsPerPage = request()->integer('documentsPerPage', 15);
|
||||
$documents = $this->caseDataService->getDocuments($case, $contractIds, $contractsPerPage);
|
||||
|
||||
// Get archive metadata using service
|
||||
|
||||
$archiveMeta = $this->caseDataService->getArchiveMeta();
|
||||
|
||||
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, // Now paginated
|
||||
'documents' => $mergedDocs,
|
||||
'contracts' => $contracts,
|
||||
'documents' => $documents,
|
||||
])->with([
|
||||
// Active document templates for contracts (latest version per slug)
|
||||
'contract_doc_templates' => \App\Models\DocumentTemplate::query()
|
||||
->where('active', true)
|
||||
->where('core_entity', 'contract')
|
||||
@@ -819,38 +719,10 @@ public function show(ClientCase $clientCase)
|
||||
->groupBy('slug')
|
||||
->map(fn ($g) => $g->sortByDesc('version')->first())
|
||||
->values(),
|
||||
'archive_meta' => [
|
||||
'archive_segment_id' => $archiveSegmentId,
|
||||
'related_tables' => $relatedArchiveTables,
|
||||
],
|
||||
'activities' => tap(
|
||||
(function () use ($case, $segmentId, $contractIds) {
|
||||
$q = $case->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at');
|
||||
if (! empty($segmentId)) {
|
||||
// Only activities for filtered contracts or unlinked (contract_id null)
|
||||
$q->where(function ($qq) use ($contractIds) {
|
||||
$qq->whereNull('contract_id');
|
||||
if ($contractIds->isNotEmpty()) {
|
||||
$qq->orWhereIn('contract_id', $contractIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $q->paginate(20, ['*'], 'activities')->withQueryString();
|
||||
})(),
|
||||
function ($p) {
|
||||
$p->getCollection()->transform(function ($a) {
|
||||
$a->setAttribute('user_name', optional($a->user)->name);
|
||||
|
||||
return $a;
|
||||
});
|
||||
}
|
||||
),
|
||||
'archive_meta' => $archiveMeta,
|
||||
'activities' => $activities,
|
||||
'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([
|
||||
'decisions' => function ($q) {
|
||||
@@ -865,7 +737,6 @@ function ($p) {
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id', 'name']),
|
||||
'current_segment' => $currentSegment,
|
||||
// SMS helpers for per-case sending UI
|
||||
'sms_profiles' => \App\Models\SmsProfile::query()
|
||||
->select(['id', 'name', 'default_sender_id'])
|
||||
->where('active', true)
|
||||
@@ -950,7 +821,7 @@ public function deleteContractDocument(Contract $contract, Document $document, R
|
||||
|
||||
$document->delete();
|
||||
|
||||
return back()->with('success', 'Document deleted.')->with('flash_method', 'DELETE');
|
||||
return back()->with('success', 'Document deleted.')->with('flash_method', 'DELETE');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ActivityCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
// Transform data to add user_name attribute
|
||||
$this->collection->transform(function ($activity) {
|
||||
$activity->setAttribute('user_name', optional($activity->user)->name);
|
||||
|
||||
return $activity;
|
||||
});
|
||||
|
||||
return $this->resource->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class ContractCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return $this->resource->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class DocumentCollection extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @return array<int|string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->collection,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -57,6 +59,69 @@ protected static function booted()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope activities to those linked to contracts within a specific segment.
|
||||
*/
|
||||
#[Scope]
|
||||
public function scopeForSegment(Builder $query, int $segmentId, array $contractIds): Builder
|
||||
{
|
||||
return $query->where(function ($q) use ($contractIds) {
|
||||
$q->whereNull('contract_id');
|
||||
if (! empty($contractIds)) {
|
||||
$q->orWhereIn('contract_id', $contractIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope activities with decoded base64 filters.
|
||||
*/
|
||||
#[Scope]
|
||||
public function scopeWithFilters(Builder $query, ?string $encodedFilters, \App\Models\ClientCase $clientCase): Builder
|
||||
{
|
||||
if (empty($encodedFilters)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
try {
|
||||
$decompressed = base64_decode($encodedFilters);
|
||||
$filters = json_decode($decompressed, true);
|
||||
|
||||
if (! is_array($filters)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if (! empty($filters['action_id'])) {
|
||||
$query->where('action_id', $filters['action_id']);
|
||||
}
|
||||
|
||||
if (! empty($filters['contract_uuid'])) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $filters['contract_uuid'])->first(['id']);
|
||||
if ($contract) {
|
||||
$query->where('contract_id', $contract->id);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($filters['user_id'])) {
|
||||
$query->where('user_id', $filters['user_id']);
|
||||
}
|
||||
|
||||
if (! empty($filters['date_from'])) {
|
||||
$query->whereDate('created_at', '>=', $filters['date_from']);
|
||||
}
|
||||
|
||||
if (! empty($filters['date_to'])) {
|
||||
$query->whereDate('created_at', '<=', $filters['date_to']);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Invalid activity filter format', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function action(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Action::class);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Uuid;
|
||||
use Illuminate\Database\Eloquent\Attributes\Scope;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -55,6 +57,7 @@ protected function startDate(): Attribute
|
||||
return null;
|
||||
}
|
||||
$str = is_string($value) ? $value : (string) $value;
|
||||
|
||||
return \App\Services\DateNormalizer::toDate($str);
|
||||
}
|
||||
);
|
||||
@@ -71,11 +74,26 @@ protected function endDate(): Attribute
|
||||
return null;
|
||||
}
|
||||
$str = is_string($value) ? $value : (string) $value;
|
||||
|
||||
return \App\Services\DateNormalizer::toDate($str);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope contracts to those in a specific segment with active pivot.
|
||||
*/
|
||||
#[Scope]
|
||||
public function scopeForSegment(Builder $query, int $segmentId): Builder
|
||||
{
|
||||
return $query->whereExists(function ($q) use ($segmentId) {
|
||||
$q->from('contract_segment')
|
||||
->whereColumn('contract_segment.contract_id', 'contracts.id')
|
||||
->where('contract_segment.segment_id', $segmentId)
|
||||
->where('contract_segment.active', true);
|
||||
});
|
||||
}
|
||||
|
||||
public function type(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\ContractType::class, 'type_id');
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class ClientCaseDataService
|
||||
{
|
||||
/**
|
||||
* Get paginated contracts for a client case with optional segment filtering.
|
||||
*/
|
||||
public function getContracts(ClientCase $clientCase, ?int $segmentId = null, int $perPage = 50): LengthAwarePaginator
|
||||
{
|
||||
$query = $clientCase->contracts()
|
||||
->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'meta', 'active', 'type_id', 'client_case_id', 'created_at'])
|
||||
->with([
|
||||
'type:id,name',
|
||||
'account' => function ($q) {
|
||||
$q->select([
|
||||
'accounts.id',
|
||||
'accounts.contract_id',
|
||||
'accounts.type_id',
|
||||
'accounts.initial_amount',
|
||||
'accounts.balance_amount',
|
||||
'accounts.promise_date',
|
||||
'accounts.created_at',
|
||||
'accounts.updated_at',
|
||||
])->orderByDesc('accounts.id');
|
||||
},
|
||||
'segments:id,name',
|
||||
'objects:id,contract_id,reference,name,description,type,created_at',
|
||||
])
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if (! empty($segmentId)) {
|
||||
$query->forSegment($segmentId);
|
||||
}
|
||||
|
||||
$perPage = max(1, min(100, $perPage));
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'contracts_page')->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated activities for a client case with optional segment and filter constraints.
|
||||
*/
|
||||
public function getActivities(
|
||||
ClientCase $clientCase,
|
||||
?int $segmentId = null,
|
||||
?string $encodedFilters = null,
|
||||
array $contractIds = [],
|
||||
int $perPage = 20
|
||||
): LengthAwarePaginator {
|
||||
$query = $clientCase->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at');
|
||||
|
||||
if (! empty($segmentId)) {
|
||||
$query->forSegment($segmentId, $contractIds);
|
||||
}
|
||||
|
||||
if (! empty($encodedFilters)) {
|
||||
$query->withFilters($encodedFilters, $clientCase);
|
||||
}
|
||||
|
||||
$perPage = max(1, min(100, $perPage));
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'activities_page')->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merged documents from case and its contracts.
|
||||
*/
|
||||
public function getDocuments(ClientCase $clientCase, array $contractIds = [], int $perPage = 15): LengthAwarePaginator
|
||||
{
|
||||
$query = null;
|
||||
$caseDocsQuery = Document::query()
|
||||
->select([
|
||||
'documents.id',
|
||||
'documents.uuid',
|
||||
'documents.documentable_id',
|
||||
'documents.documentable_type',
|
||||
'documents.name',
|
||||
'documents.file_name',
|
||||
'documents.original_name',
|
||||
'documents.extension',
|
||||
'documents.mime_type',
|
||||
'documents.size',
|
||||
'documents.created_at',
|
||||
'documents.is_public',
|
||||
\DB::raw('NULL as contract_reference'),
|
||||
\DB::raw('NULL as contract_uuid'),
|
||||
\DB::raw("'{$clientCase->uuid}' as client_case_uuid"),
|
||||
\DB::raw('users.name as created_by'),
|
||||
])
|
||||
->join('users', 'documents.user_id', '=', 'users.id')
|
||||
->where('documents.documentable_type', ClientCase::class)
|
||||
->where('documents.documentable_id', $clientCase->id);
|
||||
|
||||
if (! empty($contractIds)) {
|
||||
// Get contract references for mapping
|
||||
$contracts = Contract::query()
|
||||
->whereIn('id', $contractIds)
|
||||
->get(['id', 'uuid', 'reference'])
|
||||
->keyBy('id');
|
||||
|
||||
$contractDocsQuery = Document::query()
|
||||
->select([
|
||||
'documents.id',
|
||||
'documents.uuid',
|
||||
'documents.documentable_id',
|
||||
'documents.documentable_type',
|
||||
'documents.name',
|
||||
'documents.file_name',
|
||||
'documents.original_name',
|
||||
'documents.extension',
|
||||
'documents.mime_type',
|
||||
'documents.size',
|
||||
'documents.created_at',
|
||||
'documents.is_public',
|
||||
'contracts.reference as contract_reference',
|
||||
'contracts.uuid as contract_uuid',
|
||||
\DB::raw('NULL as client_case_uuid'),
|
||||
\DB::raw('users.name as created_by'),
|
||||
])
|
||||
->join('users', 'documents.user_id', '=', 'users.id')
|
||||
->join('contracts', 'documents.documentable_id', '=', 'contracts.id')
|
||||
->where('documents.documentable_type', Contract::class)
|
||||
->whereIn('documents.documentable_id', $contractIds);
|
||||
|
||||
// Union the queries
|
||||
$query = $caseDocsQuery->union($contractDocsQuery);
|
||||
} else {
|
||||
$query = $caseDocsQuery;
|
||||
}
|
||||
|
||||
return \DB::table(\DB::raw("({$query->toSql()}) as documents"))
|
||||
->mergeBindings($query->getQuery())
|
||||
->orderByDesc('created_at')
|
||||
->paginate($perPage, ['*'], 'documentsPage')
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive metadata from latest non-reactivate archive setting.
|
||||
*/
|
||||
public function getArchiveMeta(): array
|
||||
{
|
||||
$latestArchiveSetting = \App\Models\ArchiveSetting::query()
|
||||
->where('enabled', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('reactivate')->orWhere('reactivate', false);
|
||||
})
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$archiveSegmentId = optional($latestArchiveSetting)->segment_id;
|
||||
$relatedArchiveTables = [];
|
||||
|
||||
if ($latestArchiveSetting) {
|
||||
$entities = (array) $latestArchiveSetting->entities;
|
||||
foreach ($entities as $edef) {
|
||||
if (isset($edef['related']) && is_array($edef['related'])) {
|
||||
foreach ($edef['related'] as $rel) {
|
||||
$relatedArchiveTables[] = $rel;
|
||||
}
|
||||
}
|
||||
}
|
||||
$relatedArchiveTables = array_values(array_unique($relatedArchiveTables));
|
||||
}
|
||||
|
||||
return [
|
||||
'archive_segment_id' => $archiveSegmentId,
|
||||
'related_tables' => $relatedArchiveTables,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user