Prevents "M.forEach is not a function" crashes when API returns error objects or unexpected shapes instead of arrays. Guards all useQuery consumers with Array.isArray checks and safe defaults for object props. Files fixed: DeadlineList, AppointmentList, TenantSwitcher, DeadlineTrafficLights, UpcomingTimeline, CaseOverviewGrid, AISummaryCard, TeamSettings, and all page-level components (dashboard, cases, fristen, termine, ai/extract).
143 lines
4.1 KiB
TypeScript
143 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { Brain } from "lucide-react";
|
|
import { api } from "@/lib/api";
|
|
import type {
|
|
Case,
|
|
ExtractedDeadline,
|
|
ExtractionResponse,
|
|
PaginatedResponse,
|
|
} from "@/lib/types";
|
|
import { ExtractionForm } from "@/components/ai/ExtractionForm";
|
|
import { ExtractionResults } from "@/components/ai/ExtractionResults";
|
|
|
|
export default function AIExtractPage() {
|
|
const router = useRouter();
|
|
const [selectedCaseId, setSelectedCaseId] = useState("");
|
|
const [isExtracting, setIsExtracting] = useState(false);
|
|
const [isAdopting, setIsAdopting] = useState(false);
|
|
const [results, setResults] = useState<ExtractedDeadline[] | null>(null);
|
|
|
|
const { data: casesData } = useQuery({
|
|
queryKey: ["cases"],
|
|
queryFn: () => api.get<PaginatedResponse<Case>>("/cases"),
|
|
});
|
|
|
|
const cases = Array.isArray(casesData?.data) ? casesData.data : [];
|
|
|
|
async function handleExtract(file: File | null, text: string) {
|
|
setIsExtracting(true);
|
|
setResults(null);
|
|
|
|
try {
|
|
let response: ExtractionResponse;
|
|
|
|
if (file) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
response = await api.postFormData<ExtractionResponse>(
|
|
"/ai/extract-deadlines",
|
|
formData,
|
|
);
|
|
} else {
|
|
response = await api.post<ExtractionResponse>(
|
|
"/ai/extract-deadlines",
|
|
{ text },
|
|
);
|
|
}
|
|
|
|
setResults(response.deadlines);
|
|
|
|
if (response.count === 0) {
|
|
toast.info("Keine Fristen im Dokument gefunden.");
|
|
} else {
|
|
toast.success(`${response.count} Frist(en) erkannt.`);
|
|
}
|
|
} catch (err: unknown) {
|
|
const message =
|
|
err && typeof err === "object" && "error" in err
|
|
? (err as { error: string }).error
|
|
: "Analyse fehlgeschlagen";
|
|
toast.error(message);
|
|
} finally {
|
|
setIsExtracting(false);
|
|
}
|
|
}
|
|
|
|
async function handleAdopt(deadlines: ExtractedDeadline[]) {
|
|
if (!selectedCaseId) return;
|
|
setIsAdopting(true);
|
|
|
|
try {
|
|
const promises = deadlines.map((d) =>
|
|
api.post(`/cases/${selectedCaseId}/deadlines`, {
|
|
title: d.title,
|
|
due_date: d.due_date ?? "",
|
|
source: "ai_extraction",
|
|
notes: [
|
|
d.rule_reference ? `Rechtsgrundlage: ${d.rule_reference}` : "",
|
|
d.source_quote ? `Quelle: "${d.source_quote}"` : "",
|
|
`Konfidenz: ${Math.round(d.confidence * 100)}%`,
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
}),
|
|
);
|
|
|
|
await Promise.all(promises);
|
|
toast.success(
|
|
`${deadlines.length} Frist(en) erfolgreich übernommen.`,
|
|
);
|
|
router.push(`/cases/${selectedCaseId}`);
|
|
} catch (err: unknown) {
|
|
const message =
|
|
err && typeof err === "object" && "error" in err
|
|
? (err as { error: string }).error
|
|
: "Übernahme fehlgeschlagen";
|
|
toast.error(message);
|
|
} finally {
|
|
setIsAdopting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="animate-fade-in mx-auto max-w-4xl">
|
|
<div className="mb-6 flex items-center gap-3">
|
|
<Brain className="h-5 w-5 text-neutral-500" />
|
|
<div>
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
AI Fristenanalyse
|
|
</h1>
|
|
<p className="text-sm text-neutral-500">
|
|
Fristen automatisch aus Dokumenten extrahieren
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-4 sm:p-6">
|
|
<ExtractionForm
|
|
cases={cases}
|
|
selectedCaseId={selectedCaseId}
|
|
onCaseChange={setSelectedCaseId}
|
|
onExtract={handleExtract}
|
|
isLoading={isExtracting}
|
|
/>
|
|
</div>
|
|
|
|
{results !== null && (
|
|
<div className="animate-fade-in mt-6 rounded-lg border border-neutral-200 bg-white p-4 sm:p-6">
|
|
<ExtractionResults
|
|
deadlines={results}
|
|
onAdopt={handleAdopt}
|
|
isAdopting={isAdopting}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|