added new view in contracts customer and contracts
This commit is contained in:
parent
97b4670643
commit
2b23ed64c4
20
README.md
20
README.md
|
|
@ -558,6 +558,26 @@ Der X-Button zum Aufheben der Vertragszuordnung erscheint nur bei **manuell zuge
|
|||
> - `isAutoAssigned = true`: E-Mail wurde direkt aus dem Vertragskontext gesendet → X-Button ausgeblendet
|
||||
> - `isAutoAssigned = false`: E-Mail wurde manuell dem Vertrag zugeordnet → X-Button sichtbar
|
||||
|
||||
### Vertragsbaum in Kundenansicht
|
||||
|
||||
In der Kundendetailansicht werden Verträge als **Baumstruktur** mit Vorgänger-Verknüpfung dargestellt:
|
||||
|
||||
```
|
||||
▼ GAS-ML781A4FYXU │ Gas │ ACTIVE │ 01.01.2025 - 31.12.2026
|
||||
└─ GAS-ML24GKR...│ Gas │ EXPIRED│ 05.05.2023 - 05.05.2025 (Vorgänger)
|
||||
└─ GAS-OLD123 │ Gas │ EXPIRED│ 01.01.2021 - 04.05.2023 (Vorgänger)
|
||||
|
||||
▶ MOB-ML77W560A73 │ Mobil│ DRAFT │ 02.01.2024 - 02.01.2026
|
||||
```
|
||||
|
||||
**Funktionsweise:**
|
||||
- **Aktuellste Verträge oben** - Verträge ohne Nachfolger werden als Wurzelknoten angezeigt
|
||||
- **Standardmäßig eingeklappt** - Klick auf ▶ zeigt die Vorgängerkette
|
||||
- **Vorgänger eingerückt** - Mit grauem Rand und "(Vorgänger)" Label
|
||||
- **Verknüpfung über `previousContractId`** - Wird beim Erstellen eines Folgevertrags automatisch gesetzt
|
||||
|
||||
> **Hinweis:** In der Hauptvertragsliste (`/contracts`) wird weiterhin die flache Ansicht ohne Baumstruktur verwendet.
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -5,7 +5,16 @@ import { ApiResponse, AuthRequest } from '../types/index.js';
|
|||
|
||||
export async function getContracts(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const { customerId, type, status, search, page, limit } = req.query;
|
||||
const { customerId, type, status, search, page, limit, tree } = req.query;
|
||||
|
||||
// Baumstruktur für Kundenansicht
|
||||
if (tree === 'true' && customerId) {
|
||||
const treeData = await contractService.getContractTreeForCustomer(
|
||||
parseInt(customerId as string)
|
||||
);
|
||||
res.json({ success: true, data: treeData } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
// Für Kundenportal-Benutzer: nur eigene + vertretene Kunden-Verträge anzeigen
|
||||
let customerIds: number[] | undefined;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export async function getAllContracts(filters: ContractFilters) {
|
|||
where,
|
||||
skip,
|
||||
take,
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
orderBy: [{ createdAt: 'desc' }],
|
||||
include: {
|
||||
customer: {
|
||||
select: {
|
||||
|
|
@ -778,3 +778,96 @@ export async function getSipCredentials(phoneNumberId: number): Promise<{ passwo
|
|||
return { password: null };
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== VERTRAGSBAUM FÜR KUNDENANSICHT ====================
|
||||
|
||||
export interface ContractTreeNode {
|
||||
contract: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
type: ContractType;
|
||||
status: ContractStatus;
|
||||
startDate: Date | null;
|
||||
endDate: Date | null;
|
||||
providerName: string | null;
|
||||
tariffName: string | null;
|
||||
previousContractId: number | null;
|
||||
provider?: { id: number; name: string } | null;
|
||||
tariff?: { id: number; name: string } | null;
|
||||
contractCategory?: { id: number; name: string } | null;
|
||||
};
|
||||
predecessors: ContractTreeNode[];
|
||||
hasHistory: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verträge eines Kunden als Baumstruktur abrufen.
|
||||
* Wurzelknoten = Verträge ohne Nachfolger (aktuellste Verträge)
|
||||
* Vorgänger werden rekursiv eingebettet.
|
||||
*/
|
||||
export async function getContractTreeForCustomer(customerId: number): Promise<ContractTreeNode[]> {
|
||||
// Alle Verträge des Kunden laden (außer DEACTIVATED)
|
||||
const allContracts = await prisma.contract.findMany({
|
||||
where: {
|
||||
customerId,
|
||||
status: { not: ContractStatus.DEACTIVATED },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
type: true,
|
||||
status: true,
|
||||
startDate: true,
|
||||
endDate: true,
|
||||
providerName: true,
|
||||
tariffName: true,
|
||||
previousContractId: true,
|
||||
provider: { select: { id: true, name: true } },
|
||||
tariff: { select: { id: true, name: true } },
|
||||
contractCategory: { select: { id: true, name: true } },
|
||||
},
|
||||
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
|
||||
});
|
||||
|
||||
// Map für schnellen Zugriff: contractId -> contract
|
||||
const contractMap = new Map(allContracts.map(c => [c.id, c]));
|
||||
|
||||
// Set der IDs die als Vorgänger referenziert werden
|
||||
const predecessorIds = new Set(
|
||||
allContracts
|
||||
.filter(c => c.previousContractId !== null)
|
||||
.map(c => c.previousContractId!)
|
||||
);
|
||||
|
||||
// Wurzelverträge = Verträge die keinen Nachfolger haben
|
||||
// (werden von keinem anderen Vertrag als previousContractId referenziert)
|
||||
const rootContracts = allContracts.filter(c => !predecessorIds.has(c.id));
|
||||
|
||||
// Rekursive Funktion um Vorgängerkette aufzubauen
|
||||
function buildPredecessorChain(contractId: number | null): ContractTreeNode[] {
|
||||
if (contractId === null) return [];
|
||||
|
||||
const contract = contractMap.get(contractId);
|
||||
if (!contract) return [];
|
||||
|
||||
const predecessors = buildPredecessorChain(contract.previousContractId);
|
||||
|
||||
return [{
|
||||
contract,
|
||||
predecessors,
|
||||
hasHistory: predecessors.length > 0,
|
||||
}];
|
||||
}
|
||||
|
||||
// Baumstruktur für jeden Wurzelvertrag aufbauen
|
||||
const tree: ContractTreeNode[] = rootContracts.map(contract => {
|
||||
const predecessors = buildPredecessorChain(contract.previousContractId);
|
||||
return {
|
||||
contract,
|
||||
predecessors,
|
||||
hasHistory: predecessors.length > 0,
|
||||
};
|
||||
});
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenCRM</title>
|
||||
<script type="module" crossorigin src="/assets/index-DxzmsVZ0.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CUVVQncv.css">
|
||||
<script type="module" crossorigin src="/assets/index-CooZFd_R.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DWDTTlpk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default function EmailDetail({
|
|||
onReply,
|
||||
onAssignContract,
|
||||
onDeleted,
|
||||
isSentFolder = false,
|
||||
isSentFolder: _isSentFolder = false,
|
||||
isContractView = false,
|
||||
isTrashView = false,
|
||||
onRestored,
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export default function ContractForm() {
|
|||
customerId: preselectedCustomerId || '',
|
||||
type: 'ELECTRICITY',
|
||||
status: 'DRAFT',
|
||||
previousContractId: '',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -61,6 +62,13 @@ export default function ContractForm() {
|
|||
enabled: !!customerId,
|
||||
});
|
||||
|
||||
// Fetch contracts for same customer (for predecessor selection)
|
||||
const { data: customerContractsData } = useQuery({
|
||||
queryKey: ['customer-contracts-for-predecessor', customerId],
|
||||
queryFn: () => contractApi.getAll({ customerId: parseInt(customerId), limit: 1000 }),
|
||||
enabled: !!customerId,
|
||||
});
|
||||
|
||||
// Fetch platforms
|
||||
const { data: platformsData } = useQuery({
|
||||
queryKey: ['platforms'],
|
||||
|
|
@ -256,6 +264,8 @@ export default function ContractForm() {
|
|||
cancellationConfirmationDate: c.cancellationConfirmationDate ? c.cancellationConfirmationDate.split('T')[0] : '',
|
||||
cancellationConfirmationOptionsDate: c.cancellationConfirmationOptionsDate ? c.cancellationConfirmationOptionsDate.split('T')[0] : '',
|
||||
wasSpecialCancellation: c.wasSpecialCancellation || false,
|
||||
// Vorgänger-Vertrag
|
||||
previousContractId: c.previousContractId?.toString() || '',
|
||||
});
|
||||
|
||||
// Load simCards if available
|
||||
|
|
@ -427,6 +437,7 @@ export default function ContractForm() {
|
|||
cancellationConfirmationDate: data.cancellationConfirmationDate ? new Date(data.cancellationConfirmationDate) : null,
|
||||
cancellationConfirmationOptionsDate: data.cancellationConfirmationOptionsDate ? new Date(data.cancellationConfirmationOptionsDate) : null,
|
||||
wasSpecialCancellation: data.wasSpecialCancellation || false,
|
||||
previousContractId: safeParseInt(data.previousContractId) ?? null,
|
||||
};
|
||||
|
||||
// Add type-specific details
|
||||
|
|
@ -540,6 +551,11 @@ export default function ContractForm() {
|
|||
const contractCategories = contractCategoriesData?.data?.filter(c => c.isActive).sort((a, b) => a.sortOrder - b.sortOrder) || [];
|
||||
const typeOptions = contractCategories.map(c => ({ value: c.code, label: c.name }));
|
||||
|
||||
// Available predecessor contracts (same customer, excluding current contract if editing)
|
||||
const predecessorContracts = (customerContractsData?.data || [])
|
||||
.filter(c => !isEdit || c.id !== parseInt(id!))
|
||||
.sort((a, b) => new Date(b.startDate || 0).getTime() - new Date(a.startDate || 0).getTime());
|
||||
|
||||
// Get tariffs for selected provider
|
||||
const selectedProvider = providers.find(p => p.id === parseInt(selectedProviderId || '0'));
|
||||
const availableTariffs = selectedProvider?.tariffs?.filter(t => t.isActive) || [];
|
||||
|
|
@ -615,6 +631,19 @@ export default function ContractForm() {
|
|||
{...register('salesPlatformId')}
|
||||
options={platforms.map((p) => ({ value: p.id, label: p.name }))}
|
||||
/>
|
||||
|
||||
{/* Vorgänger-Vertrag auswählen (nur wenn Kunde gewählt) */}
|
||||
{customerId && (
|
||||
<Select
|
||||
label="Vorgänger-Vertrag"
|
||||
{...register('previousContractId')}
|
||||
options={predecessorContracts.map((c) => ({
|
||||
value: c.id,
|
||||
label: `${c.contractNumber} (${c.type}${c.startDate ? ` - ${new Date(c.startDate).toLocaleDateString('de-DE')}` : ''})`,
|
||||
}))}
|
||||
placeholder="Keinen Vorgänger auswählen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, StressfreiEmail } from '../../services/api';
|
||||
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, StressfreiEmail, ContractTreeNode } from '../../services/api';
|
||||
import { EmailClientTab } from '../../components/email';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import Card from '../../components/ui/Card';
|
||||
|
|
@ -12,7 +12,7 @@ import Modal from '../../components/ui/Modal';
|
|||
import Input from '../../components/ui/Input';
|
||||
import Select from '../../components/ui/Select';
|
||||
import FileUpload from '../../components/ui/FileUpload';
|
||||
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check } from 'lucide-react';
|
||||
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary } from '../../types';
|
||||
|
||||
|
|
@ -146,7 +146,6 @@ export default function CustomerDetail() {
|
|||
content: (
|
||||
<ContractsTab
|
||||
customerId={customerId}
|
||||
contracts={c.contracts || []}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
@ -1488,14 +1487,21 @@ function MetersTab({
|
|||
|
||||
function ContractsTab({
|
||||
customerId,
|
||||
contracts,
|
||||
}: {
|
||||
customerId: number;
|
||||
contracts: any[];
|
||||
}) {
|
||||
const { hasPermission } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [expandedContracts, setExpandedContracts] = useState<Set<number>>(new Set());
|
||||
|
||||
// Lade Vertragsbaum statt flacher Liste
|
||||
const { data: treeData, isLoading } = useQuery({
|
||||
queryKey: ['contract-tree', customerId],
|
||||
queryFn: () => contractApi.getTreeForCustomer(customerId),
|
||||
});
|
||||
|
||||
const contractTree = treeData?.data || [];
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: contractApi.delete,
|
||||
|
|
@ -1503,6 +1509,7 @@ function ContractsTab({
|
|||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-tree', customerId] });
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error?.message || 'Fehler beim Löschen des Vertrags');
|
||||
|
|
@ -1528,6 +1535,142 @@ function ContractsTab({
|
|||
DEACTIVATED: 'default',
|
||||
};
|
||||
|
||||
const toggleExpand = (contractId: number) => {
|
||||
setExpandedContracts(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(contractId)) {
|
||||
next.delete(contractId);
|
||||
} else {
|
||||
next.add(contractId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Rekursive Rendering-Funktion für Vorgänger
|
||||
const renderPredecessors = (predecessors: ContractTreeNode[], depth: number): React.ReactNode => {
|
||||
return predecessors.map(node => (
|
||||
<div key={node.contract.id}>
|
||||
{renderContractNode(node, depth)}
|
||||
</div>
|
||||
));
|
||||
};
|
||||
|
||||
// Einzelnen Vertragsknoten rendern
|
||||
const renderContractNode = (node: ContractTreeNode, depth: number = 0): React.ReactNode => {
|
||||
const { contract, predecessors, hasHistory } = node;
|
||||
const isExpanded = expandedContracts.has(contract.id);
|
||||
const isPredecessor = depth > 0;
|
||||
|
||||
return (
|
||||
<div key={contract.id}>
|
||||
<div
|
||||
className={`
|
||||
border rounded-lg p-4 transition-colors
|
||||
${isPredecessor ? 'ml-6 border-l-4 border-l-gray-300 bg-gray-50' : 'hover:bg-gray-50'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Aufklapp-Button nur bei Wurzelknoten mit Historie */}
|
||||
{!isPredecessor && hasHistory ? (
|
||||
<button
|
||||
onClick={() => toggleExpand(contract.id)}
|
||||
className="p-1 hover:bg-gray-200 rounded transition-colors"
|
||||
title={isExpanded ? 'Einklappen' : 'Vorgänger anzeigen'}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
) : !isPredecessor ? (
|
||||
<div className="w-6" /> // Platzhalter für Ausrichtung
|
||||
) : null}
|
||||
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
{contract.contractNumber}
|
||||
<CopyButton value={contract.contractNumber} />
|
||||
</span>
|
||||
<Badge>{typeLabels[contract.type] || contract.type}</Badge>
|
||||
<Badge variant={statusVariants[contract.status] || 'default'}>{contract.status}</Badge>
|
||||
|
||||
{isPredecessor && (
|
||||
<span className="text-xs text-gray-500 ml-2">(Vorgänger)</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/contracts/${contract.id}`, {
|
||||
state: { from: 'customer', customerId: customerId.toString() }
|
||||
})}
|
||||
title="Ansehen"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{hasPermission('contracts:update') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/contracts/${contract.id}/edit`)}
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('contracts:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (confirm('Vertrag wirklich löschen?')) {
|
||||
deleteMutation.mutate(contract.id);
|
||||
}
|
||||
}}
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{(contract.providerName || contract.provider?.name) && (
|
||||
<p className={`flex items-center gap-1 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
{contract.providerName || contract.provider?.name}
|
||||
{(contract.tariffName || contract.tariff?.name) && ` - ${contract.tariffName || contract.tariff?.name}`}
|
||||
<CopyButton value={(contract.providerName || contract.provider?.name || '') + ((contract.tariffName || contract.tariff?.name) ? ` - ${contract.tariffName || contract.tariff?.name}` : '')} />
|
||||
</p>
|
||||
)}
|
||||
{contract.startDate && (
|
||||
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
Beginn: {new Date(contract.startDate).toLocaleDateString('de-DE')}
|
||||
{contract.endDate &&
|
||||
` | Ende: ${new Date(contract.endDate).toLocaleDateString('de-DE')}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vorgänger rekursiv rendern - für Wurzel nur wenn aufgeklappt, für Vorgänger immer */}
|
||||
{((depth === 0 && isExpanded) || depth > 0) && predecessors.length > 0 && (
|
||||
<div className="mt-2">
|
||||
{renderPredecessors(predecessors, depth + 1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{hasPermission('contracts:create') && (
|
||||
|
|
@ -1541,75 +1684,9 @@ function ContractsTab({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{contracts.length > 0 ? (
|
||||
{contractTree.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{contracts.map((contract) => (
|
||||
<div
|
||||
key={contract.id}
|
||||
className="border rounded-lg p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
{contract.contractNumber}
|
||||
<CopyButton value={contract.contractNumber} />
|
||||
</span>
|
||||
<Badge>{typeLabels[contract.type]}</Badge>
|
||||
<Badge variant={statusVariants[contract.status]}>{contract.status}</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/contracts/${contract.id}`, {
|
||||
state: { from: 'customer', customerId: customerId.toString() }
|
||||
})}
|
||||
title="Ansehen"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{hasPermission('contracts:update') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/contracts/${contract.id}/edit`)}
|
||||
title="Bearbeiten"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{hasPermission('contracts:delete') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (confirm('Vertrag wirklich löschen?')) {
|
||||
deleteMutation.mutate(contract.id);
|
||||
}
|
||||
}}
|
||||
title="Löschen"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-red-500" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{contract.providerName && (
|
||||
<p className="flex items-center gap-1">
|
||||
{contract.providerName}
|
||||
{contract.tariffName && ` - ${contract.tariffName}`}
|
||||
<CopyButton value={contract.providerName + (contract.tariffName ? ` - ${contract.tariffName}` : '')} />
|
||||
</p>
|
||||
)}
|
||||
{contract.startDate && (
|
||||
<p className="text-sm text-gray-500">
|
||||
Beginn: {new Date(contract.startDate).toLocaleDateString('de-DE')}
|
||||
{contract.endDate &&
|
||||
` | Ende: ${new Date(contract.endDate).toLocaleDateString('de-DE')}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{contractTree.map(node => renderContractNode(node, 0))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Keine Verträge vorhanden.</p>
|
||||
|
|
|
|||
|
|
@ -508,12 +508,37 @@ export const cachedEmailApi = {
|
|||
},
|
||||
};
|
||||
|
||||
// Contracts
|
||||
// Contracts - Vertragsbaum für Kundenansicht
|
||||
export interface ContractTreeNodeContract {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
type: string;
|
||||
status: string;
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
providerName: string | null;
|
||||
tariffName: string | null;
|
||||
previousContractId: number | null;
|
||||
provider?: { id: number; name: string } | null;
|
||||
tariff?: { id: number; name: string } | null;
|
||||
contractCategory?: { id: number; name: string } | null;
|
||||
}
|
||||
|
||||
export interface ContractTreeNode {
|
||||
contract: ContractTreeNodeContract;
|
||||
predecessors: ContractTreeNode[];
|
||||
hasHistory: boolean;
|
||||
}
|
||||
|
||||
export const contractApi = {
|
||||
getAll: async (params?: { customerId?: number; type?: string; status?: string; search?: string; page?: number; limit?: number }) => {
|
||||
const res = await api.get<ApiResponse<Contract[]>>('/contracts', { params });
|
||||
return res.data;
|
||||
},
|
||||
getTreeForCustomer: async (customerId: number) => {
|
||||
const res = await api.get<ApiResponse<ContractTreeNode[]>>('/contracts', { params: { customerId, tree: 'true' } });
|
||||
return res.data;
|
||||
},
|
||||
getById: async (id: number) => {
|
||||
const res = await api.get<ApiResponse<Contract>>(`/contracts/${id}`);
|
||||
return res.data;
|
||||
|
|
|
|||
Loading…
Reference in New Issue