Files
Webklar-Kundenbereich/server/services/customerAdmin.js
T
ClaudeandClaude Sonnet 5.5 91b1a3bcd8 Terminplaner TP2: Entwuerfe nach 90 Tagen und bei Kundenloeschung entfernen
Neu: server/services/termine/entwuerfe.js. Ein Terminplaner im Zustand off ist ein
Entwurf (Fragebogen, Einstellung). loescheEntwuerfe(now) im Tagesjob (nach den
abgeschalteten Funktionen, nur im vollen Lauf): letzte Aenderung = juengerer Zeitpunkt
aus $updatedAt des Funktionsdokuments und termineConfig.updatedAt, aelter als 90 Tage
=> Einstellung weg, das Funktionsdokument nur, wenn es nie in Betrieb oder bezahlt war
(sonst bleibt es wegen des siteKeys; ein von E8 zurueckgesetztes Dokument nie).
Vor jedem Loeschen unter feature-order, feature-admin und termine-cfg frisch lesen
(noch off, Frist, keine Buchung). Seiten zu 100 per cursorAfter, hoechstens 2000
geprueft und 100 geloescht je Lauf, der Rest folgt an Folgetagen; Einzelfehler zaehlen
in fehler. Tagesbericht: Segment "Entwuerfe n geloescht (m offen)".
loescheEntwuerfeVonKunde: Kundenloeschung entfernt alle off-Entwuerfe vollstaendig,
Fehler werden nur protokolliert.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-04 20:33:33 +00:00

316 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config } from '../config.js'
import {
createAppwriteUser,
createDocument,
deleteDocument,
getCustomerByEmail,
getDocument,
getPortalAccessByCustomerId,
listDocuments,
updateAppwriteUserEmail,
updateAppwriteUserPassword,
deleteAppwriteUser,
updateDocument,
Query,
} from './appwriteAdmin.js'
import { forgetPasswordVersion } from './passwordVersion.js'
function readPortalPassword(customer) {
if (customer?.portalPassword) return customer.portalPassword
// Fallback: Altbestand, bei dem das Passwort noch in den Notizen lag
const notes = customer?.notes || ''
const match = notes.match(/^__portalPassword__:([^\n]+)/)
return match ? match[1] : ''
}
function notesWithoutPortalPassword(notes) {
return (notes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim()
}
function sanitizeCustomer(customer, { portalPassword } = {}) {
const storedPassword = portalPassword || readPortalPassword(customer)
return {
$id: customer.$id,
code: customer.code || '',
name: customer.name || '',
location: customer.location || '',
email: customer.email || '',
phone: customer.phone || '',
companyName: customer.companyName || '',
portalAccessEnabled: Boolean(customer.portalAccessEnabled),
appwriteUserId: customer.appwriteUserId || '',
customerStatus: customer.customerStatus || '',
archived: Boolean(customer.archived),
portalPassword: storedPassword || '',
}
}
async function ensurePortalAccess(customerId, appwriteUserId) {
const now = new Date().toISOString()
let portalAccess = await getPortalAccessByCustomerId(customerId)
if (portalAccess) {
portalAccess = await updateDocument(config.collections.customerPortalAccess, portalAccess.$id, {
enabled: true,
appwriteUserId,
passwordSet: true,
})
} else {
portalAccess = await createDocument(config.collections.customerPortalAccess, {
customerId,
enabled: true,
appwriteUserId,
passwordSet: true,
})
}
return portalAccess
}
export async function createCustomerWithPortalAccess(payload) {
const {
code = '',
name,
location = '',
email,
phone = '',
password,
} = payload || {}
if (!name?.trim()) {
const error = new Error('Name ist erforderlich')
error.status = 400
throw error
}
if (!email?.trim()) {
const error = new Error('E-Mail ist erforderlich')
error.status = 400
throw error
}
if (!password || password.length < 8) {
const error = new Error('Passwort muss mindestens 8 Zeichen haben')
error.status = 400
throw error
}
const existing = await getCustomerByEmail(email.trim())
if (existing) {
const error = new Error('Ein Kunde mit dieser E-Mail existiert bereits')
error.status = 409
throw error
}
const appwriteUser = await createAppwriteUser({
email: email.trim(),
password,
name: name.trim(),
})
const now = new Date().toISOString()
let customer
try {
customer = await createDocument(config.collections.customers, {
code: code.trim(),
name: name.trim(),
location: location.trim(),
email: email.trim(),
phone: phone.trim(),
portalPassword: password,
portalAccessEnabled: true,
appwriteUserId: appwriteUser.$id,
customerStatus: 'active',
createdAt: now,
updatedAt: now,
})
await ensurePortalAccess(customer.$id, appwriteUser.$id)
} catch (err) {
try {
await deleteAppwriteUser(appwriteUser.$id)
} catch {
/* ignore rollback failure */
}
throw err
}
return {
customer: sanitizeCustomer(customer, { portalPassword: password }),
}
}
export async function updateCustomerWithPortalAccess(customerId, payload) {
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
if (!customer) {
const error = new Error('Kunde nicht gefunden')
error.status = 404
throw error
}
const {
code,
name,
location,
email,
phone,
password,
} = payload || {}
const nextEmail = (email ?? customer.email ?? '').trim()
if (!nextEmail) {
const error = new Error('E-Mail ist erforderlich')
error.status = 400
throw error
}
if (password && password.length < 8) {
const error = new Error('Passwort muss mindestens 8 Zeichen haben')
error.status = 400
throw error
}
const normalizedPassword =
typeof password === 'string' && password.trim().length > 0 ? password : undefined
if (nextEmail.toLowerCase() !== (customer.email || '').trim().toLowerCase()) {
const existing = await getCustomerByEmail(nextEmail)
if (existing && existing.$id !== customerId) {
const error = new Error('Ein anderer Kunde nutzt diese E-Mail bereits')
error.status = 409
throw error
}
}
let appwriteUserId = customer.appwriteUserId || ''
let portalPassword
if (!appwriteUserId && nextEmail && normalizedPassword) {
const appwriteUser = await createAppwriteUser({
email: nextEmail,
password: normalizedPassword,
name: (name ?? customer.name ?? nextEmail).trim(),
})
appwriteUserId = appwriteUser.$id
portalPassword = normalizedPassword
} else if (appwriteUserId) {
if (nextEmail && nextEmail !== customer.email) {
await updateAppwriteUserEmail(appwriteUserId, nextEmail)
}
if (normalizedPassword) {
await updateAppwriteUserPassword(appwriteUserId, normalizedPassword)
// Neues Passwort von uns: der Kunde ist auf allen Geraeten abgemeldet (passwordVersion.js)
forgetPasswordVersion(appwriteUserId)
portalPassword = normalizedPassword
}
} else if (normalizedPassword) {
const error = new Error('Portal-Login fehlt � bitte E-Mail setzen und Passwort erneut speichern')
error.status = 400
throw error
}
const now = new Date().toISOString()
const nextPortalPassword =
normalizedPassword || readPortalPassword(customer) || undefined
// Legacy-Prefix aus den Notizen entfernen, Passwort liegt jetzt im eigenen Feld
const cleanedNotes = notesWithoutPortalPassword(customer.notes)
const notesChanged = cleanedNotes !== (customer.notes || '')
const updated = await updateDocument(config.collections.customers, customerId, {
...(code !== undefined ? { code: String(code).trim() } : {}),
...(name !== undefined ? { name: String(name).trim() } : {}),
...(location !== undefined ? { location: String(location).trim() } : {}),
email: nextEmail,
...(phone !== undefined ? { phone: String(phone).trim() } : {}),
...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}),
...(notesChanged ? { notes: cleanedNotes } : {}),
// CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer
...(appwriteUserId
? {
appwriteUserId,
portalAccessEnabled: true,
...(customer.customerStatus ? {} : { customerStatus: 'active' }),
}
: {}),
updatedAt: now,
})
if (appwriteUserId) {
await ensurePortalAccess(customerId, appwriteUserId)
}
return {
customer: sanitizeCustomer(updated, portalPassword ? { portalPassword } : {}),
}
}
/**
* Der Kunde hat sein Passwort im Kundenbereich selbst geaendert: die Klartext-Kopie, die das
* Ticketsystem zum Weitergeben anzeigt, stimmt nicht mehr. Sie wird geleert statt ueberschrieben,
* denn ein selbst gewaehltes Passwort gehoert nirgends im Klartext hin. Das Ticketsystem zeigt dann "–".
* @returns {Promise<boolean>} true, wenn eine Kopie entfernt wurde
*/
export async function forgetStoredPortalPassword(customerId) {
const customer = await getDocument(config.collections.customers, customerId)
const legacyInNotes = /^__portalPassword__:/.test(customer.notes || '')
if (!customer.portalPassword && !legacyInNotes) return false
await updateDocument(config.collections.customers, customerId, {
portalPassword: '',
...(legacyInNotes ? { notes: notesWithoutPortalPassword(customer.notes) } : {}),
updatedAt: new Date().toISOString(),
})
return true
}
export async function listCustomersForAdmin() {
// Ohne Limit liefert Appwrite nur 25 Dokumente - neue Kunden fielen dann aus der Liste.
const customers = await listDocuments(config.collections.customers, [
Query.orderAsc('name'),
Query.limit(5000),
])
return customers.map((customer) => sanitizeCustomer(customer))
}
export async function deleteCustomerWithPortalAccess(customerId) {
const customer = await getDocument(config.collections.customers, customerId)
.catch(() => null)
if (!customer) {
const error = new Error('Kunde nicht gefunden')
error.status = 404
throw error
}
// Website-Analyse zuerst abschalten (kein Ticket, keine Mail); Fehler halten das Loeschen nicht auf, der taegliche Abgleich holt nach
try {
const { endAnalyseForCustomer } = await import('./analyse.js')
await endAnalyseForCustomer(customerId, { reason: 'kunde_geloescht', mail: false })
} catch (err) {
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', reason: 'kunde_geloescht' })
} catch (err) {
console.error(`[customerAdmin] Terminplaner von Kunde ${customerId} nicht abgeschaltet:`, err.message)
}
// Entwuerfe (Terminplaner im Zustand off: nie bestellt oder nach dem Abo-Ende zurueckgesetzt) gehen sofort mit dem Kunden, Funktionsdokument
// und Einstellung. Fehler halten das Loeschen nicht auf (wie oben); ein uebrig gebliebener Entwurf faellt nach 90 Tagen im Tageslauf.
try {
const { loescheEntwuerfeVonKunde } = await import('./termine/entwuerfe.js')
await loescheEntwuerfeVonKunde(customerId)
} catch (err) {
console.error(`[customerAdmin] Entwürfe von Kunde ${customerId} nicht gelöscht:`, err.message)
}
const portalAccess = await getPortalAccessByCustomerId(customerId)
if (portalAccess) {
await deleteDocument(
config.collections.customerPortalAccess,
portalAccess.$id
)
}
await deleteDocument(config.collections.customers, customerId)
if (customer.appwriteUserId) {
await deleteAppwriteUser(customer.appwriteUserId).catch(() => {})
}
return { success: true }
}