added option to import payments from csv file
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user