423 lines
15 KiB
Vue
423 lines
15 KiB
Vue
<script setup>
|
||
import AppLayout from "@/Layouts/AppLayout.vue";
|
||
import { Link, useForm } from "@inertiajs/vue3";
|
||
import { computed, ref, watch } from "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);
|
||
});
|
||
|
||
// Pagination state
|
||
const unassignedPage = ref(1);
|
||
const unassignedPerPage = ref(10);
|
||
const assignedPage = ref(1);
|
||
const assignedPerPage = ref(10);
|
||
|
||
// Reset pages when filters change
|
||
watch([search], () => {
|
||
unassignedPage.value = 1;
|
||
assignedPage.value = 1;
|
||
});
|
||
watch([assignedFilterUserId], () => {
|
||
assignedPage.value = 1;
|
||
});
|
||
|
||
// Paginated lists
|
||
const unassignedTotal = computed(() => unassignedFiltered.value.length);
|
||
const unassignedTotalPages = computed(() =>
|
||
Math.max(1, Math.ceil(unassignedTotal.value / unassignedPerPage.value))
|
||
);
|
||
const unassignedPageItems = computed(() => {
|
||
const start = (unassignedPage.value - 1) * unassignedPerPage.value;
|
||
return unassignedFiltered.value.slice(start, start + unassignedPerPage.value);
|
||
});
|
||
|
||
const assignedTotal = computed(() => assignedContractsFiltered.value.length);
|
||
const assignedTotalPages = computed(() =>
|
||
Math.max(1, Math.ceil(assignedTotal.value / assignedPerPage.value))
|
||
);
|
||
const assignedPageItems = computed(() => {
|
||
const start = (assignedPage.value - 1) * assignedPerPage.value;
|
||
return assignedContractsFiltered.value.slice(start, start + assignedPerPage.value);
|
||
});
|
||
</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 -->
|
||
<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>
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full text-left text-sm">
|
||
<thead>
|
||
<tr class="border-b">
|
||
<th class="py-2 pr-4">Pogodba</th>
|
||
<th class="py-2 pr-4">Primer</th>
|
||
<th class="py-2 pr-4">Naslov</th>
|
||
<th class="py-2 pr-4">Stranka</th>
|
||
<th class="py-2 pr-4">Začetek</th>
|
||
<th class="py-2 pr-4">Stanje</th>
|
||
<th class="py-2 pr-4">Dejanje</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="c in unassignedPageItems"
|
||
:key="c.uuid"
|
||
class="border-b last:border-0"
|
||
>
|
||
<td class="py-2 pr-4">{{ c.reference }}</td>
|
||
<td class="py-2 pr-4">
|
||
<Link
|
||
v-if="c.client_case?.uuid"
|
||
:href="
|
||
route('clientCase.show', { client_case: c.client_case.uuid })
|
||
"
|
||
class="text-indigo-600 hover:underline"
|
||
>
|
||
{{ c.client_case?.person?.full_name || "Primer stranke" }}
|
||
</Link>
|
||
<span v-else>{{ c.client_case?.person?.full_name || "-" }}</span>
|
||
</td>
|
||
<td class="py-2 pr-4">{{ primaryCaseAddress(c) }}</td>
|
||
<td class="py-2 pr-4">
|
||
{{ c.client?.person?.full_name || "-" }}
|
||
</td>
|
||
<td class="py-2 pr-4">{{ formatDate(c.start_date) }}</td>
|
||
<td class="py-2 pr-4">
|
||
{{ formatCurrencyEUR(c.account?.balance_amount) }}
|
||
</td>
|
||
<td class="py-2 pr-4 flex items-center gap-2">
|
||
<button
|
||
class="px-3 py-1 text-sm rounded bg-indigo-600 text-white"
|
||
@click="assign(c)"
|
||
>
|
||
Dodeli
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="unassignedPageItems.length === 0">
|
||
<td colspan="9" class="py-4 text-gray-500">Ni najdenih pogodb.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<!-- Unassigned pagination -->
|
||
<div class="flex items-center justify-between mt-4 text-sm text-gray-700">
|
||
<div>
|
||
Prikazano
|
||
{{
|
||
Math.min((unassignedPage - 1) * unassignedPerPage + 1, unassignedTotal)
|
||
}}–{{ Math.min(unassignedPage * unassignedPerPage, unassignedTotal) }} od
|
||
{{ unassignedTotal }}
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<select v-model.number="unassignedPerPage" class="border rounded px-2 py-1">
|
||
<option :value="10">10</option>
|
||
<option :value="25">25</option>
|
||
<option :value="50">50</option>
|
||
</select>
|
||
<button
|
||
class="px-2 py-1 border rounded disabled:opacity-50"
|
||
:disabled="unassignedPage <= 1"
|
||
@click="unassignedPage = Math.max(1, unassignedPage - 1)"
|
||
>
|
||
Prejšnja
|
||
</button>
|
||
<span>Stran {{ unassignedPage }} / {{ unassignedTotalPages }}</span>
|
||
<button
|
||
class="px-2 py-1 border rounded disabled:opacity-50"
|
||
:disabled="unassignedPage >= unassignedTotalPages"
|
||
@click="
|
||
unassignedPage = Math.min(unassignedTotalPages, unassignedPage + 1)
|
||
"
|
||
>
|
||
Naslednja
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Assigned Contracts -->
|
||
<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>
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full text-left text-sm">
|
||
<thead>
|
||
<tr class="border-b">
|
||
<th class="py-2 pr-4">Pogodba</th>
|
||
<th class="py-2 pr-4">Primer</th>
|
||
<th class="py-2 pr-4">Naslov</th>
|
||
<th class="py-2 pr-4">Stranka</th>
|
||
<th class="py-2 pr-4">Dodeljeno dne</th>
|
||
<th class="py-2 pr-4">Dodeljeno komu</th>
|
||
<th class="py-2 pr-4">Stanje</th>
|
||
<th class="py-2 pr-4">Dejanje</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="c in assignedPageItems"
|
||
:key="c.uuid"
|
||
class="border-b last:border-0"
|
||
>
|
||
<td class="py-2 pr-4">{{ c.reference }}</td>
|
||
<td class="py-2 pr-4">
|
||
<Link
|
||
v-if="c.client_case?.uuid"
|
||
:href="
|
||
route('clientCase.show', { client_case: c.client_case.uuid })
|
||
"
|
||
class="text-indigo-600 hover:underline"
|
||
>
|
||
{{ c.client_case?.person?.full_name || "Primer stranke" }}
|
||
</Link>
|
||
<span v-else>{{ c.client_case?.person?.full_name || "-" }}</span>
|
||
</td>
|
||
<td class="py-2 pr-4">{{ primaryCaseAddress(c) }}</td>
|
||
<td class="py-2 pr-4">
|
||
{{ c.client?.person?.full_name || "-" }}
|
||
</td>
|
||
<td class="py-2 pr-4">
|
||
{{ formatDate(props.assignments?.[c.uuid]?.assigned_at) }}
|
||
</td>
|
||
<td class="py-2 pr-4">{{ assignedTo(c) || "-" }}</td>
|
||
<td class="py-2 pr-4">
|
||
{{ formatCurrencyEUR(c.account?.balance_amount) }}
|
||
</td>
|
||
<td class="py-2 pr-4">
|
||
<button
|
||
class="px-3 py-1 text-sm rounded bg-red-600 text-white"
|
||
@click="cancelAssignment(c)"
|
||
>
|
||
Prekliči
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="assignedPageItems.length === 0">
|
||
<td colspan="8" class="py-4 text-gray-500">
|
||
Ni dodeljenih pogodb za izbran filter.
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<!-- Assigned pagination -->
|
||
<div class="flex items-center justify-between mt-4 text-sm text-gray-700">
|
||
<div>
|
||
Prikazano
|
||
{{ Math.min((assignedPage - 1) * assignedPerPage + 1, assignedTotal) }}–{{
|
||
Math.min(assignedPage * assignedPerPage, assignedTotal)
|
||
}}
|
||
od {{ assignedTotal }}
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<select v-model.number="assignedPerPage" class="border rounded px-2 py-1">
|
||
<option :value="10">10</option>
|
||
<option :value="25">25</option>
|
||
<option :value="50">50</option>
|
||
</select>
|
||
<button
|
||
class="px-2 py-1 border rounded disabled:opacity-50"
|
||
:disabled="assignedPage <= 1"
|
||
@click="assignedPage = Math.max(1, assignedPage - 1)"
|
||
>
|
||
Prejšnja
|
||
</button>
|
||
<span>Stran {{ assignedPage }} / {{ assignedTotalPages }}</span>
|
||
<button
|
||
class="px-2 py-1 border rounded disabled:opacity-50"
|
||
:disabled="assignedPage >= assignedTotalPages"
|
||
@click="assignedPage = Math.min(assignedTotalPages, assignedPage + 1)"
|
||
>
|
||
Naslednja
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</AppLayout>
|
||
</template>
|