Changes to import and notifications

This commit is contained in:
Simon Pocrnjič
2025-10-13 21:14:10 +02:00
parent 0bbed64542
commit 79b3e20b02
28 changed files with 2173 additions and 438 deletions
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use App\Models\ActivityNotificationRead;
use Illuminate\Http\Request;
class ActivityNotificationController extends Controller
{
/**
* Handle the incoming request.
*/
public function __invoke(Request $request)
{
$request->validate([
'activity_id' => ['required', 'integer', 'exists:activities,id'],
]);
$userId = optional($request->user())->id;
if (! $userId) {
abort(403);
}
$activity = Activity::query()->select(['id', 'due_date'])->findOrFail($request->integer('activity_id'));
$due = optional($activity->due_date) ? date('Y-m-d', strtotime($activity->due_date)) : now()->toDateString();
ActivityNotificationRead::query()->updateOrCreate(
[
'user_id' => $userId,
'activity_id' => $activity->id,
'due_date' => $due,
],
[
'read_at' => now(),
]
);
return response()->json(['status' => 'ok']);
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ public function index(ClientCase $clientCase, Request $request)
return Inertia::render('Cases/Index', [
'client_cases' => $query
->paginate(15, ['*'], 'client-cases-page')
->paginate($request->integer('perPage', 15), ['*'], 'client-cases-page')
->withQueryString(),
'filters' => $request->only(['search']),
]);
+3 -3
View File
@@ -53,7 +53,7 @@ public function index(Client $client, Request $request)
return Inertia::render('Client/Index', [
'clients' => $query
->paginate(15)
->paginate($request->integer('perPage', 15))
->withQueryString(),
'filters' => $request->only(['search']),
]);
@@ -104,7 +104,7 @@ public function show(Client $client, Request $request)
])
->where('active', 1)
->orderByDesc('created_at')
->paginate(15)
->paginate($request->integer('perPage', 15))
->withQueryString(),
'types' => $types,
'filters' => $request->only(['search']),
@@ -158,7 +158,7 @@ public function contracts(Client $client, Request $request)
return Inertia::render('Client/Contracts', [
'client' => $data,
'contracts' => $contractsQuery->paginate(20)->withQueryString(),
'contracts' => $contractsQuery->paginate($request->integer('perPage', 20))->withQueryString(),
'filters' => $request->only(['from', 'to', 'search']),
'types' => $types,
]);
+114
View File
@@ -146,6 +146,7 @@ public function store(Request $request)
'size' => $file->getSize(),
'sheet_name' => $validated['sheet_name'] ?? null,
'status' => 'uploaded',
'show_missing' => false,
'meta' => [
'has_header' => $validated['has_header'] ?? true,
],
@@ -155,6 +156,7 @@ public function store(Request $request)
'id' => $import->id,
'uuid' => $import->uuid,
'status' => $import->status,
'show_missing' => (bool) ($import->show_missing ?? false),
]);
}
@@ -354,6 +356,116 @@ public function getMappings(Import $import)
return response()->json(['mappings' => $rows]);
}
/**
* List active, non-archived contracts for the import's client that are NOT present
* in the processed import file (based on mapped contract.reference values).
* Only available when contract.reference mapping apply_mode is 'keyref'.
*/
public function missingContracts(Import $import)
{
// Ensure client context is available
if (empty($import->client_id)) {
return response()->json(['error' => 'Import has no client bound.'], 422);
}
// Respect optional feature flag on import
if (! (bool) ($import->show_missing ?? false)) {
return response()->json(['error' => 'Missing contracts listing is disabled for this import.'], 422);
}
// Check that this import's mappings set contract.reference to keyref mode
$mappings = \DB::table('import_mappings')
->where('import_id', $import->id)
->get(['target_field', 'apply_mode']);
$isKeyref = false;
foreach ($mappings as $map) {
$tf = strtolower((string) ($map->target_field ?? ''));
$am = strtolower((string) ($map->apply_mode ?? ''));
if (in_array($tf, ['contract.reference', 'contracts.reference'], true) && $am === 'keyref') {
$isKeyref = true;
break;
}
}
if (! $isKeyref) {
return response()->json(['error' => 'Missing contracts are only available for keyref mapping on contract.reference.'], 422);
}
// Collect referenced contract references from processed rows
$present = [];
foreach (\App\Models\ImportRow::query()->where('import_id', $import->id)->get(['mapped_data']) as $row) {
$md = $row->mapped_data ?? [];
if (is_array($md) && isset($md['contract']['reference'])) {
$ref = (string) $md['contract']['reference'];
if ($ref !== '') {
$present[] = preg_replace('/\s+/', '', trim($ref));
}
}
}
$present = array_values(array_unique(array_filter($present)));
// Query active, non-archived contracts for this client that were not in import
// Include person full_name (owner of the client case) and aggregate active accounts' balance_amount
$contractsQ = \App\Models\Contract::query()
->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
->join('person', 'person.id', '=', 'client_cases.person_id')
->leftJoin('accounts', function ($join) {
$join->on('accounts.contract_id', '=', 'contracts.id')
->where('accounts.active', 1);
})
->where('client_cases.client_id', $import->client_id)
->where('contracts.active', 1)
->whereNull('contracts.deleted_at')
->when(count($present) > 0, function ($q) use ($present) {
$q->whereNotIn('contracts.reference', $present);
})
->groupBy('contracts.uuid', 'contracts.reference', 'client_cases.uuid', 'person.full_name')
->orderBy('contracts.reference')
->get([
'contracts.uuid as uuid',
'contracts.reference as reference',
'client_cases.uuid as case_uuid',
'person.full_name as full_name',
\DB::raw('COALESCE(SUM(accounts.balance_amount), 0) as balance_amount'),
]);
return response()->json([
'missing' => $contractsQ,
'count' => $contractsQ->count(),
]);
}
/**
* Update import options (e.g., booleans like show_missing, reactivate) from the UI.
*/
public function updateOptions(Request $request, Import $import)
{
$data = $request->validate([
'show_missing' => 'nullable|boolean',
'reactivate' => 'nullable|boolean',
]);
$payload = [];
if (array_key_exists('show_missing', $data)) {
$payload['show_missing'] = (bool) $data['show_missing'];
}
if (array_key_exists('reactivate', $data)) {
$payload['reactivate'] = (bool) $data['reactivate'];
}
if (! empty($payload)) {
$import->update($payload);
}
return response()->json([
'ok' => true,
'import' => [
'id' => $import->id,
'uuid' => $import->uuid,
'show_missing' => (bool) ($import->show_missing ?? false),
'reactivate' => (bool) ($import->reactivate ?? false),
],
]);
}
// Fetch recent import events (logs) for an import
public function getEvents(Import $import)
{
@@ -533,6 +645,8 @@ public function show(Import $import)
'client_id' => $import->client_id,
'client_uuid' => optional($client)->uuid,
'import_template_id' => $import->import_template_id,
'show_missing' => (bool) ($import->show_missing ?? false),
'reactivate' => (bool) ($import->reactivate ?? false),
'total_rows' => $import->total_rows,
'imported_rows' => $import->imported_rows,
'invalid_rows' => $import->invalid_rows,
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers;
use App\Models\Activity;
use Illuminate\Http\Request;
use Inertia\Inertia;
class NotificationController extends Controller
{
public function unread(Request $request)
{
$user = $request->user();
if (! $user) {
abort(403);
}
$today = now()->toDateString();
$perPage = max(1, min(100, (int) $request->integer('perPage', 15)));
$search = trim((string) $request->input('search', ''));
$query = Activity::query()
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
->whereNotNull('due_date')
->whereDate('due_date', '<=', $today)
->whereNotExists(function ($q) use ($user) {
$q->from('activity_notification_reads as anr')
->whereColumn('anr.activity_id', 'activities.id')
->where('anr.user_id', $user->id)
->whereColumn('anr.due_date', 'activities.due_date');
})
// allow simple search by contract reference or person name
->when($search !== '', function ($q) use ($search) {
$s = mb_strtolower($search);
$q->leftJoin('contracts', 'contracts.id', '=', 'activities.contract_id')
->leftJoin('client_cases', 'client_cases.id', '=', 'activities.client_case_id')
->leftJoin('person', 'person.id', '=', 'client_cases.person_id')
->where(function ($qq) use ($s) {
$qq->whereRaw('LOWER(COALESCE(contracts.reference, \'\')) LIKE ?', ['%'.$s.'%'])
->orWhereRaw('LOWER(COALESCE(person.full_name, \'\')) LIKE ?', ['%'.$s.'%']);
});
})
->with([
'contract' => function ($q) {
$q->select(['contracts.id', 'contracts.uuid', 'contracts.reference', 'contracts.client_case_id'])
->with([
'clientCase' => function ($qq) {
$qq->select(['client_cases.id', 'client_cases.uuid']);
},
'account' => function ($qq) {
$qq->select(['accounts.id', 'accounts.contract_id', 'accounts.balance_amount', 'accounts.initial_amount']);
},
]);
},
'clientCase' => function ($q) {
$q->select(['client_cases.id', 'client_cases.uuid', 'client_cases.person_id'])
->with([
'person' => function ($qq) {
$qq->select(['person.id', 'person.full_name']);
},
]);
},
])
// force ordering by due_date DESC only
->orderByDesc('activities.due_date');
// Use a custom page parameter name to match the frontend DataTableServer
$activities = $query->paginate($perPage, ['*'], 'unread-page')->withQueryString();
return Inertia::render('Notifications/Unread', [
'activities' => $activities,
'today' => $today,
]);
}
}
+32 -9
View File
@@ -66,21 +66,44 @@ public function share(Request $request): array
}
$today = now()->toDateString();
// Base fetch to avoid serialization issues; eager load relations afterwards
$activities = \App\Models\Activity::query()
->with([
// Include contract uuid and reference, keep id for relation mapping, and client_case_id for nested eager load
'contract:id,uuid,reference,client_case_id',
// Include client case uuid (id required for mapping, will be hidden in JSON)
'contract.clientCase:id,uuid',
// Include account amounts; contract_id needed for relation mapping
'contract.account:contract_id,balance_amount,initial_amount',
])
->select(['id', 'due_date', 'amount', 'contract_id', 'client_case_id', 'created_at'])
->whereDate('due_date', $today)
->where('user_id', $user->id)
->whereNotExists(function ($q) use ($request) {
$q->from('activity_notification_reads as anr')
->whereColumn('anr.activity_id', 'activities.id')
->where('anr.user_id', optional($request->user())->id)
->whereColumn('anr.due_date', 'activities.due_date');
})
->orderBy('created_at')
->limit(20)
->get();
// Eager load needed relations (contracts and client cases) with qualified selects
$activities->load([
'contract' => function ($q) {
$q->select(['contracts.id', 'contracts.uuid', 'contracts.reference', 'contracts.client_case_id'])
->with([
'clientCase' => function ($qq) {
$qq->select(['client_cases.id', 'client_cases.uuid']);
},
'account' => function ($qq) {
$qq->select(['accounts.id', 'accounts.contract_id', 'accounts.balance_amount', 'accounts.initial_amount']);
},
]);
},
'clientCase' => function ($q) {
$q->select(['client_cases.id', 'client_cases.uuid', 'client_cases.person_id'])
->with([
'person' => function ($qq) {
$qq->select(['person.id', 'person.full_name']);
},
]);
},
]);
return [
'dueToday' => [
'count' => $activities->count(),
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ActivityNotificationRead extends Model
{
//
protected $fillable = [
'user_id', 'activity_id', 'due_date', 'read_at',
];
protected $casts = [
'due_date' => 'date',
'read_at' => 'datetime',
];
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
public function activity(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Activity::class);
}
}
+2 -1
View File
@@ -12,7 +12,7 @@ class Import extends Model
use HasFactory;
protected $fillable = [
'uuid', 'user_id', 'import_template_id', 'client_id', 'source_type', 'file_name', 'original_name', 'disk', 'path', 'size', 'sheet_name', 'status', 'reactivate', 'total_rows', 'valid_rows', 'invalid_rows', 'imported_rows', 'started_at', 'finished_at', 'failed_at', 'error_summary', 'meta',
'uuid', 'user_id', 'import_template_id', 'client_id', 'source_type', 'file_name', 'original_name', 'disk', 'path', 'size', 'sheet_name', 'status', 'reactivate', 'show_missing', 'total_rows', 'valid_rows', 'invalid_rows', 'imported_rows', 'started_at', 'finished_at', 'failed_at', 'error_summary', 'meta',
];
protected $casts = [
@@ -22,6 +22,7 @@ class Import extends Model
'finished_at' => 'datetime',
'failed_at' => 'datetime',
'reactivate' => 'boolean',
'show_missing' => 'boolean',
];
public function user(): BelongsTo
+51 -3
View File
@@ -499,7 +499,7 @@ public function process(Import $import, ?Authenticatable $user = null): array
$contractKeyMode = $tplMeta['contract_key_mode'] ?? null;
if (! $accountIdForPayment && $paymentsImport && $contractKeyMode === 'reference') {
$contractRef = $mapped['contract']['reference'] ?? null;
if ($contractRef) {
if (! $contractId) {
$contract = \App\Models\Contract::query()
->when($import->client_id, function ($q, $clientId) {
$q->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
@@ -508,9 +508,25 @@ public function process(Import $import, ?Authenticatable $user = null): array
->where('contracts.reference', $contractRef)
->select('contracts.id')
->first();
if ($contract) {
$accountIdForPayment = \App\Models\Account::where('contract_id', $contract->id)->value('id');
} elseif ($hasContractRoot) {
// If mapping for contract.reference is keyref, do NOT create contract lookup only
$refMode = $this->mappingMode($mappings, 'contract.reference');
if ($refMode === 'keyref') {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => null,
'event' => 'row_skipped',
'level' => 'warning',
'message' => 'Contract reference '.$contractRef.' does not exist (keyref); row skipped.',
]);
return [
'action' => 'skipped',
'message' => 'contract.reference keyref lookup failed: not found',
];
}
/* Lines 1242-1269 omitted */
$contractId = $createdContract->id;
}
}
@@ -1583,6 +1599,20 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
}
}
// If contract not found and contract.reference is keyref, skip without creating entities
$refMode = $this->mappingMode($mappings, 'contract.reference');
if (! $existing && $refMode === 'keyref') {
ImportEvent::create([
'import_id' => $import->id,
'user_id' => null,
'event' => 'row_skipped',
'level' => 'warning',
'message' => 'Contract reference '.$reference.' does not exist (keyref); row skipped.',
]);
return ['action' => 'skipped', 'message' => 'contract.reference keyref lookup failed: not found'];
}
if ($existing) {
// 1) Prepare contract field changes (non-null)
$changes = array_filter($applyUpdate, fn ($v) => ! is_null($v));
@@ -2040,6 +2070,24 @@ private function normalizeTargetField(string $target, array $rootAliasMap, array
return $bracket !== null ? ($root.'['.$bracket.']') : $root;
}
/**
* Get apply mode for a specific mapping target field, normalized via normalizeMappings beforehand.
* Returns lowercased mode string like 'insert', 'update', 'both', 'keyref', or null if not present.
*/
private function mappingMode($mappings, string $targetField): ?string
{
foreach ($mappings as $map) {
$target = (string) ($map->target_field ?? '');
if ($target === $targetField) {
$mode = $map->apply_mode ?? null;
return is_string($mode) ? strtolower($mode) : null;
}
}
return null;
}
protected function loadImportEntityConfig(): array
{
$entities = ImportEntity::all();
+130 -8
View File
@@ -20,6 +20,11 @@
*/
class ImportSimulationService
{
/**
* Optional client scoping for lookups during a simulation run.
*/
private ?int $clientId = null;
/**
* Public entry: simulate import applying mappings to first $limit rows.
* Keeps existing machine keys for backward compatibility, but adds Slovenian
@@ -27,6 +32,8 @@ class ImportSimulationService
*/
public function simulate(Import $import, int $limit = 100, bool $verbose = false): array
{
// Store client context for the duration of this simulation
$this->clientId = $import->client_id ?: null;
$meta = $import->meta ?? [];
$hasHeader = (bool) ($meta['has_header'] ?? true);
$delimiter = $meta['forced_delimiter'] ?? $meta['detected_delimiter'] ?? ',';
@@ -70,9 +77,14 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
$translatedStatuses = $this->statusTranslations();
$simRows = [];
// Determine keyref behavior for contract.reference from mappings/template
$tplMeta = optional($import->template)->meta ?? [];
$contractKeyModeTpl = $tplMeta['contract_key_mode'] ?? null; // e.g. 'reference'
$contractRefMode = $this->mappingModeForImport($import, 'contract.reference'); // e.g. 'keyref'
foreach ($rows as $idx => $rawValues) {
$assoc = $this->associateRow($columns, $rawValues);
$rowEntities = [];
$keyrefSkipRow = false; // if true, downstream creations are skipped for this row
// Reactivation intent detection (row > import > template)
$rowReactivate = false;
@@ -139,6 +151,24 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
$contractEntity['action'] = 'reactivate';
$contractEntity['reactivation'] = true;
}
// Keyref enforcement: if mapping is keyref (or template says reference) and contract doesn't exist, skip row creations
$ref = $contractEntity['reference'] ?? null;
if (($contractRefMode === 'keyref' || $contractKeyModeTpl === 'reference')
&& ($contractEntity['action'] === 'create')
) {
// Adjust summaries: revert create -> invalid
if (isset($summaries['contract'])) {
if (($summaries['contract']['create'] ?? 0) > 0) {
$summaries['contract']['create']--;
}
$summaries['contract']['invalid'] = ($summaries['contract']['invalid'] ?? 0) + 1;
}
$contractEntity['original_action'] = 'create';
$contractEntity['action'] = 'skip';
$contractEntity['warning'] = 'Contract reference '.(string) $ref.' does not exist (keyref); row skipped.';
$contractEntity['skipped_due_to_keyref'] = true;
$keyrefSkipRow = true;
}
// Attach contract meta preview from mappings (group-aware)
$metaGroups = [];
// Grouped contract.meta.* via groupedLookup
@@ -199,6 +229,19 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
}
}
[$accountEntity, $summaries, $accountCache] = $this->simulateAccount($val, $summaries, $accountCache, $rawAccountRef);
// If row is being skipped due to keyref missing contract, do not create accounts
if ($keyrefSkipRow && ($accountEntity['action'] ?? null) === 'create') {
if (isset($summaries['account'])) {
if (($summaries['account']['create'] ?? 0) > 0) {
$summaries['account']['create']--;
}
$summaries['account']['invalid'] = ($summaries['account']['invalid'] ?? 0) + 1;
}
$accountEntity['original_action'] = 'create';
$accountEntity['action'] = 'skip';
$accountEntity['warning'] = 'Skipped due to missing contract.reference in keyref mode.';
$accountEntity['skipped_due_to_keyref'] = true;
}
if ($inherited) {
$accountEntity['inherited_reference'] = true;
}
@@ -212,6 +255,10 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
// Generic roots (person, address, email, phone, client_case, etc.) excluding already handled ones
foreach (array_keys($entityRoots) as $rootKey) {
// If keyref skip is active for this row, suppress downstream creations (person, client_case, etc.)
if ($keyrefSkipRow && ! in_array($rootKey, ['contract', 'account', 'payment'], true)) {
continue;
}
if (in_array($rootKey, ['contract', 'account', 'payment'], true)) {
continue; // already simulated explicitly
}
@@ -220,6 +267,16 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
if ($existingContract && in_array($rootKey, ['person', 'client_case'], true)) {
continue;
}
// Special-case: when contract exists and email/phone/address mapping uses the same column as contract.reference,
// treat it as a root declaration only and defer to chain attachments instead of generic simulation.
if ($existingContract && in_array($rootKey, ['email', 'phone', 'address'], true)) {
$crSrc = $targetToSource['contract.reference'] ?? null;
$rk = $rootKey === 'email' ? 'email.value' : ($rootKey === 'phone' ? 'phone.nu' : 'address.address');
$rkSrc = $targetToSource[$rk] ?? null;
if ($crSrc !== null && $rkSrc !== null && $crSrc === $rkSrc) {
continue;
}
}
$reference = $val($rootKey.'.reference');
$identityCandidates = $this->genericIdentityCandidates($rootKey, $val);
if (isset($multiRoots[$rootKey]) && $multiRoots[$rootKey] === true) {
@@ -237,12 +294,18 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
$verbose,
$targetToSource
);
// Add action labels and attach
$rowEntities[$rootKey] = array_map(function ($ent) use ($translatedActions) {
// Add action labels
$items = array_map(function ($ent) use ($translatedActions) {
$ent['action_label'] = $translatedActions[$ent['action']] ?? $ent['action'];
return $ent;
}, $items);
// If only a single, ungrouped item, flatten to a single entity for convenience
if (count($items) === 1 && (($items[0]['group'] ?? '') === '')) {
$rowEntities[$rootKey] = $items[0];
} else {
$rowEntities[$rootKey] = $items;
}
} else {
[$genericEntity, $summaries, $genericCaches, $genericExistingIdentities, $genericSeenIdentities]
= $this->simulateGenericRoot(
@@ -491,6 +554,10 @@ public function simulate(Import $import, int $limit = 100, bool $verbose = false
if (isset($rowEntities['payment']['status']) && $rowEntities['payment']['status'] !== 'ok') {
$rowStatus = $rowEntities['payment']['status'];
}
// If we skipped due to keyref, surface a warning status for the row if not already set
if ($rowStatus === 'ok' && $keyrefSkipRow) {
$rowStatus = 'warning';
}
$simRows[] = [
'index' => $idx + 1,
'entities' => $rowEntities,
@@ -806,7 +873,14 @@ private function simulateContract(callable $val, array $summaries, array $cache,
if (array_key_exists($reference, $cache)) {
$contract = $cache[$reference];
} else {
$contract = Contract::query()->where('reference', $reference)->first(['id', 'reference', 'client_case_id', 'active', 'deleted_at']);
$q = Contract::query()->where('reference', $reference);
// Scope to selected client when available
if (! is_null($this->clientId)) {
$q->whereHas('clientCase', function ($qq): void {
$qq->where('client_id', $this->clientId);
});
}
$contract = $q->first(['id', 'reference', 'client_case_id', 'active', 'deleted_at']);
$cache[$reference] = $contract; // may be null
}
}
@@ -838,10 +912,16 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
if (array_key_exists($reference, $cache)) {
$account = $cache[$reference];
} else {
$account = Account::query()
$q = Account::query()
->where('reference', $reference)
->where('active', 1)
->first(['id', 'reference', 'balance_amount']);
->where('active', 1);
// Scope to selected client when available via contract -> clientCase
if (! is_null($this->clientId)) {
$q->whereHas('contract.clientCase', function ($qq): void {
$qq->where('client_id', $this->clientId);
});
}
$account = $q->first(['id', 'reference', 'balance_amount']);
$cache[$reference] = $account;
}
}
@@ -897,6 +977,39 @@ private function simulateAccount(callable $val, array $summaries, array $cache,
return [$entity, $summaries, $cache];
}
/**
* Lookup apply_mode for a mapping target field on this import.
* Returns lowercased mode like 'insert', 'update', 'both', 'keyref', or null if not found.
*/
private function mappingModeForImport(Import $import, string $targetField): ?string
{
$rows = \DB::table('import_mappings')
->where('import_id', $import->id)
->get(['target_field', 'apply_mode']);
foreach ($rows as $row) {
$tf = (string) ($row->target_field ?? '');
if ($tf === '') {
continue;
}
// Normalize root part to match canonical keys like contract.reference
if (str_contains($tf, '.')) {
[$root, $rest] = explode('.', $tf, 2);
} else {
$root = $tf;
$rest = null;
}
$norm = $this->normalizeRoot($root);
$tfNorm = $rest !== null ? ($norm.'.'.$rest) : $norm;
if ($tfNorm === $targetField) {
$mode = $row->apply_mode ?? null;
return is_string($mode) ? strtolower($mode) : null;
}
}
return null;
}
private function simulateImplicitAccount(int $contractId, array $summaries, array $cache): array
{
$acct = Account::query()->where('contract_id', $contractId)->orderBy('id')->first(['id', 'reference', 'balance_amount']);
@@ -1070,7 +1183,12 @@ private function simulateGenericRoot(
// Try/catch to avoid issues if column doesn't exist
try {
if (\Schema::hasColumn((new $modelClass)->getTable(), 'reference')) {
$record = $modelClass::query()->where('reference', $reference)->first(['id', 'reference']);
$qb = $modelClass::query()->where('reference', $reference);
// Scope client_case lookups to selected client
if ($modelClass === \App\Models\ClientCase::class && ! is_null($this->clientId)) {
$qb->where('client_id', $this->clientId);
}
$record = $qb->first(['id', 'reference']);
}
} catch (\Throwable) {
$record = null;
@@ -1471,7 +1589,11 @@ private function simulateGenericRootMulti(
} elseif ($modelClass && class_exists($modelClass)) {
try {
if (\Schema::hasColumn((new $modelClass)->getTable(), 'reference')) {
$record = $modelClass::query()->where('reference', $reference)->first(['id', 'reference']);
$qb = $modelClass::query()->where('reference', $reference);
if ($modelClass === \App\Models\ClientCase::class && ! is_null($this->clientId)) {
$qb->where('client_id', $this->clientId);
}
$record = $qb->first(['id', 'reference']);
}
} catch (\Throwable) {
$record = null;