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:
1 parent
c95c713717
commit
76ebe7d042
6 files changed
+82
-13
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1063,6 +1063,10 @@ export const de: Dictionary = {
|
||||
"Leer lassen: es gilt der Satz der Kategorie (Honig 7 %, Kerzen 19 %). Nur ausfüllen, wenn dieses Produkt davon abweicht. 0 bedeutet steuerfrei nach § 19 UStG.",
|
||||
imagesLabel: "Produktbilder",
|
||||
visibleInShop: "Im Shop sichtbar",
|
||||
sectionText: "Text und Bilder",
|
||||
sectionTaste: "Geschmack",
|
||||
sectionPriceStock: "Preis und Bestand",
|
||||
sectionVisibility: "Sichtbarkeit",
|
||||
save: "Speichern",
|
||||
},
|
||||
adminProductNewPage: {
|
||||
|
||||
@@ -1049,6 +1049,10 @@ export const en: Dictionary = {
|
||||
"Leave empty to use the category rate (honey 7%, candles 19%). Fill in only if this product differs. 0 means exempt under § 19 German VAT Act.",
|
||||
imagesLabel: "Product images",
|
||||
visibleInShop: "Visible in shop",
|
||||
sectionText: "Text and images",
|
||||
sectionTaste: "Taste",
|
||||
sectionPriceStock: "Price and stock",
|
||||
sectionVisibility: "Visibility",
|
||||
save: "Save",
|
||||
},
|
||||
adminProductNewPage: {
|
||||
|
||||
@@ -732,6 +732,10 @@ export interface Dictionary {
|
||||
vatRateHint: string;
|
||||
imagesLabel: string;
|
||||
visibleInShop: string;
|
||||
sectionText: string;
|
||||
sectionTaste: string;
|
||||
sectionPriceStock: string;
|
||||
sectionVisibility: string;
|
||||
save: string;
|
||||
};
|
||||
adminProductNewPage: {
|
||||
|
||||
Reference in new issue
Block a user