Funktionen: Team-Route fuer die Terminplaner-Einbauanleitung
GET /api/team/funktionen/:customerId/termine/anleitung?projectId= liefert die volle Einbauanleitung (ohne kurz) fuer das WOMS-Panel: stand, warnungen mit Drift-Warnung gegen die Version im Ticket, abschnitte, text, state, ticketWoid, ticketVersion und hindernis (nur in setup). Nur lesend, in jedem Zustand und Modus, ohne customerViewLog. Die Teamansicht GET /:customerId traegt hindernis je Terminplaner in setup, mit einem Abo-Lauf je Anfrage und ohne portalFeatures erneut zu lesen. Die Einbau-Doku nennt die Anleitung je Kunde im WOMS-Panel und die Kurzfassung im Ticket. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f830698dc3
commit
938b7099dd
3 files changed
+399
-7
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { Router } from 'express'
|
||||
import { config } from '../../config.js'
|
||||
import { aboMemo } from '../../services/aboStatus.js'
|
||||
import { getDocument } from '../../services/appwriteAdmin.js'
|
||||
import { featureConfig } from '../../services/featureRegistry.js'
|
||||
import { withLocks } from '../../services/keyedLock.js'
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
FEATURE_LABELS,
|
||||
adminActivate,
|
||||
assertOwnProject,
|
||||
customerOf,
|
||||
disable,
|
||||
freischaltenNachAbnahme,
|
||||
getFeatureDoc,
|
||||
@@ -15,6 +17,9 @@ import {
|
||||
listCustomerFeatures,
|
||||
ticketNachlegen,
|
||||
} from '../../services/siteFeatures.js'
|
||||
import { driftWarnung, einbauanleitung, ticketStand } from '../../services/termine/einbauanleitung.js'
|
||||
import { freischaltHindernis } from '../../services/termine/freischaltung.js'
|
||||
import { ladeStand } from '../../services/termine/kontext.js'
|
||||
|
||||
/**
|
||||
* Funktionen (News-Pinnwand, Terminplaner) je Kundenprojekt im Ticketsystem (Kunden-Reiter "Funktionen").
|
||||
@@ -38,27 +43,44 @@ export function composeAdminNote(previous, { by, action, note }, now = new Date(
|
||||
return older ? `${entry} | ${older}`.slice(0, NOTE_MAX) : entry
|
||||
}
|
||||
|
||||
async function ticketExists(ticketId) {
|
||||
if (!ticketId) return false
|
||||
/** Das Ticket oder null (keine ticketId, Ticket geloescht); jeder andere Lesefehler wird weitergereicht. */
|
||||
async function readTicket(ticketId) {
|
||||
if (!ticketId) return null
|
||||
try {
|
||||
await getDocument(config.collections.workorders, ticketId)
|
||||
return true
|
||||
return await getDocument(config.collections.workorders, ticketId)
|
||||
} catch (err) {
|
||||
if (err.status === 404) return false
|
||||
if (err.status === 404) return null
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// Die Funktionsdokumente des Kunden werden einmal gelesen und fuer alle Projekte und Funktionen verwendet
|
||||
/** Name des Kunden fuer die Anleitung; wie im Ticket (termineTicketDetails) die Kunden-ID, wenn er fehlt oder nicht lesbar ist (nur Kosmetik, kein Fehler). */
|
||||
async function customerName(customerId) {
|
||||
try {
|
||||
return (await customerOf(customerId))?.name || customerId
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-admin] Kunde ${customerId} fuer die Anleitung nicht lesbar:`, err.message)
|
||||
return customerId
|
||||
}
|
||||
}
|
||||
|
||||
const ticketExists = async (ticketId) => (await readTicket(ticketId)) !== null
|
||||
|
||||
// Die Funktionsdokumente des Kunden werden einmal gelesen und fuer alle Projekte und Funktionen verwendet.
|
||||
// hindernis (freischaltHindernis, termine/freischaltung.js): was die Freischaltung eines Terminplaners in setup noch hindert
|
||||
// ('kein_abo' | 'keine_einstellung' | 'abo_unbekannt'), null = nichts; ausserhalb von setup und bei News immer null. Das Abo wird
|
||||
// je Anfrage einmal gelesen (ein gemeinsamer aboMemo fuer alle Projekte des Kunden), portalFeatures nicht erneut.
|
||||
async function adminView(customerId) {
|
||||
const docs = await listCustomerFeatureDocs(customerId)
|
||||
const projects = await listCustomerFeatures(customerId, { docs })
|
||||
const abo = aboMemo()
|
||||
for (const p of projects) {
|
||||
for (const key of FEATURE_KEYS) {
|
||||
const view = p.features[key]
|
||||
const doc = docs.find((d) => d.projectId === p.projectId && d.featureKey === key)
|
||||
view.adminNote = doc?.adminNote || ''
|
||||
view.ticketMissing = view.state === 'setup' && !(doc?.ticketId && (await ticketExists(doc.ticketId)))
|
||||
view.hindernis = key === 'termine' && doc?.state === 'setup' ? await freischaltHindernis(doc, { abo }) : null
|
||||
}
|
||||
}
|
||||
return projects
|
||||
@@ -72,6 +94,55 @@ funktionenAdminRouter.get('/:customerId', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Einbauanleitung des Terminplaners eines Projekts fuer das WOMS-Panel (Task D8): die volle Fassung des Generators ohne `kurz` (die geht nur
|
||||
* ins Ticket), nur lesend. Gilt in jedem Zustand und bei jedem Modus (kein TERMINE_ENABLED-Gate) und ist kein Kundenzugriff (kein
|
||||
* customerViewLog, kein Pfad im Kundenrouter). Rechnung und Preis kommen live aus dem Dokument.
|
||||
* state: Zustand des Dokuments. ticketWoid, ticketVersion: Ticket des Dokuments und der Stand, auf dem es beruht (null ohne Ticket oder
|
||||
* ohne Standzeile). hindernis: nur in setup, was die Freischaltung noch hindert (siehe adminView), sonst null.
|
||||
* Ein gespeicherter Stand, der neuer ist als das Ticket, haengt eine Warnung ans Ende von warnungen (driftWarnung); ein geloeschtes
|
||||
* Ticket (404) ist keine Warnung und kein Fehler, jeder andere Lesefehler ist einer (keine stille Luecke).
|
||||
*/
|
||||
funktionenAdminRouter.get('/:customerId/termine/anleitung', async (req, res) => {
|
||||
try {
|
||||
const { customerId } = req.params
|
||||
const { projectId } = req.query
|
||||
if (typeof projectId !== 'string' || !projectId) return res.status(400).json({ error: 'Projekt fehlt' })
|
||||
const project = await assertOwnProject(customerId, projectId)
|
||||
const doc = await getFeatureDoc(customerId, projectId, 'termine')
|
||||
if (!doc) {
|
||||
throw Object.assign(new Error(`Für dieses Projekt gibt es noch ${FEATURE_LABELS.termine.keine}`), { status: 409 })
|
||||
}
|
||||
const stand = await ladeStand(doc.$id)
|
||||
const ticket = await readTicket(doc.ticketId)
|
||||
const ticketVersion = ticket ? ticketStand(ticket.details) : null
|
||||
const { stand: anleitungsStand, warnungen, abschnitte, text } = einbauanleitung({
|
||||
feature: doc,
|
||||
project,
|
||||
einstellung: stand?.einstellung ?? null,
|
||||
fragebogen: stand?.fragebogen ?? null,
|
||||
version: stand?.version ?? 0,
|
||||
updatedAt: stand?.updatedAt ?? null,
|
||||
kunde: { name: await customerName(customerId) },
|
||||
now: new Date(),
|
||||
})
|
||||
const drift = driftWarnung(ticketVersion, anleitungsStand.version)
|
||||
// Felder einzeln benannt: `kurz` (nur fuers Ticket) und alles, was der Generator spaeter dazubekommt, geht nie unbesehen nach aussen
|
||||
res.json({
|
||||
stand: anleitungsStand,
|
||||
warnungen: drift ? [...warnungen, drift] : warnungen,
|
||||
abschnitte,
|
||||
text,
|
||||
state: doc.state,
|
||||
ticketWoid: ticket?.woid || null,
|
||||
ticketVersion,
|
||||
hindernis: doc.state === 'setup' ? await freischaltHindernis(doc) : null,
|
||||
})
|
||||
} catch (err) {
|
||||
sendError(res, err)
|
||||
}
|
||||
})
|
||||
|
||||
// Je Funktion (/news, /termine; jeder andere Schluessel -> 404): activate (ohne Zahlung), disable, abnahme (Freischalten nach Abnahme, in
|
||||
// setup, ohne das Ticket zu schliessen) und ticket (fehlendes Einrichtungs-Ticket nachlegen, in setup). abnahme und ticket gibt es nur fuer
|
||||
// Funktionen mit ticketFreischaltung im Register.
|
||||
|
||||
Reference in new issue
Block a user