Initial commit of testapi
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
<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>
|
||||
@@ -0,0 +1,279 @@
|
||||
<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>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
import { Head, Link, usePage } from '@inertiajs/vue3';
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Plus } from 'lucide-vue-next';
|
||||
|
||||
import WorkHourDropdown from './WorkHourDropdown.vue';
|
||||
|
||||
interface WorkHour {
|
||||
id: string
|
||||
user_name: string
|
||||
ordinary_hours: number
|
||||
projects: { id: number; project_name: string }[]
|
||||
};
|
||||
|
||||
const page = usePage<{
|
||||
workHours: {
|
||||
data: WorkHour[]
|
||||
current_page: number
|
||||
last_page: number
|
||||
per_page: number
|
||||
total: number
|
||||
}
|
||||
}>();
|
||||
|
||||
// Reactive list of work hours
|
||||
const workHoursList = computed(() => page.props.workHours.data);
|
||||
|
||||
// Breadcrumbs
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{ title: 'Dashbord', href: '/dashboard' },
|
||||
{ title: 'Arbeidstimer', href: '/smartdok/work-hours' },
|
||||
];
|
||||
|
||||
// Pagination meta
|
||||
const currentPage = computed(() => page.props.workHours.current_page);
|
||||
const lastPage = computed(() => page.props.workHours.last_page);
|
||||
|
||||
const prevPage = computed(() =>
|
||||
currentPage.value > 1 ? currentPage.value - 1 : 1
|
||||
);
|
||||
|
||||
const nextPage = computed(() =>
|
||||
currentPage.value < lastPage.value ? currentPage.value + 1 : lastPage.value
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Arbeidstimer" />
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<!-- Header + Create-button -->
|
||||
<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">
|
||||
Arbeidstimer
|
||||
</h3>
|
||||
<Link :href="route('smartdok.work-hours.create')">
|
||||
<Button variant="outline">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Opprett
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<!-- Table -->
|
||||
<div class="px-6 overflow-auto">
|
||||
<Table>
|
||||
<TableCaption>En liste over arbeidstimer.</TableCaption>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Bruker</TableHead>
|
||||
<TableHead>Timer</TableHead>
|
||||
<TableHead>Prosjekt</TableHead>
|
||||
<TableHead class="text-right">Handlinger</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-for="wh in workHoursList"
|
||||
:key="wh.id"
|
||||
>
|
||||
<TableCell class="font-medium">
|
||||
{{ wh.user_name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{{ wh.ordinary_hours }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<!-- list project names, comma-separated -->
|
||||
{{ wh.project_name }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<WorkHourDropdown :workHour="wh" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<!-- Pagination controls -->
|
||||
<div class="flex items-center justify-between p-6">
|
||||
<div class="text-sm text-gray-600">
|
||||
Side {{ currentPage }} av {{ lastPage }} ·
|
||||
Totalt {{ page.props.workHours.total }} poster
|
||||
</div>
|
||||
<div class="space-x-2">
|
||||
<Link
|
||||
:href="route('smartdok.work-hours.index', { page: prevPage })"
|
||||
>
|
||||
<Button variant="outline" :disabled="currentPage === 1">
|
||||
Forrige
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
:href="route('smartdok.work-hours.index', { page: nextPage })"
|
||||
>
|
||||
<Button variant="outline" :disabled="currentPage === lastPage">
|
||||
Neste
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
import { Head, Link, usePage } from '@inertiajs/vue3';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
interface WorkHour {
|
||||
id: string;
|
||||
user_name: string;
|
||||
ordinary_hours: number;
|
||||
work_date: string;
|
||||
project_name: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
const page = usePage<{ workHour: WorkHour }>();
|
||||
|
||||
const workHour = computed(() => page.props.workHour);
|
||||
|
||||
const breadcrumbs = computed<BreadcrumbItem[]>(() => [
|
||||
{ title: 'Dashbord', href: '/dashboard' },
|
||||
{ title: 'Arbeidstimer', href: '/smartdok/work-hours' },
|
||||
{
|
||||
title: workHour.value.id,
|
||||
href: route('smartdok.work-hours.show', workHour.value.id),
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Vis arbeidstime" />
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="p-6 space-y-8">
|
||||
<!-- Heading + actions -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="scroll-m-20 text-2xl font-extrabold tracking-tight lg:text-3xl">
|
||||
Vis arbeidstime
|
||||
</h3>
|
||||
<div class="flex space-x-2">
|
||||
<Link :href="route('smartdok.work-hours.index')">
|
||||
<Button variant="outline">Tilbake</Button>
|
||||
</Link>
|
||||
<Link :href="route('smartdok.work-hours.edit', workHour.id)">
|
||||
<Button>Rediger</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<!-- WorkHour details -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label for="id" className="font-semibold">ID</Label>
|
||||
<p id="id" class="mt-1 text-base">
|
||||
{{ workHour.id }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="user_name" className="font-semibold">Bruker</Label>
|
||||
<p id="user_name" class="mt-1 text-base">
|
||||
{{ workHour.user_name }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="ordinary_hours" className="font-semibold">Timer</Label>
|
||||
<p id="ordinary_hours" class="mt-1 text-base">
|
||||
{{ workHour.ordinary_hours }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="work_date" className="font-semibold">Arbeidsdato</Label>
|
||||
<p id="work_date" class="mt-1 text-base">
|
||||
{{ workHour.work_date }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label for="comment" class="font-semibold">Kommentar</Label>
|
||||
<p id="comment" class="mt-1 text-base">
|
||||
{{ workHour.comment }}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="font-semibold">Prosjekter</Label>
|
||||
<p id="project_name" className="mt-1 text-base">{{ workHour.project_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { MoreHorizontal } from 'lucide-vue-next';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogCancel,
|
||||
AlertDialogAction,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps<{
|
||||
workHour: {
|
||||
id: number
|
||||
}
|
||||
}>();
|
||||
|
||||
function copy(id: string) {
|
||||
navigator.clipboard.writeText(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" class="w-8 h-8 p-0">
|
||||
<span class="sr-only">Åpne meny</span>
|
||||
<MoreHorizontal class="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Handlinger</DropdownMenuLabel>
|
||||
<DropdownMenuItem @click="copy(workHour.id)">
|
||||
Kopier ID
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem as-child>
|
||||
<Link :href="route('smartdok.work-hours.show', workHour.id)">
|
||||
Vis arbeidstime(r)
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem as-child>
|
||||
<Link :href="route('smartdok.work-hours.edit', workHour.id)">
|
||||
Rediger
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="text-red-600 rounded-sm hover:!bg-zinc-800 cursor-pointer flex items-center py-1 px-2"
|
||||
:onSelect="(e) => e.preventDefault()"
|
||||
>Slett</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
|
||||
<!-- Confirmation dialog -->
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Er du sikker på at du vil slette denne arbeidstime oppføringen?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Denne handlingen kan ikke angres.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Avbryt</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Link
|
||||
method="delete"
|
||||
:href="route('smartdok.work-hours.destroy', workHour.id)"
|
||||
class="text-red-600"
|
||||
as="button"
|
||||
>
|
||||
Slett
|
||||
</Link>
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
Reference in New Issue
Block a user