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.');
|
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.');
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
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,
|
TestUserSeeder::class,
|
||||||
ProductionUserSeeder::class,
|
ProductionUserSeeder::class,
|
||||||
AdditionalProductionUsersSeeder::class,
|
AdditionalProductionUsersSeeder::class,
|
||||||
|
// Roles/permissions: ensure Manager role exists (admin, manager, staff, viewer)
|
||||||
|
AddManagerRoleSeeder::class,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">{{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
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 );
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user