168 lines
6.7 KiB
TypeScript
168 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState, useState } from "react";
|
|
import { ArrowDown, ArrowUp, Plus, X } from "lucide-react";
|
|
import type { GeraeteTyp } from "@/generated/prisma/enums";
|
|
import { GERAETE_TYP_TEXT } from "@/lib/geraete-text";
|
|
import { schnellstartSpeichernAktion, type AktionsErgebnis } from "@/server/actions/schnellstart";
|
|
|
|
export type Eintrag = {
|
|
readonly vorlageId: string;
|
|
readonly name: string;
|
|
readonly kategorie: string | null;
|
|
readonly kundeName: string | null;
|
|
};
|
|
|
|
/**
|
|
* Schnellstart eines Geraetetyps pflegen (#022).
|
|
*
|
|
* Die Reihenfolge laeuft ueber Hoch und Runter, nicht ueber Drag & Drop. Der
|
|
* Editor benutzt dnd-kit, weil dort hundert Schritte in Abschnitten liegen; hier
|
|
* sind es zwei bis vier Kacheln, und zwei Knoepfe sind am Handy verlaesslicher
|
|
* als eine Ziehgeste neben einer scrollenden Seite.
|
|
*
|
|
* Gespeichert wird die **ganze** Liste als ein Feld, eine Kennung je Zeile. So
|
|
* gibt es eine Wahrheit ueber die Reihenfolge: die des Formulars.
|
|
*/
|
|
export function SchnellstartPflege({
|
|
typ,
|
|
zugewiesen,
|
|
auswahl,
|
|
}: {
|
|
typ: GeraeteTyp;
|
|
zugewiesen: readonly Eintrag[];
|
|
auswahl: readonly Eintrag[];
|
|
}) {
|
|
const [liste, setListe] = useState<readonly Eintrag[]>(zugewiesen);
|
|
const [ergebnis, aktion, laeuft] = useActionState<AktionsErgebnis, FormData>(
|
|
schnellstartSpeichernAktion,
|
|
{},
|
|
);
|
|
|
|
const offen = auswahl.filter(
|
|
(eintrag) => !liste.some((gewaehlt) => gewaehlt.vorlageId === eintrag.vorlageId),
|
|
);
|
|
|
|
function verschieben(stelle: number, richtung: -1 | 1) {
|
|
const ziel = stelle + richtung;
|
|
if (ziel < 0 || ziel >= liste.length) return;
|
|
const kopie = [...liste];
|
|
const [heraus] = kopie.splice(stelle, 1);
|
|
if (heraus) kopie.splice(ziel, 0, heraus);
|
|
setListe(kopie);
|
|
}
|
|
|
|
const geaendert =
|
|
liste.length !== zugewiesen.length ||
|
|
liste.some((eintrag, stelle) => eintrag.vorlageId !== zugewiesen[stelle]?.vorlageId);
|
|
|
|
return (
|
|
<section className="rounded-karte border border-linie bg-flaeche p-5">
|
|
<h3 className="text-lg font-semibold">{GERAETE_TYP_TEXT[typ]}</h3>
|
|
|
|
{liste.length === 0 ? (
|
|
<p className="mt-2 text-sm text-nebentext">
|
|
Kein Schnellstart. Auf der Geräteseite erscheint dann nur „Vorlage zuweisen“.
|
|
</p>
|
|
) : (
|
|
<ol className="mt-3 flex flex-col gap-2">
|
|
{liste.map((eintrag, stelle) => (
|
|
<li
|
|
key={eintrag.vorlageId}
|
|
className="flex flex-wrap items-center gap-2 border-t border-linie py-2 first:border-t-0"
|
|
>
|
|
<span className="mono w-7 shrink-0 text-xs text-nebentext">
|
|
{String(stelle + 1).padStart(2, "0")}
|
|
</span>
|
|
<span className="min-w-0 flex-1 break-words">{eintrag.name}</span>
|
|
{eintrag.kundeName ? (
|
|
<span className="rounded-pille bg-flaeche-hoch px-2.5 py-1 text-xs text-nebentext">
|
|
nur {eintrag.kundeName}
|
|
</span>
|
|
) : null}
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => verschieben(stelle, -1)}
|
|
disabled={stelle === 0}
|
|
aria-label={`${eintrag.name} nach oben`}
|
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-text disabled:opacity-40 md:size-10"
|
|
>
|
|
<ArrowUp className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => verschieben(stelle, 1)}
|
|
disabled={stelle === liste.length - 1}
|
|
aria-label={`${eintrag.name} nach unten`}
|
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-text disabled:opacity-40 md:size-10"
|
|
>
|
|
<ArrowDown className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setListe(liste.filter((andere) => andere.vorlageId !== eintrag.vorlageId))}
|
|
aria-label={`${eintrag.name} entfernen`}
|
|
className="flex size-11 items-center justify-center rounded-pille text-nebentext hover:bg-flaeche-hoch hover:text-fehler md:size-10"
|
|
>
|
|
<X className="size-4" strokeWidth={1.75} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
)}
|
|
|
|
{offen.length > 0 ? (
|
|
<div className="mt-4 flex flex-wrap items-end gap-2">
|
|
<label className="flex min-w-0 flex-1 flex-col gap-1 text-sm">
|
|
<span className="text-nebentext">Vorlage hinzufügen</span>
|
|
<select
|
|
aria-label={`Vorlage für ${GERAETE_TYP_TEXT[typ]} hinzufügen`}
|
|
value=""
|
|
onChange={(ereignis) => {
|
|
const gewaehlt = offen.find((eintrag) => eintrag.vorlageId === ereignis.target.value);
|
|
if (gewaehlt) setListe([...liste, gewaehlt]);
|
|
}}
|
|
className="min-h-11 w-full rounded-pille border border-linie bg-flaeche-hoch px-4 text-sm md:min-h-10"
|
|
>
|
|
<option value="">auswählen …</option>
|
|
{offen.map((eintrag) => (
|
|
<option key={eintrag.vorlageId} value={eintrag.vorlageId}>
|
|
{eintrag.name}
|
|
{eintrag.kundeName ? ` (nur ${eintrag.kundeName})` : ""}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<Plus className="mb-3 size-4 text-nebentext" strokeWidth={1.75} aria-hidden="true" />
|
|
</div>
|
|
) : null}
|
|
|
|
<form action={aktion} className="mt-4 flex flex-wrap items-center gap-3">
|
|
<input type="hidden" name="geraeteTyp" value={typ} />
|
|
<input type="hidden" name="vorlageIds" value={liste.map((eintrag) => eintrag.vorlageId).join("\n")} />
|
|
<button
|
|
type="submit"
|
|
disabled={laeuft}
|
|
className="min-h-11 rounded-pille bg-akzent px-4 text-sm font-semibold text-grund md:min-h-10"
|
|
>
|
|
Speichern
|
|
</button>
|
|
{geaendert ? <span className="text-sm text-faellig">nicht gespeichert</span> : null}
|
|
</form>
|
|
|
|
{ergebnis.fehler ? (
|
|
<p role="alert" className="mt-2 max-w-prose text-sm text-fehler">
|
|
{ergebnis.fehler}
|
|
</p>
|
|
) : null}
|
|
{ergebnis.erfolg && !geaendert ? (
|
|
<p role="status" className="mt-2 text-sm text-erledigt">
|
|
{ergebnis.erfolg}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|