Files
AfiS-Aktensystem/web/components/sections/SectionsForm.tsx
T
Harlekin e0b03ea336
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

128 lines
3.9 KiB
TypeScript

'use client'
import { createContext, useContext, useRef, useState, type ReactNode } from 'react'
import { sectionText } from '@/lib/i18n/cases'
import type { SaveSectionResult, SaveSections } from './types'
/**
* Das Formular um die Abschnitte herum: **ein** Knopf für das ganze Blatt.
*
* Vorher trug jedes Feld seinen eigenen. Einen Arbeitsvertrag auszufüllen
* hiess damit zwölfmal speichern, und wer eines vergass, merkte es erst beim
* Nachlesen. Jetzt sammelt dieses Formular ein, was sich geändert hat, und
* schickt es in einem Zug.
*
* Gespeichert wird **je Feld** und nicht als Ganzes. Das ist kein Versehen:
* Abschnitte sind einzeln versioniert, und wenn eines zwischenzeitlich von
* jemand anderem geändert wurde, soll das die übrigen nicht aufhalten. Die
* Rückmeldung sagt dann genau, was durchging und was noch offen ist.
*
* Die Felder melden sich hier an, statt ihren Zustand nach oben zu geben:
* so löst ein Tastendruck in einem Feld kein Neuzeichnen des ganzen Blattes
* aus.
*/
export type FieldHandle = {
fieldKey: string
/** Der geänderte Wert, oder null wenn unverändert. */
collect: () => { fieldKey: string; value: unknown; baseVersion: number } | null
apply: (result: SaveSectionResult) => void
}
type Kontext = {
register: (handle: FieldHandle) => () => void
changed: (fieldKey: string, dirty: boolean) => void
pending: boolean
}
const SectionsContext = createContext<Kontext | null>(null)
export function useSectionsForm(): Kontext | null {
return useContext(SectionsContext)
}
export function SectionsForm({
save,
label,
children,
}: {
save: SaveSections
/** „Dokument speichern" oder „Abschnitte speichern" - je nachdem, was es ist. */
label: string
children: ReactNode
}) {
const felder = useRef(new Map<string, FieldHandle>())
const [geaendert, setGeaendert] = useState<Set<string>>(new Set())
const [pending, setPending] = useState(false)
const [meldung, setMeldung] = useState<string | null>(null)
const kontext: Kontext = {
register(handle) {
felder.current.set(handle.fieldKey, handle)
return () => {
felder.current.delete(handle.fieldKey)
}
},
changed(fieldKey, dirty) {
setGeaendert((bisher) => {
if (dirty === bisher.has(fieldKey)) return bisher
const naechste = new Set(bisher)
if (dirty) naechste.add(fieldKey)
else naechste.delete(fieldKey)
return naechste
})
},
pending,
}
async function speichern() {
const eintraege = [...felder.current.values()]
.map((handle) => handle.collect())
.filter((eintrag) => eintrag !== null)
if (eintraege.length === 0) {
setMeldung(sectionText.unchanged)
return
}
setPending(true)
setMeldung(null)
try {
const ergebnisse = await save(eintraege)
let gut = 0
let offen = 0
for (const [fieldKey, ergebnis] of Object.entries(ergebnisse)) {
felder.current.get(fieldKey)?.apply(ergebnis)
if (ergebnis.ok) gut++
else offen++
}
setMeldung(offen === 0 ? sectionText.savedCount(gut) : sectionText.partly(gut, offen))
} finally {
setPending(false)
}
}
return (
<SectionsContext.Provider value={kontext}>
{children}
<div className="sticky bottom-0 mt-8 flex flex-wrap items-center gap-4 border-t border-line bg-surface py-3">
<button
type="button"
onClick={() => void speichern()}
disabled={pending}
aria-busy={pending}
className="btn btn-primary"
>
{pending ? sectionText.saving : label}
</button>
<span className="font-condensed text-2xs text-text-dim">
{geaendert.size > 0 ? sectionText.changedCount(geaendert.size) : sectionText.unchanged}
</span>
{meldung ? <span className="font-condensed text-2xs text-accent">{meldung}</span> : null}
</div>
</SectionsContext.Provider>
)
}