Compare commits

...

2 Commits

Author SHA1 Message Date
duffyduck 0d024b94c2 Kundenakte → Zähler: Checkbox "Zähler ohne Verträge anzeigen"
Standardmäßig werden nur Zähler angezeigt, die mindestens einem
Vertrag zugeordnet sind (entweder als Hauptzähler oder über die
Folgezähler-Kette). Mit der neuen Checkbox lassen sich auch
verwaiste Zähler ins Listing holen – nützlich beim Aufräumen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:50:24 +02:00
duffyduck b4b0dbb004 Kundenakte → Zähler: Aufklapp-Liste der zugeordneten Verträge
Pro Zähler wird jetzt ein "Verträge (N)" Aufklapp-Bereich angezeigt,
der alle Verträge auflistet, die diesen Zähler nutzen – sowohl als
aktueller Hauptzähler (energyDetails.meterId) als auch über die
Folgezähler-Kette (ContractMeter). Dedupliziert auf contractId.

Jeder Eintrag ist Link auf den Vertrag im neuen Tab, mit
Vertragsnummer, Anbieter und Status-Badge. Folgezähler-Ketten-
Einträge werden mit "(über Folgezähler-Kette)" markiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 14:48:22 +02:00
3 changed files with 142 additions and 1 deletions
+18
View File
@@ -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">
+32
View File
@@ -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 {