feat: add deadline management frontend (Phase 1G)

- Fristen page with list view (sortable, filterable by status/case)
- Calendar view with month navigation and deadline dots
- Deadline calculator page (proceeding type + trigger date = timeline)
- Traffic light urgency: red (overdue), amber (this week), green (OK)
- Backend: GET /api/deadlines (all tenant deadlines), GET /api/proceeding-types
- API client: added patch() method
- Types: DeadlineRule, ProceedingType, CalculatedDeadline, RuleTreeNode
This commit is contained in:
m
2026-03-25 13:53:12 +01:00
parent 0fac764211
commit 1fa7d90050
11 changed files with 790 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
"use client";
import type { Deadline } from "@/lib/types";
import {
format,
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
eachDayOfInterval,
isSameMonth,
isToday,
parseISO,
isPast,
isThisWeek,
addMonths,
subMonths,
} from "date-fns";
import { de } from "date-fns/locale";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useState, useMemo } from "react";
interface DeadlineCalendarViewProps {
deadlines: Deadline[];
}
function getUrgency(deadline: Deadline): "red" | "amber" | "green" {
if (deadline.status === "completed") return "green";
const due = parseISO(deadline.due_date);
if (isPast(due)) return "red";
if (isThisWeek(due, { weekStartsOn: 1 })) return "amber";
return "green";
}
const dotColors = {
red: "bg-red-500",
amber: "bg-amber-500",
green: "bg-green-500",
};
export function DeadlineCalendarView({ deadlines }: DeadlineCalendarViewProps) {
const [currentMonth, setCurrentMonth] = useState(new Date());
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(currentMonth);
const calStart = startOfWeek(monthStart, { weekStartsOn: 1 });
const calEnd = endOfWeek(monthEnd, { weekStartsOn: 1 });
const days = eachDayOfInterval({ start: calStart, end: calEnd });
const deadlinesByDay = useMemo(() => {
const map = new Map<string, Deadline[]>();
for (const d of deadlines) {
if (d.status === "completed") continue;
const key = d.due_date.slice(0, 10);
const existing = map.get(key) || [];
existing.push(d);
map.set(key, existing);
}
return map;
}, [deadlines]);
const weekDays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
return (
<div className="rounded-lg border border-neutral-200 bg-white">
{/* Header */}
<div className="flex items-center justify-between border-b border-neutral-200 px-4 py-3">
<button
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
className="rounded-md p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-sm font-medium text-neutral-900">
{format(currentMonth, "MMMM yyyy", { locale: de })}
</span>
<button
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
className="rounded-md p-1 text-neutral-400 hover:bg-neutral-100 hover:text-neutral-600"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
{/* Weekday labels */}
<div className="grid grid-cols-7 border-b border-neutral-100">
{weekDays.map((d) => (
<div key={d} className="px-2 py-2 text-center text-xs font-medium text-neutral-400">
{d}
</div>
))}
</div>
{/* Days grid */}
<div className="grid grid-cols-7">
{days.map((day, i) => {
const key = format(day, "yyyy-MM-dd");
const dayDeadlines = deadlinesByDay.get(key) || [];
const inMonth = isSameMonth(day, currentMonth);
const today = isToday(day);
return (
<div
key={i}
className={`min-h-[4.5rem] border-b border-r border-neutral-100 p-1.5 ${
!inMonth ? "bg-neutral-50" : ""
}`}
>
<div
className={`mb-1 text-right text-xs ${
today
? "font-bold text-neutral-900"
: inMonth
? "text-neutral-600"
: "text-neutral-300"
}`}
>
{today ? (
<span className="inline-flex h-5 w-5 items-center justify-center rounded-full bg-neutral-900 text-white">
{format(day, "d")}
</span>
) : (
format(day, "d")
)}
</div>
<div className="space-y-0.5">
{dayDeadlines.slice(0, 3).map((dl) => {
const urgency = getUrgency(dl);
return (
<div
key={dl.id}
className="flex items-center gap-1 truncate"
title={dl.title}
>
<div className={`h-1.5 w-1.5 shrink-0 rounded-full ${dotColors[urgency]}`} />
<span className="truncate text-[10px] text-neutral-700">
{dl.title}
</span>
</div>
);
})}
{dayDeadlines.length > 3 && (
<div className="text-[10px] text-neutral-400">
+{dayDeadlines.length - 3} mehr
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}