Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 2m27s
Neue deleteHarvestAction/deleteBatchAction, jeweils mit vorheriger Referenzpruefung statt rohem Datenbankfehler: eine Ernte laesst sich nur loeschen, wenn keine Chargen/Produkte mehr darauf verweisen, eine Charge nur, wenn sie keinem Produkt zugeordnet ist und in keiner Bestellung verwendet wurde (Chargen-Code steht auf Rechnungen/QR-Codes und darf aus Nachweispflicht nie verschwinden). Loeschen-Button erscheint in den Listen nur, wenn die Loeschung tatsaechlich moeglich ist, mit Bestaetigungsdialog wie beim bestehenden Benutzer-Loeschen. Co-Authored-By: Claude Sonnet 5 <[email protected]>
188 lines
8.1 KiB
TypeScript
188 lines
8.1 KiB
TypeScript
import Link from "next/link";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { BackLink } from "@/components/back-link";
|
|
import { ConfirmForm } from "@/components/confirm-form";
|
|
import { formatDate } from "@/lib/format";
|
|
import {
|
|
createHarvestAction,
|
|
toggleHarvestPublishedAction,
|
|
syncHarvestsFromApiCoreAction,
|
|
deleteHarvestAction,
|
|
} from "../actions";
|
|
import { getLocale } from "@/i18n/locale";
|
|
import { getDictionary } from "@/i18n/get-dictionary";
|
|
|
|
export default async function AdminHarvestsPage(props: PageProps<"/admin/ernten">) {
|
|
const t = getDictionary(await getLocale());
|
|
const { synced, updated, skipped, failed, syncError, batchSynced, batchSkipped, batchFailed, batchLoadError, error } =
|
|
await props.searchParams;
|
|
const harvests = await prisma.harvest.findMany({
|
|
orderBy: [{ year: "desc" }, { harvestDate: "desc" }],
|
|
include: { _count: { select: { batches: true, products: true } } },
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<BackLink href="/admin" label={t.adminHarvestsPage.backToDashboard} />
|
|
<h1 className="text-2xl text-forest mb-8">{t.adminHarvestsPage.title}</h1>
|
|
|
|
{error ? (
|
|
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">{error}</p>
|
|
) : null}
|
|
{synced !== undefined ? (
|
|
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">
|
|
{t.adminHarvestsPage.syncResult(Number(synced), Number(updated ?? 0), Number(skipped ?? 0), Number(failed ?? 0))}
|
|
</p>
|
|
) : null}
|
|
{syncError !== undefined ? (
|
|
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
|
|
{t.adminHarvestsPage.syncError} ({syncError})
|
|
</p>
|
|
) : null}
|
|
{batchSynced !== undefined ? (
|
|
<p className="mb-6 text-sm text-forest bg-forest/5 border border-forest/20 px-4 py-3">
|
|
{t.adminHarvestsPage.batchSyncResult(Number(batchSynced), Number(batchSkipped ?? 0), Number(batchFailed ?? 0))}
|
|
</p>
|
|
) : null}
|
|
{batchLoadError !== undefined ? (
|
|
<p className="mb-6 text-sm text-red-700 bg-red-50 border border-red-200 px-4 py-3">
|
|
{t.adminHarvestsPage.batchSyncError} ({batchLoadError})
|
|
</p>
|
|
) : null}
|
|
|
|
<div className="overflow-x-auto bg-background border border-ink/10">
|
|
<table className="w-full text-sm">
|
|
<thead className="text-left text-ink/65 border-b border-ink/10">
|
|
<tr>
|
|
<th className="p-4">{t.adminHarvestsPage.colType}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colYear}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colHarvestDate}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colLocation}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colBatches}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colProducts}</th>
|
|
<th className="p-4">{t.adminHarvestsPage.colStatus}</th>
|
|
<th className="p-4" />
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-ink/10">
|
|
{harvests.map((harvest) => (
|
|
<tr key={harvest.id} className="transition-colors hover:bg-ink/[0.03]">
|
|
<td className="p-4 text-forest">{harvest.type}</td>
|
|
<td className="p-4">{harvest.year}</td>
|
|
<td className="p-4">{formatDate(harvest.harvestDate)}</td>
|
|
<td className="p-4">{harvest.location}</td>
|
|
<td className="p-4">{harvest._count.batches}</td>
|
|
<td className="p-4">{harvest._count.products}</td>
|
|
<td className="p-4">
|
|
<span
|
|
className={
|
|
harvest.isPublished
|
|
? "text-forest bg-forest/10 px-2 py-1 text-xs"
|
|
: "text-honey-dark bg-honey/10 px-2 py-1 text-xs"
|
|
}
|
|
>
|
|
{harvest.isPublished ? t.adminHarvestsPage.statusPublished : t.adminHarvestsPage.statusDraft}
|
|
</span>
|
|
</td>
|
|
<td className="p-4 whitespace-nowrap">
|
|
<Link href={`/admin/ernten/${harvest.id}`} className="text-forest underline mr-4">
|
|
{t.adminHarvestsPage.edit}
|
|
</Link>
|
|
<form action={toggleHarvestPublishedAction} className="inline">
|
|
<input type="hidden" name="id" value={harvest.id} />
|
|
{harvest.isPublished ? null : <input type="hidden" name="isPublished" value="on" />}
|
|
<button type="submit" className="text-forest underline mr-4">
|
|
{harvest.isPublished ? t.adminHarvestsPage.unpublish : t.adminHarvestsPage.publish}
|
|
</button>
|
|
</form>
|
|
{harvest._count.batches === 0 && harvest._count.products === 0 ? (
|
|
<ConfirmForm
|
|
action={deleteHarvestAction}
|
|
confirmMessage={t.adminHarvestsPage.deleteConfirm(`${harvest.type} ${harvest.year}`)}
|
|
className="inline"
|
|
>
|
|
<input type="hidden" name="id" value={harvest.id} />
|
|
<button type="submit" className="text-red-700 underline">
|
|
{t.adminHarvestsPage.delete}
|
|
</button>
|
|
</ConfirmForm>
|
|
) : null}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{harvests.length === 0 ? <p className="p-6 text-ink/65">{t.adminHarvestsPage.noHarvestsYet}</p> : null}
|
|
</div>
|
|
|
|
<div className="mt-10 grid md:grid-cols-2 gap-6">
|
|
<div className="bg-background border border-ink/10 p-6">
|
|
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.newHarvestHeading}</h2>
|
|
<form action={createHarvestAction} className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field name="type" label={t.adminHarvestsPage.typeLabel} required />
|
|
<Field name="year" label={t.adminHarvestsPage.yearLabel} type="number" required />
|
|
</div>
|
|
<Field name="harvestDate" label={t.adminHarvestsPage.harvestDateLabel} type="date" required />
|
|
<Field name="location" label={t.adminHarvestsPage.locationLabel} defaultValue="Altenmünster" />
|
|
<TextArea name="description" label={t.adminHarvestsPage.descriptionLabel} required />
|
|
<label className="flex items-center gap-2 text-sm text-ink/70">
|
|
<input type="checkbox" name="isPublished" defaultChecked />
|
|
{t.adminHarvestsPage.isPublishedLabel}
|
|
</label>
|
|
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
|
{t.adminHarvestsPage.create}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="bg-background border border-ink/10 p-6">
|
|
<h2 className="text-lg text-forest mb-4">{t.adminHarvestsPage.syncHeading}</h2>
|
|
<p className="text-sm text-ink/65 mb-4">{t.adminHarvestsPage.syncIntro}</p>
|
|
<form action={syncHarvestsFromApiCoreAction}>
|
|
<button type="submit" className="bg-forest text-cream px-6 py-2 text-sm">
|
|
{t.adminHarvestsPage.syncButton}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</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>
|
|
);
|
|
}
|
|
|
|
function TextArea({ name, label, required = false }: { name: string; label: string; required?: boolean }) {
|
|
return (
|
|
<label className="block text-sm">
|
|
<span className="text-ink/70">{label}</span>
|
|
<textarea name={name} required={required} rows={3} className="mt-1 w-full border border-ink/20 bg-background px-3 py-2" />
|
|
</label>
|
|
);
|
|
}
|