production #1
|
|
@ -184,7 +184,7 @@ public function store(Request $request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kick off processing of an import - simple synchronous step for now
|
// Kick off processing of an import - simple synchronous step for now
|
||||||
public function process(Import $import, Request $request, ImportServiceV2 $processor)
|
public function process(Import $import, Request $request, ImportProcessor $processor)
|
||||||
{
|
{
|
||||||
$import->update(['status' => 'validating', 'started_at' => now()]);
|
$import->update(['status' => 'validating', 'started_at' => now()]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ public function process(Import $import, array $mapped, array $raw, array $contex
|
||||||
$payload = $this->buildPayloadForAddress($address);
|
$payload = $this->buildPayloadForAddress($address);
|
||||||
$payload['person_id'] = $personId;
|
$payload['person_id'] = $personId;
|
||||||
|
|
||||||
$addressEntity = new \App\Models\Person\PersonAddress;
|
$addressEntity = new PersonAddress;
|
||||||
$addressEntity->fill($payload);
|
$addressEntity->fill($payload);
|
||||||
$addressEntity->save();
|
$addressEntity->save();
|
||||||
|
|
||||||
|
|
@ -129,7 +129,7 @@ public function process(Import $import, array $mapped, array $raw, array $contex
|
||||||
|
|
||||||
protected function resolveAddress(string $address, int $personId): mixed
|
protected function resolveAddress(string $address, int $personId): mixed
|
||||||
{
|
{
|
||||||
return \App\Models\Person\PersonAddress::where('person_id', $personId)
|
return PersonAddress::where('person_id', $personId)
|
||||||
->where('address', $address)
|
->where('address', $address)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,7 @@ const switchToTab = (tab) => {
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="person" class="py-2">
|
<TabsContent value="person" class="py-2">
|
||||||
<PersonInfoPersonTab
|
<PersonInfoPersonTab
|
||||||
|
:is-client-case="clientCaseUuid ? true : false"
|
||||||
:person="person"
|
:person="person"
|
||||||
:edit="edit"
|
:edit="edit"
|
||||||
:person-edit="personEdit"
|
:person-edit="personEdit"
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { UserEditIcon } from "@/Utilities/Icons";
|
import { UserEditIcon } from "@/Utilities/Icons";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { fmtDateDMY } from "@/Utilities/functions";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
person: Object,
|
person: Object,
|
||||||
|
isClientCase: { type: Boolean, default: false },
|
||||||
edit: { type: Boolean, default: true },
|
edit: { type: Boolean, default: true },
|
||||||
personEdit: { type: Boolean, default: true },
|
personEdit: { type: Boolean, default: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['edit']);
|
const emit = defineEmits(["edit"]);
|
||||||
|
|
||||||
const getMainAddress = (adresses) => {
|
const getMainAddress = (adresses) => {
|
||||||
const addr = adresses.filter((a) => a.type.id === 1)[0] ?? "";
|
const addr = adresses.filter((a) => a.type.id === 1)[0] ?? "";
|
||||||
|
|
@ -30,7 +32,7 @@ const getMainPhone = (phones) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
emit('edit');
|
emit("edit");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -44,51 +46,126 @@ const handleEdit = () => {
|
||||||
>
|
>
|
||||||
<UserEditIcon size="md" />
|
<UserEditIcon size="md" />
|
||||||
<span>Uredi</span>
|
<span>Uredi</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Nu.</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Primer ref.
|
||||||
|
</p>
|
||||||
<p class="text-sm font-semibold text-gray-900">{{ person.nu }}</p>
|
<p class="text-sm font-semibold text-gray-900">{{ person.nu }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Name.</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Naziv</p>
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
<p class="text-sm font-semibold text-gray-900">
|
||||||
{{ person.full_name }}
|
{{ person.full_name }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Tax NU.</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Davčna
|
||||||
|
</p>
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
<p class="text-sm font-semibold text-gray-900">
|
||||||
{{ person.tax_number }}
|
{{ person.tax_number }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Social security NU.</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Emšo</p>
|
||||||
<p class="text-sm font-semibold text-gray-900">
|
<p class="text-sm font-semibold text-gray-900">
|
||||||
{{ person.social_security_number }}
|
{{ person.social_security_number }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3">
|
<div
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
v-if="isClientCase"
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Address</p>
|
class="grid grid-rows-* grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mt-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="md:col-span-full lg:col-span-1 rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Naslov
|
||||||
|
</p>
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
{{ getMainAddress(person.addresses) }}
|
{{ getMainAddress(person.addresses) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Phone</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Telefon
|
||||||
|
</p>
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
{{ getMainPhone(person.phones) }}
|
{{ getMainPhone(person.phones) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="md:col-span-full lg:col-span-1 rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
<div
|
||||||
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Description</p>
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Dat. rojstva
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
{{ fmtDateDMY(person.birthday) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||||
|
<div
|
||||||
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Naslov
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
{{ getMainAddress(person.addresses) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Telefon
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
{{ getMainPhone(person.phones) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-rows-* grid-cols-1 md:grid-cols-2 gap-3 mt-3"
|
||||||
|
:class="[isClientCase ? 'md:grid-cols-2' : '']"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isClientCase"
|
||||||
|
class="md:col-span-full lg:col-span-1 rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">
|
||||||
|
Delodajalec
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
{{ person.employer }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="md:col-span-full rounded-lg p-3 bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
:class="[isClientCase ? 'lg:col-span-1' : '']"
|
||||||
|
>
|
||||||
|
<p class="text-xs font-medium uppercase tracking-wider text-gray-500 mb-1">Opis</p>
|
||||||
<p class="text-sm font-medium text-gray-900">
|
<p class="text-sm font-medium text-gray-900">
|
||||||
{{ person.description }}
|
{{ person.description }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,182 +1,205 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import UpdateDialog from '@/Components/Dialogs/UpdateDialog.vue';
|
import UpdateDialog from "@/Components/Dialogs/UpdateDialog.vue";
|
||||||
import SectionTitle from '@/Components/SectionTitle.vue';
|
import SectionTitle from "@/Components/SectionTitle.vue";
|
||||||
import { useForm, Field as FormField } from "vee-validate";
|
import { useForm, Field as FormField } from "vee-validate";
|
||||||
import { toTypedSchema } from "@vee-validate/zod";
|
import { toTypedSchema } from "@vee-validate/zod";
|
||||||
import * as z from "zod";
|
import * as z from "zod";
|
||||||
import { router } from '@inertiajs/vue3';
|
import { router } from "@inertiajs/vue3";
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import {
|
import { FormControl, FormItem, FormLabel, FormMessage } from "@/Components/ui/form";
|
||||||
FormControl,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/Components/ui/form";
|
|
||||||
import { Input } from "@/Components/ui/input";
|
import { Input } from "@/Components/ui/input";
|
||||||
import { Textarea } from "@/Components/ui/textarea";
|
import { Textarea } from "@/Components/ui/textarea";
|
||||||
|
import DatePicker from "../DatePicker.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false,
|
||||||
},
|
},
|
||||||
person: Object
|
person: Object,
|
||||||
});
|
});
|
||||||
|
|
||||||
const processingUpdate = ref(false);
|
const processingUpdate = ref(false);
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(["close"]);
|
||||||
|
|
||||||
const formSchema = toTypedSchema(
|
const formSchema = toTypedSchema(
|
||||||
z.object({
|
z.object({
|
||||||
full_name: z.string().min(1, "Naziv je obvezen."),
|
full_name: z.string().min(1, "Naziv je obvezen."),
|
||||||
tax_number: z.string().optional(),
|
tax_number: z.string().optional(),
|
||||||
social_security_number: z.string().optional(),
|
social_security_number: z.string().optional(),
|
||||||
|
birthday: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
employer: z.string().optional(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
validationSchema: formSchema,
|
validationSchema: formSchema,
|
||||||
initialValues: {
|
initialValues: {
|
||||||
full_name: props.person?.full_name || '',
|
full_name: props.person?.full_name || "",
|
||||||
tax_number: props.person?.tax_number || '',
|
tax_number: props.person?.tax_number || "",
|
||||||
social_security_number: props.person?.social_security_number || '',
|
social_security_number: props.person?.social_security_number || "",
|
||||||
description: props.person?.description || ''
|
birthday: props.person?.birthday || "",
|
||||||
|
description: props.person?.description || "",
|
||||||
|
employer: props.person?.employer || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('close');
|
emit("close");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
form.resetForm({
|
form.resetForm({
|
||||||
values: {
|
values: {
|
||||||
full_name: props.person?.full_name || '',
|
full_name: props.person?.full_name || "",
|
||||||
tax_number: props.person?.tax_number || '',
|
tax_number: props.person?.tax_number || "",
|
||||||
social_security_number: props.person?.social_security_number || '',
|
social_security_number: props.person?.social_security_number || "",
|
||||||
description: props.person?.description || ''
|
birthday: props.person?.birthday || "",
|
||||||
}
|
description: props.person?.description || "",
|
||||||
});
|
employer: props.person?.employer || "",
|
||||||
}, 500);
|
},
|
||||||
}
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const updatePerson = async () => {
|
const updatePerson = async () => {
|
||||||
processingUpdate.value = true;
|
processingUpdate.value = true;
|
||||||
const { values } = form;
|
const { values } = form;
|
||||||
|
|
||||||
router.put(
|
router.put(route("person.update", props.person), values, {
|
||||||
route('person.update', props.person),
|
preserveScroll: true,
|
||||||
values,
|
onSuccess: () => {
|
||||||
{
|
processingUpdate.value = false;
|
||||||
preserveScroll: true,
|
close();
|
||||||
onSuccess: () => {
|
},
|
||||||
processingUpdate.value = false;
|
onError: (errors) => {
|
||||||
close();
|
// Map Inertia errors to VeeValidate field errors
|
||||||
},
|
Object.keys(errors).forEach((field) => {
|
||||||
onError: (errors) => {
|
const errorMessages = Array.isArray(errors[field])
|
||||||
// Map Inertia errors to VeeValidate field errors
|
? errors[field]
|
||||||
Object.keys(errors).forEach((field) => {
|
: [errors[field]];
|
||||||
const errorMessages = Array.isArray(errors[field])
|
form.setFieldError(field, errorMessages[0]);
|
||||||
? errors[field]
|
});
|
||||||
: [errors[field]];
|
processingUpdate.value = false;
|
||||||
form.setFieldError(field, errorMessages[0]);
|
},
|
||||||
});
|
onFinish: () => {
|
||||||
processingUpdate.value = false;
|
processingUpdate.value = false;
|
||||||
},
|
},
|
||||||
onFinish: () => {
|
});
|
||||||
processingUpdate.value = false;
|
};
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(() => {
|
const onSubmit = form.handleSubmit(() => {
|
||||||
updatePerson();
|
updatePerson();
|
||||||
});
|
});
|
||||||
|
|
||||||
const onConfirm = () => {
|
const onConfirm = () => {
|
||||||
onSubmit();
|
onSubmit();
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<UpdateDialog
|
<UpdateDialog
|
||||||
:show="show"
|
:show="show"
|
||||||
:title="`Posodobi ${person.full_name}`"
|
:title="`Posodobi ${person.full_name}`"
|
||||||
confirm-text="Shrani"
|
confirm-text="Shrani"
|
||||||
:processing="processingUpdate"
|
:processing="processingUpdate"
|
||||||
@close="close"
|
@close="close"
|
||||||
@confirm="onConfirm"
|
@confirm="onConfirm"
|
||||||
>
|
>
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="onSubmit">
|
||||||
<SectionTitle class="border-b mb-4">
|
<SectionTitle class="border-b mb-4">
|
||||||
<template #title>
|
<template #title> Oseba </template>
|
||||||
Oseba
|
</SectionTitle>
|
||||||
</template>
|
|
||||||
</SectionTitle>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<FormField v-slot="{ componentField }" name="full_name">
|
<FormField v-slot="{ componentField }" name="full_name">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Naziv</FormLabel>
|
<FormLabel>Naziv</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
id="cfullname"
|
id="cfullname"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Naziv"
|
placeholder="Naziv"
|
||||||
autocomplete="full-name"
|
autocomplete="full-name"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="tax_number">
|
<FormField v-slot="{ componentField }" name="tax_number">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Davčna</FormLabel>
|
<FormLabel>Davčna</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
id="ctaxnumber"
|
id="ctaxnumber"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Davčna številka"
|
placeholder="Davčna številka"
|
||||||
autocomplete="tax-number"
|
autocomplete="tax-number"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="social_security_number">
|
<FormField v-slot="{ componentField }" name="social_security_number">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Matična / Emšo</FormLabel>
|
<FormLabel>Matična / Emšo</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
id="csocialSecurityNumber"
|
id="csocialSecurityNumber"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Matična / Emšo"
|
placeholder="Matična / Emšo"
|
||||||
autocomplete="social-security-number"
|
autocomplete="social-security-number"
|
||||||
v-bind="componentField"
|
v-bind="componentField"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField v-slot="{ componentField }" name="description">
|
<FormField v-slot="{ componentField }" name="employer">
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Opis</FormLabel>
|
<FormLabel>Delodajalec</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Input
|
||||||
id="cdescription"
|
id="cemployer"
|
||||||
placeholder="Opis"
|
type="text"
|
||||||
v-bind="componentField"
|
placeholder="Delodajalec"
|
||||||
/>
|
autocomplete="employer"
|
||||||
</FormControl>
|
v-bind="componentField"
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
</FormField>
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</form>
|
</FormField>
|
||||||
</UpdateDialog>
|
|
||||||
|
<FormField v-slot="{ value, handleChange }" name="birthday">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Datum rojstva</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<DatePicker
|
||||||
|
id="cbirthday"
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="handleChange"
|
||||||
|
format="dd.MM.yyyy"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-slot="{ componentField }" name="description">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Opis</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea id="cdescription" placeholder="Opis" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UpdateDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/Components/ui/dialog";
|
||||||
import { Button } from "@/Components/ui/button";
|
import { Button } from "@/Components/ui/button";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/Components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/Components/ui/table";
|
|
||||||
import { Badge } from "@/Components/ui/badge";
|
import { Badge } from "@/Components/ui/badge";
|
||||||
import { Label } from "@/Components/ui/label";
|
import { Label } from "@/Components/ui/label";
|
||||||
|
import { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import { ChevronRightIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
limit: Number,
|
limit: Number,
|
||||||
|
|
@ -14,72 +17,187 @@ const props = defineProps({
|
||||||
truncated: Boolean,
|
truncated: Boolean,
|
||||||
hasHeader: Boolean,
|
hasHeader: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emits = defineEmits(['close','change-limit','refresh'])
|
const emits = defineEmits(['close','change-limit','refresh'])
|
||||||
function onLimit(e){ emits('change-limit', Number(e.target.value)); emits('refresh') }
|
|
||||||
|
// State
|
||||||
|
const selectedRow = ref(null);
|
||||||
|
const hideEmptyRows = ref(true);
|
||||||
|
|
||||||
|
// Filter out columns with empty headers
|
||||||
|
const visibleColumns = computed(() => {
|
||||||
|
if (!props.columns) return [];
|
||||||
|
return props.columns.filter(col => col && String(col).trim() !== '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if row is empty (first 2 columns are empty)
|
||||||
|
function isRowEmpty(row) {
|
||||||
|
if (!visibleColumns.value || visibleColumns.value.length === 0) return false;
|
||||||
|
const firstCols = visibleColumns.value.slice(0, 2);
|
||||||
|
return firstCols.every(col => !row[col] || String(row[col]).trim() === '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtered rows
|
||||||
|
const visibleRows = computed(() => {
|
||||||
|
if (!props.rows) return [];
|
||||||
|
let filtered = props.rows;
|
||||||
|
if (hideEmptyRows.value) {
|
||||||
|
filtered = filtered.filter(r => !isRowEmpty(r));
|
||||||
|
}
|
||||||
|
return filtered.map((r, idx) => ({ ...r, index: idx + 1 }));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select row
|
||||||
|
function selectRow(row) {
|
||||||
|
selectedRow.value = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLimit(val) {
|
||||||
|
emits('change-limit', Number(val));
|
||||||
|
emits('refresh');
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="show" @update:open="(val) => !val && $emit('close')">
|
<Dialog :open="show" @update:open="(val) => !val && $emit('close')">
|
||||||
<DialogContent class="max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
|
<DialogContent class="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col p-0">
|
||||||
<DialogHeader>
|
<!-- Header -->
|
||||||
<DialogTitle>CSV Preview ({{ rows.length }} / {{ limit }})</DialogTitle>
|
<div class="px-6 py-4 border-b bg-linear-to-r from-gray-50 to-white">
|
||||||
</DialogHeader>
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
<div class="flex items-center gap-3 pb-3 border-b">
|
<h2 class="text-xl font-semibold text-gray-900">CSV Preview</h2>
|
||||||
<div class="flex items-center gap-2">
|
<p class="text-sm text-gray-500 mt-1">
|
||||||
<Label for="limit-select" class="text-sm text-gray-600">Limit:</Label>
|
Showing {{ visibleRows.length }} of {{ rows.length }} rows
|
||||||
<Select :model-value="String(limit)" @update:model-value="(val) => { emits('change-limit', Number(val)); emits('refresh'); }">
|
</p>
|
||||||
<SelectTrigger id="limit-select" class="w-24 h-8">
|
</div>
|
||||||
<SelectValue />
|
<div class="flex items-center gap-3">
|
||||||
</SelectTrigger>
|
<div class="flex items-center gap-2">
|
||||||
<SelectContent>
|
<Label for="limit-select" class="text-sm text-gray-600">Limit:</Label>
|
||||||
<SelectItem value="50">50</SelectItem>
|
<Select :model-value="String(limit)" @update:model-value="onLimit">
|
||||||
<SelectItem value="100">100</SelectItem>
|
<SelectTrigger id="limit-select" class="w-24 h-8">
|
||||||
<SelectItem value="200">200</SelectItem>
|
<SelectValue />
|
||||||
<SelectItem value="300">300</SelectItem>
|
</SelectTrigger>
|
||||||
<SelectItem value="500">500</SelectItem>
|
<SelectContent>
|
||||||
</SelectContent>
|
<SelectItem value="50">50</SelectItem>
|
||||||
</Select>
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
<SelectItem value="200">200</SelectItem>
|
||||||
|
<SelectItem value="300">300</SelectItem>
|
||||||
|
<SelectItem value="500">500</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button @click="$emit('refresh')" variant="outline" size="sm" :disabled="loading">
|
||||||
|
{{ loading ? 'Loading…' : 'Refresh' }}
|
||||||
|
</Button>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="hide-empty-rows"
|
||||||
|
:checked="hideEmptyRows"
|
||||||
|
@update:checked="(val) => hideEmptyRows = val"
|
||||||
|
/>
|
||||||
|
<Label for="hide-empty-rows" class="text-xs cursor-pointer">
|
||||||
|
Hide empty rows
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Badge v-if="truncated" variant="outline" class="bg-amber-50 text-amber-700 border-amber-200">
|
||||||
|
Truncated at limit
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button @click="$emit('refresh')" variant="outline" size="sm" :disabled="loading">
|
|
||||||
{{ loading ? 'Loading…' : 'Refresh' }}
|
|
||||||
</Button>
|
|
||||||
<Badge v-if="truncated" variant="outline" class="bg-amber-50 text-amber-700 border-amber-200">
|
|
||||||
Truncated at limit
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-auto border rounded-lg">
|
<!-- Split View -->
|
||||||
<Table>
|
<div class="flex-1 flex overflow-hidden">
|
||||||
<TableHeader class="sticky top-0 bg-white z-10">
|
<!-- Left Panel - Row List -->
|
||||||
<TableRow>
|
<div class="w-96 border-r bg-gray-50 overflow-y-auto">
|
||||||
<TableHead class="w-16">#</TableHead>
|
<div v-if="loading" class="p-8 text-center text-gray-500">
|
||||||
<TableHead v-for="col in columns" :key="col">{{ col }}</TableHead>
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||||
</TableRow>
|
Loading...
|
||||||
</TableHeader>
|
</div>
|
||||||
<TableBody>
|
<div v-else-if="!visibleRows.length" class="p-8 text-center text-gray-500">
|
||||||
<TableRow v-if="loading">
|
No rows to display
|
||||||
<TableCell :colspan="columns.length + 1" class="text-center text-gray-500">
|
</div>
|
||||||
Loading…
|
<div v-else class="divide-y">
|
||||||
</TableCell>
|
<button
|
||||||
</TableRow>
|
v-for="row in visibleRows"
|
||||||
<TableRow v-for="(r, idx) in rows" :key="idx">
|
:key="row.index"
|
||||||
<TableCell class="text-gray-500 font-medium">{{ idx + 1 }}</TableCell>
|
@click="selectRow(row)"
|
||||||
<TableCell v-for="col in columns" :key="col" class="whitespace-pre-wrap">
|
class="w-full px-4 py-3 text-left hover:bg-white transition-colors"
|
||||||
{{ r[col] }}
|
:class="{
|
||||||
</TableCell>
|
'bg-white shadow-sm': selectedRow?.index === row.index,
|
||||||
</TableRow>
|
}"
|
||||||
<TableRow v-if="!loading && !rows.length">
|
>
|
||||||
<TableCell :colspan="columns.length + 1" class="text-center text-gray-500">
|
<div class="flex items-center justify-between gap-3">
|
||||||
No rows
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||||
</TableCell>
|
<!-- Row Number -->
|
||||||
</TableRow>
|
<div class="flex-shrink-0">
|
||||||
</TableBody>
|
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-semibold">
|
||||||
</Table>
|
{{ row.index }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row Preview -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-xs font-semibold text-gray-900 mb-1">
|
||||||
|
Row #{{ row.index }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 truncate">
|
||||||
|
{{
|
||||||
|
visibleColumns.slice(0, 2).map(col => row[col]).filter(Boolean).join(' • ') || 'Empty row'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Arrow -->
|
||||||
|
<ChevronRightIcon class="h-4 w-4 text-gray-400 flex-shrink-0" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Panel - Row Details -->
|
||||||
|
<div v-if="selectedRow" class="flex-1 overflow-y-auto p-6">
|
||||||
|
<!-- Row Header -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
|
Row #{{ selectedRow.index }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500">Full row details</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row Data -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4">
|
||||||
|
<dl class="grid grid-cols-1 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="col in visibleColumns"
|
||||||
|
:key="col"
|
||||||
|
class="flex items-start gap-3 py-2 border-b border-gray-200 last:border-0"
|
||||||
|
>
|
||||||
|
<dt class="text-sm font-medium text-gray-600 w-48 flex-shrink-0">
|
||||||
|
{{ col }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm text-gray-900 flex-1 font-medium whitespace-pre-wrap break-words">
|
||||||
|
{{ selectedRow[col] || '—' }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State for Right Panel -->
|
||||||
|
<div v-else class="flex-1 flex items-center justify-center text-gray-400">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-5xl mb-3">📄</div>
|
||||||
|
<p class="text-sm">Select a row to view details</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-gray-500 pt-3 border-t">
|
<!-- Footer -->
|
||||||
Showing up to {{ limit }} rows from source file.
|
<div class="px-6 py-3 border-t bg-gray-50 text-xs text-gray-500">
|
||||||
Header detection: <span class="font-medium">{{ hasHeader ? 'header present' : 'no header' }}</span>
|
Header detection: <span class="font-medium">{{ hasHeader ? 'header present' : 'no header' }}</span>
|
||||||
|
• Click a row to view full details
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user