diff --git a/app/Http/Controllers/AccountPaymentController.php b/app/Http/Controllers/AccountPaymentController.php
index 6332aa6..778b9a8 100644
--- a/app/Http/Controllers/AccountPaymentController.php
+++ b/app/Http/Controllers/AccountPaymentController.php
@@ -8,8 +8,8 @@
use App\Models\Booking;
use App\Models\Payment;
use App\Models\PaymentSetting;
-use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
+use Illuminate\Http\RedirectResponse;
use Inertia\Inertia;
use Inertia\Response;
@@ -20,11 +20,12 @@ public function index(Account $account): Response
$payments = Payment::query()
->where('account_id', $account->id)
->orderByDesc('paid_at')
- ->get(['id', 'amount_cents', 'currency', 'reference', 'paid_at', 'created_at'])
+ ->get(['id', 'amount', 'balance_before', 'currency', 'reference', 'paid_at', 'created_at'])
->map(function (Payment $p) {
return [
'id' => $p->id,
- 'amount' => $p->amount, // accessor divides cents
+ 'amount' => (float) $p->amount,
+ 'balance_before' => (float) ($p->balance_before ?? 0),
'currency' => $p->currency,
'reference' => $p->reference,
'paid_at' => $p->paid_at,
@@ -43,11 +44,12 @@ public function list(Account $account): JsonResponse
$payments = Payment::query()
->where('account_id', $account->id)
->orderByDesc('paid_at')
- ->get(['id', 'amount_cents', 'currency', 'reference', 'paid_at', 'created_at'])
+ ->get(['id', 'amount', 'balance_before', 'currency', 'reference', 'paid_at', 'created_at'])
->map(function (Payment $p) {
return [
'id' => $p->id,
- 'amount' => $p->amount,
+ 'amount' => (float) $p->amount,
+ 'balance_before' => (float) ($p->balance_before ?? 0),
'currency' => $p->currency,
'reference' => $p->reference,
'paid_at' => optional($p->paid_at)?->toDateString(),
@@ -76,7 +78,8 @@ public function store(StorePaymentRequest $request, Account $account): RedirectR
$payment = Payment::query()->create([
'account_id' => $account->id,
- 'amount_cents' => $amountCents,
+ 'balance_before' => (float) ($account->balance_amount ?? 0),
+ 'amount' => (float) $validated['amount'],
'currency' => strtoupper($validated['currency'] ?? $defaultCurrency),
'reference' => $validated['reference'] ?? null,
'paid_at' => $validated['paid_at'] ?? now(),
@@ -94,10 +97,17 @@ public function store(StorePaymentRequest $request, Account $account): RedirectR
'booked_at' => $payment->paid_at ?? now(),
]);
- // Optionally create an activity entry with default decision/action
+ // Optionally create an activity entry with default decision/action
if ($settings && ($settings->create_activity_on_payment ?? false)) {
$note = $settings->activity_note_template ?? 'Prejeto plačilo';
$note = str_replace(['{amount}', '{currency}'], [number_format($amountCents / 100, 2, ',', '.'), $payment->currency], $note);
+
+ // Append balance context and cause
+ $account->refresh();
+ $beforeStr = number_format((float) ($payment->balance_before ?? 0), 2, ',', '.').' '.$payment->currency;
+ $afterStr = number_format((float) ($account->balance_amount ?? 0), 2, ',', '.').' '.$payment->currency;
+ $note .= " (Stanje pred: {$beforeStr}, Stanje po: {$afterStr}; Izvor: plačilo)";
+
$account->loadMissing('contract');
$clientCaseId = $account->contract?->client_case_id;
if ($clientCaseId) {
@@ -139,6 +149,7 @@ public function destroy(Account $account, Payment $payment): RedirectResponse|Js
if (request()->wantsJson()) {
$account->refresh();
+
return response()->json([
'ok' => true,
'balance_amount' => $account->balance_amount,
diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php
index 6eb8207..8494737 100644
--- a/app/Http/Controllers/ClientCaseContoller.php
+++ b/app/Http/Controllers/ClientCaseContoller.php
@@ -152,6 +152,7 @@ public function storeContract(ClientCase $clientCase, StoreContractRequest $requ
// Preserve segment filter if present
$segment = request('segment');
+
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
}
@@ -172,6 +173,8 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
$shouldUpsertAccount = (! is_null($initial)) || (! is_null($balance)) || $request->has('account_type_id');
if ($shouldUpsertAccount) {
$accountData = [];
+ // Track old balance before applying changes
+ $oldBalance = (float) optional($contract->account)->balance_amount;
if (! is_null($initial)) {
$accountData['initial_amount'] = $initial;
}
@@ -189,12 +192,37 @@ public function updateContract(ClientCase $clientCase, string $uuid, UpdateContr
$accountData = array_merge(['initial_amount' => 0, 'balance_amount' => 0], $accountData);
$contract->account()->create($accountData);
}
+
+ // After update/create, if balance_amount changed (and not through a payment), log an activity with before/after
+ if (array_key_exists('balance_amount', $accountData)) {
+ $newBalance = (float) optional($contract->account)->fresh()->balance_amount;
+ if ($newBalance !== $oldBalance) {
+ try {
+ $currency = optional(\App\Models\PaymentSetting::query()->first())->default_currency ?? 'EUR';
+ $beforeStr = number_format($oldBalance, 2, ',', '.').' '.$currency;
+ $afterStr = number_format($newBalance, 2, ',', '.').' '.$currency;
+ $note = 'Sprememba stanja (Stanje pred: '.$beforeStr.', Stanje po: '.$afterStr.'; Izvor: sprememba)';
+ \App\Models\Activity::create([
+ 'due_date' => null,
+ 'amount' => null,
+ 'note' => $note,
+ 'action_id' => null,
+ 'decision_id' => null,
+ 'client_case_id' => $contract->client_case_id,
+ 'contract_id' => $contract->id,
+ ]);
+ } catch (\Throwable $e) {
+ // non-fatal
+ }
+ }
+ }
}
});
// Preserve segment filter if present
$segment = request('segment');
+
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
}
@@ -210,7 +238,6 @@ public function storeActivity(ClientCase $clientCase, Request $request)
'contract_uuid' => 'nullable|uuid',
]);
-
// Map contract_uuid to contract_id within the same client case, if provided
$contractId = null;
if (! empty($attributes['contract_uuid'])) {
@@ -276,6 +303,7 @@ public function deleteContract(ClientCase $clientCase, string $uuid, Request $re
// Preserve segment filter if present
$segment = request('segment');
+
return to_route('clientCase.show', ['client_case' => $clientCase, 'segment' => $segment]);
}
@@ -1048,11 +1076,13 @@ public function show(ClientCase $clientCase)
}
});
}
+
return $q->paginate(20, ['*'], 'activities')->withQueryString();
})(),
function ($p) {
$p->getCollection()->transform(function ($a) {
$a->setAttribute('user_name', optional($a->user)->name);
+
return $a;
});
}
diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php
index 6c8facd..0b51aa5 100644
--- a/app/Http/Controllers/ImportController.php
+++ b/app/Http/Controllers/ImportController.php
@@ -255,10 +255,10 @@ public function saveMappings(Request $request, Import $import)
$data = $request->validate([
'mappings' => 'required|array',
'mappings.*.source_column' => 'required|string',
- 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases',
+ 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'mappings.*.target_field' => 'required|string',
'mappings.*.transform' => 'nullable|string|in:trim,upper,lower,decimal,ref',
- 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both',
+ 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
'mappings.*.options' => 'nullable|array',
]);
diff --git a/app/Http/Controllers/ImportEntityController.php b/app/Http/Controllers/ImportEntityController.php
index fb69dd5..0446bd5 100644
--- a/app/Http/Controllers/ImportEntityController.php
+++ b/app/Http/Controllers/ImportEntityController.php
@@ -77,7 +77,13 @@ public function suggest(Request $request)
if (! is_array($cols)) {
$cols = [];
}
- $entities = ImportEntity::all();
+ // Optional filter: only suggest for specific entity keys (e.g., template meta.entities)
+ $only = $request->input('only_entities');
+ $query = ImportEntity::query()->orderByRaw("(ui->>'order')::int nulls last");
+ if (is_array($only) && ! empty($only)) {
+ $query->whereIn('key', array_values(array_filter($only, fn ($v) => is_string($v) && $v !== '')));
+ }
+ $entities = $query->get();
$suggestions = [];
foreach ($cols as $col) {
$s = $this->suggestFor($col, $entities);
@@ -92,6 +98,8 @@ public function suggest(Request $request)
private function suggestFor(string $source, $entities): ?array
{
$s = trim(mb_strtolower($source));
+ $best = null;
+ $bestRank = PHP_INT_MAX;
foreach ($entities as $ent) {
$rules = (array) ($ent->rules ?? []);
foreach ($rules as $rule) {
@@ -101,15 +109,27 @@ private function suggestFor(string $source, $entities): ?array
continue;
}
if (@preg_match($pattern, $s)) {
- return [
- 'entity' => $ent->key, // UI key (plural except person)
- 'field' => $field,
- 'canonical_root' => $ent->canonical_root,
- ];
+ // Rank preferences: payments over address for amount/date-like terms
+ $rank = (int) ($ent->ui['order'] ?? 999);
+ $isPayment = ($ent->canonical_root ?? null) === 'payment';
+ $isAddress = ($ent->canonical_root ?? null) === 'address';
+ if ($isPayment) {
+ $rank -= 10; // boost payments
+ } elseif ($isAddress && in_array($field, ['amount', 'payment_date', 'payment_nu', 'reference'])) {
+ $rank += 10; // demote addresses for these
+ }
+ if ($rank < $bestRank) {
+ $bestRank = $rank;
+ $best = [
+ 'entity' => $ent->key,
+ 'field' => $field,
+ 'canonical_root' => $ent->canonical_root,
+ ];
+ }
}
}
}
- return null;
+ return $best;
}
}
diff --git a/app/Http/Controllers/ImportTemplateController.php b/app/Http/Controllers/ImportTemplateController.php
index 9e8fd48..c28ed5b 100644
--- a/app/Http/Controllers/ImportTemplateController.php
+++ b/app/Http/Controllers/ImportTemplateController.php
@@ -110,19 +110,21 @@ public function store(Request $request)
'client_id' => 'nullable|integer|exists:clients,id',
'is_active' => 'boolean',
'entities' => 'nullable|array',
- 'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts',
+ 'entities.*' => 'string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'mappings' => 'array',
'mappings.*.source_column' => 'required|string',
- 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases',
+ 'mappings.*.entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'mappings.*.target_field' => 'nullable|string',
'mappings.*.transform' => 'nullable|string|max:50',
- 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both',
+ 'mappings.*.apply_mode' => 'nullable|string|in:insert,update,both,keyref',
'mappings.*.options' => 'nullable|array',
'mappings.*.position' => 'nullable|integer',
'meta' => 'nullable|array',
'meta.segment_id' => 'nullable|integer|exists:segments,id',
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
'meta.action_id' => 'nullable|integer|exists:actions,id',
+ 'meta.payments_import' => 'nullable|boolean',
+ 'meta.contract_key_mode' => 'nullable|string|in:reference',
])->validate();
// Ensure decision belongs to action if both provided in meta
@@ -138,6 +140,11 @@ public function store(Request $request)
}
$template = null;
DB::transaction(function () use (&$template, $request, $data) {
+ $paymentsImport = (bool) (data_get($data, 'meta.payments_import') ?? false);
+ $entities = $data['entities'] ?? [];
+ if ($paymentsImport) {
+ $entities = ['contracts', 'accounts', 'payments'];
+ }
$template = ImportTemplate::create([
'uuid' => (string) Str::uuid(),
'name' => $data['name'],
@@ -149,10 +156,12 @@ public function store(Request $request)
'client_id' => $data['client_id'] ?? null,
'is_active' => $data['is_active'] ?? true,
'meta' => array_filter([
- 'entities' => $data['entities'] ?? [],
+ 'entities' => $entities,
'segment_id' => data_get($data, 'meta.segment_id'),
'decision_id' => data_get($data, 'meta.decision_id'),
'action_id' => data_get($data, 'meta.action_id'),
+ 'payments_import' => $paymentsImport ?: null,
+ 'contract_key_mode' => data_get($data, 'meta.contract_key_mode'),
], fn ($v) => ! is_null($v) && $v !== ''),
]);
@@ -232,10 +241,10 @@ public function addMapping(Request $request, ImportTemplate $template)
}
$data = validator($raw, [
'source_column' => 'required|string',
- 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases',
+ 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'target_field' => 'nullable|string',
'transform' => 'nullable|string|in:trim,upper,lower',
- 'apply_mode' => 'nullable|string|in:insert,update,both',
+ 'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
'options' => 'nullable|array',
'position' => 'nullable|integer',
])->validate();
@@ -295,6 +304,8 @@ public function update(Request $request, ImportTemplate $template)
'meta.segment_id' => 'nullable|integer|exists:segments,id',
'meta.decision_id' => 'nullable|integer|exists:decisions,id',
'meta.action_id' => 'nullable|integer|exists:actions,id',
+ 'meta.payments_import' => 'nullable|boolean',
+ 'meta.contract_key_mode' => 'nullable|string|in:reference',
])->validate();
// Validate decision/action consistency on update as well
@@ -316,7 +327,7 @@ public function update(Request $request, ImportTemplate $template)
if (array_key_exists('delimiter', $newMeta) && (! is_string($newMeta['delimiter']) || trim((string) $newMeta['delimiter']) === '')) {
unset($newMeta['delimiter']);
}
- foreach (['segment_id', 'decision_id', 'action_id'] as $k) {
+ foreach (['segment_id', 'decision_id', 'action_id', 'payments_import', 'contract_key_mode'] as $k) {
if (array_key_exists($k, $newMeta) && ($newMeta[$k] === '' || is_null($newMeta[$k]))) {
unset($newMeta[$k]);
}
@@ -331,7 +342,16 @@ public function update(Request $request, ImportTemplate $template)
'client_id' => $data['client_id'] ?? null,
'is_active' => $data['is_active'] ?? $template->is_active,
'sample_headers' => $data['sample_headers'] ?? $template->sample_headers,
- 'meta' => $newMeta,
+ 'meta' => (function () use ($newMeta) {
+ // If payments import mode is enabled, force entities sequence in meta
+ $meta = $newMeta;
+ $payments = (bool) ($meta['payments_import'] ?? false);
+ if ($payments) {
+ $meta['entities'] = ['contracts', 'accounts', 'payments'];
+ }
+
+ return $meta;
+ })(),
]);
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
@@ -348,14 +368,21 @@ public function bulkAddMappings(Request $request, ImportTemplate $template)
}
$data = validator($raw, [
'sources' => 'required|string', // comma and/or newline separated
- 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases',
+ 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'default_field' => 'nullable|string', // if provided, used as the field name for all entries
- 'apply_mode' => 'nullable|string|in:insert,update,both',
+ 'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
'transform' => 'nullable|string|in:trim,upper,lower',
])->validate();
- $list = preg_split('/\r?\n|,/', $data['sources']);
- $list = array_values(array_filter(array_map(fn ($s) => trim($s), $list), fn ($s) => $s !== ''));
+ // Accept commas, semicolons, and newlines; strip surrounding quotes/apostrophes and whitespace
+ $list = preg_split('/[\r\n,;]+/', $data['sources']);
+ $list = array_values(array_filter(array_map(function ($s) {
+ $s = trim((string) $s);
+ // remove surrounding double/single quotes if present
+ $s = preg_replace('/^([\"\'])|([\"\'])$/u', '', $s) ?? $s;
+
+ return $s;
+ }, $list), fn ($s) => $s !== ''));
if (empty($list)) {
return redirect()->route('importTemplates.edit', ['template' => $template->uuid])
@@ -437,10 +464,10 @@ public function updateMapping(Request $request, ImportTemplate $template, Import
}
$data = validator($raw, [
'source_column' => 'required|string',
- 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts',
+ 'entity' => 'nullable|string|in:person,person_addresses,person_phones,emails,accounts,contracts,client_cases,payments',
'target_field' => 'nullable|string',
'transform' => 'nullable|string|in:trim,upper,lower',
- 'apply_mode' => 'nullable|string|in:insert,update,both',
+ 'apply_mode' => 'nullable|string|in:insert,update,both,keyref',
'options' => 'nullable|array',
'position' => 'nullable|integer',
])->validate();
diff --git a/app/Models/Payment.php b/app/Models/Payment.php
index b3fe440..0be46a7 100644
--- a/app/Models/Payment.php
+++ b/app/Models/Payment.php
@@ -2,13 +2,11 @@
namespace App\Models;
-use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
-use App\Models\Activity;
class Payment extends Model
{
@@ -17,7 +15,8 @@ class Payment extends Model
protected $fillable = [
'account_id',
- 'amount_cents',
+ 'amount',
+ 'balance_before',
'currency',
'reference',
'paid_at',
@@ -31,7 +30,8 @@ protected function casts(): array
return [
'paid_at' => 'datetime',
'meta' => 'array',
- 'amount_cents' => 'integer',
+ 'amount' => 'decimal:4',
+ 'balance_before' => 'decimal:4',
];
}
@@ -55,23 +55,5 @@ public function type(): BelongsTo
return $this->belongsTo(\App\Models\PaymentType::class);
}
- /**
- * Accessor to expose decimal amount for JSON serialization and UI convenience.
- */
- protected function amount(): Attribute
- {
- return Attribute::get(function () {
- $cents = (int) ($this->attributes['amount_cents'] ?? 0);
-
- return $cents / 100;
- });
- }
-
- /**
- * Mutator to set amount via decimal; stores in cents.
- */
- public function setAmountAttribute($value): void
- {
- $this->attributes['amount_cents'] = (int) round(((float) $value) * 100);
- }
+ // amount is stored as decimal(20,4); default Eloquent get/set is sufficient
}
diff --git a/app/Services/ImportProcessor.php b/app/Services/ImportProcessor.php
index e3ad8ea..41279f5 100644
--- a/app/Services/ImportProcessor.php
+++ b/app/Services/ImportProcessor.php
@@ -15,6 +15,7 @@
use App\Models\ImportEntity;
use App\Models\ImportEvent;
use App\Models\ImportRow;
+use App\Models\Payment;
use App\Models\Person\AddressType;
use App\Models\Person\Person;
use App\Models\Person\PersonAddress;
@@ -68,6 +69,10 @@ public function process(Import $import, ?Authenticatable $user = null): array
$this->validateMappingRoots($mappings, $validRoots);
$header = $import->meta['columns'] ?? null;
+ // Template meta flags
+ $tplMeta = optional($import->template)->meta ?? [];
+ $paymentsImport = (bool) ($tplMeta['payments_import'] ?? false);
+ $contractKeyMode = $tplMeta['contract_key_mode'] ?? null;
// Prefer explicitly chosen delimiter, then template meta, else detected
$delimiter = $import->meta['forced_delimiter']
?? optional($import->template)->meta['delimiter']
@@ -102,6 +107,43 @@ public function process(Import $import, ?Authenticatable $user = null): array
'level' => 'info',
'message' => 'Processing started.',
]);
+ // Preflight recommendations for payments-import
+ if ($paymentsImport) {
+ $hasContractRef = $this->mappingIncludes($mappings, 'contract.reference');
+ $hasPaymentAmount = $this->mappingIncludes($mappings, 'payment.amount');
+ $hasPaymentDate = $this->mappingIncludes($mappings, 'payment.payment_date') || $this->mappingIncludes($mappings, 'payment.paid_at');
+ $hasPaymentRef = $this->mappingIncludes($mappings, 'payment.reference');
+ $hasPaymentNu = $this->mappingIncludes($mappings, 'payment.payment_nu');
+ $missing = [];
+ if (! $hasContractRef) {
+ $missing[] = 'contract.reference';
+ }
+ if (! $hasPaymentAmount) {
+ $missing[] = 'payment.amount';
+ }
+ if (! $hasPaymentDate) {
+ $missing[] = 'payment.payment_date';
+ }
+ if (! empty($missing)) {
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'event' => 'payments_mapping_recommendation',
+ 'level' => 'warning',
+ 'message' => 'Payments import: recommended mappings missing.',
+ 'context' => ['missing' => $missing],
+ ]);
+ }
+ if (! $hasPaymentRef && ! $hasPaymentNu) {
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'event' => 'payments_idempotency_recommendation',
+ 'level' => 'warning',
+ 'message' => 'For idempotency, map payment.reference (preferred) or payment.payment_nu.',
+ ]);
+ }
+ }
$rowNum = 0;
if ($hasHeader) {
@@ -177,7 +219,32 @@ public function process(Import $import, ?Authenticatable $user = null): array
// Contracts
$contractResult = null;
if (isset($mapped['contract'])) {
- $contractResult = $this->upsertContractChain($import, $mapped, $mappings);
+ // In payments-import with contract_key_mode=reference, treat contract.reference as a keyref only
+ if ($paymentsImport && $contractKeyMode === 'reference') {
+ $ref = $mapped['contract']['reference'] ?? null;
+ if (is_string($ref)) {
+ $ref = preg_replace('/\s+/', '', trim($ref));
+ }
+ if ($ref) {
+ $q = Contract::query()
+ ->when($import->client_id, function ($q2, $clientId) {
+ $q2->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
+ ->where('client_cases.client_id', $clientId);
+ })
+ ->where('contracts.reference', $ref)
+ ->select('contracts.*');
+ $found = $q->first();
+ if ($found) {
+ $contractResult = ['action' => 'resolved', 'contract' => $found];
+ } else {
+ $contractResult = null; // let requireContract logic flag invalid later
+ }
+ } else {
+ $contractResult = null;
+ }
+ } else {
+ $contractResult = $this->upsertContractChain($import, $mapped, $mappings);
+ }
if ($contractResult['action'] === 'skipped') {
// Even if no contract fields were updated, we may still need to apply template meta
// like attaching a segment or creating an activity. Do that if we have the contract.
@@ -330,6 +397,233 @@ public function process(Import $import, ?Authenticatable $user = null): array
$ccId = $contractResult['contract']->client_case_id;
$personIdForRow = ClientCase::where('id', $ccId)->value('person_id');
}
+
+ // Payments: when present, require account resolution and create payment
+ if (isset($mapped['payment'])) {
+ // If no account yet, try to resolve via contract + first account
+ $accountIdForPayment = $accountResult['account']->id ?? null;
+
+ // If payments-import mode with contract_key_mode=reference and we have contract.reference mapped, resolve by reference only
+ $tplMeta = optional($import->template)->meta ?? [];
+ $paymentsImport = (bool) ($tplMeta['payments_import'] ?? false);
+ $contractKeyMode = $tplMeta['contract_key_mode'] ?? null;
+ if (! $accountIdForPayment && $paymentsImport && $contractKeyMode === 'reference') {
+ $contractRef = $mapped['contract']['reference'] ?? null;
+ if ($contractRef) {
+ $contract = \App\Models\Contract::query()
+ ->when($import->client_id, function ($q, $clientId) {
+ $q->join('client_cases', 'contracts.client_case_id', '=', 'client_cases.id')
+ ->where('client_cases.client_id', $clientId);
+ })
+ ->where('contracts.reference', $contractRef)
+ ->select('contracts.id')
+ ->first();
+ if ($contract) {
+ $accountIdForPayment = \App\Models\Account::where('contract_id', $contract->id)->value('id');
+ }
+ }
+ }
+
+ if (! $accountIdForPayment) {
+ $invalid++;
+ $importRow->update(['status' => 'invalid', 'errors' => ['Payment requires an account. Could not resolve account for payment.']]);
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'row_invalid',
+ 'level' => 'error',
+ 'message' => 'Payment requires an account (not resolved).',
+ ]);
+
+ continue;
+ }
+
+ // Build payment payload
+ $p = $mapped['payment'];
+ // Normalize reference and payment number (if provided)
+ $refVal = isset($p['reference']) ? (is_string($p['reference']) ? trim($p['reference']) : $p['reference']) : null;
+ $nuVal = isset($p['payment_nu']) ? (is_string($p['payment_nu']) ? trim($p['payment_nu']) : $p['payment_nu']) : null;
+ $payload = [
+ 'account_id' => $accountIdForPayment,
+ 'reference' => $p['reference'] ?? null,
+ 'paid_at' => $p['payment_date'] ?? ($p['paid_at'] ?? null),
+ 'currency' => $p['currency'] ?? 'EUR',
+ 'created_by' => $user?->getAuthIdentifier(),
+ ];
+ // Attach payment_nu into meta for idempotency if provided
+ $meta = [];
+ if (is_array($p['meta'] ?? null)) {
+ $meta = $p['meta'];
+ }
+ if (! empty($nuVal)) {
+ $meta['payment_nu'] = $nuVal;
+ }
+ if (! empty($meta)) {
+ $payload['meta'] = $meta;
+ }
+ // Amount: accept either amount (preferred) or legacy amount_cents; convert cents -> decimal
+ if (array_key_exists('amount', $p)) {
+ $payload['amount'] = is_string($p['amount']) ? (float) $this->normalizeDecimal($p['amount']) : (float) $p['amount'];
+ } elseif (array_key_exists('amount_cents', $p)) {
+ $payload['amount'] = ((int) $p['amount_cents']) / 100.0;
+ }
+
+ // Idempotency: skip creating if a payment with same (account_id, reference) already exists
+ if (! empty($refVal)) {
+ $exists = Payment::query()
+ ->where('account_id', $accountIdForPayment)
+ ->where('reference', $refVal)
+ ->exists();
+ if ($exists) {
+ $skipped++;
+ $importRow->update(['status' => 'skipped']);
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'payment_duplicate_skipped',
+ 'level' => 'info',
+ 'message' => 'Skipped duplicate payment (by reference).',
+ 'context' => [
+ 'account_id' => $accountIdForPayment,
+ 'reference' => $refVal,
+ ],
+ ]);
+
+ continue;
+ }
+ }
+
+ $payment = new Payment;
+ $payment->fill($payload);
+ // Save the account balance before applying this payment
+ $accForBal = Account::find($accountIdForPayment);
+ if ($accForBal) {
+ $payment->balance_before = (float) ($accForBal->balance_amount ?? 0);
+ }
+ // If amount not in payload yet but provided, set it directly
+ if (! array_key_exists('amount', $payload) && isset($p['amount'])) {
+ $payment->amount = (float) $this->normalizeDecimal($p['amount']);
+ }
+ try {
+ $payment->save();
+ } catch (\Throwable $e) {
+ // Gracefully skip if unique index on (account_id, reference) is violated due to race conditions
+ if (str_contains($e->getMessage(), 'payments_account_id_reference_unique')) {
+ $skipped++;
+ $importRow->update(['status' => 'skipped']);
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'payment_duplicate_skipped_db',
+ 'level' => 'info',
+ 'message' => 'Skipped duplicate payment due to database unique constraint (account_id, reference).',
+ 'context' => [
+ 'account_id' => $accountIdForPayment,
+ 'reference' => $refVal,
+ ],
+ ]);
+
+ continue;
+ }
+ throw $e;
+ }
+
+ // Option A: create a credit booking so account balance updates via booking events
+ try {
+ if (isset($payment->amount)) {
+ \App\Models\Booking::query()->create([
+ 'account_id' => $accountIdForPayment,
+ 'payment_id' => $payment->id,
+ 'amount_cents' => (int) round(((float) $payment->amount) * 100),
+ 'type' => 'credit',
+ 'description' => $payment->reference ? ('Plačilo '.$payment->reference) : 'Plačilo',
+ 'booked_at' => $payment->paid_at ?? now(),
+ ]);
+ }
+ } catch (\Throwable $e) {
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'booking_create_failed',
+ 'level' => 'warning',
+ 'message' => 'Failed to create booking for payment: '.$e->getMessage(),
+ ]);
+ }
+
+ // Optionally create an activity entry for this payment
+ try {
+ $settings = \App\Models\PaymentSetting::query()->first();
+ if ($settings && ($settings->create_activity_on_payment ?? false)) {
+ $amountCents = (int) round(((float) $payment->amount) * 100);
+ $note = $settings->activity_note_template ?? 'Prejeto plačilo';
+ $note = str_replace(['{amount}', '{currency}'], [number_format($amountCents / 100, 2, ',', '.'), $payment->currency ?? 'EUR'], $note);
+
+ // Append balance context (before/after) and mark cause as payment
+ // At this point, booking has been created so the account balance should reflect the new amount
+ $accountAfter = Account::find($accountIdForPayment);
+ $beforeStr = number_format((float) ($payment->balance_before ?? 0), 2, ',', '.').' '.($payment->currency ?? 'EUR');
+ $afterStr = number_format((float) ($accountAfter?->balance_amount ?? 0), 2, ',', '.').' '.($payment->currency ?? 'EUR');
+ $note .= " (Stanje pred: {$beforeStr}, Stanje po: {$afterStr}; Izvor: plačilo)";
+
+ // Resolve client_case_id via account->contract
+ $accountForActivity = $accForBal ?: Account::find($accountIdForPayment);
+ $accountForActivity?->loadMissing('contract');
+ $contractId = $accountForActivity?->contract_id;
+ $clientCaseId = $accountForActivity?->contract?->client_case_id;
+
+ if ($clientCaseId) {
+ $activity = \App\Models\Activity::query()->create([
+ 'due_date' => null,
+ 'amount' => $amountCents / 100,
+ 'note' => $note,
+ 'action_id' => $settings->default_action_id,
+ 'decision_id' => $settings->default_decision_id,
+ 'client_case_id' => $clientCaseId,
+ 'contract_id' => $contractId,
+ ]);
+ $payment->update(['activity_id' => $activity->id]);
+ } else {
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'payment_activity_skipped',
+ 'level' => 'info',
+ 'message' => 'Skipped creating activity for payment due to missing client_case_id on contract.',
+ ]);
+ }
+ }
+ } catch (\Throwable $e) {
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'payment_activity_failed',
+ 'level' => 'warning',
+ 'message' => 'Failed to create activity for payment: '.$e->getMessage(),
+ ]);
+ }
+
+ $imported++;
+ $importRow->update([
+ 'status' => 'imported',
+ 'entity_type' => Payment::class,
+ 'entity_id' => $payment->id,
+ ]);
+ ImportEvent::create([
+ 'import_id' => $import->id,
+ 'user_id' => $user?->getAuthIdentifier(),
+ 'import_row_id' => $importRow->id,
+ 'event' => 'row_imported',
+ 'level' => 'info',
+ 'message' => 'Inserted payment',
+ 'context' => ['id' => $payment->id],
+ ]);
+ }
// If we have a contract reference, resolve existing contract for this client and derive person
if (! $personIdForRow && $import->client_id && ! empty($mapped['contract']['reference'] ?? null)) {
$existingContract = Contract::query()
@@ -709,10 +1003,16 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
continue;
}
$value = $acc[$field] ?? null;
- if (in_array($field, ['balance_amount','initial_amount'], true) && is_string($value)) {
+ if (in_array($field, ['balance_amount', 'initial_amount'], true) && is_string($value)) {
$value = $this->normalizeDecimal($value);
}
$mode = $map->apply_mode ?? 'both';
+ if ($mode === 'keyref') {
+ // treat as insert-only field (lookup + create), never update
+ $applyInsert[$field] = $value;
+
+ continue;
+ }
$applyModeByField[$field] = $mode;
if (in_array($mode, ['insert', 'both'])) {
$applyInsert[$field] = $value;
@@ -731,9 +1031,39 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
if (empty($changes)) {
return ['action' => 'skipped', 'message' => 'No non-null changes'];
}
+ // Track balance change
+ $oldBalance = (float) ($existing->balance_amount ?? 0);
$existing->fill($changes);
$existing->save();
+ // If balance_amount changed and this wasn't caused by a payment (we are in account upsert), log an activity with before/after
+ if (array_key_exists('balance_amount', $changes)) {
+ $newBalance = (float) ($existing->balance_amount ?? 0);
+ if ($newBalance !== $oldBalance) {
+ try {
+ $contractId = $existing->contract_id;
+ $clientCaseId = Contract::where('id', $contractId)->value('client_case_id');
+ $currency = optional(\App\Models\PaymentSetting::query()->first())->default_currency ?? 'EUR';
+ $beforeStr = number_format($oldBalance, 2, ',', '.').' '.$currency;
+ $afterStr = number_format($newBalance, 2, ',', '.').' '.$currency;
+ $note = 'Sprememba stanja (Stanje pred: '.$beforeStr.', Stanje po: '.$afterStr.'; Izvor: sprememba)';
+ if ($clientCaseId) {
+ Activity::create([
+ 'due_date' => null,
+ 'amount' => null,
+ 'note' => $note,
+ 'action_id' => null,
+ 'decision_id' => null,
+ 'client_case_id' => $clientCaseId,
+ 'contract_id' => $contractId,
+ ]);
+ }
+ } catch (\Throwable $e) {
+ // Non-fatal: ignore activity creation failures
+ }
+ }
+ }
+
// also include contract hints for downstream contact resolution
return ['action' => 'updated', 'account' => $existing, 'contract_id' => $contractId];
} else {
@@ -743,7 +1073,7 @@ private function upsertAccount(Import $import, array $mapped, $mappings): array
if ((! array_key_exists('initial_amount', $applyInsert) || is_null($applyInsert['initial_amount'] ?? null))
&& array_key_exists('balance_amount', $applyInsert)
&& ($applyInsert['balance_amount'] !== null && $applyInsert['balance_amount'] !== '')
- && ($initMode === null || in_array($initMode, ['insert','both'], true))) {
+ && ($initMode === null || in_array($initMode, ['insert', 'both'], true))) {
$applyInsert['initial_amount'] = $applyInsert['balance_amount'];
}
if (empty($applyInsert)) {
@@ -902,6 +1232,12 @@ private function upsertContractChain(Import $import, array $mapped, $mappings):
$value = preg_replace('/\s+/', '', trim((string) $value));
}
$mode = $map->apply_mode ?? 'both';
+ // keyref: used as lookup and applied on insert, but not on update
+ if ($mode === 'keyref') {
+ $applyInsert[$field] = $value;
+
+ continue;
+ }
if (in_array($mode, ['insert', 'both'])) {
$applyInsert[$field] = $value;
}
diff --git a/database/migrations/2025_10_02_000000_change_payments_amount_to_decimal.php b/database/migrations/2025_10_02_000000_change_payments_amount_to_decimal.php
new file mode 100644
index 0000000..5446a43
--- /dev/null
+++ b/database/migrations/2025_10_02_000000_change_payments_amount_to_decimal.php
@@ -0,0 +1,52 @@
+decimal('amount', 20, 4)->nullable()->after('account_id');
+ }
+ });
+
+ // Backfill amount from amount_cents if present
+ if (Schema::hasColumn('payments', 'amount_cents')) {
+ DB::statement('UPDATE payments SET amount = CASE WHEN amount_cents IS NOT NULL THEN amount_cents / 100.0 ELSE amount END');
+ }
+
+ // Make amount non-null if desired; keeping nullable to avoid breaking existing rows
+ // Drop amount_cents column
+ Schema::table('payments', function (Blueprint $table): void {
+ if (Schema::hasColumn('payments', 'amount_cents')) {
+ $table->dropColumn('amount_cents');
+ }
+ });
+ }
+
+ public function down(): void
+ {
+ // Recreate amount_cents and backfill from amount, then drop amount
+ Schema::table('payments', function (Blueprint $table): void {
+ if (! Schema::hasColumn('payments', 'amount_cents')) {
+ $table->integer('amount_cents')->nullable()->after('account_id');
+ }
+ });
+
+ if (Schema::hasColumn('payments', 'amount')) {
+ DB::statement('UPDATE payments SET amount_cents = CASE WHEN amount IS NOT NULL THEN ROUND(amount * 100) ELSE amount_cents END');
+ }
+
+ Schema::table('payments', function (Blueprint $table): void {
+ if (Schema::hasColumn('payments', 'amount')) {
+ $table->dropColumn('amount');
+ }
+ });
+ }
+};
diff --git a/database/migrations/2025_10_02_000010_add_balance_before_to_payments.php b/database/migrations/2025_10_02_000010_add_balance_before_to_payments.php
new file mode 100644
index 0000000..6f7bee3
--- /dev/null
+++ b/database/migrations/2025_10_02_000010_add_balance_before_to_payments.php
@@ -0,0 +1,26 @@
+decimal('balance_before', 20, 4)->nullable()->after('amount');
+ }
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('payments', function (Blueprint $table): void {
+ if (Schema::hasColumn('payments', 'balance_before')) {
+ $table->dropColumn('balance_before');
+ }
+ });
+ }
+};
diff --git a/database/migrations/2025_10_02_000020_add_unique_index_payments_account_reference.php b/database/migrations/2025_10_02_000020_add_unique_index_payments_account_reference.php
new file mode 100644
index 0000000..4d869b1
--- /dev/null
+++ b/database/migrations/2025_10_02_000020_add_unique_index_payments_account_reference.php
@@ -0,0 +1,41 @@
+hasUniqueIndex('payments', 'payments_account_id_reference_unique')) {
+ $table->unique(['account_id', 'reference'], 'payments_account_id_reference_unique');
+ }
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('payments', function (Blueprint $table): void {
+ if ($this->hasUniqueIndex('payments', 'payments_account_id_reference_unique')) {
+ $table->dropUnique('payments_account_id_reference_unique');
+ }
+ });
+ }
+
+ private function hasUniqueIndex(string $table, string $indexName): bool
+ {
+ // Works for both SQLite and others by checking existing indexes from connection schema manager when available.
+ try {
+ $connection = Schema::getConnection();
+ $schemaManager = $connection->getDoctrineSchemaManager();
+ $indexes = $schemaManager->listTableIndexes($table);
+
+ return array_key_exists($indexName, $indexes);
+ } catch (\Throwable $e) {
+ // Fallback: attempt dropping/creating blindly in migration operations
+ return false;
+ }
+ }
+};
diff --git a/database/migrations/2025_10_02_000021_update_unique_index_payments_ignore_soft_deletes.php b/database/migrations/2025_10_02_000021_update_unique_index_payments_ignore_soft_deletes.php
new file mode 100644
index 0000000..4853f5f
--- /dev/null
+++ b/database/migrations/2025_10_02_000021_update_unique_index_payments_ignore_soft_deletes.php
@@ -0,0 +1,58 @@
+unique(['account_id', 'reference'], 'payments_account_id_reference_unique');
+ } catch (\Throwable $e) {
+ // ignore if already exists
+ }
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ $driver = DB::getDriverName();
+ if ($driver === 'pgsql') {
+ DB::statement('DROP INDEX IF EXISTS uniq_payments_account_reference_not_deleted');
+ // Recreate a standard unique index (non-partial) with the original name
+ DB::statement('CREATE UNIQUE INDEX IF NOT EXISTS payments_account_id_reference_unique ON payments (account_id, reference)');
+ } else {
+ // For other drivers, nothing special beyond ensuring the standard index exists
+ Schema::table('payments', function ($table): void {
+ try {
+ $table->unique(['account_id', 'reference'], 'payments_account_id_reference_unique');
+ } catch (\Throwable $e) {
+ // ignore
+ }
+ });
+ }
+ }
+};
diff --git a/database/seeders/ImportEntitySeeder.php b/database/seeders/ImportEntitySeeder.php
index 8e536cb..5367898 100644
--- a/database/seeders/ImportEntitySeeder.php
+++ b/database/seeders/ImportEntitySeeder.php
@@ -37,11 +37,25 @@ public function run(): void
'key' => 'person_addresses',
'canonical_root' => 'address',
'label' => 'Person Addresses',
- 'fields' => ['address', 'country', 'type_id', 'description'],
- 'aliases' => ['address', 'person_addresses'],
+ 'fields' => ['address', 'city', 'postal_code', 'country', 'type_id', 'description'],
+ 'field_aliases' => [
+ 'ulica' => 'address',
+ 'naslov' => 'address',
+ 'mesto' => 'city',
+ 'posta' => 'postal_code',
+ 'pošta' => 'postal_code',
+ 'zip' => 'postal_code',
+ 'drzava' => 'country',
+ 'država' => 'country',
+ 'opis' => 'description',
+ ],
+ 'aliases' => ['person_addresses', 'address', 'addresses'],
'rules' => [
['pattern' => '/^(naslov|ulica|address)\b/i', 'field' => 'address'],
+ ['pattern' => '/^(mesto|city|kraj)\b/i', 'field' => 'city'],
+ ['pattern' => '/^(posta|pošta|zip|postal)\b/i', 'field' => 'postal_code'],
['pattern' => '/^(drzava|država|country)\b/i', 'field' => 'country'],
+ ['pattern' => '/^(komentar|opis|opomba|comment|description|note)\b/i', 'field' => 'description'],
],
'ui' => ['order' => 2],
],
@@ -106,6 +120,41 @@ public function run(): void
],
'ui' => ['order' => 7],
],
+ [
+ 'key' => 'payments',
+ 'canonical_root' => 'payment',
+ 'label' => 'Payments',
+ // include common fields and helpful references for mapping
+ 'fields' => [
+ 'reference',
+ 'payment_nu',
+ 'payment_date',
+ 'amount',
+ 'type_id',
+ 'active',
+ // optional helpers for mapping by related records
+ 'debt_id',
+ 'account_id',
+ 'account_reference',
+ 'contract_reference',
+ ],
+ 'field_aliases' => [
+ 'date' => 'payment_date',
+ 'datum' => 'payment_date',
+ 'paid_at' => 'payment_date',
+ 'number' => 'payment_nu',
+ 'znesek' => 'amount',
+ 'value' => 'amount',
+ ],
+ 'aliases' => ['payment', 'payments', 'placila', 'plačila'],
+ 'rules' => [
+ ['pattern' => '/^(sklic|reference|ref)\b/i', 'field' => 'reference'],
+ ['pattern' => '/^(stevilka|številka|number|payment\s*no\.?|payment\s*nu)\b/i', 'field' => 'payment_nu'],
+ ['pattern' => '/^(datum|date|paid\s*at|payment\s*date)\b/i', 'field' => 'payment_date'],
+ ['pattern' => '/^(znesek|amount|vplacilo|vplačilo|placilo|plačilo)\b/i', 'field' => 'amount'],
+ ],
+ 'ui' => ['order' => 8],
+ ],
];
foreach ($defs as $d) {
diff --git a/database/seeders/PaymentsImportTemplateSeeder.php b/database/seeders/PaymentsImportTemplateSeeder.php
new file mode 100644
index 0000000..869acb1
--- /dev/null
+++ b/database/seeders/PaymentsImportTemplateSeeder.php
@@ -0,0 +1,88 @@
+firstOrCreate([
+ 'name' => 'Payments CSV (reference)',
+ ], [
+ 'uuid' => (string) Str::uuid(),
+ 'description' => 'Payments import by contract reference: contract.reference lookup + payment fields',
+ 'source_type' => 'csv',
+ 'default_record_type' => 'payments',
+ 'sample_headers' => [
+ 'Pogodba sklic',
+ 'Številka plačila',
+ 'Datum',
+ 'Znesek',
+ 'Sklic plačila',
+ ],
+ 'is_active' => true,
+ 'meta' => [
+ 'delimiter' => ',',
+ 'enclosure' => '"',
+ 'escape' => '\\',
+ 'payments_import' => true,
+ 'contract_key_mode' => 'reference',
+ ],
+ ]);
+
+ $mappings = [
+ [
+ 'entity' => 'contracts',
+ 'source_column' => 'Pogodba sklic',
+ 'target_field' => 'contract.reference',
+ 'transform' => 'trim',
+ 'position' => 1,
+ ],
+ [
+ 'entity' => 'payments',
+ 'source_column' => 'Številka plačila',
+ 'target_field' => 'payment.payment_nu',
+ 'transform' => 'trim',
+ 'position' => 2,
+ ],
+ [
+ 'entity' => 'payments',
+ 'source_column' => 'Datum',
+ 'target_field' => 'payment.payment_date',
+ 'transform' => null,
+ 'position' => 3,
+ ],
+ [
+ 'entity' => 'payments',
+ 'source_column' => 'Znesek',
+ 'target_field' => 'payment.amount',
+ 'transform' => 'decimal',
+ 'position' => 4,
+ ],
+ [
+ 'entity' => 'payments',
+ 'source_column' => 'Sklic plačila',
+ 'target_field' => 'payment.reference',
+ 'transform' => 'trim',
+ 'position' => 5,
+ ],
+ ];
+
+ foreach ($mappings as $map) {
+ ImportTemplateMapping::firstOrCreate([
+ 'import_template_id' => $template->id,
+ 'source_column' => $map['source_column'],
+ ], [
+ 'entity' => $map['entity'],
+ 'target_field' => $map['target_field'],
+ 'transform' => $map['transform'],
+ 'position' => $map['position'],
+ ]);
+ }
+ }
+}
diff --git a/resources/examples/payments_sample.csv b/resources/examples/payments_sample.csv
new file mode 100644
index 0000000..b1626cd
--- /dev/null
+++ b/resources/examples/payments_sample.csv
@@ -0,0 +1,5 @@
+"Pogodba sklic","Številka plačila","Datum","Znesek","Sklic plačila"
+"5362030581","P-2025-0001","2025-09-01","120.50","REF-53620-A"
+"5362017358","P-2025-0002","2025-09-15","80.00","REF-53620-B"
+"5362011838","P-2025-0201","2025-09-03","300.00","REF-53413-A"
+"5362017783","P-2025-0202","2025-09-20","150.00","REF-53413-B"
\ No newline at end of file
diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue
index f2971cc..0c2cc64 100644
--- a/resources/js/Layouts/AppLayout.vue
+++ b/resources/js/Layouts/AppLayout.vue
@@ -1,18 +1,18 @@
-
Only global templates are shown until a client is selected.
++ Only global templates are shown until a client is selected. +
@@ -411,19 +481,26 @@ onMounted(async () => {| @@ -515,16 +629,22 @@ onMounted(async () => { |
{{ processResult }}
+ {{
+ processResult
+ }}
Leave empty to make this template global (visible to all clients).
++ Leave empty to make this template global (visible to all clients). +
Choose which tables this template targets. You can still define per-column mappings later.
++ Choose which tables this template targets. You can still define per-column + mappings later. +
++ When importing payments, Contract records are located using the selected + key. Use your CSV mapping to map the appropriate column to the contract + reference. +
+Select an Action to see its Decisions.
-+ Select an Action to see its Decisions. +
Ni mogoče spremeniti naročnika, ker ta predloga že vsebuje preslikave.
++ Ni mogoče spremeniti naročnika, ker ta predloga že vsebuje preslikave. +
Pusti prazno za samodejno zaznavo. Uporabi, ko zaznavanje ne deluje pravilno.
++ Pusti prazno za samodejno zaznavo. Uporabi, ko zaznavanje ne deluje + pravilno. +
Najprej izberi dejanje, nato odločitev.
+ Najprej izberi dejanje, nato odločitev. +
+ When enabled, entities are locked to Contracts → Accounts → Payments. +
++ Map your CSV column to contract.reference to resolve contracts for this + client. +
Ni nedodeljenih virov. Uporabi Bulk ali najprej dodaj vire.
++ Ni nedodeljenih virov. Uporabi Bulk ali najprej dodaj vire. +
Tega dejanja ni mogoče razveljaviti. Vse preslikave te predloge bodo izbrisane.
-+ Tega dejanja ni mogoče razveljaviti. Vse preslikave te predloge bodo izbrisane. +
+