419 lines
13 KiB
Vue
419 lines
13 KiB
Vue
<script setup>
|
|
import { computed, ref } from "vue";
|
|
import AppLayout from "@/Layouts/AppLayout.vue";
|
|
import { Link, router, usePage } from "@inertiajs/vue3";
|
|
import CreateDialog from "@/Components/Dialogs/CreateDialog.vue";
|
|
import DataTable from "@/Components/DataTable/DataTableNew2.vue";
|
|
import { hasPermission } from "@/Services/permissions";
|
|
import { Button } from "@/Components/ui/button";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "@/Components/ui/card";
|
|
import { Input } from "@/Components/ui/input";
|
|
import ActionMenuItem from "@/Components/DataTable/ActionMenuItem.vue";
|
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/Components/ui/select";
|
|
import {
|
|
FormControl,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/Components/ui/form";
|
|
import { useForm } from "vee-validate";
|
|
import { toTypedSchema } from "@vee-validate/zod";
|
|
import * as z from "zod";
|
|
import ActionMessage from "@/Components/ActionMessage.vue";
|
|
import { Mail, Plug2Icon, Plus } from "lucide-vue-next";
|
|
import { Separator } from "@/Components/ui/separator";
|
|
|
|
const props = defineProps({
|
|
clients: Object,
|
|
filters: Object,
|
|
});
|
|
|
|
const page = usePage();
|
|
// Expose as a callable computed: use in templates as hasPerm('permission-slug')
|
|
const hasPerm = computed(() => (permission) =>
|
|
hasPermission(page.props.auth?.user, permission)
|
|
);
|
|
|
|
const formSchema = toTypedSchema(
|
|
z.object({
|
|
first_name: z.string().optional(),
|
|
last_name: z.string().optional(),
|
|
full_name: z.string().min(1, "Naziv je obvezen."),
|
|
tax_number: z.string().optional(),
|
|
social_security_number: z.string().optional(),
|
|
description: z.string().optional(),
|
|
address: z.object({
|
|
address: z.string().optional(),
|
|
country: z.string().optional(),
|
|
type_id: z.number().default(1),
|
|
}),
|
|
phone: z.object({
|
|
nu: z.string().optional(),
|
|
country_code: z.string().default("00386"),
|
|
type_id: z.number().default(1),
|
|
}),
|
|
})
|
|
);
|
|
|
|
const formClient = useForm({
|
|
validationSchema: formSchema,
|
|
initialValues: {
|
|
first_name: "",
|
|
last_name: "",
|
|
full_name: "",
|
|
tax_number: "",
|
|
social_security_number: "",
|
|
description: "",
|
|
address: {
|
|
address: "",
|
|
country: "",
|
|
type_id: 1,
|
|
},
|
|
phone: {
|
|
nu: "",
|
|
country_code: "00386",
|
|
type_id: 1,
|
|
},
|
|
},
|
|
});
|
|
|
|
//Create client drawer
|
|
const drawerCreateClient = ref(false);
|
|
|
|
// Initial search (passed to DataTable toolbar)
|
|
const initialSearch = ref(props.filters?.search || "");
|
|
|
|
//Open drawer create client
|
|
const openDrawerCreateClient = () => {
|
|
drawerCreateClient.value = true;
|
|
};
|
|
|
|
//Close any drawer on page
|
|
const closeDrawer = () => {
|
|
drawerCreateClient.value = false;
|
|
formClient.resetForm();
|
|
};
|
|
|
|
const processing = ref(false);
|
|
|
|
//Ajax call post to store new client
|
|
const storeClient = async () => {
|
|
processing.value = true;
|
|
const { values } = formClient;
|
|
|
|
// Ensure type_id is a number
|
|
const payload = {
|
|
...values,
|
|
address: {
|
|
...values.address,
|
|
type_id: Number(values.address.type_id),
|
|
},
|
|
phone: {
|
|
...values.phone,
|
|
type_id: Number(values.phone.type_id),
|
|
},
|
|
};
|
|
|
|
router.post(route("client.store"), payload, {
|
|
onSuccess: () => {
|
|
closeDrawer();
|
|
formClient.resetForm();
|
|
processing.value = false;
|
|
},
|
|
onError: (errors) => {
|
|
Object.keys(errors).forEach((field) => {
|
|
const errorMessages = Array.isArray(errors[field])
|
|
? errors[field]
|
|
: [errors[field]];
|
|
formClient.setFieldError(field, errorMessages[0]);
|
|
});
|
|
processing.value = false;
|
|
},
|
|
onFinish: () => {
|
|
processing.value = false;
|
|
},
|
|
});
|
|
};
|
|
|
|
const onConfirmCreate = formClient.handleSubmit(() => {
|
|
storeClient();
|
|
});
|
|
|
|
// Formatting helpers
|
|
const fmtCurrency = (v) => {
|
|
const n = Number(v ?? 0);
|
|
try {
|
|
return new Intl.NumberFormat("sl-SI", { style: "currency", currency: "EUR" }).format(
|
|
n
|
|
);
|
|
} catch (e) {
|
|
return `${n.toFixed(2)} €`;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout title="Client">
|
|
<template #header> </template>
|
|
<div class="py-6">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
<Card class="p-0">
|
|
<CardHeader>
|
|
<CardTitle>Naročniki</CardTitle>
|
|
</CardHeader>
|
|
<CardContent class="p-0">
|
|
<DataTable
|
|
:columns="[
|
|
{ key: 'nu', label: 'Št.', sortable: false },
|
|
{ key: 'name', label: 'Naročnik', sortable: false },
|
|
{
|
|
key: 'cases',
|
|
label: 'Primeri z aktivnimi pogodbami',
|
|
sortable: false,
|
|
align: 'right',
|
|
},
|
|
{
|
|
key: 'balance',
|
|
label: 'Skupaj stanje',
|
|
sortable: false,
|
|
align: 'right',
|
|
},
|
|
]"
|
|
:data="clients.data || []"
|
|
:show-pagination="false"
|
|
:show-toolbar="true"
|
|
:hoverable="true"
|
|
row-key="uuid"
|
|
:striped="true"
|
|
empty-text="Ni najdenih naročnikov."
|
|
>
|
|
<template #toolbar-actions>
|
|
<Button
|
|
variant="outline"
|
|
v-if="hasPerm('client-edit')"
|
|
@click="openDrawerCreateClient"
|
|
>
|
|
<Plus class="w-4 h-4" /> Novi naročnik
|
|
</Button>
|
|
</template>
|
|
<template #cell-nu="{ row }">
|
|
{{ row.person?.nu || "-" }}
|
|
</template>
|
|
<template #cell-name="{ row }">
|
|
<Link
|
|
:href="route('client.show', { uuid: row.uuid })"
|
|
class="font-semibold hover:underline text-primary-700"
|
|
>
|
|
{{ row.person?.full_name || "-" }}
|
|
</Link>
|
|
<div v-if="!row.person" class="mt-1">
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
@click.prevent="
|
|
router.post(route('client.emergencyPerson', { uuid: row.uuid }))
|
|
"
|
|
>Add Person</Button
|
|
>
|
|
</div>
|
|
</template>
|
|
<template #cell-cases="{ row }">
|
|
<div class="text-right">
|
|
{{ row.cases_with_active_contracts_count ?? 0 }}
|
|
</div>
|
|
</template>
|
|
<template #cell-balance="{ row }">
|
|
<div class="text-right">
|
|
{{ fmtCurrency(row.active_contracts_balance_sum) }}
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
<CreateDialog
|
|
:show="drawerCreateClient"
|
|
title="Novi naročnik"
|
|
confirm-text="Shrani"
|
|
:processing="processing"
|
|
@close="drawerCreateClient = false"
|
|
@confirm="onConfirmCreate"
|
|
>
|
|
<form @submit.prevent="onConfirmCreate">
|
|
<div class="space-y-4">
|
|
<FormField v-slot="{ componentField }" name="full_name">
|
|
<FormItem>
|
|
<FormLabel>Naziv *</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="fullname"
|
|
type="text"
|
|
autocomplete="full-name"
|
|
placeholder="Naziv"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
<CardTitle class="full">Osebni podatki</CardTitle>
|
|
<Separator />
|
|
<div class="flex flex-row gap-2">
|
|
<FormField v-slot="{ componentField }" name="tax_number">
|
|
<FormItem class="flex-1">
|
|
<FormLabel>Davčna</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="taxnumber"
|
|
type="text"
|
|
autocomplete="tax-number"
|
|
placeholder="Davčna številka"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ componentField }" name="social_security_number">
|
|
<FormItem class="flex-1">
|
|
<FormLabel>Matična / Emšo</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="socialSecurityNumber"
|
|
type="text"
|
|
autocomplete="social-security-number"
|
|
placeholder="Matična / Emšo"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
<CardTitle class="full">Naslov</CardTitle>
|
|
<Separator />
|
|
<div class="grid sm:grid-cols-2 gap-2">
|
|
<FormField v-slot="{ componentField }" name="address.address">
|
|
<FormItem>
|
|
<FormLabel>Naslov</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="address"
|
|
type="text"
|
|
autocomplete="address"
|
|
placeholder="Naslov"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ componentField }" name="address.country">
|
|
<FormItem>
|
|
<FormLabel>Država</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="addressCountry"
|
|
type="text"
|
|
autocomplete="address-country"
|
|
placeholder="Država"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ value, handleChange }" name="address.type_id">
|
|
<FormItem>
|
|
<FormLabel>Vrsta naslova</FormLabel>
|
|
<Select :model-value="value" @update:model-value="handleChange">
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Izberi vrsto naslova" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent align="end" position="popper">
|
|
<SelectItem :value="1">Stalni</SelectItem>
|
|
<SelectItem :value="2">Začasni</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
<CardTitle class="full">Naslov</CardTitle>
|
|
<Separator />
|
|
<div class="flex flex-row gap-2">
|
|
<FormField v-slot="{ value, handleChange }" name="phone.country_code">
|
|
<FormItem class="flex-1/3">
|
|
<FormLabel>Koda države tel.</FormLabel>
|
|
<Select :model-value="value" @update:model-value="handleChange">
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Izberi kodo države" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="00386">+386 (Slovenija)</SelectItem>
|
|
<SelectItem value="00385">+385 (Hrvaška)</SelectItem>
|
|
<SelectItem value="0039">+39 (Italija)</SelectItem>
|
|
<SelectItem value="0036">+36 (Madžarska)</SelectItem>
|
|
<SelectItem value="0043">+43 (Avstrija)</SelectItem>
|
|
<SelectItem value="00381">+381 (Srbija)</SelectItem>
|
|
<SelectItem value="00387">+387 (Bosna in Hercegovina)</SelectItem>
|
|
<SelectItem value="00382">+382 (Črna gora)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
|
|
<FormField v-slot="{ componentField }" name="phone.nu">
|
|
<FormItem class="flex-2/3">
|
|
<FormLabel>Telefonska št.</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="phoneNu"
|
|
type="text"
|
|
autocomplete="phone-nu"
|
|
placeholder="Telefonska številka"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
<FormField v-slot="{ componentField }" name="description">
|
|
<FormItem>
|
|
<FormLabel>Opis</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
id="description"
|
|
type="text"
|
|
autocomplete="description"
|
|
placeholder="Opis"
|
|
v-bind="componentField"
|
|
/>
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
</FormField>
|
|
</div>
|
|
</form>
|
|
</CreateDialog>
|
|
</template>
|