fix(knowledge): improve upload workflow and seed manufacturers
This commit is contained in:
parent
964b545bc5
commit
612e1ad8ed
13 changed files with 429 additions and 20 deletions
|
|
@ -6,6 +6,7 @@ import { Edit, Eye, Plus, Trash2 } from "lucide-react";
|
|||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
||||
import EmptyState from "@/components/common/EmptyState";
|
||||
import SearchInput from "@/components/common/SearchInput";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import { DeviceFormDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
|
|
@ -139,7 +140,27 @@ export default function KnowledgeDevicesPage() {
|
|||
{manufacturers.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Geräte" emptyDescription="Lege zunächst Hersteller und Geräte an." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
{!loading && !error && manufacturers.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Hersteller vorhanden"
|
||||
description="Bitte lege zuerst einen Hersteller an. Geräte müssen immer einem Hersteller zugeordnet werden."
|
||||
action={<Link href="/knowledge/manufacturers" className={buttonVariants({ variant: "default" })}><Plus size={16} />Hersteller anlegen</Link>}
|
||||
/>
|
||||
) : !loading && !error && devices.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Gerät vorhanden"
|
||||
description="Lege ein Gerät an, damit Dokumente wie Schaltpläne oder Service Manuals sauber zugeordnet werden können."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Gerät anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && rows.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Keine Geräte gefunden"
|
||||
description="Für deine Suche oder den ausgewählten Hersteller gibt es keinen Treffer."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Gerät anlegen</Button>}
|
||||
/>
|
||||
) : (
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Geräte" emptyDescription="Lege zunächst Hersteller und Geräte an." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
)}
|
||||
<DeviceFormDialog open={formOpen} device={selected} manufacturers={manufacturers} pending={pending} error={formError} onOpenChange={setFormOpen} onSubmit={save} />
|
||||
<ConfirmDialog open={Boolean(deleteItem)} title="Gerät löschen" description="Dieses Gerät wird gelöscht, sofern keine abhängigen Einträge blockieren." pending={pending} onOpenChange={(open) => !open && setDeleteItem(null)} onConfirm={confirmDelete}>
|
||||
{deleteItem && <p className="text-sm font-medium">{deleteItem.name}</p>}
|
||||
|
|
|
|||
|
|
@ -6,14 +6,15 @@ import { Download, Edit, Eye, Plus, Trash2 } from "lucide-react";
|
|||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
||||
import EmptyState from "@/components/common/EmptyState";
|
||||
import SearchInput from "@/components/common/SearchInput";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import DocumentTypeBadge from "@/components/knowledge/DocumentTypeBadge";
|
||||
import { DocumentUploadDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
import { DeviceFormDialog, DocumentUploadDialog, ManufacturerFormDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
import TagList from "@/components/knowledge/TagList";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { api } from "@/lib/api";
|
||||
import type { DocumentPayload, DocumentType, KnowledgeDevice, KnowledgeDocument, KnowledgeManufacturer } from "@/types/knowledge";
|
||||
import type { DevicePayload, DocumentPayload, DocumentType, KnowledgeDevice, KnowledgeDocument, KnowledgeManufacturer, ManufacturerPayload } from "@/types/knowledge";
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
if (typeof error === "object" && error !== null && "response" in error) {
|
||||
|
|
@ -43,6 +44,9 @@ export default function KnowledgeDocumentsPage() {
|
|||
const [sortKey, setSortKey] = useState("created_at");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [manufacturerFormOpen, setManufacturerFormOpen] = useState(false);
|
||||
const [deviceFormOpen, setDeviceFormOpen] = useState(false);
|
||||
const [deviceInitialManufacturerId, setDeviceInitialManufacturerId] = useState<number | null>(null);
|
||||
const [selected, setSelected] = useState<KnowledgeDocument | null>(null);
|
||||
const [deleteItem, setDeleteItem] = useState<KnowledgeDocument | null>(null);
|
||||
|
||||
|
|
@ -82,6 +86,17 @@ export default function KnowledgeDocumentsPage() {
|
|||
return sortDirection === "asc" ? result : -result;
|
||||
}), [documents, manufacturerFilter, search, sortDirection, sortKey, typeFilter]);
|
||||
|
||||
const selectedManufacturerId = manufacturerFilter === "all"
|
||||
? devices[0]?.manufacturer_id ?? manufacturers[0]?.id ?? null
|
||||
: Number(manufacturerFilter);
|
||||
const selectedManufacturer = manufacturers.find((manufacturer) => manufacturer.id === selectedManufacturerId) ?? null;
|
||||
const selectedManufacturerDevices = selectedManufacturerId
|
||||
? devices.filter((device) => device.manufacturer_id === selectedManufacturerId)
|
||||
: [];
|
||||
const canUpload = manufacturers.length > 0
|
||||
&& devices.length > 0
|
||||
&& (manufacturerFilter === "all" || selectedManufacturerDevices.length > 0);
|
||||
|
||||
const columns: DataTableColumn<KnowledgeDocument>[] = [
|
||||
{ key: "title", label: "Titel", sortable: true, render: (item) => <span className="font-medium">{item.title}</span> },
|
||||
{ key: "manufacturer", label: "Hersteller", sortable: true, render: (item) => item.manufacturer.name },
|
||||
|
|
@ -106,6 +121,13 @@ export default function KnowledgeDocumentsPage() {
|
|||
];
|
||||
|
||||
async function save(payload: DocumentPayload, file: File | null) {
|
||||
if (!payload.manufacturer_id || !payload.device_id || (!selected && !file)) {
|
||||
const message = "Die Datei konnte nicht hochgeladen werden, weil die Zuordnung unvollständig ist.";
|
||||
setFormError(message);
|
||||
showToast({ type: "error", title: "Dokument kann nicht gespeichert werden", description: message });
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
setFormError("");
|
||||
try {
|
||||
|
|
@ -134,6 +156,49 @@ export default function KnowledgeDocumentsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveManufacturer(payload: ManufacturerPayload) {
|
||||
setPending(true);
|
||||
setFormError("");
|
||||
try {
|
||||
const response = await api.post<KnowledgeManufacturer>("/knowledge/manufacturers", payload);
|
||||
showToast({ type: "success", title: "Hersteller erstellt", description: response.data.name });
|
||||
setManufacturerFilter(String(response.data.id));
|
||||
setDeviceInitialManufacturerId(response.data.id);
|
||||
setManufacturerFormOpen(false);
|
||||
await loadItems();
|
||||
} catch (err) {
|
||||
const message = errorMessage(err);
|
||||
setFormError(message);
|
||||
showToast({ type: "error", title: "Hersteller konnte nicht gespeichert werden", description: message });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDevice(payload: DevicePayload) {
|
||||
setPending(true);
|
||||
setFormError("");
|
||||
try {
|
||||
const response = await api.post<KnowledgeDevice>("/knowledge/devices", payload);
|
||||
showToast({ type: "success", title: "Gerät erstellt", description: response.data.name });
|
||||
setManufacturerFilter(String(response.data.manufacturer_id));
|
||||
setDeviceFormOpen(false);
|
||||
await loadItems();
|
||||
} catch (err) {
|
||||
const message = errorMessage(err);
|
||||
setFormError(message);
|
||||
showToast({ type: "error", title: "Gerät konnte nicht gespeichert werden", description: message });
|
||||
} finally {
|
||||
setPending(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openDeviceCreate(manufacturerId: number | null = selectedManufacturerId) {
|
||||
setDeviceInitialManufacturerId(manufacturerId);
|
||||
setFormError("");
|
||||
setDeviceFormOpen(true);
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!deleteItem) return;
|
||||
setPending(true);
|
||||
|
|
@ -153,7 +218,7 @@ export default function KnowledgeDocumentsPage() {
|
|||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div><h1 className="text-3xl font-bold text-slate-950">Dokumente</h1><p className="mt-1 text-sm text-slate-500">{rows.length} Unterlagen</p></div>
|
||||
<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }} disabled={manufacturers.length === 0}><Plus size={16} />Upload</Button>
|
||||
<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }} disabled={!canUpload}><Plus size={16} />Upload</Button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 rounded-lg border bg-white p-4 lg:flex-row">
|
||||
<SearchInput value={search} onChange={setSearch} placeholder="Dokumente suchen" />
|
||||
|
|
@ -174,9 +239,54 @@ export default function KnowledgeDocumentsPage() {
|
|||
<option value="other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Dokumente" emptyDescription="Lade das erste Service-Dokument hoch." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
<DocumentUploadDialog open={formOpen} document={selected} manufacturers={manufacturers} devices={devices} pending={pending} error={formError} onOpenChange={setFormOpen} onSubmit={save} />
|
||||
<ConfirmDialog open={Boolean(deleteItem)} title="Dokument löschen" description="Der Datenbankeintrag wird entfernt. Lokal gespeicherte Dateien bleiben für Nachvollziehbarkeit erhalten." pending={pending} onOpenChange={(open) => !open && setDeleteItem(null)} onConfirm={confirmDelete}>
|
||||
{!loading && !error && manufacturers.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Hersteller vorhanden"
|
||||
description="Bitte lege zuerst einen Hersteller an. Danach kannst du Geräte und Dokumente wie Schaltpläne oder Service Manuals erfassen."
|
||||
action={<Button onClick={() => { setFormError(""); setManufacturerFormOpen(true); }}><Plus size={16} />Hersteller anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && devices.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Gerät vorhanden"
|
||||
description="Dokumente wie Schaltpläne oder Service Manuals müssen einem Gerät zugeordnet werden."
|
||||
action={<Button onClick={() => openDeviceCreate()}><Plus size={16} />Gerät anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && selectedManufacturer && manufacturerFilter !== "all" && selectedManufacturerDevices.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Für diesen Hersteller existiert noch kein Gerät"
|
||||
description="Lege zuerst ein Gerät für diesen Hersteller an. Danach kannst du Dokumente diesem Gerät zuordnen."
|
||||
action={<Button onClick={() => openDeviceCreate(selectedManufacturer.id)}><Plus size={16} />Gerät für diesen Hersteller anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && documents.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Dokument vorhanden"
|
||||
description="Lade das erste Dokument hoch, sobald Hersteller und Gerät angelegt sind."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }} disabled={!canUpload}><Plus size={16} />Dokument hochladen</Button>}
|
||||
/>
|
||||
) : !loading && !error && rows.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Keine Dokumente gefunden"
|
||||
description="Für Suche oder Filter gibt es keinen Treffer. Passe die Filter an oder lade ein neues Dokument hoch."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }} disabled={!canUpload}><Plus size={16} />Dokument hochladen</Button>}
|
||||
/>
|
||||
) : (
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Dokumente" emptyDescription="Lade das erste Service-Dokument hoch." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
)}
|
||||
<DocumentUploadDialog
|
||||
open={formOpen}
|
||||
document={selected}
|
||||
manufacturers={manufacturers}
|
||||
devices={devices}
|
||||
initialManufacturerId={selectedManufacturerId}
|
||||
pending={pending}
|
||||
error={formError}
|
||||
onOpenChange={setFormOpen}
|
||||
onSubmit={save}
|
||||
onValidationError={(message) => showToast({ type: "error", title: "Upload unvollständig", description: message })}
|
||||
/>
|
||||
<ManufacturerFormDialog open={manufacturerFormOpen} manufacturer={null} pending={pending} error={formError} onOpenChange={setManufacturerFormOpen} onSubmit={saveManufacturer} />
|
||||
<DeviceFormDialog open={deviceFormOpen} device={null} manufacturers={manufacturers} initialManufacturerId={deviceInitialManufacturerId} pending={pending} error={formError} onOpenChange={setDeviceFormOpen} onSubmit={saveDevice} />
|
||||
<ConfirmDialog open={Boolean(deleteItem)} title="Dokument löschen" description="Der Datenbankeintrag und die gespeicherte Datei werden entfernt." pending={pending} onOpenChange={(open) => !open && setDeleteItem(null)} onConfirm={confirmDelete}>
|
||||
{deleteItem && <p className="text-sm font-medium">{deleteItem.title}</p>}
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Edit, Plus, Trash2 } from "lucide-react";
|
|||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
||||
import EmptyState from "@/components/common/EmptyState";
|
||||
import SearchInput from "@/components/common/SearchInput";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import { ManufacturerFormDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
|
|
@ -122,7 +123,21 @@ export default function KnowledgeManufacturersPage() {
|
|||
<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Hersteller</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-white p-4"><SearchInput value={search} onChange={setSearch} placeholder="Hersteller suchen" /></div>
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Hersteller" emptyDescription="Lege den ersten Hersteller an." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
{!loading && !error && items.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch kein Hersteller vorhanden"
|
||||
description="Bitte lege zuerst einen Hersteller an. Danach kannst du Geräte und Dokumente wie Schaltpläne oder Service Manuals erfassen."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Hersteller anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && rows.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Keine Hersteller gefunden"
|
||||
description="Für deine Suche gibt es keinen Treffer. Passe den Suchbegriff an oder lege einen neuen Hersteller an."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Hersteller anlegen</Button>}
|
||||
/>
|
||||
) : (
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Hersteller" emptyDescription="Lege den ersten Hersteller an." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
)}
|
||||
<ManufacturerFormDialog open={formOpen} manufacturer={selected} pending={pending} error={formError} onOpenChange={setFormOpen} onSubmit={save} />
|
||||
<ConfirmDialog open={Boolean(deleteItem)} title="Hersteller löschen" description="Dieser Hersteller wird gelöscht, sofern keine abhängigen Dokumente bestehen." pending={pending} onOpenChange={(open) => !open && setDeleteItem(null)} onConfirm={confirmDelete}>
|
||||
{deleteItem && <p className="text-sm font-medium">{deleteItem.name}</p>}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Edit, Plus, Trash2 } from "lucide-react";
|
|||
|
||||
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
||||
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
||||
import EmptyState from "@/components/common/EmptyState";
|
||||
import SearchInput from "@/components/common/SearchInput";
|
||||
import { useToast } from "@/components/common/ToastProvider";
|
||||
import { KnowledgeNoteFormDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
|
|
@ -142,7 +143,21 @@ export default function KnowledgeNotesPage() {
|
|||
<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Notiz</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-white p-4"><SearchInput value={search} onChange={setSearch} placeholder="Notizen suchen" /></div>
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Notizen" emptyDescription="Erstelle den ersten Reparaturhinweis." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
{!loading && !error && notes.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Es ist noch keine Notiz vorhanden"
|
||||
description="Erfasse Reparaturhinweise, bekannte Fehler oder Abgleichnotizen, sobald erstes Wissen dokumentiert werden soll."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Notiz anlegen</Button>}
|
||||
/>
|
||||
) : !loading && !error && rows.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Keine Notizen gefunden"
|
||||
description="Für deine Suche gibt es keinen Treffer. Passe den Suchbegriff an oder erstelle eine neue Notiz."
|
||||
action={<Button onClick={() => { setSelected(null); setFormError(""); setFormOpen(true); }}><Plus size={16} />Notiz anlegen</Button>}
|
||||
/>
|
||||
) : (
|
||||
<DataTable columns={columns} rows={rows} rowKey={(item) => item.id} sortKey={sortKey} sortDirection={sortDirection} loading={loading} error={error} emptyTitle="Keine Notizen" emptyDescription="Erstelle den ersten Reparaturhinweis." onSort={(key) => { setSortKey(key); setSortDirection((current) => sortKey === key && current === "asc" ? "desc" : "asc"); }} />
|
||||
)}
|
||||
<KnowledgeNoteFormDialog open={formOpen} note={selected} manufacturers={manufacturers} devices={devices} pending={pending} error={formError} onOpenChange={setFormOpen} onSubmit={save} />
|
||||
<ConfirmDialog open={Boolean(deleteItem)} title="Notiz löschen" description="Diese Wissensnotiz wird dauerhaft gelöscht." pending={pending} onOpenChange={(open) => !open && setDeleteItem(null)} onConfirm={confirmDelete}>
|
||||
{deleteItem && <p className="text-sm font-medium">{deleteItem.title}</p>}
|
||||
|
|
|
|||
19
frontend/athena/components/common/EmptyState.tsx
Normal file
19
frontend/athena/components/common/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description: string;
|
||||
action?: ReactNode;
|
||||
};
|
||||
|
||||
export default function EmptyState({ title, description, action }: Props) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-8 text-center">
|
||||
<h2 className="text-lg font-semibold text-slate-950">{title}</h2>
|
||||
<p className="mx-auto mt-2 max-w-xl text-sm text-slate-500">{description}</p>
|
||||
{action && <div className="mt-5 flex justify-center">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -101,6 +101,7 @@ export function DeviceFormDialog({
|
|||
open,
|
||||
device,
|
||||
manufacturers,
|
||||
initialManufacturerId,
|
||||
pending,
|
||||
error,
|
||||
onOpenChange,
|
||||
|
|
@ -109,6 +110,7 @@ export function DeviceFormDialog({
|
|||
open: boolean;
|
||||
device: KnowledgeDevice | null;
|
||||
manufacturers: KnowledgeManufacturer[];
|
||||
initialManufacturerId?: number | null;
|
||||
pending: boolean;
|
||||
error: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
|
|
@ -137,7 +139,7 @@ export function DeviceFormDialog({
|
|||
production_year_to: device.production_year_to,
|
||||
notes: device.notes,
|
||||
} : {
|
||||
manufacturer_id: manufacturers[0]?.id ?? 0,
|
||||
manufacturer_id: initialManufacturerId ?? manufacturers[0]?.id ?? 0,
|
||||
name: "",
|
||||
model_number: "",
|
||||
device_type: "",
|
||||
|
|
@ -147,7 +149,7 @@ export function DeviceFormDialog({
|
|||
notes: "",
|
||||
});
|
||||
});
|
||||
}, [device, manufacturers, open]);
|
||||
}, [device, initialManufacturerId, manufacturers, open]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
|
|
@ -178,19 +180,23 @@ export function DocumentUploadDialog({
|
|||
document,
|
||||
manufacturers,
|
||||
devices,
|
||||
initialManufacturerId,
|
||||
pending,
|
||||
error,
|
||||
onOpenChange,
|
||||
onSubmit,
|
||||
onValidationError,
|
||||
}: {
|
||||
open: boolean;
|
||||
document: KnowledgeDocument | null;
|
||||
manufacturers: KnowledgeManufacturer[];
|
||||
devices: KnowledgeDevice[];
|
||||
initialManufacturerId?: number | null;
|
||||
pending: boolean;
|
||||
error: string;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSubmit: (payload: DocumentPayload, file: File | null) => void;
|
||||
onValidationError?: (message: string) => void;
|
||||
}) {
|
||||
const [payload, setPayload] = useState<DocumentPayload>({
|
||||
manufacturer_id: 0,
|
||||
|
|
@ -205,6 +211,7 @@ export function DocumentUploadDialog({
|
|||
});
|
||||
const [tags, setTags] = useState("");
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
|
|
@ -219,7 +226,7 @@ export function DocumentUploadDialog({
|
|||
description: document.description,
|
||||
tags: document.tags,
|
||||
} : {
|
||||
manufacturer_id: manufacturers[0]?.id ?? 0,
|
||||
manufacturer_id: initialManufacturerId ?? manufacturers[0]?.id ?? 0,
|
||||
device_id: null,
|
||||
title: "",
|
||||
document_type: "service_manual",
|
||||
|
|
@ -231,31 +238,92 @@ export function DocumentUploadDialog({
|
|||
});
|
||||
setTags(document ? tagsToText(document.tags) : "");
|
||||
setFile(null);
|
||||
setFieldErrors({});
|
||||
});
|
||||
}, [document, manufacturers, open]);
|
||||
}, [document, initialManufacturerId, manufacturers, open]);
|
||||
|
||||
const filteredDevices = devices.filter((device) => device.manufacturer_id === payload.manufacturer_id);
|
||||
const selectedManufacturer = manufacturers.find((manufacturer) => manufacturer.id === payload.manufacturer_id);
|
||||
|
||||
function validate() {
|
||||
const errors: Record<string, string> = {};
|
||||
if (!payload.manufacturer_id) {
|
||||
errors.manufacturer_id = "Bitte wählen Sie einen Hersteller aus.";
|
||||
}
|
||||
if (!payload.device_id) {
|
||||
errors.device_id = "Bitte wählen Sie ein Gerät aus.";
|
||||
}
|
||||
if (!payload.document_type) {
|
||||
errors.document_type = "Bitte wählen Sie einen Dokumenttyp aus.";
|
||||
}
|
||||
if (!payload.title.trim()) {
|
||||
errors.title = "Bitte geben Sie einen Titel ein.";
|
||||
}
|
||||
if (!document && !file) {
|
||||
errors.file = "Bitte wählen Sie eine Datei aus.";
|
||||
}
|
||||
|
||||
setFieldErrors(errors);
|
||||
const firstMessage = Object.values(errors)[0];
|
||||
if (firstMessage) {
|
||||
onValidationError?.(firstMessage);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
onSubmit({ ...payload, tags: textToTags(tags) }, file);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader><DialogTitle>{document ? "Dokument bearbeiten" : "Dokument hochladen"}</DialogTitle></DialogHeader>
|
||||
<p className="text-sm text-slate-500">
|
||||
Dokumente werden immer einem Gerät zugeordnet. Lege daher zuerst Hersteller und Gerät an.
|
||||
</p>
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className={fieldClass()}><Label>Hersteller</Label><select className="h-9 rounded-lg border px-2 text-sm" value={payload.manufacturer_id} onChange={(event) => setPayload({ ...payload, manufacturer_id: Number(event.target.value), device_id: null })}>{manufacturers.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}</select></div>
|
||||
<div className={fieldClass()}><Label>Gerät</Label><select className="h-9 rounded-lg border px-2 text-sm" value={payload.device_id ?? ""} onChange={(event) => setPayload({ ...payload, device_id: event.target.value ? Number(event.target.value) : null })}><option value="">Kein Gerät</option>{filteredDevices.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}</select></div>
|
||||
<div className={fieldClass()}><Label>Titel</Label><Input value={payload.title} onChange={(event) => setPayload({ ...payload, title: event.target.value })} /></div>
|
||||
<div className={fieldClass()}><Label>Typ</Label><select className="h-9 rounded-lg border px-2 text-sm" value={payload.document_type} onChange={(event) => setPayload({ ...payload, document_type: event.target.value as DocumentType })}>{documentTypes.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select></div>
|
||||
<div className={fieldClass()}>
|
||||
<Label>Hersteller</Label>
|
||||
<select className="h-9 rounded-lg border px-2 text-sm" value={payload.manufacturer_id} onChange={(event) => setPayload({ ...payload, manufacturer_id: Number(event.target.value), device_id: null })}>
|
||||
<option value={0}>Hersteller auswählen</option>
|
||||
{manufacturers.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
{fieldErrors.manufacturer_id && <p className="text-xs text-red-600">{fieldErrors.manufacturer_id}</p>}
|
||||
</div>
|
||||
<div className={fieldClass()}>
|
||||
<Label>Gerät</Label>
|
||||
<select className="h-9 rounded-lg border px-2 text-sm" value={payload.device_id ?? ""} onChange={(event) => setPayload({ ...payload, device_id: event.target.value ? Number(event.target.value) : null })} disabled={!payload.manufacturer_id || filteredDevices.length === 0}>
|
||||
<option value="">Gerät auswählen</option>
|
||||
{filteredDevices.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||
</select>
|
||||
{payload.manufacturer_id && filteredDevices.length === 0 && (
|
||||
<p className="text-xs text-amber-700">Für diesen Hersteller existiert noch kein Gerät.</p>
|
||||
)}
|
||||
{fieldErrors.device_id && <p className="text-xs text-red-600">{fieldErrors.device_id}</p>}
|
||||
</div>
|
||||
<div className={fieldClass()}><Label>Titel</Label><Input value={payload.title} onChange={(event) => setPayload({ ...payload, title: event.target.value })} />{fieldErrors.title && <p className="text-xs text-red-600">{fieldErrors.title}</p>}</div>
|
||||
<div className={fieldClass()}><Label>Typ</Label><select className="h-9 rounded-lg border px-2 text-sm" value={payload.document_type} onChange={(event) => setPayload({ ...payload, document_type: event.target.value as DocumentType })}>{documentTypes.map(([value, label]) => <option key={value} value={value}>{label}</option>)}</select>{fieldErrors.document_type && <p className="text-xs text-red-600">{fieldErrors.document_type}</p>}</div>
|
||||
<div className={fieldClass()}><Label>Sprache</Label><Input value={payload.language} onChange={(event) => setPayload({ ...payload, language: event.target.value })} /></div>
|
||||
<div className={fieldClass()}><Label>Paperless-ID</Label><Input value={payload.paperless_document_id} onChange={(event) => setPayload({ ...payload, paperless_document_id: event.target.value })} /></div>
|
||||
<div className={fieldClass()}><Label>Externe URL</Label><Input value={payload.external_url ?? ""} onChange={(event) => setPayload({ ...payload, external_url: event.target.value || null })} /></div>
|
||||
{!document && <div className={fieldClass()}><Label>Datei</Label><input type="file" className="h-9 rounded-lg border px-2 py-1 text-sm" onChange={(event) => setFile(event.target.files?.[0] ?? null)} /></div>}
|
||||
{!document && <div className={fieldClass()}><Label>Datei</Label><input type="file" className="h-9 rounded-lg border px-2 py-1 text-sm" onChange={(event) => setFile(event.target.files?.[0] ?? null)} />{fieldErrors.file && <p className="text-xs text-red-600">{fieldErrors.file}</p>}</div>}
|
||||
<div className={fieldClass()}><Label>Tags</Label><Input value={tags} onChange={(event) => setTags(event.target.value)} placeholder="cb-funk, schaltplan" /></div>
|
||||
<div className="grid gap-2 sm:col-span-2"><Label>Beschreibung</Label><textarea className="min-h-24 rounded-lg border border-input p-2 text-sm" value={payload.description} onChange={(event) => setPayload({ ...payload, description: event.target.value })} /></div>
|
||||
{selectedManufacturer && filteredDevices.length === 0 && (
|
||||
<p className="text-sm text-amber-700 sm:col-span-2">
|
||||
Für {selectedManufacturer.name} existiert noch kein Gerät. Dokumente wie Schaltpläne oder Service Manuals müssen einem Gerät zugeordnet werden.
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="text-sm text-red-600 sm:col-span-2">{error}</p>}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={pending}>Abbrechen</Button>
|
||||
<Button onClick={() => onSubmit({ ...payload, tags: textToTags(tags) }, file)} disabled={pending || !payload.manufacturer_id || (!document && !file)}>Speichern</Button>
|
||||
<Button onClick={submit} disabled={pending}>Speichern</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue