In der neuen Ansicht trägt jede Funktion unter projects[].features.<key> zusätzlich kurzinfo (Text oder null). Nur aktive Funktionen bekommen einen Text: Terminplaner "3 Termine heute · nächster 10:30", Webshop "2 neue Bestellungen", News-Pinnwand "4 Beiträge sichtbar", Buchungskalender "1 offene Anfrage" bzw. "Nächste Anreise 24.10.", Öffnungszeiten live der Status der Website (ohne eigene Abfrage). Berliner Zeit, je Funktion höchstens zwei schmale Abfragen (limit, select, Index-Felder), alles gleichzeitig; jede Funktion einzeln begrenzt (1,5 s) und fehlertolerant: Fehler oder Zeitüberschreitung ergibt null. Ohne neue Ansicht bleibt die Antwort wie sie war und es wird nichts gelesen. Die Analyse hat keine Kurzinfo (Zahlen kommen aus dem Auswertungsdienst). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
159 lines
7.7 KiB
JavaScript
159 lines
7.7 KiB
JavaScript
import { config } from '../../config.js'
|
|
import { Query, countDocuments, listDocuments } from '../appwriteAdmin.js'
|
|
import { isHanging } from '../newsPosts.js'
|
|
import { heute, plusTage, uhrzeit, zuUtc } from '../termine/zeit.js'
|
|
|
|
/**
|
|
* Kurzinfo der aktiven Funktionen fuer GET /api/funktionen in der neuen Ansicht: je Projekt und Funktion `kurzinfo`, ein kurzer
|
|
* Text fuer die zweite Zeile in "Meine" ("3 Termine heute · nächster 10:30", "2 neue Bestellungen", ...) oder null. Berliner Zeit.
|
|
*
|
|
* Regeln:
|
|
* - Nur Funktionen im Zustand active bekommen einen Text (und eine Abfrage); alle anderen null. Die Analyse hat keine Kurzinfo
|
|
* (ihre Zahlen kommen aus dem Auswertungsdienst, nicht aus Appwrite: eine Netzabfrage je Seitenaufruf lohnt dafuer nicht).
|
|
* - Je Funktion hoechstens zwei schmale Abfragen (limit, select, Filter auf Index-Felder), alle Funktionen und Websites gleichzeitig.
|
|
* - Jede Funktion ist einzeln begrenzt (kurzinfoDeps.limitMs) und fehlertolerant: Fehler oder Zeitueberschreitung ergibt null, die
|
|
* Oberflaeche zeigt dann "Aktiv". Im Log stehen nur Funktion, Art und Status, nie Text oder Daten (Gaeste, Kaeufer).
|
|
* - Ist eine Seite voll (limit erreicht), ist die Zahl unbekannt: null statt einer falschen Zahl.
|
|
* - Die Texte nennen weder Namen noch Adressen noch Betraege.
|
|
*/
|
|
|
|
/** Einhaengepunkt fuer Tests: Zeitgrenze je Funktion in Millisekunden. */
|
|
export const kurzinfoDeps = { limitMs: 1500 }
|
|
|
|
const SEITE = 100
|
|
const SEITE_NEWS = 200
|
|
const MAX_ZEICHEN = 80
|
|
|
|
const mehrzahl = (n, eins, viele) => `${n} ${n === 1 ? eins : viele}`
|
|
const istText = (wert) => typeof wert === 'string' && wert.trim() !== ''
|
|
const zeitMs = (wert) => Date.parse(wert)
|
|
|
|
/** Texte der Funktionen (rein): Eingabe sind die gezaehlten Werte, Ausgabe der Satz. */
|
|
export const KURZINFO_TEXTE = {
|
|
termine: ({ anzahl, naechster }) =>
|
|
anzahl === 0 ? 'Heute keine Termine' : `${mehrzahl(anzahl, 'Termin', 'Termine')} heute${naechster ? ` · nächster ${naechster}` : ''}`,
|
|
shop: ({ anzahl }) => (anzahl === 0 ? 'Keine offenen Bestellungen' : `${mehrzahl(anzahl, 'neue Bestellung', 'neue Bestellungen')}`),
|
|
news: ({ anzahl }) => (anzahl === 0 ? 'Keine Beiträge sichtbar' : `${mehrzahl(anzahl, 'Beitrag', 'Beiträge')} sichtbar`),
|
|
unterkunft: ({ offen, anreise }) => {
|
|
if (offen > 0) return mehrzahl(offen, 'offene Anfrage', 'offene Anfragen')
|
|
return anreise ? anreise : 'Keine offenen Anfragen'
|
|
},
|
|
zeitenLeer: () => 'Noch keine Öffnungszeiten eingetragen',
|
|
}
|
|
|
|
/** 'JJJJ-MM-TT' → 'TT.MM.' */
|
|
const tagKurz = (tag) => `${tag.slice(8, 10)}.${tag.slice(5, 7)}.`
|
|
|
|
/** Terminplaner: bestaetigte und erledigte Termine von heute (Berliner Tag) ohne Sperren; der naechste beginnt nach jetzt. */
|
|
async function termine({ dok, jetzt }) {
|
|
if (!dok) return null
|
|
const tag = heute(jetzt)
|
|
const von = zuUtc(tag, '00:00')
|
|
const bis = zuUtc(plusTage(tag, 1), '00:00')
|
|
if (!von || !bis) return null
|
|
const liste = await listDocuments(config.collections.termineBookings, [
|
|
Query.equal('featureId', dok.$id),
|
|
Query.equal('status', ['bestaetigt', 'erledigt']),
|
|
Query.greaterThanEqual('startsAt', von.toISOString()),
|
|
Query.lessThan('startsAt', bis.toISOString()),
|
|
Query.limit(SEITE),
|
|
Query.select(['startsAt', 'quelle']),
|
|
])
|
|
if (liste.length >= SEITE) return null
|
|
const starts = liste.filter((b) => b.quelle !== 'sperre').map((b) => zeitMs(b.startsAt)).filter(Number.isFinite)
|
|
const spaeter = starts.filter((ms) => ms > jetzt.getTime())
|
|
const naechster = spaeter.length ? uhrzeit(new Date(Math.min(...spaeter))) : ''
|
|
return KURZINFO_TEXTE.termine({ anzahl: starts.length, naechster })
|
|
}
|
|
|
|
/** Webshop: bezahlte Bestellungen, die noch niemand bearbeitet hat (nur die Anzahl, total der Antwort). */
|
|
async function shop({ dok }) {
|
|
if (!dok) return null
|
|
const anzahl = await countDocuments(config.collections.shopOrders, [Query.equal('shopId', dok.$id), Query.equal('status', 'bezahlt')])
|
|
return Number.isInteger(anzahl) ? KURZINFO_TEXTE.shop({ anzahl }) : null
|
|
}
|
|
|
|
/** News-Pinnwand: haengende Beitraege dieser Website (Zeiten und Abnahme wie die Website sie auswertet, isHanging). */
|
|
async function news({ projekt, jetzt }) {
|
|
const liste = await listDocuments(config.collections.newsPosts, [
|
|
Query.equal('projectId', projekt.projectId),
|
|
Query.limit(SEITE_NEWS),
|
|
Query.select(['visibleFrom', 'visibleUntil', 'takenDownAt']),
|
|
])
|
|
if (liste.length >= SEITE_NEWS) return null
|
|
return KURZINFO_TEXTE.news({ anzahl: liste.filter((p) => isHanging(p, jetzt)).length })
|
|
}
|
|
|
|
/** Buchungskalender: offene (neue) Anfragen; sonst die naechste Anreise einer gebuchten oder bezahlten Anfrage. */
|
|
async function unterkunft({ dok, jetzt }) {
|
|
if (!dok) return null
|
|
const tag = heute(jetzt)
|
|
const [offen, kommende] = await Promise.all([
|
|
countDocuments(config.collections.ukAnfragen, [Query.equal('featureId', dok.$id), Query.equal('status', 'neu')]),
|
|
listDocuments(config.collections.ukAnfragen, [
|
|
Query.equal('featureId', dok.$id),
|
|
Query.equal('status', ['gebucht', 'bezahlt']),
|
|
Query.greaterThanEqual('bis', tag),
|
|
Query.limit(SEITE),
|
|
Query.select(['von', 'bis']),
|
|
]),
|
|
])
|
|
if (!Number.isInteger(offen) || kommende.length >= SEITE) return null
|
|
const anreisen = kommende.map((a) => a.von).filter((v) => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) && v >= tag).sort()
|
|
let anreise = ''
|
|
if (anreisen.length) {
|
|
const erste = anreisen[0]
|
|
anreise = erste === tag ? 'Anreise heute' : erste === plusTage(tag, 1) ? 'Anreise morgen' : `Nächste Anreise ${tagKurz(erste)}`
|
|
}
|
|
return KURZINFO_TEXTE.unterkunft({ offen, anreise })
|
|
}
|
|
|
|
/** Oeffnungszeiten live: der Status, den die Projektliste ohnehin berechnet hat (zeitenStatus), ohne eigene Abfrage. */
|
|
function zeiten({ projekt }) {
|
|
const text = projekt.zeitenStatus?.text
|
|
if (istText(text)) return text
|
|
return projekt.zeitenLeer === true ? KURZINFO_TEXTE.zeitenLeer() : null
|
|
}
|
|
|
|
const LESER = { termine, shop, news, unterkunft, zeiten }
|
|
|
|
/** Nur ein nicht leerer, kurzer Text zaehlt, alles andere ist null. */
|
|
const gueltig = (wert) => (istText(wert) && wert.length <= MAX_ZEICHEN ? wert : null)
|
|
|
|
function logge(key, err) {
|
|
console.error(`[funktionen] Kurzinfo (${key}) nicht lesbar: ${err ? `${err?.name ?? 'Fehler'} ${err?.status ?? ''}` : 'Zeitüberschreitung'}`.trim())
|
|
}
|
|
|
|
/** Fuehrt einen Leser mit Zeitgrenze aus: Text oder null, wirft nie. Eine zu spaete Antwort wird verworfen. */
|
|
function begrenzt(key, leser) {
|
|
return new Promise((resolve) => {
|
|
const uhr = setTimeout(() => { logge(key, null); resolve(null) }, kurzinfoDeps.limitMs)
|
|
Promise.resolve()
|
|
.then(leser)
|
|
.then(
|
|
(wert) => { clearTimeout(uhr); resolve(gueltig(wert)) },
|
|
(err) => { clearTimeout(uhr); logge(key, err); resolve(null) },
|
|
)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Setzt `kurzinfo` an jede Funktion jedes Projekts (projects[].features[key], die Form von listCustomerFeatures): einen Text fuer aktive
|
|
* Funktionen, sonst null. Wirft nie wegen einer einzelnen Funktion; alle Abfragen laufen gleichzeitig.
|
|
* @param {{ projects: Array<{ projectId: string, features: object, zeitenStatus?: object|null, zeitenLeer?: boolean }>, docs: object[], jetzt: Date }} p
|
|
* docs: portalFeatures-Dokumente des Kunden; jetzt: der eine Zeitpunkt der Antwort
|
|
*/
|
|
export async function ergaenzeKurzinfos({ projects, docs, jetzt }) {
|
|
const aufgaben = []
|
|
for (const projekt of projects) {
|
|
for (const [key, funktion] of Object.entries(projekt.features ?? {})) {
|
|
funktion.kurzinfo = null
|
|
const leser = LESER[key]
|
|
if (!leser || funktion.state !== 'active') continue
|
|
const dok = docs.find((d) => d.projectId === projekt.projectId && d.featureKey === key)
|
|
aufgaben.push(begrenzt(key, () => leser({ projekt, dok, jetzt })).then((text) => { funktion.kurzinfo = text }))
|
|
}
|
|
}
|
|
await Promise.all(aufgaben)
|
|
}
|