gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery

This commit is contained in:
2026-03-21 11:59:53 +01:00
parent 09e87c951b
commit c3edb8ad2e
1491 changed files with 265550 additions and 1292 deletions
@@ -0,0 +1,262 @@
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>
);
}