feat(ui): Polish-Durchgang Admin und Mobil, Erstattung nur mit Rueckfrage
Sicherheit: "Jetzt per Stripe erstatten" loeste die Rueckerstattung bisher mit einem einzigen Klick aus. Erstatten und Ablehnen einer Retoure laufen jetzt ueber ConfirmForm; die Rueckfrage nennt Bestellnummer und Betrag. Admin: Produkte, Bestellungen und Retouren nutzen dieselben Bausteine wie die uebrigen Listen (Seitenkopf, Suche mit Zusatzfiltern und Archiv- Parameter, Status-Badges, Leerzustaende). Produkttabelle ohne seitliches Scrollen bei 1440 px: Bearbeiten ueber den Produktnamen, Charge ohne Umbruch. Geschenk-Markierung als SVG statt Emoji. Letzte rohe Knoepfe (Bestelldetail, ApiCore, Inhalte) auf Button umgestellt. Mobil (390 px): Ueberlauf auf /konto (Datenexport) und /kontakt (E-Mail-Adresse) behoben. Tippziele unter 24 px vergroessert (Zurueck-Link, "Zum Honig", Links auf der Produktseite). Wunschliste mit SVG-Stern statt Unicode-Zeichen. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
b7a75dddca
commit
11eccec01b
16 files changed
+221
-155
No files matched your search
@@ -11,6 +11,7 @@ import { getDictionary } from "@/i18n/get-dictionary";
|
||||
import { isArchiveView } from "@/lib/archive";
|
||||
import { ensureAutoArchived } from "@/lib/archive-runner";
|
||||
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
||||
import { AdminEmptyState, AdminPageHeader, AdminSearchForm } from "@/components/admin-list";
|
||||
|
||||
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
|
||||
const t = getDictionary(await getLocale());
|
||||
@@ -49,7 +50,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
return (
|
||||
<div>
|
||||
<BackLink href="/admin" label={t.adminOrdersPage.backToDashboard} />
|
||||
<h1 className="text-2xl text-forest mb-8">{t.adminOrdersPage.title}</h1>
|
||||
<AdminPageHeader title={t.adminOrdersPage.title} />
|
||||
{/* Die DHL-Aktionen leiten bei einer nicht mehr auffindbaren Bestellung
|
||||
hierher zurueck (admin/actions/dhl.ts). Ohne diese Ausgabe sah das
|
||||
aus wie ein folgenloser Klick. */}
|
||||
@@ -67,18 +68,16 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
/>
|
||||
{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
|
||||
type="text"
|
||||
name="q"
|
||||
defaultValue={query}
|
||||
placeholder={t.adminOrdersPage.searchPlaceholder}
|
||||
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
|
||||
/>
|
||||
</label>
|
||||
<AdminSearchForm
|
||||
query={query}
|
||||
label={t.adminOrdersPage.searchLabel}
|
||||
placeholder={t.adminOrdersPage.searchPlaceholder}
|
||||
submitLabel={t.adminOrdersPage.filterButton}
|
||||
resetHref={isArchive ? "/admin/bestellungen?ansicht=archiv" : "/admin/bestellungen"}
|
||||
resetLabel={t.adminOrdersPage.resetFilter}
|
||||
hiddenFields={isArchive ? { ansicht: "archiv" } : {}}
|
||||
isFiltered={Boolean(query || statusFilter)}
|
||||
>
|
||||
<label className="text-sm">
|
||||
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.statusLabel}</span>
|
||||
<select
|
||||
@@ -94,18 +93,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
||||
{t.adminOrdersPage.filterButton}
|
||||
</button>
|
||||
{query || statusFilter ? (
|
||||
<Link
|
||||
href={isArchive ? "/admin/bestellungen?ansicht=archiv" : "/admin/bestellungen"}
|
||||
className="text-sm text-ink/65 underline"
|
||||
>
|
||||
{t.adminOrdersPage.resetFilter}
|
||||
</Link>
|
||||
) : null}
|
||||
</form>
|
||||
</AdminSearchForm>
|
||||
|
||||
<div className="overflow-x-auto bg-background border border-ink/10">
|
||||
<table className="w-full text-sm">
|
||||
@@ -125,12 +113,19 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
{orders.map((order) => (
|
||||
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||
<td className="p-4 text-forest whitespace-nowrap">
|
||||
<Link href={`/admin/bestellungen/${order.id}`} className="underline">
|
||||
<Link href={`/admin/bestellungen/${order.id}`} className="link-draw">
|
||||
{order.orderNumber}
|
||||
</Link>
|
||||
{order.giftMessage ? (
|
||||
<span className="ml-1.5" title={t.adminOrdersPage.giftOrderTitle}>
|
||||
🎁
|
||||
<span
|
||||
className="ml-1.5 inline-flex align-[-2px] text-honey-dark"
|
||||
title={t.adminOrdersPage.giftOrderTitle}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" aria-hidden="true">
|
||||
<rect x="3.5" y="8.5" width="17" height="4" />
|
||||
<path d="M5 12.5v8h14v-8M12 8.5v12M12 8.5c-1.5-3.5-5-4-5-1.5 0 1.5 2.5 1.5 5 1.5zm0 0c1.5-3.5 5-4 5-1.5 0 1.5-2.5 1.5-5 1.5z" />
|
||||
</svg>
|
||||
<span className="sr-only">{t.adminOrdersPage.giftOrderTitle}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
@@ -139,9 +134,9 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
<br />
|
||||
<span className="text-ink/65 text-xs">{order.email}</span>
|
||||
</td>
|
||||
<td className="p-4">{formatDate(order.createdAt)}</td>
|
||||
<td className="p-4">{order.items.length}</td>
|
||||
<td className="p-4">{formatPrice(order.totalCents, order.currency)}</td>
|
||||
<td className="p-4 whitespace-nowrap tabular-nums">{formatDate(order.createdAt)}</td>
|
||||
<td className="p-4 tabular-nums">{order.items.length}</td>
|
||||
<td className="p-4 whitespace-nowrap tabular-nums">{formatPrice(order.totalCents, order.currency)}</td>
|
||||
<td className="p-4">
|
||||
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
|
||||
<form action={updateOrderStatusAction} className="mt-2 flex items-center gap-2">
|
||||
@@ -149,7 +144,8 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
<select
|
||||
name="status"
|
||||
defaultValue={order.status}
|
||||
className="border border-ink/20 bg-background px-2 py-1 text-xs"
|
||||
aria-label={t.adminOrdersPage.statusLabel}
|
||||
className="border border-ink/20 bg-background px-2 py-1.5 text-xs"
|
||||
>
|
||||
{Object.entries(statusLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
@@ -157,7 +153,10 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" className="text-xs text-forest underline">
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-forest px-3 py-1.5 text-xs text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||
>
|
||||
{t.adminOrdersPage.save}
|
||||
</button>
|
||||
</form>
|
||||
@@ -184,13 +183,15 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
||||
</tbody>
|
||||
</table>
|
||||
{orders.length === 0 ? (
|
||||
<p className="p-6 text-ink/65">
|
||||
{query || statusFilter
|
||||
? t.adminOrdersPage.noResultsFiltered
|
||||
: isArchive
|
||||
? t.adminArchive.emptyArchive
|
||||
: t.adminOrdersPage.noOrdersYet}
|
||||
</p>
|
||||
<AdminEmptyState
|
||||
message={
|
||||
query || statusFilter
|
||||
? t.adminOrdersPage.noResultsFiltered
|
||||
: isArchive
|
||||
? t.adminArchive.emptyArchive
|
||||
: t.adminOrdersPage.noOrdersYet
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user