Compare commits
No commits in common. "master" and "production" have entirely different histories.
master
...
production
|
|
@ -1,132 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\StoreInstallmentRequest;
|
|
||||||
use App\Models\Account;
|
|
||||||
use App\Models\Activity;
|
|
||||||
use App\Models\Booking;
|
|
||||||
use App\Models\Installment;
|
|
||||||
use App\Models\InstallmentSetting;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
|
|
||||||
class AccountInstallmentController extends Controller
|
|
||||||
{
|
|
||||||
public function list(Account $account): JsonResponse
|
|
||||||
{
|
|
||||||
$installments = Installment::query()
|
|
||||||
->where('account_id', $account->id)
|
|
||||||
->orderByDesc('installment_at')
|
|
||||||
->get(['id', 'amount', 'balance_before', 'currency', 'reference', 'installment_at', 'created_at'])
|
|
||||||
->map(function (Installment $i) {
|
|
||||||
return [
|
|
||||||
'id' => $i->id,
|
|
||||||
'amount' => (float) $i->amount,
|
|
||||||
'balance_before' => (float) ($i->balance_before ?? 0),
|
|
||||||
'currency' => $i->currency,
|
|
||||||
'reference' => $i->reference,
|
|
||||||
'installment_at' => optional($i->installment_at)?->toDateString(),
|
|
||||||
'created_at' => optional($i->created_at)?->toDateTimeString(),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'account' => [
|
|
||||||
'id' => $account->id,
|
|
||||||
'balance_amount' => $account->balance_amount,
|
|
||||||
],
|
|
||||||
'installments' => $installments,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(StoreInstallmentRequest $request, Account $account): RedirectResponse
|
|
||||||
{
|
|
||||||
$validated = $request->validated();
|
|
||||||
|
|
||||||
$amountCents = (int) round(((float) $validated['amount']) * 100);
|
|
||||||
|
|
||||||
$settings = InstallmentSetting::query()->first();
|
|
||||||
$defaultCurrency = strtoupper($settings->default_currency ?? 'EUR');
|
|
||||||
|
|
||||||
$installment = Installment::query()->create([
|
|
||||||
'account_id' => $account->id,
|
|
||||||
'balance_before' => (float) ($account->balance_amount ?? 0),
|
|
||||||
'amount' => (float) $validated['amount'],
|
|
||||||
'currency' => strtoupper($validated['currency'] ?? $defaultCurrency),
|
|
||||||
'reference' => $validated['reference'] ?? null,
|
|
||||||
'installment_at' => $validated['installment_at'] ?? now(),
|
|
||||||
'meta' => $validated['meta'] ?? null,
|
|
||||||
'created_by' => $request->user()?->id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Debit booking — increases the account balance
|
|
||||||
Booking::query()->create([
|
|
||||||
'account_id' => $account->id,
|
|
||||||
'payment_id' => null,
|
|
||||||
'amount_cents' => $amountCents,
|
|
||||||
'type' => 'debit',
|
|
||||||
'description' => $installment->reference ? ('Obremenitev '.$installment->reference) : 'Obremenitev',
|
|
||||||
'booked_at' => $installment->installment_at ?? now(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($settings && ($settings->create_activity_on_installment ?? false)) {
|
|
||||||
$note = $settings->activity_note_template ?? 'Dodan obrok';
|
|
||||||
$note = str_replace(['{amount}', '{currency}'], [number_format($amountCents / 100, 2, ',', '.'), $installment->currency], $note);
|
|
||||||
|
|
||||||
$account->refresh();
|
|
||||||
$beforeStr = number_format((float) ($installment->balance_before ?? 0), 2, ',', '.').' '.$installment->currency;
|
|
||||||
$afterStr = number_format((float) ($account->balance_amount ?? 0), 2, ',', '.').' '.$installment->currency;
|
|
||||||
$note .= " (Stanje pred: {$beforeStr}, Stanje po: {$afterStr}; Izvor: obrok)";
|
|
||||||
|
|
||||||
$account->loadMissing('contract');
|
|
||||||
$clientCaseId = $account->contract?->client_case_id;
|
|
||||||
if ($clientCaseId) {
|
|
||||||
$activity = 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' => $account->contract_id,
|
|
||||||
]);
|
|
||||||
$installment->update(['activity_id' => $activity->id]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return back()->with('success', 'Installment created.');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(Account $account, Installment $installment): RedirectResponse|JsonResponse
|
|
||||||
{
|
|
||||||
if ($installment->account_id !== $account->id) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete related debit booking(s) to revert balance via model events
|
|
||||||
Booking::query()
|
|
||||||
->where('account_id', $account->id)
|
|
||||||
->where('type', 'debit')
|
|
||||||
->whereDate('booked_at', optional($installment->installment_at)?->toDateString())
|
|
||||||
->where('amount_cents', (int) round(((float) $installment->amount) * 100))
|
|
||||||
->whereNull('payment_id')
|
|
||||||
->get()
|
|
||||||
->each->delete();
|
|
||||||
|
|
||||||
if ($installment->activity_id) {
|
|
||||||
$activity = Activity::query()->find($installment->activity_id);
|
|
||||||
if ($activity) {
|
|
||||||
$activity->delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$installment->delete();
|
|
||||||
|
|
||||||
if (request()->wantsJson()) {
|
|
||||||
return response()->json(['success' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return back()->with('success', 'Installment deleted.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
use App\Models\SmsTemplate;
|
use App\Models\SmsTemplate;
|
||||||
use App\Services\Contact\PhoneSelector;
|
use App\Services\Contact\PhoneSelector;
|
||||||
use App\Services\Sms\SmsService;
|
use App\Services\Sms\SmsService;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
|
@ -29,7 +30,7 @@ public function index(Request $request): Response
|
||||||
->latest('id')
|
->latest('id')
|
||||||
->paginate($perPage);
|
->paginate($perPage);
|
||||||
|
|
||||||
return Inertia::render('Packages/Index', [
|
return Inertia::render('Admin/Packages/Index', [
|
||||||
'packages' => $packages,
|
'packages' => $packages,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +70,7 @@ public function create(Request $request): Response
|
||||||
})
|
})
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
return Inertia::render('Packages/Create', [
|
return Inertia::render('Admin/Packages/Create', [
|
||||||
'profiles' => $profiles,
|
'profiles' => $profiles,
|
||||||
'senders' => $senders,
|
'senders' => $senders,
|
||||||
'templates' => $templates,
|
'templates' => $templates,
|
||||||
|
|
@ -212,7 +213,7 @@ public function show(Package $package, SmsService $sms): Response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Inertia::render('Packages/Show', [
|
return Inertia::render('Admin/Packages/Show', [
|
||||||
'package' => $package,
|
'package' => $package,
|
||||||
'items' => $items,
|
'items' => $items,
|
||||||
'preview' => $preview,
|
'preview' => $preview,
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Models\CallLater;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
|
|
||||||
class CallLaterController extends Controller
|
|
||||||
{
|
|
||||||
public function index(Request $request): \Inertia\Response
|
|
||||||
{
|
|
||||||
$query = CallLater::query()
|
|
||||||
->with([
|
|
||||||
'clientCase.person',
|
|
||||||
'contract',
|
|
||||||
'user',
|
|
||||||
'activity',
|
|
||||||
])
|
|
||||||
->whereNull('completed_at')
|
|
||||||
->orderBy('call_back_at', 'asc');
|
|
||||||
|
|
||||||
if ($request->filled('date_from')) {
|
|
||||||
$query->whereDate('call_back_at', '>=', $request->date_from);
|
|
||||||
}
|
|
||||||
if ($request->filled('date_to')) {
|
|
||||||
$query->whereDate('call_back_at', '<=', $request->date_to);
|
|
||||||
}
|
|
||||||
if ($request->filled('search')) {
|
|
||||||
$term = '%'.$request->search.'%';
|
|
||||||
$query->whereHas('clientCase.person', function ($q) use ($term) {
|
|
||||||
$q->where('first_name', 'ilike', $term)
|
|
||||||
->orWhere('last_name', 'ilike', $term)
|
|
||||||
->orWhere('full_name', 'ilike', $term)
|
|
||||||
->orWhereRaw("CONCAT(first_name, ' ', last_name) ILIKE ?", [$term]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$callLaters = $query->paginate(50)->withQueryString();
|
|
||||||
|
|
||||||
return Inertia::render('CallLaters/Index', [
|
|
||||||
'callLaters' => $callLaters,
|
|
||||||
'filters' => $request->only(['date_from', 'date_to', 'search']),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function complete(CallLater $callLater): \Illuminate\Http\RedirectResponse
|
|
||||||
{
|
|
||||||
$callLater->update(['completed_at' => now()]);
|
|
||||||
|
|
||||||
return back()->with('success', 'Klic označen kot opravljen.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -306,7 +306,6 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||||
try {
|
try {
|
||||||
$attributes = $request->validate([
|
$attributes = $request->validate([
|
||||||
'due_date' => 'nullable|date',
|
'due_date' => 'nullable|date',
|
||||||
'call_back_at' => 'nullable|date_format:Y-m-d H:i:s|after_or_equal:now',
|
|
||||||
'amount' => 'nullable|decimal:0,4',
|
'amount' => 'nullable|decimal:0,4',
|
||||||
'note' => 'nullable|string',
|
'note' => 'nullable|string',
|
||||||
'action_id' => 'exists:\App\Models\Action,id',
|
'action_id' => 'exists:\App\Models\Action,id',
|
||||||
|
|
@ -327,14 +326,14 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||||
|
|
||||||
// Determine which contracts to process
|
// Determine which contracts to process
|
||||||
$contractIds = [];
|
$contractIds = [];
|
||||||
if ($createForAll && ! empty($contractUuids)) {
|
if ($createForAll && !empty($contractUuids)) {
|
||||||
// Get all contract IDs from the provided UUIDs
|
// Get all contract IDs from the provided UUIDs
|
||||||
$contracts = Contract::withTrashed()
|
$contracts = Contract::withTrashed()
|
||||||
->whereIn('uuid', $contractUuids)
|
->whereIn('uuid', $contractUuids)
|
||||||
->where('client_case_id', $clientCase->id)
|
->where('client_case_id', $clientCase->id)
|
||||||
->get();
|
->get();
|
||||||
$contractIds = $contracts->pluck('id')->toArray();
|
$contractIds = $contracts->pluck('id')->toArray();
|
||||||
} elseif (! empty($contractUuids) && isset($contractUuids[0])) {
|
} elseif (!empty($contractUuids) && isset($contractUuids[0])) {
|
||||||
// Single contract mode
|
// Single contract mode
|
||||||
$contract = Contract::withTrashed()
|
$contract = Contract::withTrashed()
|
||||||
->where('uuid', $contractUuids[0])
|
->where('uuid', $contractUuids[0])
|
||||||
|
|
@ -343,7 +342,7 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||||
if ($contract) {
|
if ($contract) {
|
||||||
$contractIds = [$contract->id];
|
$contractIds = [$contract->id];
|
||||||
}
|
}
|
||||||
} elseif (! empty($attributes['contract_uuid'])) {
|
} elseif (!empty($attributes['contract_uuid'])) {
|
||||||
// Legacy single contract_uuid support
|
// Legacy single contract_uuid support
|
||||||
$contract = Contract::withTrashed()
|
$contract = Contract::withTrashed()
|
||||||
->where('uuid', $attributes['contract_uuid'])
|
->where('uuid', $attributes['contract_uuid'])
|
||||||
|
|
@ -372,7 +371,6 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||||
// Create activity
|
// Create activity
|
||||||
$row = $clientCase->activities()->create([
|
$row = $clientCase->activities()->create([
|
||||||
'due_date' => $attributes['due_date'] ?? null,
|
'due_date' => $attributes['due_date'] ?? null,
|
||||||
'call_back_at' => $attributes['call_back_at'] ?? null,
|
|
||||||
'amount' => $attributes['amount'] ?? null,
|
'amount' => $attributes['amount'] ?? null,
|
||||||
'note' => $attributes['note'] ?? null,
|
'note' => $attributes['note'] ?? null,
|
||||||
'action_id' => $attributes['action_id'],
|
'action_id' => $attributes['action_id'],
|
||||||
|
|
@ -419,25 +417,25 @@ public function storeActivity(ClientCase $clientCase, Request $request)
|
||||||
->whereIn('id', $attachmentIds)
|
->whereIn('id', $attachmentIds)
|
||||||
->pluck('id');
|
->pluck('id');
|
||||||
$validAttachmentIds = Document::query()
|
$validAttachmentIds = Document::query()
|
||||||
->where('documentable_type', Contract::class)
|
->where('documentable_type', Contract::class)
|
||||||
->where('documentable_id', $contractId)
|
->where('documentable_id', $contractId)
|
||||||
->whereIn('id', $attachmentIds)
|
->whereIn('id', $attachmentIds)
|
||||||
->pluck('id');
|
->pluck('id');
|
||||||
}
|
|
||||||
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [
|
|
||||||
'attachment_ids' => $validAttachmentIds->all(),
|
|
||||||
]);
|
|
||||||
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
|
||||||
// If template requires contract and user attempted to send, surface a validation message
|
|
||||||
logger()->warning('Email not queued: required contract is missing for the selected template.');
|
|
||||||
}
|
|
||||||
if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) {
|
|
||||||
logger()->warning('Email not queued: no eligible client emails to receive auto mails.');
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// Do not fail activity creation due to mailing issues
|
|
||||||
logger()->warning('Auto mail dispatch failed: '.$e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
$result = app(\App\Services\AutoMailDispatcher::class)->maybeQueue($row, $sendFlag, [
|
||||||
|
'attachment_ids' => $validAttachmentIds->all(),
|
||||||
|
]);
|
||||||
|
if (($result['skipped'] ?? null) === 'missing-contract' && $sendFlag) {
|
||||||
|
// If template requires contract and user attempted to send, surface a validation message
|
||||||
|
logger()->warning('Email not queued: required contract is missing for the selected template.');
|
||||||
|
}
|
||||||
|
if (($result['skipped'] ?? null) === 'no-recipients' && $sendFlag) {
|
||||||
|
logger()->warning('Email not queued: no eligible client emails to receive auto mails.');
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// Do not fail activity creation due to mailing issues
|
||||||
|
logger()->warning('Auto mail dispatch failed: '.$e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$activityCount = count($createdActivities);
|
$activityCount = count($createdActivities);
|
||||||
|
|
@ -827,8 +825,9 @@ public function show(ClientCase $clientCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get contracts using service
|
// Get contracts using service
|
||||||
$contracts = $this->caseDataService->getContracts($case, $segmentId);
|
$contractsPerPage = request()->integer('contracts_per_page', 10);
|
||||||
$contractIds = collect($contracts)->pluck('id')->all();
|
$contracts = $this->caseDataService->getContracts($case, $segmentId, $contractsPerPage);
|
||||||
|
$contractIds = collect($contracts->items())->pluck('id')->all();
|
||||||
|
|
||||||
// Get activities using service
|
// Get activities using service
|
||||||
$activitiesPerPage = request()->integer('activities_per_page', 15);
|
$activitiesPerPage = request()->integer('activities_per_page', 15);
|
||||||
|
|
@ -869,14 +868,11 @@ public function show(ClientCase $clientCase)
|
||||||
'decisions.emailTemplate' => function ($q) {
|
'decisions.emailTemplate' => function ($q) {
|
||||||
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
$q->select('id', 'name', 'entity_types', 'allow_attachments');
|
||||||
},
|
},
|
||||||
'decisions.events' => function ($q) {
|
|
||||||
$q->select('events.id', 'events.key', 'events.name');
|
|
||||||
},
|
|
||||||
])
|
])
|
||||||
->get(['id', 'name', 'color_tag', 'segment_id']),
|
->get(['id', 'name', 'color_tag', 'segment_id']),
|
||||||
'types' => $types,
|
'types' => $types,
|
||||||
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
'segments' => $case->segments()->wherePivot('active', true)->get(['segments.id', 'segments.name']),
|
||||||
'all_segments' => Segment::query()->where('active', true)->get(['id', 'name']),
|
'all_segments' => \App\Models\Segment::query()->where('active', true)->get(['id', 'name']),
|
||||||
'current_segment' => $currentSegment,
|
'current_segment' => $currentSegment,
|
||||||
'sms_profiles' => \App\Models\SmsProfile::query()
|
'sms_profiles' => \App\Models\SmsProfile::query()
|
||||||
->select(['id', 'name', 'default_sender_id'])
|
->select(['id', 'name', 'default_sender_id'])
|
||||||
|
|
@ -893,7 +889,6 @@ public function show(ClientCase $clientCase)
|
||||||
->select(['id', 'name', 'content', 'allow_custom_body'])
|
->select(['id', 'name', 'content', 'allow_custom_body'])
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(),
|
->get(),
|
||||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1107,7 +1102,6 @@ public function archiveBatch(Request $request)
|
||||||
|
|
||||||
if (! $setting) {
|
if (! $setting) {
|
||||||
\Log::warning('No archive settings found for batch archive');
|
\Log::warning('No archive settings found for batch archive');
|
||||||
|
|
||||||
return back()->with('flash', [
|
return back()->with('flash', [
|
||||||
'error' => 'No archive settings found',
|
'error' => 'No archive settings found',
|
||||||
]);
|
]);
|
||||||
|
|
@ -1123,9 +1117,8 @@ public function archiveBatch(Request $request)
|
||||||
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
|
$contract = Contract::where('uuid', $contractUuid)->firstOrFail();
|
||||||
|
|
||||||
// Skip if contract is already archived (active = 0)
|
// Skip if contract is already archived (active = 0)
|
||||||
if (! $contract->active) {
|
if (!$contract->active) {
|
||||||
$skippedCount++;
|
$skippedCount++;
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1215,7 +1208,7 @@ public function archiveBatch(Request $request)
|
||||||
if ($skippedCount > 0) {
|
if ($skippedCount > 0) {
|
||||||
$message .= ", skipped $skippedCount already archived";
|
$message .= ", skipped $skippedCount already archived";
|
||||||
}
|
}
|
||||||
$message .= ', '.count($errors).' failed';
|
$message .= ", " . count($errors) . " failed";
|
||||||
|
|
||||||
return back()->with('flash', [
|
return back()->with('flash', [
|
||||||
'error' => $message,
|
'error' => $message,
|
||||||
|
|
@ -1352,10 +1345,10 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
if (! empty($validated['sender_id'])) {
|
if (! empty($validated['sender_id'])) {
|
||||||
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
$sender = \App\Models\SmsSender::query()->find($validated['sender_id']);
|
||||||
if (! $sender) {
|
if (! $sender) {
|
||||||
return back()->with('error', 'Izbran pošiljatelj ne obstaja.');
|
return back()->with('error', 'Izbran pošiljatelj ne obstaja.');
|
||||||
}
|
}
|
||||||
if ($profile && (int) $sender->profile_id !== (int) $profile->id) {
|
if ($profile && (int) $sender->profile_id !== (int) $profile->id) {
|
||||||
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
return back()->with('error', 'Izbran pošiljatelj ne pripada izbranemu profilu.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (! $profile) {
|
if (! $profile) {
|
||||||
|
|
@ -1398,7 +1391,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an activity before sending
|
// Create an activity before sending
|
||||||
$activityNote = sprintf('Št: %s | Telo: %s', (string) $phone->nu, (string) $validated['message']);
|
$activityNote = sprintf('Št: %s | Telo: %s', (string) $phone->nu, (string) $validated['message']);
|
||||||
$activityData = [
|
$activityData = [
|
||||||
'note' => $activityNote,
|
'note' => $activityNote,
|
||||||
'user_id' => optional($request->user())->id,
|
'user_id' => optional($request->user())->id,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ public function index(Client $client, Request $request)
|
||||||
->where('person.full_name', 'ilike', '%'.$search.'%')
|
->where('person.full_name', 'ilike', '%'.$search.'%')
|
||||||
->groupBy('clients.id');
|
->groupBy('clients.id');
|
||||||
})
|
})
|
||||||
// ->where('clients.active', 1)
|
//->where('clients.active', 1)
|
||||||
// Use LEFT JOINs for aggregated data to avoid subqueries
|
// Use LEFT JOINs for aggregated data to avoid subqueries
|
||||||
->leftJoin('client_cases', 'client_cases.client_id', '=', 'clients.id')
|
->leftJoin('client_cases', 'client_cases.client_id', '=', 'clients.id')
|
||||||
->leftJoin('contracts', function ($join) {
|
->leftJoin('contracts', function ($join) {
|
||||||
|
|
@ -71,7 +71,6 @@ public function show(Client $client, Request $request)
|
||||||
|
|
||||||
return Inertia::render('Client/Show', [
|
return Inertia::render('Client/Show', [
|
||||||
'client' => $data,
|
'client' => $data,
|
||||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
|
||||||
'client_cases' => $data->clientCases()
|
'client_cases' => $data->clientCases()
|
||||||
->select('client_cases.*')
|
->select('client_cases.*')
|
||||||
->when($request->input('search'), function ($que, $search) {
|
->when($request->input('search'), function ($que, $search) {
|
||||||
|
|
@ -163,7 +162,6 @@ public function contracts(Client $client, Request $request)
|
||||||
|
|
||||||
return Inertia::render('Client/Contracts', [
|
return Inertia::render('Client/Contracts', [
|
||||||
'client' => $data,
|
'client' => $data,
|
||||||
'auto_mail_decisions' => \App\Models\Decision::query()->where('auto_mail', true)->orderBy('name')->get(['id', 'name']),
|
|
||||||
'contracts' => $contractsQuery
|
'contracts' => $contractsQuery
|
||||||
->paginate($perPage, ['*'], 'contracts_page', $pageNumber)
|
->paginate($perPage, ['*'], 'contracts_page', $pageNumber)
|
||||||
->withQueryString(),
|
->withQueryString(),
|
||||||
|
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Http\Requests\UpdateInstallmentSettingRequest;
|
|
||||||
use App\Models\Action;
|
|
||||||
use App\Models\Decision;
|
|
||||||
use App\Models\InstallmentSetting;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Inertia\Inertia;
|
|
||||||
use Inertia\Response;
|
|
||||||
|
|
||||||
class InstallmentSettingController extends Controller
|
|
||||||
{
|
|
||||||
public function edit(): Response
|
|
||||||
{
|
|
||||||
$setting = InstallmentSetting::query()->first();
|
|
||||||
if (! $setting) {
|
|
||||||
$setting = InstallmentSetting::query()->create([
|
|
||||||
'default_currency' => 'EUR',
|
|
||||||
'create_activity_on_installment' => false,
|
|
||||||
'default_decision_id' => null,
|
|
||||||
'default_action_id' => null,
|
|
||||||
'activity_note_template' => 'Dodan obrok: {amount} {currency}',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$decisions = Decision::query()->orderBy('name')->get(['id', 'name']);
|
|
||||||
$actions = Action::query()
|
|
||||||
->with(['decisions:id'])
|
|
||||||
->orderBy('name')
|
|
||||||
->get()
|
|
||||||
->map(function (Action $a) {
|
|
||||||
return [
|
|
||||||
'id' => $a->id,
|
|
||||||
'name' => $a->name,
|
|
||||||
'decision_ids' => $a->decisions->pluck('id')->values(),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
return Inertia::render('Settings/Installments/Index', [
|
|
||||||
'setting' => [
|
|
||||||
'id' => $setting->id,
|
|
||||||
'default_currency' => $setting->default_currency,
|
|
||||||
'create_activity_on_installment' => (bool) $setting->create_activity_on_installment,
|
|
||||||
'default_decision_id' => $setting->default_decision_id,
|
|
||||||
'default_action_id' => $setting->default_action_id,
|
|
||||||
'activity_note_template' => $setting->activity_note_template,
|
|
||||||
],
|
|
||||||
'decisions' => $decisions,
|
|
||||||
'actions' => $actions,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(UpdateInstallmentSettingRequest $request): RedirectResponse
|
|
||||||
{
|
|
||||||
$data = $request->validated();
|
|
||||||
$setting = InstallmentSetting::query()->firstOrFail();
|
|
||||||
|
|
||||||
$data['create_activity_on_installment'] = (bool) ($data['create_activity_on_installment'] ?? false);
|
|
||||||
|
|
||||||
$setting->update($data);
|
|
||||||
|
|
||||||
return back()->with('success', 'Nastavitve shranjene.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\BankAccount;
|
||||||
use App\Models\Person\Person;
|
use App\Models\Person\Person;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
|
@ -27,6 +28,8 @@ public function update(Person $person, Request $request)
|
||||||
|
|
||||||
return back()->with('success', 'Person updated')->with('flash_method', 'PUT');
|
return back()->with('success', 'Person updated')->with('flash_method', 'PUT');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createAddress(Person $person, Request $request)
|
public function createAddress(Person $person, Request $request)
|
||||||
|
|
@ -77,6 +80,7 @@ public function deleteAddress(Person $person, int $address_id, Request $request)
|
||||||
$address = $person->addresses()->findOrFail($address_id);
|
$address = $person->addresses()->findOrFail($address_id);
|
||||||
$address->delete(); // soft delete
|
$address->delete(); // soft delete
|
||||||
|
|
||||||
|
|
||||||
return back()->with('success', 'Address deleted')->with('flash_method', 'DELETE');
|
return back()->with('success', 'Address deleted')->with('flash_method', 'DELETE');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,14 +142,8 @@ public function createEmail(Person $person, Request $request)
|
||||||
'verified_at' => 'nullable|date',
|
'verified_at' => 'nullable|date',
|
||||||
'preferences' => 'nullable|array',
|
'preferences' => 'nullable|array',
|
||||||
'meta' => 'nullable|array',
|
'meta' => 'nullable|array',
|
||||||
'decision_ids' => 'nullable|array',
|
|
||||||
'decision_ids.*' => 'integer|exists:decisions,id',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$decisionIds = array_map('intval', $attributes['decision_ids'] ?? []);
|
|
||||||
unset($attributes['decision_ids']);
|
|
||||||
$attributes['preferences'] = array_merge($attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]);
|
|
||||||
|
|
||||||
// Dedup: avoid duplicate email per person by value
|
// Dedup: avoid duplicate email per person by value
|
||||||
$email = $person->emails()->firstOrCreate([
|
$email = $person->emails()->firstOrCreate([
|
||||||
'value' => $attributes['value'],
|
'value' => $attributes['value'],
|
||||||
|
|
@ -166,16 +164,10 @@ public function updateEmail(Person $person, int $email_id, Request $request)
|
||||||
'verified_at' => 'nullable|date',
|
'verified_at' => 'nullable|date',
|
||||||
'preferences' => 'nullable|array',
|
'preferences' => 'nullable|array',
|
||||||
'meta' => 'nullable|array',
|
'meta' => 'nullable|array',
|
||||||
'decision_ids' => 'nullable|array',
|
|
||||||
'decision_ids.*' => 'integer|exists:decisions,id',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$email = $person->emails()->findOrFail($email_id);
|
$email = $person->emails()->findOrFail($email_id);
|
||||||
|
|
||||||
$decisionIds = array_map('intval', $attributes['decision_ids'] ?? []);
|
|
||||||
unset($attributes['decision_ids']);
|
|
||||||
$attributes['preferences'] = array_merge($email->preferences ?? [], $attributes['preferences'] ?? [], ['decision_ids' => $decisionIds]);
|
|
||||||
|
|
||||||
$email->update($attributes);
|
$email->update($attributes);
|
||||||
|
|
||||||
return back()->with('success', 'Email updated successfully')->with('flash_method', 'PUT');
|
return back()->with('success', 'Email updated successfully')->with('flash_method', 'PUT');
|
||||||
|
|
@ -212,8 +204,10 @@ public function createTrr(Person $person, Request $request)
|
||||||
// Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided
|
// Create without dedup (IBAN may be null or vary); could dedup by IBAN if provided
|
||||||
$trr = $person->bankAccounts()->create($attributes);
|
$trr = $person->bankAccounts()->create($attributes);
|
||||||
|
|
||||||
|
|
||||||
return back()->with('success', 'TRR added successfully')->with('flash_method', 'POST');
|
return back()->with('success', 'TRR added successfully')->with('flash_method', 'POST');
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updateTrr(Person $person, int $trr_id, Request $request)
|
public function updateTrr(Person $person, int $trr_id, Request $request)
|
||||||
|
|
@ -244,6 +238,7 @@ public function deleteTrr(Person $person, int $trr_id, Request $request)
|
||||||
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
$trr = $person->bankAccounts()->findOrFail($trr_id);
|
||||||
$trr->delete();
|
$trr->delete();
|
||||||
|
|
||||||
|
|
||||||
return back()->with('success', 'TRR deleted')->with('flash_method', 'DELETE');
|
return back()->with('success', 'TRR deleted')->with('flash_method', 'DELETE');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,6 @@ public function share(Request $request): array
|
||||||
'info' => fn () => $request->session()->get('info'),
|
'info' => fn () => $request->session()->get('info'),
|
||||||
'method' => fn () => $request->session()->get('flash_method'), // HTTP method for toast styling
|
'method' => fn () => $request->session()->get('flash_method'), // HTTP method for toast styling
|
||||||
],
|
],
|
||||||
'callLaterCount' => function () use ($request) {
|
|
||||||
if (! $request->user()) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return \App\Models\CallLater::query()
|
|
||||||
->whereNull('completed_at')
|
|
||||||
->count();
|
|
||||||
},
|
|
||||||
'notifications' => function () use ($request) {
|
'notifications' => function () use ($request) {
|
||||||
try {
|
try {
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class StoreInstallmentRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'amount' => ['required', 'numeric', 'min:0.01'],
|
|
||||||
'currency' => ['nullable', 'string', 'size:3'],
|
|
||||||
'reference' => ['nullable', 'string', 'max:100'],
|
|
||||||
'installment_at' => ['nullable', 'date'],
|
|
||||||
'meta' => ['nullable', 'array'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Requests;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
|
|
||||||
class UpdateInstallmentSettingRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'default_currency' => ['required', 'string', 'size:3'],
|
|
||||||
'create_activity_on_installment' => ['sometimes', 'boolean'],
|
|
||||||
'default_decision_id' => ['nullable', 'integer', 'exists:decisions,id'],
|
|
||||||
'default_action_id' => ['nullable', 'integer', 'exists:actions,id'],
|
|
||||||
'activity_note_template' => ['nullable', 'string', 'max:255'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,10 +10,9 @@
|
||||||
|
|
||||||
class Account extends Model
|
class Account extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/** @use HasFactory<\Database\Factories\Person/AccountFactory> */
|
/** @use HasFactory<\Database\Factories\Person/AccountFactory> */
|
||||||
use SoftDeletes;
|
use SoftDeletes;
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'reference',
|
'reference',
|
||||||
|
|
@ -59,11 +58,6 @@ public function payments(): HasMany
|
||||||
return $this->hasMany(\App\Models\Payment::class);
|
return $this->hasMany(\App\Models\Payment::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function installments(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(\App\Models\Installment::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function bookings(): HasMany
|
public function bookings(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(\App\Models\Booking::class);
|
return $this->hasMany(\App\Models\Booking::class);
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ class Activity extends Model
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'due_date',
|
'due_date',
|
||||||
'call_back_at',
|
|
||||||
'amount',
|
'amount',
|
||||||
'note',
|
'note',
|
||||||
'action_id',
|
'action_id',
|
||||||
|
|
@ -28,13 +27,6 @@ class Activity extends Model
|
||||||
'client_case_id',
|
'client_case_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
/*protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'call_back_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}*/
|
|
||||||
|
|
||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
'action_id',
|
'action_id',
|
||||||
'decision_id',
|
'decision_id',
|
||||||
|
|
@ -154,9 +146,4 @@ public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(\App\Models\User::class);
|
return $this->belongsTo(\App\Models\User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function callLaters(): \Illuminate\Database\Eloquent\Relations\HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(\App\Models\CallLater::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
class CallLater extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = [
|
|
||||||
'activity_id',
|
|
||||||
'client_case_id',
|
|
||||||
'contract_id',
|
|
||||||
'user_id',
|
|
||||||
'call_back_at',
|
|
||||||
'completed_at',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'call_back_at' => 'datetime',
|
|
||||||
'completed_at' => 'datetime',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function activity(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Activity::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clientCase(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(ClientCase::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function contract(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Contract::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(User::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
|
|
||||||
class Installment extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'account_id',
|
|
||||||
'amount',
|
|
||||||
'balance_before',
|
|
||||||
'currency',
|
|
||||||
'reference',
|
|
||||||
'installment_at',
|
|
||||||
'meta',
|
|
||||||
'created_by',
|
|
||||||
'activity_id',
|
|
||||||
];
|
|
||||||
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'installment_at' => 'datetime',
|
|
||||||
'meta' => 'array',
|
|
||||||
'amount' => 'decimal:4',
|
|
||||||
'balance_before' => 'decimal:4',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function account(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Account::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function activity(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Activity::class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
|
|
||||||
class InstallmentSetting extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $fillable = [
|
|
||||||
'default_currency',
|
|
||||||
'create_activity_on_installment',
|
|
||||||
'default_decision_id',
|
|
||||||
'default_action_id',
|
|
||||||
'activity_note_template',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
@ -59,23 +59,10 @@ public function maybeQueue(Activity $activity, bool $sendFlag = true, array $opt
|
||||||
// Resolve eligible recipients: client's person emails with receive_auto_mails = true
|
// Resolve eligible recipients: client's person emails with receive_auto_mails = true
|
||||||
$recipients = [];
|
$recipients = [];
|
||||||
if ($client && $client->person) {
|
if ($client && $client->person) {
|
||||||
$emails = Email::query()
|
$recipients = Email::query()
|
||||||
->where('person_id', $client->person->id)
|
->where('person_id', $client->person->id)
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->where('receive_auto_mails', true)
|
->where('receive_auto_mails', true)
|
||||||
->get(['value', 'preferences']);
|
|
||||||
|
|
||||||
$recipients = $emails
|
|
||||||
->filter(function (Email $email) use ($decision): bool {
|
|
||||||
$decisionIds = $email->preferences['decision_ids'] ?? [];
|
|
||||||
|
|
||||||
// Empty list means "all decisions" — always receive
|
|
||||||
if (empty($decisionIds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return in_array((int) $decision->id, array_map('intval', $decisionIds), true);
|
|
||||||
})
|
|
||||||
->pluck('value')
|
->pluck('value')
|
||||||
->map(fn ($v) => strtolower(trim((string) $v)))
|
->map(fn ($v) => strtolower(trim((string) $v)))
|
||||||
->filter(fn ($v) => filter_var($v, FILTER_VALIDATE_EMAIL))
|
->filter(fn ($v) => filter_var($v, FILTER_VALIDATE_EMAIL))
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,9 @@
|
||||||
class ClientCaseDataService
|
class ClientCaseDataService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Get contracts for a client case with optional segment filtering.
|
* Get paginated contracts for a client case with optional segment filtering.
|
||||||
*/
|
*/
|
||||||
public function getContracts(ClientCase $clientCase, ?int $segmentId = null): Collection
|
public function getContracts(ClientCase $clientCase, ?int $segmentId = null, int $perPage = 50): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$query = $clientCase->contracts()
|
$query = $clientCase->contracts()
|
||||||
->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'meta', 'active', 'type_id', 'client_case_id', 'created_at'])
|
->select(['id', 'uuid', 'reference', 'start_date', 'end_date', 'description', 'meta', 'active', 'type_id', 'client_case_id', 'created_at'])
|
||||||
|
|
@ -40,8 +40,9 @@ public function getContracts(ClientCase $clientCase, ?int $segmentId = null): Co
|
||||||
$query->forSegment($segmentId);
|
$query->forSegment($segmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$perPage = max(1, min(100, $perPage));
|
||||||
|
|
||||||
return $query->get();
|
return $query->paginate($perPage, ['*'], 'contracts_page')->withQueryString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\DecisionEvents\Handlers;
|
|
||||||
|
|
||||||
use App\Models\CallLater;
|
|
||||||
use App\Services\DecisionEvents\Contracts\DecisionEventHandler;
|
|
||||||
use App\Services\DecisionEvents\DecisionEventContext;
|
|
||||||
|
|
||||||
class CallLaterHandler implements DecisionEventHandler
|
|
||||||
{
|
|
||||||
public function handle(DecisionEventContext $context, array $config = []): void
|
|
||||||
{
|
|
||||||
$activity = $context->activity;
|
|
||||||
|
|
||||||
if (empty($activity->call_back_at)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CallLater::create([
|
|
||||||
'activity_id' => $activity->id,
|
|
||||||
'client_case_id' => $activity->client_case_id,
|
|
||||||
'contract_id' => $activity->contract_id,
|
|
||||||
'user_id' => $activity->user_id,
|
|
||||||
'call_back_at' => $activity->call_back_at,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -17,19 +17,15 @@ class Registry
|
||||||
'add_segment' => AddSegmentHandler::class,
|
'add_segment' => AddSegmentHandler::class,
|
||||||
'archive_contract' => \App\Services\DecisionEvents\Handlers\ArchiveContractHandler::class,
|
'archive_contract' => \App\Services\DecisionEvents\Handlers\ArchiveContractHandler::class,
|
||||||
'end_field_job' => \App\Services\DecisionEvents\Handlers\EndFieldJobHandler::class,
|
'end_field_job' => \App\Services\DecisionEvents\Handlers\EndFieldJobHandler::class,
|
||||||
'add_call_later' => \App\Services\DecisionEvents\Handlers\CallLaterHandler::class,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public static function resolve(string $key): DecisionEventHandler
|
public static function resolve(string $key): DecisionEventHandler
|
||||||
{
|
{
|
||||||
$key = trim(strtolower($key));
|
$key = trim(strtolower($key));
|
||||||
$class = static::$map[$key] ?? null;
|
$class = static::$map[$key] ?? null;
|
||||||
if (! $class) {
|
if (! $class || ! class_exists($class)) {
|
||||||
throw new InvalidArgumentException("Unknown decision event handler for key: {$key}");
|
throw new InvalidArgumentException("Unknown decision event handler for key: {$key}");
|
||||||
}
|
}
|
||||||
if (! class_exists($class)) {
|
|
||||||
throw new InvalidArgumentException("Handler class {$class} for key {$key} does not exist (check autoload)");
|
|
||||||
}
|
|
||||||
$handler = app($class);
|
$handler = app($class);
|
||||||
if (! $handler instanceof DecisionEventHandler) {
|
if (! $handler instanceof DecisionEventHandler) {
|
||||||
throw new InvalidArgumentException("Handler for key {$key} must implement DecisionEventHandler");
|
throw new InvalidArgumentException("Handler for key {$key} must implement DecisionEventHandler");
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::table('activities', function (Blueprint $table) {
|
|
||||||
$table->dateTime('call_back_at')->nullable()->after('due_date');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::table('activities', function (Blueprint $table) {
|
|
||||||
$table->dropColumn('call_back_at');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('call_laters', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('activity_id')->constrained('activities')->cascadeOnDelete();
|
|
||||||
$table->foreignId('client_case_id')->constrained('client_cases')->cascadeOnDelete();
|
|
||||||
$table->foreignId('contract_id')->nullable()->constrained('contracts')->nullOnDelete();
|
|
||||||
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
|
|
||||||
$table->dateTime('call_back_at');
|
|
||||||
$table->dateTime('completed_at')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('call_laters');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('installments', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->foreignId('account_id')->constrained('accounts')->cascadeOnDelete();
|
|
||||||
$table->decimal('amount', 20, 4);
|
|
||||||
$table->decimal('balance_before', 20, 4)->nullable();
|
|
||||||
$table->string('currency', 3)->default('EUR');
|
|
||||||
$table->string('reference', 100)->nullable();
|
|
||||||
$table->timestamp('installment_at')->nullable();
|
|
||||||
$table->json('meta')->nullable();
|
|
||||||
$table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
|
|
||||||
$table->foreignId('activity_id')->nullable()->constrained('activities')->nullOnDelete();
|
|
||||||
$table->softDeletes();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('installments');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('installment_settings', function (Blueprint $table): void {
|
|
||||||
$table->id();
|
|
||||||
$table->string('default_currency', 3)->default('EUR');
|
|
||||||
$table->boolean('create_activity_on_installment')->default(false);
|
|
||||||
$table->foreignId('default_decision_id')->nullable()->constrained('decisions')->nullOnDelete();
|
|
||||||
$table->foreignId('default_action_id')->nullable()->constrained('actions')->nullOnDelete();
|
|
||||||
$table->string('activity_note_template', 255)->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('installment_settings');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -31,11 +31,6 @@ public function run(): void
|
||||||
'name' => 'End field job',
|
'name' => 'End field job',
|
||||||
'description' => 'Dispatches a queued job to finalize field-related processing (implementation-specific).',
|
'description' => 'Dispatches a queued job to finalize field-related processing (implementation-specific).',
|
||||||
],
|
],
|
||||||
[
|
|
||||||
'key' => 'add_call_later',
|
|
||||||
'name' => 'Klic kasneje',
|
|
||||||
'description' => 'Ustvari zapis za povratni klic ob določenem datumu in uri.',
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
foreach ($rows as $row) {
|
||||||
|
|
|
||||||
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -6029,6 +6029,24 @@
|
||||||
"which": "bin/which"
|
"which": "bin/which"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"yaml": "bin.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/eemeli"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@
|
||||||
<testsuite name="Feature">
|
<testsuite name="Feature">
|
||||||
<directory>tests/Feature</directory>
|
<directory>tests/Feature</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="Pure">
|
|
||||||
<directory>tests/Pure</directory>
|
|
||||||
</testsuite>
|
|
||||||
</testsuites>
|
</testsuites>
|
||||||
<source>
|
<source>
|
||||||
<include>
|
<include>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
} from "@tanstack/vue-table";
|
} from "@tanstack/vue-table";
|
||||||
import { valueUpdater } from "@/lib/utils";
|
import { valueUpdater } from "@/lib/utils";
|
||||||
import DataTableColumnHeader from "./DataTableColumnHeader.vue";
|
import DataTableColumnHeader from "./DataTableColumnHeader.vue";
|
||||||
import DataTablePaginationClient from "./DataTablePaginationClient.vue";
|
import DataTablePagination from "./DataTablePagination.vue";
|
||||||
import DataTableViewOptions from "./DataTableViewOptions.vue";
|
import DataTableViewOptions from "./DataTableViewOptions.vue";
|
||||||
import DataTableToolbar from "./DataTableToolbar.vue";
|
import DataTableToolbar from "./DataTableToolbar.vue";
|
||||||
import SkeletonTable from "../Skeleton/SkeletonTable.vue";
|
import SkeletonTable from "../Skeleton/SkeletonTable.vue";
|
||||||
|
|
@ -618,14 +618,7 @@ defineExpose({
|
||||||
|
|
||||||
<!-- Client-side pagination -->
|
<!-- Client-side pagination -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<DataTablePaginationClient
|
<DataTablePagination :table="table" />
|
||||||
:current-page="table.getState().pagination.pageIndex"
|
|
||||||
:last-page="table.getPageCount()"
|
|
||||||
:total="table.getFilteredRowModel().rows.length"
|
|
||||||
:showing-from="table.getFilteredSelectedRowModel().rows.length"
|
|
||||||
:showing-to="table.getFilteredRowModel().rows.length"
|
|
||||||
:table="table"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ const props = defineProps({
|
||||||
showGoto: { type: Boolean, default: true },
|
showGoto: { type: Boolean, default: true },
|
||||||
maxPageLinks: { type: Number, default: 5 },
|
maxPageLinks: { type: Number, default: 5 },
|
||||||
perPage: { type: Number, default: 10 },
|
perPage: { type: Number, default: 10 },
|
||||||
table: { type: Object, required: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["update:page"]);
|
const emit = defineEmits(["update:page"]);
|
||||||
|
|
@ -35,7 +34,7 @@ function goToPageInput() {
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
if (!Number.isFinite(n)) return;
|
if (!Number.isFinite(n)) return;
|
||||||
const target = Math.max(1, Math.min(props.lastPage, Math.floor(n)));
|
const target = Math.max(1, Math.min(props.lastPage, Math.floor(n)));
|
||||||
if (target !== props.currentPage) props.table.setPageIndex(target - 1);
|
if (target !== props.currentPage) setPage(target);
|
||||||
gotoInput.value = "";
|
gotoInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,17 +136,14 @@ function setPage(p) {
|
||||||
>
|
>
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
<!-- First -->
|
<!-- First -->
|
||||||
<PaginationFirst
|
<PaginationFirst :disabled="currentPage <= 1" @click="setPage(1)">
|
||||||
:disabled="!table.getCanPreviousPage()"
|
|
||||||
@click="table.setPageIndex(0)"
|
|
||||||
>
|
|
||||||
<ChevronsLeft />
|
<ChevronsLeft />
|
||||||
</PaginationFirst>
|
</PaginationFirst>
|
||||||
|
|
||||||
<!-- Previous -->
|
<!-- Previous -->
|
||||||
<PaginationPrevious
|
<PaginationPrevious
|
||||||
:disabled="!table.getCanPreviousPage()"
|
:disabled="currentPage <= 1"
|
||||||
@click="table.previousPage()"
|
@click="setPage(currentPage - 1)"
|
||||||
>
|
>
|
||||||
<ChevronLeft />
|
<ChevronLeft />
|
||||||
</PaginationPrevious>
|
</PaginationPrevious>
|
||||||
|
|
@ -158,22 +154,25 @@ function setPage(p) {
|
||||||
<PaginationItem
|
<PaginationItem
|
||||||
v-else
|
v-else
|
||||||
:value="item"
|
:value="item"
|
||||||
:is-active="currentPage === index"
|
:is-active="currentPage === item"
|
||||||
@click="table.setPageIndex(index)"
|
@click="setPage(item)"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</PaginationItem>
|
</PaginationItem>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Next -->
|
<!-- Next -->
|
||||||
<PaginationNext :disabled="!table.getCanNextPage()" @click="table.nextPage()">
|
<PaginationNext
|
||||||
|
:disabled="currentPage >= lastPage"
|
||||||
|
@click="setPage(currentPage + 1)"
|
||||||
|
>
|
||||||
<ChevronRight />
|
<ChevronRight />
|
||||||
</PaginationNext>
|
</PaginationNext>
|
||||||
|
|
||||||
<!-- Last -->
|
<!-- Last -->
|
||||||
<PaginationLast
|
<PaginationLast
|
||||||
:disabled="!table.getCanNextPage()"
|
:disabled="currentPage >= lastPage"
|
||||||
@click="table.setPageIndex(table.getPageCount() - 1)"
|
@click="setPage(lastPage)"
|
||||||
>
|
>
|
||||||
<ChevronsRight />
|
<ChevronsRight />
|
||||||
</PaginationLast>
|
</PaginationLast>
|
||||||
|
|
@ -192,7 +191,7 @@ function setPage(p) {
|
||||||
:max="lastPage"
|
:max="lastPage"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
class="w-10 h-full text-sm text-center bg-transparent border-0 outline-none focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
class="w-10 h-full text-sm text-center bg-transparent border-0 outline-none focus:outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
:placeholder="String(currentPage + 1)"
|
:placeholder="String(currentPage)"
|
||||||
aria-label="Pojdi na stran"
|
aria-label="Pojdi na stran"
|
||||||
@keyup.enter="goToPageInput"
|
@keyup.enter="goToPageInput"
|
||||||
@blur="goToPageInput"
|
@blur="goToPageInput"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,11 @@
|
||||||
import { computed, ref, useAttrs } from "vue";
|
import { computed, ref, useAttrs } from "vue";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Calendar } from "@/Components/ui/calendar";
|
import { Calendar } from "@/Components/ui/calendar";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/Components/ui/popover";
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/Components/ui/popover";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CalendarIcon } from "lucide-vue-next";
|
import { CalendarIcon } from "lucide-vue-next";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
@ -82,9 +86,7 @@ const toCalendarDate = (value) => {
|
||||||
// Convert CalendarDate to ISO string (YYYY-MM-DD)
|
// Convert CalendarDate to ISO string (YYYY-MM-DD)
|
||||||
const fromCalendarDate = (calendarDate) => {
|
const fromCalendarDate = (calendarDate) => {
|
||||||
if (!calendarDate) return null;
|
if (!calendarDate) return null;
|
||||||
return `${String(calendarDate.year).padStart(4, "0")}-${String(
|
return `${String(calendarDate.year).padStart(4, "0")}-${String(calendarDate.month).padStart(2, "0")}-${String(calendarDate.day).padStart(2, "0")}`;
|
||||||
calendarDate.month
|
|
||||||
).padStart(2, "0")}-${String(calendarDate.day).padStart(2, "0")}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const calendarDate = computed({
|
const calendarDate = computed({
|
||||||
|
|
@ -140,10 +142,11 @@ const open = ref(false);
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-auto p-0" align="start">
|
<PopoverContent class="w-auto p-0" align="start">
|
||||||
<Calendar locale="si-SI" v-model="calendarDate" :disabled="disabled" />
|
<Calendar v-model="calendarDate" :disabled="disabled" />
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<p v-if="error" class="mt-1 text-sm text-red-600">
|
<p v-if="error" class="mt-1 text-sm text-red-600">
|
||||||
{{ Array.isArray(error) ? error[0] : error }}
|
{{ Array.isArray(error) ? error[0] : error }}
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onUnmounted } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@/Components/ui/dialog";
|
} from "@/Components/ui/dialog";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Badge } from "../ui/badge";
|
import { Badge } from "../ui/badge";
|
||||||
import { Loader2, RotateCcwIcon } from "lucide-vue-next";
|
import { Loader2 } from "lucide-vue-next";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -26,141 +26,6 @@ const loading = ref(false);
|
||||||
const previewGenerating = ref(false);
|
const previewGenerating = ref(false);
|
||||||
const previewError = ref("");
|
const previewError = ref("");
|
||||||
|
|
||||||
// Image viewer – zoom & pan state
|
|
||||||
const containerRef = ref(null);
|
|
||||||
const imageRef = ref(null);
|
|
||||||
const imageScale = ref(1);
|
|
||||||
const translateX = ref(0);
|
|
||||||
const translateY = ref(0);
|
|
||||||
const fitScale = ref(1);
|
|
||||||
const isDragging = ref(false);
|
|
||||||
const hasMoved = ref(false);
|
|
||||||
const dragStartX = ref(0);
|
|
||||||
const dragStartY = ref(0);
|
|
||||||
const dragStartTX = ref(0);
|
|
||||||
const dragStartTY = ref(0);
|
|
||||||
|
|
||||||
const MAX_SCALE = 8;
|
|
||||||
|
|
||||||
const imageCursorClass = computed(() => {
|
|
||||||
if (isDragging.value && hasMoved.value) return "cursor-grabbing";
|
|
||||||
if (imageScale.value > fitScale.value + 0.01) return "cursor-grab";
|
|
||||||
return "cursor-default";
|
|
||||||
});
|
|
||||||
|
|
||||||
const initImageView = () => {
|
|
||||||
const container = containerRef.value;
|
|
||||||
const img = imageRef.value;
|
|
||||||
if (!container || !img) return;
|
|
||||||
const cW = container.clientWidth;
|
|
||||||
const cH = container.clientHeight;
|
|
||||||
const iW = img.naturalWidth || cW;
|
|
||||||
const iH = img.naturalHeight || cH;
|
|
||||||
const fs = Math.min(cW / iW, cH / iH);
|
|
||||||
fitScale.value = fs;
|
|
||||||
imageScale.value = fs;
|
|
||||||
translateX.value = (cW - iW * fs) / 2;
|
|
||||||
translateY.value = (cH - iH * fs) / 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetImageView = () => {
|
|
||||||
initImageView();
|
|
||||||
};
|
|
||||||
|
|
||||||
const clampTranslate = (tx, ty, scale) => {
|
|
||||||
const container = containerRef.value;
|
|
||||||
const img = imageRef.value;
|
|
||||||
if (!container || !img) return { tx, ty };
|
|
||||||
const cW = container.clientWidth;
|
|
||||||
const cH = container.clientHeight;
|
|
||||||
const iW = img.naturalWidth * scale;
|
|
||||||
const iH = img.naturalHeight * scale;
|
|
||||||
// When image fills the container: clamp so image edges stay within container.
|
|
||||||
// When image is smaller than container: keep it centered.
|
|
||||||
const minX = iW >= cW ? cW - iW : (cW - iW) / 2;
|
|
||||||
const maxX = iW >= cW ? 0 : (cW - iW) / 2;
|
|
||||||
const minY = iH >= cH ? cH - iH : (cH - iH) / 2;
|
|
||||||
const maxY = iH >= cH ? 0 : (cH - iH) / 2;
|
|
||||||
return {
|
|
||||||
tx: Math.min(maxX, Math.max(minX, tx)),
|
|
||||||
ty: Math.min(maxY, Math.max(minY, ty)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const zoomAt = (mx, my, factor) => {
|
|
||||||
const img = imageRef.value;
|
|
||||||
const iW = img?.naturalWidth ?? 1;
|
|
||||||
const iH = img?.naturalHeight ?? 1;
|
|
||||||
const raw = imageScale.value * factor;
|
|
||||||
const newScale = Math.min(MAX_SCALE, Math.max(fitScale.value, raw));
|
|
||||||
if (newScale === imageScale.value) return;
|
|
||||||
let tx = mx - ((mx - translateX.value) / imageScale.value) * newScale;
|
|
||||||
let ty = my - ((my - translateY.value) / imageScale.value) * newScale;
|
|
||||||
const clamped = clampTranslate(tx, ty, newScale);
|
|
||||||
translateX.value = clamped.tx;
|
|
||||||
translateY.value = clamped.ty;
|
|
||||||
imageScale.value = newScale;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mousePos = (e) => {
|
|
||||||
const rect = containerRef.value.getBoundingClientRect();
|
|
||||||
return { mx: e.clientX - rect.left, my: e.clientY - rect.top };
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
|
||||||
initImageView();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleWheel = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { mx, my } = mousePos(e);
|
|
||||||
zoomAt(mx, my, e.deltaY < 0 ? 1.2 : 1 / 1.2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (e) => {
|
|
||||||
if (!isDragging.value) return;
|
|
||||||
const dx = e.clientX - dragStartX.value;
|
|
||||||
const dy = e.clientY - dragStartY.value;
|
|
||||||
if (!hasMoved.value && (Math.abs(dx) > 3 || Math.abs(dy) > 3)) {
|
|
||||||
hasMoved.value = true;
|
|
||||||
}
|
|
||||||
if (hasMoved.value) {
|
|
||||||
const clamped = clampTranslate(
|
|
||||||
dragStartTX.value + dx,
|
|
||||||
dragStartTY.value + dy,
|
|
||||||
imageScale.value
|
|
||||||
);
|
|
||||||
translateX.value = clamped.tx;
|
|
||||||
translateY.value = clamped.ty;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
setTimeout(() => {
|
|
||||||
hasMoved.value = false;
|
|
||||||
}, 0);
|
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseDown = (e) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
isDragging.value = true;
|
|
||||||
hasMoved.value = false;
|
|
||||||
dragStartX.value = e.clientX;
|
|
||||||
dragStartY.value = e.clientY;
|
|
||||||
dragStartTX.value = translateX.value;
|
|
||||||
dragStartTY.value = translateY.value;
|
|
||||||
window.addEventListener("mousemove", onMouseMove);
|
|
||||||
window.addEventListener("mouseup", onMouseUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener("mousemove", onMouseMove);
|
|
||||||
window.removeEventListener("mouseup", onMouseUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileExtension = computed(() => {
|
const fileExtension = computed(() => {
|
||||||
if (props.filename) {
|
if (props.filename) {
|
||||||
return props.filename.split(".").pop()?.toLowerCase() || "";
|
return props.filename.split(".").pop()?.toLowerCase() || "";
|
||||||
|
|
@ -253,10 +118,6 @@ watch(
|
||||||
previewGenerating.value = false;
|
previewGenerating.value = false;
|
||||||
previewError.value = "";
|
previewError.value = "";
|
||||||
docxPreviewUrl.value = "";
|
docxPreviewUrl.value = "";
|
||||||
imageScale.value = 1;
|
|
||||||
translateX.value = 0;
|
|
||||||
translateY.value = 0;
|
|
||||||
fitScale.value = 1;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
|
|
@ -318,50 +179,11 @@ watch(
|
||||||
|
|
||||||
<!-- Image Viewer -->
|
<!-- Image Viewer -->
|
||||||
<template v-else-if="viewerType === 'image' && props.src">
|
<template v-else-if="viewerType === 'image' && props.src">
|
||||||
<div
|
<img
|
||||||
ref="containerRef"
|
:src="props.src"
|
||||||
class="relative h-full overflow-hidden select-none"
|
:alt="props.title"
|
||||||
:class="imageCursorClass"
|
class="max-w-full max-h-full mx-auto object-contain"
|
||||||
@mousedown="handleMouseDown"
|
/>
|
||||||
@wheel.prevent="handleWheel"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
ref="imageRef"
|
|
||||||
:src="props.src"
|
|
||||||
:alt="props.title"
|
|
||||||
class="absolute top-0 left-0 max-w-none pointer-events-none"
|
|
||||||
:style="{
|
|
||||||
transformOrigin: '0 0',
|
|
||||||
transform: `translate(${translateX}px, ${translateY}px) scale(${imageScale})`,
|
|
||||||
transition: isDragging ? 'none' : 'transform 0.12s ease',
|
|
||||||
}"
|
|
||||||
@load="handleImageLoad"
|
|
||||||
/>
|
|
||||||
<!-- Zoom level badge -->
|
|
||||||
<div
|
|
||||||
class="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded pointer-events-none"
|
|
||||||
>
|
|
||||||
{{ Math.round(imageScale * 100) }}%
|
|
||||||
</div>
|
|
||||||
<!-- Reset button -->
|
|
||||||
<Button
|
|
||||||
v-if="imageScale > fitScale + 0.01"
|
|
||||||
size="icon-sm"
|
|
||||||
variant="secondary"
|
|
||||||
class="absolute top-2 right-2 opacity-70 hover:opacity-100"
|
|
||||||
title="Ponastavi pogled"
|
|
||||||
@click.stop="resetImageView"
|
|
||||||
>
|
|
||||||
<RotateCcwIcon class="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
<!-- Hint -->
|
|
||||||
<div
|
|
||||||
v-if="imageScale <= fitScale + 0.01"
|
|
||||||
class="absolute bottom-2 left-1/2 -translate-x-1/2 bg-black/40 text-white text-xs px-2 py-1 rounded pointer-events-none select-none"
|
|
||||||
>
|
|
||||||
Kolesce za povečavo / pomanjšavo · Povleči za premik
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Text/CSV/XML Viewer -->
|
<!-- Text/CSV/XML Viewer -->
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,7 @@ import { computed, ref, watch } from "vue";
|
||||||
import { useForm, Field as FormField } from "vee-validate";
|
import { useForm, Field as FormField } from "vee-validate";
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { router, usePage } from "@inertiajs/vue3";
|
import { router } from "@inertiajs/vue3";
|
||||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
|
||||||
import CreateDialog from "../Dialogs/CreateDialog.vue";
|
import CreateDialog from "../Dialogs/CreateDialog.vue";
|
||||||
import UpdateDialog from "../Dialogs/UpdateDialog.vue";
|
import UpdateDialog from "../Dialogs/UpdateDialog.vue";
|
||||||
import SectionTitle from "../SectionTitle.vue";
|
import SectionTitle from "../SectionTitle.vue";
|
||||||
|
|
@ -28,22 +27,12 @@ const props = defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(["close"]);
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
// Decisions with auto_mail = true from shared Inertia data
|
|
||||||
const page = usePage();
|
|
||||||
const decisionOptions = computed(() =>
|
|
||||||
(page.props.auto_mail_decisions ?? []).map((d) => ({
|
|
||||||
value: String(d.id),
|
|
||||||
label: d.name,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Zod schema for form validation
|
// Zod schema for form validation
|
||||||
const formSchema = toTypedSchema(
|
const formSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
value: z.string().email("E-pošta mora biti veljavna.").min(1, "E-pošta je obvezna."),
|
value: z.string().email("E-pošta mora biti veljavna.").min(1, "E-pošta je obvezna."),
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
receive_auto_mails: z.boolean().optional(),
|
receive_auto_mails: z.boolean().optional(),
|
||||||
decision_ids: z.array(z.string()).optional().default([]),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -54,13 +43,9 @@ const form = useForm({
|
||||||
value: "",
|
value: "",
|
||||||
label: "",
|
label: "",
|
||||||
receive_auto_mails: false,
|
receive_auto_mails: false,
|
||||||
decision_ids: [],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Whether to limit sending to specific decisions (UI-only toggle)
|
|
||||||
const limitToDecisions = ref(false);
|
|
||||||
|
|
||||||
const processing = ref(false);
|
const processing = ref(false);
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
|
|
@ -72,44 +57,22 @@ const close = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
limitToDecisions.value = false;
|
|
||||||
form.resetForm({
|
form.resetForm({
|
||||||
values: {
|
values: {
|
||||||
value: "",
|
value: "",
|
||||||
label: "",
|
label: "",
|
||||||
receive_auto_mails: false,
|
receive_auto_mails: false,
|
||||||
decision_ids: [],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// When auto mails is disabled, collapse the decision filter
|
|
||||||
watch(
|
|
||||||
() => form.values.receive_auto_mails,
|
|
||||||
(val) => {
|
|
||||||
if (!val) {
|
|
||||||
limitToDecisions.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// When limit toggle is turned off, clear the selection
|
|
||||||
watch(limitToDecisions, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
form.setFieldValue("decision_ids", []);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
const payload = {
|
const { values } = form;
|
||||||
...form.values,
|
|
||||||
decision_ids: limitToDecisions.value ? (form.values.decision_ids ?? []) : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
route("person.email.create", props.person),
|
route("person.email.create", props.person),
|
||||||
payload,
|
values,
|
||||||
{
|
{
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -135,14 +98,11 @@ const create = async () => {
|
||||||
|
|
||||||
const update = async () => {
|
const update = async () => {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
const payload = {
|
const { values } = form;
|
||||||
...form.values,
|
|
||||||
decision_ids: limitToDecisions.value ? (form.values.decision_ids ?? []) : [],
|
|
||||||
};
|
|
||||||
|
|
||||||
router.put(
|
router.put(
|
||||||
route("person.email.update", { person: props.person, email_id: props.id }),
|
route("person.email.update", { person: props.person, email_id: props.id }),
|
||||||
payload,
|
values,
|
||||||
{
|
{
|
||||||
preserveScroll: true,
|
preserveScroll: true,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -176,13 +136,10 @@ watch(
|
||||||
const list = Array.isArray(props.person?.emails) ? props.person.emails : [];
|
const list = Array.isArray(props.person?.emails) ? props.person.emails : [];
|
||||||
const email = list.find((e) => e.id === props.id);
|
const email = list.find((e) => e.id === props.id);
|
||||||
if (email) {
|
if (email) {
|
||||||
const existingDecisionIds = (email.preferences?.decision_ids ?? []).map(String);
|
|
||||||
limitToDecisions.value = existingDecisionIds.length > 0;
|
|
||||||
form.setValues({
|
form.setValues({
|
||||||
value: email.value ?? email.email ?? email.address ?? "",
|
value: email.value ?? email.email ?? email.address ?? "",
|
||||||
label: email.label ?? "",
|
label: email.label ?? "",
|
||||||
receive_auto_mails: !!email.receive_auto_mails,
|
receive_auto_mails: !!email.receive_auto_mails,
|
||||||
decision_ids: existingDecisionIds,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|
@ -271,36 +228,6 @@ const onConfirm = () => {
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<!-- Limit to specific decisions — only shown when receive_auto_mails is on and decisions exist -->
|
|
||||||
<template v-if="(props.person?.client || isClientContext) && form.values.receive_auto_mails && decisionOptions.length > 0">
|
|
||||||
<div class="flex flex-row items-start space-x-3 space-y-0">
|
|
||||||
<Switch
|
|
||||||
:model-value="limitToDecisions"
|
|
||||||
@update:model-value="(val) => (limitToDecisions = val)"
|
|
||||||
/>
|
|
||||||
<div class="space-y-1 leading-none">
|
|
||||||
<label class="text-sm font-medium leading-none cursor-pointer" @click="limitToDecisions = !limitToDecisions">
|
|
||||||
Omeji na posamezne odločitve
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormField v-if="limitToDecisions" v-slot="{ value, handleChange }" name="decision_ids">
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Odločitve, za katere se pošlje e-pošta</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<AppMultiSelect
|
|
||||||
:model-value="value ?? []"
|
|
||||||
:items="decisionOptions"
|
|
||||||
placeholder="Izberi odločitve..."
|
|
||||||
@update:model-value="handleChange"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
</FormField>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
InboxIcon,
|
InboxIcon,
|
||||||
AtSignIcon,
|
AtSignIcon,
|
||||||
BookUserIcon,
|
BookUserIcon,
|
||||||
|
MessageSquareIcon,
|
||||||
ArrowLeftIcon,
|
ArrowLeftIcon,
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
|
|
@ -210,6 +211,13 @@ const navGroups = computed(() => [
|
||||||
icon: Settings2Icon,
|
icon: Settings2Icon,
|
||||||
active: ["admin.sms-profiles.index"],
|
active: ["admin.sms-profiles.index"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "admin.packages.index",
|
||||||
|
label: "SMS paketi",
|
||||||
|
route: "admin.packages.index",
|
||||||
|
icon: MessageSquareIcon,
|
||||||
|
active: ["admin.packages.index", "admin.packages.show"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ import { SettingsIcon } from "lucide-vue-next";
|
||||||
import { ShieldUserIcon } from "lucide-vue-next";
|
import { ShieldUserIcon } from "lucide-vue-next";
|
||||||
import { SmartphoneIcon } from "lucide-vue-next";
|
import { SmartphoneIcon } from "lucide-vue-next";
|
||||||
import { TabletSmartphoneIcon } from "lucide-vue-next";
|
import { TabletSmartphoneIcon } from "lucide-vue-next";
|
||||||
import { PhoneCallIcon } from "lucide-vue-next";
|
|
||||||
import { PackageIcon } from "lucide-vue-next";
|
|
||||||
import { Badge } from "@/Components/ui/badge";
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
|
|
@ -160,13 +157,6 @@ const rawMenuGroups = [
|
||||||
routeName: "segments.index",
|
routeName: "segments.index",
|
||||||
active: ["segments.index"],
|
active: ["segments.index"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "call-laters",
|
|
||||||
icon: PhoneCallIcon,
|
|
||||||
title: "Pokliči kasneje",
|
|
||||||
routeName: "callLaters.index",
|
|
||||||
active: ["callLaters.index"],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -222,13 +212,6 @@ const rawMenuGroups = [
|
||||||
routeName: "settings",
|
routeName: "settings",
|
||||||
active: ["settings", "settings.*"],
|
active: ["settings", "settings.*"],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "packages",
|
|
||||||
icon: PackageIcon,
|
|
||||||
title: "SMS paketi",
|
|
||||||
routeName: "packages.index",
|
|
||||||
active: ["packages.index", "packages.show", "packages.create"],
|
|
||||||
},
|
|
||||||
// Admin panel (roles & permissions management)
|
// Admin panel (roles & permissions management)
|
||||||
// Only shown if current user has admin role or manage-settings permission.
|
// Only shown if current user has admin role or manage-settings permission.
|
||||||
// We'll filter it out below if not authorized.
|
// We'll filter it out below if not authorized.
|
||||||
|
|
@ -285,14 +268,6 @@ function isActive(patterns) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBadge(item) {
|
|
||||||
if (item.key === "call-laters") {
|
|
||||||
return page.props.callLaterCount || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -366,18 +341,11 @@ function getBadge(item) {
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<span
|
<span
|
||||||
v-if="!sidebarCollapsed"
|
v-if="!sidebarCollapsed"
|
||||||
class="flex-1 truncate transition-opacity"
|
class="truncate transition-opacity"
|
||||||
:class="{ 'font-medium': isActive(item.active) }"
|
:class="{ 'font-medium': isActive(item.active) }"
|
||||||
>
|
>
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
|
||||||
v-if="!sidebarCollapsed && getBadge(item) > 0"
|
|
||||||
variant="destructive"
|
|
||||||
class="ml-auto shrink-0 px-1.5 py-0.5 text-xs font-mono text-amber-50"
|
|
||||||
>
|
|
||||||
{{ getBadge(item) }}
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,12 @@ const cards = [
|
||||||
route: "admin.sms-logs.index",
|
route: "admin.sms-logs.index",
|
||||||
icon: InboxIcon,
|
icon: InboxIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "SMS paketi",
|
||||||
|
description: "Kreiranje in pošiljanje serijskih SMS paketov",
|
||||||
|
route: "admin.packages.index",
|
||||||
|
icon: MessageSquareIcon,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||||
import { Link, router, useForm } from "@inertiajs/vue3";
|
import { Link, router, useForm } from "@inertiajs/vue3";
|
||||||
import { ref, computed, nextTick } from "vue";
|
import { ref, computed, nextTick } from "vue";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
@ -112,9 +112,9 @@ function submitCreate() {
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post(route("packages.store"), payload, {
|
router.post(route("admin.packages.store"), payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.visit(route("packages.index"));
|
router.visit(route("admin.packages.index"));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ async function loadContracts(url = null) {
|
||||||
if (onlyValidated.value) params.append("only_validated", "1");
|
if (onlyValidated.value) params.append("only_validated", "1");
|
||||||
params.append("per_page", perPage.value);
|
params.append("per_page", perPage.value);
|
||||||
|
|
||||||
const target = url || `${route("packages.contracts")}?${params.toString()}`;
|
const target = url || `${route("admin.packages.contracts")}?${params.toString()}`;
|
||||||
const { data: json } = await axios.get(target, {
|
const { data: json } = await axios.get(target, {
|
||||||
headers: { "X-Requested-With": "XMLHttpRequest" },
|
headers: { "X-Requested-With": "XMLHttpRequest" },
|
||||||
});
|
});
|
||||||
|
|
@ -268,7 +268,7 @@ function goToPage(page) {
|
||||||
params.append("per_page", perPage.value);
|
params.append("per_page", perPage.value);
|
||||||
params.append("page", page);
|
params.append("page", page);
|
||||||
|
|
||||||
const url = `${route("packages.contracts")}?${params.toString()}`;
|
const url = `${route("admin.packages.contracts")}?${params.toString()}`;
|
||||||
loadContracts(url);
|
loadContracts(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,9 +312,9 @@ function submitCreateFromContracts() {
|
||||||
};
|
};
|
||||||
|
|
||||||
creatingFromContracts.value = true;
|
creatingFromContracts.value = true;
|
||||||
router.post(route("packages.store-from-contracts"), payload, {
|
router.post(route("admin.packages.store-from-contracts"), payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.visit(route("packages.index"));
|
router.visit(route("admin.packages.index"));
|
||||||
},
|
},
|
||||||
onError: (errors) => {
|
onError: (errors) => {
|
||||||
const first = errors && Object.values(errors)[0];
|
const first = errors && Object.values(errors)[0];
|
||||||
|
|
@ -337,11 +337,11 @@ const numbersCount = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="Ustvari SMS paket">
|
<AdminLayout title="Ustvari SMS paket">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<Link :href="route('packages.index')">
|
<Link :href="route('admin.packages.index')">
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||||
Nazaj
|
Nazaj
|
||||||
|
|
@ -520,7 +520,7 @@ const numbersCount = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
@click="router.visit(route('packages.index'))"
|
@click="router.visit(route('admin.packages.index'))"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Prekliči
|
Prekliči
|
||||||
|
|
@ -703,7 +703,7 @@ const numbersCount = computed(() => {
|
||||||
Izbrano: {{ selectedContractIds.size }}
|
Izbrano: {{ selectedContractIds.size }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
@click="router.visit(route('packages.index'))"
|
@click="router.visit(route('admin.packages.index'))"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
Prekliči
|
Prekliči
|
||||||
|
|
@ -806,5 +806,5 @@ const numbersCount = computed(() => {
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</AppLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
import { Link, router } from "@inertiajs/vue3";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Card, CardHeader, CardTitle } from "@/Components/ui/card";
|
import { Card, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||||
|
|
@ -48,7 +48,7 @@ function getStatusVariant(status) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function goShow(id) {
|
function goShow(id) {
|
||||||
router.visit(route("packages.show", id));
|
router.visit(route("admin.packages.show", id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDeleteDialog(pkg) {
|
function openDeleteDialog(pkg) {
|
||||||
|
|
@ -60,7 +60,7 @@ function openDeleteDialog(pkg) {
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
if (!packageToDelete.value) return;
|
if (!packageToDelete.value) return;
|
||||||
deletingId.value = packageToDelete.value.id;
|
deletingId.value = packageToDelete.value.id;
|
||||||
router.delete(route("packages.destroy", packageToDelete.value.id), {
|
router.delete(route("admin.packages.destroy", packageToDelete.value.id), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.reload({ only: ["packages"] });
|
router.reload({ only: ["packages"] });
|
||||||
},
|
},
|
||||||
|
|
@ -74,7 +74,7 @@ function confirmDelete() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout title="SMS paketi">
|
<AdminLayout title="SMS paketi">
|
||||||
<Card class="mb-4">
|
<Card class="mb-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|
@ -82,7 +82,7 @@ function confirmDelete() {
|
||||||
<PackageIcon class="h-5 w-5 text-muted-foreground" />
|
<PackageIcon class="h-5 w-5 text-muted-foreground" />
|
||||||
<CardTitle>SMS paketi</CardTitle>
|
<CardTitle>SMS paketi</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<Link :href="route('packages.create')">
|
<Link :href="route('admin.packages.create')">
|
||||||
<Button>
|
<Button>
|
||||||
<PlusIcon class="h-4 w-4" />
|
<PlusIcon class="h-4 w-4" />
|
||||||
Nov paket
|
Nov paket
|
||||||
|
|
@ -109,7 +109,7 @@ function confirmDelete() {
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="packages.data"
|
:data="packages.data"
|
||||||
:meta="packages"
|
:meta="packages"
|
||||||
route-name="packages.index"
|
route-name="admin.packages.index"
|
||||||
>
|
>
|
||||||
<template #cell-name="{ row }">
|
<template #cell-name="{ row }">
|
||||||
<span class="text-sm">{{ row.name ?? "—" }}</span>
|
<span class="text-sm">{{ row.name ?? "—" }}</span>
|
||||||
|
|
@ -172,5 +172,5 @@ function confirmDelete() {
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</AppLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
import { Link, router } from "@inertiajs/vue3";
|
||||||
import { onMounted, onUnmounted, ref, computed } from "vue";
|
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||||
import {
|
import {
|
||||||
|
|
@ -88,14 +88,14 @@ function reload() {
|
||||||
|
|
||||||
function dispatchPkg() {
|
function dispatchPkg() {
|
||||||
router.post(
|
router.post(
|
||||||
route("packages.dispatch", props.package.id),
|
route("admin.packages.dispatch", props.package.id),
|
||||||
{},
|
{},
|
||||||
{ onSuccess: reload }
|
{ onSuccess: reload }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function cancelPkg() {
|
function cancelPkg() {
|
||||||
router.post(
|
router.post(
|
||||||
route("packages.cancel", props.package.id),
|
route("admin.packages.cancel", props.package.id),
|
||||||
{},
|
{},
|
||||||
{ onSuccess: reload }
|
{ onSuccess: reload }
|
||||||
);
|
);
|
||||||
|
|
@ -132,7 +132,7 @@ async function copyText(text) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AppLayout :title="`Paket #${package.id}`">
|
<AdminLayout :title="`Paket #${package.id}`">
|
||||||
<Card class="mb-4">
|
<Card class="mb-4">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|
@ -147,7 +147,7 @@ async function copyText(text) {
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button variant="ghost" size="sm" as-child>
|
<Button variant="ghost" size="sm" as-child>
|
||||||
<Link :href="route('packages.index')">
|
<Link :href="route('admin.packages.index')">
|
||||||
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
<ArrowLeftIcon class="h-4 w-4 mr-2" />
|
||||||
Nazaj
|
Nazaj
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -281,7 +281,7 @@ async function copyText(text) {
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="items.data"
|
:data="items.data"
|
||||||
:meta="items"
|
:meta="items"
|
||||||
route-name="packages.show"
|
route-name="admin.packages.show"
|
||||||
:route-params="{ id: package.id }"
|
:route-params="{ id: package.id }"
|
||||||
>
|
>
|
||||||
<template #cell-target="{ row }">
|
<template #cell-target="{ row }">
|
||||||
|
|
@ -333,5 +333,5 @@ async function copyText(text) {
|
||||||
<div v-if="refreshing" class="mt-2 text-xs text-muted-foreground">
|
<div v-if="refreshing" class="mt-2 text-xs text-muted-foreground">
|
||||||
Osveževanje ...
|
Osveževanje ...
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -2,13 +2,7 @@
|
||||||
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
import AdminLayout from "@/Layouts/AdminLayout.vue";
|
||||||
import { useForm, Link } from "@inertiajs/vue3";
|
import { useForm, Link } from "@inertiajs/vue3";
|
||||||
import { KeyRoundIcon, ArrowLeftIcon, SaveIcon } from "lucide-vue-next";
|
import { KeyRoundIcon, ArrowLeftIcon, SaveIcon } from "lucide-vue-next";
|
||||||
import {
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/Components/ui/card";
|
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Input } from "@/Components/ui/input";
|
import { Input } from "@/Components/ui/input";
|
||||||
import { Label } from "@/Components/ui/label";
|
import { Label } from "@/Components/ui/label";
|
||||||
|
|
@ -42,16 +36,12 @@ function submit() {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div
|
<div class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary">
|
||||||
class="inline-flex items-center justify-center h-10 w-10 rounded-lg bg-primary/10 text-primary"
|
|
||||||
>
|
|
||||||
<KeyRoundIcon class="h-5 w-5" />
|
<KeyRoundIcon class="h-5 w-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Uredi dovoljenje</CardTitle>
|
<CardTitle>Uredi dovoljenje</CardTitle>
|
||||||
<CardDescription
|
<CardDescription>Posodobi sistemsko dovoljenje in pripete vloge.</CardDescription>
|
||||||
>Posodobi sistemsko dovoljenje in pripete vloge.</CardDescription
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" as-child>
|
<Button variant="ghost" size="sm" as-child>
|
||||||
|
|
@ -63,6 +53,7 @@ function submit() {
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
||||||
<form @submit.prevent="submit" class="space-y-6">
|
<form @submit.prevent="submit" class="space-y-6">
|
||||||
<div class="grid sm:grid-cols-2 gap-6">
|
<div class="grid sm:grid-cols-2 gap-6">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
@ -95,19 +86,16 @@ function submit() {
|
||||||
class="flex items-center gap-2 text-sm cursor-pointer"
|
class="flex items-center gap-2 text-sm cursor-pointer"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:default-value="form.roles.includes(r.id)"
|
:value="r.id"
|
||||||
@update:model-value="
|
:checked="form.roles.includes(r.id)"
|
||||||
(checked) => {
|
@update:checked="(checked) => {
|
||||||
if (checked) form.roles.push(r.id);
|
if (checked) form.roles.push(r.id)
|
||||||
else form.roles = form.roles.filter((id) => id !== r.id);
|
else form.roles = form.roles.filter(id => id !== r.id)
|
||||||
}
|
}"
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
><span class="font-medium">{{ r.name }}</span>
|
><span class="font-medium">{{ r.name }}</span>
|
||||||
<span class="text-xs text-muted-foreground"
|
<span class="text-xs text-muted-foreground">({{ r.slug }})</span></span
|
||||||
>({{ r.slug }})</span
|
|
||||||
></span
|
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,288 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
|
||||||
import { Link, router } from "@inertiajs/vue3";
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
|
||||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
|
||||||
import CardTitle from "@/Components/ui/card/CardTitle.vue";
|
|
||||||
import { Button } from "@/Components/ui/button";
|
|
||||||
import { Input } from "@/Components/ui/input";
|
|
||||||
import InputLabel from "@/Components/InputLabel.vue";
|
|
||||||
import Pagination from "@/Components/Pagination.vue";
|
|
||||||
import {
|
|
||||||
PhoneCallIcon,
|
|
||||||
CheckIcon,
|
|
||||||
Filter,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
MoreHorizontalIcon,
|
|
||||||
} from "lucide-vue-next";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/Components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
|
||||||
import { fmtDateTime } from "@/Utilities/functions";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
callLaters: Object,
|
|
||||||
filters: Object,
|
|
||||||
});
|
|
||||||
|
|
||||||
const search = ref(props.filters?.search || "");
|
|
||||||
const dateFrom = ref(props.filters?.date_from || "");
|
|
||||||
const dateTo = ref(props.filters?.date_to || "");
|
|
||||||
const filterPopoverOpen = ref(false);
|
|
||||||
|
|
||||||
const appliedFilterCount = computed(() => {
|
|
||||||
let count = 0;
|
|
||||||
if (search.value?.trim()) count += 1;
|
|
||||||
if (dateFrom.value) count += 1;
|
|
||||||
if (dateTo.value) count += 1;
|
|
||||||
return count;
|
|
||||||
});
|
|
||||||
|
|
||||||
function applyFilters() {
|
|
||||||
filterPopoverOpen.value = false;
|
|
||||||
const params = {};
|
|
||||||
if (search.value?.trim()) {
|
|
||||||
params.search = search.value.trim();
|
|
||||||
}
|
|
||||||
if (dateFrom.value) {
|
|
||||||
params.date_from = dateFrom.value;
|
|
||||||
}
|
|
||||||
if (dateTo.value) {
|
|
||||||
params.date_to = dateTo.value;
|
|
||||||
}
|
|
||||||
router.get(route("callLaters.index"), params, {
|
|
||||||
preserveState: true,
|
|
||||||
replace: true,
|
|
||||||
preserveScroll: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFilters() {
|
|
||||||
search.value = "";
|
|
||||||
dateFrom.value = "";
|
|
||||||
dateTo.value = "";
|
|
||||||
applyFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
function markDone(item) {
|
|
||||||
router.patch(
|
|
||||||
route("callLaters.complete", item.id),
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
preserveScroll: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAndComplete(item) {
|
|
||||||
router.patch(
|
|
||||||
route("callLaters.complete", item.id),
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
preserveScroll: false,
|
|
||||||
onSuccess: () => {
|
|
||||||
if (item.client_case?.uuid) {
|
|
||||||
router.visit(route("clientCase.show", { client_case: item.client_case.uuid }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOverdue(item) {
|
|
||||||
if (!item.call_back_at) return false;
|
|
||||||
// Strip Z so the value is parsed as local time (datetimes are stored as local time with a wrong Z suffix)
|
|
||||||
const localDateStr = item.call_back_at.replace("Z", "").replace("T", " ");
|
|
||||||
return new Date(localDateStr) < new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ key: "person", label: "Stranka / Primer", sortable: false },
|
|
||||||
{ key: "contract", label: "Pogodba", sortable: false },
|
|
||||||
{ key: "call_back_at", label: "Datum klica", sortable: false },
|
|
||||||
{ key: "user", label: "Agent", sortable: false },
|
|
||||||
{ key: "note", label: "Opomba", sortable: false },
|
|
||||||
{ key: "actions", label: "", sortable: false, class: "w-12" },
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AppLayout title="Pokliči kasneje">
|
|
||||||
<template #header></template>
|
|
||||||
<div class="py-6">
|
|
||||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
||||||
<AppCard
|
|
||||||
title=""
|
|
||||||
padding="none"
|
|
||||||
class="p-0! gap-0"
|
|
||||||
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
|
||||||
body-class=""
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<PhoneCallIcon :size="18" />
|
|
||||||
<CardTitle class="uppercase">Pokliči kasneje</CardTitle>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<DataTable
|
|
||||||
:columns="columns"
|
|
||||||
:data="callLaters.data || []"
|
|
||||||
:meta="callLaters"
|
|
||||||
:search="search"
|
|
||||||
route-name="callLaters.index"
|
|
||||||
:show-toolbar="true"
|
|
||||||
:show-pagination="false"
|
|
||||||
:hoverable="true"
|
|
||||||
row-key="id"
|
|
||||||
empty-text="Ni zakazanih klicev."
|
|
||||||
:row-class="(row) => (isOverdue(row) ? 'bg-red-50 dark:bg-red-950/20' : '')"
|
|
||||||
>
|
|
||||||
<template #toolbar-filters>
|
|
||||||
<AppPopover
|
|
||||||
v-model:open="filterPopoverOpen"
|
|
||||||
align="start"
|
|
||||||
content-class="w-[420px]"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<Button variant="outline" size="sm" class="gap-2">
|
|
||||||
<Filter class="h-4 w-4" />
|
|
||||||
Filtri
|
|
||||||
<span
|
|
||||||
v-if="appliedFilterCount > 0"
|
|
||||||
class="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground"
|
|
||||||
>
|
|
||||||
{{ appliedFilterCount }}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<h4 class="font-medium text-sm">Filtri klicev</h4>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<InputLabel>Iskanje (stranka)</InputLabel>
|
|
||||||
<Input
|
|
||||||
v-model="search"
|
|
||||||
type="text"
|
|
||||||
placeholder="Ime stranke..."
|
|
||||||
@keydown.enter="applyFilters"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<InputLabel>Datum od</InputLabel>
|
|
||||||
<Input v-model="dateFrom" type="date" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-1.5">
|
|
||||||
<InputLabel>Datum do</InputLabel>
|
|
||||||
<Input v-model="dateTo" type="date" />
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end gap-2 pt-2 border-t">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
:disabled="appliedFilterCount === 0"
|
|
||||||
@click="clearFilters"
|
|
||||||
>
|
|
||||||
Počisti
|
|
||||||
</Button>
|
|
||||||
<Button type="button" size="sm" @click="applyFilters">
|
|
||||||
Uporabi
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppPopover>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-person="{ row }">
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
v-if="row.client_case"
|
|
||||||
:href="route('clientCase.show', { client_case: row.client_case.uuid })"
|
|
||||||
class="font-medium text-indigo-600 hover:underline"
|
|
||||||
>
|
|
||||||
{{ row.client_case.person?.full_name || "-" }}
|
|
||||||
</Link>
|
|
||||||
<span v-else class="text-muted-foreground">-</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-contract="{ row }">
|
|
||||||
<span v-if="row.contract">{{ row.contract.reference }}</span>
|
|
||||||
<span v-else class="text-muted-foreground">-</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-call_back_at="{ row }">
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'font-medium',
|
|
||||||
isOverdue(row) ? 'text-red-600 dark:text-red-400' : '',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ fmtDateTime(row.call_back_at) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="isOverdue(row)" class="ml-2 text-xs text-red-500 font-semibold">
|
|
||||||
Zamuda
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-user="{ row }">
|
|
||||||
<span v-if="row.user">{{ row.user.name }}</span>
|
|
||||||
<span v-else class="text-muted-foreground">-</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-note="{ row }">
|
|
||||||
<span class="line-clamp-2 text-sm text-muted-foreground">
|
|
||||||
{{ row.activity?.note || "-" }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #cell-actions="{ row }">
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger as-child>
|
|
||||||
<Button size="icon" variant="ghost" class="h-8 w-8">
|
|
||||||
<MoreHorizontalIcon class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem @click="markDone(row)">
|
|
||||||
<CheckIcon class="mr-2 h-4 w-4" />
|
|
||||||
Opravljeno
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
v-if="row.client_case?.uuid"
|
|
||||||
@click="openAndComplete(row)"
|
|
||||||
>
|
|
||||||
<ExternalLinkIcon class="mr-2 h-4 w-4" />
|
|
||||||
Odpri in opravi
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</template>
|
|
||||||
</DataTable>
|
|
||||||
|
|
||||||
<div class="border-t border-gray-200 p-4">
|
|
||||||
<Pagination
|
|
||||||
:links="callLaters.links"
|
|
||||||
:from="callLaters.from"
|
|
||||||
:to="callLaters.to"
|
|
||||||
:total="callLaters.total"
|
|
||||||
:per-page="callLaters.per_page || 50"
|
|
||||||
:last-page="callLaters.last_page"
|
|
||||||
:current-page="callLaters.current_page"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</AppCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
|
||||||
</template>
|
|
||||||
|
|
@ -58,8 +58,6 @@ const form = useInertiaForm({
|
||||||
send_auto_mail: true,
|
send_auto_mail: true,
|
||||||
attach_documents: false,
|
attach_documents: false,
|
||||||
attachment_document_ids: [],
|
attachment_document_ids: [],
|
||||||
call_back_at_date: null,
|
|
||||||
call_back_at_time: null,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -129,20 +127,6 @@ const store = async () => {
|
||||||
|
|
||||||
const isMultipleContracts = contractUuids && contractUuids.length > 1;
|
const isMultipleContracts = contractUuids && contractUuids.length > 1;
|
||||||
|
|
||||||
const buildCallBackAt = (date, time) => {
|
|
||||||
if (!date) return null;
|
|
||||||
const t = time || '00:00';
|
|
||||||
const [h, m] = t.split(':');
|
|
||||||
const d = date instanceof Date ? date : new Date(date);
|
|
||||||
if (isNaN(d.getTime())) return null;
|
|
||||||
const y = d.getFullYear();
|
|
||||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
||||||
const dy = String(d.getDate()).padStart(2, '0');
|
|
||||||
const hh = String(Number(h || 0)).padStart(2, '0');
|
|
||||||
const mm = String(Number(m || 0)).padStart(2, '0');
|
|
||||||
return `${y}-${mo}-${dy} ${hh}:${mm}:00`;
|
|
||||||
};
|
|
||||||
|
|
||||||
form
|
form
|
||||||
.transform((data) => ({
|
.transform((data) => ({
|
||||||
...data,
|
...data,
|
||||||
|
|
@ -154,16 +138,11 @@ const store = async () => {
|
||||||
templateAllowsAttachments.value && data.attach_documents && !isMultipleContracts
|
templateAllowsAttachments.value && data.attach_documents && !isMultipleContracts
|
||||||
? data.attachment_document_ids
|
? data.attachment_document_ids
|
||||||
: [],
|
: [],
|
||||||
call_back_at: hasCallLaterEvent.value
|
|
||||||
? buildCallBackAt(data.call_back_at_date, data.call_back_at_time)
|
|
||||||
: null,
|
|
||||||
call_back_at_date: undefined,
|
|
||||||
call_back_at_time: undefined,
|
|
||||||
}))
|
}))
|
||||||
.post(route("clientCase.activity.store", props.client_case), {
|
.post(route("clientCase.activity.store", props.client_case), {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
close();
|
close();
|
||||||
form.reset("due_date", "amount", "note", "contract_uuids", "call_back_at_date", "call_back_at_time");
|
form.reset("due_date", "amount", "note", "contract_uuids");
|
||||||
emit("saved");
|
emit("saved");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -177,22 +156,6 @@ const currentDecision = () => {
|
||||||
decisions.value.find((d) => d.id === form.decision_id) || decisions.value[0] || null
|
decisions.value.find((d) => d.id === form.decision_id) || decisions.value[0] || null
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasCallLaterEvent = computed(() => {
|
|
||||||
const d = currentDecision();
|
|
||||||
if (!d) return false;
|
|
||||||
return Array.isArray(d.events) && d.events.some((e) => e.key === 'add_call_later');
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => hasCallLaterEvent.value,
|
|
||||||
(has) => {
|
|
||||||
if (!has) {
|
|
||||||
form.call_back_at_date = null;
|
|
||||||
form.call_back_at_time = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const showSendAutoMail = () => {
|
const showSendAutoMail = () => {
|
||||||
const d = currentDecision();
|
const d = currentDecision();
|
||||||
return !!(d && d.auto_mail && d.email_template_id);
|
return !!(d && d.auto_mail && d.email_template_id);
|
||||||
|
|
@ -446,26 +409,6 @@ watch(
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="hasCallLaterEvent" class="space-y-2">
|
|
||||||
<Label>Datum in ura povratnega klica</Label>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<DatePicker
|
|
||||||
v-model="form.call_back_at_date"
|
|
||||||
format="dd.MM.yyyy"
|
|
||||||
:error="form.errors.call_back_at"
|
|
||||||
class="flex-1"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
v-model="form.call_back_at_time"
|
|
||||||
type="time"
|
|
||||||
class="flex-1 border rounded-md px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="form.errors.call_back_at" class="text-xs text-destructive">
|
|
||||||
{{ form.errors.call_back_at }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="activityAmount">Znesek</Label>
|
<Label for="activityAmount">Znesek</Label>
|
||||||
<CurrencyInput
|
<CurrencyInput
|
||||||
|
|
@ -537,7 +480,7 @@ watch(
|
||||||
/>
|
/>
|
||||||
<div class="wrap-anywhere">
|
<div class="wrap-anywhere">
|
||||||
<p>
|
<p>
|
||||||
<span>{{ doc.name }}.{{ doc.extension }}</span>
|
{{ doc.original_name || doc.name }}
|
||||||
</p>
|
</p>
|
||||||
<span class="text-xs text-gray-400"
|
<span class="text-xs text-gray-400"
|
||||||
>({{ doc.extension?.toUpperCase() || "" }},
|
>({{ doc.extension?.toUpperCase() || "" }},
|
||||||
|
|
|
||||||
|
|
@ -741,16 +741,8 @@ const copyToClipboard = async (text) => {
|
||||||
<span class="text-gray-500">D:</span>
|
<span class="text-gray-500">D:</span>
|
||||||
<span class="ml-1">{{ fmtDate(row.due_date) }}</span>
|
<span class="ml-1">{{ fmtDate(row.due_date) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="row.call_back_at" class="leading-tight">
|
|
||||||
<span class="text-gray-500">K:</span>
|
|
||||||
<span class="ml-1">{{ fmtDateTime(row.call_back_at) }}</span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="!row.due_date && (!row.amount || Number(row.amount) === 0)"
|
||||||
!row.due_date &&
|
|
||||||
(!row.amount || Number(row.amount) === 0) &&
|
|
||||||
!row.call_back_at
|
|
||||||
"
|
|
||||||
class="text-gray-400"
|
class="text-gray-400"
|
||||||
>
|
>
|
||||||
—
|
—
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ import CaseObjectCreateDialog from "./CaseObjectCreateDialog.vue";
|
||||||
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
import CaseObjectsDialog from "./CaseObjectsDialog.vue";
|
||||||
import PaymentDialog from "./PaymentDialog.vue";
|
import PaymentDialog from "./PaymentDialog.vue";
|
||||||
import ViewPaymentsDialog from "./ViewPaymentsDialog.vue";
|
import ViewPaymentsDialog from "./ViewPaymentsDialog.vue";
|
||||||
import InstallmentDialog from "./InstallmentDialog.vue";
|
|
||||||
import ViewInstallmentsDialog from "./ViewInstallmentsDialog.vue";
|
|
||||||
import ContractMetaEditDialog from "./ContractMetaEditDialog.vue";
|
import ContractMetaEditDialog from "./ContractMetaEditDialog.vue";
|
||||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
||||||
import ConfirmationDialog from "@/Components/Dialogs/ConfirmationDialog.vue";
|
import ConfirmationDialog from "@/Components/Dialogs/ConfirmationDialog.vue";
|
||||||
|
|
@ -33,7 +31,6 @@ import {
|
||||||
faSpinner,
|
faSpinner,
|
||||||
faTags,
|
faTags,
|
||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faArrowUp,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import EmptyState from "@/Components/EmptyState.vue";
|
import EmptyState from "@/Components/EmptyState.vue";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
|
|
@ -447,52 +444,6 @@ const closePaymentsDialog = () => {
|
||||||
selectedContract.value = null;
|
selectedContract.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Installments
|
|
||||||
const showInstallmentDialog = ref(false);
|
|
||||||
const installmentContract = ref(null);
|
|
||||||
const installmentForm = useForm({
|
|
||||||
amount: null,
|
|
||||||
currency: "EUR",
|
|
||||||
installment_at: null,
|
|
||||||
reference: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const openInstallmentDialog = (c) => {
|
|
||||||
installmentContract.value = c;
|
|
||||||
installmentForm.reset();
|
|
||||||
installmentForm.installment_at = todayStr.value;
|
|
||||||
showInstallmentDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeInstallmentDialog = () => {
|
|
||||||
showInstallmentDialog.value = false;
|
|
||||||
installmentContract.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitInstallment = () => {
|
|
||||||
if (!installmentContract.value?.account?.id) return;
|
|
||||||
const accountId = installmentContract.value.account.id;
|
|
||||||
installmentForm.post(route("accounts.installments.store", { account: accountId }), {
|
|
||||||
preserveScroll: true,
|
|
||||||
onSuccess: () => {
|
|
||||||
closeInstallmentDialog();
|
|
||||||
router.reload({ only: ["contracts", "activities"] });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const showInstallmentsDialog = ref(false);
|
|
||||||
|
|
||||||
const openInstallmentsDialog = (c) => {
|
|
||||||
selectedContract.value = c;
|
|
||||||
showInstallmentsDialog.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeInstallmentsDialog = () => {
|
|
||||||
showInstallmentsDialog.value = false;
|
|
||||||
selectedContract.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Meta edit dialog
|
// Meta edit dialog
|
||||||
const showMetaEditDialog = ref(false);
|
const showMetaEditDialog = ref(false);
|
||||||
|
|
||||||
|
|
@ -538,7 +489,7 @@ const availableSegmentsCount = computed(() => {
|
||||||
:empty-icon="faFolderOpen"
|
:empty-icon="faFolderOpen"
|
||||||
empty-text="Ni pogodb"
|
empty-text="Ni pogodb"
|
||||||
empty-description="Za ta primer še ni ustvarjenih pogodb. Ustvarite novo pogodbo za začetek."
|
empty-description="Za ta primer še ni ustvarjenih pogodb. Ustvarite novo pogodbo za začetek."
|
||||||
:show-pagination="true"
|
:show-pagination="false"
|
||||||
:show-toolbar="true"
|
:show-toolbar="true"
|
||||||
:hoverable="true"
|
:hoverable="true"
|
||||||
>
|
>
|
||||||
|
|
@ -799,6 +750,7 @@ const availableSegmentsCount = computed(() => {
|
||||||
|
|
||||||
<!-- Add Activity -->
|
<!-- Add Activity -->
|
||||||
<ActionMenuItem
|
<ActionMenuItem
|
||||||
|
v-if="row.active"
|
||||||
:icon="faListCheck"
|
:icon="faListCheck"
|
||||||
label="Dodaj aktivnost"
|
label="Dodaj aktivnost"
|
||||||
@click="onAddActivity(row)"
|
@click="onAddActivity(row)"
|
||||||
|
|
@ -881,26 +833,6 @@ const availableSegmentsCount = computed(() => {
|
||||||
@click="openPaymentDialog(row)"
|
@click="openPaymentDialog(row)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="my-1 border-t border-gray-100" />
|
|
||||||
|
|
||||||
<!-- Installments -->
|
|
||||||
<div
|
|
||||||
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
|
|
||||||
>
|
|
||||||
Obroki
|
|
||||||
</div>
|
|
||||||
<ActionMenuItem
|
|
||||||
:icon="faCircleInfo"
|
|
||||||
label="Pokaži obroke"
|
|
||||||
@click="openInstallmentsDialog(row)"
|
|
||||||
/>
|
|
||||||
<ActionMenuItem
|
|
||||||
v-if="row.active && row?.account"
|
|
||||||
:icon="faArrowUp"
|
|
||||||
label="Dodaj obrok"
|
|
||||||
@click="openInstallmentDialog(row)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Archive -->
|
<!-- Archive -->
|
||||||
<template v-if="edit">
|
<template v-if="edit">
|
||||||
<div class="my-1 border-t border-gray-100" />
|
<div class="my-1 border-t border-gray-100" />
|
||||||
|
|
@ -1006,20 +938,6 @@ const availableSegmentsCount = computed(() => {
|
||||||
:edit="edit"
|
:edit="edit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InstallmentDialog
|
|
||||||
:show="showInstallmentDialog"
|
|
||||||
:form="installmentForm"
|
|
||||||
@close="closeInstallmentDialog"
|
|
||||||
@submit="submitInstallment"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ViewInstallmentsDialog
|
|
||||||
:show="showInstallmentsDialog"
|
|
||||||
:contract="selectedContract"
|
|
||||||
@close="closeInstallmentsDialog"
|
|
||||||
:edit="edit"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ContractMetaEditDialog
|
<ContractMetaEditDialog
|
||||||
:show="showMetaEditDialog"
|
:show="showMetaEditDialog"
|
||||||
:client_case="client_case"
|
:client_case="client_case"
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
|
||||||
import CurrencyInput from "@/Components/CurrencyInput.vue";
|
|
||||||
import { Label } from "@/Components/ui/label";
|
|
||||||
import { Input } from "@/Components/ui/input";
|
|
||||||
import DatePicker from "@/Components/DatePicker.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: { type: Boolean, default: false },
|
|
||||||
form: { type: Object, required: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["close", "submit"]);
|
|
||||||
|
|
||||||
const onClose = () => emit("close");
|
|
||||||
const onSubmit = () => emit("submit");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<CreateDialog
|
|
||||||
:show="show"
|
|
||||||
title="Dodaj obrok"
|
|
||||||
confirm-text="Shrani"
|
|
||||||
:processing="form.processing"
|
|
||||||
@close="onClose"
|
|
||||||
@confirm="onSubmit"
|
|
||||||
>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="installmentAmount">Znesek</Label>
|
|
||||||
<CurrencyInput
|
|
||||||
id="installmentAmount"
|
|
||||||
v-model="form.amount"
|
|
||||||
:precision="{ min: 0, max: 2 }"
|
|
||||||
placeholder="0,00"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<p v-if="form.errors?.amount" class="text-sm text-red-600">
|
|
||||||
{{ form.errors.amount }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="installmentCurrency">Valuta</Label>
|
|
||||||
<Input
|
|
||||||
id="installmentCurrency"
|
|
||||||
type="text"
|
|
||||||
v-model="form.currency"
|
|
||||||
maxlength="3"
|
|
||||||
placeholder="EUR"
|
|
||||||
/>
|
|
||||||
<p v-if="form.errors?.currency" class="text-sm text-red-600">
|
|
||||||
{{ form.errors.currency }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="installmentDate">Datum</Label>
|
|
||||||
<DatePicker
|
|
||||||
id="installmentDate"
|
|
||||||
v-model="form.installment_at"
|
|
||||||
format="dd.MM.yyyy"
|
|
||||||
:error="form.errors?.installment_at"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="installmentReference">Sklic</Label>
|
|
||||||
<Input
|
|
||||||
id="installmentReference"
|
|
||||||
type="text"
|
|
||||||
v-model="form.reference"
|
|
||||||
placeholder="Sklic"
|
|
||||||
/>
|
|
||||||
<p v-if="form.errors?.reference" class="text-sm text-red-600">
|
|
||||||
{{ form.errors.reference }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CreateDialog>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import DialogModal from "@/Components/DialogModal.vue";
|
|
||||||
import { ref, watch, computed } from "vue";
|
|
||||||
import { router } from "@inertiajs/vue3";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
show: { type: Boolean, default: false },
|
|
||||||
contract: { type: Object, default: null },
|
|
||||||
edit: { type: Boolean, default: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const emit = defineEmits(["close"]);
|
|
||||||
|
|
||||||
const installments = ref([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const contractRef = computed(() => props.contract?.reference || "—");
|
|
||||||
const accountId = computed(() => props.contract?.account?.id || null);
|
|
||||||
|
|
||||||
function formatDate(d) {
|
|
||||||
if (!d) return "-";
|
|
||||||
const dt = new Date(d);
|
|
||||||
return isNaN(dt.getTime()) ? "-" : dt.toLocaleDateString("de");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInstallments() {
|
|
||||||
if (!accountId.value) {
|
|
||||||
installments.value = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
const { data } = await axios.get(
|
|
||||||
route("accounts.installments.list", { account: accountId.value })
|
|
||||||
);
|
|
||||||
installments.value = data.installments || [];
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
emit("close");
|
|
||||||
installments.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteInstallment(installmentId) {
|
|
||||||
if (!accountId.value) return;
|
|
||||||
router.delete(
|
|
||||||
route("accounts.installments.destroy", {
|
|
||||||
account: accountId.value,
|
|
||||||
installment: installmentId,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
preserveScroll: true,
|
|
||||||
preserveState: true,
|
|
||||||
only: ["contracts", "activities"],
|
|
||||||
onSuccess: async () => {
|
|
||||||
await loadInstallments();
|
|
||||||
},
|
|
||||||
onError: async () => {
|
|
||||||
await loadInstallments();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
async (visible) => {
|
|
||||||
if (visible) {
|
|
||||||
await loadInstallments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
watch(
|
|
||||||
() => props.contract?.account?.id,
|
|
||||||
async () => {
|
|
||||||
if (props.show) {
|
|
||||||
await loadInstallments();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<DialogModal :show="show" @close="close">
|
|
||||||
<template #title>
|
|
||||||
Obroki za pogodbo <span class="text-gray-600">{{ contractRef }}</span>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<div>
|
|
||||||
<div v-if="loading" class="text-sm text-gray-500">Nalaganje…</div>
|
|
||||||
<template v-else>
|
|
||||||
<div v-if="installments.length === 0" class="text-sm text-gray-500">Ni obrokov.</div>
|
|
||||||
<div v-else class="divide-y divide-gray-100 border rounded">
|
|
||||||
<div
|
|
||||||
v-for="i in installments"
|
|
||||||
:key="i.id"
|
|
||||||
class="px-3 py-2 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div class="text-sm text-gray-800">
|
|
||||||
{{
|
|
||||||
Intl.NumberFormat("de-DE", {
|
|
||||||
style: "currency",
|
|
||||||
currency: i.currency || "EUR",
|
|
||||||
}).format(i.amount ?? 0)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-500">
|
|
||||||
<span>{{ formatDate(i.installment_at) }}</span>
|
|
||||||
<span v-if="i.reference" class="ml-2">Sklic: {{ i.reference }}</span>
|
|
||||||
<span v-if="i.balance_before !== undefined" class="ml-2">
|
|
||||||
Stanje pred:
|
|
||||||
{{
|
|
||||||
Intl.NumberFormat("de-DE", {
|
|
||||||
style: "currency",
|
|
||||||
currency: i.currency || "EUR",
|
|
||||||
}).format(i.balance_before ?? 0)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2" v-if="edit">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="inline-flex items-center gap-1 px-2 py-1 rounded text-red-700 hover:bg-red-50"
|
|
||||||
@click="deleteInstallment(i.id)"
|
|
||||||
title="Izbriši obrok"
|
|
||||||
>
|
|
||||||
<span class="text-sm">Briši</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #footer>
|
|
||||||
<div class="flex justify-end gap-2 w-full">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
|
|
||||||
@click="loadInstallments"
|
|
||||||
>
|
|
||||||
Osveži
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
|
|
||||||
@click="close"
|
|
||||||
>
|
|
||||||
Zapri
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</DialogModal>
|
|
||||||
</template>
|
|
||||||
|
|
@ -31,7 +31,7 @@ import {
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
client: Object,
|
client: Object,
|
||||||
client_case: Object,
|
client_case: Object,
|
||||||
contracts: { type: Array, default: () => [] }, // Resource Collection with data/links/meta
|
contracts: Object, // Resource Collection with data/links/meta
|
||||||
activities: Object, // Resource Collection with data/links/meta
|
activities: Object, // Resource Collection with data/links/meta
|
||||||
contract_types: Array,
|
contract_types: Array,
|
||||||
account_types: { type: Array, default: () => [] },
|
account_types: { type: Array, default: () => [] },
|
||||||
|
|
@ -46,7 +46,7 @@ const props = defineProps({
|
||||||
|
|
||||||
// Extract contracts array from Resource Collection
|
// Extract contracts array from Resource Collection
|
||||||
const contractsArray = computed(() => {
|
const contractsArray = computed(() => {
|
||||||
return props.contracts || [];
|
return props.contracts?.data || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contracts are always paginated now (Resource Collection)
|
// Contracts are always paginated now (Resource Collection)
|
||||||
|
|
@ -356,6 +356,19 @@ const submitAttachSegment = () => {
|
||||||
@create="openDrawerCreateContract"
|
@create="openDrawerCreateContract"
|
||||||
@attach-segment="openAttachSegment"
|
@attach-segment="openAttachSegment"
|
||||||
/>
|
/>
|
||||||
|
<div v-if="contractsPaginated" class="border-t border-gray-200 p-4">
|
||||||
|
<Pagination
|
||||||
|
:links="contracts.links"
|
||||||
|
:from="contracts.from"
|
||||||
|
:to="contracts.to"
|
||||||
|
:total="contracts.total"
|
||||||
|
:per-page="contracts.per_page || 50"
|
||||||
|
:last-page="contracts.last_page"
|
||||||
|
:current-page="contracts.current_page"
|
||||||
|
per-page-param="contracts_per_page"
|
||||||
|
page-param="contracts_page"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
Archive,
|
Archive,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
CalendarDays,
|
|
||||||
} from "lucide-vue-next";
|
} from "lucide-vue-next";
|
||||||
|
|
||||||
const settingsCards = [
|
const settingsCards = [
|
||||||
|
|
@ -28,12 +27,6 @@ const settingsCards = [
|
||||||
route: "settings.payment.edit",
|
route: "settings.payment.edit",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Installments",
|
|
||||||
description: "Defaults for installments and auto-activity.",
|
|
||||||
route: "settings.installment.edit",
|
|
||||||
icon: CalendarDays,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Workflow",
|
title: "Workflow",
|
||||||
description: "Configure actions and decisions relationships.",
|
description: "Configure actions and decisions relationships.",
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
<script setup>
|
|
||||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
|
||||||
import { useForm } from "@inertiajs/vue3";
|
|
||||||
import { computed, watch } from "vue";
|
|
||||||
import AppCard from "@/Components/app/ui/card/AppCard.vue";
|
|
||||||
import CardTitle from "@/Components/ui/card/CardTitle.vue";
|
|
||||||
import { CalendarDays } from "lucide-vue-next";
|
|
||||||
import { Button } from "@/Components/ui/button";
|
|
||||||
import { Input } from "@/Components/ui/input";
|
|
||||||
import InputLabel from "@/Components/InputLabel.vue";
|
|
||||||
import { Checkbox } from "@/Components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/Components/ui/select";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
setting: Object,
|
|
||||||
decisions: Array,
|
|
||||||
actions: Array,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
default_currency: props.setting?.default_currency ?? "EUR",
|
|
||||||
create_activity_on_installment: !!props.setting?.create_activity_on_installment,
|
|
||||||
default_action_id: props.setting?.default_action_id ?? null,
|
|
||||||
default_decision_id: props.setting?.default_decision_id ?? null,
|
|
||||||
activity_note_template:
|
|
||||||
props.setting?.activity_note_template ?? "Dodan obrok: {amount} {currency}",
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredDecisions = computed(() => {
|
|
||||||
const actionId = form.default_action_id;
|
|
||||||
if (!actionId) return [];
|
|
||||||
const action = props.actions?.find((a) => a.id === actionId);
|
|
||||||
if (!action || !action.decision_ids) return [];
|
|
||||||
const ids = new Set(action.decision_ids);
|
|
||||||
return (props.decisions || []).filter((d) => ids.has(d.id));
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => form.default_action_id,
|
|
||||||
(newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
form.default_decision_id = null;
|
|
||||||
} else {
|
|
||||||
const ids = new Set((filteredDecisions.value || []).map((d) => d.id));
|
|
||||||
if (!ids.has(form.default_decision_id)) {
|
|
||||||
form.default_decision_id = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
form.put(route("settings.installment.update"), {
|
|
||||||
preserveScroll: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AppLayout title="Nastavitve obrokov">
|
|
||||||
<template #header></template>
|
|
||||||
<div class="max-w-3xl mx-auto p-6">
|
|
||||||
<AppCard
|
|
||||||
title=""
|
|
||||||
padding="none"
|
|
||||||
class="p-0! gap-0"
|
|
||||||
header-class="py-3! px-4 gap-0 text-muted-foreground"
|
|
||||||
body-class=""
|
|
||||||
>
|
|
||||||
<template #header>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<CalendarDays :size="18" />
|
|
||||||
<CardTitle class="uppercase">Nastavitve obrokov</CardTitle>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="space-y-6 p-4 border-t">
|
|
||||||
<div>
|
|
||||||
<InputLabel for="currency">Privzeta valuta</InputLabel>
|
|
||||||
<Input
|
|
||||||
id="currency"
|
|
||||||
v-model="form.default_currency"
|
|
||||||
maxlength="3"
|
|
||||||
class="w-40"
|
|
||||||
/>
|
|
||||||
<div v-if="form.errors.default_currency" class="text-sm text-red-600 mt-1">
|
|
||||||
{{ form.errors.default_currency }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Checkbox id="create-activity" v-model="form.create_activity_on_installment" />
|
|
||||||
<InputLabel for="create-activity" class="text-sm font-normal cursor-pointer">
|
|
||||||
Ustvari aktivnost ob dodanem obroku
|
|
||||||
</InputLabel>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
<div>
|
|
||||||
<InputLabel for="default-action">Privzeto dejanje</InputLabel>
|
|
||||||
<Select v-model="form.default_action_id">
|
|
||||||
<SelectTrigger id="default-action">
|
|
||||||
<SelectValue placeholder="— Brez —" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem :value="null">— Brez —</SelectItem>
|
|
||||||
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">{{
|
|
||||||
a.name
|
|
||||||
}}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div v-if="form.errors.default_action_id" class="text-sm text-red-600 mt-1">
|
|
||||||
{{ form.errors.default_action_id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<InputLabel for="default-decision">Privzeta odločitev</InputLabel>
|
|
||||||
<Select
|
|
||||||
v-model="form.default_decision_id"
|
|
||||||
:disabled="!form.default_action_id"
|
|
||||||
>
|
|
||||||
<SelectTrigger id="default-decision">
|
|
||||||
<SelectValue placeholder="— Najprej izberite dejanje —" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem :value="null">— Najprej izberite dejanje —</SelectItem>
|
|
||||||
<SelectItem v-for="d in filteredDecisions" :key="d.id" :value="d.id">{{
|
|
||||||
d.name
|
|
||||||
}}</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div
|
|
||||||
v-if="form.errors.default_decision_id"
|
|
||||||
class="text-sm text-red-600 mt-1"
|
|
||||||
>
|
|
||||||
{{ form.errors.default_decision_id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<InputLabel for="note-template">Predloga opombe aktivnosti</InputLabel>
|
|
||||||
<Input id="note-template" v-model="form.activity_note_template" />
|
|
||||||
<p class="text-xs text-gray-500 mt-1">Podprti žetoni: {amount}, {currency}</p>
|
|
||||||
<div
|
|
||||||
v-if="form.errors.activity_note_template"
|
|
||||||
class="text-sm text-red-600 mt-1"
|
|
||||||
>
|
|
||||||
{{ form.errors.activity_note_template }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<Button variant="outline" @click="form.reset()">Ponastavi</Button>
|
|
||||||
<Button @click="submit" :disabled="form.processing">Shrani</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AppCard>
|
|
||||||
</div>
|
|
||||||
</AppLayout>
|
|
||||||
</template>
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// flowbite-vue table imports removed; using DataTableClient
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -26,7 +27,7 @@ import { Input } from "@/Components/ui/input";
|
||||||
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
|
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
|
||||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
|
||||||
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
||||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||||
import { FilterIcon, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
|
import { FilterIcon, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
|
||||||
|
|
@ -59,13 +60,16 @@ const segmentOptions = computed(() =>
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// DataTable state
|
||||||
|
const sort = ref({ key: null, direction: null });
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(25);
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "id", label: "#", sortable: true, class: "w-16" },
|
{ key: "id", label: "#", sortable: true, class: "w-16" },
|
||||||
{ key: "name", label: "Ime", sortable: true },
|
{ key: "name", label: "Ime", sortable: true },
|
||||||
{ key: "color_tag", label: "Barva", sortable: false },
|
{ key: "color_tag", label: "Barva", sortable: false },
|
||||||
{ key: "segment", label: "Segment", sortable: false },
|
{ key: "segment", label: "Segment", sortable: false },
|
||||||
{ key: "decisions", label: "Odločitve", sortable: false, class: "w-32" },
|
{ key: "decisions", label: "Odločitve", sortable: false, class: "w-32" },
|
||||||
{ key: "actions", label: "", sortable: false, class: "w-12" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
|
@ -227,12 +231,18 @@ const destroyAction = () => {
|
||||||
<Button @click="openCreateDrawer">+ Dodaj akcijo</Button>
|
<Button @click="openCreateDrawer">+ Dodaj akcijo</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DataTableNew2
|
<DataTableClient
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="filtered"
|
:rows="filtered"
|
||||||
:pageSize="25"
|
:sort="sort"
|
||||||
|
:search="''"
|
||||||
|
:page="page"
|
||||||
|
:pageSize="pageSize"
|
||||||
:showToolbar="false"
|
:showToolbar="false"
|
||||||
:showPagination="true"
|
:showPagination="true"
|
||||||
|
@update:sort="(v) => (sort = v)"
|
||||||
|
@update:page="(v) => (page = v)"
|
||||||
|
@update:pageSize="(v) => (pageSize = v)"
|
||||||
>
|
>
|
||||||
<template #cell-color_tag="{ row }">
|
<template #cell-color_tag="{ row }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
@ -252,7 +262,7 @@ const destroyAction = () => {
|
||||||
{{ row.segment?.name || "" }}
|
{{ row.segment?.name || "" }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
|
|
@ -275,7 +285,7 @@ const destroyAction = () => {
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</DataTableNew2>
|
</DataTableClient>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog v-model:open="drawerEdit">
|
<Dialog v-model:open="drawerEdit">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { DottedMenu } from "@/Utilities/Icons";
|
// flowbite-vue table imports removed; using DataTableClient
|
||||||
|
import { EditIcon, TrashBinIcon, DottedMenu } from "@/Utilities/Icons";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -29,11 +30,11 @@ import {
|
||||||
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
|
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
|
||||||
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import DataTableNew2 from "@/Components/DataTable/DataTableNew2.vue";
|
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
|
||||||
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
|
||||||
import Dropdown from "@/Components/Dropdown.vue";
|
import Dropdown from "@/Components/Dropdown.vue";
|
||||||
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
import AppPopover from "@/Components/app/ui/AppPopover.vue";
|
||||||
import { FilterIcon, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
|
import { FilterIcon, Trash2, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -63,6 +64,10 @@ const selectedEvents = ref([]);
|
||||||
|
|
||||||
const actionOptions = ref([]);
|
const actionOptions = ref([]);
|
||||||
|
|
||||||
|
// DataTable state
|
||||||
|
const sort = ref({ key: null, direction: null });
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(25);
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: "id", label: "#", sortable: true },
|
{ key: "id", label: "#", sortable: true },
|
||||||
{ key: "name", label: "Ime", sortable: true },
|
{ key: "name", label: "Ime", sortable: true },
|
||||||
|
|
@ -70,7 +75,6 @@ const columns = [
|
||||||
{ key: "events", label: "Dogodki", sortable: false },
|
{ key: "events", label: "Dogodki", sortable: false },
|
||||||
{ key: "belongs", label: "Pripada akcijam", sortable: false },
|
{ key: "belongs", label: "Pripada akcijam", sortable: false },
|
||||||
{ key: "auto_mail", label: "Auto mail", sortable: false },
|
{ key: "auto_mail", label: "Auto mail", sortable: false },
|
||||||
{ key: "actions", label: "", sortable: false, class: "w-12" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
|
|
@ -187,8 +191,6 @@ function defaultConfigForKey(key) {
|
||||||
return { archive_setting_id: null, reactivate: false };
|
return { archive_setting_id: null, reactivate: false };
|
||||||
case "end_field_job":
|
case "end_field_job":
|
||||||
return {};
|
return {};
|
||||||
case "add_call_later":
|
|
||||||
return {};
|
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -464,12 +466,18 @@ const destroyDecision = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<DataTableNew2
|
<DataTableClient
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="filtered"
|
:rows="filtered"
|
||||||
:pageSize="25"
|
:sort="sort"
|
||||||
|
:search="''"
|
||||||
|
:page="page"
|
||||||
|
:pageSize="pageSize"
|
||||||
:showToolbar="false"
|
:showToolbar="false"
|
||||||
:showPagination="true"
|
:showPagination="true"
|
||||||
|
@update:sort="(v) => (sort = v)"
|
||||||
|
@update:page="(v) => (page = v)"
|
||||||
|
@update:pageSize="(v) => (pageSize = v)"
|
||||||
>
|
>
|
||||||
<template #cell-color_tag="{ row }">
|
<template #cell-color_tag="{ row }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
@ -486,13 +494,14 @@ const destroyDecision = () => {
|
||||||
</template>
|
</template>
|
||||||
<template #cell-events="{ row }">
|
<template #cell-events="{ row }">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">{{ row.events?.length ?? 0 }}</span>
|
||||||
<Dropdown align="left" width="64" :close-on-content-click="false">
|
<Dropdown align="left" width="64" :close-on-content-click="false">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="p-1 rounded hover:bg-gray-100 border border-transparent hover:border-gray-200"
|
class="p-1 rounded hover:bg-gray-100 border border-transparent hover:border-gray-200"
|
||||||
>
|
>
|
||||||
{{ row.events?.length ?? 0 }}
|
<DottedMenu size="sm" css="text-gray-600" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
@ -540,7 +549,7 @@ const destroyDecision = () => {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-actions="{ row }">
|
<template #actions="{ row }">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
|
|
@ -563,7 +572,7 @@ const destroyDecision = () => {
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
</DataTableNew2>
|
</DataTableClient>
|
||||||
</div>
|
</div>
|
||||||
<Dialog v-model:open="drawerEdit">
|
<Dialog v-model:open="drawerEdit">
|
||||||
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
|
@ -743,11 +752,6 @@ const destroyDecision = () => {
|
||||||
Ta dogodek nima dodatnih nastavitev.
|
Ta dogodek nima dodatnih nastavitev.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="eventKey(ev) === 'add_call_later'">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Datum in ura povratnega klica se vneseta ob ustvarjanju aktivnosti.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Fallback advanced editor for unknown event keys -->
|
<!-- Fallback advanced editor for unknown event keys -->
|
||||||
<InputLabel :for="`cfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
<InputLabel :for="`cfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
||||||
|
|
@ -977,11 +981,6 @@ const destroyDecision = () => {
|
||||||
Ta dogodek nima dodatnih nastavitev.
|
Ta dogodek nima dodatnih nastavitev.
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="eventKey(ev) === 'add_call_later'">
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Datum in ura povratnega klica se vneseta ob ustvarjanju aktivnosti.
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<InputLabel :for="`ccfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
<InputLabel :for="`ccfg-${idx}`" value="Napredna nastavitev (JSON)" />
|
||||||
<textarea
|
<textarea
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,11 @@ export function fmtDateTime(d) {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
timeZone: "UTC",
|
|
||||||
});
|
});
|
||||||
const timePart = dt.toLocaleTimeString("sl-SI", {
|
const timePart = dt.toLocaleTimeString("sl-SI", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
hour12: false,
|
hour12: false,
|
||||||
timeZone: "UTC",
|
|
||||||
});
|
});
|
||||||
return `${datePart} ${timePart}`;
|
return `${datePart} ${timePart}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\AccountBookingController;
|
use App\Http\Controllers\AccountBookingController;
|
||||||
use App\Http\Controllers\AccountInstallmentController;
|
|
||||||
use App\Http\Controllers\AccountPaymentController;
|
use App\Http\Controllers\AccountPaymentController;
|
||||||
use App\Http\Controllers\ActivityNotificationController;
|
use App\Http\Controllers\ActivityNotificationController;
|
||||||
use App\Http\Controllers\ArchiveSettingController;
|
use App\Http\Controllers\ArchiveSettingController;
|
||||||
|
|
@ -13,7 +12,6 @@
|
||||||
use App\Http\Controllers\FieldJobSettingController;
|
use App\Http\Controllers\FieldJobSettingController;
|
||||||
use App\Http\Controllers\ImportController;
|
use App\Http\Controllers\ImportController;
|
||||||
use App\Http\Controllers\ImportTemplateController;
|
use App\Http\Controllers\ImportTemplateController;
|
||||||
use App\Http\Controllers\InstallmentSettingController;
|
|
||||||
use App\Http\Controllers\NotificationController;
|
use App\Http\Controllers\NotificationController;
|
||||||
use App\Http\Controllers\PaymentSettingController;
|
use App\Http\Controllers\PaymentSettingController;
|
||||||
use App\Http\Controllers\PersonController;
|
use App\Http\Controllers\PersonController;
|
||||||
|
|
@ -159,19 +157,18 @@
|
||||||
Route::get('sms-logs', [\App\Http\Controllers\Admin\SmsLogController::class, 'index'])->name('sms-logs.index');
|
Route::get('sms-logs', [\App\Http\Controllers\Admin\SmsLogController::class, 'index'])->name('sms-logs.index');
|
||||||
Route::get('sms-logs/{smsLog}', [\App\Http\Controllers\Admin\SmsLogController::class, 'show'])->name('sms-logs.show');
|
Route::get('sms-logs/{smsLog}', [\App\Http\Controllers\Admin\SmsLogController::class, 'show'])->name('sms-logs.show');
|
||||||
|
|
||||||
});
|
// Packages (batch jobs)
|
||||||
|
Route::get('packages', [\App\Http\Controllers\Admin\PackageController::class, 'index'])->name('packages.index');
|
||||||
|
Route::get('packages/create', [\App\Http\Controllers\Admin\PackageController::class, 'create'])->name('packages.create');
|
||||||
|
Route::get('packages/{package}', [\App\Http\Controllers\Admin\PackageController::class, 'show'])->name('packages.show');
|
||||||
|
Route::post('packages', [\App\Http\Controllers\Admin\PackageController::class, 'store'])->name('packages.store');
|
||||||
|
Route::post('packages/{package}/dispatch', [\App\Http\Controllers\Admin\PackageController::class, 'dispatch'])->name('packages.dispatch');
|
||||||
|
Route::post('packages/{package}/cancel', [\App\Http\Controllers\Admin\PackageController::class, 'cancel'])->name('packages.cancel');
|
||||||
|
Route::delete('packages/{package}', [\App\Http\Controllers\Admin\PackageController::class, 'destroy'])->name('packages.destroy');
|
||||||
|
// Packages - contract-based helpers
|
||||||
|
Route::get('packages-contracts', [\App\Http\Controllers\Admin\PackageController::class, 'contracts'])->name('packages.contracts');
|
||||||
|
Route::post('packages-from-contracts', [\App\Http\Controllers\Admin\PackageController::class, 'storeFromContracts'])->name('packages.store-from-contracts');
|
||||||
|
|
||||||
// Packages (SMS batch sender) — accessible to users with manage-settings permission
|
|
||||||
Route::middleware(['permission:manage-settings'])->prefix('packages')->name('packages.')->group(function () {
|
|
||||||
Route::get('/', [\App\Http\Controllers\Admin\PackageController::class, 'index'])->name('index');
|
|
||||||
Route::get('/create', [\App\Http\Controllers\Admin\PackageController::class, 'create'])->name('create');
|
|
||||||
Route::get('/{package}', [\App\Http\Controllers\Admin\PackageController::class, 'show'])->name('show');
|
|
||||||
Route::post('/', [\App\Http\Controllers\Admin\PackageController::class, 'store'])->name('store');
|
|
||||||
Route::post('/{package}/dispatch', [\App\Http\Controllers\Admin\PackageController::class, 'dispatch'])->name('dispatch');
|
|
||||||
Route::post('/{package}/cancel', [\App\Http\Controllers\Admin\PackageController::class, 'cancel'])->name('cancel');
|
|
||||||
Route::delete('/{package}', [\App\Http\Controllers\Admin\PackageController::class, 'destroy'])->name('destroy');
|
|
||||||
Route::get('/contracts/list', [\App\Http\Controllers\Admin\PackageController::class, 'contracts'])->name('contracts');
|
|
||||||
Route::post('/from-contracts', [\App\Http\Controllers\Admin\PackageController::class, 'storeFromContracts'])->name('store-from-contracts');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service
|
// Contract document generation (JSON) - protected by auth+verified; permission enforced inside controller service
|
||||||
|
|
@ -504,20 +501,12 @@
|
||||||
Route::get('bookings', [AccountBookingController::class, 'index'])->name('bookings.index');
|
Route::get('bookings', [AccountBookingController::class, 'index'])->name('bookings.index');
|
||||||
Route::post('bookings', [AccountBookingController::class, 'store'])->name('bookings.store');
|
Route::post('bookings', [AccountBookingController::class, 'store'])->name('bookings.store');
|
||||||
Route::delete('bookings/{booking}', [AccountBookingController::class, 'destroy'])->name('bookings.destroy');
|
Route::delete('bookings/{booking}', [AccountBookingController::class, 'destroy'])->name('bookings.destroy');
|
||||||
|
|
||||||
Route::get('installments/list', [AccountInstallmentController::class, 'list'])->name('installments.list');
|
|
||||||
Route::post('installments', [AccountInstallmentController::class, 'store'])->name('installments.store');
|
|
||||||
Route::delete('installments/{installment}', [AccountInstallmentController::class, 'destroy'])->name('installments.destroy');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// settings - payment settings
|
// settings - payment settings
|
||||||
Route::get('settings/payment', [PaymentSettingController::class, 'edit'])->name('settings.payment.edit');
|
Route::get('settings/payment', [PaymentSettingController::class, 'edit'])->name('settings.payment.edit');
|
||||||
Route::put('settings/payment', [PaymentSettingController::class, 'update'])->name('settings.payment.update');
|
Route::put('settings/payment', [PaymentSettingController::class, 'update'])->name('settings.payment.update');
|
||||||
|
|
||||||
// settings - installment settings
|
|
||||||
Route::get('settings/installment', [InstallmentSettingController::class, 'edit'])->name('settings.installment.edit');
|
|
||||||
Route::put('settings/installment', [InstallmentSettingController::class, 'update'])->name('settings.installment.update');
|
|
||||||
|
|
||||||
Route::get('types/address', function (Request $request) {
|
Route::get('types/address', function (Request $request) {
|
||||||
$types = App\Models\Person\AddressType::all();
|
$types = App\Models\Person\AddressType::all();
|
||||||
|
|
||||||
|
|
@ -537,8 +526,4 @@
|
||||||
});
|
});
|
||||||
Route::get('reports/{slug}/export', [\App\Http\Controllers\ReportController::class, 'export'])->middleware('permission:reports-export')->name('reports.export');
|
Route::get('reports/{slug}/export', [\App\Http\Controllers\ReportController::class, 'export'])->middleware('permission:reports-export')->name('reports.export');
|
||||||
|
|
||||||
// Call laters
|
|
||||||
Route::get('call-laters', [\App\Http\Controllers\CallLaterController::class, 'index'])->name('callLaters.index');
|
|
||||||
Route::patch('call-laters/{callLater}/complete', [\App\Http\Controllers\CallLaterController::class, 'complete'])->name('callLaters.complete');
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
$contract3->segments()->attach($segment->id, ['active' => true]);
|
$contract3->segments()->attach($segment->id, ['active' => true]);
|
||||||
|
|
||||||
// Test without date filters - should return all contracts
|
// Test without date filters - should return all contracts
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
expect($data)->toHaveCount(3);
|
expect($data)->toHaveCount(3);
|
||||||
|
|
||||||
// Test with start_date_from filter
|
// Test with start_date_from filter
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
'start_date_from' => '2024-02-01',
|
'start_date_from' => '2024-02-01',
|
||||||
]));
|
]));
|
||||||
|
|
@ -92,7 +92,7 @@
|
||||||
expect(collect($data)->pluck('reference'))->toContain('CONTRACT-2024-002', 'CONTRACT-2024-003');
|
expect(collect($data)->pluck('reference'))->toContain('CONTRACT-2024-002', 'CONTRACT-2024-003');
|
||||||
|
|
||||||
// Test with start_date_to filter
|
// Test with start_date_to filter
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
'start_date_to' => '2024-03-31',
|
'start_date_to' => '2024-03-31',
|
||||||
]));
|
]));
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
expect(collect($data)->pluck('reference'))->toContain('CONTRACT-2024-001', 'CONTRACT-2024-002');
|
expect(collect($data)->pluck('reference'))->toContain('CONTRACT-2024-001', 'CONTRACT-2024-002');
|
||||||
|
|
||||||
// Test with both date filters
|
// Test with both date filters
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
'start_date_from' => '2024-02-01',
|
'start_date_from' => '2024-02-01',
|
||||||
'start_date_to' => '2024-04-30',
|
'start_date_to' => '2024-04-30',
|
||||||
|
|
@ -133,7 +133,7 @@
|
||||||
$segment = Segment::factory()->create(['active' => true]);
|
$segment = Segment::factory()->create(['active' => true]);
|
||||||
|
|
||||||
// Test invalid start_date_from
|
// Test invalid start_date_from
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
'start_date_from' => 'invalid-date',
|
'start_date_from' => 'invalid-date',
|
||||||
]));
|
]));
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
$response->assertJsonValidationErrors('start_date_from');
|
$response->assertJsonValidationErrors('start_date_from');
|
||||||
|
|
||||||
// Test invalid start_date_to
|
// Test invalid start_date_to
|
||||||
$response = $this->getJson(route('packages.contracts', [
|
$response = $this->getJson(route('admin.packages.contracts', [
|
||||||
'segment_id' => $segment->id,
|
'segment_id' => $segment->id,
|
||||||
'start_date_to' => 'invalid-date',
|
'start_date_to' => 'invalid-date',
|
||||||
]));
|
]));
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
// Override the global uses() so these pure-logic tests skip RefreshDatabase
|
|
||||||
uses(\PHPUnit\Framework\TestCase::class);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit-level tests for the decision_ids filter logic used in AutoMailDispatcher.
|
|
||||||
* These tests execute the filter predicate in isolation without database interaction.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates the filter closure from AutoMailDispatcher::maybeQueue().
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $preferences
|
|
||||||
*/
|
|
||||||
function emailPassesDecisionFilter(array $preferences, int $decisionId): bool
|
|
||||||
{
|
|
||||||
$decisionIds = $preferences['decision_ids'] ?? [];
|
|
||||||
|
|
||||||
if (empty($decisionIds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return in_array($decisionId, array_map('intval', $decisionIds), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('email with no decision_ids restriction passes the filter for any decision', function () {
|
|
||||||
expect(emailPassesDecisionFilter([], 5))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with a matching decision_id in preferences passes the filter', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => [3, 7]], 7))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with a non-matching decision_id in preferences is filtered out', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => [3, 7]], 99))->toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with empty preferences is treated as no restriction', function () {
|
|
||||||
expect(emailPassesDecisionFilter([], 42))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('string decision ids in preferences are cast to int for comparison', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => ['3', '7']], 7))->toBeTrue();
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => ['3', '7']], 99))->toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
@ -3,4 +3,3 @@
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
uses(TestCase::class)->in('Feature', 'Unit');
|
uses(TestCase::class)->in('Feature', 'Unit');
|
||||||
uses(\PHPUnit\Framework\TestCase::class)->in('Pure');
|
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit-level tests for the decision_ids filter logic used in AutoMailDispatcher.
|
|
||||||
* These tests execute the filter predicate in isolation without database interaction.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulates the filter closure from AutoMailDispatcher::maybeQueue().
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $preferences
|
|
||||||
*/
|
|
||||||
function emailPassesDecisionFilter(array $preferences, int $decisionId): bool
|
|
||||||
{
|
|
||||||
$decisionIds = $preferences['decision_ids'] ?? [];
|
|
||||||
|
|
||||||
if (empty($decisionIds)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return in_array($decisionId, array_map('intval', $decisionIds), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('email with no decision_ids restriction passes the filter for any decision', function () {
|
|
||||||
expect(emailPassesDecisionFilter([], 5))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with a matching decision_id in preferences passes the filter', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => [3, 7]], 7))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with a non-matching decision_id in preferences is filtered out', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => [3, 7]], 99))->toBeFalse();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('email with empty preferences is treated as no restriction', function () {
|
|
||||||
expect(emailPassesDecisionFilter([], 42))->toBeTrue();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('string decision ids in preferences are cast to int for comparison', function () {
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => ['3', '7']], 7))->toBeTrue();
|
|
||||||
expect(emailPassesDecisionFilter(['decision_ids' => ['3', '7']], 99))->toBeFalse();
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user