added invoices and status in cockpit, created info button for contract status types

This commit is contained in:
2026-02-08 01:18:12 +01:00
parent 1ad4fe0819
commit aee48a8ccb
45 changed files with 4543 additions and 863 deletions
@@ -1,10 +1,13 @@
import { useState } from 'react';
import { FileText, User, CreditCard, IdCard, AlertTriangle, Check, ChevronDown, ChevronRight } from 'lucide-react';
import { FileText, User, CreditCard, IdCard, AlertTriangle, Check, ChevronDown, ChevronRight, Receipt } from 'lucide-react';
import Modal from '../ui/Modal';
import Button from '../ui/Button';
import Input from '../ui/Input';
import Select from '../ui/Select';
import { cachedEmailApi, AttachmentTargetSlot, AttachmentEntityWithSlots } from '../../services/api';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import type { InvoiceType } from '../../types';
interface SaveAttachmentModalProps {
isOpen: boolean;
@@ -22,6 +25,8 @@ type SelectedTarget = {
label: string;
};
type SaveMode = 'document' | 'invoice';
export default function SaveAttachmentModal({
isOpen,
onClose,
@@ -31,6 +36,12 @@ export default function SaveAttachmentModal({
}: SaveAttachmentModalProps) {
const [selectedTarget, setSelectedTarget] = useState<SelectedTarget | null>(null);
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['customer']));
const [saveMode, setSaveMode] = useState<SaveMode>('document');
const [invoiceData, setInvoiceData] = useState({
invoiceDate: new Date().toISOString().split('T')[0],
invoiceType: 'INTERIM' as InvoiceType,
notes: '',
});
const queryClient = useQueryClient();
// Ziele laden
@@ -42,6 +53,9 @@ 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';
const saveMutation = useMutation({
mutationFn: () => {
if (!selectedTarget) throw new Error('Kein Ziel ausgewählt');
@@ -73,8 +87,40 @@ export default function SaveAttachmentModal({
},
});
const saveInvoiceMutation = useMutation({
mutationFn: () => {
return cachedEmailApi.saveAttachmentAsInvoice(emailId, attachmentFilename, {
invoiceDate: invoiceData.invoiceDate,
invoiceType: invoiceData.invoiceType,
notes: invoiceData.notes || undefined,
});
},
onSuccess: () => {
toast.success('Anhang als Rechnung gespeichert');
queryClient.invalidateQueries({ queryKey: ['attachment-targets', emailId] });
queryClient.invalidateQueries({ queryKey: ['customers'] });
queryClient.invalidateQueries({ queryKey: ['contracts'] });
if (targets?.contract?.id) {
queryClient.invalidateQueries({ queryKey: ['contract', targets.contract.id.toString()] });
}
onSuccess?.();
handleClose();
},
onError: (error: Error) => {
toast.error(error.message || 'Fehler beim Speichern der Rechnung');
},
});
const handleClose = () => {
setSelectedTarget(null);
setSaveMode('document');
setInvoiceData({
invoiceDate: new Date().toISOString().split('T')[0],
invoiceType: 'INTERIM',
notes: '',
});
onClose();
};
@@ -215,59 +261,127 @@ export default function SaveAttachmentModal({
</div>
)}
{/* Targets */}
{targets && (
<div className="space-y-3 max-h-96 overflow-auto">
{/* Kunde */}
{renderSection(
`Kunde: ${targets.customer.name}`,
'customer',
<User className="w-4 h-4 text-blue-600" />,
renderSlots(targets.customer.slots, 'customer'),
targets.customer.slots.length === 0
)}
{/* Ausweise */}
{renderSection(
'Ausweisdokumente',
'identityDocuments',
<IdCard className="w-4 h-4 text-green-600" />,
targets.identityDocuments.map((doc) =>
renderEntityWithSlots(doc, 'identityDocument')
),
targets.identityDocuments.length === 0
)}
{/* Bankkarten */}
{renderSection(
'Bankkarten',
'bankCards',
<CreditCard className="w-4 h-4 text-purple-600" />,
targets.bankCards.map((card) => renderEntityWithSlots(card, 'bankCard')),
targets.bankCards.length === 0
)}
{/* Vertrag */}
{targets.contract && renderSection(
`Vertrag: ${targets.contract.contractNumber}`,
'contract',
<FileText className="w-4 h-4 text-orange-600" />,
renderSlots(targets.contract.slots, 'contract'),
targets.contract.slots.length === 0
)}
{!targets.contract && (
<div className="p-3 bg-gray-50 rounded-lg text-sm text-gray-600">
<FileText className="w-4 h-4 inline-block mr-2 text-gray-400" />
E-Mail ist keinem Vertrag zugeordnet. Ordnen Sie die E-Mail einem Vertrag zu, um
Vertragsdokumente als Ziel auswählen zu können.
<>
{/* Mode Toggle für Energieverträge */}
{isEnergyContract && (
<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 ${
saveMode === 'document'
? 'bg-white text-blue-600 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<FileText className="w-4 h-4" />
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'
: 'text-gray-600 hover:text-gray-900'
}`}
>
<Receipt className="w-4 h-4" />
Als Rechnung
</button>
</div>
)}
</div>
{/* Document Mode */}
{saveMode === 'document' && (
<div className="space-y-3 max-h-96 overflow-auto">
{/* Kunde */}
{renderSection(
`Kunde: ${targets.customer.name}`,
'customer',
<User className="w-4 h-4 text-blue-600" />,
renderSlots(targets.customer.slots, 'customer'),
targets.customer.slots.length === 0
)}
{/* Ausweise */}
{renderSection(
'Ausweisdokumente',
'identityDocuments',
<IdCard className="w-4 h-4 text-green-600" />,
targets.identityDocuments.map((doc) =>
renderEntityWithSlots(doc, 'identityDocument')
),
targets.identityDocuments.length === 0
)}
{/* Bankkarten */}
{renderSection(
'Bankkarten',
'bankCards',
<CreditCard className="w-4 h-4 text-purple-600" />,
targets.bankCards.map((card) => renderEntityWithSlots(card, 'bankCard')),
targets.bankCards.length === 0
)}
{/* Vertrag */}
{targets.contract && renderSection(
`Vertrag: ${targets.contract.contractNumber}`,
'contract',
<FileText className="w-4 h-4 text-orange-600" />,
renderSlots(targets.contract.slots, 'contract'),
targets.contract.slots.length === 0
)}
{!targets.contract && (
<div className="p-3 bg-gray-50 rounded-lg text-sm text-gray-600">
<FileText className="w-4 h-4 inline-block mr-2 text-gray-400" />
E-Mail ist keinem Vertrag zugeordnet. Ordnen Sie die E-Mail einem Vertrag zu, um
Vertragsdokumente als Ziel auswählen zu können.
</div>
)}
</div>
)}
{/* Invoice Mode */}
{saveMode === 'invoice' && isEnergyContract && (
<div className="space-y-4">
<div className="p-3 bg-green-50 rounded-lg">
<p className="text-sm text-green-700">
Der Anhang wird als Rechnung für den Vertrag <strong>{targets.contract?.contractNumber}</strong> gespeichert.
</p>
</div>
<Input
label="Rechnungsdatum"
type="date"
value={invoiceData.invoiceDate}
onChange={(e) => setInvoiceData({ ...invoiceData, invoiceDate: e.target.value })}
required
/>
<Select
label="Rechnungstyp"
value={invoiceData.invoiceType}
onChange={(e) => setInvoiceData({ ...invoiceData, invoiceType: e.target.value as InvoiceType })}
options={[
{ value: 'INTERIM', label: 'Zwischenrechnung' },
{ value: 'FINAL', label: 'Schlussrechnung' },
]}
/>
<Input
label="Notizen (optional)"
value={invoiceData.notes}
onChange={(e) => setInvoiceData({ ...invoiceData, notes: e.target.value })}
placeholder="Optionale Anmerkungen..."
/>
</div>
)}
</>
)}
{/* Warning if replacing */}
{selectedTarget?.hasDocument && (
{saveMode === 'document' && selectedTarget?.hasDocument && (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-2">
<AlertTriangle className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-yellow-800">
@@ -282,12 +396,21 @@ export default function SaveAttachmentModal({
<Button variant="secondary" onClick={handleClose}>
Abbrechen
</Button>
<Button
onClick={() => saveMutation.mutate()}
disabled={!selectedTarget || saveMutation.isPending}
>
{saveMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
</Button>
{saveMode === 'document' ? (
<Button
onClick={() => saveMutation.mutate()}
disabled={!selectedTarget || saveMutation.isPending || saveInvoiceMutation.isPending}
>
{saveMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
</Button>
) : (
<Button
onClick={() => saveInvoiceMutation.mutate()}
disabled={!invoiceData.invoiceDate || saveMutation.isPending || saveInvoiceMutation.isPending}
>
{saveInvoiceMutation.isPending ? 'Wird gespeichert...' : 'Als Rechnung speichern'}
</Button>
)}
</div>
</div>
</Modal>