New report system and views
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
+348
-307
@@ -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>
|
||||
Reference in New Issue
Block a user