Merge #5: Filtermöglichkeit der angelegten Codes
This commit was merged in pull request #13.
This commit is contained in:
@@ -618,13 +618,18 @@ Dazu kommen
|
||||
|
||||
- ein **Suchfeld** über Name, Ziel, Kurz-URL und Typ,
|
||||
- eine **Sortierung** nach Datum, Name oder Scan-Anzahl,
|
||||
- ein **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen.
|
||||
- ein **Gruppenfilter**, sobald Codes in mehr als einer Gruppe liegen,
|
||||
- ein **Filter nach Art**, sobald dynamische und statische Codes
|
||||
nebeneinander liegen.
|
||||
|
||||
Auf schmalen Bildschirmen blendet die Liste die hinteren Spalten aus; die
|
||||
Kurz-URL rückt dann unter den Namen. Zum Herunterladen dient dort die
|
||||
Kartenansicht.
|
||||
|
||||
Statische Codes erscheinen hier nicht, weil sie nicht gespeichert werden.
|
||||
Abgelegte **statische Codes** stehen mit in der Liste, tragen aber ein
|
||||
Abzeichen „statisch“ statt einer Kurz-URL und zählen keine Scans. Steht der
|
||||
Filter auf „Nur statische“, fällt die Scan-Spalte weg und die Sortierung nach
|
||||
Scans mit ihr — bei einer Liste aus lauter Nullen sagt sie nichts.
|
||||
|
||||
Bei einem **Feedback-Code** führt aus der Zeile ein Stern-Symbol zur
|
||||
Auswertung: Durchschnitt, Verteilung der Noten und die abgegebenen Kommentare.
|
||||
|
||||
@@ -27,9 +27,9 @@ export default async function MyCodesPage() {
|
||||
Meine Codes
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Alle dynamischen QR-Codes mit ihrem aktuellen Ziel und der Anzahl der
|
||||
Scans. Statische Codes erscheinen hier nicht — sie werden nicht
|
||||
gespeichert.
|
||||
Dynamische Codes mit ihrem aktuellen Ziel und der Anzahl der Scans,
|
||||
dazu abgelegte statische Codes. Über die Auswahl „Nach Art filtern“
|
||||
lässt sich die Liste auf eine der beiden Arten einschränken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ function formatShortDate(value: string | null): string {
|
||||
|
||||
type View = "list" | "cards";
|
||||
type SortKey = "neueste" | "aelteste" | "name" | "scans";
|
||||
/** Betriebsart, auf die die Liste eingeschränkt ist. */
|
||||
type ModeFilter = "alle" | "dynamic" | "static";
|
||||
|
||||
const SORT_LABELS: Record<SortKey, string> = {
|
||||
neueste: "Neueste zuerst",
|
||||
@@ -51,6 +53,12 @@ const SORT_LABELS: Record<SortKey, string> = {
|
||||
scans: "Meiste Scans",
|
||||
};
|
||||
|
||||
const MODE_LABELS: Record<ModeFilter, string> = {
|
||||
alle: "Alle Arten",
|
||||
dynamic: "Nur dynamische",
|
||||
static: "Nur statische",
|
||||
};
|
||||
|
||||
/** Merkt sich die gewählte Ansicht über Seitenwechsel hinweg. */
|
||||
const VIEW_STORAGE_KEY = "qrgen.codes.view";
|
||||
|
||||
@@ -94,6 +102,7 @@ export function CodesList({
|
||||
const [query, setQuery] = useState("");
|
||||
const [sort, setSort] = useState<SortKey>("neueste");
|
||||
const [groupFilter, setGroupFilter] = useState("alle");
|
||||
const [modeFilter, setModeFilter] = useState<ModeFilter>("alle");
|
||||
|
||||
// Erst nach dem Einhängen lesen — auf dem Server gibt es keinen
|
||||
// localStorage, und ein abweichender Startwert führte zu Hydrationsfehlern.
|
||||
@@ -107,6 +116,14 @@ export function CodesList({
|
||||
window.localStorage.setItem(VIEW_STORAGE_KEY, next);
|
||||
};
|
||||
|
||||
const changeModeFilter = (next: ModeFilter) => {
|
||||
setModeFilter(next);
|
||||
// Statische Codes zählen keine Scans. Bleibt die Sortierung danach
|
||||
// stehen, sortiert die Liste nach lauter Nullen — und die Auswahl bietet
|
||||
// eine Reihenfolge an, die es dort gar nicht gibt.
|
||||
if (next === "static" && sort === "scans") setSort("neueste");
|
||||
};
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const result = await apiJson<{ codes: QrCodeDto[] }>("/api/codes");
|
||||
@@ -241,11 +258,25 @@ export function CodesList({
|
||||
);
|
||||
}, [codes]);
|
||||
|
||||
// Welche Betriebsarten überhaupt vorkommen. Ein Filter, der nichts zu
|
||||
// filtern hat, steht nur im Weg — deshalb erscheint er erst, wenn beide
|
||||
// Arten in der Liste stehen.
|
||||
const kinds = useMemo(() => {
|
||||
let dynamisch = false;
|
||||
let statisch = false;
|
||||
for (const code of codes ?? []) {
|
||||
if (code.mode === "static") statisch = true;
|
||||
else dynamisch = true;
|
||||
}
|
||||
return { dynamisch, statisch, beide: dynamisch && statisch };
|
||||
}, [codes]);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const needle = query.trim().toLowerCase();
|
||||
|
||||
const filtered = (codes ?? []).filter((code) => {
|
||||
if (groupFilter !== "alle" && code.groupId !== groupFilter) return false;
|
||||
if (modeFilter !== "alle" && code.mode !== modeFilter) return false;
|
||||
if (!needle) return true;
|
||||
return [
|
||||
displayName(code),
|
||||
@@ -273,7 +304,7 @@ export function CodesList({
|
||||
}
|
||||
});
|
||||
return sorted;
|
||||
}, [codes, query, sort, groupFilter]);
|
||||
}, [codes, query, sort, groupFilter, modeFilter]);
|
||||
|
||||
/* ---------------------------------------------------------------- View */
|
||||
|
||||
@@ -287,6 +318,10 @@ export function CodesList({
|
||||
const svgHref = (code: QrCodeDto) =>
|
||||
`/api/codes/${code.id}/qr?format=svg&download=1`;
|
||||
|
||||
// Zeigt die Liste ausschließlich statische Codes, hat die Scan-Spalte
|
||||
// nichts zu sagen — dann fällt sie weg, samt der Sortierung danach.
|
||||
const nurStatische = modeFilter === "static" || (kinds.statisch && !kinds.dynamisch);
|
||||
|
||||
const toolbar = codes.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="relative min-w-0 flex-1 basis-56">
|
||||
@@ -320,13 +355,32 @@ export function CodesList({
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
{kinds.beide ? (
|
||||
<Select
|
||||
aria-label="Nach Art filtern"
|
||||
className="!w-auto"
|
||||
value={modeFilter}
|
||||
onChange={(event) =>
|
||||
changeModeFilter(event.target.value as ModeFilter)
|
||||
}
|
||||
>
|
||||
{(Object.keys(MODE_LABELS) as ModeFilter[]).map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{MODE_LABELS[key]}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
) : null}
|
||||
|
||||
<Select
|
||||
aria-label="Sortierung"
|
||||
className="!w-auto"
|
||||
value={sort}
|
||||
onChange={(event) => setSort(event.target.value as SortKey)}
|
||||
>
|
||||
{(Object.keys(SORT_LABELS) as SortKey[]).map((key) => (
|
||||
{(Object.keys(SORT_LABELS) as SortKey[])
|
||||
.filter((key) => key !== "scans" || !nurStatische)
|
||||
.map((key) => (
|
||||
<option key={key} value={key}>
|
||||
{SORT_LABELS[key]}
|
||||
</option>
|
||||
@@ -406,12 +460,13 @@ export function CodesList({
|
||||
{codes.length > 0 && visible.length === 0 ? (
|
||||
<Card className="text-center">
|
||||
<p className="text-sm text-slate-600">
|
||||
Kein Code passt zur Suche.{" "}
|
||||
Kein Code passt zu Suche und Filtern.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setQuery("");
|
||||
setGroupFilter("alle");
|
||||
setModeFilter("alle");
|
||||
}}
|
||||
className="font-medium text-accent-text underline"
|
||||
>
|
||||
@@ -446,9 +501,11 @@ export function CodesList({
|
||||
Gruppe
|
||||
</th>
|
||||
) : null}
|
||||
{nurStatische ? null : (
|
||||
<th scope="col" className="w-16 px-3 py-2.5 text-right sm:w-20">
|
||||
Scans
|
||||
</th>
|
||||
)}
|
||||
<th
|
||||
scope="col"
|
||||
className="hidden w-24 px-3 py-2.5 whitespace-nowrap sm:table-cell"
|
||||
@@ -571,15 +628,20 @@ export function CodesList({
|
||||
</td>
|
||||
) : null}
|
||||
|
||||
{nurStatische ? null : (
|
||||
<td className="px-3 py-2.5 text-right font-semibold text-slate-900 tabular-nums">
|
||||
{code.mode === "static" ? (
|
||||
<span className="font-normal text-slate-400" title="Statische Codes werden nicht gezählt">
|
||||
<span
|
||||
className="font-normal text-slate-400"
|
||||
title="Statische Codes werden nicht gezählt"
|
||||
>
|
||||
—
|
||||
</span>
|
||||
) : (
|
||||
code.scans
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
|
||||
<td
|
||||
className="hidden px-3 py-2.5 whitespace-nowrap text-slate-500 tabular-nums sm:table-cell"
|
||||
|
||||
Reference in New Issue
Block a user