changes 0328092025
This commit is contained in:
@@ -2,16 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreContractRequest;
|
||||
use App\Http\Requests\UpdateContractRequest;
|
||||
use App\Models\ClientCase;
|
||||
use App\Models\Contract;
|
||||
use App\Models\Document;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Exception;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\StoreContractRequest;
|
||||
use App\Http\Requests\UpdateContractRequest;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ClientCaseContoller extends Controller
|
||||
@@ -23,17 +22,16 @@ public function index(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
return Inertia::render('Cases/Index', [
|
||||
'client_cases' => $clientCase::with(['person'])
|
||||
->when($request->input('search'), fn($que, $search) =>
|
||||
$que->whereHas(
|
||||
'person',
|
||||
fn($q) => $q->where('full_name', 'ilike', '%' . $search . '%')
|
||||
)
|
||||
->when($request->input('search'), fn ($que, $search) => $que->whereHas(
|
||||
'person',
|
||||
fn ($q) => $q->where('full_name', 'ilike', '%'.$search.'%')
|
||||
)
|
||||
)
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15, ['*'], 'client-cases-page')
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search'])
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -55,13 +53,13 @@ public function store(Request $request)
|
||||
|
||||
$client = \App\Models\Client::where('uuid', $cuuid)->firstOrFail();
|
||||
|
||||
if( isset($client->id) ){
|
||||
if (isset($client->id)) {
|
||||
|
||||
\DB::transaction(function() use ($request, $client){
|
||||
\DB::transaction(function () use ($request, $client) {
|
||||
$pq = $request->input('person');
|
||||
|
||||
$person = $client->person()->create([
|
||||
'nu' => rand(100000,200000),
|
||||
'nu' => rand(100000, 200000),
|
||||
'first_name' => $pq['first_name'],
|
||||
'last_name' => $pq['last_name'],
|
||||
'full_name' => $pq['full_name'],
|
||||
@@ -71,23 +69,23 @@ public function store(Request $request)
|
||||
'social_security_number' => $pq['social_security_number'],
|
||||
'description' => $pq['description'],
|
||||
'group_id' => 2,
|
||||
'type_id' => 1
|
||||
'type_id' => 1,
|
||||
]);
|
||||
|
||||
$person->addresses()->create([
|
||||
'address' => $pq['address']['address'],
|
||||
'country' => $pq['address']['country'],
|
||||
'type_id' => $pq['address']['type_id']
|
||||
'type_id' => $pq['address']['type_id'],
|
||||
]);
|
||||
|
||||
$person->phones()->create([
|
||||
'nu' => $pq['phone']['nu'],
|
||||
'country_code' => $pq['phone']['country_code'],
|
||||
'type_id' => $pq['phone']['type_id']
|
||||
'type_id' => $pq['phone']['type_id'],
|
||||
]);
|
||||
|
||||
$person->clientCase()->create([
|
||||
'client_id' => $client->id
|
||||
'client_id' => $client->id,
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -97,8 +95,8 @@ public function store(Request $request)
|
||||
|
||||
public function storeContract(ClientCase $clientCase, StoreContractRequest $request)
|
||||
{
|
||||
|
||||
\DB::transaction(function() use ($request, $clientCase){
|
||||
|
||||
\DB::transaction(function () use ($request, $clientCase) {
|
||||
|
||||
// Create contract
|
||||
$contract = $clientCase->contracts()->create([
|
||||
@@ -110,11 +108,13 @@ public function storeContract(ClientCase $clientCase, StoreContractRequest $requ
|
||||
|
||||
// Note: Contract config auto-application is handled in Contract model created hook.
|
||||
|
||||
// Optionally create/update related account amounts
|
||||
// Optionally create related account with amounts and/or type
|
||||
$initial = $request->input('initial_amount');
|
||||
$balance = $request->input('balance_amount');
|
||||
if (!is_null($initial) || !is_null($balance)) {
|
||||
$accountTypeId = $request->input('account_type_id');
|
||||
if (! is_null($initial) || ! is_null($balance) || ! is_null($accountTypeId)) {
|
||||
$contract->account()->create([
|
||||
'type_id' => $accountTypeId,
|
||||
'initial_amount' => $initial ?? 0,
|
||||
'balance_amount' => $balance ?? 0,
|
||||
]);
|
||||
@@ -125,11 +125,11 @@ public function storeContract(ClientCase $clientCase, StoreContractRequest $requ
|
||||
return to_route('clientCase.show', $clientCase);
|
||||
}
|
||||
|
||||
public function updateContract(ClientCase $clientCase, String $uuid, UpdateContractRequest $request)
|
||||
public function updateContract(ClientCase $clientCase, string $uuid, UpdateContractRequest $request)
|
||||
{
|
||||
$contract = Contract::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
\DB::transaction(function() use ($request, $contract){
|
||||
\DB::transaction(function () use ($request, $contract) {
|
||||
$contract->update([
|
||||
'reference' => $request->input('reference'),
|
||||
'type_id' => $request->input('type_id'),
|
||||
@@ -139,14 +139,24 @@ public function updateContract(ClientCase $clientCase, String $uuid, UpdateContr
|
||||
|
||||
$initial = $request->input('initial_amount');
|
||||
$balance = $request->input('balance_amount');
|
||||
if (!is_null($initial) || !is_null($balance)) {
|
||||
$accountData = [
|
||||
'initial_amount' => $initial ?? 0,
|
||||
'balance_amount' => $balance ?? 0,
|
||||
];
|
||||
$shouldUpsertAccount = (! is_null($initial)) || (! is_null($balance)) || $request->has('account_type_id');
|
||||
if ($shouldUpsertAccount) {
|
||||
$accountData = [];
|
||||
if (! is_null($initial)) {
|
||||
$accountData['initial_amount'] = $initial;
|
||||
}
|
||||
if (! is_null($balance)) {
|
||||
$accountData['balance_amount'] = $balance;
|
||||
}
|
||||
if ($request->has('account_type_id')) {
|
||||
$accountData['type_id'] = $request->input('account_type_id');
|
||||
}
|
||||
|
||||
if ($contract->account) {
|
||||
$contract->account->update($accountData);
|
||||
} else {
|
||||
// For create, ensure defaults exist if not provided
|
||||
$accountData = array_merge(['initial_amount' => 0, 'balance_amount' => 0], $accountData);
|
||||
$contract->account()->create($accountData);
|
||||
}
|
||||
}
|
||||
@@ -156,7 +166,8 @@ public function updateContract(ClientCase $clientCase, String $uuid, UpdateContr
|
||||
return to_route('clientCase.show', $clientCase);
|
||||
}
|
||||
|
||||
public function storeActivity(ClientCase $clientCase, Request $request) {
|
||||
public function storeActivity(ClientCase $clientCase, Request $request)
|
||||
{
|
||||
try {
|
||||
$attributes = $request->validate([
|
||||
'due_date' => 'nullable|date',
|
||||
@@ -166,16 +177,16 @@ public function storeActivity(ClientCase $clientCase, Request $request) {
|
||||
'decision_id' => 'exists:\App\Models\Decision,id',
|
||||
'contract_uuid' => 'nullable|uuid',
|
||||
]);
|
||||
|
||||
|
||||
// Map contract_uuid to contract_id within the same client case, if provided
|
||||
$contractId = null;
|
||||
if (!empty($attributes['contract_uuid'])) {
|
||||
if (! empty($attributes['contract_uuid'])) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $attributes['contract_uuid'])->firstOrFail('id');
|
||||
if ($contract) {
|
||||
$contractId = $contract->id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create activity
|
||||
$row = $clientCase->activities()->create([
|
||||
'due_date' => $attributes['due_date'] ?? null,
|
||||
@@ -189,24 +200,30 @@ public function storeActivity(ClientCase $clientCase, Request $request) {
|
||||
$class = '\\App\\Events\\' . $e->name;
|
||||
event(new $class($clientCase));
|
||||
}*/
|
||||
|
||||
|
||||
logger()->info('Activity successfully inserted', $attributes);
|
||||
return to_route('clientCase.show', $clientCase)->with('success', 'Successful created!');
|
||||
|
||||
// Stay on the current page (desktop or phone) instead of forcing a redirect to the desktop route.
|
||||
// Use 303 to align with Inertia's recommended POST/Redirect/GET behavior.
|
||||
return back(303)->with('success', 'Successful created!');
|
||||
} catch (QueryException $e) {
|
||||
logger()->error('Database error occurred:', ['error' => $e->getMessage()]);
|
||||
return back()->with('error', 'Failed to insert activity. ' . $e->getMessage());
|
||||
|
||||
return back()->with('error', 'Failed to insert activity. '.$e->getMessage());
|
||||
} catch (Exception $e) {
|
||||
logger()->error('An unexpected error occurred:', ['error' => $e->getMessage()]);
|
||||
|
||||
// Return a generic error response
|
||||
return back()->with('error', 'An unexpected error occurred. Please try again later.');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function deleteContract(ClientCase $clientCase, String $uuid, Request $request) {
|
||||
public function deleteContract(ClientCase $clientCase, string $uuid, Request $request)
|
||||
{
|
||||
$contract = Contract::where('uuid', $uuid)->firstOrFail();
|
||||
|
||||
\DB::transaction(function() use ($request, $contract){
|
||||
\DB::transaction(function () use ($contract) {
|
||||
$contract->delete();
|
||||
});
|
||||
|
||||
@@ -254,22 +271,22 @@ public function attachSegment(ClientCase $clientCase, Request $request)
|
||||
'make_active_for_contract' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
\DB::transaction(function () use ($clientCase, $validated) {
|
||||
\DB::transaction(function () use ($clientCase, $validated) {
|
||||
// Attach segment to client case if not already attached
|
||||
$attached = \DB::table('client_case_segment')
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->where('segment_id', $validated['segment_id'])
|
||||
->first();
|
||||
if (!$attached) {
|
||||
if (! $attached) {
|
||||
$clientCase->segments()->attach($validated['segment_id'], ['active' => true]);
|
||||
} else if (!$attached->active) {
|
||||
} elseif (! $attached->active) {
|
||||
\DB::table('client_case_segment')
|
||||
->where('id', $attached->id)
|
||||
->update(['active' => true, 'updated_at' => now()]);
|
||||
}
|
||||
|
||||
// Optionally make it active for a specific contract
|
||||
if (!empty($validated['contract_uuid']) && ($validated['make_active_for_contract'] ?? false)) {
|
||||
if (! empty($validated['contract_uuid']) && ($validated['make_active_for_contract'] ?? false)) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $validated['contract_uuid'])->firstOrFail();
|
||||
\DB::table('contract_segment')
|
||||
->where('contract_id', $contract->id)
|
||||
@@ -300,11 +317,18 @@ public function storeDocument(ClientCase $clientCase, Request $request)
|
||||
'name' => 'nullable|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'is_public' => 'sometimes|boolean',
|
||||
'contract_uuid' => 'nullable|uuid',
|
||||
]);
|
||||
|
||||
$file = $validated['file'];
|
||||
$disk = 'public';
|
||||
$directory = 'cases/' . $clientCase->uuid . '/documents';
|
||||
$contract = null;
|
||||
if (! empty($validated['contract_uuid'])) {
|
||||
$contract = $clientCase->contracts()->where('uuid', $validated['contract_uuid'])->first();
|
||||
}
|
||||
$directory = $contract
|
||||
? ('contracts/'.$contract->uuid.'/documents')
|
||||
: ('cases/'.$clientCase->uuid.'/documents');
|
||||
$path = $file->store($directory, $disk);
|
||||
|
||||
$doc = new Document([
|
||||
@@ -319,14 +343,18 @@ public function storeDocument(ClientCase $clientCase, Request $request)
|
||||
'mime_type' => $file->getMimeType(),
|
||||
'size' => $file->getSize(),
|
||||
'checksum' => null,
|
||||
'is_public' => (bool)($validated['is_public'] ?? false),
|
||||
'is_public' => (bool) ($validated['is_public'] ?? false),
|
||||
]);
|
||||
|
||||
$clientCase->documents()->save($doc);
|
||||
if ($contract) {
|
||||
$contract->documents()->save($doc);
|
||||
} else {
|
||||
$clientCase->documents()->save($doc);
|
||||
}
|
||||
|
||||
// Generate preview immediately for Office docs to avoid first-view delay
|
||||
$ext = strtolower($doc->extension ?? pathinfo($doc->original_name ?? $doc->file_name, PATHINFO_EXTENSION));
|
||||
if (in_array($ext, ['doc','docx'])) {
|
||||
if (in_array($ext, ['doc', 'docx'])) {
|
||||
\App\Jobs\GenerateDocumentPreview::dispatch($doc->id);
|
||||
}
|
||||
|
||||
@@ -335,22 +363,52 @@ public function storeDocument(ClientCase $clientCase, Request $request)
|
||||
|
||||
public function viewDocument(ClientCase $clientCase, Document $document, Request $request)
|
||||
{
|
||||
// Ensure the document belongs to this client case
|
||||
if ($document->documentable_type !== ClientCase::class || $document->documentable_id !== $clientCase->id) {
|
||||
// Ensure the document belongs to this client case or its contracts
|
||||
$belongsToCase = $document->documentable_type === ClientCase::class && $document->documentable_id === $clientCase->id;
|
||||
$belongsToContractOfCase = false;
|
||||
if ($document->documentable_type === Contract::class) {
|
||||
// Include soft-deleted contracts when verifying ownership to this case
|
||||
$belongsToContractOfCase = Contract::withTrashed()
|
||||
->where('id', $document->documentable_id)
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->exists();
|
||||
|
||||
}
|
||||
if (! ($belongsToCase || $belongsToContractOfCase)) {
|
||||
logger()->warning('Document view 404: document does not belong to case or its contracts', [
|
||||
'document_id' => $document->id,
|
||||
'document_uuid' => $document->uuid,
|
||||
'documentable_type' => $document->documentable_type,
|
||||
'documentable_id' => $document->documentable_id,
|
||||
'client_case_id' => $clientCase->id,
|
||||
'client_case_uuid' => $clientCase->uuid,
|
||||
]);
|
||||
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, [
|
||||
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') . '"',
|
||||
'Content-Disposition' => 'inline; filename="'.addslashes(($document->original_name ?: $document->file_name).'.pdf').'"',
|
||||
'Cache-Control' => 'private, max-age=0, no-cache',
|
||||
'Pragma' => 'no-cache',
|
||||
]);
|
||||
@@ -358,17 +416,147 @@ public function viewDocument(ClientCase $clientCase, Document $document, Request
|
||||
|
||||
// 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'])) {
|
||||
if (in_array($ext, ['doc', 'docx'])) {
|
||||
\App\Jobs\GenerateDocumentPreview::dispatch($document->id);
|
||||
|
||||
return response('Preview is being generated. Please try again shortly.', 202);
|
||||
}
|
||||
|
||||
if (!Storage::disk($disk)->exists($document->path)) {
|
||||
// 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($document->path);
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -376,7 +564,7 @@ public function viewDocument(ClientCase $clientCase, Document $document, Request
|
||||
fpassthru($stream);
|
||||
}, 200, [
|
||||
'Content-Type' => $document->mime_type ?: 'application/octet-stream',
|
||||
'Content-Disposition' => 'inline; filename="' . addslashes($document->original_name ?: $document->file_name) . '"',
|
||||
'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',
|
||||
]);
|
||||
@@ -384,57 +572,418 @@ public function viewDocument(ClientCase $clientCase, Document $document, Request
|
||||
|
||||
public function downloadDocument(ClientCase $clientCase, Document $document, Request $request)
|
||||
{
|
||||
if ($document->documentable_type !== ClientCase::class || $document->documentable_id !== $clientCase->id) {
|
||||
$belongsToCase = $document->documentable_type === ClientCase::class && $document->documentable_id === $clientCase->id;
|
||||
$belongsToContractOfCase = false;
|
||||
if ($document->documentable_type === Contract::class) {
|
||||
$belongsToContractOfCase = Contract::withTrashed()
|
||||
->where('id', $document->documentable_id)
|
||||
->where('client_case_id', $clientCase->id)
|
||||
->exists();
|
||||
}
|
||||
if (! ($belongsToCase || $belongsToContractOfCase)) {
|
||||
logger()->warning('Document download 404: document does not belong to case or its contracts', [
|
||||
'document_id' => $document->id,
|
||||
'document_uuid' => $document->uuid,
|
||||
'documentable_type' => $document->documentable_type,
|
||||
'documentable_id' => $document->documentable_id,
|
||||
'client_case_id' => $clientCase->id,
|
||||
'client_case_uuid' => $clientCase->uuid,
|
||||
]);
|
||||
abort(404);
|
||||
}
|
||||
$disk = $document->disk ?: 'public';
|
||||
if (!Storage::disk($disk)->exists($document->path)) {
|
||||
// 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);
|
||||
}
|
||||
$name = $document->original_name ?: $document->file_name;
|
||||
$stream = Storage::disk($disk)->readStream($document->path);
|
||||
$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) . '"',
|
||||
'Content-Disposition' => 'attachment; filename="'.addslashes($name).'"',
|
||||
'Cache-Control' => 'private, max-age=0, no-cache',
|
||||
'Pragma' => 'no-cache',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View a contract document using contract route binding.
|
||||
*/
|
||||
public function viewContractDocument(Contract $contract, Document $document, Request $request)
|
||||
{
|
||||
// Ensure the document belongs to this contract (including trashed docs)
|
||||
$belongs = $document->documentable_type === Contract::class && $document->documentable_id === $contract->id;
|
||||
if (! $belongs) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// Reuse the existing logic by delegating to a small helper
|
||||
return $this->streamDocumentForDisk($document, inline: true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a contract document using contract route binding.
|
||||
*/
|
||||
public function downloadContractDocument(Contract $contract, Document $document, Request $request)
|
||||
{
|
||||
$belongs = $document->documentable_type === Contract::class && $document->documentable_id === $contract->id;
|
||||
if (! $belongs) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(ClientCase $clientCase)
|
||||
{
|
||||
$case = $clientCase::with([
|
||||
'person' => fn($que) => $que->with(['addresses', 'phones'])
|
||||
])->where('active', 1)->findOrFail($clientCase->id);
|
||||
'person' => fn ($que) => $que->with(['addresses', 'phones', 'emails', 'bankAccounts']),
|
||||
])->where('active', 1)->findOrFail($clientCase->id);
|
||||
|
||||
$types = [
|
||||
'address_types' => \App\Models\Person\AddressType::all(),
|
||||
'phone_types' => \App\Models\Person\PhoneType::all()
|
||||
'phone_types' => \App\Models\Person\PhoneType::all(),
|
||||
];
|
||||
|
||||
return Inertia::render('Cases/Show', [
|
||||
'client' => $case->client()->with('person', fn($q) => $q->with(['addresses', 'phones']))->firstOrFail(),
|
||||
// Prepare contracts and a reference map
|
||||
$contracts = $case->contracts()
|
||||
->with(['type', 'account', 'objects', 'segments:id,name'])
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
$contractRefMap = [];
|
||||
foreach ($contracts as $c) {
|
||||
$contractRefMap[$c->id] = $c->reference;
|
||||
}
|
||||
|
||||
// Merge client case and contract documents into a single array and include contract reference when applicable
|
||||
$contractIds = $contracts->pluck('id');
|
||||
$contractDocs = Document::query()
|
||||
->where('documentable_type', Contract::class)
|
||||
->whereIn('documentable_id', $contractIds)
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->map(function ($d) use ($contractRefMap) {
|
||||
$arr = $d->toArray();
|
||||
$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 = $d->toArray();
|
||||
$arr['documentable_type'] = ClientCase::class;
|
||||
$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', 'bankAccounts']))->firstOrFail(),
|
||||
'client_case' => $case,
|
||||
'contracts' => $case->contracts()
|
||||
->with(['type', 'account', 'objects', 'segments:id,name'])
|
||||
->orderByDesc('created_at')->get(),
|
||||
'activities' => $case->activities()->with(['action', 'decision', 'contract:id,uuid,reference'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20, ['*'], 'activities'),
|
||||
'documents' => $case->documents()->orderByDesc('created_at')->get(),
|
||||
'contracts' => $contracts,
|
||||
'activities' => tap(
|
||||
$case->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(20, ['*'], 'activities'),
|
||||
function ($p) {
|
||||
$p->getCollection()->transform(function ($a) {
|
||||
$a->setAttribute('user_name', optional($a->user)->name);
|
||||
return $a;
|
||||
});
|
||||
}
|
||||
),
|
||||
'documents' => $mergedDocs,
|
||||
'contract_types' => \App\Models\ContractType::whereNull('deleted_at')->get(),
|
||||
'account_types' => \App\Models\AccountType::all(),
|
||||
'actions' => \App\Models\Action::with('decisions')->get(),
|
||||
'types' => $types,
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id','segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id','name'])
|
||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
||||
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id', 'name']),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,62 +9,62 @@
|
||||
|
||||
class ClientController extends Controller
|
||||
{
|
||||
public function index(Client $client, Request $request){
|
||||
return Inertia::render('Client/Index',[
|
||||
public function index(Client $client, Request $request)
|
||||
{
|
||||
return Inertia::render('Client/Index', [
|
||||
'clients' => $client::query()
|
||||
->with('person')
|
||||
->when($request->input('search'), fn($que, $search) =>
|
||||
$que->whereHas(
|
||||
'person',
|
||||
fn($q) => $q->where('full_name', 'ilike', '%' . $search . '%')
|
||||
)
|
||||
->when($request->input('search'), fn ($que, $search) => $que->whereHas(
|
||||
'person',
|
||||
fn ($q) => $q->where('full_name', 'ilike', '%'.$search.'%')
|
||||
)
|
||||
)
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15)
|
||||
->withQueryString(),
|
||||
'filters' => $request->only(['search'])
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Client $client, Request $request) {
|
||||
public function show(Client $client, Request $request)
|
||||
{
|
||||
|
||||
$data = $client::query()
|
||||
->with(['person' => fn($que) => $que->with(['addresses','phones'])])
|
||||
->with(['person' => fn ($que) => $que->with(['addresses', 'phones', 'bankAccounts'])])
|
||||
->findOrFail($client->id);
|
||||
|
||||
$types = [
|
||||
'address_types' => \App\Models\Person\AddressType::all(),
|
||||
'phone_types' => \App\Models\Person\PhoneType::all()
|
||||
'phone_types' => \App\Models\Person\PhoneType::all(),
|
||||
];
|
||||
|
||||
return Inertia::render('Client/Show', [
|
||||
'client' => $data,
|
||||
'client_cases' => $data->clientCases()
|
||||
->with('person')
|
||||
->when($request->input('search'), fn($que, $search) =>
|
||||
$que->whereHas(
|
||||
'person',
|
||||
fn($q) => $q->where('full_name', 'ilike', '%' . $search . '%')
|
||||
)
|
||||
->when($request->input('search'), fn ($que, $search) => $que->whereHas(
|
||||
'person',
|
||||
fn ($q) => $q->where('full_name', 'ilike', '%'.$search.'%')
|
||||
)
|
||||
)
|
||||
->where('active', 1)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(15)
|
||||
->withQueryString(),
|
||||
'types' => $types,
|
||||
'filters' => $request->only(['search'])
|
||||
'filters' => $request->only(['search']),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
|
||||
DB::transaction(function() use ($request){
|
||||
DB::transaction(function () use ($request) {
|
||||
$address = $request->input('address');
|
||||
$phone = $request->input('phone');
|
||||
$person = \App\Models\Person\Person::create([
|
||||
'nu' => rand(100000,200000),
|
||||
'nu' => rand(100000, 200000),
|
||||
'first_name' => $request->input('first_name'),
|
||||
'last_name' => $request->input('last_name'),
|
||||
'full_name' => $request->input('full_name'),
|
||||
@@ -74,31 +74,32 @@ public function store(Request $request)
|
||||
'social_security_number' => $request->input('social_security_number'),
|
||||
'description' => $request->input('description'),
|
||||
'group_id' => 1,
|
||||
'type_id' => 2
|
||||
'type_id' => 2,
|
||||
]);
|
||||
|
||||
$person->addresses()->create([
|
||||
'address' => $address['address'],
|
||||
'country' => $address['country'],
|
||||
'type_id' => $address['type_id']
|
||||
'type_id' => $address['type_id'],
|
||||
]);
|
||||
|
||||
$person->phones()->create([
|
||||
'nu' => $phone['nu'],
|
||||
'country_code' => $phone['country_code'],
|
||||
'type_id' => $phone['type_id']
|
||||
'type_id' => $phone['type_id'],
|
||||
]);
|
||||
|
||||
$person->client()->create();
|
||||
});
|
||||
|
||||
//\App\Models\Person\PersonAddress::create($address);
|
||||
// \App\Models\Person\PersonAddress::create($address);
|
||||
|
||||
return to_route('client');
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function update(Client $client, Request $request) {
|
||||
public function update(Client $client, Request $request)
|
||||
{
|
||||
|
||||
return to_route('client.show', $client);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Activity;
|
||||
use App\Models\Contract;
|
||||
use App\Models\FieldJob;
|
||||
use App\Models\FieldJobSetting;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FieldJobController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$setting = FieldJobSetting::query()->latest('id')->first();
|
||||
// Only fetch contracts that are currently in either the primary segment
|
||||
// or the optional queue segment defined on the latest FieldJobSetting.
|
||||
$segmentIds = collect([
|
||||
optional($setting)->queue_segment_id,
|
||||
optional($setting)->segment_id,
|
||||
])->filter()->unique()->values();
|
||||
|
||||
$contracts = Contract::query()
|
||||
->with(['clientCase.person', 'type', 'account'])
|
||||
->when($segmentIds->isNotEmpty(), function ($q) use ($segmentIds) {
|
||||
$q->whereHas('segments', function ($sq) use ($segmentIds) {
|
||||
// Relation already filters on active pivots
|
||||
$sq->whereIn('segments.id', $segmentIds);
|
||||
});
|
||||
}, function ($q) {
|
||||
// No segments configured on FieldJobSetting -> return none
|
||||
$q->whereRaw('1 = 0');
|
||||
})
|
||||
->latest('id')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
// Build active assignment map keyed by contract uuid for quicker UI checks
|
||||
$assignments = collect();
|
||||
if ($contracts->isNotEmpty()) {
|
||||
$activeJobs = FieldJob::query()
|
||||
->whereIn('contract_id', $contracts->pluck('id'))
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->with(['assignedUser:id,name', 'user:id,name', 'contract:id,uuid'])
|
||||
->get();
|
||||
|
||||
$assignments = $activeJobs->mapWithKeys(function (FieldJob $job) {
|
||||
return [
|
||||
optional($job->contract)->uuid => [
|
||||
'assigned_to' => $job->assignedUser ? ['id' => $job->assignedUser->id, 'name' => $job->assignedUser->name] : null,
|
||||
'assigned_by' => $job->user ? ['id' => $job->user->id, 'name' => $job->user->name] : null,
|
||||
'assigned_at' => $job->assigned_at,
|
||||
],
|
||||
];
|
||||
})->filter();
|
||||
}
|
||||
|
||||
$users = User::query()->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return Inertia::render('FieldJob/Index', [
|
||||
'setting' => $setting,
|
||||
'contracts' => $contracts,
|
||||
'users' => $users,
|
||||
'assignments' => $assignments,
|
||||
]);
|
||||
}
|
||||
|
||||
public function assign(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'contract_uuid' => 'required|string|exists:contracts,uuid',
|
||||
'assigned_user_id' => 'required|integer|exists:users,id',
|
||||
]);
|
||||
|
||||
$setting = FieldJobSetting::query()->latest('id')->first();
|
||||
if (! $setting) {
|
||||
return back()->withErrors(['setting' => 'No Field Job Setting found. Create one in Settings → Field Job Settings.']);
|
||||
}
|
||||
|
||||
$contract = Contract::query()->where('uuid', $data['contract_uuid'])->firstOrFail();
|
||||
|
||||
$job = FieldJob::create([
|
||||
'field_job_setting_id' => $setting->id,
|
||||
'assigned_user_id' => $data['assigned_user_id'],
|
||||
'contract_id' => $contract->id,
|
||||
'assigned_at' => now(),
|
||||
]);
|
||||
|
||||
// Create an activity for the assignment
|
||||
// Find the first action linked to the assign decision via pivot; also prefer actions within the same segment as the setting
|
||||
$decisionId = $setting->assign_decision_id;
|
||||
$actionId = null;
|
||||
if ($decisionId) {
|
||||
// Strictly use the action_decision pivot: take the first action mapped to this decision
|
||||
$actionId = DB::table('action_decision')
|
||||
->where('decision_id', $decisionId)
|
||||
->orderBy('id')
|
||||
->value('action_id');
|
||||
}
|
||||
|
||||
if ($actionId) {
|
||||
$assigneeName = User::query()->where('id', $data['assigned_user_id'])->value('name');
|
||||
// Localized note: "Terensko opravilo dodeljeno" + assignee when present
|
||||
$note = 'Terensko opravilo dodeljeno'.($assigneeName ? ' uporabniku '.$assigneeName : '');
|
||||
Activity::create([
|
||||
'due_date' => null,
|
||||
'amount' => null,
|
||||
'note' => $note,
|
||||
'action_id' => $actionId,
|
||||
'decision_id' => $decisionId,
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
'contract_id' => $contract->id,
|
||||
]);
|
||||
}
|
||||
|
||||
return back()->with('success', 'Field job assigned.');
|
||||
}
|
||||
|
||||
public function cancel(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'contract_uuid' => 'required|string|exists:contracts,uuid',
|
||||
]);
|
||||
|
||||
$contract = Contract::query()->where('uuid', $data['contract_uuid'])->firstOrFail();
|
||||
|
||||
$job = FieldJob::query()
|
||||
->where('contract_id', $contract->id)
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($job) {
|
||||
$job->cancelled_at = now();
|
||||
$job->save();
|
||||
|
||||
// Create an activity for the cancellation, mirroring the assign flow
|
||||
// Prefer the job's setting for a consistent decision
|
||||
$job->loadMissing('setting');
|
||||
$decisionId = optional($job->setting)->cancel_decision_id;
|
||||
if ($decisionId) {
|
||||
$actionId = DB::table('action_decision')
|
||||
->where('decision_id', $decisionId)
|
||||
->orderBy('id')
|
||||
->value('action_id');
|
||||
|
||||
if ($actionId) {
|
||||
Activity::create([
|
||||
'due_date' => null,
|
||||
'amount' => null,
|
||||
'note' => 'Terensko opravilo preklicano',
|
||||
'action_id' => $actionId,
|
||||
'decision_id' => $decisionId,
|
||||
'client_case_id' => $contract->client_case_id,
|
||||
'contract_id' => $contract->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return back()->with('success', 'Field job cancelled.');
|
||||
}
|
||||
|
||||
public function complete(Request $request, \App\Models\ClientCase $clientCase)
|
||||
{
|
||||
// Complete all active field jobs for contracts of this case assigned to current user
|
||||
$userId = optional($request->user())->id;
|
||||
$setting = FieldJobSetting::query()->latest('id')->first();
|
||||
if (! $setting) {
|
||||
return back()->withErrors(['setting' => 'No Field Job Setting found.']);
|
||||
}
|
||||
|
||||
$decisionId = $setting->complete_decision_id;
|
||||
$actionId = null;
|
||||
if ($decisionId) {
|
||||
$actionId = DB::table('action_decision')
|
||||
->where('decision_id', $decisionId)
|
||||
->orderBy('id')
|
||||
->value('action_id');
|
||||
}
|
||||
|
||||
// Find all active jobs for this case for the current user
|
||||
$jobs = FieldJob::query()
|
||||
->whereHas('contract', function ($q) use ($clientCase) {
|
||||
$q->where('client_case_id', $clientCase->id);
|
||||
})
|
||||
->where(function ($q) use ($userId) {
|
||||
if ($userId) {
|
||||
$q->where('assigned_user_id', $userId);
|
||||
}
|
||||
})
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->with(['contract:id,client_case_id', 'setting'])
|
||||
->get();
|
||||
|
||||
DB::transaction(function () use ($jobs, $decisionId, $actionId) {
|
||||
foreach ($jobs as $job) {
|
||||
// Mark job complete
|
||||
$job->completed_at = now();
|
||||
$job->save();
|
||||
|
||||
// Log completion activity on the contract/case
|
||||
if ($actionId && $decisionId) {
|
||||
Activity::create([
|
||||
'due_date' => null,
|
||||
'amount' => null,
|
||||
'note' => 'Terensko opravilo zaključeno',
|
||||
'action_id' => $actionId,
|
||||
'decision_id' => $decisionId,
|
||||
'client_case_id' => $job->contract->client_case_id,
|
||||
'contract_id' => $job->contract_id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Move contract to configured return segment
|
||||
$job->returnContractToConfiguredSegment();
|
||||
}
|
||||
});
|
||||
|
||||
// Redirect back to phone index
|
||||
return to_route('phone.index');
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreFieldJobSettingRequest;
|
||||
use App\Http\Requests\UpdateFieldJobSettingRequest;
|
||||
use App\Models\Decision;
|
||||
use App\Models\FieldJobSetting;
|
||||
use App\Models\Segment;
|
||||
use App\Models\Decision;
|
||||
use App\Http\Requests\StoreFieldJobSettingRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
@@ -14,7 +15,7 @@ class FieldJobSettingController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$settings = FieldJobSetting::query()
|
||||
->with(['segment', 'initialDecision', 'asignDecision', 'completeDecision'])
|
||||
->with(['segment', 'initialDecision', 'assignDecision', 'completeDecision', 'cancelDecision', 'returnSegment', 'queueSegment'])
|
||||
->get();
|
||||
|
||||
return Inertia::render('Settings/FieldJob/Index', [
|
||||
@@ -31,10 +32,30 @@ public function store(StoreFieldJobSettingRequest $request)
|
||||
FieldJobSetting::create([
|
||||
'segment_id' => $attributes['segment_id'],
|
||||
'initial_decision_id' => $attributes['initial_decision_id'],
|
||||
'asign_decision_id' => $attributes['asign_decision_id'],
|
||||
'assign_decision_id' => $attributes['assign_decision_id'],
|
||||
'complete_decision_id' => $attributes['complete_decision_id'],
|
||||
'cancel_decision_id' => $attributes['cancel_decision_id'] ?? null,
|
||||
'return_segment_id' => $attributes['return_segment_id'] ?? null,
|
||||
'queue_segment_id' => $attributes['queue_segment_id'] ?? null,
|
||||
]);
|
||||
|
||||
return to_route('settings.fieldjob.index')->with('success', 'Field job setting created successfully!');
|
||||
}
|
||||
|
||||
public function update(FieldJobSetting $setting, UpdateFieldJobSettingRequest $request)
|
||||
{
|
||||
$attributes = $request->validated();
|
||||
|
||||
$setting->update([
|
||||
'segment_id' => $attributes['segment_id'],
|
||||
'initial_decision_id' => $attributes['initial_decision_id'],
|
||||
'assign_decision_id' => $attributes['assign_decision_id'],
|
||||
'complete_decision_id' => $attributes['complete_decision_id'],
|
||||
'cancel_decision_id' => $attributes['cancel_decision_id'] ?? null,
|
||||
'return_segment_id' => $attributes['return_segment_id'] ?? null,
|
||||
'queue_segment_id' => $attributes['queue_segment_id'] ?? null,
|
||||
]);
|
||||
|
||||
return to_route('settings.fieldjob.index')->with('success', 'Field job setting updated successfully!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\FieldJob;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class PhoneViewController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
|
||||
$jobs = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->with([
|
||||
'contract' => function ($q) {
|
||||
$q->with(['type:id,name', 'account', 'clientCase.person' => function ($pq) {
|
||||
$pq->with(['addresses', 'phones']);
|
||||
}]);
|
||||
},
|
||||
])
|
||||
->orderByDesc('assigned_at')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return Inertia::render('Phone/Index', [
|
||||
'jobs' => $jobs,
|
||||
]);
|
||||
}
|
||||
|
||||
public function showCase(\App\Models\ClientCase $clientCase, Request $request)
|
||||
{
|
||||
$userId = $request->user()->id;
|
||||
|
||||
// Eager load client case with person details
|
||||
$case = \App\Models\ClientCase::query()
|
||||
->with(['person' => fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts'])])
|
||||
->findOrFail($clientCase->id);
|
||||
|
||||
// Determine contracts of this case assigned to the current user via FieldJobs and still active
|
||||
$assignedContractIds = FieldJob::query()
|
||||
->where('assigned_user_id', $userId)
|
||||
->whereNull('completed_at')
|
||||
->whereNull('cancelled_at')
|
||||
->whereHas('contract', fn ($q) => $q->where('client_case_id', $case->id))
|
||||
->pluck('contract_id')
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$contracts = \App\Models\Contract::query()
|
||||
->where('client_case_id', $case->id)
|
||||
->whereIn('id', $assignedContractIds)
|
||||
->with(['type:id,name', 'account'])
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
// Attach latest object (if any) to each contract as last_object for display
|
||||
if ($contracts->isNotEmpty()) {
|
||||
$byId = $contracts->keyBy('id');
|
||||
$latestObjects = \App\Models\CaseObject::query()
|
||||
->whereIn('contract_id', $byId->keys())
|
||||
->whereNull('deleted_at')
|
||||
->select('id', 'reference', 'name', 'description', 'type', 'contract_id', 'created_at')
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->groupBy('contract_id')
|
||||
->map(function ($group) {
|
||||
return $group->first();
|
||||
});
|
||||
|
||||
foreach ($latestObjects as $cid => $obj) {
|
||||
if (isset($byId[$cid])) {
|
||||
$byId[$cid]->setAttribute('last_object', $obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build merged documents: case documents + documents of assigned contracts
|
||||
$contractRefMap = [];
|
||||
foreach ($contracts as $c) {
|
||||
$contractRefMap[$c->id] = $c->reference;
|
||||
}
|
||||
|
||||
$contractDocs = \App\Models\Document::query()
|
||||
->where('documentable_type', \App\Models\Contract::class)
|
||||
->whereIn('documentable_id', $assignedContractIds)
|
||||
->orderByDesc('created_at')
|
||||
->get()
|
||||
->map(function ($d) use ($contractRefMap) {
|
||||
$arr = $d->toArray();
|
||||
$arr['contract_reference'] = $contractRefMap[$d->documentable_id] ?? null;
|
||||
$arr['documentable_type'] = \App\Models\Contract::class;
|
||||
$arr['contract_uuid'] = optional(\App\Models\Contract::withTrashed()->find($d->documentable_id))->uuid;
|
||||
|
||||
return $arr;
|
||||
});
|
||||
|
||||
$caseDocs = $case->documents()->orderByDesc('created_at')->get()->map(function ($d) use ($case) {
|
||||
$arr = $d->toArray();
|
||||
$arr['documentable_type'] = \App\Models\ClientCase::class;
|
||||
$arr['client_case_uuid'] = $case->uuid;
|
||||
|
||||
return $arr;
|
||||
});
|
||||
|
||||
$documents = $caseDocs->concat($contractDocs)->sortByDesc('created_at')->values();
|
||||
|
||||
// Provide minimal types for PersonInfoGrid
|
||||
$types = [
|
||||
'address_types' => \App\Models\Person\AddressType::all(),
|
||||
'phone_types' => \App\Models\Person\PhoneType::all(),
|
||||
];
|
||||
|
||||
// Case activities (compact for phone): latest 20 with relations
|
||||
$activities = $case->activities()
|
||||
->with(['action', 'decision', 'contract:id,uuid,reference', 'user:id,name'])
|
||||
->orderByDesc('created_at')
|
||||
->limit(20)
|
||||
->get()
|
||||
->map(function ($a) {
|
||||
$a->setAttribute('user_name', optional($a->user)->name);
|
||||
|
||||
return $a;
|
||||
});
|
||||
|
||||
return Inertia::render('Phone/Case/Index', [
|
||||
'client' => $case->client()->with('person', fn ($q) => $q->with(['addresses', 'phones', 'emails', 'bankAccounts']))->firstOrFail(),
|
||||
'client_case' => $case,
|
||||
'contracts' => $contracts,
|
||||
'documents' => $documents,
|
||||
'types' => $types,
|
||||
'account_types' => \App\Models\AccountType::all(),
|
||||
'actions' => \App\Models\Action::with('decisions')->get(),
|
||||
'activities' => $activities,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public function rules(): array
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'initial_amount' => ['nullable', 'numeric'],
|
||||
'balance_amount' => ['nullable', 'numeric'],
|
||||
'account_type_id' => ['nullable', 'integer', 'exists:account_types,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StoreFieldJobSettingRequest extends FormRequest
|
||||
{
|
||||
@@ -16,8 +17,11 @@ public function rules(): array
|
||||
return [
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'initial_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'asign_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'assign_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'complete_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'cancel_decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||
'return_segment_id' => ['nullable', 'integer', 'exists:segments,id'],
|
||||
'queue_segment_id' => ['nullable', 'integer', 'exists:segments,id'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -26,8 +30,51 @@ public function messages(): array
|
||||
return [
|
||||
'segment_id.required' => 'Segment is required.',
|
||||
'initial_decision_id.required' => 'Initial decision is required.',
|
||||
'asign_decision_id.required' => 'Assign decision is required.',
|
||||
'assign_decision_id.required' => 'Assign decision is required.',
|
||||
'complete_decision_id.required' => 'Complete decision is required.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
// Validate that the assign_decision_id has a mapped action
|
||||
$assignDecisionId = $this->input('assign_decision_id');
|
||||
if (! empty($assignDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $assignDecisionId)
|
||||
->exists();
|
||||
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('assign_decision_id', 'The selected assign decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that the complete_decision_id has a mapped action
|
||||
$completeDecisionId = $this->input('complete_decision_id');
|
||||
if (! empty($completeDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $completeDecisionId)
|
||||
->exists();
|
||||
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('complete_decision_id', 'The selected complete decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
|
||||
$cancelDecisionId = $this->input('cancel_decision_id');
|
||||
if (! empty($cancelDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $cancelDecisionId)
|
||||
->exists();
|
||||
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('cancel_decision_id', 'The selected cancel decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public function rules(): array
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'initial_amount' => ['nullable', 'numeric'],
|
||||
'balance_amount' => ['nullable', 'numeric'],
|
||||
'account_type_id' => ['sometimes', 'nullable', 'integer', 'exists:account_types,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateFieldJobSettingRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'segment_id' => ['required', 'integer', 'exists:segments,id'],
|
||||
'initial_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'assign_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'complete_decision_id' => ['required', 'integer', 'exists:decisions,id'],
|
||||
'cancel_decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
||||
'return_segment_id' => ['nullable', 'integer', 'exists:segments,id'],
|
||||
'queue_segment_id' => ['nullable', 'integer', 'exists:segments,id'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'segment_id.required' => 'Segment is required.',
|
||||
'initial_decision_id.required' => 'Initial decision is required.',
|
||||
'assign_decision_id.required' => 'Assign decision is required.',
|
||||
'complete_decision_id.required' => 'Complete decision is required.',
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator($validator): void
|
||||
{
|
||||
$validator->after(function ($validator): void {
|
||||
$assignDecisionId = $this->input('assign_decision_id');
|
||||
if (! empty($assignDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $assignDecisionId)
|
||||
->exists();
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('assign_decision_id', 'The selected assign decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
|
||||
$completeDecisionId = $this->input('complete_decision_id');
|
||||
if (! empty($completeDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $completeDecisionId)
|
||||
->exists();
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('complete_decision_id', 'The selected complete decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
|
||||
$cancelDecisionId = $this->input('cancel_decision_id');
|
||||
if (! empty($cancelDecisionId)) {
|
||||
$mapped = DB::table('action_decision')
|
||||
->where('decision_id', $cancelDecisionId)
|
||||
->exists();
|
||||
if (! $mapped) {
|
||||
$validator->errors()->add('cancel_decision_id', 'The selected cancel decision must have a mapped action. Please map an action to this decision first.');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user