feat(vorlagen): Abschnitte nur fuer bestimmte Abteilungen
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
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user