Files
AfiS-Aktensystem/web/components/sections/SectionsForm.tsx
T
HarlekinandClaude Opus 5 5f9f5d99fb Protokoll im Adminbereich, ein Knopf für das ganze Blatt
Die restlichen zwei Änderungswünsche.

**Protokoll.** „Wer was wo macht" stand seit dem ersten Tag in AuditLog —
harte Regel 3 sorgt dafür. Was fehlte, war die Ansicht darauf. /admin/protokoll
ist genau das und keine zweite Aufzeichnung: filterbar nach Person, Aktion,
Art und Zeitraum, rückwärts blätternd.

Nur für die Leitung. Eine Zeile nennt Aktenzeichen und Namen, und schon die
Existenz einer Zeile verrät, dass es einen Vorgang gibt; ein gefiltertes
Protokoll für alle wäre ein Weg, die Sichtbarkeitsregeln von hinten
aufzurollen. Aus demselben Grund werden Kennungen ohne Sichtbarkeitsfilter zu
Aktenzeichen aufgelöst — ein Protokoll aus UUIDs liest niemand, und eines, das
niemand liest, ist keine Kontrolle.

**Speichern.** Jedes Feld trug seinen eigenen Knopf: ein Arbeitsvertrag mit
zwölf Feldern hiess zwölfmal speichern. Der Knopf steht jetzt am Blatt.

Geschrieben wird trotzdem je Feld und nicht als Transaktion, und das folgt aus
dem Datenmodell: Abschnitte sind einzeln versioniert, weil mehrere Leute
gleichzeitig an einer Akte arbeiten. Als Transaktion blockierte ein einziges
zwischenzeitlich geändertes Feld das ganze Blatt — elf gute Änderungen verloren,
weil jemand das zwölfte angefasst hat. So geht durch, was durchgeht, und die
Rückmeldung sagt, was offen blieb und wo. Lichtbilder bleiben ausgenommen: ein
Upload mit Prüfkette ist keine Feldänderung.

lib/audit.ts liegt jetzt als lib/audit/write.ts neben lib/audit/query.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:16:10 +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>
)
}