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>
|
||||
Reference in New Issue
Block a user