29eceef26b
- PDF-Template-Editor in Einstellungen: Vorlagen hochladen, Formularfelder automatisch auslesen, CRM-Felder zuordnen - PDF-Vorschau mit annotierten Feldnamen, seitenweise Sortierung der Felder - Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail) - Dynamische Rufnummern-Felder mit Vorwahl-Extraktion und konfigurierbarer Maximalanzahl - Nicht zugeordnete Felder bleiben editierbar im generierten PDF - Eigentümer-Felder mit Namens-Kombinationen (Firma+Name etc.) und Fallback auf Kundendaten - Stressfrei-E-Mail als Feld-Option im Template-Editor - Objekttyp, Lage und Lage des Anschlusses als neue Felder bei Festnetz-Verträgen (DSL, Glasfaser, Kabel) - Bankverbindung-Fallback: wenn keine am Vertrag verknüpft, wird automatisch die neueste aktive des Kunden genommen Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
499 lines
22 KiB
TypeScript
499 lines
22 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { pdfTemplateApi, contractApi } from '../../services/api';
|
|
import type { PdfTemplate, CrmField, Contract } from '../../types';
|
|
import Card from '../../components/ui/Card';
|
|
import Button from '../../components/ui/Button';
|
|
import Input from '../../components/ui/Input';
|
|
import Badge from '../../components/ui/Badge';
|
|
import Modal from '../../components/ui/Modal';
|
|
import { ArrowLeft, Plus, Edit, Trash2, FileText, Upload, Link2, Eye, Play } from 'lucide-react';
|
|
|
|
export default function PdfTemplates() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [editingTemplate, setEditingTemplate] = useState<PdfTemplate | null>(null);
|
|
const [mappingTemplate, setMappingTemplate] = useState<PdfTemplate | null>(null);
|
|
const [testTemplate, setTestTemplate] = useState<PdfTemplate | null>(null);
|
|
|
|
const { data: templatesData, isLoading } = useQuery({
|
|
queryKey: ['pdf-templates'],
|
|
queryFn: () => pdfTemplateApi.getAll(),
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: (id: number) => pdfTemplateApi.delete(id),
|
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['pdf-templates'] }),
|
|
});
|
|
|
|
const templates = templatesData?.data || [];
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-4 mb-6">
|
|
<Button variant="ghost" onClick={() => navigate('/settings')}>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
</Button>
|
|
<h1 className="text-2xl font-bold flex-1">Auftragsvorlagen</h1>
|
|
<Button onClick={() => setShowCreateModal(true)}>
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
Vorlage hochladen
|
|
</Button>
|
|
</div>
|
|
|
|
<Card className="mb-6">
|
|
<p className="text-sm text-gray-600">
|
|
Laden Sie PDF-Formulare hoch (z.B. EWE Auftragsformular) und verknüpfen Sie die Formularfelder mit CRM-Daten.
|
|
Beim Erstellen eines Auftrags werden die Felder automatisch mit den Kundendaten befüllt.
|
|
</p>
|
|
</Card>
|
|
|
|
{isLoading ? (
|
|
<div className="text-center py-8 text-gray-500">Laden...</div>
|
|
) : templates.length === 0 ? (
|
|
<Card>
|
|
<div className="text-center py-8 text-gray-500">
|
|
Noch keine Vorlagen vorhanden. Laden Sie eine PDF-Vorlage hoch.
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{templates.map((t) => (
|
|
<Card key={t.id}>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-start gap-3">
|
|
<FileText className="w-5 h-5 text-blue-500 mt-0.5" />
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="font-semibold">{t.name}</h3>
|
|
{t.providerName && <Badge variant="info">{t.providerName}</Badge>}
|
|
{!t.isActive && <Badge variant="danger">Inaktiv</Badge>}
|
|
</div>
|
|
{t.description && <p className="text-sm text-gray-500 mt-1">{t.description}</p>}
|
|
<p className="text-xs text-gray-400 mt-1">
|
|
Datei: {t.originalName}
|
|
{t.maxPhoneFields && ` · Max. ${t.maxPhoneFields} Rufnummern`}
|
|
</p>
|
|
{(() => {
|
|
const mapping = JSON.parse(t.fieldMapping || '{}');
|
|
const count = Object.keys(mapping).length;
|
|
return (
|
|
<p className="text-xs text-gray-400">
|
|
{count > 0 ? `${count} Felder verknüpft` : 'Noch keine Felder verknüpft'}
|
|
</p>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" size="sm" onClick={() => setMappingTemplate(t)}>
|
|
<Link2 className="w-4 h-4 mr-1" />
|
|
Felder zuordnen
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => setTestTemplate(t)} title="Testvorschau mit Vertragsdaten">
|
|
<Play className="w-4 h-4 text-green-500" />
|
|
</Button>
|
|
<a href={`/api${t.templatePath}`} target="_blank" rel="noopener noreferrer">
|
|
<Button variant="ghost" size="sm" title="Leere Vorlage anzeigen">
|
|
<Eye className="w-4 h-4" />
|
|
</Button>
|
|
</a>
|
|
<Button variant="ghost" size="sm" onClick={() => setEditingTemplate(t)} title="Bearbeiten">
|
|
<Edit className="w-4 h-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="sm" onClick={() => {
|
|
if (confirm(`Vorlage "${t.name}" wirklich löschen?`)) deleteMutation.mutate(t.id);
|
|
}} title="Löschen">
|
|
<Trash2 className="w-4 h-4 text-red-500" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Erstellen Modal */}
|
|
{showCreateModal && (
|
|
<CreateTemplateModal onClose={() => { setShowCreateModal(false); queryClient.invalidateQueries({ queryKey: ['pdf-templates'] }); }} />
|
|
)}
|
|
|
|
{/* Bearbeiten Modal */}
|
|
{editingTemplate && (
|
|
<EditTemplateModal template={editingTemplate} onClose={() => { setEditingTemplate(null); queryClient.invalidateQueries({ queryKey: ['pdf-templates'] }); }} />
|
|
)}
|
|
|
|
{/* Feld-Mapping Modal */}
|
|
{mappingTemplate && (
|
|
<FieldMappingModal template={mappingTemplate} onClose={() => { setMappingTemplate(null); queryClient.invalidateQueries({ queryKey: ['pdf-templates'] }); }} />
|
|
)}
|
|
|
|
{/* Test-Vorschau Modal */}
|
|
{testTemplate && (
|
|
<TestPreviewModal template={testTemplate} onClose={() => setTestTemplate(null)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ==================== CREATE MODAL ====================
|
|
|
|
function CreateTemplateModal({ onClose }: { onClose: () => void }) {
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [providerName, setProviderName] = useState('');
|
|
const [maxPhoneFields, setMaxPhoneFields] = useState('8');
|
|
const [file, setFile] = useState<File | null>(null);
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
if (description) formData.append('description', description);
|
|
if (providerName) formData.append('providerName', providerName);
|
|
formData.append('maxPhoneFields', maxPhoneFields);
|
|
formData.append('template', file!);
|
|
return pdfTemplateApi.create(formData);
|
|
},
|
|
onSuccess: () => onClose(),
|
|
});
|
|
|
|
return (
|
|
<Modal isOpen={true} onClose={onClose} title="Neue Auftragsvorlage">
|
|
<div className="space-y-4">
|
|
<Input label="Name *" value={name} onChange={(e) => setName(e.target.value)} placeholder="z.B. EWE Auftragsformular" required />
|
|
<Input label="Anbieter" value={providerName} onChange={(e) => setProviderName(e.target.value)} placeholder="z.B. EWE" />
|
|
<Input label="Beschreibung" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optionale Beschreibung" />
|
|
<Input label="Max. Rufnummern-Felder" type="number" value={maxPhoneFields} onChange={(e) => setMaxPhoneFields(e.target.value)} />
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">PDF-Vorlage *</label>
|
|
<label className="flex items-center justify-center gap-2 p-4 border-2 border-dashed rounded-lg cursor-pointer hover:bg-gray-50">
|
|
<Upload className="w-5 h-5 text-gray-400" />
|
|
<span className="text-sm text-gray-600">{file ? file.name : 'PDF-Datei auswählen'}</span>
|
|
<input type="file" accept=".pdf" className="hidden" onChange={(e) => setFile(e.target.files?.[0] || null)} />
|
|
</label>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Die PDF muss Formularfelder enthalten. Diese werden nach dem Hochladen automatisch erkannt.
|
|
</p>
|
|
</div>
|
|
|
|
{createMutation.isError && (
|
|
<div className="p-3 bg-red-50 text-red-700 text-sm rounded-lg">
|
|
{createMutation.error instanceof Error ? createMutation.error.message : 'Fehler beim Erstellen'}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
|
|
<Button onClick={() => createMutation.mutate()} disabled={!name || !file || createMutation.isPending}>
|
|
{createMutation.isPending ? 'Hochladen...' : 'Hochladen'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
// ==================== EDIT MODAL ====================
|
|
|
|
function EditTemplateModal({ template, onClose }: { template: PdfTemplate; onClose: () => void }) {
|
|
const [name, setName] = useState(template.name);
|
|
const [description, setDescription] = useState(template.description || '');
|
|
const [providerName, setProviderName] = useState(template.providerName || '');
|
|
const [maxPhoneFields, setMaxPhoneFields] = useState(template.maxPhoneFields?.toString() || '8');
|
|
const [isActive, setIsActive] = useState(template.isActive);
|
|
|
|
const updateMutation = useMutation({
|
|
mutationFn: () => pdfTemplateApi.update(template.id, { name, description, providerName, maxPhoneFields: parseInt(maxPhoneFields), isActive }),
|
|
onSuccess: () => onClose(),
|
|
});
|
|
|
|
return (
|
|
<Modal isOpen={true} onClose={onClose} title="Vorlage bearbeiten">
|
|
<div className="space-y-4">
|
|
<Input label="Name" value={name} onChange={(e) => setName(e.target.value)} required />
|
|
<Input label="Anbieter" value={providerName} onChange={(e) => setProviderName(e.target.value)} />
|
|
<Input label="Beschreibung" value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
<Input label="Max. Rufnummern-Felder" type="number" value={maxPhoneFields} onChange={(e) => setMaxPhoneFields(e.target.value)} />
|
|
<label className="flex items-center gap-2">
|
|
<input type="checkbox" checked={isActive} onChange={(e) => setIsActive(e.target.checked)} className="rounded" />
|
|
<span className="text-sm">Aktiv</span>
|
|
</label>
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
|
|
<Button onClick={() => updateMutation.mutate()} disabled={updateMutation.isPending}>
|
|
{updateMutation.isPending ? 'Speichern...' : 'Speichern'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
// ==================== FIELD MAPPING MODAL ====================
|
|
|
|
function FieldMappingModal({ template, onClose }: { template: PdfTemplate; onClose: () => void }) {
|
|
const [mapping, setMapping] = useState<Record<string, string>>(JSON.parse(template.fieldMapping || '{}'));
|
|
|
|
const { data: fieldsData } = useQuery({
|
|
queryKey: ['pdf-fields', template.id],
|
|
queryFn: () => pdfTemplateApi.getFields(template.id),
|
|
});
|
|
|
|
const { data: crmFieldsData } = useQuery({
|
|
queryKey: ['crm-fields', template.maxPhoneFields],
|
|
queryFn: () => pdfTemplateApi.getCrmFields(template.maxPhoneFields || 8),
|
|
});
|
|
|
|
const saveMutation = useMutation({
|
|
mutationFn: () => pdfTemplateApi.update(template.id, { fieldMapping: mapping }),
|
|
onSuccess: () => onClose(),
|
|
});
|
|
|
|
const pdfFields = fieldsData?.data || [];
|
|
const totalPages = (fieldsData as any)?.totalPages || 1;
|
|
const crmFields = crmFieldsData?.data || [];
|
|
|
|
// CRM-Felder nach Gruppe
|
|
const crmGroups = crmFields.reduce((acc, f) => {
|
|
if (!acc[f.group]) acc[f.group] = [];
|
|
acc[f.group].push(f);
|
|
return acc;
|
|
}, {} as Record<string, CrmField[]>);
|
|
|
|
const [highlightedField, setHighlightedField] = useState<string | null>(null);
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex">
|
|
{/* PDF-Vorschau links (nur zur Ansicht, nicht interaktiv) */}
|
|
<div className="w-1/2 bg-gray-800 flex flex-col">
|
|
<div className="p-3 bg-gray-900 text-white text-sm flex items-center justify-between">
|
|
<span>PDF-Vorschau mit Feldnamen</span>
|
|
<span className="text-gray-400 text-xs">[Feldname] zeigt wo das Feld in der PDF liegt</span>
|
|
</div>
|
|
<iframe
|
|
src={`/api/pdf-templates/${template.id}/preview?token=${localStorage.getItem('token')}`}
|
|
className="flex-1 w-full bg-white"
|
|
title="PDF Vorschau mit Feldnamen"
|
|
/>
|
|
</div>
|
|
|
|
{/* Zuordnung rechts */}
|
|
<div className="w-1/2 bg-white flex flex-col">
|
|
<div className="p-4 border-b flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">Felder zuordnen</h2>
|
|
<p className="text-xs text-gray-500">
|
|
PDF-Feld links in der Vorschau finden, dann rechts das CRM-Feld zuordnen
|
|
</p>
|
|
</div>
|
|
<Badge>{Object.keys(mapping).length} / {pdfFields.length}</Badge>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto p-4">
|
|
{pdfFields.length === 0 ? (
|
|
<div className="text-center py-8 text-amber-600 bg-amber-50 rounded-lg">
|
|
<p className="font-medium">Keine Formularfelder gefunden.</p>
|
|
<p className="text-sm mt-1">Die PDF muss interaktive Formularfelder enthalten.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex gap-2 mb-3">
|
|
<button
|
|
onClick={() => setHighlightedField('all')}
|
|
className={`px-3 py-1 rounded text-xs font-medium ${highlightedField === 'all' || !highlightedField ? 'bg-gray-200 text-gray-800' : 'text-gray-500 hover:bg-gray-100'}`}
|
|
>
|
|
Alle ({pdfFields.length})
|
|
</button>
|
|
<button
|
|
onClick={() => setHighlightedField('open')}
|
|
className={`px-3 py-1 rounded text-xs font-medium ${highlightedField === 'open' ? 'bg-amber-100 text-amber-800' : 'text-gray-500 hover:bg-gray-100'}`}
|
|
>
|
|
Offen ({pdfFields.filter(f => !mapping[f.name]).length})
|
|
</button>
|
|
<button
|
|
onClick={() => setHighlightedField('done')}
|
|
className={`px-3 py-1 rounded text-xs font-medium ${highlightedField === 'done' ? 'bg-green-100 text-green-800' : 'text-gray-500 hover:bg-gray-100'}`}
|
|
>
|
|
Zugeordnet ({pdfFields.filter(f => !!mapping[f.name]).length})
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
{(() => {
|
|
const filtered = pdfFields.filter((f: any) => {
|
|
if (highlightedField === 'open') return !mapping[f.name];
|
|
if (highlightedField === 'done') return !!mapping[f.name];
|
|
return true;
|
|
});
|
|
|
|
let lastPage = -1;
|
|
let fieldNum = 0;
|
|
|
|
return filtered.map((pdfField: any) => {
|
|
fieldNum++;
|
|
const isAssigned = !!mapping[pdfField.name];
|
|
const assignedLabel = isAssigned
|
|
? crmFields.find(c => c.path === mapping[pdfField.name])?.label
|
|
: null;
|
|
const showPageHeader = totalPages > 1 && pdfField.page !== lastPage;
|
|
lastPage = pdfField.page;
|
|
|
|
return (
|
|
<div key={pdfField.name}>
|
|
{showPageHeader && (
|
|
<div className="flex items-center gap-2 py-2 mt-2 first:mt-0">
|
|
<div className="flex-1 border-t border-blue-200" />
|
|
<span className="text-xs font-medium text-blue-600 px-2 bg-blue-50 rounded-full">
|
|
Seite {pdfField.page + 1}
|
|
</span>
|
|
<div className="flex-1 border-t border-blue-200" />
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`p-3 rounded-lg border transition-colors ${
|
|
isAssigned ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-gray-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="text-xs text-gray-400 w-5">{fieldNum}.</span>
|
|
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${isAssigned ? 'bg-green-500' : 'bg-amber-400'}`} />
|
|
<span className="font-mono text-sm font-medium truncate" title={pdfField.name}>{pdfField.name}</span>
|
|
<Badge variant="default">{pdfField.type}</Badge>
|
|
{assignedLabel && (
|
|
<span className="text-xs text-green-700 ml-auto flex-shrink-0">→ {assignedLabel}</span>
|
|
)}
|
|
</div>
|
|
<select
|
|
value={mapping[pdfField.name] || ''}
|
|
onChange={(e) => {
|
|
const newMapping = { ...mapping };
|
|
if (e.target.value) newMapping[pdfField.name] = e.target.value;
|
|
else delete newMapping[pdfField.name];
|
|
setMapping(newMapping);
|
|
}}
|
|
className={`block w-full px-3 py-1.5 border rounded text-sm ${
|
|
isAssigned ? 'border-green-300 bg-white' : 'border-gray-300'
|
|
}`}
|
|
>
|
|
<option value="">-- Nicht zuordnen --</option>
|
|
{Object.entries(crmGroups).map(([group, fields]) => (
|
|
<optgroup key={group} label={group}>
|
|
{fields.map((f) => (
|
|
<option key={f.path} value={f.path}>{f.label}</option>
|
|
))}
|
|
</optgroup>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
})()}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-4 border-t flex justify-between items-center">
|
|
<p className="text-xs text-gray-500">
|
|
{Object.keys(mapping).length} von {pdfFields.length} zugeordnet
|
|
</p>
|
|
<div className="flex gap-3">
|
|
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
|
|
<Button onClick={() => saveMutation.mutate()} disabled={saveMutation.isPending}>
|
|
{saveMutation.isPending ? 'Speichern...' : 'Speichern'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ==================== TEST PREVIEW MODAL ====================
|
|
|
|
function TestPreviewModal({ template, onClose }: { template: PdfTemplate; onClose: () => void }) {
|
|
const [search, setSearch] = useState('');
|
|
const [selectedContractId, setSelectedContractId] = useState<number | null>(null);
|
|
|
|
const { data: contractsData, isLoading } = useQuery({
|
|
queryKey: ['contracts-search', search],
|
|
queryFn: () => contractApi.getAll({ search: search || undefined, limit: 10 }),
|
|
enabled: search.length >= 2,
|
|
});
|
|
|
|
const contracts: Contract[] = contractsData?.data || [];
|
|
const token = localStorage.getItem('token');
|
|
|
|
const handleGenerate = () => {
|
|
if (!selectedContractId) return;
|
|
const url = `${pdfTemplateApi.generateUrl(template.id, selectedContractId)}?token=${token}`;
|
|
window.open(url, '_blank');
|
|
};
|
|
|
|
return (
|
|
<Modal isOpen={true} onClose={onClose} title={`Testvorschau: ${template.name}`}>
|
|
<div className="space-y-4">
|
|
<p className="text-sm text-gray-600">
|
|
Wählen Sie einen Vertrag aus, um die Vorlage mit echten Daten zu testen.
|
|
</p>
|
|
|
|
<Input
|
|
label="Vertrag suchen (Vertragsnr., Kunde, Kundennr.)"
|
|
value={search}
|
|
onChange={(e) => { setSearch(e.target.value); setSelectedContractId(null); }}
|
|
placeholder="z.B. ELE-ML9U66 oder Robbers"
|
|
/>
|
|
|
|
{isLoading && <p className="text-sm text-gray-500">Suche...</p>}
|
|
|
|
{contracts.length > 0 && (
|
|
<div className="max-h-48 overflow-y-auto border rounded-lg">
|
|
{contracts.map((c) => (
|
|
<button
|
|
key={c.id}
|
|
onClick={() => setSelectedContractId(c.id)}
|
|
className={`w-full text-left px-4 py-2 text-sm hover:bg-gray-50 flex items-center justify-between border-b last:border-0 ${
|
|
selectedContractId === c.id ? 'bg-blue-50 border-blue-200' : ''
|
|
}`}
|
|
>
|
|
<div>
|
|
<span className="font-mono font-medium">{c.contractNumber}</span>
|
|
{c.customer && (
|
|
<span className="text-gray-500 ml-2">
|
|
{c.customer.companyName || `${c.customer.firstName} ${c.customer.lastName}`}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<Badge variant="default">{c.type}</Badge>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{search.length >= 2 && !isLoading && contracts.length === 0 && (
|
|
<p className="text-sm text-gray-500">Keine Verträge gefunden.</p>
|
|
)}
|
|
|
|
{selectedContractId && (
|
|
<div className="p-3 bg-green-50 border border-green-200 rounded-lg text-sm text-green-800">
|
|
Vertrag ausgewählt. Klicken Sie auf "PDF generieren" um die befüllte Vorlage zu öffnen.
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end gap-3">
|
|
<Button variant="secondary" onClick={onClose}>Schließen</Button>
|
|
<Button onClick={handleGenerate} disabled={!selectedContractId}>
|
|
<Play className="w-4 h-4 mr-2" />
|
|
PDF generieren
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|