feat(laeufe): Service, Abarbeitung, Kommentare und Archivschutz
Ein Lauf wird vorbereitet, abgearbeitet, abgeschlossen und archiviert. Der Archiv-Schreibschutz sitzt in genau einer Funktion und wird vor jeder Aenderung an Kopfdaten, Schrittergebnis und Kommentar gerufen; abgeschlossen bleibt aenderbar, archiviert nicht. Die Abarbeitungsansicht speichert jeden Schritt einzeln, frischt sich alle zehn Sekunden auf und pausiert im Hintergrund. Sie traegt in der Polling-Antwort keine Befehle und laedt sich auch nicht von selbst neu, solange ein generiertes Passwort auf der Seite steht: beides tauschte es dem Techniker zwischen Lesen und Einfuegen aus. Der unabhaengige Review fand zwei Blocker: ein Befehl konnte zwei verschiedene Passwoerter tragen (Konto bekommt A, Benutzer erfaehrt B), und „nicht zutreffend“ liess sich nicht zuruecknehmen, waehrend die Oberflaeche behauptete, nichts sei gewaehlt. Beides behoben und in Tests festgehalten. Closes #13 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eec5de404a
commit
ed6e6b81eb
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useState } from "react";
|
||||
import { SchrittTyp, SchrittZustand } from "@/generated/prisma/enums";
|
||||
import { FELD } from "@/lib/formular";
|
||||
import { schrittergebnisSetzenAktion, type AktionsErgebnis } from "@/server/actions/laeufe";
|
||||
|
||||
/**
|
||||
* Das Eingabefeld eines Schritts, passend zu seinem Typ (#013).
|
||||
*
|
||||
* Gespeichert wird mit einem eigenen Knopf und nicht bei jedem Tastendruck: ein
|
||||
* halb getippter Wert soll nicht in die Datenbank, und ein Lauf wird oft am
|
||||
* Handy mit wackligem Netz abgearbeitet.
|
||||
*
|
||||
* Ein `ZAHL`-Wert ausserhalb von Min/Max wird **markiert, nicht blockiert**. Die
|
||||
* Grenzen sind eine Erwartung, kein Gesetz — ein Rechner mit 4 GB statt der
|
||||
* erwarteten 8 ist ein Befund, den der Lauf festhalten soll, kein Eingabefehler.
|
||||
*/
|
||||
export function SchrittWert({
|
||||
laufId,
|
||||
vorlagenSchrittId,
|
||||
typ,
|
||||
konfig,
|
||||
wert,
|
||||
zustand,
|
||||
lesend,
|
||||
}: {
|
||||
laufId: string;
|
||||
vorlagenSchrittId: string;
|
||||
typ: SchrittTyp;
|
||||
konfig: Record<string, unknown>;
|
||||
wert: unknown;
|
||||
zustand: SchrittZustand;
|
||||
lesend: boolean;
|
||||
}) {
|
||||
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
|
||||
schrittergebnisSetzenAktion,
|
||||
{},
|
||||
);
|
||||
const [eingabe, setzeEingabe] = useState(wert === null || wert === undefined ? "" : String(wert));
|
||||
|
||||
if (typ === SchrittTyp.HAKEN || typ === SchrittTyp.BEFEHL) return null;
|
||||
|
||||
if (lesend) {
|
||||
return eingabe ? (
|
||||
<p className="mono text-sm text-text">{eingabe}</p>
|
||||
) : (
|
||||
<p className="text-sm text-nebentext">kein Wert erfasst</p>
|
||||
);
|
||||
}
|
||||
|
||||
const zahl = typ === SchrittTyp.ZAHL ? Number(eingabe) : Number.NaN;
|
||||
const min = typeof konfig["min"] === "number" ? konfig["min"] : undefined;
|
||||
const max = typeof konfig["max"] === "number" ? konfig["max"] : undefined;
|
||||
const ausserhalb =
|
||||
!Number.isNaN(zahl) && ((min !== undefined && zahl < min) || (max !== undefined && zahl > max));
|
||||
|
||||
return (
|
||||
<form action={aktion} className="flex flex-col gap-2">
|
||||
<input type="hidden" name="laufId" value={laufId} />
|
||||
<input type="hidden" name="vorlagenSchrittId" value={vorlagenSchrittId} />
|
||||
{/* Der Zustand bleibt, wie er ist – hier wird ein Wert erfasst, kein Haken gesetzt. */}
|
||||
<input type="hidden" name="zustand" value={zustand} />
|
||||
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="flex min-w-0 flex-1 flex-col gap-1.5 text-sm md:max-w-sm">
|
||||
<span className="text-nebentext">{beschriftungFuer(typ, konfig)}</span>
|
||||
{typ === SchrittTyp.AUSWAHL ? (
|
||||
<select
|
||||
name="wert"
|
||||
value={eingabe}
|
||||
onChange={(ereignis) => setzeEingabe(ereignis.target.value)}
|
||||
className={FELD}
|
||||
>
|
||||
<option value="">bitte wählen</option>
|
||||
{optionenAus(konfig).map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : typ === SchrittTyp.TEXT && konfig["mehrzeilig"] === true ? (
|
||||
<textarea
|
||||
name="wert"
|
||||
rows={3}
|
||||
value={eingabe}
|
||||
onChange={(ereignis) => setzeEingabe(ereignis.target.value)}
|
||||
className={`${FELD} py-2`}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
name="wert"
|
||||
type={typ === SchrittTyp.ZAHL ? "number" : typ === SchrittTyp.DATUM ? "date" : "text"}
|
||||
value={eingabe}
|
||||
onChange={(ereignis) => setzeEingabe(ereignis.target.value)}
|
||||
className={FELD}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={laeuft}
|
||||
className="flex min-h-11 items-center rounded-pille border border-linie px-4 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text md:min-h-10"
|
||||
>
|
||||
{laeuft ? "Wird gespeichert …" : "Wert speichern"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{ausserhalb ? (
|
||||
<p className="text-xs text-faellig">
|
||||
Erwartet werden {min ?? "–"} bis {max ?? "–"}
|
||||
{typeof konfig["einheit"] === "string" ? ` ${konfig["einheit"]}` : ""}. Der Wert wird trotzdem
|
||||
gespeichert — ein Befund ist kein Eingabefehler.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{ergebnis.fehler ? (
|
||||
<p role="alert" className="max-w-prose text-sm text-fehler">
|
||||
{ergebnis.fehler}
|
||||
</p>
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function beschriftungFuer(typ: SchrittTyp, konfig: Record<string, unknown>): string {
|
||||
if (typ === SchrittTyp.ZAHL) {
|
||||
const einheit = typeof konfig["einheit"] === "string" ? ` in ${konfig["einheit"]}` : "";
|
||||
return `Wert${einheit}`;
|
||||
}
|
||||
if (typ === SchrittTyp.DATUM) return "Datum";
|
||||
if (typ === SchrittTyp.AUSWAHL) return "Auswahl";
|
||||
return "Wert";
|
||||
}
|
||||
|
||||
function optionenAus(konfig: Record<string, unknown>): readonly string[] {
|
||||
const roh = konfig["optionen"];
|
||||
return Array.isArray(roh) ? roh.filter((eintrag): eintrag is string => typeof eintrag === "string") : [];
|
||||
}
|
||||
Reference in New Issue
Block a user