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
+212
-146
No files matched your search
@@ -15,6 +15,7 @@ import { getLocale } from "@/i18n/locale";
|
|||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
import { isDhlConfigured } from "@/lib/dhl";
|
import { isDhlConfigured } from "@/lib/dhl";
|
||||||
import { OrderStatusBadge, OrderStatusTimeline } from "@/components/account-ui";
|
import { OrderStatusBadge, OrderStatusTimeline } from "@/components/account-ui";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
export default async function AdminOrderDetailPage(props: PageProps<"/admin/bestellungen/[id]">) {
|
export default async function AdminOrderDetailPage(props: PageProps<"/admin/bestellungen/[id]">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
@@ -69,9 +70,9 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
<Button type="submit" className="px-5 py-2.5">
|
||||||
{t.adminOrderDetailPage.saveStatus}
|
{t.adminOrderDetailPage.saveStatus}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -192,9 +193,9 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
|||||||
className="mt-1 w-56 border border-ink/20 bg-background px-3 py-2 text-sm"
|
className="mt-1 w-56 border border-ink/20 bg-background px-3 py-2 text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
<Button type="submit" className="px-5 py-2.5">
|
||||||
{t.adminOrderDetailPage.saveTracking}
|
{t.adminOrderDetailPage.saveTracking}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -281,7 +282,7 @@ export default async function AdminOrderDetailPage(props: PageProps<"/admin/best
|
|||||||
<span>{order.invoice.invoiceNumber}</span>
|
<span>{order.invoice.invoiceNumber}</span>
|
||||||
<a
|
<a
|
||||||
href={`/rechnung/${order.invoice.invoiceNumber}/pdf`}
|
href={`/rechnung/${order.invoice.invoiceNumber}/pdf`}
|
||||||
className="text-forest underline text-sm"
|
className="link-draw text-sm text-forest"
|
||||||
>
|
>
|
||||||
{t.adminOrderDetailPage.downloadPdf}
|
{t.adminOrderDetailPage.downloadPdf}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getDictionary } from "@/i18n/get-dictionary";
|
|||||||
import { isArchiveView } from "@/lib/archive";
|
import { isArchiveView } from "@/lib/archive";
|
||||||
import { ensureAutoArchived } from "@/lib/archive-runner";
|
import { ensureAutoArchived } from "@/lib/archive-runner";
|
||||||
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
||||||
|
import { AdminEmptyState, AdminPageHeader, AdminSearchForm } from "@/components/admin-list";
|
||||||
|
|
||||||
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
|
export default async function AdminOrdersPage(props: PageProps<"/admin/bestellungen">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
@@ -49,7 +50,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BackLink href="/admin" label={t.adminOrdersPage.backToDashboard} />
|
<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
|
{/* Die DHL-Aktionen leiten bei einer nicht mehr auffindbaren Bestellung
|
||||||
hierher zurueck (admin/actions/dhl.ts). Ohne diese Ausgabe sah das
|
hierher zurueck (admin/actions/dhl.ts). Ohne diese Ausgabe sah das
|
||||||
aus wie ein folgenloser Klick. */}
|
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}
|
{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">
|
<AdminSearchForm
|
||||||
{isArchive ? <input type="hidden" name="ansicht" value="archiv" /> : null}
|
query={query}
|
||||||
<label className="text-sm">
|
label={t.adminOrdersPage.searchLabel}
|
||||||
<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}
|
placeholder={t.adminOrdersPage.searchPlaceholder}
|
||||||
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
|
submitLabel={t.adminOrdersPage.filterButton}
|
||||||
/>
|
resetHref={isArchive ? "/admin/bestellungen?ansicht=archiv" : "/admin/bestellungen"}
|
||||||
</label>
|
resetLabel={t.adminOrdersPage.resetFilter}
|
||||||
|
hiddenFields={isArchive ? { ansicht: "archiv" } : {}}
|
||||||
|
isFiltered={Boolean(query || statusFilter)}
|
||||||
|
>
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.statusLabel}</span>
|
<span className="block text-ink/65 text-xs mb-1">{t.adminOrdersPage.statusLabel}</span>
|
||||||
<select
|
<select
|
||||||
@@ -94,18 +93,7 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
</AdminSearchForm>
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="overflow-x-auto bg-background border border-ink/10">
|
<div className="overflow-x-auto bg-background border border-ink/10">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
@@ -125,12 +113,19 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
{orders.map((order) => (
|
{orders.map((order) => (
|
||||||
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
|
<tr key={order.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||||
<td className="p-4 text-forest whitespace-nowrap">
|
<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}
|
{order.orderNumber}
|
||||||
</Link>
|
</Link>
|
||||||
{order.giftMessage ? (
|
{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>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
@@ -139,9 +134,9 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
<br />
|
<br />
|
||||||
<span className="text-ink/65 text-xs">{order.email}</span>
|
<span className="text-ink/65 text-xs">{order.email}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">{formatDate(order.createdAt)}</td>
|
<td className="p-4 whitespace-nowrap tabular-nums">{formatDate(order.createdAt)}</td>
|
||||||
<td className="p-4">{order.items.length}</td>
|
<td className="p-4 tabular-nums">{order.items.length}</td>
|
||||||
<td className="p-4">{formatPrice(order.totalCents, order.currency)}</td>
|
<td className="p-4 whitespace-nowrap tabular-nums">{formatPrice(order.totalCents, order.currency)}</td>
|
||||||
<td className="p-4">
|
<td className="p-4">
|
||||||
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
|
<OrderStatusBadge status={order.status} label={statusLabels[order.status] ?? order.status} />
|
||||||
<form action={updateOrderStatusAction} className="mt-2 flex items-center gap-2">
|
<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
|
<select
|
||||||
name="status"
|
name="status"
|
||||||
defaultValue={order.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]) => (
|
{Object.entries(statusLabels).map(([value, label]) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value}>
|
||||||
@@ -157,7 +153,10 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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}
|
{t.adminOrdersPage.save}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -184,13 +183,15 @@ export default async function AdminOrdersPage(props: PageProps<"/admin/bestellun
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{orders.length === 0 ? (
|
{orders.length === 0 ? (
|
||||||
<p className="p-6 text-ink/65">
|
<AdminEmptyState
|
||||||
{query || statusFilter
|
message={
|
||||||
|
query || statusFilter
|
||||||
? t.adminOrdersPage.noResultsFiltered
|
? t.adminOrdersPage.noResultsFiltered
|
||||||
: isArchive
|
: isArchive
|
||||||
? t.adminArchive.emptyArchive
|
? t.adminArchive.emptyArchive
|
||||||
: t.adminOrdersPage.noOrdersYet}
|
: t.adminOrdersPage.noOrdersYet
|
||||||
</p>
|
}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getLocale } from "@/i18n/locale";
|
|||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
import type { Dictionary } from "@/i18n/dictionary-type";
|
import type { Dictionary } from "@/i18n/dictionary-type";
|
||||||
import type { Locale } from "@/i18n/config";
|
import type { Locale } from "@/i18n/config";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
|
const ROWS_BY_TYPE: Record<ContentFieldDef["type"], number> = {
|
||||||
text: 1,
|
text: 1,
|
||||||
@@ -190,9 +191,9 @@ function GroupForm({
|
|||||||
<FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} />
|
<FieldEditor key={def.key} def={def} row={current.get(def.key)} locale={locale} t={t} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
<Button type="submit" className="px-5 py-2.5">
|
||||||
{t.adminContentPage.save}
|
{t.adminContentPage.save}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { BackLink } from "@/components/back-link";
|
import { BackLink } from "@/components/back-link";
|
||||||
import { formatPrice } from "@/lib/format";
|
import { formatPrice } from "@/lib/format";
|
||||||
@@ -10,6 +9,15 @@ import { getDictionary } from "@/i18n/get-dictionary";
|
|||||||
import { isArchiveView } from "@/lib/archive";
|
import { isArchiveView } from "@/lib/archive";
|
||||||
import { ensureAutoArchived } from "@/lib/archive-runner";
|
import { ensureAutoArchived } from "@/lib/archive-runner";
|
||||||
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
||||||
|
import {
|
||||||
|
AdminActionButton,
|
||||||
|
AdminActionLink,
|
||||||
|
AdminActionNote,
|
||||||
|
AdminBadge,
|
||||||
|
AdminEmptyState,
|
||||||
|
AdminPageHeader,
|
||||||
|
AdminRowActions,
|
||||||
|
} from "@/components/admin-list";
|
||||||
|
|
||||||
export default async function AdminProductsPage(props: PageProps<"/admin/produkte">) {
|
export default async function AdminProductsPage(props: PageProps<"/admin/produkte">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
@@ -34,15 +42,12 @@ export default async function AdminProductsPage(props: PageProps<"/admin/produkt
|
|||||||
prisma.product.count({ where: { archivedAt: { not: null } } }),
|
prisma.product.count({ where: { archivedAt: { not: null } } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const newProduct = { href: "/admin/produkte/neu", label: t.adminProductsPage.newProduct };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BackLink href="/admin" label={t.adminProductsPage.backToDashboard} />
|
<BackLink href="/admin" label={t.adminProductsPage.backToDashboard} />
|
||||||
<div className="flex items-center justify-between mb-8">
|
<AdminPageHeader title={t.adminProductsPage.title} action={newProduct} />
|
||||||
<h1 className="text-2xl text-forest">{t.adminProductsPage.title}</h1>
|
|
||||||
<Link href="/admin/produkte/neu" className="bg-forest text-cream px-5 py-2 text-sm">
|
|
||||||
{t.adminProductsPage.newProduct}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ApiCoreSyncResult searchParams={searchParams} />
|
<ApiCoreSyncResult searchParams={searchParams} />
|
||||||
|
|
||||||
@@ -63,44 +68,51 @@ export default async function AdminProductsPage(props: PageProps<"/admin/produkt
|
|||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="text-left text-ink/65 border-b border-ink/10">
|
<thead className="text-left text-ink/65 border-b border-ink/10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-4">{t.adminProductsPage.colProduct}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colProduct}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colCategory}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colCategory}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colHarvest}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colHarvest}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colBatch}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colBatch}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colWeight}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colWeight}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colPrice}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colPrice}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colStock}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colStock}</th>
|
||||||
<th className="p-4">{t.adminProductsPage.colStatus}</th>
|
<th className="px-3 py-4">{t.adminProductsPage.colStatus}</th>
|
||||||
<th className="p-4" />
|
<th className="px-3 py-4" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-ink/10">
|
<tbody className="divide-y divide-ink/10">
|
||||||
{products.map((product) => (
|
{products.map((product) => (
|
||||||
<tr key={product.id} className="transition-colors hover:bg-ink/[0.03]">
|
<tr key={product.id} className="transition-colors hover:bg-ink/[0.03]">
|
||||||
<td className="p-4 text-forest">{product.name}</td>
|
<td className="px-3 py-4">
|
||||||
<td className="p-4">{t.common.categories[product.category]}</td>
|
<AdminActionLink href={`/admin/produkte/${product.id}`}>{product.name}</AdminActionLink>
|
||||||
<td className="p-4">
|
</td>
|
||||||
|
<td className="px-3 py-4">{t.common.categories[product.category]}</td>
|
||||||
|
<td className="px-3 py-4">
|
||||||
{product.harvest ? `${product.harvest.type} ${product.harvest.year}` : "—"}
|
{product.harvest ? `${product.harvest.type} ${product.harvest.year}` : "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4">{product.batch?.code ?? "—"}</td>
|
<td className="px-3 py-4 whitespace-nowrap">{product.batch?.code ?? "—"}</td>
|
||||||
<td className="p-4">{product.weightGrams} g</td>
|
<td className="px-3 py-4 whitespace-nowrap tabular-nums">{product.weightGrams} g</td>
|
||||||
<td className="p-4">{formatPrice(product.priceCents, product.currency)}</td>
|
<td className="px-3 py-4 whitespace-nowrap tabular-nums">{formatPrice(product.priceCents, product.currency)}</td>
|
||||||
<td className="p-4">{product.inventory?.quantity ?? 0}</td>
|
<td className={`px-3 py-4 tabular-nums ${(product.inventory?.quantity ?? 0) === 0 ? "text-ink/55" : ""}`}>
|
||||||
<td className="p-4">
|
{product.inventory?.quantity ?? 0}
|
||||||
{product.isActive ? t.adminProductsPage.statusActive : t.adminProductsPage.statusInactive}
|
</td>
|
||||||
|
<td className="px-3 py-4">
|
||||||
|
{product.isActive ? (
|
||||||
|
<AdminBadge tone="positive">{t.adminProductsPage.statusActive}</AdminBadge>
|
||||||
|
) : (
|
||||||
|
<AdminBadge tone="neutral">{t.adminProductsPage.statusInactive}</AdminBadge>
|
||||||
|
)}
|
||||||
{/* Ein Produkt ohne Preis laesst sich nicht aktiv schalten
|
{/* Ein Produkt ohne Preis laesst sich nicht aktiv schalten
|
||||||
(admin/actions/products.ts). Aus ApiCore importierte
|
(admin/actions/products.ts). Aus ApiCore importierte
|
||||||
Entwuerfe sehen sonst wie ein normales inaktives Produkt
|
Entwuerfe sehen sonst wie ein normales inaktives Produkt
|
||||||
aus, und es bliebe unklar, WARUM das Aktivieren scheitert. */}
|
aus, und es bliebe unklar, WARUM das Aktivieren scheitert. */}
|
||||||
{product.priceCents <= 0 ? (
|
{product.priceCents <= 0 ? (
|
||||||
<span className="block text-xs text-honey-dark">{t.adminProductsPage.draftHint}</span>
|
<span className="mt-1.5 block text-xs text-honey-dark">{t.adminProductsPage.draftHint}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-4 whitespace-nowrap">
|
<td className="px-3 py-4">
|
||||||
<Link href={`/admin/produkte/${product.id}`} className="text-forest underline mr-4">
|
{/* Bearbeiten ueber den Produktnamen links — hier nur die
|
||||||
{t.adminProductsPage.edit}
|
selteneren Aktionen, damit die Tabelle nicht seitlich scrollt. */}
|
||||||
</Link>
|
<AdminRowActions>
|
||||||
<span className="mr-4">
|
|
||||||
<AdminArchiveToggle
|
<AdminArchiveToggle
|
||||||
kind="products"
|
kind="products"
|
||||||
id={product.id}
|
id={product.id}
|
||||||
@@ -113,7 +125,6 @@ export default async function AdminProductsPage(props: PageProps<"/admin/produkt
|
|||||||
manualHint: t.adminArchive.manualHint,
|
manualHint: t.adminArchive.manualHint,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
{/* Loeschen nur, solange nie bestellt: OrderItem verweist mit
|
{/* Loeschen nur, solange nie bestellt: OrderItem verweist mit
|
||||||
onDelete: Restrict auf das Produkt, damit gestellte
|
onDelete: Restrict auf das Produkt, damit gestellte
|
||||||
Rechnungen ihren Bezug behalten (siehe
|
Rechnungen ihren Bezug behalten (siehe
|
||||||
@@ -123,25 +134,24 @@ export default async function AdminProductsPage(props: PageProps<"/admin/produkt
|
|||||||
<ConfirmForm
|
<ConfirmForm
|
||||||
action={deleteProductAction}
|
action={deleteProductAction}
|
||||||
confirmMessage={t.adminProductsPage.deleteConfirm(product.name)}
|
confirmMessage={t.adminProductsPage.deleteConfirm(product.name)}
|
||||||
className="inline"
|
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={product.id} />
|
<input type="hidden" name="id" value={product.id} />
|
||||||
<button type="submit" className="text-red-700 underline">
|
<AdminActionButton tone="danger">{t.adminProductsPage.delete}</AdminActionButton>
|
||||||
{t.adminProductsPage.delete}
|
|
||||||
</button>
|
|
||||||
</ConfirmForm>
|
</ConfirmForm>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-ink/50">{t.adminProductsPage.notDeletable}</span>
|
<AdminActionNote>{t.adminProductsPage.notDeletable}</AdminActionNote>
|
||||||
)}
|
)}
|
||||||
|
</AdminRowActions>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{products.length === 0 ? (
|
{products.length === 0 ? (
|
||||||
<p className="p-6 text-ink/65">
|
<AdminEmptyState
|
||||||
{isArchive ? t.adminArchive.emptyArchive : t.adminProductsPage.noProductsYet}
|
message={isArchive ? t.adminArchive.emptyArchive : t.adminProductsPage.noProductsYet}
|
||||||
</p>
|
action={isArchive ? undefined : newProduct}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { BackLink } from "@/components/back-link";
|
import { BackLink } from "@/components/back-link";
|
||||||
import { formatDate, formatPrice } from "@/lib/format";
|
import { formatDate, formatPrice } from "@/lib/format";
|
||||||
@@ -9,6 +8,23 @@ import { getDictionary } from "@/i18n/get-dictionary";
|
|||||||
import { isArchiveView } from "@/lib/archive";
|
import { isArchiveView } from "@/lib/archive";
|
||||||
import { ensureAutoArchived } from "@/lib/archive-runner";
|
import { ensureAutoArchived } from "@/lib/archive-runner";
|
||||||
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
import { AdminArchiveTabs, AdminArchiveToggle } from "@/components/admin-archive";
|
||||||
|
import { ConfirmForm } from "@/components/confirm-form";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
import {
|
||||||
|
AdminActionButton,
|
||||||
|
AdminBadge,
|
||||||
|
AdminEmptyState,
|
||||||
|
AdminPageHeader,
|
||||||
|
AdminSearchForm,
|
||||||
|
type AdminBadgeTone,
|
||||||
|
} from "@/components/admin-list";
|
||||||
|
|
||||||
|
const STATUS_TONES: Record<string, AdminBadgeTone> = {
|
||||||
|
ANGEFRAGT: "pending",
|
||||||
|
ANGENOMMEN: "pending",
|
||||||
|
ABGELEHNT: "neutral",
|
||||||
|
ERSTATTET: "positive",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function AdminRetourenPage(props: PageProps<"/admin/retouren">) {
|
export default async function AdminRetourenPage(props: PageProps<"/admin/retouren">) {
|
||||||
const t = getDictionary(await getLocale());
|
const t = getDictionary(await getLocale());
|
||||||
@@ -53,7 +69,7 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BackLink href="/admin" label={t.adminReturnsPage.backToDashboard} />
|
<BackLink href="/admin" label={t.adminReturnsPage.backToDashboard} />
|
||||||
<h1 className="text-2xl text-forest mb-8">{t.adminReturnsPage.title}</h1>
|
<AdminPageHeader title={t.adminReturnsPage.title} />
|
||||||
|
|
||||||
<AdminArchiveTabs
|
<AdminArchiveTabs
|
||||||
basePath="/admin/retouren"
|
basePath="/admin/retouren"
|
||||||
@@ -68,30 +84,15 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
|||||||
/>
|
/>
|
||||||
{isArchive ? <p className="mb-6 text-xs text-ink/60 max-w-2xl">{t.adminArchive.autoRuleReturns}</p> : null}
|
{isArchive ? <p className="mb-6 text-xs text-ink/60 max-w-2xl">{t.adminArchive.autoRuleReturns}</p> : null}
|
||||||
|
|
||||||
<form method="GET" className="flex flex-wrap items-end gap-3 mb-6">
|
<AdminSearchForm
|
||||||
{isArchive ? <input type="hidden" name="ansicht" value="archiv" /> : null}
|
query={query}
|
||||||
<label className="text-sm">
|
label={t.adminReturnsPage.searchLabel}
|
||||||
<span className="block text-ink/65 text-xs mb-1">{t.adminReturnsPage.searchLabel}</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="q"
|
|
||||||
defaultValue={query}
|
|
||||||
placeholder={t.adminReturnsPage.searchPlaceholder}
|
placeholder={t.adminReturnsPage.searchPlaceholder}
|
||||||
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
|
submitLabel={t.adminReturnsPage.filterButton}
|
||||||
|
resetHref={isArchive ? "/admin/retouren?ansicht=archiv" : "/admin/retouren"}
|
||||||
|
resetLabel={t.adminReturnsPage.resetFilter}
|
||||||
|
hiddenFields={isArchive ? { ansicht: "archiv" } : {}}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
|
||||||
{t.adminReturnsPage.filterButton}
|
|
||||||
</button>
|
|
||||||
{query ? (
|
|
||||||
<Link
|
|
||||||
href={isArchive ? "/admin/retouren?ansicht=archiv" : "/admin/retouren"}
|
|
||||||
className="text-sm text-ink/65 underline"
|
|
||||||
>
|
|
||||||
{t.adminReturnsPage.resetFilter}
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{returnRequests.map((request) => (
|
{returnRequests.map((request) => (
|
||||||
@@ -102,11 +103,16 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
|||||||
<p className="text-sm text-ink/70">
|
<p className="text-sm text-ink/70">
|
||||||
{request.order.firstName} {request.order.lastName} · {request.order.email}
|
{request.order.firstName} {request.order.lastName} · {request.order.email}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-ink/65 mt-1">{formatDate(request.createdAt)}</p>
|
<p className="text-xs text-ink/65 mt-1 tabular-nums">{formatDate(request.createdAt)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<p className="text-sm">{statusLabels[request.status] ?? request.status}</p>
|
<AdminBadge
|
||||||
<div className="mt-2">
|
tone={STATUS_TONES[request.status] ?? "neutral"}
|
||||||
|
pulse={request.status === "ANGEFRAGT"}
|
||||||
|
>
|
||||||
|
{statusLabels[request.status] ?? request.status}
|
||||||
|
</AdminBadge>
|
||||||
|
<div>
|
||||||
<AdminArchiveToggle
|
<AdminArchiveToggle
|
||||||
kind="returns"
|
kind="returns"
|
||||||
id={request.id}
|
id={request.id}
|
||||||
@@ -130,46 +136,59 @@ export default async function AdminRetourenPage(props: PageProps<"/admin/retoure
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-2 text-forest">{formatPrice(request.order.totalCents, request.order.currency)}</p>
|
<p className="mt-2 text-forest tabular-nums">{formatPrice(request.order.totalCents, request.order.currency)}</p>
|
||||||
|
|
||||||
<p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p>
|
<p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p>
|
||||||
|
|
||||||
{request.status === "ANGEFRAGT" ? (
|
{request.status === "ANGEFRAGT" ? (
|
||||||
<div className="mt-4 flex gap-4">
|
<div className="mt-4 flex flex-wrap items-center gap-6 text-sm">
|
||||||
<form action={updateReturnRequestStatusAction}>
|
<form action={updateReturnRequestStatusAction}>
|
||||||
<input type="hidden" name="id" value={request.id} />
|
<input type="hidden" name="id" value={request.id} />
|
||||||
<input type="hidden" name="status" value="ANGENOMMEN" />
|
<input type="hidden" name="status" value="ANGENOMMEN" />
|
||||||
<button type="submit" className="text-xs text-forest underline">
|
<Button type="submit" variant="secondary" className="px-5 py-2.5">
|
||||||
{t.adminReturnsPage.accept}
|
{t.adminReturnsPage.accept}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
<form action={updateReturnRequestStatusAction}>
|
<ConfirmForm
|
||||||
|
action={updateReturnRequestStatusAction}
|
||||||
|
confirmMessage={t.adminReturnsPage.rejectConfirm(request.order.orderNumber)}
|
||||||
|
>
|
||||||
<input type="hidden" name="id" value={request.id} />
|
<input type="hidden" name="id" value={request.id} />
|
||||||
<input type="hidden" name="status" value="ABGELEHNT" />
|
<input type="hidden" name="status" value="ABGELEHNT" />
|
||||||
<button type="submit" className="text-xs text-red-700 underline">
|
<AdminActionButton tone="danger">{t.adminReturnsPage.reject}</AdminActionButton>
|
||||||
{t.adminReturnsPage.reject}
|
</ConfirmForm>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{request.status === "ANGENOMMEN" ? (
|
{request.status === "ANGENOMMEN" ? (
|
||||||
<form action={refundReturnRequestAction} className="mt-4">
|
// Erstattung bewegt echtes Geld und laesst sich nicht zuruecknehmen —
|
||||||
|
// deshalb mit Rueckfrage samt Betrag statt Ein-Klick-Link.
|
||||||
|
<ConfirmForm
|
||||||
|
action={refundReturnRequestAction}
|
||||||
|
confirmMessage={t.adminReturnsPage.refundConfirm(
|
||||||
|
request.order.orderNumber,
|
||||||
|
formatPrice(request.order.totalCents, request.order.currency)
|
||||||
|
)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
<input type="hidden" name="id" value={request.id} />
|
<input type="hidden" name="id" value={request.id} />
|
||||||
<button type="submit" className="text-xs text-forest underline">
|
<Button type="submit" className="px-5 py-2.5">
|
||||||
{t.adminReturnsPage.refundNow}
|
{t.adminReturnsPage.refundNow}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</ConfirmForm>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{returnRequests.length === 0 ? (
|
{returnRequests.length === 0 ? (
|
||||||
<p className="text-ink/65">
|
<AdminEmptyState
|
||||||
{query
|
bordered
|
||||||
|
message={
|
||||||
|
query
|
||||||
? t.adminReturnsPage.noResultsFiltered
|
? t.adminReturnsPage.noResultsFiltered
|
||||||
: isArchive
|
: isArchive
|
||||||
? t.adminArchive.emptyArchive
|
? t.adminArchive.emptyArchive
|
||||||
: t.adminReturnsPage.noRequestsYet}
|
: t.adminReturnsPage.noRequestsYet
|
||||||
</p>
|
}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ export default async function ProductDetailPage(
|
|||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/versand-zahlung"
|
href="/versand-zahlung"
|
||||||
className="mt-2 inline-block text-xs text-ink/65 underline decoration-ink/30 hover:text-forest hover:decoration-forest"
|
className="mt-1 inline-block py-1 text-xs text-ink/65 underline decoration-ink/30 hover:text-forest hover:decoration-forest"
|
||||||
>
|
>
|
||||||
{t.productPage.shippingHint(
|
{t.productPage.shippingHint(
|
||||||
baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents, "EUR", locale)
|
baseShippingCents === 0 ? t.cartPage.free : formatPrice(baseShippingCents, "EUR", locale)
|
||||||
@@ -367,7 +367,21 @@ export default async function ProductDetailPage(
|
|||||||
<form action={toggleWishlistAction} className="mt-3">
|
<form action={toggleWishlistAction} className="mt-3">
|
||||||
<input type="hidden" name="productId" value={product.id} />
|
<input type="hidden" name="productId" value={product.id} />
|
||||||
<input type="hidden" name="redirectPath" value={`/honig/${product.slug}`} />
|
<input type="hidden" name="redirectPath" value={`/honig/${product.slug}`} />
|
||||||
<button type="submit" className="text-sm text-ink/70 hover:text-forest underline">
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex items-center gap-2 py-1.5 text-sm text-ink/70 underline hover:text-forest"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
className={`h-4 w-4 ${isWishlisted ? "text-honey" : "text-honey-dark"}`}
|
||||||
|
fill={isWishlisted ? "currentColor" : "none"}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path d="M10 1.5l2.47 5.27 5.66.6-4.24 3.9 1.15 5.6L10 14.9l-5.04 2.97 1.15-5.6-4.24-3.9 5.66-.6z" />
|
||||||
|
</svg>
|
||||||
{isWishlisted ? t.productPage.removeFromWishlist : t.productPage.addToWishlist}
|
{isWishlisted ? t.productPage.removeFromWishlist : t.productPage.addToWishlist}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -413,11 +427,11 @@ export default async function ProductDetailPage(
|
|||||||
{isHoney ? (
|
{isHoney ? (
|
||||||
<div className="mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
|
<div className="mt-6 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm">
|
||||||
<span className="text-ink/65">{t.productPage.learnMoreLabel}</span>
|
<span className="text-ink/65">{t.productPage.learnMoreLabel}</span>
|
||||||
<Link href="/wissen/warum-kristallisiert-honig" className="text-forest underline">
|
<Link href="/wissen/warum-kristallisiert-honig" className="inline-block py-0.5 text-forest underline">
|
||||||
{t.wissenArticles["warum-kristallisiert-honig"].title}
|
{t.wissenArticles["warum-kristallisiert-honig"].title}
|
||||||
</Link>
|
</Link>
|
||||||
{harvestWissenSlug ? (
|
{harvestWissenSlug ? (
|
||||||
<Link href={`/wissen/${harvestWissenSlug}`} className="text-forest underline">
|
<Link href={`/wissen/${harvestWissenSlug}`} className="inline-block py-0.5 text-forest underline">
|
||||||
{t.wissenArticles[harvestWissenSlug].title}
|
{t.wissenArticles[harvestWissenSlug].title}
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -427,7 +441,7 @@ export default async function ProductDetailPage(
|
|||||||
{isHoney && product.batch ? (
|
{isHoney && product.batch ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/charge/${product.batch.code}`}
|
href={`/charge/${product.batch.code}`}
|
||||||
className="mt-3 inline-block text-sm text-forest underline"
|
className="mt-2.5 inline-block py-0.5 text-sm text-forest underline"
|
||||||
>
|
>
|
||||||
{t.productPage.viewBatchOrigin}
|
{t.productPage.viewBatchOrigin}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ export default async function KontaktPage() {
|
|||||||
<div className="mt-12 grid gap-6 md:grid-cols-5">
|
<div className="mt-12 grid gap-6 md:grid-cols-5">
|
||||||
{/* Kontaktkarte auf Wabenraster: die E-Mail ist der Hauptweg und
|
{/* Kontaktkarte auf Wabenraster: die E-Mail ist der Hauptweg und
|
||||||
steht deshalb gross in Serifenschrift. */}
|
steht deshalb gross in Serifenschrift. */}
|
||||||
<ScrollReveal delay={0.1} className="md:col-span-3">
|
<ScrollReveal delay={0.1} className="min-w-0 md:col-span-3">
|
||||||
<div className="relative h-full overflow-hidden bg-forest px-8 py-10 text-cream md:px-10">
|
<div className="relative h-full overflow-hidden bg-forest px-8 py-10 text-cream md:px-10">
|
||||||
<HoneycombPattern className="text-honey-light/[0.1]" size={24} fade="right" />
|
<HoneycombPattern className="text-honey-light/[0.1]" size={24} fade="right" />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<a
|
<a
|
||||||
href={`mailto:${company.email}`}
|
href={`mailto:${company.email}`}
|
||||||
className="group inline-flex items-center gap-3 font-serif text-3xl md:text-4xl text-cream"
|
className="group inline-flex items-center gap-3 font-serif text-2xl sm:text-3xl md:text-4xl text-cream"
|
||||||
>
|
>
|
||||||
<span className="link-draw pb-1">{company.email}</span>
|
<span className="link-draw pb-1">{company.email}</span>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export default async function KontoPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2 className="text-2xl text-forest mb-4">{t.account.privacy.heading}</h2>
|
<h2 className="text-2xl text-forest mb-4">{t.account.privacy.heading}</h2>
|
||||||
<div className="border border-ink/10 p-6 flex items-center justify-between gap-4">
|
<div className="border border-ink/10 p-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg text-forest">{t.account.privacy.downloadDataHeading}</h3>
|
<h3 className="text-lg text-forest">{t.account.privacy.downloadDataHeading}</h3>
|
||||||
<p className="mt-1 text-sm text-ink/70">{t.account.privacy.downloadDataDescription}</p>
|
<p className="mt-1 text-sm text-ink/70">{t.account.privacy.downloadDataDescription}</p>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function AdminArchiveToggle({
|
|||||||
{archivedManually ? ` · ${labels.manualHint}` : null}
|
{archivedManually ? ` · ${labels.manualHint}` : null}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<button type="submit" className="text-xs text-ink/65 underline hover:text-forest">
|
<button type="submit" className="link-draw cursor-pointer text-ink/70 hover:text-forest">
|
||||||
{archivedAt ? labels.restore : labels.archive}
|
{archivedAt ? labels.restore : labels.archive}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ export function AdminSearchForm({
|
|||||||
submitLabel,
|
submitLabel,
|
||||||
resetHref,
|
resetHref,
|
||||||
resetLabel,
|
resetLabel,
|
||||||
|
hiddenFields = {},
|
||||||
|
children,
|
||||||
|
isFiltered,
|
||||||
}: {
|
}: {
|
||||||
query: string;
|
query: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -46,9 +49,18 @@ export function AdminSearchForm({
|
|||||||
submitLabel: string;
|
submitLabel: string;
|
||||||
resetHref: string;
|
resetHref: string;
|
||||||
resetLabel: string;
|
resetLabel: string;
|
||||||
|
/** Mitzusendende Parameter, z.B. die Archiv-Ansicht. */
|
||||||
|
hiddenFields?: Record<string, string>;
|
||||||
|
/** Weitere Filterfelder (z.B. Status-Auswahl) zwischen Suche und Knopf. */
|
||||||
|
children?: ReactNode;
|
||||||
|
/** Ob "Filter zuruecksetzen" erscheint; Standard: sobald gesucht wird. */
|
||||||
|
isFiltered?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<form method="GET" role="search" className="mb-6 flex flex-wrap items-end gap-3">
|
<form method="GET" role="search" className="mb-6 flex flex-wrap items-end gap-3">
|
||||||
|
{Object.entries(hiddenFields).map(([name, value]) => (
|
||||||
|
<input key={name} type="hidden" name={name} value={value} />
|
||||||
|
))}
|
||||||
<label className="text-sm">
|
<label className="text-sm">
|
||||||
<span className="mb-1 block text-xs text-ink/65">{label}</span>
|
<span className="mb-1 block text-xs text-ink/65">{label}</span>
|
||||||
<input
|
<input
|
||||||
@@ -59,13 +71,14 @@ export function AdminSearchForm({
|
|||||||
className="w-64 max-w-full border border-ink/20 bg-background px-3 py-2 text-sm"
|
className="w-64 max-w-full border border-ink/20 bg-background px-3 py-2 text-sm"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{children}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="border border-forest px-4 py-2 text-sm text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
className="border border-forest px-4 py-2 text-sm text-forest transition-colors duration-300 hover:bg-forest hover:text-cream"
|
||||||
>
|
>
|
||||||
{submitLabel}
|
{submitLabel}
|
||||||
</button>
|
</button>
|
||||||
{query ? (
|
{(isFiltered ?? query !== "") ? (
|
||||||
<Link href={resetHref} className="link-draw py-2 text-sm text-ink/65">
|
<Link href={resetHref} className="link-draw py-2 text-sm text-ink/65">
|
||||||
{resetLabel}
|
{resetLabel}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { syncApiCoreAction } from "@/app/admin/actions";
|
import { syncApiCoreAction } from "@/app/admin/actions";
|
||||||
import { getLocale } from "@/i18n/locale";
|
import { getLocale } from "@/i18n/locale";
|
||||||
import { getDictionary } from "@/i18n/get-dictionary";
|
import { getDictionary } from "@/i18n/get-dictionary";
|
||||||
|
import { Button } from "@/components/ui";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Der eine Sync-Knopf und die Anzeige seines Ergebnisses.
|
* Der eine Sync-Knopf und die Anzeige seines Ergebnisses.
|
||||||
@@ -95,9 +96,9 @@ export async function ApiCoreSyncPanel({ returnTo }: { returnTo: string }) {
|
|||||||
<p className="text-sm text-ink/65 mb-4">{t.intro}</p>
|
<p className="text-sm text-ink/65 mb-4">{t.intro}</p>
|
||||||
<form action={syncApiCoreAction}>
|
<form action={syncApiCoreAction}>
|
||||||
<input type="hidden" name="returnTo" value={returnTo} />
|
<input type="hidden" name="returnTo" value={returnTo} />
|
||||||
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
<Button type="submit" className="px-5 py-2.5">
|
||||||
{t.button}
|
{t.button}
|
||||||
</button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export function BackLink({ href, label }: { href: string; label: string }) {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-ink/70 hover:text-forest mb-6"
|
className="-mt-1.5 mb-4.5 inline-flex items-center gap-1.5 py-1.5 text-sm text-ink/70 hover:text-forest"
|
||||||
>
|
>
|
||||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75">
|
||||||
<path d="M15 6l-6 6 6 6" />
|
<path d="M15 6l-6 6 6 6" />
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ export function ProductCard({
|
|||||||
) : null}
|
) : null}
|
||||||
<Link
|
<Link
|
||||||
href={`/honig/${product.slug}`}
|
href={`/honig/${product.slug}`}
|
||||||
className="group/more mt-4 inline-flex items-center gap-2 self-start text-sm tracking-wide-caps text-forest"
|
className="group/more mt-3 inline-flex items-center gap-2 self-start py-1 text-sm tracking-wide-caps text-forest"
|
||||||
>
|
>
|
||||||
<span className="link-draw pb-0.5">
|
<span className="link-draw pb-0.5">
|
||||||
{product.category === "HONIG" ? t.product.toHoney : t.product.toProduct}
|
{product.category === "HONIG" ? t.product.toHoney : t.product.toProduct}
|
||||||
|
|||||||
@@ -1120,6 +1120,8 @@ export const de: Dictionary = {
|
|||||||
create: "Anlegen",
|
create: "Anlegen",
|
||||||
},
|
},
|
||||||
adminReturnsPage: {
|
adminReturnsPage: {
|
||||||
|
refundConfirm: (orderNumber, amount) => `Bestellung ${orderNumber} jetzt mit ${amount} per Stripe erstatten? Das lässt sich nicht rückgängig machen.`,
|
||||||
|
rejectConfirm: (orderNumber) => `Retoure zu Bestellung ${orderNumber} wirklich ablehnen?`,
|
||||||
backToDashboard: "Zurück zum Dashboard",
|
backToDashboard: "Zurück zum Dashboard",
|
||||||
title: "Retouren",
|
title: "Retouren",
|
||||||
searchLabel: "Suche",
|
searchLabel: "Suche",
|
||||||
@@ -1588,8 +1590,8 @@ export const de: Dictionary = {
|
|||||||
notifyMeLabel: "Benachrichtige mich, wenn es wieder verfügbar ist",
|
notifyMeLabel: "Benachrichtige mich, wenn es wieder verfügbar ist",
|
||||||
emailPlaceholder: "E-Mail-Adresse",
|
emailPlaceholder: "E-Mail-Adresse",
|
||||||
notifySubmit: "Anmelden",
|
notifySubmit: "Anmelden",
|
||||||
removeFromWishlist: "★ Von der Wunschliste entfernen",
|
removeFromWishlist: "Von der Wunschliste entfernen",
|
||||||
addToWishlist: "☆ Zur Wunschliste hinzufügen",
|
addToWishlist: "Zur Wunschliste hinzufügen",
|
||||||
loginToSave: "um dieses Produkt zu merken.",
|
loginToSave: "um dieses Produkt zu merken.",
|
||||||
loginLinkLabel: "Melde dich an",
|
loginLinkLabel: "Melde dich an",
|
||||||
checklist: {
|
checklist: {
|
||||||
|
|||||||
@@ -1106,6 +1106,8 @@ export const en: Dictionary = {
|
|||||||
create: "Create",
|
create: "Create",
|
||||||
},
|
},
|
||||||
adminReturnsPage: {
|
adminReturnsPage: {
|
||||||
|
refundConfirm: (orderNumber, amount) => `Refund ${amount} for order ${orderNumber} via Stripe now? This cannot be undone.`,
|
||||||
|
rejectConfirm: (orderNumber) => `Really reject the return for order ${orderNumber}?`,
|
||||||
backToDashboard: "Back to dashboard",
|
backToDashboard: "Back to dashboard",
|
||||||
title: "Returns",
|
title: "Returns",
|
||||||
searchLabel: "Search",
|
searchLabel: "Search",
|
||||||
@@ -1574,8 +1576,8 @@ export const en: Dictionary = {
|
|||||||
notifyMeLabel: "Notify me when it's back in stock",
|
notifyMeLabel: "Notify me when it's back in stock",
|
||||||
emailPlaceholder: "Email address",
|
emailPlaceholder: "Email address",
|
||||||
notifySubmit: "Sign up",
|
notifySubmit: "Sign up",
|
||||||
removeFromWishlist: "★ Remove from wishlist",
|
removeFromWishlist: "Remove from wishlist",
|
||||||
addToWishlist: "☆ Add to wishlist",
|
addToWishlist: "Add to wishlist",
|
||||||
loginToSave: "to save this product.",
|
loginToSave: "to save this product.",
|
||||||
loginLinkLabel: "Sign in",
|
loginLinkLabel: "Sign in",
|
||||||
checklist: {
|
checklist: {
|
||||||
|
|||||||
@@ -787,6 +787,8 @@ export interface Dictionary {
|
|||||||
create: string;
|
create: string;
|
||||||
};
|
};
|
||||||
adminReturnsPage: {
|
adminReturnsPage: {
|
||||||
|
refundConfirm: (orderNumber: string, amount: string) => string;
|
||||||
|
rejectConfirm: (orderNumber: string) => string;
|
||||||
backToDashboard: string;
|
backToDashboard: string;
|
||||||
title: string;
|
title: string;
|
||||||
searchLabel: string;
|
searchLabel: string;
|
||||||
|
|||||||
Reference in new issue
Block a user