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

View File

@ -26,10 +26,13 @@ public function index(Request $request)
])->filter()->unique()->values(); ])->filter()->unique()->values();
$search = $request->input('search'); $search = $request->input('search');
$searchAssigned = $request->input('search_assigned');
$assignedUserId = $request->input('assigned_user_id'); $assignedUserId = $request->input('assigned_user_id');
$unassignedClientUuids = $request->input('unassigned_client_uuids');
$assignedClientUuids = $request->input('assigned_client_uuids');
$unassignedContracts = Contract::query() $unassignedContracts = Contract::query()
->with(['clientCase.person.addresses', 'clientCase.client.person', 'type', 'account']) ->with(['clientCase.person.addresses', 'clientCase.client.person:id,uuid,full_name', 'type', 'account'])
->when($segmentIds->isNotEmpty(), fn($q) => ->when($segmentIds->isNotEmpty(), fn($q) =>
$q->whereHas('segments', fn($rq) => $rq->whereIn('segments.id', $segmentIds)), $q->whereHas('segments', fn($rq) => $rq->whereIn('segments.id', $segmentIds)),
fn($q) => $q->whereRaw('1 = 0') fn($q) => $q->whereRaw('1 = 0')
@ -45,50 +48,83 @@ public function index(Request $request)
) )
) )
) )
->when(!empty($unassignedClientUuids) && is_array($unassignedClientUuids), fn ($q) =>
$q->whereHas('clientCase.client', fn($cq) =>
$cq->whereIn('uuid', $unassignedClientUuids)
)
)
->whereDoesntHave('fieldJobs', fn ($q) => ->whereDoesntHave('fieldJobs', fn ($q) =>
$q->whereNull('completed_at') $q->whereNull('completed_at')
->whereNull('cancelled_at') ->whereNull('cancelled_at')
) )
->latest('id') ->latest('id');
->paginate(
$request->input('per_page_contracts', 10),
['*'],
'page_contracts',
$request->input('page_contracts', 1)
);
$unassignedClients = $unassignedContracts->get()
->pluck('clientCase.client')
->filter()
->unique('id');
$assignedContracts = Contract::query() $assignedContracts = Contract::query()
->with(['clientCase.person.addresses', 'clientCase.client.person', 'type', 'account', 'lastFieldJobs', 'lastFieldJobs.assignedUser', 'lastFieldJobs.user']) ->with(['clientCase.person.addresses', 'clientCase.client.person:id,uuid,full_name', 'type', 'account', 'lastFieldJobs', 'lastFieldJobs.assignedUser', 'lastFieldJobs.user'])
->when($segmentIds->isNotEmpty(), fn($q) => ->when($segmentIds->isNotEmpty(), fn($q) =>
$q->whereHas('segments', fn($rq) => $rq->whereIn('segments.id', $segmentIds)), $q->whereHas('segments', fn($rq) => $rq->whereIn('segments.id', $segmentIds)),
fn($q) => $q->whereRaw('1 = 0') fn($q) => $q->whereRaw('1 = 0')
) )
->when( !empty($searchAssigned), fn ($q) =>
$q->where(fn($sq) =>
$sq->where('reference', 'like', "%{$searchAssigned}%")
->orWhereHas('clientCase.person', fn($psq) =>
$psq->where('full_name', 'ilike', "%{$searchAssigned}%")
)
->orWhereHas('clientCase.person.addresses', fn ($ccpaq) =>
$ccpaq->where('address', 'ilike', "%{$searchAssigned}")
)
)
)
->when(!empty($assignedClientUuids) && is_array($assignedClientUuids), fn ($q) =>
$q->whereHas('clientCase.client', fn($cq) =>
$cq->whereIn('uuid', $assignedClientUuids)
)
)
->whereHas('lastFieldJobs', fn ($q) => ->whereHas('lastFieldJobs', fn ($q) =>
$q->whereNull('completed_at') $q->whereNull('completed_at')
->whereNull('cancelled_at') ->whereNull('cancelled_at')
->when($assignedUserId && $assignedUserId !== 'all', fn ($jq) => ->when($assignedUserId && $assignedUserId !== 'all', fn ($jq) =>
$jq->where('assigned_user_id', $assignedUserId)) $jq->where('assigned_user_id', $assignedUserId))
) )
->latest('id') ->latest('id');
->paginate(
$request->input('per_page_assignments', 10), $assignedClients = $assignedContracts->get()
['*'], ->pluck('clientCase.client')
'page_assignments', ->filter()
$request->input('page_assignments', 1) ->unique('id');
);
$users = User::query()->orderBy('name')->get(['id', 'name']); $users = User::query()->orderBy('name')->get(['id', 'name']);
return Inertia::render('FieldJob/Index', [ return Inertia::render('FieldJob/Index', [
'setting' => $setting, 'setting' => $setting,
'unassignedContracts' => $unassignedContracts, 'unassignedContracts' => $unassignedContracts->paginate(
'assignedContracts' => $assignedContracts, $request->input('per_page_contracts', 10),
['*'],
'page_contracts',
$request->input('page_contracts', 1)
),
'assignedContracts' => $assignedContracts->paginate(
$request->input('per_page_assignments', 10),
['*'],
'page_assignments',
$request->input('page_assignments', 1)
),
'unassignedClients' => $unassignedClients,
'assignedClients' => $assignedClients,
'users' => $users, 'users' => $users,
'filters' => [ 'filters' => [
'search' => $search, 'search' => $search,
'search_assigned' => $searchAssigned,
'assigned_user_id' => $assignedUserId, 'assigned_user_id' => $assignedUserId,
'unassigned_client_uuids' => $unassignedClientUuids,
'assigned_client_uuids' => $assignedClientUuids,
], ],
]); ]);
} }

View File

@ -3,7 +3,7 @@ import AppLayout from "@/Layouts/AppLayout.vue";
import { Link, useForm, router } from "@inertiajs/vue3"; import { Link, useForm, router } from "@inertiajs/vue3";
import { computed, ref, watch } from "vue"; import { computed, ref, watch } from "vue";
import DataTable from "@/Components/DataTable/DataTableNew2.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 { Input } from "@/Components/ui/input";
import { Button } from "@/Components/ui/button"; import { Button } from "@/Components/ui/button";
import { Label } from "@/Components/ui/label"; import { Label } from "@/Components/ui/label";
@ -15,19 +15,45 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/Components/ui/select"; } 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 Checkbox from "@/Components/ui/checkbox/Checkbox.vue";
import Pagination from "@/Components/Pagination.vue"; import Pagination from "@/Components/Pagination.vue";
import { watchDebounced } from "@vueuse/core"; 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({ const props = defineProps({
setting: Object, setting: Object,
unassignedContracts: Object, unassignedContracts: Object,
assignedContracts: Object, assignedContracts: Object,
users: Array, users: Array,
unassignedClients: Array,
assignedClients: Array,
filters: Object, 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({ const form = useForm({
contract_uuid: null, contract_uuid: null,
assigned_user_id: null, assigned_user_id: null,
@ -85,6 +111,7 @@ function toggleContractSelection(uuid, checked) {
// Initialize search and filter from URL params // Initialize search and filter from URL params
const search = ref(props.filters?.search || ""); const search = ref(props.filters?.search || "");
const searchAssigned = ref(props.filters?.search_assigned || "");
const assignedFilterUserId = ref(props.filters?.assigned_user_id || "all"); const assignedFilterUserId = ref(props.filters?.assigned_user_id || "all");
// Navigation helpers // 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 and filter changes
/*watch(search, (value) => { /*watch(search, (value) => {
navigateWithParams({ navigateWithParams({
@ -143,6 +201,44 @@ watch(assignedFilterUserId, (value) => {
navigateWithParams({ navigateWithParams({
search: search.value || undefined, search: search.value || undefined,
assigned_user_id: value !== "all" ? 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_contracts: props.unassignedContracts?.current_page,
page_assignments: 1, // Reset to first page on filter change page_assignments: 1, // Reset to first page on filter change
}); });
@ -248,7 +344,7 @@ const unassignedRows = computed(() =>
? null ? null
: Number(c.account.balance_amount), : Number(c.account.balance_amount),
case_person: c.client_case?.person?.full_name || null, 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, address: primaryCaseAddress(c) || null,
})) }))
); );
@ -261,7 +357,7 @@ const assignedRows = computed(() =>
? null ? null
: Number(c.account.balance_amount), : Number(c.account.balance_amount),
case_person: c.client_case?.person?.full_name || null, 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, address: primaryCaseAddress(c) || null,
assigned_to: c.last_field_jobs.assigned_user.name || null, assigned_to: c.last_field_jobs.assigned_user.name || null,
assigned_at_formatted: formatDate(c.last_field_jobs.assigned_at), assigned_at_formatted: formatDate(c.last_field_jobs.assigned_at),
@ -272,32 +368,42 @@ const assignedRows = computed(() =>
<template> <template>
<AppLayout title="Dodeljevanje terenskih opravil"> <AppLayout title="Dodeljevanje terenskih opravil">
<template #header></template> <template #header></template>
<div class="pt-12"> <div class="pt-6">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8"> <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=""
>
<template #header>
<div class="flex items-center gap-2">
<FilesIcon size="18" />
<CardTitle class="uppercase">Pogodbe (nedodeljene)</CardTitle>
</div>
</template>
<div <div
v-if="!setting" v-if="!setting"
class="mb-6 flex items-start gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4" class="mb-6 flex items-start gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4"
> >
<AlertCircle class="h-5 w-5 text-yellow-600 shrink-0 mt-0.5" /> <AlertCircle class="h-5 w-5 text-yellow-600 shrink-0 mt-0.5" />
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800">
Nastavitev za terenska opravila ni najdena. Najprej jo ustvarite v Nastavitve Nastavitev za terenska opravila ni najdena. Najprej jo ustvarite v
Nastavitve terenskih opravil. Nastavitve Nastavitve terenskih opravil.
</p> </p>
</div> </div>
<!-- Unassigned (Assignable) Contracts --> <!-- Unassigned (Assignable) Contracts -->
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg mb-8"> <div class="overflow-hidden mb-8 border-t">
<div class="p-4 border-b"> <div class="p-4 flex flex-row gap-2 items-end">
<h2 class="text-xl font-semibold">Pogodbe (nedodeljene)</h2>
</div>
<div class="p-4 border-b space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<Label for="assign-user">Dodeli uporabniku</Label> <Label for="assign-user">Dodeli uporabniku</Label>
<Select v-model="form.assigned_user_id"> <Select v-model="form.assigned_user_id">
<SelectTrigger id="assign-user" class="max-w-xs"> <SelectTrigger id="assign-user" class="max-w-xs min-w-60">
<SelectValue placeholder="Izberite uporabnika" /> <SelectValue placeholder="Izberite uporabnika" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent class="min-w-60">
<SelectItem v-for="u in users || []" :key="u.id" :value="u.id"> <SelectItem v-for="u in users || []" :key="u.id" :value="u.id">
{{ u.name }} {{ u.name }}
</SelectItem> </SelectItem>
@ -352,12 +458,44 @@ const assignedRows = computed(() =>
> >
<template #toolbar-filters> <template #toolbar-filters>
<div class="flex items-center gap-2 w-full"> <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 <Input
v-model="search" v-model="search"
placeholder="Išči po pogodbi, primeru, stranki, naslovu..." placeholder="Išči po pogodbi, primeru, stranki, naslovu..."
class="w-[320px]" class="w-full"
/> />
</div> </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>
<template #cell-_select="{ row }"> <template #cell-_select="{ row }">
@ -390,12 +528,21 @@ const assignedRows = computed(() =>
</template> </template>
</DataTable> </DataTable>
</div> </div>
</AppCard>
<!-- Assigned Contracts --> <!-- Assigned Contracts -->
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg"> <AppCard
<div class="p-4 border-b"> title=""
<h2 class="text-xl font-semibold">Dodeljene pogodbe</h2> 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> </div>
</template>
<DataTable <DataTable
:columns="assignedColumns" :columns="assignedColumns"
:data="assignedRows" :data="assignedRows"
@ -418,17 +565,27 @@ const assignedRows = computed(() =>
> >
<template #toolbar-filters> <template #toolbar-filters>
<div class="flex items-center gap-2 w-full"> <div class="flex items-center gap-2 w-full">
<Input <AppPopover
v-model="search_contract" v-model:open="filterAssignedContracts"
placeholder="Išči po pogodbi, primeru, stranki..." align="start"
class="w-[320px]" content-class="w-[350px]"
/>
<div class="flex items-center gap-2 ml-4">
<Label for="filter-user" class="text-sm whitespace-nowrap"
>Filter po uporabniku</Label
> >
<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"> <Select v-model="assignedFilterUserId">
<SelectTrigger id="filter-user" class="w-48"> <SelectTrigger id="filter-user">
<SelectValue placeholder="Vsi" /> <SelectValue placeholder="Vsi" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -443,6 +600,24 @@ const assignedRows = computed(() =>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </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> </div>
</template> </template>
<template #cell-case_person="{ row }"> <template #cell-case_person="{ row }">
@ -469,7 +644,7 @@ const assignedRows = computed(() =>
</Button> </Button>
</template> </template>
</DataTable> </DataTable>
</div> </AppCard>
</div> </div>
</div> </div>
</AppLayout> </AppLayout>

View File

@ -32,7 +32,9 @@ const filteredTemplates = computed(() => {
if (!form.client_uuid) { if (!form.client_uuid) {
return props.templates.filter((t) => !t.client_id); return props.templates.filter((t) => !t.client_id);
} }
return props.templates.filter((t) => t.client_uuid === form.client_uuid || !t.client_id); return props.templates.filter(
(t) => t.client_uuid === form.client_uuid || !t.client_id
);
}); });
const uploading = ref(false); const uploading = ref(false);
@ -153,7 +155,30 @@ async function startImport() {
<Label>Predloga</Label> <Label>Predloga</Label>
<Select v-model="form.import_template_id"> <Select v-model="form.import_template_id">
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Izberite predlogo..." /> <SelectValue placeholder="Izberite predlogo...">
<template v-if="form.import_template_id">
<div class="flex w-full items-center justify-between">
<span class="truncate">
{{
filteredTemplates.find(
(t) => t.id === form.import_template_id
)?.name
}}
</span>
<span
class="ml-2 rounded bg-gray-100 px-1.5 py-0.5 text-[10px] text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{{
filteredTemplates.find(
(t) => t.id === form.import_template_id
)?.client_id
? "Client"
: "Global"
}}
</span>
</div>
</template>
</SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
@ -209,7 +234,9 @@ async function startImport() {
<span class="text-xs text-gray-500"> <span class="text-xs text-gray-500">
{{ (form.file.size / 1024).toFixed(1) }} kB {{ (form.file.size / 1024).toFixed(1) }} kB
</span> </span>
<span class="inline-block rounded bg-gray-100 px-1.5 py-0.5 text-[10px]"> <span
class="inline-block rounded bg-gray-100 px-1.5 py-0.5 text-[10px]"
>
Zamenjaj Zamenjaj
</span> </span>
</div> </div>