404 lines
13 KiB
Vue
404 lines
13 KiB
Vue
<script setup>
|
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
|
import { Link, useForm } from "@inertiajs/vue3";
|
|
import { computed, ref, watch } from "vue";
|
|
import DataTableClient from "@/Components/DataTable/DataTableClient.vue";
|
|
|
|
const props = defineProps({
|
|
setting: Object,
|
|
contracts: Array,
|
|
users: Array,
|
|
assignments: Object,
|
|
});
|
|
|
|
const form = useForm({
|
|
contract_uuid: null,
|
|
assigned_user_id: null,
|
|
start_date: null,
|
|
end_date: null,
|
|
});
|
|
|
|
// Global search (applies to both tables)
|
|
const search = ref("");
|
|
|
|
// Format helpers (Slovenian formatting)
|
|
function formatDate(value) {
|
|
if (!value) {
|
|
return "-";
|
|
}
|
|
const d = new Date(value);
|
|
if (isNaN(d)) {
|
|
return value;
|
|
}
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
const yyyy = d.getFullYear();
|
|
return `${dd}.${mm}.${yyyy}`;
|
|
}
|
|
|
|
function formatCurrencyEUR(value) {
|
|
if (value === null || value === undefined) {
|
|
return "-";
|
|
}
|
|
const n = Number(value);
|
|
if (isNaN(n)) {
|
|
return String(value);
|
|
}
|
|
// Thousands separator as dot, decimal as comma, with € suffix
|
|
return (
|
|
n.toLocaleString("sl-SI", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +
|
|
" €"
|
|
);
|
|
}
|
|
|
|
function primaryCaseAddress(contract) {
|
|
const addrs = contract?.client_case?.person?.addresses || [];
|
|
if (!Array.isArray(addrs) || addrs.length === 0) {
|
|
return "-";
|
|
}
|
|
const a = addrs[0];
|
|
const address = a?.address || "";
|
|
const country = a?.country || "";
|
|
return [address, country].filter(Boolean).join(", ");
|
|
}
|
|
|
|
function assign(contract) {
|
|
form.contract_uuid = contract.uuid;
|
|
// minimal UX: if no user selected yet, just post will fail with error; page can be enhanced later with dropdown.
|
|
form.post(route("fieldjobs.assign"));
|
|
}
|
|
|
|
function cancelAssignment(contract) {
|
|
const payload = { contract_uuid: contract.uuid };
|
|
form.transform(() => payload).post(route("fieldjobs.cancel"));
|
|
}
|
|
|
|
function isAssigned(contract) {
|
|
return !!(props.assignments && props.assignments[contract.uuid]);
|
|
}
|
|
|
|
function assignedTo(contract) {
|
|
return props.assignments?.[contract.uuid]?.assigned_to?.name || null;
|
|
}
|
|
|
|
function assignedBy(contract) {
|
|
return props.assignments?.[contract.uuid]?.assigned_by?.name || null;
|
|
}
|
|
|
|
// removed window.open behavior; default SPA navigation via Inertia Link
|
|
|
|
// Derived lists
|
|
const unassignedContracts = computed(() => {
|
|
return (props.contracts || []).filter((c) => !isAssigned(c));
|
|
});
|
|
|
|
const assignedContracts = computed(() => {
|
|
return (props.contracts || []).filter((c) => isAssigned(c));
|
|
});
|
|
|
|
// Apply search to lists
|
|
function matchesSearch(c) {
|
|
if (!search.value) {
|
|
return true;
|
|
}
|
|
const q = String(search.value).toLowerCase();
|
|
const ref = String(c.reference || "").toLowerCase();
|
|
const casePerson = String(c.client_case?.person?.full_name || "").toLowerCase();
|
|
// Optionally include client person in search as well for convenience
|
|
const clientPerson = String(c.client?.person?.full_name || "").toLowerCase();
|
|
// Include address fields
|
|
const primaryAddr = String(primaryCaseAddress(c) || "").toLowerCase();
|
|
const allAddrs = String(
|
|
(c.client_case?.person?.addresses || [])
|
|
.map((a) => `${a?.address || ""} ${a?.country || ""}`.trim())
|
|
.join(" ")
|
|
).toLowerCase();
|
|
return (
|
|
ref.includes(q) ||
|
|
casePerson.includes(q) ||
|
|
clientPerson.includes(q) ||
|
|
primaryAddr.includes(q) ||
|
|
allAddrs.includes(q)
|
|
);
|
|
}
|
|
|
|
const unassignedFiltered = computed(() =>
|
|
unassignedContracts.value.filter(matchesSearch)
|
|
);
|
|
|
|
// Filter for assigned table
|
|
const assignedFilterUserId = ref("");
|
|
const assignedContractsFiltered = computed(() => {
|
|
let list = assignedContracts.value;
|
|
if (assignedFilterUserId.value) {
|
|
list = list.filter((c) => {
|
|
const uid = props.assignments?.[c.uuid]?.assigned_to?.id;
|
|
return String(uid) === String(assignedFilterUserId.value);
|
|
});
|
|
}
|
|
return list.filter(matchesSearch);
|
|
});
|
|
|
|
// DataTableClient state per table
|
|
const unassignedSort = ref({ key: null, direction: null });
|
|
const unassignedPage = ref(1);
|
|
const unassignedPageSize = ref(10);
|
|
const assignedSort = ref({ key: null, direction: null });
|
|
const assignedPage = ref(1);
|
|
const assignedPageSize = ref(10);
|
|
|
|
watch([search, assignedFilterUserId], () => {
|
|
unassignedPage.value = 1;
|
|
assignedPage.value = 1;
|
|
});
|
|
|
|
// Column definitions for DataTableClient
|
|
const unassignedColumns = [
|
|
{ key: "reference", label: "Pogodba", sortable: true, class: "w-32" },
|
|
{
|
|
key: "case_person",
|
|
label: "Primer",
|
|
sortable: true,
|
|
formatter: (c) => c.client_case?.person?.full_name || "-",
|
|
},
|
|
{
|
|
key: "address",
|
|
label: "Naslov",
|
|
sortable: true,
|
|
formatter: (c) => primaryCaseAddress(c),
|
|
},
|
|
{
|
|
key: "client_person",
|
|
label: "Stranka",
|
|
sortable: true,
|
|
formatter: (c) => c.client?.person?.full_name || "-",
|
|
},
|
|
{
|
|
key: "start_date",
|
|
label: "Začetek",
|
|
sortable: true,
|
|
formatter: (c) => formatDate(c.start_date),
|
|
},
|
|
{
|
|
key: "balance_amount",
|
|
label: "Stanje",
|
|
align: "right",
|
|
sortable: true,
|
|
formatter: (c) => formatCurrencyEUR(c.account?.balance_amount),
|
|
},
|
|
{ key: "_actions", label: "Dejanje", class: "w-32" },
|
|
];
|
|
|
|
const assignedColumns = [
|
|
{ key: "reference", label: "Pogodba", sortable: true, class: "w-32" },
|
|
{
|
|
key: "case_person",
|
|
label: "Primer",
|
|
sortable: true,
|
|
formatter: (c) => c.client_case?.person?.full_name || "-",
|
|
},
|
|
{
|
|
key: "address",
|
|
label: "Naslov",
|
|
sortable: true,
|
|
formatter: (c) => primaryCaseAddress(c),
|
|
},
|
|
{
|
|
key: "client_person",
|
|
label: "Stranka",
|
|
sortable: true,
|
|
formatter: (c) => c.client?.person?.full_name || "-",
|
|
},
|
|
{
|
|
key: "assigned_at",
|
|
label: "Dodeljeno dne",
|
|
sortable: true,
|
|
formatter: (c) => formatDate(props.assignments?.[c.uuid]?.assigned_at),
|
|
},
|
|
{
|
|
key: "assigned_to",
|
|
label: "Dodeljeno komu",
|
|
sortable: true,
|
|
formatter: (c) => assignedTo(c) || "-",
|
|
},
|
|
{
|
|
key: "balance_amount",
|
|
label: "Stanje",
|
|
align: "right",
|
|
sortable: true,
|
|
formatter: (c) => formatCurrencyEUR(c.account?.balance_amount),
|
|
},
|
|
{ key: "_actions", label: "Dejanje", class: "w-32" },
|
|
];
|
|
|
|
// Provide derived row arrays for DataTable (already filtered)
|
|
// Add a flat numeric property `balance_amount` so the generic table sorter can sort by value
|
|
// (original data nests it under account.balance_amount which the sorter cannot reach).
|
|
const unassignedRows = computed(() =>
|
|
unassignedFiltered.value.map((c) => ({
|
|
...c,
|
|
// Ensure numeric so sorter treats it as number (server often returns string)
|
|
balance_amount:
|
|
c?.account?.balance_amount === null || c?.account?.balance_amount === undefined
|
|
? null
|
|
: Number(c.account.balance_amount),
|
|
// Flatten derived text fields so DataTable sorting/searching works
|
|
case_person: c.client_case?.person?.full_name || null,
|
|
client_person: c.client?.person?.full_name || null,
|
|
address: primaryCaseAddress(c) || null,
|
|
assigned_to: null, // not assigned yet
|
|
}))
|
|
);
|
|
const assignedRows = computed(() =>
|
|
assignedContractsFiltered.value.map((c) => ({
|
|
...c,
|
|
balance_amount:
|
|
c?.account?.balance_amount === null || c?.account?.balance_amount === undefined
|
|
? null
|
|
: Number(c.account.balance_amount),
|
|
case_person: c.client_case?.person?.full_name || null,
|
|
client_person: c.client?.person?.full_name || null,
|
|
address: primaryCaseAddress(c) || null,
|
|
assigned_to: assignedTo(c) || null,
|
|
}))
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout title="Dodeljevanje terenskih opravil">
|
|
<template #header></template>
|
|
<div class="pt-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<div
|
|
v-if="!setting"
|
|
class="bg-yellow-50 border border-yellow-200 text-yellow-800 rounded p-4 mb-6"
|
|
>
|
|
Nastavitev za terenska opravila ni najdena. Najprej jo ustvarite v Nastavitve →
|
|
Nastavitve terenskih opravil.
|
|
</div>
|
|
<!-- Global search -->
|
|
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-4 mb-6">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1"
|
|
>Iskanje (št. pogodbe, nazivu ali naslovu)</label
|
|
>
|
|
<input
|
|
v-model="search"
|
|
type="text"
|
|
placeholder="Išči po številki pogodbe, nazivu ali naslovu"
|
|
class="border rounded px-3 py-2 w-full max-w-xl"
|
|
/>
|
|
</div>
|
|
<!-- Unassigned (Assignable) Contracts via DataTableClient -->
|
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6 mb-8">
|
|
<h2 class="text-xl font-semibold mb-4">Pogodbe (nedodeljene)</h2>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 mb-1"
|
|
>Dodeli uporabniku</label
|
|
>
|
|
<select
|
|
v-model="form.assigned_user_id"
|
|
class="border rounded px-3 py-2 w-full max-w-xs"
|
|
>
|
|
<option :value="null" disabled>Izberite uporabnika</option>
|
|
<option v-for="u in users || []" :key="u.id" :value="u.id">
|
|
{{ u.name }}
|
|
</option>
|
|
</select>
|
|
<div v-if="form.errors.assigned_user_id" class="text-red-600 text-sm mt-1">
|
|
{{ form.errors.assigned_user_id }}
|
|
</div>
|
|
</div>
|
|
<DataTableClient
|
|
:columns="unassignedColumns"
|
|
:rows="unassignedRows"
|
|
:search-keys="['reference', 'case_person', 'client_person', 'address']"
|
|
v-model:sort="unassignedSort"
|
|
v-model:search="search"
|
|
v-model:page="unassignedPage"
|
|
v-model:pageSize="unassignedPageSize"
|
|
>
|
|
<template #cell-case_person="{ row }">
|
|
<Link
|
|
v-if="row.client_case?.uuid"
|
|
:href="route('clientCase.show', { client_case: row.client_case.uuid })"
|
|
class="text-indigo-600 hover:underline"
|
|
>
|
|
{{ row.client_case?.person?.full_name || "Primer stranke" }}
|
|
</Link>
|
|
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
|
|
</template>
|
|
<template #cell-_actions="{ row }">
|
|
<button
|
|
class="px-3 py-1 text-xs rounded bg-indigo-600 text-white"
|
|
@click="assign(row)"
|
|
>
|
|
Dodeli
|
|
</button>
|
|
</template>
|
|
<template #empty>
|
|
<div class="text-sm text-gray-500 py-4 text-center">
|
|
Ni najdenih pogodb.
|
|
</div>
|
|
</template>
|
|
</DataTableClient>
|
|
</div>
|
|
|
|
<!-- Assigned Contracts via DataTableClient -->
|
|
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xl font-semibold">Dodeljene pogodbe</h2>
|
|
<div class="flex items-center gap-2">
|
|
<label class="text-sm text-gray-700">Filter po uporabniku</label>
|
|
<select v-model="assignedFilterUserId" class="border rounded px-3 py-2">
|
|
<option value="">Vsi</option>
|
|
<option v-for="u in users || []" :key="u.id" :value="u.id">
|
|
{{ u.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<DataTableClient
|
|
:columns="assignedColumns"
|
|
:rows="assignedRows"
|
|
:search-keys="[
|
|
'reference',
|
|
'case_person',
|
|
'client_person',
|
|
'address',
|
|
'assigned_to',
|
|
]"
|
|
v-model:sort="assignedSort"
|
|
v-model:search="search"
|
|
v-model:page="assignedPage"
|
|
v-model:pageSize="assignedPageSize"
|
|
>
|
|
<template #cell-case_person="{ row }">
|
|
<Link
|
|
v-if="row.client_case?.uuid"
|
|
:href="route('clientCase.show', { client_case: row.client_case.uuid })"
|
|
class="text-indigo-600 hover:underline"
|
|
>
|
|
{{ row.client_case?.person?.full_name || "Primer stranke" }}
|
|
</Link>
|
|
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
|
|
</template>
|
|
<template #cell-_actions="{ row }">
|
|
<button
|
|
class="px-3 py-1 text-xs rounded bg-red-600 text-white"
|
|
@click="cancelAssignment(row)"
|
|
>
|
|
Prekliči
|
|
</button>
|
|
</template>
|
|
<template #empty>
|
|
<div class="text-sm text-gray-500 py-4 text-center">
|
|
Ni dodeljenih pogodb za izbran filter.
|
|
</div>
|
|
</template>
|
|
</DataTableClient>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|