Projektkonto: reine Rechenregeln (vereinbart, berechnet, offen, Zeitwert je Tag gerundet, Ohne Projekt)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:11:32 +00:00
1 parent 8627ed1124
commit 3305371111
2 files changed
+306

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
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 }
}