Add more permissions
This commit is contained in:
parent
7d4d18143d
commit
ed4f67effb
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
30
database/seeders/AddManagerRoleSeeder.php
Normal file
30
database/seeders/AddManagerRoleSeeder.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
<script setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import { useCurrencyInput } from 'vue-currency-input'
|
||||
import { watch, onMounted } from "vue";
|
||||
import { useCurrencyInput } from "vue-currency-input";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: [Number, String, null], default: null },
|
||||
id: String,
|
||||
name: String,
|
||||
placeholder: { type: String, default: '0,00' },
|
||||
placeholder: { type: String, default: "0,00" },
|
||||
disabled: Boolean,
|
||||
required: Boolean,
|
||||
currency: { type: String, default: 'EUR' },
|
||||
locale: { type: String, default: 'sl-SI' },
|
||||
currency: { type: String, default: "EUR" },
|
||||
locale: { type: String, default: "sl-SI" },
|
||||
precision: { type: [Number, Object], default: 2 },
|
||||
allowNegative: { type: Boolean, default: false },
|
||||
useGrouping: { type: Boolean, default: true },
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
const { inputRef, numberValue, setValue, setOptions } = useCurrencyInput({
|
||||
currency: props.currency,
|
||||
|
|
@ -24,35 +24,51 @@ const { inputRef, numberValue, setValue, setOptions } = useCurrencyInput({
|
|||
precision: props.precision,
|
||||
useGrouping: props.useGrouping,
|
||||
valueRange: props.allowNegative ? {} : { min: 0 },
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
const numeric = typeof val === 'string' ? parseFloat(val) : val
|
||||
if (numeric !== numberValue.value) {
|
||||
setValue(isNaN(numeric) ? null : numeric)
|
||||
}
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
const numeric = typeof val === "string" ? parseFloat(val) : val;
|
||||
if (numeric !== numberValue.value) {
|
||||
setValue(isNaN(numeric) ? null : numeric);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(numberValue, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
watch(() => [props.currency, props.locale, props.precision, props.useGrouping, props.allowNegative], () => {
|
||||
setOptions({
|
||||
currency: props.currency,
|
||||
locale: props.locale,
|
||||
precision: props.precision,
|
||||
useGrouping: props.useGrouping,
|
||||
valueRange: props.allowNegative ? {} : { min: 0 },
|
||||
})
|
||||
})
|
||||
watch(
|
||||
() => [
|
||||
props.currency,
|
||||
props.locale,
|
||||
props.precision,
|
||||
props.useGrouping,
|
||||
props.allowNegative,
|
||||
],
|
||||
() => {
|
||||
setOptions({
|
||||
currency: props.currency,
|
||||
locale: props.locale,
|
||||
precision: props.precision,
|
||||
useGrouping: props.useGrouping,
|
||||
valueRange: props.allowNegative ? {} : { min: 0 },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue != null) {
|
||||
const numeric = typeof props.modelValue === 'string' ? parseFloat(props.modelValue) : props.modelValue
|
||||
setValue(isNaN(numeric) ? null : numeric)
|
||||
const numeric =
|
||||
typeof props.modelValue === "string"
|
||||
? parseFloat(props.modelValue)
|
||||
: props.modelValue;
|
||||
setValue(isNaN(numeric) ? null : numeric);
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -67,5 +83,6 @@ onMounted(() => {
|
|||
: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"
|
||||
autocomplete="off"
|
||||
@change="$emit('change', numberValue)"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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 <VDatePicker> 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";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<InputLabel v-if="label" :for="id" :value="label" />
|
||||
|
||||
<!-- VCalendar DatePicker with custom input to keep Tailwind styling -->
|
||||
<VDatePicker
|
||||
<VueDatePicker
|
||||
v-model="valueProxy"
|
||||
:mode="enableTimePicker ? 'dateTime' : 'date'"
|
||||
:masks="{ input: inputMask }"
|
||||
:popover="popoverCfg"
|
||||
:is24hr="true"
|
||||
>
|
||||
<template #default="{ inputValue, inputEvents }">
|
||||
<input
|
||||
: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"
|
||||
:value="inputValue"
|
||||
autocomplete="off"
|
||||
v-on="inputEvents"
|
||||
/>
|
||||
</template>
|
||||
</VDatePicker>
|
||||
:format="format"
|
||||
:enable-time-picker="enableTimePicker"
|
||||
:is-24="true"
|
||||
:inline="inline"
|
||||
:auto-apply="autoApply"
|
||||
:teleport="false"
|
||||
:close-on-auto-apply="closeOnAutoApply"
|
||||
:close-on-scroll="closeOnScroll"
|
||||
:input-class-name="inputClasses"
|
||||
:menu-class-name="'z-[1000]'"
|
||||
:locale="'sl'"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
/>
|
||||
|
||||
<template v-if="error">
|
||||
<InputError
|
||||
|
|
@ -102,5 +90,5 @@ const popoverCfg = computed(() => ({
|
|||
</template>
|
||||
|
||||
<style>
|
||||
/* Ensure the date picker menu overlays modals/dialogs */
|
||||
/* vue-datepicker provides its own menu layering; base CSS imported above */
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,13 @@ import { router, usePage } from "@inertiajs/vue3";
|
|||
|
||||
const props = defineProps({
|
||||
person: Object,
|
||||
personEdit: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
edit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
},
|
||||
tabColor: {
|
||||
type: String,
|
||||
|
|
@ -584,7 +588,7 @@ const submitSms = () => {
|
|||
<CusTab name="person" title="Oseba">
|
||||
<div class="flex justify-end mb-2">
|
||||
<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" />
|
||||
</button>
|
||||
</span>
|
||||
|
|
@ -636,7 +640,7 @@ const submitSms = () => {
|
|||
</CusTab>
|
||||
<CusTab name="addresses" title="Naslovi">
|
||||
<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>
|
||||
<PlusIcon
|
||||
@click="openDrawerAddAddress(false, 0)"
|
||||
|
|
@ -653,7 +657,7 @@ const submitSms = () => {
|
|||
<FwbBadge type="yellow">{{ address.country }}</FwbBadge>
|
||||
<FwbBadge>{{ address.type.name }}</FwbBadge>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2" v-if="edit">
|
||||
<button>
|
||||
<EditIcon
|
||||
@click="openDrawerAddAddress(true, address.id)"
|
||||
|
|
@ -678,7 +682,7 @@ const submitSms = () => {
|
|||
</CusTab>
|
||||
<CusTab name="phones" title="Telefonske">
|
||||
<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>
|
||||
<PlusIcon
|
||||
@click="operDrawerAddPhone(false, 0)"
|
||||
|
|
@ -707,14 +711,14 @@ const submitSms = () => {
|
|||
>
|
||||
SMS
|
||||
</button>
|
||||
<button>
|
||||
<button v-if="edit">
|
||||
<EditIcon
|
||||
@click="operDrawerAddPhone(true, phone.id)"
|
||||
size="md"
|
||||
css="text-gray-500 hover:text-gray-800"
|
||||
/>
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -725,7 +729,7 @@ const submitSms = () => {
|
|||
</CusTab>
|
||||
<CusTab name="emails" title="Email">
|
||||
<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>
|
||||
<PlusIcon
|
||||
@click="openDrawerAddEmail(false, 0)"
|
||||
|
|
@ -742,7 +746,10 @@ const submitSms = () => {
|
|||
v-for="(email, idx) in getEmails(person)"
|
||||
: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">
|
||||
<FwbBadge v-if="email?.label">{{ email.label }}</FwbBadge>
|
||||
<FwbBadge v-else type="indigo">Email</FwbBadge>
|
||||
|
|
@ -777,7 +784,7 @@ const submitSms = () => {
|
|||
</CusTab>
|
||||
<CusTab name="trr" title="TRR">
|
||||
<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>
|
||||
<PlusIcon
|
||||
@click="openDrawerAddTrr(false, 0)"
|
||||
|
|
@ -794,7 +801,10 @@ const submitSms = () => {
|
|||
v-for="(acc, idx) in getTRRs(person)"
|
||||
: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">
|
||||
<FwbBadge v-if="acc?.bank_name">{{ acc.bank_name }}</FwbBadge>
|
||||
<FwbBadge v-if="acc?.holder_name" type="indigo">{{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,12 @@ defineProps({
|
|||
type: String,
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ const rawMenuGroups = [
|
|||
},
|
||||
{
|
||||
label: "Uvoz",
|
||||
requires: { permission: "manage-imports" },
|
||||
items: [
|
||||
{
|
||||
key: "imports",
|
||||
|
|
@ -196,6 +197,9 @@ const rawMenuGroups = [
|
|||
},
|
||||
{
|
||||
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: [
|
||||
{
|
||||
key: "settings",
|
||||
|
|
@ -211,7 +215,7 @@ const rawMenuGroups = [
|
|||
title: "Administrator",
|
||||
routeName: "admin.index",
|
||||
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 permissions = user.permissions || [];
|
||||
|
||||
// Helper to determine inclusion based on optional requires meta
|
||||
function allowed(item) {
|
||||
if (!item.requires) return true;
|
||||
const needRole = item.requires.role;
|
||||
const needPerm = item.requires.permission;
|
||||
// Generic helper to determine inclusion based on optional `requires` meta
|
||||
function allowedMeta(entity) {
|
||||
const req = entity?.requires;
|
||||
if (!req) {
|
||||
return true;
|
||||
}
|
||||
const needRole = req.role ?? "admin";
|
||||
const needPerm = req.permission;
|
||||
return (
|
||||
(needRole && roles.includes(needRole)) ||
|
||||
(needPerm && permissions.includes(needPerm))
|
||||
);
|
||||
}
|
||||
|
||||
return rawMenuGroups.map((g) => {
|
||||
const items = g.items
|
||||
.filter(allowed)
|
||||
.sort((a, b) => a.title.localeCompare(b.title, "sl", { sensitivity: "base" }));
|
||||
return { label: g.label, items };
|
||||
});
|
||||
return (
|
||||
rawMenuGroups
|
||||
// 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" }));
|
||||
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
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ watch(
|
|||
:auto-position="true"
|
||||
:teleport-target="'body'"
|
||||
:inline="false"
|
||||
:auto-apply="false"
|
||||
:auto-apply="true"
|
||||
:fixed="false"
|
||||
:close-on-auto-apply="true"
|
||||
:close-on-scroll="true"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ library.add(faTrash, faEllipsisVertical, faCopy);
|
|||
const props = defineProps({
|
||||
client_case: Object,
|
||||
activities: Object,
|
||||
edit: Boolean,
|
||||
});
|
||||
|
||||
const fmtDate = (d) => {
|
||||
|
|
@ -91,11 +92,11 @@ const copyToClipboard = async (text) => {
|
|||
// You could add a toast notification here if available
|
||||
} catch (err) {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
};
|
||||
|
|
@ -115,7 +116,7 @@ const copyToClipboard = async (text) => {
|
|||
<th>Opomba</th>
|
||||
<th>Obljuba</th>
|
||||
<th>Dodal</th>
|
||||
<th class="w-8"></th>
|
||||
<th class="w-8" v-if="edit"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -174,7 +175,9 @@ const copyToClipboard = async (text) => {
|
|||
</template>
|
||||
<template #content>
|
||||
<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>
|
||||
<button
|
||||
@click="copyToClipboard(row.note)"
|
||||
|
|
@ -226,7 +229,7 @@ const copyToClipboard = async (text) => {
|
|||
>
|
||||
</div>
|
||||
</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">
|
||||
<template #trigger>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ const props = defineProps({
|
|||
segments: { type: Array, default: () => [] },
|
||||
all_segments: { type: Array, default: () => [] },
|
||||
templates: { type: Array, default: () => [] }, // active document templates (latest per slug)
|
||||
edit: { type: Boolean, default: () => false },
|
||||
});
|
||||
|
||||
// Debug: log incoming contract balances (remove after fix)
|
||||
|
|
@ -484,7 +485,7 @@ const closePaymentsDialog = () => {
|
|||
>
|
||||
Opis</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell class="w-px" />
|
||||
<FwbTableHeadCell class="w-px" v-if="edit" />
|
||||
</FwbTableHead>
|
||||
<FwbTableBody>
|
||||
<template v-for="(c, i) in contracts" :key="c.uuid || i">
|
||||
|
|
@ -497,7 +498,7 @@ const closePaymentsDialog = () => {
|
|||
<span class="text-gray-700">{{
|
||||
contractActiveSegment(c)?.name || "-"
|
||||
}}</span>
|
||||
<Dropdown align="left">
|
||||
<Dropdown align="left" v-if="edit">
|
||||
<template #trigger>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -701,7 +702,7 @@ const closePaymentsDialog = () => {
|
|||
</Dropdown>
|
||||
</div>
|
||||
</FwbTableCell>
|
||||
<FwbTableCell class="text-right whitespace-nowrap">
|
||||
<FwbTableCell class="text-right whitespace-nowrap" v-if="edit">
|
||||
<Dropdown align="right" width="56">
|
||||
<template #trigger>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -13,11 +13,12 @@ import DocumentEditDialog from "@/Components/DocumentEditDialog.vue";
|
|||
import DocumentUploadDialog from "@/Components/DocumentUploadDialog.vue";
|
||||
import DocumentViewerDialog from "@/Components/DocumentViewerDialog.vue";
|
||||
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 Pagination from "@/Components/Pagination.vue";
|
||||
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
|
||||
import DialogModal from "@/Components/DialogModal.vue";
|
||||
import { hasPermission } from "@/Services/permissions";
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
|
|
@ -35,6 +36,7 @@ const props = defineProps({
|
|||
contract_doc_templates: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const page = usePage();
|
||||
const showUpload = ref(false);
|
||||
const openUpload = () => {
|
||||
showUpload.value = true;
|
||||
|
|
@ -47,20 +49,25 @@ const onUploaded = () => {
|
|||
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
|
||||
const showDocEdit = ref(false)
|
||||
const editingDoc = ref(null)
|
||||
const showDocEdit = ref(false);
|
||||
const editingDoc = ref(null);
|
||||
const openDocEdit = (doc) => {
|
||||
editingDoc.value = doc
|
||||
showDocEdit.value = true
|
||||
}
|
||||
editingDoc.value = doc;
|
||||
showDocEdit.value = true;
|
||||
};
|
||||
const closeDocEdit = () => {
|
||||
showDocEdit.value = false
|
||||
editingDoc.value = null
|
||||
}
|
||||
showDocEdit.value = false;
|
||||
editingDoc.value = null;
|
||||
};
|
||||
const onDocSaved = () => {
|
||||
router.reload({ only: ['documents'] })
|
||||
}
|
||||
router.reload({ only: ["documents"] });
|
||||
};
|
||||
|
||||
const viewer = ref({ open: false, src: "", title: "" });
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -274,6 +285,7 @@ const submitAttachSegment = () => {
|
|||
:types="types"
|
||||
tab-color="red-600"
|
||||
:person="client_case.person"
|
||||
:person-edit="hasPerm('person-edit')"
|
||||
:enable-sms="true"
|
||||
:client-case-uuid="client_case.uuid"
|
||||
/>
|
||||
|
|
@ -289,7 +301,7 @@ const submitAttachSegment = () => {
|
|||
<SectionTitle>
|
||||
<template #title> Pogodbe </template>
|
||||
</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
|
||||
color="light"
|
||||
|
|
@ -311,6 +323,7 @@ const submitAttachSegment = () => {
|
|||
:contract_types="contract_types"
|
||||
:segments="segments"
|
||||
:templates="contract_doc_templates"
|
||||
:edit="hasPerm('contract-edit')"
|
||||
@edit="openDrawerEditContract"
|
||||
@delete="requestDeleteContract"
|
||||
@add-activity="openDrawerAddActivity"
|
||||
|
|
@ -330,7 +343,11 @@ const submitAttachSegment = () => {
|
|||
</SectionTitle>
|
||||
<FwbButton @click="openDrawerAddActivity">Nova</FwbButton>
|
||||
</div>
|
||||
<ActivityTable :client_case="client_case" :activities="activities" />
|
||||
<ActivityTable
|
||||
:client_case="client_case"
|
||||
:activities="activities"
|
||||
:edit="hasPerm('activity-edit')"
|
||||
/>
|
||||
<Pagination
|
||||
:links="activities.links"
|
||||
:from="activities.from"
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import PrimaryButton from "@/Components/PrimaryButton.vue";
|
||||
import InputLabel from "@/Components/InputLabel.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 DialogModal from "@/Components/DialogModal.vue";
|
||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
||||
import { hasPermission } from "@/Services/permissions";
|
||||
|
||||
const props = defineProps({
|
||||
clients: 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 = {
|
||||
address: "",
|
||||
country: "",
|
||||
|
|
@ -87,7 +94,10 @@ const fmtCurrency = (v) => {
|
|||
<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">
|
||||
<!-- 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
|
||||
@click="openDrawerCreateClient"
|
||||
class="bg-blue-600 hover:bg-blue-700"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
<script setup>
|
||||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||||
import PrimaryButton from "@/Components/PrimaryButton.vue";
|
||||
import { ref } from "vue";
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import { computed, ref } from "vue";
|
||||
import { Link, usePage } from "@inertiajs/vue3";
|
||||
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
|
||||
import FormCreateCase from "./Partials/FormCreateCase.vue";
|
||||
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
|
||||
import { hasPermission } from "@/Services/permissions";
|
||||
|
||||
const props = defineProps({
|
||||
client: Object,
|
||||
|
|
@ -19,6 +20,11 @@ const props = defineProps({
|
|||
// Removed page-level search; DataTable or server can handle filtering elsewhere if needed
|
||||
// DataTable search state
|
||||
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);
|
||||
|
||||
|
|
@ -52,7 +58,7 @@ const openDrawerCreateCase = () => {
|
|||
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
|
||||
route().current('client.show')
|
||||
? '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
|
||||
|
|
@ -65,7 +71,7 @@ const openDrawerCreateCase = () => {
|
|||
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
|
||||
route().current('client.contracts')
|
||||
? '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
|
||||
|
|
@ -93,7 +99,10 @@ const openDrawerCreateCase = () => {
|
|||
<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="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"
|
||||
>Dodaj</PrimaryButton
|
||||
>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ const toDelete = ref(null);
|
|||
const search = ref("");
|
||||
const selectedTemplateId = ref(null);
|
||||
const onlyAutoMail = ref(false);
|
||||
// Filter: selected events (multi-select)
|
||||
const selectedEvents = ref([]);
|
||||
|
||||
const actionOptions = ref([]);
|
||||
|
||||
|
|
@ -214,6 +216,7 @@ function tryAdoptRaw(ev) {
|
|||
const filtered = computed(() => {
|
||||
const term = search.value?.toLowerCase() ?? "";
|
||||
const tplId = selectedTemplateId.value ? Number(selectedTemplateId.value) : null;
|
||||
const evIdSet = new Set((selectedEvents.value || []).map((e) => Number(e.id)));
|
||||
return (props.decisions || []).filter((d) => {
|
||||
const matchesSearch =
|
||||
!term ||
|
||||
|
|
@ -221,7 +224,10 @@ const filtered = computed(() => {
|
|||
d.color_tag?.toLowerCase().includes(term);
|
||||
const matchesAuto = !onlyAutoMail.value || !!d.auto_mail;
|
||||
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,28 +349,67 @@ const destroyDecision = () => {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div class="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
|
||||
<TextInput v-model="search" placeholder="Iskanje..." class="w-full sm:w-72" />
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option :value="null">Vse predloge</option>
|
||||
<option v-for="t in emailTemplates" :key="t.id" :value="t.id">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</select>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="onlyAutoMail"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
|
||||
/>
|
||||
Samo auto mail
|
||||
</label>
|
||||
<div class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="w-full bg-gray-50 border rounded-md p-3">
|
||||
<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
|
||||
v-model="selectedTemplateId"
|
||||
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 v-for="t in emailTemplates" :key="t.id" :value="t.id">
|
||||
{{ t.name }}
|
||||
</option>
|
||||
</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">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="onlyAutoMail"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 h-4 w-4"
|
||||
/>
|
||||
Samo auto mail
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton>
|
||||
</div>
|
||||
<PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton>
|
||||
</div>
|
||||
<div class="px-4 pb-4">
|
||||
<DataTableClient
|
||||
|
|
|
|||
3
resources/js/Services/permissions.ts
Normal file
3
resources/js/Services/permissions.ts
Normal 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 );
|
||||
}
|
||||
101
routes/web.php
101
routes/web.php
|
|
@ -284,7 +284,7 @@
|
|||
})->name('search');
|
||||
|
||||
// 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::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');
|
||||
|
|
@ -302,10 +302,14 @@
|
|||
Route::get('clients', [ClientController::class, 'index'])->name('client');
|
||||
Route::get('clients/{client:uuid}', [ClientController::class, 'show'])->name('client.show');
|
||||
Route::get('clients/{client:uuid}/contracts', [ClientController::class, 'contracts'])->name('client.contracts');
|
||||
Route::post('clients', [ClientController::class, 'store'])->name('client.store');
|
||||
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::middleware('permission:client-edit')->group( function() {
|
||||
Route::post('clients', [ClientController::class, 'store'])->name('client.store');
|
||||
Route::put('clients/{client:uuid}', [ClientController::class, 'update'])->name('client.update');
|
||||
Route::post('clients/{client:uuid}/emergency-person', [ClientController::class, 'emergencyCreatePerson'])->name('client.emergencyPerson');
|
||||
|
||||
});
|
||||
|
||||
// client-case
|
||||
Route::get('client-cases', [ClientCaseContoller::class, 'index'])->name('clientCase');
|
||||
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/{client_case:uuid}/emergency-person', [ClientCaseContoller::class, 'emergencyCreatePerson'])->name('clientCase.emergencyPerson');
|
||||
// client-case / contract
|
||||
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::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::middleware('permission:contract-edit')->group( function () {
|
||||
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::delete('client-cases/{client_case:uuid}/contract/{uuid}', [ClientCaseContoller::class, 'deleteContract'])->name('clientCase.contract.delete');
|
||||
});
|
||||
|
||||
// client-case / contract / objects
|
||||
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::delete('client-cases/{client_case:uuid}/objects/{id}', [CaseObjectController::class, 'destroy'])->name('clientCase.object.delete');
|
||||
// client-case / activity
|
||||
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
|
||||
Route::post('client-cases/{client_case:uuid}/segments', [ClientCaseContoller::class, 'attachSegment'])->name('clientCase.segments.attach');
|
||||
// client-case / documents
|
||||
|
|
@ -385,45 +393,46 @@
|
|||
// segments index overview
|
||||
Route::get('segments', [SegmentController::class, 'index'])->name('segments.index');
|
||||
Route::get('segments/{segment}', [SegmentController::class, 'show'])->name('segments.show');
|
||||
|
||||
Route::middleware("permission:manage-imports")->group( function () {
|
||||
// imports
|
||||
Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create');
|
||||
Route::get('imports', [ImportController::class, 'index'])->name('imports.index');
|
||||
Route::get('imports/import/{import:uuid}', [ImportController::class, 'show'])->name('imports.continue');
|
||||
Route::post('imports', [ImportController::class, 'store'])->name('imports.store');
|
||||
Route::get('imports/{import}/columns', [ImportController::class, 'columns'])->name('imports.columns');
|
||||
Route::post('imports/{import}/process', [ImportController::class, 'process'])->name('imports.process');
|
||||
Route::post('imports/{import}/mappings', [ImportController::class, 'saveMappings'])->name('imports.mappings.save');
|
||||
Route::get('imports/{import}/mappings', [ImportController::class, 'getMappings'])->name('imports.mappings.get');
|
||||
Route::get('imports/{import}/events', [ImportController::class, 'getEvents'])->name('imports.events');
|
||||
Route::get('imports/{import}/missing-keyref-rows', [ImportController::class, 'missingKeyrefRows'])->name('imports.missing-keyref-rows');
|
||||
Route::get('imports/{import}/missing-keyref-csv', [ImportController::class, 'exportMissingKeyrefCsv'])->name('imports.missing-keyref-csv');
|
||||
Route::get('imports/{import}/preview', [ImportController::class, 'preview'])->name('imports.preview');
|
||||
Route::get('imports/{import}/missing-contracts', [ImportController::class, 'missingContracts'])->name('imports.missing-contracts');
|
||||
Route::post('imports/{import}/options', [ImportController::class, 'updateOptions'])->name('imports.options');
|
||||
// Generic simulation endpoint (new) – provides projected effects for first N rows regardless of payments template
|
||||
Route::get('imports/{import}/simulate', [ImportController::class, 'simulate'])->name('imports.simulate');
|
||||
// Backwards compatible payments simulation route (legacy name) – now proxies to generic simulate method
|
||||
Route::get('imports/{import}/simulate-payments', [ImportController::class, 'simulatePayments'])->name('imports.simulatePayments');
|
||||
|
||||
// imports
|
||||
Route::get('imports/create', [ImportController::class, 'create'])->name('imports.create');
|
||||
Route::get('imports', [ImportController::class, 'index'])->name('imports.index');
|
||||
Route::get('imports/import/{import:uuid}', [ImportController::class, 'show'])->name('imports.continue');
|
||||
Route::post('imports', [ImportController::class, 'store'])->name('imports.store');
|
||||
Route::get('imports/{import}/columns', [ImportController::class, 'columns'])->name('imports.columns');
|
||||
Route::post('imports/{import}/process', [ImportController::class, 'process'])->name('imports.process');
|
||||
Route::post('imports/{import}/mappings', [ImportController::class, 'saveMappings'])->name('imports.mappings.save');
|
||||
Route::get('imports/{import}/mappings', [ImportController::class, 'getMappings'])->name('imports.mappings.get');
|
||||
Route::get('imports/{import}/events', [ImportController::class, 'getEvents'])->name('imports.events');
|
||||
Route::get('imports/{import}/missing-keyref-rows', [ImportController::class, 'missingKeyrefRows'])->name('imports.missing-keyref-rows');
|
||||
Route::get('imports/{import}/missing-keyref-csv', [ImportController::class, 'exportMissingKeyrefCsv'])->name('imports.missing-keyref-csv');
|
||||
Route::get('imports/{import}/preview', [ImportController::class, 'preview'])->name('imports.preview');
|
||||
Route::get('imports/{import}/missing-contracts', [ImportController::class, 'missingContracts'])->name('imports.missing-contracts');
|
||||
Route::post('imports/{import}/options', [ImportController::class, 'updateOptions'])->name('imports.options');
|
||||
// Generic simulation endpoint (new) – provides projected effects for first N rows regardless of payments template
|
||||
Route::get('imports/{import}/simulate', [ImportController::class, 'simulate'])->name('imports.simulate');
|
||||
// Backwards compatible payments simulation route (legacy name) – now proxies to generic simulate method
|
||||
Route::get('imports/{import}/simulate-payments', [ImportController::class, 'simulatePayments'])->name('imports.simulatePayments');
|
||||
|
||||
// import templates
|
||||
Route::get('imports/templates', [ImportTemplateController::class, 'index'])->name('importTemplates.index');
|
||||
Route::get('imports/templates/create', [ImportTemplateController::class, 'create'])->name('importTemplates.create');
|
||||
Route::post('imports/templates', [ImportTemplateController::class, 'store'])->name('importTemplates.store');
|
||||
Route::get('imports/templates/edit/{template:uuid}', [ImportTemplateController::class, 'edit'])->name('importTemplates.edit');
|
||||
Route::put('imports/templates/{template:uuid}', [ImportTemplateController::class, 'update'])->name('importTemplates.update');
|
||||
Route::delete('imports/templates/{template:uuid}', [ImportTemplateController::class, 'destroy'])->name('importTemplates.destroy');
|
||||
Route::post('imports/templates/{template:uuid}/mappings', [ImportTemplateController::class, 'addMapping'])->name('importTemplates.mappings.add');
|
||||
Route::post('imports/templates/{template:uuid}/mappings/bulk', [ImportTemplateController::class, 'bulkAddMappings'])->name('importTemplates.mappings.bulk');
|
||||
Route::put('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'updateMapping'])->name('importTemplates.mappings.update');
|
||||
Route::delete('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'deleteMapping'])->name('importTemplates.mappings.delete');
|
||||
Route::post('imports/templates{template:uuid}/mappings/reorder', [ImportTemplateController::class, 'reorderMappings'])->name('importTemplates.mappings.reorder');
|
||||
Route::post('imports/templates/{template}/apply/{import}', [ImportTemplateController::class, 'applyToImport'])->name('importTemplates.apply');
|
||||
// Delete an unfinished import
|
||||
Route::delete('imports/{import}', [ImportController::class, 'destroy'])->name('imports.destroy');
|
||||
// Route::put()
|
||||
// types
|
||||
|
||||
// import templates
|
||||
Route::get('imports/templates', [ImportTemplateController::class, 'index'])->name('importTemplates.index');
|
||||
Route::get('imports/templates/create', [ImportTemplateController::class, 'create'])->name('importTemplates.create');
|
||||
Route::post('imports/templates', [ImportTemplateController::class, 'store'])->name('importTemplates.store');
|
||||
Route::get('imports/templates/edit/{template:uuid}', [ImportTemplateController::class, 'edit'])->name('importTemplates.edit');
|
||||
Route::put('imports/templates/{template:uuid}', [ImportTemplateController::class, 'update'])->name('importTemplates.update');
|
||||
Route::delete('imports/templates/{template:uuid}', [ImportTemplateController::class, 'destroy'])->name('importTemplates.destroy');
|
||||
Route::post('imports/templates/{template:uuid}/mappings', [ImportTemplateController::class, 'addMapping'])->name('importTemplates.mappings.add');
|
||||
Route::post('imports/templates/{template:uuid}/mappings/bulk', [ImportTemplateController::class, 'bulkAddMappings'])->name('importTemplates.mappings.bulk');
|
||||
Route::put('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'updateMapping'])->name('importTemplates.mappings.update');
|
||||
Route::delete('imports/templates/{template:uuid}/mappings/{mapping}', [ImportTemplateController::class, 'deleteMapping'])->name('importTemplates.mappings.delete');
|
||||
Route::post('imports/templates{template:uuid}/mappings/reorder', [ImportTemplateController::class, 'reorderMappings'])->name('importTemplates.mappings.reorder');
|
||||
Route::post('imports/templates/{template}/apply/{import}', [ImportTemplateController::class, 'applyToImport'])->name('importTemplates.apply');
|
||||
// Delete an unfinished import
|
||||
Route::delete('imports/{import}', [ImportController::class, 'destroy'])->name('imports.destroy');
|
||||
// Route::put()
|
||||
// types
|
||||
});
|
||||
// accounts / payments & bookings
|
||||
Route::prefix('accounts/{account}')->name('accounts.')->group(function (): void {
|
||||
Route::get('payments', [AccountPaymentController::class, 'index'])->name('payments.index');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user