New report system and views

This commit is contained in:
Simon Pocrnjič
2026-01-02 12:32:20 +01:00
parent 9fc5b54b8a
commit 703b52ff59
67 changed files with 8255 additions and 2794 deletions
+98 -433
View File
@@ -1,7 +1,14 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Alert, AlertDescription, AlertTitle } from "@/Components/ui/alert";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Archive } from "lucide-vue-next";
import ArchiveRuleCard from "./Partials/ArchiveRuleCard.vue";
import CreateRuleForm from "./Partials/CreateRuleForm.vue";
import EditRuleForm from "./Partials/EditRuleForm.vue";
const props = defineProps({
settings: Object,
@@ -29,7 +36,6 @@ const newForm = useForm({
// Editing state & form
const editingSetting = ref(null);
// Conditions temporarily inactive in backend; keep placeholder for future restore
const originalEntityMeta = ref({ columns: ["id"] });
const editForm = useForm({
name: "",
@@ -47,14 +53,6 @@ const editForm = useForm({
options: { batch_size: 200 },
});
const selectedEntity = ref(null);
function onFocusChange() {
const found = props.archiveEntities.find((e) => e.focus === newForm.focus);
selectedEntity.value = found || null;
newForm.related = [];
}
function submitCreate() {
if (!newForm.focus) {
alert("Select a focus entity.");
@@ -68,11 +66,11 @@ function submitCreate() {
{
table: newForm.focus,
related: newForm.related,
// conditions omitted while inactive
columns: ["id"],
},
];
newForm.post(route("settings.archive.store"), {
preserveScroll: true,
onSuccess: () => {
newForm.focus = "";
newForm.related = [];
@@ -80,21 +78,26 @@ function submitCreate() {
newForm.action_id = null;
newForm.decision_id = null;
newForm.segment_id = null;
selectedEntity.value = null;
newForm.reset();
},
});
}
function toggleEnabled(setting) {
router.put(route("settings.archive.update", setting.id), {
...setting,
enabled: !setting.enabled,
});
router.put(
route("settings.archive.update", setting.id),
{
...setting,
enabled: !setting.enabled,
},
{
preserveScroll: true,
}
);
}
function startEdit(setting) {
editingSetting.value = setting;
// Populate editForm
editForm.name = setting.name || "";
editForm.description = setting.description || "";
editForm.enabled = setting.enabled;
@@ -104,7 +107,7 @@ function startEdit(setting) {
editForm.action_id = setting.action_id ?? null;
editForm.decision_id = setting.decision_id ?? null;
editForm.segment_id = setting.segment_id ?? null;
// Entities (first only)
const first = Array.isArray(setting.entities) ? setting.entities[0] : null;
if (first) {
editForm.focus = first.table || "";
@@ -112,20 +115,16 @@ function startEdit(setting) {
originalEntityMeta.value = {
columns: first.columns || ["id"],
};
const found = props.archiveEntities.find((e) => e.focus === editForm.focus);
selectedEntity.value = found || null;
} else {
editForm.focus = "";
editForm.related = [];
originalEntityMeta.value = { columns: ["id"] };
// If reactivate is checked it implies soft semantics; keep soft true (UI might show both)
}
}
function cancelEdit() {
editingSetting.value = null;
editForm.reset();
selectedEntity.value = null;
}
function submitUpdate() {
@@ -142,11 +141,11 @@ function submitUpdate() {
{
table: editForm.focus,
related: editForm.related,
// conditions omitted while inactive
columns: originalEntityMeta.value.columns || ["id"],
},
];
editForm.put(route("settings.archive.update", editingSetting.value.id), {
preserveScroll: true,
onSuccess: () => {
cancelEdit();
},
@@ -155,427 +154,93 @@ function submitUpdate() {
function remove(setting) {
if (!confirm("Delete archive rule?")) return;
router.delete(route("settings.archive.destroy", setting.id));
router.delete(route("settings.archive.destroy", setting.id), {
preserveScroll: true,
});
}
// Run Now removed (feature temporarily disabled)
</script>
<template>
<AppLayout title="Archive Settings">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Archive Settings</h2>
</template>
<template #header />
<div class="pt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Alert variant="default" class="mb-6 border-l-4 border-amber-500">
<AlertTitle class="text-sm font-medium text-amber-800">
Archive rule conditions are temporarily inactive
</AlertTitle>
<AlertDescription class="text-xs text-amber-800 space-y-2 mt-2">
<p>
All enabled rules apply to the focus entity and its selected related tables
without date/other filters. Stored condition JSON is preserved for future
reactivation.
</p>
<p class="font-medium">The "Run Now" action is currently disabled.</p>
<div class="mt-3 bg-white/60 rounded p-3 border border-amber-200">
<p class="font-semibold mb-1 text-amber-900">Chain Path Help</p>
<p class="mb-1">Supported chained related tables (dot notation):</p>
<ul class="list-disc ml-4 space-y-0.5">
<li v-for="cp in chainPatterns" :key="cp">
<code class="px-1 bg-amber-100 rounded text-xs">{{ cp }}</code>
</li>
</ul>
<p class="mt-1 italic">Only these chains are processed; others are ignored.</p>
</div>
</AlertDescription>
</Alert>
<div class="py-6 max-w-6xl mx-auto px-4">
<div class="mb-6 border-l-4 border-amber-500 bg-amber-50 text-amber-800 px-4 py-3 rounded">
<p class="text-sm font-medium">Archive rule conditions are temporarily inactive.</p>
<p class="text-xs mt-1">All enabled rules apply to the focus entity and its selected related tables without date/other filters. Stored condition JSON is preserved for future reactivation.</p>
<p class="text-xs mt-1 font-medium">The "Run Now" action is currently disabled.</p>
<div class="mt-3 text-xs bg-white/60 rounded p-3 border border-amber-200">
<p class="font-semibold mb-1 text-amber-900">Chain Path Help</p>
<p class="mb-1">Supported chained related tables (dot notation):</p>
<ul class="list-disc ml-4 space-y-0.5">
<li v-for="cp in chainPatterns" :key="cp">
<code class="px-1 bg-amber-100 rounded">{{ cp }}</code>
</li>
</ul>
<p class="mt-1 italic">Only these chains are processed; others are ignored.</p>
</div>
</div>
<div class="grid gap-6 md:grid-cols-3">
<div class="md:col-span-2 space-y-4">
<div
v-for="s in settings.data"
:key="s.id"
class="border rounded-lg p-4 bg-white shadow-sm"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h3 class="font-medium text-gray-900 flex items-center gap-2">
<span class="truncate">{{ s.name || "Untitled Rule #" + s.id }}</span>
<span
v-if="!s.enabled"
class="inline-flex text-xs px-2 py-0.5 rounded-full bg-amber-100 text-amber-800"
>Disabled</span
>
</h3>
<p v-if="s.description" class="text-sm text-gray-600 mt-1">
{{ s.description }}
</p>
<p class="mt-2 text-xs text-gray-500">
Strategy: {{ s.strategy }} Soft: {{ s.soft ? "Yes" : "No" }}
</p>
</div>
<div class="flex flex-col items-end gap-2 shrink-0">
<button
@click="startEdit(s)"
class="text-xs px-3 py-1.5 rounded bg-gray-200 text-gray-800 hover:bg-gray-300"
>
Edit
</button>
<!-- Run Now removed -->
<button
@click="toggleEnabled(s)"
class="text-xs px-3 py-1.5 rounded bg-indigo-600 text-white hover:bg-indigo-700"
>
{{ s.enabled ? "Disable" : "Enable" }}
</button>
<button
@click="remove(s)"
class="text-xs px-3 py-1.5 rounded bg-red-600 text-white hover:bg-red-700"
>
Delete
</button>
</div>
</div>
<div class="mt-3 text-xs bg-gray-50 border rounded p-2 overflow-x-auto">
<pre class="whitespace-pre-wrap">{{
JSON.stringify(s.entities, null, 2)
}}</pre>
</div>
</div>
<div v-if="!settings.data.length" class="text-sm text-gray-600">
No archive rules.
</div>
</div>
<div class="space-y-4">
<div v-if="!editingSetting" class="border rounded-lg p-4 bg-white shadow-sm">
<h3 class="font-semibold text-gray-900 mb-2 text-sm">New Rule</h3>
<div class="space-y-3 text-sm">
<div>
<label class="block text-xs font-medium text-gray-600"
>Segment (optional)</label
>
<select
v-model="newForm.segment_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Action (optional)</label
>
<select
v-model="newForm.action_id"
@change="
() => {
newForm.decision_id = null;
}
"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Decision (optional)</label
>
<select
v-model="newForm.decision_id"
:disabled="!newForm.action_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option
v-for="d in actions.find((a) => a.id === newForm.action_id)
?.decisions || []"
:key="d.id"
:value="d.id"
>
{{ d.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Name</label>
<input
v-model="newForm.name"
type="text"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
<div class="grid gap-6 md:grid-cols-3">
<div class="md:col-span-2 space-y-4">
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
>
<template #header>
<div class="flex items-center gap-2">
<Archive :size="18" />
<CardTitle class="uppercase">Archive Rules</CardTitle>
</div>
</template>
<div class="p-4 space-y-4">
<ArchiveRuleCard
v-for="rule in settings.data"
:key="rule.id"
:rule="rule"
@edit="startEdit"
@toggle-enabled="toggleEnabled"
@delete="remove"
/>
<div v-if="newForm.errors.name" class="text-red-600 text-xs mt-1">
{{ newForm.errors.name }}
<div
v-if="!settings.data.length"
class="text-sm text-muted-foreground text-center py-8"
>
No archive rules defined yet.
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Focus Entity</label
>
<select
v-model="newForm.focus"
@change="onFocusChange"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="" disabled>-- choose --</option>
<option v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</option>
</select>
</div>
<div v-if="selectedEntity" class="space-y-1">
<div class="text-xs font-medium text-gray-600">Related Tables</div>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-1 text-xs bg-gray-100 px-2 py-1 rounded border"
>
<input
type="checkbox"
:value="r"
v-model="newForm.related"
class="rounded"
/>
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Description</label>
<textarea
v-model="newForm.description"
rows="2"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
></textarea>
<div v-if="newForm.errors.description" class="text-red-600 text-xs mt-1">
{{ newForm.errors.description }}
</div>
</div>
<div class="flex items-center gap-2">
<input id="enabled" type="checkbox" v-model="newForm.enabled" />
<label for="enabled" class="text-xs font-medium text-gray-700"
>Enabled</label
>
</div>
<div class="flex items-center gap-2">
<input id="soft" type="checkbox" v-model="newForm.soft" />
<label for="soft" class="text-xs font-medium text-gray-700"
>Soft Archive</label
>
</div>
<div class="flex items-center gap-2">
<input id="reactivate" type="checkbox" v-model="newForm.reactivate" />
<label for="reactivate" class="text-xs font-medium text-gray-700"
>Reactivate (undo archive)</label
>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Strategy</label>
<select
v-model="newForm.strategy"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="immediate">Immediate</option>
<option value="scheduled">Scheduled</option>
<option value="queued">Queued</option>
<option value="manual">Manual (never auto-run)</option>
</select>
<div v-if="newForm.errors.strategy" class="text-red-600 text-xs mt-1">
{{ newForm.errors.strategy }}
</div>
</div>
<button
@click="submitCreate"
type="button"
:disabled="newForm.processing"
class="w-full text-sm px-3 py-2 rounded bg-green-600 text-white hover:bg-green-700 disabled:opacity-50"
>
Create
</button>
<div v-if="Object.keys(newForm.errors).length" class="text-xs text-red-600">
Please fix validation errors.
</div>
</div>
</AppCard>
</div>
<div v-else class="border rounded-lg p-4 bg-white shadow-sm">
<h3 class="font-semibold text-gray-900 mb-2 text-sm">
Edit Rule #{{ editingSetting.id }}
</h3>
<div class="space-y-3 text-sm">
<div
class="text-xs text-gray-500"
v-if="editingSetting.strategy === 'manual'"
>
Manual strategy: this rule will only run when triggered manually.
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Segment (optional)</label
>
<select
v-model="editForm.segment_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Action (optional)</label
>
<select
v-model="editForm.action_id"
@change="
() => {
editForm.decision_id = null;
}
"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Decision (optional)</label
>
<select
v-model="editForm.decision_id"
:disabled="!editForm.action_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option
v-for="d in actions.find((a) => a.id === editForm.action_id)
?.decisions || []"
:key="d.id"
:value="d.id"
>
{{ d.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Name</label>
<input
v-model="editForm.name"
type="text"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
/>
<div v-if="editForm.errors.name" class="text-red-600 text-xs mt-1">
{{ editForm.errors.name }}
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Focus Entity</label
>
<select
v-model="editForm.focus"
@change="onFocusChange() /* reuse selectedEntity for preview */"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="" disabled>-- choose --</option>
<option v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</option>
</select>
</div>
<div
v-if="selectedEntity && editForm.focus === selectedEntity.focus"
class="space-y-1"
>
<div class="text-xs font-medium text-gray-600">Related Tables</div>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-1 text-xs bg-gray-100 px-2 py-1 rounded border"
>
<input
type="checkbox"
:value="r"
v-model="editForm.related"
class="rounded"
/>
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Description</label>
<textarea
v-model="editForm.description"
rows="2"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
></textarea>
<div v-if="editForm.errors.description" class="text-red-600 text-xs mt-1">
{{ editForm.errors.description }}
</div>
</div>
<div class="flex items-center gap-2">
<input id="edit_enabled" type="checkbox" v-model="editForm.enabled" />
<label for="edit_enabled" class="text-xs font-medium text-gray-700"
>Enabled</label
>
</div>
<div class="flex items-center gap-2">
<input id="edit_soft" type="checkbox" v-model="editForm.soft" />
<label for="edit_soft" class="text-xs font-medium text-gray-700"
>Soft Archive</label
>
</div>
<div class="flex items-center gap-2">
<input id="edit_reactivate" type="checkbox" v-model="editForm.reactivate" />
<label for="edit_reactivate" class="text-xs font-medium text-gray-700"
>Reactivate (undo archive)</label
>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Strategy</label>
<select
v-model="editForm.strategy"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="immediate">Immediate</option>
<option value="scheduled">Scheduled</option>
<option value="queued">Queued</option>
<option value="manual">Manual (never auto-run)</option>
</select>
<div v-if="editForm.errors.strategy" class="text-red-600 text-xs mt-1">
{{ editForm.errors.strategy }}
</div>
</div>
<div class="flex gap-2">
<button
@click="submitUpdate"
type="button"
:disabled="editForm.processing"
class="flex-1 text-sm px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
>
Update
</button>
<button
@click="cancelEdit"
type="button"
class="px-3 py-2 rounded text-sm bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
</div>
<div
v-if="Object.keys(editForm.errors).length"
class="text-xs text-red-600"
>
Please fix validation errors.
</div>
</div>
<div>
<CreateRuleForm
v-if="!editingSetting"
:form="newForm"
:archive-entities="archiveEntities"
:actions="actions"
:segments="segments"
@submit="submitCreate"
/>
<EditRuleForm
v-else
:form="editForm"
:setting="editingSetting"
:archive-entities="archiveEntities"
:actions="actions"
:segments="segments"
@submit="submitUpdate"
@cancel="cancelEdit"
/>
</div>
</div>
</div>
@@ -0,0 +1,72 @@
<script setup>
import { Button } from "@/Components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import { MoreHorizontal, Pencil, Trash, Power, PowerOff } from "lucide-vue-next";
import { Badge } from "@/Components/ui/badge";
defineProps({
rule: Object,
});
const emit = defineEmits(["edit", "toggle-enabled", "delete"]);
</script>
<template>
<div class="border rounded-lg p-4 bg-white shadow-sm">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<h3 class="font-medium text-gray-900 truncate">
{{ rule.name || "Untitled Rule #" + rule.id }}
</h3>
<Badge v-if="!rule.enabled" variant="secondary" class="text-xs">
Disabled
</Badge>
</div>
<p v-if="rule.description" class="text-sm text-muted-foreground mt-1">
{{ rule.description }}
</p>
<div class="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
<span>Strategy: <span class="font-medium">{{ rule.strategy }}</span></span>
<span></span>
<span>Soft: <span class="font-medium">{{ rule.soft ? "Yes" : "No" }}</span></span>
<span v-if="rule.reactivate" class="text-amber-600 font-medium">
Reactivate Mode
</span>
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="emit('edit', rule)">
<Pencil class="w-4 h-4 mr-2" />
Uredi
</DropdownMenuItem>
<DropdownMenuItem @click="emit('toggle-enabled', rule)">
<component :is="rule.enabled ? PowerOff : Power" class="w-4 h-4 mr-2" />
{{ rule.enabled ? "Disable" : "Enable" }}
</DropdownMenuItem>
<DropdownMenuItem
@click="emit('delete', rule)"
class="text-red-600 focus:text-red-600"
>
<Trash class="w-4 h-4 mr-2" />
Izbriši
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div class="mt-3 text-xs bg-muted rounded p-3 overflow-x-auto">
<pre class="whitespace-pre-wrap">{{ JSON.stringify(rule.entities, null, 2) }}</pre>
</div>
</div>
</template>
@@ -0,0 +1,184 @@
<script setup>
import { Button } from "@/Components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import AppCheckboxArray from "@/Components/app/ui/AppCheckboxArray.vue";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import { ref, computed } from "vue";
const props = defineProps({
form: Object,
archiveEntities: Array,
actions: Array,
segments: Array,
});
const emit = defineEmits(["submit"]);
const selectedEntity = ref(null);
function onFocusChange() {
const found = props.archiveEntities.find((e) => e.focus === props.form.focus);
selectedEntity.value = found || null;
props.form.related = [];
}
const availableDecisions = computed(() => {
if (!props.form.action_id) return [];
const action = props.actions.find((a) => a.id === props.form.action_id);
return action?.decisions || [];
});
function handleActionChange() {
props.form.decision_id = null;
}
</script>
<template>
<Card>
<CardHeader>
<CardTitle class="text-base">New Archive Rule</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<div>
<InputLabel for="new_segment">Segment (optional)</InputLabel>
<Select v-model="form.segment_id">
<SelectTrigger id="new_segment" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="new_action">Action (optional)</InputLabel>
<Select v-model="form.action_id" @update:model-value="handleActionChange">
<SelectTrigger id="new_action" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="new_decision">Decision (optional)</InputLabel>
<Select v-model="form.decision_id" :disabled="!form.action_id">
<SelectTrigger id="new_decision" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="d in availableDecisions" :key="d.id" :value="d.id">
{{ d.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="new_name">Name</InputLabel>
<Input id="new_name" v-model="form.name" type="text" />
<InputError :message="form.errors.name" class="mt-1" />
</div>
<div>
<InputLabel for="new_focus">Focus Entity</InputLabel>
<Select v-model="form.focus" @update:model-value="onFocusChange">
<SelectTrigger id="new_focus" class="w-full">
<SelectValue placeholder="-- choose --" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="selectedEntity" class="space-y-2">
<InputLabel>Related Tables</InputLabel>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-2 text-sm bg-muted px-3 py-1.5 rounded-md border cursor-pointer hover:bg-muted/80"
>
<AppCheckboxArray :value="r" v-model="form.related" />
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<InputLabel for="new_description">Description</InputLabel>
<Textarea id="new_description" v-model="form.description" rows="2" />
<InputError :message="form.errors.description" class="mt-1" />
</div>
<div class="flex items-center gap-2">
<Checkbox id="new_enabled" v-model="form.enabled" />
<InputLabel for="new_enabled" class="text-sm font-normal cursor-pointer">
Enabled
</InputLabel>
</div>
<div class="flex items-center gap-2">
<Checkbox id="new_soft" v-model="form.soft" />
<InputLabel for="new_soft" class="text-sm font-normal cursor-pointer">
Soft Archive
</InputLabel>
</div>
<div class="flex items-center gap-2">
<Checkbox id="new_reactivate" v-model="form.reactivate" />
<InputLabel for="new_reactivate" class="text-sm font-normal cursor-pointer">
Reactivate (undo archive)
</InputLabel>
</div>
<div>
<InputLabel for="new_strategy">Strategy</InputLabel>
<Select v-model="form.strategy">
<SelectTrigger id="new_strategy" class="w-full">
<SelectValue placeholder="Select strategy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate">Immediate</SelectItem>
<SelectItem value="scheduled">Scheduled</SelectItem>
<SelectItem value="queued">Queued</SelectItem>
<SelectItem value="manual">Manual (never auto-run)</SelectItem>
</SelectContent>
</Select>
<InputError :message="form.errors.strategy" class="mt-1" />
</div>
<Button @click="emit('submit')" :disabled="form.processing" class="w-full">
Create Rule
</Button>
<div v-if="Object.keys(form.errors).length" class="text-xs text-red-600">
Please fix validation errors.
</div>
</CardContent>
</Card>
</template>
@@ -0,0 +1,199 @@
<script setup>
import { Button } from "@/Components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/Components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import AppCheckboxArray from "@/Components/app/ui/AppCheckboxArray.vue";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
import { Alert, AlertDescription } from "@/Components/ui/alert";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import { ref, computed, watch } from "vue";
const props = defineProps({
form: Object,
setting: Object,
archiveEntities: Array,
actions: Array,
segments: Array,
});
const emit = defineEmits(["submit", "cancel"]);
const selectedEntity = ref(null);
// Initialize selectedEntity based on form.focus
watch(
() => props.form.focus,
(newFocus) => {
const found = props.archiveEntities.find((e) => e.focus === newFocus);
selectedEntity.value = found || null;
},
{ immediate: true }
);
const availableDecisions = computed(() => {
if (!props.form.action_id) return [];
const action = props.actions.find((a) => a.id === props.form.action_id);
return action?.decisions || [];
});
function handleActionChange() {
props.form.decision_id = null;
}
</script>
<template>
<Card>
<CardHeader>
<CardTitle class="text-base">Edit Rule #{{ setting.id }}</CardTitle>
</CardHeader>
<CardContent class="space-y-4">
<Alert v-if="setting.strategy === 'manual'" variant="default">
<AlertDescription class="text-xs">
Manual strategy: this rule will only run when triggered manually.
</AlertDescription>
</Alert>
<div>
<InputLabel for="edit_segment">Segment (optional)</InputLabel>
<Select v-model="form.segment_id">
<SelectTrigger id="edit_segment" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="edit_action">Action (optional)</InputLabel>
<Select v-model="form.action_id" @update:model-value="handleActionChange">
<SelectTrigger id="edit_action" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="edit_decision">Decision (optional)</InputLabel>
<Select v-model="form.decision_id" :disabled="!form.action_id">
<SelectTrigger id="edit_decision" class="w-full">
<SelectValue placeholder="-- none --" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">-- none --</SelectItem>
<SelectItem v-for="d in availableDecisions" :key="d.id" :value="d.id">
{{ d.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="edit_name">Name</InputLabel>
<Input id="edit_name" v-model="form.name" type="text" />
<InputError :message="form.errors.name" class="mt-1" />
</div>
<div>
<InputLabel for="edit_focus">Focus Entity</InputLabel>
<Select v-model="form.focus">
<SelectTrigger id="edit_focus" class="w-full">
<SelectValue placeholder="-- choose --" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="selectedEntity && form.focus === selectedEntity.focus" class="space-y-2">
<InputLabel>Related Tables</InputLabel>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-2 text-sm bg-muted px-3 py-1.5 rounded-md border cursor-pointer hover:bg-muted/80"
>
<AppCheckboxArray :value="r" v-model="form.related" />
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<InputLabel for="edit_description">Description</InputLabel>
<Textarea id="edit_description" v-model="form.description" rows="2" />
<InputError :message="form.errors.description" class="mt-1" />
</div>
<div class="flex items-center gap-2">
<Checkbox id="edit_enabled" v-model="form.enabled" />
<InputLabel for="edit_enabled" class="text-sm font-normal cursor-pointer">
Enabled
</InputLabel>
</div>
<div class="flex items-center gap-2">
<Checkbox id="edit_soft" v-model="form.soft" />
<InputLabel for="edit_soft" class="text-sm font-normal cursor-pointer">
Soft Archive
</InputLabel>
</div>
<div class="flex items-center gap-2">
<Checkbox id="edit_reactivate" v-model="form.reactivate" />
<InputLabel for="edit_reactivate" class="text-sm font-normal cursor-pointer">
Reactivate (undo archive)
</InputLabel>
</div>
<div>
<InputLabel for="edit_strategy">Strategy</InputLabel>
<Select v-model="form.strategy">
<SelectTrigger id="edit_strategy" class="w-full">
<SelectValue placeholder="Select strategy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="immediate">Immediate</SelectItem>
<SelectItem value="scheduled">Scheduled</SelectItem>
<SelectItem value="queued">Queued</SelectItem>
<SelectItem value="manual">Manual (never auto-run)</SelectItem>
</SelectContent>
</Select>
<InputError :message="form.errors.strategy" class="mt-1" />
</div>
<div class="flex gap-2">
<Button @click="emit('submit')" :disabled="form.processing" class="flex-1">
Update
</Button>
<Button @click="emit('cancel')" variant="outline"> Cancel </Button>
</div>
<div v-if="Object.keys(form.errors).length" class="text-xs text-red-600">
Please fix validation errors.
</div>
</CardContent>
</Card>
</template>
@@ -1,58 +1,141 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import SectionTitle from '@/Components/SectionTitle.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue'
import UpdateDialog from '@/Components/Dialogs/UpdateDialog.vue'
import ConfirmationModal from '@/Components/ConfirmationModal.vue'
import InputLabel from '@/Components/InputLabel.vue'
import InputError from '@/Components/InputError.vue'
import { useForm, router } from '@inertiajs/vue3'
import { ref } from 'vue'
import AppLayout from "@/Layouts/AppLayout.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Button } from "@/Components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/Components/ui/dialog";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogFooter,
} from "@/Components/ui/alert-dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import { useForm, router } from "@inertiajs/vue3";
import { ref, nextTick } from "vue";
import {
MoreHorizontal,
Pencil,
Trash,
FileText,
Badge as BadgeIcon,
Check as CheckIcon,
} from "lucide-vue-next";
const props = defineProps({
configs: Array,
types: Array,
segments: Array,
})
});
// create modal
const showCreate = ref(false)
const openCreate = () => { showCreate.value = true }
const closeCreate = () => { showCreate.value = false; createForm.reset() }
const createForm = useForm({ contract_type_id: null, segment_id: null, is_initial: false })
const showCreate = ref(false);
const openCreate = () => {
showCreate.value = true;
};
const closeCreate = () => {
showCreate.value = false;
createForm.reset();
};
const createForm = useForm({
contract_type_id: null,
segment_id: null,
is_initial: false,
});
const submitCreate = () => {
createForm.post(route('settings.contractConfigs.store'), {
createForm.post(route("settings.contractConfigs.store"), {
preserveScroll: true,
onSuccess: () => closeCreate(),
})
}
});
};
// inline edit
const editing = ref(null)
const editForm = useForm({ segment_id: null, is_initial: false, active: true })
const openEdit = (row) => { editing.value = row; editForm.segment_id = row?.segment_id ?? row?.segment?.id; editForm.is_initial = !!row.is_initial; editForm.active = !!row.active }
const closeEdit = () => { editing.value = null }
const showEdit = ref(false);
const editing = ref(null);
const editFormIsInitial = ref(false);
const editFormActive = ref(false);
const editForm = useForm({ segment_id: null, is_initial: false, active: false });
const openEdit = (row) => {
editing.value = row;
editForm.clearErrors();
// Set values
editForm.segment_id = row?.segment_id ?? row?.segment?.id ?? null;
editFormIsInitial.value = row.is_initial === 1 || row.is_initial === true;
editFormActive.value = row.active === 1 || row.active === true;
showEdit.value = true;
};
const closeEdit = () => {
showEdit.value = false;
editing.value = null;
editFormIsInitial.value = false;
editFormActive.value = false;
editForm.reset();
editForm.clearErrors();
};
const submitEdit = () => {
if (!editing.value) return
editForm.put(route('settings.contractConfigs.update', editing.value.id), {
if (!editing.value) return;
editForm.is_initial = editFormIsInitial.value;
editForm.active = editFormActive.value;
editForm.put(route("settings.contractConfigs.update", editing.value.id), {
preserveScroll: true,
onSuccess: () => closeEdit(),
})
}
});
};
// delete confirmation
const showDelete = ref(false)
const toDelete = ref(null)
const confirmDelete = (row) => { toDelete.value = row; showDelete.value = true }
const cancelDelete = () => { toDelete.value = null; showDelete.value = false }
const showDelete = ref(false);
const toDelete = ref(null);
const confirmDelete = (row) => {
toDelete.value = row;
showDelete.value = true;
};
const cancelDelete = () => {
toDelete.value = null;
showDelete.value = false;
};
const destroyConfig = () => {
if (!toDelete.value) return
router.delete(route('settings.contractConfigs.destroy', toDelete.value.id), {
if (!toDelete.value) return;
router.delete(route("settings.contractConfigs.destroy", toDelete.value.id), {
preserveScroll: true,
onFinish: () => cancelDelete(),
})
}
});
};
// DataTable state
const sort = ref({ key: null, direction: null });
const page = ref(1);
const pageSize = ref(25);
const columns = [
{ key: "id", label: "ID", sortable: true, class: "w-16" },
{ key: "type", label: "Type", sortable: false },
{ key: "segment", label: "Segment", sortable: false },
{ key: "active", label: "Active", sortable: false, class: "w-24" },
];
</script>
<template>
@@ -60,116 +143,195 @@ const destroyConfig = () => {
<template #header />
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg border-l-4 border-indigo-400">
<div class="p-4 flex items-center justify-between">
<SectionTitle>
<template #title>Contract configurations</template>
</SectionTitle>
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
</div>
<div class="px-4 pb-4">
<table class="min-w-full text-left text-sm">
<thead>
<tr class="border-b">
<th class="py-2 pr-4">Type</th>
<th class="py-2 pr-4">Segment</th>
<th class="py-2 pr-4">Active</th>
<th class="py-2 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="cfg in configs" :key="cfg.id" class="border-b last:border-0">
<td class="py-2 pr-4">{{ cfg.type?.name }}</td>
<td class="py-2 pr-4">{{ cfg.segment?.name }} <span v-if="cfg.is_initial" class="ml-2 text-xs text-indigo-600">(initial)</span></td>
<td class="py-2 pr-4">{{ cfg.active ? 'Yes' : 'No' }}</td>
<td class="py-2 pr-4 text-right">
<button class="px-2 py-1 text-indigo-600 hover:underline" @click="openEdit(cfg)">Edit</button>
<button class="ml-2 px-2 py-1 text-red-600 hover:underline" @click="confirmDelete(cfg)">Delete</button>
</td>
</tr>
<tr v-if="!configs || configs.length === 0">
<td colspan="4" class="py-6 text-center text-gray-500">No configurations.</td>
</tr>
</tbody>
</table>
</div>
</div>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<FileText :size="18" />
<CardTitle class="uppercase">Contract configurations</CardTitle>
</div>
<Button @click="openCreate">+ New</Button>
</div>
</template>
<DataTableClient
:columns="columns"
:rows="configs"
:sort="sort"
:search="''"
:page="page"
:pageSize="pageSize"
:showToolbar="false"
:showPagination="true"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
>
<template #cell-type="{ row }">
{{ row.type?.name }}
</template>
<template #cell-segment="{ row }">
<div class="flex items-center gap-2">
{{ row.segment?.name }}
<CheckIcon v-if="row.is_initial" class="w-5 h-5 text-primary" />
</div>
</template>
<template #cell-active="{ row }">
<div class="flex items-center">
<div
class="w-3 h-3 rounded-full"
:class="row.active ? 'bg-green-500' : 'bg-red-500'"
></div>
</div>
</template>
<template #actions="{ row }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEdit(row)">
<Pencil class="w-4 h-4 mr-2" />
Uredi
</DropdownMenuItem>
<DropdownMenuItem
@click="confirmDelete(row)"
class="text-red-600 focus:text-red-600"
>
<Trash class="w-4 h-4 mr-2" />
Izbriši
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
</DataTableClient>
</AppCard>
</div>
</div>
<!-- create modal -->
<CreateDialog
:show="showCreate"
title="New Contract Configuration"
confirm-text="Create"
:processing="createForm.processing"
:disabled="!createForm.contract_type_id || !createForm.segment_id"
@close="closeCreate"
@confirm="submitCreate"
>
<div class="space-y-4">
<Dialog v-model:open="showCreate">
<DialogContent class="max-w-xl">
<DialogHeader>
<DialogTitle>New Contract Configuration</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="type">Contract Type</InputLabel>
<select id="type" v-model="createForm.contract_type_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option :value="null" disabled>-- select type --</option>
<option v-for="t in types" :key="t.id" :value="t.id">{{ t.name }}</option>
</select>
<InputError :message="createForm.errors.contract_type_id" />
<Select v-model="createForm.contract_type_id">
<SelectTrigger id="type" class="w-full">
<SelectValue placeholder="-- select type --" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="t in types" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
<InputError :message="createForm.errors.contract_type_id" class="mt-1" />
</div>
<div>
<InputLabel for="segment">Segment</InputLabel>
<select id="segment" v-model="createForm.segment_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option :value="null" disabled>-- select segment --</option>
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
<InputError :message="createForm.errors.segment_id" />
<Select v-model="createForm.segment_id">
<SelectTrigger id="segment" class="w-full">
<SelectValue placeholder="-- select segment --" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</SelectItem>
</SelectContent>
</Select>
<InputError :message="createForm.errors.segment_id" class="mt-1" />
<div class="mt-3 flex items-center gap-2">
<input id="is_initial" type="checkbox" v-model="createForm.is_initial" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<label for="is_initial" class="text-sm text-gray-700">Mark as initial</label>
<Checkbox id="is_initial" v-model="createForm.is_initial" />
<InputLabel for="is_initial" class="text-sm font-normal cursor-pointer"
>Mark as initial</InputLabel
>
</div>
</div>
</div>
</CreateDialog>
</div>
<DialogFooter>
<Button variant="outline" @click="closeCreate">Cancel</Button>
<Button
@click="submitCreate"
:disabled="
createForm.processing ||
!createForm.contract_type_id ||
!createForm.segment_id
"
>Create</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- simple inline edit dialog -->
<UpdateDialog
:show="!!editing"
title="Edit Configuration"
confirm-text="Save"
:processing="editForm.processing"
:disabled="!editForm.segment_id"
@close="closeEdit"
@confirm="submitEdit"
>
<div class="space-y-4">
<Dialog v-model:open="showEdit">
<DialogContent class="max-w-xl">
<DialogHeader>
<DialogTitle>Edit Configuration</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel>Segment</InputLabel>
<select v-model="editForm.segment_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
</select>
<InputError :message="editForm.errors.segment_id" />
<Select v-model="editForm.segment_id">
<SelectTrigger class="w-full">
<SelectValue placeholder="-- select segment --" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</SelectItem>
</SelectContent>
</Select>
<InputError :message="editForm.errors.segment_id" class="mt-1" />
</div>
<div class="flex items-center gap-2">
<input id="is_initial_edit" type="checkbox" v-model="editForm.is_initial" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<label for="is_initial_edit" class="text-sm text-gray-700">Initial</label>
<Checkbox id="is_initial_edit" v-model="editFormIsInitial" />
<InputLabel for="is_initial_edit" class="text-sm font-normal cursor-pointer"
>Initial</InputLabel
>
</div>
<div class="flex items-center gap-2">
<input id="active" type="checkbox" v-model="editForm.active" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<label for="active" class="text-sm text-gray-700">Active</label>
<Checkbox id="active" v-model="editFormActive" />
<InputLabel for="active" class="text-sm font-normal cursor-pointer"
>Active</InputLabel
>
</div>
</div>
</UpdateDialog>
</div>
<DialogFooter>
<Button variant="outline" @click="closeEdit">Cancel</Button>
<Button
@click="submitEdit"
:disabled="editForm.processing || !editForm.segment_id"
>Save</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
</AppLayout>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title>
Delete configuration
</template>
<template #content>
Are you sure you want to delete configuration for type "{{ toDelete?.type?.name }}"?
</template>
<template #footer>
<button @click="cancelDelete" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2">Cancel</button>
<PrimaryButton @click="destroyConfig">Delete</PrimaryButton>
</template>
</ConfirmationModal>
<AlertDialog v-model:open="showDelete">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete configuration</AlertDialogTitle>
</AlertDialogHeader>
<div class="text-sm text-muted-foreground">
Are you sure you want to delete configuration for type "{{
toDelete?.type?.name
}}"?
</div>
<AlertDialogFooter>
<Button variant="outline" @click="cancelDelete">Cancel</Button>
<Button variant="destructive" @click="destroyConfig">Delete</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
+340 -400
View File
@@ -1,14 +1,28 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, useForm } from "@inertiajs/vue3";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { useForm } from "@inertiajs/vue3";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Button } from "@/Components/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/Components/ui/dialog";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import { ref, onMounted, watch } from "vue";
import Multiselect from "vue-multiselect";
import { de } from "date-fns/locale";
import { ref, onMounted, watch, computed } from "vue";
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import { MoreHorizontal, Pencil, Settings } from "lucide-vue-next";
const props = defineProps({
settings: Array,
@@ -25,12 +39,18 @@ const decisionOptions = ref([]);
const actionOptions = ref([]);
onMounted(() => {
segmentOptions.value = (props.segments || []).map((s) => ({ id: s.id, name: s.name }));
decisionOptions.value = (props.decisions || []).map((d) => ({
id: d.id,
name: d.name,
segmentOptions.value = (props.segments || []).map((s) => ({
label: s.name,
value: s.id,
}));
decisionOptions.value = (props.decisions || []).map((d) => ({
label: d.name,
value: d.id,
}));
actionOptions.value = (props.actions || []).map((a) => ({
label: a.name,
value: a.id,
}));
actionOptions.value = (props.actions || []).map((a) => ({ id: a.id, name: a.name }));
});
const form = useForm({
@@ -117,18 +137,10 @@ const update = () => {
watch(
() => editForm.action_id,
(newActionId) => {
// Clear decision fields when action changes
/*editForm.initial_decision_id = null;
editForm.assign_decision_id = null;
editForm.complete_decision_id = null;
editForm.cancel_decision_id = null;*/
if (newActionId !== null) {
// Optionally, you can filter decisionOptions based on the selected action here
decisionOptions.value = (props.decisions || [])
.filter((decision) => decision.actions?.some((a) => a.id === newActionId) ?? true)
.map((d) => ({ id: d.id, name: d.name }));
// For simplicity, we are not implementing that logic now
console.log(decisionOptions.value);
.map((d) => ({ label: d.name, value: d.id }));
}
}
);
@@ -136,20 +148,32 @@ watch(
watch(
() => form.action_id,
(newActionId) => {
// Clear decision fields when action changes
form.initial_decision_id = null;
form.assign_decision_id = null;
form.complete_decision_id = null;
if (newActionId !== null) {
// Optionally, you can filter decisionOptions based on the selected action here
decisionOptions.value = (props.decisions || [])
.filter((decision) => decision.actions?.some((a) => a.id === newActionId) ?? true)
.map((d) => ({ id: d.id, name: d.name }));
// For simplicity, we are not implementing that logic now
console.log(decisionOptions.value);
.map((d) => ({ label: d.name, value: d.id }));
}
}
);
// DataTable state
const sort = ref({ key: null, direction: null });
const page = ref(1);
const pageSize = ref(25);
const columns = [
{ key: "id", label: "ID", sortable: true, class: "w-16" },
{ key: "segment", label: "Segment", sortable: false },
{ key: "action", label: "Action", sortable: false },
{ key: "initial_decision", label: "Initial Decision", sortable: false },
{ key: "assign_decision", label: "Assign Decision", sortable: false },
{ key: "complete_decision", label: "Complete Decision", sortable: false },
{ key: "cancel_decision", label: "Cancel Decision", sortable: false },
{ key: "return_segment", label: "Return Segment", sortable: false },
{ key: "queue_segment", label: "Queue Segment", sortable: false },
];
</script>
<template>
@@ -157,383 +181,299 @@ watch(
<template #header></template>
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Field Job Settings</h2>
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
</div>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<Settings :size="18" />
<CardTitle class="uppercase">Field Job Settings</CardTitle>
</div>
<Button @click="openCreate">+ New</Button>
</div>
</template>
<CreateDialog
:show="showCreate"
title="Create Field Job Setting"
confirm-text="Create"
:processing="form.processing"
@close="closeCreate"
@confirm="store"
>
<form @submit.prevent="store">
<div class="grid grid-cols-1 gap-4">
<div>
<InputLabel for="segment" value="Segment" />
<multiselect
id="segment"
v-model="form.segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select segment"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.segment_id" class="mt-1" />
</div>
<div>
<InputLabel for="action" value="Action" />
<multiselect
id="action"
v-model="form.action_id"
:options="actionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select action"
:append-to-body="true"
:custom-label="
(opt) => actionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.action_id" class="mt-1" />
</div>
<div>
<InputLabel for="initialDecision" value="Initial Decision" />
<multiselect
id="initialDecision"
v-model="form.initial_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select initial decision"
:append-to-body="true"
:disabled="!form.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.initial_decision_id" class="mt-1" />
</div>
<div>
<InputLabel for="assignDecision" value="Assign Decision" />
<multiselect
id="assignDecision"
v-model="form.assign_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select assign decision"
:append-to-body="true"
:disabled="!form.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.assign_decision_id" class="mt-1" />
</div>
<div class="mt-2">
<InputLabel for="completeDecision" value="Complete Decision" />
<multiselect
id="completeDecision"
v-model="form.complete_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select complete decision"
:append-to-body="true"
:disabled="!form.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="form.errors.complete_decision_id"
class="mt-1"
/>
</div>
<div class="mt-2">
<InputLabel for="cancelDecision" value="Cancel Decision" />
<multiselect
id="cancelDecision"
v-model="form.cancel_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select cancel decision (optional)"
:append-to-body="true"
:disabled="!form.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.cancel_decision_id" class="mt-1" />
</div>
<div class="mt-2">
<InputLabel for="returnSegment" value="Return Segment" />
<multiselect
id="returnSegment"
v-model="form.return_segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select return segment (optional)"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.return_segment_id" class="mt-1" />
</div>
<div class="mt-2">
<InputLabel for="queueSegment" value="Queue Segment" />
<multiselect
id="queueSegment"
v-model="form.queue_segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select queue segment (optional)"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="form.errors.queue_segment_id" class="mt-1" />
</div>
<Dialog v-model:open="showCreate">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create Field Job Setting</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="segment" value="Segment" />
<AppCombobox
id="segment"
v-model="form.segment_id"
:items="segmentOptions"
placeholder="Select segment"
button-class="w-full"
/>
<InputError :message="form.errors.segment_id" class="mt-1" />
</div>
</form>
</CreateDialog>
<UpdateDialog
:show="showEdit"
title="Edit Field Job Setting"
confirm-text="Save"
:processing="editForm.processing"
@close="closeEdit"
@confirm="update"
>
<form @submit.prevent="update">
<div class="grid grid-cols-1 gap-4">
<div>
<InputLabel for="edit-segment" value="Segment" />
<multiselect
id="edit-segment"
v-model="editForm.segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select segment"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="editForm.errors.segment_id" class="mt-1" />
</div>
<div>
<InputLabel for="edit-action" value="Action" />
<multiselect
id="edit-action"
v-model="editForm.action_id"
:options="actionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select action"
:append-to-body="true"
:custom-label="
(opt) => actionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError :message="editForm.errors.action_id" class="mt-1" />
</div>
<div>
<InputLabel for="edit-initialDecision" value="Initial Decision" />
<multiselect
id="edit-initialDecision"
v-model="editForm.initial_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select initial decision"
:append-to-body="true"
:disabled="!editForm.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.initial_decision_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="edit-assignDecision" value="Assign Decision" />
<multiselect
id="edit-assignDecision"
v-model="editForm.assign_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select assign decision"
:append-to-body="true"
:disabled="!editForm.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.assign_decision_id"
class="mt-1"
/>
</div>
<div class="mt-2">
<InputLabel for="edit-completeDecision" value="Complete Decision" />
<multiselect
id="edit-completeDecision"
v-model="editForm.complete_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select complete decision"
:disabled="!editForm.action_id"
:append-to-body="true"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.complete_decision_id"
class="mt-1"
/>
</div>
<div class="mt-2">
<InputLabel for="edit-cancelDecision" value="Cancel Decision" />
<multiselect
id="edit-cancelDecision"
v-model="editForm.cancel_decision_id"
:options="decisionOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select cancel decision (optional)"
:append-to-body="true"
:disabled="!editForm.action_id"
:custom-label="
(opt) => decisionOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.cancel_decision_id"
class="mt-1"
/>
</div>
<div class="mt-2">
<InputLabel for="edit-returnSegment" value="Return Segment" />
<multiselect
id="edit-returnSegment"
v-model="editForm.return_segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select return segment (optional)"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.return_segment_id"
class="mt-1"
/>
</div>
<div class="mt-2">
<InputLabel for="edit-queueSegment" value="Queue Segment" />
<multiselect
id="edit-queueSegment"
v-model="editForm.queue_segment_id"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Select queue segment (optional)"
:append-to-body="true"
:custom-label="
(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''
"
/>
<InputError
:message="editForm.errors.queue_segment_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="action" value="Action" />
<AppCombobox
id="action"
v-model="form.action_id"
:items="actionOptions"
placeholder="Select action"
button-class="w-full"
/>
<InputError :message="form.errors.action_id" class="mt-1" />
</div>
</form>
</UpdateDialog>
<table class="min-w-full text-left text-sm">
<thead>
<tr class="border-b">
<th class="py-2 pr-4">ID</th>
<th class="py-2 pr-4">Segment</th>
<th class="py-2 pr-4">Action</th>
<th class="py-2 pr-4">Initial Decision</th>
<th class="py-2 pr-4">Assign Decision</th>
<th class="py-2 pr-4">Complete Decision</th>
<th class="py-2 pr-4">Cancel Decision</th>
<th class="py-2 pr-4">Return Segment</th>
<th class="py-2 pr-4">Queue Segment</th>
<th class="py-2 pr-4">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="row in settings" :key="row.id" class="border-b last:border-0">
<td class="py-2 pr-4">{{ row.id }}</td>
<td class="py-2 pr-4">{{ row.segment?.name }}</td>
<td class="py-2 pr-4">{{ row.action?.name }}</td>
<td class="py-2 pr-4">
{{ row.initial_decision?.name || row.initialDecision?.name }}
</td>
<td class="py-2 pr-4">
{{ row.assign_decision?.name || row.assignDecision?.name }}
</td>
<td class="py-2 pr-4">
{{ row.complete_decision?.name || row.completeDecision?.name }}
</td>
<td class="py-2 pr-4">
{{ row.cancel_decision?.name || row.cancelDecision?.name }}
</td>
<td class="py-2 pr-4">
{{ row.return_segment?.name || row.returnSegment?.name }}
</td>
<td class="py-2 pr-4">
{{ row.queue_segment?.name || row.queueSegment?.name }}
</td>
<td class="py-2 pr-4">
<button
@click="openEdit(row)"
class="px-3 py-1 rounded bg-indigo-600 text-white hover:bg-indigo-700"
>
Edit
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div>
<InputLabel for="initialDecision" value="Initial Decision" />
<AppCombobox
id="initialDecision"
v-model="form.initial_decision_id"
:items="decisionOptions"
placeholder="Select initial decision"
:disabled="!form.action_id"
button-class="w-full"
/>
<InputError :message="form.errors.initial_decision_id" class="mt-1" />
</div>
<div>
<InputLabel for="assignDecision" value="Assign Decision" />
<AppCombobox
id="assignDecision"
v-model="form.assign_decision_id"
:items="decisionOptions"
placeholder="Select assign decision"
:disabled="!form.action_id"
button-class="w-full"
/>
<InputError :message="form.errors.assign_decision_id" class="mt-1" />
</div>
<div>
<InputLabel for="completeDecision" value="Complete Decision" />
<AppCombobox
id="completeDecision"
v-model="form.complete_decision_id"
:items="decisionOptions"
placeholder="Select complete decision"
:disabled="!form.action_id"
button-class="w-full"
/>
<InputError :message="form.errors.complete_decision_id" class="mt-1" />
</div>
<div>
<InputLabel for="cancelDecision" value="Cancel Decision" />
<AppCombobox
id="cancelDecision"
v-model="form.cancel_decision_id"
:items="decisionOptions"
placeholder="Select cancel decision (optional)"
:disabled="!form.action_id"
button-class="w-full"
/>
<InputError :message="form.errors.cancel_decision_id" class="mt-1" />
</div>
<div>
<InputLabel for="returnSegment" value="Return Segment" />
<AppCombobox
id="returnSegment"
v-model="form.return_segment_id"
:items="segmentOptions"
placeholder="Select return segment (optional)"
button-class="w-full"
/>
<InputError :message="form.errors.return_segment_id" class="mt-1" />
</div>
<div>
<InputLabel for="queueSegment" value="Queue Segment" />
<AppCombobox
id="queueSegment"
v-model="form.queue_segment_id"
:items="segmentOptions"
placeholder="Select queue segment (optional)"
button-class="w-full"
/>
<InputError :message="form.errors.queue_segment_id" class="mt-1" />
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="closeCreate">Cancel</Button>
<Button @click="store" :disabled="form.processing">Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="showEdit">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Field Job Setting</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="edit-segment" value="Segment" />
<AppCombobox
id="edit-segment"
v-model="editForm.segment_id"
:items="segmentOptions"
placeholder="Select segment"
button-class="w-full"
/>
<InputError :message="editForm.errors.segment_id" class="mt-1" />
</div>
<div>
<InputLabel for="edit-action" value="Action" />
<AppCombobox
id="edit-action"
v-model="editForm.action_id"
:items="actionOptions"
placeholder="Select action"
button-class="w-full"
/>
<InputError :message="editForm.errors.action_id" class="mt-1" />
</div>
<div>
<InputLabel for="edit-initialDecision" value="Initial Decision" />
<AppCombobox
id="edit-initialDecision"
v-model="editForm.initial_decision_id"
:items="decisionOptions"
placeholder="Select initial decision"
:disabled="!editForm.action_id"
button-class="w-full"
/>
<InputError
:message="editForm.errors.initial_decision_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="edit-assignDecision" value="Assign Decision" />
<AppCombobox
id="edit-assignDecision"
v-model="editForm.assign_decision_id"
:items="decisionOptions"
placeholder="Select assign decision"
:disabled="!editForm.action_id"
button-class="w-full"
/>
<InputError
:message="editForm.errors.assign_decision_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="edit-completeDecision" value="Complete Decision" />
<AppCombobox
id="edit-completeDecision"
v-model="editForm.complete_decision_id"
:items="decisionOptions"
placeholder="Select complete decision"
:disabled="!editForm.action_id"
button-class="w-full"
/>
<InputError
:message="editForm.errors.complete_decision_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="edit-cancelDecision" value="Cancel Decision" />
<AppCombobox
id="edit-cancelDecision"
v-model="editForm.cancel_decision_id"
:items="decisionOptions"
placeholder="Select cancel decision (optional)"
:disabled="!editForm.action_id"
button-class="w-full"
/>
<InputError
:message="editForm.errors.cancel_decision_id"
class="mt-1"
/>
</div>
<div>
<InputLabel for="edit-returnSegment" value="Return Segment" />
<AppCombobox
id="edit-returnSegment"
v-model="editForm.return_segment_id"
:items="segmentOptions"
placeholder="Select return segment (optional)"
button-class="w-full"
/>
<InputError :message="editForm.errors.return_segment_id" class="mt-1" />
</div>
<div>
<InputLabel for="edit-queueSegment" value="Queue Segment" />
<AppCombobox
id="edit-queueSegment"
v-model="editForm.queue_segment_id"
:items="segmentOptions"
placeholder="Select queue segment (optional)"
button-class="w-full"
/>
<InputError :message="editForm.errors.queue_segment_id" class="mt-1" />
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="closeEdit">Cancel</Button>
<Button @click="update" :disabled="editForm.processing">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DataTableClient
:columns="columns"
:rows="settings"
:sort="sort"
:search="''"
:page="page"
:pageSize="pageSize"
:showToolbar="false"
:showPagination="true"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
>
<template #cell-segment="{ row }">
{{ row.segment?.name }}
</template>
<template #cell-action="{ row }">
{{ row.action?.name }}
</template>
<template #cell-initial_decision="{ row }">
{{ row.initial_decision?.name || row.initialDecision?.name }}
</template>
<template #cell-assign_decision="{ row }">
{{ row.assign_decision?.name || row.assignDecision?.name }}
</template>
<template #cell-complete_decision="{ row }">
{{ row.complete_decision?.name || row.completeDecision?.name }}
</template>
<template #cell-cancel_decision="{ row }">
{{ row.cancel_decision?.name || row.cancelDecision?.name }}
</template>
<template #cell-return_segment="{ row }">
{{ row.return_segment?.name || row.returnSegment?.name }}
</template>
<template #cell-queue_segment="{ row }">
{{ row.queue_segment?.name || row.queueSegment?.name }}
</template>
<template #actions="{ row }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEdit(row)">
<Pencil class="w-4 h-4 mr-2" />
Uredi
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
</DataTableClient>
</AppCard>
</div>
</div>
</AppLayout>
+94 -73
View File
@@ -1,84 +1,105 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Link } from "@inertiajs/vue3";
import {
Layers,
CreditCard,
GitBranch,
Briefcase,
FileText,
Archive,
ArrowRight,
BarChart3,
} from "lucide-vue-next";
const settingsCards = [
{
title: "Segments",
description: "Manage segments used across the app.",
route: "settings.segments",
icon: Layers,
},
{
title: "Payments",
description: "Defaults for payments and auto-activity.",
route: "settings.payment.edit",
icon: CreditCard,
},
{
title: "Workflow",
description: "Configure actions and decisions relationships.",
route: "settings.workflow",
icon: GitBranch,
},
{
title: "Field Job Settings",
description: "Configure segment-based field job rules.",
route: "settings.fieldjob.index",
icon: Briefcase,
},
{
title: "Contract Configs",
description: "Auto-assign initial segments for contracts by type.",
route: "settings.contractConfigs.index",
icon: FileText,
},
{
title: "Archive Settings",
description: "Define rules for archiving or soft-deleting aged data.",
route: "settings.archive.index",
icon: Archive,
},
{
title: "Reports",
description: "Configure database-driven reports with dynamic queries.",
route: "settings.reports.index",
icon: BarChart3,
},
];
</script>
<template>
<AppLayout title="Settings">
<template #header></template>
<template #header />
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Segments</h3>
<p class="text-sm text-gray-600 mb-4">Manage segments used across the app.</p>
<Link
:href="route('settings.segments')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Segments</Link
>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Payments</h3>
<p class="text-sm text-gray-600 mb-4">
Defaults for payments and auto-activity.
</p>
<Link
:href="route('settings.payment.edit')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Payment Settings</Link
>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Workflow</h3>
<p class="text-sm text-gray-600 mb-4">
Configure actions and decisions relationships.
</p>
<Link
:href="route('settings.workflow')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Workflow</Link
>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Field Job Settings</h3>
<p class="text-sm text-gray-600 mb-4">
Configure segment-based field job rules.
</p>
<Link
:href="route('settings.fieldjob.index')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Field Job</Link
>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Contract Configs</h3>
<p class="text-sm text-gray-600 mb-4">
Auto-assign initial segments for contracts by type.
</p>
<Link
:href="route('settings.contractConfigs.index')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Contract Configs</Link
>
</div>
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<h3 class="text-lg font-semibold mb-2">Archive Settings</h3>
<p class="text-sm text-gray-600 mb-4">
Define rules for archiving or soft-deleting aged data.
</p>
<Link
:href="route('settings.archive.index')"
class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
>
Open Archive Settings</Link
>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Settings</h1>
<p class="mt-2 text-sm text-muted-foreground">
Manage your application configuration and preferences
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card
v-for="card in settingsCards"
:key="card.route"
class="hover:shadow-lg transition-shadow"
>
<CardHeader>
<div class="flex items-center gap-3 mb-2">
<div
class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 text-primary"
>
<component :is="card.icon" :size="20" />
</div>
<CardTitle class="text-lg">{{ card.title }}</CardTitle>
</div>
<CardDescription>{{ card.description }}</CardDescription>
</CardHeader>
<CardContent>
<Link :href="route(card.route)">
<Button class="w-full group">
Open Settings
<ArrowRight
class="ml-2 h-4 w-4 transition-transform group-hover:translate-x-1"
/>
</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
</div>
@@ -1,362 +0,0 @@
<script setup>
// flowbite-vue table imports removed; using DataTableClient
import { EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import { computed, onMounted, ref } from "vue";
import { router, useForm } from "@inertiajs/vue3";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import Multiselect from "vue-multiselect";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import ActionMessage from "@/Components/ActionMessage.vue";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
const props = defineProps({
actions: Array,
decisions: Array,
segments: Array,
});
const drawerEdit = ref(false);
const drawerCreate = ref(false);
const showDelete = ref(false);
const toDelete = ref(null);
const search = ref("");
const selectedSegment = ref(null);
const selectOptions = ref([]);
const segmentOptions = ref([]);
// DataTable state
const sort = ref({ key: null, direction: null });
const page = ref(1);
const pageSize = ref(25);
const columns = [
{ key: "id", label: "#", sortable: true, class: "w-16" },
{ key: "name", label: "Ime", sortable: true },
{ key: "color_tag", label: "Barva", sortable: false },
{ key: "segment", label: "Segment", sortable: false },
{ key: "decisions", label: "Odločitve", sortable: false, class: "w-32" },
];
const form = useForm({
id: 0,
name: "",
color_tag: "",
segment_id: null,
decisions: [],
});
const createForm = useForm({
name: "",
color_tag: "",
segment_id: null,
decisions: [],
});
const openEditDrawer = (item) => {
form.decisions = [];
form.id = item.id;
form.name = item.name;
form.color_tag = item.color_tag;
form.segment_id = item.segment ? item.segment.id : null;
drawerEdit.value = true;
item.decisions.forEach((d) => {
form.decisions.push({
name: d.name,
id: d.id,
});
});
};
const closeEditDrawer = () => {
drawerEdit.value = false;
form.reset();
};
const openCreateDrawer = () => {
createForm.reset();
drawerCreate.value = true;
};
const closeCreateDrawer = () => {
drawerCreate.value = false;
createForm.reset();
};
// removed unused color picker change handler; InlineColorPicker handles updates
onMounted(() => {
props.decisions.forEach((d) => {
selectOptions.value.push({
name: d.name,
id: d.id,
});
});
props.segments.forEach((s) => {
segmentOptions.value.push({
name: s.name,
id: s.id,
});
});
});
const filtered = computed(() => {
const term = search.value?.toLowerCase() ?? "";
return (props.actions || []).filter((a) => {
const matchesSearch =
!term ||
a.name?.toLowerCase().includes(term) ||
a.color_tag?.toLowerCase().includes(term);
const matchesSegment =
!selectedSegment.value || a.segment?.id === selectedSegment.value;
return matchesSearch && matchesSegment;
});
});
const update = () => {
form.put(route("settings.actions.update", { id: form.id }), {
onSuccess: () => {
closeEditDrawer();
},
});
};
const store = () => {
createForm.post(route("settings.actions.store"), {
onSuccess: () => {
closeCreateDrawer();
},
});
};
const confirmDelete = (action) => {
toDelete.value = action;
showDelete.value = true;
};
const cancelDelete = () => {
toDelete.value = null;
showDelete.value = false;
};
const destroyAction = () => {
if (!toDelete.value) return;
router.delete(route("settings.actions.destroy", { id: toDelete.value.id }), {
preserveScroll: true,
onFinish: () => cancelDelete(),
});
};
</script>
<template>
<div class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex gap-3 items-center w-full sm:w-auto">
<TextInput v-model="search" placeholder="Iskanje..." class="w-full sm:w-64" />
<div class="w-64">
<Multiselect
v-model="selectedSegment"
:options="segmentOptions.map((o) => o.id)"
:multiple="false"
:searchable="true"
placeholder="Filter po segmentu"
:append-to-body="true"
:custom-label="(opt) => segmentOptions.find((o) => o.id === opt)?.name || ''"
/>
</div>
</div>
<PrimaryButton @click="openCreateDrawer">+ Dodaj akcijo</PrimaryButton>
</div>
<div class="px-4 pb-4">
<DataTableClient
:columns="columns"
:rows="filtered"
:sort="sort"
:search="''"
:page="page"
:pageSize="pageSize"
:showToolbar="false"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
>
<template #cell-color_tag="{ row }">
<div class="flex items-center gap-2">
<span
v-if="row.color_tag"
class="inline-block h-4 w-4 rounded"
:style="{ backgroundColor: row.color_tag }"
></span>
<span>{{ row.color_tag || "" }}</span>
</div>
</template>
<template #cell-decisions="{ row }">
{{ row.decisions?.length ?? 0 }}
</template>
<template #cell-segment="{ row }">
<span>
{{ row.segment?.name || "" }}
</span>
</template>
<template #actions="{ row }">
<button class="px-2" @click="openEditDrawer(row)">
<EditIcon size="md" css="text-gray-500" />
</button>
<button
class="px-2 disabled:opacity-40"
:disabled="(row.activities_count ?? 0) > 0"
@click="confirmDelete(row)"
>
<TrashBinIcon size="md" css="text-red-500" />
</button>
</template>
</DataTableClient>
</div>
<UpdateDialog
:show="drawerEdit"
title="Spremeni akcijo"
confirm-text="Shrani"
:processing="form.processing"
@close="closeEditDrawer"
@confirm="update"
>
<form @submit.prevent="update">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Ime" />
<TextInput
id="name"
ref="nameInput"
v-model="form.name"
type="text"
class="mt-1 block w-full"
autocomplete="name"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="colorTag" value="Barva" />
<div class="mt-1">
<InlineColorPicker v-model="form.color_tag" />
</div>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="segmentEdit" value="Segment" />
<multiselect
id="segmentEdit"
v-model="form.segment_id"
:options="segmentOptions.map((s) => s.id)"
:multiple="false"
:searchable="true"
:taggable="false"
placeholder="Izberi segment"
:append-to-body="true"
:custom-label="(opt) => segmentOptions.find((s) => s.id === opt)?.name || ''"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="decisions" value="Odločitve" />
<multiselect
id="decisions"
ref="decisionsSelect"
v-model="form.decisions"
:options="selectOptions"
:multiple="true"
track-by="id"
:taggable="true"
placeholder="Dodaj odločitev"
:append-to-body="true"
label="name"
/>
</div>
<div v-if="form.recentlySuccessful" class="mt-4 text-sm text-green-600">
Shranjuje.
</div>
</form>
</UpdateDialog>
<CreateDialog
:show="drawerCreate"
title="Dodaj akcijo"
confirm-text="Dodaj"
:processing="createForm.processing"
@close="closeCreateDrawer"
@confirm="store"
>
<form @submit.prevent="store">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="nameCreate" value="Ime" />
<TextInput
id="nameCreate"
v-model="createForm.name"
type="text"
class="mt-1 block w-full"
autocomplete="name"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="colorTagCreate" value="Barva" />
<div class="mt-1">
<InlineColorPicker v-model="createForm.color_tag" />
</div>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="segmentCreate" value="Segment" />
<multiselect
id="segmentCreate"
v-model="createForm.segment_id"
:options="segmentOptions.map((s) => s.id)"
:multiple="false"
:searchable="true"
:taggable="false"
placeholder="Izberi segment"
:append-to-body="true"
:custom-label="(opt) => segmentOptions.find((s) => s.id === opt)?.name || ''"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="decisionsCreate" value="Odločitve" />
<multiselect
id="decisionsCreate"
v-model="createForm.decisions"
:options="selectOptions"
:multiple="true"
track-by="id"
:taggable="true"
placeholder="Dodaj odločitev"
:append-to-body="true"
label="name"
/>
</div>
<div v-if="createForm.recentlySuccessful" class="mt-4 text-sm text-green-600">
Shranjuje.
</div>
</form>
</CreateDialog>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title> Delete action </template>
<template #content>
Are you sure you want to delete action "{{ toDelete?.name }}"? This cannot be
undone.
</template>
<template #footer>
<button
@click="cancelDelete"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2"
>
Cancel
</button>
<PrimaryButton @click="destroyAction">Delete</PrimaryButton>
</template>
</ConfirmationModal>
</template>
+132 -67
View File
@@ -1,103 +1,168 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import { useForm } from '@inertiajs/vue3'
import { computed, watch } from 'vue'
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm } from "@inertiajs/vue3";
import { computed, watch } from "vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Wallet } from "lucide-vue-next";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import InputLabel from "@/Components/InputLabel.vue";
import { Checkbox } from "@/Components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
setting: Object,
decisions: Array,
actions: Array,
})
});
const form = useForm({
default_currency: props.setting?.default_currency ?? 'EUR',
default_currency: props.setting?.default_currency ?? "EUR",
create_activity_on_payment: !!props.setting?.create_activity_on_payment,
default_action_id: props.setting?.default_action_id ?? null,
default_decision_id: props.setting?.default_decision_id ?? null,
activity_note_template: props.setting?.activity_note_template ?? 'Prejeto plačilo: {amount} {currency}',
})
activity_note_template:
props.setting?.activity_note_template ?? "Prejeto plačilo: {amount} {currency}",
});
const filteredDecisions = computed(() => {
const actionId = form.default_action_id
if (!actionId) return []
const action = props.actions?.find(a => a.id === actionId)
if (!action || !action.decision_ids) return []
const ids = new Set(action.decision_ids)
return (props.decisions || []).filter(d => ids.has(d.id))
})
const actionId = form.default_action_id;
if (!actionId) return [];
const action = props.actions?.find((a) => a.id === actionId);
if (!action || !action.decision_ids) return [];
const ids = new Set(action.decision_ids);
return (props.decisions || []).filter((d) => ids.has(d.id));
});
watch(() => form.default_action_id, (newVal) => {
if (!newVal) {
form.default_decision_id = null
} else {
// If current decision not in filtered list, clear it
const ids = new Set((filteredDecisions.value || []).map(d => d.id))
if (!ids.has(form.default_decision_id)) {
form.default_decision_id = null
watch(
() => form.default_action_id,
(newVal) => {
if (!newVal) {
form.default_decision_id = null;
} else {
// If current decision not in filtered list, clear it
const ids = new Set((filteredDecisions.value || []).map((d) => d.id));
if (!ids.has(form.default_decision_id)) {
form.default_decision_id = null;
}
}
}
})
);
const submit = () => {
form.put(route('settings.payment.update'), {
form.put(route("settings.payment.update"), {
preserveScroll: true,
})
}
});
};
</script>
<template>
<AppLayout title="Nastavitve plačil">
<template #header></template>
<div class="max-w-3xl mx-auto p-6">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h1 class="text-xl font-semibold text-gray-900">Nastavitve plačil</h1>
<div class="mt-6 grid gap-6">
<div>
<label class="block text-sm text-gray-700 mb-1">Privzeta valuta</label>
<input type="text" maxlength="3" v-model="form.default_currency" class="w-40 rounded border-gray-300" />
<div v-if="form.errors.default_currency" class="text-sm text-red-600 mt-1">{{ form.errors.default_currency }}</div>
</div>
<div>
<label class="inline-flex items-center gap-2">
<input type="checkbox" v-model="form.create_activity_on_payment" />
<span class="text-sm text-gray-700">Ustvari aktivnost ob dodanem plačilu</span>
</label>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm text-gray-700 mb-1">Privzeto dejanje</label>
<select v-model="form.default_action_id" class="w-full rounded border-gray-300">
<option :value="null"> Brez </option>
<option v-for="a in actions" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
<div v-if="form.errors.default_action_id" class="text-sm text-red-600 mt-1">{{ form.errors.default_action_id }}</div>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<Wallet :size="18" />
<CardTitle class="uppercase">Nastavitve plačil</CardTitle>
</div>
</template>
<div class="space-y-6 p-4 border-t">
<div>
<label class="block text-sm text-gray-700 mb-1">Privzeta odločitev</label>
<select v-model="form.default_decision_id" class="w-full rounded border-gray-300" :disabled="!form.default_action_id">
<option :value="null"> Najprej izberite dejanje </option>
<option v-for="d in filteredDecisions" :key="d.id" :value="d.id">{{ d.name }}</option>
</select>
<div v-if="form.errors.default_decision_id" class="text-sm text-red-600 mt-1">{{ form.errors.default_decision_id }}</div>
<InputLabel for="currency">Privzeta valuta</InputLabel>
<Input
id="currency"
v-model="form.default_currency"
maxlength="3"
class="w-40"
/>
<div v-if="form.errors.default_currency" class="text-sm text-red-600 mt-1">
{{ form.errors.default_currency }}
</div>
</div>
</div>
<div>
<label class="block text-sm text-gray-700 mb-1">Predloga opombe aktivnosti</label>
<input type="text" v-model="form.activity_note_template" class="w-full rounded border-gray-300" />
<p class="text-xs text-gray-500 mt-1">Podprti žetoni: {amount}, {currency}</p>
<div v-if="form.errors.activity_note_template" class="text-sm text-red-600 mt-1">{{ form.errors.activity_note_template }}</div>
</div>
<div class="flex items-center gap-2">
<Checkbox id="create-activity" v-model="form.create_activity_on_payment" />
<InputLabel for="create-activity" class="text-sm font-normal cursor-pointer">
Ustvari aktivnost ob dodanem plačilu
</InputLabel>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<InputLabel for="default-action">Privzeto dejanje</InputLabel>
<Select v-model="form.default_action_id">
<SelectTrigger id="default-action">
<SelectValue placeholder="— Brez —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Brez </SelectItem>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">{{
a.name
}}</SelectItem>
</SelectContent>
</Select>
<div v-if="form.errors.default_action_id" class="text-sm text-red-600 mt-1">
{{ form.errors.default_action_id }}
</div>
</div>
<div>
<InputLabel for="default-decision">Privzeta odločitev</InputLabel>
<Select
v-model="form.default_decision_id"
:disabled="!form.default_action_id"
>
<SelectTrigger id="default-decision">
<SelectValue placeholder="— Najprej izberite dejanje —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Najprej izberite dejanje </SelectItem>
<SelectItem v-for="d in filteredDecisions" :key="d.id" :value="d.id">{{
d.name
}}</SelectItem>
</SelectContent>
</Select>
<div
v-if="form.errors.default_decision_id"
class="text-sm text-red-600 mt-1"
>
{{ form.errors.default_decision_id }}
</div>
</div>
</div>
<div>
<InputLabel for="note-template">Predloga opombe aktivnosti</InputLabel>
<Input id="note-template" v-model="form.activity_note_template" />
<p class="text-xs text-gray-500 mt-1">Podprti žetoni: {amount}, {currency}</p>
<div
v-if="form.errors.activity_note_template"
class="text-sm text-red-600 mt-1"
>
{{ form.errors.activity_note_template }}
</div>
</div>
<div class="flex justify-end gap-2">
<button type="button" class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300" @click="form.reset()">Ponastavi</button>
<button type="button" class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700" :disabled="form.processing" @click="submit">Shrani</button>
<Button variant="outline" @click="form.reset()">Ponastavi</Button>
<Button @click="submit" :disabled="form.processing">Shrani</Button>
</div>
</div>
</div>
</AppCard>
</div>
</AppLayout>
</template>
@@ -0,0 +1,134 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/Components/ui/tabs";
import { Badge } from "@/Components/ui/badge";
import { Link } from "@inertiajs/vue3";
import { ArrowLeft, BarChart3, Database, Columns, Filter, Code, ArrowUpDown } from "lucide-vue-next";
import EntitiesSection from "./Partials/EntitiesSection.vue";
import ColumnsSection from "./Partials/ColumnsSection.vue";
import FiltersSection from "./Partials/FiltersSection.vue";
import ConditionsSection from "./Partials/ConditionsSection.vue";
import OrdersSection from "./Partials/OrdersSection.vue";
const props = defineProps({
report: Object,
});
</script>
<template>
<AppLayout :title="`Edit Report: ${report.name}`">
<template #header>
<div class="flex items-center gap-4">
<Link :href="route('settings.reports.index')">
<Button variant="ghost" size="icon">
<ArrowLeft class="h-5 w-5" />
</Button>
</Link>
<div>
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Edit Report: {{ report.name }}
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
Configure entities, columns, filters, and conditions
</p>
</div>
</div>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<!-- Report Info Header -->
<Card class="mb-6">
<CardHeader>
<div class="flex items-start justify-between">
<div class="flex items-center gap-3">
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<BarChart3 class="h-6 w-6 text-primary" />
</div>
<div>
<CardTitle>{{ report.name }}</CardTitle>
<CardDescription class="mt-1">
{{ report.description || "No description" }}
</CardDescription>
</div>
</div>
<div class="flex gap-2">
<Badge v-if="!report.enabled" variant="secondary">Disabled</Badge>
<Badge v-if="report.category" variant="outline">{{ report.category }}</Badge>
</div>
</div>
</CardHeader>
<CardContent>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<span class="text-gray-500 dark:text-gray-400">Slug:</span>
<span class="ml-2 font-mono text-xs">{{ report.slug }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Order:</span>
<span class="ml-2">{{ report.order }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Status:</span>
<span class="ml-2">{{ report.enabled ? "Enabled" : "Disabled" }}</span>
</div>
</div>
</CardContent>
</Card>
<!-- Configuration Tabs -->
<Tabs default-value="entities" class="space-y-6">
<TabsList class="grid w-full grid-cols-5">
<TabsTrigger value="entities" class="flex items-center gap-2">
<Database class="h-4 w-4" />
Entities
<Badge variant="secondary" class="ml-1">{{ report.entities?.length || 0 }}</Badge>
</TabsTrigger>
<TabsTrigger value="columns" class="flex items-center gap-2">
<Columns class="h-4 w-4" />
Columns
<Badge variant="secondary" class="ml-1">{{ report.columns?.length || 0 }}</Badge>
</TabsTrigger>
<TabsTrigger value="filters" class="flex items-center gap-2">
<Filter class="h-4 w-4" />
Filters
<Badge variant="secondary" class="ml-1">{{ report.filters?.length || 0 }}</Badge>
</TabsTrigger>
<TabsTrigger value="conditions" class="flex items-center gap-2">
<Code class="h-4 w-4" />
Conditions
<Badge variant="secondary" class="ml-1">{{ report.conditions?.length || 0 }}</Badge>
</TabsTrigger>
<TabsTrigger value="orders" class="flex items-center gap-2">
<ArrowUpDown class="h-4 w-4" />
Orders
<Badge variant="secondary" class="ml-1">{{ report.orders?.length || 0 }}</Badge>
</TabsTrigger>
</TabsList>
<TabsContent value="entities">
<EntitiesSection :report="report" :entities="report.entities || []" />
</TabsContent>
<TabsContent value="columns">
<ColumnsSection :report="report" :columns="report.columns || []" />
</TabsContent>
<TabsContent value="filters">
<FiltersSection :report="report" :filters="report.filters || []" />
</TabsContent>
<TabsContent value="conditions">
<ConditionsSection :report="report" :conditions="report.conditions || []" />
</TabsContent>
<TabsContent value="orders">
<OrdersSection :report="report" :orders="report.orders || []" />
</TabsContent>
</Tabs>
</div>
</div>
</AppLayout>
</template>
@@ -0,0 +1,359 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/Components/ui/dropdown-menu";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Textarea } from "@/Components/ui/textarea";
import { Checkbox } from "@/Components/ui/checkbox";
import { Badge } from "@/Components/ui/badge";
import { useForm, router, Link } from "@inertiajs/vue3";
import { ref } from "vue";
import { BarChart3, MoreHorizontal, Pencil, Trash, Power, PowerOff, Plus, Database } from "lucide-vue-next";
const props = defineProps({
reports: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingReport = ref(null);
const createForm = useForm({
slug: "",
name: "",
description: "",
category: "",
enabled: true,
order: 0,
});
const editForm = useForm({
slug: "",
name: "",
description: "",
category: "",
enabled: true,
order: 0,
});
function openCreateDialog() {
createForm.reset();
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.store"), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(report) {
editingReport.value = report;
editForm.slug = report.slug;
editForm.name = report.name;
editForm.description = report.description || "";
editForm.category = report.category || "";
editForm.enabled = report.enabled;
editForm.order = report.order;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.update", editingReport.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingReport.value = null;
},
});
}
function toggleEnabled(report) {
router.post(
route("settings.reports.toggle", report.id),
{},
{
preserveScroll: true,
}
);
}
function deleteReport(report) {
if (confirm(`Are you sure you want to delete "${report.name}"?`)) {
router.delete(route("settings.reports.destroy", report.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<AppLayout title="Reports Settings">
<template #header>
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
Reports Settings
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<AppCard>
<template #icon>
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-primary/10">
<BarChart3 class="h-6 w-6 text-primary" />
</div>
</template>
<template #header>
<CardTitle>Database Reports</CardTitle>
<CardDescription>
Manage configurable reports with dynamic queries and filters
</CardDescription>
</template>
<template #headerActions>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Create Report
</Button>
</template>
<div class="space-y-4">
<div v-if="reports.length === 0" class="text-center py-8 text-gray-500">
No reports configured yet. Create your first report to get started.
</div>
<Card v-for="report in reports" :key="report.id" class="overflow-hidden">
<CardHeader class="bg-gray-50 dark:bg-gray-800/50">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<CardTitle class="text-base">{{ report.name }}</CardTitle>
<Badge v-if="!report.enabled" variant="secondary">Disabled</Badge>
<Badge v-if="report.category" variant="outline">{{ report.category }}</Badge>
</div>
<CardDescription class="mt-1">
{{ report.description || "No description" }}
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEditDialog(report)">
<Pencil class="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem as-child>
<Link :href="route('settings.reports.edit', report.id)" class="flex items-center cursor-pointer">
<Database class="mr-2 h-4 w-4" />
Configure Details
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="toggleEnabled(report)">
<Power v-if="report.enabled" class="mr-2 h-4 w-4" />
<PowerOff v-else class="mr-2 h-4 w-4" />
{{ report.enabled ? "Disable" : "Enable" }}
</DropdownMenuItem>
<DropdownMenuItem @click="deleteReport(report)" class="text-destructive">
<Trash class="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent class="pt-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500 dark:text-gray-400">Slug:</span>
<span class="ml-2 font-mono text-xs">{{ report.slug }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Order:</span>
<span class="ml-2">{{ report.order }}</span>
</div>
</div>
</CardContent>
</Card>
</div>
</AppCard>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Report</DialogTitle>
<DialogDescription>
Create a new database-driven report configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="space-y-2">
<Label for="create-slug">Slug *</Label>
<Input
id="create-slug"
v-model="createForm.slug"
placeholder="active-contracts"
required
/>
<p class="text-xs text-gray-500">Unique identifier for the report</p>
</div>
<div class="space-y-2">
<Label for="create-name">Name *</Label>
<Input
id="create-name"
v-model="createForm.name"
placeholder="Active Contracts"
required
/>
</div>
<div class="space-y-2">
<Label for="create-description">Description</Label>
<Textarea
id="create-description"
v-model="createForm.description"
placeholder="Report description..."
rows="3"
/>
</div>
<div class="space-y-2">
<Label for="create-category">Category</Label>
<Input
id="create-category"
v-model="createForm.category"
placeholder="contracts, activities, financial..."
/>
</div>
<div class="space-y-2">
<Label for="create-order">Display Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
placeholder="0"
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="create-enabled"
v-model="createForm.enabled"
/>
<Label for="create-enabled" class="cursor-pointer">
Enabled
</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Create Report
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Report</DialogTitle>
<DialogDescription>
Update report configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="space-y-2">
<Label for="edit-slug">Slug *</Label>
<Input
id="edit-slug"
v-model="editForm.slug"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-name">Name *</Label>
<Input
id="edit-name"
v-model="editForm.name"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-description">Description</Label>
<Textarea
id="edit-description"
v-model="editForm.description"
rows="3"
/>
</div>
<div class="space-y-2">
<Label for="edit-category">Category</Label>
<Input
id="edit-category"
v-model="editForm.category"
/>
</div>
<div class="space-y-2">
<Label for="edit-order">Display Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="edit-enabled"
v-model="editForm.enabled"
/>
<Label for="edit-enabled" class="cursor-pointer">
Enabled
</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Report
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</div>
</AppLayout>
</template>
@@ -0,0 +1,354 @@
<script setup>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Badge } from "@/Components/ui/badge";
import { Textarea } from "@/Components/ui/textarea";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Plus, Pencil, Trash, Eye, EyeOff } from "lucide-vue-next";
const props = defineProps({
report: Object,
columns: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingColumn = ref(null);
const createForm = useForm({
key: "",
label: "",
type: "string",
expression: "",
sortable: true,
visible: true,
order: 0,
format_options: null,
});
const editForm = useForm({
key: "",
label: "",
type: "string",
expression: "",
sortable: true,
visible: true,
order: 0,
format_options: null,
});
function openCreateDialog() {
createForm.reset();
createForm.order = props.columns.length;
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.columns.store", props.report.id), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(column) {
editingColumn.value = column;
editForm.key = column.key;
editForm.label = column.label;
editForm.type = column.type;
editForm.expression = column.expression;
editForm.sortable = column.sortable;
editForm.visible = column.visible;
editForm.order = column.order;
editForm.format_options = column.format_options;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.columns.update", editingColumn.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingColumn.value = null;
},
});
}
function deleteColumn(column) {
if (confirm("Are you sure you want to delete this column?")) {
router.delete(route("settings.reports.columns.destroy", column.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div>
<CardTitle>Report Columns</CardTitle>
<CardDescription>
Define which columns to select and display in the report
</CardDescription>
</div>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Add Column
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="columns.length === 0" class="text-center py-8 text-gray-500">
No columns configured. Add columns to display in the report.
</div>
<div v-else class="space-y-3">
<div
v-for="column in columns"
:key="column.id"
class="flex items-start justify-between rounded-lg border p-4"
>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Eye v-if="column.visible" class="h-4 w-4 text-green-600" />
<EyeOff v-else class="h-4 w-4 text-gray-400" />
<span class="font-semibold">{{ column.label }}</span>
<Badge variant="outline">{{ column.type }}</Badge>
<Badge v-if="column.sortable" variant="secondary">sortable</Badge>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono mb-1">
{{ column.key }}
</div>
<div class="text-sm text-gray-500 font-mono">
{{ column.expression }}
</div>
<div class="text-xs text-gray-500 mt-1">Order: {{ column.order }}</div>
</div>
<div class="flex gap-2">
<Button variant="ghost" size="icon" @click="openEditDialog(column)">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="deleteColumn(column)">
<Trash class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Column</DialogTitle>
<DialogDescription>
Add a new column to the report output
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="create-key">Key *</Label>
<Input
id="create-key"
v-model="createForm.key"
placeholder="contract_reference"
required
/>
</div>
<div class="space-y-2">
<Label for="create-label">Label *</Label>
<Input
id="create-label"
v-model="createForm.label"
placeholder="Contract Reference"
required
/>
</div>
</div>
<div class="space-y-2">
<Label for="create-type">Type *</Label>
<Select v-model="createForm.type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="currency">currency</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-expression">SQL Expression *</Label>
<Textarea
id="create-expression"
v-model="createForm.expression"
placeholder="contracts.reference"
rows="2"
required
/>
<p class="text-xs text-gray-500">SQL expression or column path</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center space-x-2">
<Checkbox
id="create-sortable"
v-model="createForm.sortable"
/>
<Label for="create-sortable" class="cursor-pointer">
Sortable
</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="create-visible"
v-model="createForm.visible"
/>
<Label for="create-visible" class="cursor-pointer">
Visible
</Label>
</div>
</div>
<div class="space-y-2">
<Label for="create-order">Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Add Column
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Column</DialogTitle>
<DialogDescription>
Update column configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-key">Key *</Label>
<Input
id="edit-key"
v-model="editForm.key"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-label">Label *</Label>
<Input
id="edit-label"
v-model="editForm.label"
required
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-type">Type *</Label>
<Select v-model="editForm.type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="currency">currency</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-expression">SQL Expression *</Label>
<Textarea
id="edit-expression"
v-model="editForm.expression"
rows="2"
required
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center space-x-2">
<Checkbox
id="edit-sortable"
v-model="editForm.sortable"
/>
<Label for="edit-sortable" class="cursor-pointer">
Sortable
</Label>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="edit-visible"
v-model="editForm.visible"
/>
<Label for="edit-visible" class="cursor-pointer">
Visible
</Label>
</div>
</div>
<div class="space-y-2">
<Label for="edit-order">Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Column
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,418 @@
<script setup>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Badge } from "@/Components/ui/badge";
import { Textarea } from "@/Components/ui/textarea";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Plus, Pencil, Trash } from "lucide-vue-next";
const props = defineProps({
report: Object,
conditions: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingCondition = ref(null);
const createForm = useForm({
column: "",
operator: "=",
value_type: "static",
value: "",
filter_key: "",
logical_operator: "AND",
group_id: 1,
order: 0,
enabled: true,
});
const editForm = useForm({
column: "",
operator: "=",
value_type: "static",
value: "",
filter_key: "",
logical_operator: "AND",
group_id: 1,
order: 0,
enabled: true,
});
function openCreateDialog() {
createForm.reset();
createForm.order = props.conditions.length;
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.conditions.store", props.report.id), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(condition) {
editingCondition.value = condition;
editForm.column = condition.column;
editForm.operator = condition.operator;
editForm.value_type = condition.value_type;
editForm.value = condition.value || "";
editForm.filter_key = condition.filter_key || "";
editForm.logical_operator = condition.logical_operator;
editForm.group_id = condition.group_id;
editForm.order = condition.order;
editForm.enabled = condition.enabled;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.conditions.update", editingCondition.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingCondition.value = null;
},
});
}
function deleteCondition(condition) {
if (confirm("Are you sure you want to delete this condition?")) {
router.delete(route("settings.reports.conditions.destroy", condition.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div>
<CardTitle>WHERE Conditions</CardTitle>
<CardDescription>
Define WHERE clause rules for filtering data
</CardDescription>
</div>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Add Condition
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="conditions.length === 0" class="text-center py-8 text-gray-500">
No conditions configured. Add WHERE conditions to filter query results.
</div>
<div v-else class="space-y-3">
<div
v-for="condition in conditions"
:key="condition.id"
class="flex items-start justify-between rounded-lg border p-4"
:class="{ 'opacity-50': !condition.enabled }"
>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Badge :variant="condition.logical_operator === 'AND' ? 'default' : 'secondary'">
{{ condition.logical_operator }}
</Badge>
<Badge variant="outline">Group {{ condition.group_id || 0 }}</Badge>
<Badge v-if="!condition.enabled" variant="secondary">disabled</Badge>
</div>
<div class="text-sm font-mono mb-1">
{{ condition.column }} {{ condition.operator }}
<span v-if="condition.value_type === 'static'" class="text-blue-600">"{{ condition.value }}"</span>
<span v-else-if="condition.value_type === 'filter'" class="text-green-600">filter({{ condition.filter_key }})</span>
<span v-else class="text-purple-600">{{ condition.value }}</span>
</div>
<div class="text-xs text-gray-500">
Type: {{ condition.value_type }} | Order: {{ condition.order }}
</div>
</div>
<div class="flex gap-2">
<Button variant="ghost" size="icon" @click="openEditDialog(condition)">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="deleteCondition(condition)">
<Trash class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Condition</DialogTitle>
<DialogDescription>
Add a new WHERE clause condition
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="space-y-2">
<Label for="create-column">Column *</Label>
<Input
id="create-column"
v-model="createForm.column"
placeholder="contracts.start_date"
required
/>
</div>
<div class="space-y-2">
<Label for="create-operator">Operator *</Label>
<Select v-model="createForm.operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">></SelectItem>
<SelectItem value="<"><</SelectItem>
<SelectItem value=">=">>=</SelectItem>
<SelectItem value="<="><=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem>
<SelectItem value="IN">IN</SelectItem>
<SelectItem value="NOT IN">NOT IN</SelectItem>
<SelectItem value="BETWEEN">BETWEEN</SelectItem>
<SelectItem value="IS NULL">IS NULL</SelectItem>
<SelectItem value="IS NOT NULL">IS NOT NULL</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-value-type">Value Type *</Label>
<Select v-model="createForm.value_type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="static">static (hardcoded value)</SelectItem>
<SelectItem value="filter">filter (from user input)</SelectItem>
<SelectItem value="expression">expression (SQL expression)</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="createForm.value_type === 'filter'" class="space-y-2">
<Label for="create-filter-key">Filter Key *</Label>
<Input
id="create-filter-key"
v-model="createForm.filter_key"
placeholder="client_uuid"
/>
</div>
<div v-else class="space-y-2">
<Label for="create-value">Value</Label>
<Textarea
id="create-value"
v-model="createForm.value"
placeholder="Value or expression..."
rows="2"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="create-logical">Logical Operator *</Label>
<Select v-model="createForm.logical_operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-group">Group ID</Label>
<Input
id="create-group"
v-model.number="createForm.group_id"
type="number"
placeholder="1"
/>
</div>
</div>
<div class="space-y-2">
<Label for="create-order">Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="create-enabled"
v-model="createForm.enabled"
/>
<Label for="create-enabled" class="cursor-pointer">
Enabled
</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Add Condition
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Condition</DialogTitle>
<DialogDescription>
Update condition configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="space-y-2">
<Label for="edit-column">Column *</Label>
<Input
id="edit-column"
v-model="editForm.column"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-operator">Operator *</Label>
<Select v-model="editForm.operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">></SelectItem>
<SelectItem value="<"><</SelectItem>
<SelectItem value=">=">>=</SelectItem>
<SelectItem value="<="><=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem>
<SelectItem value="IN">IN</SelectItem>
<SelectItem value="NOT IN">NOT IN</SelectItem>
<SelectItem value="BETWEEN">BETWEEN</SelectItem>
<SelectItem value="IS NULL">IS NULL</SelectItem>
<SelectItem value="IS NOT NULL">IS NOT NULL</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-value-type">Value Type *</Label>
<Select v-model="editForm.value_type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="static">static (hardcoded value)</SelectItem>
<SelectItem value="filter">filter (from user input)</SelectItem>
<SelectItem value="expression">expression (SQL expression)</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="editForm.value_type === 'filter'" class="space-y-2">
<Label for="edit-filter-key">Filter Key *</Label>
<Input
id="edit-filter-key"
v-model="editForm.filter_key"
/>
</div>
<div v-else class="space-y-2">
<Label for="edit-value">Value</Label>
<Textarea
id="edit-value"
v-model="editForm.value"
rows="2"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-logical">Logical Operator *</Label>
<Select v-model="editForm.logical_operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-group">Group ID</Label>
<Input
id="edit-group"
v-model.number="editForm.group_id"
type="number"
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-order">Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="edit-enabled"
v-model="editForm.enabled"
/>
<Label for="edit-enabled" class="cursor-pointer">
Enabled
</Label>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Condition
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,344 @@
<script setup>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Plus, Pencil, Trash } from "lucide-vue-next";
const props = defineProps({
report: Object,
entities: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingEntity = ref(null);
const createForm = useForm({
model_class: "",
alias: "",
join_type: "base",
join_first: "",
join_operator: "=",
join_second: "",
order: 0,
});
const editForm = useForm({
model_class: "",
alias: "",
join_type: "base",
join_first: "",
join_operator: "=",
join_second: "",
order: 0,
});
function openCreateDialog() {
createForm.reset();
createForm.order = props.entities.length;
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.entities.store", props.report.id), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(entity) {
editingEntity.value = entity;
editForm.model_class = entity.model_class;
editForm.alias = entity.alias || "";
editForm.join_type = entity.join_type;
editForm.join_first = entity.join_first || "";
editForm.join_operator = entity.join_operator || "=";
editForm.join_second = entity.join_second || "";
editForm.order = entity.order;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.entities.update", editingEntity.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingEntity.value = null;
},
});
}
function deleteEntity(entity) {
if (confirm("Are you sure you want to delete this entity?")) {
router.delete(route("settings.reports.entities.destroy", entity.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div>
<CardTitle>Database Entities & Joins</CardTitle>
<CardDescription>
Configure which models/tables to query and how to join them
</CardDescription>
</div>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Add Entity
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="entities.length === 0" class="text-center py-8 text-gray-500">
No entities configured. Add a base entity to get started.
</div>
<div v-else class="space-y-3">
<div
v-for="entity in entities"
:key="entity.id"
class="flex items-start justify-between rounded-lg border p-4"
>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<Badge :variant="entity.join_type === 'base' ? 'default' : 'secondary'">
{{ entity.join_type }}
</Badge>
<span class="font-mono text-sm">{{ entity.model_class }}</span>
<Badge v-if="entity.alias" variant="outline">as {{ entity.alias }}</Badge>
</div>
<div v-if="entity.join_type !== 'base'" class="text-sm text-gray-600 dark:text-gray-400">
{{ entity.join_first }} {{ entity.join_operator }} {{ entity.join_second }}
</div>
<div class="text-xs text-gray-500 mt-1">Order: {{ entity.order }}</div>
</div>
<div class="flex gap-2">
<Button variant="ghost" size="icon" @click="openEditDialog(entity)">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="deleteEntity(entity)">
<Trash class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Entity</DialogTitle>
<DialogDescription>
Add a model/table to the report query
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="space-y-2">
<Label for="create-model">Model Class *</Label>
<Input
id="create-model"
v-model="createForm.model_class"
placeholder="App\Models\Contract"
required
/>
</div>
<div class="space-y-2">
<Label for="create-alias">Alias (optional)</Label>
<Input
id="create-alias"
v-model="createForm.alias"
placeholder="contracts"
/>
</div>
<div class="space-y-2">
<Label for="create-join-type">Join Type *</Label>
<Select v-model="createForm.join_type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="base">base (no join)</SelectItem>
<SelectItem value="join">join (INNER JOIN)</SelectItem>
<SelectItem value="leftJoin">leftJoin (LEFT JOIN)</SelectItem>
<SelectItem value="rightJoin">rightJoin (RIGHT JOIN)</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="createForm.join_type !== 'base'" class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="create-join-first">Join First Column *</Label>
<Input
id="create-join-first"
v-model="createForm.join_first"
placeholder="contracts.client_id"
/>
</div>
<div class="space-y-2">
<Label for="create-join-op">Operator *</Label>
<Select v-model="createForm.join_operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">></SelectItem>
<SelectItem value="<"><</SelectItem>
<SelectItem value=">=">>=</SelectItem>
<SelectItem value="<="><=</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-join-second">Join Second Column *</Label>
<Input
id="create-join-second"
v-model="createForm.join_second"
placeholder="clients.id"
/>
</div>
</div>
<div class="space-y-2">
<Label for="create-order">Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Add Entity
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Entity</DialogTitle>
<DialogDescription>
Update entity configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="space-y-2">
<Label for="edit-model">Model Class *</Label>
<Input
id="edit-model"
v-model="editForm.model_class"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-alias">Alias (optional)</Label>
<Input
id="edit-alias"
v-model="editForm.alias"
/>
</div>
<div class="space-y-2">
<Label for="edit-join-type">Join Type *</Label>
<Select v-model="editForm.join_type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="base">base (no join)</SelectItem>
<SelectItem value="join">join (INNER JOIN)</SelectItem>
<SelectItem value="leftJoin">leftJoin (LEFT JOIN)</SelectItem>
<SelectItem value="rightJoin">rightJoin (RIGHT JOIN)</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="editForm.join_type !== 'base'" class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="edit-join-first">Join First Column *</Label>
<Input
id="edit-join-first"
v-model="editForm.join_first"
/>
</div>
<div class="space-y-2">
<Label for="edit-join-op">Operator *</Label>
<Select v-model="editForm.join_operator">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">></SelectItem>
<SelectItem value="<"><</SelectItem>
<SelectItem value=">=">>=</SelectItem>
<SelectItem value="<="><=</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-join-second">Join Second Column *</Label>
<Input
id="edit-join-second"
v-model="editForm.join_second"
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-order">Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Entity
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,344 @@
<script setup>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { Badge } from "@/Components/ui/badge";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Plus, Pencil, Trash } from "lucide-vue-next";
const props = defineProps({
report: Object,
filters: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingFilter = ref(null);
const createForm = useForm({
key: "",
label: "",
type: "string",
nullable: true,
default_value: "",
data_source: "",
order: 0,
});
const editForm = useForm({
key: "",
label: "",
type: "string",
nullable: true,
default_value: "",
data_source: "",
order: 0,
});
function openCreateDialog() {
createForm.reset();
createForm.order = props.filters.length;
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.filters.store", props.report.id), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(filter) {
editingFilter.value = filter;
editForm.key = filter.key;
editForm.label = filter.label;
editForm.type = filter.type;
editForm.nullable = filter.nullable;
editForm.default_value = filter.default_value || "";
editForm.data_source = filter.data_source || "";
editForm.order = filter.order;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.filters.update", editingFilter.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingFilter.value = null;
},
});
}
function deleteFilter(filter) {
if (confirm("Are you sure you want to delete this filter?")) {
router.delete(route("settings.reports.filters.destroy", filter.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div>
<CardTitle>Report Filters</CardTitle>
<CardDescription>
Define input parameters that users can provide to filter the report
</CardDescription>
</div>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Add Filter
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="filters.length === 0" class="text-center py-8 text-gray-500">
No filters configured. Add filters to allow users to filter report results.
</div>
<div v-else class="space-y-3">
<div
v-for="filter in filters"
:key="filter.id"
class="flex items-start justify-between rounded-lg border p-4"
>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="font-semibold">{{ filter.label }}</span>
<Badge variant="outline">{{ filter.type }}</Badge>
<Badge v-if="filter.nullable" variant="secondary">nullable</Badge>
<Badge v-if="filter.data_source" variant="secondary">{{ filter.data_source }}</Badge>
</div>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{{ filter.key }}
</div>
<div v-if="filter.default_value" class="text-sm text-gray-500 mt-1">
Default: {{ filter.default_value }}
</div>
<div class="text-xs text-gray-500 mt-1">Order: {{ filter.order }}</div>
</div>
<div class="flex gap-2">
<Button variant="ghost" size="icon" @click="openEditDialog(filter)">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="deleteFilter(filter)">
<Trash class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Filter</DialogTitle>
<DialogDescription>
Add a new filter parameter for the report
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="create-key">Key *</Label>
<Input
id="create-key"
v-model="createForm.key"
placeholder="client_uuid"
required
/>
</div>
<div class="space-y-2">
<Label for="create-label">Label *</Label>
<Input
id="create-label"
v-model="createForm.label"
placeholder="Client"
required
/>
</div>
</div>
<div class="space-y-2">
<Label for="create-type">Type *</Label>
<Select v-model="createForm.type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="select">select</SelectItem>
<SelectItem value="select:client">select:client</SelectItem>
<SelectItem value="select:user">select:user</SelectItem>
<SelectItem value="multiselect">multiselect</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-data-source">Data Source (optional)</Label>
<Input
id="create-data-source"
v-model="createForm.data_source"
placeholder="clients, users, segments..."
/>
<p class="text-xs text-gray-500">For dynamic selects</p>
</div>
<div class="space-y-2">
<Label for="create-default">Default Value (optional)</Label>
<Input
id="create-default"
v-model="createForm.default_value"
placeholder="Default value..."
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="create-nullable"
v-model="createForm.nullable"
/>
<Label for="create-nullable" class="cursor-pointer">
Nullable (filter is optional)
</Label>
</div>
<div class="space-y-2">
<Label for="create-order">Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Add Filter
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent class="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Filter</DialogTitle>
<DialogDescription>
Update filter configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="edit-key">Key *</Label>
<Input
id="edit-key"
v-model="editForm.key"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-label">Label *</Label>
<Input
id="edit-label"
v-model="editForm.label"
required
/>
</div>
</div>
<div class="space-y-2">
<Label for="edit-type">Type *</Label>
<Select v-model="editForm.type">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="string">string</SelectItem>
<SelectItem value="date">date</SelectItem>
<SelectItem value="number">number</SelectItem>
<SelectItem value="boolean">boolean</SelectItem>
<SelectItem value="select">select</SelectItem>
<SelectItem value="select:client">select:client</SelectItem>
<SelectItem value="select:user">select:user</SelectItem>
<SelectItem value="multiselect">multiselect</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-data-source">Data Source (optional)</Label>
<Input
id="edit-data-source"
v-model="editForm.data_source"
/>
</div>
<div class="space-y-2">
<Label for="edit-default">Default Value (optional)</Label>
<Input
id="edit-default"
v-model="editForm.default_value"
/>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="edit-nullable"
v-model="editForm.nullable"
/>
<Label for="edit-nullable" class="cursor-pointer">
Nullable (filter is optional)
</Label>
</div>
<div class="space-y-2">
<Label for="edit-order">Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Filter
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
@@ -0,0 +1,238 @@
<script setup>
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Label } from "@/Components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Plus, Pencil, Trash, ArrowUp, ArrowDown } from "lucide-vue-next";
const props = defineProps({
report: Object,
orders: Array,
});
const showCreateDialog = ref(false);
const showEditDialog = ref(false);
const editingOrder = ref(null);
const createForm = useForm({
column: "",
direction: "ASC",
order: 0,
});
const editForm = useForm({
column: "",
direction: "ASC",
order: 0,
});
function openCreateDialog() {
createForm.reset();
createForm.order = props.orders.length;
showCreateDialog.value = true;
}
function submitCreate() {
createForm.post(route("settings.reports.orders.store", props.report.id), {
preserveScroll: true,
onSuccess: () => {
showCreateDialog.value = false;
createForm.reset();
},
});
}
function openEditDialog(order) {
editingOrder.value = order;
editForm.column = order.column;
editForm.direction = order.direction;
editForm.order = order.order;
showEditDialog.value = true;
}
function submitEdit() {
editForm.put(route("settings.reports.orders.update", editingOrder.value.id), {
preserveScroll: true,
onSuccess: () => {
showEditDialog.value = false;
editForm.reset();
editingOrder.value = null;
},
});
}
function deleteOrder(order) {
if (confirm("Are you sure you want to delete this order clause?")) {
router.delete(route("settings.reports.orders.destroy", order.id), {
preserveScroll: true,
});
}
}
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-start justify-between">
<div>
<CardTitle>ORDER BY Clauses</CardTitle>
<CardDescription>
Define how to sort the report results
</CardDescription>
</div>
<Button @click="openCreateDialog">
<Plus class="mr-2 h-4 w-4" />
Add Order
</Button>
</div>
</CardHeader>
<CardContent>
<div v-if="orders.length === 0" class="text-center py-8 text-gray-500">
No order clauses configured. Add ORDER BY clauses to sort results.
</div>
<div v-else class="space-y-3">
<div
v-for="orderClause in orders"
:key="orderClause.id"
class="flex items-start justify-between rounded-lg border p-4"
>
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<ArrowUp v-if="orderClause.direction === 'ASC'" class="h-4 w-4 text-blue-600" />
<ArrowDown v-else class="h-4 w-4 text-orange-600" />
<span class="font-mono text-sm">{{ orderClause.column }}</span>
<Badge :variant="orderClause.direction === 'ASC' ? 'default' : 'secondary'">
{{ orderClause.direction }}
</Badge>
</div>
<div class="text-xs text-gray-500">Order: {{ orderClause.order }}</div>
</div>
<div class="flex gap-2">
<Button variant="ghost" size="icon" @click="openEditDialog(orderClause)">
<Pencil class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" @click="deleteOrder(orderClause)">
<Trash class="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Create Dialog -->
<Dialog v-model:open="showCreateDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Add Order Clause</DialogTitle>
<DialogDescription>
Add a new ORDER BY clause
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitCreate" class="space-y-4">
<div class="space-y-2">
<Label for="create-column">Column *</Label>
<Input
id="create-column"
v-model="createForm.column"
placeholder="contracts.start_date"
required
/>
</div>
<div class="space-y-2">
<Label for="create-direction">Direction *</Label>
<Select v-model="createForm.direction">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ASC">ASC (Ascending)</SelectItem>
<SelectItem value="DESC">DESC (Descending)</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="create-order">Order</Label>
<Input
id="create-order"
v-model.number="createForm.order"
type="number"
/>
<p class="text-xs text-gray-500">Determines sort priority (lower = higher priority)</p>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showCreateDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="createForm.processing">
Add Order
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<!-- Edit Dialog -->
<Dialog v-model:open="showEditDialog">
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Order Clause</DialogTitle>
<DialogDescription>
Update order clause configuration
</DialogDescription>
</DialogHeader>
<form @submit.prevent="submitEdit" class="space-y-4">
<div class="space-y-2">
<Label for="edit-column">Column *</Label>
<Input
id="edit-column"
v-model="editForm.column"
required
/>
</div>
<div class="space-y-2">
<Label for="edit-direction">Direction *</Label>
<Select v-model="editForm.direction">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="ASC">ASC (Ascending)</SelectItem>
<SelectItem value="DESC">DESC (Descending)</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="edit-order">Order</Label>
<Input
id="edit-order"
v-model.number="editForm.order"
type="number"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="showEditDialog = false">
Cancel
</Button>
<Button type="submit" :disabled="editForm.processing">
Update Order
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</template>
+114 -103
View File
@@ -2,12 +2,17 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { LayoutGrid } from "lucide-vue-next";
import { Button } from "@/Components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/Components/ui/dialog";
import { Input } from "@/Components/ui/input";
import { Checkbox } from "@/Components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
import { Badge } from "@/Components/ui/badge";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import TextInput from "@/Components/TextInput.vue";
const props = defineProps({
segments: Array,
@@ -79,144 +84,150 @@ const update = () => {
<template #header></template>
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Segments</h2>
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<LayoutGrid :size="18" />
<CardTitle class="uppercase">Segments</CardTitle>
</div>
<Button @click="openCreate">+ New</Button>
</div>
</template>
<div class="border-t">
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead>Name</TableHead>
<TableHead>Description</TableHead>
<TableHead>Active</TableHead>
<TableHead>Exclude</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="s in segments" :key="s.id">
<TableCell>{{ s.id }}</TableCell>
<TableCell>{{ s.name }}</TableCell>
<TableCell>{{ s.description }}</TableCell>
<TableCell>
<Badge :variant="s.active ? 'default' : 'secondary'">
{{ s.active ? "Yes" : "No" }}
</Badge>
</TableCell>
<TableCell>
<Badge :variant="s.exclude ? 'default' : 'secondary'">
{{ s.exclude ? "Yes" : "No" }}
</Badge>
</TableCell>
<TableCell>
<Button variant="ghost" size="sm" @click="openEdit(s)">
Edit
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<CreateDialog
:show="showCreate"
title="New Segment"
confirm-text="Create"
:processing="createForm.processing"
@close="closeCreate"
@confirm="store"
>
<form @submit.prevent="store" class="space-y-4">
<!-- Create Dialog -->
<Dialog v-model:open="showCreate">
<DialogContent>
<DialogHeader>
<DialogTitle>New Segment</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="nameCreate" value="Name" />
<TextInput
<InputLabel for="nameCreate">Name</InputLabel>
<Input
id="nameCreate"
v-model="createForm.name"
type="text"
class="mt-1 block w-full"
/>
<InputError :message="createForm.errors.name" class="mt-1" />
</div>
<div>
<InputLabel for="descCreate" value="Description" />
<TextInput
<InputLabel for="descCreate">Description</InputLabel>
<Input
id="descCreate"
v-model="createForm.description"
type="text"
class="mt-1 block w-full"
/>
<InputError :message="createForm.errors.description" class="mt-1" />
</div>
<div class="flex items-center gap-2">
<input id="activeCreate" type="checkbox" v-model="createForm.active" />
<label for="activeCreate">Active</label>
<Checkbox id="activeCreate" v-model="createForm.active" />
<InputLabel for="activeCreate" class="text-sm font-normal cursor-pointer">
Active
</InputLabel>
</div>
<div class="flex items-center gap-2">
<input
id="excludeCreate"
type="checkbox"
v-model="createForm.exclude"
/>
<label for="excludeCreate">Exclude</label>
<Checkbox id="excludeCreate" v-model="createForm.exclude" />
<InputLabel for="excludeCreate" class="text-sm font-normal cursor-pointer">
Exclude
</InputLabel>
</div>
</form>
</CreateDialog>
</div>
<DialogFooter>
<Button variant="outline" @click="closeCreate">Cancel</Button>
<Button @click="store" :disabled="createForm.processing">Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<UpdateDialog
:show="showEdit"
title="Edit Segment"
confirm-text="Save"
:processing="editForm.processing"
@close="closeEdit"
@confirm="update"
>
<form @submit.prevent="update" class="space-y-4">
<!-- Edit Dialog -->
<Dialog v-model:open="showEdit">
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Segment</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="nameEdit" value="Name" />
<TextInput
<InputLabel for="nameEdit">Name</InputLabel>
<Input
id="nameEdit"
v-model="editForm.name"
type="text"
class="mt-1 block w-full"
/>
<InputError :message="editForm.errors.name" class="mt-1" />
</div>
<div>
<InputLabel for="descEdit" value="Description" />
<TextInput
<InputLabel for="descEdit">Description</InputLabel>
<Input
id="descEdit"
v-model="editForm.description"
type="text"
class="mt-1 block w-full"
/>
<InputError :message="editForm.errors.description" class="mt-1" />
</div>
<div class="flex items-center gap-2">
<input id="activeEdit" type="checkbox" v-model="editForm.active" />
<label for="activeEdit">Active</label>
<Checkbox id="activeEdit" v-model="editForm.active" />
<InputLabel for="activeEdit" class="text-sm font-normal cursor-pointer">
Active
</InputLabel>
</div>
<div class="flex items-center gap-2">
<input id="excludeEdit" type="checkbox" v-model="editForm.exclude" />
<label for="excludeEdit">Exclude</label>
<Checkbox id="excludeEdit" v-model="editForm.exclude" />
<InputLabel for="excludeEdit" class="text-sm font-normal cursor-pointer">
Exclude
</InputLabel>
</div>
</form>
</UpdateDialog>
<table class="min-w-full text-left text-sm">
<thead>
<tr class="border-b">
<th class="py-2 pr-4">ID</th>
<th class="py-2 pr-4">Name</th>
<th class="py-2 pr-4">Description</th>
<th class="py-2 pr-4">Active</th>
<th class="py-2 pr-4">Exclude</th>
<th class="py-2 pr-4">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="s in segments" :key="s.id" class="border-b last:border-0">
<td class="py-2 pr-4">{{ s.id }}</td>
<td class="py-2 pr-4">{{ s.name }}</td>
<td class="py-2 pr-4">{{ s.description }}</td>
<td class="py-2 pr-4">
<span class="inline-flex items-center gap-1">
<span
:class="s.active ? 'bg-green-500' : 'bg-gray-400'"
class="inline-block w-2 h-2 rounded-full"
></span>
{{ s.active ? "Yes" : "No" }}
</span>
</td>
<td class="py-2 pr-4">
<span class="inline-flex items-center gap-1">
<span
:class="s.exclude ? 'bg-green-500' : 'bg-gray-400'"
class="inline-block w-2 h-2 rounded-full"
></span>
{{ s.exclude ? "Yes" : "No" }}
</span>
</td>
<td class="py-2 pr-4">
<button
class="text-indigo-600 hover:text-indigo-800"
@click="openEdit(s)"
>
Edit
</button>
<!-- Delete intentionally skipped as requested -->
</td>
</tr>
</tbody>
</table>
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="closeEdit">Cancel</Button>
<Button @click="update" :disabled="editForm.processing">Save</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</AppCard>
</div>
</div>
</AppLayout>
+23 -8
View File
@@ -1,9 +1,12 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { ref } from "vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Workflow } from "lucide-vue-next";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/Components/ui/tabs";
import ActionTable from "../Partials/ActionTable.vue";
import DecisionTable from "../Partials/DecisionTable.vue";
import ActionTable from "./Partials/ActionTable.vue";
import DecisionTable from "./Partials/DecisionTable.vue";
const props = defineProps({
actions: Array,
@@ -21,11 +24,23 @@ const activeTab = ref("actions");
<template #header></template>
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<Tabs v-model="activeTab" class="w-full">
<TabsList class="w-full justify-start border-b rounded-none bg-transparent p-0">
<TabsTrigger value="actions" class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Akcije</TabsTrigger>
<TabsTrigger value="decisions" class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Odločitve</TabsTrigger>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<Workflow :size="18" />
<CardTitle class="uppercase">Workflow</CardTitle>
</div>
</template>
<Tabs v-model="activeTab" class="border-t">
<TabsList class="border-b w-full flex flex-row justify-baseline rounded-none">
<TabsTrigger value="actions">Akcije</TabsTrigger>
<TabsTrigger value="decisions">Odločitve</TabsTrigger>
</TabsList>
<TabsContent value="actions" class="mt-0">
<ActionTable
@@ -45,7 +60,7 @@ const activeTab = ref("actions");
/>
</TabsContent>
</Tabs>
</div>
</AppCard>
</div>
</div>
</AppLayout>
@@ -0,0 +1,406 @@
<script setup>
// flowbite-vue table imports removed; using DataTableClient
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/Components/ui/dialog";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogFooter,
} from "@/Components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import { computed, onMounted, ref } from "vue";
import { router, useForm } from "@inertiajs/vue3";
import InputLabel from "@/Components/InputLabel.vue";
import { Input } from "@/Components/ui/input";
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
import { Button } from "@/Components/ui/button";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
import AppPopover from "@/Components/app/ui/AppPopover.vue";
import { FilterIcon, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
const props = defineProps({
actions: Array,
decisions: Array,
segments: Array,
});
const drawerEdit = ref(false);
const drawerCreate = ref(false);
const showDelete = ref(false);
const toDelete = ref(null);
const search = ref("");
const selectedSegment = ref(null);
const selectOptions = computed(() =>
props.decisions.map((d) => ({
label: d.name,
value: d.id,
}))
);
const segmentOptions = computed(() =>
props.segments.map((d) => ({
label: d.name,
value: d.id,
}))
);
// DataTable state
const sort = ref({ key: null, direction: null });
const page = ref(1);
const pageSize = ref(25);
const columns = [
{ key: "id", label: "#", sortable: true, class: "w-16" },
{ key: "name", label: "Ime", sortable: true },
{ key: "color_tag", label: "Barva", sortable: false },
{ key: "segment", label: "Segment", sortable: false },
{ key: "decisions", label: "Odločitve", sortable: false, class: "w-32" },
];
const form = useForm({
id: 0,
name: "",
color_tag: "",
segment_id: null,
decisions: [],
});
const createForm = useForm({
name: "",
color_tag: "",
segment_id: null,
decisions: [],
});
const openEditDrawer = (item) => {
form.decisions = [];
form.id = item.id;
form.name = item.name;
form.color_tag = item.color_tag;
form.segment_id = item.segment ? item.segment.id : null;
drawerEdit.value = true;
// AppMultiSelect expects array of values
form.decisions = item.decisions.map((d) => d.id);
};
const closeEditDrawer = () => {
drawerEdit.value = false;
form.reset();
};
const openCreateDrawer = () => {
createForm.reset();
drawerCreate.value = true;
};
const closeCreateDrawer = () => {
drawerCreate.value = false;
createForm.reset();
};
const filtered = computed(() => {
const term = search.value?.toLowerCase() ?? "";
return (props.actions || []).filter((a) => {
const matchesSearch =
!term ||
a.name?.toLowerCase().includes(term) ||
a.color_tag?.toLowerCase().includes(term);
const matchesSegment =
!selectedSegment.value || a.segment?.id === selectedSegment.value;
return matchesSearch && matchesSegment;
});
});
const update = () => {
// Transform decisions from array of IDs to array of objects
const decisionsPayload = form.decisions
.map((id) => {
const decision = props.decisions.find((d) => d.id === Number(id) || d.id === id);
if (!decision) {
console.warn("Decision not found for id:", id);
return null;
}
return { id: decision.id, name: decision.name };
})
.filter(Boolean); // Remove null entries
form
.transform((data) => ({
...data,
decisions: decisionsPayload,
}))
.put(route("settings.actions.update", { id: form.id }), {
onSuccess: () => {
closeEditDrawer();
},
});
};
const store = () => {
// Transform decisions from array of IDs to array of objects
const decisionsPayload = createForm.decisions
.map((id) => {
const decision = props.decisions.find((d) => d.id === Number(id) || d.id === id);
if (!decision) {
console.warn("Decision not found for id:", id);
return null;
}
return { id: decision.id, name: decision.name };
})
.filter(Boolean); // Remove null entries
createForm
.transform((data) => ({
...data,
decisions: decisionsPayload,
}))
.post(route("settings.actions.store"), {
onSuccess: () => {
closeCreateDrawer();
},
});
};
const confirmDelete = (action) => {
toDelete.value = action;
showDelete.value = true;
};
const cancelDelete = () => {
toDelete.value = null;
showDelete.value = false;
};
const destroyAction = () => {
if (!toDelete.value) return;
router.delete(route("settings.actions.destroy", { id: toDelete.value.id }), {
preserveScroll: true,
onFinish: () => cancelDelete(),
});
};
</script>
<template>
<div class="p-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex gap-3 items-center">
<AppPopover align="start" side="bottom" content-class="w-80">
<template #trigger>
<Button variant="outline" size="sm">
<FilterIcon class="w-4 h-4 mr-2" />
Filtri
</Button>
</template>
<div class="p-1">
<div>
<InputLabel for="searchFilter" value="Iskanje" class="mb-1" />
<Input
id="searchFilter"
v-model="search"
placeholder="Iskanje..."
class="w-full"
/>
</div>
<div>
<InputLabel for="segmentFilter" value="Segment" class="mb-1" />
<AppCombobox
id="segmentFilter"
v-model="selectedSegment"
:items="segmentOptions"
placeholder="Filter po segmentu"
button-class="w-full"
/>
</div>
</div>
</AppPopover>
</div>
<Button @click="openCreateDrawer">+ Dodaj akcijo</Button>
</div>
<div>
<DataTableClient
:columns="columns"
:rows="filtered"
:sort="sort"
:search="''"
:page="page"
:pageSize="pageSize"
:showToolbar="false"
:showPagination="true"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
>
<template #cell-color_tag="{ row }">
<div class="flex items-center gap-2">
<span
v-if="row.color_tag"
class="inline-block h-4 w-4 rounded"
:style="{ backgroundColor: row.color_tag }"
></span>
<span>{{ row.color_tag || "" }}</span>
</div>
</template>
<template #cell-decisions="{ row }">
{{ row.decisions?.length ?? 0 }}
</template>
<template #cell-segment="{ row }">
<span>
{{ row.segment?.name || "" }}
</span>
</template>
<template #actions="{ row }">
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEditDrawer(row)">
<Pencil class="w-4 h-4 mr-2" />
Uredi
</DropdownMenuItem>
<DropdownMenuItem
:disabled="(row.activities_count ?? 0) > 0"
@click="confirmDelete(row)"
class="text-red-600 focus:text-red-600"
>
<Trash class="w-4 h-4 mr-2" />
Izbriši
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
</DataTableClient>
</div>
<Dialog v-model:open="drawerEdit">
<DialogContent>
<DialogHeader>
<DialogTitle>Spremeni akcijo</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="name">Ime</InputLabel>
<Input id="name" ref="nameInput" v-model="form.name" type="text" />
</div>
<div>
<InputLabel for="colorTag">Barva</InputLabel>
<div class="mt-1">
<InlineColorPicker v-model="form.color_tag" />
</div>
</div>
<div>
<InputLabel for="segmentEdit">Segment</InputLabel>
<AppCombobox
id="segmentEdit"
v-model="form.segment_id"
:items="segmentOptions"
placeholder="Izberi segment"
button-class="w-full"
/>
</div>
<div>
<InputLabel for="decisions">Odločitve</InputLabel>
<AppMultiSelect
id="decisions"
v-model="form.decisions"
:items="selectOptions"
placeholder="Dodaj odločitev"
content-class="p-0 w-full"
/>
</div>
<div v-if="form.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="closeEditDrawer">Cancel</Button>
<Button @click="update" :disabled="form.processing">Shrani</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog v-model:open="drawerCreate">
<DialogContent>
<DialogHeader>
<DialogTitle>Dodaj akcijo</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="nameCreate">Ime</InputLabel>
<Input id="nameCreate" v-model="createForm.name" type="text" />
</div>
<div>
<InputLabel for="colorTagCreate">Barva</InputLabel>
<div class="mt-1">
<InlineColorPicker v-model="createForm.color_tag" />
</div>
</div>
<div>
<InputLabel for="segmentCreate">Segment</InputLabel>
<AppCombobox
id="segmentCreate"
v-model="createForm.segment_id"
:items="segmentOptions"
placeholder="Izberi segment"
button-class="w-full"
/>
</div>
<div>
<InputLabel for="decisionsCreate">Odločitve</InputLabel>
<AppMultiSelect
id="decisionsCreate"
v-model="createForm.decisions"
:items="selectOptions"
placeholder="Dodaj odločitev"
content-class="p-0 w-full"
/>
</div>
<div v-if="createForm.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</div>
</div>
<DialogFooter>
<Button variant="outline" @click="closeCreateDrawer">Cancel</Button>
<Button @click="store" :disabled="createForm.processing">Dodaj</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog v-model:open="showDelete">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete action</AlertDialogTitle>
</AlertDialogHeader>
<div class="text-sm text-muted-foreground">
Are you sure you want to delete action "{{ toDelete?.name }}"? This cannot be
undone.
</div>
<AlertDialogFooter>
<Button variant="outline" @click="cancelDelete">Cancel</Button>
<Button variant="destructive" @click="destroyAction">Delete</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>
@@ -1,19 +1,46 @@
<script setup>
// flowbite-vue table imports removed; using DataTableClient
import { EditIcon, TrashBinIcon, DottedMenu } from "@/Utilities/Icons";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/Components/ui/dialog";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogFooter,
} from "@/Components/ui/alert-dialog";
import { computed, onMounted, ref, watch, nextTick } from "vue";
import { router, useForm } from "@inertiajs/vue3";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import Multiselect from "vue-multiselect";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import ActionMessage from "@/Components/ActionMessage.vue";
import { Input } from "@/Components/ui/input";
import { Checkbox } from "@/Components/ui/checkbox";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/Components/ui/select";
import AppCombobox from "@/Components/app/ui/AppCombobox.vue";
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
import { Button } from "@/Components/ui/button";
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
import InlineColorPicker from "@/Components/InlineColorPicker.vue";
import Dropdown from "@/Components/Dropdown.vue";
import AppPopover from "@/Components/app/ui/AppPopover.vue";
import { FilterIcon, Trash2, MoreHorizontal, Pencil, Trash } from "lucide-vue-next";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
const props = defineProps({
decisions: Array,
@@ -42,12 +69,12 @@ const sort = ref({ key: null, direction: null });
const page = ref(1);
const pageSize = ref(25);
const columns = [
{ key: "id", label: "#", sortable: true, class: "w-16" },
{ key: "id", label: "#", sortable: true },
{ key: "name", label: "Ime", sortable: true },
{ key: "color_tag", label: "Barva", sortable: false },
{ key: "events", label: "Dogodki", sortable: false, class: "w-40" },
{ key: "belongs", label: "Pripada akcijam", sortable: false, class: "w-40" },
{ key: "auto_mail", label: "Auto mail", sortable: false, class: "w-46" },
{ key: "events", label: "Dogodki", sortable: false },
{ key: "belongs", label: "Pripada akcijam", sortable: false },
{ key: "auto_mail", label: "Auto mail", sortable: false },
];
const form = useForm({
@@ -119,12 +146,7 @@ const openEditDrawer = (item) => {
});
drawerEdit.value = true;
item.actions.forEach((a) => {
form.actions.push({
name: a.name,
id: a.id,
});
});
form.actions = item.actions.map((a) => a.id);
};
const closeEditDrawer = () => {
@@ -145,8 +167,8 @@ const closeCreateDrawer = () => {
onMounted(() => {
props.actions.forEach((a) => {
actionOptions.value.push({
name: a.name,
id: a.id,
label: a.name,
value: a.id,
});
});
});
@@ -217,7 +239,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)));
const evIdSet = new Set((selectedEvents.value || []).map((e) => Number(e)));
return (props.decisions || []).filter((d) => {
const matchesSearch =
!term ||
@@ -241,7 +263,22 @@ const update = () => {
return;
}
form.put(route("settings.decisions.update", { id: form.id }), {
// Transform actions from array of IDs to array of objects
const actionsPayload = form.actions
.map(id => {
const action = props.actions.find(a => a.id === Number(id) || a.id === id);
if (!action) {
console.warn('Action not found for id:', id);
return null;
}
return { id: action.id, name: action.name };
})
.filter(Boolean); // Remove null entries
form.transform((data) => ({
...data,
actions: actionsPayload
})).put(route("settings.decisions.update", { id: form.id }), {
onSuccess: () => {
closeEditDrawer();
},
@@ -260,7 +297,22 @@ const store = () => {
return;
}
createForm.post(route("settings.decisions.store"), {
// Transform actions from array of IDs to array of objects
const actionsPayload = createForm.actions
.map(id => {
const action = props.actions.find(a => a.id === Number(id) || a.id === id);
if (!action) {
console.warn('Action not found for id:', id);
return null;
}
return { id: action.id, name: action.name };
})
.filter(Boolean); // Remove null entries
createForm.transform((data) => ({
...data,
actions: actionsPayload
})).post(route("settings.decisions.store"), {
onSuccess: () => {
closeCreateDrawer();
},
@@ -351,68 +403,65 @@ const destroyDecision = () => {
</script>
<template>
<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"
<div class="flex gap-3 items-center">
<AppPopover align="start" side="bottom" content-class="w-96">
<template #trigger>
<Button variant="outline" size="sm">
<FilterIcon class="w-4 h-4 mr-2" />
Filtri
</Button>
</template>
<div class="space-y-3 p-1">
<div>
<InputLabel for="searchFilter" value="Iskanje" class="mb-1" />
<Input
id="searchFilter"
v-model="search"
placeholder="Iskanje..."
class="w-full"
/>
</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"
</div>
<div>
<InputLabel for="templateFilter" value="Email predloga" class="mb-1" />
<Select v-model="selectedTemplateId">
<SelectTrigger id="templateFilter" class="w-full">
<SelectValue placeholder="Vse predloge" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null">Vse predloge</SelectItem>
<SelectItem v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<InputLabel for="eventsFilter" value="Dogodki" class="mb-1" />
<AppMultiSelect
id="eventsFilter"
v-model="selectedEvents"
:items="availableEvents.map((e) => ({ value: e.id, label: e.name }))"
placeholder="Filtriraj po dogodkih"
class="w-full"
/>
Samo auto mail
</label>
</div>
<div class="flex items-center gap-2">
<Checkbox id="onlyAutoMailFilter" v-model="onlyAutoMail" />
<InputLabel
for="onlyAutoMailFilter"
class="text-sm font-normal cursor-pointer"
>
Samo auto mail
</InputLabel>
</div>
</div>
</div>
</AppPopover>
</div>
<div class="flex-shrink-0">
<PrimaryButton @click="openCreateDrawer">+ Dodaj odločitev</PrimaryButton>
<div class="shrink-0">
<Button @click="openCreateDrawer">+ Dodaj odločitev</Button>
</div>
</div>
<div class="px-4 pb-4">
<div>
<DataTableClient
:columns="columns"
:rows="filtered"
@@ -421,6 +470,7 @@ const destroyDecision = () => {
:page="page"
:pageSize="pageSize"
:showToolbar="false"
:showPagination="true"
@update:sort="(v) => (sort = v)"
@update:page="(v) => (page = v)"
@update:pageSize="(v) => (pageSize = v)"
@@ -496,63 +546,61 @@ const destroyDecision = () => {
</div>
</template>
<template #actions="{ row }">
<button class="px-2" @click="openEditDrawer(row)">
<EditIcon size="md" css="text-gray-500" />
</button>
<button
class="px-2 disabled:opacity-40"
:disabled="(row.activities_count ?? 0) > 0"
@click="confirmDelete(row)"
>
<TrashBinIcon size="md" css="text-red-500" />
</button>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" size="icon">
<MoreHorizontal class="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="openEditDrawer(row)">
<Pencil class="w-4 h-4 mr-2" />
Uredi
</DropdownMenuItem>
<DropdownMenuItem
:disabled="(row.activities_count ?? 0) > 0"
@click="confirmDelete(row)"
class="text-red-600 focus:text-red-600"
>
<Trash class="w-4 h-4 mr-2" />
Izbriši
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</template>
</DataTableClient>
</div>
<UpdateDialog
:show="drawerEdit"
title="Spremeni odločitev"
confirm-text="Shrani"
:processing="form.processing"
:disabled="!eventsValidEdit"
@close="closeEditDrawer"
@confirm="update"
>
<form @submit.prevent="update">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Ime" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
autocomplete="name"
/>
<Dialog v-model:open="drawerEdit">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Spremeni odločitev</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="name">Ime</InputLabel>
<Input id="name" v-model="form.name" type="text" />
</div>
<div class="mt-4 flex items-center gap-2">
<input
id="autoMailEdit"
type="checkbox"
v-model="form.auto_mail"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<label for="autoMailEdit" class="text-sm">Samodejna pošta (auto mail)</label>
<Checkbox id="autoMailEdit" v-model="form.auto_mail" />
<InputLabel for="autoMailEdit" class="text-sm font-normal cursor-pointer">
Samodejna pošta (auto mail)
</InputLabel>
</div>
<div class="col-span-6 sm:col-span-4 mt-2">
<InputLabel for="emailTemplateEdit" value="Email predloga" />
<select
id="emailTemplateEdit"
v-model="form.email_template_id"
:disabled="!form.auto_mail"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<option :value="null"> Brez </option>
<option v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }}
</option>
</select>
<Select v-model="form.email_template_id" :disabled="!form.auto_mail">
<SelectTrigger id="emailTemplateEdit" class="w-full">
<SelectValue placeholder="— Brez —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Brez </SelectItem>
<SelectItem v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="form.email_template_id" class="text-xs text-gray-500 mt-1">
<span
v-if="
@@ -574,16 +622,11 @@ const destroyDecision = () => {
<div class="col-span-6 sm:col-span-4">
<InputLabel for="actionsSelect" value="Akcije" />
<multiselect
<AppMultiSelect
id="actionsSelect"
v-model="form.actions"
:options="actionOptions"
:multiple="true"
track-by="id"
:taggable="true"
:items="actionOptions"
placeholder="Dodaj akcijo"
:append-to-body="true"
label="name"
/>
</div>
@@ -595,43 +638,44 @@ const destroyDecision = () => {
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<InputLabel :for="`event-${idx}`" value="Dogodek" />
<select
:id="`event-${idx}`"
v-model.number="ev.id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
@change="onEventChange(ev)"
>
<option :value="null"> Izberi </option>
<option v-for="opt in availableEvents" :key="opt.id" :value="opt.id">
{{ opt.name || opt.key || `#${opt.id}` }}
</option>
</select>
<Select v-model="ev.id" @update:model-value="onEventChange(ev)">
<SelectTrigger :id="`event-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi </SelectItem>
<SelectItem
v-for="opt in availableEvents"
:key="opt.id"
:value="opt.id"
>
{{ opt.name || opt.key || `#${opt.id}` }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="w-36">
<InputLabel :for="`order-${idx}`" value="Vrstni red" />
<TextInput
<InputLabel :for="`order-${idx}`">Vrstni red</InputLabel>
<Input
:id="`order-${idx}`"
v-model.number="ev.run_order"
type="number"
class="w-full"
/>
</div>
<div class="flex items-end gap-2">
<div class="flex items-center gap-2 self-end">
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
v-model="ev.active"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.active" />
Aktivno
</label>
<button
type="button"
class="text-red-600 text-sm"
<Button
variant="ghost"
size="icon"
class="text-red-600 hover:text-red-700 hover:bg-red-50"
@click="form.events.splice(idx, 1)"
>
Odstrani
</button>
<Trash2 class="w-4 h-4" />
</Button>
</div>
</div>
<div class="mt-3">
@@ -639,16 +683,17 @@ const destroyDecision = () => {
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<InputLabel :for="`seg-${idx}`" value="Segment" />
<select
:id="`seg-${idx}`"
v-model.number="ev.config.segment_id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
<option :value="null"> Izberi segment </option>
<option v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
<Select v-model="ev.config.segment_id">
<SelectTrigger :id="`seg-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi segment —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi segment </SelectItem>
<SelectItem v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</SelectItem>
</SelectContent>
</Select>
<p
v-if="form.errors[`events.${idx}.config.segment_id`]"
class="text-xs text-red-600 mt-1"
@@ -658,11 +703,7 @@ const destroyDecision = () => {
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm mt-6">
<input
type="checkbox"
v-model="ev.config.deactivate_previous"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.config.deactivate_previous" />
Deaktiviraj prejšnje
</label>
</div>
@@ -672,16 +713,21 @@ const destroyDecision = () => {
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<InputLabel :for="`as-${idx}`" value="Archive setting" />
<select
:id="`as-${idx}`"
v-model.number="ev.config.archive_setting_id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
<option :value="null"> Izberi nastavitev </option>
<option v-for="a in archiveSettings" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
<Select v-model="ev.config.archive_setting_id">
<SelectTrigger :id="`as-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi nastavitev —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi nastavitev </SelectItem>
<SelectItem
v-for="a in archiveSettings"
:key="a.id"
:value="a.id"
>
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
<p
v-if="form.errors[`events.${idx}.config.archive_setting_id`]"
class="text-xs text-red-600 mt-1"
@@ -691,11 +737,7 @@ const destroyDecision = () => {
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm mt-6">
<input
type="checkbox"
v-model="ev.config.reactivate"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.config.reactivate" />
Reactivate namesto arhiva
</label>
</div>
@@ -725,65 +767,63 @@ const destroyDecision = () => {
</div>
</div>
<div>
<PrimaryButton
<Button
type="button"
variant="outline"
@click="form.events.push(defaultEventPayload())"
>+ Dodaj dogodek</PrimaryButton
>+ Dodaj dogodek</Button
>
</div>
</div>
</div>
<div v-if="form.recentlySuccessful" class="mt-6 text-sm text-green-600">
<div v-if="form.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</div>
</form>
</UpdateDialog>
</div>
<DialogFooter>
<Button variant="outline" @click="closeEditDrawer">Cancel</Button>
<Button @click="update" :disabled="form.processing || !eventsValidEdit"
>Shrani</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
<CreateDialog
:show="drawerCreate"
title="Dodaj odločitev"
confirm-text="Dodaj"
:processing="createForm.processing"
:disabled="!eventsValidCreate"
@close="closeCreateDrawer"
@confirm="store"
>
<form @submit.prevent="store">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="nameCreate" value="Ime" />
<TextInput
id="nameCreate"
v-model="createForm.name"
type="text"
class="mt-1 block w-full"
autocomplete="name"
/>
<Dialog v-model:open="drawerCreate">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Dodaj odločitev</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div>
<InputLabel for="nameCreate">Ime</InputLabel>
<Input id="nameCreate" v-model="createForm.name" type="text" />
</div>
<div class="mt-4 flex items-center gap-2">
<input
id="autoMailCreate"
type="checkbox"
v-model="createForm.auto_mail"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<label for="autoMailCreate" class="text-sm">Samodejna pošta (auto mail)</label>
<Checkbox id="autoMailCreate" v-model="createForm.auto_mail" />
<InputLabel for="autoMailCreate" class="text-sm font-normal cursor-pointer">
Samodejna pošta (auto mail)
</InputLabel>
</div>
<div class="col-span-6 sm:col-span-4 mt-2">
<InputLabel for="emailTemplateCreate" value="Email predloga" />
<select
id="emailTemplateCreate"
<Select
v-model="createForm.email_template_id"
:disabled="!createForm.auto_mail"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm disabled:opacity-50 disabled:cursor-not-allowed"
>
<option :value="null"> Brez </option>
<option v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }}
</option>
</select>
<SelectTrigger id="emailTemplateCreate" class="w-full">
<SelectValue placeholder="— Brez —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Brez </SelectItem>
<SelectItem v-for="t in emailTemplates" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="createForm.email_template_id" class="text-xs text-gray-500 mt-1">
<span
v-if="
@@ -805,16 +845,11 @@ const destroyDecision = () => {
<div class="col-span-6 sm:col-span-4">
<InputLabel for="actionsCreate" value="Akcije" />
<multiselect
<AppMultiSelect
id="actionsCreate"
v-model="createForm.actions"
:options="actionOptions"
:multiple="true"
track-by="id"
:taggable="true"
:items="actionOptions"
placeholder="Dodaj akcijo"
:append-to-body="true"
label="name"
/>
</div>
@@ -830,43 +865,44 @@ const destroyDecision = () => {
<div class="flex flex-col sm:flex-row gap-3">
<div class="flex-1">
<InputLabel :for="`cevent-${idx}`" value="Dogodek" />
<select
:id="`cevent-${idx}`"
v-model.number="ev.id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
@change="onEventChange(ev)"
>
<option :value="null"> Izberi </option>
<option v-for="opt in availableEvents" :key="opt.id" :value="opt.id">
{{ opt.name || opt.key || `#${opt.id}` }}
</option>
</select>
<Select v-model="ev.id" @update:model-value="onEventChange(ev)">
<SelectTrigger :id="`cevent-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi </SelectItem>
<SelectItem
v-for="opt in availableEvents"
:key="opt.id"
:value="opt.id"
>
{{ opt.name || opt.key || `#${opt.id}` }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="w-36">
<InputLabel :for="`corder-${idx}`" value="Vrstni red" />
<TextInput
<InputLabel :for="`corder-${idx}`">Vrstni red</InputLabel>
<Input
:id="`corder-${idx}`"
v-model.number="ev.run_order"
type="number"
class="w-full"
/>
</div>
<div class="flex items-end gap-2">
<div class="flex items-center gap-2 self-end">
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
v-model="ev.active"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.active" />
Aktivno
</label>
<button
type="button"
class="text-red-600 text-sm"
<Button
variant="ghost"
size="icon"
class="text-red-600 hover:text-red-700 hover:bg-red-50"
@click="createForm.events.splice(idx, 1)"
>
Odstrani
</button>
<Trash2 class="w-4 h-4" />
</Button>
</div>
</div>
<div class="mt-3">
@@ -874,16 +910,17 @@ const destroyDecision = () => {
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<InputLabel :for="`cseg-${idx}`" value="Segment" />
<select
:id="`cseg-${idx}`"
v-model.number="ev.config.segment_id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
<option :value="null"> Izberi segment </option>
<option v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
<Select v-model="ev.config.segment_id">
<SelectTrigger :id="`cseg-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi segment —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi segment </SelectItem>
<SelectItem v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</SelectItem>
</SelectContent>
</Select>
<p
v-if="createForm.errors[`events.${idx}.config.segment_id`]"
class="text-xs text-red-600 mt-1"
@@ -893,11 +930,7 @@ const destroyDecision = () => {
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm mt-6">
<input
type="checkbox"
v-model="ev.config.deactivate_previous"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.config.deactivate_previous" />
Deaktiviraj prejšnje
</label>
</div>
@@ -907,16 +940,21 @@ const destroyDecision = () => {
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<InputLabel :for="`cas-${idx}`" value="Archive setting" />
<select
:id="`cas-${idx}`"
v-model.number="ev.config.archive_setting_id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
<option :value="null"> Izberi nastavitev </option>
<option v-for="a in archiveSettings" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
<Select v-model="ev.config.archive_setting_id">
<SelectTrigger :id="`cas-${idx}`" class="w-full">
<SelectValue placeholder="— Izberi nastavitev —" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"> Izberi nastavitev </SelectItem>
<SelectItem
v-for="a in archiveSettings"
:key="a.id"
:value="a.id"
>
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
<p
v-if="
createForm.errors[`events.${idx}.config.archive_setting_id`]
@@ -928,11 +966,7 @@ const destroyDecision = () => {
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm mt-6">
<input
type="checkbox"
v-model="ev.config.reactivate"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500"
/>
<Checkbox v-model:checked="ev.config.reactivate" />
Reactivate namesto arhiva
</label>
</div>
@@ -961,35 +995,42 @@ const destroyDecision = () => {
</div>
</div>
<div>
<PrimaryButton
<Button
type="button"
variant="outline"
@click="createForm.events.push(defaultEventPayload())"
>+ Dodaj dogodek</PrimaryButton
>+ Dodaj dogodek</Button
>
</div>
</div>
</div>
<div v-if="createForm.recentlySuccessful" class="mt-6 text-sm text-green-600">
<div v-if="createForm.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</div>
</form>
</CreateDialog>
</div>
<DialogFooter>
<Button variant="outline" @click="closeCreateDrawer">Cancel</Button>
<Button @click="store" :disabled="createForm.processing || !eventsValidCreate"
>Dodaj</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title> Delete decision </template>
<template #content>
Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be
undone.
</template>
<template #footer>
<button
@click="cancelDelete"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300 me-2"
>
Cancel
</button>
<PrimaryButton @click="destroyDecision">Delete</PrimaryButton>
</template>
</ConfirmationModal>
<AlertDialog v-model:open="showDelete">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete decision</AlertDialogTitle>
</AlertDialogHeader>
<div class="text-sm text-muted-foreground">
Are you sure you want to delete decision "{{ toDelete?.name }}"? This cannot be
undone.
</div>
<AlertDialogFooter>
<Button variant="outline" @click="cancelDelete">Cancel</Button>
<Button variant="destructive" @click="destroyDecision">Delete</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</template>