Files
ClaudeandClaude Opus 5.5 e87140e5ba Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.

Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.

Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 06:12:23 +00:00

68 lines
2.3 KiB
JavaScript

import { config } from '../config.js'
import { createDocument, deleteDocument, listDocuments, Query } from './appwriteAdmin.js'
/**
* Protokoll der Kundenansicht (Datenschutzerklaerung Abschnitt 6): wer wann das Portal eines Kunden geoeffnet
* oder darin etwas geaendert hat. Schreiben nur ueber den Server-Schluessel; Eintraege leben 12 Monate.
*/
export const VIEW_ACTIONS = [
'geoeffnet',
'beendet',
'abgelaufen',
'news_angelegt',
'news_bild',
'news_geaendert',
'news_geloescht',
'termin_einstellungen',
'termin_aktion',
'termin_manuell',
'shop_aenderung',
'zeiten_gespeichert',
]
const DETAIL_MAX = 300
const RETENTION_MS = 365 * 24 * 60 * 60 * 1000
const PAGE = 100
const COLLECTION = () => config.collections.customerViewLog
/** Eintrag schreiben; wirft nie (die protokollierte Aktion selbst bleibt gueltig). */
export async function logViewAction({ customerId, staffUserId, staffName, action, detail = '' }) {
try {
await createDocument(COLLECTION(), {
customerId: String(customerId || ''),
staffUserId: String(staffUserId || ''),
staffName: String(staffName || ''),
action: String(action || ''),
detail: String(detail || '').slice(0, DETAIL_MAX),
createdAt: new Date().toISOString(),
})
} catch (err) {
console.warn(`[kundenansicht] Protokoll (${action}, Kunde ${customerId}) nicht geschrieben:`, err.message)
}
}
/** Eintraege eines Kunden, neueste zuerst. */
export async function listViewLog(customerId, { limit = 200 } = {}) {
const docs = await listDocuments(COLLECTION(), [
Query.equal('customerId', customerId),
Query.orderDesc('createdAt'),
Query.limit(limit),
])
return docs.map((d) => ({ id: d.$id, at: d.createdAt, staffName: d.staffName || '', action: d.action || '', detail: d.detail || '' }))
}
/** Eintraege aelter als 12 Monate loeschen (taeglicher Lauf), seitenweise bis keine mehr uebrig sind. */
export async function purgeViewLog({ now = new Date() } = {}) {
const cutoff = new Date(now.getTime() - RETENTION_MS).toISOString()
let deleted = 0
for (;;) {
const docs = await listDocuments(COLLECTION(), [Query.lessThan('createdAt', cutoff), Query.limit(PAGE)])
for (const doc of docs) {
await deleteDocument(COLLECTION(), doc.$id)
deleted += 1
}
if (docs.length < PAGE) return { deleted }
}
}