diff --git a/frontend/src/components/common/CalendarWeek.test.tsx b/frontend/src/components/common/CalendarWeek.test.tsx
index f5224ab..874951f 100644
--- a/frontend/src/components/common/CalendarWeek.test.tsx
+++ b/frontend/src/components/common/CalendarWeek.test.tsx
@@ -1155,6 +1155,95 @@ describe('CalendarWeek', () => {
})
})
+ describe('band names: once per parent per week row, not once per segment', () => {
+ it('shows the name once when ad-hoc days for one parent split into a vacation run then a sjukpenning run', () => {
+ // Mon/Tue vacation and Wed-Fri sjukpenning are two separate ad-hoc
+ // bands (bandKeyFor keys on parent+dayType), not one band with a
+ // mid-period type change — each has its own single-run label. The
+ // parent's name belongs only on the leftmost (vacation) one.
+ const plannedDays = [
+ pd({ id: '1', date: '2026-01-05', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
+ pd({ id: '2', date: '2026-01-06', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
+ pd({ id: '3', date: '2026-01-07', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
+ pd({ id: '4', date: '2026-01-08', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
+ pd({ id: '5', date: '2026-01-09', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
+ ]
+ const { container } = render()
+ expect(screen.getAllByTestId(/^calendar-band-/)).toHaveLength(2)
+ const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
+ expect(labels).toHaveLength(2)
+ expect(labels[0]?.textContent).toContain('Test Parent')
+ expect(labels[0]?.textContent).toContain('Semester')
+ expect(labels[1]?.textContent).not.toContain('Test Parent')
+ expect(labels[1]?.textContent).toContain('Sjukpenning')
+ })
+
+ it('shows the name once across two adjacent wishes for the same parent', () => {
+ const plannedDays = [
+ pd({ id: '1', date: '2026-01-05', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
+ pd({ id: '2', date: '2026-01-06', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
+ pd({ id: '3', date: '2026-01-07', wishId: 'wish-2', parentPersonalDataUuid: 'parent-1' }),
+ pd({ id: '4', date: '2026-01-08', wishId: 'wish-2', parentPersonalDataUuid: 'parent-1' }),
+ ]
+ const { container } = render()
+ expect(screen.getAllByTestId(/^calendar-band-/)).toHaveLength(2)
+ const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
+ expect(labels).toHaveLength(2)
+ expect(labels[0]?.textContent).toContain('Test Parent')
+ expect(labels[1]?.textContent).not.toContain('Test Parent')
+ })
+
+ it('shows each parent\'s name once, independently, when both parents have a mid-week type change', () => {
+ const plannedDays = [
+ pd({ id: '1', date: '2026-01-05', parentPersonalDataUuid: 'parent-1', dayType: 'vacation' }),
+ pd({ id: '2', date: '2026-01-06', parentPersonalDataUuid: 'parent-1', dayType: 'sjukpenning' }),
+ pd({ id: '3', date: '2026-01-07', parentPersonalDataUuid: 'other-parent', dayType: 'vacation' }),
+ pd({ id: '4', date: '2026-01-08', parentPersonalDataUuid: 'other-parent', dayType: 'sjukpenning' }),
+ ]
+ const { container } = render()
+ const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
+ expect(labels).toHaveLength(4)
+ expect(labels[0]?.textContent).toContain('Test Parent')
+ expect(labels[1]?.textContent).not.toContain('Test Parent')
+ expect(labels[1]?.textContent).not.toContain('Partner')
+ expect(labels[2]?.textContent).toContain('Partner')
+ expect(labels[3]?.textContent).not.toContain('Partner')
+ expect(labels[3]?.textContent).not.toContain('Test Parent')
+ })
+
+ it('still shows the name on a band continuing into the next week row (each row recomputes its own leftmost segment)', () => {
+ const plannedDays = [
+ pd({ id: '1', date: '2026-01-10', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
+ pd({ id: '2', date: '2026-01-13', wishId: 'wish-1', parentPersonalDataUuid: 'parent-1' }),
+ ]
+ const calendarDaysByDate = new Map([
+ ['2026-01-11', calDay({ date: '2026-01-11', dayType: 'weekend' })],
+ ['2026-01-12', calDay({ date: '2026-01-12', dayType: 'weekend' })],
+ ])
+ const { container } = render(
+ <>
+
+
+ >,
+ )
+ const labels = Array.from(container.querySelectorAll('.calendar-band__label'))
+ expect(labels).toHaveLength(2)
+ expect(labels[0]?.textContent).toContain('Test Parent')
+ expect(labels[1]?.textContent).toContain('Test Parent')
+ })
+ })
+
describe('per-day hover/focus card', () => {
beforeEach(() => {
vi.useFakeTimers()
diff --git a/frontend/src/components/common/CalendarWeek.tsx b/frontend/src/components/common/CalendarWeek.tsx
index 1828dfc..7f33640 100644
--- a/frontend/src/components/common/CalendarWeek.tsx
+++ b/frontend/src/components/common/CalendarWeek.tsx
@@ -452,6 +452,34 @@ export function CalendarWeek({
[weekDates, plannedDays, calendarDaysByDate],
)
+ // A parent's leave in one week row can be split across several band
+ // segments — ad-hoc days are keyed by parent+dayType (`bandKeyFor`), so a
+ // Mon-Tue vacation stretch and a Wed-Fri sjukpenning stretch for the same
+ // parent are two segments, not one — and separate wishes are always
+ // separate segments regardless of adjacency. Showing the parent name on
+ // every segment's first run would repeat "Name · Type" once per segment;
+ // the spec is ONE name per parent per week row, on that parent's
+ // leftmost segment (lowest `startCol`, tie-broken by lowest `lane` for
+ // determinism when two segments happen to start in the same column).
+ // Every other segment/run for that parent in this row shows the type
+ // only. This is computed per week (each row calls `computeBandSegments`
+ // independently), so a band continuing into the next week row still gets
+ // its own leftmost-segment name there.
+ const nameSegmentIdByParent = useMemo(() => {
+ const bestByParent = new Map()
+ for (const seg of bandSegments) {
+ const current = bestByParent.get(seg.parentUuid)
+ if (!current || seg.startCol < current.startCol || (seg.startCol === current.startCol && seg.lane < current.lane)) {
+ bestByParent.set(seg.parentUuid, seg)
+ }
+ }
+ const map = new Map()
+ for (const [parentUuid, seg] of bestByParent) {
+ map.set(parentUuid, seg.id)
+ }
+ return map
+ }, [bandSegments])
+
// Bands overlay the day-cell row itself (grid-row: 1, position: absolute)
// instead of occupying separate rows below it — see the comment on
// `.calendar-week` in calendar.css. `barHeight`/`laneGap`/`bandBottomInset`
@@ -679,6 +707,7 @@ export function CalendarWeek({
const runDubbeldag = segment.dayInfos
.slice(run.startIdx, run.endIdx + 1)
.some((d) => !d.isGap && d.isDubbeldag)
+ const showName = runIdx === 0 && nameSegmentIdByParent.get(segment.parentUuid) === segment.id
return (