Projektkonto: Anzeigelogik für Zeilen, Rechnungsspalten und Abzeichen „abgerechnet“ mit Tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99ab4bd47b
commit
241f7cdec6
2 files changed
+314
No files matched your search
@@ -0,0 +1,154 @@
|
||||
// Anzeigelogik des Projektkontos (rein, ohne React und Appwrite; unter Node testbar).
|
||||
// Aufgaben: Zeilen der Projektliste im Kundenreiter „Aufträge und Rechnungen“, die Spalten „Projekt“ und „Art“ der
|
||||
// Rechnungstabelle und das Abzeichen „abgerechnet“ der Arbeitsblätter. Alle Beträge sind Cent; Ausgaben sind Zeichenketten.
|
||||
import { euro, euroWhole } from './financeFormat'
|
||||
|
||||
const PLATZHALTER = '–'
|
||||
|
||||
export const STRIPE_HINWEIS = 'Stripe ist gerade nicht erreichbar. Berechnete und bezahlte Beträge sind deshalb nicht verfügbar.'
|
||||
export const KEINE_PROJEKTE_HINWEIS = 'Dieser Kunde hat noch keine Projekte. Rechnungen entstehen ohne Projektbezug.'
|
||||
|
||||
const ART_LABEL = {
|
||||
frei: 'Ohne Projekt',
|
||||
festbetrag: 'Festbetrag',
|
||||
aufwand: 'Aufwand',
|
||||
anzahlung: 'Anzahlung',
|
||||
schluss: 'Schlussrechnung',
|
||||
abo: 'Abo',
|
||||
}
|
||||
|
||||
function istZahl(v) {
|
||||
return typeof v === 'number' && Number.isFinite(v)
|
||||
}
|
||||
|
||||
function eigen(obj, key) {
|
||||
return obj !== null && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, key)
|
||||
}
|
||||
|
||||
/** 750 -> '12,5 Std.' (Dezimalstunden, höchstens zwei Nachkommastellen, ohne überflüssige Nullen). */
|
||||
export function stundenDe(minuten) {
|
||||
const min = istZahl(minuten) ? minuten : 0
|
||||
const h = Math.round((min / 60) * 100) / 100
|
||||
return `${String(h).replace('.', ',')} Std.`
|
||||
}
|
||||
|
||||
/** 10000 -> '100 €/Std.', 8550 -> '85,50 €/Std.', keine Zahl -> '–'. */
|
||||
export function satzText(cents) {
|
||||
if (!istZahl(cents)) return PLATZHALTER
|
||||
const ganz = Math.round(cents) % 100 === 0
|
||||
return `${ganz ? euroWhole(cents) : euro(cents)}/Std.`
|
||||
}
|
||||
|
||||
/** [{ woid: '100131' }, …] -> '#100131, #100140'; Tickets ohne Nummer fallen weg. */
|
||||
export function ticketsText(tickets) {
|
||||
if (!Array.isArray(tickets)) return ''
|
||||
return tickets
|
||||
.map((t) => String(t?.woid ?? '').trim())
|
||||
.filter(Boolean)
|
||||
.map((w) => `#${w}`)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
/** Hinweis nur, wenn Stripe ausdrücklich nicht erreichbar war. */
|
||||
export function stripeHinweis(stripeOk) {
|
||||
return stripeOk === false ? STRIPE_HINWEIS : ''
|
||||
}
|
||||
|
||||
/** Text der Spalte „Vereinbart“: Festpreis, Aufwand mit Satz und Schätzung, beides oder „keine AB“. */
|
||||
export function vereinbartText(vereinbart) {
|
||||
const teile = []
|
||||
if (istZahl(vereinbart?.festpreisCents)) teile.push(euro(vereinbart.festpreisCents))
|
||||
const aufwand = vereinbart?.aufwand
|
||||
if (aufwand) {
|
||||
let t = `nach Aufwand, ${satzText(aufwand.stundensatzCents)}`
|
||||
if (istZahl(aufwand.schaetzungMinuten) && aufwand.schaetzungMinuten > 0) t += `, Schätzung ${stundenDe(aufwand.schaetzungMinuten)}`
|
||||
teile.push(t)
|
||||
}
|
||||
return teile.length ? teile.join(' und ') : 'keine AB'
|
||||
}
|
||||
|
||||
/** Eine Zeile der Projektliste. `stripeOk: false` ersetzt Berechnet, bezahlt und (Festpreis) Noch offen durch „–“. */
|
||||
export function kontoZeile(konto, { stripeOk = true } = {}) {
|
||||
const stripe = stripeOk !== false
|
||||
const k = konto || {}
|
||||
const zeit = k.zeit || {}
|
||||
const vereinbart = k.vereinbart || {}
|
||||
const name = String(k.name ?? '').trim() || 'Projekt'
|
||||
const meta = [String(k.projektTyp ?? '').trim(), ticketsText(k.tickets)].filter(Boolean).join(' · ')
|
||||
let offen = PLATZHALTER
|
||||
if (istZahl(vereinbart.festpreisCents)) offen = stripe ? euro(k.offenCents) : PLATZHALTER
|
||||
else if (vereinbart.aufwand) offen = euro(zeit.offenerWertCents)
|
||||
return {
|
||||
key: k.projektId,
|
||||
projektId: k.projektId,
|
||||
name,
|
||||
meta,
|
||||
vereinbart: vereinbartText(vereinbart),
|
||||
berechnet: stripe ? euro(k.berechnetCents) : PLATZHALTER,
|
||||
bezahlt: stripe ? `bezahlt ${euro(k.bezahltCents)}` : '',
|
||||
offen,
|
||||
zeit: {
|
||||
stunden: stundenDe(zeit.minuten),
|
||||
wert: `Wert ${euro(zeit.wertCents)}`,
|
||||
abo: istZahl(zeit.aboMinuten) && zeit.aboMinuten > 0 ? `über Abo: ${stundenDe(zeit.aboMinuten)}` : '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** Die feste Zeile „Ohne Projekt“ am Ende der Liste. */
|
||||
export function ohneProjektZeile(ohne, { stripeOk = true } = {}) {
|
||||
const stripe = stripeOk !== false
|
||||
const anzahl = istZahl(ohne?.anzahl) ? ohne.anzahl : 0
|
||||
const meta = anzahl <= 0 ? 'keine Rechnungen' : anzahl === 1 ? '1 Rechnung' : `${anzahl} Rechnungen`
|
||||
return {
|
||||
key: 'ohne-projekt',
|
||||
projektId: '',
|
||||
name: 'Ohne Projekt',
|
||||
meta,
|
||||
vereinbart: PLATZHALTER,
|
||||
berechnet: stripe ? euro(ohne?.berechnetCents ?? 0) : PLATZHALTER,
|
||||
bezahlt: stripe ? `bezahlt ${euro(ohne?.bezahltCents ?? 0)}` : '',
|
||||
offen: PLATZHALTER,
|
||||
zeit: { stunden: PLATZHALTER, wert: '', abo: '' },
|
||||
}
|
||||
}
|
||||
|
||||
/** Spalte „Art“ der Rechnungstabelle. */
|
||||
export function artLabel(art) {
|
||||
return typeof art === 'string' && eigen(ART_LABEL, art) ? ART_LABEL[art] : PLATZHALTER
|
||||
}
|
||||
|
||||
/** Spalte „Projekt“ der Rechnungstabelle. */
|
||||
export function projektSpalte(invoice) {
|
||||
return String(invoice?.projektName ?? '').trim() || PLATZHALTER
|
||||
}
|
||||
|
||||
function abgerechnetWert(abgerechnet, worksheetId) {
|
||||
if (!eigen(abgerechnet, worksheetId)) return null
|
||||
const wert = abgerechnet[worksheetId]
|
||||
return typeof wert === 'string' && wert.trim() ? wert.trim() : null
|
||||
}
|
||||
|
||||
/** Abzeichen am Arbeitsblatt: 'abgerechnet (WEBKLAR-0009)' oder 'abgerechnet (Entwurf)'; sonst null. */
|
||||
export function abgerechnetText(abgerechnet, worksheetId) {
|
||||
const wert = abgerechnetWert(abgerechnet, worksheetId)
|
||||
return wert ? `abgerechnet (${wert})` : null
|
||||
}
|
||||
|
||||
/** Warnung beim Ändern der Zeit eines abgerechneten Arbeitsblatts; sonst null. */
|
||||
export function abgerechnetWarnung(abgerechnet, worksheetId) {
|
||||
const wert = abgerechnetWert(abgerechnet, worksheetId)
|
||||
return wert ? `Dieses Arbeitsblatt ist abgerechnet (${wert}). Eine Änderung ändert die Rechnung nicht.` : null
|
||||
}
|
||||
|
||||
/** Eindeutige Ticket-Kennungen der Arbeitsblätter in Reihenfolge des ersten Auftretens, höchstens fünf. */
|
||||
export function ticketIdsOf(worksheets) {
|
||||
if (!Array.isArray(worksheets)) return []
|
||||
const ids = []
|
||||
for (const w of worksheets) {
|
||||
const id = String(w?.workorderId ?? '').trim()
|
||||
if (id && !ids.includes(id)) ids.push(id)
|
||||
if (ids.length >= 5) break
|
||||
}
|
||||
return ids
|
||||
}
|
||||
Reference in new issue
Block a user