From 09af0f775f68718efca6b075f917bd374e42632b Mon Sep 17 00:00:00 2001 From: Gregor Vostrak Date: Thu, 30 Apr 2026 02:50:02 +0200 Subject: [PATCH] add timesheets page --- .../Components/Timesheet/RemoveRowDialog.vue | 46 +++ .../js/Components/Timesheet/TimesheetCell.vue | 65 ++++ .../Timesheet/TimesheetFooterActions.vue | 50 +++ .../js/Components/Timesheet/TimesheetGrid.vue | 159 +++++++++ .../Components/Timesheet/TimesheetHeader.vue | 60 ++++ .../js/Components/Timesheet/TimesheetRow.vue | 132 +++++++ resources/js/Layouts/AppLayout.vue | 10 +- resources/js/Pages/Calendar.vue | 7 +- resources/js/Pages/Timesheet.vue | 199 +++++++++++ .../ui/src/FullCalendar/TimeEntryCalendar.vue | 2 +- .../src/FullCalendar/useCalendarNavigation.ts | 20 +- .../ui/src/Input/DurationSecondsInput.vue | 145 ++++++++ .../ui/src/Input/EstimatedTimeInput.vue | 66 +--- .../js/packages/ui/src/Input/TextInput.vue | 21 +- .../TimeTrackerProjectTaskDropdown.vue | 50 +-- .../js/packages/ui/src/utils/settings.ts | 14 + resources/js/packages/ui/src/utils/time.ts | 9 + resources/js/packages/ui/tailwind.theme.js | 2 +- resources/js/utils/notification.ts | 7 + resources/js/utils/timesheet/cellMath.ts | 202 +++++++++++ .../js/utils/timesheet/useCopyLastWeek.ts | 242 +++++++++++++ .../timesheet/useTimesheetCellMutations.ts | 322 ++++++++++++++++++ .../timesheet/useTimesheetRowDeletion.ts | 70 ++++ .../timesheet/useTimesheetRowMutations.ts | 150 ++++++++ .../js/utils/timesheet/useTimesheetWeek.ts | 82 +++++ .../js/utils/useTimeEntriesCalendarQuery.ts | 53 +-- resources/js/utils/useTimesheetGrid.ts | 284 +++++++++++++++ resources/js/utils/useTimesheetQuery.ts | 102 ++++++ 28 files changed, 2424 insertions(+), 147 deletions(-) create mode 100644 resources/js/Components/Timesheet/RemoveRowDialog.vue create mode 100644 resources/js/Components/Timesheet/TimesheetCell.vue create mode 100644 resources/js/Components/Timesheet/TimesheetFooterActions.vue create mode 100644 resources/js/Components/Timesheet/TimesheetGrid.vue create mode 100644 resources/js/Components/Timesheet/TimesheetHeader.vue create mode 100644 resources/js/Components/Timesheet/TimesheetRow.vue create mode 100644 resources/js/Pages/Timesheet.vue create mode 100644 resources/js/packages/ui/src/Input/DurationSecondsInput.vue create mode 100644 resources/js/utils/timesheet/cellMath.ts create mode 100644 resources/js/utils/timesheet/useCopyLastWeek.ts create mode 100644 resources/js/utils/timesheet/useTimesheetCellMutations.ts create mode 100644 resources/js/utils/timesheet/useTimesheetRowDeletion.ts create mode 100644 resources/js/utils/timesheet/useTimesheetRowMutations.ts create mode 100644 resources/js/utils/timesheet/useTimesheetWeek.ts create mode 100644 resources/js/utils/useTimesheetGrid.ts create mode 100644 resources/js/utils/useTimesheetQuery.ts diff --git a/resources/js/Components/Timesheet/RemoveRowDialog.vue b/resources/js/Components/Timesheet/RemoveRowDialog.vue new file mode 100644 index 00000000..65623f26 --- /dev/null +++ b/resources/js/Components/Timesheet/RemoveRowDialog.vue @@ -0,0 +1,46 @@ + + + diff --git a/resources/js/Components/Timesheet/TimesheetCell.vue b/resources/js/Components/Timesheet/TimesheetCell.vue new file mode 100644 index 00000000..31556672 --- /dev/null +++ b/resources/js/Components/Timesheet/TimesheetCell.vue @@ -0,0 +1,65 @@ + + + diff --git a/resources/js/Components/Timesheet/TimesheetFooterActions.vue b/resources/js/Components/Timesheet/TimesheetFooterActions.vue new file mode 100644 index 00000000..b5a73cf2 --- /dev/null +++ b/resources/js/Components/Timesheet/TimesheetFooterActions.vue @@ -0,0 +1,50 @@ + + + diff --git a/resources/js/Components/Timesheet/TimesheetGrid.vue b/resources/js/Components/Timesheet/TimesheetGrid.vue new file mode 100644 index 00000000..df54539a --- /dev/null +++ b/resources/js/Components/Timesheet/TimesheetGrid.vue @@ -0,0 +1,159 @@ + + + diff --git a/resources/js/Components/Timesheet/TimesheetHeader.vue b/resources/js/Components/Timesheet/TimesheetHeader.vue new file mode 100644 index 00000000..8ce79740 --- /dev/null +++ b/resources/js/Components/Timesheet/TimesheetHeader.vue @@ -0,0 +1,60 @@ + + + diff --git a/resources/js/Components/Timesheet/TimesheetRow.vue b/resources/js/Components/Timesheet/TimesheetRow.vue new file mode 100644 index 00000000..26ceb7ec --- /dev/null +++ b/resources/js/Components/Timesheet/TimesheetRow.vue @@ -0,0 +1,132 @@ + + + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue index da7135b8..4f878468 100644 --- a/resources/js/Layouts/AppLayout.vue +++ b/resources/js/Layouts/AppLayout.vue @@ -17,6 +17,7 @@ import { UserGroupIcon, XMarkIcon, DocumentTextIcon, + TableCellsIcon, } from '@heroicons/vue/20/solid'; import { PanelLeft } from 'lucide-vue-next'; import NavigationSidebarItem from '@/Components/NavigationSidebarItem.vue'; @@ -135,7 +136,7 @@ const page = usePage<{ ? 'max-lg:translate-x-0 max-lg:shadow-xl' : 'max-lg:-translate-x-full', ]" - class="flex-shrink-0 h-screen fixed w-[280px] px-2.5 py-4 hidden lg:flex flex-col justify-between bg-background border-r border-default-background-separator max-lg:z-50 max-lg:transition-transform max-lg:duration-200 max-lg:ease-in-out lg:w-[230px] 2xl:w-[250px] 2xl:px-3 lg:border-r-0" + class="flex-shrink-0 h-screen fixed w-[280px] px-2.5 py-4 hidden lg:flex flex-col justify-between bg-background border-r border-default-background-separator max-lg:z-50 max-lg:transition-transform max-lg:duration-200 max-lg:ease-in-out lg:w-[230px] lg:border-r-0" :style="showSidebarMenu ? { display: 'flex' } : undefined">
+
-
+
(undefined); -const calendarEnd = ref(undefined); +const calendarStart = ref(undefined); +const calendarEnd = ref(undefined); // Test-injectable activity periods (for E2E testing). // These hooks are no-ops in production — they only take effect when test code @@ -99,7 +100,7 @@ const { tags } = useTagsQuery(); const queryClient = useQueryClient(); -function onDatesChange({ start, end }: { start: Date; end: Date }) { +function onDatesChange({ start, end }: { start: Dayjs; end: Dayjs }) { calendarStart.value = start; calendarEnd.value = end; } diff --git a/resources/js/Pages/Timesheet.vue b/resources/js/Pages/Timesheet.vue new file mode 100644 index 00000000..5a9e6c62 --- /dev/null +++ b/resources/js/Pages/Timesheet.vue @@ -0,0 +1,199 @@ + + + diff --git a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue index df5f212a..a7e9cf5d 100644 --- a/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue +++ b/resources/js/packages/ui/src/FullCalendar/TimeEntryCalendar.vue @@ -57,7 +57,7 @@ import type { import type { Dayjs } from 'dayjs'; const emit = defineEmits<{ - (e: 'dates-change', payload: { start: Date; end: Date }): void; + (e: 'dates-change', payload: { start: Dayjs; end: Dayjs }): void; (e: 'refresh'): void; }>(); diff --git a/resources/js/packages/ui/src/FullCalendar/useCalendarNavigation.ts b/resources/js/packages/ui/src/FullCalendar/useCalendarNavigation.ts index a1fd79b5..af16d54c 100644 --- a/resources/js/packages/ui/src/FullCalendar/useCalendarNavigation.ts +++ b/resources/js/packages/ui/src/FullCalendar/useCalendarNavigation.ts @@ -1,27 +1,17 @@ import { computed, ref } from 'vue'; import type { Dayjs } from 'dayjs'; import { getLocalizedDayJs } from '../utils/time'; -import { getWeekStart } from '../utils/settings'; +import { getWeekStartDayNumber } from '../utils/settings'; export function useCalendarNavigation(callbacks: { - onDatesChange: (payload: { start: Date; end: Date }) => void; + onDatesChange: (payload: { start: Dayjs; end: Dayjs }) => void; scrollToCurrentTime: () => void; }) { const activeView = ref('timeGridWeek'); const currentDate = ref(getLocalizedDayJs()); function getFirstDay(): number { - const weekStart = getWeekStart(); - const weekStartMap: Record = { - sunday: 0, - monday: 1, - tuesday: 2, - wednesday: 3, - thursday: 4, - friday: 5, - saturday: 6, - }; - return weekStartMap[weekStart] ?? 1; + return getWeekStartDayNumber(); } const viewDays = computed(() => { @@ -67,8 +57,8 @@ export function useCalendarNavigation(callbacks: { const days = viewDays.value; if (days.length === 0) return; - const start = days[0]!.toDate(); - const end = days[days.length - 1]!.add(1, 'day').toDate(); + const start = days[0]!; + const end = days[days.length - 1]!.add(1, 'day'); callbacks.onDatesChange({ start, end }); } diff --git a/resources/js/packages/ui/src/Input/DurationSecondsInput.vue b/resources/js/packages/ui/src/Input/DurationSecondsInput.vue new file mode 100644 index 00000000..4042f00b --- /dev/null +++ b/resources/js/packages/ui/src/Input/DurationSecondsInput.vue @@ -0,0 +1,145 @@ + + + diff --git a/resources/js/packages/ui/src/Input/EstimatedTimeInput.vue b/resources/js/packages/ui/src/Input/EstimatedTimeInput.vue index cf927843..ff11e88c 100644 --- a/resources/js/packages/ui/src/Input/EstimatedTimeInput.vue +++ b/resources/js/packages/ui/src/Input/EstimatedTimeInput.vue @@ -1,12 +1,6 @@ - - diff --git a/resources/js/packages/ui/src/Input/TextInput.vue b/resources/js/packages/ui/src/Input/TextInput.vue index 33669f71..3c767191 100644 --- a/resources/js/packages/ui/src/Input/TextInput.vue +++ b/resources/js/packages/ui/src/Input/TextInput.vue @@ -1,11 +1,15 @@