Add more permissions

This commit is contained in:
Simon Pocrnjič 2025-10-31 10:16:38 +01:00
parent 7d4d18143d
commit ed4f67effb
18 changed files with 404 additions and 193 deletions

View File

@ -1778,7 +1778,31 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
return back()->with('error', 'Unable to verify SMS credits.'); 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( \App\Jobs\SendSmsJob::dispatch(
profileId: $profile->id, profileId: $profile->id,
to: (string) $phone->nu, to: (string) $phone->nu,
@ -1790,6 +1814,7 @@ public function sendSmsToPhone(ClientCase $clientCase, Request $request, int $ph
templateId: $validated['template_id'] ?? null, templateId: $validated['template_id'] ?? null,
clientCaseId: $clientCase->id, clientCaseId: $clientCase->id,
userId: optional($request->user())->id, userId: optional($request->user())->id,
activityId: $activity?->id,
); );
return back()->with('success', 'SMS je bil dodan v čakalno vrsto.'); return back()->with('success', 'SMS je bil dodan v čakalno vrsto.');

View File

@ -33,6 +33,8 @@ public function __construct(
public ?int $templateId = null, public ?int $templateId = null,
public ?int $clientCaseId = null, public ?int $clientCaseId = null,
public ?int $userId = 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, clientReference: $this->clientReference,
); );
} }
// If invoked from the case UI with a selected template, create an Activity // Update an existing pre-created activity ONLY on failure when activityId is provided
if ($this->templateId && $this->clientCaseId && $log) { 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 { try {
/** @var SmsTemplate|null $template */ /** @var SmsTemplate|null $template */
$template = SmsTemplate::find($this->templateId); $template = SmsTemplate::find($this->templateId);

View File

@ -0,0 +1,30 @@
<?php
namespace Database\Seeders;
use App\Models\Permission;
use App\Models\Role;
use Illuminate\Database\Seeder;
class AddManagerRoleSeeder extends Seeder
{
public function run(): void
{
// Ensure the Manager role exists
$manager = Role::firstOrCreate(
['slug' => '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);
}
}

View File

@ -40,6 +40,8 @@ public function run(): void
TestUserSeeder::class, TestUserSeeder::class,
ProductionUserSeeder::class, ProductionUserSeeder::class,
AdditionalProductionUsersSeeder::class, AdditionalProductionUsersSeeder::class,
// Roles/permissions: ensure Manager role exists (admin, manager, staff, viewer)
AddManagerRoleSeeder::class,
]); ]);
} }
} }

View File

@ -1,22 +1,22 @@
<script setup> <script setup>
import { watch, onMounted } from 'vue' import { watch, onMounted } from "vue";
import { useCurrencyInput } from 'vue-currency-input' import { useCurrencyInput } from "vue-currency-input";
const props = defineProps({ const props = defineProps({
modelValue: { type: [Number, String, null], default: null }, modelValue: { type: [Number, String, null], default: null },
id: String, id: String,
name: String, name: String,
placeholder: { type: String, default: '0,00' }, placeholder: { type: String, default: "0,00" },
disabled: Boolean, disabled: Boolean,
required: Boolean, required: Boolean,
currency: { type: String, default: 'EUR' }, currency: { type: String, default: "EUR" },
locale: { type: String, default: 'sl-SI' }, locale: { type: String, default: "sl-SI" },
precision: { type: [Number, Object], default: 2 }, precision: { type: [Number, Object], default: 2 },
allowNegative: { type: Boolean, default: false }, allowNegative: { type: Boolean, default: false },
useGrouping: { type: Boolean, default: true }, useGrouping: { type: Boolean, default: true },
}) });
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(["update:modelValue", "change"]);
const { inputRef, numberValue, setValue, setOptions } = useCurrencyInput({ const { inputRef, numberValue, setValue, setOptions } = useCurrencyInput({
currency: props.currency, currency: props.currency,
@ -24,35 +24,51 @@ const { inputRef, numberValue, setValue, setOptions } = useCurrencyInput({
precision: props.precision, precision: props.precision,
useGrouping: props.useGrouping, useGrouping: props.useGrouping,
valueRange: props.allowNegative ? {} : { min: 0 }, valueRange: props.allowNegative ? {} : { min: 0 },
}) });
watch(() => props.modelValue, (val) => { watch(
const numeric = typeof val === 'string' ? parseFloat(val) : val () => props.modelValue,
(val) => {
const numeric = typeof val === "string" ? parseFloat(val) : val;
if (numeric !== numberValue.value) { if (numeric !== numberValue.value) {
setValue(isNaN(numeric) ? null : numeric) setValue(isNaN(numeric) ? null : numeric);
} }
}, { immediate: true }) },
{ immediate: true }
);
watch(numberValue, (val) => { watch(numberValue, (val) => {
emit('update:modelValue', val) emit("update:modelValue", val);
}) });
watch(() => [props.currency, props.locale, props.precision, props.useGrouping, props.allowNegative], () => { watch(
() => [
props.currency,
props.locale,
props.precision,
props.useGrouping,
props.allowNegative,
],
() => {
setOptions({ setOptions({
currency: props.currency, currency: props.currency,
locale: props.locale, locale: props.locale,
precision: props.precision, precision: props.precision,
useGrouping: props.useGrouping, useGrouping: props.useGrouping,
valueRange: props.allowNegative ? {} : { min: 0 }, valueRange: props.allowNegative ? {} : { min: 0 },
}) });
}) }
);
onMounted(() => { onMounted(() => {
if (props.modelValue != null) { if (props.modelValue != null) {
const numeric = typeof props.modelValue === 'string' ? parseFloat(props.modelValue) : props.modelValue const numeric =
setValue(isNaN(numeric) ? null : numeric) typeof props.modelValue === "string"
? parseFloat(props.modelValue)
: props.modelValue;
setValue(isNaN(numeric) ? null : numeric);
} }
}) });
</script> </script>
<template> <template>
@ -67,5 +83,6 @@ onMounted(() => {
:required="required" :required="required"
class="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-800 dark:border-gray-600" class="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-800 dark:border-gray-600"
autocomplete="off" autocomplete="off"
@change="$emit('change', numberValue)"
/> />
</template> </template>

View File

@ -2,23 +2,24 @@
import InputLabel from "./InputLabel.vue"; import InputLabel from "./InputLabel.vue";
import InputError from "./InputError.vue"; import InputError from "./InputError.vue";
import { computed } from "vue"; import { computed } from "vue";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
/* /*
DatePickerField (v-calendar) DatePickerField (vue-datepicker wrapper)
- A thin wrapper around <VDatePicker> with a label and error support. - Replaces previous v-calendar usage to avoid range/dayIndex runtime errors.
- Uses v-calendar which handles popovers/teleport well inside modals. - Keeps API compatible with existing callers.
API: kept compatible with previous usage where possible.
Props: Props:
- modelValue: Date | string | number | null - modelValue: Date | string | number | null
- id: string - id: string
- label: string - label: string
- format: string (default 'dd.MM.yyyy') - format: string (default 'dd.MM.yyyy')
- enableTimePicker: boolean (default false) - enableTimePicker: boolean (default false)
- inline: boolean (default false) // When true, keeps the popover visible - inline: boolean (default false)
- placeholder: string - placeholder: string
- error: string | string[] - error: string | string[]
Note: Props like teleportTarget/autoPosition/menuClassName/fixed/closeOn... were for the old picker - legacy props (teleportTarget, autoPosition, menuClassName, fixed, closeOnAutoApply, closeOnScroll)
and are accepted for compatibility but are not used by v-calendar. are accepted for compatibility but only mapped where applicable.
*/ */
const props = defineProps({ const props = defineProps({
@ -28,7 +29,7 @@ const props = defineProps({
format: { type: String, default: "dd.MM.yyyy" }, format: { type: String, default: "dd.MM.yyyy" },
enableTimePicker: { type: Boolean, default: false }, enableTimePicker: { type: Boolean, default: false },
inline: { 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 }, autoApply: { type: Boolean, default: false },
teleportTarget: { type: [Boolean, String], default: "body" }, teleportTarget: { type: [Boolean, String], default: "body" },
autoPosition: { type: Boolean, default: true }, autoPosition: { type: Boolean, default: true },
@ -50,44 +51,31 @@ const valueProxy = computed({
}, },
}); });
// Convert common date mask from lowercase tokens to v-calendar tokens // vue-datepicker accepts format like 'dd.MM.yyyy' and controls 24h time via is-24 prop
const inputMask = computed(() => { const inputClasses =
let m = props.format || "dd.MM.yyyy"; "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500";
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",
}));
</script> </script>
<template> <template>
<div class="col-span-6 sm:col-span-4"> <div class="col-span-6 sm:col-span-4">
<InputLabel v-if="label" :for="id" :value="label" /> <InputLabel v-if="label" :for="id" :value="label" />
<!-- VCalendar DatePicker with custom input to keep Tailwind styling --> <VueDatePicker
<VDatePicker
v-model="valueProxy" v-model="valueProxy"
:mode="enableTimePicker ? 'dateTime' : 'date'" :format="format"
:masks="{ input: inputMask }" :enable-time-picker="enableTimePicker"
:popover="popoverCfg" :is-24="true"
:is24hr="true" :inline="inline"
> :auto-apply="autoApply"
<template #default="{ inputValue, inputEvents }"> :teleport="false"
<input :close-on-auto-apply="closeOnAutoApply"
:close-on-scroll="closeOnScroll"
:input-class-name="inputClasses"
:menu-class-name="'z-[1000]'"
:locale="'sl'"
:id="id" :id="id"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
:placeholder="placeholder" :placeholder="placeholder"
:value="inputValue"
autocomplete="off"
v-on="inputEvents"
/> />
</template>
</VDatePicker>
<template v-if="error"> <template v-if="error">
<InputError <InputError
@ -102,5 +90,5 @@ const popoverCfg = computed(() => ({
</template> </template>
<style> <style>
/* Ensure the date picker menu overlays modals/dialogs */ /* vue-datepicker provides its own menu layering; base CSS imported above */
</style> </style>

View File

@ -19,9 +19,13 @@ import { router, usePage } from "@inertiajs/vue3";
const props = defineProps({ const props = defineProps({
person: Object, person: Object,
personEdit: {
type: Boolean,
default: true,
},
edit: { edit: {
type: Boolean, type: Boolean,
default: false, default: true,
}, },
tabColor: { tabColor: {
type: String, type: String,
@ -584,7 +588,7 @@ const submitSms = () => {
<CusTab name="person" title="Oseba"> <CusTab name="person" title="Oseba">
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800"> <span class="border-b-2 border-gray-500 hover:border-gray-800">
<button @click="openDrawerUpdateClient"> <button @click="openDrawerUpdateClient" v-if="edit && personEdit">
<UserEditIcon size="lg" css="text-gray-500 hover:text-gray-800" /> <UserEditIcon size="lg" css="text-gray-500 hover:text-gray-800" />
</button> </button>
</span> </span>
@ -636,7 +640,7 @@ const submitSms = () => {
</CusTab> </CusTab>
<CusTab name="addresses" title="Naslovi"> <CusTab name="addresses" title="Naslovi">
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800"> <span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button> <button>
<PlusIcon <PlusIcon
@click="openDrawerAddAddress(false, 0)" @click="openDrawerAddAddress(false, 0)"
@ -653,7 +657,7 @@ const submitSms = () => {
<FwbBadge type="yellow">{{ address.country }}</FwbBadge> <FwbBadge type="yellow">{{ address.country }}</FwbBadge>
<FwbBadge>{{ address.type.name }}</FwbBadge> <FwbBadge>{{ address.type.name }}</FwbBadge>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2" v-if="edit">
<button> <button>
<EditIcon <EditIcon
@click="openDrawerAddAddress(true, address.id)" @click="openDrawerAddAddress(true, address.id)"
@ -678,7 +682,7 @@ const submitSms = () => {
</CusTab> </CusTab>
<CusTab name="phones" title="Telefonske"> <CusTab name="phones" title="Telefonske">
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800"> <span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button> <button>
<PlusIcon <PlusIcon
@click="operDrawerAddPhone(false, 0)" @click="operDrawerAddPhone(false, 0)"
@ -707,14 +711,14 @@ const submitSms = () => {
> >
SMS SMS
</button> </button>
<button> <button v-if="edit">
<EditIcon <EditIcon
@click="operDrawerAddPhone(true, phone.id)" @click="operDrawerAddPhone(true, phone.id)"
size="md" size="md"
css="text-gray-500 hover:text-gray-800" css="text-gray-500 hover:text-gray-800"
/> />
</button> </button>
<button @click="openConfirm('phone', phone.id, phone.nu)"> <button @click="openConfirm('phone', phone.id, phone.nu)" v-if="edit">
<TrashBinIcon size="md" css="text-red-600 hover:text-red-700" /> <TrashBinIcon size="md" css="text-red-600 hover:text-red-700" />
</button> </button>
</div> </div>
@ -725,7 +729,7 @@ const submitSms = () => {
</CusTab> </CusTab>
<CusTab name="emails" title="Email"> <CusTab name="emails" title="Email">
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800"> <span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button> <button>
<PlusIcon <PlusIcon
@click="openDrawerAddEmail(false, 0)" @click="openDrawerAddEmail(false, 0)"
@ -742,7 +746,10 @@ const submitSms = () => {
v-for="(email, idx) in getEmails(person)" v-for="(email, idx) in getEmails(person)"
:key="idx" :key="idx"
> >
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"> <div
class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"
v-if="edit"
>
<div class="flex gap-2"> <div class="flex gap-2">
<FwbBadge v-if="email?.label">{{ email.label }}</FwbBadge> <FwbBadge v-if="email?.label">{{ email.label }}</FwbBadge>
<FwbBadge v-else type="indigo">Email</FwbBadge> <FwbBadge v-else type="indigo">Email</FwbBadge>
@ -777,7 +784,7 @@ const submitSms = () => {
</CusTab> </CusTab>
<CusTab name="trr" title="TRR"> <CusTab name="trr" title="TRR">
<div class="flex justify-end mb-2"> <div class="flex justify-end mb-2">
<span class="border-b-2 border-gray-500 hover:border-gray-800"> <span class="border-b-2 border-gray-500 hover:border-gray-800" v-if="edit">
<button> <button>
<PlusIcon <PlusIcon
@click="openDrawerAddTrr(false, 0)" @click="openDrawerAddTrr(false, 0)"
@ -794,7 +801,10 @@ const submitSms = () => {
v-for="(acc, idx) in getTRRs(person)" v-for="(acc, idx) in getTRRs(person)"
:key="idx" :key="idx"
> >
<div class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"> <div
class="text-sm leading-5 md:text-sm text-gray-500 flex justify-between"
v-if="edit"
>
<div class="flex gap-2"> <div class="flex gap-2">
<FwbBadge v-if="acc?.bank_name">{{ acc.bank_name }}</FwbBadge> <FwbBadge v-if="acc?.bank_name">{{ acc.bank_name }}</FwbBadge>
<FwbBadge v-if="acc?.holder_name" type="indigo">{{ <FwbBadge v-if="acc?.holder_name" type="indigo">{{

View File

@ -10,6 +10,12 @@ defineProps({
type: String, type: String,
default: 'bg-blue-500' default: 'bg-blue-500'
}, },
// Optional text color utility class (e.g., 'text-white', 'text-gray-800').
// Left empty by default because the base class already includes 'text-white'.
color: {
type: String,
default: ''
},
}); });
const isHover = ref(false); const isHover = ref(false);

View File

@ -162,6 +162,7 @@ const rawMenuGroups = [
}, },
{ {
label: "Uvoz", label: "Uvoz",
requires: { permission: "manage-imports" },
items: [ items: [
{ {
key: "imports", key: "imports",
@ -196,6 +197,9 @@ const rawMenuGroups = [
}, },
{ {
label: "Konfiguracija", label: "Konfiguracija",
// Group-level authorization: show "Konfiguracija" only to admins or users with manage-settings
// You can set requires on any group to hide the whole section unless allowed
requires: { permission: "manage-settings" },
items: [ items: [
{ {
key: "settings", key: "settings",
@ -211,7 +215,7 @@ const rawMenuGroups = [
title: "Administrator", title: "Administrator",
routeName: "admin.index", routeName: "admin.index",
active: ["admin.index", "admin.users.index", "admin.permissions.create"], active: ["admin.index", "admin.users.index", "admin.permissions.create"],
requires: { role: "admin", permission: "manage-settings" }, requires: { role: "admin" },
}, },
], ],
}, },
@ -222,23 +226,33 @@ const menuGroups = computed(() => {
const roles = (user.roles || []).map((r) => r.slug); const roles = (user.roles || []).map((r) => r.slug);
const permissions = user.permissions || []; const permissions = user.permissions || [];
// Helper to determine inclusion based on optional requires meta // Generic helper to determine inclusion based on optional `requires` meta
function allowed(item) { function allowedMeta(entity) {
if (!item.requires) return true; const req = entity?.requires;
const needRole = item.requires.role; if (!req) {
const needPerm = item.requires.permission; return true;
}
const needRole = req.role ?? "admin";
const needPerm = req.permission;
return ( return (
(needRole && roles.includes(needRole)) || (needRole && roles.includes(needRole)) ||
(needPerm && permissions.includes(needPerm)) (needPerm && permissions.includes(needPerm))
); );
} }
return rawMenuGroups.map((g) => { return (
const items = g.items rawMenuGroups
.filter(allowed) // Group-level permission check (hide whole group if not allowed)
.filter((g) => allowedMeta(g))
.map((g) => {
const items = (g.items || [])
.filter((item) => allowedMeta(item))
.sort((a, b) => a.title.localeCompare(b.title, "sl", { sensitivity: "base" })); .sort((a, b) => a.title.localeCompare(b.title, "sl", { sensitivity: "base" }));
return { label: g.label, items }; return { label: g.label, items };
}); })
// Drop groups that end up empty after item filtering
.filter((g) => g.items.length > 0)
);
}); });
// Icon map for menu keys -> FontAwesome icon definitions // Icon map for menu keys -> FontAwesome icon definitions

View File

@ -319,7 +319,7 @@ watch(
:auto-position="true" :auto-position="true"
:teleport-target="'body'" :teleport-target="'body'"
:inline="false" :inline="false"
:auto-apply="false" :auto-apply="true"
:fixed="false" :fixed="false"
:close-on-auto-apply="true" :close-on-auto-apply="true"
:close-on-scroll="true" :close-on-scroll="true"

View File

@ -14,6 +14,7 @@ library.add(faTrash, faEllipsisVertical, faCopy);
const props = defineProps({ const props = defineProps({
client_case: Object, client_case: Object,
activities: Object, activities: Object,
edit: Boolean,
}); });
const fmtDate = (d) => { const fmtDate = (d) => {
@ -91,11 +92,11 @@ const copyToClipboard = async (text) => {
// You could add a toast notification here if available // You could add a toast notification here if available
} catch (err) { } catch (err) {
// Fallback for older browsers // Fallback for older browsers
const textArea = document.createElement('textarea'); const textArea = document.createElement("textarea");
textArea.value = text; textArea.value = text;
document.body.appendChild(textArea); document.body.appendChild(textArea);
textArea.select(); textArea.select();
document.execCommand('copy'); document.execCommand("copy");
document.body.removeChild(textArea); document.body.removeChild(textArea);
} }
}; };
@ -115,7 +116,7 @@ const copyToClipboard = async (text) => {
<th>Opomba</th> <th>Opomba</th>
<th>Obljuba</th> <th>Obljuba</th>
<th>Dodal</th> <th>Dodal</th>
<th class="w-8"></th> <th class="w-8" v-if="edit"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -174,7 +175,9 @@ const copyToClipboard = async (text) => {
</template> </template>
<template #content> <template #content>
<div class="relative" @click.stop> <div class="relative" @click.stop>
<div class="flex items-center justify-between p-1 border-b border-gray-200"> <div
class="flex items-center justify-between p-1 border-b border-gray-200"
>
<span class="text-xs font-medium text-gray-600">Opomba</span> <span class="text-xs font-medium text-gray-600">Opomba</span>
<button <button
@click="copyToClipboard(row.note)" @click="copyToClipboard(row.note)"
@ -226,7 +229,7 @@ const copyToClipboard = async (text) => {
> >
</div> </div>
</td> </td>
<td class="py-2 pl-2 pr-2 align-middle text-right"> <td class="py-2 pl-2 pr-2 align-middle text-right" v-if="edit">
<Dropdown align="right" width="30"> <Dropdown align="right" width="30">
<template #trigger> <template #trigger>
<button <button

View File

@ -35,6 +35,7 @@ const props = defineProps({
segments: { type: Array, default: () => [] }, segments: { type: Array, default: () => [] },
all_segments: { type: Array, default: () => [] }, all_segments: { type: Array, default: () => [] },
templates: { type: Array, default: () => [] }, // active document templates (latest per slug) templates: { type: Array, default: () => [] }, // active document templates (latest per slug)
edit: { type: Boolean, default: () => false },
}); });
// Debug: log incoming contract balances (remove after fix) // Debug: log incoming contract balances (remove after fix)
@ -484,7 +485,7 @@ const closePaymentsDialog = () => {
> >
Opis</FwbTableHeadCell Opis</FwbTableHeadCell
> >
<FwbTableHeadCell class="w-px" /> <FwbTableHeadCell class="w-px" v-if="edit" />
</FwbTableHead> </FwbTableHead>
<FwbTableBody> <FwbTableBody>
<template v-for="(c, i) in contracts" :key="c.uuid || i"> <template v-for="(c, i) in contracts" :key="c.uuid || i">
@ -497,7 +498,7 @@ const closePaymentsDialog = () => {
<span class="text-gray-700">{{ <span class="text-gray-700">{{
contractActiveSegment(c)?.name || "-" contractActiveSegment(c)?.name || "-"
}}</span> }}</span>
<Dropdown align="left"> <Dropdown align="left" v-if="edit">
<template #trigger> <template #trigger>
<button <button
type="button" type="button"
@ -701,7 +702,7 @@ const closePaymentsDialog = () => {
</Dropdown> </Dropdown>
</div> </div>
</FwbTableCell> </FwbTableCell>
<FwbTableCell class="text-right whitespace-nowrap"> <FwbTableCell class="text-right whitespace-nowrap" v-if="edit">
<Dropdown align="right" width="56"> <Dropdown align="right" width="56">
<template #trigger> <template #trigger>
<button <button

View File

@ -13,11 +13,12 @@ import DocumentEditDialog from "@/Components/DocumentEditDialog.vue";
import DocumentUploadDialog from "@/Components/DocumentUploadDialog.vue"; import DocumentUploadDialog from "@/Components/DocumentUploadDialog.vue";
import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue"; import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue";
import { classifyDocument } from "@/Services/documents"; import { classifyDocument } from "@/Services/documents";
import { router, useForm } from "@inertiajs/vue3"; import { router, useForm, usePage } from "@inertiajs/vue3";
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons"; import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";
import Pagination from "@/Components/Pagination.vue"; import Pagination from "@/Components/Pagination.vue";
import ConfirmDialog from "@/Components/ConfirmDialog.vue"; import ConfirmDialog from "@/Components/ConfirmDialog.vue";
import DialogModal from "@/Components/DialogModal.vue"; import DialogModal from "@/Components/DialogModal.vue";
import { hasPermission } from "@/Services/permissions";
const props = defineProps({ const props = defineProps({
client: Object, client: Object,
@ -35,6 +36,7 @@ const props = defineProps({
contract_doc_templates: { type: Array, default: () => [] }, contract_doc_templates: { type: Array, default: () => [] },
}); });
const page = usePage();
const showUpload = ref(false); const showUpload = ref(false);
const openUpload = () => { const openUpload = () => {
showUpload.value = true; showUpload.value = true;
@ -47,20 +49,25 @@ const onUploaded = () => {
router.reload({ only: ["documents"] }); router.reload({ only: ["documents"] });
}; };
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
const hasPerm = computed(() => (permission) =>
hasPermission(page.props.auth?.user, permission)
);
// Document edit dialog state // Document edit dialog state
const showDocEdit = ref(false) const showDocEdit = ref(false);
const editingDoc = ref(null) const editingDoc = ref(null);
const openDocEdit = (doc) => { const openDocEdit = (doc) => {
editingDoc.value = doc editingDoc.value = doc;
showDocEdit.value = true showDocEdit.value = true;
} };
const closeDocEdit = () => { const closeDocEdit = () => {
showDocEdit.value = false showDocEdit.value = false;
editingDoc.value = null editingDoc.value = null;
} };
const onDocSaved = () => { const onDocSaved = () => {
router.reload({ only: ['documents'] }) router.reload({ only: ["documents"] });
} };
const viewer = ref({ open: false, src: "", title: "" }); const viewer = ref({ open: false, src: "", title: "" });
const openViewer = (doc) => { const openViewer = (doc) => {
@ -236,7 +243,11 @@ const submitAttachSegment = () => {
class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400" class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-blue-400"
> >
<div class="mx-auto max-w-4x1 p-3"> <div class="mx-auto max-w-4x1 p-3">
<PersonInfoGrid :types="types" :person="client.person" /> <PersonInfoGrid
:types="types"
:person="client.person"
:edit="hasPerm('client-edit')"
/>
</div> </div>
</div> </div>
</div> </div>
@ -274,6 +285,7 @@ const submitAttachSegment = () => {
:types="types" :types="types"
tab-color="red-600" tab-color="red-600"
:person="client_case.person" :person="client_case.person"
:person-edit="hasPerm('person-edit')"
:enable-sms="true" :enable-sms="true"
:client-case-uuid="client_case.uuid" :client-case-uuid="client_case.uuid"
/> />
@ -289,7 +301,7 @@ const submitAttachSegment = () => {
<SectionTitle> <SectionTitle>
<template #title> Pogodbe </template> <template #title> Pogodbe </template>
</SectionTitle> </SectionTitle>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2" v-if="hasPerm('contract-edit')">
<FwbButton @click="openDrawerCreateContract">Nova</FwbButton> <FwbButton @click="openDrawerCreateContract">Nova</FwbButton>
<FwbButton <FwbButton
color="light" color="light"
@ -311,6 +323,7 @@ const submitAttachSegment = () => {
:contract_types="contract_types" :contract_types="contract_types"
:segments="segments" :segments="segments"
:templates="contract_doc_templates" :templates="contract_doc_templates"
:edit="hasPerm('contract-edit')"
@edit="openDrawerEditContract" @edit="openDrawerEditContract"
@delete="requestDeleteContract" @delete="requestDeleteContract"
@add-activity="openDrawerAddActivity" @add-activity="openDrawerAddActivity"
@ -330,7 +343,11 @@ const submitAttachSegment = () => {
</SectionTitle> </SectionTitle>
<FwbButton @click="openDrawerAddActivity">Nova</FwbButton> <FwbButton @click="openDrawerAddActivity">Nova</FwbButton>
</div> </div>
<ActivityTable :client_case="client_case" :activities="activities" /> <ActivityTable
:client_case="client_case"
:activities="activities"
:edit="hasPerm('activity-edit')"
/>
<Pagination <Pagination
:links="activities.links" :links="activities.links"
:from="activities.from" :from="activities.from"

View File

@ -1,19 +1,26 @@
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import AppLayout from "@/Layouts/AppLayout.vue"; import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue"; import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue"; import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue"; import TextInput from "@/Components/TextInput.vue";
import { Link, useForm, router } from "@inertiajs/vue3"; import { Link, useForm, router, usePage } from "@inertiajs/vue3";
import ActionMessage from "@/Components/ActionMessage.vue"; import ActionMessage from "@/Components/ActionMessage.vue";
import DialogModal from "@/Components/DialogModal.vue"; import DialogModal from "@/Components/DialogModal.vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue"; import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import { hasPermission } from "@/Services/permissions";
const props = defineProps({ const props = defineProps({
clients: Object, clients: Object,
filters: Object, filters: Object,
}); });
const page = usePage();
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
const hasPerm = computed(() => (permission) =>
hasPermission(page.props.auth?.user, permission)
);
const Address = { const Address = {
address: "", address: "",
country: "", country: "",
@ -87,7 +94,10 @@ const fmtCurrency = (v) => {
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg"> <div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3 space-y-3"> <div class="mx-auto max-w-4x1 py-3 space-y-3">
<!-- Top actions --> <!-- Top actions -->
<div class="flex items-center justify-between gap-3"> <div
class="flex items-center justify-between gap-3"
v-if="hasPerm('client-edit')"
>
<PrimaryButton <PrimaryButton
@click="openDrawerCreateClient" @click="openDrawerCreateClient"
class="bg-blue-600 hover:bg-blue-700" class="bg-blue-600 hover:bg-blue-700"

View File

@ -1,12 +1,13 @@
<script setup> <script setup>
import AppLayout from "@/Layouts/AppLayout.vue"; import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue"; import PrimaryButton from "@/Components/PrimaryButton.vue";
import { ref } from "vue"; import { computed, ref } from "vue";
import { Link } from "@inertiajs/vue3"; import { Link, usePage } from "@inertiajs/vue3";
import SectionTitle from "@/Components/SectionTitle.vue"; import SectionTitle from "@/Components/SectionTitle.vue";
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue"; import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
import FormCreateCase from "./Partials/FormCreateCase.vue"; import FormCreateCase from "./Partials/FormCreateCase.vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue"; import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import { hasPermission } from "@/Services/permissions";
const props = defineProps({ const props = defineProps({
client: Object, client: Object,
@ -19,6 +20,11 @@ const props = defineProps({
// Removed page-level search; DataTable or server can handle filtering elsewhere if needed // Removed page-level search; DataTable or server can handle filtering elsewhere if needed
// DataTable search state // DataTable search state
const search = ref(props.filters?.search || ""); const search = ref(props.filters?.search || "");
const page = usePage();
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
const hasPerm = computed(() => (permission) =>
hasPermission(page.props.auth?.user, permission)
);
const drawerCreateCase = ref(false); const drawerCreateCase = ref(false);
@ -52,7 +58,7 @@ const openDrawerCreateCase = () => {
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2', 'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.show') route().current('client.show')
? 'text-indigo-600 border-indigo-600' ? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300' : 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300',
]" ]"
> >
Primeri Primeri
@ -65,7 +71,7 @@ const openDrawerCreateCase = () => {
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2', 'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.contracts') route().current('client.contracts')
? 'text-indigo-600 border-indigo-600' ? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300' : 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300',
]" ]"
> >
Pogodbe Pogodbe
@ -93,7 +99,10 @@ const openDrawerCreateCase = () => {
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg"> <div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3"> <div class="mx-auto max-w-4x1 py-3">
<div class="flex items-center justify-between gap-3"> <div
class="flex items-center justify-between gap-3"
v-if="hasPerm('case-edit')"
>
<PrimaryButton @click="openDrawerCreateCase" class="bg-blue-400" <PrimaryButton @click="openDrawerCreateCase" class="bg-blue-400"
>Dodaj</PrimaryButton >Dodaj</PrimaryButton
> >

View File

@ -31,6 +31,8 @@ const toDelete = ref(null);
const search = ref(""); const search = ref("");
const selectedTemplateId = ref(null); const selectedTemplateId = ref(null);
const onlyAutoMail = ref(false); const onlyAutoMail = ref(false);
// Filter: selected events (multi-select)
const selectedEvents = ref([]);
const actionOptions = ref([]); const actionOptions = ref([]);
@ -214,6 +216,7 @@ function tryAdoptRaw(ev) {
const filtered = computed(() => { const filtered = computed(() => {
const term = search.value?.toLowerCase() ?? ""; const term = search.value?.toLowerCase() ?? "";
const tplId = selectedTemplateId.value ? Number(selectedTemplateId.value) : null; const tplId = selectedTemplateId.value ? Number(selectedTemplateId.value) : null;
const evIdSet = new Set((selectedEvents.value || []).map((e) => Number(e.id)));
return (props.decisions || []).filter((d) => { return (props.decisions || []).filter((d) => {
const matchesSearch = const matchesSearch =
!term || !term ||
@ -221,7 +224,10 @@ const filtered = computed(() => {
d.color_tag?.toLowerCase().includes(term); d.color_tag?.toLowerCase().includes(term);
const matchesAuto = !onlyAutoMail.value || !!d.auto_mail; const matchesAuto = !onlyAutoMail.value || !!d.auto_mail;
const matchesTemplate = !tplId || Number(d.email_template_id || 0) === tplId; const matchesTemplate = !tplId || Number(d.email_template_id || 0) === tplId;
return matchesSearch && matchesAuto && matchesTemplate; const rowEvents = Array.isArray(d.events) ? d.events : [];
const matchesEvents =
evIdSet.size === 0 || rowEvents.some((ev) => evIdSet.has(Number(ev.id)));
return matchesSearch && matchesAuto && matchesTemplate && matchesEvents;
}); });
}); });
@ -343,29 +349,68 @@ const destroyDecision = () => {
}; };
</script> </script>
<template> <template>
<div class="p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"> <div class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex flex-col sm:flex-row gap-2 items-start sm:items-center"> <div class="w-full bg-gray-50 border rounded-md p-3">
<TextInput v-model="search" placeholder="Iskanje..." class="w-full sm:w-72" /> <div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-center">
<!-- Search -->
<div class="relative sm:col-span-3">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 21l-4.35-4.35m0 0A7.5 7.5 0 1010.5 18.5a7.5 7.5 0 006.15-1.85z"
/>
</svg>
<TextInput v-model="search" placeholder="Iskanje..." class="w-full pl-9 h-10" />
</div>
<!-- Template select -->
<div class="sm:col-span-3">
<select <select
v-model="selectedTemplateId" v-model="selectedTemplateId"
class="block w-full sm:w-64 border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" class="block w-full h-10 border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
> >
<option :value="null">Vse predloge</option> <option :value="null">Vse predloge</option>
<option v-for="t in emailTemplates" :key="t.id" :value="t.id"> <option v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }} {{ t.name }}
</option> </option>
</select> </select>
</div>
<!-- Events multiselect -->
<div class="sm:col-span-4">
<multiselect
v-model="selectedEvents"
:options="availableEvents"
:multiple="true"
track-by="id"
label="name"
placeholder="Filtriraj po dogodkih"
class="w-full"
/>
</div>
<!-- Only auto mail -->
<div class="sm:col-span-2">
<label class="flex items-center gap-2 text-sm"> <label class="flex items-center gap-2 text-sm">
<input <input
type="checkbox" type="checkbox"
v-model="onlyAutoMail" v-model="onlyAutoMail"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 h-4 w-4"
/> />
Samo auto mail Samo auto mail
</label> </label>
</div> </div>
</div>
</div>
<div class="flex-shrink-0">
<PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton> <PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton>
</div> </div>
</div>
<div class="px-4 pb-4"> <div class="px-4 pb-4">
<DataTableClient <DataTableClient
:columns="columns" :columns="columns"

View File

@ -0,0 +1,3 @@
export function hasPermission(user: { roles: Array<{slug: string}>, permissions: Array<string> }, permission: string): boolean {
return user.roles.some( role => role.slug === "admin" ) || user.permissions.includes( permission, 0 );
}

View File

@ -284,7 +284,7 @@
})->name('search'); })->name('search');
// person // person
Route::put('person/{person:uuid}', [PersonController::class, 'update'])->name('person.update'); Route::put('person/{person:uuid}', [PersonController::class, 'update'])->name('person.update')->middleware('permission:person-edit');
Route::post('person/{person:uuid}/address', [PersonController::class, 'createAddress'])->name('person.address.create'); Route::post('person/{person:uuid}/address', [PersonController::class, 'createAddress'])->name('person.address.create');
Route::put('person/{person:uuid}/address/{address_id}', [PersonController::class, 'updateAddress'])->name('person.address.update'); Route::put('person/{person:uuid}/address/{address_id}', [PersonController::class, 'updateAddress'])->name('person.address.update');
Route::delete('person/{person:uuid}/address/{address_id}', [PersonController::class, 'deleteAddress'])->name('person.address.delete'); Route::delete('person/{person:uuid}/address/{address_id}', [PersonController::class, 'deleteAddress'])->name('person.address.delete');
@ -302,10 +302,14 @@
Route::get('clients', [ClientController::class, 'index'])->name('client'); Route::get('clients', [ClientController::class, 'index'])->name('client');
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show'); Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts'); Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts');
Route::middleware('permission:client-edit')->group( function() {
Route::post('clients', [ClientController::class, 'store'])->name('client.store'); Route::post('clients', [ClientController::class, 'store'])->name('client.store');
Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update'); Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update');
Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson'); Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson');
});
// client-case // client-case
Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase'); Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase');
Route::get('client-cases/{client_case:uuid}', [ClientCaseContoller::class, 'show'])->name('clientCase.show'); Route::get('client-cases/{client_case:uuid}', [ClientCaseContoller::class, 'show'])->name('clientCase.show');
@ -314,17 +318,21 @@
Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store'); Route::post('client-cases', [ClientCaseContoller::class, 'store'])->name('clientCase.store');
Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson'); Route::post('client-cases/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson');
// client-case / contract // client-case / contract
Route::get('client-cases/{client_case:uuid}/contract/{uuid}/debug-accounts', [ClientCaseContoller::class, 'debugContractAccounts'])->name('clientCase.contract.debugAccounts');
Route::middleware('permission:contract-edit')->group( function () {
Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store'); Route::post('client-cases/{client_case:uuid}/contract', [ClientCaseContoller::class, 'storeContract'])->name('clientCase.contract.store');
Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update'); Route::put('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'updateContract'])->name('clientCase.contract.update');
Route::get('client-cases/{client_case:uuid}/contract/{uuid}/debug-accounts', [ClientCaseContoller::class, 'debugContractAccounts'])->name('clientCase.contract.debugAccounts');
Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete'); Route::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
});
// client-case / contract / objects // client-case / contract / objects
Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store'); Route::post('client-cases/{client_case:uuid}/contract/{uuid}/objects', [CaseObjectController::class, 'store'])->name('clientCase.contract.object.store');
Route::put('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'update'])->name('clientCase.object.update'); Route::put('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'update'])->name('clientCase.object.update');
Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete'); Route::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete');
// client-case / activity // client-case / activity
Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store'); Route::post('client-cases/{client_case:uuid}/activity', [ClientCaseContoller::class, 'storeActivity'])->name('clientCase.activity.store');
Route::delete('client-cases/{client_case:uuid}/activity/{activity}', [ClientCaseContoller::class, 'deleteActivity'])->name('clientCase.activity.delete'); Route::delete('client-cases/{client_case:uuid}/activity/{activity}', [ClientCaseContoller::class, 'deleteActivity'])->name('clientCase.activity.delete')->middleware("permission:activity-edit");
// client-case / segments // client-case / segments
Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach'); Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach');
// client-case / documents // client-case / documents
@ -386,6 +394,7 @@
Route::get('segments', [SegmentController::class, 'index'])->name('segments.index'); Route::get('segments', [SegmentController::class, 'index'])->name('segments.index');
Route::get('segments/{segment}', [SegmentController::class, 'show'])->name('segments.show'); Route::get('segments/{segment}', [SegmentController::class, 'show'])->name('segments.show');
Route::middleware("permission:manage-imports")->group( function () {
// imports // imports
Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create'); Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create');
Route::get('imports', [ImportController::class, 'index'])->name('imports.index'); Route::get('imports', [ImportController::class, 'index'])->name('imports.index');
@ -423,7 +432,7 @@
Route::delete('imports/{import}', [ImportController::class, 'destroy'])->name('imports.destroy'); Route::delete('imports/{import}', [ImportController::class, 'destroy'])->name('imports.destroy');
// Route::put() // Route::put()
// types // types
});
// accounts / payments & bookings // accounts / payments & bookings
Route::prefix('accounts/{account}')->name('accounts.')->group(function (): void { Route::prefix('accounts/{account}')->name('accounts.')->group(function (): void {
Route::get('payments', [AccountPaymentController::class, 'index'])->name('payments.index'); Route::get('payments', [AccountPaymentController::class, 'index'])->name('payments.index');