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.
170 lines
5.9 KiB
TypeScript
170 lines
5.9 KiB
TypeScript
import { de } from '@/lib/i18n/de'
|
|
import { fillPlaceholders, isInputField } from '@/lib/templates/fields'
|
|
import { RedactedText } from '@/components/Redacted'
|
|
import { SectionEditor } from './SectionEditor'
|
|
import { SectionsForm } from './SectionsForm'
|
|
import { PhotoField } from '@/components/files/PhotoField'
|
|
import type { SaveSection, SaveSections } from './types'
|
|
import type { RenderedSection } from '@/lib/sections/render'
|
|
|
|
/**
|
|
* Ausgefüllte Vorlage: der Sachstand einer Fallakte oder der Inhalt eines
|
|
* Dokuments. Beide sind dasselbe Gebilde, deshalb dieselbe Darstellung.
|
|
*
|
|
* Feste Textbausteine der Vorlage werden gerendert, nicht bearbeitet — ihre
|
|
* Platzhalter ziehen die Werte der Eingabefelder. Damit trägt dieselbe
|
|
* Darstellung ein Formular und einen Vertragstext.
|
|
*
|
|
* Gespeichert wird das Blatt als Ganzes: ein Knopf unten, nicht einer je Feld.
|
|
* Lichtbilder sind die Ausnahme — ein Bild wird hochgeladen und geprüft, das
|
|
* ist ein eigener Vorgang und keine Feldänderung.
|
|
*/
|
|
export function Sections({
|
|
scope,
|
|
save,
|
|
saveAll,
|
|
saveAllLabel,
|
|
sections,
|
|
canEdit,
|
|
documentNumber,
|
|
topic,
|
|
photoUrls,
|
|
}: {
|
|
scope: string
|
|
save: SaveSection
|
|
/** Alle geänderten Felder in einem Zug. Fehlt sie, trägt jedes Feld seinen Knopf. */
|
|
saveAll?: SaveSections
|
|
saveAllLabel?: string
|
|
sections: RenderedSection[]
|
|
canEdit: boolean
|
|
/** Nur bei Dokumenten gesetzt: Ziel für das Lichtbild. */
|
|
documentNumber?: string
|
|
/** Thema für die Rückmeldung der Dateiprüfung. */
|
|
topic?: string
|
|
/** Bereits geprüfte Lichtbilder, je Feldschlüssel. */
|
|
photoUrls?: Record<string, string>
|
|
}) {
|
|
if (sections.length === 0) {
|
|
return <p className="text-text-dim">{de.section.empty}</p>
|
|
}
|
|
|
|
const werte = Object.fromEntries(
|
|
sections.filter((s) => s.visible).map((s) => [s.fieldKey, s.value]),
|
|
)
|
|
|
|
const blatt = (
|
|
<dl className="space-y-8">
|
|
{sections.map((section) => {
|
|
const field = section.field
|
|
|
|
// Feste Bausteine der Vorlage.
|
|
if (!isInputField(field)) {
|
|
if (field.type === 'pagebreak') return <hr key={field.key} className="border-line" />
|
|
const text = fillPlaceholders(field.text ?? '', werte)
|
|
return field.type === 'heading' ? (
|
|
<h3 key={field.key} className="font-condensed">
|
|
{text || field.label}
|
|
</h3>
|
|
) : (
|
|
<p key={field.key} className="max-w-(--container-measure) text-sm whitespace-pre-wrap">
|
|
{text}
|
|
</p>
|
|
)
|
|
}
|
|
|
|
if (field.type === 'photo') {
|
|
// Ohne Dokument gibt es kein Ziel für den Upload — Fallabschnitte
|
|
// tragen keine Bilder, ihre Dateien hängen an der Akte.
|
|
if (!documentNumber) {
|
|
return (
|
|
<div key={field.key}>
|
|
<dt className="font-condensed text-xs text-text-dim">{field.label}</dt>
|
|
<dd className="mt-1 text-2xs text-text-dim">{de.document.photoLater}</dd>
|
|
</div>
|
|
)
|
|
}
|
|
return (
|
|
<PhotoField
|
|
key={field.key}
|
|
save={save}
|
|
fieldKey={field.key}
|
|
label={field.label}
|
|
documentNumber={documentNumber}
|
|
topic={topic ?? ''}
|
|
initialAttachmentId={
|
|
section.visible && typeof section.value === 'string' ? section.value : null
|
|
}
|
|
initialUrl={photoUrls?.[field.key] ?? null}
|
|
initialVersion={section.version}
|
|
readOnly={!canEdit}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div key={field.key}>
|
|
<dt className="flex flex-wrap items-baseline gap-x-3">
|
|
<span className="font-condensed text-xs text-text-dim">{field.label}</span>
|
|
{field.requiredToClose ? (
|
|
<span className="font-condensed text-2xs text-stamp-3">
|
|
{de.section.requiredToClose}
|
|
</span>
|
|
) : null}
|
|
</dt>
|
|
<dd>
|
|
{!section.visible ? (
|
|
// Serverseitig ersetzt: der echte Wert ist gar nicht erst hier.
|
|
<p className="mt-1.5 flex items-center gap-2">
|
|
<span className="redaction inline-block h-[1.1rem] w-40" aria-hidden />
|
|
<span className="font-mono text-2xs text-text-dim">
|
|
{de.section.lockedField(section.value.requiredLevel)}
|
|
</span>
|
|
</p>
|
|
) : section.segments ? (
|
|
<div className="mt-1.5">
|
|
<RedactedText segments={section.segments} />
|
|
{canEdit ? (
|
|
<SectionEditor
|
|
save={save}
|
|
scope={scope}
|
|
fieldKey={field.key}
|
|
label={field.label}
|
|
type={field.type}
|
|
options={field.options}
|
|
requiredToClose={field.requiredToClose}
|
|
initialValue={section.value}
|
|
initialVersion={section.version}
|
|
readOnly={false}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<SectionEditor
|
|
save={save}
|
|
scope={scope}
|
|
fieldKey={field.key}
|
|
label={field.label}
|
|
type={field.type}
|
|
options={field.options}
|
|
requiredToClose={field.requiredToClose}
|
|
initialValue={section.value}
|
|
initialVersion={section.version}
|
|
readOnly={!canEdit}
|
|
/>
|
|
)}
|
|
</dd>
|
|
</div>
|
|
)
|
|
})}
|
|
</dl>
|
|
)
|
|
|
|
if (!canEdit || !saveAll) return blatt
|
|
|
|
return (
|
|
<SectionsForm save={saveAll} label={saveAllLabel ?? de.section.saveAll}>
|
|
{blatt}
|
|
</SectionsForm>
|
|
)
|
|
}
|