Changes to import and notifications
This commit is contained in:
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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']),
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user