feat(admin): Formulare fuer Gutscheine, Versand, Benutzer, Ernten und Chargen vereinheitlicht

Alle Anlege- und Bearbeiten-Formulare nutzen jetzt die Bausteine aus
components/admin-form.tsx wie das Produktformular: Rahmen mit fester
Speicherleiste, Abschnitte mit Serif-Ueberschrift (Gutschein: Code und
Rabatt / Bedingungen / Zeitraum und Status; Ernte: Angaben / Bilder /
Sichtbarkeit; Charge: Abfuellung und Bestand / Bilder), Schalter statt
Checkbox, buendige Eingabefelder in Rastern.

Benutzer bearbeiten: Sicherheitsbereich mit Status-Badges, Passwort-Reset
als Sekundaerknopf, Sperren/Loeschen in eigenem abgesetzten Bereich.
AdminField/AdminSelect koennen jetzt min und einen Hinweistext.

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 21:14:10 +02:00
1 parent a09057d960
commit b7a75dddca
12 files changed
+364 -524

No files matched your search

+73 -101
View File
@@ -2,8 +2,11 @@ import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { ConfirmForm } from "@/components/confirm-form"; import { ConfirmForm } from "@/components/confirm-form";
import { Button } from "@/components/ui";
import { getCurrentAdmin } from "@/lib/auth"; import { getCurrentAdmin } from "@/lib/auth";
import { formatDate } from "@/lib/format"; import { formatDate } from "@/lib/format";
import { AdminField, AdminFieldset, AdminFormPanel, AdminSelect } from "@/components/admin-form";
import { AdminActionButton, AdminBadge } from "@/components/admin-list";
import { import {
updateCustomerAction, updateCustomerAction,
blockCustomerAction, blockCustomerAction,
@@ -16,6 +19,7 @@ import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBenutzerEditPage(props: PageProps<"/admin/benutzer/[id]">) { export default async function AdminBenutzerEditPage(props: PageProps<"/admin/benutzer/[id]">) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const tu = t.adminUserEditPage;
const { id } = await props.params; const { id } = await props.params;
const [admin, customer] = await Promise.all([ const [admin, customer] = await Promise.all([
@@ -28,60 +32,56 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
const isTemporarilyLocked = Boolean(customer.lockedUntil && customer.lockedUntil > new Date()); const isTemporarilyLocked = Boolean(customer.lockedUntil && customer.lockedUntil > new Date());
return ( return (
<div className="max-w-xl"> <div className="max-w-2xl">
<BackLink href="/admin/benutzer" label={t.adminUserEditPage.backToUsers} /> <BackLink href="/admin/benutzer" label={tu.backToUsers} />
<h1 className="text-2xl text-forest mb-2"> <h1 className="text-2xl text-forest mb-2">
{customer.firstName} {customer.lastName} {customer.firstName} {customer.lastName}
</h1> </h1>
<p className="text-sm text-ink/65 mb-8">{t.adminUserEditPage.createdOn(formatDate(customer.createdAt))}</p> <p className="text-sm text-ink/65 mb-8">{tu.createdOn(formatDate(customer.createdAt))}</p>
<AdminFormPanel action={updateCustomerAction} actions={<Button type="submit">{tu.save}</Button>}>
<form action={updateCustomerAction} className="space-y-4 bg-background border border-ink/10 p-6">
<input type="hidden" name="id" value={customer.id} /> <input type="hidden" name="id" value={customer.id} />
<div className="grid grid-cols-2 gap-4"> <AdminFieldset legend={tu.sectionProfile} last>
<Field name="firstName" label={t.adminUserEditPage.firstNameLabel} defaultValue={customer.firstName} required /> <div className="grid grid-cols-2 items-end gap-4">
<Field name="lastName" label={t.adminUserEditPage.lastNameLabel} defaultValue={customer.lastName} required /> <AdminField name="firstName" label={tu.firstNameLabel} defaultValue={customer.firstName} required />
</div> <AdminField name="lastName" label={tu.lastNameLabel} defaultValue={customer.lastName} required />
<Field name="email" label={t.adminUserEditPage.emailLabel} type="email" defaultValue={customer.email} required />
<Field name="phone" label={t.adminUserEditPage.phoneLabel} defaultValue={customer.phone ?? ""} />
<label className="block text-sm">
<span className="text-ink/70">{t.adminUserEditPage.roleLabel}</span>
<select
name="role"
defaultValue={customer.role}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
>
<option value="KUNDE">{t.adminUserEditPage.roleCustomer}</option>
<option value="ADMIN">{t.adminUserEditPage.roleAdmin}</option>
</select>
{isSelf ? (
<span className="mt-1 block text-xs text-ink/65">{t.adminUserEditPage.cannotChangeOwnRole}</span>
) : null}
</label>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminUserEditPage.save}
</button>
</form>
<div className="mt-6 bg-background border border-ink/10 p-6 space-y-4">
<h2 className="text-sm text-ink/70">{t.adminUserEditPage.securityHeading}</h2>
<dl className="text-sm space-y-1">
<div className="flex justify-between">
<dt className="text-ink/65">{t.adminUserEditPage.twoFactorLabel}</dt>
<dd>{customer.twoFactorEnabled ? t.adminUserEditPage.twoFactorActive : t.adminUserEditPage.twoFactorInactive}</dd>
</div> </div>
<div className="flex justify-between"> <AdminField name="email" label={tu.emailLabel} type="email" defaultValue={customer.email} required />
<dt className="text-ink/65">{t.adminUserEditPage.statusLabel}</dt> <AdminField name="phone" label={tu.phoneLabel} type="tel" defaultValue={customer.phone ?? ""} />
<AdminSelect
name="role"
label={tu.roleLabel}
defaultValue={customer.role}
hint={isSelf ? tu.cannotChangeOwnRole : undefined}
>
<option value="KUNDE">{tu.roleCustomer}</option>
<option value="ADMIN">{tu.roleAdmin}</option>
</AdminSelect>
</AdminFieldset>
</AdminFormPanel>
<section className="mt-8 space-y-6 border border-ink/10 bg-background p-6 sm:p-8">
<h2 className="font-serif text-xl text-forest">{tu.securityHeading}</h2>
<dl className="divide-y divide-ink/10 text-sm">
<div className="flex items-center justify-between gap-4 py-3">
<dt className="text-ink/70">{tu.twoFactorLabel}</dt>
<dd> <dd>
{customer.blockedAt ? ( {customer.twoFactorEnabled ? (
<span className="text-red-700"> <AdminBadge tone="positive">{tu.twoFactorActive}</AdminBadge>
{t.adminUserEditPage.blockedSince(formatDate(customer.blockedAt))}
</span>
) : isTemporarilyLocked ? (
<span className="text-honey-dark">{t.adminUserEditPage.temporarilyLockedFailedAttempts}</span>
) : ( ) : (
<span className="text-forest">{t.adminUserEditPage.statusActive}</span> <AdminBadge tone="neutral">{tu.twoFactorInactive}</AdminBadge>
)}
</dd>
</div>
<div className="flex items-center justify-between gap-4 py-3">
<dt className="text-ink/70">{tu.statusLabel}</dt>
<dd className="text-right">
{customer.blockedAt ? (
<AdminBadge tone="danger">{tu.blockedSince(formatDate(customer.blockedAt))}</AdminBadge>
) : isTemporarilyLocked ? (
<AdminBadge tone="pending">{tu.temporarilyLockedFailedAttempts}</AdminBadge>
) : (
<AdminBadge tone="positive">{tu.statusActive}</AdminBadge>
)} )}
</dd> </dd>
</div> </div>
@@ -89,67 +89,39 @@ export default async function AdminBenutzerEditPage(props: PageProps<"/admin/ben
<form action={sendCustomerPasswordResetAction}> <form action={sendCustomerPasswordResetAction}>
<input type="hidden" name="id" value={customer.id} /> <input type="hidden" name="id" value={customer.id} />
<button type="submit" className="bg-forest text-cream px-5 py-2 text-sm"> <Button type="submit" variant="secondary" className="px-5 py-2.5">
{t.adminUserEditPage.sendPasswordReset} {tu.sendPasswordReset}
</button> </Button>
</form> </form>
{/* Sperren/Loeschen des eigenen Kontos ist serverseitig ohnehin
verboten — hier gar nicht erst angeboten. */}
{!isSelf ? ( {!isSelf ? (
<div className="flex items-center gap-4 pt-2 border-t border-ink/10"> <div className="border-t border-ink/10 pt-6">
{customer.blockedAt ? ( <h3 className="mb-3 text-xs tracking-wide-caps text-ink/65">{tu.sectionDanger}</h3>
<form action={unblockCustomerAction}> <div className="flex flex-wrap items-center gap-6 text-sm">
{customer.blockedAt ? (
<form action={unblockCustomerAction}>
<input type="hidden" name="id" value={customer.id} />
<AdminActionButton>{tu.unblock}</AdminActionButton>
</form>
) : (
<form action={blockCustomerAction}>
<input type="hidden" name="id" value={customer.id} />
<AdminActionButton tone="muted">{tu.block}</AdminActionButton>
</form>
)}
<ConfirmForm
action={deleteCustomerAction}
confirmMessage={tu.deleteConfirm(`${customer.firstName} ${customer.lastName}`)}
>
<input type="hidden" name="id" value={customer.id} /> <input type="hidden" name="id" value={customer.id} />
<button type="submit" className="text-forest underline text-sm"> <AdminActionButton tone="danger">{tu.deleteAccount}</AdminActionButton>
{t.adminUserEditPage.unblock} </ConfirmForm>
</button> </div>
</form>
) : (
<form action={blockCustomerAction}>
<input type="hidden" name="id" value={customer.id} />
<button type="submit" className="text-ink/70 underline text-sm">
{t.adminUserEditPage.block}
</button>
</form>
)}
<ConfirmForm
action={deleteCustomerAction}
confirmMessage={t.adminUserEditPage.deleteConfirm(`${customer.firstName} ${customer.lastName}`)}
>
<input type="hidden" name="id" value={customer.id} />
<button type="submit" className="text-red-700 underline text-sm">
{t.adminUserEditPage.deleteAccount}
</button>
</ConfirmForm>
</div> </div>
) : null} ) : null}
</div> </section>
</div> </div>
); );
} }
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>
);
}
+18 -49
View File
@@ -1,64 +1,33 @@
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import { AdminField, AdminFormPanel, AdminSelect } from "@/components/admin-form";
import { createCustomerAction } from "../../actions"; import { createCustomerAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBenutzerNewPage() { export default async function AdminBenutzerNewPage() {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const tu = t.adminUserNewPage;
return ( return (
<div className="max-w-xl"> <div className="max-w-xl">
<BackLink href="/admin/benutzer" label={t.adminUserNewPage.backToUsers} /> <BackLink href="/admin/benutzer" label={tu.backToUsers} />
<h1 className="text-2xl text-forest mb-2">{t.adminUserNewPage.title}</h1> <h1 className="text-2xl text-forest mb-2">{tu.title}</h1>
<p className="text-sm text-ink/70 mb-8">{t.adminUserNewPage.intro}</p> <p className="text-sm text-ink/70 mb-8">{tu.intro}</p>
<AdminFormPanel action={createCustomerAction} actions={<Button type="submit">{tu.createAndSend}</Button>}>
<form action={createCustomerAction} className="space-y-4 bg-background border border-ink/10 p-6"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 items-end gap-4">
<Field name="firstName" label={t.adminUserNewPage.firstNameLabel} required /> <AdminField name="firstName" label={tu.firstNameLabel} required />
<Field name="lastName" label={t.adminUserNewPage.lastNameLabel} required /> <AdminField name="lastName" label={tu.lastNameLabel} required />
</div>
<AdminField name="email" label={tu.emailLabel} type="email" required />
<AdminSelect name="role" label={tu.roleLabel} defaultValue="KUNDE">
<option value="KUNDE">{tu.roleCustomer}</option>
<option value="ADMIN">{tu.roleAdmin}</option>
</AdminSelect>
</div> </div>
<Field name="email" label={t.adminUserNewPage.emailLabel} type="email" required /> </AdminFormPanel>
<label className="block text-sm">
<span className="text-ink/70">{t.adminUserNewPage.roleLabel}</span>
<select
name="role"
defaultValue="KUNDE"
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
>
<option value="KUNDE">{t.adminUserNewPage.roleCustomer}</option>
<option value="ADMIN">{t.adminUserNewPage.roleAdmin}</option>
</select>
</label>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminUserNewPage.createAndSend}
</button>
</form>
</div> </div>
); );
} }
function Field({
name,
label,
type = "text",
required = false,
}: {
name: string;
label: string;
type?: string;
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
type={type}
required={required}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
+32 -49
View File
@@ -2,96 +2,79 @@ import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { asStringArray } from "@/lib/json-array"; import { asStringArray } from "@/lib/json-array";
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import { AdminImageField } from "@/components/admin-image-field"; import { AdminImageField } from "@/components/admin-image-field";
import { AdminField, AdminFieldset, AdminFormPanel } from "@/components/admin-form";
import { updateBatchAction } from "../../actions"; import { updateBatchAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminBatchEditPage(props: PageProps<"/admin/chargen/[id]">) { export default async function AdminBatchEditPage(props: PageProps<"/admin/chargen/[id]">) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const tb = t.adminBatchEditPage;
const { id } = await props.params; const { id } = await props.params;
const rawBatch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } }); const rawBatch = await prisma.batch.findUnique({ where: { id }, include: { harvest: true } });
if (!rawBatch) notFound(); if (!rawBatch) notFound();
const batch = { ...rawBatch, images: asStringArray(rawBatch.images) }; const batch = { ...rawBatch, images: asStringArray(rawBatch.images) };
return ( return (
<div className="max-w-xl"> <div className="max-w-2xl">
<BackLink href="/admin/chargen" label={t.adminBatchEditPage.backToBatches} /> <BackLink href="/admin/chargen" label={tb.backToBatches} />
<h1 className="text-2xl text-forest mb-2">{t.adminBatchEditPage.title}</h1> <h1 className="text-2xl text-forest mb-2">{tb.title}</h1>
<p className="text-sm text-ink/65 mb-8"> <p className="text-sm text-ink/65 mb-8">
{batch.code} · {batch.harvest.type} {batch.harvest.year} {batch.code} · {batch.harvest.type} {batch.harvest.year}
</p> </p>
<form action={updateBatchAction} className="space-y-4 bg-background border border-ink/10 p-6">
<AdminFormPanel action={updateBatchAction} actions={<Button type="submit">{tb.save}</Button>}>
<input type="hidden" name="id" value={batch.id} /> <input type="hidden" name="id" value={batch.id} />
<div className="grid grid-cols-2 gap-4"> <AdminFieldset legend={tb.sectionDetails}>
<label className="block text-sm"> <div className="grid grid-cols-2 items-end gap-4">
<span className="text-ink/70">{t.adminBatchEditPage.fillDateLabel}</span> <AdminField
<input
name="fillDate" name="fillDate"
label={tb.fillDateLabel}
type="date" type="date"
defaultValue={batch.fillDate.toISOString().slice(0, 10)} defaultValue={batch.fillDate.toISOString().slice(0, 10)}
required required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/> />
</label> <AdminField
<label className="block text-sm">
<span className="text-ink/70">{t.adminBatchEditPage.bestBeforeDateLabel}</span>
<input
name="bestBeforeDate" name="bestBeforeDate"
label={tb.bestBeforeDateLabel}
type="date" type="date"
defaultValue={batch.bestBeforeDate.toISOString().slice(0, 10)} defaultValue={batch.bestBeforeDate.toISOString().slice(0, 10)}
required required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/> />
</label> </div>
</div> <div className="grid grid-cols-2 items-end gap-4">
<div className="grid grid-cols-2 gap-4"> <AdminField
<label className="block text-sm">
<span className="text-ink/70">{t.adminBatchEditPage.glassCountLabel}</span>
<input
name="glassCount" name="glassCount"
label={tb.glassCountLabel}
type="number" type="number"
defaultValue={batch.glassCount} min={0}
defaultValue={String(batch.glassCount)}
required required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/> />
</label> <AdminField
<label className="block text-sm">
<span className="text-ink/70">{t.adminBatchEditPage.remainingGlassesLabel}</span>
<input
name="remainingGlasses" name="remainingGlasses"
label={tb.remainingGlassesLabel}
type="number" type="number"
defaultValue={batch.remainingGlasses} min={0}
defaultValue={String(batch.remainingGlasses)}
required required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/> />
</label> </div>
</div> </AdminFieldset>
{/* Zwei einzeln setzbare Slots statt einer Liste: die Position {/* Zwei einzeln setzbare Slots statt einer Liste: die Position
entscheidet, welche Kachel auf /charge/[code] gefuellt wird. entscheidet, welche Kachel auf /charge/[code] gefuellt wird.
Beide tragen denselben Feldnamen "images" und werden in dieser Beide tragen denselben Feldnamen "images" und werden in dieser
Reihenfolge uebermittelt (siehe imageSlots() in Reihenfolge uebermittelt (siehe imageSlots() in
admin/actions/harvests-batches.ts). Ein leerer Slot faellt auf admin/actions/harvests-batches.ts). Ein leerer Slot faellt auf
das entsprechende Erntebild zurueck. */} das entsprechende Erntebild zurueck. */}
<fieldset className="border border-ink/10 p-4"> <AdminFieldset legend={tb.imagesLabel} last>
<legend className="px-1 text-sm text-ink/70">{t.adminBatchEditPage.imagesLabel}</legend> <AdminImageField name="images" label={tb.image1Label} initialValue={batch.images[0] ?? ""} />
<div className="space-y-4"> <AdminImageField name="images" label={tb.image2Label} initialValue={batch.images[1] ?? ""} />
<AdminImageField </AdminFieldset>
name="images" </AdminFormPanel>
label={t.adminBatchEditPage.image1Label}
initialValue={batch.images[0] ?? ""}
/>
<AdminImageField
name="images"
label={t.adminBatchEditPage.image2Label}
initialValue={batch.images[1] ?? ""}
/>
</div>
</fieldset>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminBatchEditPage.save}
</button>
</form>
</div> </div>
); );
} }
+47 -76
View File
@@ -2,13 +2,23 @@ import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { asStringArray } from "@/lib/json-array"; import { asStringArray } from "@/lib/json-array";
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import { AdminImageField } from "@/components/admin-image-field"; import { AdminImageField } from "@/components/admin-image-field";
import {
AdminField,
AdminFieldset,
AdminFormPanel,
AdminHint,
AdminSwitch,
AdminTextarea,
} from "@/components/admin-form";
import { updateHarvestAction, toggleHarvestPublishedAction } from "../../actions"; import { updateHarvestAction, toggleHarvestPublishedAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernten/[id]">) { export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernten/[id]">) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const th = t.adminHarvestEditPage;
const { id } = await props.params; const { id } = await props.params;
const rawHarvest = await prisma.harvest.findUnique({ where: { id } }); const rawHarvest = await prisma.harvest.findUnique({ where: { id } });
if (!rawHarvest) notFound(); if (!rawHarvest) notFound();
@@ -17,102 +27,63 @@ export default async function AdminHarvestEditPage(props: PageProps<"/admin/ernt
const harvestDateValue = harvest.harvestDate.toISOString().slice(0, 10); const harvestDateValue = harvest.harvestDate.toISOString().slice(0, 10);
return ( return (
<div className="max-w-xl"> <div className="max-w-2xl">
<BackLink href="/admin/ernten" label={t.adminHarvestEditPage.backToHarvests} /> <BackLink href="/admin/ernten" label={th.backToHarvests} />
<h1 className="text-2xl text-forest mb-8">{t.adminHarvestEditPage.title}</h1> <h1 className="text-2xl text-forest mb-8">{th.title}</h1>
{harvest.isPublished ? null : ( {harvest.isPublished ? null : (
<div className="mb-6 bg-honey/10 border border-honey/30 px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-3 justify-between"> <div className="mb-6 flex flex-col justify-between gap-3 border border-honey/40 bg-honey/5 px-4 py-3 sm:flex-row sm:items-center">
<p className="text-sm text-honey-dark">{t.adminHarvestEditPage.draftNotice}</p> <p className="text-sm text-honey-dark">{th.draftNotice}</p>
<form action={toggleHarvestPublishedAction}> <form action={toggleHarvestPublishedAction}>
<input type="hidden" name="id" value={harvest.id} /> <input type="hidden" name="id" value={harvest.id} />
<input type="hidden" name="isPublished" value="on" /> <input type="hidden" name="isPublished" value="on" />
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm whitespace-nowrap"> <Button type="submit" className="whitespace-nowrap px-5 py-2.5">
{t.adminHarvestEditPage.publishNow} {th.publishNow}
</button> </Button>
</form> </form>
</div> </div>
)} )}
<form action={updateHarvestAction} className="space-y-4 bg-background border border-ink/10 p-6"> <AdminFormPanel action={updateHarvestAction} actions={<Button type="submit">{th.save}</Button>}>
<input type="hidden" name="id" value={harvest.id} /> <input type="hidden" name="id" value={harvest.id} />
<div className="grid grid-cols-2 gap-4"> <AdminFieldset legend={th.sectionDetails}>
<label className="block text-sm"> <div className="grid grid-cols-2 items-end gap-4">
<span className="text-ink/70">{t.adminHarvestEditPage.typeLabel}</span> <AdminField name="type" label={th.typeLabel} defaultValue={harvest.type} required />
<input <AdminField name="year" label={th.yearLabel} type="number" defaultValue={String(harvest.year)} required />
name="type" </div>
defaultValue={harvest.type} <div className="grid items-end gap-4 sm:grid-cols-2">
<AdminField
name="harvestDate"
label={th.harvestDateLabel}
type="date"
defaultValue={harvestDateValue}
required required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/> />
</label> <AdminField name="location" label={th.locationLabel} defaultValue={harvest.location} />
<label className="block text-sm"> </div>
<span className="text-ink/70">{t.adminHarvestEditPage.yearLabel}</span> <AdminTextarea
<input
name="year"
type="number"
defaultValue={harvest.year}
required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
</div>
<label className="block text-sm">
<span className="text-ink/70">{t.adminHarvestEditPage.harvestDateLabel}</span>
<input
name="harvestDate"
type="date"
defaultValue={harvestDateValue}
required
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
<label className="block text-sm">
<span className="text-ink/70">{t.adminHarvestEditPage.locationLabel}</span>
<input
name="location"
defaultValue={harvest.location}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
<label className="block text-sm">
<span className="text-ink/70">{t.adminHarvestEditPage.descriptionLabel}</span>
<textarea
name="description" name="description"
label={th.descriptionLabel}
defaultValue={harvest.description} defaultValue={harvest.description}
required
rows={4} rows={4}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2" required
/> />
</label> </AdminFieldset>
{/* Siehe Kommentar im Chargen-Formular: zwei benannte Slots statt {/* Siehe Kommentar im Chargen-Formular: zwei benannte Slots statt
einer Liste, damit sich das zweite Bild auch ohne erstes setzen einer Liste, damit sich das zweite Bild auch ohne erstes setzen
laesst. Slot 1 erscheint zusaetzlich auf /ernten und der laesst. Slot 1 erscheint zusaetzlich auf /ernten und der
Ernten-Detailseite. */} Ernten-Detailseite. */}
<fieldset className="border border-ink/10 p-4"> <AdminFieldset legend={th.imagesLabel}>
<legend className="px-1 text-sm text-ink/70">{t.adminHarvestEditPage.imagesLabel}</legend> <AdminImageField name="images" label={th.image1Label} initialValue={harvest.images[0] ?? ""} />
<div className="space-y-4"> <AdminImageField name="images" label={th.image2Label} initialValue={harvest.images[1] ?? ""} />
<AdminImageField </AdminFieldset>
name="images"
label={t.adminHarvestEditPage.image1Label} <AdminFieldset legend={th.sectionVisibility} last>
initialValue={harvest.images[0] ?? ""} <AdminSwitch name="isPublished" label={th.isPublishedLabel} defaultChecked={harvest.isPublished} />
/> <AdminHint>{th.isPublishedHint}</AdminHint>
<AdminImageField </AdminFieldset>
name="images" </AdminFormPanel>
label={t.adminHarvestEditPage.image2Label}
initialValue={harvest.images[1] ?? ""}
/>
</div>
</fieldset>
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isPublished" defaultChecked={harvest.isPublished} />
{t.adminHarvestEditPage.isPublishedLabel}
</label>
<p className="text-xs text-ink/70">{t.adminHarvestEditPage.isPublishedHint}</p>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminHarvestEditPage.save}
</button>
</form>
</div> </div>
); );
} }
+70 -91
View File
@@ -1,6 +1,14 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import {
AdminField,
AdminFieldset,
AdminFormPanel,
AdminSelect,
AdminSwitch,
} from "@/components/admin-form";
import { updateDiscountCodeAction } from "../../actions"; import { updateDiscountCodeAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
@@ -12,108 +20,79 @@ function toDateInputValue(date: Date | null): string {
export default async function AdminGutscheinEditPage(props: PageProps<"/admin/gutscheine/[id]">) { export default async function AdminGutscheinEditPage(props: PageProps<"/admin/gutscheine/[id]">) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const tv = t.adminVoucherEditPage;
const { id } = await props.params; const { id } = await props.params;
const code = await prisma.discountCode.findUnique({ where: { id } }); const code = await prisma.discountCode.findUnique({ where: { id } });
if (!code) notFound(); if (!code) notFound();
return ( return (
<div className="max-w-md"> <div className="max-w-2xl">
<BackLink href="/admin/gutscheine" label={t.adminVoucherEditPage.backToVouchers} /> <BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
<h1 className="text-2xl text-forest mb-8">{t.adminVoucherEditPage.title}</h1> <h1 className="text-2xl text-forest mb-2">{tv.title}</h1>
<p className="text-sm text-ink/65 mb-8">{tv.redeemedSoFar(code.usedCount, code.maxUses)}</p>
<AdminFormPanel action={updateDiscountCodeAction} actions={<Button type="submit">{tv.save}</Button>}>
<form action={updateDiscountCodeAction} className="space-y-4 bg-background border border-ink/10 p-6">
<input type="hidden" name="id" value={code.id} /> <input type="hidden" name="id" value={code.id} />
<Field name="code" label={t.adminVoucherEditPage.codeLabel} defaultValue={code.code} required /> <AdminFieldset legend={tv.sectionDiscount}>
<AdminField name="code" label={tv.codeLabel} defaultValue={code.code} required />
<div className="grid grid-cols-2 items-end gap-4">
<AdminSelect name="type" label={tv.typeLabel} defaultValue={code.type}>
<option value="PERCENT">{tv.typePercent}</option>
<option value="FIXED">{tv.typeFixed}</option>
</AdminSelect>
<AdminField
name="value"
label={tv.valueLabel}
type="number"
min={0}
defaultValue={String(code.value)}
required
/>
</div>
</AdminFieldset>
<div className="grid grid-cols-2 gap-4"> <AdminFieldset legend={tv.sectionConditions}>
<label className="block text-sm"> <AdminField
<span className="text-ink/70">{t.adminVoucherEditPage.typeLabel}</span> name="minOrderCents"
<select label={tv.minOrderLabel}
name="type" type="number"
defaultValue={code.type} min={0}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2" defaultValue={String(code.minOrderCents)}
>
<option value="PERCENT">{t.adminVoucherEditPage.typePercent}</option>
<option value="FIXED">{t.adminVoucherEditPage.typeFixed}</option>
</select>
</label>
<Field name="value" label={t.adminVoucherEditPage.valueLabel} type="number" defaultValue={String(code.value)} required />
</div>
<Field
name="minOrderCents"
label={t.adminVoucherEditPage.minOrderLabel}
type="number"
defaultValue={String(code.minOrderCents)}
/>
<Field
name="minQuantity"
label={t.adminVoucherEditPage.minQuantityLabel}
type="number"
defaultValue={code.minQuantity !== null ? String(code.minQuantity) : ""}
/>
<Field
name="maxUses"
label={t.adminVoucherEditPage.maxUsesLabel}
type="number"
defaultValue={code.maxUses !== null ? String(code.maxUses) : ""}
/>
<div className="grid grid-cols-2 gap-4">
<Field
name="validFrom"
label={t.adminVoucherEditPage.validFromLabel}
type="date"
defaultValue={toDateInputValue(code.validFrom)}
/> />
<Field <AdminField
name="validUntil" name="minQuantity"
label={t.adminVoucherEditPage.validUntilLabel} label={tv.minQuantityLabel}
type="date" type="number"
defaultValue={toDateInputValue(code.validUntil)} min={0}
defaultValue={code.minQuantity !== null ? String(code.minQuantity) : ""}
/> />
</div> <AdminField
name="maxUses"
label={tv.maxUsesLabel}
type="number"
min={0}
defaultValue={code.maxUses !== null ? String(code.maxUses) : ""}
/>
</AdminFieldset>
<label className="flex items-center gap-2 text-sm text-ink/70"> <AdminFieldset legend={tv.sectionValidity} last>
<input type="checkbox" name="isActive" defaultChecked={code.isActive} /> <div className="grid grid-cols-2 items-end gap-4">
{t.adminVoucherEditPage.activeLabel} <AdminField
</label> name="validFrom"
label={tv.validFromLabel}
<p className="text-xs text-ink/65">{t.adminVoucherEditPage.redeemedSoFar(code.usedCount, code.maxUses)}</p> type="date"
defaultValue={toDateInputValue(code.validFrom)}
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm"> />
{t.adminVoucherEditPage.save} <AdminField
</button> name="validUntil"
</form> label={tv.validUntilLabel}
type="date"
defaultValue={toDateInputValue(code.validUntil)}
/>
</div>
<AdminSwitch name="isActive" label={tv.activeLabel} defaultChecked={code.isActive} />
</AdminFieldset>
</AdminFormPanel>
</div> </div>
); );
} }
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}
min={0}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
+36 -62
View File
@@ -1,77 +1,51 @@
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import {
AdminField,
AdminFieldset,
AdminFormPanel,
AdminSelect,
AdminSwitch,
} from "@/components/admin-form";
import { createDiscountCodeAction } from "../../actions"; import { createDiscountCodeAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminGutscheinNewPage() { export default async function AdminGutscheinNewPage() {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const tv = t.adminVoucherNewPage;
return ( return (
<div className="max-w-md"> <div className="max-w-2xl">
<BackLink href="/admin/gutscheine" label={t.adminVoucherNewPage.backToVouchers} /> <BackLink href="/admin/gutscheine" label={tv.backToVouchers} />
<h1 className="text-2xl text-forest mb-8">{t.adminVoucherNewPage.title}</h1> <h1 className="text-2xl text-forest mb-8">{tv.title}</h1>
<AdminFormPanel action={createDiscountCodeAction} actions={<Button type="submit">{tv.create}</Button>}>
<AdminFieldset legend={tv.sectionDiscount}>
<AdminField name="code" label={tv.codeLabel} placeholder={tv.codePlaceholder} required />
<div className="grid grid-cols-2 items-end gap-4">
<AdminSelect name="type" label={tv.typeLabel} defaultValue="PERCENT">
<option value="PERCENT">{tv.typePercent}</option>
<option value="FIXED">{tv.typeFixed}</option>
</AdminSelect>
<AdminField name="value" label={tv.valueLabel} type="number" min={0} placeholder="10" required />
</div>
</AdminFieldset>
<form action={createDiscountCodeAction} className="space-y-4 bg-background border border-ink/10 p-6"> <AdminFieldset legend={tv.sectionConditions}>
<Field name="code" label={t.adminVoucherNewPage.codeLabel} placeholder={t.adminVoucherNewPage.codePlaceholder} required /> <AdminField name="minOrderCents" label={tv.minOrderLabel} type="number" min={0} placeholder="0" />
<AdminField name="minQuantity" label={tv.minQuantityLabel} type="number" min={0} placeholder="3" />
<AdminField name="maxUses" label={tv.maxUsesLabel} type="number" min={0} />
</AdminFieldset>
<div className="grid grid-cols-2 gap-4"> <AdminFieldset legend={tv.sectionValidity} last>
<label className="block text-sm"> <div className="grid grid-cols-2 items-end gap-4">
<span className="text-ink/70">{t.adminVoucherNewPage.typeLabel}</span> <AdminField name="validFrom" label={tv.validFromLabel} type="date" />
<select name="type" className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"> <AdminField name="validUntil" label={tv.validUntilLabel} type="date" />
<option value="PERCENT">{t.adminVoucherNewPage.typePercent}</option> </div>
<option value="FIXED">{t.adminVoucherNewPage.typeFixed}</option> <AdminSwitch name="isActive" label={tv.activeLabel} defaultChecked />
</select> </AdminFieldset>
</label> </AdminFormPanel>
<Field name="value" label={t.adminVoucherNewPage.valueLabel} type="number" placeholder="10" required />
</div>
<Field name="minOrderCents" label={t.adminVoucherNewPage.minOrderLabel} type="number" placeholder="0" />
<Field name="minQuantity" label={t.adminVoucherNewPage.minQuantityLabel} type="number" placeholder="3" />
<Field name="maxUses" label={t.adminVoucherNewPage.maxUsesLabel} type="number" />
<div className="grid grid-cols-2 gap-4">
<Field name="validFrom" label={t.adminVoucherNewPage.validFromLabel} type="date" />
<Field name="validUntil" label={t.adminVoucherNewPage.validUntilLabel} type="date" />
</div>
<label className="flex items-center gap-2 text-sm text-ink/70">
<input type="checkbox" name="isActive" defaultChecked />
{t.adminVoucherNewPage.activeLabel}
</label>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminVoucherNewPage.create}
</button>
</form>
</div> </div>
); );
} }
function Field({
name,
label,
type = "text",
placeholder,
required = false,
}: {
name: string;
label: string;
type?: string;
placeholder?: string;
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
type={type}
placeholder={placeholder}
required={required}
min={0}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
+31 -55
View File
@@ -1,78 +1,54 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import { AdminField, AdminFormPanel } from "@/components/admin-form";
import { updateShippingTierAction } from "../../actions"; import { updateShippingTierAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminVersandEditPage(props: PageProps<"/admin/versand/[id]">) { export default async function AdminVersandEditPage(props: PageProps<"/admin/versand/[id]">) {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const ts = t.adminShippingEditPage;
const { id } = await props.params; const { id } = await props.params;
const tier = await prisma.shippingTier.findUnique({ where: { id } }); const tier = await prisma.shippingTier.findUnique({ where: { id } });
if (!tier) notFound(); if (!tier) notFound();
return ( return (
<div className="max-w-md"> <div className="max-w-xl">
<BackLink href="/admin/versand" label={t.adminShippingEditPage.backToShipping} /> <BackLink href="/admin/versand" label={ts.backToShipping} />
<h1 className="text-2xl text-forest mb-8">{t.adminShippingEditPage.title}</h1> <h1 className="text-2xl text-forest mb-8">{ts.title}</h1>
<AdminFormPanel action={updateShippingTierAction} actions={<Button type="submit">{ts.save}</Button>}>
<form action={updateShippingTierAction} className="space-y-4 bg-background border border-ink/10 p-6">
<input type="hidden" name="id" value={tier.id} /> <input type="hidden" name="id" value={tier.id} />
<div className="grid grid-cols-2 gap-4"> <div className="space-y-4">
<Field <div className="grid grid-cols-2 items-end gap-4">
name="minQuantity" <AdminField
label={t.adminShippingEditPage.minQuantityLabel} name="minQuantity"
label={ts.minQuantityLabel}
type="number"
min={0}
defaultValue={String(tier.minQuantity)}
required
/>
<AdminField
name="maxQuantity"
label={ts.maxQuantityLabel}
type="number"
min={0}
defaultValue={tier.maxQuantity !== null ? String(tier.maxQuantity) : ""}
/>
</div>
<AdminField
name="priceCents"
label={ts.priceLabel}
type="number" type="number"
defaultValue={String(tier.minQuantity)} min={0}
defaultValue={String(tier.priceCents)}
required required
/> />
<Field
name="maxQuantity"
label={t.adminShippingEditPage.maxQuantityLabel}
type="number"
defaultValue={tier.maxQuantity !== null ? String(tier.maxQuantity) : ""}
/>
</div> </div>
<Field </AdminFormPanel>
name="priceCents"
label={t.adminShippingEditPage.priceLabel}
type="number"
defaultValue={String(tier.priceCents)}
required
/>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminShippingEditPage.save}
</button>
</form>
</div> </div>
); );
} }
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}
min={0}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
+14 -41
View File
@@ -1,55 +1,28 @@
import { BackLink } from "@/components/back-link"; import { BackLink } from "@/components/back-link";
import { Button } from "@/components/ui";
import { AdminField, AdminFormPanel } from "@/components/admin-form";
import { createShippingTierAction } from "../../actions"; import { createShippingTierAction } from "../../actions";
import { getLocale } from "@/i18n/locale"; import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary"; import { getDictionary } from "@/i18n/get-dictionary";
export default async function AdminVersandNewPage() { export default async function AdminVersandNewPage() {
const t = getDictionary(await getLocale()); const t = getDictionary(await getLocale());
const ts = t.adminShippingNewPage;
return ( return (
<div className="max-w-md"> <div className="max-w-xl">
<BackLink href="/admin/versand" label={t.adminShippingNewPage.backToShipping} /> <BackLink href="/admin/versand" label={ts.backToShipping} />
<h1 className="text-2xl text-forest mb-8">{t.adminShippingNewPage.title}</h1> <h1 className="text-2xl text-forest mb-8">{ts.title}</h1>
<AdminFormPanel action={createShippingTierAction} actions={<Button type="submit">{ts.create}</Button>}>
<form action={createShippingTierAction} className="space-y-4 bg-background border border-ink/10 p-6"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 items-end gap-4">
<Field name="minQuantity" label={t.adminShippingNewPage.minQuantityLabel} type="number" required placeholder="0" /> <AdminField name="minQuantity" label={ts.minQuantityLabel} type="number" min={0} required placeholder="0" />
<Field name="maxQuantity" label={t.adminShippingNewPage.maxQuantityLabel} type="number" placeholder="3" /> <AdminField name="maxQuantity" label={ts.maxQuantityLabel} type="number" min={0} placeholder="3" />
</div>
<AdminField name="priceCents" label={ts.priceLabel} type="number" min={0} required placeholder="700" />
</div> </div>
<Field name="priceCents" label={t.adminShippingNewPage.priceLabel} type="number" required placeholder="700" /> </AdminFormPanel>
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
{t.adminShippingNewPage.create}
</button>
</form>
</div> </div>
); );
} }
function Field({
name,
label,
type = "text",
placeholder,
required = false,
}: {
name: string;
label: string;
type?: string;
placeholder?: string;
required?: boolean;
}) {
return (
<label className="block text-sm">
<span className="text-ink/70">{label}</span>
<input
name={name}
type={type}
placeholder={placeholder}
required={required}
min={0}
className="mt-1 w-full border border-ink/20 bg-background px-3 py-2"
/>
</label>
);
}
+10
View File
@@ -13,6 +13,8 @@ export function AdminField({
defaultValue, defaultValue,
placeholder, placeholder,
required = false, required = false,
min,
hint,
}: { }: {
name: string; name: string;
label: string; label: string;
@@ -20,6 +22,9 @@ export function AdminField({
defaultValue?: string; defaultValue?: string;
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
min?: number;
/** Kurzer Zusatz unter dem Feld, statt ihn in die Beschriftung zu packen. */
hint?: string;
}) { }) {
return ( return (
<label className="block text-sm"> <label className="block text-sm">
@@ -30,8 +35,10 @@ export function AdminField({
defaultValue={defaultValue} defaultValue={defaultValue}
placeholder={placeholder} placeholder={placeholder}
required={required} required={required}
min={min}
className={inputClass} className={inputClass}
/> />
{hint ? <span className="mt-1 block text-xs text-ink/65">{hint}</span> : null}
</label> </label>
); );
} }
@@ -61,11 +68,13 @@ export function AdminSelect({
name, name,
label, label,
defaultValue, defaultValue,
hint,
children, children,
}: { }: {
name: string; name: string;
label: string; label: string;
defaultValue?: string; defaultValue?: string;
hint?: string;
children: ReactNode; children: ReactNode;
}) { }) {
return ( return (
@@ -74,6 +83,7 @@ export function AdminSelect({
<select name={name} defaultValue={defaultValue} className={inputClass}> <select name={name} defaultValue={defaultValue} className={inputClass}>
{children} {children}
</select> </select>
{hint ? <span className="mt-1 block text-xs text-ink/65">{hint}</span> : null}
</label> </label>
); );
} }
+11
View File
@@ -725,6 +725,9 @@ export const de: Dictionary = {
noVouchersYet: "Noch keine Gutscheine angelegt.", noVouchersYet: "Noch keine Gutscheine angelegt.",
}, },
adminVoucherEditPage: { adminVoucherEditPage: {
sectionDiscount: "Code und Rabatt",
sectionConditions: "Bedingungen",
sectionValidity: "Zeitraum und Status",
backToVouchers: "Zurück zu Gutscheinen", backToVouchers: "Zurück zu Gutscheinen",
title: "Gutschein bearbeiten", title: "Gutschein bearbeiten",
codeLabel: "Code", codeLabel: "Code",
@@ -742,6 +745,9 @@ export const de: Dictionary = {
save: "Speichern", save: "Speichern",
}, },
adminVoucherNewPage: { adminVoucherNewPage: {
sectionDiscount: "Code und Rabatt",
sectionConditions: "Bedingungen",
sectionValidity: "Zeitraum und Status",
backToVouchers: "Zurück zu Gutscheinen", backToVouchers: "Zurück zu Gutscheinen",
title: "Neuer Gutschein", title: "Neuer Gutschein",
codeLabel: "Code", codeLabel: "Code",
@@ -818,6 +824,8 @@ export const de: Dictionary = {
noUsersYet: "Noch keine Benutzer angelegt.", noUsersYet: "Noch keine Benutzer angelegt.",
}, },
adminUserEditPage: { adminUserEditPage: {
sectionProfile: "Stammdaten",
sectionDanger: "Konto sperren oder löschen",
backToUsers: "Zurück zu Benutzern", backToUsers: "Zurück zu Benutzern",
createdOn: (date) => `Konto erstellt am ${date}`, createdOn: (date) => `Konto erstellt am ${date}`,
firstNameLabel: "Vorname", firstNameLabel: "Vorname",
@@ -881,6 +889,7 @@ export const de: Dictionary = {
create: "Anlegen", create: "Anlegen",
}, },
adminBatchEditPage: { adminBatchEditPage: {
sectionDetails: "Abfüllung und Bestand",
backToBatches: "Zurück zu Chargen", backToBatches: "Zurück zu Chargen",
title: "Charge bearbeiten", title: "Charge bearbeiten",
fillDateLabel: "Abfülldatum", fillDateLabel: "Abfülldatum",
@@ -988,6 +997,8 @@ export const de: Dictionary = {
create: "Anlegen", create: "Anlegen",
}, },
adminHarvestEditPage: { adminHarvestEditPage: {
sectionDetails: "Angaben",
sectionVisibility: "Sichtbarkeit",
backToHarvests: "Zurück zu Ernten", backToHarvests: "Zurück zu Ernten",
title: "Ernte bearbeiten", title: "Ernte bearbeiten",
typeLabel: "Typ", typeLabel: "Typ",
+11
View File
@@ -713,6 +713,9 @@ export const en: Dictionary = {
noVouchersYet: "No vouchers created yet.", noVouchersYet: "No vouchers created yet.",
}, },
adminVoucherEditPage: { adminVoucherEditPage: {
sectionDiscount: "Code and discount",
sectionConditions: "Conditions",
sectionValidity: "Period and status",
backToVouchers: "Back to vouchers", backToVouchers: "Back to vouchers",
title: "Edit voucher", title: "Edit voucher",
codeLabel: "Code", codeLabel: "Code",
@@ -730,6 +733,9 @@ export const en: Dictionary = {
save: "Save", save: "Save",
}, },
adminVoucherNewPage: { adminVoucherNewPage: {
sectionDiscount: "Code and discount",
sectionConditions: "Conditions",
sectionValidity: "Period and status",
backToVouchers: "Back to vouchers", backToVouchers: "Back to vouchers",
title: "New voucher", title: "New voucher",
codeLabel: "Code", codeLabel: "Code",
@@ -806,6 +812,8 @@ export const en: Dictionary = {
noUsersYet: "No users created yet.", noUsersYet: "No users created yet.",
}, },
adminUserEditPage: { adminUserEditPage: {
sectionProfile: "Profile",
sectionDanger: "Block or delete account",
backToUsers: "Back to users", backToUsers: "Back to users",
createdOn: (date) => `Account created on ${date}`, createdOn: (date) => `Account created on ${date}`,
firstNameLabel: "First name", firstNameLabel: "First name",
@@ -869,6 +877,7 @@ export const en: Dictionary = {
create: "Create", create: "Create",
}, },
adminBatchEditPage: { adminBatchEditPage: {
sectionDetails: "Filling and stock",
backToBatches: "Back to batches", backToBatches: "Back to batches",
title: "Edit batch", title: "Edit batch",
fillDateLabel: "Fill date", fillDateLabel: "Fill date",
@@ -975,6 +984,8 @@ export const en: Dictionary = {
create: "Create", create: "Create",
}, },
adminHarvestEditPage: { adminHarvestEditPage: {
sectionDetails: "Details",
sectionVisibility: "Visibility",
backToHarvests: "Back to harvests", backToHarvests: "Back to harvests",
title: "Edit harvest", title: "Edit harvest",
typeLabel: "Type", typeLabel: "Type",
+11
View File
@@ -404,6 +404,9 @@ export interface Dictionary {
noVouchersYet: string; noVouchersYet: string;
}; };
adminVoucherEditPage: { adminVoucherEditPage: {
sectionDiscount: string;
sectionConditions: string;
sectionValidity: string;
backToVouchers: string; backToVouchers: string;
title: string; title: string;
codeLabel: string; codeLabel: string;
@@ -421,6 +424,9 @@ export interface Dictionary {
save: string; save: string;
}; };
adminVoucherNewPage: { adminVoucherNewPage: {
sectionDiscount: string;
sectionConditions: string;
sectionValidity: string;
backToVouchers: string; backToVouchers: string;
title: string; title: string;
codeLabel: string; codeLabel: string;
@@ -496,6 +502,8 @@ export interface Dictionary {
noUsersYet: string; noUsersYet: string;
}; };
adminUserEditPage: { adminUserEditPage: {
sectionProfile: string;
sectionDanger: string;
backToUsers: string; backToUsers: string;
createdOn: (date: string) => string; createdOn: (date: string) => string;
firstNameLabel: string; firstNameLabel: string;
@@ -558,6 +566,7 @@ export interface Dictionary {
create: string; create: string;
}; };
adminBatchEditPage: { adminBatchEditPage: {
sectionDetails: string;
backToBatches: string; backToBatches: string;
title: string; title: string;
fillDateLabel: string; fillDateLabel: string;
@@ -663,6 +672,8 @@ export interface Dictionary {
/** created, skipped, failed */ /** created, skipped, failed */
}; };
adminHarvestEditPage: { adminHarvestEditPage: {
sectionDetails: string;
sectionVisibility: string;
backToHarvests: string; backToHarvests: string;
title: string; title: string;
typeLabel: string; typeLabel: string;