Frontend api.ts baseUrl is already "/api", so paths like "/api/cases" produced "/api/api/cases". Stripped the redundant prefix from all component calls. Rewrite destination correctly adds /api/ back for the Go backend.
210 lines
7.4 KiB
TypeScript
210 lines
7.4 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { api } from "@/lib/api";
|
|
import type {
|
|
ProceedingType,
|
|
CalculateResponse,
|
|
CalculatedDeadline,
|
|
} from "@/lib/types";
|
|
import { format, parseISO, isPast, isThisWeek } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
import {
|
|
Calculator,
|
|
Calendar,
|
|
ArrowRight,
|
|
AlertTriangle,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
function getTimelineUrgency(dueDate: string): "red" | "amber" | "green" {
|
|
const due = parseISO(dueDate);
|
|
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",
|
|
};
|
|
|
|
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";
|
|
|
|
export function DeadlineCalculator() {
|
|
const [proceedingType, setProceedingType] = useState("");
|
|
const [triggerDate, setTriggerDate] = useState("");
|
|
|
|
const { data: proceedingTypes, isLoading: typesLoading } = useQuery({
|
|
queryKey: ["proceeding-types"],
|
|
queryFn: () => api.get<ProceedingType[]>("/proceeding-types"),
|
|
});
|
|
|
|
const calculateMutation = useMutation({
|
|
mutationFn: (params: {
|
|
proceeding_type: string;
|
|
trigger_event_date: string;
|
|
}) => api.post<CalculateResponse>("/deadlines/calculate", params),
|
|
});
|
|
|
|
function handleCalculate(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
if (!proceedingType || !triggerDate) return;
|
|
calculateMutation.mutate({
|
|
proceeding_type: proceedingType,
|
|
trigger_event_date: triggerDate,
|
|
});
|
|
}
|
|
|
|
const results = calculateMutation.data;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Input form */}
|
|
<form
|
|
onSubmit={handleCalculate}
|
|
className="rounded-lg border border-neutral-200 bg-white p-5"
|
|
>
|
|
<div className="flex items-center gap-2 text-sm font-medium text-neutral-900">
|
|
<Calculator className="h-4 w-4" />
|
|
Fristenberechnung
|
|
</div>
|
|
<div className="mt-4 grid gap-4 sm:grid-cols-3">
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
|
Verfahrensart
|
|
</label>
|
|
<select
|
|
value={proceedingType}
|
|
onChange={(e) => setProceedingType(e.target.value)}
|
|
disabled={typesLoading}
|
|
className={inputClass}
|
|
>
|
|
<option value="">Bitte wählen...</option>
|
|
{proceedingTypes?.map((pt) => (
|
|
<option key={pt.id} value={pt.code}>
|
|
{pt.name} ({pt.code})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1 block text-xs font-medium text-neutral-500">
|
|
Auslösedatum
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={triggerDate}
|
|
onChange={(e) => setTriggerDate(e.target.value)}
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<button
|
|
type="submit"
|
|
disabled={
|
|
!proceedingType ||
|
|
!triggerDate ||
|
|
calculateMutation.isPending
|
|
}
|
|
className="flex w-full items-center justify-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:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{calculateMutation.isPending ? "Berechne..." : "Berechnen"}
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Error */}
|
|
{calculateMutation.isError && (
|
|
<div className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
<AlertTriangle className="h-4 w-4 shrink-0" />
|
|
Fehler bei der Berechnung. Bitte Eingaben prüfen.
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{results && results.deadlines && (
|
|
<div className="animate-fade-in space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium text-neutral-900">
|
|
Berechnete Fristen
|
|
</h3>
|
|
<span className="text-xs text-neutral-500">
|
|
{results.deadlines.length} Fristen ab{" "}
|
|
{format(parseISO(results.trigger_event_date), "dd. MMM yyyy", {
|
|
locale: de,
|
|
})}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Timeline */}
|
|
<div className="relative rounded-lg border border-neutral-200 bg-white">
|
|
{results.deadlines.map((d: CalculatedDeadline, i: number) => {
|
|
const urgency = getTimelineUrgency(d.due_date);
|
|
const isLast = i === results.deadlines.length - 1;
|
|
|
|
return (
|
|
<div
|
|
key={d.rule_id}
|
|
className={`flex gap-3 px-4 py-3 ${!isLast ? "border-b border-neutral-100" : ""}`}
|
|
>
|
|
<div className="flex flex-col items-center pt-1">
|
|
<div
|
|
className={`h-2.5 w-2.5 shrink-0 rounded-full ${dotColors[urgency]}`}
|
|
/>
|
|
{!isLast && (
|
|
<div className="mt-1 w-px flex-1 bg-neutral-200" />
|
|
)}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-col gap-1 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
|
<span className="text-sm font-medium text-neutral-900">
|
|
{d.title}
|
|
</span>
|
|
<span className="shrink-0 text-sm font-medium tabular-nums text-neutral-700">
|
|
{format(parseISO(d.due_date), "dd.MM.yyyy")}
|
|
</span>
|
|
</div>
|
|
<div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-neutral-500">
|
|
{d.rule_code && <span>{d.rule_code}</span>}
|
|
{d.was_adjusted && (
|
|
<>
|
|
{d.rule_code && <span>·</span>}
|
|
<span className="text-amber-600">
|
|
Angepasst (Original:{" "}
|
|
{format(
|
|
parseISO(d.original_due_date),
|
|
"dd.MM.yyyy",
|
|
)}
|
|
)
|
|
</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Empty state */}
|
|
{!results && !calculateMutation.isPending && (
|
|
<div className="flex flex-col items-center rounded-lg border border-dashed border-neutral-300 bg-white px-6 py-12 text-center">
|
|
<div className="rounded-xl bg-neutral-100 p-3">
|
|
<Calendar className="h-6 w-6 text-neutral-400" />
|
|
</div>
|
|
<p className="mt-3 text-sm text-neutral-500">
|
|
Verfahrensart und Auslösedatum wählen, um Fristen zu berechnen
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|