updates to UI and add archiving option

This commit is contained in:
Simon Pocrnjič
2025-10-05 19:45:49 +02:00
parent fe91c7e4bc
commit bab9d6561f
50 changed files with 3337 additions and 416 deletions
@@ -73,11 +73,22 @@ const store = async () => {
amount: form.amount,
note: form.note,
});
// Helper to safely format a selected date (Date instance or parsable value) to YYYY-MM-DD
const formatDateForSubmit = (value) => {
if (!value) return null; // leave empty as null
const d = value instanceof Date ? value : new Date(value);
if (isNaN(d.getTime())) return null; // invalid date -> null
// Avoid timezone shifting by constructing in local time
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`; // matches en-CA style YYYY-MM-DD
};
form
.transform((data) => ({
...data,
due_date: new Date(data.due_date).toLocaleDateString("en-CA"),
due_date: formatDateForSubmit(data.due_date),
}))
.post(route("clientCase.activity.store", props.client_case), {
onSuccess: () => {
@@ -179,17 +179,17 @@ const confirmDeleteAction = () => {
>
</div>
</td>
<td class="py-2 pl-2 pr-2 align-top text-right">
<Dropdown align="right" width="30" :content-classes="['py-1', 'bg-white']">
<td class="py-2 pl-2 pr-2 align-middle text-right">
<Dropdown align="right" width="30">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center w-8 h-8 rounded hover:bg-gray-100"
aria-haspopup="menu"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
:title="'Actions'"
>
<FontAwesomeIcon
:icon="['fas', 'ellipsis-vertical']"
class="text-gray-600 text-[20px]"
:icon="faEllipsisVertical"
class="h-4 w-4 text-gray-700"
/>
</button>
</template>
@@ -21,6 +21,7 @@ import {
faTrash,
faListCheck,
faPlus,
faBoxArchive,
} from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
@@ -119,6 +120,10 @@ const confirmChange = ref({
fromAll: false,
});
const askChangeSegment = (c, segmentId, fromAll = false) => {
// Prevent segment change for archived contracts
if (!c?.active) {
return;
}
confirmChange.value = { show: true, contract: c, segmentId, fromAll };
};
const closeConfirm = () => {
@@ -262,13 +267,16 @@ const closePaymentsDialog = () => {
class="inline-flex items-center justify-center h-7 w-7 rounded-full hover:bg-gray-100"
:class="{
'opacity-50 cursor-not-allowed':
!segments || segments.length === 0,
!segments || segments.length === 0 || !c.active,
}"
:title="
segments && segments.length
!c.active
? 'Segmenta ni mogoče spremeniti za arhivirano pogodbo'
: segments && segments.length
? 'Spremeni segment'
: 'Ni segmentov na voljo za ta primer'
"
:disabled="!c.active || !segments || !segments.length"
>
<FontAwesomeIcon
:icon="faPenToSquare"
@@ -313,6 +321,11 @@ const closePaymentsDialog = () => {
</div>
</template>
</Dropdown>
<span
v-if="!c.active"
class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-gray-200 text-gray-700 uppercase tracking-wide"
>Arhivirano</span
>
</div>
</FwbTableCell>
<FwbTableCell class="text-right">{{
@@ -433,6 +446,7 @@ const closePaymentsDialog = () => {
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
v-if="c.active"
@click="onEdit(c)"
>
<FontAwesomeIcon
@@ -444,6 +458,7 @@ const closePaymentsDialog = () => {
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
v-if="c.active"
@click="onAddActivity(c)"
>
<FontAwesomeIcon :icon="faListCheck" class="h-4 w-4 text-gray-600" />
@@ -468,6 +483,7 @@ const closePaymentsDialog = () => {
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
v-if="c.active"
@click="openObjectDialog(c)"
>
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" />
@@ -492,12 +508,62 @@ const closePaymentsDialog = () => {
<button
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
v-if="c.active && c?.account"
@click="openPaymentDialog(c)"
>
<FontAwesomeIcon :icon="faPlus" class="h-4 w-4 text-gray-600" />
<span>Dodaj plačilo</span>
</button>
<div class="my-1 border-t border-gray-100" />
<!-- Arhiviranje / Ponovna aktivacija -->
<div
class="px-3 pt-2 pb-1 text-[11px] uppercase tracking-wide text-gray-400"
>
{{ c.active ? "Arhiviranje" : "Ponovna aktivacija" }}
</div>
<button
v-if="c.active"
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: c.uuid,
}),
{},
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
>
<FontAwesomeIcon :icon="faBoxArchive" class="h-4 w-4 text-gray-600" />
<span>Arhiviraj</span>
</button>
<button
v-else
type="button"
class="w-full px-3 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
@click="
router.post(
route('clientCase.contract.archive', {
client_case: client_case.uuid,
uuid: c.uuid,
}),
{ reactivate: true },
{
preserveScroll: true,
only: ['contracts', 'activities', 'documents'],
}
)
"
>
<FontAwesomeIcon :icon="faBoxArchive" class="h-4 w-4 text-gray-600" />
<span>Ponovno aktiviraj</span>
</button>
<div class="my-1 border-t border-gray-100" />
<!-- Destruktivno -->
<button
@@ -373,7 +373,14 @@ function referenceOf(entityName, ent) {
<span>{{ activeEntity }}</span>
<span
v-if="r.entities[activeEntity].action_label"
class="text-[10px] px-1 py-0.5 rounded bg-gray-100"
:class="[
'text-[10px] px-1 py-0.5 rounded',
r.entities[activeEntity].action === 'create' && 'bg-emerald-100 text-emerald-700',
r.entities[activeEntity].action === 'update' && 'bg-blue-100 text-blue-700',
r.entities[activeEntity].action === 'reactivate' && 'bg-purple-100 text-purple-700 font-semibold',
r.entities[activeEntity].action === 'skip' && 'bg-gray-100 text-gray-600',
r.entities[activeEntity].action === 'implicit' && 'bg-teal-100 text-teal-700'
].filter(Boolean)"
>{{ r.entities[activeEntity].action_label }}</span
>
<span
@@ -502,10 +509,25 @@ function referenceOf(entityName, ent) {
</div>
<div>
Akcija:
<span class="font-medium">{{
r.entities[activeEntity].action_label ||
r.entities[activeEntity].action
}}</span>
<span
:class="[
'font-medium inline-flex items-center gap-1',
r.entities[activeEntity].action === 'reactivate' && 'text-purple-700'
].filter(Boolean)"
>{{
r.entities[activeEntity].action_label ||
r.entities[activeEntity].action
}}
<span
v-if="r.entities[activeEntity].reactivation"
class="text-[9px] px-1 py-0.5 rounded bg-purple-100 text-purple-700"
title="Pogodba bo reaktivirana"
>react</span
></span
>
</div>
<div v-if="r.entities[activeEntity].original_action === 'update' && r.entities[activeEntity].action === 'reactivate'" class="text-[10px] text-purple-600 mt-0.5">
(iz neaktivnega aktivno)
</div>
</template>
<template v-else>
@@ -18,6 +18,7 @@ const form = useForm({
source_type: "csv",
default_record_type: "",
is_active: true,
reactivate: false,
client_uuid: null,
entities: [],
meta: {
@@ -285,6 +286,10 @@ watch(
<label for="is_active" class="text-sm font-medium text-gray-700"
>Active</label
>
<div class="flex items-center gap-2 ml-6">
<input id="reactivate" v-model="form.reactivate" type="checkbox" class="rounded" />
<label for="reactivate" class="text-sm font-medium text-gray-700">Reactivation import</label>
</div>
</div>
<div class="pt-4">
@@ -20,6 +20,7 @@ const form = useForm({
source_type: props.template.source_type,
default_record_type: props.template.default_record_type || "",
is_active: props.template.is_active,
reactivate: props.template.reactivate ?? false,
client_uuid: props.template.client_uuid || null,
sample_headers: props.template.sample_headers || [],
// Add meta with default delimiter support
@@ -434,9 +435,11 @@ watch(
type="checkbox"
class="rounded"
/>
<label for="is_active" class="text-sm font-medium text-gray-700"
>Aktivna</label
>
<label for="is_active" class="text-sm font-medium text-gray-700">Aktivna</label>
<div class="flex items-center gap-2 ml-6">
<input id="reactivate" v-model="form.reactivate" type="checkbox" class="rounded" />
<label for="reactivate" class="text-sm font-medium text-gray-700">Reaktivacija</label>
</div>
<button
@click.prevent="save"
class="ml-auto px-3 py-2 bg-indigo-600 text-white rounded"
+261 -63
View File
@@ -73,6 +73,27 @@ function formatAmount(val) {
});
}
function formatDateShort(val) {
if (!val) return "";
try {
const d = new Date(val);
if (Number.isNaN(d.getTime())) return "";
return d.toLocaleDateString("sl-SI", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return "";
}
}
function activityActionLine(a) {
const base = a?.action?.name || "";
const decision = a?.decision?.name ? `${a.decision.name}` : "";
return base + decision;
}
// Activity drawer state
const drawerAddActivity = ref(false);
const activityContractUuid = ref(null);
@@ -139,6 +160,35 @@ const submitComplete = () => {
},
});
};
// Contracts objects (Predmeti) modal state
const objectsModal = reactive({ open: false, items: [], contract: null });
function getContractObjects(c) {
if (!c) return [];
// Try a few common property names; fallback empty
return c.objects || c.contract_objects || c.items || [];
}
function openObjectsModal(c) {
objectsModal.contract = c;
objectsModal.items = getContractObjects(c) || [];
objectsModal.open = true;
}
function closeObjectsModal() {
objectsModal.open = false;
objectsModal.items = [];
objectsModal.contract = null;
}
// Client details (Stranka) summary
const clientSummary = computed(() => {
const p = props.client?.person || {};
return {
name: p.full_name || p.name || "—",
tax: p.tax_number || p.davcna || p.tax || null,
emso: p.emso || p.ems || null,
trr: p.trr || p.bank_account || null,
};
});
</script>
<template>
@@ -172,10 +222,13 @@ const submitComplete = () => {
<!-- Client details (account holder) -->
<div class="bg-white rounded-lg shadow border overflow-hidden">
<div class="p-3 sm:p-4">
<SectionTitle>
<template #title>Stranka</template>
</SectionTitle>
<div class="mt-2">
<h3
class="text-base font-semibold text-gray-900 leading-tight flex items-center gap-2"
>
<span class="truncate">{{ clientSummary.name }}</span>
<span class="chip-base chip-indigo">Naročnik</span>
</h3>
<div class="mt-4 pt-4 border-t border-dashed">
<PersonDetailPhone
:types="types"
:person="client.person"
@@ -188,14 +241,17 @@ const submitComplete = () => {
<!-- Person (case person) -->
<div class="bg-white rounded-lg shadow border overflow-hidden">
<div class="p-3 sm:p-4">
<SectionTitle>
<template #title>Primer - oseba</template>
</SectionTitle>
<div class="mt-2">
<h3
class="text-base font-semibold text-gray-900 leading-tight flex items-center gap-2"
>
<span class="truncate">{{ client_case.person.full_name }}</span>
<span class="chip-base chip-indigo">Primer</span>
</h3>
<div class="mt-4 pt-4 border-t border-dashed">
<PersonDetailPhone
:types="types"
:person="client_case.person"
default-tab="phones"
default-tab="addresses"
/>
</div>
</div>
@@ -211,48 +267,82 @@ const submitComplete = () => {
<div
v-for="c in contracts"
:key="c.uuid || c.id"
class="rounded border p-3 sm:p-4"
class="rounded border p-3 sm:p-4 bg-white shadow-sm"
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-gray-900">{{ c.reference || c.uuid }}</p>
<p class="text-sm text-gray-600">Tip: {{ c.type?.name || "—" }}</p>
</div>
<div class="text-right">
<div class="space-y-2">
<p v-if="c.account" class="text-sm text-gray-700">
Odprto: {{ formatAmount(c.account.balance_amount) }}
<!-- Header Row -->
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<p
class="font-semibold text-gray-900 text-sm leading-tight truncate"
>
{{ c.reference || c.uuid }}
</p>
<button
type="button"
class="text-sm px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700 w-full sm:w-auto"
@click="openDrawerAddActivity(c)"
<span
v-if="c.type?.name"
class="inline-flex items-center px-2 py-0.5 rounded-full bg-indigo-100 text-indigo-700 text-[11px] font-medium"
>
+ Aktivnost
</button>
<button
type="button"
class="text-sm px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700 w-full sm:w-auto"
@click="openDocDialog(c)"
{{ c.type.name }}
</span>
</div>
<div v-if="c.account" class="mt-2 flex items-baseline gap-2">
<span class="uppercase tracking-wide text-[11px] text-gray-400"
>Odprto</span
>
<span
class="text-lg font-semibold text-gray-900 leading-none tracking-tight"
>{{ formatAmount(c.account.balance_amount) }} </span
>
+ Dokument
</button>
</div>
</div>
<div class="flex flex-col gap-1.5 w-32 text-right shrink-0">
<button
type="button"
class="text-sm px-3 py-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 active:scale-[.97] transition shadow"
@click="openDrawerAddActivity(c)"
>
+ Aktivnost
</button>
<button
type="button"
class="text-sm px-3 py-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 active:scale-[.97] transition shadow"
@click="openDocDialog(c)"
>
+ Dokument
</button>
<!--button
type="button"
:disabled="!getContractObjects(c).length"
@click="openObjectsModal(c)"
class="relative text-sm px-3 py-2 rounded-md flex items-center justify-center transition disabled:cursor-not-allowed disabled:opacity-50 bg-slate-600 text-white hover:bg-slate-700 active:scale-[.97] shadow"
>
Predmeti
<span
class="ml-1 inline-flex items-center justify-center min-w-[1.1rem] h-5 text-[11px] px-1.5 rounded-full bg-white/90 text-slate-700 font-medium"
>{{ getContractObjects(c).length }}</span
>
</button-->
</div>
</div>
<div v-if="c.last_object" class="mt-2 text-sm text-gray-700">
<p class="font-medium">Predmet:</p>
<p>
<span class="text-gray-900">{{
c.last_object.name || c.last_object.reference
}}</span>
<span v-if="c.last_object.type" class="ml-2 text-gray-500"
<!-- Subject / Last Object -->
<div v-if="c.last_object" class="mt-3 border-t pt-3">
<p class="text-[11px] uppercase tracking-wide text-gray-400 mb-1">
Zadnji predmet
</p>
<div class="text-sm font-medium text-gray-800">
{{ c.last_object.name || c.last_object.reference }}
<span
v-if="c.last_object.type"
class="ml-2 text-xs font-normal text-gray-500"
>({{ c.last_object.type }})</span
>
</p>
<p v-if="c.last_object.description" class="text-gray-600 mt-1">
</div>
<div
v-if="c.last_object.description"
class="mt-1 text-sm text-gray-600 leading-snug"
>
{{ c.last_object.description }}
</p>
</div>
</div>
</div>
<p v-if="!contracts?.length" class="text-sm text-gray-600">
@@ -270,40 +360,66 @@ const submitComplete = () => {
<template #title>Aktivnosti</template>
</SectionTitle>
<button
class="text-sm px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
class="text-xs font-medium px-3 py-2 rounded-md bg-indigo-600 text-white shadow-sm active:scale-[.98] hover:bg-indigo-700"
@click="openDrawerAddActivity()"
>
Nova
</button>
</div>
<div class="mt-2 divide-y">
<div v-for="a in activities" :key="a.id" class="py-2 text-sm">
<div class="flex items-center justify-between">
<div class="text-gray-800">
{{ a.action?.name
}}<span v-if="a.decision"> {{ a.decision?.name }}</span>
<div class="mt-3 space-y-3">
<div
v-for="a in activities"
:key="a.id"
class="rounded-md border border-gray-200 bg-gray-50/70 px-3 py-3 shadow-sm text-[13px]"
>
<!-- Top line: action + date/user -->
<div class="flex items-start justify-between gap-3">
<div class="font-medium text-gray-800 leading-snug truncate">
{{ activityActionLine(a) || "Aktivnost" }}
</div>
<div class="text-right text-gray-500">
<div v-if="a.contract">Pogodba: {{ a.contract.reference }}</div>
<div class="text-xs" v-if="a.created_at || a.user || a.user_name">
<span v-if="a.created_at">{{
new Date(a.created_at).toLocaleDateString("sl-SI")
}}</span>
<span v-if="(a.user && a.user.name) || a.user_name" class="ml-1"
>· {{ a.user?.name || a.user_name }}</span
>
<div
class="shrink-0 text-right text-[11px] text-gray-500 leading-tight"
>
<div v-if="a.created_at">{{ formatDateShort(a.created_at) }}</div>
<div v-if="(a.user && a.user.name) || a.user_name" class="truncate">
{{ a.user?.name || a.user_name }}
</div>
</div>
</div>
<div v-if="a.note" class="text-gray-600">{{ a.note }}</div>
<div class="text-gray-500">
<span v-if="a.due_date">Zapadlost: {{ a.due_date }}</span>
<span v-if="a.amount != null" class="ml-2"
<!-- Badges row -->
<div class="mt-2 flex flex-wrap gap-1.5">
<span
v-if="a.contract"
class="inline-flex items-center rounded-full bg-indigo-100 text-indigo-700 px-2 py-0.5 text-[10px] font-medium"
>Pogodba: {{ a.contract.reference }}</span
>
<span
v-if="a.due_date"
class="inline-flex items-center rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-[10px] font-medium"
>Zapadlost: {{ formatDateShort(a.due_date) || a.due_date }}</span
>
<span
v-if="a.amount != null"
class="inline-flex items-center rounded-full bg-emerald-100 text-emerald-700 px-2 py-0.5 text-[10px] font-medium"
>Znesek: {{ formatAmount(a.amount) }} </span
>
<span
v-if="a.status"
class="inline-flex items-center rounded-full bg-gray-200 text-gray-700 px-2 py-0.5 text-[10px] font-medium"
>{{ a.status }}</span
>
</div>
<!-- Note -->
<div v-if="a.note" class="mt-2 text-gray-700 leading-snug">
{{ a.note }}
</div>
</div>
<div v-if="!activities?.length" class="text-gray-600 py-2">
<div
v-if="!activities?.length"
class="text-gray-600 text-sm py-2 text-center"
>
Ni aktivnosti.
</div>
</div>
@@ -423,6 +539,65 @@ const submitComplete = () => {
</template>
</ConfirmationModal>
<!-- Contract Objects (Predmeti) Modal -->
<DialogModal :show="objectsModal.open" @close="closeObjectsModal">
<template #title>
Predmeti
<span
v-if="objectsModal.contract"
class="block text-xs font-normal text-gray-500 mt-0.5"
>
{{ objectsModal.contract.reference || objectsModal.contract.uuid }}
</span>
</template>
<template #content>
<div
v-if="objectsModal.items.length"
class="space-y-3 max-h-[60vh] overflow-y-auto pr-1"
>
<div
v-for="(o, idx) in objectsModal.items"
:key="o.id || o.uuid || idx"
class="rounded border border-gray-200 bg-gray-50 px-3 py-2 text-sm"
>
<div class="font-medium text-gray-800 truncate">
{{ o.name || o.reference || "#" + (o.id || o.uuid || idx + 1) }}
</div>
<div class="mt-0.5 text-xs text-gray-500 flex flex-wrap gap-x-2 gap-y-0.5">
<span
v-if="o.type"
class="inline-flex items-center bg-indigo-100 text-indigo-700 px-1.5 py-0.5 rounded-full"
>{{ o.type }}</span
>
<span
v-if="o.status"
class="inline-flex items-center bg-gray-200 text-gray-700 px-1.5 py-0.5 rounded-full"
>{{ o.status }}</span
>
<span
v-if="o.amount != null"
class="inline-flex items-center bg-emerald-100 text-emerald-700 px-1.5 py-0.5 rounded-full"
>{{ formatAmount(o.amount) }} </span
>
</div>
<div v-if="o.description" class="mt-1 text-gray-600 leading-snug">
{{ o.description }}
</div>
</div>
</div>
<div v-else class="text-gray-600 text-sm">Ni predmetov.</div>
</template>
<template #footer>
<button
type="button"
class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200"
@click="closeObjectsModal"
>
Zapri
</button>
</template>
</DialogModal>
<!-- Upload Document Modal -->
<DialogModal :show="docDialogOpen" @close="closeDocDialog">
<template #title>Dodaj dokument</template>
@@ -493,4 +668,27 @@ const submitComplete = () => {
</AppPhoneLayout>
</template>
<style scoped></style>
<style scoped>
/* Using basic CSS since @apply is not processed in this scoped block by default */
.chip-base {
display: inline-flex;
align-items: center;
padding: 0.125rem 0.5rem; /* py-0.5 px-2 */
border-radius: 9999px;
font-size: 11px;
font-weight: 500;
line-height: 1.1;
}
.chip-indigo {
background: #eef2ff;
color: #3730a3;
} /* approx indigo-50 / indigo-700 */
.chip-default {
background: #f1f5f9;
color: #334155;
} /* slate-100 / slate-700 */
.chip-emerald {
background: #ecfdf5;
color: #047857;
} /* emerald-50 / emerald-700 */
</style>
@@ -0,0 +1,591 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
const props = defineProps({
settings: Object,
archiveEntities: Array,
actions: Array,
segments: Array,
chainPatterns: Array,
});
const newForm = useForm({
name: "",
description: "",
enabled: true,
strategy: "immediate",
soft: true,
reactivate: false,
focus: "",
related: [],
entities: [],
action_id: null,
decision_id: null,
segment_id: null,
options: { batch_size: 200 },
});
// 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: "",
description: "",
enabled: true,
strategy: "immediate",
soft: true,
reactivate: false,
focus: "",
related: [],
entities: [],
action_id: null,
decision_id: null,
segment_id: null,
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.");
return;
}
if (newForm.decision_id && !newForm.action_id) {
alert("Select an action before choosing a decision.");
return;
}
newForm.entities = [
{
table: newForm.focus,
related: newForm.related,
// conditions omitted while inactive
columns: ["id"],
},
];
newForm.post(route("settings.archive.store"), {
onSuccess: () => {
newForm.focus = "";
newForm.related = [];
newForm.entities = [];
newForm.action_id = null;
newForm.decision_id = null;
newForm.segment_id = null;
selectedEntity.value = null;
},
});
}
function toggleEnabled(setting) {
router.put(route("settings.archive.update", setting.id), {
...setting,
enabled: !setting.enabled,
});
}
function startEdit(setting) {
editingSetting.value = setting;
// Populate editForm
editForm.name = setting.name || "";
editForm.description = setting.description || "";
editForm.enabled = setting.enabled;
editForm.strategy = setting.strategy || "immediate";
editForm.soft = setting.soft;
editForm.reactivate = setting.reactivate ?? false;
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 || "";
editForm.related = first.related || [];
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() {
if (!editingSetting.value) return;
if (!editForm.focus) {
alert("Select a focus entity.");
return;
}
if (editForm.decision_id && !editForm.action_id) {
alert("Select an action before choosing a decision.");
return;
}
editForm.entities = [
{
table: editForm.focus,
related: editForm.related,
// conditions omitted while inactive
columns: originalEntityMeta.value.columns || ["id"],
},
];
editForm.put(route("settings.archive.update", editingSetting.value.id), {
onSuccess: () => {
cancelEdit();
},
});
}
function remove(setting) {
if (!confirm("Delete archive rule?")) return;
router.delete(route("settings.archive.destroy", setting.id));
}
// 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>
<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 v-if="newForm.errors.name" class="text-red-600 text-xs mt-1">
{{ newForm.errors.name }}
</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>
</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>
</div>
</div>
</div>
</AppLayout>
</template>
<style scoped>
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
monospace;
}
</style>
+81 -36
View File
@@ -1,41 +1,86 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import { Link } from '@inertiajs/vue3';
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link } from "@inertiajs/vue3";
</script>
<template>
<AppLayout title="Settings">
<template #header></template>
<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>
</div>
<AppLayout title="Settings">
<template #header></template>
<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>
</AppLayout>
</template>
</div>
</div>
</AppLayout>
</template>