E-Mail als PDF speichern: Tab "Vertragsdokument" ergänzt
Bisher hatte das "E-Mail als PDF speichern"-Modal nur die Tabs
"Als Dokument" + "Als Rechnung" (nur Energieverträge). Wenn die
E-Mail einem Vertrag zugeordnet ist, fehlte die Möglichkeit, sie
direkt als Vertragsdokument (Auftragsformular, Lieferbestätigung
etc.) zu hinterlegen – analog zum Anhang-Modal.
Backend: neuer Endpoint POST /api/emails/:id/save-as-contract-document
{ documentType, notes?, deliveryDate? } – generiert das Mail-PDF,
speichert es unter /uploads/contract-documents und legt einen
ContractDocument-Eintrag an. Bei documentType "Lieferbestätigung"
wird der bestehende maybeActivateOnDeliveryConfirmation-Workflow
getriggert (DRAFT → ACTIVE, startDate-Übernahme).
Frontend: SaveEmailAsPdfModal bekommt den dritten Tab parallel zu
SaveAttachmentModal. Tab erscheint, sobald die E-Mail einem Vertrag
zugeordnet ist (auch bei Nicht-Energieverträgen); Tab "Als Rechnung"
bleibt auf Energieverträge beschränkt. Dokumenttyp-Dropdown und
Notizen-Feld werden aus dem Anhang-Modal übernommen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { FileText, User, CreditCard, IdCard, AlertTriangle, Check, ChevronDown, ChevronRight, Receipt } from 'lucide-react';
|
||||
import { FileText, User, CreditCard, IdCard, AlertTriangle, Check, ChevronDown, ChevronRight, Receipt, FolderOpen } from 'lucide-react';
|
||||
import Modal from '../ui/Modal';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
@@ -9,6 +9,17 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import toast from 'react-hot-toast';
|
||||
import type { InvoiceType } from '../../types';
|
||||
|
||||
const CONTRACT_DOCUMENT_TYPES = [
|
||||
'Auftragsformular',
|
||||
'Auftragsbestätigung',
|
||||
'Lieferbestätigung',
|
||||
'Vertragsunterlagen',
|
||||
'Vollmacht',
|
||||
'Widerrufsbelehrung',
|
||||
'Preisblatt',
|
||||
'Sonstiges',
|
||||
];
|
||||
|
||||
interface SaveEmailAsPdfModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
@@ -24,7 +35,7 @@ type SelectedTarget = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
type SaveMode = 'document' | 'invoice';
|
||||
type SaveMode = 'document' | 'invoice' | 'contractDocument';
|
||||
|
||||
export default function SaveEmailAsPdfModal({
|
||||
isOpen,
|
||||
@@ -40,6 +51,11 @@ export default function SaveEmailAsPdfModal({
|
||||
invoiceType: 'INTERIM' as InvoiceType,
|
||||
notes: '',
|
||||
});
|
||||
const [contractDocumentData, setContractDocumentData] = useState({
|
||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||
notes: '',
|
||||
deliveryDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Ziele laden (gleiche wie bei Anhängen)
|
||||
@@ -51,8 +67,10 @@ export default function SaveEmailAsPdfModal({
|
||||
|
||||
const targets = targetsData?.data;
|
||||
|
||||
// Prüfen ob es ein Energievertrag ist
|
||||
// Prüfen ob es ein Energievertrag ist (für Rechnungs-Modus)
|
||||
const isEnergyContract = targets?.contract?.type === 'ELECTRICITY' || targets?.contract?.type === 'GAS';
|
||||
// Vertrag zugeordnet? → Vertragsdokument-Modus erlaubt
|
||||
const hasContract = !!targets?.contract;
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
@@ -111,6 +129,33 @@ export default function SaveEmailAsPdfModal({
|
||||
},
|
||||
});
|
||||
|
||||
const saveContractDocumentMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const isDelivery = contractDocumentData.documentType.trim().toLowerCase() === 'lieferbestätigung';
|
||||
return cachedEmailApi.saveEmailAsContractDocument(emailId, {
|
||||
documentType: contractDocumentData.documentType,
|
||||
notes: contractDocumentData.notes || undefined,
|
||||
deliveryDate: isDelivery ? contractDocumentData.deliveryDate : undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('E-Mail als Vertragsdokument gespeichert');
|
||||
queryClient.invalidateQueries({ queryKey: ['attachment-targets', emailId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
|
||||
if (targets?.contract?.id) {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', targets.contract.id.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-documents', targets.contract.id] });
|
||||
}
|
||||
|
||||
onSuccess?.();
|
||||
handleClose();
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || 'Fehler beim Speichern des Vertragsdokuments');
|
||||
},
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedTarget(null);
|
||||
setSaveMode('document');
|
||||
@@ -119,6 +164,11 @@ export default function SaveEmailAsPdfModal({
|
||||
invoiceType: 'INTERIM',
|
||||
notes: '',
|
||||
});
|
||||
setContractDocumentData({
|
||||
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||
notes: '',
|
||||
deliveryDate: new Date().toISOString().split('T')[0],
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
@@ -235,7 +285,7 @@ export default function SaveEmailAsPdfModal({
|
||||
);
|
||||
};
|
||||
|
||||
const isPending = saveMutation.isPending || saveInvoiceMutation.isPending;
|
||||
const isPending = saveMutation.isPending || saveInvoiceMutation.isPending || saveContractDocumentMutation.isPending;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="E-Mail als PDF speichern" size="lg">
|
||||
@@ -263,12 +313,13 @@ export default function SaveEmailAsPdfModal({
|
||||
|
||||
{targets && (
|
||||
<>
|
||||
{/* Mode Toggle für Energieverträge */}
|
||||
{isEnergyContract && (
|
||||
{/* Mode Toggle: Vertragsdokument-Tab wenn ein Vertrag verknüpft ist,
|
||||
Rechnungs-Tab nur bei Energieverträgen. */}
|
||||
{hasContract && (
|
||||
<div className="flex gap-2 p-1 bg-gray-100 rounded-lg">
|
||||
<button
|
||||
onClick={() => setSaveMode('document')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
saveMode === 'document'
|
||||
? 'bg-white text-blue-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
@@ -278,16 +329,29 @@ export default function SaveEmailAsPdfModal({
|
||||
Als Dokument
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSaveMode('invoice')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
saveMode === 'invoice'
|
||||
? 'bg-white text-green-600 shadow-sm'
|
||||
onClick={() => setSaveMode('contractDocument')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
saveMode === 'contractDocument'
|
||||
? 'bg-white text-orange-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Receipt className="w-4 h-4" />
|
||||
Als Rechnung
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
Vertragsdokument
|
||||
</button>
|
||||
{isEnergyContract && (
|
||||
<button
|
||||
onClick={() => setSaveMode('invoice')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
saveMode === 'invoice'
|
||||
? 'bg-white text-green-600 shadow-sm'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
<Receipt className="w-4 h-4" />
|
||||
Als Rechnung
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -377,6 +441,46 @@ export default function SaveEmailAsPdfModal({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Contract Document Mode */}
|
||||
{saveMode === 'contractDocument' && hasContract && (
|
||||
<div className="space-y-4">
|
||||
<div className="p-3 bg-orange-50 rounded-lg">
|
||||
<p className="text-sm text-orange-700">
|
||||
Die E-Mail wird als Vertragsdokument für den Vertrag <strong>{targets.contract?.contractNumber}</strong> gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
label="Dokumenttyp"
|
||||
value={contractDocumentData.documentType}
|
||||
onChange={(e) => setContractDocumentData({ ...contractDocumentData, documentType: e.target.value })}
|
||||
options={CONTRACT_DOCUMENT_TYPES.map((t) => ({ value: t, label: t }))}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Notizen (optional)"
|
||||
value={contractDocumentData.notes}
|
||||
onChange={(e) => setContractDocumentData({ ...contractDocumentData, notes: e.target.value })}
|
||||
placeholder="Optionale Anmerkungen..."
|
||||
/>
|
||||
|
||||
{contractDocumentData.documentType.trim().toLowerCase() === 'lieferbestätigung' && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Lieferdatum</label>
|
||||
<input
|
||||
type="date"
|
||||
value={contractDocumentData.deliveryDate}
|
||||
onChange={(e) => setContractDocumentData({ ...contractDocumentData, deliveryDate: e.target.value })}
|
||||
className="block w-full max-w-[220px] px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-600 mt-1">
|
||||
Falls der Vertrag noch auf Entwurf steht, wird er auf Aktiv gesetzt und dieses Datum als Vertragsbeginn übernommen.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -396,14 +500,15 @@ export default function SaveEmailAsPdfModal({
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
{saveMode === 'document' ? (
|
||||
{saveMode === 'document' && (
|
||||
<Button
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={!selectedTarget || isPending}
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Als PDF speichern'}
|
||||
</Button>
|
||||
) : (
|
||||
)}
|
||||
{saveMode === 'invoice' && (
|
||||
<Button
|
||||
onClick={() => saveInvoiceMutation.mutate()}
|
||||
disabled={!invoiceData.invoiceDate || isPending}
|
||||
@@ -411,6 +516,14 @@ export default function SaveEmailAsPdfModal({
|
||||
{isPending ? 'Wird erstellt...' : 'Als Rechnung speichern'}
|
||||
</Button>
|
||||
)}
|
||||
{saveMode === 'contractDocument' && (
|
||||
<Button
|
||||
onClick={() => saveContractDocumentMutation.mutate()}
|
||||
disabled={!contractDocumentData.documentType || isPending}
|
||||
>
|
||||
{isPending ? 'Wird erstellt...' : 'Als Vertragsdokument speichern'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -710,6 +710,18 @@ export const cachedEmailApi = {
|
||||
);
|
||||
return res.data;
|
||||
},
|
||||
// E-Mail als Vertragsdokument speichern (PDF aus der Mail wird als
|
||||
// ContractDocument hinterlegt – parallel zur Anhang-Variante)
|
||||
saveEmailAsContractDocument: async (
|
||||
emailId: number,
|
||||
params: { documentType: string; notes?: string; deliveryDate?: string },
|
||||
) => {
|
||||
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
|
||||
`/emails/${emailId}/save-as-contract-document`,
|
||||
params,
|
||||
);
|
||||
return res.data;
|
||||
},
|
||||
// Anhang als Rechnung speichern (für Energieverträge)
|
||||
saveAttachmentAsInvoice: async (emailId: number, filename: string, params: { invoiceDate: string; invoiceType: string; notes?: string }) => {
|
||||
const encodedFilename = encodeURIComponent(filename);
|
||||
|
||||
Reference in New Issue
Block a user