add description/project labels to break placement modal for existing

time entries
This commit is contained in:
Gregor Vostrak
2026-07-28 15:21:58 +02:00
parent bbd7d286d9
commit 40dc3842ce
3 changed files with 50 additions and 17 deletions

View File

@@ -697,8 +697,12 @@ test('test that adding a timesheet break to a full day splits the work entry via
const breakCell = await fillBreakCell(page, '0.5');
await breakCell.press('Enter');
// The placement modal opens with the split preview
// The placement modal opens with the split preview, naming the entry that
// will be split so the user can recognize it.
await expect(page.getByTestId('break_placement_summary')).toBeVisible();
await expect(page.getByTestId('break_placement_summary')).toContainText(
'No Project · Split me'
);
await Promise.all([
waitForBreakCreated(page),
page.getByRole('button', { name: 'Add break' }).click(),

View File

@@ -13,12 +13,14 @@ import {
planMoveInsert,
planSplitEntry,
type BreakPlacementRequest,
type Interval,
} from '@/utils/timesheet/breakPlacementMath';
import { BREAK_GAP_TOLERANCE_MINUTES } from '@/packages/ui/src/utils/breakPlacement';
const props = defineProps<{
request: BreakPlacementRequest | null;
apply: (breakStart: string, durationSeconds: number) => Promise<void>;
entryLabel: (id: string) => string;
}>();
const emit = defineEmits<{ cancel: [] }>();
@@ -124,30 +126,39 @@ const explanation = computed(() => {
: "There's no free gap that fits this break, so the surrounding entries will be shifted to make room.";
});
const changeSummary = computed<string[]>(() => {
interface PlanLine {
times: string;
label: string;
}
const changeSummary = computed<PlanLine[]>(() => {
const req = props.request;
if (!req) return [];
const range = (interval: Interval) => `${fmt(interval.start)}${fmt(interval.end)}`;
const moved = (from: Interval, to: Interval) => `${range(from)}${range(to)}`;
if (mode.value === 'split') {
const plan = splitPlan.value;
if (!plan) return [];
const workLabel = props.entryLabel(req.workEntries[0]!.id);
return [
`${fmt(plan.firstHalf.start)}${fmt(plan.firstHalf.end)} (work)`,
`${fmt(plan.breakSlot.start)}${fmt(plan.breakSlot.end)} (break)`,
`${fmt(plan.secondHalf.start)}${fmt(plan.secondHalf.end)} (work)`,
...plan.shifted.map((shift) => {
const original = props.request!.otherEntries.find((e) => e.id === shift.id)!;
return `${fmt(original.start)}${fmt(original.end)}${fmt(shift.start)}${fmt(shift.end)} (break)`;
}),
{ times: range(plan.firstHalf), label: workLabel },
{ times: range(plan.breakSlot), label: 'Break' },
{ times: range(plan.secondHalf), label: workLabel },
...plan.shifted.map((shift) => ({
times: moved(req.otherEntries.find((e) => e.id === shift.id)!, shift),
label: props.entryLabel(shift.id),
})),
];
}
const plan = movePlan.value;
if (!plan) return [];
if (plan.shifted.length === 0) return ['No entries need to move.'];
if (plan.shifted.length === 0) return [{ times: 'No entries need to move.', label: '' }];
return plan.shifted.map((shift) => {
const isBreak = props.request!.otherEntries.some((e) => e.id === shift.id);
const original =
props.request!.workEntries.find((e) => e.id === shift.id) ??
props.request!.otherEntries.find((e) => e.id === shift.id)!;
const label = `${fmt(original.start)}${fmt(original.end)}${fmt(shift.start)}${fmt(shift.end)}`;
return isBreak ? `${label} (break)` : label;
req.workEntries.find((e) => e.id === shift.id) ??
req.otherEntries.find((e) => e.id === shift.id)!;
return { times: moved(original, shift), label: props.entryLabel(shift.id) };
});
});
@@ -189,8 +200,14 @@ async function submit() {
<div class="text-xs uppercase tracking-wide text-text-tertiary">
{{ mode === 'split' ? 'Result' : 'Entries that move' }}
</div>
<div v-for="(line, index) in changeSummary" :key="index" class="tabular-nums">
{{ line }}
<div
v-for="(line, index) in changeSummary"
:key="index"
class="flex items-baseline gap-2">
<span class="tabular-nums whitespace-nowrap">{{ line.times }}</span>
<span v-if="line.label" class="text-text-tertiary truncate">
{{ line.label }}
</span>
</div>
</div>
<div

View File

@@ -133,6 +133,17 @@ const {
() => organization.value?.prevent_overlapping_time_entries ?? false
);
function breakPlanEntryLabel(id: string): string {
const entry = allTimeEntries.value.find((e) => e.id === id);
if (!entry) return '';
if (entry.type === 'break') return 'Break';
const project = projects.value.find((p) => p.id === entry.project_id);
const task = tasks.value.find((t) => t.id === entry.task_id);
return [project?.name ?? 'No Project', task?.name, entry.description]
.filter((part): part is string => !!part)
.join(' · ');
}
// Local dates (YYYY-MM-DD) that have a misplaced break. There is only one break
// row, so a flat set is enough — its cells show a warning for dates in the set.
const misplacedBreakDates = computed<Set<string>>(() => {
@@ -257,6 +268,7 @@ async function createTag(name: string): Promise<Tag | undefined> {
<BreakPlacementModal
:request="breakPlacementRequest"
:apply="applyBreakPlacement"
:entry-label="breakPlanEntryLabel"
@cancel="dismissBreakPlacement" />
</AppLayout>
</template>