152 lines
7.2 KiB
TypeScript
152 lines
7.2 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import { Edit, Plus, Trash2 } from "lucide-react";
|
|
|
|
import ConfirmDialog from "@/components/common/ConfirmDialog";
|
|
import DataTable, { type DataTableColumn } from "@/components/common/DataTable";
|
|
import SearchInput from "@/components/common/SearchInput";
|
|
import { useToast } from "@/components/common/ToastProvider";
|
|
import { KnowledgeNoteFormDialog } from "@/components/knowledge/KnowledgeForms";
|
|
import TagList from "@/components/knowledge/TagList";
|
|
import { Button } from "@/components/ui/button";
|
|
import { api } from "@/lib/api";
|
|
import type { KnowledgeDevice, KnowledgeManufacturer, KnowledgeNote, NotePayload } from "@/types/knowledge";
|
|
|
|
function errorMessage(error: unknown) {
|
|
if (typeof error === "object" && error !== null && "response" in error) {
|
|
return (error as { response?: { data?: { detail?: string } } }).response?.data?.detail ?? "Aktion fehlgeschlagen";
|
|
}
|
|
return "Aktion fehlgeschlagen";
|
|
}
|
|
|
|
const noteTypeLabels: Record<string, string> = {
|
|
repair: "Reparaturhinweis",
|
|
known_fault: "Bekannter Fehler",
|
|
alignment: "Abgleichhinweis",
|
|
spare_part: "Ersatzteilhinweis",
|
|
general: "Allgemein",
|
|
};
|
|
|
|
export default function KnowledgeNotesPage() {
|
|
const { showToast } = useToast();
|
|
const [notes, setNotes] = useState<KnowledgeNote[]>([]);
|
|
const [manufacturers, setManufacturers] = useState<KnowledgeManufacturer[]>([]);
|
|
const [devices, setDevices] = useState<KnowledgeDevice[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState("");
|
|
const [formError, setFormError] = useState("");
|
|
const [pending, setPending] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
const [sortKey, setSortKey] = useState("created_at");
|
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
|
|
const [formOpen, setFormOpen] = useState(false);
|
|
const [selected, setSelected] = useState<KnowledgeNote | null>(null);
|
|
const [deleteItem, setDeleteItem] = useState<KnowledgeNote | null>(null);
|
|
|
|
const loadItems = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [manufacturerResponse, deviceResponse, noteResponse] = await Promise.all([
|
|
api.get<KnowledgeManufacturer[]>("/knowledge/manufacturers?limit=100"),
|
|
api.get<KnowledgeDevice[]>("/knowledge/devices?limit=100"),
|
|
api.get<KnowledgeNote[]>("/knowledge/notes?limit=100"),
|
|
]);
|
|
setManufacturers(manufacturerResponse.data);
|
|
setDevices(deviceResponse.data);
|
|
setNotes(noteResponse.data);
|
|
setError("");
|
|
} catch (err) {
|
|
setError(errorMessage(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
queueMicrotask(() => {
|
|
void loadItems();
|
|
});
|
|
}, [loadItems]);
|
|
|
|
const rows = useMemo(() => notes
|
|
.filter((item) => [item.title, item.content, item.device?.name ?? "", item.manufacturer?.name ?? "", ...item.tags].some((value) => value.toLowerCase().includes(search.toLowerCase())))
|
|
.sort((a, b) => {
|
|
const av = String(a[sortKey as keyof KnowledgeNote] ?? "");
|
|
const bv = String(b[sortKey as keyof KnowledgeNote] ?? "");
|
|
const result = av.toLowerCase() > bv.toLowerCase() ? 1 : av.toLowerCase() < bv.toLowerCase() ? -1 : 0;
|
|
return sortDirection === "asc" ? result : -result;
|
|
}), [notes, search, sortDirection, sortKey]);
|
|
|
|
const columns: DataTableColumn<KnowledgeNote>[] = [
|
|
{ key: "title", label: "Titel", sortable: true, render: (item) => <span className="font-medium">{item.title}</span> },
|
|
{ key: "note_type", label: "Typ", sortable: true, render: (item) => noteTypeLabels[item.note_type] ?? item.note_type },
|
|
{ key: "manufacturer", label: "Hersteller", render: (item) => item.manufacturer?.name ?? "-" },
|
|
{ key: "device", label: "Gerät", render: (item) => item.device?.name ?? "-" },
|
|
{ key: "severity", label: "Schweregrad", sortable: true, render: (item) => item.severity || "-" },
|
|
{ key: "tags", label: "Tags", render: (item) => <TagList tags={item.tags} /> },
|
|
{
|
|
key: "actions",
|
|
label: "Aktionen",
|
|
className: "px-4 py-3 text-right",
|
|
render: (item) => (
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="ghost" size="icon-sm" title="Bearbeiten" onClick={() => { setSelected(item); setFormError(""); setFormOpen(true); }}><Edit size={16} /></Button>
|
|
<Button variant="destructive" size="icon-sm" title="Löschen" onClick={() => setDeleteItem(item)}><Trash2 size={16} /></Button>
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
async function save(payload: NotePayload) {
|
|
setPending(true);
|
|
setFormError("");
|
|
try {
|
|
if (selected) {
|
|
await api.put(`/knowledge/notes/${selected.id}`, payload);
|
|
showToast({ type: "success", title: "Notiz aktualisiert", description: payload.title });
|
|
} else {
|
|
await api.post("/knowledge/notes", payload);
|
|
showToast({ type: "success", title: "Notiz erstellt", description: payload.title });
|
|
}
|
|
setFormOpen(false);
|
|
await loadItems();
|
|
} catch (err) {
|
|
const message = errorMessage(err);
|
|
setFormError(message);
|
|
showToast({ type: "error", title: "Notiz konnte nicht gespeichert werden", description: message });
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
async function confirmDelete() {
|
|
if (!deleteItem) return;
|
|
setPending(true);
|
|
try {
|
|
await api.delete(`/knowledge/notes/${deleteItem.id}`);
|
|
showToast({ type: "success", title: "Notiz gelöscht", description: deleteItem.title });
|
|
setDeleteItem(null);
|
|
await loadItems();
|
|
} catch (err) {
|
|
showToast({ type: "error", title: "Notiz konnte nicht gelöscht werden", description: errorMessage(err) });
|
|
} finally {
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<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">Knowledge Notes</h1><p className="mt-1 text-sm text-slate-500">{rows.length} Reparatur- und Servicehinweise</p></div>
|
|
<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"); }} />
|
|
<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>}
|
|
</ConfirmDialog>
|
|
</div>
|
|
);
|
|
}
|