added place to telecommunication, added contract documents, added invoice to other contracts
This commit is contained in:
@@ -17,15 +17,17 @@ const invoiceTypeLabels: Record<InvoiceType, string> = {
|
||||
};
|
||||
|
||||
interface InvoicesSectionProps {
|
||||
ecdId: number; // energyContractDetailsId
|
||||
ecdId?: number; // energyContractDetailsId (optional - für Energie-Verträge)
|
||||
invoices: Invoice[];
|
||||
contractId: number;
|
||||
canEdit: boolean;
|
||||
showInvoiceWarnings?: boolean; // Warnungen für fehlende Schluss-/Zwischenrechnung (nur Energie)
|
||||
}
|
||||
|
||||
export default function InvoicesSection({
|
||||
ecdId,
|
||||
invoices,
|
||||
showInvoiceWarnings = false,
|
||||
contractId,
|
||||
canEdit,
|
||||
}: InvoicesSectionProps) {
|
||||
@@ -35,9 +37,9 @@ export default function InvoicesSection({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const deleteInvoiceMutation = useMutation({
|
||||
mutationFn: (invoiceId: number) => invoiceApi.deleteInvoice(ecdId, invoiceId),
|
||||
mutationFn: (invoiceId: number) => ecdId ? invoiceApi.deleteInvoice(ecdId, invoiceId) : invoiceApi.deleteInvoice(0, invoiceId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -56,18 +58,18 @@ export default function InvoicesSection({
|
||||
<FileText className="w-4 h-4 text-gray-500" />
|
||||
<h4 className="text-sm font-medium text-gray-700">Rechnungen</h4>
|
||||
<Badge variant="default">{invoices.length}</Badge>
|
||||
{/* Status-Indicator */}
|
||||
{hasFinalInvoice ? (
|
||||
{/* Status-Indicator (nur bei Energie-Verträgen) */}
|
||||
{showInvoiceWarnings && hasFinalInvoice ? (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-green-100 text-green-800">
|
||||
<Check className="w-3 h-3" />
|
||||
Schlussrechnung
|
||||
</span>
|
||||
) : hasNotAvailable ? (
|
||||
) : showInvoiceWarnings && hasNotAvailable ? (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-yellow-100 text-yellow-800">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
Nicht verfügbar
|
||||
</span>
|
||||
) : invoices.length > 0 ? (
|
||||
) : showInvoiceWarnings && invoices.length > 0 ? (
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-orange-100 text-orange-800">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
Schlussrechnung fehlt
|
||||
@@ -198,7 +200,7 @@ function InvoiceModal({
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
ecdId: number;
|
||||
ecdId?: number;
|
||||
contractId: number;
|
||||
invoice?: Invoice | null;
|
||||
}) {
|
||||
@@ -216,10 +218,16 @@ function InvoiceModal({
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const addInvoiceFn = async (data: { invoiceDate: string; invoiceType: string; notes?: string }) => {
|
||||
if (ecdId) {
|
||||
return invoiceApi.addInvoice(ecdId, data as any);
|
||||
}
|
||||
return invoiceApi.addInvoiceByContract(contractId, data as any);
|
||||
};
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (file: File) => {
|
||||
// 1. Invoice erstellen
|
||||
const result = await invoiceApi.addInvoice(ecdId, {
|
||||
const result = await addInvoiceFn({
|
||||
invoiceDate: formData.invoiceDate,
|
||||
invoiceType: formData.invoiceType,
|
||||
notes: formData.notes || undefined,
|
||||
@@ -233,7 +241,7 @@ function InvoiceModal({
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
@@ -243,15 +251,14 @@ function InvoiceModal({
|
||||
|
||||
const createWithoutFileMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
// Für NOT_AVAILABLE Typ - kein Dokument erforderlich
|
||||
return await invoiceApi.addInvoice(ecdId, {
|
||||
return addInvoiceFn({
|
||||
invoiceDate: formData.invoiceDate,
|
||||
invoiceType: formData.invoiceType,
|
||||
notes: formData.notes || undefined,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
@@ -262,7 +269,7 @@ function InvoiceModal({
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async (file: File | null) => {
|
||||
// 1. Invoice aktualisieren
|
||||
const result = await invoiceApi.updateInvoice(ecdId, invoice!.id, {
|
||||
const result = await invoiceApi.updateInvoice(ecdId || 0, invoice!.id, {
|
||||
invoiceDate: formData.invoiceDate,
|
||||
invoiceType: formData.invoiceType,
|
||||
notes: formData.notes || undefined,
|
||||
@@ -276,7 +283,7 @@ function InvoiceModal({
|
||||
return result;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
onClose();
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
|
||||
@@ -13,11 +13,11 @@ import Badge from '../../components/ui/Badge';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Modal from '../../components/ui/Modal';
|
||||
import FileUpload from '../../components/ui/FileUpload';
|
||||
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield } from 'lucide-react';
|
||||
import { Edit, Trash2, Copy, Eye, EyeOff, ArrowLeft, ArrowRight, Download, ExternalLink, Plus, ChevronDown, ChevronUp, Gauge, CheckCircle, Circle, ClipboardList, MessageSquare, Calculator, Info, X, BellOff, Lock, Shield, FileText } from 'lucide-react';
|
||||
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
|
||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter } from '../../types';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter, ContractDocument } from '../../types';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
ELECTRICITY: 'Strom',
|
||||
@@ -2576,6 +2576,7 @@ export default function ContractDetail() {
|
||||
invoices={c.energyDetails.invoices || []}
|
||||
contractId={contractId}
|
||||
canEdit={hasPermission('contracts:update') && !isCustomer}
|
||||
showInvoiceWarnings={true}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
@@ -2614,6 +2615,24 @@ export default function ContractDetail() {
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.internetDetails.propertyType && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Objekttyp</dt>
|
||||
<dd>{c.internetDetails.propertyType}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.internetDetails.propertyLocation && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Lage</dt>
|
||||
<dd>{c.internetDetails.propertyLocation}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.internetDetails.connectionLocation && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Anschluss-Lage</dt>
|
||||
<dd>{c.internetDetails.connectionLocation}</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.internetDetails.installationDate && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Installation</dt>
|
||||
@@ -2960,6 +2979,25 @@ export default function ContractDetail() {
|
||||
isCustomerPortal={isCustomerPortal}
|
||||
/>
|
||||
|
||||
{/* Rechnungen (bei allen Vertragstypen, außer Energie - die haben ihre eigene Section) */}
|
||||
{!['ELECTRICITY', 'GAS'].includes(c.type) && !isCustomerPortal && (
|
||||
<Card className="mb-6">
|
||||
<InvoicesSection
|
||||
invoices={c.invoices || []}
|
||||
contractId={contractId}
|
||||
canEdit={hasPermission('contracts:update')}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Vertragsdokumente */}
|
||||
{!isCustomerPortal && (
|
||||
<ContractDocumentsSection
|
||||
contractId={contractId}
|
||||
canEdit={hasPermission('contracts:update')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Zugeordnete E-Mails */}
|
||||
{!isCustomerPortal && hasPermission('contracts:read') && c.customerId && (
|
||||
<ContractEmailsSection
|
||||
@@ -3057,3 +3095,177 @@ export default function ContractDetail() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== VERTRAGSDOKUMENTE ====================
|
||||
|
||||
const DOCUMENT_TYPES = [
|
||||
'Auftragsformular',
|
||||
'Auftragsbestätigung',
|
||||
'Lieferbestätigung',
|
||||
'Vertragsunterlagen',
|
||||
'Vollmacht',
|
||||
'Widerrufsbelehrung',
|
||||
'Preisblatt',
|
||||
'Sonstiges',
|
||||
];
|
||||
|
||||
function ContractDocumentsSection({
|
||||
contractId,
|
||||
canEdit,
|
||||
}: {
|
||||
contractId: number;
|
||||
canEdit: boolean;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [uploadType, setUploadType] = useState(DOCUMENT_TYPES[0]);
|
||||
const [uploadNotes, setUploadNotes] = useState('');
|
||||
|
||||
const { data: docsData } = useQuery({
|
||||
queryKey: ['contract-documents', contractId],
|
||||
queryFn: () => contractApi.getDocuments(contractId),
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: ({ file, documentType, notes }: { file: File; documentType: string; notes?: string }) =>
|
||||
contractApi.uploadDocument(contractId, file, documentType, notes),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
|
||||
setShowUpload(false);
|
||||
setUploadNotes('');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (documentId: number) => contractApi.deleteDocument(contractId, documentId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-documents', contractId] });
|
||||
},
|
||||
});
|
||||
|
||||
const documents: ContractDocument[] = docsData?.data || [];
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
uploadMutation.mutate({ file, documentType: uploadType, notes: uploadNotes || undefined });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="mb-6" title={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5" />
|
||||
<span>Vertragsdokumente</span>
|
||||
<span className="text-sm font-normal text-gray-500">({documents.length})</span>
|
||||
</div>
|
||||
{canEdit && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowUpload(!showUpload)}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
}>
|
||||
{/* Upload-Bereich */}
|
||||
{showUpload && (
|
||||
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mb-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Dokumenttyp</label>
|
||||
<select
|
||||
value={uploadType}
|
||||
onChange={(e) => setUploadType(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
{DOCUMENT_TYPES.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Notiz (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={uploadNotes}
|
||||
onChange={(e) => setUploadNotes(e.target.value)}
|
||||
placeholder="z.B. Unterschrieben am 15.03.2026"
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 cursor-pointer text-sm">
|
||||
<Plus className="w-4 h-4" />
|
||||
Datei wählen (PDF, JPG, PNG)
|
||||
<input type="file" accept=".pdf,.jpg,.jpeg,.png" className="hidden" onChange={handleFileSelect} />
|
||||
</label>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowUpload(false)}>Abbrechen</Button>
|
||||
{uploadMutation.isPending && <span className="text-sm text-gray-500">Hochladen...</span>}
|
||||
</div>
|
||||
{uploadMutation.isError && (
|
||||
<p className="text-xs text-red-600 mt-2">Fehler beim Hochladen</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dokumentliste */}
|
||||
{documents.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">Keine Dokumente vorhanden.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{documents.map((doc) => (
|
||||
<div key={doc.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<FileText className="w-4 h-4 text-gray-400" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-100 text-blue-700">
|
||||
{doc.documentType}
|
||||
</span>
|
||||
<a
|
||||
href={`/api${doc.documentPath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 hover:underline"
|
||||
>
|
||||
{doc.originalName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-0.5 text-xs text-gray-500">
|
||||
<span>{formatDate(doc.createdAt)}</span>
|
||||
{doc.uploadedBy && <span>von {doc.uploadedBy}</span>}
|
||||
{doc.notes && <span>– {doc.notes}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={`/api${doc.documentPath}`}
|
||||
download
|
||||
className="text-gray-400 hover:text-blue-600"
|
||||
title="Herunterladen"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</a>
|
||||
{canEdit && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (confirm(`Dokument "${doc.originalName}" wirklich löschen?`)) {
|
||||
deleteMutation.mutate(doc.id);
|
||||
}
|
||||
}}
|
||||
className="text-gray-400 hover:text-red-600"
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -288,6 +288,9 @@ export default function ContractForm() {
|
||||
routerSerialNumber: c.internetDetails?.routerSerialNumber || '',
|
||||
installationDate: c.internetDetails?.installationDate ? c.internetDetails.installationDate.split('T')[0] : '',
|
||||
internetUsername: c.internetDetails?.internetUsername || '',
|
||||
propertyType: c.internetDetails?.propertyType || '',
|
||||
propertyLocation: c.internetDetails?.propertyLocation || '',
|
||||
connectionLocation: c.internetDetails?.connectionLocation || '',
|
||||
homeId: c.internetDetails?.homeId || '',
|
||||
activationCode: c.internetDetails?.activationCode || '',
|
||||
// Mobile details
|
||||
@@ -531,6 +534,10 @@ export default function ContractForm() {
|
||||
// Internet-Zugangsdaten
|
||||
internetUsername: emptyToNull(data.internetUsername),
|
||||
internetPassword: data.internetPassword || undefined, // Passwort: undefined = nicht ändern
|
||||
// Objekt & Lage
|
||||
propertyType: emptyToNull(data.propertyType),
|
||||
propertyLocation: emptyToNull(data.propertyLocation),
|
||||
connectionLocation: emptyToNull(data.connectionLocation),
|
||||
// Glasfaser-spezifisch
|
||||
homeId: emptyToNull(data.homeId),
|
||||
// Vodafone DSL/Kabel spezifisch
|
||||
@@ -1027,6 +1034,65 @@ export default function ContractForm() {
|
||||
value={watch('installationDate') || ''}
|
||||
onClear={() => setValue('installationDate', '')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Objekt & Lage */}
|
||||
<h4 className="text-sm font-medium text-gray-700 mt-4 mb-2">Objekt & Lage</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Select
|
||||
label="Objekttyp"
|
||||
{...register('propertyType')}
|
||||
options={[
|
||||
{ value: '', label: 'Bitte wählen...' },
|
||||
{ value: 'Mehrparteienhaus', label: 'Mehrparteienhaus' },
|
||||
{ value: 'Freistehendes Haus', label: 'Freistehendes Haus' },
|
||||
{ value: 'Doppelhaushälfte', label: 'Doppelhaushälfte' },
|
||||
{ value: 'Reihenhaus', label: 'Reihenhaus' },
|
||||
{ value: 'Wohnung', label: 'Wohnung' },
|
||||
{ value: 'Bürogebäude', label: 'Bürogebäude' },
|
||||
{ value: 'Gewerbeeinheit', label: 'Gewerbeeinheit' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Lage"
|
||||
{...register('propertyLocation')}
|
||||
options={[
|
||||
{ value: '', label: 'Bitte wählen...' },
|
||||
{ value: 'Vorderhaus', label: 'Vorderhaus' },
|
||||
{ value: 'Hinterhaus', label: 'Hinterhaus' },
|
||||
{ value: 'Links', label: 'Links' },
|
||||
{ value: 'Rechts', label: 'Rechts' },
|
||||
{ value: 'Mitte', label: 'Mitte' },
|
||||
{ value: 'Keller', label: 'Keller' },
|
||||
{ value: 'Souterrain', label: 'Souterrain' },
|
||||
{ value: 'Erdgeschoss', label: 'Erdgeschoss' },
|
||||
...[...Array(25)].map((_, i) => ({ value: `${i + 1}. OG`, label: `${i + 1}. Obergeschoss` })),
|
||||
{ value: 'Dachgeschoss', label: 'Dachgeschoss' },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
label="Lage des Anschlusses"
|
||||
{...register('connectionLocation')}
|
||||
options={[
|
||||
{ value: '', label: 'Bitte wählen...' },
|
||||
{ value: 'Flur', label: 'Flur' },
|
||||
{ value: 'Wohnzimmer', label: 'Wohnzimmer' },
|
||||
{ value: 'Schlafzimmer', label: 'Schlafzimmer' },
|
||||
{ value: 'Kinderzimmer', label: 'Kinderzimmer' },
|
||||
{ value: 'Küche', label: 'Küche' },
|
||||
{ value: 'Büro', label: 'Büro' },
|
||||
{ value: 'HWR', label: 'Hauswirtschaftsraum (HWR)' },
|
||||
{ value: 'Hausanschlussraum', label: 'Hausanschlussraum' },
|
||||
{ value: 'Abstellraum', label: 'Abstellraum' },
|
||||
{ value: 'Garage', label: 'Garage' },
|
||||
{ value: 'Serverraum', label: 'Serverraum' },
|
||||
{ value: 'Empfang', label: 'Empfang / Rezeption' },
|
||||
{ value: 'Keller', label: 'Keller' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
{/* HomeID nur bei Glasfaser */}
|
||||
{contractType === 'FIBER' && (
|
||||
<Input label="Home-ID" {...register('homeId')} />
|
||||
|
||||
@@ -232,6 +232,10 @@ export const invoiceApi = {
|
||||
const res = await api.post<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices`, data);
|
||||
return res.data;
|
||||
},
|
||||
addInvoiceByContract: async (contractId: number, data: Partial<Invoice>) => {
|
||||
const res = await api.post<ApiResponse<Invoice>>(`/contracts/${contractId}/invoices`, data);
|
||||
return res.data;
|
||||
},
|
||||
updateInvoice: async (ecdId: number, invoiceId: number, data: Partial<Invoice>) => {
|
||||
const res = await api.put<ApiResponse<Invoice>>(`/energy-details/${ecdId}/invoices/${invoiceId}`, data);
|
||||
return res.data;
|
||||
@@ -655,6 +659,25 @@ export const contractApi = {
|
||||
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
|
||||
return res.data;
|
||||
},
|
||||
// Vertragsdokumente
|
||||
getDocuments: async (contractId: number) => {
|
||||
const res = await api.get<ApiResponse<import('../types').ContractDocument[]>>(`/contracts/${contractId}/documents`);
|
||||
return res.data;
|
||||
},
|
||||
uploadDocument: async (contractId: number, file: File, documentType: string, notes?: string) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('documentType', documentType);
|
||||
if (notes) formData.append('notes', notes);
|
||||
const res = await api.post<ApiResponse<import('../types').ContractDocument>>(`/contracts/${contractId}/documents`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
return res.data;
|
||||
},
|
||||
deleteDocument: async (contractId: number, documentId: number) => {
|
||||
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/documents/${documentId}`);
|
||||
return res.data;
|
||||
},
|
||||
// Folgezähler
|
||||
addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => {
|
||||
const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data);
|
||||
|
||||
@@ -152,6 +152,17 @@ export interface IdentityDocument {
|
||||
licenseIssueDate?: string;
|
||||
}
|
||||
|
||||
export interface ContractDocument {
|
||||
id: number;
|
||||
contractId: number;
|
||||
documentType: string;
|
||||
documentPath: string;
|
||||
originalName: string;
|
||||
notes?: string;
|
||||
uploadedBy?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type MeterTariffModel = 'SINGLE' | 'DUAL';
|
||||
|
||||
export interface Meter {
|
||||
@@ -391,6 +402,8 @@ export interface Contract {
|
||||
mobileDetails?: MobileContractDetails;
|
||||
tvDetails?: TvContractDetails;
|
||||
carInsuranceDetails?: CarInsuranceDetails;
|
||||
invoices?: Invoice[];
|
||||
documents?: ContractDocument[];
|
||||
// Snooze: Vertrag zurückstellen
|
||||
nextReviewDate?: string;
|
||||
followUpContract?: {
|
||||
@@ -431,6 +444,10 @@ export interface InternetContractDetails {
|
||||
// Internet-Zugangsdaten
|
||||
internetUsername?: string;
|
||||
internetPasswordEncrypted?: string;
|
||||
// Objekt & Lage
|
||||
propertyType?: string;
|
||||
propertyLocation?: string;
|
||||
connectionLocation?: string;
|
||||
// Glasfaser-spezifisch
|
||||
homeId?: string;
|
||||
// Vodafone DSL/Kabel spezifisch
|
||||
|
||||
Reference in New Issue
Block a user