Öffnungszeiten: Einbau-Ticket und Ersatz-HTML für das Team
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
302b58cf72
commit
8d670ca5dc
4 files changed
+355
-7
No files matched your search
@@ -12,6 +12,9 @@ import {
|
|||||||
listCustomerFeatureDocs,
|
listCustomerFeatureDocs,
|
||||||
listCustomerFeatures,
|
listCustomerFeatures,
|
||||||
} from '../../services/siteFeatures.js'
|
} from '../../services/siteFeatures.js'
|
||||||
|
import { ersatzHtml } from '../../services/zeiten/ersatz.js'
|
||||||
|
import { ladeZeiten } from '../../services/zeiten/speicher.js'
|
||||||
|
import { hauptplan, planNachKurz } from '../../services/zeiten/zeitlogik.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Funktionen (News-Pinnwand, Terminplaner) je Kundenprojekt im Ticketsystem (Kunden-Reiter "Funktionen").
|
* Funktionen (News-Pinnwand, Terminplaner) je Kundenprojekt im Ticketsystem (Kunden-Reiter "Funktionen").
|
||||||
@@ -69,6 +72,35 @@ funktionenAdminRouter.get('/:customerId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ersatz-HTML (statische Wochentabelle) eines Plans von Oeffnungszeiten live fuer den Einbau (Spec 4.5): { html }.
|
||||||
|
* Ohne Parameter plan gilt der Hauptplan; ist plan angegeben (auch leer), muss es diesen Kurznamen geben, sonst 404 und
|
||||||
|
* kein Rueckfall auf den Hauptplan (Ruling R9). Noch keine gespeicherten Zeiten -> 409. Nur lesend, ohne Sperre.
|
||||||
|
*/
|
||||||
|
funktionenAdminRouter.get('/:customerId/zeiten/ersatz', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { customerId } = req.params
|
||||||
|
const { projectId, plan } = req.query
|
||||||
|
if (typeof projectId !== 'string' || !projectId) return res.status(400).json({ error: 'Projekt fehlt' })
|
||||||
|
await assertOwnProject(customerId, projectId)
|
||||||
|
const doc = await getFeatureDoc(customerId, projectId, 'zeiten')
|
||||||
|
if (!doc) return res.status(404).json({ error: `Für dieses Projekt gibt es noch ${FEATURE_LABELS.zeiten.keine}` })
|
||||||
|
const daten = (await ladeZeiten(doc.$id))?.daten
|
||||||
|
const keineZeiten = () => res.status(409).json({ error: 'Der Kunde hat noch keine Zeiten eingetragen.' })
|
||||||
|
if (!Array.isArray(daten?.plaene) || daten.plaene.length === 0) return keineZeiten()
|
||||||
|
if (plan !== undefined) {
|
||||||
|
const gewaehlt = planNachKurz(daten, plan)
|
||||||
|
if (!gewaehlt) return res.status(404).json({ error: 'Plan nicht gefunden' })
|
||||||
|
return res.json({ html: ersatzHtml(daten, gewaehlt) })
|
||||||
|
}
|
||||||
|
const haupt = hauptplan(daten)
|
||||||
|
if (!haupt) return keineZeiten()
|
||||||
|
res.json({ html: ersatzHtml(daten, haupt) })
|
||||||
|
} catch (err) {
|
||||||
|
sendError(res, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Aktivieren (ohne Zahlung) und Abschalten je Funktion: /news wie bisher, /termine neu; jeder andere Schluessel -> 404
|
// Aktivieren (ohne Zahlung) und Abschalten je Funktion: /news wie bisher, /termine neu; jeder andere Schluessel -> 404
|
||||||
funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
|
funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,10 +1,72 @@
|
|||||||
import { minimaleTicketDetails } from '../featureRegistry.js'
|
import { kuerze, minimaleTicketDetails, TICKET_DETAILS_MAX, TICKET_GEKUERZT } from '../featureRegistry.js'
|
||||||
|
import { ladeZeiten } from './speicher.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Details des Einrichtungs-Tickets von Oeffnungszeiten live. Vorlaeufig die Notfall-Details des Registers (Kopfzeilen und
|
* Details des Einbau-Tickets von Oeffnungszeiten live (Spec 4.6, config.features.zeiten.ticket.details).
|
||||||
* Verweis auf die Anleitung im WOMS); Task 14 baut daraus die vollstaendige Anleitung.
|
* Kopfzeilen und Verweis auf das Kundenpanel im WOMS (Notfall-Details des Registers), darunter die Einbau-Zeile mit
|
||||||
* ctx = { feature, doc, project, customer }.
|
* siteKey, das Ergebnis des Neubau-Anstosses (doc.einbau), Beispiele der Platzhalter mit den Kurznamen der Plaene,
|
||||||
|
* der Hinweis auf das Ersatz-HTML und die Pruefliste. ctx = { feature, doc, project, customer }.
|
||||||
|
* Wirft nie wegen der gespeicherten Zeiten: ohne Dokument-ID, ohne Zeiten oder bei einem Lesefehler heisst es
|
||||||
|
* "Plaene folgen". Der Text bleibt hoechstens TICKET_DETAILS_MAX Zeichen lang.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const EMBED_URL = 'https://project.webklar.com/embed/zeiten.js'
|
||||||
|
const PLATZHALTER_MODI = Object.freeze(['tabelle', 'status', 'badge'])
|
||||||
|
|
||||||
|
const EINBAU_ERGEBNIS = Object.freeze({
|
||||||
|
angestossen: 'automatisch eingesetzt (nach dem nächsten Veröffentlichen prüfen)',
|
||||||
|
manuell: 'bitte von Hand einsetzen',
|
||||||
|
})
|
||||||
|
|
||||||
|
const PRUEFLISTE = Object.freeze([
|
||||||
|
'Feste Zeiten im Repo suchen und ersetzen (grep -ril "öffnungszeit" u. Ä.), auch Footer, Kontaktseite und eigene Status-Skripte.',
|
||||||
|
'Header: Überlappt die Hinweis-Leiste den Kopf der Seite? Sonst im WOMS den navSelektor setzen.',
|
||||||
|
'Hat die Seite schon eine eigene Anruf-Leiste? Dann nicht doppelt.',
|
||||||
|
'Vorhandenes JSON-LD (Öffnungszeiten der Seite) prüfen, damit nichts doppelt oder widersprüchlich steht.',
|
||||||
|
'Datenschutz-Absatz zu Öffnungszeiten live auf der Website ergänzt?',
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Kurznamen der gespeicherten Plaene, Hauptplan zuerst: [{ kurz, haupt }]; leer ohne Zeiten oder bei jedem Fehler. */
|
||||||
|
async function planKurznamen(doc) {
|
||||||
|
if (typeof doc?.$id !== 'string' || doc.$id === '') return []
|
||||||
|
try {
|
||||||
|
const gespeichert = await ladeZeiten(doc.$id)
|
||||||
|
const plaene = Array.isArray(gespeichert?.daten?.plaene) ? gespeichert.daten.plaene : []
|
||||||
|
const liste = plaene
|
||||||
|
.filter((p) => p && typeof p.kurz === 'string' && p.kurz !== '')
|
||||||
|
.map((p) => ({ kurz: p.kurz, haupt: p.haupt === true }))
|
||||||
|
return [...liste.filter((p) => p.haupt), ...liste.filter((p) => !p.haupt)]
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[zeiten] Pläne für das Ticket von ${doc.$id} nicht lesbar: ${err?.message}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function zeitenTicketDetails(ctx) {
|
export async function zeitenTicketDetails(ctx) {
|
||||||
return minimaleTicketDetails(ctx)
|
const doc = ctx?.doc || {}
|
||||||
|
const siteKey = typeof doc.siteKey === 'string' ? doc.siteKey : ''
|
||||||
|
const plaene = await planKurznamen(doc)
|
||||||
|
const beispielKurz = plaene[0]?.kurz ?? '<kurz>'
|
||||||
|
// einbau fehlt oder ist unbekannt: nichts wurde automatisch eingesetzt
|
||||||
|
const ergebnis = EINBAU_ERGEBNIS[doc.einbau] ?? EINBAU_ERGEBNIS.manuell
|
||||||
|
|
||||||
|
const text = [
|
||||||
|
minimaleTicketDetails(ctx),
|
||||||
|
'',
|
||||||
|
'Einbau-Zeile (im <head> jeder HTML-Seite):',
|
||||||
|
`<script data-wk-zeiten src="${EMBED_URL}" data-site="${siteKey}" defer></script>`,
|
||||||
|
`Einbau: ${ergebnis}`,
|
||||||
|
'',
|
||||||
|
'Platzhalter (dort, wo die Zeiten stehen sollen; innen bleibt die statische Ersatz-Tabelle):',
|
||||||
|
...PLATZHALTER_MODI.map((modus) => `<div data-webklar-zeiten data-modus="${modus}" data-plan="${beispielKurz}">Ersatz-HTML</div>`),
|
||||||
|
plaene.length > 0
|
||||||
|
? `Pläne (Kurznamen für data-plan): ${plaene.map((p) => (p.haupt ? `${p.kurz} (Hauptplan)` : p.kurz)).join(', ')}`
|
||||||
|
: 'Pläne folgen, sobald der Kunde gespeichert hat',
|
||||||
|
'Ersatz-HTML: WOMS → Funktionen → Ersatz-HTML kopieren (erst nach dem ersten Speichern des Kunden)',
|
||||||
|
'',
|
||||||
|
'Prüfliste:',
|
||||||
|
...PRUEFLISTE.map((punkt) => `- ${punkt}`),
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
return kuerze(text, TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT })
|
||||||
}
|
}
|
||||||
@@ -145,15 +145,16 @@ test('Mails: Werte aus dem Register, Preis aus zeitenSetup', () => {
|
|||||||
assert.equal(k15.values.portal_url, `${config.publicBaseUrl}/dashboard.html#zeiten`)
|
assert.equal(k15.values.portal_url, `${config.publicBaseUrl}/dashboard.html#zeiten`)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Ticket: Betreff mit Projektname, Details wie die Notfall-Details', async () => {
|
test('Ticket: Betreff mit Projektname, Details beginnen mit den Notfall-Details (Einbau-Text: test/zeitenTicket.test.js)', async () => {
|
||||||
const f = feature()
|
const f = feature()
|
||||||
const project = { projectName: 'Salon Haarlinie', subdomain: 'salon' }
|
const project = { projectName: 'Salon Haarlinie', subdomain: 'salon' }
|
||||||
assert.equal(f.ticket.topic(project), 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
assert.equal(f.ticket.topic(project), 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
||||||
assert.equal(f.ticket.topic({ subdomain: 'salon' }), 'Öffnungszeiten live einbauen – salon')
|
assert.equal(f.ticket.topic({ subdomain: 'salon' }), 'Öffnungszeiten live einbauen – salon')
|
||||||
const ctx = { feature: f, doc: { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, project, customer: { name: 'Kunde Eins' } }
|
const ctx = { feature: f, doc: { customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop' }, project, customer: { name: 'Kunde Eins' } }
|
||||||
const details = await f.ticket.details(ctx)
|
const details = await f.ticket.details(ctx)
|
||||||
assert.equal(details, reg.minimaleTicketDetails(ctx))
|
assert.ok(details.startsWith(reg.minimaleTicketDetails(ctx)))
|
||||||
assert.match(details, /^Funktion: zeiten$/m)
|
assert.match(details, /^Funktion: zeiten$/m)
|
||||||
|
assert.match(details, /data-wk-zeiten src="https:\/\/project\.webklar\.com\/embed\/zeiten\.js" data-site="abcdefghijklmnop"/)
|
||||||
const texte = await reg.ticketTexte(ctx)
|
const texte = await reg.ticketTexte(ctx)
|
||||||
assert.equal(texte.topic, 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
assert.equal(texte.topic, 'Öffnungszeiten live einbauen – Salon Haarlinie')
|
||||||
assert.equal(texte.details, details)
|
assert.equal(texte.details, details)
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import { test, after, beforeEach } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||||
|
import { schemaKeys } from './support/schema.js'
|
||||||
|
import { useTestEnv } from './support/testEnv.js'
|
||||||
|
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||||
|
|
||||||
|
// Einbau-Ticket von Oeffnungszeiten live (Aufgabe 15, Spec 4.6) und Ersatz-HTML fuer das Team (Spec 4.5, Ruling R9):
|
||||||
|
// zeitenTicketDetails(ctx) und GET /api/team/funktionen/:customerId/zeiten/ersatz?projectId=&plan= hinter requireStaffJwt.
|
||||||
|
const aw = await startAppwriteMock({
|
||||||
|
schema: {
|
||||||
|
portalFeatures: schemaKeys('portalFeatures'),
|
||||||
|
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||||||
|
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||||||
|
},
|
||||||
|
datetimes: ZEITEN_DATETIMES,
|
||||||
|
defaultLimit: 25,
|
||||||
|
})
|
||||||
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||||||
|
|
||||||
|
const { startTestApp, send } = await import('./support/testApp.js')
|
||||||
|
const { seedFixtures } = await import('./support/fixtures.js')
|
||||||
|
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||||
|
const { TICKET_DETAILS_MAX } = await import('../server/services/featureRegistry.js')
|
||||||
|
const { config } = await import('../server/config.js')
|
||||||
|
const { zeitenTicketDetails } = await import('../server/services/zeiten/ticketDetails.js')
|
||||||
|
const { speichereZeiten, speichereTeam } = await import('../server/services/zeiten/speicher.js')
|
||||||
|
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||||||
|
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||||
|
|
||||||
|
const app = await startTestApp({ '/api/team': teamRoutes })
|
||||||
|
after(() => Promise.all([app.close(), aw.close()]))
|
||||||
|
seedFixtures(aw)
|
||||||
|
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
||||||
|
|
||||||
|
const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } }
|
||||||
|
const SITE_KEY = 'abcdefghijklmnop'
|
||||||
|
const P1 = 'p_aaaaaaa1'
|
||||||
|
const P2 = 'p_aaaaaaa2'
|
||||||
|
|
||||||
|
const featureDoc = (projectId, over = {}) => ({
|
||||||
|
$id: `f_${projectId}_zeiten`,
|
||||||
|
customerId: 'c1',
|
||||||
|
projectId,
|
||||||
|
featureKey: 'zeiten',
|
||||||
|
state: 'setup',
|
||||||
|
enabled: false,
|
||||||
|
siteKey: SITE_KEY,
|
||||||
|
...over,
|
||||||
|
})
|
||||||
|
const projekt = (over = {}) => ({ $id: 'p1', customerId: 'c1', projectName: 'Salon Haarlinie', subdomain: 'salon', ...over })
|
||||||
|
const ctxVon = (doc, extra = {}) => ({
|
||||||
|
feature: config.features.zeiten,
|
||||||
|
doc,
|
||||||
|
project: projekt(),
|
||||||
|
customer: { name: 'Kunde Eins' },
|
||||||
|
...extra,
|
||||||
|
})
|
||||||
|
|
||||||
|
function tage(woche = {}) {
|
||||||
|
const t = {}
|
||||||
|
for (let d = 1; d <= 7; d += 1) {
|
||||||
|
const w = woche[d]
|
||||||
|
t[d] = w ? { modus: 'zeiten', fenster: [w] } : { modus: 'zu', fenster: [] }
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
const planDaten = (id, name, extra = {}) => ({ id, name, haupt: false, tage: tage({ 1: ['09:00', '18:00'] }), ...extra })
|
||||||
|
const eingabe = (plaene) => ({
|
||||||
|
land: 'RP',
|
||||||
|
plaene,
|
||||||
|
feiertage: {},
|
||||||
|
ausnahmen: [],
|
||||||
|
hinweise: [],
|
||||||
|
kontakt: { an: false, telefon: '', adresse: '', whatsapp: null },
|
||||||
|
})
|
||||||
|
const EIN_PLAN = () => eingabe([planDaten(P1, 'Standard', { haupt: true })])
|
||||||
|
const ZWEI_PLAENE = () => eingabe([planDaten(P1, 'Standard', { haupt: true }), planDaten(P2, 'Weihnachten', { tage: tage({ 2: ['10:00', '14:00'] }) })])
|
||||||
|
|
||||||
|
/** Speichert Zeiten wie der Kunde (Normalisierung vergibt die Kurznamen) und liefert die Funktionsdokument-Felder. */
|
||||||
|
async function mitZeiten(doc, daten) {
|
||||||
|
await speichereZeiten({ feature: doc, eingabe: daten, version: 0, now: new Date('2026-10-05T08:00:00.000Z') })
|
||||||
|
}
|
||||||
|
const ersatz = (customerId, query, opts = STAFF) => send('GET', `/api/team/funktionen/${customerId}/zeiten/ersatz${query}`, opts)
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
for (const collection of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'websiteProjects']) {
|
||||||
|
for (const id of [...aw.docs(collection).keys()]) await deleteDocument(collection, id)
|
||||||
|
}
|
||||||
|
resetZeitenCacheForTests()
|
||||||
|
aw.seed('websiteProjects', projekt())
|
||||||
|
aw.seed('websiteProjects', projekt({ $id: 'p2', projectName: 'Studio', subdomain: 'studio' }))
|
||||||
|
aw.seed('websiteProjects', projekt({ $id: 'p3', customerId: 'c2', projectName: 'Fremder Salon', subdomain: 'fremd' }))
|
||||||
|
})
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------ Ticket-Text
|
||||||
|
|
||||||
|
test('Ticket nennt siteKey, Einbau-Zeile und Prüfliste', async () => {
|
||||||
|
const doc = featureDoc('p1', { einbau: 'angestossen' })
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
await mitZeiten(doc, ZWEI_PLAENE())
|
||||||
|
const text = await zeitenTicketDetails(ctxVon(doc))
|
||||||
|
assert.equal(typeof text, 'string')
|
||||||
|
assert.match(text, /^Funktion: zeiten$/m)
|
||||||
|
assert.match(text, /^Projekt: Salon Haarlinie$/m)
|
||||||
|
assert.match(text, new RegExp(`^siteKey: ${SITE_KEY}$`, 'm'))
|
||||||
|
// Einbau-Zeile wortgleich
|
||||||
|
assert.ok(
|
||||||
|
text.split('\n').includes(`<script data-wk-zeiten src="https://project.webklar.com/embed/zeiten.js" data-site="${SITE_KEY}" defer></script>`),
|
||||||
|
'Einbau-Zeile als eigene Zeile'
|
||||||
|
)
|
||||||
|
assert.match(text, /automatisch eingesetzt \(nach dem nächsten Veröffentlichen prüfen\)/)
|
||||||
|
assert.doesNotMatch(text, /bitte von Hand einsetzen/)
|
||||||
|
// Platzhalter-Beispiele mit den Kurznamen der gespeicherten Pläne
|
||||||
|
assert.match(text, /<div data-webklar-zeiten data-modus="tabelle" data-plan="standard">Ersatz-HTML<\/div>/)
|
||||||
|
assert.match(text, /<div data-webklar-zeiten data-modus="status" data-plan="standard">Ersatz-HTML<\/div>/)
|
||||||
|
assert.match(text, /<div data-webklar-zeiten data-modus="badge" data-plan="standard">Ersatz-HTML<\/div>/)
|
||||||
|
assert.match(text, /Pläne \(Kurznamen für data-plan\): standard \(Hauptplan\), weihnachten/)
|
||||||
|
assert.doesNotMatch(text, /Pläne folgen/)
|
||||||
|
// Hinweis auf das Ersatz-HTML im WOMS
|
||||||
|
assert.match(text, /Ersatz-HTML: WOMS → Funktionen → Ersatz-HTML kopieren \(erst nach dem ersten Speichern des Kunden\)/)
|
||||||
|
// Prüfliste aus Spec 4.6
|
||||||
|
assert.match(text, /Prüfliste/)
|
||||||
|
assert.match(text, /grep -ril "öffnungszeit"/)
|
||||||
|
assert.match(text, /Footer/)
|
||||||
|
assert.match(text, /Kontaktseite/)
|
||||||
|
assert.match(text, /Status-Skript/)
|
||||||
|
assert.match(text, /Header/)
|
||||||
|
assert.match(text, /navSelektor/)
|
||||||
|
assert.match(text, /Anruf-Leiste/)
|
||||||
|
assert.match(text, /JSON-LD/)
|
||||||
|
assert.match(text, /Datenschutz/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('einbau manuell ⇒ „bitte von Hand einsetzen“', async () => {
|
||||||
|
for (const einbau of ['manuell', undefined, '']) {
|
||||||
|
const doc = featureDoc('p1', einbau === undefined ? {} : { einbau })
|
||||||
|
const text = await zeitenTicketDetails(ctxVon(doc))
|
||||||
|
assert.match(text, /bitte von Hand einsetzen/, `einbau=${einbau}`)
|
||||||
|
assert.doesNotMatch(text, /automatisch eingesetzt/, `einbau=${einbau}`)
|
||||||
|
assert.match(text, new RegExp(`data-site="${SITE_KEY}"`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ticket ohne gespeicherte Zeiten: Pläne folgen, Kurzname als Platzhalter', async () => {
|
||||||
|
const doc = featureDoc('p1', { einbau: 'manuell' })
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
const text = await zeitenTicketDetails(ctxVon(doc))
|
||||||
|
assert.match(text, /Pläne folgen, sobald der Kunde gespeichert hat/)
|
||||||
|
assert.match(text, /<div data-webklar-zeiten data-modus="tabelle" data-plan="<kurz>">Ersatz-HTML<\/div>/)
|
||||||
|
// ohne Dokument-ID (Notfall) und bei Lesefehlern bleibt das Ticket erhalten
|
||||||
|
const ohneId = await zeitenTicketDetails(ctxVon({ customerId: 'c1', projectId: 'p1', siteKey: SITE_KEY }))
|
||||||
|
assert.match(ohneId, /Pläne folgen, sobald der Kunde gespeichert hat/)
|
||||||
|
aw.failNext({ collection: 'oeffnungszeiten', method: 'GET', status: 500, message: 'kaputt' })
|
||||||
|
const mitFehler = await zeitenTicketDetails(ctxVon(doc))
|
||||||
|
assert.match(mitFehler, /Pläne folgen, sobald der Kunde gespeichert hat/)
|
||||||
|
assert.match(mitFehler, /Prüfliste/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ticket bleibt unter TICKET_DETAILS_MAX', async () => {
|
||||||
|
const doc = featureDoc('p1', { einbau: 'angestossen' })
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
const fuenf = eingabe(
|
||||||
|
['Montag bis Freitag', 'Samstags im Sommerhalbjahr', 'Weihnachten', 'Ostern und Pfingsten', 'Betriebsferien'].map((name, i) =>
|
||||||
|
planDaten(`p_plan_${i + 1}`, `${name} ${'x'.repeat(30)}`, { haupt: i === 0 })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await mitZeiten(doc, fuenf)
|
||||||
|
const normal = await zeitenTicketDetails(ctxVon(doc))
|
||||||
|
assert.ok(normal.length <= TICKET_DETAILS_MAX, `normal: ${normal.length}`)
|
||||||
|
assert.match(normal, /Prüfliste/, 'nichts wird abgeschnitten')
|
||||||
|
// ein riesiger Projektname schiebt den Text nie ueber den Deckel
|
||||||
|
const riesig = await zeitenTicketDetails(ctxVon(doc, { project: projekt({ projectName: 'x'.repeat(20000) }) }))
|
||||||
|
assert.ok(riesig.length <= TICKET_DETAILS_MAX, `riesig: ${riesig.length}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ticket über das Register: Zeiten-Details, nicht mehr die Notfall-Details allein', async () => {
|
||||||
|
const doc = featureDoc('p1', { einbau: 'angestossen' })
|
||||||
|
const text = await config.features.zeiten.ticket.details(ctxVon(doc))
|
||||||
|
assert.match(text, /data-wk-zeiten/)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------------------------ Ersatz-Endpunkt
|
||||||
|
|
||||||
|
test('Ersatz-Endpunkt: Team-JWT nötig, {html} als JSON, 409 ohne gespeicherte Zeiten', async () => {
|
||||||
|
const doc = featureDoc('p1')
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
// ohne Anmeldung / falsche Anmeldung
|
||||||
|
assert.equal((await ersatz('c1', `?projectId=p1`, {})).status, 401)
|
||||||
|
assert.equal((await ersatz('c1', `?projectId=p1`, { headers: { Authorization: 'Bearer jwt-unbekannt' } })).status, 401)
|
||||||
|
// Funktion da, Kunde hat noch nichts gespeichert
|
||||||
|
const leer = await ersatz('c1', '?projectId=p1')
|
||||||
|
assert.equal(leer.status, 409)
|
||||||
|
assert.deepEqual(leer.body, { error: 'Der Kunde hat noch keine Zeiten eingetragen.' })
|
||||||
|
// nach dem Speichern: Tabelle des Hauptplans
|
||||||
|
await mitZeiten(doc, ZWEI_PLAENE())
|
||||||
|
const res = await ersatz('c1', '?projectId=p1')
|
||||||
|
assert.equal(res.status, 200)
|
||||||
|
assert.match(res.headers.get('content-type'), /application\/json/)
|
||||||
|
assert.deepEqual(Object.keys(res.body), ['html'])
|
||||||
|
assert.match(res.body.html, /^<table class="wkz-ersatz">/)
|
||||||
|
assert.match(res.body.html, /<caption>Standard<\/caption>/)
|
||||||
|
assert.match(res.body.html, /<th scope="row">Montag<\/th><td>09:00–18:00<\/td>/)
|
||||||
|
assert.doesNotMatch(res.body.html, /[^\x00-\x7f]/, 'reines ASCII')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ersatz-Endpunkt: Plan per Kurzname, unbekannter Plan ⇒ 404 ohne Rückfall auf den Hauptplan (R9)', async () => {
|
||||||
|
const doc = featureDoc('p1')
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
await mitZeiten(doc, ZWEI_PLAENE())
|
||||||
|
const zweiter = await ersatz('c1', '?projectId=p1&plan=weihnachten')
|
||||||
|
assert.equal(zweiter.status, 200)
|
||||||
|
assert.match(zweiter.body.html, /<caption>Weihnachten<\/caption>/)
|
||||||
|
assert.match(zweiter.body.html, /<th scope="row">Dienstag<\/th><td>10:00–14:00<\/td>/)
|
||||||
|
// leerer Wert, falsche Schreibweise und doppelter Parameter zaehlen als angegeben und nicht gefunden
|
||||||
|
for (const query of ['&plan=gibtsnicht', '&plan=', '&plan=Standard', '&plan=standard&plan=weihnachten']) {
|
||||||
|
const res = await ersatz('c1', `?projectId=p1${query}`)
|
||||||
|
assert.equal(res.status, 404, query)
|
||||||
|
assert.deepEqual(res.body, { error: 'Plan nicht gefunden' }, query)
|
||||||
|
}
|
||||||
|
// ohne Parameter gilt der Hauptplan
|
||||||
|
const haupt = await ersatz('c1', '?projectId=p1')
|
||||||
|
assert.match(haupt.body.html, /<caption>Standard<\/caption>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ersatz-Endpunkt: nur Team-Einstellungen gespeichert ⇒ 409, Plan angegeben ⇒ ebenfalls 409 (nichts zu suchen)', async () => {
|
||||||
|
const doc = featureDoc('p1')
|
||||||
|
aw.seed('portalFeatures', doc)
|
||||||
|
await speichereTeam({ feature: doc, eingabe: { navSelektor: 'header' }, teamVersion: 0, by: 'team:u_staff2' })
|
||||||
|
const res = await ersatz('c1', '?projectId=p1')
|
||||||
|
assert.equal(res.status, 409)
|
||||||
|
assert.equal(res.body.error, 'Der Kunde hat noch keine Zeiten eingetragen.')
|
||||||
|
assert.equal((await ersatz('c1', '?projectId=p1&plan=standard')).status, 409)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Ersatz-Endpunkt: Projekt muss dem Kunden gehören, Funktionsdokument muss existieren', async () => {
|
||||||
|
aw.seed('portalFeatures', featureDoc('p1'))
|
||||||
|
aw.seed('portalFeatures', featureDoc('p3', { customerId: 'c2' }))
|
||||||
|
await mitZeiten(featureDoc('p1'), EIN_PLAN())
|
||||||
|
// Projekt fehlt im Aufruf
|
||||||
|
const ohne = await ersatz('c1', '')
|
||||||
|
assert.equal(ohne.status, 400)
|
||||||
|
assert.deepEqual(ohne.body, { error: 'Projekt fehlt' })
|
||||||
|
// fremdes Projekt / unbekanntes Projekt
|
||||||
|
assert.equal((await ersatz('c1', '?projectId=p3')).status, 403)
|
||||||
|
assert.equal((await ersatz('c1', '?projectId=gibtsnicht')).status, 403)
|
||||||
|
// eigenes Projekt ohne Funktionsdokument
|
||||||
|
const keine = await ersatz('c1', '?projectId=p2')
|
||||||
|
assert.equal(keine.status, 404)
|
||||||
|
assert.match(keine.body.error, /Öffnungszeiten live/)
|
||||||
|
// der Pfad kollidiert nicht mit den Funktionen-Routen
|
||||||
|
assert.equal((await send('GET', '/api/team/funktionen/c1', STAFF)).status, 200)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user