diff --git a/src/components/WorksheetList.jsx b/src/components/WorksheetList.jsx index b9daa22..a4a9c8f 100644 --- a/src/components/WorksheetList.jsx +++ b/src/components/WorksheetList.jsx @@ -1,6 +1,8 @@ import { EmptyState } from './ui' import { needsTimeEntry, isGitWorksheet } from '../lib/ticketForm' import { parseSupporters } from '../lib/support' +import { useAbgerechnet } from '../hooks/useAbgerechnet' +import { abgerechnetText, ticketIdsOf } from '../lib/projektkontoView' import { showAboToggle, statusAfterLabel, @@ -23,6 +25,8 @@ export default function WorksheetList({ onOwnSupport, currentUserId, }) { + // Hooks laufen immer, auch bei den frühen Rückgaben unten; die Abzeichen sehen alle Mitarbeiter und zeigen nie Beträge + const abgerechnet = useAbgerechnet(ticketIdsOf(worksheets)) if (loading) return
Wird geladen …
if (!worksheets || worksheets.length === 0) { @@ -35,6 +39,7 @@ export default function WorksheetList({ const when = worksheetWhen(ws) const kind = worksheetKindLabel(ws) const badge = worksheetBadge(ws) + const billed = abgerechnetText(abgerechnet, ws.$id) const lines = supportLines(ws) const status = statusAfterLabel(ws) const time = timeLabel(ws) @@ -58,6 +63,18 @@ export default function WorksheetList({ {kind && {kind}} WSID {ws.wsid} {badge && {badge.text}} + {billed && ( + + {billed} + + )} {showAbo ? ( : null} + /> + ) + } + return +} + +function AuftraegeInhalt({ customer, onShowOverview }) { + const res = useFinanceResource(() => ladeAuftraegeAnsicht(customer.$id, { getProjektkonten, listAuftraege }), KONTEN_FEHLER) + const ansicht = ansichtsZustand(res) + const reload = res.reload + + const [meldung, setMeldung] = useState(null) // { tone, text } oder null + const [rechnungsDialog, setRechnungsDialog] = useState(null) // { projektId } oder null + const [auftragsDialog, setAuftragsDialog] = useState(null) // { projektId, auftrag } oder null + const [listeToken, setListeToken] = useState(0) + const meldungRef = useRef(null) + useScrollIntoView(meldungRef, meldung) + + // Die Dialoge bekommen die frische Kundenart des Portals (die Kundenseite hält ggf. noch den alten Datensatz) + const kunde = kundeMitKundenart(customer, ansicht.kunde) + + function openRechnung(projektId) { + setMeldung(null) + setRechnungsDialog({ projektId }) + } + + function openAuftrag(projektId, auftrag = null) { + setMeldung(null) + setAuftragsDialog({ projektId, auftrag }) + } + + function handleCreated(rechnung, info) { + setRechnungsDialog(null) + setMeldung(erstelltMeldung(rechnung, info)) + setListeToken((n) => n + 1) + reload() + } + + function handleSaved(auftrag) { + setAuftragsDialog(null) + setMeldung(auftragMeldung(auftrag)) + reload() + } + + return ( + <> + + + {meldung && ( +

+ {meldung.text} +

+ )} + + {ansicht.projekteStatus === 'bereit' && ansicht.nachladeFehler && ( +

+ Die Übersicht konnte nicht neu geladen werden: {ansicht.nachladeFehler}{' '} + +

+ )} + + {ansicht.projekteStatus === 'laden' && ( + +
Projekte werden geladen …
+
+ )} + {ansicht.projekteStatus === 'fehler' && ( + +
+

{ansicht.fehler}

+ +
+
+ )} + {ansicht.projekteStatus === 'bereit' && ( + openAuftrag(projektId)} + /> + )} + + {ansicht.auftraegeStatus === 'bereit' && ( + openAuftrag(auftrag.projektId || null, auftrag)} + onShowOverview={onShowOverview} + /> + )} + {ansicht.auftraegeStatus === 'fehler' && ( + +
+

{ansicht.auftraegeFehler}

+ +
+
+ )} + + + + {rechnungsDialog && ( + setRechnungsDialog(null)} + onCreated={handleCreated} + kunde={kunde} + projektId={rechnungsDialog.projektId} + auftraegeAktiv={ansicht.auftraegeAktiv} + onShowOverview={onShowOverview} + /> + )} + {auftragsDialog && ( + setAuftragsDialog(null)} + onSaved={handleSaved} + kunde={kunde} + projektId={auftragsDialog.projektId} + auftrag={auftragsDialog.auftrag} + konten={ansicht.konten} + onShowOverview={onShowOverview} + /> + )} + + ) +} diff --git a/src/components/auftraege/AuftragDialog.jsx b/src/components/auftraege/AuftragDialog.jsx new file mode 100644 index 0000000..2bded8c --- /dev/null +++ b/src/components/auftraege/AuftragDialog.jsx @@ -0,0 +1,275 @@ +import { useEffect, useRef, useState } from 'react' +import { Button, Dialog } from '../ui' +import { createAuftrag, getAuftragPdf, getSettings, patchAuftrag, sendAuftrag } from '../../lib/financeApi' +import { alsPdfBlob } from '../../lib/auftragPdf' +import { + SCHRITTE, + auftragBestaetigung, + auftragFehler, + auftragHinweise, + auftragZuFormular, + formularZuNutzlast, + heuteBerlin, + kundenartWirksam, + neueSperre, + neuesFormular, + pruefeSchritt, + sendeGate, + zahlungsplanOptionen, +} from '../../lib/auftragView' +import { SchrittAuftrag, SchrittProjekt, SchrittZahlung } from './AuftragSchritte' +import AuftragVorschau from './AuftragVorschau' +import './auftrag.css' + +/** + * Dialog „Auftragsbestätigung“ in vier Schritten (Projekt, Auftrag, Zahlung und Termine, Vorschau). Beim Wechsel nach + * Schritt 4 wird der Entwurf angelegt bzw. aktualisiert und das PDF geholt (die Vorschau gibt es nur zu einem gespeicherten + * Entwurf; archiviert wird sie nicht). „Senden“ fragt vorher nach und ist gesperrt, solange Kundendaten fehlen; Hinweise + * (Steuernummer, „Vor Ort“ bei Privatperson) blockieren nie. Nach Speichern oder Senden ruft der Dialog onSaved(auftrag) und + * danach immer onClose(): der Container schließt, lädt neu und meldet. Nur Entwürfe lassen sich bearbeiten. + */ +export default function AuftragDialog({ open, onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview }) { + if (!open) return null + if (auftrag && auftrag.status !== 'entwurf') { + return ( + Schließen}> +

+ Nur Entwürfe lassen sich bearbeiten. Eine gesendete Auftragsbestätigung lässt sich nur über „Neue Fassung“ in der Liste ändern. +

+
+ ) + } + return ( + + ) +} + +// Ids der Felder (Task 6): wenige Felder haben eine abweichende id +function feldId(feld, art) { + if (feld === 'art') return 'ab-art-festpreis' + if (feld === 'zahlungsart') return `ab-zahlungsart-${zahlungsplanOptionen(art)[0].value}` + return `ab-${feld}` +} + +function AuftragForm({ onClose, onSaved, kunde, projektId, auftrag, konten, onShowOverview }) { + const [schritt, setSchritt] = useState(1) + const [form, setForm] = useState(() => (auftrag ? auftragZuFormular(auftrag) : neuesFormular({ projektId: projektId || '', konten, heute: heuteBerlin() }))) + const [fehler, setFehler] = useState({}) + const [meldung, setMeldung] = useState(null) // { text, zurUebersicht } + const [entwurf, setEntwurf] = useState(auftrag || null) + const [signatur, setSignatur] = useState('') + const [pdfUrl, setPdfUrl] = useState('') + const [phase, setPhase] = useState('') // '' | 'speichern' | 'pdf' | 'senden' + const [einstellungen, setEinstellungen] = useState(undefined) // undefined = lädt, null = nicht ladbar + const [fokus, setFokus] = useState(null) // { id } + const sperre = useRef(neueSperre()).current + const aliveRef = useRef(true) + const schmutzigRef = useRef(false) // es wurde gespeichert und noch nicht gemeldet + const urlRef = useRef('') + + useEffect(() => { + aliveRef.current = true + getSettings() + .then((d) => { + if (aliveRef.current) setEinstellungen(d?.einstellungen ?? null) + }) + .catch(() => { + if (aliveRef.current) setEinstellungen(null) + }) + return () => { + aliveRef.current = false + if (urlRef.current) URL.revokeObjectURL(urlRef.current) + } + }, []) + + useEffect(() => { + if (fokus) document.getElementById(fokus.id)?.focus() + }, [fokus]) + + const hinweise = auftragHinweise({ kunde, form, einstellungen }) + const gate = sendeGate(kunde, form.kundenart) + const privat = kundenartWirksam(kunde, form.kundenart) === 'privat' + const beschaeftigt = phase !== '' + + function zeigeFehler(schrittNr, r, art) { + setSchritt(schrittNr) + setFehler(r.fehler) + setFokus({ id: feldId(r.erstes, art) }) + } + + function weiter() { + const r = pruefeSchritt(schritt, form, { kunde }) + if (!r.ok) { + setFehler(r.fehler) + setFokus({ id: feldId(r.erstes, form.art) }) + return + } + setFehler({}) + if (schritt === 3) zurVorschau() + else setSchritt(schritt + 1) + } + + function zurueck() { + setMeldung(null) + setSchritt(Math.max(1, schritt - 1)) + } + + function zurVorschau() { + const r = formularZuNutzlast(form, { kunde }) + if (!r.ok) { + zeigeFehler(r.schritt, r, form.art) + return + } + setSchritt(4) + vorschauLaden(r.payload) + } + + async function vorschauLaden(payload) { + const neu = JSON.stringify(payload) + if (neu === signatur && pdfUrl) return + if (sperre.belegt) return + await sperre.run(async () => { + if (urlRef.current) URL.revokeObjectURL(urlRef.current) + urlRef.current = '' + setPdfUrl('') + setPhase('speichern') + setMeldung(null) + try { + const data = entwurf ? await patchAuftrag(entwurf.id, payload) : await createAuftrag({ kundenId: kunde.$id, ...payload }) + if (!aliveRef.current) return + setEntwurf(data.auftrag) + schmutzigRef.current = true + setSignatur(neu) + setPhase('pdf') + const blob = await getAuftragPdf(data.auftrag.id) + const url = URL.createObjectURL(alsPdfBlob(blob)) + if (!aliveRef.current) { + URL.revokeObjectURL(url) + return + } + urlRef.current = url + setPdfUrl(url) + } catch (err) { + if (aliveRef.current) setMeldung(auftragFehler(err)) // ein schon gespeicherter Entwurf bleibt + } finally { + if (aliveRef.current) setPhase('') + } + }) + } + + function speichernUndSchliessen() { + schmutzigRef.current = false + if (entwurf) onSaved(entwurf) + onClose() + } + + function schliessen() { + if (beschaeftigt) return + if (schmutzigRef.current && entwurf) onSaved(entwurf) + schmutzigRef.current = false + onClose() + } + + async function senden() { + if (sperre.belegt || !entwurf) return + const g = sendeGate(kunde, form.kundenart) + if (!g.erlaubt) { + setMeldung({ text: g.grund, zurUebersicht: g.zurUebersicht }) + return + } + if (!window.confirm(auftragBestaetigung('senden', entwurf, { email: kunde?.email }))) return + await sperre.run(async () => { + setPhase('senden') + setMeldung(null) + try { + const data = await sendAuftrag(entwurf.id) + schmutzigRef.current = false + onSaved(data.auftrag) + onClose() + } catch (err) { + if (aliveRef.current) setMeldung(auftragFehler(err)) + } finally { + if (aliveRef.current) setPhase('') + } + }) + } + + const fuss = ( +
+ {schritt > 1 && ( + + )} + + {schritt < 4 ? ( + <> + + + + ) : ( + <> + + + + )} +
+ ) + + return ( + { + e.preventDefault() + if (schritt < 4) weiter() + }} + footer={fuss} + > +
    + {SCHRITTE.map((s) => ( +
  1. + {s.id} + {s.label} +
  2. + ))} +
+ {schritt === 1 && } + {schritt === 2 && } + {schritt === 3 && } + {schritt === 4 && ( + { + schliessen() + onShowOverview() + }} + /> + )} +
+ ) +} diff --git a/src/components/auftraege/AuftragListe.jsx b/src/components/auftraege/AuftragListe.jsx new file mode 100644 index 0000000..d66ba50 --- /dev/null +++ b/src/components/auftraege/AuftragListe.jsx @@ -0,0 +1,244 @@ +import { useEffect, useMemo, useRef, useState } from 'react' +import { Badge, Button, Card, EmptyState } from '../ui' +import { noticeProps } from '../../lib/financeUi' +import { + deleteAuftrag, + erinnereAuftrag, + getAuftragPdf, + neueFassungAuftrag, + ordneAuftragProjektZu, + sendAuftrag, + storniereAuftrag, +} from '../../lib/financeApi' +import { oeffnePdfImTab } from '../../lib/auftragPdf' +import { + auftragBestaetigung, + auftragErfolg, + auftragFehler, + auftragZeilen, + neueSperre, + zuordnenOptionen, +} from '../../lib/auftragView' +import './auftrag.css' + +const LAUFEND = { senden: 'Sende …', erinnern: 'Sende …', stornieren: 'Storniere …', loeschen: 'Lösche …', neueFassung: 'Lege an …', pdf: 'Öffne …' } +const AUFRUFE = { + senden: sendAuftrag, + erinnern: erinnereAuftrag, + stornieren: storniereAuftrag, + loeschen: deleteAuftrag, + neueFassung: neueFassungAuftrag, +} + +/** + * Karte „Aufträge“ im Kundenreiter: Auftragsbestätigungen (AB) mit Status, Zusatzzeilen und den Aktionen je Status. + * Die Regeln stecken in src/lib/auftragView.js. Nach jeder Aktion ruft die Liste onChanged() (auch nach einem Fehler: + * der Stand kann ein anderer sein). „Bearbeiten“ und „Neue Fassung“ geben den Entwurf über onBearbeiten an den Container, + * „Zur Übersicht“ in Meldungen läuft über onShowOverview. Eine Sperre fängt den Doppelklick ab (eine Anfrage, eine Rückfrage). + * Schmal (Wrapper .auftrag-body bis 860 px) wird jede Zeile ein Raster. + */ +export default function AuftragListe({ auftraege, konten, onChanged, onBearbeiten, onShowOverview }) { + const zeilen = useMemo(() => auftragZeilen(auftraege, { konten }), [auftraege, konten]) + const optionen = useMemo(() => zuordnenOptionen(konten), [konten]) + const [busy, setBusy] = useState(null) // `${id}:${aktion}` der laufenden Anfrage + const [message, setMessage] = useState(null) // { tone, text, zurUebersicht? } + const [zuordnen, setZuordnen] = useState(null) // { id, projektId } der Zeile mit offener Auswahl + const sperre = useRef(neueSperre()).current + const aliveRef = useRef(true) + + useEffect(() => { + aliveRef.current = true + return () => { + aliveRef.current = false + } + }, []) + + const zeigeFehler = (err) => { + const f = auftragFehler(err) + if (aliveRef.current) setMessage({ tone: 'warn', text: f.text, zurUebersicht: f.zurUebersicht }) + } + + async function run(zeile, aktion) { + if (aktion.key === 'bearbeiten') { + onBearbeiten(zeile.auftrag) + return + } + if (aktion.key === 'projekt') { + setZuordnen({ id: zeile.id, projektId: '' }) + setMessage(null) + return + } + if (sperre.belegt) return + const frage = auftragBestaetigung(aktion.key, zeile.auftrag) + if (frage && !window.confirm(frage)) return + await sperre.run(async () => { + setBusy(`${zeile.id}:${aktion.key}`) + setMessage(null) + try { + if (aktion.key === 'pdf') { + // kein await davor: der neue Tab muss im Klick aufgehen + await oeffnePdfImTab(() => getAuftragPdf(zeile.id)) + return + } + const data = await AUFRUFE[aktion.key](zeile.id) + if (aliveRef.current) setMessage(auftragErfolg(aktion.key, data?.auftrag || zeile.auftrag, { mailFehler: data?.mailFehler })) + onChanged() + if (aktion.key === 'neueFassung' && data?.auftrag) onBearbeiten(data.auftrag) + } catch (err) { + if (aktion.key === 'pdf') { + if (aliveRef.current) setMessage({ tone: 'warn', text: auftragFehler(err).text }) + return + } + zeigeFehler(err) + onChanged() // der Stand kann ein anderer sein + } finally { + if (aliveRef.current) setBusy(null) + } + }) + } + + async function ordneZu(zeile) { + const gewaehlt = optionen.find((o) => o.value === zuordnen?.projektId) + if (!gewaehlt || sperre.belegt) return + const frage = auftragBestaetigung('projekt', zeile.auftrag, { projektName: gewaehlt.label }) + if (frage && !window.confirm(frage)) return + await sperre.run(async () => { + setBusy(`${zeile.id}:projekt`) + setMessage(null) + try { + const data = await ordneAuftragProjektZu(zeile.id, gewaehlt.value) + if (aliveRef.current) { + setMessage(auftragErfolg('projekt', data?.auftrag || zeile.auftrag, { projektName: gewaehlt.label })) + setZuordnen(null) + } + onChanged() + } catch (err) { + zeigeFehler(err) + if (aliveRef.current) setZuordnen(null) + onChanged() + } finally { + if (aliveRef.current) setBusy(null) + } + }) + } + + return ( + +
+ {message && ( +

+ {message.text} + {message.zurUebersicht && ( + <> + {' '} + + + )} +

+ )} + + {zeilen.length === 0 ? ( + + ) : ( +
+ + + + + + + + + + + + + + {zeilen.map((zeile) => { + const offen = zuordnen?.id === zeile.id + const wahl = offen && optionen.some((o) => o.value === zuordnen.projektId) ? zuordnen.projektId : '' + const ordnet = busy === `${zeile.id}:projekt` + return ( + + + + + + + + + ) + })} + +
Auftragsbestätigungen, neueste zuerst
NummerProjekt / TitelDatumSummeStatusAktionen
+ {zeile.istEntwurf ? {zeile.nummerText} : zeile.nummerText} + + {zeile.titel} + {zeile.projektText &&
{zeile.projektText}
} +
{zeile.datumText} + {zeile.summeText} + {zeile.summeSub &&
{zeile.summeSub}
} +
+ {zeile.statusLabel} + {zeile.notizen.map((note) => ( +
+ {note.text} +
+ ))} +
+
+ {zeile.aktionen.map((aktion) => { + const label = `${aktion.label}: ${zeile.nummerText}, ${zeile.titel}` + const text = busy === `${zeile.id}:${aktion.key}` && LAUFEND[aktion.key] ? LAUFEND[aktion.key] : aktion.label + if (aktion.danger) { + return ( + + ) + } + return ( + + ) + })} +
+ {offen && ( +
+ + + +
+ )} +
+
+ )} +
+
+ ) +} diff --git a/src/components/auftraege/AuftragSchritte.jsx b/src/components/auftraege/AuftragSchritte.jsx new file mode 100644 index 0000000..088de25 --- /dev/null +++ b/src/components/auftraege/AuftragSchritte.jsx @@ -0,0 +1,321 @@ +import { Badge } from '../ui' +import FormField from '../finance/FormField' +import { useWorkorders } from '../../hooks/useWorkorders' +import { euro } from '../../lib/financeFormat' +import { customerName, lineTotalCents } from '../../lib/financeView' +import { KUNDENART_OPTIONEN, kundenartLabel } from '../../lib/kundenFelder' +import { + AUFTRAG_LIMITS, + AUFTRAG_WEGE, + formularSummeCents, + kundenartWirksam, + neuePosition, + parseProzent, + parseTage, + schalteZeitquelle, + vorOrtHinweis, + waehleProjekt, + wechselArt, + zahlungsplanOptionen, + zahlungsplanVorschau, + zeitquelleOptionen, +} from '../../lib/auftragView' +import './auftrag.css' + +/** Meldung eines Feldes ohne FormField (Art, Positionen, Zahlungsplan, Zeitquelle). */ +function FeldFehler({ text }) { + return text ?
{text}
: null +} + +/** Schritt 1: Projekt, Titel und (falls am Kunden noch leer) die Kundenart. */ +export function SchrittProjekt({ form, onChange, fehler, kunde, konten, neueFassung = false }) { + const name = customerName(kunde) || 'Kunde' + return ( +
+

+ Kunde: {name} + {kunde?.email ? ` · ${kunde.email}` : ''} +

+ {neueFassung &&

Neue Fassung: Beim Senden ersetzt sie die bisherige Auftragsbestätigung.

} + + + + + onChange({ ...form, titel: e.target.value, titelGeaendert: true })} + /> + + {!kunde?.kundenart ? ( + + + + ) : ( +

Kundenart: {kundenartLabel(kunde.kundenart)}. Änderbar in der Übersicht des Kunden.

+ )} +
+ ) +} + +/** Schritt 2: Absprache (Datum, Weg), Art des Auftrags und Positionen bzw. Stundensatz. */ +export function SchrittAuftrag({ form, onChange, fehler, kunde }) { + const vorOrt = vorOrtHinweis(form.auftragWeg, kundenartWirksam(kunde, form.kundenart)) + const positionen = form.positionen + const setPosition = (uid, feld, wert) => + onChange({ ...form, positionen: positionen.map((p) => (p.uid === uid ? { ...p, [feld]: wert } : p)) }) + const entferne = (uid) => onChange({ ...form, positionen: positionen.filter((p) => p.uid !== uid) }) + const summe = formularSummeCents(form) + + return ( +
+
+ + onChange({ ...form, auftragVom: e.target.value })} /> + + + + +
+ {vorOrt &&

{vorOrt}

} + +
+ Art des Auftrags + + + +
+ + {form.art === 'festpreis' && ( +
+ Positionen + {positionen.map((p, i) => { + const n = i + 1 + const zeile = lineTotalCents(p) + return ( +
+
+ Beschreibung + setPosition(p.uid, 'description', e.target.value)} + /> +
+
+ Menge + setPosition(p.uid, 'quantity', e.target.value)} + /> +
+
+ Einzelpreis in € + setPosition(p.uid, 'price', e.target.value)} + /> +
+
+ Summe + {zeile === null ? '–' : euro(zeile)} +
+
+ +
+
+ ) + })} +
+ +
+
+ Summe + {euro(summe)} +
+

Gemäß § 19 UStG ohne Umsatzsteuer.

+ +
+ )} + + {form.art === 'aufwand' && ( +
+ + onChange({ ...form, stundensatz: e.target.value })} /> + + + onChange({ ...form, schaetzung: e.target.value })} + /> + +
+ )} +
+ ) +} + +/** Zeitquelle: Tickets des Kunden, deren Arbeitsblätter zum Auftrag zählen (höchstens 10). */ +function ZeitquelleAuswahl({ kundeId, form, onChange, projektTicketIds, fehler }) { + const { workorders, loading, error } = useWorkorders({ customerId: kundeId, limit: 100 }) + const optionen = zeitquelleOptionen(workorders, form.zeitquelle, projektTicketIds) + const voll = form.zeitquelle.length >= AUFTRAG_LIMITS.maxZeitquellen + return ( +
+ Zeiterfassung aus diesen Tickets +

Arbeitsblätter dieser Tickets zählen zum Auftrag (Projektkonto, Aufwandsrechnung). Höchstens 10.

+ {loading && optionen.length === 0 &&
Tickets werden geladen …
} + {error &&

Die Tickets konnten nicht geladen werden: {error}. Die bisherige Auswahl bleibt.

} + {!loading && !error && optionen.length === 0 &&

Dieser Kunde hat noch keine Tickets.

} + {optionen.length > 0 && ( +
+ {optionen.map((o) => ( + + ))} +
+ )} + +
+ ) +} + +/** Schritt 3: Zahlungsplan, Zahlungsziel, Termine und Zeitquelle. */ +export function SchrittZahlung({ form, onChange, fehler, kunde, konten }) { + const konto = (Array.isArray(konten) ? konten : []).find((k) => k.projektId && k.projektId === form.projektId) + const projektTicketIds = konto && Array.isArray(konto.tickets) ? konto.tickets.map((t) => t.id) : [] + const vorschau = zahlungsplanVorschau({ + art: form.zahlungsart, + prozent: parseProzent(form.prozent), + text: form.zahlungstext, + summeCents: formularSummeCents(form), + zielTage: parseTage(form.zahlungsziel), + }) + return ( +
+
+ Zahlungsplan + {zahlungsplanOptionen(form.art).map((o) => ( + + ))} + +
+ {form.zahlungsart === 'anzahlung' && ( + + onChange({ ...form, prozent: e.target.value })} /> + + )} + {form.zahlungsart === 'frei' && ( +
+ +