Buchungskalender: Kalender und Unterkünfte im Portal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3ecea05b52
commit
db9fc61894
8 files changed
+814
-8
No files matched your search
@@ -164,3 +164,208 @@ test('Storno bei Online-Zahlung: Erstattungsbetrag wird gesendet', mit(server({
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx)[0].body.erstattungCent, 20000)
|
||||
}))
|
||||
|
||||
// Task 13: Reiter „Kalender“ und „Unterkünfte & Preise“ (Antwortformen: ui-erwartungen.md)
|
||||
const UK = { id: 'u1', name: 'Haus <i>am</i> See', beschreibung: 'Schön', maxPersonen: 4, mindestNaechte: 2, checkIn: '15:00', checkOut: '10:00', sort: 0, aktiv: true, icalExportUrl: 'https://x.test/unterkunft/ical/tok.ics',
|
||||
icalImporte: [{ id: 'i1', name: 'Booking.com', url: 'https://b.test/x.ics', letzterErfolgAt: '2026-10-07T10:00:00.000Z', fehler: '' }, { id: 'i2', name: 'Airbnb', url: 'https://a.test/x.ics', fehler: 'Zeitüberschreitung' }] }
|
||||
const PREISE = { staffeln: [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 7, preisProNachtCent: 5000 }], reinigungCent: 6000, personenInklusive: 2, aufpreisPersonNachtCent: 1000, direktrabattProzent: 10, steuerart: 'regel', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: 'Kostenlos bis 14 Tage vorher.' }
|
||||
const BEREICHE = [
|
||||
{ von: '2026-11-03', bis: '2026-11-06', quelle: 'gebucht', anfrageNummer: 'A-1' }, { von: '2026-11-10', bis: '2026-11-12', quelle: 'reserviert' },
|
||||
{ von: '2026-11-15', bis: '2026-11-17', quelle: 'ical:i1' }, { von: '2026-11-20', bis: '2026-11-22', quelle: 'manuell', sperreId: 's1', notiz: 'Eigenbedarf' },
|
||||
]
|
||||
const ukServer = (o = {}) => (a) => {
|
||||
const key = `${a.method} ${a.url.split('?')[0]}`
|
||||
if (o[key]) return o[key](a)
|
||||
if (key === 'GET /api/unterkunft/unterkuenfte') return { body: { unterkuenfte: [UK] } }
|
||||
if (key === 'GET /api/unterkunft/unterkuenfte/u1/kalender') return { body: { bereiche: BEREICHE } }
|
||||
if (key === 'GET /api/unterkunft/unterkuenfte/u1/preise') return { body: { preise: PREISE } }
|
||||
if (key === 'POST /api/unterkunft/preisvorschau') return { body: { naechte: 7, posten: [], summen: { bruttoCent: 40650 } } }
|
||||
return { body: {} }
|
||||
}
|
||||
const zeigeTab = async (fx, modul) => {
|
||||
const ctx = baueCtx()
|
||||
const panel = h('div')
|
||||
fx.document.body.append(panel)
|
||||
await modul.render(panel, ctx)
|
||||
await fx.ruhe()
|
||||
return { panel, ctx }
|
||||
}
|
||||
const kal = await import('../public/buchungen-tab-kalender.js')
|
||||
const ukTab = await import('../public/buchungen-tab-unterkuenfte.js')
|
||||
const tag = (fx, panel, t) => fx.finde(panel, `[data-tag="${t}"]`)
|
||||
const gehZuNovember = async (fx, panel) => { while (!/November 2026/.test(fx.text(fx.finde(panel, '[data-monat]')))) { await fx.klick(aktion(fx, panel, 'weiter')); await fx.ruhe() } }
|
||||
|
||||
test('rasterTage: November 2026 beginnt am Montag 26.10. und endet am Sonntag 29.11.... ', () => {
|
||||
const t = kal.rasterTage(2026, 10)
|
||||
assert.equal(t[0], '2026-10-26')
|
||||
assert.equal(t.length % 7, 0)
|
||||
assert.equal(t.at(-1), '2026-12-06')
|
||||
assert.equal(kal.rasterTage(2026, 5)[0], '2026-06-01') // Juni 2026 beginnt am Montag
|
||||
})
|
||||
|
||||
test('Kalender: Raster Montag zuerst, Farben nach Quelle, Legende', mit(ukServer(), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, kal)
|
||||
await gehZuNovember(fx, panel)
|
||||
assert.equal(fx.text(fx.alle(panel, '[role="columnheader"]')[0]), 'Mo')
|
||||
assert.equal(fx.alle(panel, '[data-tag]')[0].dataset.tag, '2026-10-26')
|
||||
assert.ok(tag(fx, panel, '2026-11-03').classList.contains('ist-gebucht'))
|
||||
assert.ok(!tag(fx, panel, '2026-11-06').classList.contains('ist-gebucht'), 'Abreisetag ist frei')
|
||||
assert.ok(tag(fx, panel, '2026-11-10').classList.contains('ist-reserviert'))
|
||||
assert.ok(tag(fx, panel, '2026-11-16').classList.contains('ist-ical'))
|
||||
assert.ok(tag(fx, panel, '2026-11-20').classList.contains('ist-manuell'))
|
||||
assert.ok(tag(fx, panel, '2026-11-08').classList.contains('ist-frei'))
|
||||
for (const t of ['Direkt gebucht', 'Reserviert', 'Booking.com / Airbnb', 'Von dir gesperrt']) assert.match(fx.text(panel), new RegExp(t))
|
||||
assert.ok(fx.fetchLog.some((a) => /kalender\?projectId=p1&von=2026-10-26&bis=2026-12-07$/.test(a.url)))
|
||||
}))
|
||||
|
||||
test('Kalender: zwei Klicks auf freie Tage → Notiz → POST sperren', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte/u1/sperren': () => ({ status: 201, body: {} }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, kal)
|
||||
await gehZuNovember(fx, panel)
|
||||
await fx.klick(tag(fx, panel, '2026-11-25'))
|
||||
assert.equal(posts(fx).length, 0)
|
||||
await fx.klick(tag(fx, panel, '2026-11-27'))
|
||||
assert.match(fx.text(panel), /Zeitraum sperren/)
|
||||
await fx.setze(fx.finde(panel, '#bk-k-notiz'), 'Familie')
|
||||
await fx.klick(aktion(fx, panel, 'sperren-senden'))
|
||||
await fx.ruhe()
|
||||
const [p] = posts(fx)
|
||||
assert.equal(p.url, '/api/unterkunft/unterkuenfte/u1/sperren?projectId=p1')
|
||||
assert.deepEqual(p.body, { von: '2026-11-25', bis: '2026-11-28', notiz: 'Familie' })
|
||||
}))
|
||||
|
||||
test('Kalender: Zeitraum über belegte Tage wird nicht angeboten; 409 vom Server wird gezeigt', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte/u1/sperren': () => ({ status: 409, body: { fehler: 'zeitraum_belegt' } }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, kal)
|
||||
await gehZuNovember(fx, panel)
|
||||
await fx.klick(tag(fx, panel, '2026-11-08'))
|
||||
await fx.klick(tag(fx, panel, '2026-11-13'))
|
||||
assert.equal(aktion(fx, panel, 'sperren-senden'), null)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-hinweis]')), /belegte Tage/)
|
||||
await fx.klick(tag(fx, panel, '2026-11-24'))
|
||||
await fx.klick(tag(fx, panel, '2026-11-24'))
|
||||
await fx.klick(aktion(fx, panel, 'sperren-senden'))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(panel), /Der Zeitraum ist bereits belegt\./)
|
||||
}))
|
||||
|
||||
test('Kalender: Klick auf manuelle Sperre bietet „Sperre aufheben“ (DELETE); andere Quellen nicht', mit(ukServer({ 'DELETE /api/unterkunft/sperren/s1': () => ({ body: {} }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, kal)
|
||||
await gehZuNovember(fx, panel)
|
||||
await fx.klick(tag(fx, panel, '2026-11-04'))
|
||||
assert.equal(aktion(fx, panel, 'aufheben'), null)
|
||||
await fx.klick(tag(fx, panel, '2026-11-21'))
|
||||
assert.match(fx.text(panel), /Eigenbedarf/)
|
||||
await fx.klick(aktion(fx, panel, 'aufheben'))
|
||||
await fx.ruhe()
|
||||
const [p] = posts(fx)
|
||||
assert.equal(p.method, 'DELETE')
|
||||
assert.equal(p.url, '/api/unterkunft/sperren/s1?projectId=p1')
|
||||
}))
|
||||
|
||||
test('Unterkünfte: Liste mit Fremdtext als Text; Formular hat alle Felder; Speichern sendet PUT', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1': () => ({ body: { unterkunft: UK } }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
assert.match(fx.text(panel), /Haus <i>am<\/i> See/)
|
||||
assert.equal(fx.finde(panel, 'i'), null)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
for (const f of ['name', 'beschreibung', 'maxPersonen', 'mindestNaechte', 'checkIn', 'checkOut', 'aktiv']) assert.ok(fx.finde(panel, `[data-feld="${f}"]`), f)
|
||||
await fx.setze(fx.finde(panel, '#bk-u-max'), '6')
|
||||
await fx.klick(fx.finde(panel, '.bk-unterkuenfte form [data-aktion="speichern"]'))
|
||||
await fx.ruhe()
|
||||
const put = posts(fx).find((a) => a.method === 'PUT')
|
||||
assert.equal(put.url, '/api/unterkunft/unterkuenfte/u1?projectId=p1')
|
||||
assert.deepEqual(put.body, { name: UK.name, beschreibung: 'Schön', maxPersonen: 6, mindestNaechte: 2, checkIn: '15:00', checkOut: '10:00', sort: 0, aktiv: true })
|
||||
}))
|
||||
|
||||
test('Unterkünfte: Feldfehler (400 mit felder) kommen am Feld an', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1': () => ({ status: 400, body: { fehler: 'ungueltig', felder: { name: 'Name ist schon vergeben.' } } }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
await fx.klick(fx.finde(panel, '.bk-unterkuenfte form [data-aktion="speichern"]'))
|
||||
await fx.ruhe()
|
||||
assert.equal(fx.text(fx.finde(panel, '[data-fehler="name"]')), 'Name ist schon vergeben.')
|
||||
}))
|
||||
|
||||
test('Unterkünfte: Neue Unterkunft prüft Felder lokal und sendet POST', mit(ukServer({ 'POST /api/unterkunft/unterkuenfte': () => ({ status: 201, body: { unterkunft: { ...UK, id: 'u2', name: 'Neu' } } }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'neu'))
|
||||
await fx.klick(aktion(fx, panel, 'speichern'))
|
||||
assert.match(fx.text(fx.finde(panel, '[data-fehler="name"]')), /Namen/)
|
||||
assert.equal(posts(fx).length, 0)
|
||||
await fx.setze(fx.finde(panel, '#bk-u-name'), 'Neu')
|
||||
await fx.klick(aktion(fx, panel, 'speichern'))
|
||||
await fx.ruhe()
|
||||
const [p] = posts(fx)
|
||||
assert.equal(p.method, 'POST')
|
||||
assert.equal(p.body.name, 'Neu')
|
||||
assert.equal(p.body.sort, 1)
|
||||
}))
|
||||
|
||||
test('Preis-Editor: Staffelzeilen +/−, Beispielrechnung über /preisvorschau, Speichern sendet Cent', mit(ukServer({ 'PUT /api/unterkunft/unterkuenfte/u1/preise': () => ({ body: {} }) }), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(panel, '[data-vorschau]')), /7 Nächte, 2 Personen = 406,50\s€/)
|
||||
const vor = fx.fetchLog.find((a) => a.url.startsWith('/api/unterkunft/preisvorschau'))
|
||||
assert.equal(vor.method, 'POST')
|
||||
assert.equal(vor.body.unterkunftId, 'u1')
|
||||
assert.equal(vor.body.personen, 2)
|
||||
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 2)
|
||||
await fx.klick(aktion(fx, panel, 'staffel-dazu'))
|
||||
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 3)
|
||||
const neu = fx.alle(panel, '[data-staffel-zeile]')[2]
|
||||
await fx.setze(fx.finde(neu, '[data-staffel="ab"]'), '14')
|
||||
await fx.setze(fx.finde(neu, '[data-staffel="preis"]'), '45')
|
||||
await fx.klick(fx.finde(fx.alle(panel, '[data-staffel-zeile]')[1], '[data-aktion="staffel-weg"]'))
|
||||
assert.equal(fx.alle(panel, '[data-staffel-zeile]').length, 2)
|
||||
await fx.klick(aktion(fx, panel, 'preise-speichern'))
|
||||
await fx.ruhe()
|
||||
const put = posts(fx).find((a) => a.url.includes('/preise'))
|
||||
assert.equal(put.method, 'PUT')
|
||||
assert.deepEqual(put.body.staffeln, [{ abNaechten: 1, preisProNachtCent: 5500 }, { abNaechten: 14, preisProNachtCent: 4500 }])
|
||||
assert.equal(put.body.reinigungCent, 6000)
|
||||
assert.equal(put.body.aufpreisPersonNachtCent, 1000)
|
||||
assert.equal(put.body.steuerart, 'regel')
|
||||
assert.equal(put.body.direktrabattProzent, 10)
|
||||
assert.equal(put.body.stornobedingungen, 'Kostenlos bis 14 Tage vorher.')
|
||||
}))
|
||||
|
||||
test('iCal: Status je Import, Fehlertext, Export-Link, Anleitungen', mit(ukServer(), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
assert.match(fx.text(fx.finde(panel, '[data-import="i1"]')), /Zuletzt abgeglichen 07\.10\.2026/)
|
||||
assert.match(fx.text(fx.finde(panel, '[data-import="i2"]')), /nicht geklappt: Zeitüberschreitung/)
|
||||
assert.equal(fx.finde(panel, '[data-export]').getAttribute('value'), UK.icalExportUrl)
|
||||
assert.ok(aktion(fx, panel, 'export-kopieren'))
|
||||
assert.match(fx.text(fx.finde(panel, '[data-anleitung="booking"]')), /Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen/)
|
||||
assert.ok(fx.finde(panel, '[data-anleitung="airbnb"]'))
|
||||
}))
|
||||
|
||||
test('iCal: Import hinzufügen (POST), entfernen mit Rückfrage, Link erneuern mit Bestätigung', mit(ukServer({
|
||||
'POST /api/unterkunft/unterkuenfte/u1/ical-importe': () => ({ status: 201, body: {} }),
|
||||
'DELETE /api/unterkunft/unterkuenfte/u1/ical-importe/i1': () => ({ body: {} }),
|
||||
'POST /api/unterkunft/unterkuenfte/u1/ical-export/erneuern': () => ({ body: {} }),
|
||||
}), async (fx) => {
|
||||
const { panel } = await zeigeTab(fx, ukTab)
|
||||
await fx.klick(aktion(fx, panel, 'bearbeiten'))
|
||||
await fx.ruhe()
|
||||
await fx.setze(fx.finde(panel, '#bk-i-name'), 'Vrbo')
|
||||
await fx.setze(fx.finde(panel, '#bk-i-url'), 'http://unsicher.test/x.ics')
|
||||
const vorher = posts(fx).length
|
||||
await fx.klick(aktion(fx, panel, 'import-dazu'))
|
||||
assert.equal(posts(fx).length, vorher, 'nur https')
|
||||
await fx.setze(fx.finde(panel, '#bk-i-url'), 'https://vrbo.test/x.ics')
|
||||
await fx.klick(aktion(fx, panel, 'import-dazu'))
|
||||
await fx.ruhe()
|
||||
assert.deepEqual(posts(fx).find((a) => a.url.includes('ical-importe')).body, { name: 'Vrbo', url: 'https://vrbo.test/x.ics' })
|
||||
await fx.klick(fx.finde(fx.finde(panel, '[data-import="i1"]'), '[data-aktion="import-weg"]'))
|
||||
assert.equal(posts(fx).filter((a) => a.method === 'DELETE').length, 0)
|
||||
await fx.klick(aktion(fx, panel, 'import-weg-ok'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx).find((a) => a.method === 'DELETE').url, '/api/unterkunft/unterkuenfte/u1/ical-importe/i1?projectId=p1')
|
||||
await fx.klick(aktion(fx, panel, 'export-erneuern'))
|
||||
assert.equal(posts(fx).filter((a) => a.url.includes('erneuern')).length, 0)
|
||||
await fx.klick(aktion(fx, panel, 'erneuern-ok'))
|
||||
await fx.ruhe()
|
||||
assert.equal(posts(fx).filter((a) => a.url.includes('erneuern')).length, 1)
|
||||
}))
|
||||
Reference in new issue
Block a user