Files
opencrm/frontend/src/pages/settings/AuthorizationTemplateEditor.tsx
T

263 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import TiptapLink from '@tiptap/extension-link';
import { gdprApi } from '../../services/api';
import Button from '../../components/ui/Button';
import Card from '../../components/ui/Card';
import { ArrowLeft, Save, Eye, Bold, Italic, List, ListOrdered, Heading1, Heading2, Heading3, Link as LinkIcon, Undo, Redo, Type } from 'lucide-react';
import { Link as RouterLink } from 'react-router-dom';
const DEFAULT_TEMPLATE = `<h1>Vollmacht</h1>
<p>Hiermit bevollmächtige ich,</p>
<p><strong>{{vollmachtgeber_vorname}} {{vollmachtgeber_nachname}}</strong><br>
Kundennummer: {{vollmachtgeber_kundennummer}}</p>
<p>den/die</p>
<p><strong>{{bevollmaechtigter_vorname}} {{bevollmaechtigter_nachname}}</strong><br>
Kundennummer: {{bevollmaechtigter_kundennummer}}</p>
<p>mich in allen Angelegenheiten rund um meine Telekommunikationsverträge bei der Firma Hacker-Net Telekommunikation Stefan Hacker zu vertreten. Dies umfasst insbesondere:</p>
<ul>
<li>Einsicht in meine Vertragsdaten, Rechnungen und Kundendaten</li>
<li>Kommunikation mit dem Kundenservice in meinem Namen</li>
<li>Entgegennahme von Informationen zu meinen Verträgen</li>
</ul>
<p>Diese Vollmacht gilt bis auf Widerruf. Ich kann sie jederzeit schriftlich oder über das Kundenportal widerrufen.</p>
<h2>Datenschutzhinweis</h2>
<p>Mit der Erteilung dieser Vollmacht erkläre ich mich damit einverstanden, dass die oben genannte bevollmächtigte Person Zugriff auf meine bei Hacker-Net Telekommunikation gespeicherten personenbezogenen Daten erhält. Dies geschieht auf Grundlage meiner ausdrücklichen Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO.</p>
<p>Ich bin darüber informiert, dass ich diese Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen kann.</p>
<p>&nbsp;</p>
<p>Oldenburg, den {{datum}}</p>
<p>&nbsp;</p>
<p>_______________________________<br>
Unterschrift des Vollmachtgebers</p>
<p style="color: #9ca3af; font-size: 12px; margin-top: 32px;">
Hacker-Net Telekommunikation Stefan Hacker<br>
Am Wunderburgpark 5b, 26135 Oldenburg<br>
info@hacker-net.de
</p>`;
const PLACEHOLDERS = [
{ key: '{{vollmachtgeber_vorname}}', label: 'Vorname (Vollmachtgeber)' },
{ key: '{{vollmachtgeber_nachname}}', label: 'Nachname (Vollmachtgeber)' },
{ key: '{{vollmachtgeber_kundennummer}}', label: 'Kundennr. (Vollmachtgeber)' },
{ key: '{{bevollmaechtigter_vorname}}', label: 'Vorname (Bevollmächtigter)' },
{ key: '{{bevollmaechtigter_nachname}}', label: 'Nachname (Bevollmächtigter)' },
{ key: '{{bevollmaechtigter_kundennummer}}', label: 'Kundennr. (Bevollmächtigter)' },
{ key: '{{datum}}', label: 'Aktuelles Datum' },
];
function MenuBar({ editor }: { editor: ReturnType<typeof useEditor> }) {
if (!editor) return null;
const setLink = useCallback(() => {
const previousUrl = editor.getAttributes('link').href;
const url = window.prompt('URL eingeben:', previousUrl);
if (url === null) return;
if (url === '') {
editor.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
}, [editor]);
const btnClass = (active: boolean) =>
`p-1.5 rounded hover:bg-gray-200 transition-colors ${active ? 'bg-gray-200 text-blue-600' : 'text-gray-600'}`;
return (
<div className="flex flex-wrap items-center gap-1 p-2 border-b bg-gray-50">
<button type="button" onClick={() => editor.chain().focus().toggleBold().run()} className={btnClass(editor.isActive('bold'))} title="Fett">
<Bold className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().toggleItalic().run()} className={btnClass(editor.isActive('italic'))} title="Kursiv">
<Italic className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-gray-300 mx-1" />
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()} className={btnClass(editor.isActive('heading', { level: 1 }))} title="Überschrift 1">
<Heading1 className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()} className={btnClass(editor.isActive('heading', { level: 2 }))} title="Überschrift 2">
<Heading2 className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()} className={btnClass(editor.isActive('heading', { level: 3 }))} title="Überschrift 3">
<Heading3 className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().setParagraph().run()} className={btnClass(editor.isActive('paragraph'))} title="Absatz">
<Type className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-gray-300 mx-1" />
<button type="button" onClick={() => editor.chain().focus().toggleBulletList().run()} className={btnClass(editor.isActive('bulletList'))} title="Aufzählung">
<List className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().toggleOrderedList().run()} className={btnClass(editor.isActive('orderedList'))} title="Nummerierung">
<ListOrdered className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-gray-300 mx-1" />
<button type="button" onClick={setLink} className={btnClass(editor.isActive('link'))} title="Link">
<LinkIcon className="w-4 h-4" />
</button>
<div className="w-px h-5 bg-gray-300 mx-1" />
<button type="button" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Rückgängig">
<Undo className="w-4 h-4" />
</button>
<button type="button" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} className="p-1.5 rounded hover:bg-gray-200 text-gray-600 disabled:opacity-30" title="Wiederherstellen">
<Redo className="w-4 h-4" />
</button>
</div>
);
}
export default function AuthorizationTemplateEditor() {
const queryClient = useQueryClient();
const [showPreview, setShowPreview] = useState(false);
const [saved, setSaved] = useState(false);
const { data: templateData, isLoading } = useQuery({
queryKey: ['authorization-template'],
queryFn: () => gdprApi.getAuthorizationTemplate(),
});
const saveMutation = useMutation({
mutationFn: (html: string) => gdprApi.updateAuthorizationTemplate(html),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['authorization-template'] });
setSaved(true);
setTimeout(() => setSaved(false), 2000);
},
});
// Wenn noch keine Vorlage gespeichert: Default-Template verwenden
const initialContent = templateData?.data?.html || DEFAULT_TEMPLATE;
const editor = useEditor({
extensions: [
StarterKit,
TiptapLink.configure({
openOnClick: false,
HTMLAttributes: { target: '_blank', rel: 'noopener noreferrer' },
}),
],
content: initialContent,
editorProps: {
attributes: {
class: 'prose prose-sm max-w-none p-4 min-h-[400px] focus:outline-none',
},
},
}, [initialContent]);
const insertPlaceholder = (key: string) => {
if (editor) {
editor.chain().focus().insertContent(key).run();
}
};
const handleSave = () => {
if (editor) {
saveMutation.mutate(editor.getHTML());
}
};
const handleResetToDefault = () => {
if (confirm('Vorlage auf den Standardtext zurücksetzen? Alle Änderungen gehen verloren.')) {
editor?.commands.setContent(DEFAULT_TEMPLATE);
}
};
if (isLoading) {
return <div className="text-center py-8 text-gray-500">Laden...</div>;
}
return (
<div>
<div className="flex items-center gap-4 mb-6">
<RouterLink to="/settings">
<Button variant="ghost" size="sm">
<ArrowLeft className="w-4 h-4" />
</Button>
</RouterLink>
<h1 className="text-2xl font-bold flex-1">Vollmacht-Vorlage bearbeiten</h1>
<Button
variant="ghost"
size="sm"
onClick={handleResetToDefault}
>
Standardtext laden
</Button>
<Button
variant="secondary"
onClick={() => setShowPreview(!showPreview)}
>
<Eye className="w-4 h-4 mr-2" />
{showPreview ? 'Editor' : 'Vorschau'}
</Button>
<Button onClick={handleSave} disabled={saveMutation.isPending}>
<Save className="w-4 h-4 mr-2" />
{saved ? 'Gespeichert!' : saveMutation.isPending ? 'Speichern...' : 'Speichern'}
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Editor / Vorschau */}
<div className="lg:col-span-3">
{showPreview ? (
<Card>
<div
className="prose prose-sm max-w-none p-4"
dangerouslySetInnerHTML={{ __html: editor?.getHTML() || '' }}
/>
</Card>
) : (
<div className="border rounded-lg bg-white overflow-hidden">
<MenuBar editor={editor} />
<EditorContent editor={editor} />
</div>
)}
</div>
{/* Platzhalter-Sidebar */}
<div className="lg:col-span-1">
<Card title="Platzhalter">
<p className="text-xs text-gray-500 mb-3">
Klicken Sie auf einen Platzhalter, um ihn an der Cursorposition einzufügen.
</p>
<div className="space-y-2">
{PLACEHOLDERS.map((p) => (
<button
key={p.key}
onClick={() => insertPlaceholder(p.key)}
className="w-full text-left px-3 py-2 text-sm bg-gray-50 border rounded hover:bg-blue-50 hover:border-blue-300 transition-colors"
>
<span className="font-mono text-blue-600 text-xs">{p.key}</span>
<br />
<span className="text-gray-600">{p.label}</span>
</button>
))}
</div>
</Card>
{saveMutation.isError && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
Fehler beim Speichern. Bitte versuchen Sie es erneut.
</div>
)}
</div>
</div>
</div>
);
}