Terminplaner: Verfuegbarkeit, Abwesenheit schlaegt Sonderoeffnung, waehleRessource ignoriert abgelaufene Haltungen

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:43 +00:00
1 parent cce6e64dd1
commit 4def5c735d
2 files changed
+48 -10

No files matched your search

+11 -8
View File
@@ -7,10 +7,10 @@ import { heute, monatInfo, plusTage, wochentag, zuUtc } from './zeit.js'
* sind UTC-Zeitpunkte und werden nur als Zeitpunkte verglichen (nie je Kalendertag), damit eine Belegung vom Vortag
* (22:00 bis 06:00) und die Zeitumstellung stimmen.
*
* Rangfolge fuer die Fenster eines Tages: Sonderoeffnung > Abwesenheit > Feiertag > Plan.
* Eine Sonderoeffnung gilt fuer alle Ressourcen und ersetzt Plan, Feiertag und Abwesenheit (leere Liste = geschlossen).
* Sonst schliesst eine Abwesenheit (Betrieb: ressourceId null) die Ressource, dann ein Feiertag (wenn
* feiertageGeschlossen), sonst gilt der Wochenplan.
* Fenster eines Tages: Oeffnungsfenster aus dem Wochenplan der Ressource oder aus der Sonderoeffnung des Datums
* (gilt fuer alle Ressourcen, ersetzt Plan und Feiertagssperre; leere Liste = geschlossen), minus Feiertag (wenn
* feiertageGeschlossen und keine Sonderoeffnung), minus Abwesenheiten: eine Abwesenheit der Ressource oder des
* Betriebs (ressourceId null) nimmt den Tag der betroffenen Ressourcen immer heraus, auch an einem Sonderoeffnungstag.
*/
const MIN = 60000
@@ -184,8 +184,8 @@ function tagZeiten(ctx, tag, nurPruefen) {
const treffer = new Map() // ms -> Eintrag
for (const r of ctx.ressourcen) {
let fenster = sonder
if (fenster === null) fenster = feiertagZu || abwesend(e.abwesenheiten, r.id, tag) ? [] : (r.plan?.[wt] || [])
let fenster = []
if (!abwesend(e.abwesenheiten, r.id, tag)) fenster = sonder ?? (feiertagZu ? [] : (r.plan?.[wt] || []))
const belegtR = ctx.belegt.get(r.id) || []
for (const f of Array.isArray(fenster) ? fenster : []) {
const von = minuten(f?.[0])
@@ -240,14 +240,17 @@ export function freieTage(parameter) {
/**
* Wahl unter mehreren freien Ressourcen ('egal'): die mit den wenigsten Buchungen an dem Tag; bei Gleichstand die
* in der Einstellung zuerst genannte. Abgesagte, abgelehnte und verfallene Buchungen sowie Sperren zaehlen nicht.
* Keine Kandidaten => null.
* Mit now (optional) zaehlt eine Haltung nur, solange heldUntil > now. Keine Kandidaten => null.
*/
export function waehleRessource(ids, buchungenAmTag, einstellung) {
export function waehleRessource(ids, buchungenAmTag, einstellung, now = null) {
if (now !== null) pruefeNow(now, 'waehleRessource')
const jetztMs = now === null ? null : now.getTime()
const kandidaten = [...new Set((Array.isArray(ids) ? ids : []).filter((id) => typeof id === 'string'))]
if (kandidaten.length === 0) return null
const anzahl = new Map(kandidaten.map((id) => [id, 0]))
for (const b of Array.isArray(buchungenAmTag) ? buchungenAmTag : []) {
if (!b || b.quelle === 'sperre' || NICHT_BELEGT.has(b.status) || !anzahl.has(b.ressourceId)) continue
if (jetztMs !== null && b.status === 'gehalten' && !istBelegend(b, jetztMs)) continue
anzahl.set(b.ressourceId, anzahl.get(b.ressourceId) + 1)
}
const rang = new Map((einstellung?.ressourcen || []).map((r, i) => [r?.id, i]))
+37 -2
View File
@@ -244,18 +244,41 @@ test('Sonderoeffnung mit leerer Liste: an diesem Tag geschlossen', () => {
assert.ok(frei(e, '2026-10-13').length > 0)
})
test('Sonderoeffnung gilt fuer alle Ressourcen und schlaegt die Abwesenheit (Rangfolge Sonderoeffnung > Abwesenheit > Feiertag > Plan)', () => {
test('Sonderoeffnung gilt fuer alle Ressourcen, auch fuer solche ohne Plan an diesem Tag', () => {
const e = einstellung({
ressourcen: [ressource(R1, wochenplan(TAG)), ressource(R2, wochenplan([['08:00', '09:00']]))],
raster: 60,
sonderoeffnungen: { [MO]: [['10:00', '12:00']] },
abwesenheiten: [{ id: 'a_cccccccc', ressourceId: R1, von: MO, bis: MO, titel: 'Urlaub' }],
})
const liste = frei(e, MO)
assert.deepEqual(uhrzeiten(liste), ['10:00', '11:00'])
assert.deepEqual(liste[0].ressourceIds, [R1, R2])
})
test('Person im Urlaub ist an einem Sonderoeffnungstag nicht buchbar', () => {
const e = einstellung({
ressourcen: [ressource(R1, wochenplan(TAG)), ressource(R2, wochenplan(TAG))],
raster: 60,
sonderoeffnungen: { [MO]: [['10:00', '12:00']] },
abwesenheiten: [{ id: 'a_cccccccc', ressourceId: R1, von: MO, bis: MO, titel: 'Urlaub' }],
})
assert.deepEqual(frei(e, MO, { ressourceId: R1 }), [])
assert.deepEqual(frei(e, MO, { ressourceId: R2 }).map((z) => [z.uhrzeit, z.ressourceIds]), [['10:00', [R2]], ['11:00', [R2]]])
assert.deepEqual(frei(e, MO).map((z) => z.ressourceIds), [[R2], [R2]], 'egal: nur die anwesende Person')
})
test('Betriebsurlaub schlaegt Sonderoeffnung', () => {
const e = einstellung({
ressourcen: [ressource(R1, wochenplan(TAG)), ressource(R2, wochenplan(TAG))],
raster: 60,
sonderoeffnungen: { [MO]: [['10:00', '12:00']], '2026-10-13': [['10:00', '12:00']] },
abwesenheiten: [{ id: 'a_dddddddd', ressourceId: null, von: MO, bis: MO, titel: 'Betriebsurlaub' }],
})
assert.deepEqual(frei(e, MO), [])
assert.deepEqual(frei(e, MO, { ressourceId: R1 }), [])
assert.deepEqual(uhrzeiten(frei(e, '2026-10-13')), ['10:00', '11:00'], 'ohne Abwesenheit gilt die Sonderoeffnung')
})
// ---------------------------------------------------------------- Belegung
test('Belegung blockiert Ueberlappung, angrenzende Zeiten bleiben frei', () => {
@@ -520,6 +543,18 @@ test('waehleRessource: bei Gleichstand gilt die Reihenfolge der Liste, abgesagte
assert.equal(waehleRessource([R2, R1], [{ ressourceId: R1, status: 'erledigt' }], e), R2, 'erledigte Termine des Tages zaehlen mit')
})
test('waehleRessource: mit now zaehlt eine abgelaufene Haltung nicht, eine gueltige schon', () => {
const e = einstellung({ ressourcen: [ressource(R1, {}), ressource(R2, {})] })
const now = new Date('2026-10-12T08:00:00Z')
const haltung = (ressourceId, heldUntil) => ({ ressourceId, status: 'gehalten', heldUntil: heldUntil && appwrite(new Date(heldUntil)) })
const abgelaufen = [haltung(R1, '2026-10-12T07:59:00Z'), haltung(R1, '2026-10-12T08:00:00Z'), haltung(R1, undefined)]
assert.equal(waehleRessource([R1, R2], abgelaufen, e), R2, 'ohne now zaehlt jede Haltung')
assert.equal(waehleRessource([R2, R1], abgelaufen, e, now), R1, 'mit now: Gleichstand 0:0, Reihenfolge der Einstellung')
assert.equal(waehleRessource([R1, R2], [haltung(R1, '2026-10-12T08:00:01Z')], e, now), R2, 'gueltige Haltung zaehlt')
assert.equal(waehleRessource([R1, R2], [...abgelaufen, { ressourceId: R1, status: 'bestaetigt' }], e, now), R2, 'andere Status zaehlen weiter')
assert.throws(() => waehleRessource([R1], [], e, 'gestern'), TypeError)
})
test('waehleRessource: keine Kandidaten ergibt null, fremde Ressourcen werden nicht gezaehlt', () => {
const e = einstellung()
assert.equal(waehleRessource([], [], e), null)