280 lines
10 KiB
Vue
280 lines
10 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,
|
|
parseDate,
|
|
} from '@internationalized/date';
|
|
|
|
interface Profile { id: string; name: string }
|
|
interface ProjectEntry { id: number; project_name: string }
|
|
interface WorkHour {
|
|
id: string;
|
|
user_id: string;
|
|
ordinary_hours: number;
|
|
work_date: string;
|
|
project_id: number | null;
|
|
comment: string;
|
|
}
|
|
|
|
const page = usePage<{
|
|
workHour: WorkHour;
|
|
profiles: Profile[];
|
|
projects: ProjectEntry[];
|
|
}>();
|
|
|
|
const workHour = computed(() => page.props.workHour);
|
|
const profilesList = computed(() => page.props.profiles);
|
|
const projectsList = computed(() => page.props.projects);
|
|
|
|
const form = useForm({
|
|
user_id: workHour.value.user_id,
|
|
ordinary_hours: workHour.value.ordinary_hours,
|
|
work_date: workHour.value.work_date,
|
|
project_id: workHour.value.project_id,
|
|
comment: workHour.value.comment,
|
|
});
|
|
|
|
// datepicker state
|
|
const dateValue = ref<DateValue | null>( null );
|
|
const df = new DateFormatter('nb-NO', { dateStyle: 'medium' });
|
|
|
|
// initialize dateValue from form.work_date
|
|
if (form.work_date) {
|
|
// parseDate will give us a CalendarDate DateValue, which has toDate()
|
|
dateValue.value = parseDate(form.work_date);
|
|
}
|
|
|
|
// keep form.work_date in sync
|
|
watch(dateValue, (val) => {
|
|
if (val) {
|
|
const jsDate = val.toDate(getLocalTimeZone());
|
|
form.work_date = jsDate.toISOString().slice(0, 10);
|
|
} else {
|
|
form.work_date = '';
|
|
}
|
|
});
|
|
|
|
// Combobox selection for user
|
|
const selectedProfile = ref<Profile | null>(
|
|
profilesList.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: 'Rediger',
|
|
href: route('smartdok.work-hours.edit', { work_hour: workHour.value.id }),
|
|
},
|
|
];
|
|
|
|
function submit() {
|
|
form.put(route('smartdok.work-hours.update', { work_hour: workHour.value.id }));
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Rediger arbeidstime(r)" />
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<div class="p-6 space-y-8">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-3xl">
|
|
Rediger 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="user-combobox">Bruker</Label>
|
|
<Combobox
|
|
v-model:modelValue="selectedProfile"
|
|
by="name"
|
|
class="mt-2 w-full"
|
|
>
|
|
<ComboboxAnchor>
|
|
<div class="relative w-full">
|
|
<ComboboxInput
|
|
id="user-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>Fant ingen brukere...</ComboboxEmpty>
|
|
<ComboboxGroup>
|
|
<ComboboxItem
|
|
v-for="p in profilesList"
|
|
: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 }"
|
|
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>Fant ingen prosjekter...</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">
|
|
Oppdater
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|