Funktionen: Terminplaner als zweite Funktion (Team-Route, Kundenansicht)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ae1886c030
commit
9e8fd33713
12 files changed
+434
-33
No files matched your search
@@ -282,6 +282,15 @@ export async function deleteCustomerWithPortalAccess(customerId) {
|
||||
console.error(`[customerAdmin] Analyse von Kunde ${customerId} nicht beendet:`, err.message)
|
||||
}
|
||||
|
||||
// Terminplaner abschalten: Gaeste koennen ihre Termine noch absagen, die Daten loescht der Tageslauf 30 Tage nach disabledAt.
|
||||
// Fehler halten das Loeschen nicht auf (wie bei der Analyse).
|
||||
try {
|
||||
const { disableCustomerFeatures } = await import('./siteFeatures.js')
|
||||
await disableCustomerFeatures(customerId, 'termine', { adminNote: 'Kunde gelöscht' })
|
||||
} catch (err) {
|
||||
console.error(`[customerAdmin] Terminplaner von Kunde ${customerId} nicht abgeschaltet:`, err.message)
|
||||
}
|
||||
|
||||
const portalAccess = await getPortalAccessByCustomerId(customerId)
|
||||
if (portalAccess) {
|
||||
await deleteDocument(
|
||||
|
||||
@@ -5,7 +5,18 @@ import { createDocument, deleteDocument, listDocuments, Query } from './appwrite
|
||||
* 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']
|
||||
export const VIEW_ACTIONS = [
|
||||
'geoeffnet',
|
||||
'beendet',
|
||||
'abgelaufen',
|
||||
'news_angelegt',
|
||||
'news_bild',
|
||||
'news_geaendert',
|
||||
'news_geloescht',
|
||||
'termin_einstellungen',
|
||||
'termin_aktion',
|
||||
'termin_manuell',
|
||||
]
|
||||
|
||||
const DETAIL_MAX = 300
|
||||
const RETENTION_MS = 365 * 24 * 60 * 60 * 1000
|
||||
|
||||
@@ -6,7 +6,12 @@ import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
|
||||
import { createFeatureTicket } from './ticketAdmin.js'
|
||||
|
||||
export const FEATURE_KEYS = ['news']
|
||||
export const FEATURE_KEYS = ['news', 'termine']
|
||||
/** Bezeichnung (name) und Verneinung im Satz "Für dieses Projekt gibt es noch {keine}" (keine) je Funktion. */
|
||||
export const FEATURE_LABELS = {
|
||||
news: { name: 'News-Pinnwand', keine: 'keine News-Pinnwand' },
|
||||
termine: { name: 'Terminplaner', keine: 'keinen Terminplaner' },
|
||||
}
|
||||
export const STATES = ['off', 'ordered', 'setup', 'active', 'disabled']
|
||||
|
||||
export const DISABLED_MESSAGE =
|
||||
@@ -119,16 +124,19 @@ async function featureView(doc) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function listCustomerFeatures(customerId) {
|
||||
/** Alle Funktionsdokumente eines Kunden (alle Projekte, alle Funktionen). */
|
||||
export function listCustomerFeatureDocs(customerId) {
|
||||
return listDocuments(config.collections.portalFeatures, [Query.equal('customerId', customerId), Query.limit(1000)])
|
||||
}
|
||||
|
||||
/** docs: bereits geladene Funktionsdokumente des Kunden (listCustomerFeatureDocs), damit sie nicht doppelt gelesen werden. */
|
||||
export async function listCustomerFeatures(customerId, { docs: geladen } = {}) {
|
||||
const projects = await listDocuments(config.collections.websiteProjects, [
|
||||
Query.equal('customerId', customerId),
|
||||
Query.orderDesc('$createdAt'),
|
||||
Query.limit(1000),
|
||||
])
|
||||
const docs = await listDocuments(config.collections.portalFeatures, [
|
||||
Query.equal('customerId', customerId),
|
||||
Query.limit(1000),
|
||||
])
|
||||
const docs = geladen ?? (await listCustomerFeatureDocs(customerId))
|
||||
const result = []
|
||||
for (const p of projects) {
|
||||
const features = {}
|
||||
@@ -287,6 +295,8 @@ export async function activate(docId, { by, adminNote } = {}) {
|
||||
return updateDocument(config.collections.portalFeatures, docId, {
|
||||
...stateFields('active'),
|
||||
activatedAt: new Date().toISOString(),
|
||||
// Eine wieder eingeschaltete Funktion darf nie in den 30-Tage-Loeschlauf des Terminplaners fallen (disabledAt-Marker)
|
||||
disabledAt: null,
|
||||
...(note ? { adminNote: note } : {}),
|
||||
})
|
||||
}
|
||||
@@ -302,6 +312,30 @@ export async function disable(docId, { by, adminNote } = {}) {
|
||||
return doc
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle noch nicht abgeschalteten Dokumente einer Funktion eines Kunden abschalten (Kunde wird geloescht). Schon abgeschaltete
|
||||
* behalten ihr disabledAt (Beginn der Loeschfrist), 'off' hat nichts zu sperren. Scheitert eines, werden die uebrigen trotzdem
|
||||
* abgeschaltet und danach der erste Fehler weitergereicht.
|
||||
* @returns {Promise<object[]>} die abgeschalteten Dokumente
|
||||
*/
|
||||
export async function disableCustomerFeatures(customerId, key, { by, adminNote } = {}) {
|
||||
assertKey(key)
|
||||
const docs = (await listCustomerFeatureDocs(customerId)).filter(
|
||||
(d) => d.featureKey === key && d.state !== 'off' && d.state !== 'disabled'
|
||||
)
|
||||
const disabled = []
|
||||
let failure = null
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
disabled.push(await disable(doc.$id, { by, adminNote }))
|
||||
} catch (err) {
|
||||
failure ||= err
|
||||
}
|
||||
}
|
||||
if (failure) throw failure
|
||||
return disabled
|
||||
}
|
||||
|
||||
// Analyse gesperrt (auch Loeschfrist): Sammeldienst abschalten, Neubau und Entfernen-Ticket. Best effort, Zustand ist schon gespeichert.
|
||||
// Dynamischer Import: analyse.js importiert dieses Modul.
|
||||
async function stopAnalyseCollecting(doc) {
|
||||
@@ -352,11 +386,12 @@ export async function activateClosedTickets({ now = new Date(), customerId = nul
|
||||
return activated
|
||||
}
|
||||
|
||||
export async function findActiveBySiteKey(siteKey) {
|
||||
if (!/^[a-z2-7]{16}$/.test(String(siteKey || ''))) return null
|
||||
/** Aktive Funktion zu einem siteKey; ohne featureKey 'news' (Pinnwand), unbekannte Funktion oder siteKey => null. */
|
||||
export async function findActiveBySiteKey(siteKey, featureKey = 'news') {
|
||||
if (!/^[a-z2-7]{16}$/.test(String(siteKey || '')) || !FEATURE_KEYS.includes(featureKey)) return null
|
||||
const [doc] = await listDocuments(config.collections.portalFeatures, [
|
||||
Query.equal('siteKey', siteKey),
|
||||
Query.equal('featureKey', 'news'),
|
||||
Query.equal('featureKey', featureKey),
|
||||
Query.equal('state', 'active'),
|
||||
Query.limit(1),
|
||||
])
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { config } from '../../config.js'
|
||||
import { Query, createDocument, getDocument, listDocuments, updateDocument } from '../appwriteAdmin.js'
|
||||
import { createDocument, getDocument, updateDocument } from '../appwriteAdmin.js'
|
||||
import { analyseHosts } from '../analyse.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { findActiveBySiteKey } from '../siteFeatures.js'
|
||||
import { termineAus } from './schalter.js'
|
||||
import { pruefeNow, pruefeText } from './util.js'
|
||||
|
||||
@@ -95,12 +96,7 @@ export async function kontextPerSiteKey(siteKey, now = new Date()) {
|
||||
if (gemerkt && gemerkt.bis > nowMs) return gemerkt.ctx
|
||||
|
||||
const stand = generation
|
||||
const [feature] = await listDocuments(config.collections.portalFeatures, [
|
||||
Query.equal('siteKey', siteKey),
|
||||
Query.equal('featureKey', FUNKTION),
|
||||
Query.equal('state', 'active'),
|
||||
Query.limit(1),
|
||||
])
|
||||
const feature = await findActiveBySiteKey(siteKey, FUNKTION)
|
||||
let ctx = feature ? await baueKontext(feature) : null
|
||||
if (ctx && !ctx.einstellung) ctx = null
|
||||
if (stand === generation) merke(siteKey, ctx, nowMs)
|
||||
|
||||
Reference in new issue
Block a user