feat: Archiv-Tab fuer Bestellungen, Retouren und Produkte im Admin
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 3m24s

Abgeschlossene Eintraege wandern automatisch in einen Tab "Archiv", damit
die aktiven Listen nur zeigen, was noch zu tun ist:
- Bestellungen: versendet/abgeholt/storniert, keine offene Retoure,
  seit einem Monat nicht bearbeitet
- Retouren: angenommen/abgelehnt/erstattet, seit einem Monat nicht bearbeitet
- Produkte: seit einem Monat Bestand 0 (nur Admin-Sicht, Shop unveraendert)

Die Automatik laeuft beim Oeffnen der jeweiligen Liste (hoechstens alle
5 Minuten) und holt Eintraege zurueck, sobald sie wieder offen sind.
Zusaetzlich "Archivieren"/"Wiederherstellen" per Hand; von Hand
archivierte Eintraege fasst die Automatik nicht an. Alle Verschiebungen
landen im Audit-Log.

Neue Felder archivedAt/archivedManually samt Index an orders,
return_requests und products (Migration 20260925010000_archive_fields).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 04:05:10 +02:00
1 parent 01eaca39dc
commit fb9c2353c6
14 files changed
+772 -54

No files matched your search

+68 -20
View File
@@ -8,31 +8,43 @@ import type { OrderStatus } from "@/generated/prisma/client";
import { insensitiveContains } from "@/lib/prisma-search";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { isArchiveView } from "@/lib/archive";
import { ensureAutoArchived } from "@/lib/archive-runner";
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
const t = getDictionary(await getLocale());
const statusLabels = t.orderStatusBadge;
const { q, status, error, message } = await props.searchParams;
const { q, status, error, message, ansicht } = await props.searchParams;
const query = typeof q === "string" ? q.trim() : "";
const statusFilter = typeof status === "string" && status in statusLabels ? (status as OrderStatus) : undefined;
const isArchive = isArchiveView(ansicht);
const orders = await prisma.order.findMany({
where: {
...(statusFilter ? { status: statusFilter } : {}),
...(query
? {
OR: [
{ orderNumber: insensitiveContains(query) },
{ email: insensitiveContains(query) },
{ lastName: insensitiveContains(query) },
],
}
: {}),
},
orderBy: { createdAt: "desc" },
take: 100,
include: { items: true },
});
await ensureAutoArchived("orders");
const filterWhere = {
...(statusFilter ? { status: statusFilter } : {}),
...(query
? {
OR: [
{ orderNumber: insensitiveContains(query) },
{ email: insensitiveContains(query) },
{ lastName: insensitiveContains(query) },
],
}
: {}),
};
const [orders, activeCount, archivedCount] = await Promise.all([
prisma.order.findMany({
where: { ...filterWhere, archivedAt: isArchive ? { not: null } : null },
orderBy: { createdAt: "desc" },
take: 100,
include: { items: true },
}),
prisma.order.count({ where: { ...filterWhere, archivedAt: null } }),
prisma.order.count({ where: { ...filterWhere, archivedAt: { not: null } } }),
]);
return (
<div>
@@ -48,7 +60,21 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">{error}</p>
) : null}
<AdminArchiveTabs
basePath="/admin/bestellungen"
isArchive={isArchive}
counts={{ active: activeCount, archived: archivedCount }}
keepParams={{ q: query, status: statusFilter ?? "" }}
labels={{
ariaLabel: t.adminArchive.tabsAriaLabel,
active: t.adminArchive.tabActive,
archive: t.adminArchive.tabArchive,
}}
/>
{isArchive ? <p className="mb-6 text-xs text-ink/60 max-w-2xl">{t.adminArchive.autoRuleOrders}</p> : null}
<form method="GET" className="flex flex-wrap items-end gap-3 mb-6">
{isArchive ? <input type="hidden" name="ansicht" value="archiv" /> : null}
<label className="text-sm">
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.searchLabel}</span>
<input
@@ -78,7 +104,10 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
{t.adminOrdersPage.filterButton}
</button>
{query || statusFilter ? (
<Link href="/admin/bestellungen" className="text-sm text-ink/65 underline">
<Link
href={isArchive ? "/admin/bestellungen?ansicht=archiv" : "/admin/bestellungen"}
className="text-sm text-ink/65 underline"
>
{t.adminOrdersPage.resetFilter}
</Link>
) : null}
@@ -95,6 +124,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
<th className="p-4">{t.adminOrdersPage.colTotal}</th>
<th className="p-4">{t.adminOrdersPage.colStatus}</th>
<th className="p-4">{t.adminOrdersPage.colFulfillment}</th>
<th className="p-4" />
</tr>
</thead>
<tbody className="divide-y divide-ink/10">
@@ -141,13 +171,31 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
<td className="p-4">
{order.fulfillmentMethod === "VERSAND" ? t.adminOrdersPage.shipping : t.adminOrdersPage.pickup}
</td>
<td className="p-4 whitespace-nowrap">
<AdminArchiveToggle
kind="orders"
id={order.id}
archivedAt={order.archivedAt}
archivedManually={order.archivedManually}
labels={{
archive: t.adminArchive.archive,
restore: t.adminArchive.restore,
archivedOn: t.adminArchive.archivedOn,
manualHint: t.adminArchive.manualHint,
}}
/>
</td>
</tr>
))}
</tbody>
</table>
{orders.length === 0 ? (
<p className="p-6 text-ink/65">
{query || statusFilter ? t.adminOrdersPage.noResultsFiltered : t.adminOrdersPage.noOrdersYet}
{query || statusFilter
? t.adminOrdersPage.noResultsFiltered
: isArchive
? t.adminArchive.emptyArchive
: t.adminOrdersPage.noOrdersYet}
</p>
) : null}
</div>