Compare commits
2 Commits
2ee06630b9
...
0d024b94c2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d024b94c2 | |||
| b4b0dbb004 |
@@ -87,6 +87,24 @@ export async function getCustomerById(id: number) {
|
|||||||
readings: {
|
readings: {
|
||||||
orderBy: { readingDate: 'desc' },
|
orderBy: { readingDate: 'desc' },
|
||||||
},
|
},
|
||||||
|
// Verträge, die diesen Zähler aktuell als Hauptzähler nutzen
|
||||||
|
// (energyDetails.meterId === meter.id)
|
||||||
|
energyDetails: {
|
||||||
|
include: {
|
||||||
|
contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Verträge, in denen der Zähler in der ContractMeter-Kette steht
|
||||||
|
// (Vorgänger oder Nachfolger über Zählerwechsel)
|
||||||
|
contractMeters: {
|
||||||
|
include: {
|
||||||
|
energyContractDetails: {
|
||||||
|
include: {
|
||||||
|
contract: { select: { id: true, contractNumber: true, status: true, type: true, providerName: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
stressfreiEmails: { orderBy: { isActive: 'desc' } },
|
stressfreiEmails: { orderBy: { isActive: 'desc' } },
|
||||||
|
|||||||
@@ -1267,6 +1267,7 @@ function MetersTab({
|
|||||||
const hasDeliveryAddress = addresses.some((a) => a.type === 'DELIVERY_RESIDENCE');
|
const hasDeliveryAddress = addresses.some((a) => a.type === 'DELIVERY_RESIDENCE');
|
||||||
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null);
|
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null);
|
||||||
const [expandedMeter, setExpandedMeter] = useState<number | null>(null);
|
const [expandedMeter, setExpandedMeter] = useState<number | null>(null);
|
||||||
|
const [expandedContractsForMeter, setExpandedContractsForMeter] = useState<Set<number>>(new Set());
|
||||||
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null);
|
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -1295,7 +1296,17 @@ function MetersTab({
|
|||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const filtered = showInactive ? meters : meters.filter((m) => m.isActive);
|
const [showWithoutContracts, setShowWithoutContracts] = useState(false);
|
||||||
|
|
||||||
|
// Liefert true, wenn der Zähler mindestens einem Vertrag zugeordnet ist
|
||||||
|
// (entweder als Hauptzähler oder über die Folgezähler-Kette).
|
||||||
|
const hasAnyContract = (m: Meter) =>
|
||||||
|
(m.energyDetails?.some((ed) => ed.contract) ?? false)
|
||||||
|
|| (m.contractMeters?.some((cm) => cm.energyContractDetails?.contract) ?? false);
|
||||||
|
|
||||||
|
const filtered = meters
|
||||||
|
.filter((m) => showInactive ? true : m.isActive)
|
||||||
|
.filter((m) => showWithoutContracts ? true : hasAnyContract(m));
|
||||||
|
|
||||||
// Sort readings by date (newest first)
|
// Sort readings by date (newest first)
|
||||||
const getSortedReadings = (readings: any[] | undefined) => {
|
const getSortedReadings = (readings: any[] | undefined) => {
|
||||||
@@ -1328,6 +1339,15 @@ function MetersTab({
|
|||||||
/>
|
/>
|
||||||
Inaktive anzeigen
|
Inaktive anzeigen
|
||||||
</label>
|
</label>
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showWithoutContracts}
|
||||||
|
onChange={(e) => setShowWithoutContracts(e.target.checked)}
|
||||||
|
className="rounded"
|
||||||
|
/>
|
||||||
|
Zähler ohne Verträge anzeigen
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{canEdit && !hasDeliveryAddress && (
|
{canEdit && !hasDeliveryAddress && (
|
||||||
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
|
||||||
@@ -1441,6 +1461,77 @@ function MetersTab({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(() => {
|
||||||
|
// Zugeordnete Verträge zusammenstellen: zum einen über
|
||||||
|
// energyDetails (aktueller Hauptzähler), zum anderen über
|
||||||
|
// contractMeters (Folgezähler-Kette). Dedupliziert auf
|
||||||
|
// contractId, da ein Zähler über beide Wege auftauchen kann.
|
||||||
|
const seen = new Set<number>();
|
||||||
|
const linkedContracts: Array<{ id: number; contractNumber: string; status: string; type: string; providerName?: string; via: 'main' | 'chain' }> = [];
|
||||||
|
for (const ed of meter.energyDetails || []) {
|
||||||
|
if (ed.contract && !seen.has(ed.contract.id)) {
|
||||||
|
seen.add(ed.contract.id);
|
||||||
|
linkedContracts.push({ ...ed.contract, via: 'main' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const cm of meter.contractMeters || []) {
|
||||||
|
const ct = cm.energyContractDetails?.contract;
|
||||||
|
if (ct && !seen.has(ct.id)) {
|
||||||
|
seen.add(ct.id);
|
||||||
|
linkedContracts.push({ ...ct, via: 'chain' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (linkedContracts.length === 0) return null;
|
||||||
|
const isContractsExpanded = expandedContractsForMeter.has(meter.id);
|
||||||
|
return (
|
||||||
|
<div className="mt-3 pt-3 border-t">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 w-full text-left"
|
||||||
|
onClick={() => {
|
||||||
|
setExpandedContractsForMeter((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(meter.id)) next.delete(meter.id);
|
||||||
|
else next.add(meter.id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isContractsExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />}
|
||||||
|
Verträge ({linkedContracts.length})
|
||||||
|
</button>
|
||||||
|
{isContractsExpanded && (
|
||||||
|
<div className="mt-2 space-y-1 ml-6">
|
||||||
|
{linkedContracts.map((ct) => (
|
||||||
|
<Link
|
||||||
|
key={ct.id}
|
||||||
|
to={`/contracts/${ct.id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 text-sm text-blue-600 hover:text-blue-800 hover:underline"
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 flex-shrink-0" />
|
||||||
|
<span className="font-mono">{ct.contractNumber}</span>
|
||||||
|
{ct.providerName && (
|
||||||
|
<span className="text-gray-500 font-sans">– {ct.providerName}</span>
|
||||||
|
)}
|
||||||
|
<Badge variant={
|
||||||
|
ct.status === 'ACTIVE' ? 'success'
|
||||||
|
: ct.status === 'CANCELLED' ? 'danger'
|
||||||
|
: ct.status === 'EXPIRED' ? 'warning'
|
||||||
|
: 'default'
|
||||||
|
}>{ct.status}</Badge>
|
||||||
|
{ct.via === 'chain' && (
|
||||||
|
<span className="text-xs text-gray-400">(über Folgezähler-Kette)</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{sortedReadings.length > 0 && (
|
{sortedReadings.length > 0 && (
|
||||||
<div className="mt-3 pt-3 border-t">
|
<div className="mt-3 pt-3 border-t">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
|||||||
@@ -221,6 +221,38 @@ export interface Meter {
|
|||||||
addressId?: number | null;
|
addressId?: number | null;
|
||||||
address?: Address;
|
address?: Address;
|
||||||
readings?: MeterReading[];
|
readings?: MeterReading[];
|
||||||
|
// Verträge, die diesen Zähler aktuell als Hauptzähler nutzen
|
||||||
|
energyDetails?: Array<{
|
||||||
|
id: number;
|
||||||
|
contractId: number;
|
||||||
|
contract?: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
status: ContractStatus;
|
||||||
|
type: string;
|
||||||
|
providerName?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
// Verträge, in denen der Zähler in der ContractMeter-Kette steht
|
||||||
|
// (Vorgänger oder Nachfolger über Zählerwechsel)
|
||||||
|
contractMeters?: Array<{
|
||||||
|
id: number;
|
||||||
|
energyContractDetailsId: number;
|
||||||
|
position: number;
|
||||||
|
installedAt?: string;
|
||||||
|
removedAt?: string;
|
||||||
|
energyContractDetails?: {
|
||||||
|
id: number;
|
||||||
|
contractId: number;
|
||||||
|
contract?: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
status: ContractStatus;
|
||||||
|
type: string;
|
||||||
|
providerName?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContractMeter {
|
export interface ContractMeter {
|
||||||
|
|||||||
Reference in New Issue
Block a user