Kundenart in der Kunden-Uebersicht und Steuernummer in den Finanz-Einstellungen (Helfer mit Tests)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99ab4bd47b
commit
674cce26f0
4 files changed
+185
-6
No files matched your search
@@ -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 }) {
|
||||
<div className={`fin-counter${tooLong ? ' fin-counter-over' : ''}`}>
|
||||
{length} / {SETTINGS_LIMITS.maxBankDetails} Zeichen
|
||||
</div>
|
||||
<FormField id={fieldId('steuernummer')} label="Steuernummer" hint={STEUERNUMMER_HINWEIS} error={errors.steuernummer}>
|
||||
<input
|
||||
className="input"
|
||||
autoComplete="off"
|
||||
value={steuernummer}
|
||||
onChange={(e) => {
|
||||
setSteuernummer(e.target.value)
|
||||
setErrors((er) => (er.steuernummer ? { ...er, steuernummer: undefined } : er))
|
||||
if (notice) setNotice(null)
|
||||
}}
|
||||
placeholder="z. B. 12/345/67890 oder DE123456789"
|
||||
/>
|
||||
</FormField>
|
||||
</fieldset>
|
||||
|
||||
{notice && <p {...noticeProps(notice.tone, 'fin-notice')}>{notice.text}</p>}
|
||||
|
||||
@@ -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() === ''
|
||||
}
|
||||
@@ -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 }) {
|
||||
<Card title="Rechnungsdaten">
|
||||
{!ready && <div className="kunde-hinweis"><Badge tone="warn">Adresse unvollständig</Badge></div>}
|
||||
<dl className="kv kunde-kv">
|
||||
<dt><label htmlFor="kunde-kundenart">Kundenart</label></dt>
|
||||
<dd>
|
||||
<select id="kunde-kundenart" className="select" value={kundenart} onChange={(e) => setKundenart(e.target.value)}>
|
||||
{KUNDENART_OPTIONEN.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
<div className="hint">{KUNDENART_HINWEIS}</div>
|
||||
</dd>
|
||||
<dt><label htmlFor="kunde-street">Strasse + Nr.</label></dt>
|
||||
<dd><input id="kunde-street" className="input" value={form.street} onChange={(e) => setForm({ ...form, street: e.target.value })} /></dd>
|
||||
<dt><label htmlFor="kunde-postal">PLZ</label></dt>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { loadModule } from './load.mjs'
|
||||
|
||||
const {
|
||||
KUNDENARTEN, KUNDENART_OPTIONEN, KUNDENART_HINWEIS, kundenartLabel, kundenartAenderung,
|
||||
STEUERNUMMER_MAX, STEUERNUMMER_HINWEIS, steuernummerAenderung, STEUERNUMMER_FEHLT_HINWEIS, steuernummerFehlt,
|
||||
} = await loadModule('src/lib/kundenFelder.js')
|
||||
|
||||
test('Kundenart: Werte (Vertrag V2), Auswahl und Beschriftung', () => {
|
||||
assert.deepEqual(KUNDENARTEN, ['unternehmen', 'privat'])
|
||||
assert.deepEqual(KUNDENART_OPTIONEN, [
|
||||
{ value: '', label: 'Nicht festgelegt' },
|
||||
{ value: 'unternehmen', label: 'Unternehmen' },
|
||||
{ value: 'privat', label: 'Privatperson' },
|
||||
])
|
||||
assert.equal(KUNDENART_HINWEIS, 'Für Auftragsbestätigungen: Privatpersonen bekommen die Widerrufsbelehrung.')
|
||||
assert.equal(kundenartLabel('unternehmen'), 'Unternehmen')
|
||||
assert.equal(kundenartLabel('privat'), 'Privatperson')
|
||||
for (const v of ['', undefined, null, 'firma']) assert.equal(kundenartLabel(v), 'Nicht festgelegt')
|
||||
})
|
||||
|
||||
test('kundenartAenderung: nur eine geänderte, bekannte Kundenart wird geschrieben', () => {
|
||||
assert.deepEqual(kundenartAenderung('', ''), {})
|
||||
assert.deepEqual(kundenartAenderung('', undefined), {})
|
||||
assert.deepEqual(kundenartAenderung('privat', 'privat'), {})
|
||||
assert.deepEqual(kundenartAenderung('privat', ''), { kundenart: 'privat' })
|
||||
assert.deepEqual(kundenartAenderung('privat', undefined), { kundenart: 'privat' })
|
||||
assert.deepEqual(kundenartAenderung('unternehmen', 'privat'), { kundenart: 'unternehmen' })
|
||||
assert.deepEqual(kundenartAenderung('', 'privat'), { kundenart: '' })
|
||||
// unbekannte Werte werden nie geschrieben
|
||||
assert.deepEqual(kundenartAenderung('firma', ''), {})
|
||||
assert.deepEqual(kundenartAenderung('firma', 'privat'), {})
|
||||
})
|
||||
|
||||
test('Steuernummer: Hinweis und Grenze', () => {
|
||||
assert.equal(STEUERNUMMER_HINWEIS, 'Steht auf jeder Auftragsbestätigung. Für Rechnungen bitte zusätzlich im Stripe-Dashboard hinterlegen.')
|
||||
assert.equal(STEUERNUMMER_MAX, 40)
|
||||
})
|
||||
|
||||
test('steuernummerAenderung: unverändert sendet nichts, geändert den getrimmten Wert, leer löscht', () => {
|
||||
assert.deepEqual(steuernummerAenderung('12/345/67890', '12/345/67890'), { ok: true, payload: {} })
|
||||
assert.deepEqual(steuernummerAenderung('', ''), { ok: true, payload: {} })
|
||||
assert.deepEqual(steuernummerAenderung('', null), { ok: true, payload: {} })
|
||||
assert.deepEqual(steuernummerAenderung(' 12/345/67890 ', '12/345/67890'), { ok: true, payload: {} })
|
||||
assert.deepEqual(steuernummerAenderung(' 12/345/67890 ', ''), { ok: true, payload: { steuernummer: '12/345/67890' } })
|
||||
assert.deepEqual(steuernummerAenderung('', '12/345/67890'), { ok: true, payload: { steuernummer: '' } })
|
||||
assert.equal(steuernummerAenderung('123 456 789-0', '').ok, true)
|
||||
assert.equal(steuernummerAenderung('1'.repeat(40), '').ok, true)
|
||||
})
|
||||
|
||||
test('steuernummerAenderung: Buchstaben, Ziffern, Leerzeichen, „/“, „-“ und „.“ sind erlaubt wie auf dem Server (V4)', () => {
|
||||
for (const gut of ['12/345/67890', '12.345.67890', 'DE123456789', 'de 123 456 789', 'DE-123.456/789', 'a', 'USt-IdNr 12/345']) {
|
||||
assert.deepEqual(steuernummerAenderung(gut, ''), { ok: true, payload: { steuernummer: gut } }, gut)
|
||||
}
|
||||
assert.deepEqual(steuernummerAenderung(' DE123456789 ', ''), { ok: true, payload: { steuernummer: 'DE123456789' } })
|
||||
assert.deepEqual(steuernummerAenderung('A'.repeat(40), ''), { ok: true, payload: { steuernummer: 'A'.repeat(40) } })
|
||||
})
|
||||
|
||||
test('steuernummerAenderung: andere Zeichen und zu lang', () => {
|
||||
const zeichen = 'Die Steuernummer darf nur Buchstaben (ohne Umlaute), Ziffern, Leerzeichen, „/“, „-“ und „.“ enthalten.'
|
||||
for (const schlecht of ['12_345', '12,345', '12#34', 'Müller 12', 'ß12', '12\t34', '12\n34', '12+34', '123']) {
|
||||
assert.deepEqual(steuernummerAenderung(schlecht, ''), { ok: false, fehler: zeichen }, JSON.stringify(schlecht))
|
||||
}
|
||||
const LANG = 'Die Steuernummer darf höchstens 40 Zeichen lang sein.'
|
||||
assert.deepEqual(steuernummerAenderung('1'.repeat(41), ''), { ok: false, fehler: LANG })
|
||||
assert.deepEqual(steuernummerAenderung('A'.repeat(41), ''), { ok: false, fehler: LANG })
|
||||
assert.equal(steuernummerAenderung('_'.repeat(41), '').fehler, LANG) // die Länge wird zuerst geprüft
|
||||
})
|
||||
|
||||
test('steuernummerFehlt: Hinweis nur, wenn die Einstellungen da sind und die Steuernummer leer ist', () => {
|
||||
assert.equal(STEUERNUMMER_FEHLT_HINWEIS, 'Die Steuernummer fehlt noch – sie steht dann nicht auf der Auftragsbestätigung. Eintragen unter Finanzen → Einstellungen.')
|
||||
for (const leer of [{ steuernummer: '' }, { steuernummer: ' ' }, { steuernummer: null }, { steuernummer: 12 }, {}, { bankDetailsText: 'IBAN' }]) {
|
||||
assert.equal(steuernummerFehlt(leer), true, JSON.stringify(leer))
|
||||
}
|
||||
for (const da of [{ steuernummer: '12/345/67890' }, { steuernummer: ' DE123456789 ' }]) assert.equal(steuernummerFehlt(da), false)
|
||||
// noch nicht geladen oder nicht ladbar: es wird nichts behauptet
|
||||
for (const unbekannt of [undefined, null, 'x', 5, []]) assert.equal(steuernummerFehlt(unbekannt), false, String(unbekannt))
|
||||
})
|
||||
Reference in new issue
Block a user