PDF-Auftragsvorlagen-System, Objekttyp/Lage-Felder, Eigentümer-Fallback bei Bankverbindung
- 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>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { pushHistory, popHistory } from '../../utils/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi } from '../../services/api';
|
||||
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi, pdfTemplateApi } from '../../services/api';
|
||||
import { ContractEmailsSection } from '../../components/email';
|
||||
import { ContractDetailModal, ContractHistorySection } from '../../components/contracts';
|
||||
import InvoicesSection from '../../components/contracts/InvoicesSection';
|
||||
@@ -1772,6 +1772,7 @@ export default function ContractDetail() {
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<GenerateOrderButton contractId={contractId} />
|
||||
{hasPermission('contracts:delete') && (
|
||||
<Button
|
||||
variant="danger"
|
||||
@@ -3269,3 +3270,163 @@ function ContractDocumentsSection({
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== AUFTRAG GENERIEREN BUTTON ====================
|
||||
|
||||
function GenerateOrderButton({ contractId }: { contractId: number }) {
|
||||
const [showDropdown, setShowDropdown] = useState(false);
|
||||
const [showInputModal, setShowInputModal] = useState<{ templateId: number; templateName: string } | null>(null);
|
||||
|
||||
const { data: templatesData } = useQuery({
|
||||
queryKey: ['pdf-templates'],
|
||||
queryFn: () => pdfTemplateApi.getAll(),
|
||||
});
|
||||
|
||||
const templates = (templatesData?.data || []).filter(t => t.isActive);
|
||||
if (templates.length === 0) return null;
|
||||
|
||||
const handleSelect = async (templateId: number, templateName: string) => {
|
||||
setShowDropdown(false);
|
||||
// Prüfen ob manuelle Inputs nötig sind
|
||||
try {
|
||||
const result = await pdfTemplateApi.getRequiredInputs(templateId, contractId);
|
||||
const inputs = result.data;
|
||||
if (inputs && (inputs.needsStressfreiEmail || inputs.manualFields.length > 0)) {
|
||||
setShowInputModal({ templateId, templateName });
|
||||
} else {
|
||||
// Direkt generieren (GET-Link)
|
||||
const token = localStorage.getItem('token');
|
||||
window.open(`${pdfTemplateApi.generateUrl(templateId, contractId)}?token=${token}`, '_blank');
|
||||
}
|
||||
} catch {
|
||||
// Fallback: direkt generieren
|
||||
const token = localStorage.getItem('token');
|
||||
window.open(`${pdfTemplateApi.generateUrl(templateId, contractId)}?token=${token}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{templates.length === 1 ? (
|
||||
<Button variant="secondary" onClick={() => handleSelect(templates[0].id, templates[0].name)}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{templates[0].name}
|
||||
</Button>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<Button variant="secondary" onClick={() => setShowDropdown(!showDropdown)}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
Auftrag generieren
|
||||
<ChevronDown className="w-3 h-3 ml-1" />
|
||||
</Button>
|
||||
{showDropdown && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-10" onClick={() => setShowDropdown(false)} />
|
||||
<div className="absolute right-0 mt-1 w-64 bg-white border rounded-lg shadow-lg z-20 py-1">
|
||||
{templates.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-50 flex items-center gap-2"
|
||||
onClick={() => handleSelect(t.id, t.name)}
|
||||
>
|
||||
<FileText className="w-4 h-4 text-blue-500" />
|
||||
<div>
|
||||
<span className="font-medium">{t.name}</span>
|
||||
{t.providerName && <span className="text-xs text-gray-500 ml-1">({t.providerName})</span>}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showInputModal && (
|
||||
<GenerateInputModal
|
||||
templateId={showInputModal.templateId}
|
||||
templateName={showInputModal.templateName}
|
||||
contractId={contractId}
|
||||
onClose={() => setShowInputModal(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GenerateInputModal({ templateId, templateName, contractId, onClose }: {
|
||||
templateId: number;
|
||||
templateName: string;
|
||||
contractId: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [stressfreiEmailId, setStressfreiEmailId] = useState('');
|
||||
const [manualValues, setManualValues] = useState<Record<string, string>>({});
|
||||
const [generating] = useState(false);
|
||||
|
||||
const { data: inputsData, isLoading } = useQuery({
|
||||
queryKey: ['pdf-inputs', templateId, contractId],
|
||||
queryFn: () => pdfTemplateApi.getRequiredInputs(templateId, contractId),
|
||||
});
|
||||
|
||||
const inputs = inputsData?.data;
|
||||
|
||||
const handleGenerate = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const params = new URLSearchParams();
|
||||
params.set('token', token || '');
|
||||
if (stressfreiEmailId) params.set('stressfreiEmailId', stressfreiEmailId);
|
||||
for (const [key, value] of Object.entries(manualValues)) {
|
||||
if (value) params.set(`manual_${key}`, value);
|
||||
}
|
||||
window.open(`${pdfTemplateApi.generateUrl(templateId, contractId)}?${params}`, '_blank');
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={true} onClose={onClose} title={`${templateName} – Zusätzliche Angaben`}>
|
||||
<div className="space-y-4">
|
||||
{inputs?.needsStressfreiEmail && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Stressfrei-Wechseln E-Mail</label>
|
||||
<select
|
||||
value={stressfreiEmailId}
|
||||
onChange={(e) => setStressfreiEmailId(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
{inputs.stressfreiEmails.map((e) => (
|
||||
<option key={e.id} value={e.id}>{e.email}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inputs?.manualFields && inputs.manualFields.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-gray-700">Freitextfelder</p>
|
||||
{inputs.manualFields.map((f) => (
|
||||
<Input
|
||||
key={f.key}
|
||||
label={f.pdfFieldName}
|
||||
value={manualValues[f.key] || ''}
|
||||
onChange={(e) => setManualValues({ ...manualValues, [f.key]: e.target.value })}
|
||||
placeholder="Eingabe..."
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>Abbrechen</Button>
|
||||
<Button onClick={handleGenerate} disabled={generating}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
{generating ? 'Generiere...' : 'PDF generieren'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user