Dev branch

This commit is contained in:
Simon Pocrnjič
2025-11-02 12:31:01 +01:00
parent 5f879c9436
commit 63e0958b66
241 changed files with 17686 additions and 7327 deletions
+26 -36
View File
@@ -1,6 +1,7 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import { Head, Link, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -220,10 +221,16 @@ const statusClass = (p) => {
</table>
</div>
<DialogModal :show="createOpen" max-width="2xl" @close="closeCreate">
<template #title> Nov Mail profil </template>
<template #content>
<form @submit.prevent="submitCreate" id="create-mail-profile" class="space-y-5">
<CreateDialog
:show="createOpen"
title="Nov Mail profil"
max-width="2xl"
confirm-text="Shrani"
:processing="form.processing"
@close="closeCreate"
@confirm="submitCreate"
>
<form @submit.prevent="submitCreate" id="create-mail-profile" class="space-y-5">
<div class="grid gap-4 grid-cols-2">
<div class="col-span-1">
<label class="label">Ime</label>
@@ -271,31 +278,19 @@ const statusClass = (p) => {
<input v-model="form.priority" type="number" class="input" />
</div>
</div>
</form>
</template>
<template #footer>
<button
type="button"
@click="closeCreate"
class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50"
>
Prekliči
</button>
<button
form="create-mail-profile"
type="submit"
:disabled="form.processing"
class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50"
>
Shrani
</button>
</template>
</DialogModal>
</form>
</CreateDialog>
<!-- Edit Modal -->
<DialogModal :show="editOpen" max-width="2xl" @close="closeEdit">
<template #title> Uredi Mail profil </template>
<template #content>
<form @submit.prevent="submitEdit" id="edit-mail-profile" class="space-y-5">
<UpdateDialog
:show="editOpen"
title="Uredi Mail profil"
max-width="2xl"
confirm-text="Shrani"
:processing="form.processing"
@close="closeEdit"
@confirm="submitEdit"
>
<form @submit.prevent="submitEdit" id="edit-mail-profile" class="space-y-5">
<div class="grid gap-4 grid-cols-2">
<div>
<label class="label">Ime</label>
@@ -339,13 +334,8 @@ const statusClass = (p) => {
</div>
</div>
<p class="text-xs text-gray-500">Pusti geslo prazno, če želiš obdržati obstoječe.</p>
</form>
</template>
<template #footer>
<button type="button" @click="closeEdit" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Prekliči</button>
<button form="edit-mail-profile" type="submit" :disabled="form.processing" class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50">Shrani</button>
</template>
</DialogModal>
</form>
</UpdateDialog>
</AdminLayout>
</template>
+341 -114
View File
@@ -1,7 +1,7 @@
<script setup>
import AdminLayout from '@/Layouts/AdminLayout.vue'
import { Link, router, useForm } from '@inertiajs/vue3'
import { ref, computed } from 'vue'
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Link, router, useForm } from "@inertiajs/vue3";
import { ref, computed } from "vue";
const props = defineProps({
packages: { type: Object, required: true },
@@ -10,138 +10,228 @@ const props = defineProps({
templates: { type: Array, default: () => [] },
segments: { type: Array, default: () => [] },
clients: { type: Array, default: () => [] },
})
});
function goShow(id) {
router.visit(route('admin.packages.show', id))
router.visit(route("admin.packages.show", id));
}
const showCreate = ref(false)
const createMode = ref('numbers') // 'numbers' | 'contracts'
const showCreate = ref(false);
const createMode = ref("numbers"); // 'numbers' | 'contracts'
const form = useForm({
type: 'sms',
name: '',
description: '',
type: "sms",
name: "",
description: "",
profile_id: null,
sender_id: null,
template_id: null,
delivery_report: false,
body: '',
numbers: '', // one per line
})
body: "",
numbers: "", // one per line
});
const filteredSenders = computed(() => {
if (!form.profile_id) return props.senders
return props.senders.filter(s => s.profile_id === form.profile_id)
})
if (!form.profile_id) return props.senders;
return props.senders.filter((s) => s.profile_id === form.profile_id);
});
function submitCreate() {
const lines = (form.numbers || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
if (!lines.length) return
const lines = (form.numbers || "")
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean);
if (!lines.length) return;
if (!form.profile_id && !form.template_id) {
// require profile if no template/default profile resolution available
alert('Izberi SMS profil ali predlogo.')
return
alert("Izberi SMS profil ali predlogo.");
return;
}
if (!form.template_id && !form.body) {
alert('Vnesi vsebino sporočila ali izberi predlogo.')
return
alert("Vnesi vsebino sporočila ali izberi predlogo.");
return;
}
const payload = {
type: 'sms',
type: "sms",
name: form.name || `SMS paket ${new Date().toLocaleString()}`,
description: form.description || '',
items: lines.map(number => ({
description: form.description || "",
items: lines.map((number) => ({
number,
payload: {
profile_id: form.profile_id,
sender_id: form.sender_id,
template_id: form.template_id,
delivery_report: !!form.delivery_report,
body: (form.body && form.body.trim()) ? form.body.trim() : null,
body: form.body && form.body.trim() ? form.body.trim() : null,
},
})),
}
};
router.post(route('admin.packages.store'), payload, {
router.post(route("admin.packages.store"), payload, {
onSuccess: () => {
form.reset()
showCreate.value = false
router.reload({ only: ['packages'] })
form.reset();
showCreate.value = false;
router.reload({ only: ["packages"] });
},
})
});
}
// Contracts mode state & actions
const contracts = ref({ data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } })
const segmentId = ref(null)
const search = ref('')
const clientId = ref(null)
const onlyMobile = ref(false)
const onlyValidated = ref(false)
const loadingContracts = ref(false)
const selectedContractIds = ref(new Set())
const contracts = ref({
data: [],
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
});
const segmentId = ref(null);
const search = ref("");
const clientId = ref(null);
const onlyMobile = ref(false);
const onlyValidated = ref(false);
const loadingContracts = ref(false);
const selectedContractIds = ref(new Set());
const deletingId = ref(null);
const creatingFromContracts = ref(false);
const allOnPageSelected = computed(() => {
const ids = (contracts.value.data || []).map((c) => c.id);
if (!ids.length) return false;
return ids.every((id) => selectedContractIds.value.has(id));
});
function pageContractIds() {
return (contracts.value.data || []).map((c) => c.id);
}
function toggleSelectAllOnPage() {
const ids = pageContractIds();
if (!ids.length) return;
const set = new Set(selectedContractIds.value);
if (allOnPageSelected.value) {
// deselect all visible
ids.forEach((id) => set.delete(id));
} else {
// select all visible
ids.forEach((id) => set.add(id));
}
selectedContractIds.value = new Set(Array.from(set));
}
async function loadContracts(url = null) {
if (!segmentId.value) {
contracts.value = { data: [], meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
return
contracts.value = {
data: [],
meta: { current_page: 1, last_page: 1, per_page: 25, total: 0 },
};
return;
}
loadingContracts.value = true
loadingContracts.value = true;
try {
const target = url || `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}`
const res = await fetch(target, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
const json = await res.json()
contracts.value = { data: json.data || [], meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 } }
const target =
url ||
`${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
segmentId.value
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
}${onlyMobile.value ? `&only_mobile=1` : ""}${
onlyValidated.value ? `&only_validated=1` : ""
}`;
const res = await fetch(target, {
headers: { "X-Requested-With": "XMLHttpRequest" },
});
const json = await res.json();
contracts.value = {
data: json.data || [],
meta: json.meta || { current_page: 1, last_page: 1, per_page: 25, total: 0 },
};
} finally {
loadingContracts.value = false
loadingContracts.value = false;
}
}
function toggleSelectContract(id) {
const s = selectedContractIds.value
if (s.has(id)) { s.delete(id) } else { s.add(id) }
const s = selectedContractIds.value;
if (s.has(id)) {
s.delete(id);
} else {
s.add(id);
}
// force reactivity
selectedContractIds.value = new Set(Array.from(s))
selectedContractIds.value = new Set(Array.from(s));
}
function clearSelection() {
selectedContractIds.value = new Set()
selectedContractIds.value = new Set();
}
function deletePackage(pkg) {
if (!pkg || pkg.status !== 'draft') return;
if (!confirm(`Izbrišem paket #${pkg.id}?`)) return;
deletingId.value = pkg.id;
router.delete(route('admin.packages.destroy', pkg.id), {
onSuccess: () => {
router.reload({ only: ['packages'] });
},
onFinish: () => {
deletingId.value = null;
},
});
}
function goContractsPage(delta) {
const { current_page } = contracts.value.meta
const nextPage = current_page + delta
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return
const base = `${route('admin.packages.contracts')}?segment_id=${encodeURIComponent(segmentId.value)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ''}${clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ''}${onlyMobile.value ? `&only_mobile=1` : ''}${onlyValidated.value ? `&only_validated=1` : ''}&page=${nextPage}`
loadContracts(base)
const { current_page } = contracts.value.meta;
const nextPage = current_page + delta;
if (nextPage < 1 || nextPage > contracts.value.meta.last_page) return;
const base = `${route("admin.packages.contracts")}?segment_id=${encodeURIComponent(
segmentId.value
)}${search.value ? `&q=${encodeURIComponent(search.value)}` : ""}${
clientId.value ? `&client_id=${encodeURIComponent(clientId.value)}` : ""
}${onlyMobile.value ? `&only_mobile=1` : ""}${
onlyValidated.value ? `&only_validated=1` : ""
}&page=${nextPage}`;
loadContracts(base);
}
function submitCreateFromContracts() {
const ids = Array.from(selectedContractIds.value)
if (!ids.length) return
const ids = Array.from(selectedContractIds.value);
if (!ids.length) return;
// Optional quick client-side sanity: if all selected are from current page and none have phones, warn early.
const visibleById = new Map((contracts.value.data || []).map((c) => [c.id, c]));
const selectedVisible = ids.map((id) => visibleById.get(id)).filter(Boolean);
if (selectedVisible.length && selectedVisible.every((c) => !c?.selected_phone)) {
alert("Za izbrane pogodbe ni mogoče najti prejemnikov (telefonov).");
return;
}
const payload = {
type: 'sms',
type: "sms",
name: form.name || `SMS paket (segment) ${new Date().toLocaleString()}`,
description: form.description || '',
description: form.description || "",
payload: {
profile_id: form.profile_id,
sender_id: form.sender_id,
template_id: form.template_id,
delivery_report: !!form.delivery_report,
body: (form.body && form.body.trim()) ? form.body.trim() : null,
body: form.body && form.body.trim() ? form.body.trim() : null,
},
contract_ids: ids,
}
};
router.post(route('admin.packages.store-from-contracts'), payload, {
creatingFromContracts.value = true;
router.post(route("admin.packages.store-from-contracts"), payload, {
onSuccess: () => {
clearSelection()
showCreate.value = false
router.reload({ only: ['packages'] })
clearSelection();
showCreate.value = false;
router.reload({ only: ["packages"] });
},
})
onError: (errors) => {
// Show the first validation error if present
const first = errors && Object.values(errors)[0];
if (first) {
alert(String(first));
}
},
onFinish: () => {
creatingFromContracts.value = false;
},
});
}
</script>
@@ -149,58 +239,93 @@ function submitCreateFromContracts() {
<AdminLayout title="SMS paketi">
<div class="flex items-center justify-between mb-4">
<h2 class="text-base font-semibold text-gray-800">Seznam paketov</h2>
<button @click="showCreate = !showCreate" class="px-3 py-1.5 rounded bg-indigo-600 text-white text-sm">{{ showCreate ? 'Zapri' : 'Nov paket' }}</button>
<button
@click="showCreate = !showCreate"
class="px-3 py-1.5 rounded bg-indigo-600 text-white text-sm"
>
{{ showCreate ? "Zapri" : "Nov paket" }}
</button>
</div>
<div v-if="showCreate" class="mb-6 rounded border bg-white p-4">
<div class="mb-4 flex items-center gap-4 text-sm">
<label class="inline-flex items-center gap-2">
<input type="radio" value="numbers" v-model="createMode"> Vnos številk
<input type="radio" value="numbers" v-model="createMode" /> Vnos številk
</label>
<label class="inline-flex items-center gap-2">
<input type="radio" value="contracts" v-model="createMode"> Iz pogodb (segment)
<input type="radio" value="contracts" v-model="createMode" /> Iz pogodb
(segment)
</label>
</div>
<div class="grid sm:grid-cols-3 gap-4">
<div>
<label class="block text-xs text-gray-500 mb-1">Profil</label>
<select v-model.number="form.profile_id" class="w-full rounded border-gray-300 text-sm">
<select
v-model.number="form.profile_id"
class="w-full rounded border-gray-300 text-sm"
>
<option :value="null"></option>
<option v-for="p in profiles" :key="p.id" :value="p.id">{{ p.name }}</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Pošiljatelj</label>
<select v-model.number="form.sender_id" class="w-full rounded border-gray-300 text-sm">
<select
v-model.number="form.sender_id"
class="w-full rounded border-gray-300 text-sm"
>
<option :value="null"></option>
<option v-for="s in filteredSenders" :key="s.id" :value="s.id">{{ s.sname }} <span v-if="s.phone_number">({{ s.phone_number }})</span></option>
<option v-for="s in filteredSenders" :key="s.id" :value="s.id">
{{ s.sname }} <span v-if="s.phone_number">({{ s.phone_number }})</span>
</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Predloga</label>
<select v-model.number="form.template_id" class="w-full rounded border-gray-300 text-sm">
<select
v-model.number="form.template_id"
class="w-full rounded border-gray-300 text-sm"
>
<option :value="null"></option>
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
</select>
</div>
<div class="sm:col-span-3">
<label class="block text-xs text-gray-500 mb-1">Vsebina (če ni predloge)</label>
<textarea v-model="form.body" rows="3" class="w-full rounded border-gray-300 text-sm" placeholder="Sporočilo..."></textarea>
<textarea
v-model="form.body"
rows="3"
class="w-full rounded border-gray-300 text-sm"
placeholder="Sporočilo..."
></textarea>
<label class="inline-flex items-center gap-2 mt-2 text-sm text-gray-600">
<input type="checkbox" v-model="form.delivery_report" /> Zahtevaj delivery report
<input type="checkbox" v-model="form.delivery_report" /> Zahtevaj delivery
report
</label>
</div>
<!-- Numbers mode -->
<template v-if="createMode === 'numbers'">
<div class="sm:col-span-3">
<label class="block text-xs text-gray-500 mb-1">Telefonske številke (ena na vrstico)</label>
<textarea v-model="form.numbers" rows="4" class="w-full rounded border-gray-300 text-sm" placeholder="+38640123456
+38640123457"></textarea>
<label class="block text-xs text-gray-500 mb-1"
>Telefonske številke (ena na vrstico)</label
>
<textarea
v-model="form.numbers"
rows="4"
class="w-full rounded border-gray-300 text-sm"
placeholder="+38640123456
+38640123457"
></textarea>
</div>
<div class="sm:col-span-3 flex items-center justify-end gap-2">
<button @click="submitCreate" class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm">Ustvari paket</button>
<button
@click="submitCreate"
class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm"
>
Ustvari paket
</button>
</div>
</template>
@@ -208,14 +333,24 @@ function submitCreateFromContracts() {
<template v-else>
<div>
<label class="block text-xs text-gray-500 mb-1">Segment</label>
<select v-model.number="segmentId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
<select
v-model.number="segmentId"
@change="loadContracts()"
class="w-full rounded border-gray-300 text-sm"
>
<option :value="null"></option>
<option v-for="s in segments" :key="s.id" :value="s.id">{{ s.name }}</option>
<option v-for="s in segments" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Stranka</label>
<select v-model.number="clientId" @change="loadContracts()" class="w-full rounded border-gray-300 text-sm">
<select
v-model.number="clientId"
@change="loadContracts()"
class="w-full rounded border-gray-300 text-sm"
>
<option :value="null"></option>
<option v-for="c in clients" :key="c.id" :value="c.id">{{ c.name }}</option>
</select>
@@ -223,16 +358,26 @@ function submitCreateFromContracts() {
<div class="sm:col-span-2">
<label class="block text-xs text-gray-500 mb-1">Iskanje</label>
<div class="flex gap-2">
<input v-model="search" @keyup.enter="loadContracts()" type="text" class="w-full rounded border-gray-300 text-sm" placeholder="referenca...">
<button @click="loadContracts()" class="px-3 py-1.5 rounded border text-sm">Išči</button>
<input
v-model="search"
@keyup.enter="loadContracts()"
type="text"
class="w-full rounded border-gray-300 text-sm"
placeholder="referenca..."
/>
<button @click="loadContracts()" class="px-3 py-1.5 rounded border text-sm">
Išči
</button>
</div>
</div>
<div class="sm:col-span-3 flex items-center gap-6 text-sm text-gray-700">
<label class="inline-flex items-center gap-2">
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()"> Samo s mobilno številko
<input type="checkbox" v-model="onlyMobile" @change="loadContracts()" />
Samo s mobilno številko
</label>
<label class="inline-flex items-center gap-2">
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()"> Telefonska številka mora biti potrjena
<input type="checkbox" v-model="onlyValidated" @change="loadContracts()" />
Telefonska številka mora biti potrjena
</label>
</div>
<div class="sm:col-span-3">
@@ -240,7 +385,15 @@ function submitCreateFromContracts() {
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr class="text-xs text-gray-500">
<th class="px-3 py-2"></th>
<th class="px-3 py-2 w-8">
<input
type="checkbox"
:checked="allOnPageSelected"
:disabled="!contracts.data?.length"
@change="toggleSelectAllOnPage"
title="Izberi vse na strani"
/>
</th>
<th class="px-3 py-2 text-left">Pogodba</th>
<th class="px-3 py-2 text-left">Primer</th>
<th class="px-3 py-2 text-left">Stranka</th>
@@ -251,49 +404,88 @@ function submitCreateFromContracts() {
<tbody class="divide-y divide-gray-200" v-if="!loadingContracts">
<tr v-for="c in contracts.data" :key="c.id" class="text-sm">
<td class="px-3 py-2">
<input type="checkbox" :checked="selectedContractIds.has(c.id)" @change="toggleSelectContract(c.id)">
<input
type="checkbox"
:checked="selectedContractIds.has(c.id)"
@change="toggleSelectContract(c.id)"
/>
</td>
<td class="px-3 py-2">
<div class="font-mono text-xs text-gray-600">{{ c.uuid }}</div>
<div class="text-xs text-gray-800">{{ c.reference }}</div>
</td>
<td class="px-3 py-2">
<div class="text-xs text-gray-800">{{ c.person?.full_name || '—' }}</div>
<div class="text-xs text-gray-800">
{{ c.person?.full_name || "—" }}
</div>
</td>
<td class="px-3 py-2">
<div class="text-xs text-gray-800">{{ c.client?.name || '—' }}</div>
<div class="text-xs text-gray-800">{{ c.client?.name || "—" }}</div>
</td>
<td class="px-3 py-2">
<div v-if="c.selected_phone" class="text-xs">
{{ c.selected_phone.number }}
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 text-[10px]">{{ c.selected_phone.validated ? 'validated' : 'unverified' }} / {{ c.selected_phone.type || 'unknown' }}</span>
<span
class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 text-[10px]"
>{{ c.selected_phone.validated ? "validated" : "unverified" }} /
{{ c.selected_phone.type || "unknown" }}</span
>
</div>
<div v-else class="text-xs text-gray-500"></div>
</td>
<td class="px-3 py-2 text-xs text-gray-500">{{ c.no_phone_reason || '—' }}</td>
<td class="px-3 py-2 text-xs text-gray-500">
{{ c.no_phone_reason || "—" }}
</td>
</tr>
<tr v-if="!contracts.data?.length">
<td colspan="6" class="px-3 py-8 text-center text-sm text-gray-500">Ni rezultatov.</td>
<tr v-if="!contracts.data?.length">
<td colspan="6" class="px-3 py-8 text-center text-sm text-gray-500">
Ni rezultatov.
</td>
</tr>
</tbody>
<tbody v-else>
<tr><td colspan="6" class="px-3 py-6 text-center text-sm text-gray-500">Nalaganje...</td></tr>
<tr>
<td colspan="6" class="px-3 py-6 text-center text-sm text-gray-500">
Nalaganje...
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3 flex items-center justify-between text-sm">
<div class="text-gray-600">
Prikazano stran {{ contracts.meta.current_page }} od {{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
Prikazano stran {{ contracts.meta.current_page }} od
{{ contracts.meta.last_page }} (skupaj {{ contracts.meta.total }})
</div>
<div class="space-x-2">
<button @click="goContractsPage(-1)" :disabled="contracts.meta.current_page <= 1" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50">Nazaj</button>
<button @click="goContractsPage(1)" :disabled="contracts.meta.current_page >= contracts.meta.last_page" class="px-3 py-1.5 rounded border text-sm disabled:opacity-50">Naprej</button>
<button
@click="goContractsPage(-1)"
:disabled="contracts.meta.current_page <= 1"
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
>
Nazaj
</button>
<button
@click="goContractsPage(1)"
:disabled="contracts.meta.current_page >= contracts.meta.last_page"
class="px-3 py-1.5 rounded border text-sm disabled:opacity-50"
>
Naprej
</button>
</div>
</div>
</div>
<div class="sm:col-span-3 flex items-center justify-end gap-2">
<div class="text-sm text-gray-600 mr-auto">Izbrano: {{ selectedContractIds.size }}</div>
<button @click="submitCreateFromContracts" :disabled="selectedContractIds.size === 0" class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm disabled:opacity-50">Ustvari paket</button>
<div class="text-sm text-gray-600 mr-auto">
Izbrano: {{ selectedContractIds.size }}
</div>
<button
@click="submitCreateFromContracts"
:disabled="selectedContractIds.size === 0 || creatingFromContracts"
class="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm disabled:opacity-50"
>
Ustvari paket
</button>
</div>
</template>
</div>
@@ -319,28 +511,44 @@ function submitCreateFromContracts() {
<tr v-for="p in packages.data" :key="p.id" class="text-sm">
<td class="px-3 py-2">{{ p.id }}</td>
<td class="px-3 py-2 font-mono text-xs text-gray-500">{{ p.uuid }}</td>
<td class="px-3 py-2">{{ p.name ?? '—' }}</td>
<td class="px-3 py-2">{{ p.name ?? "—" }}</td>
<td class="px-3 py-2 uppercase text-xs text-gray-600">{{ p.type }}</td>
<td class="px-3 py-2">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs"
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs"
:class="{
'bg-yellow-50 text-yellow-700': ['queued','running'].includes(p.status),
'bg-yellow-50 text-yellow-700': ['queued', 'running'].includes(
p.status
),
'bg-emerald-50 text-emerald-700': p.status === 'completed',
'bg-rose-50 text-rose-700': p.status === 'failed',
'bg-gray-100 text-gray-600': p.status === 'draft',
}"
>{{ p.status }}</span>
>{{ p.status }}</span
>
</td>
<td class="px-3 py-2">{{ p.total_items }}</td>
<td class="px-3 py-2">{{ p.sent_count }}</td>
<td class="px-3 py-2">{{ p.failed_count }}</td>
<td class="px-3 py-2 text-xs text-gray-500">{{ p.finished_at ?? '—' }}</td>
<td class="px-3 py-2 text-right">
<button @click="goShow(p.id)" class="text-indigo-600 hover:underline text-sm">Odpri</button>
<td class="px-3 py-2 text-xs text-gray-500">{{ p.finished_at ?? "—" }}</td>
<td class="px-3 py-2 text-right space-x-3">
<button @click="goShow(p.id)" class="text-indigo-600 hover:underline text-sm">
Odpri
</button>
<button
v-if="p.status === 'draft'"
@click="deletePackage(p)"
:disabled="deletingId === p.id"
class="text-rose-600 hover:underline text-sm disabled:opacity-50"
>
Izbriši
</button>
</td>
</tr>
<tr v-if="!packages.data?.length">
<td colspan="10" class="px-3 py-8 text-center text-sm text-gray-500">Ni paketov za prikaz.</td>
<td colspan="10" class="px-3 py-8 text-center text-sm text-gray-500">
Ni paketov za prikaz.
</td>
</tr>
</tbody>
</table>
@@ -348,12 +556,31 @@ function submitCreateFromContracts() {
<div class="mt-4 flex items-center justify-between text-sm">
<div class="text-gray-600">
Prikazano {{ packages.from || 0 }}{{ packages.to || 0 }} od {{ packages.total || 0 }}
Prikazano {{ packages.from || 0 }}{{ packages.to || 0 }} od
{{ packages.total || 0 }}
</div>
<div class="space-x-2">
<Link :href="packages.prev_page_url || '#'" :class="['px-3 py-1.5 rounded border', packages.prev_page_url ? 'text-gray-700 hover:bg-gray-50' : 'text-gray-400 cursor-not-allowed']">Nazaj</Link>
<Link :href="packages.next_page_url || '#'" :class="['px-3 py-1.5 rounded border', packages.next_page_url ? 'text-gray-700 hover:bg-gray-50' : 'text-gray-400 cursor-not-allowed']">Naprej</Link>
<Link
:href="packages.prev_page_url || '#'"
:class="[
'px-3 py-1.5 rounded border',
packages.prev_page_url
? 'text-gray-700 hover:bg-gray-50'
: 'text-gray-400 cursor-not-allowed',
]"
>Nazaj</Link
>
<Link
:href="packages.next_page_url || '#'"
:class="[
'px-3 py-1.5 rounded border',
packages.next_page_url
? 'text-gray-700 hover:bg-gray-50'
: 'text-gray-400 cursor-not-allowed',
]"
>Naprej</Link
>
</div>
</div>
</AdminLayout>
</template>
</template>
+13 -12
View File
@@ -1,6 +1,6 @@
<script setup>
import AdminLayout from "@/Layouts/AdminLayout.vue";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import { Head, useForm, router } from "@inertiajs/vue3";
import { ref, watch } from "vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
@@ -191,10 +191,16 @@ const formatDateTime = (s) => (s ? new Date(s).toLocaleString() : "—");
</div>
<!-- Create Profile Modal -->
<DialogModal :show="createOpen" max-width="2xl" @close="() => (createOpen = false)">
<template #title> Nov SMS profil </template>
<template #content>
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-5">
<CreateDialog
:show="createOpen"
title="Nov SMS profil"
max-width="2xl"
confirm-text="Shrani"
:processing="createForm.processing"
@close="() => (createOpen = false)"
@confirm="submitCreate"
>
<form @submit.prevent="submitCreate" id="create-sms-profile" class="space-y-5">
<div class="grid gap-4 grid-cols-2">
<div>
<label class="label">Ime</label>
@@ -216,13 +222,8 @@ const formatDateTime = (s) => (s ? new Date(s).toLocaleString() : "—");
<input v-model="createForm.api_password" type="password" class="input" autocomplete="new-password" />
</div>
</div>
</form>
</template>
<template #footer>
<button type="button" @click="() => (createOpen = false)" class="px-4 py-2 text-sm rounded-md border bg-white hover:bg-gray-50">Prekliči</button>
<button form="create-sms-profile" type="submit" :disabled="createForm.processing" class="px-4 py-2 text-sm rounded-md bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50">Shrani</button>
</template>
</DialogModal>
</form>
</CreateDialog>
<!-- Test Send Modal -->
<DialogModal :show="testOpen" max-width="2xl" @close="() => (testOpen = false)">
+105 -60
View File
@@ -1,12 +1,10 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Checkbox } from '@/Components/ui/checkbox';
import { Button } from '@/Components/ui/button';
defineProps({
canResetPassword: Boolean,
@@ -30,61 +28,108 @@ const submit = () => {
</script>
<template>
<Head title="Log in" />
<Head title="Prijava" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gradient-to-br from-neutral-50 via-white to-primary-50/30">
<div class="w-full sm:max-w-md">
<div class="mb-8 flex justify-center">
<AuthenticationCardLogo />
</div>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
{{ status }}
<div class="w-full sm:max-w-md px-8 py-10 bg-white/80 backdrop-blur-sm shadow-xl border border-neutral-200/50 rounded-2xl overflow-hidden">
<div class="mb-8">
<h1 class="text-3xl font-bold text-neutral-900 mb-2">Dobrodošli nazaj</h1>
<p class="text-sm text-neutral-600">Prijavite se v svoj račun za nadaljevanje</p>
</div>
<div v-if="status" class="mb-6 p-4 rounded-lg bg-success-50 border border-success-200">
<p class="text-sm font-medium text-success-700">
{{ status }}
</p>
</div>
<form @submit.prevent="submit" class="space-y-5">
<div class="space-y-2">
<Label for="email" class="text-sm font-medium text-neutral-700">E-poštni naslov</Label>
<Input
id="email"
v-model="form.email"
type="email"
required
autofocus
autocomplete="username"
placeholder="vas@example.com"
class="h-11 transition-all"
:class="form.errors.email ? 'border-error-500 focus:border-error-500 focus:ring-error-500/20' : 'focus:border-primary-500 focus:ring-primary-500/20'"
/>
<p v-if="form.errors.email" class="text-sm text-error-600 mt-1.5 flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{{ form.errors.email }}
</p>
</div>
<div class="space-y-2">
<Label for="password" class="text-sm font-medium text-neutral-700">Geslo</Label>
<Input
id="password"
v-model="form.password"
type="password"
required
autocomplete="current-password"
placeholder="••••••••"
class="h-11 transition-all"
:class="form.errors.password ? 'border-error-500 focus:border-error-500 focus:ring-error-500/20' : 'focus:border-primary-500 focus:ring-primary-500/20'"
/>
<p v-if="form.errors.password" class="text-sm text-error-600 mt-1.5 flex items-center gap-1.5">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
{{ form.errors.password }}
</p>
</div>
<div class="flex items-center justify-between pt-1">
<div class="flex items-center space-x-2.5">
<Checkbox
id="remember"
v-model="form.remember"
name="remember"
class="data-[state=checked]:bg-primary-600"
/>
<Label for="remember" class="text-sm font-normal text-neutral-600 cursor-pointer select-none">
Zapomni si me
</Label>
</div>
<Link
v-if="canResetPassword"
:href="route('password.request')"
class="text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500/20 focus:ring-offset-2 rounded-md px-2 py-1"
>
Ste pozabili geslo?
</Link>
</div>
<div class="pt-2">
<Button
type="submit"
class="w-full h-11 text-base font-semibold shadow-md hover:shadow-lg transition-all duration-200"
:disabled="form.processing"
>
<span v-if="!form.processing">Prijavi se</span>
<span v-else class="flex items-center gap-2">
<svg class="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Prijavljanje...
</span>
</Button>
</div>
</form>
</div>
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="current-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="block mt-4">
<label class="flex items-center">
<Checkbox v-model:checked="form.remember" name="remember" />
<span class="ms-2 text-sm text-gray-600">Remember me</span>
</label>
</div>
<div class="flex items-center justify-end mt-4">
<Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Forgot your password?
</Link>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Log in
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</div>
</template>
+13 -6
View File
@@ -3,7 +3,9 @@ import AppLayout from "@/Layouts/AppLayout.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import { Link, router } from "@inertiajs/vue3";
import { ref } from "vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import DataTable from "@/Components/DataTable/DataTable.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faFolderOpen } from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
client_cases: Object,
@@ -47,7 +49,9 @@ const fmtDateDMY = (v) => {
<template #title>Primeri</template>
</SectionTitle>
</div>
<DataTableServer
<DataTable
:show-search="true"
:show-page-size="true"
:columns="[
{ key: 'nu', label: 'Št.', sortable: false, class: 'w-40' },
{ key: 'case', label: 'Primer', sortable: false },
@@ -73,11 +77,17 @@ const fmtDateDMY = (v) => {
per_page: client_cases.per_page,
total: client_cases.total,
last_page: client_cases.last_page,
from: client_cases.from,
to: client_cases.to,
links: client_cases.links,
}"
:search="search"
route-name="clientCase"
page-param-name="client-cases-page"
:only-props="['client_cases']"
:empty-icon="faFolderOpen"
empty-text="Ni zadetkov"
empty-description="Ni najdenih primerov. Ustvarite nov primer ali preverite iskalne kriterije."
>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
@@ -119,10 +129,7 @@ const fmtDateDMY = (v) => {
{{ fmtCurrency(row.active_contracts_balance_sum) }}
</div>
</template>
<template #empty>
<div class="p-6 text-center text-gray-500">Ni zadetkov.</div>
</template>
</DataTableServer>
</DataTable>
</div>
<!-- Pagination handled by DataTableServer -->
</div>
@@ -1,12 +1,21 @@
<script setup>
import ActionMessage from "@/Components/ActionMessage.vue";
import BasicButton from "@/Components/buttons/BasicButton.vue";
import DialogModal from "@/Components/DialogModal.vue";
import InputLabel from "@/Components/InputLabel.vue";
import DatePickerField from "@/Components/DatePickerField.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import DatePicker from "@/Components/DatePicker.vue";
import CurrencyInput from "@/Components/CurrencyInput.vue";
import { useForm, usePage } from "@inertiajs/vue3";
import { FwbTextarea } from "flowbite-vue";
import { useForm as useInertiaForm, usePage, router } from "@inertiajs/vue3";
// Note: This form uses Inertia's useForm for API calls but shadcn-vue components for UI
import { Textarea } from "@/Components/ui/textarea";
import { Label } from "@/Components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Checkbox } from "@/Components/ui/checkbox";
import { ref, watch, computed } from "vue";
const props = defineProps({
@@ -15,7 +24,6 @@ const props = defineProps({
actions: { type: Array, default: () => [] },
contractUuid: { type: String, default: null },
phoneMode: { type: Boolean, default: false },
// Prefer passing these from parent (e.g., phone view) to avoid reliance on global page props in teleports
documents: { type: Array, default: null },
contracts: { type: Array, default: null },
});
@@ -31,7 +39,8 @@ const decisions = ref(
const emit = defineEmits(["close", "saved"]);
const close = () => emit("close");
const form = useForm({
// Using Inertia's useForm for API calls, but wrapping fields with shadcn-vue components
const form = useInertiaForm({
due_date: null,
amount: null,
note: "",
@@ -123,7 +132,6 @@ const store = async () => {
onSuccess: () => {
close();
form.reset("due_date", "amount", "note");
// Notify parent to react (e.g., refresh, redirect in phone mode when no contracts left)
emit("saved");
},
});
@@ -180,11 +188,9 @@ const isToday = (val) => {
if (val instanceof Date) {
d = val;
} else if (typeof val === "string") {
// Normalize common MySQL timestamp 'YYYY-MM-DD HH:mm:ss' for Safari/iOS
const s = val.includes("T") ? val : val.replace(" ", "T");
d = new Date(s);
if (isNaN(d.getTime())) {
// Fallback: parse manually as local date
const m = val.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/);
if (m) {
const [_, yy, mm, dd, hh, mi, ss] = m;
@@ -243,7 +249,13 @@ const pageContracts = computed(() => {
if (Array.isArray(props.contracts)) {
return props.contracts;
}
if (props.contracts?.data) {
return props.contracts.data;
}
const propsVal = page?.props?.value || {};
if (propsVal.contracts?.data) {
return propsVal.contracts.data;
}
return Array.isArray(propsVal.contracts) ? propsVal.contracts : [];
});
@@ -273,143 +285,149 @@ watch(
</script>
<template>
<DialogModal :show="show" @close="close">
<template #title>Dodaj aktivnost</template>
<template #content>
<form @submit.prevent="store">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="activityAction" value="Akcija" />
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="activityAction"
ref="activityActionSelect"
v-model="form.action_id"
:disabled="!actions || !actions.length"
>
<option v-for="a in actions" :key="a.id" :value="a.id">{{ a.name }}</option>
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="activityDecision" value="Odločitev" />
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="activityDecision"
ref="activityDecisionSelect"
v-model="form.decision_id"
:disabled="!decisions || !decisions.length"
>
<option v-for="d in decisions" :key="d.id" :value="d.id">{{ d.name }}</option>
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<FwbTextarea
label="Opomba"
id="activityNote"
ref="activityNoteTextarea"
v-model="form.note"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
/>
</div>
<DatePickerField
id="activityDueDate"
label="Datum zapadlosti"
v-model="form.due_date"
format="dd.MM.yyyy"
:enable-time-picker="false"
:auto-position="true"
:teleport-target="'body'"
:inline="false"
:auto-apply="true"
:fixed="false"
:close-on-auto-apply="true"
:close-on-scroll="true"
/>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="activityAmount" value="Znesek" />
<CurrencyInput
id="activityAmount"
ref="activityAmountinput"
v-model="form.amount"
:precision="{ min: 0, max: 4 }"
placeholder="0,00"
/>
</div>
<div class="mt-2" v-if="showSendAutoMail()">
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 text-sm">
<input
type="checkbox"
v-model="form.send_auto_mail"
:disabled="autoMailDisabled"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
/>
<span>Send auto email</span>
</label>
<CreateDialog
:show="show"
title="Dodaj aktivnost"
confirm-text="Shrani"
:processing="form.processing"
@close="close"
@confirm="store"
>
<form @submit.prevent="store">
<div class="space-y-4">
<div class="space-y-2">
<Label>Akcija</Label>
<Select v-model="form.action_id" :disabled="!actions || !actions.length">
<SelectTrigger>
<SelectValue placeholder="Izberi akcijo" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="a in actions" :key="a.id" :value="a.id">
{{ a.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<p v-if="autoMailDisabled" class="mt-1 text-xs text-amber-600">
{{ autoMailDisabledHint }}
</p>
<div v-if="templateAllowsAttachments && form.contract_uuid" class="mt-3">
<label class="inline-flex items-center gap-2">
<input type="checkbox" v-model="form.attach_documents" />
<span class="text-sm">Dodaj priponke iz izbrane pogodbe</span>
</label>
<div
v-if="form.attach_documents"
class="mt-2 border rounded p-2 max-h-48 overflow-auto"
>
<div class="text-xs text-gray-600 mb-2">
Izberite dokumente, ki bodo poslani kot priponke:
<div class="space-y-2">
<Label>Odločitev</Label>
<Select v-model="form.decision_id" :disabled="!decisions || !decisions.length">
<SelectTrigger>
<SelectValue placeholder="Izberi odločitev" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="d in decisions" :key="d.id" :value="d.id">
{{ d.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="activityNote">Opomba</Label>
<Textarea
id="activityNote"
v-model="form.note"
class="block w-full"
placeholder="Opomba"
/>
</div>
<div class="space-y-2">
<Label for="activityDueDate">Datum zapadlosti</Label>
<DatePicker
id="activityDueDate"
v-model="form.due_date"
format="dd.MM.yyyy"
:error="form.errors.due_date"
/>
</div>
<div class="space-y-2">
<Label for="activityAmount">Znesek</Label>
<CurrencyInput
id="activityAmount"
v-model="form.amount"
:precision="{ min: 0, max: 4 }"
placeholder="0,00"
class="w-full"
/>
</div>
<div v-if="showSendAutoMail()" class="space-y-2">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-2">
<Checkbox
v-model="form.send_auto_mail"
:disabled="autoMailDisabled"
/>
<Label class="cursor-pointer">Send auto email</Label>
</div>
<div class="space-y-1">
<template v-for="c in pageContracts" :key="c.uuid || c.id">
<div v-if="c.uuid === form.contract_uuid">
<div class="font-medium text-sm text-gray-700 mb-1">
Pogodba {{ c.reference }}
</div>
<div class="space-y-1">
<div
v-for="doc in availableContractDocs"
:key="doc.uuid || doc.id"
class="flex items-center gap-2 text-sm"
>
<input
type="checkbox"
:value="doc.id"
v-model="form.attachment_document_ids"
/>
<span>{{ doc.original_name || doc.name }}</span>
<span class="text-xs text-gray-400"
>({{ doc.extension?.toUpperCase() || "" }},
{{ (doc.size / 1024 / 1024).toFixed(2) }} MB)</span
</div>
<p v-if="autoMailDisabled" class="text-xs text-amber-600">
{{ autoMailDisabledHint }}
</p>
<div v-if="templateAllowsAttachments && form.contract_uuid" class="mt-3">
<label class="inline-flex items-center gap-2">
<Checkbox v-model="form.attach_documents" />
<span class="text-sm">Dodaj priponke iz izbrane pogodbe</span>
</label>
<div
v-if="form.attach_documents"
class="mt-2 border rounded p-2 max-h-48 overflow-auto"
>
<div class="text-xs text-gray-600 mb-2">
Izberite dokumente, ki bodo poslani kot priponke:
</div>
<div class="space-y-1">
<template v-for="c in pageContracts" :key="c.uuid || c.id">
<div v-if="c.uuid === form.contract_uuid">
<div class="font-medium text-sm text-gray-700 mb-1">
Pogodba {{ c.reference }}
</div>
<div class="space-y-1">
<div
v-for="doc in availableContractDocs"
:key="doc.uuid || doc.id"
class="flex items-center gap-2 text-sm"
>
<Checkbox
:checked="form.attachment_document_ids.includes(doc.id)"
@update:checked="(checked) => {
if (checked) {
if (!form.attachment_document_ids.includes(doc.id)) {
form.attachment_document_ids.push(doc.id);
}
} else {
form.attachment_document_ids = form.attachment_document_ids.filter(id => id !== doc.id);
}
}"
/>
<span>{{ doc.original_name || doc.name }}</span>
<span class="text-xs text-gray-400"
>({{ doc.extension?.toUpperCase() || "" }},
{{ (doc.size / 1024 / 1024).toFixed(2) }} MB)</span
>
</div>
</div>
</div>
</template>
<div
v-if="availableContractDocs.length === 0"
class="text-sm text-gray-500"
>
Ni dokumentov, povezanih s to pogodbo.
</div>
</template>
<div
v-if="availableContractDocs.length === 0"
class="text-sm text-gray-500"
>
Ni dokumentov, povezanih s to pogodbo.
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="form.recentlySuccessful" class="me-3">
<ActionMessage :on="form.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</ActionMessage>
<BasicButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Shrani
</BasicButton>
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
</template>
@@ -1,10 +1,9 @@
<script setup>
import { ref } from "vue";
import { ref, computed } from "vue";
import { Link, router } from "@inertiajs/vue3";
import DataTable from "@/Components/DataTable/DataTable.vue";
import Dropdown from "@/Components/Dropdown.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import SecondaryButton from "@/Components/SecondaryButton.vue";
import DangerButton from "@/Components/DangerButton.vue";
import DeleteDialog from "@/Components/Dialogs/DeleteDialog.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTrash, faEllipsisVertical, faCopy } from "@fortawesome/free-solid-svg-icons";
@@ -17,6 +16,17 @@ const props = defineProps({
edit: Boolean,
});
const columns = computed(() => [
{ key: "decision_dot", label: "", class: "w-[6%]" },
{ key: "contract", label: "Pogodba", class: "w-[14%]" },
{ key: "decision", label: "Odločitev", class: "w-[26%]" },
{ key: "note", label: "Opomba", class: "w-[14%]" },
{ key: "promise", label: "Obljuba", class: "w-[20%]" },
{ key: "user", label: "Dodal", class: "w-[10%]" },
]);
const rows = computed(() => props.activities?.data || []);
const fmtDate = (d) => {
if (!d) return "";
try {
@@ -105,236 +115,212 @@ const copyToClipboard = async (text) => {
<template>
<div class="relative">
<div class="activity-scroll-wrapper max-h-[32rem] overflow-y-auto overflow-x-auto">
<table
class="activity-basic-table min-w-full table-fixed text-left text-sm border-collapse"
<DataTable
:columns="columns"
:rows="rows"
:show-toolbar="true"
:show-pagination="false"
:show-search="false"
:show-page-size="false"
:hoverable="true"
row-key="id"
empty-text="Ni aktivnosti."
class="border-0"
>
<thead>
<tr>
<th></th>
<th>Pogodba</th>
<th>Odločitev</th>
<th>Opomba</th>
<th>Obljuba</th>
<th>Dodal</th>
<th class="w-8" v-if="edit"></th>
</tr>
</thead>
<tbody>
<tr
v-for="row in activities.data"
:key="row.id"
class="border-b last:border-b-0"
>
<td class="py-2 decision-dot text-center">
<span
v-if="row.decision?.color_tag"
class="inline-block h-4 w-4 rounded-full ring-1 ring-gray-300 dark:ring-gray-600"
:style="{ backgroundColor: row.decision?.color_tag }"
:title="row.decision?.color_tag"
aria-hidden="true"
></span>
</td>
<td class="py-2 pr-4 align-top">
<template v-if="row.contract?.reference">
{{ row.contract.reference }}
</template>
<template v-else>
<span class="text-gray-400"></span>
</template>
</td>
<template #toolbar-add>
<slot name="add" />
</template>
<template #cell-decision_dot="{ row }">
<div class="flex justify-center">
<span
v-if="row.decision?.color_tag"
class="inline-block h-4 w-4 rounded-full ring-1 ring-gray-300"
:style="{ backgroundColor: row.decision?.color_tag }"
:title="row.decision?.color_tag"
aria-hidden="true"
></span>
</div>
</template>
<td class="py-2 pr-4 align-top">
<div class="flex flex-col gap-1">
<span
v-if="row.action?.name"
class="inline-block w-fit px-2 py-0.5 rounded text-[10px] font-medium bg-indigo-100 text-indigo-700 tracking-wide uppercase"
>{{ row.action.name }}</span
>
<span class="text-gray-800">{{ row.decision?.name || "" }}</span>
</div>
</td>
<td class="py-2 pr-4 align-top">
<div class="max-w-[280px] whitespace-pre-wrap break-words leading-snug">
<template v-if="row.note && row.note.length <= 60">{{
row.note
}}</template>
<template v-else-if="row.note">
<span>{{ row.note.slice(0, 60) }} </span>
<Dropdown
align="left"
width="56"
:content-classes="['p-2', 'bg-white', 'shadow', 'max-w-xs']"
>
<template #trigger>
<button
type="button"
class="inline-flex items-center text-[11px] text-indigo-600 hover:underline focus:outline-none"
>
Več
</button>
</template>
<template #content>
<div class="relative" @click.stop>
<div
class="flex items-center justify-between p-1 border-b border-gray-200"
>
<span class="text-xs font-medium text-gray-600">Opomba</span>
<button
@click="copyToClipboard(row.note)"
class="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded transition-colors"
title="Kopiraj v odložišče"
>
<FontAwesomeIcon :icon="faCopy" class="text-xs" />
<span>Kopiraj</span>
</button>
</div>
<div
class="max-h-60 overflow-auto text-[12px] whitespace-pre-wrap break-words p-2"
>
{{ row.note }}
</div>
</div>
</template>
</Dropdown>
</template>
<template v-else><span class="text-gray-400"></span></template>
</div>
</td>
<td class="py-2 pr-4 align-top">
<div class="flex flex-col gap-1 text-[12px]">
<div v-if="row.amount && Number(row.amount) !== 0" class="leading-tight">
<span class="text-gray-500">Z:</span
><span class="font-medium ml-1">{{ fmtCurrency(row.amount) }}</span>
</div>
<div v-if="row.due_date" class="leading-tight">
<span class="text-gray-500">D:</span
><span class="ml-1">{{ fmtDate(row.due_date) }}</span>
</div>
<div
v-if="!row.due_date && (!row.amount || Number(row.amount) === 0)"
class="text-gray-400"
>
</div>
</div>
</td>
<td class="py-2 pr-4 align-top">
<div class="text-gray-800 font-medium leading-tight">
{{ row.user?.name || row.user_name || "" }}
</div>
<div v-if="row.created_at" class="mt-1">
<span
class="inline-block px-2 py-0.5 rounded text-[11px] bg-gray-100 text-gray-600 tracking-wide"
>{{ fmtDateTime(row.created_at) }}</span
>
</div>
</td>
<td class="py-2 pl-2 pr-2 align-middle text-right" v-if="edit">
<Dropdown align="right" width="30">
<template #cell-contract="{ row }">
<template v-if="row.contract?.reference">
{{ row.contract.reference }}
</template>
<template v-else>
<span class="text-gray-400"></span>
</template>
</template>
<template #cell-decision="{ row }">
<div class="flex flex-col gap-1">
<span
v-if="row.action?.name"
class="inline-block w-fit px-2 py-0.5 rounded text-[10px] font-medium bg-indigo-100 text-indigo-700 tracking-wide uppercase"
>
{{ row.action.name }}
</span>
<span class="text-gray-800">{{ row.decision?.name || "" }}</span>
</div>
</template>
<template #cell-note="{ row }">
<div class="max-w-[280px] whitespace-pre-wrap break-words leading-snug">
<template v-if="row.note && row.note.length <= 60">
{{ row.note }}
</template>
<template v-else-if="row.note">
<span>{{ row.note.slice(0, 60) }} </span>
<Dropdown
align="left"
width="56"
:content-classes="['p-2', 'bg-white', 'shadow', 'max-w-xs']"
>
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
:title="'Actions'"
class="inline-flex items-center text-[11px] text-indigo-600 hover:underline focus:outline-none"
>
<FontAwesomeIcon
:icon="faEllipsisVertical"
class="h-4 w-4 text-gray-700"
/>
Več
</button>
</template>
<template #content>
<button
type="button"
class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-red-50 text-red-600"
@click.stop="openDelete(row)"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="text-[16px]" />
<span>Izbriši</span>
</button>
<div class="relative" @click.stop>
<div
class="flex items-center justify-between p-1 border-b border-gray-200"
>
<span class="text-xs font-medium text-gray-600">Opomba</span>
<button
@click="copyToClipboard(row.note)"
class="flex items-center gap-1 px-2 py-1 text-xs text-indigo-600 hover:text-indigo-800 hover:bg-indigo-50 rounded transition-colors"
title="Kopiraj v odložišče"
>
<FontAwesomeIcon :icon="faCopy" class="w-3 h-3" />
<span>Kopiraj</span>
</button>
</div>
<div
class="max-h-60 overflow-auto text-[12px] whitespace-pre-wrap break-words p-2"
>
{{ row.note }}
</div>
</div>
</template>
</Dropdown>
</td>
</tr>
<tr v-if="!activities?.data || activities.data.length === 0">
<td :colspan="6" class="py-4 text-gray-500">Ni aktivnosti.</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<span class="text-gray-400"></span>
</template>
</div>
</template>
<template #cell-promise="{ row }">
<div class="flex flex-col gap-1 text-[12px]">
<div v-if="row.amount && Number(row.amount) !== 0" class="leading-tight">
<span class="text-gray-500">Z:</span>
<span class="font-medium ml-1">{{ fmtCurrency(row.amount) }}</span>
</div>
<div v-if="row.due_date" class="leading-tight">
<span class="text-gray-500">D:</span>
<span class="ml-1">{{ fmtDate(row.due_date) }}</span>
</div>
<div
v-if="!row.due_date && (!row.amount || Number(row.amount) === 0)"
class="text-gray-400"
>
</div>
</div>
</template>
<template #cell-user="{ row }">
<div class="text-gray-800 font-medium leading-tight">
{{ row.user?.name || row.user_name || "" }}
</div>
<div v-if="row.created_at" class="mt-1">
<span
class="inline-block px-2 py-0.5 rounded text-[11px] bg-gray-100 text-gray-600 tracking-wide"
>
{{ fmtDateTime(row.created_at) }}
</span>
</div>
</template>
<template #actions="{ row }" v-if="edit">
<Dropdown align="right" width="30">
<template #trigger>
<button
type="button"
class="inline-flex items-center justify-center h-8 w-8 rounded-full hover:bg-gray-100 focus:outline-none"
title="Actions"
>
<FontAwesomeIcon
:icon="faEllipsisVertical"
class="h-4 w-4 text-gray-700"
/>
</button>
</template>
<template #content>
<button
type="button"
class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-red-50 text-red-600"
@click.stop="openDelete(row)"
>
<FontAwesomeIcon :icon="['fas', 'trash']" class="w-4 h-4" />
<span>Izbriši</span>
</button>
</template>
</Dropdown>
</template>
</DataTable>
</div>
</div>
<ConfirmationModal :show="confirmDelete" @close="cancelDelete">
<template #title>Potrditev</template>
<template #content
>Ali ste prepričani, da želite izbrisati to aktivnost? Tega dejanja ni mogoče
razveljaviti.</template
>
<template #footer>
<SecondaryButton type="button" @click="cancelDelete">Prekliči</SecondaryButton>
<DangerButton type="button" class="ml-2" @click="confirmDeleteAction"
>Izbriši</DangerButton
>
</template>
</ConfirmationModal>
<DeleteDialog
:show="confirmDelete"
title="Izbriši aktivnost"
message="Ali ste prepričani, da želite izbrisati to aktivnost?"
confirm-text="Izbriši"
@close="cancelDelete"
@confirm="confirmDeleteAction"
/>
</template>
<style scoped>
.activity-scroll-wrapper {
scrollbar-gutter: stable;
}
.activity-basic-table thead th {
/* Ensure sticky header works within scroll container */
.activity-scroll-wrapper :deep(table) {
border-collapse: separate;
border-spacing: 0;
}
.activity-scroll-wrapper :deep([data-table-container]) {
overflow: visible !important;
}
.activity-scroll-wrapper :deep([data-table-container] > div) {
overflow-x: visible !important;
overflow-y: visible !important;
}
.activity-scroll-wrapper :deep(table thead) {
position: sticky;
top: 0;
z-index: 20;
background: #ffffff;
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.05em;
color: #374151;
padding: 0.5rem 1rem; /* unified horizontal padding */
}
.activity-scroll-wrapper :deep(table thead tr) {
background-color: white;
}
.activity-scroll-wrapper :deep(table thead th) {
background-color: white !important;
position: sticky;
top: 0;
z-index: 20;
box-shadow: 0 1px 0 0 #e5e7eb;
border-bottom: 1px solid #e5e7eb;
}
.activity-basic-table tbody td {
vertical-align: top;
padding: 0.625rem 1rem; /* match header horizontal padding */
}
/* Center the decision dot in its column */
.activity-basic-table td.decision-dot {
vertical-align: middle;
}
/* Ensure first column lines up exactly (no extra offset) */
.activity-basic-table th:first-child,
.activity-basic-table td:first-child {
padding-left: 1rem;
}
.activity-basic-table tbody tr:hover {
background: #f9fafb;
}
/* Column sizing hints (optional fine tuning) */
.activity-basic-table th:nth-child(1),
.activity-basic-table td:nth-child(1) {
width: 6%;
}
.activity-basic-table th:nth-child(2),
.activity-basic-table td:nth-child(2) {
width: 14%;
}
.activity-basic-table th:nth-child(3),
.activity-basic-table td:nth-child(3) {
width: 26%;
}
.activity-basic-table th:nth-child(4),
.activity-basic-table td:nth-child(4) {
width: 14%;
}
.activity-basic-table th:nth-child(5),
.activity-basic-table td:nth-child(5) {
width: 20%;
}
.activity-basic-table th:nth-child(6),
.activity-basic-table td:nth-child(6) {
width: 10%;
}
</style>
@@ -1,10 +1,11 @@
<script setup>
import DialogModal from '@/Components/DialogModal.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import SectionTitle from '@/Components/SectionTitle.vue'
import { useForm } from '@inertiajs/vue3'
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
const props = defineProps({
show: { type: Boolean, default: false },
@@ -36,35 +37,75 @@ const submit = () => {
</script>
<template>
<DialogModal :show="show" @close="close">
<template #title>Dodaj premet</template>
<template #content>
<form @submit.prevent="submit">
<CreateDialog
:show="show"
title="Dodaj premet"
confirm-text="Shrani"
:processing="form.processing"
@close="close"
@confirm="submit"
>
<form @submit.prevent="submit">
<SectionTitle class="mt-2 border-b mb-4">
<template #title>Premet</template>
</SectionTitle>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<InputLabel for="objRef" value="Referenca" />
<TextInput id="objRef" v-model="form.reference" type="text" class="mt-1 block w-full" />
<div class="space-y-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="objRef">Referenca</Label>
<Input
id="objRef"
v-model="form.reference"
type="text"
placeholder="Referenca"
/>
<p v-if="form.errors.reference" class="text-sm text-red-600">
{{ form.errors.reference }}
</p>
</div>
<div class="space-y-2">
<Label for="objType">Tip</Label>
<Input
id="objType"
v-model="form.type"
type="text"
placeholder="Tip"
/>
<p v-if="form.errors.type" class="text-sm text-red-600">
{{ form.errors.type }}
</p>
</div>
</div>
<div>
<InputLabel for="objType" value="Tip" />
<TextInput id="objType" v-model="form.type" type="text" class="mt-1 block w-full" />
<div class="space-y-2">
<Label for="objName">Naziv</Label>
<Input
id="objName"
v-model="form.name"
type="text"
placeholder="Naziv"
required
/>
<p v-if="form.errors.name" class="text-sm text-red-600">
{{ form.errors.name }}
</p>
</div>
</div>
<div class="mt-4">
<InputLabel for="objName" value="Naziv" />
<TextInput id="objName" v-model="form.name" type="text" class="mt-1 block w-full" required />
</div>
<div class="mt-4">
<InputLabel for="objDesc" value="Opis" />
<textarea id="objDesc" v-model="form.description" class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm" rows="3" />
</div>
<div class="flex justify-end mt-6">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">Shrani</PrimaryButton>
<div class="space-y-2">
<Label for="objDesc">Opis</Label>
<Textarea
id="objDesc"
v-model="form.description"
rows="3"
placeholder="Opis"
/>
<p v-if="form.errors.description" class="text-sm text-red-600">
{{ form.errors.description }}
</p>
</div>
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
</template>
@@ -92,7 +92,7 @@ const deleteObject = (o) => {
:title="'Prekliči'"
@click.stop="confirmingId = null"
>
<FontAwesomeIcon :icon="['fas', 'xmark']" class="text-[16px]" />
<FontAwesomeIcon :icon="['fas', 'xmark']" class="w-4 h-4" />
</button>
<button
type="button"
@@ -100,7 +100,7 @@ const deleteObject = (o) => {
:title="'Potrdi brisanje'"
@click.stop="deleteObject(o)"
>
<FontAwesomeIcon :icon="['fas', 'check']" class="text-[16px]" />
<FontAwesomeIcon :icon="['fas', 'check']" class="w-4 h-4" />
</button>
</template>
<template v-else>
@@ -1,15 +1,22 @@
<script setup>
import ActionMessage from "@/Components/ActionMessage.vue";
import DialogModal from "@/Components/DialogModal.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import TextInput from "@/Components/TextInput.vue";
import CurrencyInput from "@/Components/CurrencyInput.vue";
import DatePickerField from "@/Components/DatePickerField.vue";
import DatePicker from "@/Components/DatePicker.vue";
import { useForm, router } from "@inertiajs/vue3";
import { watch, nextTick, ref as vRef } from "vue";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import { Textarea } from "@/Components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
client_case: Object,
@@ -20,8 +27,6 @@ const props = defineProps({
contract: { type: Object, default: null },
});
console.log(props.types);
const emit = defineEmits(["close"]);
const close = () => {
@@ -155,11 +160,128 @@ const storeOrUpdate = () => {
</script>
<template>
<DialogModal :show="show" @close="close">
<template #title>{{
formContract.uuid ? "Uredi pogodbo" : "Dodaj pogodbo"
}}</template>
<template #content>
<CreateDialog
v-if="!formContract.uuid"
:show="show"
title="Dodaj pogodbo"
confirm-text="Shrani"
:processing="formContract.processing"
@close="close"
@confirm="storeOrUpdate"
>
<form @submit.prevent="storeOrUpdate">
<div
v-if="formContract.errors.reference"
class="mb-4 rounded border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700"
>
{{ formContract.errors.reference }}
</div>
<SectionTitle class="mt-4 border-b mb-4">
<template #title> Pogodba </template>
</SectionTitle>
<div class="space-y-4">
<div class="space-y-2">
<Label for="contractRef">Referenca</Label>
<Input
id="contractRef"
ref="contractRefInput"
v-model="formContract.reference"
type="text"
:class="[
formContract.errors.reference
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: '',
]"
autocomplete="contract-reference"
/>
<p v-if="formContract.errors.reference" class="text-sm text-red-600">
{{ formContract.errors.reference }}
</p>
</div>
<div class="space-y-2">
<Label for="contractStartDate">Datum pričetka</Label>
<DatePicker
id="contractStartDate"
v-model="formContract.start_date"
format="dd.MM.yyyy"
:error="formContract.errors.start_date"
/>
</div>
<div class="space-y-2">
<Label for="contractTypeSelect">Tip</Label>
<Select v-model="formContract.type_id">
<SelectTrigger>
<SelectValue placeholder="Izberi tip" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="t in types" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="contractDescription">Opis</Label>
<Textarea
id="contractDescription"
v-model="formContract.description"
rows="3"
placeholder="Opis"
/>
</div>
<SectionTitle class="mt-6 border-b mb-4">
<template #title> Račun </template>
</SectionTitle>
<div class="space-y-2">
<Label for="accountTypeSelect">Tip računa</Label>
<Select v-model="formContract.account_type_id">
<SelectTrigger>
<SelectValue placeholder="Izberi tip računa" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="at in account_types" :key="at.id" :value="at.id">
{{ at.name ?? "#" + at.id }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="formContract.errors.account_type_id" class="text-sm text-red-600">
{{ formContract.errors.account_type_id }}
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="initialAmount">Predani znesek</Label>
<CurrencyInput id="initialAmount" v-model="formContract.initial_amount" />
</div>
<div class="space-y-2">
<Label for="balanceAmount">Odprti znesek</Label>
<CurrencyInput id="balanceAmount" v-model="formContract.balance_amount" />
</div>
</div>
<ActionMessage :on="formContract.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</ActionMessage>
</div>
</form>
</CreateDialog>
<UpdateDialog
v-else
:show="show"
title="Uredi pogodbo"
confirm-text="Posodobi"
:processing="formContract.processing"
@close="close"
@confirm="storeOrUpdate"
>
<form @submit.prevent="storeOrUpdate">
<div
v-if="formContract.errors.reference"
@@ -170,90 +292,98 @@ const storeOrUpdate = () => {
<SectionTitle class="mt-4 border-b mb-4">
<template #title> Pogodba </template>
</SectionTitle>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="contractRef" value="Referenca" />
<TextInput
id="contractRef"
ref="contractRefInput"
v-model="formContract.reference"
type="text"
:class="[
'mt-1 block w-full',
formContract.errors.reference
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: '',
]"
autocomplete="contract-reference"
/>
<InputError :message="formContract.errors.reference" />
</div>
<DatePickerField
id="contractStartDate"
label="Datum pričetka"
v-model="formContract.start_date"
format="dd.MM.yyyy"
:enable-time-picker="false"
/>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="contractTypeSelect" value="Tip" />
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="contractTypeSelect"
v-model="formContract.type_id"
>
<option v-for="t in types" :value="t.id">{{ t.name }}</option>
<!-- ... -->
</select>
</div>
<div class="col-span-6 sm:col-span-4 mt-4">
<InputLabel for="contractDescription" value="Opis" />
<textarea
id="contractDescription"
v-model="formContract.description"
class="mt-1 block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
rows="3"
/>
</div>
<SectionTitle class="mt-6 border-b mb-4">
<template #title> Račun </template>
</SectionTitle>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="accountTypeSelect" value="Tip računa" />
<select
id="accountTypeSelect"
v-model="formContract.account_type_id"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
>
<option :value="null"></option>
<option v-for="at in account_types" :key="at.id" :value="at.id">
{{ at.name ?? "#" + at.id }}
</option>
</select>
<InputError :message="formContract.errors.account_type_id" />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<InputLabel for="initialAmount" value="Predani znesek" />
<CurrencyInput id="initialAmount" v-model="formContract.initial_amount" />
<div class="space-y-4">
<div class="space-y-2">
<Label for="contractRef">Referenca</Label>
<Input
id="contractRef"
ref="contractRefInput"
v-model="formContract.reference"
type="text"
:class="[
formContract.errors.reference
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: '',
]"
autocomplete="contract-reference"
/>
<p v-if="formContract.errors.reference" class="text-sm text-red-600">
{{ formContract.errors.reference }}
</p>
</div>
<div>
<InputLabel for="balanceAmount" value="Odprti znesek" />
<CurrencyInput id="balanceAmount" v-model="formContract.balance_amount" />
<div class="space-y-2">
<Label for="contractStartDate">Datum pričetka</Label>
<DatePicker
id="contractStartDate"
v-model="formContract.start_date"
format="dd.MM.yyyy"
:error="formContract.errors.start_date"
/>
</div>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="formContract.recentlySuccessful" class="me-3">
<div class="space-y-2">
<Label for="contractTypeSelect">Tip</Label>
<Select v-model="formContract.type_id">
<SelectTrigger>
<SelectValue placeholder="Izberi tip" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="t in types" :key="t.id" :value="t.id">
{{ t.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="contractDescription">Opis</Label>
<Textarea
id="contractDescription"
v-model="formContract.description"
rows="3"
placeholder="Opis"
/>
</div>
<SectionTitle class="mt-6 border-b mb-4">
<template #title> Račun </template>
</SectionTitle>
<div class="space-y-2">
<Label for="accountTypeSelect">Tip računa</Label>
<Select v-model="formContract.account_type_id">
<SelectTrigger>
<SelectValue placeholder="Izberi tip računa" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="null"></SelectItem>
<SelectItem v-for="at in account_types" :key="at.id" :value="at.id">
{{ at.name ?? "#" + at.id }}
</SelectItem>
</SelectContent>
</Select>
<p v-if="formContract.errors.account_type_id" class="text-sm text-red-600">
{{ formContract.errors.account_type_id }}
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="initialAmount">Predani znesek</Label>
<CurrencyInput id="initialAmount" v-model="formContract.initial_amount" />
</div>
<div class="space-y-2">
<Label for="balanceAmount">Odprti znesek</Label>
<CurrencyInput id="balanceAmount" v-model="formContract.balance_amount" />
</div>
</div>
<ActionMessage :on="formContract.recentlySuccessful" class="text-sm text-green-600">
Shranjuje.
</ActionMessage>
<PrimaryButton
:class="{ 'opacity-25': formContract.processing }"
:disabled="formContract.processing"
>
{{ formContract.uuid ? "Posodobi" : "Shrani" }}
</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
</UpdateDialog>
</template>
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,9 @@
<script setup>
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import CurrencyInput from "@/Components/CurrencyInput.vue";
import { Label } from "@/Components/ui/label";
import { Input } from "@/Components/ui/input";
import DatePicker from "@/Components/DatePicker.vue";
const props = defineProps({
show: { type: Boolean, default: false },
@@ -14,12 +17,17 @@ const onSubmit = () => emit("submit");
</script>
<template>
<DialogModal :show="show" @close="onClose">
<template #title>Dodaj plačilo</template>
<template #content>
<div class="space-y-3">
<div>
<label class="block text-sm text-gray-700 mb-1">Znesek</label>
<CreateDialog
:show="show"
title="Dodaj plačilo"
confirm-text="Shrani"
:processing="form.processing"
@close="onClose"
@confirm="onSubmit"
>
<div class="space-y-4">
<div class="space-y-2">
<Label for="paymentAmount">Znesek</Label>
<CurrencyInput
id="paymentAmount"
v-model="form.amount"
@@ -27,64 +35,48 @@ const onSubmit = () => emit("submit");
placeholder="0,00"
class="w-full"
/>
<div v-if="form.errors?.amount" class="text-sm text-red-600 mt-0.5">
<p v-if="form.errors?.amount" class="text-sm text-red-600">
{{ form.errors.amount }}
</div>
</p>
</div>
<div class="flex gap-2">
<div class="flex-1">
<label class="block text-sm text-gray-700 mb-1">Valuta</label>
<input
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="paymentCurrency">Valuta</Label>
<Input
id="paymentCurrency"
type="text"
v-model="form.currency"
class="w-full rounded border-gray-300"
maxlength="3"
placeholder="EUR"
/>
<div v-if="form.errors?.currency" class="text-sm text-red-600 mt-0.5">
<p v-if="form.errors?.currency" class="text-sm text-red-600">
{{ form.errors.currency }}
</div>
</p>
</div>
<div class="flex-1">
<label class="block text-sm text-gray-700 mb-1">Datum</label>
<input
type="date"
<div class="space-y-2">
<Label for="paymentDate">Datum</Label>
<DatePicker
id="paymentDate"
v-model="form.paid_at"
class="w-full rounded border-gray-300"
format="dd.MM.yyyy"
:error="form.errors?.paid_at"
/>
<div v-if="form.errors?.paid_at" class="text-sm text-red-600 mt-0.5">
{{ form.errors.paid_at }}
</div>
</div>
</div>
<div>
<label class="block text-sm text-gray-700 mb-1">Sklic</label>
<input
<div class="space-y-2">
<Label for="paymentReference">Sklic</Label>
<Input
id="paymentReference"
type="text"
v-model="form.reference"
class="w-full rounded border-gray-300"
placeholder="Sklic"
/>
<div v-if="form.errors?.reference" class="text-sm text-red-600 mt-0.5">
<p v-if="form.errors?.reference" class="text-sm text-red-600">
{{ form.errors.reference }}
</div>
</p>
</div>
</div>
</template>
<template #footer>
<button
type="button"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300"
@click="onClose"
>
Prekliči
</button>
<button
type="button"
class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
:disabled="form.processing"
@click="onSubmit"
>
Shrani
</button>
</template>
</DialogModal>
</CreateDialog>
</template>
+107 -55
View File
@@ -1,8 +1,8 @@
<script setup>
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
import PersonInfoGrid from "@/Components/PersonInfo/PersonInfoGrid.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import AppLayout from "@/Layouts/AppLayout.vue";
import { FwbButton } from "flowbite-vue";
import { Button } from "@/Components/ui/button";
import { onBeforeMount, ref, computed } from "vue";
import ContractDrawer from "./Partials/ContractDrawer.vue";
import ContractTable from "./Partials/ContractTable.vue";
@@ -16,14 +16,16 @@ import { classifyDocument } from "@/Services/documents";
import { router, useForm, usePage } from "@inertiajs/vue3";
import { AngleDownIcon, AngleUpIcon } from "@/Utilities/Icons";
import Pagination from "@/Components/Pagination.vue";
import ConfirmDialog from "@/Components/ConfirmDialog.vue";
import DialogModal from "@/Components/DialogModal.vue";
import DeleteDialog from "@/Components/Dialogs/DeleteDialog.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import { hasPermission } from "@/Services/permissions";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
client: Object,
client_case: Object,
contracts: Array,
contracts: [Object, Array], // Can be paginated object or array (backward compatibility)
activities: Object,
contract_types: Array,
account_types: { type: Array, default: () => [] },
@@ -36,6 +38,23 @@ const props = defineProps({
contract_doc_templates: { type: Array, default: () => [] },
});
// Extract contracts array from paginated object or use array directly
const contractsArray = computed(() => {
if (Array.isArray(props.contracts)) {
return props.contracts;
}
// Handle paginated contracts
if (props.contracts?.data) {
return props.contracts.data;
}
return [];
});
// Check if contracts are paginated
const contractsPaginated = computed(() => {
return props.contracts && !Array.isArray(props.contracts) && props.contracts.data;
});
const page = usePage();
const showUpload = ref(false);
const openUpload = () => {
@@ -49,6 +68,21 @@ const onUploaded = () => {
router.reload({ only: ["documents"] });
};
// Helper to reload contracts while preserving pagination and segment filter
const reloadContracts = () => {
const params = {};
try {
const url = new URL(window.location.href);
const seg = url.searchParams.get("segment");
if (seg) params.segment = seg;
if (contractsPaginated.value) {
const contractsPage = url.searchParams.get("contracts_page");
if (contractsPage) params.contracts_page = contractsPage;
}
} catch (e) {}
router.reload({ only: ["contracts"], data: params });
};
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
const hasPerm = computed(() => (permission) =>
hasPermission(page.props.auth?.user, permission)
@@ -139,12 +173,17 @@ const closeConfirmDelete = () => {
const doDeleteContract = () => {
const c = confirmDelete.value.contract;
if (!c) return closeConfirmDelete();
// Keep segment filter in redirect
// Keep segment filter and pagination in redirect
const params = {};
try {
const url = new URL(window.location.href);
const seg = url.searchParams.get("segment");
if (seg) params.segment = seg;
// Keep contracts page if paginated
if (contractsPaginated.value) {
const contractsPage = url.searchParams.get("contracts_page");
if (contractsPage) params.contracts_page = contractsPage;
}
} catch (e) {}
router.delete(
route("clientCase.contract.delete", {
@@ -155,6 +194,8 @@ const doDeleteContract = () => {
{
preserveScroll: true,
onFinish: () => closeConfirmDelete(),
// Reload contracts after delete to refresh pagination
only: contractsPaginated.value ? ["contracts"] : undefined,
}
);
};
@@ -302,9 +343,9 @@ const submitAttachSegment = () => {
<template #title> Pogodbe </template>
</SectionTitle>
<div class="flex items-center gap-2" v-if="hasPerm('contract-edit')">
<FwbButton @click="openDrawerCreateContract">Nova</FwbButton>
<FwbButton
color="light"
<Button @click="openDrawerCreateContract">Nova</Button>
<Button
variant="outline"
:disabled="availableSegments.length === 0"
@click="openAttachSegment"
>
@@ -313,13 +354,13 @@ const submitAttachSegment = () => {
? "Dodaj segment"
: "Ni razpoložljivih segmentov"
}}
</FwbButton>
</Button>
</div>
</div>
<ContractTable
:client="client"
:client_case="client_case"
:contracts="contracts"
:contracts="contractsArray"
:contract_types="contract_types"
:segments="segments"
:templates="contract_doc_templates"
@@ -329,6 +370,14 @@ const submitAttachSegment = () => {
@delete="requestDeleteContract"
@add-activity="openDrawerAddActivity"
/>
<div v-if="contractsPaginated" class="border-t border-gray-200">
<Pagination
:links="contracts.links"
:from="contracts.from"
:to="contracts.to"
:total="contracts.total"
/>
</div>
</div>
</div>
</div>
@@ -342,13 +391,20 @@ const submitAttachSegment = () => {
<SectionTitle>
<template #title>Aktivnosti</template>
</SectionTitle>
<FwbButton @click="openDrawerAddActivity">Nova</FwbButton>
</div>
<ActivityTable
:client_case="client_case"
:activities="activities"
:edit="hasPerm('activity-edit')"
/>
>
<template #add>
<ActionMenuItem
label="Nova aktivnost"
:icon="faPlus"
@click="openDrawerAddActivity"
/>
</template>
</ActivityTable>
<Pagination
:links="activities.links"
:from="activities.from"
@@ -368,7 +424,7 @@ const submitAttachSegment = () => {
<SectionTitle>
<template #title>Dokumenti</template>
</SectionTitle>
<FwbButton @click="openUpload">Dodaj</FwbButton>
<Button @click="openUpload">Dodaj</Button>
</div>
<DocumentsTable
:documents="documents"
@@ -401,13 +457,13 @@ const submitAttachSegment = () => {
@close="closeUpload"
@uploaded="onUploaded"
:post-url="route('clientCase.document.store', client_case)"
:contracts="contracts"
:contracts="contractsArray"
/>
<DocumentEditDialog
:show="showDocEdit"
:client_case_uuid="client_case.uuid"
:document="editingDoc"
:contracts="contracts"
:contracts="contractsArray"
@close="closeDocEdit"
@saved="onDocSaved"
v-if="hasPerm('doc-edit')"
@@ -427,51 +483,47 @@ const submitAttachSegment = () => {
:client_case="client_case"
:contract="contractEditing"
/>
<ActivityDrawer
:show="drawerAddActivity"
@close="closeDrawer"
:client_case="client_case"
:actions="actions"
:contract-uuid="activityContractUuid"
:documents="documents"
:contracts="contracts"
/>
<ConfirmDialog
<ActivityDrawer
:show="drawerAddActivity"
@close="closeDrawer"
:client_case="client_case"
:actions="actions"
:contract-uuid="activityContractUuid"
:documents="documents"
:contracts="contractsArray"
/>
<DeleteDialog
:show="confirmDelete.show"
title="Izbriši pogodbo"
message="Ali ste prepričani, da želite izbrisati pogodbo?"
confirm-text="Izbriši"
:danger="true"
:processing="false"
@close="closeConfirmDelete"
@confirm="doDeleteContract"
/>
<DialogModal :show="showAttachSegment" @close="closeAttachSegment">
<template #title>Dodaj segment k primeru</template>
<template #content>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Segment</label>
<select
v-model="attachForm.segment_id"
class="w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
>
<option :value="null" disabled>-- izberi segment --</option>
<option v-for="s in availableSegments" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
<div v-if="attachForm.errors.segment_id" class="text-sm text-red-600">
{{ attachForm.errors.segment_id }}
</div>
</div>
</template>
<template #footer>
<FwbButton color="light" @click="closeAttachSegment">Prekliči</FwbButton>
<FwbButton
class="ml-2"
:disabled="attachForm.processing || !attachForm.segment_id"
@click="submitAttachSegment"
>Dodaj</FwbButton
<CreateDialog
:show="showAttachSegment"
title="Dodaj segment k primeru"
confirm-text="Dodaj"
:processing="attachForm.processing"
:disabled="!attachForm.segment_id"
@close="closeAttachSegment"
@confirm="submitAttachSegment"
>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Segment</label>
<select
v-model="attachForm.segment_id"
class="w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
>
</template>
</DialogModal>
<option :value="null" disabled>-- izberi segment --</option>
<option v-for="s in availableSegments" :key="s.id" :value="s.id">
{{ s.name }}
</option>
</select>
<div v-if="attachForm.errors.segment_id" class="text-sm text-red-600">
{{ attachForm.errors.segment_id }}
</div>
</div>
</CreateDialog>
</template>
+106 -97
View File
@@ -2,9 +2,20 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { ref } from "vue";
import { Link, router } from "@inertiajs/vue3";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
import DataTable from "@/Components/DataTable/DataTable.vue";
import { Button } from "@/Components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import PersonInfoGrid from "@/Components/PersonInfo/PersonInfoGrid.vue";
import SectionTitle from "@/Components/SectionTitle.vue";
import DateRangePicker from "@/Components/DateRangePicker.vue";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { ButtonGroup } from "@/Components/ui/button-group";
const props = defineProps({
client: Object,
@@ -14,21 +25,24 @@ const props = defineProps({
types: Object,
});
const fromDate = ref(props.filters?.from || "");
const toDate = ref(props.filters?.to || "");
const dateRange = ref({
start: props.filters?.from || null,
end: props.filters?.to || null,
});
const search = ref(props.filters?.search || "");
const selectedSegment = ref(props.filters?.segment || "");
const selectedSegment = ref(props.filters?.segment || null);
function applyDateFilter() {
const params = Object.fromEntries(
new URLSearchParams(window.location.search).entries()
);
if (fromDate.value) {
params.from = fromDate.value;
if (dateRange.value?.start) {
params.from = dateRange.value.start;
} else {
delete params.from;
}
if (toDate.value) {
params.to = toDate.value;
if (dateRange.value?.end) {
params.to = dateRange.value.end;
} else {
delete params.to;
}
@@ -38,7 +52,7 @@ function applyDateFilter() {
delete params.search;
}
if (selectedSegment.value) {
params.segment = selectedSegment.value;
params.segment = String(selectedSegment.value);
} else {
delete params.segment;
}
@@ -47,13 +61,22 @@ function applyDateFilter() {
preserveState: true,
replace: true,
preserveScroll: true,
only: ['contracts'],
});
}
function clearDateFilter() {
fromDate.value = "";
toDate.value = "";
selectedSegment.value = "";
dateRange.value = { start: null, end: null };
selectedSegment.value = null;
applyDateFilter();
}
function handleDateRangeUpdate() {
applyDateFilter();
}
function handleSegmentChange(value) {
selectedSegment.value = value;
applyDateFilter();
}
@@ -101,36 +124,6 @@ function formatDate(value) {
</template>
</SectionTitle>
</div>
<nav class="mt-2 border-b border-gray-200">
<ul class="flex gap-6 -mb-px">
<li>
<Link
:href="route('client.show', { uuid: client.uuid })"
:class="[
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.show')
? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300'
]"
>
Primeri
</Link>
</li>
<li>
<Link
:href="route('client.contracts', { uuid: client.uuid })"
:class="[
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.contracts')
? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300'
]"
>
Pogodbe
</Link>
</li>
</ul>
</nav>
</div>
</div>
</div>
@@ -154,56 +147,31 @@ function formatDate(value) {
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3">
<div
class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between"
>
<div class="flex items-center gap-3 flex-wrap">
<label class="font-medium mr-2">Filtri:</label>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Od</span>
<input
type="date"
v-model="fromDate"
@change="applyDateFilter"
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm"
/>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Do</span>
<input
type="date"
v-model="toDate"
@change="applyDateFilter"
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm"
/>
</div>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">Segment</span>
<select
v-model="selectedSegment"
@change="applyDateFilter"
class="rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2 text-sm"
>
<option value="">Vsi segmenti</option>
<option v-for="segment in segments" :key="segment.id" :value="segment.id">
{{ segment.name }}
</option>
</select>
</div>
<button
type="button"
class="inline-flex items-center px-3 py-2 text-sm font-medium rounded border border-gray-300 text-gray-700 hover:bg-gray-50 disabled:opacity-50"
:disabled="!fromDate && !toDate && !selectedSegment"
@click="clearDateFilter"
title="Počisti filtre"
<div class="mb-4">
<ButtonGroup>
<Button
as-child
:variant="route().current('client.show') ? 'default' : 'ghost'"
>
Počisti
</button>
</div>
<!-- Search lives in DataTable toolbar -->
<Link :href="route('client.show', { uuid: client.uuid })">
Primeri
</Link>
</Button>
<Button
as-child
:variant="route().current('client.contracts') ? 'default' : 'ghost'"
>
<Link :href="route('client.contracts', { uuid: client.uuid })">
Pogodbe
</Link>
</Button>
</ButtonGroup>
</div>
<DataTableServer
class="mt-3"
<DataTable
:show-search="true"
:show-page-size="true"
:show-filters="true"
:has-active-filters="!!(dateRange?.start || dateRange?.end || selectedSegment)"
:columns="[
{ key: 'reference', label: 'Referenca', sortable: false },
{ key: 'customer', label: 'Stranka', sortable: false },
@@ -212,14 +180,58 @@ function formatDate(value) {
{ key: 'balance', label: 'Stanje', sortable: false, align: 'right' },
]"
:rows="contracts.data || []"
:meta="{ current_page: contracts.current_page, per_page: contracts.per_page, total: contracts.total, last_page: contracts.last_page }"
:meta="{ current_page: contracts.current_page, per_page: contracts.per_page, total: contracts.total, last_page: contracts.last_page, from: contracts.from, to: contracts.to, links: contracts.links }"
route-name="client.contracts"
:route-params="{ uuid: client.uuid }"
:query="{ from: fromDate || undefined, to: toDate || undefined, segment: selectedSegment || undefined }"
:search="search"
row-key="uuid"
:only-props="['contracts']"
>
<template #toolbar-filters>
<div class="space-y-4">
<div class="space-y-2">
<label class="text-sm font-medium">Datumska območja</label>
<DateRangePicker
v-model="dateRange"
format="dd.MM.yyyy"
@update:model-value="handleDateRangeUpdate"
placeholder="Izberi datumska območja"
/>
</div>
<div class="space-y-2">
<label class="text-sm font-medium">Segment</label>
<Select
:model-value="selectedSegment"
@update:model-value="handleSegmentChange"
>
<SelectTrigger class="w-full">
<SelectValue placeholder="Vsi segmenti" />
</SelectTrigger>
<SelectContent class="w-[var(--radix-select-trigger-width)]">
<SelectItem :value="null">Vsi segmenti</SelectItem>
<SelectItem
v-for="segment in segments"
:key="segment.id"
:value="String(segment.id)"
>
{{ segment.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="flex justify-end pt-2 border-t">
<Button
type="button"
variant="outline"
size="sm"
:disabled="!dateRange?.start && !dateRange?.end && !selectedSegment"
@click="clearDateFilter"
>
Počisti
</Button>
</div>
</div>
</template>
<template #cell-reference="{ row }">
<Link :href="route('clientCase.show', caseShowParams(row))" class="text-indigo-600 hover:underline">
{{ row.reference }}
@@ -239,10 +251,7 @@ function formatDate(value) {
{{ new Intl.NumberFormat('sl-SI', { style: 'currency', currency: 'EUR' }).format(Number(row.account?.balance_amount ?? 0)) }}
</div>
</template>
<template #empty>
<div class="p-6 text-center text-gray-500">Ni zadetkov.</div>
</template>
</DataTableServer>
</DataTable>
</div>
<!-- Pagination handled by DataTableServer -->
</div>
+292 -180
View File
@@ -1,14 +1,34 @@
<script setup>
import { computed, ref } from "vue";
import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import { Link, useForm, router, usePage } from "@inertiajs/vue3";
import ActionMessage from "@/Components/ActionMessage.vue";
import DialogModal from "@/Components/DialogModal.vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import { Link, router, usePage } from "@inertiajs/vue3";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import DataTable from "@/Components/DataTable/DataTable.vue";
import { hasPermission } from "@/Services/permissions";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faUserGroup } from "@fortawesome/free-solid-svg-icons";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { useForm } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import ActionMessage from "@/Components/ActionMessage.vue";
const props = defineProps({
clients: Object,
@@ -21,27 +41,47 @@ const hasPerm = computed(() => (permission) =>
hasPermission(page.props.auth?.user, permission)
);
const Address = {
address: "",
country: "",
type_id: 1,
};
const Phone = {
nu: "",
country_code: "00386",
type_id: 1,
};
const formSchema = toTypedSchema(
z.object({
first_name: z.string().optional(),
last_name: z.string().optional(),
full_name: z.string().min(1, "Naziv je obvezen."),
tax_number: z.string().optional(),
social_security_number: z.string().optional(),
description: z.string().optional(),
address: z.object({
address: z.string().optional(),
country: z.string().optional(),
type_id: z.number().default(1),
}),
phone: z.object({
nu: z.string().optional(),
country_code: z.string().default("00386"),
type_id: z.number().default(1),
}),
})
);
const formClient = useForm({
first_name: "",
last_name: "",
full_name: "",
tax_number: "",
social_security_number: "",
description: "",
address: Address,
phone: Phone,
validationSchema: formSchema,
initialValues: {
first_name: "",
last_name: "",
full_name: "",
tax_number: "",
social_security_number: "",
description: "",
address: {
address: "",
country: "",
type_id: 1,
},
phone: {
nu: "",
country_code: "00386",
type_id: 1,
},
},
});
//Create client drawer
@@ -58,21 +98,58 @@ const openDrawerCreateClient = () => {
//Close any drawer on page
const closeDrawer = () => {
drawerCreateClient.value = false;
formClient.resetForm();
};
const processing = ref(false);
//Ajax call post to store new client
const storeClient = () => {
formClient.post(route("client.store"), {
onBefore: () => {
formClient.address.type_id = Number(formClient.address.type_id);
const storeClient = async () => {
processing.value = true;
const { values } = formClient;
// Ensure type_id is a number
const payload = {
...values,
address: {
...values.address,
type_id: Number(values.address.type_id),
},
onSuccess: () => {
closeDrawer();
formClient.reset();
phone: {
...values.phone,
type_id: Number(values.phone.type_id),
},
});
};
router.post(
route("client.store"),
payload,
{
onSuccess: () => {
closeDrawer();
formClient.resetForm();
processing.value = false;
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
formClient.setFieldError(field, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
};
const onConfirmCreate = formClient.handleSubmit(() => {
storeClient();
});
// Formatting helpers
const fmtCurrency = (v) => {
const n = Number(v ?? 0);
@@ -93,20 +170,11 @@ const fmtCurrency = (v) => {
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3 space-y-3">
<!-- Top actions -->
<div
class="flex items-center justify-between gap-3"
v-if="hasPerm('client-edit')"
>
<PrimaryButton
@click="openDrawerCreateClient"
class="bg-blue-600 hover:bg-blue-700"
>Dodaj</PrimaryButton
>
</div>
<!-- DataTable (server-side) -->
<DataTableServer
<DataTable
:show-search="true"
:show-page-size="true"
:show-add="true"
:columns="[
{ key: 'nu', label: 'Št.', sortable: false, class: 'w-40' },
{ key: 'name', label: 'Naročnik', sortable: false },
@@ -129,6 +197,9 @@ const fmtCurrency = (v) => {
per_page: clients.per_page,
total: clients.total,
last_page: clients.last_page,
from: clients.from,
to: clients.to,
links: clients.links,
}"
:sort="{
key: props.filters?.sort || null,
@@ -137,9 +208,19 @@ const fmtCurrency = (v) => {
:search="initialSearch"
route-name="client"
row-key="uuid"
:page-size-options="[clients.per_page]"
:only-props="['clients']"
:empty-icon="faUserGroup"
empty-text="Ni zadetkov"
empty-description="Ni najdenih naročnikov. Ustvarite novega naročnika ali preverite iskalne kriterije."
>
<template #toolbar-add>
<ActionMenuItem
v-if="hasPerm('client-edit')"
label="Dodaj naročnika"
:icon="faPlus"
@click="openDrawerCreateClient"
/>
</template>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
</template>
@@ -151,12 +232,13 @@ const fmtCurrency = (v) => {
{{ row.person?.full_name || "-" }}
</Link>
<div v-if="!row.person" class="mt-1">
<PrimaryButton
class="!py-0.5 !px-2 bg-red-500 hover:bg-red-600 text-xs"
<Button
variant="destructive"
size="sm"
@click.prevent="
router.post(route('client.emergencyPerson', { uuid: row.uuid }))
"
>Add Person</PrimaryButton
>Add Person</Button
>
</div>
</template>
@@ -170,146 +252,176 @@ const fmtCurrency = (v) => {
{{ fmtCurrency(row.active_contracts_balance_sum) }}
</div>
</template>
<template #empty>
<div class="p-6 text-center text-gray-500">Ni zadetkov.</div>
</template>
</DataTableServer>
</DataTable>
</div>
</div>
</div>
</div>
</AppLayout>
<DialogModal :show="drawerCreateClient" @close="drawerCreateClient = false">
<template #title>Novi naročnik</template>
<template #content>
<form @submit.prevent="storeClient">
<div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="fullname" value="Naziv" />
<TextInput
id="fullname"
ref="fullnameInput"
v-model="formClient.full_name"
type="text"
class="mt-1 block w-full"
autocomplete="full-name"
/>
</div>
<CreateDialog
:show="drawerCreateClient"
title="Novi naročnik"
confirm-text="Shrani"
:processing="processing"
@close="drawerCreateClient = false"
@confirm="onConfirmCreate"
>
<form @submit.prevent="onConfirmCreate">
<div class="space-y-4">
<FormField v-slot="{ componentField }" name="full_name">
<FormItem>
<FormLabel>Naziv</FormLabel>
<FormControl>
<Input
id="fullname"
type="text"
autocomplete="full-name"
placeholder="Naziv"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="taxnumber" value="Davčna" />
<TextInput
id="taxnumber"
ref="taxnumberInput"
v-model="formClient.tax_number"
type="text"
class="mt-1 block w-full"
autocomplete="tax-number"
/>
</div>
<FormField v-slot="{ componentField }" name="tax_number">
<FormItem>
<FormLabel>Davčna</FormLabel>
<FormControl>
<Input
id="taxnumber"
type="text"
autocomplete="tax-number"
placeholder="Davčna številka"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="socialSecurityNumber" value="Matična / Emšo" />
<TextInput
id="socialSecurityNumber"
ref="socialSecurityNumberInput"
v-model="formClient.social_security_number"
type="text"
class="mt-1 block w-full"
autocomplete="social-security-number"
/>
</div>
<FormField v-slot="{ componentField }" name="social_security_number">
<FormItem>
<FormLabel>Matična / Emšo</FormLabel>
<FormControl>
<Input
id="socialSecurityNumber"
type="text"
autocomplete="social-security-number"
placeholder="Matična / Emšo"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="address" value="Naslov" />
<TextInput
id="address"
ref="addressInput"
v-model="formClient.address.address"
type="text"
class="mt-1 block w-full"
autocomplete="address"
/>
</div>
<FormField v-slot="{ componentField }" name="address.address">
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input
id="address"
type="text"
autocomplete="address"
placeholder="Naslov"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="addressCountry" value="Država" />
<TextInput
id="addressCountry"
ref="addressCountryInput"
v-model="formClient.address.country"
type="text"
class="mt-1 block w-full"
autocomplete="address-country"
/>
</div>
<FormField v-slot="{ componentField }" name="address.country">
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input
id="addressCountry"
type="text"
autocomplete="address-country"
placeholder="Država"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="addressType" value="Vrsta naslova" />
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="addressType"
v-model="formClient.address.type_id"
>
<option value="1">Stalni</option>
<option value="2">Začasni</option>
<!-- ... -->
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="phoneCountyCode" value="Koda države tel." />
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="phoneCountyCode"
v-model="formClient.phone.country_code"
>
<option value="00386">+386 (Slovenija)</option>
<option value="00385">+385 (Hrvaška)</option>
<option value="0039">+39 (Italija)</option>
<option value="0036">+39 (Madžarska)</option>
<option value="0043">+43 (Avstrija)</option>
<option value="00381">+381 (Srbija)</option>
<option value="00387">+387 (Bosna in Hercegovina)</option>
<option value="00382">+382 (Črna gora)</option>
<!-- ... -->
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="phoneNu" value="Telefonska št." />
<TextInput
id="phoneNu"
ref="phoneNuInput"
v-model="formClient.phone.nu"
type="text"
class="mt-1 block w-full"
autocomplete="phone-nu"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="description" value="Opis" />
<TextInput
id="description"
ref="descriptionInput"
v-model="formClient.description"
type="text"
class="mt-1 block w-full"
autocomplete="description"
/>
</div>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="formClient.recentlySuccessful" class="me-3">
Shranjeno.
</ActionMessage>
<FormField v-slot="{ value, handleChange }" name="address.type_id">
<FormItem>
<FormLabel>Vrsta naslova</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi vrsto naslova" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="1">Stalni</SelectItem>
<SelectItem :value="2">Začasni</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<PrimaryButton
:class="{ 'opacity-25': formClient.processing }"
:disabled="formClient.processing"
>
Shrani
</PrimaryButton>
<FormField v-slot="{ value, handleChange }" name="phone.country_code">
<FormItem>
<FormLabel>Koda države tel.</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi kodo države" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="00386">+386 (Slovenija)</SelectItem>
<SelectItem value="00385">+385 (Hrvaška)</SelectItem>
<SelectItem value="0039">+39 (Italija)</SelectItem>
<SelectItem value="0036">+36 (Madžarska)</SelectItem>
<SelectItem value="0043">+43 (Avstrija)</SelectItem>
<SelectItem value="00381">+381 (Srbija)</SelectItem>
<SelectItem value="00387">+387 (Bosna in Hercegovina)</SelectItem>
<SelectItem value="00382">+382 (Črna gora)</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="phone.nu">
<FormItem>
<FormLabel>Telefonska št.</FormLabel>
<FormControl>
<Input
id="phoneNu"
type="text"
autocomplete="phone-nu"
placeholder="Telefonska številka"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input
id="description"
type="text"
autocomplete="description"
placeholder="Opis"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
</template>
@@ -1,11 +1,26 @@
<script setup>
import DialogModal from '@/Components/DialogModal.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue';
import { useForm, Field as FormField } from "vee-validate";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";
import { router } from '@inertiajs/vue3';
import SectionTitle from '@/Components/SectionTitle.vue';
import ActionMessage from '@/Components/ActionMessage.vue';
import {
FormControl,
FormItem,
FormLabel,
FormMessage,
} from "@/Components/ui/form";
import { Input } from "@/Components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
const props = defineProps({
show: {
@@ -18,184 +33,267 @@ const props = defineProps({
const emit = defineEmits(['close']);
const close = () => {
form.resetForm();
emit('close');
}
const Address = {
address: '',
country: '',
type_id: 1
}
const formSchema = toTypedSchema(
z.object({
client_uuid: z.string(),
person: z.object({
first_name: z.string().optional(),
last_name: z.string().optional(),
full_name: z.string().min(1, "Naziv je obvezen."),
tax_number: z.string().optional(),
social_security_number: z.string().optional(),
description: z.string().optional(),
address: z.object({
address: z.string().optional(),
country: z.string().optional(),
type_id: z.number().default(1),
}),
phone: z.object({
nu: z.string().optional(),
country_code: z.string().default('00386'),
type_id: z.number().default(1),
}),
}),
})
);
const Phone = {
nu: '',
country_code: '00386',
type_id: 1
}
const Person = {
first_name: '',
last_name: '',
full_name: '',
tax_number: '',
social_security_number: '',
description: '',
address: Address,
phone: Phone
}
const formCreateCase = useForm({
const form = useForm({
validationSchema: formSchema,
initialValues: {
client_uuid: props.clientUuid,
person: Person
person: {
first_name: '',
last_name: '',
full_name: '',
tax_number: '',
social_security_number: '',
description: '',
address: {
address: '',
country: '',
type_id: 1
},
phone: {
nu: '',
country_code: '00386',
type_id: 1
}
}
},
});
const storeCase = () => {
formCreateCase.post(route('clientCase.store'), {
onSuccess: () => {
close();
formCreateCase.reset();
}
});
const processing = ref(false);
const storeCase = async () => {
processing.value = true;
const { values } = form;
router.post(
route('clientCase.store'),
values,
{
onSuccess: () => {
close();
form.resetForm();
processing.value = false;
},
onError: (errors) => {
Object.keys(errors).forEach((field) => {
const errorMessages = Array.isArray(errors[field])
? errors[field]
: [errors[field]];
// Handle nested field paths like "person.full_name"
const fieldPath = field.includes('.') ? field : field;
form.setFieldError(fieldPath, errorMessages[0]);
});
processing.value = false;
},
onFinish: () => {
processing.value = false;
},
}
);
};
const onConfirm = form.handleSubmit(() => {
storeCase();
});
</script>
<template>
<DialogModal
<CreateDialog
:show="show"
@close="close">
title="Nova primer"
confirm-text="Shrani"
:processing="processing"
@close="close"
@confirm="onConfirm"
>
<form @submit.prevent="onConfirm">
<SectionTitle class="border-b mb-4">
<template #title>
Oseba
</template>
</SectionTitle>
<template #title>Nova primer</template>
<template #content>
<form @submit.prevent="storeCase">
<SectionTitle class="border-b mb-4">
<template #title>
Oseba
</template>
</SectionTitle>
<div class="space-y-4">
<FormField v-slot="{ componentField }" name="person.full_name">
<FormItem>
<FormLabel>Naziv</FormLabel>
<FormControl>
<Input
id="fullname"
type="text"
placeholder="Naziv"
autocomplete="full-name"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="fullname" value="Naziv"/>
<TextInput
id="fullname"
ref="fullnameInput"
v-model="formCreateCase.person.full_name"
type="text"
class="mt-1 block w-full"
autocomplete="full-name"
/>
</div>
<FormField v-slot="{ componentField }" name="person.tax_number">
<FormItem>
<FormLabel>Davčna</FormLabel>
<FormControl>
<Input
id="taxnumber"
type="text"
placeholder="Davčna številka"
autocomplete="tax-number"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="taxnumber" value="Davčna"/>
<TextInput
id="taxnumber"
ref="taxnumberInput"
v-model="formCreateCase.tax_number"
type="text"
class="mt-1 block w-full"
autocomplete="tax-number"
/>
</div>
<FormField v-slot="{ componentField }" name="person.social_security_number">
<FormItem>
<FormLabel>Matična / Emšo</FormLabel>
<FormControl>
<Input
id="socialSecurityNumber"
type="text"
placeholder="Matična / Emšo"
autocomplete="social-security-number"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="socialSecurityNumber" value="Matična / Emšo"/>
<TextInput
id="socialSecurityNumber"
ref="socialSecurityNumberInput"
v-model="formCreateCase.social_security_number"
type="text"
class="mt-1 block w-full"
autocomplete="social-security-number"
/>
</div>
<FormField v-slot="{ componentField }" name="person.address.address">
<FormItem>
<FormLabel>Naslov</FormLabel>
<FormControl>
<Input
id="address"
type="text"
placeholder="Naslov"
autocomplete="street-address"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="address" value="Naslov"/>
<TextInput
id="address"
ref="addressInput"
v-model="formCreateCase.person.address.address"
type="text"
class="mt-1 block w-full"
autocomplete="address"
/>
</div>
<FormField v-slot="{ componentField }" name="person.address.country">
<FormItem>
<FormLabel>Država</FormLabel>
<FormControl>
<Input
id="addressCountry"
type="text"
placeholder="Država"
autocomplete="country"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="addressCountry" value="Država"/>
<TextInput
id="addressCountry"
ref="addressCountryInput"
v-model="formCreateCase.person.address.country"
type="text"
class="mt-1 block w-full"
autocomplete="address-country"
/>
</div>
<FormField v-slot="{ value, handleChange }" name="person.address.type_id">
<FormItem>
<FormLabel>Vrsta naslova</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi vrsto" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem :value="1">Stalni</SelectItem>
<SelectItem :value="2">Začasni</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="addressType" value="Vrsta naslova"/>
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="addressType"
v-model="formCreateCase.person.address.type_id"
>
<option value="1">Stalni</option>
<option value="2">Začasni</option>
<!-- ... -->
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="phoneCountyCode" value="Koda države tel."/>
<select
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
id="phoneCountyCode"
v-model="formCreateCase.person.phone.country_code"
>
<option value="00386">+386 (Slovenija)</option>
<option value="00385">+385 (Hrvaška)</option>
<option value="0039">+39 (Italija)</option>
<option value="0036">+39 (Madžarska)</option>
<option value="0043">+43 (Avstrija)</option>
<option value="00381">+381 (Srbija)</option>
<option value="00387">+387 (Bosna in Hercegovina)</option>
<option value="00382">+382 (Črna gora)</option>
<!-- ... -->
</select>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="phoneNu" value="Telefonska št."/>
<TextInput
id="phoneNu"
ref="phoneNuInput"
v-model="formCreateCase.person.phone.nu"
type="text"
class="mt-1 block w-full"
autocomplete="phone-nu"
/>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="description" value="Opis"/>
<TextInput
id="description"
ref="descriptionInput"
v-model="formCreateCase.description"
type="text"
class="mt-1 block w-full"
autocomplete="description"
/>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="formCreateCase.recentlySuccessful" class="me-3">
Shranjeno.
</ActionMessage>
<FormField v-slot="{ value, handleChange }" name="person.phone.country_code">
<FormItem>
<FormLabel>Koda države tel.</FormLabel>
<Select :model-value="value" @update:model-value="handleChange">
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Izberi kodo" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="00386">+386 (Slovenija)</SelectItem>
<SelectItem value="00385">+385 (Hrvaška)</SelectItem>
<SelectItem value="0039">+39 (Italija)</SelectItem>
<SelectItem value="0036">+36 (Madžarska)</SelectItem>
<SelectItem value="0043">+43 (Avstrija)</SelectItem>
<SelectItem value="00381">+381 (Srbija)</SelectItem>
<SelectItem value="00387">+387 (Bosna in Hercegovina)</SelectItem>
<SelectItem value="00382">+382 (Črna gora)</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
</FormField>
<PrimaryButton :class="{ 'opacity-25': formCreateCase.processing }" :disabled="formCreateCase.processing">
Shrani
</PrimaryButton>
<FormField v-slot="{ componentField }" name="person.phone.nu">
<FormItem>
<FormLabel>Telefonska št.</FormLabel>
<FormControl>
<Input
id="phoneNu"
type="text"
placeholder="Telefonska številka"
autocomplete="tel"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
<FormField v-slot="{ componentField }" name="person.description">
<FormItem>
<FormLabel>Opis</FormLabel>
<FormControl>
<Input
id="description"
type="text"
placeholder="Opis"
autocomplete="off"
v-bind="componentField"
/>
</FormControl>
<FormMessage />
</FormItem>
</FormField>
</div>
</form>
</template>
</DialogModal>
</template>
</CreateDialog>
</template>
+41 -46
View File
@@ -1,13 +1,16 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { computed, ref } from "vue";
import { Link, usePage } from "@inertiajs/vue3";
import SectionTitle from "@/Components/SectionTitle.vue";
import PersonInfoGrid from "@/Components/PersonInfoGrid.vue";
import PersonInfoGrid from "@/Components/PersonInfo/PersonInfoGrid.vue";
import FormCreateCase from "./Partials/FormCreateCase.vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
import DataTable from "@/Components/DataTable/DataTable.vue";
import { hasPermission } from "@/Services/permissions";
import { Button } from "@/Components/ui/button";
import { ButtonGroup } from "@/Components/ui/button-group";
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
import { faPlus } from "@fortawesome/free-solid-svg-icons";
const props = defineProps({
client: Object,
@@ -49,36 +52,6 @@ const openDrawerCreateCase = () => {
</template>
</SectionTitle>
</div>
<nav class="mt-2 border-b border-gray-200">
<ul class="flex gap-6 -mb-px">
<li>
<Link
:href="route('client.show', { uuid: client.uuid })"
:class="[
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.show')
? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300',
]"
>
Primeri
</Link>
</li>
<li>
<Link
:href="route('client.contracts', { uuid: client.uuid })"
:class="[
'inline-flex items-center px-3 py-2 text-sm font-medium border-b-2',
route().current('client.contracts')
? 'text-indigo-600 border-indigo-600'
: 'text-gray-600 border-transparent hover:text-gray-800 hover:border-gray-300',
]"
>
Pogodbe
</Link>
</li>
</ul>
</nav>
</div>
</div>
</div>
@@ -103,16 +76,30 @@ const openDrawerCreateCase = () => {
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="px-3 bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="mx-auto max-w-4x1 py-3">
<div
class="flex items-center justify-between gap-3"
v-if="hasPerm('case-edit')"
>
<PrimaryButton @click="openDrawerCreateCase" class="bg-blue-400"
>Dodaj</PrimaryButton
>
<div class="mb-4">
<ButtonGroup>
<Button
as-child
:variant="route().current('client.show') ? 'default' : 'ghost'"
>
<Link :href="route('client.show', { uuid: client.uuid })">
Primeri
</Link>
</Button>
<Button
as-child
:variant="route().current('client.contracts') ? 'default' : 'ghost'"
>
<Link :href="route('client.contracts', { uuid: client.uuid })">
Pogodbe
</Link>
</Button>
</ButtonGroup>
</div>
<DataTableServer
class="mt-3"
<DataTable
:show-search="true"
:show-page-size="true"
:show-add="true"
:columns="[
{ key: 'nu', label: 'Št.', sortable: false, class: 'w-40' },
{ key: 'case', label: 'Primer', sortable: false },
@@ -136,6 +123,9 @@ const openDrawerCreateCase = () => {
per_page: client_cases.per_page,
total: client_cases.total,
last_page: client_cases.last_page,
from: client_cases.from,
to: client_cases.to,
links: client_cases.links,
}"
route-name="client.show"
:route-params="{ uuid: client.uuid }"
@@ -143,6 +133,14 @@ const openDrawerCreateCase = () => {
:search="search"
:only-props="['client_cases']"
>
<template #toolbar-add>
<ActionMenuItem
v-if="hasPerm('case-edit')"
label="Dodaj primer"
:icon="faPlus"
@click="openDrawerCreateCase"
/>
</template>
<template #cell-nu="{ row }">
{{ row.person?.nu || "-" }}
</template>
@@ -170,10 +168,7 @@ const openDrawerCreateCase = () => {
}}
</div>
</template>
<template #empty>
<div class="p-6 text-center text-gray-500">Ni zadetkov.</div>
</template>
</DataTableServer>
</DataTable>
</div>
<!-- Pagination handled by DataTableServer -->
</div>
+70 -70
View File
@@ -163,22 +163,22 @@ function safeCaseHref(uuid, segment = null) {
v-for="k in kpiDefs"
:key="k.key"
:href="route(k.route)"
class="group relative bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm px-4 py-5 flex flex-col gap-3 hover:border-indigo-300 hover:shadow transition focus:outline-none focus:ring-2 focus:ring-indigo-500"
class="group relative bg-white border rounded-xl shadow-sm px-4 py-5 flex flex-col gap-3 hover:border-indigo-300 hover:shadow transition focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<div class="flex items-center justify-between">
<span
class="inline-flex items-center justify-center h-10 w-10 rounded-md bg-indigo-50 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 group-hover:bg-indigo-100 dark:group-hover:bg-indigo-800/40"
class="inline-flex items-center justify-center h-10 w-10 rounded-md bg-indigo-50 text-indigo-600 group-hover:bg-indigo-100"
>
<FontAwesomeIcon :icon="k.icon" class="w-5 h-5" />
</span>
<span
class="text-[11px] text-gray-400 dark:text-gray-500 uppercase tracking-wide"
class="text-[11px] text-gray-400 uppercase tracking-wide"
>{{ k.label }}</span
>
</div>
<div class="flex items-end gap-2">
<span
class="text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
class="text-2xl font-semibold tracking-tight text-gray-900"
>{{ props.kpis?.[k.key] ?? "—" }}</span
>
<span
@@ -251,17 +251,17 @@ function safeCaseHref(uuid, segment = null) {
<!-- Activity Feed -->
<div class="lg:col-span-1 space-y-4">
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-5 flex flex-col gap-4"
class="bg-white border rounded-xl shadow-sm p-5 flex flex-col gap-4"
>
<div class="flex items-center justify-between">
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase"
>
Aktivnost
</h3>
</div>
<ul
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
class="divide-y divide-gray-100 text-sm"
v-if="activities"
>
<li
@@ -271,18 +271,18 @@ function safeCaseHref(uuid, segment = null) {
>
<span class="w-2 h-2 mt-2 rounded-full bg-indigo-400" />
<div class="flex-1 min-w-0 space-y-1">
<p class="text-gray-700 dark:text-gray-300 line-clamp-2">
<p class="text-gray-700 line-clamp-2">
{{ a.note || "Dogodek" }}
</p>
<div class="flex flex-wrap items-center gap-2">
<span class="text-[11px] text-gray-400 dark:text-gray-500">{{
<span class="text-[11px] text-gray-400">{{
new Date(a.created_at).toLocaleString()
}}</span>
<Link
v-for="l in a.links"
:key="l.type + l.href"
:href="l.href"
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800/60 font-medium tracking-wide"
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600 hover:bg-indigo-100 font-medium tracking-wide"
>{{ l.label }}</Link
>
</div>
@@ -290,7 +290,7 @@ function safeCaseHref(uuid, segment = null) {
</li>
<li
v-if="!activities?.length"
class="py-4 text-xs text-gray-500 text-center dark:text-gray-500"
class="py-4 text-xs text-gray-500 text-center"
>
Ni zabeleženih aktivnosti.
</li>
@@ -299,17 +299,17 @@ function safeCaseHref(uuid, segment = null) {
<li
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
class="h-5 bg-gray-100 rounded"
/>
</ul>
<div class="pt-1 flex justify-between items-center text-[11px]">
<Link
:href="route('dashboard')"
class="inline-flex items-center gap-1 font-medium text-indigo-600 dark:text-indigo-400 hover:underline"
class="inline-flex items-center gap-1 font-medium text-indigo-600 hover:underline"
>Več kmalu
<FontAwesomeIcon :icon="faArrowUpRightFromSquare" class="w-3 h-3"
/></Link>
<span v-if="systemHealth" class="text-gray-400 dark:text-gray-500"
<span v-if="systemHealth" class="text-gray-400"
>Posodobljeno
{{ new Date(systemHealth.generated_at).toLocaleTimeString() }}</span
>
@@ -321,17 +321,17 @@ function safeCaseHref(uuid, segment = null) {
<div class="lg:col-span-2 space-y-8">
<!-- SMS Overview -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
SMS stanje
</h3>
<div v-if="props.smsStats?.length" class="overflow-x-auto">
<table class="w-full text-sm">
<thead
class="bg-gray-50 dark:bg-gray-900/30 text-gray-600 dark:text-gray-300 text-xs uppercase tracking-wider"
class="bg-gray-50 text-gray-600 text-xs uppercase tracking-wider"
>
<tr>
<th class="px-3 py-2 text-left">Profil</th>
@@ -346,10 +346,10 @@ function safeCaseHref(uuid, segment = null) {
<tr
v-for="p in props.smsStats"
:key="p.id"
class="border-t last:border-b dark:border-gray-700"
class="border-t last:border-b"
>
<td class="px-3 py-2">
<span class="font-medium text-gray-900 dark:text-gray-100">{{
<span class="font-medium text-gray-900">{{
p.name
}}</span>
<span
@@ -358,7 +358,7 @@ function safeCaseHref(uuid, segment = null) {
>{{ p.active ? "Aktiven" : "Neaktiven" }}</span
>
</td>
<td class="px-3 py-2 text-gray-700 dark:text-gray-300">
<td class="px-3 py-2 text-gray-700">
{{ p.balance ?? "—" }}
</td>
<td class="px-3 py-2">{{ p.today?.total ?? 0 }}</td>
@@ -371,17 +371,17 @@ function safeCaseHref(uuid, segment = null) {
</tbody>
</table>
</div>
<div v-else class="text-sm text-gray-500 dark:text-gray-400">
<div v-else class="text-sm text-gray-500">
Ni podatkov o SMS.
</div>
</div>
<!-- System Health -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
System Health
</h3>
@@ -390,27 +390,27 @@ function safeCaseHref(uuid, segment = null) {
class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4 text-sm"
>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400 dark:text-gray-500"
<span class="text-[11px] uppercase text-gray-400"
>Queue backlog</span
>
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
<span class="font-semibold text-gray-800">{{
systemHealth.queue_backlog ?? "—"
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400 dark:text-gray-500"
<span class="text-[11px] uppercase text-gray-400"
>Failed jobs</span
>
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
<span class="font-semibold text-gray-800">{{
systemHealth.failed_jobs ?? "—"
}}</span>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400 dark:text-gray-500"
<span class="text-[11px] uppercase text-gray-400"
>Last activity (min)</span
>
<span
class="font-semibold text-gray-800 dark:text-gray-100"
class="font-semibold text-gray-800"
:title="
systemHealth.last_activity_iso
? new Date(systemHealth.last_activity_iso).toLocaleString()
@@ -422,10 +422,10 @@ function safeCaseHref(uuid, segment = null) {
>
</div>
<div class="flex flex-col gap-1">
<span class="text-[11px] uppercase text-gray-400 dark:text-gray-500"
<span class="text-[11px] uppercase text-gray-400"
>Generated</span
>
<span class="font-semibold text-gray-800 dark:text-gray-100">{{
<span class="font-semibold text-gray-800">{{
new Date(systemHealth.generated_at).toLocaleTimeString()
}}</span>
</div>
@@ -434,17 +434,17 @@ function safeCaseHref(uuid, segment = null) {
<div
v-for="n in 4"
:key="n"
class="h-10 bg-gray-100 dark:bg-gray-700 rounded"
class="h-10 bg-gray-100 rounded"
/>
</div>
</div>
<!-- Completed Field Jobs Trend (7 dni) -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
Zaključena terenska dela (7 dni)
</h3>
@@ -466,7 +466,7 @@ function safeCaseHref(uuid, segment = null) {
stroke-linecap="round"
/>
</svg>
<div class="mt-2 flex gap-2 text-[10px] text-gray-400 dark:text-gray-500">
<div class="mt-2 flex gap-2 text-[10px] text-gray-400">
<span
v-for="(l, i) in trends.labels"
:key="i"
@@ -475,21 +475,21 @@ function safeCaseHref(uuid, segment = null) {
>
</div>
</div>
<div v-else class="h-24 animate-pulse bg-gray-100 dark:bg-gray-700 rounded" />
<div v-else class="h-24 animate-pulse bg-gray-100 rounded" />
</div>
<!-- Stale Cases -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
Stari primeri brez aktivnosti
</h3>
<ul
v-if="staleCases"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
class="divide-y divide-gray-100 text-sm"
>
<li
v-for="c in staleCases"
@@ -500,19 +500,19 @@ function safeCaseHref(uuid, segment = null) {
<Link
v-if="c?.uuid"
:href="safeCaseHref(c.uuid)"
class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
class="text-indigo-600 hover:underline font-medium"
>{{ c.client_ref || c.uuid.slice(0, 8) }}</Link
>
<span v-else class="text-gray-700 dark:text-gray-300 font-medium">{{
<span v-else class="text-gray-700 font-medium">{{
c.client_ref || "Primer"
}}</span>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
<p class="text-[11px] text-gray-400">
Brez aktivnosti:
{{ formatStaleDaysLabel(c.days_without_activity ?? c.days_stale) }}
</p>
</div>
<span
class="text-[10px] px-2 py-0.5 rounded bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-300"
class="text-[10px] px-2 py-0.5 rounded bg-amber-50 text-amber-600"
>Stale</span
>
</li>
@@ -527,23 +527,23 @@ function safeCaseHref(uuid, segment = null) {
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
class="h-5 bg-gray-100 rounded"
/>
</div>
</div>
<!-- Field Jobs Assigned Today -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
Današnje dodelitve terenskih
</h3>
<ul
v-if="fieldJobsAssignedToday"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
class="divide-y divide-gray-100 text-sm"
>
<li
v-for="f in fieldJobsAssignedToday"
@@ -551,7 +551,7 @@ function safeCaseHref(uuid, segment = null) {
class="py-2 flex items-start justify-between gap-3"
>
<div class="min-w-0 flex-1">
<p class="text-gray-700 dark:text-gray-300 text-sm font-medium">
<p class="text-gray-700 text-sm font-medium">
#{{ f.id }}
<template v-if="f.contract">
·
@@ -560,29 +560,29 @@ function safeCaseHref(uuid, segment = null) {
:href="
safeCaseHref(f.contract.client_case_uuid, f.contract.segment_id)
"
class="text-indigo-600 dark:text-indigo-400 hover:underline"
class="text-indigo-600 hover:underline"
>
{{ f.contract.reference || f.contract.uuid?.slice(0, 8) }}
</Link>
<span v-else class="text-gray-700 dark:text-gray-300">{{
<span v-else class="text-gray-700">{{
f.contract.reference || f.contract.uuid?.slice(0, 8)
}}</span>
<span
v-if="f.contract.person_full_name"
class="text-gray-500 dark:text-gray-400"
class="text-gray-500"
>
{{ f.contract.person_full_name }}
</span>
</template>
</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
<p class="text-[11px] text-gray-400">
{{ formatJobTime(f.created_at) }}
</p>
</div>
<div class="flex items-center gap-2">
<span
v-if="f.priority"
class="text-[10px] px-2 py-0.5 rounded bg-rose-50 dark:bg-rose-900/30 text-rose-600 dark:text-rose-300"
class="text-[10px] px-2 py-0.5 rounded bg-rose-50 text-rose-600"
>Prioriteta</span
>
</div>
@@ -598,43 +598,43 @@ function safeCaseHref(uuid, segment = null) {
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
class="h-5 bg-gray-100 rounded"
/>
</div>
</div>
<!-- Imports In Progress -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
Uvozi v teku
</h3>
<ul
v-if="importsInProgress"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
class="divide-y divide-gray-100 text-sm"
>
<li v-for="im in importsInProgress" :key="im.id" class="py-2 space-y-1">
<div class="flex items-center justify-between">
<p class="font-medium text-gray-700 dark:text-gray-300 truncate">
<p class="font-medium text-gray-700 truncate">
{{ im.file_name }}
</p>
<span
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300"
class="text-[10px] px-2 py-0.5 rounded-full bg-indigo-50 text-indigo-600"
>{{ im.status }}</span
>
</div>
<div
class="w-full h-2 bg-gray-100 dark:bg-gray-700 rounded overflow-hidden"
class="w-full h-2 bg-gray-100 rounded overflow-hidden"
>
<div
class="h-full bg-indigo-500 dark:bg-indigo-400"
class="h-full bg-indigo-500"
:style="{ width: (im.progress_pct || 0) + '%' }"
></div>
</div>
<p class="text-[10px] text-gray-400 dark:text-gray-500">
<p class="text-[10px] text-gray-400">
{{ im.imported_rows }}/{{ im.total_rows }} (veljavnih:
{{ im.valid_rows }}, neveljavnih: {{ im.invalid_rows }})
</p>
@@ -650,23 +650,23 @@ function safeCaseHref(uuid, segment = null) {
<div
v-for="n in 4"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
class="h-5 bg-gray-100 rounded"
/>
</div>
</div>
<!-- Active Document Templates -->
<div
class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl shadow-sm p-6"
class="bg-white border rounded-xl shadow-sm p-6"
>
<h3
class="text-sm font-semibold tracking-wide text-gray-700 dark:text-gray-200 uppercase mb-4"
class="text-sm font-semibold tracking-wide text-gray-700 uppercase mb-4"
>
Aktivne predloge dokumentov
</h3>
<ul
v-if="activeTemplates"
class="divide-y divide-gray-100 dark:divide-gray-700 text-sm"
class="divide-y divide-gray-100 text-sm"
>
<li
v-for="t in activeTemplates"
@@ -674,16 +674,16 @@ function safeCaseHref(uuid, segment = null) {
class="py-2 flex items-center justify-between"
>
<div class="min-w-0">
<p class="text-gray-700 dark:text-gray-300 font-medium truncate">
<p class="text-gray-700 font-medium truncate">
{{ t.name }}
</p>
<p class="text-[11px] text-gray-400 dark:text-gray-500">
<p class="text-[11px] text-gray-400">
v{{ t.version }} · {{ new Date(t.updated_at).toLocaleDateString() }}
</p>
</div>
<Link
:href="route('admin.document-templates.edit', t.id)"
class="text-[10px] px-2 py-0.5 rounded bg-indigo-50 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-800/60"
class="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 hover:bg-indigo-100"
>Uredi</Link
>
</li>
@@ -698,7 +698,7 @@ function safeCaseHref(uuid, segment = null) {
<div
v-for="n in 5"
:key="n"
class="h-5 bg-gray-100 dark:bg-gray-700 rounded"
class="h-5 bg-gray-100 rounded"
/>
</div>
</div>
+29
View File
@@ -0,0 +1,29 @@
<script setup>
import { Link } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
defineProps({
reports: { type: Array, required: true },
})
</script>
<template>
<AppLayout title="Poročila">
<template #header />
<div class="pt-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="mb-6">
<h1 class="text-2xl font-semibold">Poročila</h1>
<p class="text-gray-600">Izberite poročilo za pregled in izvoz.</p>
</div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div v-for="r in reports" :key="r.slug" class="border rounded-lg p-4 bg-white shadow-sm hover:shadow-md transition">
<h2 class="text-lg font-medium mb-1">{{ r.name }}</h2>
<p class="text-sm text-gray-600 mb-3">{{ r.description }}</p>
<Link :href="route('reports.show', r.slug)" class="inline-flex items-center text-indigo-600 hover:underline">Odpri </Link>
</div>
</div>
</div>
</div>
</AppLayout>
</template>
+312
View File
@@ -0,0 +1,312 @@
<script setup>
import { reactive, ref, computed, onMounted } from "vue";
import { Link } from "@inertiajs/vue3";
import axios from "axios";
import AppLayout from "@/Layouts/AppLayout.vue";
import DataTableServer from "@/Components/DataTable/DataTableServer.vue";
const props = defineProps({
slug: { type: String, required: true },
name: { type: String, required: true },
description: { type: String, default: null },
inputs: { type: Array, default: () => [] },
columns: { type: Array, default: () => [] },
rows: { type: Array, default: () => [] },
meta: {
type: Object,
default: () => ({ total: 0, current_page: 1, per_page: 25, last_page: 1 }),
},
query: { type: Object, default: () => ({}) },
});
// filters: start with server-provided query or defaults
const filters = reactive(
Object.assign(
Object.fromEntries((props.inputs || []).map((i) => [i.key, i.default ?? null])),
props.query || {}
)
);
function resetFilters() {
for (const i of props.inputs || []) {
filters[i.key] = i.default ?? null;
}
}
function exportFile(fmt) {
const params = new URLSearchParams({
format: fmt,
...Object.fromEntries(
Object.entries(filters).filter(([_, v]) => v !== null && v !== "")
),
});
window.location.href = route("reports.export", props.slug) + "?" + params.toString();
}
// Async user options for select:user inputs
const userOptions = ref([]);
const userLoading = ref(false);
async function fetchUsers(initial = false) {
userLoading.value = true;
try {
const res = await axios.get(route("reports.users"), {
params: { limit: initial ? 50 : 50 },
});
userOptions.value = Array.isArray(res.data) ? res.data : [];
} finally {
userLoading.value = false;
}
}
const hasUserFilter = computed(() =>
(props.inputs || []).some((i) => i.type === "select:user")
);
// Async client options for select:client inputs
const clientOptions = ref([]);
const clientLoading = ref(false);
async function fetchClients(initial = false) {
clientLoading.value = true;
try {
const res = await axios.get(route("reports.clients"));
console.log("Clients response:", res.data);
clientOptions.value = Array.isArray(res.data) ? res.data : [];
console.log("clientOptions set to:", clientOptions.value);
} finally {
clientLoading.value = false;
}
}
const hasClientFilter = computed(() =>
(props.inputs || []).some((i) => i.type === "select:client")
);
onMounted(() => {
if (hasUserFilter.value) fetchUsers(true);
if (hasClientFilter.value) fetchClients(true);
});
// Formatting helpers (EU style)
function formatNumberEU(val) {
if (typeof val !== "number") return String(val ?? "");
// Use 0 decimals for integers, 2 for decimals
const hasFraction = Math.abs(num % 1) > 0;
const opts = hasFraction
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
: { maximumFractionDigits: 0 };
return new Intl.NumberFormat("sl-SI", opts).format(num);
}
function pad2(n) {
return String(n).padStart(2, "0");
}
function formatDateEU(input) {
if (!input) return "";
if (typeof input === "string") {
const s = input.trim();
// ISO date YYYY-MM-DD
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (m) {
const [, y, mo, d] = m;
return `${d}.${mo}.${y}`;
}
}
// Fallback via Date object
const d = new Date(input);
if (isNaN(d)) return String(input);
return `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}.${d.getFullYear()}`;
}
function formatDateTimeEU(input) {
if (!input) return "";
if (typeof input === "string") {
// Handle "YYYY-MM-DD HH:MM:SS" or ISO
const s = input.replace("T", " ").split(".")[0];
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/);
if (m) {
const [, y, mo, d, hh, mm, ss] = m;
return `${d}.${mo}.${y} ${hh}:${mm}:${ss ?? "00"}`;
}
}
const d = new Date(input);
if (isNaN(d)) return String(input);
return `${pad2(d.getDate())}.${pad2(d.getMonth() + 1)}.${d.getFullYear()} ${pad2(
d.getHours()
)}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
}
function isDateLikeKey(key) {
if (typeof key !== "string") return false;
return /(^|_)(date|datum)$/.test(key);
}
function isDateTimeKey(key) {
if (typeof key !== "string") return false;
return key.endsWith("_at");
}
function isDecimalKey(key) {
if (typeof key !== "string") return false;
return /(amount|price|total|sum|avg|mean|saldo|znesek|cost)/i.test(key);
}
function isIdentifierKey(key) {
if (typeof key !== "string") return false;
return /(reference|ref|uuid|guid|identifier|oznaka|sklic)$/i.test(key);
}
function formatCell(value, key) {
if (value === null || value === undefined) return "";
// Timestamps first
if (isDateTimeKey(key)) return formatDateTimeEU(value);
// Pure dates
if (isDateLikeKey(key)) return formatDateEU(value);
// ISO-like date string in value
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}(?:[ T].*)?$/.test(value)) {
return value.length > 10 ? formatDateTimeEU(value) : formatDateEU(value);
}
// Do not format identifier-like columns (e.g., contract reference)
if (isIdentifierKey(key)) return value;
// Numeric formatting: currency/decimal keys -> two decimals
if (isDecimalKey(key)) {
if (typeof value !== "number") return value;
let num = value;
return new Intl.NumberFormat("sl-SI", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(num);
}
// Integers and generic numbers: only format actual numbers to avoid touching numeric-looking strings
if (typeof value === "number") return formatNumberEU(value);
return value;
}
</script>
<template>
<AppLayout :title="name">
<template #header />
<div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="mb-4 flex items-start justify-between gap-4">
<div>
<h1 class="text-2xl font-semibold">{{ name }}</h1>
<p v-if="description" class="text-gray-600">{{ description }}</p>
</div>
<div class="flex gap-2">
<button
type="button"
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
@click="exportFile('csv')"
>
CSV
</button>
<button
type="button"
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
@click="exportFile('pdf')"
>
PDF
</button>
<button
type="button"
class="px-3 py-2 rounded bg-gray-200 hover:bg-gray-300"
@click="exportFile('xlsx')"
>
Excel
</button>
</div>
</div>
<div class="mb-4 grid gap-3 md:grid-cols-4">
<div v-for="inp in inputs" :key="inp.key" class="flex flex-col">
<label class="text-sm text-gray-700 mb-1">{{ inp.label || inp.key }}</label>
<template v-if="inp.type === 'date'">
<DatePicker
v-model="filters[inp.key]"
format="dd.MM.yyyy"
placeholder="Izberi datum"
/>
</template>
<template v-else-if="inp.type === 'integer'">
<input
type="number"
v-model.number="filters[inp.key]"
class="border rounded px-2 py-1"
/>
</template>
<template v-else-if="inp.type === 'select:user'">
<select v-model.number="filters[inp.key]" class="border rounded px-2 py-1">
<option :value="null"> brez </option>
<option v-for="u in userOptions" :key="u.id" :value="u.id">
{{ u.name }}
</option>
</select>
<div v-if="userLoading" class="text-xs text-gray-500 mt-1">Nalagam</div>
</template>
<template v-else-if="inp.type === 'select:client'">
<select
v-model="filters[inp.key]"
class="border rounded px-2 py-1"
@change="
(e) => {
console.log('Select changed:', e.target.value, 'filters:', filters);
}
"
>
<option :value="null"> brez </option>
<option v-for="c in clientOptions" :key="c.id" :value="c.id">
{{ c.name }}
</option>
</select>
<div v-if="clientLoading" class="text-xs text-gray-500 mt-1">Nalagam</div>
</template>
<template v-else>
<input
type="text"
v-model="filters[inp.key]"
class="border rounded px-2 py-1"
/>
</template>
</div>
<div class="flex items-end gap-2">
<button
type="button"
class="px-3 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-700"
@click="
$inertia.get(
route('reports.show', slug),
{ ...filters, per_page: meta?.per_page || 25, page: 1 },
{ preserveState: false, replace: false }
)
"
>
Prikaži
</button>
<button
type="button"
@click="resetFilters"
class="px-3 py-2 rounded bg-gray-100 hover:bg-gray-200"
>
Ponastavi
</button>
</div>
</div>
<!-- data table component -->
<DataTableServer
:columns="columns.map((c) => ({ key: c.key, label: c.label || c.key }))"
:rows="rows"
:meta="meta"
route-name="reports.show"
:route-params="{ slug: slug }"
:query="filters"
:show-toolbar="false"
:only-props="['rows', 'meta', 'query']"
:preserve-state="false"
>
<template #cell="{ value, column }">
{{ formatCell(value, column.key) }}
</template>
</DataTableServer>
</div>
</div>
</AppLayout>
</template>
@@ -2,7 +2,8 @@
import AppLayout from '@/Layouts/AppLayout.vue'
import SectionTitle from '@/Components/SectionTitle.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import DialogModal from '@/Components/DialogModal.vue'
import CreateDialog from '@/Components/Dialogs/CreateDialog.vue'
import UpdateDialog from '@/Components/Dialogs/UpdateDialog.vue'
import ConfirmationModal from '@/Components/ConfirmationModal.vue'
import InputLabel from '@/Components/InputLabel.vue'
import InputError from '@/Components/InputError.vue'
@@ -97,10 +98,16 @@ const destroyConfig = () => {
</div>
<!-- create modal -->
<DialogModal :show="showCreate" @close="closeCreate">
<template #title>New Contract Configuration</template>
<template #content>
<div class="space-y-4">
<CreateDialog
:show="showCreate"
title="New Contract Configuration"
confirm-text="Create"
:processing="createForm.processing"
:disabled="!createForm.contract_type_id || !createForm.segment_id"
@close="closeCreate"
@confirm="submitCreate"
>
<div class="space-y-4">
<div>
<InputLabel for="type">Contract Type</InputLabel>
<select id="type" v-model="createForm.contract_type_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
@@ -121,19 +128,20 @@ const destroyConfig = () => {
<label for="is_initial" class="text-sm text-gray-700">Mark as initial</label>
</div>
</div>
</div>
</template>
<template #footer>
<button class="px-4 py-2 rounded bg-gray-200" @click="closeCreate">Cancel</button>
<PrimaryButton class="ml-2" :disabled="createForm.processing || !createForm.contract_type_id || !createForm.segment_id" @click="submitCreate">Create</PrimaryButton>
</template>
</DialogModal>
</div>
</CreateDialog>
<!-- simple inline edit dialog -->
<DialogModal :show="!!editing" @close="closeEdit">
<template #title>Edit Configuration</template>
<template #content>
<div class="space-y-4">
<UpdateDialog
:show="!!editing"
title="Edit Configuration"
confirm-text="Save"
:processing="editForm.processing"
:disabled="!editForm.segment_id"
@close="closeEdit"
@confirm="submitEdit"
>
<div class="space-y-4">
<div>
<InputLabel>Segment</InputLabel>
<select v-model="editForm.segment_id" class="mt-1 w-full rounded border-gray-300 focus:border-indigo-500 focus:ring-indigo-500">
@@ -149,13 +157,8 @@ const destroyConfig = () => {
<input id="active" type="checkbox" v-model="editForm.active" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
<label for="active" class="text-sm text-gray-700">Active</label>
</div>
</div>
</template>
<template #footer>
<button class="px-4 py-2 rounded bg-gray-200" @click="closeEdit">Cancel</button>
<PrimaryButton class="ml-2" :disabled="editForm.processing || !editForm.segment_id" @click="submitEdit">Save</PrimaryButton>
</template>
</DialogModal>
</div>
</UpdateDialog>
</AppLayout>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title>
+22 -35
View File
@@ -1,7 +1,8 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, useForm } from "@inertiajs/vue3";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
@@ -162,10 +163,15 @@ watch(
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
</div>
<DialogModal :show="showCreate" @close="closeCreate">
<template #title> Create Field Job Setting </template>
<template #content>
<form @submit.prevent="store">
<CreateDialog
:show="showCreate"
title="Create Field Job Setting"
confirm-text="Create"
:processing="form.processing"
@close="closeCreate"
@confirm="store"
>
<form @submit.prevent="store">
<div class="grid grid-cols-1 gap-4">
<div>
<InputLabel for="segment" value="Segment" />
@@ -308,24 +314,17 @@ watch(
<InputError :message="form.errors.queue_segment_id" class="mt-1" />
</div>
</div>
<div class="flex justify-end gap-2 mt-6">
<button
type="button"
@click="closeCreate"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
<PrimaryButton :disabled="form.processing">Create</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
<DialogModal :show="showEdit" @close="closeEdit">
<template #title> Edit Field Job Setting </template>
<template #content>
<form @submit.prevent="update">
</CreateDialog>
<UpdateDialog
:show="showEdit"
title="Edit Field Job Setting"
confirm-text="Save"
:processing="editForm.processing"
@close="closeEdit"
@confirm="update"
>
<form @submit.prevent="update">
<div class="grid grid-cols-1 gap-4">
<div>
<InputLabel for="edit-segment" value="Segment" />
@@ -483,20 +482,8 @@ watch(
/>
</div>
</div>
<div class="flex justify-end gap-2 mt-6">
<button
type="button"
@click="closeEdit"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
<PrimaryButton :disabled="editForm.processing">Save</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
</UpdateDialog>
<table class="min-w-full text-left text-sm">
<thead>
<tr class="border-b">
@@ -1,7 +1,8 @@
<script setup>
// flowbite-vue table imports removed; using DataTableClient
import { EditIcon, TrashBinIcon } from "@/Utilities/Icons";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import { computed, onMounted, ref } from "vue";
import { router, useForm } from "@inertiajs/vue3";
@@ -216,12 +217,15 @@ const destroyAction = () => {
</DataTableClient>
</div>
<DialogModal :show="drawerEdit" @close="closeEditDrawer">
<template #title>
<span>Spremeni akcijo</span>
</template>
<template #content>
<form @submit.prevent="update">
<UpdateDialog
:show="drawerEdit"
title="Spremeni akcijo"
confirm-text="Shrani"
:processing="form.processing"
@close="closeEditDrawer"
@confirm="update"
>
<form @submit.prevent="update">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Ime" />
<TextInput
@@ -271,28 +275,21 @@ const destroyAction = () => {
/>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="form.recentlySuccessful" class="me-3">
Shranjuje.
</ActionMessage>
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Shrani
</PrimaryButton>
<div v-if="form.recentlySuccessful" class="mt-4 text-sm text-green-600">
Shranjuje.
</div>
</form>
</template>
</DialogModal>
</UpdateDialog>
<DialogModal :show="drawerCreate" @close="closeCreateDrawer">
<template #title>
<span>Dodaj akcijo</span>
</template>
<template #content>
<form @submit.prevent="store">
<CreateDialog
:show="drawerCreate"
title="Dodaj akcijo"
confirm-text="Dodaj"
:processing="createForm.processing"
@close="closeCreateDrawer"
@confirm="store"
>
<form @submit.prevent="store">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="nameCreate" value="Ime" />
<TextInput
@@ -340,21 +337,11 @@ const destroyAction = () => {
/>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="createForm.recentlySuccessful" class="me-3">
Shranjuje.
</ActionMessage>
<PrimaryButton
:class="{ 'opacity-25': createForm.processing }"
:disabled="createForm.processing"
>
Dodaj
</PrimaryButton>
<div v-if="createForm.recentlySuccessful" class="mt-4 text-sm text-green-600">
Shranjuje.
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title> Delete action </template>
@@ -1,7 +1,8 @@
<script setup>
// flowbite-vue table imports removed; using DataTableClient
import { EditIcon, TrashBinIcon, DottedMenu } from "@/Utilities/Icons";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import ConfirmationModal from "@/Components/ConfirmationModal.vue";
import { computed, onMounted, ref, watch, nextTick } from "vue";
import { router, useForm } from "@inertiajs/vue3";
@@ -508,12 +509,16 @@ const destroyDecision = () => {
</template>
</DataTableClient>
</div>
<DialogModal :show="drawerEdit" @close="closeEditDrawer">
<template #title>
<span>Spremeni odločitev</span>
</template>
<template #content>
<form @submit.prevent="update">
<UpdateDialog
:show="drawerEdit"
title="Spremeni odločitev"
confirm-text="Shrani"
:processing="form.processing"
:disabled="!eventsValidEdit"
@close="closeEditDrawer"
@confirm="update"
>
<form @submit.prevent="update">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Ime" />
<TextInput
@@ -729,28 +734,22 @@ const destroyDecision = () => {
</div>
</div>
<div class="flex justify-end mt-6">
<ActionMessage :on="form.recentlySuccessful" class="me-3">
Shranjuje.
</ActionMessage>
<PrimaryButton
:class="{ 'opacity-25': form.processing || !eventsValidEdit }"
:disabled="form.processing || !eventsValidEdit"
>
Shrani
</PrimaryButton>
<div v-if="form.recentlySuccessful" class="mt-6 text-sm text-green-600">
Shranjuje.
</div>
</form>
</template>
</DialogModal>
</UpdateDialog>
<DialogModal :show="drawerCreate" @close="closeCreateDrawer">
<template #title>
<span>Dodaj odločitev</span>
</template>
<template #content>
<form @submit.prevent="store">
<CreateDialog
:show="drawerCreate"
title="Dodaj odločitev"
confirm-text="Dodaj"
:processing="createForm.processing"
:disabled="!eventsValidCreate"
@close="closeCreateDrawer"
@confirm="store"
>
<form @submit.prevent="store">
<div class="col-span-6 sm:col-span-4">
<InputLabel for="nameCreate" value="Ime" />
<TextInput
@@ -971,21 +970,11 @@ const destroyDecision = () => {
</div>
</div>
<div class="flex justify-end mt-4">
<ActionMessage :on="createForm.recentlySuccessful" class="me-3">
Shranjuje.
</ActionMessage>
<PrimaryButton
:class="{ 'opacity-25': createForm.processing || !eventsValidCreate }"
:disabled="createForm.processing || !eventsValidCreate"
>
Dodaj
</PrimaryButton>
<div v-if="createForm.recentlySuccessful" class="mt-6 text-sm text-green-600">
Shranjuje.
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
<ConfirmationModal :show="showDelete" @close="cancelDelete">
<template #title> Delete decision </template>
+22 -33
View File
@@ -2,7 +2,8 @@
import AppLayout from "@/Layouts/AppLayout.vue";
import { useForm, router } from "@inertiajs/vue3";
import { ref } from "vue";
import DialogModal from "@/Components/DialogModal.vue";
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
@@ -84,10 +85,15 @@ const update = () => {
<PrimaryButton @click="openCreate">+ New</PrimaryButton>
</div>
<DialogModal :show="showCreate" @close="closeCreate">
<template #title>New Segment</template>
<template #content>
<form @submit.prevent="store" class="space-y-4">
<CreateDialog
:show="showCreate"
title="New Segment"
confirm-text="Create"
:processing="createForm.processing"
@close="closeCreate"
@confirm="store"
>
<form @submit.prevent="store" class="space-y-4">
<div>
<InputLabel for="nameCreate" value="Name" />
<TextInput
@@ -120,24 +126,18 @@ const update = () => {
/>
<label for="excludeCreate">Exclude</label>
</div>
<div class="flex justify-end gap-2 mt-4">
<button
type="button"
@click="closeCreate"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
<PrimaryButton :disabled="createForm.processing">Create</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
</CreateDialog>
<DialogModal :show="showEdit" @close="closeEdit">
<template #title>Edit Segment</template>
<template #content>
<form @submit.prevent="update" class="space-y-4">
<UpdateDialog
:show="showEdit"
title="Edit Segment"
confirm-text="Save"
:processing="editForm.processing"
@close="closeEdit"
@confirm="update"
>
<form @submit.prevent="update" class="space-y-4">
<div>
<InputLabel for="nameEdit" value="Name" />
<TextInput
@@ -167,19 +167,8 @@ const update = () => {
<label for="excludeEdit">Exclude</label>
</div>
<div class="flex justify-end gap-2 mt-4">
<button
type="button"
@click="closeEdit"
class="px-4 py-2 rounded bg-gray-200 hover:bg-gray-300"
>
Cancel
</button>
<PrimaryButton :disabled="editForm.processing">Save</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
</UpdateDialog>
<table class="min-w-full text-left text-sm">
<thead>
+11 -7
View File
@@ -1,7 +1,7 @@
<script setup>
import AppLayout from "@/Layouts/AppLayout.vue";
import { ref } from "vue";
import { FwbTab, FwbTabs } from "flowbite-vue";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/Components/ui/tabs";
import ActionTable from "../Partials/ActionTable.vue";
import DecisionTable from "../Partials/DecisionTable.vue";
@@ -22,15 +22,19 @@ const activeTab = ref("actions");
<div class="pt-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<fwb-tabs v-model="activeTab" variant="underline">
<fwb-tab name="actions" title="Akcije">
<Tabs v-model="activeTab" class="w-full">
<TabsList class="w-full justify-start border-b rounded-none bg-transparent p-0">
<TabsTrigger value="actions" class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Akcije</TabsTrigger>
<TabsTrigger value="decisions" class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary">Odločitve</TabsTrigger>
</TabsList>
<TabsContent value="actions" class="mt-0">
<ActionTable
:actions="actions"
:decisions="decisions"
:segments="segments"
/>
</fwb-tab>
<fwb-tab name="decisions" title="Odločitve">
</TabsContent>
<TabsContent value="decisions" class="mt-0">
<DecisionTable
:decisions="decisions"
:actions="actions"
@@ -39,8 +43,8 @@ const activeTab = ref("actions");
:segments="segments"
:archive-settings="archive_settings"
/>
</fwb-tab>
</fwb-tabs>
</TabsContent>
</Tabs>
</div>
</div>
</div>
+4 -4
View File
@@ -57,21 +57,21 @@ function onRowClick(row) {
<template>
<AppLayout title="Testing Sandbox">
<div class="space-y-6 p-6">
<div class="prose dark:prose-invert max-w-none">
<div class="prose max-w-none">
<h1 class="text-2xl font-semibold">Testing Page</h1>
<p>
This page is for quick UI or component experiments. Remove or adapt as needed.
</p>
<p class="text-slate-700 dark:text-slate-200 text-sm">
<p class="text-slate-700 text-sm">
Prop example value: <span class="font-mono">{{ props.example }}</span>
</p>
</div>
<div
class="rounded-lg border border-slate-200 dark:border-slate-700 bg-white/70 dark:bg-slate-800/60 p-4 shadow-sm"
class="rounded-lg border border-slate-200 bg-white/70 p-4 shadow-sm"
>
<h2
class="text-sm font-semibold tracking-wide uppercase text-slate-500 dark:text-slate-400 mb-3"
class="text-sm font-semibold tracking-wide uppercase text-slate-500 mb-3"
>
DataTable (Client-side)
</h2>
+17 -17
View File
@@ -28,7 +28,7 @@ function handleImageError() {
<template>
<Head title="Welcome" />
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
<div class="bg-gray-50 text-black/50">
<img id="background" class="absolute -left-20 top-0 max-w-[877px]" src="https://laravel.com/assets/img/welcome/background.svg" />
<div class="relative min-h-screen flex flex-col items-center justify-center selection:bg-[#FF2D20] selection:text-white">
<div class="relative w-full max-w-2xl px-6 lg:max-w-7xl">
@@ -40,7 +40,7 @@ function handleImageError() {
<Link
v-if="$page.props.auth.user"
:href="route('dashboard')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20]"
>
Dashboard
</Link>
@@ -48,7 +48,7 @@ function handleImageError() {
<template v-else>
<Link
:href="route('login')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20]"
>
Log in
</Link>
@@ -56,7 +56,7 @@ function handleImageError() {
<Link
v-if="canRegister"
:href="route('register')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20]"
>
Register
</Link>
@@ -69,22 +69,22 @@ function handleImageError() {
<a
href="https://laravel.com/docs"
id="docs-card"
class="flex flex-col items-start gap-6 overflow-hidden rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] md:row-span-3 lg:p-10 lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
class="flex flex-col items-start gap-6 overflow-hidden rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] md:row-span-3 lg:p-10 lg:pb-10"
>
<div id="screenshot-container" class="relative flex w-full flex-1 items-stretch">
<img
src="https://laravel.com/assets/img/welcome/docs-light.svg"
alt="Laravel documentation screenshot"
class="aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.06)] dark:hidden"
class="aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.06)]"
@error="handleImageError"
/>
<img
src="https://laravel.com/assets/img/welcome/docs-dark.svg"
alt="Laravel documentation screenshot"
class="hidden aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.25)] dark:block"
class="hidden aspect-video h-full w-full flex-1 rounded-[10px] object-top object-cover drop-shadow-[0px_4px_34px_rgba(0,0,0,0.25)]"
/>
<div
class="absolute -bottom-16 -left-16 h-40 w-[calc(100%+8rem)] bg-gradient-to-b from-transparent via-white to-white dark:via-zinc-900 dark:to-zinc-900"
class="absolute -bottom-16 -left-16 h-40 w-[calc(100%+8rem)] bg-gradient-to-b from-transparent via-white to-white"
></div>
</div>
@@ -95,7 +95,7 @@ function handleImageError() {
</div>
<div class="pt-3 sm:pt-5 lg:pt-0">
<h2 class="text-xl font-semibold text-black dark:text-white">Documentation</h2>
<h2 class="text-xl font-semibold text-black">Documentation</h2>
<p class="mt-4 text-sm/relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
@@ -109,14 +109,14 @@ function handleImageError() {
<a
href="https://laracasts.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M24 8.25a.5.5 0 0 0-.5-.5H.5a.5.5 0 0 0-.5.5v12a2.5 2.5 0 0 0 2.5 2.5h19a2.5 2.5 0 0 0 2.5-2.5v-12Zm-7.765 5.868a1.221 1.221 0 0 1 0 2.264l-6.626 2.776A1.153 1.153 0 0 1 8 18.123v-5.746a1.151 1.151 0 0 1 1.609-1.035l6.626 2.776ZM19.564 1.677a.25.25 0 0 0-.177-.427H15.6a.106.106 0 0 0-.072.03l-4.54 4.543a.25.25 0 0 0 .177.427h3.783c.027 0 .054-.01.073-.03l4.543-4.543ZM22.071 1.318a.047.047 0 0 0-.045.013l-4.492 4.492a.249.249 0 0 0 .038.385.25.25 0 0 0 .14.042h5.784a.5.5 0 0 0 .5-.5v-2a2.5 2.5 0 0 0-1.925-2.432ZM13.014 1.677a.25.25 0 0 0-.178-.427H9.101a.106.106 0 0 0-.073.03l-4.54 4.543a.25.25 0 0 0 .177.427H8.4a.106.106 0 0 0 .073-.03l4.54-4.543ZM6.513 1.677a.25.25 0 0 0-.177-.427H2.5A2.5 2.5 0 0 0 0 3.75v2a.5.5 0 0 0 .5.5h1.4a.106.106 0 0 0 .073-.03l4.54-4.543Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laracasts</h2>
<h2 class="text-xl font-semibold text-black">Laracasts</h2>
<p class="mt-4 text-sm/relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
@@ -128,14 +128,14 @@ function handleImageError() {
<a
href="https://laravel-news.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10"
>
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><g fill="#FF2D20"><path d="M8.75 4.5H5.5c-.69 0-1.25.56-1.25 1.25v4.75c0 .69.56 1.25 1.25 1.25h3.25c.69 0 1.25-.56 1.25-1.25V5.75c0-.69-.56-1.25-1.25-1.25Z"/><path d="M24 10a3 3 0 0 0-3-3h-2V2.5a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2V20a3.5 3.5 0 0 0 3.5 3.5h17A3.5 3.5 0 0 0 24 20V10ZM3.5 21.5A1.5 1.5 0 0 1 2 20V3a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 .5.5v17c0 .295.037.588.11.874a.5.5 0 0 1-.484.625L3.5 21.5ZM22 20a1.5 1.5 0 1 1-3 0V9.5a.5.5 0 0 1 .5-.5H21a1 1 0 0 1 1 1v10Z"/><path d="M12.751 6.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 7.3v-.5a.75.75 0 0 1 .751-.753ZM12.751 10.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 11.3v-.5a.75.75 0 0 1 .751-.753ZM4.751 14.047h10a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-10A.75.75 0 0 1 4 15.3v-.5a.75.75 0 0 1 .751-.753ZM4.75 18.047h7.5a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-7.5A.75.75 0 0 1 4 19.3v-.5a.75.75 0 0 1 .75-.753Z"/></g></svg>
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Laravel News</h2>
<h2 class="text-xl font-semibold text-black">Laravel News</h2>
<p class="mt-4 text-sm/relaxed">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
@@ -145,7 +145,7 @@ function handleImageError() {
<svg class="size-6 shrink-0 self-center stroke-[#FF2D20]" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"/></svg>
</a>
<div class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800">
<div class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] lg:pb-10">
<div class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16">
<svg class="size-5 sm:size-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<g fill="#FF2D20">
@@ -157,17 +157,17 @@ function handleImageError() {
</div>
<div class="pt-3 sm:pt-5">
<h2 class="text-xl font-semibold text-black dark:text-white">Vibrant Ecosystem</h2>
<h2 class="text-xl font-semibold text-black">Vibrant Ecosystem</h2>
<p class="mt-4 text-sm/relaxed">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white dark:focus-visible:ring-[#FF2D20]">Forge</a>, <a href="https://vapor.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Vapor</a>, <a href="https://nova.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Nova</a>, and <a href="https://envoyer.io" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Echo</a>, <a href="https://laravel.com/docs/horizon" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white">Telescope</a>, and more.
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Forge</a>, <a href="https://vapor.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Vapor</a>, <a href="https://nova.laravel.com" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Nova</a>, and <a href="https://envoyer.io" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Echo</a>, <a href="https://laravel.com/docs/horizon" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20]">Telescope</a>, and more.
</p>
</div>
</div>
</div>
</main>
<footer class="py-16 text-center text-sm text-black dark:text-white/70">
<footer class="py-16 text-center text-sm text-black">
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
</footer>
</div>