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:
1 parent
a09057d960
commit
b7a75dddca
12 files changed
+364
-524
No files matched your search
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user