Files
Webklar-Kundenbereich/server/services/auftraege/auftragSicht.js
T

106 lines
4.9 KiB
JavaScript

/**
* Sichten auf eine Auftragsbestätigung (Übersicht V3/V4): Kundensnapshot, früheste Leistung bei Privatpersonen,
* Zahlungsplan- und Aufwandstext, Ansicht fürs Team und öffentliche Ansicht für Token- und Kundenrouten.
* Alles rein (kein Appwrite, keine Konfiguration); die Wortlaute kommen aus texte.js.
*/
import { withdrawalDeadline } from '../deadlines.js'
import { addDays, berlinDate } from '../finance/dates.js'
import { euroDe } from '../finance/format.js'
import { AGB_FASSUNG, KUNDENARTEN, LIMITS, STUNDENSATZ_STANDARD_CENTS } from './konstanten.js'
import { AB_TEXTE, BESTAETIGUNG_TEXT, FRUEHBEGINN_TEXT } from './texte.js'
// Kürzungen, damit der Snapshot samt JSON in die Spalte kundeJson (2000 Zeichen) passt
const SNAPSHOT_FELDER = [
['name', 'name', 120], ['firma', 'companyName', 120], ['strasse', 'street', 120], ['plz', 'postalCode', 20],
['ort', 'city', 80], ['land', 'country', 60], ['email', 'email', 200], ['kundennummer', 'code', 40],
]
const text = (wert, max) => (typeof wert === 'string' ? wert.trim().slice(0, max).trim() : '')
export function kundeSnapshot(customerDoc) {
const doc = customerDoc || {}
const snapshot = {}
for (const [ziel, quelle, max] of SNAPSHOT_FELDER) snapshot[ziel] = text(doc[quelle], max)
snapshot.kundenart = KUNDENARTEN.includes(doc.kundenart) ? doc.kundenart : ''
// feste Reihenfolge der Schlüssel wie KundeSnapshot in V3
const { name, firma, strasse, plz, ort, land, email, kundenart, kundennummer } = snapshot
return { name, firma, strasse, plz, ort, land, email, kundenart, kundennummer }
}
/** Früheste Leistung bei Privatpersonen ohne Frühbeginn: Tag nach Ablauf der Widerrufsfrist. */
export function fruehesterBeginn(auftrag) {
if (auftrag.kunde?.kundenart !== 'privat' || auftrag.fruehbeginn || !auftrag.gesendetAm) return null
const gesendet = new Date(auftrag.gesendetAm)
if (Number.isNaN(gesendet.getTime())) return null
return addDays(withdrawalDeadline(gesendet).lastDay, 1)
}
export function zahlungsplanText(auftrag) {
const ziel = Number.isSafeInteger(auftrag.zahlungszielTage) ? auftrag.zahlungszielTage : LIMITS.standardZahlungszielTage
const plan = auftrag.zahlungsplan || { art: 'nach_abnahme' }
const bausteine = AB_TEXTE.zahlungsplan
if (plan.art === 'monatlich') return bausteine.monatlich({ ziel })
if (plan.art === 'frei') return bausteine.frei({ text: plan.text || '', ziel })
if (plan.art === 'anzahlung' && Number.isSafeInteger(auftrag.summeCents) && typeof plan.prozent === 'number' && Number.isFinite(plan.prozent)) {
const anzahlung = Math.round((auftrag.summeCents * plan.prozent) / 100)
return bausteine.anzahlung({ prozent: plan.prozent, anzahlung: euroDe(anzahlung), rest: euroDe(auftrag.summeCents - anzahlung), ziel })
}
return bausteine.nach_abnahme({ ziel })
}
/** Stunden als Text mit Dezimalkomma ohne überflüssige Nullen: 600 → '10', 135 → '2,25', 90 → '1,5'. */
const stundenText = (minuten) => String(Math.round((minuten / 60) * 100) / 100).replace('.', ',')
export function aufwandText(auftrag) {
if (auftrag.art !== 'aufwand') return null
const minuten = auftrag.schaetzungMinuten
return AB_TEXTE.aufwand({
satz: euroDe(auftrag.stundensatzCents ?? STUNDENSATZ_STANDARD_CENTS),
schaetzung: Number.isSafeInteger(minuten) && minuten > 0 ? stundenText(minuten) : null,
})
}
/** Auftrag ohne tokenHash, pdfDatei und bestaetigtPdfSha256, plus die abgeleiteten Felder. */
export function auftragFuerTeam(auftrag) {
const { tokenHash, pdfDatei, bestaetigtPdfSha256, ...rest } = auftrag
return {
...rest,
pdfVorhanden: Boolean(pdfDatei),
fruehesterBeginn: fruehesterBeginn(auftrag),
zahlungsplanText: zahlungsplanText(auftrag),
aufwandText: aufwandText(auftrag),
}
}
/** Nur für Token- und Kundenrouten: keine Kundendaten, kein Token, keine Pfade. */
export function oeffentlicheAnsicht(auftrag) {
const kundenart = auftrag.kunde?.kundenart || ''
return {
nummer: auftrag.nummer,
datum: auftrag.gesendetAm ? berlinDate(new Date(auftrag.gesendetAm)) : null,
titel: auftrag.titel,
art: auftrag.art,
positionen: auftrag.positionen.map((p) => ({
beschreibung: p.beschreibung, menge: p.menge, einzelpreisCents: p.einzelpreisCents, summeCents: p.menge * p.einzelpreisCents,
})),
summeCents: auftrag.summeCents,
stundensatzCents: auftrag.stundensatzCents,
schaetzungMinuten: auftrag.schaetzungMinuten,
aufwandText: aufwandText(auftrag),
zahlungsplanText: zahlungsplanText(auftrag),
startAm: auftrag.startAm,
fertigBis: auftrag.fertigBis,
kundenart,
status: auftrag.status,
bestaetigtAm: auftrag.bestaetigtAm,
fruehbeginn: auftrag.fruehbeginn,
fruehesterBeginn: fruehesterBeginn(auftrag),
agbUrl: `/rechtliches/agb-${auftrag.agbFassung || AGB_FASSUNG}.pdf`,
texte: {
bestaetigung: BESTAETIGUNG_TEXT,
fruehbeginn: kundenart === 'privat' ? FRUEHBEGINN_TEXT : null,
kleinunternehmer: AB_TEXTE.kleinunternehmer,
},
}
}