feat: AI features — drafting, strategy, similar cases (P2)

This commit is contained in:
m
2026-03-30 11:29:41 +02:00
14 changed files with 1648 additions and 301 deletions

View File

@@ -0,0 +1,51 @@
"use client";
import { useState } from "react";
import { useParams } from "next/navigation";
import { Brain, FileText, Search } from "lucide-react";
import { CaseStrategy } from "@/components/ai/CaseStrategy";
import { DocumentDrafter } from "@/components/ai/DocumentDrafter";
import { SimilarCaseFinder } from "@/components/ai/SimilarCaseFinder";
type AITab = "strategy" | "draft" | "similar";
const TABS: { id: AITab; label: string; icon: typeof Brain }[] = [
{ id: "strategy", label: "KI-Strategie", icon: Brain },
{ id: "draft", label: "KI-Entwurf", icon: FileText },
{ id: "similar", label: "Aehnliche Faelle", icon: Search },
];
export default function CaseAIPage() {
const { id } = useParams<{ id: string }>();
const [activeTab, setActiveTab] = useState<AITab>("strategy");
return (
<div>
{/* Sub-tabs */}
<div className="mb-6 flex gap-1 rounded-lg border border-neutral-200 bg-neutral-50 p-1">
{TABS.map((tab) => {
const isActive = activeTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`inline-flex flex-1 items-center justify-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
isActive
? "bg-white text-neutral-900 shadow-sm"
: "text-neutral-500 hover:text-neutral-700"
}`}
>
<tab.icon className="h-4 w-4" />
{tab.label}
</button>
);
})}
</div>
{/* Content */}
{activeTab === "strategy" && <CaseStrategy caseId={id} />}
{activeTab === "draft" && <DocumentDrafter caseId={id} />}
{activeTab === "similar" && <SimilarCaseFinder caseId={id} />}
</div>
);
}

View File

@@ -17,12 +17,7 @@ import {
StickyNote,
AlertTriangle,
ScrollText,
<<<<<<< HEAD
Timer,
||||||| 8e65463
=======
FilePlus,
>>>>>>> mai/ritchie/p1-document-templates
Brain,
} from "lucide-react";
import { format } from "date-fns";
import { de } from "date-fns/locale";
@@ -52,9 +47,9 @@ const TABS = [
{ segment: "dokumente", label: "Dokumente", icon: FileText },
{ segment: "parteien", label: "Parteien", icon: Users },
{ segment: "mitarbeiter", label: "Mitarbeiter", icon: UserCheck },
{ segment: "zeiterfassung", label: "Zeiterfassung", icon: Timer },
{ segment: "notizen", label: "Notizen", icon: StickyNote },
{ segment: "protokoll", label: "Protokoll", icon: ScrollText },
{ segment: "ki", label: "KI", icon: Brain },
] as const;
const TAB_LABELS: Record<string, string> = {
@@ -63,9 +58,9 @@ const TAB_LABELS: Record<string, string> = {
dokumente: "Dokumente",
parteien: "Parteien",
mitarbeiter: "Mitarbeiter",
zeiterfassung: "Zeiterfassung",
notizen: "Notizen",
protokoll: "Protokoll",
ki: "KI",
};
function CaseDetailSkeleton() {
@@ -179,28 +174,19 @@ export default function CaseDetailLayout({
{caseDetail.court_ref && <span>({caseDetail.court_ref})</span>}
</div>
</div>
<div className="flex flex-col items-end gap-2">
<Link
href={`/vorlagen?case_id=${id}`}
className="flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
>
<FilePlus className="h-3.5 w-3.5" />
Schriftsatz erstellen
</Link>
<div className="text-right text-xs text-neutral-400">
<p>
Erstellt:{" "}
{format(new Date(caseDetail.created_at), "d. MMM yyyy", {
locale: de,
})}
</p>
<p>
Aktualisiert:{" "}
{format(new Date(caseDetail.updated_at), "d. MMM yyyy", {
locale: de,
})}
</p>
</div>
<div className="text-right text-xs text-neutral-400">
<p>
Erstellt:{" "}
{format(new Date(caseDetail.created_at), "d. MMM yyyy", {
locale: de,
})}
</p>
<p>
Aktualisiert:{" "}
{format(new Date(caseDetail.updated_at), "d. MMM yyyy", {
locale: de,
})}
</p>
</div>
</div>

View File

@@ -0,0 +1,226 @@
"use client";
import { useMutation } from "@tanstack/react-query";
import { api } from "@/lib/api";
import type { StrategyRecommendation } from "@/lib/types";
import {
Loader2,
Brain,
AlertTriangle,
ArrowRight,
Shield,
Calendar,
RefreshCw,
} from "lucide-react";
interface CaseStrategyProps {
caseId: string;
}
const PRIORITY_STYLES = {
high: "bg-red-50 text-red-700 border-red-200",
medium: "bg-amber-50 text-amber-700 border-amber-200",
low: "bg-emerald-50 text-emerald-700 border-emerald-200",
} as const;
const IMPORTANCE_STYLES = {
critical: "border-l-red-500",
important: "border-l-amber-500",
routine: "border-l-neutral-300",
} as const;
export function CaseStrategy({ caseId }: CaseStrategyProps) {
const mutation = useMutation({
mutationFn: () =>
api.post<StrategyRecommendation>("/ai/case-strategy", {
case_id: caseId,
}),
});
if (!mutation.data && !mutation.isPending && !mutation.isError) {
return (
<div className="flex flex-col items-center gap-3 py-8 text-center">
<div className="rounded-xl bg-neutral-100 p-3">
<Brain className="h-6 w-6 text-neutral-400" />
</div>
<div>
<p className="text-sm font-medium text-neutral-900">
KI-Strategieanalyse
</p>
<p className="mt-1 text-sm text-neutral-500">
Claude analysiert die Akte und gibt strategische Empfehlungen.
</p>
</div>
<button
onClick={() => mutation.mutate()}
className="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
>
<Brain className="h-4 w-4" />
Strategie analysieren
</button>
</div>
);
}
if (mutation.isPending) {
return (
<div className="flex flex-col items-center gap-3 py-12 text-center">
<Loader2 className="h-6 w-6 animate-spin text-neutral-400" />
<p className="text-sm text-neutral-500">
Claude analysiert die Akte...
</p>
<p className="text-xs text-neutral-400">
Dies kann bis zu 30 Sekunden dauern.
</p>
</div>
);
}
if (mutation.isError) {
return (
<div className="flex flex-col items-center gap-3 py-8 text-center">
<div className="rounded-xl bg-red-50 p-3">
<AlertTriangle className="h-6 w-6 text-red-500" />
</div>
<p className="text-sm text-neutral-900">Analyse fehlgeschlagen</p>
<button
onClick={() => mutation.mutate()}
className="inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
>
<RefreshCw className="h-3.5 w-3.5" />
Erneut versuchen
</button>
</div>
);
}
const data = mutation.data!;
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-neutral-900">
KI-Strategieanalyse
</h3>
<button
onClick={() => mutation.mutate()}
className="inline-flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
>
<RefreshCw className="h-3.5 w-3.5" />
Aktualisieren
</button>
</div>
{/* Summary */}
<div className="rounded-md border border-blue-100 bg-blue-50 px-4 py-3 text-sm text-blue-800">
{data.summary}
</div>
{/* Next Steps */}
{data.next_steps?.length > 0 && (
<div>
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
<ArrowRight className="h-3.5 w-3.5" />
Naechste Schritte
</h4>
<div className="space-y-2">
{data.next_steps.map((step, i) => (
<div
key={i}
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
>
<div className="flex items-start gap-3">
<span
className={`mt-0.5 inline-block shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium ${PRIORITY_STYLES[step.priority]}`}
>
{step.priority === "high"
? "Hoch"
: step.priority === "medium"
? "Mittel"
: "Niedrig"}
</span>
<div className="min-w-0">
<p className="text-sm font-medium text-neutral-900">
{step.action}
</p>
<p className="mt-1 text-sm text-neutral-500">
{step.reasoning}
</p>
{step.deadline && (
<p className="mt-1 text-xs text-neutral-400">
Frist: {step.deadline}
</p>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Risk Assessment */}
{data.risk_assessment?.length > 0 && (
<div>
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
<Shield className="h-3.5 w-3.5" />
Risikobewertung
</h4>
<div className="space-y-2">
{data.risk_assessment.map((risk, i) => (
<div
key={i}
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
>
<div className="flex items-start gap-3">
<span
className={`mt-0.5 inline-block shrink-0 rounded-full border px-2 py-0.5 text-xs font-medium ${PRIORITY_STYLES[risk.level]}`}
>
{risk.level === "high"
? "Hoch"
: risk.level === "medium"
? "Mittel"
: "Niedrig"}
</span>
<div className="min-w-0">
<p className="text-sm font-medium text-neutral-900">
{risk.risk}
</p>
<p className="mt-1 text-sm text-neutral-500">
Massnahme: {risk.mitigation}
</p>
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Timeline */}
{data.timeline?.length > 0 && (
<div>
<h4 className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-neutral-500">
<Calendar className="h-3.5 w-3.5" />
Zeitplan
</h4>
<div className="space-y-1">
{data.timeline.map((item, i) => (
<div
key={i}
className={`border-l-2 py-2 pl-4 ${IMPORTANCE_STYLES[item.importance]}`}
>
<div className="flex items-baseline gap-2">
<span className="shrink-0 text-xs font-medium text-neutral-400">
{item.date}
</span>
<span className="text-sm text-neutral-900">{item.event}</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,198 @@
"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { api } from "@/lib/api";
import type { DocumentDraft, DraftDocumentRequest } from "@/lib/types";
import { FileText, Loader2, Copy, Check, Download } from "lucide-react";
const TEMPLATES = {
klageschrift: "Klageschrift",
klageerwiderung: "Klageerwiderung",
abmahnung: "Abmahnung",
schriftsatz: "Schriftsatz",
berufung: "Berufungsschrift",
antrag: "Antrag",
stellungnahme: "Stellungnahme",
gutachten: "Gutachten",
vertrag: "Vertrag",
vollmacht: "Vollmacht",
upc_claim: "UPC Statement of Claim",
upc_defence: "UPC Statement of Defence",
upc_counterclaim: "UPC Counterclaim for Revocation",
upc_injunction: "UPC Provisional Measures",
} as const;
const LANGUAGES = [
{ value: "de", label: "Deutsch" },
{ value: "en", label: "English" },
{ value: "fr", label: "Francais" },
] as const;
const inputClass =
"w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
interface DocumentDrafterProps {
caseId: string;
}
export function DocumentDrafter({ caseId }: DocumentDrafterProps) {
const [templateType, setTemplateType] = useState("");
const [instructions, setInstructions] = useState("");
const [language, setLanguage] = useState("de");
const [copied, setCopied] = useState(false);
const mutation = useMutation({
mutationFn: (req: DraftDocumentRequest) =>
api.post<DocumentDraft>("/ai/draft-document", req),
});
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!templateType) return;
mutation.mutate({
case_id: caseId,
template_type: templateType,
instructions,
language,
});
}
function handleCopy() {
if (mutation.data?.content) {
navigator.clipboard.writeText(mutation.data.content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}
function handleDownload() {
if (!mutation.data?.content) return;
const blob = new Blob([mutation.data.content], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${templateType}_entwurf.txt`;
a.click();
URL.revokeObjectURL(url);
}
return (
<div className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Dokumenttyp
</label>
<select
value={templateType}
onChange={(e) => setTemplateType(e.target.value)}
className={inputClass}
disabled={mutation.isPending}
>
<option value="">Dokumenttyp waehlen...</option>
{Object.entries(TEMPLATES).map(([key, label]) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Sprache
</label>
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
className={inputClass}
disabled={mutation.isPending}
>
{LANGUAGES.map((lang) => (
<option key={lang.value} value={lang.value}>
{lang.label}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Anweisungen (optional)
</label>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="z.B. 'Fokus auf Patentanspruch 1, besonders die technischen Merkmale...'"
rows={3}
className={inputClass}
disabled={mutation.isPending}
/>
</div>
<button
type="submit"
disabled={!templateType || mutation.isPending}
className="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
>
{mutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Dokument wird erstellt...
</>
) : (
<>
<FileText className="h-4 w-4" />
KI-Entwurf erstellen
</>
)}
</button>
</form>
{mutation.isError && (
<div className="rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
Fehler beim Erstellen des Entwurfs. Bitte versuchen Sie es erneut.
</div>
)}
{mutation.data && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h4 className="text-sm font-medium text-neutral-900">
{mutation.data.title}
</h4>
<div className="flex items-center gap-2">
<button
onClick={handleCopy}
className="inline-flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
>
{copied ? (
<>
<Check className="h-3.5 w-3.5 text-emerald-500" />
Kopiert
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
Kopieren
</>
)}
</button>
<button
onClick={handleDownload}
className="inline-flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
>
<Download className="h-3.5 w-3.5" />
Download
</button>
</div>
</div>
<pre className="max-h-[600px] overflow-auto whitespace-pre-wrap rounded-md border border-neutral-200 bg-neutral-50 p-4 text-sm text-neutral-800">
{mutation.data.content}
</pre>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,183 @@
"use client";
import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { api } from "@/lib/api";
import type { SimilarCasesResponse } from "@/lib/types";
import {
Loader2,
Search,
ExternalLink,
AlertTriangle,
Scale,
RefreshCw,
} from "lucide-react";
interface SimilarCaseFinderProps {
caseId: string;
}
const inputClass =
"w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 outline-none transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400";
function RelevanceBadge({ score }: { score: number }) {
const pct = Math.round(score * 100);
let color = "bg-neutral-100 text-neutral-600";
if (pct >= 80) color = "bg-emerald-50 text-emerald-700";
else if (pct >= 60) color = "bg-blue-50 text-blue-700";
else if (pct >= 40) color = "bg-amber-50 text-amber-700";
return (
<span
className={`inline-block shrink-0 rounded-full px-2 py-0.5 text-xs font-medium ${color}`}
>
{pct}%
</span>
);
}
export function SimilarCaseFinder({ caseId }: SimilarCaseFinderProps) {
const [description, setDescription] = useState("");
const mutation = useMutation({
mutationFn: (req: { case_id: string; description: string }) =>
api.post<SimilarCasesResponse>("/ai/similar-cases", req),
});
function handleSearch(e?: React.FormEvent) {
e?.preventDefault();
mutation.mutate({ case_id: caseId, description });
}
return (
<div className="space-y-4">
<form onSubmit={handleSearch} className="space-y-3">
<div>
<label className="mb-1 block text-xs font-medium text-neutral-500">
Zusaetzliche Beschreibung (optional)
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="z.B. 'SEP-Lizenzierung im Mobilfunkbereich, FRAND-Verteidigung...'"
rows={2}
className={inputClass}
disabled={mutation.isPending}
/>
</div>
<button
type="submit"
disabled={mutation.isPending}
className="inline-flex items-center gap-2 rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-neutral-800 disabled:opacity-50"
>
{mutation.isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Suche laeuft...
</>
) : (
<>
<Search className="h-4 w-4" />
Aehnliche Faelle suchen
</>
)}
</button>
</form>
{mutation.isError && (
<div className="flex flex-col items-center gap-3 py-6 text-center">
<div className="rounded-xl bg-red-50 p-3">
<AlertTriangle className="h-6 w-6 text-red-500" />
</div>
<p className="text-sm text-neutral-900">Suche fehlgeschlagen</p>
<p className="text-xs text-neutral-500">
Die youpc.org-Datenbank ist moeglicherweise nicht verfuegbar.
</p>
<button
onClick={() => handleSearch()}
className="inline-flex items-center gap-1 text-sm text-neutral-500 transition-colors hover:text-neutral-700"
>
<RefreshCw className="h-3.5 w-3.5" />
Erneut versuchen
</button>
</div>
)}
{mutation.data && (
<div className="space-y-3">
<div className="flex items-center justify-between">
<p className="text-xs text-neutral-500">
{mutation.data.count} aehnliche{" "}
{mutation.data.count === 1 ? "Fall" : "Faelle"} gefunden
</p>
<button
onClick={() => handleSearch()}
disabled={mutation.isPending}
className="inline-flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:bg-neutral-50"
>
<RefreshCw className="h-3.5 w-3.5" />
Aktualisieren
</button>
</div>
{mutation.data.cases?.length === 0 && (
<div className="flex flex-col items-center gap-2 py-6 text-center">
<Scale className="h-6 w-6 text-neutral-300" />
<p className="text-sm text-neutral-500">
Keine aehnlichen UPC-Faelle gefunden.
</p>
</div>
)}
{mutation.data.cases?.map((c, i) => (
<div
key={i}
className="rounded-md border border-neutral-200 bg-white px-4 py-3"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<RelevanceBadge score={c.relevance} />
<span className="text-xs font-medium text-neutral-400">
{c.case_number}
</span>
{c.url && (
<a
href={c.url}
target="_blank"
rel="noopener noreferrer"
className="text-neutral-400 transition-colors hover:text-neutral-600"
>
<ExternalLink className="h-3.5 w-3.5" />
</a>
)}
</div>
<p className="mt-1 text-sm font-medium text-neutral-900">
{c.title}
</p>
<div className="mt-1 flex flex-wrap gap-x-3 text-xs text-neutral-400">
{c.court && <span>{c.court}</span>}
{c.date && <span>{c.date}</span>}
</div>
</div>
</div>
<p className="mt-2 text-sm text-neutral-600">{c.explanation}</p>
{c.key_holdings && (
<div className="mt-2 rounded border border-neutral-100 bg-neutral-50 px-3 py-2">
<p className="text-xs font-medium text-neutral-500">
Relevante Entscheidungsgruende
</p>
<p className="mt-0.5 text-xs text-neutral-600">
{c.key_holdings}
</p>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -223,82 +223,6 @@ export const CASE_ASSIGNMENT_ROLE_LABELS: Record<CaseAssignmentRole, string> = {
viewer: "Einsicht",
};
// Document Templates
export interface DocumentTemplate {
id: string;
tenant_id?: string;
name: string;
description?: string;
category: "schriftsatz" | "vertrag" | "korrespondenz" | "intern";
content: string;
variables: string[];
is_system: boolean;
created_at: string;
updated_at: string;
}
export const TEMPLATE_CATEGORY_LABELS: Record<string, string> = {
schriftsatz: "Schriftsatz",
vertrag: "Vertrag",
korrespondenz: "Korrespondenz",
intern: "Intern",
};
export interface RenderResponse {
content: string;
template_id: string;
name: string;
}
// Notifications
export interface Notification {
id: string;
tenant_id: string;
user_id: string;
type: string;
title: string;
message: string;
body?: string;
entity_type?: string;
entity_id?: string;
read: boolean;
read_at?: string;
created_at: string;
}
export interface NotificationListResponse {
data: Notification[];
notifications: Notification[];
total: number;
unread_count: number;
}
export interface NotificationPreferences {
deadline_reminder_days: number[];
email_enabled: boolean;
daily_digest: boolean;
}
// Audit Log
export interface AuditLogEntry {
id: string;
tenant_id: string;
user_id?: string;
action: string;
entity_type: string;
entity_id?: string;
old_values?: Record<string, unknown>;
new_values?: Record<string, unknown>;
ip_address?: string;
user_agent?: string;
created_at: string;
}
export interface AuditLogResponse {
entries: AuditLogEntry[];
total: number;
}
export interface ApiError {
error: string;
status: number;
@@ -406,148 +330,80 @@ export interface ExtractionResponse {
count: number;
}
// Notification types
// AI Document Drafting
export interface Notification {
id: string;
tenant_id: string;
user_id: string;
type: string;
entity_type?: string;
entity_id?: string;
export interface DocumentDraft {
title: string;
body?: string;
sent_at?: string;
read_at?: string;
created_at: string;
content: string;
language: string;
}
export interface NotificationPreferences {
user_id: string;
tenant_id: string;
deadline_reminder_days: number[];
email_enabled: boolean;
daily_digest: boolean;
created_at: string;
updated_at: string;
}
export interface NotificationListResponse {
data: Notification[];
total: number;
}
// Audit log types
export interface AuditLogEntry {
id: number;
tenant_id: string;
user_id?: string;
action: string;
entity_type: string;
entity_id?: string;
old_values?: Record<string, unknown>;
new_values?: Record<string, unknown>;
ip_address?: string;
user_agent?: string;
created_at: string;
}
export interface AuditLogResponse {
entries: AuditLogEntry[];
total: number;
page: number;
limit: number;
}
// Reporting types
export interface CaseStats {
period: string;
opened: number;
closed: number;
active: number;
}
export interface CasesByType {
case_type: string;
count: number;
}
export interface CasesByCourt {
court: string;
count: number;
}
export interface CaseReport {
monthly: CaseStats[];
by_type: CasesByType[];
by_court: CasesByCourt[];
total: {
opened: number;
closed: number;
active: number;
};
}
export interface DeadlineCompliance {
period: string;
total: number;
met: number;
missed: number;
pending: number;
compliance_rate: number;
}
export interface MissedDeadline {
id: string;
title: string;
due_date: string;
export interface DraftDocumentRequest {
case_id: string;
template_type: string;
instructions: string;
language: string;
}
export const TEMPLATE_TYPES: Record<string, string> = {
klageschrift: "Klageschrift",
klageerwiderung: "Klageerwiderung",
abmahnung: "Abmahnung",
schriftsatz: "Schriftsatz",
berufung: "Berufungsschrift",
antrag: "Antrag",
stellungnahme: "Stellungnahme",
gutachten: "Gutachten",
vertrag: "Vertrag",
vollmacht: "Vollmacht",
upc_claim: "UPC Statement of Claim",
upc_defence: "UPC Statement of Defence",
upc_counterclaim: "UPC Counterclaim for Revocation",
upc_injunction: "UPC Provisional Measures",
};
// AI Case Strategy
export interface StrategyStep {
priority: "high" | "medium" | "low";
action: string;
reasoning: string;
deadline?: string;
}
export interface RiskItem {
level: "high" | "medium" | "low";
risk: string;
mitigation: string;
}
export interface TimelineItem {
date: string;
event: string;
importance: "critical" | "important" | "routine";
}
export interface StrategyRecommendation {
summary: string;
next_steps: StrategyStep[];
risk_assessment: RiskItem[];
timeline: TimelineItem[];
}
// AI Similar Case Finder
export interface SimilarCase {
case_number: string;
case_title: string;
days_overdue: number;
title: string;
court: string;
date: string;
relevance: number;
explanation: string;
key_holdings: string;
url?: string;
}
export interface DeadlineReport {
monthly: DeadlineCompliance[];
missed: MissedDeadline[];
total: {
total: number;
met: number;
missed: number;
pending: number;
compliance_rate: number;
};
}
export interface UserWorkload {
user_id: string;
active_cases: number;
deadlines: number;
overdue: number;
completed: number;
}
export interface WorkloadReport {
users: UserWorkload[];
}
export interface BillingByMonth {
period: string;
cases_active: number;
cases_closed: number;
cases_new: number;
}
export interface BillingByType {
case_type: string;
active: number;
closed: number;
total: number;
}
export interface BillingReport {
monthly: BillingByMonth[];
by_type: BillingByType[];
export interface SimilarCasesResponse {
cases: SimilarCase[];
count: number;
}