263 lines
11 KiB
TypeScript
263 lines
11 KiB
TypeScript
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> </p>
|
||
|
||
<p>Oldenburg, den {{datum}}</p>
|
||
|
||
<p> </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>
|
||
);
|
||
}
|