diff --git a/frontend/src/app/(app)/cases/page.tsx b/frontend/src/app/(app)/cases/page.tsx index 5a125c8..32efc16 100644 --- a/frontend/src/app/(app)/cases/page.tsx +++ b/frontend/src/app/(app)/cases/page.tsx @@ -5,6 +5,7 @@ import { api } from "@/lib/api"; import type { Case } from "@/lib/types"; import Link from "next/link"; import { useSearchParams, useRouter } from "next/navigation"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; import { Plus, Search, FolderOpen } from "lucide-react"; import { useState } from "react"; import { SkeletonTable } from "@/components/ui/Skeleton"; @@ -72,6 +73,12 @@ export default function CasesPage() { return (
+

Akten

diff --git a/frontend/src/app/(app)/dashboard/page.tsx b/frontend/src/app/(app)/dashboard/page.tsx index 7ae7549..1bfd10a 100644 --- a/frontend/src/app/(app)/dashboard/page.tsx +++ b/frontend/src/app/(app)/dashboard/page.tsx @@ -8,6 +8,8 @@ import { CaseOverviewGrid } from "@/components/dashboard/CaseOverviewGrid"; import { UpcomingTimeline } from "@/components/dashboard/UpcomingTimeline"; import { AISummaryCard } from "@/components/dashboard/AISummaryCard"; import { QuickActions } from "@/components/dashboard/QuickActions"; +import { RecentActivityList } from "@/components/dashboard/RecentActivityList"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; import { Skeleton, SkeletonCard } from "@/components/ui/Skeleton"; import { AlertTriangle, RefreshCw } from "lucide-react"; @@ -71,9 +73,12 @@ export default function DashboardPage() { ); } + const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : []; + return (
+

Dashboard

Fristenübersicht und Kanzlei-Status @@ -91,10 +96,14 @@ export default function DashboardPage() {

- + refetch()} />
+ + {recentActivity.length > 0 && ( + + )}
); } diff --git a/frontend/src/app/(app)/fristen/page.tsx b/frontend/src/app/(app)/fristen/page.tsx index 6a620f0..b4e7ee0 100644 --- a/frontend/src/app/(app)/fristen/page.tsx +++ b/frontend/src/app/(app)/fristen/page.tsx @@ -2,16 +2,20 @@ import { DeadlineList } from "@/components/deadlines/DeadlineList"; import { DeadlineCalendarView } from "@/components/deadlines/DeadlineCalendarView"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api"; import type { Deadline } from "@/lib/types"; import { Calendar, List, Calculator } from "lucide-react"; import Link from "next/link"; import { useState } from "react"; +import { useSearchParams } from "next/navigation"; type ViewMode = "list" | "calendar"; export default function FristenPage() { + const searchParams = useSearchParams(); + const initialStatus = searchParams.get("status") ?? undefined; const [view, setView] = useState("list"); const { data: deadlines } = useQuery({ @@ -21,50 +25,58 @@ export default function FristenPage() { return (
-
-
-

Fristen

-

- Alle Fristen im Überblick -

-
-
- - - Fristenrechner - -
- - + + Fristenrechner + +
+ + +
{view === "list" ? ( - + ) : ( )} diff --git a/frontend/src/app/(app)/termine/page.tsx b/frontend/src/app/(app)/termine/page.tsx index 2c6f4d4..6b02057 100644 --- a/frontend/src/app/(app)/termine/page.tsx +++ b/frontend/src/app/(app)/termine/page.tsx @@ -6,6 +6,7 @@ import { AppointmentModal } from "@/components/appointments/AppointmentModal"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api"; import type { Appointment } from "@/lib/types"; +import { Breadcrumb } from "@/components/layout/Breadcrumb"; import { Calendar, List, Plus } from "lucide-react"; import { useState } from "react"; @@ -38,6 +39,12 @@ export default function TerminePage() { return (
+

Termine

diff --git a/frontend/src/components/dashboard/AISummaryCard.tsx b/frontend/src/components/dashboard/AISummaryCard.tsx index c8facc8..6ee004c 100644 --- a/frontend/src/components/dashboard/AISummaryCard.tsx +++ b/frontend/src/components/dashboard/AISummaryCard.tsx @@ -1,10 +1,12 @@ "use client"; -import { Sparkles } from "lucide-react"; +import { useState } from "react"; +import { Sparkles, RefreshCw } from "lucide-react"; import type { DashboardData } from "@/lib/types"; interface Props { data: DashboardData; + onRefresh?: () => void; } function generateSummary(data: DashboardData): string { @@ -51,18 +53,39 @@ function generateSummary(data: DashboardData): string { return parts.join(" "); } -export function AISummaryCard({ data }: Props) { +export function AISummaryCard({ data, onRefresh }: Props) { + const [spinning, setSpinning] = useState(false); const summary = generateSummary(data); + function handleRefresh() { + if (!onRefresh) return; + setSpinning(true); + onRefresh(); + setTimeout(() => setSpinning(false), 1000); + } + return (
-
-
- +
+
+
+ +
+

+ KI-Zusammenfassung +

-

- KI-Zusammenfassung -

+ {onRefresh && ( + + )}

{summary} diff --git a/frontend/src/components/dashboard/CaseOverviewGrid.tsx b/frontend/src/components/dashboard/CaseOverviewGrid.tsx index 1cef318..79516cb 100644 --- a/frontend/src/components/dashboard/CaseOverviewGrid.tsx +++ b/frontend/src/components/dashboard/CaseOverviewGrid.tsx @@ -1,6 +1,7 @@ "use client"; -import { FolderOpen, FolderPlus, Archive } from "lucide-react"; +import Link from "next/link"; +import { FolderOpen, FolderPlus, Archive, ChevronRight } from "lucide-react"; import type { CaseSummary } from "@/lib/types"; interface Props { @@ -16,6 +17,7 @@ export function CaseOverviewGrid({ data }: Props) { icon: FolderOpen, color: "text-blue-600", bg: "bg-blue-50", + href: "/cases?status=active", }, { label: "Neu (Monat)", @@ -23,6 +25,7 @@ export function CaseOverviewGrid({ data }: Props) { icon: FolderPlus, color: "text-violet-600", bg: "bg-violet-50", + href: "/cases?status=active&since=month", }, { label: "Abgeschlossen", @@ -30,25 +33,33 @@ export function CaseOverviewGrid({ data }: Props) { icon: Archive, color: "text-neutral-500", bg: "bg-neutral-50", + href: "/cases?status=closed", }, ]; return (

Aktenübersicht

-
+
{items.map((item) => ( -
+
{item.label}
- - {item.value} - -
+
+ + {item.value} + + +
+ ))}
diff --git a/frontend/src/components/dashboard/DeadlineTrafficLights.tsx b/frontend/src/components/dashboard/DeadlineTrafficLights.tsx index 4825072..78c83bb 100644 --- a/frontend/src/components/dashboard/DeadlineTrafficLights.tsx +++ b/frontend/src/components/dashboard/DeadlineTrafficLights.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useRef } from "react"; +import Link from "next/link"; import { AlertTriangle, Clock, CheckCircle } from "lucide-react"; import type { DeadlineSummary } from "@/lib/types"; @@ -27,10 +28,9 @@ function AnimatedCount({ value }: { value: number }) { interface Props { data: DeadlineSummary; - onFilter?: (filter: "overdue" | "this_week" | "ok") => void; } -export function DeadlineTrafficLights({ data, onFilter }: Props) { +export function DeadlineTrafficLights({ data }: Props) { const safe = data ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 }; const cards = [ { @@ -38,6 +38,7 @@ export function DeadlineTrafficLights({ data, onFilter }: Props) { label: "Überfällig", count: safe.overdue_count ?? 0, icon: AlertTriangle, + href: "/fristen?status=overdue", bg: "bg-red-50", border: "border-red-200", iconColor: "text-red-500", @@ -51,6 +52,7 @@ export function DeadlineTrafficLights({ data, onFilter }: Props) { label: "Diese Woche", count: safe.due_this_week ?? 0, icon: Clock, + href: "/fristen?status=this_week", bg: "bg-amber-50", border: "border-amber-200", iconColor: "text-amber-500", @@ -64,6 +66,7 @@ export function DeadlineTrafficLights({ data, onFilter }: Props) { label: "Im Zeitplan", count: (safe.ok_count ?? 0) + (safe.due_next_week ?? 0), icon: CheckCircle, + href: "/fristen?status=ok", bg: "bg-emerald-50", border: "border-emerald-200", iconColor: "text-emerald-500", @@ -77,9 +80,9 @@ export function DeadlineTrafficLights({ data, onFilter }: Props) { return (
{cards.map((card) => ( - + ))}
); diff --git a/frontend/src/components/dashboard/QuickActions.tsx b/frontend/src/components/dashboard/QuickActions.tsx index bfc19e5..f4ebd64 100644 --- a/frontend/src/components/dashboard/QuickActions.tsx +++ b/frontend/src/components/dashboard/QuickActions.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { FolderPlus, Clock, Sparkles, CalendarSync } from "lucide-react"; +import { FolderPlus, Clock, Sparkles, CalendarPlus } from "lucide-react"; const actions = [ { @@ -12,22 +12,22 @@ const actions = [ }, { label: "Frist eintragen", - href: "/fristen", + href: "/fristen/neu", icon: Clock, color: "text-amber-600 bg-amber-50 hover:bg-amber-100", }, + { + label: "Neuer Termin", + href: "/termine/neu", + icon: CalendarPlus, + color: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100", + }, { label: "AI Analyse", href: "/ai/extract", icon: Sparkles, color: "text-violet-600 bg-violet-50 hover:bg-violet-100", }, - { - label: "CalDAV Sync", - href: "/einstellungen", - icon: CalendarSync, - color: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100", - }, ]; export function QuickActions() { diff --git a/frontend/src/components/dashboard/RecentActivityList.tsx b/frontend/src/components/dashboard/RecentActivityList.tsx new file mode 100644 index 0000000..f13cf19 --- /dev/null +++ b/frontend/src/components/dashboard/RecentActivityList.tsx @@ -0,0 +1,80 @@ +"use client"; + +import Link from "next/link"; +import { formatDistanceToNow, parseISO } from "date-fns"; +import { de } from "date-fns/locale"; +import { + FileText, + Scale, + Calendar, + Clock, + MessageSquare, + ChevronRight, +} from "lucide-react"; +import type { RecentActivity } from "@/lib/types"; + +const EVENT_ICONS: Record = { + status_changed: Scale, + deadline_created: Clock, + appointment_created: Calendar, + document_uploaded: FileText, + note_added: MessageSquare, +}; + +interface Props { + activities: RecentActivity[]; +} + +export function RecentActivityList({ activities }: Props) { + const safe = Array.isArray(activities) ? activities : []; + + if (safe.length === 0) { + return null; + } + + return ( +
+

+ Letzte Aktivität +

+
+ {safe.map((activity) => { + const Icon = EVENT_ICONS[activity.event_type ?? ""] ?? FileText; + const timeAgo = activity.created_at + ? formatDistanceToNow(parseISO(activity.created_at), { + addSuffix: true, + locale: de, + }) + : ""; + + return ( + +
+ +
+
+

+ {activity.title} +

+
+ {activity.case_number} + {timeAgo && ( + <> + · + {timeAgo} + + )} +
+
+ + + ); + })} +
+
+ ); +} diff --git a/frontend/src/components/dashboard/UpcomingTimeline.tsx b/frontend/src/components/dashboard/UpcomingTimeline.tsx index df96255..a5c4d74 100644 --- a/frontend/src/components/dashboard/UpcomingTimeline.tsx +++ b/frontend/src/components/dashboard/UpcomingTimeline.tsx @@ -1,8 +1,9 @@ "use client"; +import Link from "next/link"; import { format, parseISO, isToday, isTomorrow } from "date-fns"; import { de } from "date-fns/locale"; -import { Clock, Calendar, MapPin } from "lucide-react"; +import { Clock, Calendar, MapPin, ChevronRight } from "lucide-react"; import type { UpcomingDeadline, UpcomingAppointment } from "@/lib/types"; interface Props { @@ -80,8 +81,12 @@ export function UpcomingTimeline({ deadlines, appointments }: Props) { function TimelineEntry({ item }: { item: TimelineItem }) { if (item.type === "deadline") { const d = item.data; + const href = `/fristen/${d.id}`; return ( -
+
@@ -90,19 +95,40 @@ function TimelineEntry({ item }: { item: TimelineItem }) { {d.title}

- {d.case_number} · {d.case_title} + {d.case_id ? ( + e.stopPropagation()} + className="inline" + > + + {d.case_number} + + {" · "} + + ) : ( + <>{d.case_number} · + )} + {d.case_title}

- - Frist - -
+
+ Frist + +
+ ); } const a = item.data; + const href = `/termine/${a.id}`; return ( -
+
@@ -121,7 +147,20 @@ function TimelineEntry({ item }: { item: TimelineItem }) { )} - {a.case_number && ( + {a.case_number && a.case_id && ( + <> + · + e.stopPropagation()}> + + {a.case_number} + + + + )} + {a.case_number && !a.case_id && ( <> · {a.case_number} @@ -129,9 +168,10 @@ function TimelineEntry({ item }: { item: TimelineItem }) { )}
- - Termin - -
+
+ Termin + +
+ ); } diff --git a/frontend/src/components/deadlines/DeadlineList.tsx b/frontend/src/components/deadlines/DeadlineList.tsx index b935c27..b8f6706 100644 --- a/frontend/src/components/deadlines/DeadlineList.tsx +++ b/frontend/src/components/deadlines/DeadlineList.tsx @@ -10,7 +10,14 @@ import { toast } from "sonner"; import { useState, useMemo } from "react"; import { EmptyState } from "@/components/ui/EmptyState"; -type StatusFilter = "all" | "pending" | "completed" | "overdue"; +type StatusFilter = "all" | "pending" | "completed" | "overdue" | "this_week" | "ok"; + +function mapUrlStatus(status?: string): StatusFilter { + if (status === "overdue") return "overdue"; + if (status === "this_week") return "this_week"; + if (status === "ok") return "ok"; + return "all"; +} function getUrgency(deadline: Deadline): "red" | "amber" | "green" { if (deadline.status === "completed") return "green"; @@ -47,9 +54,15 @@ const urgencyConfig = { const selectClass = "rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-sm text-neutral-700 transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 outline-none"; -export function DeadlineList() { +interface Props { + initialStatus?: string; +} + +export function DeadlineList({ initialStatus }: Props) { const queryClient = useQueryClient(); - const [statusFilter, setStatusFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState( + mapUrlStatus(initialStatus), + ); const [caseFilter, setCaseFilter] = useState("all"); const { data: deadlines, isLoading } = useQuery({ @@ -90,6 +103,18 @@ export function DeadlineList() { if (d.status === "completed") return false; if (!isPast(parseISO(d.due_date))) return false; } + if (statusFilter === "this_week") { + if (d.status === "completed") return false; + const due = parseISO(d.due_date); + if (isPast(due)) return false; + if (!isThisWeek(due, { weekStartsOn: 1 })) return false; + } + if (statusFilter === "ok") { + if (d.status === "completed") return false; + const due = parseISO(d.due_date); + if (isPast(due)) return false; + if (isThisWeek(due, { weekStartsOn: 1 })) return false; + } if (caseFilter !== "all" && d.case_id !== caseFilter) return false; return true; }); @@ -144,10 +169,10 @@ export function DeadlineList() {