Öffnungszeiten: Hinweise, JSON-LD, Google-Text und Erinnerungstage
Reine Funktionen in zeitlogik.js: hinweisZeitraum/gueltigeHinweise (Berliner Kalendertage, Rang, Beginn in 8 Tagen), jsonLdFuer (Mitternachtsfenster, rund 00:00–23:59, Sonderzeiten der nächsten 60 Tage, neuerBlock mit @context), googleText, erinnerungsTage und imErinnerungsfenster (08:00–20:00 Berlin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
59b0ea799d
commit
61c2f72631
2 files changed
+673
-2
No files matched your search
@@ -11,6 +11,13 @@ import {
|
||||
statusZu,
|
||||
wechselListe,
|
||||
tageListe,
|
||||
HINWEIS_RANG,
|
||||
hinweisZeitraum,
|
||||
gueltigeHinweise,
|
||||
jsonLdFuer,
|
||||
googleText,
|
||||
erinnerungsTage,
|
||||
imErinnerungsfenster,
|
||||
} from '../server/services/zeiten/zeitlogik.js'
|
||||
import { ausUtc, zuUtc } from '../server/services/termine/zeit.js'
|
||||
|
||||
@@ -27,13 +34,13 @@ function plan(woche = {}, extra = {}) {
|
||||
return { id: 'p1', kurz: 'standard', name: 'Standard', haupt: true, tage, ...extra }
|
||||
}
|
||||
|
||||
function daten({ land = 'RP', plaene = [plan({ 1: ['09:00', '18:00'] })], ausnahmen = [], feiertage = {} } = {}) {
|
||||
function daten({ land = 'RP', plaene = [plan({ 1: ['09:00', '18:00'] })], ausnahmen = [], feiertage = {}, hinweise = [] } = {}) {
|
||||
return {
|
||||
land,
|
||||
plaene,
|
||||
feiertage,
|
||||
ausnahmen,
|
||||
hinweise: [],
|
||||
hinweise,
|
||||
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
|
||||
}
|
||||
}
|
||||
@@ -1003,6 +1010,436 @@ test('tageListe: 60 Tage über den Jahreswechsel, Randfälle und Fehler', () =>
|
||||
assert.deepEqual(p.tage[1].fenster, [['09:00', '18:00']])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Hinweise (Task 6)
|
||||
|
||||
const hinweis = (id, art, von, bis, extra) => ({ id, art, text: `Text ${id}`, knopf: null, von, bis, ...extra })
|
||||
|
||||
test('HINWEIS_RANG: Wichtig > Urlaub > Aktion > Info, eingefroren', () => {
|
||||
assert.deepEqual(HINWEIS_RANG, { wichtig: 4, urlaub: 3, aktion: 2, info: 1 })
|
||||
assert.ok(Object.isFrozen(HINWEIS_RANG))
|
||||
})
|
||||
|
||||
test('Hinweis-Zeitraum in Berliner Zeit über die Zeitumstellung', () => {
|
||||
const iso = (ms) => new Date(ms).toISOString()
|
||||
// Oktober: Ende der Sommerzeit am 25.10. (Tag hat 25 Stunden)
|
||||
const z = hinweisZeitraum({ von: '2026-10-24', bis: '2026-10-25' })
|
||||
assert.equal(iso(z.ab), '2026-10-23T22:00:00.000Z')
|
||||
assert.equal(iso(z.bis), '2026-10-25T23:00:00.000Z')
|
||||
// März: Beginn der Sommerzeit am 29.03.
|
||||
const m = hinweisZeitraum({ von: '2026-03-28', bis: '2026-03-29' })
|
||||
assert.equal(iso(m.ab), '2026-03-27T23:00:00.000Z')
|
||||
assert.equal(iso(m.bis), '2026-03-29T22:00:00.000Z')
|
||||
// ein einzelner Normaltag: von 00:00 bis 24:00
|
||||
const e = hinweisZeitraum({ von: '2026-10-05', bis: '2026-10-05' })
|
||||
assert.equal(iso(e.ab), '2026-10-04T22:00:00.000Z')
|
||||
assert.equal(iso(e.bis), '2026-10-05T22:00:00.000Z')
|
||||
})
|
||||
|
||||
test('Hinweis-Zeitraum: fehlendes von/bis = unbegrenzt (als Zahl), kaputte Tage = null', () => {
|
||||
const ENDLOS = 8.64e15
|
||||
assert.deepEqual(hinweisZeitraum({}), { ab: 0, bis: ENDLOS })
|
||||
assert.deepEqual(hinweisZeitraum({ von: null, bis: '' }), { ab: 0, bis: ENDLOS })
|
||||
assert.equal(new Date(hinweisZeitraum({ von: '2026-10-05' }).ab).toISOString(), '2026-10-04T22:00:00.000Z')
|
||||
assert.equal(hinweisZeitraum({ von: '2026-10-05' }).bis, ENDLOS)
|
||||
assert.equal(hinweisZeitraum({ bis: '2026-10-05' }).ab, 0)
|
||||
assert.ok(Number.isFinite(new Date(ENDLOS).getTime()), 'ENDLOS ist noch ein gültiges Date')
|
||||
for (const kaputt of [{ von: '2026-02-30' }, { von: 'morgen' }, { bis: '05.10.2026' }, { von: 20261005 }, null, undefined, 'x']) {
|
||||
assert.equal(hinweisZeitraum(kaputt), null, JSON.stringify(kaputt))
|
||||
}
|
||||
})
|
||||
|
||||
test('gueltigeHinweise: vergangene fehlen, künftige ≤ 8 Tage sind dabei, Rang gesetzt', () => {
|
||||
const d = daten({
|
||||
hinweise: [
|
||||
hinweis('alt', 'info', '2026-09-20', '2026-10-04'), // endete gestern 24:00
|
||||
hinweis('heute', 'aktion', '2026-10-01', '2026-10-05'), // läuft bis Ende heute
|
||||
hinweis('kommt', 'urlaub', '2026-10-13', '2026-10-26', { knopf: { art: 'link', ziel: 'https://example.org/x', text: 'Mehr' } }),
|
||||
hinweis('spaet', 'wichtig', '2026-10-14', '2026-10-15'), // Beginn in 8 Tagen 14 Stunden
|
||||
hinweis('offen', 'info', '2026-10-01', undefined), // ohne Ende
|
||||
],
|
||||
})
|
||||
const liste = gueltigeHinweise(d, JETZT)
|
||||
assert.deepEqual(liste.map((h) => h.text), ['Text heute', 'Text kommt', 'Text offen'], 'gespeicherte Reihenfolge')
|
||||
assert.deepEqual(liste[1], {
|
||||
art: 'urlaub',
|
||||
text: 'Text kommt',
|
||||
knopf: { art: 'link', ziel: 'https://example.org/x', text: 'Mehr' },
|
||||
ab: Date.parse('2026-10-12T22:00:00Z'),
|
||||
bis: Date.parse('2026-10-26T23:00:00Z'),
|
||||
rang: 3,
|
||||
})
|
||||
assert.deepEqual(liste.map((h) => h.rang), [2, 3, 1])
|
||||
assert.deepEqual(Object.keys(liste[0]), ['art', 'text', 'knopf', 'ab', 'bis', 'rang'], 'keine internen IDs')
|
||||
assert.equal(liste[0].knopf, null)
|
||||
assert.equal(liste[2].bis, 8.64e15)
|
||||
// der Knopf ist eine Kopie
|
||||
assert.notEqual(liste[1].knopf, d.hinweise[2].knopf)
|
||||
})
|
||||
|
||||
test('gueltigeHinweise: Beginn genau 8 Tage voraus zählt, eine Minute später nicht', () => {
|
||||
const d = daten({ hinweise: [hinweis('x', 'info', '2026-10-13', '2026-10-13')] })
|
||||
// 05.10. 00:00 Berlin + 8 Tage = 13.10. 00:00 = Beginn des Hinweises
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-04T22:00:00Z')).length, 1)
|
||||
// 04.10. 23:59 Berlin + 8 Tage = 12.10. 23:59, der Hinweis beginnt eine Minute später
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-04T21:59:00Z')).length, 0)
|
||||
// innerhalb des Tages und nach dem Ende
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-13T21:59:00Z')).length, 1, 'bis 13.10. 23:59 gültig')
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-13T22:00:00Z')).length, 0, 'ab 14.10. 00:00 vorbei')
|
||||
})
|
||||
|
||||
test('gueltigeHinweise: Tag rechnet in Berliner Zeit (Mitternacht über die Zeitumstellung)', () => {
|
||||
const d = daten({ hinweise: [hinweis('x', 'wichtig', '2026-10-24', '2026-10-25')] })
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-25T22:59:00Z')).length, 1, '25.10. 23:59 CET… noch gültig')
|
||||
assert.equal(gueltigeHinweise(d, new Date('2026-10-25T23:00:00Z')).length, 0, '26.10. 00:00 Berlin: vorbei')
|
||||
})
|
||||
|
||||
test('gueltigeHinweise: kaputte Einträge werden übersprungen, nichts wird verändert', () => {
|
||||
const d = daten({
|
||||
hinweise: [
|
||||
null,
|
||||
'text',
|
||||
hinweis('ohnetext', 'info', '2026-10-01', '2026-10-31', { text: ' ' }),
|
||||
hinweis('textzahl', 'info', '2026-10-01', '2026-10-31', { text: 5 }),
|
||||
hinweis('artfalsch', 'dringend', '2026-10-01', '2026-10-31'),
|
||||
hinweis('artproto', 'constructor', '2026-10-01', '2026-10-31'),
|
||||
hinweis('tagkaputt', 'info', '2026-02-30', '2026-10-31'),
|
||||
hinweis('umgekehrt', 'info', '2026-10-20', '2026-10-10'),
|
||||
hinweis('knopfkaputt', 'info', '2026-10-01', '2026-10-31', { knopf: { art: 'script', ziel: 'x', text: 'y' } }),
|
||||
hinweis('gut', 'wichtig', '2026-10-01', '2026-10-31'),
|
||||
],
|
||||
})
|
||||
tiefEinfrieren(d)
|
||||
const liste = gueltigeHinweise(d, JETZT)
|
||||
assert.deepEqual(liste.map((h) => h.text), ['Text knopfkaputt', 'Text gut'])
|
||||
assert.equal(liste[0].knopf, null, 'unbekannte Knopfart wird verworfen, der Hinweis bleibt')
|
||||
assert.deepEqual(gueltigeHinweise({}, JETZT), [])
|
||||
assert.deepEqual(gueltigeHinweise(null, JETZT), [])
|
||||
assert.deepEqual(gueltigeHinweise({ hinweise: 'kaputt' }, JETZT), [])
|
||||
assert.throws(() => gueltigeHinweise(d, new Date('x')), RangeError)
|
||||
assert.throws(() => gueltigeHinweise(d, null), RangeError)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- JSON-LD (Task 6)
|
||||
|
||||
const LD = (extra) => ({ '@type': 'OpeningHoursSpecification', ...extra })
|
||||
|
||||
test('JSON-LD: Mitternachtsfenster mit closes < opens', () => {
|
||||
const d = daten({ plaene: [plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 5: ['10:00', '24:00'], 6: ['18:00', '02:00'] })] })
|
||||
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
|
||||
assert.deepEqual(ld.openingHoursSpecification, [
|
||||
LD({ dayOfWeek: 'Monday', opens: '09:00', closes: '12:30' }),
|
||||
LD({ dayOfWeek: 'Monday', opens: '13:30', closes: '18:00' }),
|
||||
LD({ dayOfWeek: 'Friday', opens: '10:00', closes: '00:00' }),
|
||||
LD({ dayOfWeek: 'Saturday', opens: '18:00', closes: '02:00' }),
|
||||
])
|
||||
// nie '24:00' im JSON-LD
|
||||
assert.doesNotMatch(JSON.stringify(ld), /24:00/)
|
||||
})
|
||||
|
||||
test('JSON-LD: rund um die Uhr 00:00–23:59, nach Vereinbarung fehlt', () => {
|
||||
const d = daten({ plaene: [plan({ 1: 'rund', 2: 'vereinbarung', 3: ['00:00', '24:00'], 4: ['09:00', '18:00'] })] })
|
||||
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
|
||||
assert.deepEqual(ld.openingHoursSpecification, [
|
||||
LD({ dayOfWeek: 'Monday', opens: '00:00', closes: '23:59' }),
|
||||
LD({ dayOfWeek: 'Wednesday', opens: '00:00', closes: '23:59' }), // Fenster 00:00–24:00 = rund
|
||||
LD({ dayOfWeek: 'Thursday', opens: '09:00', closes: '18:00' }),
|
||||
])
|
||||
assert.ok(!ld.openingHoursSpecification.some((e) => e.dayOfWeek === 'Tuesday'))
|
||||
// Tage ohne Öffnung (zu, fehlend) kommen nicht vor
|
||||
const leer = jsonLdFuer(daten({ plaene: [plan({})] }), plan({}), JETZT)
|
||||
assert.deepEqual(leer.openingHoursSpecification, [])
|
||||
})
|
||||
|
||||
test('JSON-LD: Urlaub als Sonderzeit geschlossen 00:00–00:00 mit validFrom/validThrough', () => {
|
||||
const d = daten({ plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] })
|
||||
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
|
||||
// ein Eintrag für den ganzen Urlaub, dazu Allerheiligen (RP, So 01.11.) im Fenster der nächsten 60 Tage
|
||||
assert.deepEqual(ld.specialOpeningHoursSpecification, [
|
||||
LD({ validFrom: '2026-10-12', validThrough: '2026-10-16', opens: '00:00', closes: '00:00' }),
|
||||
LD({ validFrom: '2026-11-01', validThrough: '2026-11-01', opens: '00:00', closes: '00:00' }),
|
||||
])
|
||||
})
|
||||
|
||||
test('JSON-LD: Sonderzeiten – Sondertag mit Zeiten, rund, Vereinbarung als geschlossen', () => {
|
||||
const d = daten({
|
||||
land: 'BE',
|
||||
plaene: [plan(WOCHE_ALLE_9_18)],
|
||||
ausnahmen: [
|
||||
sondertag({ id: 's1', von: '2026-10-10', bis: '2026-10-10', fenster: [['10:00', '14:00'], ['18:00', '02:00']] }),
|
||||
sondertag({ id: 's2', von: '2026-10-20', bis: '2026-10-20', modus: 'rund', fenster: [] }),
|
||||
sondertag({ id: 's3', von: '2026-10-22', bis: '2026-10-22', modus: 'vereinbarung', fenster: [] }),
|
||||
],
|
||||
})
|
||||
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
|
||||
assert.deepEqual(ld.specialOpeningHoursSpecification, [
|
||||
LD({ validFrom: '2026-10-10', validThrough: '2026-10-10', opens: '10:00', closes: '14:00' }),
|
||||
LD({ validFrom: '2026-10-10', validThrough: '2026-10-10', opens: '18:00', closes: '02:00' }),
|
||||
LD({ validFrom: '2026-10-20', validThrough: '2026-10-20', opens: '00:00', closes: '23:59' }),
|
||||
// „nach Vereinbarung“ hat in JSON-LD keine Form; sonst blieben die Wochenzeiten des Tages stehen
|
||||
LD({ validFrom: '2026-10-22', validThrough: '2026-10-22', opens: '00:00', closes: '00:00' }),
|
||||
])
|
||||
})
|
||||
|
||||
test('JSON-LD: Sonderzeiten – nächste 60 Tage ab heute, gleiche aufeinanderfolgende Tage zusammengefasst', () => {
|
||||
const d = daten({
|
||||
land: 'BE', // BE: kein Feiertag zwischen 05.10. und 03.12.
|
||||
plaene: [plan(WOCHE_ALLE_9_18)],
|
||||
ausnahmen: [
|
||||
sondertag({ id: 'gestern', von: '2026-10-04', bis: '2026-10-04' }),
|
||||
urlaub({ id: 'laeuft', von: '2026-10-01', bis: '2026-10-07' }),
|
||||
urlaub({ id: 'a', von: '2026-10-12', bis: '2026-10-13' }),
|
||||
urlaub({ id: 'b', von: '2026-10-15', bis: '2026-10-16' }), // Lücke am 14.10.: eigener Eintrag
|
||||
urlaub({ id: 'letzter', von: '2026-12-03', bis: '2026-12-03' }), // heute + 59
|
||||
urlaub({ id: 'zuweit', von: '2026-12-04', bis: '2026-12-10' }), // heute + 60
|
||||
],
|
||||
})
|
||||
const spans = jsonLdFuer(d, hauptplan(d), JETZT).specialOpeningHoursSpecification.map((e) => [e.validFrom, e.validThrough])
|
||||
assert.deepEqual(spans, [
|
||||
['2026-10-05', '2026-10-07'], // läuft schon: ab heute
|
||||
['2026-10-12', '2026-10-13'],
|
||||
['2026-10-15', '2026-10-16'],
|
||||
['2026-12-03', '2026-12-03'],
|
||||
])
|
||||
})
|
||||
|
||||
test('JSON-LD: Urlaub und angrenzender Feiertag mit gleichen Zeiten werden ein Zeitraum', () => {
|
||||
const d = daten({ plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [urlaub({ von: '2026-10-30', bis: '2026-10-31' })] })
|
||||
const spans = jsonLdFuer(d, hauptplan(d), JETZT).specialOpeningHoursSpecification.map((e) => [e.validFrom, e.validThrough])
|
||||
assert.deepEqual(spans, [['2026-10-30', '2026-11-01']], 'Allerheiligen (RP) schließt direkt an')
|
||||
})
|
||||
|
||||
test('JSON-LD: neuerBlock mit @context vom Server, ohne name/address/url', () => {
|
||||
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] })
|
||||
const ld = jsonLdFuer(d, hauptplan(d), JETZT)
|
||||
assert.deepEqual(Object.keys(ld), ['openingHoursSpecification', 'specialOpeningHoursSpecification', 'neuerBlock'])
|
||||
assert.equal(ld.neuerBlock['@context'], 'https://schema.org')
|
||||
assert.equal(ld.neuerBlock['@type'], 'LocalBusiness')
|
||||
assert.deepEqual(Object.keys(ld.neuerBlock), ['@context', '@type', 'openingHoursSpecification', 'specialOpeningHoursSpecification'])
|
||||
assert.deepEqual(ld.neuerBlock.openingHoursSpecification, ld.openingHoursSpecification)
|
||||
assert.deepEqual(ld.neuerBlock.specialOpeningHoursSpecification, ld.specialOpeningHoursSpecification)
|
||||
for (const verboten of ['name', 'address', 'url']) assert.ok(!(verboten in ld.neuerBlock), verboten)
|
||||
// als JSON ausgeliefert: dieselbe Form nach dem Rundlauf
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(ld)), ld)
|
||||
})
|
||||
|
||||
test('JSON-LD: ohne Plan keine Angaben, Eingaben bleiben unverändert', () => {
|
||||
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: ['09:00', '18:00'], 6: ['18:00', '02:00'] })], ausnahmen: [urlaub({ von: '2026-10-12', bis: '2026-10-16' })] }))
|
||||
for (const kein of [null, undefined, 'x']) {
|
||||
const ld = jsonLdFuer(d, kein, JETZT)
|
||||
assert.deepEqual(ld.openingHoursSpecification, [])
|
||||
assert.deepEqual(ld.specialOpeningHoursSpecification, [])
|
||||
assert.equal(ld.neuerBlock['@context'], 'https://schema.org')
|
||||
}
|
||||
assert.deepEqual(jsonLdFuer(d, hauptplan(d), JETZT), jsonLdFuer(d, hauptplan(d), JETZT))
|
||||
assert.throws(() => jsonLdFuer(d, hauptplan(d), new Date('x')), RangeError)
|
||||
})
|
||||
|
||||
test('JSON-LD: heute ist der Berliner Tag (kurz nach Mitternacht Berlin ist noch UTC-Vortag)', () => {
|
||||
const d = daten({ land: 'BE', plaene: [plan(WOCHE_ALLE_9_18)], ausnahmen: [sondertag({ von: '2026-10-05', bis: '2026-10-05' })] })
|
||||
// 05.10. 00:30 Berlin = 04.10. 22:30 UTC: der 05.10. ist heute und noch dabei
|
||||
assert.equal(jsonLdFuer(d, hauptplan(d), new Date('2026-10-04T22:30:00Z')).specialOpeningHoursSpecification.length, 1)
|
||||
// 06.10. 00:30 Berlin: der 05.10. ist vorbei
|
||||
assert.equal(jsonLdFuer(d, hauptplan(d), new Date('2026-10-05T22:30:00Z')).specialOpeningHoursSpecification.length, 0)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Google-Text (Task 6)
|
||||
|
||||
test('Google-Text: Montag zuerst, „geschlossen“, Sonderzeiten danach', () => {
|
||||
const d = daten({
|
||||
plaene: [plan({ 1: [['09:00', '12:30'], ['13:30', '18:00']], 2: ['09:00', '18:00'], 3: 'rund', 4: 'vereinbarung', 5: ['10:00', '24:00'], 6: ['18:00', '02:00'] })],
|
||||
ausnahmen: [
|
||||
urlaub({ id: 'u', von: '2026-10-12', bis: '2026-10-16', text: 'Betriebsurlaub' }),
|
||||
sondertag({ id: 's', von: '2026-10-10', bis: '2026-10-10', text: 'Tag der offenen Tür', fenster: [['10:00', '14:00']] }),
|
||||
],
|
||||
})
|
||||
const text = googleText(d, hauptplan(d), JETZT)
|
||||
assert.ok(text.startsWith('Montag: 09:00–12:30, 13:30–18:00\n'))
|
||||
assert.equal(
|
||||
text,
|
||||
[
|
||||
'Montag: 09:00–12:30, 13:30–18:00',
|
||||
'Dienstag: 09:00–18:00',
|
||||
'Mittwoch: rund um die Uhr',
|
||||
'Donnerstag: nach Vereinbarung',
|
||||
'Freitag: 10:00–00:00',
|
||||
'Samstag: 18:00–02:00',
|
||||
'Sonntag: geschlossen',
|
||||
'',
|
||||
'Sonderzeiten (nächste 60 Tage):',
|
||||
'10.10.: 10:00–14:00 (Tag der offenen Tür)',
|
||||
'12.10.–16.10.: geschlossen (Betriebsurlaub)',
|
||||
'01.11.: geschlossen (Allerheiligen)',
|
||||
].join('\n'),
|
||||
)
|
||||
assert.doesNotMatch(text, /24:00|24 Uhr/)
|
||||
})
|
||||
|
||||
test('Google-Text: ohne Sonderzeiten genau sieben Zeilen, ohne Anhängsel', () => {
|
||||
const d = daten({ land: 'BE', plaene: [plan({ 1: ['09:00', '18:00'] })] })
|
||||
const text = googleText(d, hauptplan(d), JETZT)
|
||||
assert.deepEqual(text.split('\n'), [
|
||||
'Montag: 09:00–18:00',
|
||||
'Dienstag: geschlossen',
|
||||
'Mittwoch: geschlossen',
|
||||
'Donnerstag: geschlossen',
|
||||
'Freitag: geschlossen',
|
||||
'Samstag: geschlossen',
|
||||
'Sonntag: geschlossen',
|
||||
])
|
||||
})
|
||||
|
||||
test('Google-Text: Sonderzeiten fassen gleiche Tage mit gleichem Grund zusammen, Zeiten statt „geschlossen“', () => {
|
||||
const d = daten({
|
||||
land: 'BE',
|
||||
plaene: [plan(WOCHE_ALLE_9_18)],
|
||||
ausnahmen: [
|
||||
urlaub({ id: 'u1', von: '2026-10-12', bis: '2026-10-14', text: 'Betriebsferien' }),
|
||||
urlaub({ id: 'u2', von: '2026-10-15', bis: '2026-10-15', text: 'Inventur' }), // anderer Grund: eigene Zeile
|
||||
urlaub({ id: 'u3', von: '2026-10-20', bis: '2026-10-20', text: '' }), // ohne Text: Vorgabe „Betriebsurlaub“
|
||||
sondertag({ id: 's1', von: '2026-10-24', bis: '2026-10-25', text: '', modus: 'zeiten', fenster: [['10:00', '12:00'], ['14:00', '24:00']] }),
|
||||
sondertag({ id: 's2', von: '2026-10-26', bis: '2026-10-26', text: 'Messe', modus: 'rund', fenster: [] }),
|
||||
sondertag({ id: 's3', von: '2026-10-27', bis: '2026-10-27', text: 'Fortbildung', modus: 'vereinbarung', fenster: [] }),
|
||||
// heute und das Ende des Fensters: 05.10. + 59 = 03.12.
|
||||
sondertag({ id: 's4', von: '2026-10-05', bis: '2026-10-05', text: 'Heute', fenster: [['12:00', '13:00']] }),
|
||||
sondertag({ id: 's5', von: '2026-12-03', bis: '2026-12-04', text: 'Ende', fenster: [['12:00', '13:00']] }),
|
||||
sondertag({ id: 's6', von: '2026-10-04', bis: '2026-10-04', text: 'Gestern', fenster: [['12:00', '13:00']] }),
|
||||
],
|
||||
})
|
||||
const zeilen = googleText(d, hauptplan(d), JETZT).split('\n').slice(9)
|
||||
assert.deepEqual(zeilen, [
|
||||
'05.10.: 12:00–13:00 (Heute)',
|
||||
'12.10.–14.10.: geschlossen (Betriebsferien)',
|
||||
'15.10.: geschlossen (Inventur)',
|
||||
'20.10.: geschlossen (Betriebsurlaub)',
|
||||
'24.10.–25.10.: 10:00–12:00, 14:00–00:00',
|
||||
'26.10.: rund um die Uhr (Messe)',
|
||||
'27.10.: nach Vereinbarung (Fortbildung)',
|
||||
'03.12.: 12:00–13:00 (Ende)',
|
||||
])
|
||||
})
|
||||
|
||||
test('Google-Text: Fenster 00:00–24:00 heißt „rund um die Uhr“, ohne Plan leer, Eingaben unverändert', () => {
|
||||
const d = tiefEinfrieren(daten({ plaene: [plan({ 1: ['00:00', '24:00'] })] }))
|
||||
assert.equal(googleText(d, hauptplan(d), JETZT).split('\n')[0], 'Montag: rund um die Uhr')
|
||||
assert.equal(googleText(d, null, JETZT), '')
|
||||
assert.equal(googleText(d, undefined, JETZT), '')
|
||||
assert.throws(() => googleText(d, hauptplan(d), new Date('x')), RangeError)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Erinnerungstage (Task 6)
|
||||
|
||||
// Mo 28.09.2026 10:00 Berlin: 1–7 Tage = 29.09. bis 05.10. (Sa 03.10. = Tag der Deutschen Einheit)
|
||||
const MO_28_09 = new Date('2026-09-28T08:00:00Z')
|
||||
const MIT_SAMSTAG = { 1: ['09:00', '18:00'], 6: ['09:00', '14:00'] }
|
||||
|
||||
test('Erinnerung: Feiertag in 1–7 Tagen, offener Wochentag', () => {
|
||||
const d = daten({ plaene: [plan(MIT_SAMSTAG)] })
|
||||
assert.deepEqual(erinnerungsTage(d, MO_28_09), [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }])
|
||||
})
|
||||
|
||||
test('Erinnerung: Grenzen 1 bis 7 Tage in Berliner Tagen', () => {
|
||||
const d = daten({ plaene: [plan(MIT_SAMSTAG)] })
|
||||
const tage = (iso) => erinnerungsTage(d, new Date(iso)).map((e) => e.tag)
|
||||
assert.deepEqual(tage('2026-09-26T08:00:00Z'), ['2026-10-03'], 'in 7 Tagen (Sa 26.09. + 7)')
|
||||
assert.deepEqual(tage('2026-09-25T08:00:00Z'), [], 'in 8 Tagen: noch nicht')
|
||||
assert.deepEqual(tage('2026-10-02T08:00:00Z'), ['2026-10-03'], 'morgen')
|
||||
assert.deepEqual(tage('2026-10-03T08:00:00Z'), [], 'heute: zu spät')
|
||||
// 02.10. 23:30 Berlin (21:30 UTC): der Feiertag ist morgen; 03.10. 00:30 Berlin (22:30 UTC Vortag): heute
|
||||
assert.deepEqual(tage('2026-10-02T21:30:00Z'), ['2026-10-03'])
|
||||
assert.deepEqual(tage('2026-10-02T22:30:00Z'), [])
|
||||
})
|
||||
|
||||
test('Erinnerung: nicht bei geschlossenem Tag, Ausnahme oder gespeicherter Abweichung', () => {
|
||||
const jetzt = MO_28_09
|
||||
// Samstag im Hauptplan ohnehin zu
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [plan({ 1: ['09:00', '18:00'] })] }), jetzt), [])
|
||||
// Ausnahme deckt den Tag ab: Urlaub, Sondertag (global oder für den Hauptplan)
|
||||
const offen = plan(MIT_SAMSTAG)
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [urlaub({ von: '2026-10-01', bis: '2026-10-05' })] }), jetzt), [])
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03' })] }), jetzt), [])
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03', plan: 'p1' })] }), jetzt), [])
|
||||
// eine Ausnahme nur für einen anderen Plan deckt den Hauptplan nicht ab
|
||||
const anderer = plan({}, { id: 'p2', kurz: 'kueche', haupt: false })
|
||||
assert.equal(
|
||||
erinnerungsTage(daten({ plaene: [offen, anderer], ausnahmen: [sondertag({ von: '2026-10-03', bis: '2026-10-03', plan: 'p2' })] }), jetzt).length,
|
||||
1,
|
||||
)
|
||||
// Ausnahme eines anderen Tages stört nicht
|
||||
assert.equal(erinnerungsTage(daten({ plaene: [offen], ausnahmen: [urlaub({ von: '2026-10-04', bis: '2026-10-10' })] }), jetzt).length, 1)
|
||||
// gespeicherte Abweichung für den Tag (egal welcher Modus)
|
||||
for (const abw of [{ modus: 'zu', fenster: [] }, { modus: 'zeiten', fenster: [['10:00', '12:00']] }, { modus: 'vereinbarung', fenster: [] }]) {
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [offen], feiertage: { '2026-10-03': abw } }), jetzt), [], abw.modus)
|
||||
}
|
||||
// Abweichung für einen anderen Tag zählt nicht
|
||||
assert.equal(erinnerungsTage(daten({ plaene: [offen], feiertage: { '2026-10-04': { modus: 'zu', fenster: [] } } }), jetzt).length, 1)
|
||||
})
|
||||
|
||||
test('Erinnerung: Wochentag nach Vereinbarung wird erinnert, ohne Hauptplan nicht', () => {
|
||||
const ver = daten({ plaene: [plan({ 6: 'vereinbarung' })] })
|
||||
assert.equal(erinnerungsTage(ver, MO_28_09).length, 1)
|
||||
const rund = daten({ plaene: [plan({ 6: 'rund' })] })
|
||||
assert.equal(erinnerungsTage(rund, MO_28_09).length, 1)
|
||||
// kein Hauptplan (noch nichts gespeichert, nur Nebenpläne): keine Erinnerung
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [] }), MO_28_09), [])
|
||||
assert.deepEqual(erinnerungsTage(daten({ plaene: [plan(MIT_SAMSTAG, { haupt: false })] }), MO_28_09), [])
|
||||
assert.deepEqual(erinnerungsTage({}, MO_28_09), [])
|
||||
assert.deepEqual(erinnerungsTage(null, MO_28_09), [])
|
||||
})
|
||||
|
||||
test('Erinnerung: Heiligabend als „pruefen“', () => {
|
||||
// Mo 21.12.2026: 22.–28.12. enthält Do 24.12. (Heiligabend), 25./26.12. (1./2. Weihnachtstag)
|
||||
const d = daten({ plaene: [plan({ 1: ['09:00', '18:00'], 4: ['09:00', '18:00'], 5: ['09:00', '18:00'], 6: ['09:00', '14:00'] })] })
|
||||
const liste = erinnerungsTage(d, new Date('2026-12-21T08:00:00Z'))
|
||||
assert.deepEqual(liste, [
|
||||
{ tag: '2026-12-24', name: 'Heiligabend', art: 'pruefen' },
|
||||
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
|
||||
{ tag: '2026-12-26', name: '2. Weihnachtstag', art: 'feiertag' },
|
||||
])
|
||||
// Silvester und Neujahr über den Jahreswechsel
|
||||
const s = erinnerungsTage(d, new Date('2026-12-28T08:00:00Z'))
|
||||
assert.deepEqual(s.map((e) => [e.tag, e.art]), [['2026-12-31', 'pruefen'], ['2027-01-01', 'feiertag']])
|
||||
})
|
||||
|
||||
test('Erinnerung: Bundesland aus den Daten, Prüftage regional (Mariä Himmelfahrt in BY)', () => {
|
||||
// 15.08.2026 ist ein Samstag; Mo 10.08. + 5
|
||||
const woche = { 6: ['09:00', '14:00'] }
|
||||
const by = erinnerungsTage(daten({ land: 'BY', plaene: [plan(woche)] }), new Date('2026-08-10T08:00:00Z'))
|
||||
assert.deepEqual(by, [{ tag: '2026-08-15', name: 'Mariä Himmelfahrt', art: 'pruefen' }])
|
||||
assert.deepEqual(erinnerungsTage(daten({ land: 'BE', plaene: [plan(woche)] }), new Date('2026-08-10T08:00:00Z')), [])
|
||||
// unbekanntes Land: Vorgabe RP (dort 03.10. Feiertag)
|
||||
assert.equal(erinnerungsTage(daten({ land: 'XX', plaene: [plan(MIT_SAMSTAG)] }), MO_28_09).length, 1)
|
||||
})
|
||||
|
||||
test('Erinnerung: Eingaben bleiben unverändert, ungültiges now wirft', () => {
|
||||
const d = tiefEinfrieren(daten({ plaene: [plan(MIT_SAMSTAG)] }))
|
||||
assert.equal(erinnerungsTage(d, MO_28_09).length, 1)
|
||||
assert.throws(() => erinnerungsTage(d, new Date('x')), RangeError)
|
||||
assert.throws(() => erinnerungsTage(d, undefined), RangeError)
|
||||
})
|
||||
|
||||
test('Erinnerungsfenster 08:00–20:00 Berlin', () => {
|
||||
// Winter (MEZ = UTC+1): 08:00 Berlin = 07:00 UTC
|
||||
const winter = (hhmm) => imErinnerungsfenster(new Date(`2026-12-01T${hhmm}:00Z`))
|
||||
assert.equal(winter('06:59'), false) // 07:59 Berlin
|
||||
assert.equal(winter('07:00'), true) // 08:00
|
||||
assert.equal(winter('18:59'), true) // 19:59
|
||||
assert.equal(winter('19:00'), false) // 20:00
|
||||
// Sommer (MESZ = UTC+2): 08:00 Berlin = 06:00 UTC
|
||||
const sommer = (hhmm) => imErinnerungsfenster(new Date(`2026-07-01T${hhmm}:00Z`))
|
||||
assert.equal(sommer('05:59'), false)
|
||||
assert.equal(sommer('06:00'), true)
|
||||
assert.equal(sommer('17:59'), true)
|
||||
assert.equal(sommer('18:00'), false)
|
||||
// Sekunden zählen nicht über die Minute hinaus
|
||||
assert.equal(imErinnerungsfenster(new Date('2026-12-01T18:59:59Z')), true)
|
||||
assert.equal(imErinnerungsfenster(new Date('2026-12-01T06:59:59Z')), false)
|
||||
// Nacht und Mittag
|
||||
assert.equal(imErinnerungsfenster(new Date('2026-12-01T00:00:00Z')), false)
|
||||
assert.equal(imErinnerungsfenster(new Date('2026-12-01T11:00:00Z')), true)
|
||||
assert.throws(() => imErinnerungsfenster(new Date('x')), RangeError)
|
||||
assert.throws(() => imErinnerungsfenster(undefined), RangeError)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Reinheit
|
||||
|
||||
test('zeitlogik.js ist rein: keine Appwrite- und keine Config-Importe, keine Millisekunden-Tagesrechnung', () => {
|
||||
|
||||
Reference in new issue
Block a user