diff --git a/frontend/src/app/(app)/cases/[id]/dokumente/page.tsx b/frontend/src/app/(app)/cases/[id]/dokumente/page.tsx new file mode 100644 index 0000000..2c87fd8 --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/dokumente/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Document } from "@/lib/types"; +import { DocumentList } from "@/components/documents/DocumentList"; +import { DocumentUpload } from "@/components/documents/DocumentUpload"; +import { Loader2 } from "lucide-react"; + +export default function DokumentePage() { + const { id } = useParams<{ id: string }>(); + + const { data, isLoading } = useQuery({ + queryKey: ["case-documents", id], + queryFn: () => api.get(`/cases/${id}/documents`), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const documents = Array.isArray(data) ? data : []; + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/app/(app)/cases/[id]/fristen/page.tsx b/frontend/src/app/(app)/cases/[id]/fristen/page.tsx new file mode 100644 index 0000000..7ef741c --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/fristen/page.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Deadline } from "@/lib/types"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; +import { Clock, Loader2 } from "lucide-react"; + +const DEADLINE_STATUS: Record = { + pending: "bg-amber-50 text-amber-700", + completed: "bg-emerald-50 text-emerald-700", + overdue: "bg-red-50 text-red-700", +}; + +const DEADLINE_STATUS_LABEL: Record = { + pending: "Offen", + completed: "Erledigt", + overdue: "Ueberfaellig", +}; + +export default function FristenPage() { + const { id } = useParams<{ id: string }>(); + + const { data, isLoading } = useQuery({ + queryKey: ["case-deadlines", id], + queryFn: () => + api.get<{ deadlines: Deadline[]; total: number }>( + `/deadlines?case_id=${id}`, + ), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const deadlines = Array.isArray(data?.deadlines) ? data.deadlines : []; + + if (deadlines.length === 0) { + return ( +
+
+ +
+

+ Keine Fristen vorhanden. +

+
+ ); + } + + return ( +
+ {deadlines.map((d) => ( +
+
+

{d.title}

+ {d.description && ( +

+ {d.description} +

+ )} +
+
+ + {DEADLINE_STATUS_LABEL[d.status] ?? d.status} + + + {format(new Date(d.due_date), "d. MMM yyyy", { locale: de })} + +
+
+ ))} +
+ ); +} diff --git a/frontend/src/app/(app)/cases/[id]/layout.tsx b/frontend/src/app/(app)/cases/[id]/layout.tsx new file mode 100644 index 0000000..faafb59 --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/layout.tsx @@ -0,0 +1,226 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams, usePathname } from "next/navigation"; +import Link from "next/link"; +import { api } from "@/lib/api"; +import type { Case } from "@/lib/types"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { + ArrowLeft, + Activity, + Clock, + FileText, + Users, + StickyNote, + AlertTriangle, +} from "lucide-react"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; + +interface CaseDetail extends Case { + parties: unknown[]; + deadlines_count: number; +} + +const STATUS_BADGE: Record = { + active: "bg-emerald-50 text-emerald-700", + pending: "bg-amber-50 text-amber-700", + closed: "bg-neutral-100 text-neutral-600", + archived: "bg-neutral-100 text-neutral-400", +}; + +const STATUS_LABEL: Record = { + active: "Aktiv", + pending: "Anhaengig", + closed: "Geschlossen", + archived: "Archiviert", +}; + +const TABS = [ + { segment: "verlauf", label: "Verlauf", icon: Activity }, + { segment: "fristen", label: "Fristen", icon: Clock }, + { segment: "dokumente", label: "Dokumente", icon: FileText }, + { segment: "parteien", label: "Parteien", icon: Users }, + { segment: "notizen", label: "Notizen", icon: StickyNote }, +] as const; + +const TAB_LABELS: Record = { + verlauf: "Verlauf", + fristen: "Fristen", + dokumente: "Dokumente", + parteien: "Parteien", + notizen: "Notizen", +}; + +function CaseDetailSkeleton() { + return ( +
+ +
+
+ + +
+
+ + +
+
+
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+ {[1, 2, 3].map((i) => ( + + ))} +
+
+ ); +} + +export default function CaseDetailLayout({ + children, +}: { + children: React.ReactNode; +}) { + const { id } = useParams<{ id: string }>(); + const pathname = usePathname(); + + const { + data: caseDetail, + isLoading, + error, + } = useQuery({ + queryKey: ["case", id], + queryFn: () => api.get(`/cases/${id}`), + }); + + // Determine active tab from pathname + const segments = pathname.split("/"); + const activeSegment = segments[segments.length - 1] || "verlauf"; + const activeTabLabel = TAB_LABELS[activeSegment]; + + if (isLoading) { + return ; + } + + if (error || !caseDetail) { + return ( +
+
+ +
+

+ Akte nicht gefunden +

+

+ Die Akte existiert nicht oder Sie haben keine Berechtigung. +

+ + + Zurueck zu Akten + +
+ ); + } + + const breadcrumbItems = [ + { label: "Dashboard", href: "/dashboard" }, + { label: "Akten", href: "/cases" }, + { label: caseDetail.case_number, href: `/cases/${id}/verlauf` }, + ...(activeTabLabel ? [{ label: activeTabLabel }] : []), + ]; + + const partiesCount = Array.isArray(caseDetail.parties) + ? caseDetail.parties.length + : 0; + + return ( +
+ + +
+
+
+

+ {caseDetail.title} +

+ + {STATUS_LABEL[caseDetail.status] ?? caseDetail.status} + +
+
+ Az. {caseDetail.case_number} + {caseDetail.case_type && {caseDetail.case_type}} + {caseDetail.court && {caseDetail.court}} + {caseDetail.court_ref && ({caseDetail.court_ref})} +
+
+
+

+ Erstellt:{" "} + {format(new Date(caseDetail.created_at), "d. MMM yyyy", { + locale: de, + })} +

+

+ Aktualisiert:{" "} + {format(new Date(caseDetail.updated_at), "d. MMM yyyy", { + locale: de, + })} +

+
+
+ + {caseDetail.ai_summary && ( +
+ {caseDetail.ai_summary} +
+ )} + +
+ +
+ +
{children}
+
+ ); +} diff --git a/frontend/src/app/(app)/cases/[id]/notizen/page.tsx b/frontend/src/app/(app)/cases/[id]/notizen/page.tsx new file mode 100644 index 0000000..e32e446 --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/notizen/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useParams } from "next/navigation"; +import { NotesList } from "@/components/notes/NotesList"; + +export default function NotizenPage() { + const { id } = useParams<{ id: string }>(); + + return ; +} diff --git a/frontend/src/app/(app)/cases/[id]/page.tsx b/frontend/src/app/(app)/cases/[id]/page.tsx index 0aac465..4196043 100644 --- a/frontend/src/app/(app)/cases/[id]/page.tsx +++ b/frontend/src/app/(app)/cases/[id]/page.tsx @@ -1,341 +1,10 @@ -"use client"; +import { redirect } from "next/navigation"; -import { useQuery } from "@tanstack/react-query"; -import { useParams } from "next/navigation"; -import { api } from "@/lib/api"; -import type { Case, CaseEvent, Party, Deadline, Document } from "@/lib/types"; -import { CaseTimeline } from "@/components/cases/CaseTimeline"; -import { PartyList } from "@/components/cases/PartyList"; -import { - ArrowLeft, - Clock, - FileText, - Users, - Activity, - AlertTriangle, -} from "lucide-react"; -import { format } from "date-fns"; -import { de } from "date-fns/locale"; -import Link from "next/link"; -import { useState } from "react"; -import { Skeleton } from "@/components/ui/Skeleton"; - -interface CaseDetail extends Case { - parties: Party[]; - recent_events: CaseEvent[]; - deadlines_count: number; -} - -const STATUS_BADGE: Record = { - active: "bg-emerald-50 text-emerald-700", - pending: "bg-amber-50 text-amber-700", - closed: "bg-neutral-100 text-neutral-600", - archived: "bg-neutral-100 text-neutral-400", -}; - -const STATUS_LABEL: Record = { - active: "Aktiv", - pending: "Anhängig", - closed: "Geschlossen", - archived: "Archiviert", -}; - -const TABS = [ - { key: "timeline", label: "Verlauf", icon: Activity }, - { key: "deadlines", label: "Fristen", icon: Clock }, - { key: "documents", label: "Dokumente", icon: FileText }, - { key: "parties", label: "Parteien", icon: Users }, -] as const; - -type TabKey = (typeof TABS)[number]["key"]; - -function CaseDetailSkeleton() { - return ( -
- -
-
- - -
-
- - -
-
-
- {[1, 2, 3, 4].map((i) => ( - - ))} -
-
- {[1, 2, 3].map((i) => ( - - ))} -
-
- ); -} - -export default function CaseDetailPage() { - const { id } = useParams<{ id: string }>(); - const [activeTab, setActiveTab] = useState("timeline"); - - const { - data: caseDetail, - isLoading, - error, - } = useQuery({ - queryKey: ["case", id], - queryFn: () => api.get(`/cases/${id}`), - }); - - const { data: deadlinesData } = useQuery({ - queryKey: ["case-deadlines", id], - queryFn: () => - api.get<{ deadlines: Deadline[]; total: number }>( - `/deadlines?case_id=${id}`, - ), - enabled: activeTab === "deadlines", - }); - - const { data: documentsData } = useQuery({ - queryKey: ["case-documents", id], - queryFn: () => api.get(`/cases/${id}/documents`), - enabled: activeTab === "documents", - }); - - if (isLoading) { - return ; - } - - if (error || !caseDetail) { - return ( -
-
- -
-

- Akte nicht gefunden -

-

- Die Akte existiert nicht oder Sie haben keine Berechtigung. -

- - - Zurück zu Akten - -
- ); - } - - const deadlines = Array.isArray(deadlinesData?.deadlines) ? deadlinesData.deadlines : []; - const documents = Array.isArray(documentsData) ? documentsData : []; - - return ( -
- - - Zurück zu Akten - - -
-
-
-

- {caseDetail.title} -

- - {STATUS_LABEL[caseDetail.status] ?? caseDetail.status} - -
-
- Az. {caseDetail.case_number} - {caseDetail.case_type && {caseDetail.case_type}} - {caseDetail.court && {caseDetail.court}} - {caseDetail.court_ref && ({caseDetail.court_ref})} -
-
-
-

- Erstellt:{" "} - {format(new Date(caseDetail.created_at), "d. MMM yyyy", { - locale: de, - })} -

-

- Aktualisiert:{" "} - {format(new Date(caseDetail.updated_at), "d. MMM yyyy", { - locale: de, - })} -

-
-
- - {caseDetail.ai_summary && ( -
- {caseDetail.ai_summary} -
- )} - -
- -
- -
- {activeTab === "timeline" && ( - - )} - - {activeTab === "deadlines" && ( - - )} - - {activeTab === "documents" && ( - - )} - - {activeTab === "parties" && ( - - )} -
-
- ); -} - -function DeadlinesList({ deadlines }: { deadlines: Deadline[] }) { - if (deadlines.length === 0) { - return ( -
-
- -
-

- Keine Fristen vorhanden. -

-
- ); - } - - const DEADLINE_STATUS: Record = { - pending: "bg-amber-50 text-amber-700", - completed: "bg-emerald-50 text-emerald-700", - overdue: "bg-red-50 text-red-700", - }; - - const DEADLINE_STATUS_LABEL: Record = { - pending: "Offen", - completed: "Erledigt", - overdue: "Überfällig", - }; - - return ( -
- {deadlines.map((d) => ( -
-
-

{d.title}

- {d.description && ( -

- {d.description} -

- )} -
-
- - {DEADLINE_STATUS_LABEL[d.status] ?? d.status} - - - {format(new Date(d.due_date), "d. MMM yyyy", { locale: de })} - -
-
- ))} -
- ); -} - -function DocumentsList({ documents }: { documents: Document[] }) { - if (documents.length === 0) { - return ( -
-
- -
-

- Keine Dokumente vorhanden. -

-
- ); - } - - return ( -
- {documents.map((doc) => ( -
-
- -
-

- {doc.title} -

-
- {doc.doc_type && {doc.doc_type}} - {doc.file_size && ( - {(doc.file_size / 1024).toFixed(0)} KB - )} -
-
-
- - Herunterladen - -
- ))} -
- ); +export default async function CaseDetailPage({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + redirect(`/cases/${id}/verlauf`); } diff --git a/frontend/src/app/(app)/cases/[id]/parteien/page.tsx b/frontend/src/app/(app)/cases/[id]/parteien/page.tsx new file mode 100644 index 0000000..54b46ca --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/parteien/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Case, Party } from "@/lib/types"; +import { PartyList } from "@/components/cases/PartyList"; +import { Loader2 } from "lucide-react"; + +interface CaseDetail extends Case { + parties: Party[]; +} + +export default function ParteienPage() { + const { id } = useParams<{ id: string }>(); + + const { data: caseDetail, isLoading } = useQuery({ + queryKey: ["case", id], + queryFn: () => api.get(`/cases/${id}`), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const parties = Array.isArray(caseDetail?.parties) + ? caseDetail.parties + : []; + + return ; +} diff --git a/frontend/src/app/(app)/cases/[id]/verlauf/page.tsx b/frontend/src/app/(app)/cases/[id]/verlauf/page.tsx new file mode 100644 index 0000000..8acb030 --- /dev/null +++ b/frontend/src/app/(app)/cases/[id]/verlauf/page.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useParams } from "next/navigation"; +import { api } from "@/lib/api"; +import type { Case, CaseEvent } from "@/lib/types"; +import { CaseTimeline } from "@/components/cases/CaseTimeline"; +import { Loader2 } from "lucide-react"; + +interface CaseDetail extends Case { + recent_events: CaseEvent[]; +} + +export default function VerlaufPage() { + const { id } = useParams<{ id: string }>(); + + const { data: caseDetail, isLoading } = useQuery({ + queryKey: ["case", id], + queryFn: () => api.get(`/cases/${id}`), + }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const events = Array.isArray(caseDetail?.recent_events) + ? caseDetail.recent_events + : []; + + return ; +} diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a88511a..458cc5b 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -176,6 +176,19 @@ export interface CalDAVSyncResponse { last_sync_at?: null; } +export interface Note { + id: string; + tenant_id: string; + case_id?: string; + deadline_id?: string; + appointment_id?: string; + case_event_id?: string; + content: string; + created_by?: string; + created_at: string; + updated_at: string; +} + export interface ApiError { error: string; status: number;