- 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
175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { useParams, useRouter } from "next/navigation";
|
|
import { api } from "@/lib/api";
|
|
import type { DocumentTemplate } from "@/lib/types";
|
|
import { TEMPLATE_CATEGORY_LABELS } from "@/lib/types";
|
|
import { Breadcrumb } from "@/components/layout/Breadcrumb";
|
|
import { TemplateEditor } from "@/components/templates/TemplateEditor";
|
|
import Link from "next/link";
|
|
import {
|
|
Loader2,
|
|
Lock,
|
|
Trash2,
|
|
FileDown,
|
|
ArrowRight,
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { useState } from "react";
|
|
|
|
export default function TemplateDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const router = useRouter();
|
|
const queryClient = useQueryClient();
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
|
|
const { data: template, isLoading } = useQuery({
|
|
queryKey: ["template", id],
|
|
queryFn: () => api.get<DocumentTemplate>(`/templates/${id}`),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: () => api.delete(`/templates/${id}`),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
|
toast.success("Vorlage gelöscht");
|
|
router.push("/vorlagen");
|
|
},
|
|
onError: () => toast.error("Fehler beim Löschen"),
|
|
});
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: (data: Partial<DocumentTemplate>) =>
|
|
api.put<DocumentTemplate>(`/templates/${id}`, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["template", id] });
|
|
queryClient.invalidateQueries({ queryKey: ["templates"] });
|
|
toast.success("Vorlage gespeichert");
|
|
setIsEditing(false);
|
|
},
|
|
onError: () => toast.error("Fehler beim Speichern"),
|
|
});
|
|
|
|
if (isLoading) {
|
|
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 },
|
|
]}
|
|
/>
|
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h1 className="text-lg font-semibold text-neutral-900">
|
|
{template.name}
|
|
</h1>
|
|
{template.is_system && (
|
|
<Lock className="h-4 w-4 text-neutral-400" aria-label="Systemvorlage" />
|
|
)}
|
|
</div>
|
|
<div className="mt-1 flex items-center gap-2">
|
|
<span className="rounded-full bg-neutral-100 px-2 py-0.5 text-xs text-neutral-600">
|
|
{TEMPLATE_CATEGORY_LABELS[template.category] ?? template.category}
|
|
</span>
|
|
{template.description && (
|
|
<span className="text-xs text-neutral-500">
|
|
{template.description}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Link
|
|
href={`/vorlagen/${id}/render`}
|
|
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"
|
|
>
|
|
<FileDown className="h-3.5 w-3.5" />
|
|
Dokument erstellen
|
|
<ArrowRight className="h-3.5 w-3.5" />
|
|
</Link>
|
|
{!template.is_system && (
|
|
<>
|
|
<button
|
|
onClick={() => setIsEditing(!isEditing)}
|
|
className="rounded-md border border-neutral-200 bg-white px-3 py-1.5 text-sm text-neutral-700 transition-colors hover:bg-neutral-50"
|
|
>
|
|
{isEditing ? "Abbrechen" : "Bearbeiten"}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (confirm("Vorlage wirklich löschen?")) {
|
|
deleteMutation.mutate();
|
|
}
|
|
}}
|
|
className="rounded-md border border-red-200 bg-white p-1.5 text-red-600 transition-colors hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{isEditing ? (
|
|
<TemplateEditor
|
|
template={template}
|
|
onSave={(data) => updateMutation.mutate(data)}
|
|
isSaving={updateMutation.isPending}
|
|
/>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Variables */}
|
|
{template.variables && template.variables.length > 0 && (
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-4">
|
|
<h3 className="mb-2 text-sm font-medium text-neutral-700">
|
|
Variablen
|
|
</h3>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{template.variables.map((v: string) => (
|
|
<code
|
|
key={v}
|
|
className="rounded bg-neutral-100 px-1.5 py-0.5 text-xs text-neutral-600"
|
|
>
|
|
{`{{${v}}}`}
|
|
</code>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content preview */}
|
|
<div className="rounded-lg border border-neutral-200 bg-white p-6">
|
|
<h3 className="mb-3 text-sm font-medium text-neutral-700">
|
|
Vorschau
|
|
</h3>
|
|
<div className="prose prose-sm prose-neutral max-w-none whitespace-pre-wrap font-mono text-xs leading-relaxed text-neutral-700">
|
|
{template.content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|