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
|
|
@ -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