- Database: kanzlai.document_templates table with RLS policies
- Seed: 4 system templates (Klageerwiderung UPC, Berufungsschrift,
Mandatsbestätigung, Kostenrechnung)
- Backend: TemplateService (CRUD + render), TemplateHandler with
endpoints: GET/POST /api/templates, GET/PUT/DELETE /api/templates/{id},
POST /api/templates/{id}/render?case_id=X
- Template variables: case.*, party.*, tenant.*, user.*, date.*, deadline.*
- Frontend: /vorlagen page with category filters, template detail/editor,
render flow (select case -> preview -> copy/download), variable toolbar
- Quick action: "Schriftsatz erstellen" button on case detail page
- Also: resolved merge conflicts between audit-trail and role-based branches,
added missing Notification/AuditLog types to frontend
178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
import { useParams } from "next/navigation";
|
|
import { api } from "@/lib/api";
|
|
import type { DocumentTemplate, Case, RenderResponse } from "@/lib/types";
|
|
import { Breadcrumb } from "@/components/layout/Breadcrumb";
|
|
import {
|
|
Loader2,
|
|
FileDown,
|
|
Copy,
|
|
Check,
|
|
} from "lucide-react";
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
export default function RenderTemplatePage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const [selectedCaseId, setSelectedCaseId] = useState("");
|
|
const [rendered, setRendered] = useState<RenderResponse | null>(null);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const { data: template, isLoading: templateLoading } = useQuery({
|
|
queryKey: ["template", id],
|
|
queryFn: () => api.get<DocumentTemplate>(`/templates/${id}`),
|
|
});
|
|
|
|
const { data: casesData, isLoading: casesLoading } = useQuery({
|
|
queryKey: ["cases"],
|
|
queryFn: () =>
|
|
api.get<{ data: Case[]; total: number }>("/cases?limit=100"),
|
|
});
|
|
|
|
const cases = casesData?.data ?? [];
|
|
|
|
const renderMutation = useMutation({
|
|
mutationFn: () =>
|
|
api.post<RenderResponse>(
|
|
`/templates/${id}/render${selectedCaseId ? `?case_id=${selectedCaseId}` : ""}`,
|
|
),
|
|
onSuccess: (data) => setRendered(data),
|
|
onError: () => toast.error("Fehler beim Erstellen"),
|
|
});
|
|
|
|
const handleCopy = async () => {
|
|
if (!rendered) return;
|
|
await navigator.clipboard.writeText(rendered.content);
|
|
setCopied(true);
|
|
toast.success("In Zwischenablage kopiert");
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
const handleDownload = () => {
|
|
if (!rendered) return;
|
|
const blob = new Blob([rendered.content], { type: "text/markdown" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${rendered.name.replace(/\s+/g, "_")}.md`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
toast.success("Dokument heruntergeladen");
|
|
};
|
|
|
|
if (templateLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-5 w-5 animate-spin text-neutral-400" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!template) {
|
|
return (
|
|
<div className="py-12 text-center text-sm text-neutral-500">
|
|
Vorlage nicht gefunden
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="animate-fade-in space-y-4">
|
|
<Breadcrumb
|
|
items={[
|
|
{ label: "Dashboard", href: "/dashboard" },
|
|
{ label: "Vorlagen", href: "/vorlagen" },
|
|
{ label: template.name, href: `/vorlagen/${id}` },
|
|
{ label: "Dokument erstellen" },
|
|
]}
|
|
/>
|
|
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
Dokument erstellen
|
|
</h1>
|
|
<p className="text-sm text-neutral-500">
|
|
Vorlage “{template.name}” mit Falldaten befüllen
|
|
</p>
|
|
|
|
{/* Step 1: Select case */}
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-4">
|
|
<h3 className="mb-3 text-sm font-medium text-neutral-700">
|
|
1. Akte auswählen
|
|
</h3>
|
|
{casesLoading ? (
|
|
<Loader2 className="h-4 w-4 animate-spin text-neutral-400" />
|
|
) : (
|
|
<select
|
|
value={selectedCaseId}
|
|
onChange={(e) => {
|
|
setSelectedCaseId(e.target.value);
|
|
setRendered(null);
|
|
}}
|
|
className="w-full rounded-md border border-neutral-200 px-3 py-2 text-sm text-neutral-700 focus:border-neutral-400 focus:outline-none"
|
|
>
|
|
<option value="">Ohne Akte (nur Datumsvariablen)</option>
|
|
{cases.map((c) => (
|
|
<option key={c.id} value={c.id}>
|
|
{c.case_number} — {c.title}
|
|
</option>
|
|
))}
|
|
</select>
|
|
)}
|
|
</div>
|
|
|
|
{/* Step 2: Render */}
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-medium text-neutral-700">
|
|
2. Vorschau erstellen
|
|
</h3>
|
|
<button
|
|
onClick={() => renderMutation.mutate()}
|
|
disabled={renderMutation.isPending}
|
|
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 disabled:opacity-50"
|
|
>
|
|
{renderMutation.isPending ? (
|
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
) : (
|
|
<FileDown className="h-3.5 w-3.5" />
|
|
)}
|
|
Vorschau
|
|
</button>
|
|
</div>
|
|
|
|
{rendered && (
|
|
<div className="mt-4">
|
|
<div className="mb-2 flex items-center justify-end gap-2">
|
|
<button
|
|
onClick={handleCopy}
|
|
className="flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1 text-xs text-neutral-600 transition-colors hover:bg-neutral-50"
|
|
>
|
|
{copied ? (
|
|
<Check className="h-3 w-3" />
|
|
) : (
|
|
<Copy className="h-3 w-3" />
|
|
)}
|
|
{copied ? "Kopiert" : "Kopieren"}
|
|
</button>
|
|
<button
|
|
onClick={handleDownload}
|
|
className="flex items-center gap-1 rounded-md border border-neutral-200 px-2.5 py-1 text-xs text-neutral-600 transition-colors hover:bg-neutral-50"
|
|
>
|
|
<FileDown className="h-3 w-3" />
|
|
Herunterladen
|
|
</button>
|
|
</div>
|
|
<div className="rounded-lg border border-neutral-200 bg-neutral-50 p-6">
|
|
<div className="whitespace-pre-wrap font-mono text-xs leading-relaxed text-neutral-700">
|
|
{rendered.content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|