Teren-app/resources/js/Pages/FieldJob/Index.vue
2025-09-30 22:00:03 +02:00

423 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>