diff --git a/e2e/time.spec.ts b/e2e/time.spec.ts index f8315b7f..486f1c88 100644 --- a/e2e/time.spec.ts +++ b/e2e/time.spec.ts @@ -2315,7 +2315,9 @@ test('test that break entries show a break badge and split day total on the time await page.goto(PLAYWRIGHT_BASE_URL + '/time'); await expect(page.getByTestId('break_badge').first()).toBeVisible(); await expect(page.getByTestId('break_badge').first()).toContainText('Break'); - // Day heading shows the break portion separately from worked time + // Day heading shows worked time first, then the break portion await expect(page.getByTestId('day_break_duration').first()).toBeVisible(); - await expect(page.getByTestId('day_break_duration').first()).toContainText('break'); + await expect(page.getByTestId('day_break_duration').first().locator('..')).toContainText( + '2h 00min work · 0h 30min break' + ); }); diff --git a/resources/js/Components/Timesheet/TimesheetGrid.vue b/resources/js/Components/Timesheet/TimesheetGrid.vue index 9cd43ea3..671a96a3 100644 --- a/resources/js/Components/Timesheet/TimesheetGrid.vue +++ b/resources/js/Components/Timesheet/TimesheetGrid.vue @@ -186,19 +186,27 @@ const emit = defineEmits<{ ? 'text-text-primary' : 'text-text-secondary', ]"> - {{ total > 0 ? formatDuration(total) : '-' }} + {{ formatDuration(total) + }} - +{{ formatDuration(breakDayTotals[dayIndex] ?? 0) }} break - + class="font-normal text-text-tertiary" + >{{ formatDuration(breakDayTotals[dayIndex] ?? 0) }} break
- {{ weekTotalFormatted }} - - +{{ formatDuration(breakGrandTotal) }} break - + {{ weekTotalFormatted + }} + {{ formatDuration(breakGrandTotal) }} break
diff --git a/resources/js/packages/ui/src/FullCalendar/FullCalendarDayHeader.vue b/resources/js/packages/ui/src/FullCalendar/FullCalendarDayHeader.vue index 41e74198..7b14e357 100644 --- a/resources/js/packages/ui/src/FullCalendar/FullCalendarDayHeader.vue +++ b/resources/js/packages/ui/src/FullCalendar/FullCalendarDayHeader.vue @@ -17,6 +17,27 @@ const breakSecondsValue = computed(() => props.breakSeconds ?? 0); const organization = inject('organization') as ComputedRef | undefined; const intervalFormat = computed(() => organization?.value?.interval_format); const numberFormat = computed(() => organization?.value?.number_format); + +const hasBreak = computed(() => breakSecondsValue.value > 0); + +// Without breaks the work time stands alone, so it needs no label. Once break +// time joins it, both halves are labelled to keep them apart. +const durationSummary = computed(() => { + const work = formatHumanReadableDuration( + totalSecondsValue.value, + intervalFormat.value, + numberFormat.value + ); + if (!hasBreak.value) { + return work; + } + const breakTime = formatHumanReadableDuration( + breakSecondsValue.value, + intervalFormat.value, + numberFormat.value + ); + return `${work} work · ${breakTime} break`; +}); diff --git a/resources/js/packages/ui/src/TimeEntry/TimeEntryRowHeading.vue b/resources/js/packages/ui/src/TimeEntry/TimeEntryRowHeading.vue index 27d14b69..934918d5 100644 --- a/resources/js/packages/ui/src/TimeEntry/TimeEntryRowHeading.vue +++ b/resources/js/packages/ui/src/TimeEntry/TimeEntryRowHeading.vue @@ -6,13 +6,13 @@ import { formatWeekday, } from '@/packages/ui/src/utils/time'; import Checkbox from '../Input/Checkbox.vue'; -import { inject, type ComputedRef } from 'vue'; +import { computed, inject, type ComputedRef } from 'vue'; import type { Organization } from '@/packages/api/src'; import { CalendarIcon } from '@heroicons/vue/20/solid'; const organization = inject>('organization'); -withDefaults( +const props = withDefaults( defineProps<{ date: string; duration: number; @@ -23,6 +23,26 @@ withDefaults( breakDuration: 0, } ); + +const hasBreak = computed(() => props.breakDuration > 0); + +function formatDuration(seconds: number) { + return formatHumanReadableDuration( + seconds, + organization?.value?.interval_format, + organization?.value?.number_format + ); +} + +// Without breaks the work time stands alone, so it needs no label. Once break +// time joins it, both halves are labelled to keep them apart. The separator and +// its spacing live inside the interpolated strings so the markup cannot collapse +// them away. +const workLabel = computed(() => + hasBreak.value ? `${formatDuration(props.duration)} work` : formatDuration(props.duration) +); +const breakLabel = computed(() => ` · ${formatDuration(props.breakDuration)} break`); + const emit = defineEmits<{ selectAll: []; unselectAll: []; @@ -60,29 +80,14 @@ function selectUnselectAll(value: boolean) { {{ formatDate(date, organization?.date_format) }} -
+
+ {{ workLabel }} - {{ - formatHumanReadableDuration( - breakDuration, - organization?.interval_format, - organization?.number_format - ) - }} - break · - - - {{ - formatHumanReadableDuration( - duration, - organization?.interval_format, - organization?.number_format - ) - }} - + class="text-text-secondary font-normal whitespace-pre" + >{{ breakLabel }}