273 lines
9.3 KiB
Vue
273 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue';
|
|
import AppLayout from '@/layouts/AppLayout.vue';
|
|
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
|
|
import type { BreadcrumbItem } from '@/types';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import {
|
|
Combobox,
|
|
ComboboxAnchor,
|
|
ComboboxEmpty,
|
|
ComboboxGroup,
|
|
ComboboxInput,
|
|
ComboboxItem,
|
|
ComboboxItemIndicator,
|
|
ComboboxList,
|
|
} from '@/components/ui/combobox';
|
|
import { Check, Search, CalendarIcon } from 'lucide-vue-next';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
NumberField,
|
|
NumberFieldContent,
|
|
NumberFieldDecrement,
|
|
NumberFieldIncrement,
|
|
NumberFieldInput,
|
|
} from '@/components/ui/number-field';
|
|
import { Calendar } from '@/components/ui/calendar';
|
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
import {
|
|
DateFormatter,
|
|
type DateValue,
|
|
getLocalTimeZone,
|
|
today,
|
|
} from '@internationalized/date';
|
|
|
|
interface Profile { id: string; name: string };
|
|
interface ProjectEntry { id: number; project_name: string };
|
|
|
|
const page = usePage<{
|
|
profiles: Profile[]
|
|
projects: ProjectEntry[]
|
|
}>();
|
|
|
|
const profiles = computed(() => page.props.profiles);
|
|
const projectsList = computed(() => page.props.projects);
|
|
|
|
const form = useForm({
|
|
user_id: '',
|
|
ordinary_hours: 0.5,
|
|
work_date: today(getLocalTimeZone()).toString(),
|
|
project_id: null as number | null,
|
|
comment: '',
|
|
});
|
|
|
|
// datepicker state
|
|
const dateValue = ref<DateValue>( today(getLocalTimeZone()) );
|
|
const df = new DateFormatter('nb-NO', { dateStyle: 'medium' });
|
|
|
|
// keep form.work_date in sync
|
|
watch(dateValue, (val) => {
|
|
if (val) {
|
|
const jsDate = val.toDate(getLocalTimeZone());
|
|
form.work_date = jsDate.toISOString().slice(0, 10); // "YYYY-MM-DD"
|
|
} else {
|
|
form.work_date = '';
|
|
}
|
|
});
|
|
|
|
// Combobox for user selection
|
|
const selectedProfile = ref<Profile | null>(
|
|
profiles.value.find(p => p.id === form.user_id) || null
|
|
);
|
|
|
|
watch(selectedProfile, val => form.user_id = val?.id ?? '');
|
|
|
|
// Combobox for project selection
|
|
const selectedProject = ref<ProjectEntry | null>(
|
|
projectsList.value.find(p => p.id === form.project_id) || null
|
|
);
|
|
watch(selectedProject, (val) => {
|
|
form.project_id = val?.id ?? null;
|
|
});
|
|
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
{ title: 'Dashbord', href: '/dashboard' },
|
|
{ title: 'Arbeidstimer', href: '/smartdok/work-hours' },
|
|
{ title: 'Opprett', href: '/smartdok/work-hours/create' },
|
|
];
|
|
|
|
function submit() {
|
|
form.post(route('smartdok.work-hours.store'));
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Opprett arbeidstime" />
|
|
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<!-- Header -->
|
|
<div class="mb-6 flex items-center justify-between p-6">
|
|
<h3 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-3xl">
|
|
Opprett arbeidstime(r)
|
|
</h3>
|
|
</div>
|
|
|
|
<!-- Form -->
|
|
<div class="flex justify-center">
|
|
<form
|
|
@submit.prevent="submit"
|
|
class="space-y-6 min-w-2xl lg:min-w-3xl xl:min-w-4xl"
|
|
>
|
|
<!-- Bruker (Combobox) -->
|
|
<div>
|
|
<Label for="combobox">Bruker</Label>
|
|
<Combobox
|
|
v-model:modelValue="selectedProfile"
|
|
by="name"
|
|
class="mt-2 w-full"
|
|
>
|
|
<ComboboxAnchor>
|
|
<div class="relative w-full">
|
|
<ComboboxInput
|
|
id="combobox"
|
|
class="pl-9"
|
|
:display-value="(val: Profile) => val?.name ?? ''"
|
|
placeholder="Velg bruker…"
|
|
/>
|
|
<span class="absolute inset-y-0 start-0 flex items-center px-3">
|
|
<Search class="h-4 w-4 text-muted-foreground" />
|
|
</span>
|
|
</div>
|
|
</ComboboxAnchor>
|
|
<ComboboxList>
|
|
<ComboboxEmpty>Ingen brukere funnet...</ComboboxEmpty>
|
|
<ComboboxGroup>
|
|
<ComboboxItem
|
|
v-for="p in profiles"
|
|
:key="p.id"
|
|
:value="p"
|
|
class="flex items-center justify-between"
|
|
>
|
|
{{ p.name }}
|
|
<ComboboxItemIndicator>
|
|
<Check class="ml-auto h-4 w-4" />
|
|
</ComboboxItemIndicator>
|
|
</ComboboxItem>
|
|
</ComboboxGroup>
|
|
</ComboboxList>
|
|
</Combobox>
|
|
<p v-if="form.errors.user_id" class="mt-1 text-sm text-red-600">
|
|
{{ form.errors.user_id }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Timer (Decimal NumberField) -->
|
|
<div>
|
|
<Label for="ordinary_hours">Timer</Label>
|
|
<NumberField
|
|
id="ordinary_hours"
|
|
v-model:modelValue="form.ordinary_hours"
|
|
:format-options="{
|
|
minimumFractionDigits: 1,
|
|
maximumFractionDigits: 1
|
|
}"
|
|
:step="0.5"
|
|
:min="0.5"
|
|
:default-value="0.5"
|
|
class="mt-2 w-full"
|
|
>
|
|
<NumberFieldContent>
|
|
<NumberFieldDecrement />
|
|
<NumberFieldInput />
|
|
<NumberFieldIncrement />
|
|
</NumberFieldContent>
|
|
</NumberField>
|
|
<p v-if="form.errors.ordinary_hours" class="mt-1 text-sm text-red-600">
|
|
{{ form.errors.ordinary_hours }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Arbeidsdato (Date Picker) -->
|
|
<div>
|
|
<Label for="work_date">Arbeidsdato</Label>
|
|
<Popover>
|
|
<PopoverTrigger as-child>
|
|
<Button
|
|
variant="outline"
|
|
:class="cn(
|
|
'w-[280px] justify-start text-left font-normal mt-2',
|
|
!dateValue && 'text-muted-foreground'
|
|
)"
|
|
>
|
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
|
{{ dateValue
|
|
? df.format(dateValue.toDate(getLocalTimeZone()))
|
|
: 'Velg dato…' }}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent class="w-auto p-0">
|
|
<Calendar v-model="dateValue" initial-focus />
|
|
</PopoverContent>
|
|
</Popover>
|
|
<p v-if="form.errors.work_date" class="mt-1 text-sm text-red-600">
|
|
{{ form.errors.work_date }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tilknyttet prosjekt -->
|
|
<div>
|
|
<Label for="combobox-project">Tilknyttet prosjekt</Label>
|
|
<Combobox v-model:modelValue="selectedProject" by="project_name" class="mt-2 w-full">
|
|
<ComboboxAnchor>
|
|
<div class="relative w-full">
|
|
<ComboboxInput
|
|
id="combobox-project"
|
|
class="pl-9"
|
|
:display-value="(val: ProjectEntry) => val?.project_name ?? ''"
|
|
placeholder="Velg prosjekt…"
|
|
/>
|
|
<span class="absolute inset-y-0 start-0 flex items-center px-3">
|
|
<Search class="h-4 w-4 text-muted-foreground" />
|
|
</span>
|
|
</div>
|
|
</ComboboxAnchor>
|
|
<ComboboxList>
|
|
<ComboboxEmpty>Ingen prosjekter funnet...</ComboboxEmpty>
|
|
<ComboboxGroup>
|
|
<ComboboxItem v-for="proj in projectsList" :key="proj.id" :value="proj" class="flex items-center justify-between">
|
|
{{ proj.project_name }}
|
|
<ComboboxItemIndicator>
|
|
<Check class="ml-auto h-4 w-4" />
|
|
</ComboboxItemIndicator>
|
|
</ComboboxItem>
|
|
</ComboboxGroup>
|
|
</ComboboxList>
|
|
</Combobox>
|
|
<p v-if="form.errors.project_id" class="mt-1 text-sm text-red-600">
|
|
{{ form.errors.project_id }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Kommentar -->
|
|
<div>
|
|
<Label for="comment">Kommentar</Label>
|
|
<div class="mt-2 space-y-1">
|
|
<Textarea
|
|
id="comment"
|
|
v-model="form.comment"
|
|
placeholder="Skriv kommentar…"
|
|
class="mt-2 w-full"
|
|
/>
|
|
</div>
|
|
<p v-if="form.errors.comment" class="mt-1 text-sm text-red-600">
|
|
{{ form.errors.comment }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex items-center justify-end space-x-4">
|
|
<Link :href="route('smartdok.work-hours.index')">
|
|
<Button variant="outline">Avbryt</Button>
|
|
</Link>
|
|
<Button type="submit" :disabled="form.processing">
|
|
Opprett
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|