Email-Anhänge als Vertragsdokumente + Rechnungen für alle Vertragstypen

Der SaveAttachmentModal hat jetzt drei Modi (wenn E-Mail einem Vertrag zugeordnet ist):

1. Als Dokument – in feste Slots (Kündigungsschreiben etc.), unverändert
2. Als Vertragsdokument – NEU: flexible ContractDocument-Tabelle mit Typ-Dropdown
   (Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht,
   Widerrufsbelehrung, Preisblatt, Sonstiges) + optionalen Notizen
3. Als Rechnung – jetzt für ALLE Vertragstypen (vorher nur Strom/Gas)

Backend:
- Neuer Endpoint POST /api/emails/:id/attachments/:filename/save-as-contract-document
- saveAttachmentAsInvoice + saveEmailAsInvoice: ELECTRICITY/GAS-Einschränkung entfernt,
  nutzt jetzt addInvoiceByContract als Fallback für Nicht-Energie-Verträge

Frontend:
- cachedEmailApi.saveAttachmentAsContractDocument hinzugefügt
- SaveAttachmentModal: neuer Mode 'contractDocument' mit Typ+Notizen
- Mode-Toggle zeigt jetzt alle drei Optionen wenn Vertrag zugeordnet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 13:06:10 +02:00
parent aa2b5ce785
commit 2879bd64d6
5 changed files with 311 additions and 66 deletions
@@ -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 SaveAttachmentModalProps {
isOpen: boolean;
onClose: () => void;
@@ -25,7 +36,7 @@ type SelectedTarget = {
label: string;
};
type SaveMode = 'document' | 'invoice';
type SaveMode = 'document' | 'invoice' | 'contractDocument';
export default function SaveAttachmentModal({
isOpen,
@@ -42,6 +53,10 @@ export default function SaveAttachmentModal({
invoiceType: 'INTERIM' as InvoiceType,
notes: '',
});
const [contractDocumentData, setContractDocumentData] = useState({
documentType: CONTRACT_DOCUMENT_TYPES[0],
notes: '',
});
const queryClient = useQueryClient();
// Ziele laden
@@ -53,8 +68,8 @@ export default function SaveAttachmentModal({
const targets = targetsData?.data;
// Prüfen ob es ein Energievertrag ist
const isEnergyContract = targets?.contract?.type === 'ELECTRICITY' || targets?.contract?.type === 'GAS';
// Vertrag zugeordnet? → dann Rechnung + Vertragsdokument möglich
const hasContract = !!targets?.contract;
const saveMutation = useMutation({
mutationFn: () => {
@@ -113,6 +128,31 @@ export default function SaveAttachmentModal({
},
});
const saveContractDocumentMutation = useMutation({
mutationFn: () => {
return cachedEmailApi.saveAttachmentAsContractDocument(emailId, attachmentFilename, {
documentType: contractDocumentData.documentType,
notes: contractDocumentData.notes || undefined,
});
},
onSuccess: () => {
toast.success('Anhang 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');
@@ -121,6 +161,10 @@ export default function SaveAttachmentModal({
invoiceType: 'INTERIM',
notes: '',
});
setContractDocumentData({
documentType: CONTRACT_DOCUMENT_TYPES[0],
notes: '',
});
onClose();
};
@@ -263,12 +307,12 @@ export default function SaveAttachmentModal({
{targets && (
<>
{/* Mode Toggle für Energieverträge */}
{isEnergyContract && (
{/* Mode Toggle (nur wenn ein Vertrag zugeordnet ist) */}
{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'
@@ -277,9 +321,20 @@ export default function SaveAttachmentModal({
<FileText className="w-4 h-4" />
Als Dokument
</button>
<button
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'
}`}
>
<FolderOpen className="w-4 h-4" />
Vertragsdokument
</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 ${
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'
@@ -343,7 +398,7 @@ export default function SaveAttachmentModal({
)}
{/* Invoice Mode */}
{saveMode === 'invoice' && isEnergyContract && (
{saveMode === 'invoice' && hasContract && (
<div className="space-y-4">
<div className="p-3 bg-green-50 rounded-lg">
<p className="text-sm text-green-700">
@@ -377,6 +432,35 @@ export default function SaveAttachmentModal({
/>
</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">
Der Anhang 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..."
/>
</div>
)}
</>
)}
@@ -396,21 +480,47 @@ export default function SaveAttachmentModal({
<Button variant="secondary" onClick={handleClose}>
Abbrechen
</Button>
{saveMode === 'document' ? (
{saveMode === 'document' && (
<Button
onClick={() => saveMutation.mutate()}
disabled={!selectedTarget || saveMutation.isPending || saveInvoiceMutation.isPending}
disabled={
!selectedTarget ||
saveMutation.isPending ||
saveInvoiceMutation.isPending ||
saveContractDocumentMutation.isPending
}
>
{saveMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
</Button>
) : (
)}
{saveMode === 'invoice' && (
<Button
onClick={() => saveInvoiceMutation.mutate()}
disabled={!invoiceData.invoiceDate || saveMutation.isPending || saveInvoiceMutation.isPending}
disabled={
!invoiceData.invoiceDate ||
saveMutation.isPending ||
saveInvoiceMutation.isPending ||
saveContractDocumentMutation.isPending
}
>
{saveInvoiceMutation.isPending ? 'Wird gespeichert...' : 'Als Rechnung speichern'}
</Button>
)}
{saveMode === 'contractDocument' && (
<Button
onClick={() => saveContractDocumentMutation.mutate()}
disabled={
!contractDocumentData.documentType ||
saveMutation.isPending ||
saveInvoiceMutation.isPending ||
saveContractDocumentMutation.isPending
}
>
{saveContractDocumentMutation.isPending
? 'Wird gespeichert...'
: 'Als Vertragsdokument speichern'}
</Button>
)}
</div>
</div>
</Modal>