Listen mit Suche/Filter/Paginierung, Popups für Anlage und Bearbeitung, Kundendetail mit Tabs, Dublettenhinweis und Zusammenführen, Objektdetail mit Kartenlink, Dokumenten-Tab und Historie, Teamverwaltung mit Mitgliedern, Dokumentenübersicht. Texte in messages de/en, Audit-Label Ansprechpartner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { getTranslations } from "next-intl/server";
|
|
import { controlClass, Field, primaryButtonClass } from "@/components/customers/form-ui";
|
|
|
|
/**
|
|
* Plain multipart form (works without JavaScript) posting to /documents/upload, which redirects
|
|
* back to `returnTo` with ?docOk=1 or ?docError=<reason> (see UploadFeedback).
|
|
*/
|
|
export async function DocumentUploadForm({
|
|
returnTo,
|
|
links = {},
|
|
lineageId,
|
|
categories,
|
|
visibilities,
|
|
defaultCategory = "other",
|
|
defaultVisibility = "team",
|
|
heading,
|
|
}: {
|
|
returnTo: string;
|
|
links?: { customerId?: string; siteId?: string; workOrderId?: string };
|
|
lineageId?: string;
|
|
categories: string[];
|
|
visibilities: string[];
|
|
defaultCategory?: string;
|
|
defaultVisibility?: string;
|
|
heading?: string;
|
|
}) {
|
|
const t = await getTranslations("documents");
|
|
const idp = lineageId ? `v-${lineageId.slice(0, 6)}` : "up";
|
|
return (
|
|
<form action="/documents/upload" method="post" encType="multipart/form-data" className="grid gap-3 sm:grid-cols-2">
|
|
{heading && <p className="font-heading text-sm font-semibold sm:col-span-2">{heading}</p>}
|
|
<input type="hidden" name="returnTo" value={returnTo} />
|
|
{links.customerId && <input type="hidden" name="customerId" value={links.customerId} />}
|
|
{links.siteId && <input type="hidden" name="siteId" value={links.siteId} />}
|
|
{links.workOrderId && <input type="hidden" name="workOrderId" value={links.workOrderId} />}
|
|
{lineageId && <input type="hidden" name="lineageId" value={lineageId} />}
|
|
<Field id={`${idp}-file`} label={t("upload.file")} required hint={t("upload.fileHint")} className="sm:col-span-2">
|
|
<input
|
|
id={`${idp}-file`}
|
|
name="file"
|
|
type="file"
|
|
required
|
|
accept="application/pdf,image/jpeg,image/png,image/webp,image/heic,audio/*"
|
|
className={`${controlClass} py-2 file:mr-3 file:font-semibold`}
|
|
/>
|
|
</Field>
|
|
<Field id={`${idp}-title`} label={t("upload.titleField")} className="sm:col-span-2">
|
|
<input id={`${idp}-title`} name="title" className={controlClass} />
|
|
</Field>
|
|
<Field id={`${idp}-category`} label={t("upload.category")} required>
|
|
<select id={`${idp}-category`} name="category" defaultValue={defaultCategory} className={controlClass}>
|
|
{categories.map((c) => (
|
|
<option key={c} value={c}>
|
|
{t(`category.${c}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<Field id={`${idp}-visibility`} label={t("upload.visibility")} required>
|
|
<select id={`${idp}-visibility`} name="visibility" defaultValue={defaultVisibility} className={controlClass}>
|
|
{visibilities.map((v) => (
|
|
<option key={v} value={v}>
|
|
{t(`visibility.${v}`)}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</Field>
|
|
<div className="sm:col-span-2">
|
|
<button type="submit" className={primaryButtonClass}>
|
|
{t("upload.submit")}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
/** Result banner of the upload redirect. */
|
|
export async function UploadFeedback({ ok, error }: { ok?: string; error?: string }) {
|
|
if (!ok && !error) return null;
|
|
const t = await getTranslations("documents");
|
|
if (ok) {
|
|
return (
|
|
<p role="status" className="mb-3 rounded-lg border-l-4 border-[var(--ok)] bg-card px-3 py-2 text-[13px] font-semibold text-[var(--ok)]">
|
|
{t("upload.ok")}
|
|
</p>
|
|
);
|
|
}
|
|
const key = t.has(`uploadErrors.${error}`) ? `uploadErrors.${error}` : "uploadErrors.generic";
|
|
return (
|
|
<p role="alert" className="mb-3 rounded-lg border-l-4 border-[var(--risk)] bg-card px-3 py-2 text-[13px] font-semibold text-[var(--risk)]">
|
|
{t(key)}
|
|
</p>
|
|
);
|
|
}
|