feat(admin): Bestelldetail mit Zeitleiste, Produktformular gegliedert

- Bestelldetail: Status-Badge neben der Nummer, Waben-Zeitleiste
  (dieselbe wie im Kundenkonto) zeigt auf einen Blick den Stand
- Produkt bearbeiten: Abschnitte (Text und Bilder, Geschmack, Preis und
  Bestand, Sichtbarkeit), "Im Shop sichtbar" als Schalter (gleiches Feld
  isActive), Speicherleiste bleibt beim Scrollen unten erreichbar
- Kartentitel im gesamten Admin als lesbare Serifentitel statt winziger
  Versal-Etiketten (zentral per .admin-content, farbige Titel bleiben)

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 19:48:29 +02:00
1 parent c95c713717
commit 76ebe7d042
6 files changed
+82 -13

No files matched your search

+25 -2
View File
@@ -14,6 +14,7 @@ import { checkOrderDeletable } from "@/lib/order-delete";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
import { isDhlConfigured } from "@/lib/dhl";
import { OrderStatusBadge, OrderStatusTimeline } from "@/components/account-ui";
export default async function AdminOrderDetailPage(props: PageProps<"/admin/bestellungen/[id]">) {
const t = getDictionary(await getLocale());
@@ -43,13 +44,16 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
}).deletable;
return (
<div className="max-w-3xl">
<div className="max-w-4xl">
<BackLink href="/admin/bestellungen" label={t.adminOrderDetailPage.backToOrders} />
<div className="flex flex-wrap items-start justify-between gap-4 mb-8">
<div>
<h1 className="text-2xl text-forest">{order.orderNumber}</h1>
<div className="flex flex-wrap items-center gap-3">
<h1 className="text-2xl text-forest">{order.orderNumber}</h1>
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
</div>
<p className="text-sm text-ink/65 mt-1">{formatDate(order.createdAt)}</p>
</div>
<form action={updateOrderStatusAction} className="flex items-center gap-2">
@@ -71,6 +75,25 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
</form>
</div>
{/* Wo steht die Bestellung? Dieselbe Waben-Zeitleiste wie im Kundenkonto. */}
<div className="mb-8 border border-ink/10 bg-background px-6 py-5">
<OrderStatusTimeline
status={order.status}
fulfillmentMethod={order.fulfillmentMethod}
labels={{
ordered: t.orderStatusTimeline.ordered,
paid: t.orderStatusTimeline.paid,
preparing: t.orderStatusTimeline.preparing,
readyToShip: t.orderStatusTimeline.readyToShip,
shipped: t.orderStatusTimeline.shipped,
readyForPickup: t.orderStatusTimeline.readyForPickup,
pickedUp: t.orderStatusTimeline.pickedUp,
cancelled: t.orderStatusTimeline.cancelled,
ariaLabel: t.orderStatusTimeline.ariaLabel,
}}
/>
</div>
{order.giftMessage ? (
<div className="bg-honey/10 border border-honey-dark/30 p-6 mb-8">
<h2 className="text-xs tracking-wide-caps text-honey-dark mb-3">{t.adminOrderDetailPage.giftHeading}</h2>
+30 -11
View File
@@ -1,4 +1,5 @@
import { notFound } from "next/navigation";
import { Button } from "@/components/ui";
import { prisma } from "@/lib/prisma";
import { asStringArray } from "@/lib/json-array";
import { BackLink } from "@/components/back-link";
@@ -22,7 +23,7 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
};
return (
<div className="max-w-xl">
<div className="max-w-2xl">
<BackLink href="/admin/produkte" label={t.adminProductEditPage.backToProducts} />
<h1 className="text-2xl text-forest mb-2">{t.adminProductEditPage.title}</h1>
<p className="text-sm text-ink/65 mb-8">
@@ -35,8 +36,10 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
)
: null}
</p>
<form action={updateProductAction} className="space-y-4 bg-background border border-ink/10 p-6">
<form action={updateProductAction} className="space-y-8 bg-background border border-ink/10 p-6 sm:p-8">
<input type="hidden" name="id" value={product.id} />
<fieldset className="space-y-4 border-b border-ink/10 pb-8">
<legend className="mb-4 font-serif text-xl text-forest">{t.adminProductEditPage.sectionText}</legend>
<Field name="name" label={t.adminProductEditPage.nameLabel} defaultValue={product.name} required />
<label className="block text-sm">
@@ -61,9 +64,11 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
label={t.adminProductEditPage.imagesLabel}
initialValues={product.images}
/>
</fieldset>
{product.category === "HONIG" ? (
<>
<fieldset className="space-y-4 border-b border-ink/10 pb-8">
<legend className="mb-4 font-serif text-xl text-forest">{t.adminProductEditPage.sectionTaste}</legend>
<Field
name="consistency"
label={t.adminProductEditPage.consistencyLabel}
@@ -89,9 +94,11 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
defaultValue={String(product.intensityKraeftig)}
/>
</div>
</>
</fieldset>
) : null}
<fieldset className="space-y-4 border-b border-ink/10 pb-8">
<legend className="mb-4 font-serif text-xl text-forest">{t.adminProductEditPage.sectionPriceStock}</legend>
<div className="grid grid-cols-2 gap-4">
<Field
name="weightGrams"
@@ -135,15 +142,27 @@ export default async function AdminProductEditPage(props: PageProps<"/admin/prod
defaultValue={product.vatRatePercent == null ? "" : String(product.vatRatePercent)}
/>
<p className="text-xs text-ink/65">{t.adminProductEditPage.vatRateHint}</p>
</fieldset>
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isActive" defaultChecked={product.isActive} />
{t.adminProductEditPage.visibleInShop}
</label>
<fieldset>
<legend className="mb-4 font-serif text-xl text-forest">{t.adminProductEditPage.sectionVisibility}</legend>
{/* Schalter statt kleiner Checkbox — dasselbe Formularfeld
(name="isActive"), nur als Umschalter dargestellt. */}
<label className="inline-flex cursor-pointer items-center gap-3 text-sm text-ink/80">
<input type="checkbox" name="isActive" defaultChecked={product.isActive} className="peer sr-only" />
<span
aria-hidden="true"
className="relative h-6 w-11 flex-shrink-0 rounded-full bg-ink/20 transition-colors duration-300 peer-checked:bg-forest peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-honey-dark after:absolute after:left-0.5 after:top-0.5 after:h-5 after:w-5 after:rounded-full after:bg-cream after:shadow after:transition-transform after:duration-300 after:content-[''] peer-checked:after:translate-x-5"
/>
{t.adminProductEditPage.visibleInShop}
</label>
</fieldset>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminProductEditPage.save}
</button>
{/* Speicherleiste bleibt beim Scrollen am unteren Rand — der Knopf
ist bei langen Formularen immer erreichbar. */}
<div className="sticky bottom-0 -mx-6 -mb-6 flex items-center justify-end gap-4 border-t border-ink/10 bg-background/95 px-6 py-4 backdrop-blur sm:-mx-8 sm:-mb-8 sm:px-8">
<Button type="submit">{t.adminProductEditPage.save}</Button>
</div>
</form>
</div>
);
+15
View File
@@ -354,3 +354,18 @@ textarea::placeholder {
.admin-content textarea:hover {
border-color: color-mix(in oklab, var(--color-ink) 40%, transparent);
}
/* Kartentitel im Admin: bisher winzige, blasse Versal-Etiketten
(text-xs tracking-wide-caps) als Ueberschrift jeder Karte. Als ruhige
Serifentitel lassen sich Abschnitte beim Ueberfliegen finden. Farbige
Titel (z.B. rote Gefahrenzone, Honig-Geschenkhinweis) behalten ihre Farbe. */
.admin-content h2.tracking-wide-caps {
font-family: var(--font-serif);
font-size: 1.25rem;
line-height: 1.3;
letter-spacing: 0;
text-transform: none;
}
.admin-content h2.tracking-wide-caps.text-ink\/65 {
color: var(--color-forest);
}