Dev branch
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user