#!/usr/bin/env node /** * Einmalig (Kundenansicht neu, P10 Umschalten, E18): die Kundenart der Bestandseintraege. Kunden, Leads und verlorene Leads, die vor * der neuen Ansicht angelegt wurden, haben kein Feld customers.kundenart; leer gilt ueberall als privat (kundenartWirksam) und * sperrte ihnen die Funktionen fuer Betriebe. Alle bisherigen Eintraege sind Betriebe: leere Felder werden "unternehmen". * * Geschrieben wird nur dort, wo das Feld leer ist (fehlt, "" oder nur Leerzeichen). Ein gueltiger Wert ("unternehmen", "privat") und * jeder unbekannte Wert bleiben stehen. Geschrieben wird erst mit der Bestaetigungszeile "Kundenart unternehmen setzen: ", * die genau zur Zahl der leeren Eintraege passt (der Probelauf nennt sie). Ausgaben nennen nur IDs und Zahlen, nie Namen oder Adressen. * * node scripts/kundenart-bestand.mjs --pruefen (zaehlt und listet, schreibt nichts) * node scripts/kundenart-bestand.mjs --ausfuehren --bestaetigung="Kundenart unternehmen setzen: " * Im Container als Datei: docker exec -w /app node scripts/kundenart-bestand.mjs --pruefen * Die Protokolldatei entsteht per tee ausserhalb des Containers. Exit 0 bei Erfolg, 1 bei Schreibfehlern (Skript erneut ausfuehren), * 2 bei falschem Aufruf oder falscher Bestaetigung. * NICHT gegen Produktion ausfuehren, ohne dass der Probelauf geprueft und die Freigabe da ist. */ import 'dotenv/config' import { pathToFileURL } from 'node:url' import { config } from '../server/config.js' import { getDocument, listAllDocuments, updateDocument } from '../server/services/appwriteAdmin.js' import { KUNDENARTEN } from '../server/services/kundenart.js' const NEUE_ART = 'unternehmen' const AUFRUF = 'Aufruf: node scripts/kundenart-bestand.mjs --pruefen | --ausfuehren --bestaetigung="Kundenart unternehmen setzen: "' /** Falscher Aufruf oder falsche Bestaetigung (Exit 2) */ class AufrufFehler extends Error {} /** Aufrufargumente: genau --pruefen, oder --ausfuehren zusammen mit --bestaetigung=; alles andere wirft. */ export function parseArgs(argv = []) { if (argv.length === 1 && argv[0] === '--pruefen') return { modus: 'pruefen' } if (argv.length === 2 && argv.includes('--ausfuehren')) { const bestaetigung = argv.find((a) => a.startsWith('--bestaetigung='))?.slice('--bestaetigung='.length) if (bestaetigung) return { modus: 'ausfuehren', bestaetigung } } throw new AufrufFehler(AUFRUF) } export const bestaetigungsZeile = (anzahl) => `Kundenart unternehmen setzen: ${anzahl}` /** Leer = fehlt, "" oder nur Leerzeichen. Nur dann wird geschrieben. */ const istLeer = (doc) => String(doc?.kundenart ?? '').trim() === '' /** Der alte Wert fuer das Protokoll: nie der Rohwert, nur fehlt oder leer (Leerzeichen zaehlen als leer). */ const vorherText = (doc) => (doc?.kundenart === undefined || doc?.kundenart === null ? '(fehlt)' : '(leer)') /** * Liest alle Kundeneintraege und sortiert sie ein. Schreibt nichts, nennt nur IDs und Zahlen. * @returns {Promise<{ gesamt: number, mitKundenart: { unternehmen: number, privat: number }, ohne: string[], * ohneNachStatus: Record, unbekannt: string[] }>} */ export async function pruefen({ log = console.log } = {}) { const docs = await listAllDocuments(config.collections.customers) const mitKundenart = Object.fromEntries(KUNDENARTEN.map((art) => [art, 0])) const ohne = [] const ohneNachStatus = {} const unbekannt = [] for (const doc of docs) { if (istLeer(doc)) { ohne.push(doc.$id) const status = String(doc.customerStatus ?? '').trim() || '(ohne Status)' ;(ohneNachStatus[status] ??= []).push(doc.$id) } else if (KUNDENARTEN.includes(doc.kundenart)) mitKundenart[doc.kundenart] += 1 else unbekannt.push(doc.$id) } log(`Einträge gesamt: ${docs.length}`) log(`Schon gesetzt: ${KUNDENARTEN.map((art) => `${art}=${mitKundenart[art]}`).join(' ')}`) log(`Ohne Kundenart: ${ohne.length}`) log(`Unbekannte Werte: ${unbekannt.length}`) if (unbekannt.length) log(`Unbekannte Kundenart bei: ${unbekannt.join(', ')}`) for (const [status, ids] of Object.entries(ohneNachStatus)) log(`${status}: ${ids.join(', ')}`) log(bestaetigungsZeile(ohne.length)) return { gesamt: docs.length, mitKundenart, ohne, ohneNachStatus, unbekannt } } /** * Setzt kundenart auf "unternehmen" bei allen leeren Eintraegen, aber nur mit passender Bestaetigungszeile (vor jedem Schreiben geprueft). * Jeder Eintrag wird vor dem Schreiben neu gelesen; ein inzwischen gesetzter Wert bleibt. Ein Schreibfehler stoppt die uebrigen nicht. * @returns {Promise<{ gesetzt: number, uebersprungen: number, fehler: { id: string, grund: string }[] }>} */ export async function ausfuehren({ bestaetigung, log = console.log } = {}) { const { ohne } = await pruefen({ log }) const ergebnis = { gesetzt: 0, uebersprungen: 0, fehler: [] } if (ohne.length === 0) { log('Nichts zu setzen.') return ergebnis } const erwartet = bestaetigungsZeile(ohne.length) if (bestaetigung !== erwartet) throw new AufrufFehler(`Bestätigung fehlt oder stimmt nicht. Erwartet: "${erwartet}"`) log(`Schreibe ${ohne.length} Einträge …`) for (const id of ohne) { const zeit = new Date().toISOString() try { const aktuell = await getDocument(config.collections.customers, id) if (!istLeer(aktuell)) { ergebnis.uebersprungen += 1 log(`${zeit}\t${id}\t(inzwischen gesetzt)\t->\tunverändert`) continue } await updateDocument(config.collections.customers, id, { kundenart: NEUE_ART }) ergebnis.gesetzt += 1 log(`${zeit}\t${id}\t${vorherText(aktuell)}\t->\t${NEUE_ART}`) } catch (err) { ergebnis.fehler.push({ id, grund: err.message }) log(`${zeit}\t${id}\tFEHLER\t${err.message}`) } } log(`Summe gesetzt=${ergebnis.gesetzt} uebersprungen=${ergebnis.uebersprungen} fehler=${ergebnis.fehler.length}`) return ergebnis } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { try { const args = parseArgs(process.argv.slice(2)) const ergebnis = args.modus === 'pruefen' ? await pruefen({}) : await ausfuehren(args) if (ergebnis.fehler?.length) process.exitCode = 1 } catch (err) { console.error('Fehler:', err.message) process.exit(err instanceof AufrufFehler ? 2 : 1) } }