diff --git a/app/Http/Controllers/ClientCaseContoller.php b/app/Http/Controllers/ClientCaseContoller.php index fd4ef03..1476525 100644 --- a/app/Http/Controllers/ClientCaseContoller.php +++ b/app/Http/Controllers/ClientCaseContoller.php @@ -1778,7 +1778,31 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph return back()->with('error', 'Unable to verify SMS credits.'); } - // Queue the SMS send; activity will be created in the job on success if a template is provided + // Create an activity before sending + $activityNote = sprintf('Št: %s | Telo: %s', (string) $phone->nu, (string) $validated['message']); + $activityData = [ + 'note' => $activityNote, + 'user_id' => optional($request->user())->id, + ]; + // If template provided, attach its action/decision to the activity + if (! empty($validated['template_id'])) { + $tpl = \App\Models\SmsTemplate::find((int) $validated['template_id']); + if ($tpl) { + $activityData['action_id'] = $tpl->action_id; + $activityData['decision_id'] = $tpl->decision_id; + } + } + // Attach contract_id if contract_uuid is provided and belongs to this case + if (! empty($validated['contract_uuid'])) { + $contract = $clientCase->contracts()->where('uuid', $validated['contract_uuid'])->first(); + if ($contract) { + $activityData['contract_id'] = $contract->id; + } + } + + $activity = $clientCase->activities()->create($activityData); + + // Queue the SMS send; pass activity_id so the job can update note on failure and skip creating a new activity \App\Jobs\SendSmsJob::dispatch( profileId: $profile->id, to: (string) $phone->nu, @@ -1790,6 +1814,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph templateId: $validated['template_id'] ?? null, clientCaseId: $clientCase->id, userId: optional($request->user())->id, + activityId: $activity?->id, ); return back()->with('success', 'SMS je bil dodan v čakalno vrsto.'); diff --git a/app/Jobs/SendSmsJob.php b/app/Jobs/SendSmsJob.php index 8d416af..6a09d20 100644 --- a/app/Jobs/SendSmsJob.php +++ b/app/Jobs/SendSmsJob.php @@ -33,6 +33,8 @@ public function __construct( public ?int $templateId = null, public ?int $clientCaseId = null, public ?int $userId = null, + // If provided, update this activity on failure instead of creating a new one + public ?int $activityId = null, ) {} /** @@ -86,8 +88,28 @@ public function handle(SmsService $sms): void clientReference: $this->clientReference, ); } - // If invoked from the case UI with a selected template, create an Activity - if ($this->templateId && $this->clientCaseId && $log) { + // Update an existing pre-created activity ONLY on failure when activityId is provided + if ($this->activityId && $log && ($log->status === 'failed')) { + try { + $activity = \App\Models\Activity::find($this->activityId); + if ($activity) { + $note = (string) ($activity->note ?? ''); + $append = sprintf( + ' | Napaka: %s', + 'SMS ni bil poslan!' + ); + $activity->update(['note' => $note.$append]); + } + } catch (\Throwable $e) { + \Log::warning('SendSmsJob activity update failed', [ + 'error' => $e->getMessage(), + 'activity_id' => $this->activityId, + ]); + } + } + + // If no pre-created activity is provided and invoked from the case UI with a selected template, create an Activity + if (!$this->activityId && $this->templateId && $this->clientCaseId && $log) { try { /** @var SmsTemplate|null $template */ $template = SmsTemplate::find($this->templateId); diff --git a/database/seeders/AddManagerRoleSeeder.php b/database/seeders/AddManagerRoleSeeder.php new file mode 100644 index 0000000..940783e --- /dev/null +++ b/database/seeders/AddManagerRoleSeeder.php @@ -0,0 +1,30 @@ + 'manager'], + [ + 'name' => 'Manager', + 'description' => 'Team manager with elevated permissions', + ] + ); + + // Give Manager all permissions except sensitive settings management (idempotent) + // If permissions are not seeded yet, this will simply sync an empty set. + $permissionIds = Permission::query() + ->where('slug', '!=', 'manage-settings') + ->pluck('id'); + + $manager->permissions()->sync($permissionIds); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 357937a..3b23a93 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -40,6 +40,8 @@ public function run(): void TestUserSeeder::class, ProductionUserSeeder::class, AdditionalProductionUsersSeeder::class, + // Roles/permissions: ensure Manager role exists (admin, manager, staff, viewer) + AddManagerRoleSeeder::class, ]); } } diff --git a/resources/js/Components/CurrencyInput.vue b/resources/js/Components/CurrencyInput.vue index 59b6554..2362c82 100644 --- a/resources/js/Components/CurrencyInput.vue +++ b/resources/js/Components/CurrencyInput.vue @@ -1,22 +1,22 @@ diff --git a/resources/js/Components/DatePickerField.vue b/resources/js/Components/DatePickerField.vue index becccc8..144eed6 100644 --- a/resources/js/Components/DatePickerField.vue +++ b/resources/js/Components/DatePickerField.vue @@ -2,23 +2,24 @@ import InputLabel from "./InputLabel.vue"; import InputError from "./InputError.vue"; import { computed } from "vue"; +import VueDatePicker from "@vuepic/vue-datepicker"; +import "@vuepic/vue-datepicker/dist/main.css"; /* - DatePickerField (v-calendar) - - A thin wrapper around with a label and error support. - - Uses v-calendar which handles popovers/teleport well inside modals. - API: kept compatible with previous usage where possible. + DatePickerField (vue-datepicker wrapper) + - Replaces previous v-calendar usage to avoid range/dayIndex runtime errors. + - Keeps API compatible with existing callers. Props: - modelValue: Date | string | number | null - id: string - label: string - format: string (default 'dd.MM.yyyy') - enableTimePicker: boolean (default false) - - inline: boolean (default false) // When true, keeps the popover visible + - inline: boolean (default false) - placeholder: string - error: string | string[] - Note: Props like teleportTarget/autoPosition/menuClassName/fixed/closeOn... were for the old picker - and are accepted for compatibility but are not used by v-calendar. + - legacy props (teleportTarget, autoPosition, menuClassName, fixed, closeOnAutoApply, closeOnScroll) + are accepted for compatibility but only mapped where applicable. */ const props = defineProps({ @@ -28,7 +29,7 @@ const props = defineProps({ format: { type: String, default: "dd.MM.yyyy" }, enableTimePicker: { type: Boolean, default: false }, inline: { type: Boolean, default: false }, - // legacy/unused in v-calendar (kept to prevent breaking callers) + // legacy props maintained for compatibility autoApply: { type: Boolean, default: false }, teleportTarget: { type: [Boolean, String], default: "body" }, autoPosition: { type: Boolean, default: true }, @@ -50,44 +51,31 @@ const valueProxy = computed({ }, }); -// Convert common date mask from lowercase tokens to v-calendar tokens -const inputMask = computed(() => { - let m = props.format || "dd.MM.yyyy"; - return ( - m.replace(/yyyy/g, "YYYY").replace(/dd/g, "DD").replace(/MM/g, "MM") + - (props.enableTimePicker ? " HH:mm" : "") - ); -}); - -const popoverCfg = computed(() => ({ - visibility: props.inline ? "visible" : "click", - placement: "bottom-start", -})); +// vue-datepicker accepts format like 'dd.MM.yyyy' and controls 24h time via is-24 prop +const inputClasses = + "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500";