Changes to field job view and controller

This commit is contained in:
Simon Pocrnjič
2025-12-28 12:15:37 +01:00
parent dea7432deb
commit 84b75143df
3 changed files with 411 additions and 173 deletions
+324 -149
View File
@@ -3,7 +3,7 @@ import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, useForm, router } from "@inertiajs/vue3";
import { computed, ref, watch } from "vue";
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
import { Card } from "@/Components/ui/card";
import { Card, CardTitle } from "@/Components/ui/card";
import { Input } from "@/Components/ui/input";
import { Button } from "@/Components/ui/button";
import { Label } from "@/Components/ui/label";
@@ -15,19 +15,45 @@ import {
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { AlertCircle } from "lucide-vue-next";
import { AlertCircle, FileCheckCornerIcon, FilesIcon, Filter } from "lucide-vue-next";
import Checkbox from "@/Components/ui/checkbox/Checkbox.vue";
import Pagination from "@/Components/Pagination.vue";
import { watchDebounced } from "@vueuse/core";
import Dropdown from "@/Components/Dropdown.vue";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/Components/ui/dropdown-menu";
import AppPopover from "@/Components/app/ui/AppPopover.vue";
import InputLabel from "@/Components/InputLabel.vue";
import AppMultiSelect from "@/Components/app/ui/AppMultiSelect.vue";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
const props = defineProps({
setting: Object,
unassignedContracts: Object,
assignedContracts: Object,
users: Array,
unassignedClients: Array,
assignedClients: Array,
filters: Object,
});
const filterUnassignedContracts = ref(false);
const filterAssignedContracts = ref(false);
const filterUnassignedSelectedClient = ref(
Array.isArray(props.filters?.unassigned_client_uuids)
? props.filters.unassigned_client_uuids
: []
);
const filterAssignedSelectedClient = ref(
Array.isArray(props.filters?.assigned_client_uuids)
? props.filters.assigned_client_uuids
: []
);
const form = useForm({
contract_uuid: null,
assigned_user_id: null,
@@ -85,6 +111,7 @@ function toggleContractSelection(uuid, checked) {
// Initialize search and filter from URL params
const search = ref(props.filters?.search || "");
const searchAssigned = ref(props.filters?.search_assigned || "");
const assignedFilterUserId = ref(props.filters?.assigned_user_id || "all");
// Navigation helpers
@@ -128,6 +155,37 @@ watchDebounced(
}
);
const applySearchAssigned = async function () {
const params = Object.fromEntries(
new URLSearchParams(window.location.search).entries()
);
const term = (searchAssigned.value || "").trim();
if (term) {
params.search_assigned = term;
} else {
delete params.search_assigned;
}
delete params.page_assignments;
router.get(route("fieldjobs.index"), params, {
preserveState: true,
replace: true,
preserveScroll: true,
only: ["assignedContracts", "filters"],
});
};
watchDebounced(
() => searchAssigned.value,
(val) => {
applySearchAssigned();
},
{
debounce: 200,
maxWait: 1000,
}
);
// Watch search and filter changes
/*watch(search, (value) => {
navigateWithParams({
@@ -143,6 +201,44 @@ watch(assignedFilterUserId, (value) => {
navigateWithParams({
search: search.value || undefined,
assigned_user_id: value !== "all" ? value : undefined,
unassigned_client_uuids:
filterUnassignedSelectedClient.value?.length > 0
? filterUnassignedSelectedClient.value
: undefined,
assigned_client_uuids:
filterAssignedSelectedClient.value?.length > 0
? filterAssignedSelectedClient.value
: undefined,
page_contracts: props.unassignedContracts?.current_page,
page_assignments: 1, // Reset to first page on filter change
});
});
watch(filterUnassignedSelectedClient, (value) => {
navigateWithParams({
search: search.value || undefined,
assigned_user_id:
assignedFilterUserId.value !== "all" ? assignedFilterUserId.value : undefined,
unassigned_client_uuids: value?.length > 0 ? value : undefined,
assigned_client_uuids:
filterAssignedSelectedClient.value?.length > 0
? filterAssignedSelectedClient.value
: undefined,
page_contracts: 1, // Reset to first page on filter change
page_assignments: props.assignedContracts?.current_page,
});
});
watch(filterAssignedSelectedClient, (value) => {
navigateWithParams({
search: search.value || undefined,
assigned_user_id:
assignedFilterUserId.value !== "all" ? assignedFilterUserId.value : undefined,
unassigned_client_uuids:
filterUnassignedSelectedClient.value?.length > 0
? filterUnassignedSelectedClient.value
: undefined,
assigned_client_uuids: value?.length > 0 ? value : undefined,
page_contracts: props.unassignedContracts?.current_page,
page_assignments: 1, // Reset to first page on filter change
});
@@ -248,7 +344,7 @@ const unassignedRows = computed(() =>
? null
: Number(c.account.balance_amount),
case_person: c.client_case?.person?.full_name || null,
client_person: c.client?.person?.full_name || null,
client_person: c.client_case?.client?.person?.full_name || null,
address: primaryCaseAddress(c) || null,
}))
);
@@ -261,7 +357,7 @@ const assignedRows = computed(() =>
? null
: Number(c.account.balance_amount),
case_person: c.client_case?.person?.full_name || null,
client_person: c.client?.person?.full_name || null,
client_person: c.client_case?.client?.person?.full_name || null,
address: primaryCaseAddress(c) || null,
assigned_to: c.last_field_jobs.assigned_user.name || null,
assigned_at_formatted: formatDate(c.last_field_jobs.assigned_at),
@@ -272,130 +368,181 @@ const assignedRows = computed(() =>
<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="mb-6 flex items-start gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4"
<div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="p-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<AlertCircle class="h-5 w-5 text-yellow-600 shrink-0 mt-0.5" />
<p class="text-sm text-yellow-800">
Nastavitev za terenska opravila ni najdena. Najprej jo ustvarite v Nastavitve
Nastavitve terenskih opravil.
</p>
</div>
<!-- Unassigned (Assignable) Contracts -->
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg mb-8">
<div class="p-4 border-b">
<h2 class="text-xl font-semibold">Pogodbe (nedodeljene)</h2>
</div>
<div class="p-4 border-b space-y-4">
<div class="space-y-2">
<Label for="assign-user">Dodeli uporabniku</Label>
<Select v-model="form.assigned_user_id">
<SelectTrigger id="assign-user" class="max-w-xs">
<SelectValue placeholder="Izberite uporabnika" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="u in users || []" :key="u.id" :value="u.id">
{{ u.name }}
</SelectItem>
</SelectContent>
</Select>
<div v-if="form.errors.assigned_user_id" class="text-red-600 text-sm">
{{ form.errors.assigned_user_id }}
</div>
</div>
<template #header>
<div class="flex items-center gap-2">
<Button
:disabled="!selectedContractUuids.length || !form.assigned_user_id"
@click="assignSelected"
>
Dodeli izbrane
<Badge
v-if="selectedContractUuids.length"
variant="secondary"
class="ml-2"
>
{{ selectedContractUuids.length }}
</Badge>
</Button>
<Button
variant="outline"
:disabled="!selectedContractUuids.length"
@click="selectedContractUuids = []"
>
Počisti izbor
</Button>
<FilesIcon size="18" />
<CardTitle class="uppercase">Pogodbe (nedodeljene)</CardTitle>
</div>
</div>
<DataTable
:columns="unassignedColumns"
:data="unassignedRows"
:meta="{
current_page: unassignedContracts.current_page,
per_page: unassignedContracts.per_page,
total: unassignedContracts.total,
last_page: unassignedContracts.last_page,
from: unassignedContracts.from,
to: unassignedContracts.to,
links: unassignedContracts.links,
}"
row-key="uuid"
:page-size="props.unassignedContracts?.per_page || 10"
:page-size-options="[10, 15, 25, 50, 100]"
:show-toolbar="true"
route-name="fieldjobs.index"
page-param-name="page_contracts"
per-page-param-name="per_page_contracts"
</template>
<div
v-if="!setting"
class="mb-6 flex items-start gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4"
>
<template #toolbar-filters>
<div class="flex items-center gap-2 w-full">
<Input
v-model="search"
placeholder="Išči po pogodbi, primeru, stranki, naslovu..."
class="w-[320px]"
/>
</div>
</template>
<template #cell-_select="{ row }">
<Checkbox
@update:model-value="
(checked) => toggleContractSelection(row.uuid, checked)
"
/>
</template>
<template #cell-case_person="{ row }">
<Link
v-if="row.client_case?.uuid"
:href="route('clientCase.show', { client_case: row.client_case.uuid })"
class="font-semibold hover:underline text-primary-700"
>
{{ row.client_case?.person?.full_name || "Primer stranke" }}
</Link>
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
</template>
<template #cell-start_date="{ row }">
{{ formatDate(row.start_date) }}
</template>
<template #cell-balance_amount="{ row }">
<div class="text-right">
{{ formatCurrencyEUR(row.account?.balance_amount) }}
</div>
</template>
<template #cell-_actions="{ row }">
<Button size="sm" @click="assign(row)">Dodeli</Button>
</template>
</DataTable>
</div>
<!-- Assigned Contracts -->
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="p-4 border-b">
<h2 class="text-xl font-semibold">Dodeljene pogodbe</h2>
<AlertCircle class="h-5 w-5 text-yellow-600 shrink-0 mt-0.5" />
<p class="text-sm text-yellow-800">
Nastavitev za terenska opravila ni najdena. Najprej jo ustvarite v
Nastavitve Nastavitve terenskih opravil.
</p>
</div>
<!-- Unassigned (Assignable) Contracts -->
<div class="overflow-hidden mb-8 border-t">
<div class="p-4 flex flex-row gap-2 items-end">
<div class="space-y-2">
<Label for="assign-user">Dodeli uporabniku</Label>
<Select v-model="form.assigned_user_id">
<SelectTrigger id="assign-user" class="max-w-xs min-w-60">
<SelectValue placeholder="Izberite uporabnika" />
</SelectTrigger>
<SelectContent class="min-w-60">
<SelectItem v-for="u in users || []" :key="u.id" :value="u.id">
{{ u.name }}
</SelectItem>
</SelectContent>
</Select>
<div v-if="form.errors.assigned_user_id" class="text-red-600 text-sm">
{{ form.errors.assigned_user_id }}
</div>
</div>
<div class="flex items-center gap-2">
<Button
:disabled="!selectedContractUuids.length || !form.assigned_user_id"
@click="assignSelected"
>
Dodeli izbrane
<Badge
v-if="selectedContractUuids.length"
variant="secondary"
class="ml-2"
>
{{ selectedContractUuids.length }}
</Badge>
</Button>
<Button
variant="outline"
:disabled="!selectedContractUuids.length"
@click="selectedContractUuids = []"
>
Počisti izbor
</Button>
</div>
</div>
<DataTable
:columns="unassignedColumns"
:data="unassignedRows"
:meta="{
current_page: unassignedContracts.current_page,
per_page: unassignedContracts.per_page,
total: unassignedContracts.total,
last_page: unassignedContracts.last_page,
from: unassignedContracts.from,
to: unassignedContracts.to,
links: unassignedContracts.links,
}"
row-key="uuid"
:page-size="props.unassignedContracts?.per_page || 10"
:page-size-options="[10, 15, 25, 50, 100]"
:show-toolbar="true"
route-name="fieldjobs.index"
page-param-name="page_contracts"
per-page-param-name="per_page_contracts"
>
<template #toolbar-filters>
<div class="flex items-center gap-2 w-full">
<AppPopover
v-model:open="filterUnassignedContracts"
align="start"
content-class="w-[350px]"
>
<template #trigger>
<Button variant="outline"
><Filter class="h-4 w-4" /> Filtri
</Button>
</template>
<div class="space-y-3">
<div class="space-y-1.5">
<InputLabel>Iskanje</InputLabel>
<Input
v-model="search"
placeholder="Išči po pogodbi, primeru, stranki, naslovu..."
class="w-full"
/>
</div>
<div class="space-y-1.5">
<InputLabel>Stranke</InputLabel>
<AppMultiSelect
v-model="filterUnassignedSelectedClient"
:items="
(props.unassignedClients || []).map((client) => ({
value: client.uuid,
label: client.person.full_name,
}))
"
placeholder="Vse stranke"
search-placeholder="Išči stranko..."
empty-text="Ni strank"
chip-variant="secondary"
/>
</div>
</div>
</AppPopover>
</div>
</template>
<template #cell-_select="{ row }">
<Checkbox
@update:model-value="
(checked) => toggleContractSelection(row.uuid, checked)
"
/>
</template>
<template #cell-case_person="{ row }">
<Link
v-if="row.client_case?.uuid"
:href="route('clientCase.show', { client_case: row.client_case.uuid })"
class="font-semibold hover:underline text-primary-700"
>
{{ row.client_case?.person?.full_name || "Primer stranke" }}
</Link>
<span v-else>{{ row.client_case?.person?.full_name || "-" }}</span>
</template>
<template #cell-start_date="{ row }">
{{ formatDate(row.start_date) }}
</template>
<template #cell-balance_amount="{ row }">
<div class="text-right">
{{ formatCurrencyEUR(row.account?.balance_amount) }}
</div>
</template>
<template #cell-_actions="{ row }">
<Button size="sm" @click="assign(row)">Dodeli</Button>
</template>
</DataTable>
</div>
</AppCard>
<!-- Assigned Contracts -->
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="p-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<FileCheckCornerIcon size="18" />
<CardTitle class="uppercase">Dodeljene pogodbe</CardTitle>
</div>
</template>
<DataTable
:columns="assignedColumns"
:data="assignedRows"
@@ -418,31 +565,59 @@ const assignedRows = computed(() =>
>
<template #toolbar-filters>
<div class="flex items-center gap-2 w-full">
<Input
v-model="search_contract"
placeholder="Išči po pogodbi, primeru, stranki..."
class="w-[320px]"
/>
<div class="flex items-center gap-2 ml-4">
<Label for="filter-user" class="text-sm whitespace-nowrap"
>Filter po uporabniku</Label
>
<Select v-model="assignedFilterUserId">
<SelectTrigger id="filter-user" class="w-48">
<SelectValue placeholder="Vsi" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Vsi</SelectItem>
<SelectItem
v-for="u in users || []"
:key="u.id"
:value="String(u.id)"
>
{{ u.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<AppPopover
v-model:open="filterAssignedContracts"
align="start"
content-class="w-[350px]"
>
<template #trigger>
<Button variant="outline"><Filter class="h-4 w-4" /> Filtri </Button>
</template>
<div class="space-y-3">
<div class="space-y-1.5">
<InputLabel>Iskanje</InputLabel>
<Input
v-model="searchAssigned"
placeholder="Išči po pogodbi, primeru, stranki, naslovu..."
class="w-full"
/>
</div>
<div class="space-y-1.5">
<InputLabel>Filter po uporabniku</InputLabel>
<Select v-model="assignedFilterUserId">
<SelectTrigger id="filter-user">
<SelectValue placeholder="Vsi" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Vsi</SelectItem>
<SelectItem
v-for="u in users || []"
:key="u.id"
:value="String(u.id)"
>
{{ u.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-1.5">
<InputLabel>Stranke</InputLabel>
<AppMultiSelect
v-model="filterAssignedSelectedClient"
:items="
(props.assignedClients || []).map((client) => ({
value: client.uuid,
label: client.person.full_name,
}))
"
placeholder="Vse stranke"
search-placeholder="Išči stranko..."
empty-text="Ni strank"
chip-variant="secondary"
/>
</div>
</div>
</AppPopover>
</div>
</template>
<template #cell-case_person="{ row }">
@@ -469,7 +644,7 @@ const assignedRows = computed(() =>
</Button>
</template>
</DataTable>
</div>
</AppCard>
</div>
</div>
</AppLayout>