feat(schnellstart): Kacheln je Geraetetyp in der Oberflaeche pflegen (#44)
This commit was merged in pull request #44.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user