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:
parent
aa2b5ce785
commit
2879bd64d6
|
|
@ -1579,7 +1579,7 @@ export async function saveEmailAsInvoice(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
// Vertrag laden
|
||||||
const contract = await prisma.contract.findUnique({
|
const contract = await prisma.contract.findUnique({
|
||||||
where: { id: email.contractId },
|
where: { id: email.contractId },
|
||||||
include: { energyDetails: true },
|
include: { energyDetails: true },
|
||||||
|
|
@ -1593,22 +1593,6 @@ export async function saveEmailAsInvoice(req: Request, res: Response): Promise<v
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
|
||||||
} as ApiResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!contract.energyDetails) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
|
||||||
} as ApiResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empfänger-Adressen parsen (JSON Array)
|
// Empfänger-Adressen parsen (JSON Array)
|
||||||
let toAddresses: string[] = [];
|
let toAddresses: string[] = [];
|
||||||
let ccAddresses: string[] = [];
|
let ccAddresses: string[] = [];
|
||||||
|
|
@ -1645,8 +1629,15 @@ export async function saveEmailAsInvoice(req: Request, res: Response): Promise<v
|
||||||
// PDF speichern
|
// PDF speichern
|
||||||
fs.writeFileSync(filePath, pdfBuffer);
|
fs.writeFileSync(filePath, pdfBuffer);
|
||||||
|
|
||||||
// Invoice in DB erstellen
|
// Invoice in DB erstellen (für alle Vertragstypen)
|
||||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
const invoice = contract.energyDetails
|
||||||
|
? await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||||
|
invoiceDate: new Date(invoiceDate),
|
||||||
|
invoiceType,
|
||||||
|
documentPath: relativePath,
|
||||||
|
notes: notes || undefined,
|
||||||
|
})
|
||||||
|
: await invoiceService.addInvoiceByContract(contract.id, {
|
||||||
invoiceDate: new Date(invoiceDate),
|
invoiceDate: new Date(invoiceDate),
|
||||||
invoiceType,
|
invoiceType,
|
||||||
documentPath: relativePath,
|
documentPath: relativePath,
|
||||||
|
|
@ -1715,7 +1706,7 @@ export async function saveAttachmentAsInvoice(req: Request, res: Response): Prom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vertrag laden und prüfen ob es ein Energievertrag ist
|
// Vertrag laden
|
||||||
const contract = await prisma.contract.findUnique({
|
const contract = await prisma.contract.findUnique({
|
||||||
where: { id: email.contractId },
|
where: { id: email.contractId },
|
||||||
include: { energyDetails: true },
|
include: { energyDetails: true },
|
||||||
|
|
@ -1729,22 +1720,6 @@ export async function saveAttachmentAsInvoice(req: Request, res: Response): Prom
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['ELECTRICITY', 'GAS'].includes(contract.type)) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Nur für Strom- und Gas-Verträge verfügbar',
|
|
||||||
} as ApiResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!contract.energyDetails) {
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Keine Energie-Details für diesen Vertrag vorhanden',
|
|
||||||
} as ApiResponse);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Für gesendete E-Mails: Prüfen ob UID vorhanden
|
// Für gesendete E-Mails: Prüfen ob UID vorhanden
|
||||||
if (email.folder === 'SENT' && email.uid === 0) {
|
if (email.folder === 'SENT' && email.uid === 0) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
|
|
@ -1816,8 +1791,15 @@ export async function saveAttachmentAsInvoice(req: Request, res: Response): Prom
|
||||||
// Datei speichern
|
// Datei speichern
|
||||||
fs.writeFileSync(filePath, attachment.content);
|
fs.writeFileSync(filePath, attachment.content);
|
||||||
|
|
||||||
// Invoice in DB erstellen
|
// Invoice in DB erstellen (für alle Vertragstypen)
|
||||||
const invoice = await invoiceService.addInvoice(contract.energyDetails.id, {
|
const invoice = contract.energyDetails
|
||||||
|
? await invoiceService.addInvoice(contract.energyDetails.id, {
|
||||||
|
invoiceDate: new Date(invoiceDate),
|
||||||
|
invoiceType,
|
||||||
|
documentPath: relativePath,
|
||||||
|
notes: notes || undefined,
|
||||||
|
})
|
||||||
|
: await invoiceService.addInvoiceByContract(contract.id, {
|
||||||
invoiceDate: new Date(invoiceDate),
|
invoiceDate: new Date(invoiceDate),
|
||||||
invoiceType,
|
invoiceType,
|
||||||
documentPath: relativePath,
|
documentPath: relativePath,
|
||||||
|
|
@ -1837,3 +1819,132 @@ export async function saveAttachmentAsInvoice(req: Request, res: Response): Prom
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anhang einer E-Mail als Vertragsdokument (ContractDocument) speichern.
|
||||||
|
* Nutzt die flexible ContractDocument-Tabelle mit documentType (Auftragsformular, Lieferbestätigung, etc.)
|
||||||
|
*/
|
||||||
|
export async function saveAttachmentAsContractDocument(req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const emailId = parseInt(req.params.id);
|
||||||
|
const filename = decodeURIComponent(req.params.filename);
|
||||||
|
const { documentType, notes } = req.body;
|
||||||
|
|
||||||
|
if (!documentType || typeof documentType !== 'string') {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'documentType ist erforderlich',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = await cachedEmailService.getCachedEmailById(emailId);
|
||||||
|
if (!email) {
|
||||||
|
res.status(404).json({ success: false, error: 'E-Mail nicht gefunden' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email.contractId) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'E-Mail ist keinem Vertrag zugeordnet',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contract = await prisma.contract.findUnique({
|
||||||
|
where: { id: email.contractId },
|
||||||
|
select: { id: true, contractNumber: true, customerId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!contract) {
|
||||||
|
res.status(404).json({ success: false, error: 'Vertrag nicht gefunden' } as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für gesendete E-Mails: Prüfen ob UID vorhanden
|
||||||
|
if (email.folder === 'SENT' && email.uid === 0) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Anhang nicht verfügbar - E-Mail wurde vor der IMAP-Speicherung gesendet',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// StressfreiEmail für IMAP-Zugangsdaten
|
||||||
|
const stressfreiEmail = await stressfreiEmailService.getEmailWithMailboxById(email.stressfreiEmailId);
|
||||||
|
if (!stressfreiEmail || !stressfreiEmail.emailPasswordEncrypted) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine Mailbox-Zugangsdaten verfügbar',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await getImapSmtpSettings();
|
||||||
|
if (!settings) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Keine E-Mail-Provider-Einstellungen gefunden',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = decrypt(stressfreiEmail.emailPasswordEncrypted);
|
||||||
|
|
||||||
|
const credentials: ImapCredentials = {
|
||||||
|
host: settings.imapServer,
|
||||||
|
port: settings.imapPort,
|
||||||
|
user: stressfreiEmail.email,
|
||||||
|
password,
|
||||||
|
encryption: settings.imapEncryption,
|
||||||
|
allowSelfSignedCerts: settings.allowSelfSignedCerts,
|
||||||
|
};
|
||||||
|
|
||||||
|
const imapFolder = email.folder === 'SENT' ? 'Sent' : 'INBOX';
|
||||||
|
|
||||||
|
const attachment = await fetchAttachment(credentials, email.uid, filename, imapFolder);
|
||||||
|
if (!attachment) {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Anhang nicht gefunden oder nicht mehr verfügbar',
|
||||||
|
} as ApiResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uploads-Verzeichnis
|
||||||
|
const uploadsDir = path.join(process.cwd(), 'uploads', 'contract-documents');
|
||||||
|
if (!fs.existsSync(uploadsDir)) {
|
||||||
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(filename) || '.pdf';
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
|
const safeType = documentType.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||||
|
const newFilename = `${safeType}-${uniqueSuffix}${ext}`;
|
||||||
|
const filePath = path.join(uploadsDir, newFilename);
|
||||||
|
const relativePath = `/uploads/contract-documents/${newFilename}`;
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, attachment.content);
|
||||||
|
|
||||||
|
const doc = await prisma.contractDocument.create({
|
||||||
|
data: {
|
||||||
|
contractId: contract.id,
|
||||||
|
documentType,
|
||||||
|
documentPath: relativePath,
|
||||||
|
originalName: filename,
|
||||||
|
notes: notes || null,
|
||||||
|
uploadedBy: (req as any).user?.email || 'email-import',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, data: doc } as ApiResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('saveAttachmentAsContractDocument error:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: `Fehler beim Speichern: ${errorMessage}`,
|
||||||
|
} as ApiResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,15 @@ router.post(
|
||||||
cachedEmailController.saveAttachmentAsInvoice
|
cachedEmailController.saveAttachmentAsInvoice
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Anhang als Vertragsdokument speichern
|
||||||
|
// POST /api/emails/:id/attachments/:filename/save-as-contract-document { documentType, notes? }
|
||||||
|
router.post(
|
||||||
|
'/emails/:id/attachments/:filename/save-as-contract-document',
|
||||||
|
authenticate,
|
||||||
|
requirePermission('contracts:update'),
|
||||||
|
cachedEmailController.saveAttachmentAsContractDocument
|
||||||
|
);
|
||||||
|
|
||||||
// ==================== VERTRAGSZUORDNUNG ====================
|
// ==================== VERTRAGSZUORDNUNG ====================
|
||||||
|
|
||||||
// E-Mail Vertrag zuordnen
|
// E-Mail Vertrag zuordnen
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,6 @@
|
||||||
|
|
||||||
### Security System testen
|
### Security System testen
|
||||||
|
|
||||||
### Email → Vertragsdokumente
|
|
||||||
Wenn eine Email einem Vertrag zugeordnet ist:
|
|
||||||
- Anhänge auch in Vertragsdokumente speichern
|
|
||||||
- Rechnungen wie Kündigungsdokumente behandeln
|
|
||||||
|
|
||||||
### Factory-Defaults: Export + Import von Lieferanten & Formularvorlagen
|
### Factory-Defaults: Export + Import von Lieferanten & Formularvorlagen
|
||||||
**Ziel:** Einmal gepflegte Stammdaten (Anbieter, Tarife, Kündigungsfristen, Laufzeiten,
|
**Ziel:** Einmal gepflegte Stammdaten (Anbieter, Tarife, Kündigungsfristen, Laufzeiten,
|
||||||
PDF-Auftragsvorlagen) sollen sich exportieren und in andere Installationen oder
|
PDF-Auftragsvorlagen) sollen sich exportieren und in andere Installationen oder
|
||||||
|
|
@ -57,6 +52,14 @@ als Factory-Default beim Initialisieren wieder einspielen lassen.
|
||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **Email-Anhänge → Vertragsdokumente + Rechnungen für alle Vertragstypen**
|
||||||
|
- Im SaveAttachmentModal (bei einem per Email zugeordneten Vertrag) gibt es jetzt drei Modi:
|
||||||
|
1. **Als Dokument** (in feste Slots wie Kündigungsschreiben) – wie bisher
|
||||||
|
2. **Als Vertragsdokument** – neu, mit Typ-Dropdown (Auftragsformular, Lieferbestätigung, Vertragsunterlagen, Vollmacht, Widerrufsbelehrung, Preisblatt, Sonstiges) + Notizen
|
||||||
|
3. **Als Rechnung** – jetzt für **alle** Vertragstypen (vorher nur Strom/Gas)
|
||||||
|
- Gleiches gilt für das Speichern der gesamten Email als PDF-Rechnung
|
||||||
|
- Neuer Backend-Endpoint `saveAttachmentAsContractDocument` für die flexible ContractDocument-Tabelle
|
||||||
|
|
||||||
- [x] **Geburtstag-Management-Modal in Kundenstammdaten**
|
- [x] **Geburtstag-Management-Modal in Kundenstammdaten**
|
||||||
- Neuer Button (Cake-Icon) neben Geburtsdatum öffnet Modal
|
- Neuer Button (Cake-Icon) neben Geburtsdatum öffnet Modal
|
||||||
- **Gruß zurücksetzen:** setzt `lastBirthdayGreetingYear` auf null zurück (fürs Debugging + Fallback)
|
- **Gruß zurücksetzen:** setzt `lastBirthdayGreetingYear` auf null zurück (fürs Debugging + Fallback)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from 'react';
|
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 Modal from '../ui/Modal';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import Input from '../ui/Input';
|
import Input from '../ui/Input';
|
||||||
|
|
@ -9,6 +9,17 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import type { InvoiceType } from '../../types';
|
import type { InvoiceType } from '../../types';
|
||||||
|
|
||||||
|
const CONTRACT_DOCUMENT_TYPES = [
|
||||||
|
'Auftragsformular',
|
||||||
|
'Auftragsbestätigung',
|
||||||
|
'Lieferbestätigung',
|
||||||
|
'Vertragsunterlagen',
|
||||||
|
'Vollmacht',
|
||||||
|
'Widerrufsbelehrung',
|
||||||
|
'Preisblatt',
|
||||||
|
'Sonstiges',
|
||||||
|
];
|
||||||
|
|
||||||
interface SaveAttachmentModalProps {
|
interface SaveAttachmentModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -25,7 +36,7 @@ type SelectedTarget = {
|
||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SaveMode = 'document' | 'invoice';
|
type SaveMode = 'document' | 'invoice' | 'contractDocument';
|
||||||
|
|
||||||
export default function SaveAttachmentModal({
|
export default function SaveAttachmentModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
|
@ -42,6 +53,10 @@ export default function SaveAttachmentModal({
|
||||||
invoiceType: 'INTERIM' as InvoiceType,
|
invoiceType: 'INTERIM' as InvoiceType,
|
||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
|
const [contractDocumentData, setContractDocumentData] = useState({
|
||||||
|
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||||
|
notes: '',
|
||||||
|
});
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Ziele laden
|
// Ziele laden
|
||||||
|
|
@ -53,8 +68,8 @@ export default function SaveAttachmentModal({
|
||||||
|
|
||||||
const targets = targetsData?.data;
|
const targets = targetsData?.data;
|
||||||
|
|
||||||
// Prüfen ob es ein Energievertrag ist
|
// Vertrag zugeordnet? → dann Rechnung + Vertragsdokument möglich
|
||||||
const isEnergyContract = targets?.contract?.type === 'ELECTRICITY' || targets?.contract?.type === 'GAS';
|
const hasContract = !!targets?.contract;
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: () => {
|
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 = () => {
|
const handleClose = () => {
|
||||||
setSelectedTarget(null);
|
setSelectedTarget(null);
|
||||||
setSaveMode('document');
|
setSaveMode('document');
|
||||||
|
|
@ -121,6 +161,10 @@ export default function SaveAttachmentModal({
|
||||||
invoiceType: 'INTERIM',
|
invoiceType: 'INTERIM',
|
||||||
notes: '',
|
notes: '',
|
||||||
});
|
});
|
||||||
|
setContractDocumentData({
|
||||||
|
documentType: CONTRACT_DOCUMENT_TYPES[0],
|
||||||
|
notes: '',
|
||||||
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -263,12 +307,12 @@ export default function SaveAttachmentModal({
|
||||||
|
|
||||||
{targets && (
|
{targets && (
|
||||||
<>
|
<>
|
||||||
{/* Mode Toggle für Energieverträge */}
|
{/* Mode Toggle (nur wenn ein Vertrag zugeordnet ist) */}
|
||||||
{isEnergyContract && (
|
{hasContract && (
|
||||||
<div className="flex gap-2 p-1 bg-gray-100 rounded-lg">
|
<div className="flex gap-2 p-1 bg-gray-100 rounded-lg">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSaveMode('document')}
|
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'
|
saveMode === 'document'
|
||||||
? 'bg-white text-blue-600 shadow-sm'
|
? 'bg-white text-blue-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
|
@ -277,9 +321,20 @@ export default function SaveAttachmentModal({
|
||||||
<FileText className="w-4 h-4" />
|
<FileText className="w-4 h-4" />
|
||||||
Als Dokument
|
Als Dokument
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setSaveMode('invoice')}
|
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'
|
saveMode === 'invoice'
|
||||||
? 'bg-white text-green-600 shadow-sm'
|
? 'bg-white text-green-600 shadow-sm'
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
|
@ -343,7 +398,7 @@ export default function SaveAttachmentModal({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Invoice Mode */}
|
{/* Invoice Mode */}
|
||||||
{saveMode === 'invoice' && isEnergyContract && (
|
{saveMode === 'invoice' && hasContract && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="p-3 bg-green-50 rounded-lg">
|
<div className="p-3 bg-green-50 rounded-lg">
|
||||||
<p className="text-sm text-green-700">
|
<p className="text-sm text-green-700">
|
||||||
|
|
@ -377,6 +432,35 @@ export default function SaveAttachmentModal({
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}>
|
<Button variant="secondary" onClick={handleClose}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
{saveMode === 'document' ? (
|
{saveMode === 'document' && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => saveMutation.mutate()}
|
onClick={() => saveMutation.mutate()}
|
||||||
disabled={!selectedTarget || saveMutation.isPending || saveInvoiceMutation.isPending}
|
disabled={
|
||||||
|
!selectedTarget ||
|
||||||
|
saveMutation.isPending ||
|
||||||
|
saveInvoiceMutation.isPending ||
|
||||||
|
saveContractDocumentMutation.isPending
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{saveMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
{saveMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
)}
|
||||||
|
{saveMode === 'invoice' && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => saveInvoiceMutation.mutate()}
|
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'}
|
{saveInvoiceMutation.isPending ? 'Wird gespeichert...' : 'Als Rechnung speichern'}
|
||||||
</Button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -585,6 +585,18 @@ export const cachedEmailApi = {
|
||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
saveAttachmentAsContractDocument: async (
|
||||||
|
emailId: number,
|
||||||
|
filename: string,
|
||||||
|
params: { documentType: string; notes?: string },
|
||||||
|
) => {
|
||||||
|
const encodedFilename = encodeURIComponent(filename);
|
||||||
|
const res = await api.post<ApiResponse<{ id: number; documentType: string; documentPath: string }>>(
|
||||||
|
`/emails/${emailId}/attachments/${encodedFilename}/save-as-contract-document`,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Contracts - Vertragsbaum für Kundenansicht
|
// Contracts - Vertragsbaum für Kundenansicht
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue