diff --git a/README.md b/README.md
index 86826ca..e6e6d2c 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/src/app/(app)/meine-codes/page.tsx b/src/app/(app)/meine-codes/page.tsx
index 26bcf7d..12357a9 100644
--- a/src/app/(app)/meine-codes/page.tsx
+++ b/src/app/(app)/meine-codes/page.tsx
@@ -27,9 +27,9 @@ export default async function MyCodesPage() {
Meine Codes
- 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.
diff --git a/src/components/codes-list.tsx b/src/components/codes-list.tsx
index 5ab8b16..b7e94b9 100644
--- a/src/components/codes-list.tsx
+++ b/src/components/codes-list.tsx
@@ -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 = {
neueste: "Neueste zuerst",
@@ -51,6 +53,12 @@ const SORT_LABELS: Record = {
scans: "Meiste Scans",
};
+const MODE_LABELS: Record = {
+ 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("neueste");
const [groupFilter, setGroupFilter] = useState("alle");
+ const [modeFilter, setModeFilter] = useState("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 && (