import { describe, expect, it } from 'vitest' import { arbeitsfensterAmTag, belegteMinuten, belegtZeit, freieFenster, kapazitaetAmTag, tagesBelegung, termineAmTag, ueberschneidungMinuten, type BelegenderTermin, type BereichKapazitaet, } from './belegung' // Dienstag, 17. März 2026. Berlin liegt dann noch auf UTC+1. const dienstag = new Date('2026-03-17T09:00:00Z') const bereich: BereichKapazitaet = { kapazitaetMo: 360, kapazitaetDi: 240, kapazitaetMi: 360, kapazitaetDo: 360, kapazitaetFr: 120, kapazitaetSa: 0, kapazitaetSo: 0, // 08:00 bis 17:00 Berliner Zeit. arbeitsfensterVon: 480, arbeitsfensterBis: 1020, } function termin( vonUtc: string, bisUtc: string, teil: Partial = {}, ): BelegenderTermin { return { beginn: new Date(vonUtc), ende: new Date(bisUtc), ganztaegig: false, zeigtAls: 'GEBUCHT', ...teil, } } describe('belegtZeit', () => { it('zählt einen gewöhnlichen Termin', () => { expect(belegtZeit(termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z'))).toBe(true) }) it('zählt ganztägige Termine nicht', () => { // Ein Urlaubstag oder Geburtstag kostet keine Arbeitszeit im Fenster. expect( belegtZeit( termin('2026-03-17T00:00:00Z', '2026-03-18T00:00:00Z', { ganztaegig: true }), ), ).toBe(false) }) it('zählt als frei markierte Termine nicht', () => { expect( belegtZeit( termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z', { zeigtAls: 'FREI' }), ), ).toBe(false) }) it('zählt Vorbehalt und Abwesenheit sehr wohl', () => { for (const zeigtAls of [ 'MIT_VORBEHALT', 'ABWESEND', 'WOANDERS', 'UNBEKANNT', ] as const) { expect( belegtZeit(termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z', { zeigtAls })), ).toBe(true) } }) }) describe('ueberschneidungMinuten', () => { const fenster = { von: new Date('2026-03-17T08:00:00Z'), bis: new Date('2026-03-17T12:00:00Z'), } it('rechnet eine vollständige Überschneidung', () => { expect( ueberschneidungMinuten( { von: new Date('2026-03-17T09:00:00Z'), bis: new Date('2026-03-17T10:00:00Z') }, fenster, ), ).toBe(60) }) it('schneidet an den Rändern ab', () => { expect( ueberschneidungMinuten( { von: new Date('2026-03-17T07:00:00Z'), bis: new Date('2026-03-17T09:00:00Z') }, fenster, ), ).toBe(60) }) it('ist ohne Überschneidung null', () => { expect( ueberschneidungMinuten( { von: new Date('2026-03-17T13:00:00Z'), bis: new Date('2026-03-17T14:00:00Z') }, fenster, ), ).toBe(0) }) it('zählt reines Aneinandergrenzen nicht als Überschneidung', () => { expect( ueberschneidungMinuten( { von: new Date('2026-03-17T12:00:00Z'), bis: new Date('2026-03-17T13:00:00Z') }, fenster, ), ).toBe(0) }) }) describe('arbeitsfensterAmTag', () => { it('legt das Fenster in Berliner Ortszeit', () => { const fenster = arbeitsfensterAmTag(dienstag, 480, 1020) // 08:00 Berlin = 07:00 UTC in der Winterzeit. expect(fenster.von.toISOString()).toBe('2026-03-17T07:00:00.000Z') expect(fenster.bis.toISOString()).toBe('2026-03-17T16:00:00.000Z') }) it('verschiebt sich mit der Sommerzeit', () => { // Ab dem 29. März 2026 gilt in Berlin UTC+2. const fenster = arbeitsfensterAmTag(new Date('2026-04-07T09:00:00Z'), 480, 1020) expect(fenster.von.toISOString()).toBe('2026-04-07T06:00:00.000Z') expect(fenster.bis.toISOString()).toBe('2026-04-07T15:00:00.000Z') }) }) describe('kapazitaetAmTag', () => { it('nimmt den richtigen Wochentag', () => { expect(kapazitaetAmTag(bereich, dienstag)).toBe(240) // Montag, 16. März. expect(kapazitaetAmTag(bereich, new Date('2026-03-16T09:00:00Z'))).toBe(360) // Sonntag, 22. März. expect(kapazitaetAmTag(bereich, new Date('2026-03-22T09:00:00Z'))).toBe(0) }) }) describe('belegteMinuten', () => { const fenster = arbeitsfensterAmTag(dienstag, 480, 1020) it('ist ohne Termine null', () => { expect(belegteMinuten([], fenster)).toBe(0) }) it('summiert getrennte Termine', () => { expect( belegteMinuten( [ termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z'), termin('2026-03-17T10:00:00Z', '2026-03-17T11:00:00Z'), ], fenster, ), ).toBe(120) }) it('legt überlappende Termine zusammen, statt doppelt zu zählen', () => { expect( belegteMinuten( [ termin('2026-03-17T08:00:00Z', '2026-03-17T10:00:00Z'), termin('2026-03-17T09:00:00Z', '2026-03-17T11:00:00Z'), ], fenster, ), ).toBe(180) }) it('legt lückenlos aneinandergrenzende Termine zusammen', () => { expect( belegteMinuten( [ termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z'), termin('2026-03-17T09:00:00Z', '2026-03-17T10:00:00Z'), ], fenster, ), ).toBe(120) }) it('zählt einen vollständig enthaltenen Termin nur einmal', () => { expect( belegteMinuten( [ termin('2026-03-17T08:00:00Z', '2026-03-17T12:00:00Z'), termin('2026-03-17T09:00:00Z', '2026-03-17T10:00:00Z'), ], fenster, ), ).toBe(240) }) it('schneidet an den Rändern des Arbeitsfensters ab', () => { // 06:00–08:00 UTC = 07:00–09:00 Berlin; nur die zweite Stunde zählt. expect( belegteMinuten([termin('2026-03-17T06:00:00Z', '2026-03-17T08:00:00Z')], fenster), ).toBe(60) }) it('lässt Termine außerhalb des Fensters ganz weg', () => { // 20:00 Berlin, das Fenster endet um 17:00. expect( belegteMinuten([termin('2026-03-17T19:00:00Z', '2026-03-17T20:00:00Z')], fenster), ).toBe(0) }) it('lässt ganztägige und freie Termine weg', () => { expect( belegteMinuten( [ termin('2026-03-17T08:00:00Z', '2026-03-17T10:00:00Z', { ganztaegig: true }), termin('2026-03-17T10:00:00Z', '2026-03-17T11:00:00Z', { zeigtAls: 'FREI' }), ], fenster, ), ).toBe(0) }) }) describe('tagesBelegung', () => { it('rechnet Kapazität und Termine gegeneinander', () => { const belegung = tagesBelegung( bereich, [termin('2026-03-17T08:00:00Z', '2026-03-17T10:00:00Z')], dienstag, ) expect(belegung.kapazitaet).toBe(240) expect(belegung.belegt).toBe(120) expect(belegung.frei).toBe(120) expect(belegung.prozent).toBe(50) expect(belegung.ueberbucht).toBe(false) }) it('erkennt einen überbuchten Tag', () => { const belegung = tagesBelegung( bereich, [termin('2026-03-17T07:00:00Z', '2026-03-17T13:00:00Z')], dienstag, ) expect(belegung.belegt).toBe(360) expect(belegung.frei).toBe(0) expect(belegung.ueberbucht).toBe(true) expect(belegung.prozent).toBe(150) }) it('behandelt einen Tag ohne Kapazität als voll', () => { // Sonntag: keine Kapazität, also gibt es dort nichts zu planen. const belegung = tagesBelegung(bereich, [], new Date('2026-03-22T09:00:00Z')) expect(belegung.kapazitaet).toBe(0) expect(belegung.prozent).toBe(100) expect(belegung.frei).toBe(0) }) }) describe('freieFenster', () => { const fenster = arbeitsfensterAmTag(dienstag, 480, 1020) it('gibt ohne Termine das ganze Arbeitsfenster zurück', () => { const luecken = freieFenster([], fenster) expect(luecken).toHaveLength(1) expect(luecken[0]?.von.toISOString()).toBe('2026-03-17T07:00:00.000Z') expect(luecken[0]?.bis.toISOString()).toBe('2026-03-17T16:00:00.000Z') }) it('findet die Lücke zwischen zwei Terminen', () => { const luecken = freieFenster( [ termin('2026-03-17T07:00:00Z', '2026-03-17T09:00:00Z'), termin('2026-03-17T11:00:00Z', '2026-03-17T16:00:00Z'), ], fenster, ) expect(luecken).toHaveLength(1) expect(luecken[0]?.von.toISOString()).toBe('2026-03-17T09:00:00.000Z') expect(luecken[0]?.bis.toISOString()).toBe('2026-03-17T11:00:00.000Z') }) it('lässt zu kurze Lücken weg', () => { const luecken = freieFenster( [ termin('2026-03-17T07:00:00Z', '2026-03-17T09:00:00Z'), // Nur zehn Minuten Pause. termin('2026-03-17T09:10:00Z', '2026-03-17T16:00:00Z'), ], fenster, 15, ) expect(luecken).toEqual([]) }) it('ist bei durchgehender Belegung leer', () => { expect( freieFenster([termin('2026-03-17T07:00:00Z', '2026-03-17T16:00:00Z')], fenster), ).toEqual([]) }) it('übergeht ganztägige Termine und findet den Tag frei', () => { const luecken = freieFenster( [termin('2026-03-17T00:00:00Z', '2026-03-18T00:00:00Z', { ganztaegig: true })], fenster, ) expect(luecken).toHaveLength(1) }) }) describe('termineAmTag', () => { it('nimmt, was den Berliner Kalendertag berührt', () => { const liste = [ termin('2026-03-17T08:00:00Z', '2026-03-17T09:00:00Z'), termin('2026-03-18T08:00:00Z', '2026-03-18T09:00:00Z'), ] expect(termineAmTag(liste, dienstag)).toHaveLength(1) }) it('nimmt auch einen Termin, der in den Tag hineinragt', () => { const uebernacht = termin('2026-03-16T22:00:00Z', '2026-03-17T02:00:00Z') expect(termineAmTag([uebernacht], dienstag)).toHaveLength(1) }) it('lässt einen Termin weg, der genau am Tagesbeginn endet', () => { // 23:00 UTC am 16. = 00:00 Berlin am 17., also gerade noch der Vortag. const davor = termin('2026-03-16T21:00:00Z', '2026-03-16T23:00:00Z') expect(termineAmTag([davor], dienstag)).toEqual([]) }) })