import { describe, expect, it } from 'vitest'; import { buildResume, resumeToText } from './resume.js'; import { atLocalTime, MINUTE } from './time.js'; const DATE = '2026-09-04'; const at = (hhmm) => atLocalTime(DATE, hhmm); const DAY = { date: DATE, status: 'closed', lunchType: 'restaurant', restaurant: 'Le Relais', startedTs: at('07:30'), endedTs: at('17:30'), }; const ENTRIES = [ { id: 1, date: DATE, seq: 0, kind: 'travel', task: 'Chargement / transport', client: 'Dupont SA', startTs: at('07:30'), endTs: at('08:15') }, { id: 2, date: DATE, seq: 1, kind: 'work', task: 'Terrassement', client: 'Dupont SA', startTs: at('08:15'), endTs: at('12:00') }, { id: 3, date: DATE, seq: 2, kind: 'lunch', task: 'Pause déjeuner', client: null, startTs: at('12:00'), endTs: at('13:00') }, { id: 4, date: DATE, seq: 3, kind: 'work', task: 'Terrassement', client: 'Dupont SA', startTs: at('13:00'), endTs: at('16:45') }, { id: 5, date: DATE, seq: 4, kind: 'work', task: 'Retour / déchargement', client: 'Dupont SA', startTs: at('16:45'), endTs: at('17:30') }, ]; describe('buildResume', () => { it('groups by client and sums repeated tasks', () => { const r = buildResume(DAY, ENTRIES, 15); expect(r.clients).toHaveLength(1); const [dupont] = r.clients; expect(dupont.name).toBe('Dupont SA'); // Terrassement appears twice, before and after lunch, and reads as one line. expect(dupont.tasks.map((t) => t.name)).toEqual([ 'Terrassement', 'Chargement / transport', 'Retour / déchargement', ]); expect(dupont.tasks[0].ms).toBe(450 * MINUTE); // 3h45 + 3h45 }); it('keeps lunch out of the worked total', () => { const r = buildResume(DAY, ENTRIES, 15); expect(r.workedMs).toBe(9 * 60 * MINUTE); // 09h00 on site, lunch excluded expect(r.totalMs).toBe(10 * 60 * MINUTE); expect(r.lunch).toEqual({ type: 'restaurant', restaurant: 'Le Relais', ms: 60 * MINUTE }); }); it('splits totals across several clients', () => { const mixed = [ ENTRIES[0], { ...ENTRIES[1], client: 'Martin & Fils' }, ...ENTRIES.slice(2), ]; const r = buildResume(DAY, mixed, 15); expect(r.clients.map((c) => c.name)).toEqual(['Dupont SA', 'Martin & Fils']); expect(r.workedMs).toBe(9 * 60 * MINUTE); }); it('omits entries shorter than one rounding step, and says so', () => { const withBlip = [ ...ENTRIES, { id: 6, date: DATE, seq: 5, kind: 'work', task: 'Mauvais appui', client: 'Dupont SA', startTs: at('17:30'), endTs: at('17:32') }, ]; const r = buildResume(DAY, withBlip, 15); expect(r.omitted).toBe(1); expect(r.clients[0].tasks.map((t) => t.name)).not.toContain('Mauvais appui'); }); it('keeps a 2-minute entry when rounding is set to 1 minute', () => { const withBlip = [ ...ENTRIES, { id: 6, date: DATE, seq: 5, kind: 'work', task: 'Mauvais appui', client: 'Dupont SA', startTs: at('17:30'), endTs: at('17:32') }, ]; const r = buildResume(DAY, withBlip, 1); expect(r.omitted).toBe(0); expect(r.clients[0].tasks.map((t) => t.name)).toContain('Mauvais appui'); }); it('returns null without a day', () => { expect(buildResume(null, [], 15)).toBeNull(); }); }); describe('resumeToText', () => { it('renders the approved logbook format', () => { expect(resumeToText(buildResume(DAY, ENTRIES, 15))).toBe( [ 'Vendredi 4 septembre', '', 'Dupont SA — 9h00', ' Terrassement 7h30', ' Chargement / transport 0h45', ' Retour / déchargement 0h45', '', 'Déjeuner : restaurant (Le Relais), 1h00', '', 'Travail hors repas : 9h00', 'Total journée (repas compris) : 10h00', ].join('\n'), ); }); it('names the panier case without a restaurant', () => { const r = buildResume({ ...DAY, lunchType: 'panier', restaurant: null }, ENTRIES, 15); expect(resumeToText(r)).toContain('Déjeuner : panier, 1h00'); }); it('is empty for a null resume', () => { expect(resumeToText(null)).toBe(''); }); });