feat(admin): Listen mit Status-Badges, Leerzustaenden und klaren Aktionen

Neue gemeinsame Bausteine in components/admin-list.tsx: Seitenkopf mit
Hauptaktion, Suchleiste, Status-Badge (gleiche Form wie der Bestellstatus,
wartende Eintraege mit pulsierendem Punkt), Leerzustand mit Wabe und
naechstem Schritt sowie Zeilenaktionen mit einheitlichem Unterstrich-
Hover (Loeschen rot, Sperren/Zurueckziehen gedaempft).

Umgestellt: Bewertungen (SVG-Sterne statt Unicode), Benutzer, Gutscheine
(neu: Status "Abgelaufen"), Versand, Ernten, Chargen (Anlegeformulare auf
admin-form), Firmenanfragen (Status als Badge, beschriftete Auswahl) und
Audit-Log. Zahlen und Daten in Tabellen mit tabular-nums.

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 20:51:50 +02:00
1 parent 95b4b5953d
commit a09057d960
12 files changed
+440 -300

No files matched your search

+42 -77
View File
@@ -1,8 +1,18 @@
import Link from "next/link";
import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link";
import { ConfirmForm } from "@/components/confirm-form";
import { formatDate } from "@/lib/format";
import { Button } from "@/components/ui";
import { AdminField, AdminSwitch, AdminTextarea } from "@/components/admin-form";
import {
AdminActionButton,
AdminActionLink,
AdminActionNote,
AdminBadge,
AdminEmptyState,
AdminPageHeader,
AdminRowActions,
} from "@/components/admin-list";
import { createHarvestAction, toggleHarvestPublishedAction, deleteHarvestAction } from "../actions";
import { ApiCoreSyncPanel, ApiCoreSyncResult } from "@/components/apicore-sync-panel";
import { getLocale } from "@/i18n/locale";
@@ -19,7 +29,7 @@ export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten"
return (
<div>
<BackLink href="/admin" label={t.adminHarvestsPage.backToDashboard} />
<h1 className="text-2xl text-forest mb-8">{t.adminHarvestsPage.title}</h1>
<AdminPageHeader title={t.adminHarvestsPage.title} />
<ApiCoreSyncResult searchParams={searchParams} />
@@ -41,74 +51,64 @@ export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten"
{harvests.map((harvest) => (
<tr key={harvest.id} className="transition-colors hover:bg-ink/[0.03]">
<td className="p-4 text-forest">{harvest.type}</td>
<td className="p-4">{harvest.year}</td>
<td className="p-4">{formatDate(harvest.harvestDate)}</td>
<td className="p-4 tabular-nums">{harvest.year}</td>
<td className="p-4 whitespace-nowrap tabular-nums">{formatDate(harvest.harvestDate)}</td>
<td className="p-4">{harvest.location}</td>
<td className="p-4">{harvest._count.batches}</td>
<td className="p-4">{harvest._count.products}</td>
<td className="p-4 tabular-nums">{harvest._count.batches}</td>
<td className="p-4 tabular-nums">{harvest._count.products}</td>
<td className="p-4">
<span
className={
harvest.isPublished
? "text-forest bg-forest/10 px-2 py-1 text-xs"
: "text-honey-dark bg-honey/10 px-2 py-1 text-xs"
}
>
{harvest.isPublished ? t.adminHarvestsPage.statusPublished : t.adminHarvestsPage.statusDraft}
</span>
{harvest.isPublished ? (
<AdminBadge tone="positive">{t.adminHarvestsPage.statusPublished}</AdminBadge>
) : (
<AdminBadge tone="pending">{t.adminHarvestsPage.statusDraft}</AdminBadge>
)}
</td>
<td className="p-4 whitespace-nowrap">
<Link href={`/admin/ernten/${harvest.id}`} className="text-forest underline mr-4">
{t.adminHarvestsPage.edit}
</Link>
<form action={toggleHarvestPublishedAction} className="inline">
<td className="p-4">
<AdminRowActions>
<AdminActionLink href={`/admin/ernten/${harvest.id}`}>{t.adminHarvestsPage.edit}</AdminActionLink>
<form action={toggleHarvestPublishedAction}>
<input type="hidden" name="id" value={harvest.id} />
{harvest.isPublished ? null : <input type="hidden" name="isPublished" value="on" />}
<button type="submit" className="text-forest underline mr-4">
<AdminActionButton tone={harvest.isPublished ? "muted" : "default"}>
{harvest.isPublished ? t.adminHarvestsPage.unpublish : t.adminHarvestsPage.publish}
</button>
</AdminActionButton>
</form>
{harvest._count.batches === 0 && harvest._count.products === 0 ? (
<ConfirmForm
action={deleteHarvestAction}
confirmMessage={t.adminHarvestsPage.deleteConfirm(`${harvest.type} ${harvest.year}`)}
className="inline"
>
<input type="hidden" name="id" value={harvest.id} />
<button type="submit" className="text-red-700 underline">
{t.adminHarvestsPage.delete}
</button>
<AdminActionButton tone="danger">{t.adminHarvestsPage.delete}</AdminActionButton>
</ConfirmForm>
) : (
// Statt den Knopf wortlos wegzulassen: der Grund steht da.
<span className="text-xs text-ink/50">{t.adminHarvestsPage.notDeletable}</span>
<AdminActionNote>{t.adminHarvestsPage.notDeletable}</AdminActionNote>
)}
</AdminRowActions>
</td>
</tr>
))}
</tbody>
</table>
{harvests.length === 0 ? <p className="p-6 text-ink/65">{t.adminHarvestsPage.noHarvestsYet}</p> : null}
{harvests.length === 0 ? <AdminEmptyState message={t.adminHarvestsPage.noHarvestsYet} /> : null}
</div>
<div className="mt-10 grid md:grid-cols-2 gap-6">
<div className="bg-background border border-ink/10 p-6">
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
<h2 className="font-serif text-xl text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
<form action={createHarvestAction} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Field name="type" label={t.adminHarvestsPage.typeLabel} required />
<Field name="year" label={t.adminHarvestsPage.yearLabel} type="number" required />
<div className="grid grid-cols-2 items-end gap-4">
<AdminField name="type" label={t.adminHarvestsPage.typeLabel} required />
<AdminField name="year" label={t.adminHarvestsPage.yearLabel} type="number" required />
</div>
<AdminField name="harvestDate" label={t.adminHarvestsPage.harvestDateLabel} type="date" required />
<AdminField name="location" label={t.adminHarvestsPage.locationLabel} defaultValue="Altenmünster" />
<AdminTextarea name="description" label={t.adminHarvestsPage.descriptionLabel} required />
<AdminSwitch name="isPublished" label={t.adminHarvestsPage.isPublishedLabel} defaultChecked />
<div className="flex justify-end border-t border-ink/10 pt-4">
<Button type="submit">{t.adminHarvestsPage.create}</Button>
</div>
<Field name="harvestDate" label={t.adminHarvestsPage.harvestDateLabel} type="date" required />
<Field name="location" label={t.adminHarvestsPage.locationLabel} defaultValue="Altenmünster" />
<TextArea name="description" label={t.adminHarvestsPage.descriptionLabel} required />
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isPublished" defaultChecked />
{t.adminHarvestsPage.isPublishedLabel}
</label>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestsPage.create}
</button>
</form>
</div>
@@ -118,38 +118,3 @@ export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten"
);
}
function Field({
name,
label,
type = "text",
defaultValue,
required = false,
}: {
name: string;
label: string;
type?: string;
defaultValue?: string;
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
type={type}
defaultValue={defaultValue}
required={required}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
function TextArea({ name, label, required = false }: { name: string; label: string; required?: boolean }) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<textarea name={name} required={required} rows={3} className="mt-1 w-full border border-ink/20 bg-background px-3 py-2" />
</label>
);
}