feat(timeline): derive header meta figures from the DTO (REQ-002)
A pure timelineMeta() returns the year range (first/last band, null when there are no bands) and the letter/event totals across all year bands plus the undated bucket — the single place these counts are computed. Refs #833 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
56
frontend/src/lib/timeline/timelineMeta.spec.ts
Normal file
56
frontend/src/lib/timeline/timelineMeta.spec.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { timelineMeta } from './timelineMeta';
|
||||||
|
import { makeEntry, makeYear, makeTimelineDTO } from './test-factories';
|
||||||
|
|
||||||
|
const letter = (id: string) => makeEntry({ kind: 'LETTER', documentId: id });
|
||||||
|
const event = (title: string) =>
|
||||||
|
makeEntry({
|
||||||
|
kind: 'EVENT',
|
||||||
|
derived: true,
|
||||||
|
derivedType: 'BIRTH',
|
||||||
|
title,
|
||||||
|
senderName: '',
|
||||||
|
receiverName: '',
|
||||||
|
documentId: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('timelineMeta', () => {
|
||||||
|
it('counts letters and events across year bands and the undated bucket (REQ-002)', () => {
|
||||||
|
const dto = makeTimelineDTO({
|
||||||
|
years: [
|
||||||
|
makeYear(1909, [letter('a'), event('Geburt'), letter('b')]),
|
||||||
|
makeYear(1924, [event('Tod')])
|
||||||
|
],
|
||||||
|
undated: [letter('c'), event('Heirat')]
|
||||||
|
});
|
||||||
|
const meta = timelineMeta(dto);
|
||||||
|
expect(meta.letterCount).toBe(3);
|
||||||
|
expect(meta.eventCount).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads the range from the first and last year band (REQ-002)', () => {
|
||||||
|
const dto = makeTimelineDTO({
|
||||||
|
years: [makeYear(1909, [letter('a')]), makeYear(1924, [letter('b')])]
|
||||||
|
});
|
||||||
|
const meta = timelineMeta(dto);
|
||||||
|
expect(meta.firstYear).toBe(1909);
|
||||||
|
expect(meta.lastYear).toBe(1924);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a null range when there are no year bands, but still counts undated (REQ-002)', () => {
|
||||||
|
const dto = makeTimelineDTO({ undated: [letter('a')] });
|
||||||
|
const meta = timelineMeta(dto);
|
||||||
|
expect(meta.firstYear).toBeNull();
|
||||||
|
expect(meta.lastYear).toBeNull();
|
||||||
|
expect(meta.letterCount).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reports zero counts and a null range for an empty timeline (REQ-002)', () => {
|
||||||
|
expect(timelineMeta(makeTimelineDTO())).toEqual({
|
||||||
|
firstYear: null,
|
||||||
|
lastYear: null,
|
||||||
|
letterCount: 0,
|
||||||
|
eventCount: 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
31
frontend/src/lib/timeline/timelineMeta.ts
Normal file
31
frontend/src/lib/timeline/timelineMeta.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { components } from '$lib/generated/api';
|
||||||
|
|
||||||
|
type TimelineDTO = components['schemas']['TimelineDTO'];
|
||||||
|
|
||||||
|
export interface TimelineMeta {
|
||||||
|
/** First year band's year, or `null` when there are no bands. */
|
||||||
|
firstYear: number | null;
|
||||||
|
/** Last year band's year, or `null` when there are no bands. */
|
||||||
|
lastYear: number | null;
|
||||||
|
/** Every `LETTER` entry across all year bands plus the undated bucket. */
|
||||||
|
letterCount: number;
|
||||||
|
/** Every `EVENT` entry (derived, curated, and historical) across all bands plus undated. */
|
||||||
|
eventCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the header meta-line figures from a loaded `TimelineDTO` (REQ-002):
|
||||||
|
* the year range (first/last band) and the letter/event totals across every
|
||||||
|
* year band plus the undated bucket. Pure and the single place these counts
|
||||||
|
* live — the route renders them; `TimelineView` never recomputes them.
|
||||||
|
*/
|
||||||
|
export function timelineMeta(timeline: TimelineDTO): TimelineMeta {
|
||||||
|
const years = timeline.years;
|
||||||
|
const allEntries = [...years.flatMap((y) => y.entries), ...timeline.undated];
|
||||||
|
return {
|
||||||
|
firstYear: years.length ? years[0].year : null,
|
||||||
|
lastYear: years.length ? years[years.length - 1].year : null,
|
||||||
|
letterCount: allEntries.filter((e) => e.kind === 'LETTER').length,
|
||||||
|
eventCount: allEntries.filter((e) => e.kind === 'EVENT').length
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user