L2 Aufträge & Backoffice: Backoffice-UI (Liste, Detail, Konflikte, Dashboard, Suche, Einstellungen) + Texte de/en
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { z } from "zod";
|
||||
import { Plus } from "lucide-react";
|
||||
import { PageHead, Pill } from "@/components/mockup-ui";
|
||||
import { Modal } from "@/components/modal";
|
||||
import { ActionForm, buttonCls } from "@/components/work-orders/action-form";
|
||||
import { SettingsTemplatesNav, templatesPageContext } from "@/components/work-orders/settings-nav";
|
||||
import { Check, Empty, Field, inputCls, Section } from "@/components/work-orders/ui";
|
||||
import { DEFAULT_CHECKLIST_ITEMS, DEFAULT_REQUIRED_PHOTOS } from "@/lib/work-orders/defaults";
|
||||
import { templateItemSchema, templatePhotoSchema, type TemplateItem, type TemplatePhoto } from "@/lib/work-orders/schemas";
|
||||
import { saveChecklistTemplateAction } from "@/server/actions/work_orders/settings";
|
||||
import { listChecklistTemplates, listOrderTypes } from "@/server/services/work-orders/settings";
|
||||
|
||||
type SP = Record<string, string | string[] | undefined>;
|
||||
const one = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : v);
|
||||
|
||||
const itemsToText = (items: readonly TemplateItem[], suffix: string) =>
|
||||
items.map((i) => `${i.required ? "* " : ""}${i.label}${i.requiresPhoto ? ` ${suffix}` : ""}`).join("\n");
|
||||
const photosToText = (photos: readonly TemplatePhoto[]) => photos.map((p) => p.label).join("\n");
|
||||
|
||||
/** Checklist templates per order type incl. required photos (spec §12.4 / §14.2). */
|
||||
export default async function ChecklistTemplatesPage({ searchParams }: { searchParams: Promise<SP> }) {
|
||||
const { ctx } = await templatesPageContext();
|
||||
const t = await getTranslations("settingsTemplates");
|
||||
const tc = await getTranslations("common");
|
||||
const sp = await searchParams;
|
||||
const [templates, orderTypes] = await Promise.all([listChecklistTemplates(ctx), listOrderTypes(ctx)]);
|
||||
|
||||
const parsed = templates.map((tpl) => ({
|
||||
...tpl,
|
||||
itemList: z.array(templateItemSchema).safeParse(tpl.items).data ?? [],
|
||||
photoList: z.array(templatePhotoSchema).safeParse(tpl.requiredPhotos).data ?? [],
|
||||
}));
|
||||
const editId = one(sp.edit);
|
||||
const editing = editId ? parsed.find((x) => x.id === editId) : undefined;
|
||||
const isNew = one(sp.new) === "1";
|
||||
const suggest = one(sp.suggest) === "1";
|
||||
const base = "/settings/checklists";
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-4 md:p-6">
|
||||
<PageHead
|
||||
crumb={t("crumb")}
|
||||
title={t("checklists.title")}
|
||||
sub={t("checklists.sub")}
|
||||
actions={
|
||||
<Link href={`${base}?new=1&suggest=1`} scroll={false} className={buttonCls("primary")}>
|
||||
<Plus className="size-4" aria-hidden />
|
||||
{t("checklists.new")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<SettingsTemplatesNav active="checklists" />
|
||||
|
||||
{parsed.length === 0 ? (
|
||||
<Empty>{t("checklists.empty")}</Empty>
|
||||
) : (
|
||||
<ul className="grid gap-3 md:grid-cols-2">
|
||||
{parsed.map((tpl) => (
|
||||
<li key={tpl.id}>
|
||||
<Section title={tpl.name} actions={!tpl.active ? <Pill tone="mut">{t("checklists.inactive")}</Pill> : undefined}>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tpl.orderType?.name ?? t("checklists.noOrderType")} · {t("checklists.itemCount", { count: tpl.itemList.length })} · {t("checklists.photoCount", { count: tpl.photoList.length })}
|
||||
</p>
|
||||
<Link href={`${base}?edit=${tpl.id}`} scroll={false} className={`${buttonCls("outline")} mt-3`}>
|
||||
{t("checklists.edit")}
|
||||
</Link>
|
||||
</Section>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{(isNew || editing) && (
|
||||
<Modal title={editing ? editing.name : t("checklists.new")} sub={t("checklists.itemsHint")} closeHref={base} closeLabel={tc("close")}>
|
||||
<ActionForm action={saveChecklistTemplateAction} namespace="settingsTemplates" submitLabel={t("checklists.save")} variant="primary" successText={t("saved")} className="grid gap-4 p-5 md:grid-cols-2">
|
||||
{editing && <input type="hidden" name="id" value={editing.id} />}
|
||||
<Field label={`${t("checklists.name")} *`} htmlFor="tpl-name">
|
||||
<input id="tpl-name" name="name" required maxLength={120} defaultValue={editing?.name ?? ""} className={inputCls} />
|
||||
</Field>
|
||||
<Field label={t("checklists.orderType")} htmlFor="tpl-type">
|
||||
<select id="tpl-type" name="orderTypeId" defaultValue={editing?.orderTypeId ?? ""} className={inputCls}>
|
||||
<option value="">{t("checklists.noOrderType")}</option>
|
||||
{orderTypes.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("checklists.items")} htmlFor="tpl-items" hint={t("checklists.itemsHint")} className="md:col-span-1">
|
||||
<textarea
|
||||
id="tpl-items"
|
||||
name="items"
|
||||
rows={10}
|
||||
defaultValue={editing ? itemsToText(editing.itemList, t("checklists.photoSuffix")) : suggest ? itemsToText(DEFAULT_CHECKLIST_ITEMS, t("checklists.photoSuffix")) : ""}
|
||||
className={`${inputCls} py-2 font-mono text-xs`}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={t("checklists.photos")} htmlFor="tpl-photos" hint={t("checklists.photosHint")}>
|
||||
<textarea
|
||||
id="tpl-photos"
|
||||
name="requiredPhotos"
|
||||
rows={10}
|
||||
defaultValue={editing ? photosToText(editing.photoList) : suggest ? photosToText(DEFAULT_REQUIRED_PHOTOS) : ""}
|
||||
className={`${inputCls} py-2 font-mono text-xs`}
|
||||
/>
|
||||
</Field>
|
||||
<Check id="tpl-active" name="active" label={t("checklists.active")} defaultChecked={editing ? editing.active : true} />
|
||||
{!editing && suggest && <p className="self-center text-xs text-muted-foreground">{t("checklists.suggestionHint")}</p>}
|
||||
{!editing && !suggest && (
|
||||
<Link href={`${base}?new=1&suggest=1`} scroll={false} className="self-center text-sm font-semibold text-[var(--primary)] hover:underline">
|
||||
{t("checklists.suggestion")}
|
||||
</Link>
|
||||
)}
|
||||
</ActionForm>
|
||||
</Modal>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user