updates to UI and add archiving option
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import InputLabel from './InputLabel.vue'
|
||||
import InputError from './InputError.vue'
|
||||
import { computed } from 'vue'
|
||||
import InputLabel from "./InputLabel.vue";
|
||||
import InputError from "./InputError.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
/*
|
||||
DatePickerField (v-calendar)
|
||||
@@ -25,45 +25,44 @@ const props = defineProps({
|
||||
modelValue: { type: [Date, String, Number, null], default: null },
|
||||
id: { type: String, default: undefined },
|
||||
label: { type: String, default: undefined },
|
||||
format: { type: String, default: 'dd.MM.yyyy' },
|
||||
format: { type: String, default: "dd.MM.yyyy" },
|
||||
enableTimePicker: { type: Boolean, default: false },
|
||||
inline: { type: Boolean, default: false },
|
||||
// legacy/unused in v-calendar (kept to prevent breaking callers)
|
||||
autoApply: { type: Boolean, default: false },
|
||||
teleportTarget: { type: [Boolean, String], default: 'body' },
|
||||
teleportTarget: { type: [Boolean, String], default: "body" },
|
||||
autoPosition: { type: Boolean, default: true },
|
||||
menuClassName: { type: String, default: 'dp-over-modal' },
|
||||
menuClassName: { type: String, default: "dp-over-modal" },
|
||||
fixed: { type: Boolean, default: true },
|
||||
closeOnAutoApply: { type: Boolean, default: true },
|
||||
closeOnScroll: { type: Boolean, default: true },
|
||||
placeholder: { type: String, default: '' },
|
||||
placeholder: { type: String, default: "" },
|
||||
error: { type: [String, Array], default: undefined },
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
const emit = defineEmits(["update:modelValue", "change"]);
|
||||
|
||||
const valueProxy = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val)
|
||||
emit("update:modelValue", val);
|
||||
emit("change", val);
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
// Convert common date mask from lowercase tokens to v-calendar tokens
|
||||
const inputMask = computed(() => {
|
||||
let m = props.format || 'dd.MM.yyyy'
|
||||
return m
|
||||
.replace(/yyyy/g, 'YYYY')
|
||||
.replace(/dd/g, 'DD')
|
||||
.replace(/MM/g, 'MM')
|
||||
+ (props.enableTimePicker ? ' HH:mm' : '')
|
||||
})
|
||||
let m = props.format || "dd.MM.yyyy";
|
||||
return (
|
||||
m.replace(/yyyy/g, "YYYY").replace(/dd/g, "DD").replace(/MM/g, "MM") +
|
||||
(props.enableTimePicker ? " HH:mm" : "")
|
||||
);
|
||||
});
|
||||
|
||||
const popoverCfg = computed(() => ({
|
||||
visibility: props.inline ? 'visible' : 'click',
|
||||
placement: 'bottom-start',
|
||||
}))
|
||||
visibility: props.inline ? "visible" : "click",
|
||||
placement: "bottom-start",
|
||||
}));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -84,20 +83,24 @@ const popoverCfg = computed(() => ({
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
:placeholder="placeholder"
|
||||
:value="inputValue"
|
||||
autocomplete="off"
|
||||
v-on="inputEvents"
|
||||
/>
|
||||
</template>
|
||||
</VDatePicker>
|
||||
|
||||
<template v-if="error">
|
||||
<InputError v-if="Array.isArray(error)" v-for="(e, idx) in error" :key="idx" :message="e" />
|
||||
<InputError
|
||||
v-if="Array.isArray(error)"
|
||||
v-for="(e, idx) in error"
|
||||
:key="idx"
|
||||
:message="e"
|
||||
/>
|
||||
<InputError v-else :message="error" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* Ensure the date picker menu overlays modals/dialogs */
|
||||
|
||||
</style>
|
||||
|
||||
@@ -255,7 +255,13 @@ function closeActions() {
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-3"
|
||||
>Vir</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell class="w-px" />
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-"
|
||||
>Opis</FwbTableHeadCell
|
||||
>
|
||||
<FwbTableHeadCell
|
||||
class="uppercase text-xs font-semibold tracking-wide text-gray-700 py-"
|
||||
></FwbTableHeadCell>
|
||||
</FwbTableHead>
|
||||
<FwbTableBody>
|
||||
<template v-for="(doc, i) in documents" :key="doc.uuid || i">
|
||||
@@ -318,7 +324,7 @@ function closeActions() {
|
||||
@click="handleDownload(doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faDownload" class="h-4 w-4 text-gray-600" />
|
||||
<span>Download file</span>
|
||||
<span>Prenos</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -326,7 +332,7 @@ function closeActions() {
|
||||
@click="askDelete(doc)"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faTrash" class="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
<span>Izbriši</span>
|
||||
</button>
|
||||
<!-- future actions can be slotted here -->
|
||||
</template>
|
||||
|
||||
@@ -1,143 +1,323 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faLocationDot,
|
||||
faPhone,
|
||||
faEnvelope,
|
||||
faLandmark,
|
||||
faChevronDown,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
person: { type: Object, required: true },
|
||||
types: { type: Object, default: () => ({}) },
|
||||
// Allow overriding the default active tab: 'addresses' | 'phones' | 'emails' | 'bank'
|
||||
defaultTab: { type: String, default: 'addresses' },
|
||||
})
|
||||
defaultTab: { type: String, default: "addresses" },
|
||||
});
|
||||
|
||||
const phoneTypes = computed(() => {
|
||||
const arr = props.types?.phone_types || []
|
||||
const map = {}
|
||||
for (const t of arr) { map[t.id] = t.name }
|
||||
return map
|
||||
})
|
||||
const arr = props.types?.phone_types || [];
|
||||
const map = {};
|
||||
for (const t of arr) {
|
||||
map[t.id] = t.name;
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const displayName = computed(() => {
|
||||
const p = props.person || {}
|
||||
const full = p.full_name?.trim()
|
||||
if (full) { return full }
|
||||
const first = p.first_name?.trim() || ''
|
||||
const last = p.last_name?.trim() || ''
|
||||
return `${first} ${last}`.trim()
|
||||
})
|
||||
const p = props.person || {};
|
||||
const full = p.full_name?.trim();
|
||||
if (full) {
|
||||
return full;
|
||||
}
|
||||
const first = p.first_name?.trim() || "";
|
||||
const last = p.last_name?.trim() || "";
|
||||
return `${first} ${last}`.trim();
|
||||
});
|
||||
|
||||
const primaryAddress = computed(() => props.person?.addresses?.[0] || null)
|
||||
const primaryEmail = computed(() => props.person?.emails?.[0]?.value || null)
|
||||
const primaryAddress = computed(() => props.person?.addresses?.[0] || null);
|
||||
const primaryEmail = computed(() => props.person?.emails?.[0]?.value || null);
|
||||
// Backend phone model uses `nu` as the number
|
||||
const allPhones = computed(() => props.person?.phones || [])
|
||||
const allAddresses = computed(() => props.person?.addresses || [])
|
||||
const allEmails = computed(() => props.person?.emails || [])
|
||||
const allPhones = computed(() => props.person?.phones || []);
|
||||
const allAddresses = computed(() => props.person?.addresses || []);
|
||||
const allEmails = computed(() => props.person?.emails || []);
|
||||
// Laravel serializes relation names to snake_case, so prefer bank_accounts, fallback to bankAccounts
|
||||
const allBankAccounts = computed(() => props.person?.bank_accounts || props.person?.bankAccounts || [])
|
||||
const bankIban = computed(() => allBankAccounts.value?.[0]?.iban || null)
|
||||
const taxNumber = computed(() => props.person?.tax_number || null)
|
||||
const ssn = computed(() => props.person?.social_security_number || null)
|
||||
const allBankAccounts = computed(
|
||||
() => props.person?.bank_accounts || props.person?.bankAccounts || []
|
||||
);
|
||||
// Use the LAST added bank account (assumes incoming order oldest -> newest)
|
||||
const bankIban = computed(() => {
|
||||
const list = allBankAccounts.value || [];
|
||||
if (!list.length) {
|
||||
return null;
|
||||
}
|
||||
return list[list.length - 1]?.iban || null;
|
||||
});
|
||||
const taxNumber = computed(() => props.person?.tax_number || null);
|
||||
const ssn = computed(() => props.person?.social_security_number || null);
|
||||
|
||||
// Summary sizing
|
||||
const showMore = ref(false)
|
||||
const summaryPhones = computed(() => allPhones.value.slice(0, showMore.value ? 2 : 1))
|
||||
const showMore = ref(false);
|
||||
const summaryPhones = computed(() => allPhones.value.slice(0, showMore.value ? 3 : 1));
|
||||
|
||||
// Tabs
|
||||
const activeTab = ref(props.defaultTab || 'addresses')
|
||||
watch(() => props.defaultTab, (val) => { if (val) activeTab.value = val })
|
||||
// Limit tabs to addresses | phones | emails (TRR tab removed)
|
||||
const allowedTabs = ["addresses", "phones", "emails"];
|
||||
const initialTab = allowedTabs.includes(props.defaultTab)
|
||||
? props.defaultTab
|
||||
: "addresses";
|
||||
const activeTab = ref(initialTab);
|
||||
watch(
|
||||
() => props.defaultTab,
|
||||
(val) => {
|
||||
if (val && allowedTabs.includes(val)) {
|
||||
activeTab.value = val;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function maskIban(iban) {
|
||||
if (!iban || typeof iban !== 'string') return null
|
||||
const clean = iban.replace(/\s+/g, '')
|
||||
if (clean.length <= 8) return clean
|
||||
return `${clean.slice(0, 4)} **** **** ${clean.slice(-4)}`
|
||||
if (!iban || typeof iban !== "string") return null;
|
||||
const clean = iban.replace(/\s+/g, "");
|
||||
if (clean.length <= 8) return clean;
|
||||
return `${clean.slice(0, 4)} **** **** ${clean.slice(-4)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Summary -->
|
||||
<div class="text-sm">
|
||||
<div v-if="displayName" class="font-medium text-gray-900">{{ displayName }}</div>
|
||||
|
||||
<div v-if="primaryAddress" class="mt-1 text-gray-700">
|
||||
<span>{{ primaryAddress.address }}</span>
|
||||
<span v-if="primaryAddress.country" class="text-gray-500 text-xs ml-1">({{ primaryAddress.country }})</span>
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
<span v-if="primaryAddress" class="pill pill-slate" title="Naslov">
|
||||
<FontAwesomeIcon :icon="faLocationDot" class="w-4 h-4 mr-1" />
|
||||
<span class="truncate max-w-[9rem]">{{ primaryAddress.address }}</span>
|
||||
</span>
|
||||
<span v-if="summaryPhones.length" class="pill pill-indigo" title="Telefon">
|
||||
<FontAwesomeIcon :icon="faPhone" class="w-4 h-4 mr-1" />
|
||||
{{ summaryPhones[0].nu
|
||||
}}<span
|
||||
v-if="
|
||||
(summaryPhones[0].type_id && phoneTypes[summaryPhones[0].type_id]) ||
|
||||
summaryPhones[0].type?.name
|
||||
"
|
||||
class="ml-1 text-[10px] opacity-80"
|
||||
>({{
|
||||
summaryPhones[0].type?.name || phoneTypes[summaryPhones[0].type_id]
|
||||
}})</span
|
||||
>
|
||||
</span>
|
||||
<span v-if="primaryEmail && showMore" class="pill pill-default" title="E-pošta">
|
||||
<FontAwesomeIcon :icon="faEnvelope" class="w-4 h-4 mr-1" />
|
||||
<span class="truncate max-w-[9rem]">{{ primaryEmail }}</span>
|
||||
</span>
|
||||
<span v-if="bankIban" class="pill pill-emerald" title="TRR (zadnji dodan)">
|
||||
<FontAwesomeIcon :icon="faLandmark" class="w-4 h-4 mr-1" />
|
||||
{{ maskIban(bankIban) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="summaryPhones?.length" class="mt-1 space-y-0.5">
|
||||
<div v-for="p in summaryPhones" :key="p.id" class="text-gray-700">
|
||||
<span>{{ p.nu }}</span>
|
||||
<span v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name" class="text-gray-500 text-xs ml-1">({{ p.type?.name || phoneTypes[p.type_id] }})</span>
|
||||
<transition name="fade">
|
||||
<div v-if="showMore" class="mt-3 grid grid-cols-2 gap-x-2 gap-y-2 text-[14px]">
|
||||
<div v-if="taxNumber">
|
||||
<div class="label">Davčna</div>
|
||||
<div class="value font-mono">{{ taxNumber }}</div>
|
||||
</div>
|
||||
<div v-if="ssn">
|
||||
<div class="label">EMŠO</div>
|
||||
<div class="value font-mono">{{ ssn }}</div>
|
||||
</div>
|
||||
<div v-if="bankIban">
|
||||
<div class="label">TRR (zadnji)</div>
|
||||
<div class="value font-mono">{{ maskIban(bankIban) }}</div>
|
||||
</div>
|
||||
<div v-if="primaryEmail">
|
||||
<div class="label">E‑pošta</div>
|
||||
<div class="value truncate">{{ primaryEmail }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showMore && primaryEmail" class="mt-1 text-gray-700">{{ primaryEmail }}</div>
|
||||
|
||||
<div v-if="showMore && bankIban" class="mt-1 text-gray-700">TRR: <span class="font-mono">{{ maskIban(bankIban) }}</span></div>
|
||||
|
||||
<div v-if="showMore && taxNumber" class="mt-1 text-gray-700">Davčna: <span class="font-mono">{{ taxNumber }}</span></div>
|
||||
<div v-if="showMore && ssn" class="mt-1 text-gray-700">EMŠO: <span class="font-mono">{{ ssn }}</span></div>
|
||||
|
||||
<button type="button" class="mt-2 text-xs text-blue-600 hover:underline" @click="showMore = !showMore">
|
||||
{{ showMore ? 'Skrij' : 'Prikaži več' }}
|
||||
</transition>
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 inline-flex items-center text-[11px] font-medium text-indigo-600 hover:text-indigo-700 focus:outline-none"
|
||||
@click="showMore = !showMore"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="faChevronDown"
|
||||
:class="[
|
||||
'w-3 h-3 mr-1 transition-transform',
|
||||
showMore ? 'rotate-180' : 'rotate-0',
|
||||
]"
|
||||
/>
|
||||
{{ showMore ? "Manj podrobnosti" : "Več podrobnosti" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="mt-3">
|
||||
<div class="flex gap-2 overflow-x-auto">
|
||||
<button type="button" @click="activeTab = 'addresses'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='addresses' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2a1 1 0 0 1 .832.445l6 8.5a1 1 0 0 1 .168.555V17a1 1 0 0 1-1 1h-4v-4H8v4H4a1 1 0 0 1-1-1v-5.5a1 1 0 0 1 .168-.555l6-8.5A1 1 0 0 1 10 2Z"/></svg>
|
||||
Naslovi ({{ allAddresses.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'phones'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='phones' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M2.3 3.3c.6-1 1.9-1.3 2.9-.7l1.7 1a2 2 0 0 1 .9 2.5l-.5 1.2a2 2 0 0 0 .4 2.2l2.8 2.8a2 2 0 0 0 2.2.4l1.2-.5a2 2 0 0 1 2.5.9l1 1.7c.6 1 .3 2.3-.7 2.9-2 1.1-4.5 1.1-6.5 0-2.5-1.3-4.8-3.6-6.1-6.1-1.1-2-1.1-4.5 0-6.5Z"/></svg>
|
||||
Telefoni ({{ allPhones.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'emails'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='emails' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M2.5 5A1.5 1.5 0 0 1 4 3.5h12A1.5 1.5 0 0 1 17.5 5v10A1.5 1.5 0 0 1 16 16.5H4A1.5 1.5 0 0 1 2.5 15V5Zm2.1.5 5.4 3.6a1 1 0 0 0 1.1 0l5.4-3.6V5H4.6Z"/></svg>
|
||||
E-pošta ({{ allEmails.length }})
|
||||
</button>
|
||||
<button type="button" @click="activeTab = 'bank'" :class="['px-3 py-1 rounded text-xs inline-flex items-center gap-1', activeTab==='bank' ? 'bg-gray-200 text-gray-900' : 'bg-white text-gray-700 border']">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor"><path d="M10 2 2 6v2h16V6l-8-4Zm-6 7h12v7H4V9Zm-1 8h14v1H3v-1Z"/></svg>
|
||||
TRR ({{ allBankAccounts.length }})
|
||||
</button>
|
||||
<!-- Segmented Tabs -->
|
||||
<div class="mt-5">
|
||||
<div class="relative">
|
||||
<div
|
||||
class="flex w-full text-[11px] font-medium rounded-lg border bg-gray-50 overflow-hidden"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@click="activeTab = 'addresses'"
|
||||
:class="['seg-btn', activeTab === 'addresses' && 'seg-active']"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faLocationDot" class="w-3.5 h-3.5 mr-1 shrink-0" />
|
||||
<span class="truncate">Naslovi ({{ allAddresses.length }})</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="activeTab = 'phones'"
|
||||
:class="['seg-btn', activeTab === 'phones' && 'seg-active']"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faPhone" class="w-3.5 h-3.5 mr-1 shrink-0" />
|
||||
<span class="truncate">Telefoni ({{ allPhones.length }})</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="activeTab = 'emails'"
|
||||
:class="['seg-btn', activeTab === 'emails' && 'seg-active']"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faEnvelope" class="w-3.5 h-3.5 mr-1 shrink-0" />
|
||||
<span class="truncate">E‑pošta ({{ allEmails.length }})</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class="mt-3 rounded-md border bg-white/60 p-2">
|
||||
<!-- Addresses -->
|
||||
<div v-if="activeTab==='addresses'">
|
||||
<div v-if="!allAddresses.length" class="text-gray-500 text-xs">Ni naslovov.</div>
|
||||
<div v-for="(a,idx) in allAddresses" :key="a.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ a.address }}</div>
|
||||
<div v-if="a.country" class="text-gray-600 text-xs">{{ a.country }}</div>
|
||||
<div v-if="activeTab === 'addresses'">
|
||||
<div v-if="!allAddresses.length" class="empty">Ni naslovov.</div>
|
||||
<div v-for="(a, idx) in allAddresses" :key="a.id || idx" class="item-row">
|
||||
<div class="font-medium text-gray-800">{{ a.address }}</div>
|
||||
<div v-if="a.country" class="sub">{{ a.country }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phones -->
|
||||
<div v-else-if="activeTab==='phones'">
|
||||
<div v-if="!allPhones.length" class="text-gray-500 text-xs">Ni telefonov.</div>
|
||||
<div v-for="(p,idx) in allPhones" :key="p.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ p.nu }} <span v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name" class="text-gray-500 text-xs">({{ p.type?.name || phoneTypes[p.type_id] }})</span></div>
|
||||
<div v-else-if="activeTab === 'phones'">
|
||||
<div v-if="!allPhones.length" class="empty">Ni telefonov.</div>
|
||||
<div v-for="(p, idx) in allPhones" :key="p.id || idx" class="item-row">
|
||||
<div class="font-medium text-gray-800">
|
||||
{{ p.nu }}
|
||||
<span
|
||||
v-if="(p.type_id && phoneTypes[p.type_id]) || p.type?.name"
|
||||
class="sub ml-1"
|
||||
>({{ p.type?.name || phoneTypes[p.type_id] }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emails -->
|
||||
<div v-else-if="activeTab==='emails'">
|
||||
<div v-if="!allEmails.length" class="text-gray-500 text-xs">Ni e-poštnih naslovov.</div>
|
||||
<div v-for="(e,idx) in allEmails" :key="e.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ e.value }}<span v-if="e.label" class="text-gray-500 text-xs ml-1">({{ e.label }})</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bank accounts -->
|
||||
<div v-else>
|
||||
<div v-if="!allBankAccounts.length" class="text-gray-500 text-xs">Ni TRR računov.</div>
|
||||
<div v-for="(b,idx) in allBankAccounts" :key="b.id || idx" class="py-1">
|
||||
<div class="text-gray-800">{{ maskIban(b.iban) }}</div>
|
||||
<div v-if="b.bank_name" class="text-gray-600 text-xs">{{ b.bank_name }}</div>
|
||||
<div v-else-if="activeTab === 'emails'">
|
||||
<div v-if="!allEmails.length" class="empty">Ni e-poštnih naslovov.</div>
|
||||
<div v-for="(e, idx) in allEmails" :key="e.id || idx" class="item-row">
|
||||
<div class="font-medium text-gray-800">
|
||||
{{ e.value }}<span v-if="e.label" class="sub ml-1">({{ e.label }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- (TRR tab removed; last bank account surfaced in summary) -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Basic utility replacements (no Tailwind processor here) */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
border-radius: 9999px;
|
||||
padding: 0.35rem 0.75rem; /* slightly larger */
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.15;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.pill-slate {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
}
|
||||
.pill-indigo {
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
}
|
||||
.pill-default {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
.pill-emerald {
|
||||
background: #d1fae5;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.seg-btn {
|
||||
flex: 1 1 0;
|
||||
min-width: 0; /* allow flex item to shrink below intrinsic size */
|
||||
white-space: nowrap;
|
||||
padding: 0.5rem 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
font-size: 11px;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.seg-btn:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.seg-btn:hover {
|
||||
background: #ffffffb3;
|
||||
color: #1f2937;
|
||||
}
|
||||
.seg-active {
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 1px #e5e7eb;
|
||||
}
|
||||
|
||||
.item-row {
|
||||
padding: 0.375rem 0;
|
||||
border-bottom: 1px dashed #e5e7eb;
|
||||
}
|
||||
.item-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.sub {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-weight: 400;
|
||||
}
|
||||
.empty {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-size: 10px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.value {
|
||||
margin-top: 0.125rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,18 @@ import Breadcrumbs from "@/Components/Breadcrumbs.vue";
|
||||
import GlobalSearch from "./Partials/GlobalSearch.vue";
|
||||
import NotificationsBell from "./Partials/NotificationsBell.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMobileScreenButton } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faMobileScreenButton,
|
||||
faGaugeHigh,
|
||||
faLayerGroup,
|
||||
faUserGroup,
|
||||
faFolderOpen,
|
||||
faFileImport,
|
||||
faTableList,
|
||||
faFileCirclePlus,
|
||||
faMap,
|
||||
faGear,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
@@ -173,7 +184,7 @@ const rawMenuGroups = [
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Terensko",
|
||||
label: "Terensko delo",
|
||||
items: [
|
||||
{
|
||||
key: "fieldjobs",
|
||||
@@ -205,6 +216,19 @@ const menuGroups = computed(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
// Icon map for menu keys -> FontAwesome icon definitions
|
||||
const menuIconMap = {
|
||||
dashboard: faGaugeHigh,
|
||||
segments: faLayerGroup,
|
||||
clients: faUserGroup,
|
||||
cases: faFolderOpen,
|
||||
imports: faFileImport,
|
||||
"import-templates": faTableList,
|
||||
"import-templates-new": faFileCirclePlus,
|
||||
fieldjobs: faMap,
|
||||
settings: faGear,
|
||||
};
|
||||
|
||||
function isActive(patterns) {
|
||||
try {
|
||||
return patterns?.some((p) => route().current(p));
|
||||
@@ -267,161 +291,12 @@ function isActive(patterns) {
|
||||
]"
|
||||
:title="item.title"
|
||||
>
|
||||
<!-- Icons -->
|
||||
<template v-if="item.key === 'dashboard'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 12l8.954-8.955a1.125 1.125 0 011.592 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.5c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v4.5h4.125c.621 0 1.125-.504 1.125-1.125V9.75"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'segments'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 3h7v7H3V3zm11 0h7v7h-7V3zM3 14h7v7H3v-7zm11 0h7v7h-7v-7z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'clients'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 19.128a9.38 9.38 0 003.745-.479 3.375 3.375 0 00-6.49-1.072M15 19.128V18a4.5 4.5 0 00-4.5-4.5H8.25A4.5 4.5 0 003.75 18v1.128M15 19.128V21m0-1.872V21M6.75 7.5a3 3 0 116 0 3 3 0 01-6 0z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'cases'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 14.25v-6a2.25 2.25 0 00-2.25-2.25H8.25A2.25 2.25 0 006 8.25v7.5A2.25 2.25 0 008.25 18h9a2.25 2.25 0 002.25-2.25z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9 9h6m-6 3h6m-6 3h3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'imports'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M7.5 10.5L12 6l4.5 4.5M12 6v12"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'import-templates'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 4.5h5.25l1.5 2.25H20.25A1.5 1.5 0 0121.75 8.25v9A2.25 2.25 0 0119.5 19.5H4.5A2.25 2.25 0 012.25 17.25V6A1.5 1.5 0 013.75 4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'import-templates-new'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'fieldjobs'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 10.5c0 7.5-7.5 10.5-7.5 10.5S4.5 18 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'settings'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93l.8.334c.486.203.682.78.4 1.223l-.5.805c-.214.343-.17.784.108 1.09l.596.654c.36.395.37 1.002.024 1.41l-.657.76c-.285.33-.347.79-.158 1.182l.3.65c.216.468-.02 1.02-.507 1.21l-.89.345c-.4.155-.68.52-.74.94l-.12.89c-.08.55-.54.96-1.09.96h-1.09c-.55 0-1.01-.41-1.09-.96l-.12-.89c-.06-.42-.34-.785-.74-.94l-.89-.345c-.49-.19-.72-.74-.507-1.21l.3-.65c.19-.392.127-.852-.158-1.182l-.657-.76a1.125 1.125 0 01-.033-1.58l.596-.654c.278-.306.322-.747.108-1.09l-.5-.805c-.282-.443-.086-1.02.4-1.223l.8-.334c.396-.166.71-.506.78-.93l.149-.894zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
<!-- Unified FontAwesome icon rendering -->
|
||||
<FontAwesomeIcon
|
||||
v-if="menuIconMap[item.key]"
|
||||
:icon="menuIconMap[item.key]"
|
||||
class="w-5 h-5 text-gray-600"
|
||||
/>
|
||||
<!-- Title -->
|
||||
<span v-if="!sidebarCollapsed">{{ item.title }}</span>
|
||||
</Link>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user