feat(knowledge): add service library
This commit is contained in:
parent
ecab0fe6b6
commit
228da8f814
32 changed files with 2878 additions and 6 deletions
132
frontend/athena/app/knowledge/manufacturers/page.tsx
Normal file
132
frontend/athena/app/knowledge/manufacturers/page.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
"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 { ManufacturerFormDialog } from "@/components/knowledge/KnowledgeForms";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/lib/api";
|
||||
import type { KnowledgeManufacturer, ManufacturerPayload } 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";
|
||||
}
|
||||
|
||||
export default function KnowledgeManufacturersPage() {
|
||||
const { showToast } = useToast();
|
||||
const [items, setItems] = useState<KnowledgeManufacturer[]>([]);
|
||||
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("name");
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
const [selected, setSelected] = useState<KnowledgeManufacturer | null>(null);
|
||||
const [deleteItem, setDeleteItem] = useState<KnowledgeManufacturer | null>(null);
|
||||
|
||||
const loadItems = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.get<KnowledgeManufacturer[]>("/knowledge/manufacturers?limit=100");
|
||||
setItems(response.data);
|
||||
setError("");
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
queueMicrotask(() => {
|
||||
void loadItems();
|
||||
});
|
||||
}, [loadItems]);
|
||||
|
||||
const rows = useMemo(() => items
|
||||
.filter((item) => item.name.toLowerCase().includes(search.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
const av = String(a[sortKey as keyof KnowledgeManufacturer] ?? "").toLowerCase();
|
||||
const bv = String(b[sortKey as keyof KnowledgeManufacturer] ?? "").toLowerCase();
|
||||
const result = av > bv ? 1 : av < bv ? -1 : 0;
|
||||
return sortDirection === "asc" ? result : -result;
|
||||
}), [items, search, sortDirection, sortKey]);
|
||||
|
||||
const columns: DataTableColumn<KnowledgeManufacturer>[] = [
|
||||
{ key: "name", label: "Name", sortable: true, render: (item) => <span className="font-medium">{item.name}</span> },
|
||||
{ key: "website", label: "Website", sortable: true, render: (item) => item.website || "-" },
|
||||
{ key: "notes", label: "Notizen", render: (item) => item.notes || "-" },
|
||||
{
|
||||
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: ManufacturerPayload) {
|
||||
setPending(true);
|
||||
setFormError("");
|
||||
try {
|
||||
if (selected) {
|
||||
await api.put(`/knowledge/manufacturers/${selected.id}`, payload);
|
||||
showToast({ type: "success", title: "Hersteller aktualisiert", description: payload.name });
|
||||
} else {
|
||||
await api.post("/knowledge/manufacturers", payload);
|
||||
showToast({ type: "success", title: "Hersteller erstellt", description: payload.name });
|
||||
}
|
||||
setFormOpen(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 confirmDelete() {
|
||||
if (!deleteItem) return;
|
||||
setPending(true);
|
||||
try {
|
||||
await api.delete(`/knowledge/manufacturers/${deleteItem.id}`);
|
||||
showToast({ type: "success", title: "Hersteller gelöscht", description: deleteItem.name });
|
||||
setDeleteItem(null);
|
||||
await loadItems();
|
||||
} catch (err) {
|
||||
showToast({ type: "error", title: "Hersteller 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">Hersteller</h1><p className="mt-1 text-sm text-slate-500">{rows.length} Einträge</p></div>
|
||||
<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"); }} />
|
||||
<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>}
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue