Datenschutz vollmacht fixed, two time counter added
This commit is contained in:
parent
0121c82412
commit
4f359df161
|
|
@ -11,5 +11,7 @@ export declare function getSimCardCredentials(req: Request, res: Response): Prom
|
|||
export declare function getInternetCredentials(req: Request, res: Response): Promise<void>;
|
||||
export declare function getSipCredentials(req: Request, res: Response): Promise<void>;
|
||||
export declare function getCockpit(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function removeContractMeter(req: AuthRequest, res: Response): Promise<void>;
|
||||
export declare function snoozeContract(req: Request, res: Response): Promise<void>;
|
||||
//# sourceMappingURL=contract.controller.d.ts.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
|
||||
{"version":3,"file":"contract.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/contract.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM5C,OAAO,EAAe,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAI7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgDjF;AAED,wBAAsB,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChF;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAuCnF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBpF;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUtF;AAED,wBAAsB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUvF;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlF;AAID,wBAAsB,UAAU,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/E;AAID,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDtF;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF;AAID,wBAAsB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAyC/E"}
|
||||
|
|
@ -44,6 +44,8 @@ exports.getSimCardCredentials = getSimCardCredentials;
|
|||
exports.getInternetCredentials = getInternetCredentials;
|
||||
exports.getSipCredentials = getSipCredentials;
|
||||
exports.getCockpit = getCockpit;
|
||||
exports.addSuccessorMeter = addSuccessorMeter;
|
||||
exports.removeContractMeter = removeContractMeter;
|
||||
exports.snoozeContract = snoozeContract;
|
||||
const client_1 = require("@prisma/client");
|
||||
const contractService = __importStar(require("../services/contract.service.js"));
|
||||
|
|
@ -265,6 +267,71 @@ async function getCockpit(req, res) {
|
|||
});
|
||||
}
|
||||
}
|
||||
// ==================== FOLGEZÄHLER ====================
|
||||
async function addSuccessorMeter(req, res) {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
const { meterId, installedAt, finalReadingPrevious } = req.body;
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: contractId },
|
||||
include: { energyDetails: { include: { contractMeters: { orderBy: { position: 'asc' } } } } },
|
||||
});
|
||||
if (!contract?.energyDetails) {
|
||||
res.status(404).json({ success: false, error: 'Energievertrag nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
const ecdId = contract.energyDetails.id;
|
||||
const existingMeters = contract.energyDetails.contractMeters;
|
||||
const nextPosition = existingMeters.length > 0
|
||||
? Math.max(...existingMeters.map(m => m.position)) + 1
|
||||
: 0;
|
||||
// Vorherigen Zähler als gewechselt markieren
|
||||
if (existingMeters.length > 0 && finalReadingPrevious !== undefined) {
|
||||
const prevMeter = existingMeters[existingMeters.length - 1];
|
||||
await prisma.contractMeter.update({
|
||||
where: { id: prevMeter.id },
|
||||
data: {
|
||||
removedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
finalReading: parseFloat(finalReadingPrevious),
|
||||
},
|
||||
});
|
||||
}
|
||||
const contractMeter = await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: ecdId,
|
||||
meterId: parseInt(meterId),
|
||||
position: nextPosition,
|
||||
installedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
},
|
||||
include: { meter: { include: { readings: true } } },
|
||||
});
|
||||
// Aktuellen Zähler am Vertrag aktualisieren
|
||||
await prisma.energyContractDetails.update({
|
||||
where: { id: ecdId },
|
||||
data: { meterId: parseInt(meterId) },
|
||||
});
|
||||
res.json({ success: true, data: contractMeter });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
|
||||
});
|
||||
}
|
||||
}
|
||||
async function removeContractMeter(req, res) {
|
||||
try {
|
||||
const contractMeterId = parseInt(req.params.contractMeterId);
|
||||
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
|
||||
res.json({ success: true, data: null });
|
||||
}
|
||||
catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
|
||||
});
|
||||
}
|
||||
}
|
||||
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||
async function snoozeContract(req, res) {
|
||||
try {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"customer.controller.d.ts","sourceRoot":"","sources":["../../src/controllers/customer.controller.ts"],"names":[],"mappings":"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"}
|
||||
|
|
@ -351,7 +351,14 @@ async function getMeterReadings(req, res) {
|
|||
}
|
||||
async function addMeterReading(req, res) {
|
||||
try {
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
|
||||
readingDate: new Date(readingDate),
|
||||
value: parseFloat(value),
|
||||
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
|
||||
unit,
|
||||
notes,
|
||||
});
|
||||
res.status(201).json({ success: true, data: reading });
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -363,7 +370,19 @@ async function addMeterReading(req, res) {
|
|||
}
|
||||
async function updateMeterReading(req, res) {
|
||||
try {
|
||||
const reading = await customerService.updateMeterReading(parseInt(req.params.meterId), parseInt(req.params.readingId), req.body);
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const updateData = {};
|
||||
if (readingDate !== undefined)
|
||||
updateData.readingDate = new Date(readingDate);
|
||||
if (value !== undefined)
|
||||
updateData.value = parseFloat(value);
|
||||
if (valueNt !== undefined)
|
||||
updateData.valueNt = valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : null;
|
||||
if (unit !== undefined)
|
||||
updateData.unit = unit;
|
||||
if (notes !== undefined)
|
||||
updateData.notes = notes;
|
||||
const reading = await customerService.updateMeterReading(parseInt(req.params.meterId), parseInt(req.params.readingId), updateData);
|
||||
res.json({ success: true, data: reading });
|
||||
}
|
||||
catch (error) {
|
||||
|
|
@ -404,15 +423,18 @@ async function reportMeterReading(req, res) {
|
|||
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' });
|
||||
return;
|
||||
}
|
||||
const reading = await prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
value: parseFloat(value),
|
||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
||||
notes,
|
||||
reportedBy: user.email,
|
||||
status: 'REPORTED',
|
||||
},
|
||||
const parsedDate = readingDate ? new Date(readingDate) : new Date();
|
||||
const parsedValue = parseFloat(value);
|
||||
// Validierung über den Service (monoton steigend)
|
||||
const reading = await customerService.addMeterReading(meterId, {
|
||||
readingDate: parsedDate,
|
||||
value: parsedValue,
|
||||
notes,
|
||||
});
|
||||
// Status auf REPORTED setzen
|
||||
await prisma.meterReading.update({
|
||||
where: { id: reading.id },
|
||||
data: { reportedBy: user.email, status: 'REPORTED' },
|
||||
});
|
||||
res.status(201).json({ success: true, data: reading });
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8BxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"contract.routes.d.ts","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAkCxB,eAAe,MAAM,CAAC"}
|
||||
|
|
@ -48,6 +48,9 @@ router.delete('/:id', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('
|
|||
router.post('/:id/follow-up', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createFollowUp);
|
||||
// Snooze (Vertrag zurückstellen)
|
||||
router.patch('/:id/snooze', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.snoozeContract);
|
||||
// Folgezähler
|
||||
router.post('/:id/successor-meter', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.addSuccessorMeter);
|
||||
router.delete('/:id/contract-meter/:contractMeterId', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.removeContractMeter);
|
||||
// Get decrypted password
|
||||
router.get('/:id/password', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContractPassword);
|
||||
// Get decrypted SimCard PIN/PUK
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AACpG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEzG,2CAA2C;AAC3C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEzG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACtG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAC3G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAE9G,qBAAqB;AACrB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtH,iCAAiC;AACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEpH,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEvH,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;AAE3I,kCAAkC;AAClC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEtI,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CAAC,6CAA6C,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAEnJ,kBAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"contract.routes.js","sourceRoot":"","sources":["../../src/routes/contract.routes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,qCAAiC;AACjC,0FAA4E;AAC5E,mDAAwE;AAExE,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AACpG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEzG,2CAA2C;AAC3C,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;AAEzG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACtG,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAC3G,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAE9G,qBAAqB;AACrB,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEtH,iCAAiC;AACjC,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;AAEpH,cAAc;AACd,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAC/H,MAAM,CAAC,MAAM,CAAC,sCAAsC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEnJ,yBAAyB;AACzB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;AAEvH,gCAAgC;AAChC,MAAM,CAAC,GAAG,CAAC,iCAAiC,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;AAE3I,kCAAkC;AAClC,MAAM,CAAC,GAAG,CAAC,2BAA2B,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;AAEtI,6BAA6B;AAC7B,MAAM,CAAC,GAAG,CAAC,6CAA6C,EAAE,sBAAY,EAAE,IAAA,2BAAiB,EAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;AAEnJ,kBAAe,MAAM,CAAC"}
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2uBxB,eAAe,MAAM,CAAC"}
|
||||
{"version":3,"file":"upload.routes.d.ts","sourceRoot":"","sources":["../../src/routes/upload.routes.ts"],"names":[],"mappings":"AAQA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA2vBxB,eAAe,MAAM,CAAC"}
|
||||
|
|
@ -338,6 +338,15 @@ router.post('/customers/:id/privacy-policy', auth_js_1.authenticate, (0, auth_js
|
|||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: relativePath },
|
||||
});
|
||||
// Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung)
|
||||
const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'];
|
||||
for (const consentType of consentTypes) {
|
||||
await prisma.customerConsent.upsert({
|
||||
where: { customerId_consentType: { customerId, consentType } },
|
||||
update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' },
|
||||
create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: req.user?.email || 'admin' },
|
||||
});
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
|
@ -376,6 +385,11 @@ router.delete('/customers/:id/privacy-policy', auth_js_1.authenticate, (0, auth_
|
|||
where: { id: customerId },
|
||||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
// Nur Consents widerrufen die per Papier erteilt wurden
|
||||
await prisma.customerConsent.updateMany({
|
||||
where: { customerId, status: 'GRANTED', source: 'papier' },
|
||||
data: { status: 'WITHDRAWN', withdrawnAt: new Date() },
|
||||
});
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (error) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -122,14 +122,14 @@ async function getContractById(id, decryptPassword = false) {
|
|||
previousProvider: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
|
|
@ -264,11 +264,41 @@ async function updateContract(id, data) {
|
|||
});
|
||||
// Update type-specific details
|
||||
if (energyDetails) {
|
||||
const existingEcd = await prisma.energyContractDetails.findUnique({
|
||||
where: { contractId: id },
|
||||
select: { id: true, meterId: true },
|
||||
});
|
||||
await prisma.energyContractDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: energyDetails,
|
||||
create: { contractId: id, ...energyDetails },
|
||||
});
|
||||
// ContractMeter synchronisieren wenn sich der Zähler ändert
|
||||
if (energyDetails.meterId !== undefined && existingEcd) {
|
||||
const oldMeterId = existingEcd.meterId;
|
||||
const newMeterId = energyDetails.meterId;
|
||||
if (oldMeterId !== newMeterId) {
|
||||
// Alle alten ContractMeter-Einträge entfernen
|
||||
await prisma.contractMeter.deleteMany({
|
||||
where: { energyContractDetailsId: existingEcd.id },
|
||||
});
|
||||
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
|
||||
if (newMeterId) {
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
select: { startDate: true },
|
||||
});
|
||||
await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: existingEcd.id,
|
||||
meterId: newMeterId,
|
||||
position: 0,
|
||||
installedAt: contract?.startDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (internetDetails) {
|
||||
const { phoneNumbers, internetPassword, ...internetData } = internetDetails;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -78,6 +78,10 @@ export interface ReportedMeterReading {
|
|||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
};
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"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"}
|
||||
{"version":3,"file":"contractCockpit.service.d.ts","sourceRoot":"","sources":["../../src/services/contractCockpit.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,cAAc,EACpC,MAAM,gBAAgB,CAAC;AAMxB,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,YAAY,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,CAAC,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,cAAc,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE;QACV,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,EAAE,EAAE,MAAM,CAAC;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;QACvB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IAEF,QAAQ,CAAC,EAAE;QACT,EAAE,EAAE,MAAM,CAAC;QACX,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IAEF,cAAc,CAAC,EAAE;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,gBAAgB,EAAE,oBAAoB,EAAE,CAAC;IACzC,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE;QACV,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAyED,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,CAAC,CAsjB7D"}
|
||||
|
|
@ -682,6 +682,7 @@ async function getReportedMeterReadings() {
|
|||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
|
|
@ -717,6 +718,10 @@ async function getReportedMeterReadings() {
|
|||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
contract: contract ? {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
} : undefined,
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -206,17 +206,17 @@ export declare function getAllTasks(filters: AllTasksFilters): Promise<({
|
|||
customerNumber: string;
|
||||
companyName: string | null;
|
||||
};
|
||||
tariff: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
contractNumber: string;
|
||||
providerName: string | null;
|
||||
tariffName: string | null;
|
||||
provider: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
tariffName: string | null;
|
||||
providerName: string | null;
|
||||
contractNumber: string;
|
||||
tariff: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
};
|
||||
subtasks: {
|
||||
id: number;
|
||||
|
|
|
|||
|
|
@ -107,13 +107,14 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
|
|
@ -123,6 +124,7 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.MeterType;
|
||||
meterNumber: string;
|
||||
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||
location: string | null;
|
||||
})[];
|
||||
stressfreiEmails: {
|
||||
|
|
@ -171,11 +173,8 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
notes: string | null;
|
||||
portalPasswordEncrypted: string | null;
|
||||
startDate: Date | null;
|
||||
status: import(".prisma/client").$Enums.ContractStatus;
|
||||
customerNumberAtProvider: string | null;
|
||||
tariffName: string | null;
|
||||
providerName: string | null;
|
||||
contractNumber: string;
|
||||
status: import(".prisma/client").$Enums.ContractStatus;
|
||||
contractCategoryId: number | null;
|
||||
addressId: number | null;
|
||||
billingAddressId: number | null;
|
||||
|
|
@ -190,6 +189,9 @@ export declare function getCustomerById(id: number): Promise<({
|
|||
previousContractNumber: string | null;
|
||||
providerId: number | null;
|
||||
tariffId: number | null;
|
||||
providerName: string | null;
|
||||
tariffName: string | null;
|
||||
customerNumberAtProvider: string | null;
|
||||
contractNumberAtProvider: string | null;
|
||||
priceFirst12Months: string | null;
|
||||
priceFrom13Months: string | null;
|
||||
|
|
@ -573,13 +575,14 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
|
|||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[];
|
||||
} & {
|
||||
id: number;
|
||||
|
|
@ -589,6 +592,7 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
|
|||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.MeterType;
|
||||
meterNumber: string;
|
||||
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||
location: string | null;
|
||||
})[]>;
|
||||
export declare function createMeter(customerId: number, data: {
|
||||
|
|
@ -603,6 +607,7 @@ export declare function createMeter(customerId: number, data: {
|
|||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.MeterType;
|
||||
meterNumber: string;
|
||||
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||
location: string | null;
|
||||
}>;
|
||||
export declare function updateMeter(id: number, data: {
|
||||
|
|
@ -618,6 +623,7 @@ export declare function updateMeter(id: number, data: {
|
|||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.MeterType;
|
||||
meterNumber: string;
|
||||
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||
location: string | null;
|
||||
}>;
|
||||
export declare function deleteMeter(id: number): Promise<{
|
||||
|
|
@ -628,11 +634,13 @@ export declare function deleteMeter(id: number): Promise<{
|
|||
updatedAt: Date;
|
||||
type: import(".prisma/client").$Enums.MeterType;
|
||||
meterNumber: string;
|
||||
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||
location: string | null;
|
||||
}>;
|
||||
export declare function addMeterReading(meterId: number, data: {
|
||||
readingDate: Date;
|
||||
value: number;
|
||||
valueNt?: number;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}): Promise<{
|
||||
|
|
@ -640,30 +648,33 @@ export declare function addMeterReading(meterId: number, data: {
|
|||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function getMeterReadings(meterId: number): Promise<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}[]>;
|
||||
export declare function updateMeterReading(meterId: number, readingId: number, data: {
|
||||
readingDate?: Date;
|
||||
value?: number;
|
||||
valueNt?: number | null;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}): Promise<{
|
||||
|
|
@ -671,26 +682,28 @@ export declare function updateMeterReading(meterId: number, readingId: number, d
|
|||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function deleteMeterReading(meterId: number, readingId: number): Promise<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
notes: string | null;
|
||||
readingDate: Date;
|
||||
meterId: number;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
value: number;
|
||||
valueNt: number | null;
|
||||
unit: string;
|
||||
reportedBy: string | null;
|
||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||
transferredAt: Date | null;
|
||||
transferredBy: string | null;
|
||||
meterId: number;
|
||||
}>;
|
||||
export declare function updatePortalSettings(customerId: number, data: {
|
||||
portalEnabled?: boolean;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -321,13 +321,42 @@ async function updateMeter(id, data) {
|
|||
});
|
||||
}
|
||||
async function deleteMeter(id) {
|
||||
// Prüfen ob der Zähler noch an Verträgen hängt
|
||||
const linkedContracts = await prisma.contractMeter.findMany({
|
||||
where: { meterId: id },
|
||||
include: { energyContractDetails: { include: { contract: { select: { contractNumber: true } } } } },
|
||||
});
|
||||
if (linkedContracts.length > 0) {
|
||||
const contractNumbers = linkedContracts
|
||||
.map(cm => cm.energyContractDetails.contract.contractNumber)
|
||||
.join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
// Auch direkte meterId-Referenz auf EnergyContractDetails prüfen
|
||||
const directLinks = await prisma.energyContractDetails.findMany({
|
||||
where: { meterId: id },
|
||||
include: { contract: { select: { contractNumber: true } } },
|
||||
});
|
||||
if (directLinks.length > 0) {
|
||||
const contractNumbers = directLinks.map(d => d.contract.contractNumber).join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
return prisma.meter.delete({ where: { id } });
|
||||
}
|
||||
async function addMeterReading(meterId, data) {
|
||||
// Validierung: Zählerstand muss monoton steigend sein
|
||||
await validateReadingValue(meterId, data.readingDate, data.value, undefined, 'HT');
|
||||
if (data.valueNt !== undefined) {
|
||||
await validateReadingValue(meterId, data.readingDate, data.valueNt, undefined, 'NT');
|
||||
}
|
||||
return prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
...data,
|
||||
readingDate: data.readingDate,
|
||||
value: data.value,
|
||||
valueNt: data.valueNt,
|
||||
unit: data.unit,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -345,11 +374,46 @@ async function updateMeterReading(meterId, readingId, data) {
|
|||
if (!reading) {
|
||||
throw new Error('Zählerstand nicht gefunden');
|
||||
}
|
||||
// Validierung bei Wertänderung
|
||||
if (data.value !== undefined || data.readingDate !== undefined) {
|
||||
await validateReadingValue(meterId, data.readingDate || reading.readingDate, data.value ?? reading.value, readingId, 'HT');
|
||||
}
|
||||
if (data.valueNt !== undefined || data.readingDate !== undefined) {
|
||||
const ntVal = data.valueNt ?? reading.valueNt;
|
||||
if (ntVal !== undefined && ntVal !== null) {
|
||||
await validateReadingValue(meterId, data.readingDate || reading.readingDate, ntVal, readingId, 'NT');
|
||||
}
|
||||
}
|
||||
return prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Validiert, dass ein Zählerstand monoton steigend ist.
|
||||
* tariffLabel: 'HT' für Hochtarif/Eintarif, 'NT' für Niedertarif
|
||||
*/
|
||||
async function validateReadingValue(meterId, readingDate, value, excludeReadingId, tariffLabel = 'HT') {
|
||||
const existing = await prisma.meterReading.findMany({
|
||||
where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) },
|
||||
orderBy: { readingDate: 'asc' },
|
||||
});
|
||||
const fmtDate = (d) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const fmtVal = (v) => v.toLocaleString('de-DE');
|
||||
const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand';
|
||||
// Vergleichswert aus bestehendem Reading extrahieren
|
||||
const getVal = (r) => tariffLabel === 'NT' ? (r.valueNt ?? 0) : r.value;
|
||||
// Stand vor dem neuen Datum
|
||||
const before = [...existing].filter(r => r.readingDate <= readingDate).pop();
|
||||
if (before && value < getVal(before)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht kleiner sein als der Stand vom ${fmtDate(before.readingDate)} (${fmtVal(getVal(before))})`);
|
||||
}
|
||||
// Stand nach dem neuen Datum
|
||||
const after = existing.find(r => r.readingDate > readingDate);
|
||||
if (after && value > getVal(after)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht größer sein als der spätere Stand vom ${fmtDate(after.readingDate)} (${fmtVal(getVal(after))})`);
|
||||
}
|
||||
}
|
||||
async function deleteMeterReading(meterId, readingId) {
|
||||
// Verify the reading belongs to the meter
|
||||
const reading = await prisma.meterReading.findFirst({
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -361,6 +361,7 @@ exports.Prisma.MeterScalarFieldEnum = {
|
|||
customerId: 'customerId',
|
||||
meterNumber: 'meterNumber',
|
||||
type: 'type',
|
||||
tariffModel: 'tariffModel',
|
||||
location: 'location',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
|||
meterId: 'meterId',
|
||||
readingDate: 'readingDate',
|
||||
value: 'value',
|
||||
valueNt: 'valueNt',
|
||||
unit: 'unit',
|
||||
notes: 'notes',
|
||||
reportedBy: 'reportedBy',
|
||||
|
|
@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
|||
annualConsumptionKwh: 'annualConsumptionKwh',
|
||||
basePrice: 'basePrice',
|
||||
unitPrice: 'unitPrice',
|
||||
unitPriceNt: 'unitPriceNt',
|
||||
bonus: 'bonus',
|
||||
previousProviderName: 'previousProviderName',
|
||||
previousCustomerNumber: 'previousCustomerNumber'
|
||||
};
|
||||
|
||||
exports.Prisma.ContractMeterScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
meterId: 'meterId',
|
||||
position: 'position',
|
||||
installedAt: 'installedAt',
|
||||
removedAt: 'removedAt',
|
||||
finalReading: 'finalReading',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
|
|
@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
|
|||
GAS: 'GAS'
|
||||
};
|
||||
|
||||
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
|
||||
SINGLE: 'SINGLE',
|
||||
DUAL: 'DUAL'
|
||||
};
|
||||
|
||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||
RECORDED: 'RECORDED',
|
||||
REPORTED: 'REPORTED',
|
||||
|
|
@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
|
|||
ContractTask: 'ContractTask',
|
||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||
EnergyContractDetails: 'EnergyContractDetails',
|
||||
ContractMeter: 'ContractMeter',
|
||||
Invoice: 'Invoice',
|
||||
InternetContractDetails: 'InternetContractDetails',
|
||||
PhoneNumber: 'PhoneNumber',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "prisma-client-c6d54e22fa4d6137f643638da5d523e99ce84f9544cc793fd89163f1612953c6",
|
||||
"name": "prisma-client-45a91d7556f300a75a0048d27fac6a72915779fc4e5c2234b54fe3547ddb1605",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"browser": "index-browser.js",
|
||||
|
|
|
|||
|
|
@ -411,18 +411,25 @@ enum MeterType {
|
|||
GAS
|
||||
}
|
||||
|
||||
enum MeterTariffModel {
|
||||
SINGLE // Eintarifzähler (Standard)
|
||||
DUAL // Zweitarifzähler (HT/NT)
|
||||
}
|
||||
|
||||
model Meter {
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
meterNumber String
|
||||
type MeterType
|
||||
location String?
|
||||
isActive Boolean @default(true)
|
||||
readings MeterReading[]
|
||||
energyDetails EnergyContractDetails[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
meterNumber String
|
||||
type MeterType
|
||||
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||
location String?
|
||||
isActive Boolean @default(true)
|
||||
readings MeterReading[]
|
||||
energyDetails EnergyContractDetails[]
|
||||
contractMeters ContractMeter[] @relation("ContractMeters")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model MeterReading {
|
||||
|
|
@ -430,7 +437,8 @@ model MeterReading {
|
|||
meterId Int
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
readingDate DateTime
|
||||
value Float
|
||||
value Float // Bei Eintarif: Gesamtwert. Bei Zweitarif: HT-Wert
|
||||
valueNt Float? // Nur bei Zweitarif: NT-Wert (Niedertarif)
|
||||
unit String @default("kWh")
|
||||
notes String?
|
||||
// Meldung & Übertragung
|
||||
|
|
@ -709,20 +717,38 @@ enum InvoiceType {
|
|||
}
|
||||
|
||||
model EnergyContractDetails {
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
id Int @id @default(autoincrement())
|
||||
contractId Int @unique
|
||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||
meterId Int?
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
meter Meter? @relation(fields: [meterId], references: [id])
|
||||
maloId String? // Marktlokations-ID
|
||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||
basePrice Float? // €/Monat
|
||||
unitPrice Float? // €/kWh (Arbeitspreis)
|
||||
unitPrice Float? // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
|
||||
unitPriceNt Float? // €/kWh NT-Preis (nur bei Zweitarifzähler)
|
||||
bonus Float?
|
||||
previousProviderName String?
|
||||
previousCustomerNumber String?
|
||||
invoices Invoice[] // Rechnungen
|
||||
contractMeters ContractMeter[] // Zähler-Zuordnungen (inkl. Folgezähler)
|
||||
}
|
||||
|
||||
model ContractMeter {
|
||||
id Int @id @default(autoincrement())
|
||||
energyContractDetailsId Int
|
||||
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
|
||||
meterId Int
|
||||
meter Meter @relation("ContractMeters", fields: [meterId], references: [id])
|
||||
position Int @default(0) // 0 = Original, 1 = erster Folgezähler, etc.
|
||||
installedAt DateTime? // Ab wann wird dieser Zähler am Vertrag genutzt?
|
||||
removedAt DateTime? // Wann wurde der Zähler gewechselt? (null = aktuell)
|
||||
finalReading Float? // Letzter Stand vor dem Wechsel
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([energyContractDetailsId, meterId])
|
||||
@@index([energyContractDetailsId])
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
|
|
|
|||
|
|
@ -361,6 +361,7 @@ exports.Prisma.MeterScalarFieldEnum = {
|
|||
customerId: 'customerId',
|
||||
meterNumber: 'meterNumber',
|
||||
type: 'type',
|
||||
tariffModel: 'tariffModel',
|
||||
location: 'location',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
|
|
@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
|||
meterId: 'meterId',
|
||||
readingDate: 'readingDate',
|
||||
value: 'value',
|
||||
valueNt: 'valueNt',
|
||||
unit: 'unit',
|
||||
notes: 'notes',
|
||||
reportedBy: 'reportedBy',
|
||||
|
|
@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
|||
annualConsumptionKwh: 'annualConsumptionKwh',
|
||||
basePrice: 'basePrice',
|
||||
unitPrice: 'unitPrice',
|
||||
unitPriceNt: 'unitPriceNt',
|
||||
bonus: 'bonus',
|
||||
previousProviderName: 'previousProviderName',
|
||||
previousCustomerNumber: 'previousCustomerNumber'
|
||||
};
|
||||
|
||||
exports.Prisma.ContractMeterScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
meterId: 'meterId',
|
||||
position: 'position',
|
||||
installedAt: 'installedAt',
|
||||
removedAt: 'removedAt',
|
||||
finalReading: 'finalReading',
|
||||
createdAt: 'createdAt'
|
||||
};
|
||||
|
||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||
id: 'id',
|
||||
energyContractDetailsId: 'energyContractDetailsId',
|
||||
|
|
@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
|
|||
GAS: 'GAS'
|
||||
};
|
||||
|
||||
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
|
||||
SINGLE: 'SINGLE',
|
||||
DUAL: 'DUAL'
|
||||
};
|
||||
|
||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||
RECORDED: 'RECORDED',
|
||||
REPORTED: 'REPORTED',
|
||||
|
|
@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
|
|||
ContractTask: 'ContractTask',
|
||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||
EnergyContractDetails: 'EnergyContractDetails',
|
||||
ContractMeter: 'ContractMeter',
|
||||
Invoice: 'Invoice',
|
||||
InternetContractDetails: 'InternetContractDetails',
|
||||
PhoneNumber: 'PhoneNumber',
|
||||
|
|
|
|||
|
|
@ -411,17 +411,24 @@ enum MeterType {
|
|||
GAS
|
||||
}
|
||||
|
||||
enum MeterTariffModel {
|
||||
SINGLE // Eintarifzähler (Standard)
|
||||
DUAL // Zweitarifzähler (HT/NT)
|
||||
}
|
||||
|
||||
model Meter {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
customerId Int
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
meterNumber String
|
||||
type MeterType
|
||||
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||
location String?
|
||||
isActive Boolean @default(true)
|
||||
readings MeterReading[]
|
||||
energyDetails EnergyContractDetails[]
|
||||
createdAt DateTime @default(now())
|
||||
isActive Boolean @default(true)
|
||||
readings MeterReading[]
|
||||
energyDetails EnergyContractDetails[]
|
||||
contractMeters ContractMeter[] @relation("ContractMeters")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
|
|
@ -430,7 +437,8 @@ model MeterReading {
|
|||
meterId Int
|
||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||
readingDate DateTime
|
||||
value Float
|
||||
value Float // Bei Eintarif: Gesamtwert. Bei Zweitarif: HT-Wert
|
||||
valueNt Float? // Nur bei Zweitarif: NT-Wert (Niedertarif)
|
||||
unit String @default("kWh")
|
||||
notes String?
|
||||
// Meldung & Übertragung
|
||||
|
|
@ -718,11 +726,29 @@ model EnergyContractDetails {
|
|||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||
basePrice Float? // €/Monat
|
||||
unitPrice Float? // €/kWh (Arbeitspreis)
|
||||
unitPrice Float? // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
|
||||
unitPriceNt Float? // €/kWh NT-Preis (nur bei Zweitarifzähler)
|
||||
bonus Float?
|
||||
previousProviderName String?
|
||||
previousCustomerNumber String?
|
||||
invoices Invoice[] // Rechnungen
|
||||
contractMeters ContractMeter[] // Zähler-Zuordnungen (inkl. Folgezähler)
|
||||
}
|
||||
|
||||
model ContractMeter {
|
||||
id Int @id @default(autoincrement())
|
||||
energyContractDetailsId Int
|
||||
energyContractDetails EnergyContractDetails @relation(fields: [energyContractDetailsId], references: [id], onDelete: Cascade)
|
||||
meterId Int
|
||||
meter Meter @relation("ContractMeters", fields: [meterId], references: [id])
|
||||
position Int @default(0) // 0 = Original, 1 = erster Folgezähler, etc.
|
||||
installedAt DateTime? // Ab wann wird dieser Zähler am Vertrag genutzt?
|
||||
removedAt DateTime? // Wann wurde der Zähler gewechselt? (null = aktuell)
|
||||
finalReading Float? // Letzter Stand vor dem Wechsel
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([energyContractDetailsId, meterId])
|
||||
@@index([energyContractDetailsId])
|
||||
}
|
||||
|
||||
model Invoice {
|
||||
|
|
|
|||
|
|
@ -244,6 +244,79 @@ export async function getCockpit(req: AuthRequest, res: Response): Promise<void>
|
|||
}
|
||||
}
|
||||
|
||||
// ==================== FOLGEZÄHLER ====================
|
||||
|
||||
export async function addSuccessorMeter(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractId = parseInt(req.params.id);
|
||||
const { meterId, installedAt, finalReadingPrevious } = req.body;
|
||||
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id: contractId },
|
||||
include: { energyDetails: { include: { contractMeters: { orderBy: { position: 'asc' } } } } },
|
||||
});
|
||||
|
||||
if (!contract?.energyDetails) {
|
||||
res.status(404).json({ success: false, error: 'Energievertrag nicht gefunden' } as ApiResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
const ecdId = contract.energyDetails.id;
|
||||
const existingMeters = contract.energyDetails.contractMeters;
|
||||
const nextPosition = existingMeters.length > 0
|
||||
? Math.max(...existingMeters.map(m => m.position)) + 1
|
||||
: 0;
|
||||
|
||||
// Vorherigen Zähler als gewechselt markieren
|
||||
if (existingMeters.length > 0 && finalReadingPrevious !== undefined) {
|
||||
const prevMeter = existingMeters[existingMeters.length - 1];
|
||||
await prisma.contractMeter.update({
|
||||
where: { id: prevMeter.id },
|
||||
data: {
|
||||
removedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
finalReading: parseFloat(finalReadingPrevious),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const contractMeter = await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: ecdId,
|
||||
meterId: parseInt(meterId),
|
||||
position: nextPosition,
|
||||
installedAt: installedAt ? new Date(installedAt) : new Date(),
|
||||
},
|
||||
include: { meter: { include: { readings: true } } },
|
||||
});
|
||||
|
||||
// Aktuellen Zähler am Vertrag aktualisieren
|
||||
await prisma.energyContractDetails.update({
|
||||
where: { id: ecdId },
|
||||
data: { meterId: parseInt(meterId) },
|
||||
});
|
||||
|
||||
res.json({ success: true, data: contractMeter } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Hinzufügen des Folgezählers',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeContractMeter(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const contractMeterId = parseInt(req.params.contractMeterId);
|
||||
await prisma.contractMeter.delete({ where: { id: contractMeterId } });
|
||||
res.json({ success: true, data: null } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Fehler beim Entfernen',
|
||||
} as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||
|
||||
export async function snoozeContract(req: Request, res: Response): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -293,7 +293,14 @@ export async function getMeterReadings(req: Request, res: Response): Promise<voi
|
|||
|
||||
export async function addMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), req.body);
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const reading = await customerService.addMeterReading(parseInt(req.params.meterId), {
|
||||
readingDate: new Date(readingDate),
|
||||
value: parseFloat(value),
|
||||
valueNt: valueNt !== undefined && valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : undefined,
|
||||
unit,
|
||||
notes,
|
||||
});
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
res.status(400).json({
|
||||
|
|
@ -305,10 +312,18 @@ export async function addMeterReading(req: Request, res: Response): Promise<void
|
|||
|
||||
export async function updateMeterReading(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { readingDate, value, valueNt, unit, notes } = req.body;
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (readingDate !== undefined) updateData.readingDate = new Date(readingDate);
|
||||
if (value !== undefined) updateData.value = parseFloat(value);
|
||||
if (valueNt !== undefined) updateData.valueNt = valueNt !== null && valueNt !== '' ? parseFloat(valueNt) : null;
|
||||
if (unit !== undefined) updateData.unit = unit;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
|
||||
const reading = await customerService.updateMeterReading(
|
||||
parseInt(req.params.meterId),
|
||||
parseInt(req.params.readingId),
|
||||
req.body
|
||||
updateData as any
|
||||
);
|
||||
res.json({ success: true, data: reading } as ApiResponse);
|
||||
} catch (error) {
|
||||
|
|
@ -358,15 +373,20 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
|
|||
return;
|
||||
}
|
||||
|
||||
const reading = await prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
value: parseFloat(value),
|
||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
||||
notes,
|
||||
reportedBy: user.email,
|
||||
status: 'REPORTED',
|
||||
},
|
||||
const parsedDate = readingDate ? new Date(readingDate) : new Date();
|
||||
const parsedValue = parseFloat(value);
|
||||
|
||||
// Validierung über den Service (monoton steigend)
|
||||
const reading = await customerService.addMeterReading(meterId, {
|
||||
readingDate: parsedDate,
|
||||
value: parsedValue,
|
||||
notes,
|
||||
});
|
||||
|
||||
// Status auf REPORTED setzen
|
||||
await prisma.meterReading.update({
|
||||
where: { id: reading.id },
|
||||
data: { reportedBy: user.email, status: 'REPORTED' },
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ const RESOURCE_MAPPING: Record<string, { type: string; extractId?: (req: AuthReq
|
|||
'/api/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
|
||||
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
||||
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
|
||||
'/api/meters': { type: 'Meter', extractId: (req) => req.params.id || req.params.meterId },
|
||||
'/api/upload': { type: 'Upload' },
|
||||
'/api/email-logs': { type: 'EmailLog' },
|
||||
'/api/auth': { type: 'Authentication' },
|
||||
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
||||
'/api/gdpr': { type: 'GDPR' },
|
||||
|
|
@ -116,6 +119,240 @@ function getClientIp(req: AuthRequest): string {
|
|||
return req.socket.remoteAddress || 'unknown';
|
||||
}
|
||||
|
||||
// Menschenlesbare Bezeichnungen für Resource-Typen
|
||||
const RESOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
Customer: 'Kunde',
|
||||
Contract: 'Vertrag',
|
||||
BankCard: 'Bankverbindung',
|
||||
IdentityDocument: 'Ausweis',
|
||||
Address: 'Adresse',
|
||||
Meter: 'Zähler',
|
||||
MeterReading: 'Zählerstand',
|
||||
User: 'Benutzer',
|
||||
Provider: 'Anbieter',
|
||||
Tariff: 'Tarif',
|
||||
SalesPlatform: 'Vertriebsplattform',
|
||||
ContractCategory: 'Vertragskategorie',
|
||||
CancellationPeriod: 'Kündigungsfrist',
|
||||
ContractDuration: 'Vertragslaufzeit',
|
||||
EmailProviderConfig: 'E-Mail-Provider',
|
||||
AppSetting: 'Einstellung',
|
||||
CustomerConsent: 'Einwilligung',
|
||||
ContractTask: 'Aufgabe',
|
||||
ContractHistoryEntry: 'Vertragshistorie',
|
||||
GDPR: 'Datenschutz',
|
||||
Authentication: 'Anmeldung',
|
||||
AuditLog: 'Audit-Protokoll',
|
||||
StressfreiEmail: 'Stressfrei-E-Mail',
|
||||
CachedEmail: 'E-Mail',
|
||||
};
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
CREATE: 'erstellt',
|
||||
READ: 'aufgerufen',
|
||||
UPDATE: 'aktualisiert',
|
||||
DELETE: 'gelöscht',
|
||||
EXPORT: 'exportiert',
|
||||
ANONYMIZE: 'anonymisiert',
|
||||
LOGIN: 'angemeldet',
|
||||
LOGOUT: 'abgemeldet',
|
||||
LOGIN_FAILED: 'Anmeldung fehlgeschlagen',
|
||||
};
|
||||
|
||||
/**
|
||||
* Erzeugt ein menschenlesbares Label für den Audit-Log-Eintrag
|
||||
*/
|
||||
function generateHumanLabel(
|
||||
action: AuditAction,
|
||||
resourceType: string,
|
||||
req: AuthRequest,
|
||||
responseBody: unknown
|
||||
): string {
|
||||
const typeName = RESOURCE_TYPE_LABELS[resourceType] || resourceType;
|
||||
const actionName = ACTION_LABELS[action] || action;
|
||||
|
||||
// Identifikator aus Response oder Request extrahieren
|
||||
let identifier = '';
|
||||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||||
if (data) {
|
||||
identifier =
|
||||
(data.contractNumber as string) ||
|
||||
(data.customerNumber as string) ||
|
||||
(data.meterNumber as string) ||
|
||||
(data.name as string) ||
|
||||
(data.email as string) ||
|
||||
(data.firstName && data.lastName ? `${data.firstName} ${data.lastName}` : '') ||
|
||||
'';
|
||||
}
|
||||
}
|
||||
|
||||
// Spezial-Labels für bestimmte Endpunkte
|
||||
const path = req.path;
|
||||
|
||||
// Auth
|
||||
if (path.includes('/auth/login') || path.includes('/auth/customer-login')) {
|
||||
const email = req.body?.email || '';
|
||||
return action === 'LOGIN'
|
||||
? `Benutzer ${email} hat sich angemeldet`
|
||||
: `Anmeldung fehlgeschlagen für ${email}`;
|
||||
}
|
||||
if (path.includes('/auth/logout')) return 'Benutzer hat sich abgemeldet';
|
||||
|
||||
// Kunden-Operationen
|
||||
if (resourceType === 'Customer') {
|
||||
if (action === 'CREATE') return `Kunde ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Kundendaten ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Kunde ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Kundendaten ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Kundenliste aufgerufen';
|
||||
}
|
||||
|
||||
// Verträge
|
||||
if (resourceType === 'Contract') {
|
||||
if (path.includes('/cockpit')) return 'Vertrags-Cockpit aufgerufen';
|
||||
if (path.includes('/follow-up')) return `Folgevertrag für ${identifier} erstellt`;
|
||||
if (path.includes('/snooze')) return `Vertrag ${identifier} zurückgestellt`;
|
||||
if (path.includes('/password')) return `Passwort für Vertrag ${identifier} abgerufen`;
|
||||
if (path.includes('/sip-credentials')) return 'SIP-Zugangsdaten abgerufen';
|
||||
if (path.includes('/internet-credentials')) return `Internet-Zugangsdaten für Vertrag ${identifier} abgerufen`;
|
||||
if (path.includes('/successor-meter')) return `Folgezähler zu Vertrag ${identifier} hinzugefügt`;
|
||||
if (action === 'CREATE') return `Vertrag ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Vertrag ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Vertrag ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Vertrag ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Vertragsliste aufgerufen';
|
||||
}
|
||||
|
||||
// Bankverbindungen
|
||||
if (resourceType === 'BankCard') {
|
||||
if (action === 'CREATE') return `Bankverbindung hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Bankverbindung aktualisiert`;
|
||||
if (action === 'DELETE') return `Bankverbindung gelöscht`;
|
||||
}
|
||||
|
||||
// Ausweise
|
||||
if (resourceType === 'IdentityDocument') {
|
||||
if (action === 'CREATE') return `Ausweis ${identifier} hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Ausweis ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Ausweis gelöscht`;
|
||||
}
|
||||
|
||||
// Adressen
|
||||
if (resourceType === 'Address') {
|
||||
if (action === 'CREATE') return `Adresse hinzugefügt`;
|
||||
if (action === 'UPDATE') return `Adresse aktualisiert`;
|
||||
if (action === 'DELETE') return `Adresse gelöscht`;
|
||||
}
|
||||
|
||||
// Zähler
|
||||
if (resourceType === 'Meter') {
|
||||
if (action === 'CREATE') return `Zähler ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Zähler ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Zähler gelöscht`;
|
||||
}
|
||||
|
||||
// Einwilligungen
|
||||
if (resourceType === 'CustomerConsent') {
|
||||
const consentType = req.params.consentType || '';
|
||||
const consentLabels: Record<string, string> = {
|
||||
DATA_PROCESSING: 'Datenverarbeitung',
|
||||
MARKETING_EMAIL: 'E-Mail-Marketing',
|
||||
MARKETING_PHONE: 'Telefonmarketing',
|
||||
DATA_SHARING_PARTNER: 'Datenweitergabe',
|
||||
};
|
||||
const consentName = consentLabels[consentType] || consentType;
|
||||
if (action === 'UPDATE') {
|
||||
const status = req.body?.status;
|
||||
return status === 'GRANTED'
|
||||
? `Einwilligung "${consentName}" erteilt`
|
||||
: `Einwilligung "${consentName}" widerrufen`;
|
||||
}
|
||||
if (action === 'READ') return 'Einwilligungen abgerufen';
|
||||
}
|
||||
|
||||
// Benutzer
|
||||
if (resourceType === 'User') {
|
||||
if (action === 'CREATE') return `Benutzer ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `Benutzer ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `Benutzer ${identifier} gelöscht`;
|
||||
if (action === 'READ' && req.params.id) return `Benutzerdaten ${identifier} aufgerufen`;
|
||||
if (action === 'READ') return 'Benutzerliste aufgerufen';
|
||||
}
|
||||
|
||||
// Aufgaben
|
||||
if (resourceType === 'ContractTask') {
|
||||
if (path.includes('/complete')) return `Aufgabe als erledigt markiert`;
|
||||
if (action === 'CREATE') return `Aufgabe erstellt`;
|
||||
if (action === 'UPDATE') return `Aufgabe aktualisiert`;
|
||||
if (action === 'DELETE') return `Aufgabe gelöscht`;
|
||||
}
|
||||
|
||||
// E-Mail-Provider
|
||||
if (resourceType === 'EmailProviderConfig') {
|
||||
if (path.includes('/test-connection')) return `E-Mail-Provider Verbindungstest`;
|
||||
if (path.includes('/provision')) return `E-Mail-Adresse provisioniert`;
|
||||
if (action === 'CREATE') return `E-Mail-Provider ${identifier} angelegt`;
|
||||
if (action === 'UPDATE') return `E-Mail-Provider ${identifier} aktualisiert`;
|
||||
if (action === 'DELETE') return `E-Mail-Provider ${identifier} gelöscht`;
|
||||
}
|
||||
|
||||
// GDPR
|
||||
if (resourceType === 'GDPR') {
|
||||
if (path.includes('/dashboard')) return 'DSGVO-Dashboard aufgerufen';
|
||||
if (path.includes('/export')) return 'Kundendaten exportiert (DSGVO Art. 15)';
|
||||
if (path.includes('/privacy-policy')) {
|
||||
return action === 'UPDATE' ? 'Datenschutzerklärung aktualisiert' : 'Datenschutzerklärung aufgerufen';
|
||||
}
|
||||
if (path.includes('/authorization-template')) {
|
||||
return action === 'UPDATE' ? 'Vollmacht-Vorlage aktualisiert' : 'Vollmacht-Vorlage aufgerufen';
|
||||
}
|
||||
if (path.includes('/send-consent-link')) return 'Datenschutz-Link versendet';
|
||||
if (path.includes('/authorizations') && path.includes('/send')) return 'Vollmacht-Anfrage versendet';
|
||||
if (path.includes('/authorizations') && path.includes('/grant')) return 'Vollmacht erteilt';
|
||||
if (path.includes('/authorizations') && path.includes('/withdraw')) return 'Vollmacht widerrufen';
|
||||
if (path.includes('/authorizations') && path.includes('/upload')) return 'Vollmacht-PDF hochgeladen';
|
||||
if (path.includes('/authorizations') && path.includes('/document') && action === 'DELETE') return 'Vollmacht-PDF gelöscht';
|
||||
if (path.includes('/my-privacy')) return 'Eigene Datenschutzseite aufgerufen';
|
||||
if (path.includes('/my-consent-status')) return 'Eigener Einwilligungsstatus geprüft';
|
||||
if (path.includes('/my-authorizations')) return 'Eigene Vollmachten aufgerufen';
|
||||
if (path.includes('/deletions')) {
|
||||
if (action === 'CREATE') return 'Löschanfrage erstellt';
|
||||
if (path.includes('/process')) return 'Löschanfrage bearbeitet';
|
||||
return 'Löschanfragen aufgerufen';
|
||||
}
|
||||
if (path.includes('/consent-status')) return 'Einwilligungsstatus geprüft';
|
||||
if (path.includes('/consents/overview')) return 'Einwilligungsübersicht aufgerufen';
|
||||
}
|
||||
|
||||
// Einstellungen
|
||||
if (resourceType === 'AppSetting') {
|
||||
if (action === 'UPDATE') return `Einstellung "${req.params.key || ''}" geändert`;
|
||||
if (action === 'READ') return 'Einstellungen aufgerufen';
|
||||
}
|
||||
|
||||
// Zähler-Readings
|
||||
if (path.includes('/readings')) {
|
||||
if (path.includes('/report')) return 'Zählerstand vom Kunden gemeldet';
|
||||
if (path.includes('/transfer')) return 'Zählerstand als übertragen markiert';
|
||||
if (action === 'CREATE') return 'Zählerstand erfasst';
|
||||
if (action === 'UPDATE') return 'Zählerstand aktualisiert';
|
||||
if (action === 'DELETE') return 'Zählerstand gelöscht';
|
||||
}
|
||||
|
||||
// Upload-Operationen
|
||||
if (path.includes('/upload') || path.includes('/privacy-policy')) {
|
||||
if (path.includes('/privacy-policy') && action === 'DELETE') return 'Datenschutzerklärung-PDF gelöscht';
|
||||
if (path.includes('/privacy-policy')) return 'Datenschutzerklärung-PDF hochgeladen';
|
||||
}
|
||||
|
||||
// Standard-Fallback
|
||||
if (identifier) {
|
||||
return `${typeName} ${identifier} ${actionName}`;
|
||||
}
|
||||
return `${typeName} ${actionName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audit Middleware - loggt alle API-Aufrufe asynchron
|
||||
*/
|
||||
|
|
@ -162,22 +399,8 @@ export function auditMiddleware(req: AuthRequest, res: Response, next: NextFunct
|
|||
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
|
||||
const auditContext = getAuditContext();
|
||||
|
||||
// Label für bessere Lesbarkeit generieren
|
||||
let resourceLabel: string | undefined;
|
||||
if (responseBody && typeof responseBody === 'object' && 'data' in responseBody) {
|
||||
const data = (responseBody as { data: Record<string, unknown> }).data;
|
||||
if (data) {
|
||||
// Versuche verschiedene Label-Felder
|
||||
resourceLabel =
|
||||
(data.contractNumber as string) ||
|
||||
(data.customerNumber as string) ||
|
||||
(data.name as string) ||
|
||||
(data.email as string) ||
|
||||
(data.firstName && data.lastName
|
||||
? `${data.firstName} ${data.lastName}`
|
||||
: undefined);
|
||||
}
|
||||
}
|
||||
// Menschenlesbares Label generieren
|
||||
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
|
||||
|
||||
await createAuditLog({
|
||||
userId: req.user?.userId,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'
|
|||
// Snooze (Vertrag zurückstellen)
|
||||
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
|
||||
|
||||
// Folgezähler
|
||||
router.post('/:id/successor-meter', authenticate, requirePermission('contracts:update'), contractController.addSuccessorMeter);
|
||||
router.delete('/:id/contract-meter/:contractMeterId', authenticate, requirePermission('contracts:update'), contractController.removeContractMeter);
|
||||
|
||||
// Get decrypted password
|
||||
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
||||
|
||||
|
|
|
|||
|
|
@ -440,6 +440,16 @@ router.post(
|
|||
data: { privacyPolicyPath: relativePath },
|
||||
});
|
||||
|
||||
// Alle Consents auf GRANTED setzen (PDF = vollständige Einwilligung)
|
||||
const consentTypes = ['DATA_PROCESSING', 'MARKETING_EMAIL', 'MARKETING_PHONE', 'DATA_SHARING_PARTNER'] as const;
|
||||
for (const consentType of consentTypes) {
|
||||
await prisma.customerConsent.upsert({
|
||||
where: { customerId_consentType: { customerId, consentType } },
|
||||
update: { status: 'GRANTED', grantedAt: new Date(), source: 'papier' },
|
||||
create: { customerId, consentType, status: 'GRANTED', grantedAt: new Date(), source: 'papier', createdBy: (req as any).user?.email || 'admin' },
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
|
|
@ -488,6 +498,12 @@ router.delete(
|
|||
data: { privacyPolicyPath: null },
|
||||
});
|
||||
|
||||
// Nur Consents widerrufen die per Papier erteilt wurden
|
||||
await prisma.customerConsent.updateMany({
|
||||
where: { customerId, status: 'GRANTED', source: 'papier' },
|
||||
data: { status: 'WITHDRAWN', withdrawnAt: new Date() },
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
|
|
|
|||
|
|
@ -146,13 +146,35 @@ export async function updateAuthorizationDocument(
|
|||
* Vollmacht-Dokument löschen
|
||||
*/
|
||||
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
||||
// Prüfen ob die Vollmacht per Papier erteilt wurde
|
||||
const auth = await prisma.representativeAuthorization.findUnique({
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
select: { source: true, documentPath: true },
|
||||
});
|
||||
|
||||
if (!auth) throw new Error('Vollmacht nicht gefunden');
|
||||
|
||||
// Datei löschen
|
||||
if (auth.documentPath) {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), auth.documentPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen der Vollmacht-PDF:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Wenn per Papier erteilt → Vollmacht widerrufen
|
||||
// Wenn per Portal/Online erteilt → nur PDF entfernen, Vollmacht bleibt
|
||||
const withdrawData = auth.source === 'papier'
|
||||
? { documentPath: null, isGranted: false, withdrawnAt: new Date() }
|
||||
: { documentPath: null };
|
||||
|
||||
return prisma.representativeAuthorization.update({
|
||||
where: {
|
||||
customerId_representativeId: { customerId, representativeId },
|
||||
},
|
||||
data: {
|
||||
documentPath: null,
|
||||
},
|
||||
where: { customerId_representativeId: { customerId, representativeId } },
|
||||
data: withdrawData,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,14 +125,14 @@ export async function getContractById(id: number, decryptPassword = false) {
|
|||
previousProvider: true,
|
||||
previousContract: {
|
||||
include: {
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
carInsuranceDetails: true,
|
||||
},
|
||||
},
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, invoices: true } },
|
||||
energyDetails: { include: { meter: { include: { readings: true } }, contractMeters: { include: { meter: { include: { readings: true } } }, orderBy: { position: 'asc' as const } }, invoices: true } },
|
||||
internetDetails: { include: { phoneNumbers: true } },
|
||||
mobileDetails: { include: { simCards: true } },
|
||||
tvDetails: true,
|
||||
|
|
@ -403,11 +403,45 @@ export async function updateContract(
|
|||
|
||||
// Update type-specific details
|
||||
if (energyDetails) {
|
||||
const existingEcd = await prisma.energyContractDetails.findUnique({
|
||||
where: { contractId: id },
|
||||
select: { id: true, meterId: true },
|
||||
});
|
||||
|
||||
await prisma.energyContractDetails.upsert({
|
||||
where: { contractId: id },
|
||||
update: energyDetails,
|
||||
create: { contractId: id, ...energyDetails },
|
||||
});
|
||||
|
||||
// ContractMeter synchronisieren wenn sich der Zähler ändert
|
||||
if (energyDetails.meterId !== undefined && existingEcd) {
|
||||
const oldMeterId = existingEcd.meterId;
|
||||
const newMeterId = energyDetails.meterId;
|
||||
|
||||
if (oldMeterId !== newMeterId) {
|
||||
// Alle alten ContractMeter-Einträge entfernen
|
||||
await prisma.contractMeter.deleteMany({
|
||||
where: { energyContractDetailsId: existingEcd.id },
|
||||
});
|
||||
|
||||
// Neuen ContractMeter-Eintrag erstellen (wenn ein Zähler gesetzt)
|
||||
if (newMeterId) {
|
||||
const contract = await prisma.contract.findUnique({
|
||||
where: { id },
|
||||
select: { startDate: true },
|
||||
});
|
||||
await prisma.contractMeter.create({
|
||||
data: {
|
||||
energyContractDetailsId: existingEcd.id,
|
||||
meterId: newMeterId,
|
||||
position: 0,
|
||||
installedAt: contract?.startDate,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (internetDetails) {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ export interface ReportedMeterReading {
|
|||
customerNumber: string;
|
||||
name: string;
|
||||
};
|
||||
// Zugehöriger Vertrag
|
||||
contract?: {
|
||||
id: number;
|
||||
contractNumber: string;
|
||||
};
|
||||
// Anbieter-Info für Quick-Login
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
|
|
@ -810,6 +815,7 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
|||
contract: {
|
||||
select: {
|
||||
id: true,
|
||||
contractNumber: true,
|
||||
portalUsername: true,
|
||||
provider: {
|
||||
select: { id: true, name: true, portalUrl: true },
|
||||
|
|
@ -847,6 +853,10 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
|||
customerNumber: r.meter.customer.customerNumber,
|
||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||
},
|
||||
contract: contract ? {
|
||||
id: contract.id,
|
||||
contractNumber: contract.contractNumber,
|
||||
} : undefined,
|
||||
providerPortal: provider?.portalUrl ? {
|
||||
providerName: provider.name,
|
||||
portalUrl: provider.portalUrl,
|
||||
|
|
|
|||
|
|
@ -443,6 +443,30 @@ export async function updateMeter(
|
|||
}
|
||||
|
||||
export async function deleteMeter(id: number) {
|
||||
// Prüfen ob der Zähler noch an Verträgen hängt
|
||||
const linkedContracts = await prisma.contractMeter.findMany({
|
||||
where: { meterId: id },
|
||||
include: { energyContractDetails: { include: { contract: { select: { contractNumber: true } } } } },
|
||||
});
|
||||
|
||||
if (linkedContracts.length > 0) {
|
||||
const contractNumbers = linkedContracts
|
||||
.map(cm => cm.energyContractDetails.contract.contractNumber)
|
||||
.join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
|
||||
// Auch direkte meterId-Referenz auf EnergyContractDetails prüfen
|
||||
const directLinks = await prisma.energyContractDetails.findMany({
|
||||
where: { meterId: id },
|
||||
include: { contract: { select: { contractNumber: true } } },
|
||||
});
|
||||
|
||||
if (directLinks.length > 0) {
|
||||
const contractNumbers = directLinks.map(d => d.contract.contractNumber).join(', ');
|
||||
throw new Error(`Zähler kann nicht gelöscht werden – noch an Vertrag/Verträgen zugeordnet: ${contractNumbers}`);
|
||||
}
|
||||
|
||||
return prisma.meter.delete({ where: { id } });
|
||||
}
|
||||
|
||||
|
|
@ -451,14 +475,25 @@ export async function addMeterReading(
|
|||
data: {
|
||||
readingDate: Date;
|
||||
value: number;
|
||||
valueNt?: number;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}
|
||||
) {
|
||||
// Validierung: Zählerstand muss monoton steigend sein
|
||||
await validateReadingValue(meterId, data.readingDate, data.value, undefined, 'HT');
|
||||
if (data.valueNt !== undefined) {
|
||||
await validateReadingValue(meterId, data.readingDate, data.valueNt, undefined, 'NT');
|
||||
}
|
||||
|
||||
return prisma.meterReading.create({
|
||||
data: {
|
||||
meterId,
|
||||
...data,
|
||||
readingDate: data.readingDate,
|
||||
value: data.value,
|
||||
valueNt: data.valueNt,
|
||||
unit: data.unit,
|
||||
notes: data.notes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -476,6 +511,7 @@ export async function updateMeterReading(
|
|||
data: {
|
||||
readingDate?: Date;
|
||||
value?: number;
|
||||
valueNt?: number | null;
|
||||
unit?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
|
@ -489,12 +525,65 @@ export async function updateMeterReading(
|
|||
throw new Error('Zählerstand nicht gefunden');
|
||||
}
|
||||
|
||||
// Validierung bei Wertänderung
|
||||
if (data.value !== undefined || data.readingDate !== undefined) {
|
||||
await validateReadingValue(
|
||||
meterId,
|
||||
data.readingDate || reading.readingDate,
|
||||
data.value ?? reading.value,
|
||||
readingId,
|
||||
'HT'
|
||||
);
|
||||
}
|
||||
if (data.valueNt !== undefined || data.readingDate !== undefined) {
|
||||
const ntVal = data.valueNt ?? reading.valueNt;
|
||||
if (ntVal !== undefined && ntVal !== null) {
|
||||
await validateReadingValue(
|
||||
meterId,
|
||||
data.readingDate || reading.readingDate,
|
||||
ntVal,
|
||||
readingId,
|
||||
'NT'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return prisma.meterReading.update({
|
||||
where: { id: readingId },
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert, dass ein Zählerstand monoton steigend ist.
|
||||
* tariffLabel: 'HT' für Hochtarif/Eintarif, 'NT' für Niedertarif
|
||||
*/
|
||||
async function validateReadingValue(meterId: number, readingDate: Date, value: number, excludeReadingId?: number, tariffLabel: 'HT' | 'NT' = 'HT') {
|
||||
const existing = await prisma.meterReading.findMany({
|
||||
where: { meterId, ...(excludeReadingId ? { id: { not: excludeReadingId } } : {}) },
|
||||
orderBy: { readingDate: 'asc' },
|
||||
});
|
||||
|
||||
const fmtDate = (d: Date) => d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const fmtVal = (v: number) => v.toLocaleString('de-DE');
|
||||
const label = tariffLabel === 'NT' ? 'NT-Zählerstand' : 'Zählerstand';
|
||||
|
||||
// Vergleichswert aus bestehendem Reading extrahieren
|
||||
const getVal = (r: typeof existing[0]) => tariffLabel === 'NT' ? (r.valueNt ?? 0) : r.value;
|
||||
|
||||
// Stand vor dem neuen Datum
|
||||
const before = [...existing].filter(r => r.readingDate <= readingDate).pop();
|
||||
if (before && value < getVal(before)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht kleiner sein als der Stand vom ${fmtDate(before.readingDate)} (${fmtVal(getVal(before))})`);
|
||||
}
|
||||
|
||||
// Stand nach dem neuen Datum
|
||||
const after = existing.find(r => r.readingDate > readingDate);
|
||||
if (after && value > getVal(after)) {
|
||||
throw new Error(`${label} (${fmtVal(value)}) darf nicht größer sein als der spätere Stand vom ${fmtDate(after.readingDate)} (${fmtVal(getVal(after))})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteMeterReading(meterId: number, readingId: number) {
|
||||
// Verify the reading belongs to the meter
|
||||
const reading = await prisma.meterReading.findFirst({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
Vertragliste bei Energie mit Anschlussadresse/Lieferadresse noch in der Liste
|
||||
Bei Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
|
||||
Bei Festnetz, die Anschlussadresse/Lieferadresse
|
||||
Bei KFZ das Kennzeichen
|
||||
|
||||
#erledigt
|
||||
Datenschutzerklärung wenn PDF hinterlegt wurde, alle Haken auf Grün setzten.
|
||||
Und wenn von Kunde im Kundenportal ein Haken weg, pdf wieder löschen und gesperrt setzen. bis endweder alle haken wieder gesetzt sind, oder pdf erneut hochgeladen
|
||||
Aktuell zählt das PDF als Alternative zu den Online-Haken. Du willst es so:
|
||||
|
||||
PDF hochgeladen → alle 4 Online-Consents automatisch auf GRANTED setzen
|
||||
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
|
||||
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
|
||||
|
||||
#erledigt
|
||||
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen.
|
||||
Auch in die Berechnung die Verbäuche dann darstellen
|
||||
|
||||
|
||||
Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist
|
||||
|
||||
Jetzt : 1.1.2026
|
||||
Und gewollt 01.01.2026
|
||||
|
||||
|
||||
Die Auditmeldungen aussagekräftig
|
||||
|
||||
|
||||
Email Log und system testen
|
||||
|
||||
|
||||
Security System testen
|
||||
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useMemo, useEffect, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { pushHistory } from '../../utils/navigation';
|
||||
import { contractApi, meterApi } from '../../services/api';
|
||||
import Card from '../../components/ui/Card';
|
||||
import Badge from '../../components/ui/Badge';
|
||||
|
|
@ -282,7 +283,7 @@ export default function ContractCockpit() {
|
|||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Link
|
||||
to={`/contracts/${contract.id}`}
|
||||
state={{ from: '/contracts/cockpit' }}
|
||||
state={pushHistory('/contracts/cockpit')}
|
||||
className="font-medium hover:underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
|
|
@ -399,7 +400,7 @@ export default function ContractCockpit() {
|
|||
|
||||
<Link
|
||||
to={`/contracts/${contract.id}`}
|
||||
state={{ from: '/contracts/cockpit' }}
|
||||
state={pushHistory('/contracts/cockpit')}
|
||||
className="p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title="Zum Vertrag"
|
||||
|
|
@ -610,6 +611,15 @@ export default function ContractCockpit() {
|
|||
{reading.customer.name}
|
||||
</Link>
|
||||
<span className="text-xs text-gray-500">({reading.customer.customerNumber})</span>
|
||||
{reading.contract && (
|
||||
<Link
|
||||
to={`/contracts/${reading.contract.id}`}
|
||||
state={pushHistory('/contracts/cockpit')}
|
||||
className="text-xs text-blue-600 hover:underline"
|
||||
>
|
||||
{reading.contract.contractNumber}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">
|
||||
Zähler {reading.meter.meterNumber} – <strong>{reading.value} {reading.unit}</strong> am{' '}
|
||||
|
|
@ -635,6 +645,7 @@ export default function ContractCockpit() {
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (!confirm(`Zählerstand ${reading.value} ${reading.unit} (Zähler ${reading.meter.meterNumber}) als übertragen markieren?`)) return;
|
||||
await meterApi.markTransferred(reading.meter.id, reading.id);
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-cockpit'] });
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { pushHistory, popHistory } from '../../utils/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi } from '../../services/api';
|
||||
import { ContractEmailsSection } from '../../components/email';
|
||||
|
|
@ -13,9 +14,9 @@ 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 { calculateConsumption, calculateCosts } from '../../utils/energyCalculations';
|
||||
import { calculateConsumption, calculateCosts, calculateMultiMeterConsumption } from '../../utils/energyCalculations';
|
||||
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask } from '../../types';
|
||||
import type { ContractType, ContractStatus, SimCard, MeterReading, ContractTask, ContractTaskSubtask, ContractMeter } from '../../types';
|
||||
|
||||
const typeLabels: Record<ContractType, string> = {
|
||||
ELECTRICITY: 'Strom',
|
||||
|
|
@ -197,16 +198,19 @@ function SimCardDisplay({ simCard }: { simCard: SimCard }) {
|
|||
function MeterReadingsSection({
|
||||
meterId,
|
||||
meterType,
|
||||
tariffModel,
|
||||
readings,
|
||||
contractId,
|
||||
canEdit,
|
||||
label,
|
||||
}: {
|
||||
meterId: number;
|
||||
meterType: 'ELECTRICITY' | 'GAS';
|
||||
tariffModel?: string;
|
||||
readings: MeterReading[];
|
||||
contractId: number;
|
||||
canEdit: boolean;
|
||||
label?: string;
|
||||
}) {
|
||||
const isDualTariff = tariffModel === 'DUAL';
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [editingReading, setEditingReading] = useState<MeterReading | null>(null);
|
||||
|
|
@ -215,7 +219,7 @@ function MeterReadingsSection({
|
|||
const deleteReadingMutation = useMutation({
|
||||
mutationFn: (readingId: number) => meterApi.deleteReading(meterId, readingId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -231,7 +235,7 @@ function MeterReadingsSection({
|
|||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gauge className="w-4 h-4 text-gray-500" />
|
||||
<h4 className="text-sm font-medium text-gray-700">Zählerstände</h4>
|
||||
<h4 className="text-sm font-medium text-gray-700">{label || 'Zählerstände'}</h4>
|
||||
<Badge variant="default">{readings.length}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -274,8 +278,12 @@ function MeterReadingsSection({
|
|||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
{reading.value.toLocaleString('de-DE')} {reading.unit}
|
||||
<CopyButton value={reading.value.toString()} title="Nur Wert kopieren" />
|
||||
{isDualTariff && reading.valueNt != null ? (
|
||||
<>HT: {reading.value.toLocaleString('de-DE')} / NT: {reading.valueNt.toLocaleString('de-DE')} {reading.unit}</>
|
||||
) : (
|
||||
<>{reading.value.toLocaleString('de-DE')} {reading.unit}</>
|
||||
)}
|
||||
<CopyButton value={reading.value.toString()} title="Wert kopieren" />
|
||||
</span>
|
||||
{canEdit && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
||||
|
|
@ -324,9 +332,9 @@ function MeterReadingsSection({
|
|||
setEditingReading(null);
|
||||
}}
|
||||
meterId={meterId}
|
||||
contractId={contractId}
|
||||
reading={editingReading}
|
||||
defaultUnit={defaultUnit}
|
||||
tariffModel={tariffModel}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -334,22 +342,135 @@ function MeterReadingsSection({
|
|||
}
|
||||
|
||||
// Meter Reading Modal Component
|
||||
function SuccessorMeterButton({
|
||||
contractId,
|
||||
customerId,
|
||||
meterType,
|
||||
existingMeterIds,
|
||||
}: {
|
||||
contractId: number;
|
||||
customerId: number;
|
||||
meterType: string;
|
||||
existingMeterIds: number[];
|
||||
}) {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [selectedMeterId, setSelectedMeterId] = useState('');
|
||||
const [installedAt, setInstalledAt] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [finalReading, setFinalReading] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: metersData } = useQuery({
|
||||
queryKey: ['customer-meters', customerId],
|
||||
queryFn: () => meterApi.getByCustomer(customerId),
|
||||
enabled: showForm,
|
||||
});
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: (data: { meterId: number; installedAt?: string; finalReadingPrevious?: number }) =>
|
||||
contractApi.addSuccessorMeter(contractId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
setShowForm(false);
|
||||
setSelectedMeterId('');
|
||||
setFinalReading('');
|
||||
},
|
||||
});
|
||||
|
||||
// Nur Zähler gleichen Typs anzeigen die noch nicht am Vertrag sind
|
||||
const availableMeters = (metersData?.data || []).filter(
|
||||
(m) => m.type === meterType && !existingMeterIds.includes(m.id) && m.isActive
|
||||
);
|
||||
|
||||
if (!showForm) {
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowForm(true)}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
Folgezähler hinzufügen
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t bg-blue-50 rounded-lg p-4">
|
||||
<h4 className="text-sm font-medium mb-3">Folgezähler hinzufügen (Zählerwechsel)</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Neuer Zähler *</label>
|
||||
<select
|
||||
value={selectedMeterId}
|
||||
onChange={(e) => setSelectedMeterId(e.target.value)}
|
||||
className="block w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
{availableMeters.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.meterNumber} {m.location ? `(${m.location})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{availableMeters.length === 0 && (
|
||||
<p className="text-xs text-amber-600 mt-1">Kein passender Zähler verfügbar. Bitte zuerst beim Kunden anlegen.</p>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
label="Wechseldatum"
|
||||
type="date"
|
||||
value={installedAt}
|
||||
onChange={(e) => setInstalledAt(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Letzter Stand alter Zähler"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={finalReading}
|
||||
onChange={(e) => setFinalReading(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 mt-3">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => addMutation.mutate({
|
||||
meterId: parseInt(selectedMeterId),
|
||||
installedAt,
|
||||
finalReadingPrevious: finalReading ? parseFloat(finalReading) : undefined,
|
||||
})}
|
||||
disabled={!selectedMeterId || addMutation.isPending}
|
||||
>
|
||||
{addMutation.isPending ? 'Speichern...' : 'Hinzufügen'}
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowForm(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
</div>
|
||||
{addMutation.isError && (
|
||||
<p className="text-xs text-red-600 mt-2">
|
||||
{addMutation.error instanceof Error ? addMutation.error.message : 'Fehler beim Hinzufügen'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MeterReadingModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
meterId,
|
||||
contractId,
|
||||
reading,
|
||||
defaultUnit,
|
||||
tariffModel,
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
meterId: number;
|
||||
contractId: number;
|
||||
reading?: MeterReading | null;
|
||||
defaultUnit: string;
|
||||
tariffModel?: string;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const isDualTariff = tariffModel === 'DUAL';
|
||||
const isEditing = !!reading;
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
|
|
@ -357,33 +478,47 @@ function MeterReadingModal({
|
|||
? new Date(reading.readingDate).toISOString().split('T')[0]
|
||||
: new Date().toISOString().split('T')[0],
|
||||
value: reading?.value?.toString() || '',
|
||||
valueNt: reading?.valueNt?.toString() || '',
|
||||
notes: reading?.notes || '',
|
||||
});
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Partial<MeterReading>) => meterApi.addReading(meterId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
setError(null);
|
||||
onClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: Partial<MeterReading>) => meterApi.updateReading(meterId, reading!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||
setError(null);
|
||||
onClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
const data: Record<string, unknown> = {
|
||||
readingDate: new Date(formData.readingDate),
|
||||
value: parseFloat(formData.value),
|
||||
unit: defaultUnit,
|
||||
notes: formData.notes || undefined,
|
||||
};
|
||||
if (isDualTariff && formData.valueNt) {
|
||||
data.valueNt = parseFloat(formData.valueNt);
|
||||
}
|
||||
if (isEditing) {
|
||||
updateMutation.mutate(data as unknown as Partial<MeterReading>);
|
||||
} else {
|
||||
|
|
@ -404,10 +539,10 @@ function MeterReadingModal({
|
|||
required
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<div className={`grid ${isDualTariff ? 'grid-cols-2' : 'grid-cols-3'} gap-4`}>
|
||||
<div className={isDualTariff ? '' : 'col-span-2'}>
|
||||
<Input
|
||||
label="Zählerstand"
|
||||
label={isDualTariff ? 'HT-Stand (Hochtarif)' : 'Zählerstand'}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.value}
|
||||
|
|
@ -415,12 +550,26 @@ function MeterReadingModal({
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
||||
{defaultUnit}
|
||||
{isDualTariff && (
|
||||
<div>
|
||||
<Input
|
||||
label="NT-Stand (Niedertarif)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.valueNt}
|
||||
onChange={(e) => setFormData({ ...formData, valueNt: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isDualTariff && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
||||
{defaultUnit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
|
|
@ -429,6 +578,12 @@ function MeterReadingModal({
|
|||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
|
|
@ -450,24 +605,67 @@ function EnergyConsumptionCalculation({
|
|||
endDate,
|
||||
basePrice,
|
||||
unitPrice,
|
||||
unitPriceNt,
|
||||
bonus,
|
||||
hasMeter,
|
||||
contractMeters,
|
||||
}: {
|
||||
contractType: 'ELECTRICITY' | 'GAS';
|
||||
readings: MeterReading[];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
basePrice?: number;
|
||||
unitPrice?: number;
|
||||
bonus?: number;
|
||||
unitPriceNt?: number;
|
||||
hasMeter?: boolean;
|
||||
contractMeters?: ContractMeter[];
|
||||
}) {
|
||||
// Berechnung durchführen
|
||||
const consumption = calculateConsumption(readings, startDate, endDate, contractType);
|
||||
const costs = consumption.consumptionKwh > 0
|
||||
? calculateCosts(consumption.consumptionKwh, basePrice, unitPrice, bonus)
|
||||
// Fehlende Felder ermitteln
|
||||
const missingFields: string[] = [];
|
||||
if (!startDate) missingFields.push('Vertragsbeginn');
|
||||
if (!endDate) missingFields.push('Vertragsende');
|
||||
if (!hasMeter) missingFields.push('Zähler verknüpft');
|
||||
if (basePrice == null) missingFields.push('Grundpreis');
|
||||
if (unitPrice == null) missingFields.push('Arbeitspreis');
|
||||
|
||||
const hasRequiredDates = !!startDate && !!endDate;
|
||||
|
||||
// Alle Readings über alle Zähler zählen
|
||||
const allReadingsCount = contractMeters && contractMeters.length > 0
|
||||
? contractMeters.reduce((sum, cm) => sum + (cm.meter?.readings?.length || 0), 0)
|
||||
: readings.length;
|
||||
|
||||
if (hasRequiredDates && hasMeter && allReadingsCount < 2) {
|
||||
missingFields.push(`Mindestens 2 Zählerstände im Vertragszeitraum (${allReadingsCount} vorhanden)`);
|
||||
}
|
||||
|
||||
// Berechnung: Multi-Zähler wenn vorhanden, sonst Single
|
||||
let consumption = null;
|
||||
if (hasRequiredDates) {
|
||||
if (contractMeters && contractMeters.length > 1) {
|
||||
consumption = calculateMultiMeterConsumption(contractMeters, startDate!, endDate!, contractType);
|
||||
} else {
|
||||
consumption = calculateConsumption(readings, startDate!, endDate!, contractType);
|
||||
}
|
||||
}
|
||||
|
||||
const htKwh = consumption?.consumptionHt ?? consumption?.consumptionKwh ?? 0;
|
||||
const ntKwh = consumption?.consumptionNt;
|
||||
const costs = consumption && consumption.consumptionKwh > 0
|
||||
? calculateCosts(htKwh, basePrice, unitPrice, bonus, ntKwh, unitPriceNt)
|
||||
: null;
|
||||
|
||||
// Nichts anzeigen wenn keine Daten
|
||||
if (consumption.type === 'none') return null;
|
||||
const canCalculate = consumption && (consumption.type === 'exact' || consumption.type === 'projected');
|
||||
|
||||
// Wenn Berechnung fehlschlägt aber keine Felder fehlen → Hinweis aus der Berechnung anzeigen
|
||||
if (!canCalculate && consumption && missingFields.length === 0) {
|
||||
if (consumption.type === 'insufficient' && consumption.message) {
|
||||
missingFields.push(consumption.message);
|
||||
} else if (consumption.type === 'none') {
|
||||
missingFields.push('Keine Zählerstände im Vertragszeitraum vorhanden');
|
||||
}
|
||||
}
|
||||
|
||||
const formatNumber = (num: number, decimals: number = 2) =>
|
||||
num.toLocaleString('de-DE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
||||
|
|
@ -481,40 +679,61 @@ function EnergyConsumptionCalculation({
|
|||
<div className="flex items-center gap-2 mb-3">
|
||||
<Calculator className="w-4 h-4 text-gray-500" />
|
||||
<h4 className="text-sm font-medium text-gray-700">Verbrauch & Kosten</h4>
|
||||
{consumption.type === 'exact' && (
|
||||
{canCalculate && consumption!.type === 'exact' && (
|
||||
<Badge variant="success">Exakt</Badge>
|
||||
)}
|
||||
{consumption.type === 'projected' && (
|
||||
{canCalculate && consumption!.type === 'projected' && (
|
||||
<Badge variant="warning">Hochrechnung</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fall C: Unzureichende Daten */}
|
||||
{consumption.type === 'insufficient' ? (
|
||||
<p className="text-sm text-gray-500 italic">{consumption.message}</p>
|
||||
{/* Fehlende Daten */}
|
||||
{!canCalculate ? (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
Für die Berechnung fehlen noch folgende Angaben:
|
||||
</p>
|
||||
<ul className="space-y-1.5">
|
||||
{missingFields.map((field) => (
|
||||
<li key={field} className="flex items-center gap-2 text-sm">
|
||||
<Circle className="w-3 h-3 text-gray-300 flex-shrink-0" />
|
||||
<span className="text-gray-500">{field}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{consumption?.type === 'insufficient' && (
|
||||
<p className="text-sm text-amber-600 mt-3 italic">{consumption.message}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
|
||||
{/* Verbrauchsanzeige */}
|
||||
<div>
|
||||
<h5 className="text-sm font-medium text-gray-600 mb-2">
|
||||
Berechneter Verbrauch
|
||||
{consumption.type === 'projected' && ' (hochgerechnet)'}
|
||||
{consumption!.type === 'projected' && ' (hochgerechnet)'}
|
||||
</h5>
|
||||
<div className="text-lg font-semibold text-gray-900">
|
||||
{contractType === 'GAS' ? (
|
||||
<>
|
||||
<span className="font-mono">{formatNumber(consumption.consumptionM3 || 0)} m³</span>
|
||||
<span className="font-mono">{formatNumber(consumption!.consumptionM3 || 0)} m³</span>
|
||||
<span className="text-gray-500 text-sm ml-2">
|
||||
= {formatNumber(consumption.consumptionKwh)} kWh
|
||||
= {formatNumber(consumption!.consumptionKwh)} kWh
|
||||
</span>
|
||||
</>
|
||||
) : consumption!.consumptionHt !== undefined && consumption!.consumptionNt !== undefined ? (
|
||||
<div className="space-y-1">
|
||||
<div><span className="text-sm text-gray-500">HT:</span> <span className="font-mono">{formatNumber(consumption!.consumptionHt)} kWh</span></div>
|
||||
<div><span className="text-sm text-gray-500">NT:</span> <span className="font-mono">{formatNumber(consumption!.consumptionNt)} kWh</span></div>
|
||||
<div className="text-sm text-gray-500">Gesamt: {formatNumber(consumption!.consumptionKwh)} kWh</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-mono">{formatNumber(consumption.consumptionKwh)} kWh</span>
|
||||
<span className="font-mono">{formatNumber(consumption!.consumptionKwh)} kWh</span>
|
||||
)}
|
||||
</div>
|
||||
{consumption.startReading && consumption.endReading && (
|
||||
{consumption!.startReading && consumption!.endReading && (
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Basierend auf Zählerständen vom {formatDate(consumption.startReading.readingDate)} bis {formatDate(consumption.endReading.readingDate)}
|
||||
Basierend auf Zählerständen vom {formatDate(consumption!.startReading.readingDate)} bis {formatDate(consumption!.endReading.readingDate)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -533,15 +752,24 @@ function EnergyConsumptionCalculation({
|
|||
<span className="font-mono">{formatNumber(costs.annualBaseCost)} €</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Arbeitspreis */}
|
||||
{/* Arbeitspreis HT */}
|
||||
{unitPrice != null && unitPrice > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">
|
||||
Arbeitspreis: {formatNumber(consumption.consumptionKwh)} kWh × {formatNumber(unitPrice, 4)} €
|
||||
{costs.annualConsumptionCostNt ? 'HT-Arbeitspreis' : 'Arbeitspreis'}: {formatNumber(htKwh)} kWh × {formatNumber(unitPrice, 4)} €
|
||||
</span>
|
||||
<span className="font-mono">{formatNumber(costs.annualConsumptionCost)} €</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Arbeitspreis NT */}
|
||||
{costs.annualConsumptionCostNt != null && costs.annualConsumptionCostNt > 0 && unitPriceNt && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">
|
||||
NT-Arbeitspreis: {formatNumber(ntKwh || 0)} kWh × {formatNumber(unitPriceNt, 4)} €
|
||||
</span>
|
||||
<span className="font-mono">{formatNumber(costs.annualConsumptionCostNt)} €</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Trennlinie */}
|
||||
<div className="border-t border-gray-300 pt-2">
|
||||
<div className="flex justify-between font-medium">
|
||||
|
|
@ -1212,7 +1440,7 @@ export default function ContractDetail() {
|
|||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const backTo = (location.state as any)?.from as string | undefined;
|
||||
const currentPath = `/contracts/${id}`;
|
||||
const { hasPermission, isCustomer, isCustomerPortal } = useAuth();
|
||||
const contractId = parseInt(id!);
|
||||
|
||||
|
|
@ -1424,13 +1652,15 @@ export default function ContractDetail() {
|
|||
}
|
||||
|
||||
const c = data.data;
|
||||
const fallbackBack = isCustomerPortal ? '/contracts' : (c.customer ? `/customers/${c.customer.id}?tab=contracts` : '/contracts');
|
||||
const back = popHistory(location.state, fallbackBack);
|
||||
|
||||
// Consent-Sperrung: Vertrag nicht anzeigen wenn Kunde keine Einwilligung hat
|
||||
if (!hasConsentApproval) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(backTo || (isCustomerPortal ? '/contracts' : (c.customer ? `/customers/${c.customer.id}?tab=contracts` : '/contracts')))}>
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">Vertrag {c.contractNumber}</h1>
|
||||
|
|
@ -1465,7 +1695,7 @@ export default function ContractDetail() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(backTo || (isCustomerPortal ? '/contracts' : (c.customer ? `/customers/${c.customer.id}?tab=contracts` : '/contracts')))}
|
||||
onClick={() => navigate(back.to, { state: back.state })}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
@ -1499,7 +1729,7 @@ export default function ContractDetail() {
|
|||
{c.customer && (
|
||||
<p className="text-gray-500 ml-10">
|
||||
Kunde:{' '}
|
||||
<Link to={`/customers/${c.customer.id}`} state={{ from: `/contracts/${id}` }} className="text-blue-600 hover:underline">
|
||||
<Link to={`/customers/${c.customer.id}`} state={pushHistory(currentPath, location.state)} className="text-blue-600 hover:underline">
|
||||
{c.customer.companyName || `${c.customer.firstName} ${c.customer.lastName}`}
|
||||
</Link>
|
||||
</p>
|
||||
|
|
@ -1534,7 +1764,7 @@ export default function ContractDetail() {
|
|||
</Link>
|
||||
)}
|
||||
{hasPermission('contracts:update') && (
|
||||
<Link to={`/contracts/${id}/edit`} state={{ from: `/contracts/${id}` }}>
|
||||
<Link to={`/contracts/${id}/edit`} state={pushHistory(currentPath, location.state)}>
|
||||
<Button variant="secondary">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Bearbeiten
|
||||
|
|
@ -2201,7 +2431,12 @@ export default function ContractDetail() {
|
|||
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{c.energyDetails.meter && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Zählernummer</dt>
|
||||
<dt className="text-sm text-gray-500 flex items-center gap-1">
|
||||
Zählernummer
|
||||
{c.energyDetails.meter.tariffModel === 'DUAL' && (
|
||||
<Badge variant="default">HT/NT</Badge>
|
||||
)}
|
||||
</dt>
|
||||
<dd className="font-mono flex items-center gap-1">
|
||||
{c.energyDetails.meter.meterNumber}
|
||||
<CopyButton value={c.energyDetails.meter.meterNumber} />
|
||||
|
|
@ -2242,12 +2477,22 @@ export default function ContractDetail() {
|
|||
)}
|
||||
{c.energyDetails.unitPrice != null && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Arbeitspreis</dt>
|
||||
<dt className="text-sm text-gray-500">
|
||||
{c.energyDetails.unitPriceNt != null ? 'HT-Arbeitspreis' : 'Arbeitspreis'}
|
||||
</dt>
|
||||
<dd>
|
||||
{c.energyDetails.unitPrice.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 10 })} €/kWh
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.energyDetails.unitPriceNt != null && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">NT-Arbeitspreis</dt>
|
||||
<dd>
|
||||
{c.energyDetails.unitPriceNt.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 10 })} €/kWh
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
{c.energyDetails.bonus && (
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Bonus</dt>
|
||||
|
|
@ -2271,29 +2516,58 @@ export default function ContractDetail() {
|
|||
)}
|
||||
</dl>
|
||||
|
||||
{/* Zählerstände */}
|
||||
{c.energyDetails.meter && (
|
||||
{/* Zählerstände - Multi-Zähler Support */}
|
||||
{c.energyDetails.contractMeters && c.energyDetails.contractMeters.length > 0 ? (
|
||||
<>
|
||||
{c.energyDetails.contractMeters.map((cm: ContractMeter, idx: number) => (
|
||||
cm.meter && (
|
||||
<MeterReadingsSection
|
||||
key={cm.id}
|
||||
meterId={cm.meter.id}
|
||||
meterType={cm.meter.type}
|
||||
tariffModel={cm.meter.tariffModel}
|
||||
readings={cm.meter.readings || []}
|
||||
canEdit={hasPermission('contracts:update') && !isCustomer}
|
||||
label={c.energyDetails!.contractMeters!.length > 1
|
||||
? `Zähler ${idx + 1}: ${cm.meter.meterNumber}${cm.removedAt ? ' (gewechselt)' : ' (aktuell)'}`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{/* Folgezähler hinzufügen */}
|
||||
{hasPermission('contracts:update') && !isCustomer && (
|
||||
<SuccessorMeterButton
|
||||
contractId={contractId}
|
||||
customerId={c.customerId}
|
||||
meterType={c.type as 'ELECTRICITY' | 'GAS'}
|
||||
existingMeterIds={c.energyDetails.contractMeters.map((cm: ContractMeter) => cm.meterId)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : c.energyDetails.meter ? (
|
||||
<MeterReadingsSection
|
||||
meterId={c.energyDetails.meter.id}
|
||||
meterType={c.energyDetails.meter.type}
|
||||
tariffModel={c.energyDetails.meter.tariffModel}
|
||||
readings={c.energyDetails.meter.readings || []}
|
||||
contractId={contractId}
|
||||
canEdit={hasPermission('contracts:update') && !isCustomer}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Verbrauchsberechnung & Kostenvorschau */}
|
||||
{c.energyDetails.meter && c.startDate && c.endDate && (
|
||||
<EnergyConsumptionCalculation
|
||||
contractType={c.type as 'ELECTRICITY' | 'GAS'}
|
||||
readings={c.energyDetails.meter.readings || []}
|
||||
startDate={c.startDate}
|
||||
endDate={c.endDate}
|
||||
basePrice={c.energyDetails.basePrice}
|
||||
unitPrice={c.energyDetails.unitPrice}
|
||||
bonus={c.energyDetails.bonus}
|
||||
/>
|
||||
)}
|
||||
<EnergyConsumptionCalculation
|
||||
contractType={c.type as 'ELECTRICITY' | 'GAS'}
|
||||
readings={c.energyDetails.meter?.readings || []}
|
||||
startDate={c.startDate}
|
||||
endDate={c.endDate}
|
||||
basePrice={c.energyDetails.basePrice}
|
||||
unitPrice={c.energyDetails.unitPrice}
|
||||
unitPriceNt={c.energyDetails.unitPriceNt}
|
||||
bonus={c.energyDetails.bonus}
|
||||
hasMeter={!!c.energyDetails.meter}
|
||||
contractMeters={c.energyDetails.contractMeters}
|
||||
/>
|
||||
|
||||
{/* Rechnungen */}
|
||||
<InvoicesSection
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import { popHistory } from '../../utils/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
|
||||
|
|
@ -64,7 +65,7 @@ export default function ContractForm() {
|
|||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const isEdit = !!id;
|
||||
const backTo = (location.state as any)?.from as string | undefined;
|
||||
const back = popHistory(location.state, isEdit ? `/contracts/${id}` : '/contracts');
|
||||
|
||||
const preselectedCustomerId = searchParams.get('customerId');
|
||||
|
||||
|
|
@ -80,6 +81,7 @@ export default function ContractForm() {
|
|||
const contractType = watch('type') as ContractType;
|
||||
const customerId = watch('customerId');
|
||||
const previousContractId = watch('previousContractId');
|
||||
const selectedMeterId = watch('meterId');
|
||||
|
||||
// Fetch existing contract for edit
|
||||
const { data: contract } = useQuery({
|
||||
|
|
@ -276,6 +278,7 @@ export default function ContractForm() {
|
|||
annualConsumptionKwh: c.energyDetails?.annualConsumptionKwh || '',
|
||||
basePrice: c.energyDetails?.basePrice || '',
|
||||
unitPrice: c.energyDetails?.unitPrice || '',
|
||||
unitPriceNt: c.energyDetails?.unitPriceNt || '',
|
||||
bonus: c.energyDetails?.bonus || '',
|
||||
// Internet details
|
||||
downloadSpeed: c.internetDetails?.downloadSpeed || '',
|
||||
|
|
@ -512,6 +515,7 @@ export default function ContractForm() {
|
|||
annualConsumptionKwh: data.annualConsumptionKwh ? parseFloat(data.annualConsumptionKwh) : null,
|
||||
basePrice: data.basePrice ? parseFloat(data.basePrice) : null,
|
||||
unitPrice: data.unitPrice ? parseFloat(data.unitPrice) : null,
|
||||
unitPriceNt: data.unitPriceNt ? parseFloat(data.unitPriceNt) : null,
|
||||
bonus: data.bonus ? parseFloat(data.bonus) : null,
|
||||
};
|
||||
}
|
||||
|
|
@ -605,7 +609,8 @@ export default function ContractForm() {
|
|||
const addresses = customer?.addresses || [];
|
||||
const bankCards = customer?.bankCards?.filter((c) => c.isActive) || [];
|
||||
const documents = customer?.identityDocuments?.filter((d) => d.isActive) || [];
|
||||
const meters = customer?.meters?.filter((m) => m.isActive) || [];
|
||||
const meters = customer?.meters || [];
|
||||
const selectedMeter = meters.find(m => m.id.toString() === selectedMeterId);
|
||||
const stressfreiEmails = customer?.stressfreiEmails?.filter((e: { isActive: boolean }) => e.isActive) || [];
|
||||
const platforms = platformsData?.data || [];
|
||||
const cancellationPeriods = cancellationPeriodsData?.data || [];
|
||||
|
|
@ -658,7 +663,7 @@ export default function ContractForm() {
|
|||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(backTo || (isEdit ? `/contracts/${id}` : '/contracts'))}>
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">
|
||||
|
|
@ -951,10 +956,10 @@ export default function ContractForm() {
|
|||
label="Zähler"
|
||||
{...register('meterId')}
|
||||
options={meters
|
||||
.filter((m) => m.type === contractType)
|
||||
.filter((m) => m.type === contractType && (m.isActive || m.id.toString() === watch('meterId')))
|
||||
.map((m) => ({
|
||||
value: m.id,
|
||||
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}`,
|
||||
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`,
|
||||
}))}
|
||||
/>
|
||||
<Input
|
||||
|
|
@ -975,11 +980,19 @@ export default function ContractForm() {
|
|||
)}
|
||||
<Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} />
|
||||
<Input
|
||||
label="Arbeitspreis (€/kWh)"
|
||||
label={selectedMeter?.tariffModel === 'DUAL' ? 'HT-Arbeitspreis (€/kWh)' : 'Arbeitspreis (€/kWh)'}
|
||||
type="number"
|
||||
step="any"
|
||||
{...register('unitPrice')}
|
||||
/>
|
||||
{selectedMeter?.tariffModel === 'DUAL' && (
|
||||
<Input
|
||||
label="NT-Arbeitspreis (€/kWh)"
|
||||
type="number"
|
||||
step="any"
|
||||
{...register('unitPriceNt')}
|
||||
/>
|
||||
)}
|
||||
<Input label="Bonus (€)" type="number" step="0.01" {...register('bonus')} />
|
||||
</div>
|
||||
|
||||
|
|
@ -1409,7 +1422,7 @@ export default function ContractForm() {
|
|||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button type="button" variant="secondary" onClick={() => navigate(backTo || (isEdit ? `/contracts/${id}` : '/contracts'))}>
|
||||
<Button type="button" variant="secondary" onClick={() => navigate(back.to, { state: back.state })}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query';
|
||||
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { pushHistory } from '../../utils/navigation';
|
||||
import { contractApi, ContractTreeNode } from '../../services/api';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import Card from '../../components/ui/Card';
|
||||
|
|
@ -271,7 +272,7 @@ export default function ContractList() {
|
|||
<div className="w-6" /> // Platzhalter für Ausrichtung
|
||||
) : null}
|
||||
|
||||
<Link to={`/contracts/${contract.id}`} state={{ from: '/contracts' }} className="font-mono flex items-center gap-1 hover:text-blue-600 hover:underline">
|
||||
<Link to={`/contracts/${contract.id}`} state={pushHistory('/contracts')} className="font-mono flex items-center gap-1 hover:text-blue-600 hover:underline">
|
||||
{contract.contractNumber}
|
||||
<CopyButton value={contract.contractNumber} />
|
||||
</Link>
|
||||
|
|
@ -456,7 +457,7 @@ export default function ContractList() {
|
|||
{data.data.map((contract) => (
|
||||
<tr key={contract.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4 font-mono text-sm">
|
||||
<Link to={`/contracts/${contract.id}`} state={{ from: '/contracts' }} className="text-blue-600 hover:underline">
|
||||
<Link to={`/contracts/${contract.id}`} state={pushHistory('/contracts')} className="text-blue-600 hover:underline">
|
||||
{contract.contractNumber}
|
||||
</Link>
|
||||
</td>
|
||||
|
|
@ -465,7 +466,7 @@ export default function ContractList() {
|
|||
{contract.customer && (
|
||||
<Link
|
||||
to={`/customers/${contract.customer.id}`}
|
||||
state={{ from: '/contracts' }}
|
||||
state={pushHistory('/contracts')}
|
||||
className="text-blue-600 hover:underline"
|
||||
>
|
||||
{contract.customer.companyName ||
|
||||
|
|
@ -505,7 +506,7 @@ export default function ContractList() {
|
|||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{hasPermission('contracts:update') && !isCustomer && (
|
||||
<Link to={`/contracts/${contract.id}/edit`} state={{ from: '/contracts' }}>
|
||||
<Link to={`/contracts/${contract.id}/edit`} state={pushHistory('/contracts')}>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useParams, Link, useNavigate, useSearchParams, useLocation } from 'react-router-dom';
|
||||
import { pushHistory, popHistory } from '../../utils/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, gdprApi, StressfreiEmail, ContractTreeNode } from '../../services/api';
|
||||
import { EmailClientTab } from '../../components/email';
|
||||
|
|
@ -22,7 +23,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||
const queryClient = useQueryClient();
|
||||
const { hasPermission, isCustomerPortal } = useAuth();
|
||||
const location = useLocation();
|
||||
const backTo = (location.state as any)?.from as string | undefined;
|
||||
const back = popHistory(location.state, isCustomerPortal ? '/' : '/customers');
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const customerId = portalCustomerId || parseInt(id!);
|
||||
const defaultTab = searchParams.get('tab') || 'addresses';
|
||||
|
|
@ -204,7 +205,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||
<div>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(backTo || (isCustomerPortal ? '/' : '/customers'))}>
|
||||
<Button variant="ghost" size="sm" onClick={() => navigate(back.to, { state: back.state })}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
|
|
@ -221,7 +222,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
|||
</div>
|
||||
<div className="flex gap-2">
|
||||
{hasPermission('customers:update') && (
|
||||
<Link to={`/customers/${id}/edit`} state={{ from: `/customers/${id}` }}>
|
||||
<Link to={`/customers/${id}/edit`} state={pushHistory(location.pathname + location.search, (location as any).state)}>
|
||||
<Button variant="secondary">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Bearbeiten
|
||||
|
|
@ -649,7 +650,7 @@ function AddressesTab({
|
|||
const queryClient = useQueryClient();
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: addressApi.delete,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
return (
|
||||
|
|
@ -746,18 +747,18 @@ function BankCardsTab({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<BankCard> }) =>
|
||||
bankCardApi.update(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: bankCardApi.delete,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const handleDocumentUpload = async (cardId: number, file: File) => {
|
||||
try {
|
||||
await uploadApi.uploadBankCardDocument(cardId, file);
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Upload fehlgeschlagen:', error);
|
||||
alert('Upload fehlgeschlagen');
|
||||
|
|
@ -768,7 +769,7 @@ function BankCardsTab({
|
|||
if (!confirm('Dokument wirklich löschen?')) return;
|
||||
try {
|
||||
await uploadApi.deleteBankCardDocument(cardId);
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Löschen fehlgeschlagen:', error);
|
||||
alert('Löschen fehlgeschlagen');
|
||||
|
|
@ -972,18 +973,18 @@ function DocumentsTab({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<IdentityDocument> }) =>
|
||||
documentApi.update(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: documentApi.delete,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const handleDocumentUpload = async (docId: number, file: File) => {
|
||||
try {
|
||||
await uploadApi.uploadIdentityDocument(docId, file);
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Upload fehlgeschlagen:', error);
|
||||
alert('Upload fehlgeschlagen');
|
||||
|
|
@ -994,7 +995,7 @@ function DocumentsTab({
|
|||
if (!confirm('Dokument wirklich löschen?')) return;
|
||||
try {
|
||||
await uploadApi.deleteIdentityDocument(docId);
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Löschen fehlgeschlagen:', error);
|
||||
alert('Löschen fehlgeschlagen');
|
||||
|
|
@ -1204,26 +1205,34 @@ function MetersTab({
|
|||
onAdd: () => void;
|
||||
onEdit: (meter: Meter) => void;
|
||||
}) {
|
||||
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS' } | null>(null);
|
||||
const [showReadingModal, setShowReadingModal] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string } | null>(null);
|
||||
const [expandedMeter, setExpandedMeter] = useState<number | null>(null);
|
||||
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; reading: any } | null>(null);
|
||||
const [editingReading, setEditingReading] = useState<{ meterId: number; meterType: 'ELECTRICITY' | 'GAS'; tariffModel?: string; reading: any } | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<Meter> }) =>
|
||||
meterApi.update(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: meterApi.delete,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
setDeleteError(null);
|
||||
},
|
||||
onError: (err) => {
|
||||
setDeleteError(err instanceof Error ? err.message : 'Fehler beim Löschen');
|
||||
},
|
||||
});
|
||||
|
||||
const deleteReadingMutation = useMutation({
|
||||
mutationFn: ({ meterId, readingId }: { meterId: number; readingId: number }) =>
|
||||
meterApi.deleteReading(meterId, readingId),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const filtered = showInactive ? meters : meters.filter((m) => m.isActive);
|
||||
|
|
@ -1273,6 +1282,9 @@ function MetersTab({
|
|||
<Badge variant={meter.type === 'ELECTRICITY' ? 'warning' : 'info'}>
|
||||
{meter.type === 'ELECTRICITY' ? 'Strom' : 'Gas'}
|
||||
</Badge>
|
||||
{meter.tariffModel === 'DUAL' && (
|
||||
<Badge variant="default">HT/NT</Badge>
|
||||
)}
|
||||
{!meter.isActive && <Badge variant="danger">Inaktiv</Badge>}
|
||||
</div>
|
||||
{canEdit && (
|
||||
|
|
@ -1281,7 +1293,7 @@ function MetersTab({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowReadingModal({ meterId: meter.id, meterType: meter.type })}
|
||||
onClick={() => setShowReadingModal({ meterId: meter.id, meterType: meter.type, tariffModel: meter.tariffModel })}
|
||||
title="Zählerstand hinzufügen"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
|
|
@ -1371,14 +1383,17 @@ function MetersTab({
|
|||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono flex items-center gap-1">
|
||||
{reading.value.toLocaleString('de-DE')} {reading.unit}
|
||||
<CopyButton value={reading.value.toString()} title="Nur Wert kopieren" />
|
||||
<CopyButton value={`${reading.value.toLocaleString('de-DE')} ${reading.unit}`} title="Mit Einheit kopieren" />
|
||||
{reading.valueNt !== undefined && reading.valueNt !== null ? (
|
||||
<>HT: {reading.value.toLocaleString('de-DE')} / NT: {reading.valueNt.toLocaleString('de-DE')} {reading.unit}</>
|
||||
) : (
|
||||
<>{reading.value.toLocaleString('de-DE')} {reading.unit}</>
|
||||
)}
|
||||
<CopyButton value={reading.value.toString()} title="Wert kopieren" />
|
||||
</span>
|
||||
{canEdit && (
|
||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
||||
<button
|
||||
onClick={() => setEditingReading({ meterId: meter.id, meterType: meter.type, reading })}
|
||||
onClick={() => setEditingReading({ meterId: meter.id, meterType: meter.type, tariffModel: meter.tariffModel, reading })}
|
||||
className="text-gray-400 hover:text-blue-600"
|
||||
title="Bearbeiten"
|
||||
>
|
||||
|
|
@ -1417,6 +1432,7 @@ function MetersTab({
|
|||
onClose={() => setShowReadingModal(null)}
|
||||
meterId={showReadingModal.meterId}
|
||||
meterType={showReadingModal.meterType}
|
||||
tariffModel={showReadingModal.tariffModel as any}
|
||||
customerId={customerId}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1427,10 +1443,43 @@ function MetersTab({
|
|||
onClose={() => setEditingReading(null)}
|
||||
meterId={editingReading.meterId}
|
||||
meterType={editingReading.meterType}
|
||||
tariffModel={editingReading.tariffModel as any}
|
||||
customerId={customerId}
|
||||
reading={editingReading.reading}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Fehler-Modal beim Löschen (z.B. Zähler noch an Vertrag) */}
|
||||
{deleteError && (
|
||||
<Modal isOpen={true} onClose={() => setDeleteError(null)} title="Zähler kann nicht gelöscht werden">
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Der Zähler ist noch folgenden Verträgen zugeordnet und kann daher nicht gelöscht werden:
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{deleteError.match(/[A-Z]+-[A-Z0-9]+/g)?.map((contractNumber) => (
|
||||
<Link
|
||||
key={contractNumber}
|
||||
to={`/contracts?search=${contractNumber}`}
|
||||
onClick={() => setDeleteError(null)}
|
||||
className="flex items-center gap-2 p-3 bg-gray-50 border rounded-lg text-blue-600 hover:bg-blue-50 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span className="font-mono">{contractNumber}</span>
|
||||
</Link>
|
||||
)) ?? (
|
||||
<p className="text-sm text-red-600">{deleteError}</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-4">
|
||||
Bitte entfernen Sie den Zähler zuerst aus den oben genannten Verträgen.
|
||||
</p>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button variant="secondary" onClick={() => setDeleteError(null)}>
|
||||
Schließen
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1457,7 +1506,7 @@ function ContractsTab({
|
|||
const deleteMutation = useMutation({
|
||||
mutationFn: contractApi.delete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customers'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['contract-tree', customerId] });
|
||||
|
|
@ -1549,7 +1598,7 @@ function ContractsTab({
|
|||
<div className="w-6" /> // Platzhalter für Ausrichtung
|
||||
) : null}
|
||||
|
||||
<Link to={`/contracts/${contract.id}`} state={{ from: `/customers/${customerId}?tab=contracts` }} className="font-mono flex items-center gap-1 text-blue-600 hover:underline">
|
||||
<Link to={`/contracts/${contract.id}`} state={pushHistory(location.pathname + location.search, (location as any).state)} className="font-mono flex items-center gap-1 text-blue-600 hover:underline">
|
||||
{contract.contractNumber}
|
||||
<CopyButton value={contract.contractNumber} />
|
||||
</Link>
|
||||
|
|
@ -2054,7 +2103,7 @@ function AddressModal({
|
|||
const createMutation = useMutation({
|
||||
mutationFn: (data: typeof formData) => addressApi.create(customerId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
setFormData({
|
||||
type: 'DELIVERY_RESIDENCE',
|
||||
|
|
@ -2071,7 +2120,7 @@ function AddressModal({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: (data: typeof formData) => addressApi.update(address!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
|
@ -2201,7 +2250,7 @@ function BankCardModal({
|
|||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => bankCardApi.create(customerId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
setFormData({ accountHolder: '', iban: '', bic: '', bankName: '', expiryDate: '', isActive: true });
|
||||
},
|
||||
|
|
@ -2210,7 +2259,7 @@ function BankCardModal({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => bankCardApi.update(bankCard!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
|
@ -2333,7 +2382,7 @@ function DocumentModal({
|
|||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => documentApi.create(customerId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
setFormData({
|
||||
type: 'ID_CARD',
|
||||
|
|
@ -2351,7 +2400,7 @@ function DocumentModal({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => documentApi.update(document!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
|
@ -2498,6 +2547,7 @@ function MeterModal({
|
|||
const getInitialFormData = () => ({
|
||||
meterNumber: meter?.meterNumber || '',
|
||||
type: meter?.type || 'ELECTRICITY' as const,
|
||||
tariffModel: meter?.tariffModel || 'SINGLE' as const,
|
||||
location: meter?.location || '',
|
||||
isActive: meter?.isActive ?? true,
|
||||
});
|
||||
|
|
@ -2507,16 +2557,16 @@ function MeterModal({
|
|||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => meterApi.create(customerId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
setFormData({ meterNumber: '', type: 'ELECTRICITY', location: '', isActive: true });
|
||||
setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true });
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => meterApi.update(meter!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
|
@ -2557,6 +2607,18 @@ function MeterModal({
|
|||
]}
|
||||
/>
|
||||
|
||||
{formData.type === 'ELECTRICITY' && (
|
||||
<Select
|
||||
label="Tarifmodell"
|
||||
value={formData.tariffModel}
|
||||
onChange={(e) => setFormData({ ...formData, tariffModel: e.target.value as any })}
|
||||
options={[
|
||||
{ value: 'SINGLE', label: 'Eintarifzähler (Standard)' },
|
||||
{ value: 'DUAL', label: 'Zweitarifzähler (HT/NT)' },
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Standort"
|
||||
value={formData.location}
|
||||
|
|
@ -2594,6 +2656,7 @@ function MeterReadingModal({
|
|||
onClose,
|
||||
meterId,
|
||||
meterType,
|
||||
tariffModel,
|
||||
customerId,
|
||||
reading,
|
||||
}: {
|
||||
|
|
@ -2601,47 +2664,63 @@ function MeterReadingModal({
|
|||
onClose: () => void;
|
||||
meterId: number;
|
||||
meterType: 'ELECTRICITY' | 'GAS';
|
||||
tariffModel?: 'SINGLE' | 'DUAL';
|
||||
customerId: number;
|
||||
reading?: { id: number; readingDate: string; value: number; unit: string; notes?: string } | null;
|
||||
reading?: { id: number; readingDate: string; value: number; valueNt?: number; unit: string; notes?: string } | null;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const isEditing = !!reading;
|
||||
const defaultUnit = meterType === 'ELECTRICITY' ? 'kWh' : 'm³';
|
||||
const isDualTariff = tariffModel === 'DUAL';
|
||||
|
||||
const getInitialFormData = () => ({
|
||||
readingDate: reading?.readingDate
|
||||
? new Date(reading.readingDate).toISOString().split('T')[0]
|
||||
: new Date().toISOString().split('T')[0],
|
||||
value: reading?.value?.toString() || '',
|
||||
valueNt: reading?.valueNt?.toString() || '',
|
||||
notes: reading?.notes || '',
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState(getInitialFormData);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: any) => meterApi.addReading(meterId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
setError(null);
|
||||
onClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||
},
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: (data: any) => meterApi.updateReading(meterId, reading!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
setError(null);
|
||||
onClose();
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
const data: Record<string, unknown> = {
|
||||
readingDate: new Date(formData.readingDate),
|
||||
value: parseFloat(formData.value),
|
||||
unit: defaultUnit,
|
||||
notes: formData.notes || undefined,
|
||||
};
|
||||
if (isDualTariff && formData.valueNt) {
|
||||
data.valueNt = parseFloat(formData.valueNt);
|
||||
}
|
||||
if (isEditing) {
|
||||
updateMutation.mutate(data);
|
||||
} else {
|
||||
|
|
@ -2667,10 +2746,10 @@ function MeterReadingModal({
|
|||
required
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2">
|
||||
<div className={`grid ${isDualTariff ? 'grid-cols-2' : 'grid-cols-3'} gap-4`}>
|
||||
<div className={isDualTariff ? '' : 'col-span-2'}>
|
||||
<Input
|
||||
label="Zählerstand"
|
||||
label={isDualTariff ? 'HT-Stand (Hochtarif)' : 'Zählerstand'}
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.value}
|
||||
|
|
@ -2678,12 +2757,26 @@ function MeterReadingModal({
|
|||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
||||
{defaultUnit}
|
||||
{isDualTariff && (
|
||||
<div>
|
||||
<Input
|
||||
label="NT-Stand (Niedertarif)"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={formData.valueNt}
|
||||
onChange={(e) => setFormData({ ...formData, valueNt: e.target.value })}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isDualTariff && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
||||
{defaultUnit}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
|
|
@ -2693,6 +2786,12 @@ function MeterReadingModal({
|
|||
placeholder="Optionale Notizen..."
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Abbrechen
|
||||
|
|
@ -2732,12 +2831,12 @@ function StressfreiEmailsTab({
|
|||
const updateMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<StressfreiEmail> }) =>
|
||||
stressfreiEmailApi.update(id, data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: stressfreiEmailApi.delete,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||
});
|
||||
|
||||
const filtered = showInactive ? emails : emails.filter((e) => e.isActive);
|
||||
|
|
@ -3087,7 +3186,7 @@ function StressfreiEmailModal({
|
|||
const result = await stressfreiEmailApi.enableMailbox(email.id);
|
||||
if (result.success) {
|
||||
setMailboxEnabled(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
||||
} else {
|
||||
setProvisionError(result.error || 'Mailbox-Aktivierung fehlgeschlagen');
|
||||
|
|
@ -3108,7 +3207,7 @@ function StressfreiEmailModal({
|
|||
setMailboxEnabled(result.data.hasMailbox);
|
||||
if (result.data.wasUpdated) {
|
||||
// DB wurde aktualisiert, Query invalidieren
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -3199,7 +3298,7 @@ function StressfreiEmailModal({
|
|||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
||||
setLocalPart('');
|
||||
setNotes('');
|
||||
|
|
@ -3216,7 +3315,7 @@ function StressfreiEmailModal({
|
|||
mutationFn: (data: Partial<StressfreiEmail>) =>
|
||||
stressfreiEmailApi.update(email!.id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
|
@ -3664,6 +3763,7 @@ function ConsentTab({
|
|||
try {
|
||||
await uploadApi.uploadPrivacyPolicy(customerId, file);
|
||||
onUpdate?.();
|
||||
queryClient.invalidateQueries({ queryKey: ['customer-consents', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Upload fehlgeschlagen:', error);
|
||||
alert('Upload fehlgeschlagen');
|
||||
|
|
@ -3675,6 +3775,7 @@ function ConsentTab({
|
|||
try {
|
||||
await uploadApi.deletePrivacyPolicy(customerId);
|
||||
onUpdate?.();
|
||||
queryClient.invalidateQueries({ queryKey: ['customer-consents', customerId] });
|
||||
} catch (error) {
|
||||
console.error('Löschen fehlgeschlagen:', error);
|
||||
alert('Löschen fehlgeschlagen');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { popHistory } from '../../utils/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { customerApi } from '../../services/api';
|
||||
|
|
@ -17,7 +18,7 @@ export default function CustomerForm() {
|
|||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const isEdit = !!id;
|
||||
const backTo = (location.state as any)?.from as string | undefined;
|
||||
const back = popHistory(location.state, isEdit ? `/customers/${id}` : '/customers');
|
||||
|
||||
const { register, handleSubmit, reset, watch, setValue, formState: { errors } } = useForm<CustomerFormData>();
|
||||
const customerType = watch('type');
|
||||
|
|
@ -234,7 +235,7 @@ export default function CustomerForm() {
|
|||
</Card>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button type="button" variant="secondary" onClick={() => navigate(backTo || (isEdit ? `/customers/${id}` : '/customers'))}>
|
||||
<Button type="button" variant="secondary" onClick={() => navigate(back.to, { state: back.state })}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { pushHistory } from '../../utils/navigation';
|
||||
import { customerApi } from '../../services/api';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import Card from '../../components/ui/Card';
|
||||
|
|
@ -78,12 +79,12 @@ export default function CustomerList() {
|
|||
{data.data.map((customer) => (
|
||||
<tr key={customer.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4 font-mono text-sm">
|
||||
<Link to={`/customers/${customer.id}`} state={{ from: '/customers' }} className="text-blue-600 hover:underline">
|
||||
<Link to={`/customers/${customer.id}`} state={pushHistory('/customers')} className="text-blue-600 hover:underline">
|
||||
{customer.customerNumber}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<Link to={`/customers/${customer.id}`} state={{ from: '/customers' }} className="text-blue-600 hover:underline">
|
||||
<Link to={`/customers/${customer.id}`} state={pushHistory('/customers')} className="text-blue-600 hover:underline">
|
||||
{customer.type === 'BUSINESS' && customer.companyName
|
||||
? customer.companyName
|
||||
: `${customer.firstName} ${customer.lastName}`}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,15 @@ const RESOURCE_OPTIONS = [
|
|||
{ value: 'Contract', label: 'Verträge' },
|
||||
{ value: 'User', label: 'Benutzer' },
|
||||
{ value: 'BankCard', label: 'Bankdaten' },
|
||||
{ value: 'IdentityDocument', label: 'Ausweisdokumente' },
|
||||
{ value: 'Authentication', label: 'Authentifizierung' },
|
||||
{ value: 'IdentityDocument', label: 'Ausweise' },
|
||||
{ value: 'Address', label: 'Adressen' },
|
||||
{ value: 'Meter', label: 'Zähler' },
|
||||
{ value: 'ContractTask', label: 'Aufgaben' },
|
||||
{ value: 'Authentication', label: 'Anmeldung' },
|
||||
{ value: 'CustomerConsent', label: 'Einwilligungen' },
|
||||
{ value: 'GDPR', label: 'DSGVO' },
|
||||
{ value: 'GDPR', label: 'DSGVO / Vollmacht' },
|
||||
{ value: 'EmailProviderConfig', label: 'E-Mail-Provider' },
|
||||
{ value: 'AppSetting', label: 'Einstellungen' },
|
||||
];
|
||||
|
||||
function formatDate(date: string): string {
|
||||
|
|
@ -354,11 +359,12 @@ export default function AuditLogs() {
|
|||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>{log.resourceType}</div>
|
||||
{log.resourceLabel && (
|
||||
<div className="text-xs text-gray-500 truncate max-w-[200px]" title={log.resourceLabel}>
|
||||
{log.resourceLabel ? (
|
||||
<div className="text-sm truncate max-w-[300px]" title={log.resourceLabel}>
|
||||
{log.resourceLabel}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">{log.resourceType}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-mono text-xs">{log.ipAddress}</td>
|
||||
|
|
|
|||
|
|
@ -655,6 +655,15 @@ export const contractApi = {
|
|||
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
|
||||
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);
|
||||
return res.data;
|
||||
},
|
||||
removeContractMeter: async (contractId: number, contractMeterId: number) => {
|
||||
const res = await api.delete<ApiResponse<void>>(`/contracts/${contractId}/contract-meter/${contractMeterId}`);
|
||||
return res.data;
|
||||
},
|
||||
// Snooze: Vertrag zurückstellen
|
||||
snooze: async (id: number, data: { nextReviewDate?: string; months?: number }) => {
|
||||
const res = await api.patch<ApiResponse<{ id: number; contractNumber: string; nextReviewDate: string | null }>>(`/contracts/${id}/snooze`, data);
|
||||
|
|
|
|||
|
|
@ -152,16 +152,30 @@ export interface IdentityDocument {
|
|||
licenseIssueDate?: string;
|
||||
}
|
||||
|
||||
export type MeterTariffModel = 'SINGLE' | 'DUAL';
|
||||
|
||||
export interface Meter {
|
||||
id: number;
|
||||
customerId: number;
|
||||
meterNumber: string;
|
||||
type: 'ELECTRICITY' | 'GAS';
|
||||
tariffModel: MeterTariffModel;
|
||||
location?: string;
|
||||
isActive: boolean;
|
||||
readings?: MeterReading[];
|
||||
}
|
||||
|
||||
export interface ContractMeter {
|
||||
id: number;
|
||||
energyContractDetailsId: number;
|
||||
meterId: number;
|
||||
meter?: Meter;
|
||||
position: number;
|
||||
installedAt?: string;
|
||||
removedAt?: string;
|
||||
finalReading?: number;
|
||||
}
|
||||
|
||||
export type MeterReadingStatus = 'RECORDED' | 'REPORTED' | 'TRANSFERRED';
|
||||
|
||||
export interface MeterReading {
|
||||
|
|
@ -169,6 +183,7 @@ export interface MeterReading {
|
|||
meterId: number;
|
||||
readingDate: string;
|
||||
value: number;
|
||||
valueNt?: number; // NT-Wert (nur bei Zweitarifzähler)
|
||||
unit: string;
|
||||
notes?: string;
|
||||
reportedBy?: string;
|
||||
|
|
@ -396,11 +411,13 @@ export interface EnergyContractDetails {
|
|||
annualConsumption?: number; // kWh für Strom, m³ für Gas
|
||||
annualConsumptionKwh?: number; // kWh für Gas (zusätzlich zu m³)
|
||||
basePrice?: number; // €/Monat
|
||||
unitPrice?: number; // €/kWh (Arbeitspreis)
|
||||
unitPrice?: number; // €/kWh (Arbeitspreis) - bei HT/NT: HT-Preis
|
||||
unitPriceNt?: number; // €/kWh NT-Preis (nur bei Zweitarifzähler)
|
||||
bonus?: number;
|
||||
previousProviderName?: string;
|
||||
previousCustomerNumber?: string;
|
||||
invoices?: Invoice[]; // Rechnungen
|
||||
contractMeters?: ContractMeter[]; // Zähler-Zuordnungen (inkl. Folgezähler)
|
||||
}
|
||||
|
||||
export interface InternetContractDetails {
|
||||
|
|
@ -575,6 +592,7 @@ export interface ReportedMeterReading {
|
|||
createdAt: string;
|
||||
meter: { id: number; meterNumber: string; type: string };
|
||||
customer: { id: number; customerNumber: string; name: string };
|
||||
contract?: { id: number; contractNumber: string };
|
||||
providerPortal?: {
|
||||
providerName: string;
|
||||
portalUrl: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { MeterReading } from '../types';
|
||||
import type { MeterReading, ContractMeter } from '../types';
|
||||
|
||||
// Konstante für Gas → kWh Umrechnung (Standard Erdgas H)
|
||||
export const GAS_TO_KWH_FACTOR = 10.5;
|
||||
|
|
@ -8,6 +8,8 @@ export interface ConsumptionCalculation {
|
|||
type: 'exact' | 'projected' | 'insufficient' | 'none';
|
||||
consumptionM3?: number; // Nur bei Gas
|
||||
consumptionKwh: number;
|
||||
consumptionHt?: number; // Nur bei HT/NT: HT-Verbrauch in kWh
|
||||
consumptionNt?: number; // Nur bei HT/NT: NT-Verbrauch in kWh
|
||||
startReading?: MeterReading;
|
||||
endReading?: MeterReading;
|
||||
projectedEndDate?: string; // Nur bei Hochrechnung
|
||||
|
|
@ -17,7 +19,8 @@ export interface ConsumptionCalculation {
|
|||
// Ergebnis der Kostenberechnung
|
||||
export interface CostCalculation {
|
||||
annualBaseCost: number; // basePrice × 12
|
||||
annualConsumptionCost: number; // verbrauch × unitPrice
|
||||
annualConsumptionCost: number; // verbrauch × unitPrice (HT bei Zweitarif)
|
||||
annualConsumptionCostNt?: number; // NT-Verbrauch × unitPriceNt
|
||||
annualTotalCost: number; // Summe
|
||||
monthlyPayment: number; // annualTotalCost / 12
|
||||
bonus?: number;
|
||||
|
|
@ -94,7 +97,6 @@ export function calculateConsumption(
|
|||
(a, b) => new Date(a.readingDate).getTime() - new Date(b.readingDate).getTime()
|
||||
);
|
||||
|
||||
const firstReading = sorted[0];
|
||||
const lastReading = sorted[sorted.length - 1];
|
||||
|
||||
// Prüfen ob Endzählerstand am/nach Vertragsende liegt
|
||||
|
|
@ -103,16 +105,39 @@ export function calculateConsumption(
|
|||
lastReadingDate.setHours(0, 0, 0, 0);
|
||||
contractEndDate.setHours(0, 0, 0, 0);
|
||||
|
||||
// Fall A: Exakter Verbrauch (Endzählerstand am oder nach Vertragsende)
|
||||
// Verbrauch zwischen aufeinanderfolgenden Ständen berechnen
|
||||
// (Erkennt Zählerwechsel: wenn ein Wert sinkt, wird ab dem neuen Stand weitergerechnet)
|
||||
let totalConsumption = 0;
|
||||
let totalConsumptionNt = 0;
|
||||
const hasNt = sorted.some(r => r.valueNt !== undefined && r.valueNt !== null);
|
||||
let effectiveFirstReading = sorted[0];
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
const diff = sorted[i].value - sorted[i - 1].value;
|
||||
if (diff < 0) {
|
||||
totalConsumption = 0;
|
||||
totalConsumptionNt = 0;
|
||||
effectiveFirstReading = sorted[i];
|
||||
} else {
|
||||
totalConsumption += diff;
|
||||
if (hasNt && sorted[i].valueNt != null && sorted[i - 1].valueNt != null) {
|
||||
const diffNt = sorted[i].valueNt! - sorted[i - 1].valueNt!;
|
||||
if (diffNt >= 0) totalConsumptionNt += diffNt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveLastReading = sorted[sorted.length - 1];
|
||||
|
||||
// Fall A: Exakter Verbrauch
|
||||
if (lastReadingDate >= contractEndDate) {
|
||||
const consumption = lastReading.value - firstReading.value;
|
||||
return formatConsumptionResult('exact', consumption, contractType, firstReading, lastReading);
|
||||
const result = formatConsumptionResult('exact', totalConsumption, contractType, effectiveFirstReading, effectiveLastReading);
|
||||
if (hasNt) { result.consumptionHt = totalConsumption; result.consumptionNt = totalConsumptionNt; }
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fall B: Hochrechnung erforderlich
|
||||
const daysBetweenReadings = daysDiff(firstReading.readingDate, lastReading.readingDate);
|
||||
const daysBetweenReadings = daysDiff(effectiveFirstReading.readingDate, effectiveLastReading.readingDate);
|
||||
|
||||
// Mindestens 1 Tag zwischen den Messungen für sinnvolle Hochrechnung
|
||||
if (daysBetweenReadings < 1) {
|
||||
return {
|
||||
type: 'insufficient',
|
||||
|
|
@ -122,17 +147,26 @@ export function calculateConsumption(
|
|||
}
|
||||
|
||||
const totalContractDays = daysDiff(startDate, endDate);
|
||||
const measuredConsumption = lastReading.value - firstReading.value;
|
||||
const projectedConsumption = (measuredConsumption / daysBetweenReadings) * totalContractDays;
|
||||
const projectedConsumption = (totalConsumption / daysBetweenReadings) * totalContractDays;
|
||||
|
||||
return formatConsumptionResult(
|
||||
const result = formatConsumptionResult(
|
||||
'projected',
|
||||
projectedConsumption,
|
||||
contractType,
|
||||
firstReading,
|
||||
lastReading,
|
||||
effectiveFirstReading,
|
||||
effectiveLastReading,
|
||||
endDate
|
||||
);
|
||||
|
||||
if (hasNt) {
|
||||
const projectedNt = (totalConsumptionNt / daysBetweenReadings) * totalContractDays;
|
||||
result.consumptionHt = projectedConsumption;
|
||||
result.consumptionNt = projectedNt;
|
||||
// Gesamt-kWh = HT + NT
|
||||
result.consumptionKwh = projectedConsumption + projectedNt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +207,9 @@ export function calculateCosts(
|
|||
consumptionKwh: number,
|
||||
basePrice?: number,
|
||||
unitPrice?: number,
|
||||
bonus?: number
|
||||
bonus?: number,
|
||||
consumptionNtKwh?: number,
|
||||
unitPriceNt?: number
|
||||
): CostCalculation | null {
|
||||
// Mindestens ein Preis muss vorhanden sein
|
||||
if (basePrice == null && unitPrice == null) {
|
||||
|
|
@ -181,17 +217,78 @@ export function calculateCosts(
|
|||
}
|
||||
|
||||
const annualBaseCost = (basePrice ?? 0) * 12;
|
||||
// Bei HT/NT: consumptionKwh ist nur HT, NT wird separat berechnet
|
||||
const annualConsumptionCost = consumptionKwh * (unitPrice ?? 0);
|
||||
const annualTotalCost = annualBaseCost + annualConsumptionCost;
|
||||
const annualConsumptionCostNt = (consumptionNtKwh ?? 0) * (unitPriceNt ?? 0);
|
||||
const annualTotalCost = annualBaseCost + annualConsumptionCost + annualConsumptionCostNt;
|
||||
const effectiveAnnualCost = annualTotalCost - (bonus ?? 0);
|
||||
const monthlyPayment = effectiveAnnualCost / 12;
|
||||
|
||||
return {
|
||||
annualBaseCost,
|
||||
annualConsumptionCost,
|
||||
annualConsumptionCostNt: annualConsumptionCostNt > 0 ? annualConsumptionCostNt : undefined,
|
||||
annualTotalCost,
|
||||
monthlyPayment,
|
||||
bonus: bonus ?? undefined,
|
||||
effectiveAnnualCost,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet den Verbrauch über mehrere Zähler (Folgezähler).
|
||||
* Pro Zähler wird der Verbrauch einzeln berechnet und dann summiert.
|
||||
*/
|
||||
export function calculateMultiMeterConsumption(
|
||||
contractMeters: ContractMeter[],
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
contractType: 'ELECTRICITY' | 'GAS'
|
||||
): ConsumptionCalculation {
|
||||
if (contractMeters.length === 0) {
|
||||
return { type: 'none', consumptionKwh: 0 };
|
||||
}
|
||||
|
||||
let totalConsumption = 0;
|
||||
let totalConsumptionM3 = 0;
|
||||
let hasExact = true;
|
||||
let hasAny = false;
|
||||
let firstStart: MeterReading | undefined;
|
||||
let lastEnd: MeterReading | undefined;
|
||||
|
||||
for (const cm of contractMeters) {
|
||||
const readings = cm.meter?.readings || [];
|
||||
if (readings.length === 0) continue;
|
||||
|
||||
// Zeitraum für diesen Zähler bestimmen
|
||||
const meterStart = cm.installedAt || startDate;
|
||||
const meterEnd = cm.removedAt || endDate;
|
||||
|
||||
const result = calculateConsumption(readings, meterStart, meterEnd, contractType);
|
||||
|
||||
if (result.type === 'none' || result.type === 'insufficient') continue;
|
||||
|
||||
hasAny = true;
|
||||
if (result.type === 'projected') hasExact = false;
|
||||
|
||||
totalConsumption += result.consumptionKwh;
|
||||
if (result.consumptionM3) totalConsumptionM3 += result.consumptionM3;
|
||||
|
||||
if (!firstStart && result.startReading) firstStart = result.startReading;
|
||||
if (result.endReading) lastEnd = result.endReading;
|
||||
}
|
||||
|
||||
if (!hasAny) {
|
||||
// Fallback: Einzelzähler-Berechnung mit allen Readings
|
||||
const allReadings = contractMeters.flatMap(cm => cm.meter?.readings || []);
|
||||
return calculateConsumption(allReadings, startDate, endDate, contractType);
|
||||
}
|
||||
|
||||
return {
|
||||
type: hasExact ? 'exact' : 'projected',
|
||||
consumptionKwh: totalConsumption,
|
||||
consumptionM3: contractType === 'GAS' ? totalConsumptionM3 : undefined,
|
||||
startReading: firstStart,
|
||||
endReading: lastEnd,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Navigation-History über location.state.
|
||||
* Jeder Link fügt die aktuelle URL zum Stack hinzu.
|
||||
* Der Zurück-Button poppt den letzten Eintrag und gibt den Rest weiter.
|
||||
*/
|
||||
|
||||
export interface NavState {
|
||||
history?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt den state für einen Link - fügt die aktuelle URL zum History-Stack hinzu.
|
||||
*/
|
||||
export function pushHistory(currentPath: string, locationState?: unknown): NavState {
|
||||
const prev = (locationState as NavState)?.history || [];
|
||||
return { history: [...prev, currentPath] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Zurück-URL und den verbleibenden History-Stack zurück.
|
||||
*/
|
||||
export function popHistory(locationState?: unknown, fallback?: string): { to: string; state: NavState } {
|
||||
const history = [...((locationState as NavState)?.history || [])];
|
||||
const to = history.pop() || fallback || '/';
|
||||
return { to, state: { history } };
|
||||
}
|
||||
Loading…
Reference in New Issue