136 lines
5.6 KiB
JavaScript
136 lines
5.6 KiB
JavaScript
import { Router } from 'express'
|
||
import { config } from '../../config.js'
|
||
import { getDocument } from '../../services/appwriteAdmin.js'
|
||
import { withLocks } from '../../services/keyedLock.js'
|
||
import {
|
||
FEATURE_KEYS,
|
||
FEATURE_LABELS,
|
||
adminActivate,
|
||
assertOwnProject,
|
||
disable,
|
||
getFeatureDoc,
|
||
listCustomerFeatureDocs,
|
||
listCustomerFeatures,
|
||
} 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").
|
||
* Eingehaengt in routes/team/index.js unter /api/team/funktionen, dort hinter requireStaffJwt.
|
||
*/
|
||
export const funktionenAdminRouter = Router()
|
||
|
||
const NOTE_MAX = 300
|
||
const adminName = (req) => req.session.adminName || req.session.name || 'Admin'
|
||
|
||
function sendError(res, err) {
|
||
const status = err.status >= 400 && err.status < 600 ? err.status : 500
|
||
if (status >= 500) console.error('[funktionen-admin]', err.message)
|
||
return res.status(status).json({ error: err.message || 'Interner Serverfehler' })
|
||
}
|
||
|
||
/** Neuester Eintrag zuerst; aeltere Eintraege fallen vorn heraus, sobald 300 Zeichen voll sind. */
|
||
export function composeAdminNote(previous, { by, action, note }, now = new Date()) {
|
||
const entry = `${now.toISOString()} ${by}: ${action}${note ? ` – ${note}` : ''}`.slice(0, NOTE_MAX)
|
||
const older = String(previous || '')
|
||
return older ? `${entry} | ${older}`.slice(0, NOTE_MAX) : entry
|
||
}
|
||
|
||
async function ticketExists(ticketId) {
|
||
if (!ticketId) return false
|
||
try {
|
||
await getDocument(config.collections.workorders, ticketId)
|
||
return true
|
||
} catch (err) {
|
||
if (err.status === 404) return false
|
||
throw err
|
||
}
|
||
}
|
||
|
||
// Die Funktionsdokumente des Kunden werden einmal gelesen und fuer alle Projekte und Funktionen verwendet
|
||
async function adminView(customerId) {
|
||
const docs = await listCustomerFeatureDocs(customerId)
|
||
const projects = await listCustomerFeatures(customerId, { docs })
|
||
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)))
|
||
}
|
||
}
|
||
return projects
|
||
}
|
||
|
||
funktionenAdminRouter.get('/:customerId', async (req, res) => {
|
||
try {
|
||
res.json({ projects: await adminView(req.params.customerId) })
|
||
} catch (err) {
|
||
sendError(res, err)
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 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
|
||
funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
|
||
try {
|
||
const { customerId, key } = req.params
|
||
if (!FEATURE_KEYS.includes(key)) return res.status(404).json({ error: 'Unbekannte Funktion' })
|
||
const { projectId, action } = req.body || {}
|
||
const note = typeof req.body?.note === 'string' ? req.body.note.trim().slice(0, 120) : ''
|
||
if (action !== 'activate' && action !== 'disable') {
|
||
return res.status(400).json({ error: 'Unbekannte Aktion' })
|
||
}
|
||
if (typeof projectId !== 'string' || !projectId) {
|
||
return res.status(400).json({ error: 'Projekt fehlt' })
|
||
}
|
||
await assertOwnProject(customerId, projectId)
|
||
const by = adminName(req)
|
||
await withLocks([`feature-admin:${projectId}`], async () => {
|
||
const doc = await getFeatureDoc(customerId, projectId, key)
|
||
const adminNote = composeAdminNote(doc?.adminNote, { by, action, note })
|
||
if (action === 'activate') {
|
||
await adminActivate(customerId, projectId, key, { by, adminNote })
|
||
} else {
|
||
if (!doc) {
|
||
throw Object.assign(new Error(`Für dieses Projekt gibt es noch ${FEATURE_LABELS[key].keine}`), { status: 409 })
|
||
}
|
||
await disable(doc.$id, { by, adminNote })
|
||
}
|
||
})
|
||
res.json({ projects: (await adminView(customerId)).filter((p) => p.projectId === projectId) })
|
||
} catch (err) {
|
||
sendError(res, err)
|
||
}
|
||
})
|