New report system and views

This commit is contained in:
Simon Pocrnjič
2026-01-02 12:32:20 +01:00
parent 9fc5b54b8a
commit 703b52ff59
67 changed files with 8255 additions and 2794 deletions
+98 -433
View File
@@ -1,7 +1,14 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import CardTitle from "@/Components/ui/card/CardTitle.vue";
import { Alert, AlertDescription, AlertTitle } from "@/Components/ui/alert";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import { Archive } from "lucide-vue-next";
import ArchiveRuleCard from "./Partials/ArchiveRuleCard.vue";
import CreateRuleForm from "./Partials/CreateRuleForm.vue";
import EditRuleForm from "./Partials/EditRuleForm.vue";
const props = defineProps({
settings: Object,
@@ -29,7 +36,6 @@ const newForm = useForm({
// Editing state & form
const editingSetting = ref(null);
// Conditions temporarily inactive in backend; keep placeholder for future restore
const originalEntityMeta = ref({ columns: ["id"] });
const editForm = useForm({
name: "",
@@ -47,14 +53,6 @@ const editForm = useForm({
options: { batch_size: 200 },
});
const selectedEntity = ref(null);
function onFocusChange() {
const found = props.archiveEntities.find((e) => e.focus === newForm.focus);
selectedEntity.value = found || null;
newForm.related = [];
}
function submitCreate() {
if (!newForm.focus) {
alert("Select a focus entity.");
@@ -68,11 +66,11 @@ function submitCreate() {
{
table: newForm.focus,
related: newForm.related,
// conditions omitted while inactive
columns: ["id"],
},
];
newForm.post(route("settings.archive.store"), {
preserveScroll: true,
onSuccess: () => {
newForm.focus = "";
newForm.related = [];
@@ -80,21 +78,26 @@ function submitCreate() {
newForm.action_id = null;
newForm.decision_id = null;
newForm.segment_id = null;
selectedEntity.value = null;
newForm.reset();
},
});
}
function toggleEnabled(setting) {
router.put(route("settings.archive.update", setting.id), {
...setting,
enabled: !setting.enabled,
});
router.put(
route("settings.archive.update", setting.id),
{
...setting,
enabled: !setting.enabled,
},
{
preserveScroll: true,
}
);
}
function startEdit(setting) {
editingSetting.value = setting;
// Populate editForm
editForm.name = setting.name || "";
editForm.description = setting.description || "";
editForm.enabled = setting.enabled;
@@ -104,7 +107,7 @@ function startEdit(setting) {
editForm.action_id = setting.action_id ?? null;
editForm.decision_id = setting.decision_id ?? null;
editForm.segment_id = setting.segment_id ?? null;
// Entities (first only)
const first = Array.isArray(setting.entities) ? setting.entities[0] : null;
if (first) {
editForm.focus = first.table || "";
@@ -112,20 +115,16 @@ function startEdit(setting) {
originalEntityMeta.value = {
columns: first.columns || ["id"],
};
const found = props.archiveEntities.find((e) => e.focus === editForm.focus);
selectedEntity.value = found || null;
} else {
editForm.focus = "";
editForm.related = [];
originalEntityMeta.value = { columns: ["id"] };
// If reactivate is checked it implies soft semantics; keep soft true (UI might show both)
}
}
function cancelEdit() {
editingSetting.value = null;
editForm.reset();
selectedEntity.value = null;
}
function submitUpdate() {
@@ -142,11 +141,11 @@ function submitUpdate() {
{
table: editForm.focus,
related: editForm.related,
// conditions omitted while inactive
columns: originalEntityMeta.value.columns || ["id"],
},
];
editForm.put(route("settings.archive.update", editingSetting.value.id), {
preserveScroll: true,
onSuccess: () => {
cancelEdit();
},
@@ -155,427 +154,93 @@ function submitUpdate() {
function remove(setting) {
if (!confirm("Delete archive rule?")) return;
router.delete(route("settings.archive.destroy", setting.id));
router.delete(route("settings.archive.destroy", setting.id), {
preserveScroll: true,
});
}
// Run Now removed (feature temporarily disabled)
</script>
<template>
<AppLayout title="Archive Settings">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">Archive Settings</h2>
</template>
<template #header />
<div class="pt-12">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Alert variant="default" class="mb-6 border-l-4 border-amber-500">
<AlertTitle class="text-sm font-medium text-amber-800">
Archive rule conditions are temporarily inactive
</AlertTitle>
<AlertDescription class="text-xs text-amber-800 space-y-2 mt-2">
<p>
All enabled rules apply to the focus entity and its selected related tables
without date/other filters. Stored condition JSON is preserved for future
reactivation.
</p>
<p class="font-medium">The "Run Now" action is currently disabled.</p>
<div class="mt-3 bg-white/60 rounded p-3 border border-amber-200">
<p class="font-semibold mb-1 text-amber-900">Chain Path Help</p>
<p class="mb-1">Supported chained related tables (dot notation):</p>
<ul class="list-disc ml-4 space-y-0.5">
<li v-for="cp in chainPatterns" :key="cp">
<code class="px-1 bg-amber-100 rounded text-xs">{{ cp }}</code>
</li>
</ul>
<p class="mt-1 italic">Only these chains are processed; others are ignored.</p>
</div>
</AlertDescription>
</Alert>
<div class="py-6 max-w-6xl mx-auto px-4">
<div class="mb-6 border-l-4 border-amber-500 bg-amber-50 text-amber-800 px-4 py-3 rounded">
<p class="text-sm font-medium">Archive rule conditions are temporarily inactive.</p>
<p class="text-xs mt-1">All enabled rules apply to the focus entity and its selected related tables without date/other filters. Stored condition JSON is preserved for future reactivation.</p>
<p class="text-xs mt-1 font-medium">The "Run Now" action is currently disabled.</p>
<div class="mt-3 text-xs bg-white/60 rounded p-3 border border-amber-200">
<p class="font-semibold mb-1 text-amber-900">Chain Path Help</p>
<p class="mb-1">Supported chained related tables (dot notation):</p>
<ul class="list-disc ml-4 space-y-0.5">
<li v-for="cp in chainPatterns" :key="cp">
<code class="px-1 bg-amber-100 rounded">{{ cp }}</code>
</li>
</ul>
<p class="mt-1 italic">Only these chains are processed; others are ignored.</p>
</div>
</div>
<div class="grid gap-6 md:grid-cols-3">
<div class="md:col-span-2 space-y-4">
<div
v-for="s in settings.data"
:key="s.id"
class="border rounded-lg p-4 bg-white shadow-sm"
>
<div class="flex items-start justify-between gap-4">
<div class="min-w-0">
<h3 class="font-medium text-gray-900 flex items-center gap-2">
<span class="truncate">{{ s.name || "Untitled Rule #" + s.id }}</span>
<span
v-if="!s.enabled"
class="inline-flex text-xs px-2 py-0.5 rounded-full bg-amber-100 text-amber-800"
>Disabled</span
>
</h3>
<p v-if="s.description" class="text-sm text-gray-600 mt-1">
{{ s.description }}
</p>
<p class="mt-2 text-xs text-gray-500">
Strategy: {{ s.strategy }} Soft: {{ s.soft ? "Yes" : "No" }}
</p>
</div>
<div class="flex flex-col items-end gap-2 shrink-0">
<button
@click="startEdit(s)"
class="text-xs px-3 py-1.5 rounded bg-gray-200 text-gray-800 hover:bg-gray-300"
>
Edit
</button>
<!-- Run Now removed -->
<button
@click="toggleEnabled(s)"
class="text-xs px-3 py-1.5 rounded bg-indigo-600 text-white hover:bg-indigo-700"
>
{{ s.enabled ? "Disable" : "Enable" }}
</button>
<button
@click="remove(s)"
class="text-xs px-3 py-1.5 rounded bg-red-600 text-white hover:bg-red-700"
>
Delete
</button>
</div>
</div>
<div class="mt-3 text-xs bg-gray-50 border rounded p-2 overflow-x-auto">
<pre class="whitespace-pre-wrap">{{
JSON.stringify(s.entities, null, 2)
}}</pre>
</div>
</div>
<div v-if="!settings.data.length" class="text-sm text-gray-600">
No archive rules.
</div>
</div>
<div class="space-y-4">
<div v-if="!editingSetting" class="border rounded-lg p-4 bg-white shadow-sm">
<h3 class="font-semibold text-gray-900 mb-2 text-sm">New Rule</h3>
<div class="space-y-3 text-sm">
<div>
<label class="block text-xs font-medium text-gray-600"
>Segment (optional)</label
>
<select
v-model="newForm.segment_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Action (optional)</label
>
<select
v-model="newForm.action_id"
@change="
() => {
newForm.decision_id = null;
}
"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Decision (optional)</label
>
<select
v-model="newForm.decision_id"
:disabled="!newForm.action_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option
v-for="d in actions.find((a) => a.id === newForm.action_id)
?.decisions || []"
:key="d.id"
:value="d.id"
>
{{ d.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Name</label>
<input
v-model="newForm.name"
type="text"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
<div class="grid gap-6 md:grid-cols-3">
<div class="md:col-span-2 space-y-4">
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
>
<template #header>
<div class="flex items-center gap-2">
<Archive :size="18" />
<CardTitle class="uppercase">Archive Rules</CardTitle>
</div>
</template>
<div class="p-4 space-y-4">
<ArchiveRuleCard
v-for="rule in settings.data"
:key="rule.id"
:rule="rule"
@edit="startEdit"
@toggle-enabled="toggleEnabled"
@delete="remove"
/>
<div v-if="newForm.errors.name" class="text-red-600 text-xs mt-1">
{{ newForm.errors.name }}
<div
v-if="!settings.data.length"
class="text-sm text-muted-foreground text-center py-8"
>
No archive rules defined yet.
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Focus Entity</label
>
<select
v-model="newForm.focus"
@change="onFocusChange"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="" disabled>-- choose --</option>
<option v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</option>
</select>
</div>
<div v-if="selectedEntity" class="space-y-1">
<div class="text-xs font-medium text-gray-600">Related Tables</div>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-1 text-xs bg-gray-100 px-2 py-1 rounded border"
>
<input
type="checkbox"
:value="r"
v-model="newForm.related"
class="rounded"
/>
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Description</label>
<textarea
v-model="newForm.description"
rows="2"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
></textarea>
<div v-if="newForm.errors.description" class="text-red-600 text-xs mt-1">
{{ newForm.errors.description }}
</div>
</div>
<div class="flex items-center gap-2">
<input id="enabled" type="checkbox" v-model="newForm.enabled" />
<label for="enabled" class="text-xs font-medium text-gray-700"
>Enabled</label
>
</div>
<div class="flex items-center gap-2">
<input id="soft" type="checkbox" v-model="newForm.soft" />
<label for="soft" class="text-xs font-medium text-gray-700"
>Soft Archive</label
>
</div>
<div class="flex items-center gap-2">
<input id="reactivate" type="checkbox" v-model="newForm.reactivate" />
<label for="reactivate" class="text-xs font-medium text-gray-700"
>Reactivate (undo archive)</label
>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Strategy</label>
<select
v-model="newForm.strategy"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="immediate">Immediate</option>
<option value="scheduled">Scheduled</option>
<option value="queued">Queued</option>
<option value="manual">Manual (never auto-run)</option>
</select>
<div v-if="newForm.errors.strategy" class="text-red-600 text-xs mt-1">
{{ newForm.errors.strategy }}
</div>
</div>
<button
@click="submitCreate"
type="button"
:disabled="newForm.processing"
class="w-full text-sm px-3 py-2 rounded bg-green-600 text-white hover:bg-green-700 disabled:opacity-50"
>
Create
</button>
<div v-if="Object.keys(newForm.errors).length" class="text-xs text-red-600">
Please fix validation errors.
</div>
</div>
</AppCard>
</div>
<div v-else class="border rounded-lg p-4 bg-white shadow-sm">
<h3 class="font-semibold text-gray-900 mb-2 text-sm">
Edit Rule #{{ editingSetting.id }}
</h3>
<div class="space-y-3 text-sm">
<div
class="text-xs text-gray-500"
v-if="editingSetting.strategy === 'manual'"
>
Manual strategy: this rule will only run when triggered manually.
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Segment (optional)</label
>
<select
v-model="editForm.segment_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="seg in segments" :key="seg.id" :value="seg.id">
{{ seg.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Action (optional)</label
>
<select
v-model="editForm.action_id"
@change="
() => {
editForm.decision_id = null;
}
"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Decision (optional)</label
>
<select
v-model="editForm.decision_id"
:disabled="!editForm.action_id"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option :value="null">-- none --</option>
<option
v-for="d in actions.find((a) => a.id === editForm.action_id)
?.decisions || []"
:key="d.id"
:value="d.id"
>
{{ d.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Name</label>
<input
v-model="editForm.name"
type="text"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
/>
<div v-if="editForm.errors.name" class="text-red-600 text-xs mt-1">
{{ editForm.errors.name }}
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600"
>Focus Entity</label
>
<select
v-model="editForm.focus"
@change="onFocusChange() /* reuse selectedEntity for preview */"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="" disabled>-- choose --</option>
<option v-for="ae in archiveEntities" :key="ae.id" :value="ae.focus">
{{ ae.name || ae.focus }}
</option>
</select>
</div>
<div
v-if="selectedEntity && editForm.focus === selectedEntity.focus"
class="space-y-1"
>
<div class="text-xs font-medium text-gray-600">Related Tables</div>
<div class="flex flex-wrap gap-2">
<label
v-for="r in selectedEntity.related"
:key="r"
class="inline-flex items-center gap-1 text-xs bg-gray-100 px-2 py-1 rounded border"
>
<input
type="checkbox"
:value="r"
v-model="editForm.related"
class="rounded"
/>
<span>{{ r }}</span>
</label>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Description</label>
<textarea
v-model="editForm.description"
rows="2"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
></textarea>
<div v-if="editForm.errors.description" class="text-red-600 text-xs mt-1">
{{ editForm.errors.description }}
</div>
</div>
<div class="flex items-center gap-2">
<input id="edit_enabled" type="checkbox" v-model="editForm.enabled" />
<label for="edit_enabled" class="text-xs font-medium text-gray-700"
>Enabled</label
>
</div>
<div class="flex items-center gap-2">
<input id="edit_soft" type="checkbox" v-model="editForm.soft" />
<label for="edit_soft" class="text-xs font-medium text-gray-700"
>Soft Archive</label
>
</div>
<div class="flex items-center gap-2">
<input id="edit_reactivate" type="checkbox" v-model="editForm.reactivate" />
<label for="edit_reactivate" class="text-xs font-medium text-gray-700"
>Reactivate (undo archive)</label
>
</div>
<div>
<label class="block text-xs font-medium text-gray-600">Strategy</label>
<select
v-model="editForm.strategy"
class="mt-1 w-full border rounded px-2 py-1.5 text-sm"
>
<option value="immediate">Immediate</option>
<option value="scheduled">Scheduled</option>
<option value="queued">Queued</option>
<option value="manual">Manual (never auto-run)</option>
</select>
<div v-if="editForm.errors.strategy" class="text-red-600 text-xs mt-1">
{{ editForm.errors.strategy }}
</div>
</div>
<div class="flex gap-2">
<button
@click="submitUpdate"
type="button"
:disabled="editForm.processing"
class="flex-1 text-sm px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50"
>
Update
</button>
<button
@click="cancelEdit"
type="button"
class="px-3 py-2 rounded text-sm bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
</div>
<div
v-if="Object.keys(editForm.errors).length"
class="text-xs text-red-600"
>
Please fix validation errors.
</div>
</div>
<div>
<CreateRuleForm
v-if="!editingSetting"
:form="newForm"
:archive-entities="archiveEntities"
:actions="actions"
:segments="segments"
@submit="submitCreate"
/>
<EditRuleForm
v-else
:form="editForm"
:setting="editingSetting"
:archive-entities="archiveEntities"
:actions="actions"
:segments="segments"
@submit="submitUpdate"
@cancel="cancelEdit"
/>
</div>
</div>
</div>