Folgezähler-Forms: Checkbox "Alten Zähler deaktivieren" (default an)

Beide Folgezähler-Forms (Kundenakte MeterModal + Vertragsansicht
SuccessorMeterForm) bekommen eine Checkbox, die standardmäßig
angehakt ist. Beim Speichern wird der Vorgänger automatisch
auf isActive=false gesetzt – ein-klick-fähiger Zählerwechsel.

Backend: createMeter mit successorOf und addSuccessorMeter
akzeptieren deactivatePredecessor (Default true).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 14:39:05 +02:00
parent 3a9cece929
commit 2ee06630b9
5 changed files with 49 additions and 3 deletions
+10 -1
View File
@@ -539,7 +539,7 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
export async function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void> { export async function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void> {
try { try {
const contractId = parseInt(req.params.id); const contractId = parseInt(req.params.id);
const { meterId, installedAt, finalReadingPrevious } = req.body; const { meterId, installedAt, finalReadingPrevious, deactivatePredecessor } = req.body;
const contract = await prisma.contract.findUnique({ const contract = await prisma.contract.findUnique({
where: { id: contractId }, where: { id: contractId },
@@ -648,6 +648,15 @@ export async function addSuccessorMeter(req: AuthRequest, res: Response): Promis
); );
} }
// Alten Zähler deaktivieren (Default), sofern der Aufrufer das nicht
// explizit auf false setzt ein-klick-fähiger Zählerwechsel.
if (predecessorMeterId && deactivatePredecessor !== false) {
await prisma.meter.update({
where: { id: predecessorMeterId },
data: { isActive: false },
});
}
await logChange({ await logChange({
req, action: 'CREATE', resourceType: 'ContractMeter', req, action: 'CREATE', resourceType: 'ContractMeter',
resourceId: contractMeter.id.toString(), resourceId: contractMeter.id.toString(),
+14
View File
@@ -488,6 +488,10 @@ export async function createMeter(
predecessorMeterId: number; predecessorMeterId: number;
installedAt?: string; installedAt?: string;
finalReadingPrevious?: number; finalReadingPrevious?: number;
// Default true im UI: alter Zähler wird nach dem Wechsel auf
// isActive=false gesetzt. Kann ausgeschaltet werden, wenn der alte
// Zähler aus irgendeinem Grund noch aktiv bleiben soll.
deactivatePredecessor?: boolean;
}; };
} }
) { ) {
@@ -622,6 +626,16 @@ export async function createMeter(
data.successorOf.finalReadingPrevious, data.successorOf.finalReadingPrevious,
); );
} }
// Alten Zähler deaktivieren (Default), sofern der Aufrufer das nicht
// explizit auf false setzt. Macht den typischen Zählerwechsel-Workflow
// ein-klick-fähig.
if (data.successorOf.deactivatePredecessor !== false) {
await prisma.meter.update({
where: { id: predecessor.id },
data: { isActive: false },
});
}
} }
return created; return created;
@@ -361,6 +361,7 @@ function SuccessorMeterButton({
const [selectedMeterId, setSelectedMeterId] = useState(''); const [selectedMeterId, setSelectedMeterId] = useState('');
const [installedAt, setInstalledAt] = useState(new Date().toISOString().split('T')[0]); const [installedAt, setInstalledAt] = useState(new Date().toISOString().split('T')[0]);
const [finalReading, setFinalReading] = useState(''); const [finalReading, setFinalReading] = useState('');
const [deactivatePredecessor, setDeactivatePredecessor] = useState(true);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: metersData } = useQuery({ const { data: metersData } = useQuery({
@@ -370,13 +371,14 @@ function SuccessorMeterButton({
}); });
const addMutation = useMutation({ const addMutation = useMutation({
mutationFn: (data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => mutationFn: (data: { meterId: number; installedAt?: string; finalReadingPrevious?: number; deactivatePredecessor?: boolean }) =>
contractApi.addSuccessorMeter(contractId, data), contractApi.addSuccessorMeter(contractId, data),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['contract'] }); queryClient.invalidateQueries({ queryKey: ['contract'] });
setShowForm(false); setShowForm(false);
setSelectedMeterId(''); setSelectedMeterId('');
setFinalReading(''); setFinalReading('');
setDeactivatePredecessor(true);
}, },
}); });
@@ -451,6 +453,15 @@ function SuccessorMeterButton({
erfasst und fließt damit in die Verbrauchsberechnung ein. erfasst und fließt damit in die Verbrauchsberechnung ein.
</p> </p>
)} )}
<label className="flex items-center gap-2 mt-3 text-sm">
<input
type="checkbox"
checked={deactivatePredecessor}
onChange={(e) => setDeactivatePredecessor(e.target.checked)}
className="rounded"
/>
Alten Zähler deaktivieren
</label>
<div className="flex gap-2 mt-3"> <div className="flex gap-2 mt-3">
<Button <Button
size="sm" size="sm"
@@ -458,6 +469,7 @@ function SuccessorMeterButton({
meterId: parseInt(selectedMeterId), meterId: parseInt(selectedMeterId),
installedAt, installedAt,
finalReadingPrevious: finalReading ? parseFloat(finalReading) : undefined, finalReadingPrevious: finalReading ? parseFloat(finalReading) : undefined,
deactivatePredecessor,
})} })}
disabled={!selectedMeterId || addMutation.isPending} disabled={!selectedMeterId || addMutation.isPending}
> >
@@ -2854,6 +2854,7 @@ function MeterModal({
predecessorMeterId: '', predecessorMeterId: '',
installedAt: today, installedAt: today,
finalReadingPrevious: '', finalReadingPrevious: '',
deactivatePredecessor: true,
}); });
const [formData, setFormData] = useState(getInitialFormData); const [formData, setFormData] = useState(getInitialFormData);
@@ -2919,6 +2920,7 @@ function MeterModal({
finalReadingPrevious: formData.finalReadingPrevious finalReadingPrevious: formData.finalReadingPrevious
? parseFloat(formData.finalReadingPrevious) ? parseFloat(formData.finalReadingPrevious)
: undefined, : undefined,
deactivatePredecessor: formData.deactivatePredecessor,
}; };
} }
if (isEditing) { if (isEditing) {
@@ -3037,6 +3039,15 @@ function MeterModal({
onChange={(e) => setFormData({ ...formData, finalReadingPrevious: e.target.value })} onChange={(e) => setFormData({ ...formData, finalReadingPrevious: e.target.value })}
placeholder="Optional" placeholder="Optional"
/> />
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={formData.deactivatePredecessor}
onChange={(e) => setFormData({ ...formData, deactivatePredecessor: e.target.checked })}
className="rounded"
/>
Alten Zähler deaktivieren
</label>
<p className="text-xs text-blue-700"> <p className="text-xs text-blue-700">
Typ, Adresse und Tarifmodell werden vom Vorgänger übernommen. Alle Verträge, Typ, Adresse und Tarifmodell werden vom Vorgänger übernommen. Alle Verträge,
die den Vorgänger-Zähler verwenden, werden automatisch auf diesen neuen Zähler die den Vorgänger-Zähler verwenden, werden automatisch auf diesen neuen Zähler
+1 -1
View File
@@ -837,7 +837,7 @@ export const contractApi = {
return res.data; return res.data;
}, },
// Folgezähler // Folgezähler
addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) => { addSuccessorMeter: async (contractId: number, data: { meterId: number; installedAt?: string; finalReadingPrevious?: number; deactivatePredecessor?: boolean }) => {
const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data); const res = await api.post<ApiResponse<any>>(`/contracts/${contractId}/successor-meter`, data);
return res.data; return res.data;
}, },