Beim Onboarding in der Buchhaltung gehoeren Schritte dazu, die sonst niemand braucht: DATEV, Drucker fuer Formulare, Bankportal. Bisher hiess die Wahl: eine Vorlage je Abteilung pflegen, oder Schritte, die fuer die meisten "nicht zutreffend" sind. Jeder Abschnitt kann jetzt nennen, fuer welche Abteilungen er gilt -- als **Namen**, nicht als Kennungen. Der Abschnitt gehoert zur Version und muss ein Umbenennen oder Archivieren einer Abteilung ueberleben; dieselbe Entscheidung wie bei Mitarbeiter.abteilung. Deshalb wandert die Einschraenkung auch durch Export und Import. Welche Abschnitte ein Lauf hat, wird beim Start festgeschrieben. Ein spaeterer Abteilungswechsel aendert ihn nicht -- sonst verschwaende ein Schritt, an dem schon eine Notiz haengt, und ein archivierter Lauf muss genau das zeigen, was damals abgearbeitet wurde. Leer heisst alle: Laeufe von vor dieser Fassung verhalten sich unveraendert, ohne dass eine Migration Daten nachfuehren muesste. Damit das eindeutig bleibt, wird ein Lauf abgewiesen, fuer den kein Abschnitt uebrig bleibt -- es gaebe ohnehin nichts abzuarbeiten. Die Abschlussregel zaehlt nur geltende Pflichtschritte. Vorher waere der Abschluss unerreichbar gewesen: abhaken laesst sich nur, was in der Abarbeitung steht. Der Test hat das sofort gezeigt. Die Regel liegt in src/lib, weil drei Schichten sie brauchen -- der Dienst beim Start und beim Abschluss, die Lesemodelle der Abarbeitung und des Drucks. In einem Lesemodell waere sie eine Abhaengigkeit in die falsche Richtung. Im gebauten Stand durchgeklickt: einschraenken, speichern, veroeffentlichen, zwei Laeufe mit verschiedenen Abteilungen starten -- 6 Abschnitte mit 23 Schritten fuer die Buchhaltung, 5 mit 20 fuer den Vertrieb. Dabei einen eigenen Numerusfehler im Hinweistext gefunden und behoben. Closes #51
138 lines
4.7 KiB
TypeScript
138 lines
4.7 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { Plus, X } from "lucide-react";
|
||
import { KAESTCHEN } from "@/lib/formular";
|
||
|
||
/**
|
||
* Fuer welche Abteilungen ein Abschnitt gilt (#031).
|
||
*
|
||
* Kein `<select multiple>`: das ist am Handy kaum bedienbar und zeigt nicht, was
|
||
* gewaehlt ist, ohne zu scrollen. Stattdessen ein Kaestchen je gepflegter
|
||
* Abteilung und darunter ein Freitextfeld — der Abschnitt haelt **Namen**, und
|
||
* eine Abteilung, die es noch nicht gibt, soll sich schon nennen lassen.
|
||
*
|
||
* Ein Name, der gewaehlt ist, aber in keiner gepflegten Abteilung vorkommt,
|
||
* erscheint als eigene Zeile mit Entfernen-Knopf. Sonst waere er unsichtbar und
|
||
* beim naechsten Speichern still weg.
|
||
*/
|
||
export function AbteilungenWaehlen({
|
||
gewaehlt,
|
||
bekannte,
|
||
lesend,
|
||
setzen,
|
||
}: {
|
||
gewaehlt: readonly string[];
|
||
bekannte: readonly string[];
|
||
lesend: boolean;
|
||
setzen: (namen: readonly string[]) => void;
|
||
}) {
|
||
const [eingabe, setzeEingabe] = useState("");
|
||
|
||
const kleinGewaehlt = new Set(gewaehlt.map((name) => name.trim().toLocaleLowerCase("de")));
|
||
const fremde = gewaehlt.filter(
|
||
(name) => !bekannte.some((bekannt) => bekannt.toLocaleLowerCase("de") === name.toLocaleLowerCase("de")),
|
||
);
|
||
|
||
function umschalten(name: string, an: boolean): void {
|
||
if (an) {
|
||
if (kleinGewaehlt.has(name.trim().toLocaleLowerCase("de"))) return;
|
||
setzen([...gewaehlt, name]);
|
||
} else {
|
||
setzen(
|
||
gewaehlt.filter(
|
||
(eintrag) => eintrag.trim().toLocaleLowerCase("de") !== name.trim().toLocaleLowerCase("de"),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
function hinzufuegen(): void {
|
||
const name = eingabe.trim();
|
||
if (!name) return;
|
||
setzeEingabe("");
|
||
umschalten(name, true);
|
||
}
|
||
|
||
return (
|
||
<fieldset className="flex flex-col gap-2">
|
||
<legend className="text-sm text-nebentext">Nur für diese Abteilungen</legend>
|
||
<p className="max-w-prose text-sm text-nebentext">
|
||
{gewaehlt.length === 0
|
||
? "Keine Einschränkung — der Abschnitt gilt für jeden Lauf."
|
||
: "Der Abschnitt erscheint nur in Läufen mit einer dieser Abteilungen. Ein Lauf ohne Abteilung bekommt ihn nicht."}
|
||
</p>
|
||
|
||
{bekannte.length > 0 ? (
|
||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||
{bekannte.map((name) => (
|
||
<label key={name} className="flex min-h-11 items-center gap-2 text-sm md:min-h-9">
|
||
<input
|
||
type="checkbox"
|
||
className={KAESTCHEN}
|
||
disabled={lesend}
|
||
checked={kleinGewaehlt.has(name.toLocaleLowerCase("de"))}
|
||
onChange={(ereignis) => umschalten(name, ereignis.target.checked)}
|
||
/>
|
||
{name}
|
||
</label>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
|
||
{fremde.length > 0 ? (
|
||
<ul className="flex flex-wrap gap-2">
|
||
{fremde.map((name) => (
|
||
<li
|
||
key={name}
|
||
className="inline-flex items-center gap-1 rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs"
|
||
>
|
||
{name}
|
||
{lesend ? null : (
|
||
<button
|
||
type="button"
|
||
onClick={() => umschalten(name, false)}
|
||
aria-label={`${name} entfernen`}
|
||
className="text-nebentext hover:text-fehler"
|
||
>
|
||
<X className="size-3.5" strokeWidth={2} aria-hidden="true" />
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : null}
|
||
|
||
{lesend ? null : (
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<input
|
||
type="text"
|
||
value={eingabe}
|
||
onChange={(ereignis) => setzeEingabe(ereignis.target.value)}
|
||
onKeyDown={(ereignis) => {
|
||
// Enter fügt hinzu, statt das Formular abzuschicken – der Editor
|
||
// speichert über einen eigenen Knopf.
|
||
if (ereignis.key === "Enter") {
|
||
ereignis.preventDefault();
|
||
hinzufuegen();
|
||
}
|
||
}}
|
||
placeholder="Abteilung, die es noch nicht gibt"
|
||
aria-label="Weitere Abteilung"
|
||
className="min-h-11 min-w-0 flex-1 rounded-karte border border-linie bg-grund px-3 text-sm outline-none focus-visible:border-akzent md:min-h-9"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={hinzufuegen}
|
||
disabled={eingabe.trim() === ""}
|
||
className="flex min-h-11 items-center gap-1.5 rounded-pille border border-linie px-3 text-sm text-nebentext hover:bg-flaeche-hoch hover:text-text disabled:opacity-40 md:min-h-9"
|
||
>
|
||
<Plus className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
||
hinzufügen
|
||
</button>
|
||
</div>
|
||
)}
|
||
</fieldset>
|
||
);
|
||
}
|