add support for week_start and time_format in calendar

also rename them so that they do not conflict with the datepicker calendar component
This commit is contained in:
Gregor Vostrak
2025-08-14 16:46:41 +02:00
parent 9fa9522237
commit 4e635cde83
4 changed files with 52 additions and 11 deletions

View File

@@ -19,6 +19,9 @@ const dateFormat = computed(() => organization?.value?.date_format);
<template> <template>
<div class="fc-day-header-custom"> <div class="fc-day-header-custom">
<div class="text-xs text-muted-foreground font-medium">
{{ date.toLocaleDateString('en-US', { weekday: 'short' }) }}
</div>
<span>{{ formatDate(date.toISOString(), dateFormat) }}</span> <span>{{ formatDate(date.toISOString(), dateFormat) }}</span>
<span class="block text-xs text-muted-foreground font-medium mt-1"> <span class="block text-xs text-muted-foreground font-medium mt-1">
{{ formatHumanReadableDuration(totalSeconds, intervalFormat, numberFormat) }} {{ formatHumanReadableDuration(totalSeconds, intervalFormat, numberFormat) }}

View File

@@ -4,12 +4,12 @@ import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid'; import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction'; import interactionPlugin from '@fullcalendar/interaction';
import type { DatesSetArg, EventClickArg, EventDropArg, EventChangeArg } from '@fullcalendar/core'; import type { DatesSetArg, EventClickArg, EventDropArg, EventChangeArg } from '@fullcalendar/core';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch, inject, type ComputedRef } from 'vue';
import { getDayJsInstance, getLocalizedDayJs } from '../utils/time'; import { getDayJsInstance, getLocalizedDayJs } from '../utils/time';
import { getUserTimezone } from '../utils/settings'; import { getUserTimezone, getWeekStart } from '../utils/settings';
import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..'; import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..';
import CalendarEventContent from './CalendarEventContent.vue'; import FullCalendarEventContent from './FullCalendarEventContent.vue';
import CalendarDayHeader from './CalendarDayHeader.vue'; import FullCalendarDayHeader from './FullCalendarDayHeader.vue';
import type { import type {
TimeEntry, TimeEntry,
Project, Project,
@@ -18,6 +18,7 @@ import type {
CreateProjectBody, CreateProjectBody,
CreateClientBody, CreateClientBody,
Tag, Tag,
Organization,
} from '@/packages/api/src'; } from '@/packages/api/src';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
@@ -58,6 +59,41 @@ const selectedTimeEntry = ref<TimeEntry | null>(null);
const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null); const calendarRef = ref<InstanceType<typeof FullCalendar> | null>(null);
// Inject organization data for settings
const organization = inject<ComputedRef<Organization>>('organization');
// Helper function to convert week start to FullCalendar firstDay value
const getFirstDay = () => {
const weekStart = getWeekStart();
const weekStartMap: Record<string, number> = {
'sunday': 0,
'monday': 1,
'tuesday': 2,
'wednesday': 3,
'thursday': 4,
'friday': 5,
'saturday': 6,
};
return weekStartMap[weekStart] ?? 1; // Default to Monday if not found
};
// Helper function to get time format for slot labels
const getSlotLabelFormat = () => {
const timeFormat = organization?.value?.time_format || '24-hours';
if (timeFormat === '12-hours') {
return {
hour: 'numeric' as const,
hour12: true,
};
} else {
return {
hour: '2-digit' as const,
minute: '2-digit' as const,
hour12: false,
};
}
};
// Map API entries to FullCalendar events // Map API entries to FullCalendar events
const events = computed(() => { const events = computed(() => {
return props.timeEntries return props.timeEntries
@@ -188,7 +224,9 @@ const calendarOptions = computed(() => ({
slotMaxTime: '24:00:00', slotMaxTime: '24:00:00',
slotDuration: '00:15:00', slotDuration: '00:15:00',
slotLabelInterval: '01:00:00', slotLabelInterval: '01:00:00',
slotLabelFormat: getSlotLabelFormat(),
snapDuration: '00:15:00', snapDuration: '00:15:00',
firstDay: getFirstDay(),
allDaySlot: false, allDaySlot: false,
nowIndicator: true, nowIndicator: true,
selectable: true, selectable: true,
@@ -261,7 +299,7 @@ watch(showEditTimeEntryModal, (value) => {
:clients="clients" /> :clients="clients" />
<FullCalendar ref="calendarRef" class="fullcalendar" :options="calendarOptions"> <FullCalendar ref="calendarRef" class="fullcalendar" :options="calendarOptions">
<template #eventContent="arg"> <template #eventContent="arg">
<CalendarEventContent <FullCalendarEventContent
:title="arg.event.title" :title="arg.event.title"
:project-name="(arg.event.extendedProps as any).project?.name" :project-name="(arg.event.extendedProps as any).project?.name"
:task-name="(arg.event.extendedProps as any).task?.name" :task-name="(arg.event.extendedProps as any).task?.name"
@@ -275,7 +313,7 @@ watch(showEditTimeEntryModal, (value) => {
:end="arg.event.end as any" /> :end="arg.event.end as any" />
</template> </template>
<template #dayHeaderContent="arg"> <template #dayHeaderContent="arg">
<CalendarDayHeader <FullCalendarDayHeader
:date="arg.date" :date="arg.date"
:total-minutes=" :total-minutes="
dailyTotals[getDayJsInstance()(arg.date).format('YYYY-MM-DD')] || 0 dailyTotals[getDayJsInstance()(arg.date).format('YYYY-MM-DD')] || 0

View File

@@ -29,9 +29,9 @@ import TimeEntryMassActionRow from './TimeEntry/TimeEntryMassActionRow.vue';
import TimeEntryCreateModal from './TimeEntry/TimeEntryCreateModal.vue'; import TimeEntryCreateModal from './TimeEntry/TimeEntryCreateModal.vue';
import TimeEntryEditModal from './TimeEntry/TimeEntryEditModal.vue'; import TimeEntryEditModal from './TimeEntry/TimeEntryEditModal.vue';
import MoreOptionsDropdown from './MoreOptionsDropdown.vue'; import MoreOptionsDropdown from './MoreOptionsDropdown.vue';
import CalendarEventContent from './Calendar/CalendarEventContent.vue'; import FullCalendarEventContent from './FullCalendar/FullCalendarEventContent.vue';
import CalendarDayHeader from './Calendar/CalendarDayHeader.vue'; import FullCalendarDayHeader from './FullCalendar/FullCalendarDayHeader.vue';
import TimeEntryCalendar from './Calendar/TimeEntryCalendar.vue'; import TimeEntryCalendar from './FullCalendar/TimeEntryCalendar.vue';
export { export {
money, money,
@@ -57,7 +57,7 @@ export {
MoreOptionsDropdown, MoreOptionsDropdown,
TimeEntryCreateModal, TimeEntryCreateModal,
TimeEntryEditModal, TimeEntryEditModal,
CalendarEventContent, FullCalendarEventContent,
CalendarDayHeader, FullCalendarDayHeader,
TimeEntryCalendar, TimeEntryCalendar,
}; };