import { STUNDENSATZ_STANDARD_CENTS, VEREINBARTE_STATUS } from './konstanten.js' import { abrechenbar } from './arbeitsblaetter.js' /** * Projektkonto (Spec 2026-10-06, Abschnitt 4; Uebersicht V4 "L1 - Projektkonto"): reine Rechenregeln und die Ladefunktionen * dazu. Alles hier liest nur, weder Appwrite noch Stripe werden geschrieben. */ // Rechnungsstatus laut mapInvoice (stripeFinance.js), die als "berechnet" zaehlen; Entwurf und storniert nie const FINALISIERT = new Set(['offen', 'ueberfaellig', 'bezahlt', 'uneinbringlich']) const summe = (liste, feld) => liste.reduce((s, eintrag) => s + eintrag[feld], 0) const zeitpunkt = (auftrag) => new Date(auftrag.gesendetAm || auftrag.erstelltAm || 0).getTime() || 0 const juengsteZuerst = (a, b) => zeitpunkt(b) - zeitpunkt(a) || String(b.id).localeCompare(String(a.id)) const zeitWert = (minuten, satzCents) => Math.round((minuten * satzCents) / 60) // Wert der Zeit = Summe der je Berliner Tag gerundeten Werte (Karte: Tag -> Minuten), genau wie die Zeilen einer Aufwandsrechnung // (aufwandPositionen in L2: eine Zeile je Tag mit Math.round(Minuten × Satz / 60)); so laufen Konto und Rechnung nicht um Cent auseinander const tageswert = (minutenJeTag, satzCents) => [...minutenJeTag.values()].reduce((s, minuten) => s + zeitWert(minuten, satzCents), 0) const addiere = (karte, tag, minuten) => karte.set(tag, (karte.get(tag) ?? 0) + minuten) /** * Die Zahlen eines Projekts. auftraege: alle ABs des Projekts (jeder Status); rechnungen: [{ art, auftragId, invoice }] mit * der Rechnung in mapInvoice-Form (nur aktive Projektrechnungen, deren Rechnung gefunden wurde); blaetter: Arbeitsblaetter der * Zeitquelle (datum = Berliner Tag); abgerechnet: Map oder Set der Blatt-IDs aus aktiven Projektrechnungen (auch Entwuerfe); * stripeOk false = die Rechnungen sind unbekannt (berechnet und bezahlt 0, offen null). */ export function rechneKonto({ auftraege = [], rechnungen = [], blaetter = [], abgerechnet = new Map(), stripeOk = true } = {}) { const gueltig = auftraege.filter((auftrag) => VEREINBARTE_STATUS.includes(auftrag.status)) const festpreise = gueltig.filter((auftrag) => auftrag.art === 'festpreis') const festpreisIds = new Set(festpreise.map((auftrag) => auftrag.id)) const festpreisCents = festpreise.length ? festpreise.reduce((s, auftrag) => s + (Number(auftrag.summeCents) || 0), 0) : null const aufwandAb = gueltig.filter((auftrag) => auftrag.art === 'aufwand').sort(juengsteZuerst)[0] ?? null const aufwand = aufwandAb ? { stundensatzCents: aufwandAb.stundensatzCents > 0 ? aufwandAb.stundensatzCents : STUNDENSATZ_STANDARD_CENTS, schaetzungMinuten: aufwandAb.schaetzungMinuten ?? null, } : null const satzCents = aufwand ? aufwand.stundensatzCents : STUNDENSATZ_STANDARD_CENTS let berechnetCents = 0 let bezahltCents = 0 let aufFestpreisCents = 0 if (stripeOk) { for (const { invoice, auftragId } of rechnungen) { if (!FINALISIERT.has(invoice.status)) continue berechnetCents += invoice.totalCents bezahltCents += invoice.paidCents // nur Rechnungen zu einer als vereinbart zaehlenden Festpreis-AB mindern das Offene if (festpreisIds.has(auftragId)) aufFestpreisCents += invoice.totalCents } } const offenCents = festpreisCents === null || !stripeOk ? null : Math.max(0, festpreisCents - aufFestpreisCents) let minuten = 0 let aboMinuten = 0 let offeneMinuten = 0 const minutenJeTag = new Map() // alle abrechenbaren Blaetter (auch schon berechnete) const offeneMinutenJeTag = new Map() // nur die offenen; ein Blatt ohne Datum (nur theoretisch) bildet einen eigenen Tag '' for (const blatt of blaetter) { if (abrechenbar(blatt)) { minuten += blatt.minuten addiere(minutenJeTag, blatt.datum, blatt.minuten) if (!abgerechnet.has(blatt.id)) { offeneMinuten += blatt.minuten addiere(offeneMinutenJeTag, blatt.datum, blatt.minuten) } } else if (blatt.aboZeit && !blatt.kommentar) { aboMinuten += blatt.minuten } } return { vereinbart: { festpreisCents, aufwand }, berechnetCents, bezahltCents, offenCents, zeit: { minuten, aboMinuten, offeneMinuten, satzCents, wertCents: tageswert(minutenJeTag, satzCents), offenerWertCents: tageswert(offeneMinutenJeTag, satzCents), }, } } /** Zeile "Ohne Projekt": Rechnungen in mapInvoice-Form; gezaehlt werden nur finalisierte. */ export function rechneOhneProjekt(rechnungen) { const gezaehlt = rechnungen.filter((invoice) => FINALISIERT.has(invoice.status)) return { berechnetCents: summe(gezaehlt, 'totalCents'), bezahltCents: summe(gezaehlt, 'paidCents'), anzahl: gezaehlt.length } }