diff --git a/src/components/finance/SettingsCard.jsx b/src/components/finance/SettingsCard.jsx index 4e87f4a..9570475 100644 --- a/src/components/finance/SettingsCard.jsx +++ b/src/components/finance/SettingsCard.jsx @@ -13,6 +13,7 @@ import { settingsToForm, validateSettingsForm, } from '../../lib/financeSettings' +import { STEUERNUMMER_HINWEIS, steuernummerAenderung } from '../../lib/kundenFelder' const LOAD_ERROR = 'Die Einstellungen konnten nicht geladen werden.' const fieldId = (name) => `fin-set-${name}` @@ -44,6 +45,8 @@ export default function SettingsCard() { function SettingsForm({ initial }) { const [form, setForm] = useState(() => settingsToForm(initial)) + const [steuernummer, setSteuernummer] = useState(() => (typeof initial?.steuernummer === 'string' ? initial.steuernummer : '')) + const gespeichertRef = useRef(typeof initial?.steuernummer === 'string' ? initial.steuernummer : '') const [errors, setErrors] = useState({}) const [busy, setBusy] = useState(false) const [notice, setNotice] = useState(null) // { tone: 'ok' | 'warn', text } @@ -68,10 +71,12 @@ function SettingsForm({ initial }) { e.preventDefault() if (lockRef.current) return const check = validateSettingsForm(form) - if (!check.ok) { - setErrors(check.errors) + const stn = steuernummerAenderung(steuernummer, gespeichertRef.current) + if (!check.ok || !stn.ok) { + const fehler = { ...(check.errors || {}), ...(stn.ok ? {} : { steuernummer: stn.fehler }) } + setErrors(fehler) setNotice({ tone: 'warn', text: 'Bitte die markierten Felder prüfen. Es wurde nichts gespeichert.' }) - const first = SETTINGS_FIELD_ORDER.find((name) => check.errors[name]) + const first = [...SETTINGS_FIELD_ORDER, 'steuernummer'].find((name) => fehler[name]) if (first) document.getElementById(fieldId(first))?.focus() return } @@ -80,9 +85,12 @@ function SettingsForm({ initial }) { setErrors({}) setNotice(null) try { - const data = await patchSettings(check.payload) + const data = await patchSettings({ ...check.payload, ...stn.payload }) if (!aliveRef.current) return if (data?.einstellungen) setForm(settingsToForm(data.einstellungen)) + const gespeichert = typeof data?.einstellungen?.steuernummer === 'string' ? data.einstellungen.steuernummer : steuernummer.trim() + setSteuernummer(gespeichert) + gespeichertRef.current = gespeichert setNotice({ tone: 'ok', text: 'Gespeichert.' }) } catch (err) { if (aliveRef.current) setNotice({ tone: 'warn', text: apiErrorMessage(err) }) @@ -148,6 +156,19 @@ function SettingsForm({ initial }) {
{notice.text}
} diff --git a/src/lib/kundenFelder.js b/src/lib/kundenFelder.js new file mode 100644 index 0000000..acaca55 --- /dev/null +++ b/src/lib/kundenFelder.js @@ -0,0 +1,67 @@ +// Reine Helfer fuer zwei Felder der Auftragsbestaetigung: die Kundenart am Kunden und die Steuernummer in den +// Finanz-Einstellungen; kein React, kein Appwrite, unter Node testbar. Texte deutsch, sachlich. + +/** Werte der Kundenart (Vertrag V2); '' = nicht festgelegt. */ +export const KUNDENARTEN = ['unternehmen', 'privat'] + +export const KUNDENART_OPTIONEN = [ + { value: '', label: 'Nicht festgelegt' }, + { value: 'unternehmen', label: 'Unternehmen' }, + { value: 'privat', label: 'Privatperson' }, +] + +export const KUNDENART_HINWEIS = 'Für Auftragsbestätigungen: Privatpersonen bekommen die Widerrufsbelehrung.' + +const text = (v) => (typeof v === 'string' ? v : '') + +/** Beschriftung der Kundenart; alles Unbekannte heisst „Nicht festgelegt“. */ +export function kundenartLabel(value) { + const treffer = KUNDENART_OPTIONEN.find((o) => o.value !== '' && o.value === value) + return treffer ? treffer.label : KUNDENART_OPTIONEN[0].label +} + +/** + * Was beim Speichern der Kundendaten zusaetzlich ins Dokument soll: nur eine geaenderte, bekannte Kundenart + * (so scheitert das Speichern der uebrigen Stammdaten nie an einem noch nicht angelegten Attribut). + */ +export function kundenartAenderung(neu, gespeichert) { + const n = text(neu) + const alt = text(gespeichert) + if (n !== '' && !KUNDENARTEN.includes(n)) return {} + if (n === alt) return {} + return { kundenart: n } +} + +export const STEUERNUMMER_MAX = 40 + +export const STEUERNUMMER_HINWEIS = 'Steht auf jeder Auftragsbestätigung. Für Rechnungen bitte zusätzlich im Stripe-Dashboard hinterlegen.' + +export const STEUERNUMMER_FEHLT_HINWEIS = + 'Die Steuernummer fehlt noch – sie steht dann nicht auf der Auftragsbestätigung. Eintragen unter Finanzen → Einstellungen.' + +const STEUERNUMMER_ZEICHEN = /^[A-Za-z0-9/ .-]*$/ + +/** + * Steuernummer fuer PATCH /einstellungen. Der Client prueft wie der Server (Vertrag V4): getrimmt hoechstens 40 Zeichen aus + * ASCII-Buchstaben, Ziffern, Leerzeichen, „/“, „-“ und „.“; leer loescht den Wert. Unveraendert wird nichts gesendet. + */ +export function steuernummerAenderung(eingabe, gespeichert) { + const neu = text(eingabe).trim() + const alt = text(gespeichert).trim() + if (neu === alt) return { ok: true, payload: {} } + if (neu.length > STEUERNUMMER_MAX) return { ok: false, fehler: `Die Steuernummer darf höchstens ${STEUERNUMMER_MAX} Zeichen lang sein.` } + if (!STEUERNUMMER_ZEICHEN.test(neu)) { + return { ok: false, fehler: 'Die Steuernummer darf nur Buchstaben (ohne Umlaute), Ziffern, Leerzeichen, „/“, „-“ und „.“ enthalten.' } + } + return { ok: true, payload: { steuernummer: neu } } +} + +/** + * Fehlt die Steuernummer? Nur wahr, wenn die Einstellungen da sind (Objekt) und der Wert leer ist; „noch nicht geladen“ + * (undefined) und „nicht ladbar“ (null) behaupten nichts. + */ +export function steuernummerFehlt(einstellungen) { + if (!einstellungen || typeof einstellungen !== 'object' || Array.isArray(einstellungen)) return false + const wert = einstellungen.steuernummer + return typeof wert !== 'string' || wert.trim() === '' +} diff --git a/src/pages/CustomerDetailPage.jsx b/src/pages/CustomerDetailPage.jsx index d7d3059..54c0d18 100644 --- a/src/pages/CustomerDetailPage.jsx +++ b/src/pages/CustomerDetailPage.jsx @@ -12,6 +12,7 @@ import { useWebsiteProjects } from '../hooks/useWebsiteProjects' import { customerTabs, resolveCustomerTab } from '../lib/customerTabs' import { isInvoiceReady, setCustomerArchived, upgradeToCustomer, markLost, reopenAsLead } from '../lib/leads' import { lastRejection } from '../lib/leadDuplicates' +import { KUNDENART_OPTIONEN, KUNDENART_HINWEIS, kundenartAenderung } from '../lib/kundenFelder' import { customerStage, isArchived, customerBadges, customerEyebrow, customerTitle } from '../lib/customerList' import { ticketPath, ticketRef } from '../lib/ticketRoutes' import { ticketNumber } from '../lib/ticketView' @@ -154,11 +155,15 @@ function OverviewTab({ customer, stage, onSaved }) { notes: customer.notes || '', importantCustomer: !!customer.importantCustomer, }) + const [kundenart, setKundenart] = useState(customer.kundenart || '') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState(null) // { tone: 'ok' | 'err' | 'warn', text } const [showLost, setShowLost] = useState(false) const { user, employee } = useAuth() + // Die Kundenart geht nur ins Dokument, wenn sie geaendert wurde (kein Fehler bei noch nicht angelegtem Attribut) + const felder = () => ({ ...form, ...kundenartAenderung(kundenart, customer.kundenart) }) + const ready = isInvoiceReady({ ...customer, ...form }) // Absage: ungespeicherte Notizen gehen mit, damit der Vermerk nichts ueberschreibt @@ -182,7 +187,7 @@ function OverviewTab({ customer, stage, onSaved }) { const save = async () => { setBusy(true); setMsg(null) try { - await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, form) + await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, felder()) setMsg({ tone: 'ok', text: 'Gespeichert' }) onSaved() } catch (err) { @@ -194,7 +199,7 @@ function OverviewTab({ customer, stage, onSaved }) { if (!ready) { setMsg({ tone: 'warn', text: 'Bitte zuerst alle Pflichtfelder (Unternehmen, E-Mail, Strasse, PLZ, Stadt) ausfuellen.' }); return } setBusy(true); setMsg(null) try { - await upgradeToCustomer(customer, form) + await upgradeToCustomer(customer, felder()) setMsg({ tone: 'ok', text: 'Zum festen Kunden gemacht.' }) onSaved() } catch (err) { @@ -249,6 +254,13 @@ function OverviewTab({ customer, stage, onSaved }) {