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 getInternetCredentials(req: Request, res: Response): Promise<void>;
|
||||||
export declare function getSipCredentials(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 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>;
|
export declare function snoozeContract(req: Request, res: Response): Promise<void>;
|
||||||
//# sourceMappingURL=contract.controller.d.ts.map
|
//# 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.getInternetCredentials = getInternetCredentials;
|
||||||
exports.getSipCredentials = getSipCredentials;
|
exports.getSipCredentials = getSipCredentials;
|
||||||
exports.getCockpit = getCockpit;
|
exports.getCockpit = getCockpit;
|
||||||
|
exports.addSuccessorMeter = addSuccessorMeter;
|
||||||
|
exports.removeContractMeter = removeContractMeter;
|
||||||
exports.snoozeContract = snoozeContract;
|
exports.snoozeContract = snoozeContract;
|
||||||
const client_1 = require("@prisma/client");
|
const client_1 = require("@prisma/client");
|
||||||
const contractService = __importStar(require("../services/contract.service.js"));
|
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) ====================
|
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||||
async function snoozeContract(req, res) {
|
async function snoozeContract(req, res) {
|
||||||
try {
|
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) {
|
async function addMeterReading(req, res) {
|
||||||
try {
|
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 });
|
res.status(201).json({ success: true, data: reading });
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|
@ -363,7 +370,19 @@ async function addMeterReading(req, res) {
|
||||||
}
|
}
|
||||||
async function updateMeterReading(req, res) {
|
async function updateMeterReading(req, res) {
|
||||||
try {
|
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 });
|
res.json({ success: true, data: reading });
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
|
@ -404,15 +423,18 @@ async function reportMeterReading(req, res) {
|
||||||
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' });
|
res.status(403).json({ success: false, error: 'Kein Zugriff auf diesen Zähler' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reading = await prisma.meterReading.create({
|
const parsedDate = readingDate ? new Date(readingDate) : new Date();
|
||||||
data: {
|
const parsedValue = parseFloat(value);
|
||||||
meterId,
|
// Validierung über den Service (monoton steigend)
|
||||||
value: parseFloat(value),
|
const reading = await customerService.addMeterReading(meterId, {
|
||||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
readingDate: parsedDate,
|
||||||
notes,
|
value: parsedValue,
|
||||||
reportedBy: user.email,
|
notes,
|
||||||
status: 'REPORTED',
|
});
|
||||||
},
|
// 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 });
|
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);
|
router.post('/:id/follow-up', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:create'), contractController.createFollowUp);
|
||||||
// Snooze (Vertrag zurückstellen)
|
// Snooze (Vertrag zurückstellen)
|
||||||
router.patch('/:id/snooze', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:update'), contractController.snoozeContract);
|
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
|
// Get decrypted password
|
||||||
router.get('/:id/password', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContractPassword);
|
router.get('/:id/password', auth_js_1.authenticate, (0, auth_js_1.requirePermission)('contracts:read'), contractController.getContractPassword);
|
||||||
// Get decrypted SimCard PIN/PUK
|
// 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 },
|
where: { id: customerId },
|
||||||
data: { privacyPolicyPath: relativePath },
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -376,6 +385,11 @@ router.delete('/customers/:id/privacy-policy', auth_js_1.authenticate, (0, auth_
|
||||||
where: { id: customerId },
|
where: { id: customerId },
|
||||||
data: { privacyPolicyPath: null },
|
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 });
|
res.json({ success: true });
|
||||||
}
|
}
|
||||||
catch (error) {
|
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,
|
previousProvider: true,
|
||||||
previousContract: {
|
previousContract: {
|
||||||
include: {
|
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 } },
|
internetDetails: { include: { phoneNumbers: true } },
|
||||||
mobileDetails: { include: { simCards: true } },
|
mobileDetails: { include: { simCards: true } },
|
||||||
tvDetails: true,
|
tvDetails: true,
|
||||||
carInsuranceDetails: 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 } },
|
internetDetails: { include: { phoneNumbers: true } },
|
||||||
mobileDetails: { include: { simCards: true } },
|
mobileDetails: { include: { simCards: true } },
|
||||||
tvDetails: true,
|
tvDetails: true,
|
||||||
|
|
@ -264,11 +264,41 @@ async function updateContract(id, data) {
|
||||||
});
|
});
|
||||||
// Update type-specific details
|
// Update type-specific details
|
||||||
if (energyDetails) {
|
if (energyDetails) {
|
||||||
|
const existingEcd = await prisma.energyContractDetails.findUnique({
|
||||||
|
where: { contractId: id },
|
||||||
|
select: { id: true, meterId: true },
|
||||||
|
});
|
||||||
await prisma.energyContractDetails.upsert({
|
await prisma.energyContractDetails.upsert({
|
||||||
where: { contractId: id },
|
where: { contractId: id },
|
||||||
update: energyDetails,
|
update: energyDetails,
|
||||||
create: { contractId: id, ...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) {
|
if (internetDetails) {
|
||||||
const { phoneNumbers, internetPassword, ...internetData } = 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;
|
customerNumber: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
contract?: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
};
|
||||||
providerPortal?: {
|
providerPortal?: {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
portalUrl: 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":"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"}
|
||||||
|
|
@ -682,6 +682,7 @@ async function getReportedMeterReadings() {
|
||||||
contract: {
|
contract: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
portalUsername: true,
|
portalUsername: true,
|
||||||
provider: {
|
provider: {
|
||||||
select: { id: true, name: true, portalUrl: true },
|
select: { id: true, name: true, portalUrl: true },
|
||||||
|
|
@ -717,6 +718,10 @@ async function getReportedMeterReadings() {
|
||||||
customerNumber: r.meter.customer.customerNumber,
|
customerNumber: r.meter.customer.customerNumber,
|
||||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||||
},
|
},
|
||||||
|
contract: contract ? {
|
||||||
|
id: contract.id,
|
||||||
|
contractNumber: contract.contractNumber,
|
||||||
|
} : undefined,
|
||||||
providerPortal: provider?.portalUrl ? {
|
providerPortal: provider?.portalUrl ? {
|
||||||
providerName: provider.name,
|
providerName: provider.name,
|
||||||
portalUrl: provider.portalUrl,
|
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;
|
customerNumber: string;
|
||||||
companyName: string | null;
|
companyName: string | null;
|
||||||
};
|
};
|
||||||
tariff: {
|
contractNumber: string;
|
||||||
id: number;
|
providerName: string | null;
|
||||||
name: string;
|
tariffName: string | null;
|
||||||
} | null;
|
|
||||||
provider: {
|
provider: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
tariffName: string | null;
|
tariff: {
|
||||||
providerName: string | null;
|
id: number;
|
||||||
contractNumber: string;
|
name: string;
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
subtasks: {
|
subtasks: {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
|
||||||
|
|
@ -107,13 +107,14 @@ export declare function getCustomerById(id: number): Promise<({
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -123,6 +124,7 @@ export declare function getCustomerById(id: number): Promise<({
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
type: import(".prisma/client").$Enums.MeterType;
|
type: import(".prisma/client").$Enums.MeterType;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
|
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
})[];
|
})[];
|
||||||
stressfreiEmails: {
|
stressfreiEmails: {
|
||||||
|
|
@ -171,11 +173,8 @@ export declare function getCustomerById(id: number): Promise<({
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
portalPasswordEncrypted: string | null;
|
portalPasswordEncrypted: string | null;
|
||||||
startDate: Date | null;
|
startDate: Date | null;
|
||||||
status: import(".prisma/client").$Enums.ContractStatus;
|
|
||||||
customerNumberAtProvider: string | null;
|
|
||||||
tariffName: string | null;
|
|
||||||
providerName: string | null;
|
|
||||||
contractNumber: string;
|
contractNumber: string;
|
||||||
|
status: import(".prisma/client").$Enums.ContractStatus;
|
||||||
contractCategoryId: number | null;
|
contractCategoryId: number | null;
|
||||||
addressId: number | null;
|
addressId: number | null;
|
||||||
billingAddressId: number | null;
|
billingAddressId: number | null;
|
||||||
|
|
@ -190,6 +189,9 @@ export declare function getCustomerById(id: number): Promise<({
|
||||||
previousContractNumber: string | null;
|
previousContractNumber: string | null;
|
||||||
providerId: number | null;
|
providerId: number | null;
|
||||||
tariffId: number | null;
|
tariffId: number | null;
|
||||||
|
providerName: string | null;
|
||||||
|
tariffName: string | null;
|
||||||
|
customerNumberAtProvider: string | null;
|
||||||
contractNumberAtProvider: string | null;
|
contractNumberAtProvider: string | null;
|
||||||
priceFirst12Months: string | null;
|
priceFirst12Months: string | null;
|
||||||
priceFrom13Months: string | null;
|
priceFrom13Months: string | null;
|
||||||
|
|
@ -573,13 +575,14 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}[];
|
}[];
|
||||||
} & {
|
} & {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -589,6 +592,7 @@ export declare function getCustomerMeters(customerId: number, showInactive?: boo
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
type: import(".prisma/client").$Enums.MeterType;
|
type: import(".prisma/client").$Enums.MeterType;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
|
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
})[]>;
|
})[]>;
|
||||||
export declare function createMeter(customerId: number, data: {
|
export declare function createMeter(customerId: number, data: {
|
||||||
|
|
@ -603,6 +607,7 @@ export declare function createMeter(customerId: number, data: {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
type: import(".prisma/client").$Enums.MeterType;
|
type: import(".prisma/client").$Enums.MeterType;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
|
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
}>;
|
}>;
|
||||||
export declare function updateMeter(id: number, data: {
|
export declare function updateMeter(id: number, data: {
|
||||||
|
|
@ -618,6 +623,7 @@ export declare function updateMeter(id: number, data: {
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
type: import(".prisma/client").$Enums.MeterType;
|
type: import(".prisma/client").$Enums.MeterType;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
|
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
}>;
|
}>;
|
||||||
export declare function deleteMeter(id: number): Promise<{
|
export declare function deleteMeter(id: number): Promise<{
|
||||||
|
|
@ -628,11 +634,13 @@ export declare function deleteMeter(id: number): Promise<{
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
type: import(".prisma/client").$Enums.MeterType;
|
type: import(".prisma/client").$Enums.MeterType;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
|
tariffModel: import(".prisma/client").$Enums.MeterTariffModel;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
}>;
|
}>;
|
||||||
export declare function addMeterReading(meterId: number, data: {
|
export declare function addMeterReading(meterId: number, data: {
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt?: number;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
|
|
@ -640,30 +648,33 @@ export declare function addMeterReading(meterId: number, data: {
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}>;
|
}>;
|
||||||
export declare function getMeterReadings(meterId: number): Promise<{
|
export declare function getMeterReadings(meterId: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}[]>;
|
}[]>;
|
||||||
export declare function updateMeterReading(meterId: number, readingId: number, data: {
|
export declare function updateMeterReading(meterId: number, readingId: number, data: {
|
||||||
readingDate?: Date;
|
readingDate?: Date;
|
||||||
value?: number;
|
value?: number;
|
||||||
|
valueNt?: number | null;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
|
|
@ -671,26 +682,28 @@ export declare function updateMeterReading(meterId: number, readingId: number, d
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}>;
|
}>;
|
||||||
export declare function deleteMeterReading(meterId: number, readingId: number): Promise<{
|
export declare function deleteMeterReading(meterId: number, readingId: number): Promise<{
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
|
meterId: number;
|
||||||
|
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
reportedBy: string | null;
|
reportedBy: string | null;
|
||||||
status: import(".prisma/client").$Enums.MeterReadingStatus;
|
|
||||||
transferredAt: Date | null;
|
transferredAt: Date | null;
|
||||||
transferredBy: string | null;
|
transferredBy: string | null;
|
||||||
meterId: number;
|
|
||||||
}>;
|
}>;
|
||||||
export declare function updatePortalSettings(customerId: number, data: {
|
export declare function updatePortalSettings(customerId: number, data: {
|
||||||
portalEnabled?: boolean;
|
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) {
|
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 } });
|
return prisma.meter.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
async function addMeterReading(meterId, data) {
|
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({
|
return prisma.meterReading.create({
|
||||||
data: {
|
data: {
|
||||||
meterId,
|
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) {
|
if (!reading) {
|
||||||
throw new Error('Zählerstand nicht gefunden');
|
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({
|
return prisma.meterReading.update({
|
||||||
where: { id: readingId },
|
where: { id: readingId },
|
||||||
data,
|
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) {
|
async function deleteMeterReading(meterId, readingId) {
|
||||||
// Verify the reading belongs to the meter
|
// Verify the reading belongs to the meter
|
||||||
const reading = await prisma.meterReading.findFirst({
|
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',
|
customerId: 'customerId',
|
||||||
meterNumber: 'meterNumber',
|
meterNumber: 'meterNumber',
|
||||||
type: 'type',
|
type: 'type',
|
||||||
|
tariffModel: 'tariffModel',
|
||||||
location: 'location',
|
location: 'location',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|
@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
||||||
meterId: 'meterId',
|
meterId: 'meterId',
|
||||||
readingDate: 'readingDate',
|
readingDate: 'readingDate',
|
||||||
value: 'value',
|
value: 'value',
|
||||||
|
valueNt: 'valueNt',
|
||||||
unit: 'unit',
|
unit: 'unit',
|
||||||
notes: 'notes',
|
notes: 'notes',
|
||||||
reportedBy: 'reportedBy',
|
reportedBy: 'reportedBy',
|
||||||
|
|
@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
||||||
annualConsumptionKwh: 'annualConsumptionKwh',
|
annualConsumptionKwh: 'annualConsumptionKwh',
|
||||||
basePrice: 'basePrice',
|
basePrice: 'basePrice',
|
||||||
unitPrice: 'unitPrice',
|
unitPrice: 'unitPrice',
|
||||||
|
unitPriceNt: 'unitPriceNt',
|
||||||
bonus: 'bonus',
|
bonus: 'bonus',
|
||||||
previousProviderName: 'previousProviderName',
|
previousProviderName: 'previousProviderName',
|
||||||
previousCustomerNumber: 'previousCustomerNumber'
|
previousCustomerNumber: 'previousCustomerNumber'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ContractMeterScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
energyContractDetailsId: 'energyContractDetailsId',
|
||||||
|
meterId: 'meterId',
|
||||||
|
position: 'position',
|
||||||
|
installedAt: 'installedAt',
|
||||||
|
removedAt: 'removedAt',
|
||||||
|
finalReading: 'finalReading',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
energyContractDetailsId: 'energyContractDetailsId',
|
energyContractDetailsId: 'energyContractDetailsId',
|
||||||
|
|
@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
|
||||||
GAS: 'GAS'
|
GAS: 'GAS'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
|
||||||
|
SINGLE: 'SINGLE',
|
||||||
|
DUAL: 'DUAL'
|
||||||
|
};
|
||||||
|
|
||||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||||
RECORDED: 'RECORDED',
|
RECORDED: 'RECORDED',
|
||||||
REPORTED: 'REPORTED',
|
REPORTED: 'REPORTED',
|
||||||
|
|
@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
|
||||||
ContractTask: 'ContractTask',
|
ContractTask: 'ContractTask',
|
||||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||||
EnergyContractDetails: 'EnergyContractDetails',
|
EnergyContractDetails: 'EnergyContractDetails',
|
||||||
|
ContractMeter: 'ContractMeter',
|
||||||
Invoice: 'Invoice',
|
Invoice: 'Invoice',
|
||||||
InternetContractDetails: 'InternetContractDetails',
|
InternetContractDetails: 'InternetContractDetails',
|
||||||
PhoneNumber: 'PhoneNumber',
|
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",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "index-browser.js",
|
"browser": "index-browser.js",
|
||||||
|
|
|
||||||
|
|
@ -411,18 +411,25 @@ enum MeterType {
|
||||||
GAS
|
GAS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MeterTariffModel {
|
||||||
|
SINGLE // Eintarifzähler (Standard)
|
||||||
|
DUAL // Zweitarifzähler (HT/NT)
|
||||||
|
}
|
||||||
|
|
||||||
model Meter {
|
model Meter {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customerId Int
|
customerId Int
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
meterNumber String
|
meterNumber String
|
||||||
type MeterType
|
type MeterType
|
||||||
location String?
|
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||||
isActive Boolean @default(true)
|
location String?
|
||||||
readings MeterReading[]
|
isActive Boolean @default(true)
|
||||||
energyDetails EnergyContractDetails[]
|
readings MeterReading[]
|
||||||
createdAt DateTime @default(now())
|
energyDetails EnergyContractDetails[]
|
||||||
updatedAt DateTime @updatedAt
|
contractMeters ContractMeter[] @relation("ContractMeters")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model MeterReading {
|
model MeterReading {
|
||||||
|
|
@ -430,7 +437,8 @@ model MeterReading {
|
||||||
meterId Int
|
meterId Int
|
||||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||||
readingDate DateTime
|
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")
|
unit String @default("kWh")
|
||||||
notes String?
|
notes String?
|
||||||
// Meldung & Übertragung
|
// Meldung & Übertragung
|
||||||
|
|
@ -709,20 +717,38 @@ enum InvoiceType {
|
||||||
}
|
}
|
||||||
|
|
||||||
model EnergyContractDetails {
|
model EnergyContractDetails {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
contractId Int @unique
|
contractId Int @unique
|
||||||
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
|
||||||
meterId Int?
|
meterId Int?
|
||||||
meter Meter? @relation(fields: [meterId], references: [id])
|
meter Meter? @relation(fields: [meterId], references: [id])
|
||||||
maloId String? // Marktlokations-ID
|
maloId String? // Marktlokations-ID
|
||||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||||
basePrice Float? // €/Monat
|
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?
|
bonus Float?
|
||||||
previousProviderName String?
|
previousProviderName String?
|
||||||
previousCustomerNumber String?
|
previousCustomerNumber String?
|
||||||
invoices Invoice[] // Rechnungen
|
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 {
|
model Invoice {
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,7 @@ exports.Prisma.MeterScalarFieldEnum = {
|
||||||
customerId: 'customerId',
|
customerId: 'customerId',
|
||||||
meterNumber: 'meterNumber',
|
meterNumber: 'meterNumber',
|
||||||
type: 'type',
|
type: 'type',
|
||||||
|
tariffModel: 'tariffModel',
|
||||||
location: 'location',
|
location: 'location',
|
||||||
isActive: 'isActive',
|
isActive: 'isActive',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
|
|
@ -372,6 +373,7 @@ exports.Prisma.MeterReadingScalarFieldEnum = {
|
||||||
meterId: 'meterId',
|
meterId: 'meterId',
|
||||||
readingDate: 'readingDate',
|
readingDate: 'readingDate',
|
||||||
value: 'value',
|
value: 'value',
|
||||||
|
valueNt: 'valueNt',
|
||||||
unit: 'unit',
|
unit: 'unit',
|
||||||
notes: 'notes',
|
notes: 'notes',
|
||||||
reportedBy: 'reportedBy',
|
reportedBy: 'reportedBy',
|
||||||
|
|
@ -529,11 +531,23 @@ exports.Prisma.EnergyContractDetailsScalarFieldEnum = {
|
||||||
annualConsumptionKwh: 'annualConsumptionKwh',
|
annualConsumptionKwh: 'annualConsumptionKwh',
|
||||||
basePrice: 'basePrice',
|
basePrice: 'basePrice',
|
||||||
unitPrice: 'unitPrice',
|
unitPrice: 'unitPrice',
|
||||||
|
unitPriceNt: 'unitPriceNt',
|
||||||
bonus: 'bonus',
|
bonus: 'bonus',
|
||||||
previousProviderName: 'previousProviderName',
|
previousProviderName: 'previousProviderName',
|
||||||
previousCustomerNumber: 'previousCustomerNumber'
|
previousCustomerNumber: 'previousCustomerNumber'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ContractMeterScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
energyContractDetailsId: 'energyContractDetailsId',
|
||||||
|
meterId: 'meterId',
|
||||||
|
position: 'position',
|
||||||
|
installedAt: 'installedAt',
|
||||||
|
removedAt: 'removedAt',
|
||||||
|
finalReading: 'finalReading',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.InvoiceScalarFieldEnum = {
|
exports.Prisma.InvoiceScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
energyContractDetailsId: 'energyContractDetailsId',
|
energyContractDetailsId: 'energyContractDetailsId',
|
||||||
|
|
@ -742,6 +756,11 @@ exports.MeterType = exports.$Enums.MeterType = {
|
||||||
GAS: 'GAS'
|
GAS: 'GAS'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.MeterTariffModel = exports.$Enums.MeterTariffModel = {
|
||||||
|
SINGLE: 'SINGLE',
|
||||||
|
DUAL: 'DUAL'
|
||||||
|
};
|
||||||
|
|
||||||
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
exports.MeterReadingStatus = exports.$Enums.MeterReadingStatus = {
|
||||||
RECORDED: 'RECORDED',
|
RECORDED: 'RECORDED',
|
||||||
REPORTED: 'REPORTED',
|
REPORTED: 'REPORTED',
|
||||||
|
|
@ -855,6 +874,7 @@ exports.Prisma.ModelName = {
|
||||||
ContractTask: 'ContractTask',
|
ContractTask: 'ContractTask',
|
||||||
ContractTaskSubtask: 'ContractTaskSubtask',
|
ContractTaskSubtask: 'ContractTaskSubtask',
|
||||||
EnergyContractDetails: 'EnergyContractDetails',
|
EnergyContractDetails: 'EnergyContractDetails',
|
||||||
|
ContractMeter: 'ContractMeter',
|
||||||
Invoice: 'Invoice',
|
Invoice: 'Invoice',
|
||||||
InternetContractDetails: 'InternetContractDetails',
|
InternetContractDetails: 'InternetContractDetails',
|
||||||
PhoneNumber: 'PhoneNumber',
|
PhoneNumber: 'PhoneNumber',
|
||||||
|
|
|
||||||
|
|
@ -411,17 +411,24 @@ enum MeterType {
|
||||||
GAS
|
GAS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum MeterTariffModel {
|
||||||
|
SINGLE // Eintarifzähler (Standard)
|
||||||
|
DUAL // Zweitarifzähler (HT/NT)
|
||||||
|
}
|
||||||
|
|
||||||
model Meter {
|
model Meter {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
customerId Int
|
customerId Int
|
||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
meterNumber String
|
meterNumber String
|
||||||
type MeterType
|
type MeterType
|
||||||
|
tariffModel MeterTariffModel @default(SINGLE) // Eintarif oder Zweitarif (HT/NT)
|
||||||
location String?
|
location String?
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
readings MeterReading[]
|
readings MeterReading[]
|
||||||
energyDetails EnergyContractDetails[]
|
energyDetails EnergyContractDetails[]
|
||||||
createdAt DateTime @default(now())
|
contractMeters ContractMeter[] @relation("ContractMeters")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -430,7 +437,8 @@ model MeterReading {
|
||||||
meterId Int
|
meterId Int
|
||||||
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
meter Meter @relation(fields: [meterId], references: [id], onDelete: Cascade)
|
||||||
readingDate DateTime
|
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")
|
unit String @default("kWh")
|
||||||
notes String?
|
notes String?
|
||||||
// Meldung & Übertragung
|
// Meldung & Übertragung
|
||||||
|
|
@ -718,11 +726,29 @@ model EnergyContractDetails {
|
||||||
annualConsumption Float? // kWh für Strom, m³ für Gas
|
annualConsumption Float? // kWh für Strom, m³ für Gas
|
||||||
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
annualConsumptionKwh Float? // kWh für Gas (zusätzlich zu m³)
|
||||||
basePrice Float? // €/Monat
|
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?
|
bonus Float?
|
||||||
previousProviderName String?
|
previousProviderName String?
|
||||||
previousCustomerNumber String?
|
previousCustomerNumber String?
|
||||||
invoices Invoice[] // Rechnungen
|
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 {
|
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) ====================
|
// ==================== SNOOZE (VERTRAG ZURÜCKSTELLEN) ====================
|
||||||
|
|
||||||
export async function snoozeContract(req: Request, res: Response): Promise<void> {
|
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> {
|
export async function addMeterReading(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
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);
|
res.status(201).json({ success: true, data: reading } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
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> {
|
export async function updateMeterReading(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
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(
|
const reading = await customerService.updateMeterReading(
|
||||||
parseInt(req.params.meterId),
|
parseInt(req.params.meterId),
|
||||||
parseInt(req.params.readingId),
|
parseInt(req.params.readingId),
|
||||||
req.body
|
updateData as any
|
||||||
);
|
);
|
||||||
res.json({ success: true, data: reading } as ApiResponse);
|
res.json({ success: true, data: reading } as ApiResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -358,15 +373,20 @@ export async function reportMeterReading(req: AuthRequest, res: Response): Promi
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const reading = await prisma.meterReading.create({
|
const parsedDate = readingDate ? new Date(readingDate) : new Date();
|
||||||
data: {
|
const parsedValue = parseFloat(value);
|
||||||
meterId,
|
|
||||||
value: parseFloat(value),
|
// Validierung über den Service (monoton steigend)
|
||||||
readingDate: readingDate ? new Date(readingDate) : new Date(),
|
const reading = await customerService.addMeterReading(meterId, {
|
||||||
notes,
|
readingDate: parsedDate,
|
||||||
reportedBy: user.email,
|
value: parsedValue,
|
||||||
status: 'REPORTED',
|
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);
|
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/contract-durations': { type: 'ContractDuration', extractId: (req) => req.params.id },
|
||||||
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
'/api/settings': { type: 'AppSetting', extractId: (req) => req.params.key },
|
||||||
'/api/email-providers': { type: 'EmailProviderConfig', extractId: (req) => req.params.id },
|
'/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/auth': { type: 'Authentication' },
|
||||||
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
'/api/audit-logs': { type: 'AuditLog', extractId: (req) => req.params.id },
|
||||||
'/api/gdpr': { type: 'GDPR' },
|
'/api/gdpr': { type: 'GDPR' },
|
||||||
|
|
@ -116,6 +119,240 @@ function getClientIp(req: AuthRequest): string {
|
||||||
return req.socket.remoteAddress || 'unknown';
|
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
|
* 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)
|
// Audit-Kontext abrufen (enthält Before/After-Werte von Prisma Middleware)
|
||||||
const auditContext = getAuditContext();
|
const auditContext = getAuditContext();
|
||||||
|
|
||||||
// Label für bessere Lesbarkeit generieren
|
// Menschenlesbares Label generieren
|
||||||
let resourceLabel: string | undefined;
|
const resourceLabel = generateHumanLabel(action, mapping.type, req, responseBody);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await createAuditLog({
|
await createAuditLog({
|
||||||
userId: req.user?.userId,
|
userId: req.user?.userId,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ router.post('/:id/follow-up', authenticate, requirePermission('contracts:create'
|
||||||
// Snooze (Vertrag zurückstellen)
|
// Snooze (Vertrag zurückstellen)
|
||||||
router.patch('/:id/snooze', authenticate, requirePermission('contracts:update'), contractController.snoozeContract);
|
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
|
// Get decrypted password
|
||||||
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
router.get('/:id/password', authenticate, requirePermission('contracts:read'), contractController.getContractPassword);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -440,6 +440,16 @@ router.post(
|
||||||
data: { privacyPolicyPath: relativePath },
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
|
|
@ -488,6 +498,12 @@ router.delete(
|
||||||
data: { privacyPolicyPath: null },
|
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 });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete error:', error);
|
console.error('Delete error:', error);
|
||||||
|
|
|
||||||
|
|
@ -146,13 +146,35 @@ export async function updateAuthorizationDocument(
|
||||||
* Vollmacht-Dokument löschen
|
* Vollmacht-Dokument löschen
|
||||||
*/
|
*/
|
||||||
export async function deleteAuthorizationDocument(customerId: number, representativeId: number) {
|
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({
|
return prisma.representativeAuthorization.update({
|
||||||
where: {
|
where: { customerId_representativeId: { customerId, representativeId } },
|
||||||
customerId_representativeId: { customerId, representativeId },
|
data: withdrawData,
|
||||||
},
|
|
||||||
data: {
|
|
||||||
documentPath: null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,14 +125,14 @@ export async function getContractById(id: number, decryptPassword = false) {
|
||||||
previousProvider: true,
|
previousProvider: true,
|
||||||
previousContract: {
|
previousContract: {
|
||||||
include: {
|
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 } },
|
internetDetails: { include: { phoneNumbers: true } },
|
||||||
mobileDetails: { include: { simCards: true } },
|
mobileDetails: { include: { simCards: true } },
|
||||||
tvDetails: true,
|
tvDetails: true,
|
||||||
carInsuranceDetails: 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 } },
|
internetDetails: { include: { phoneNumbers: true } },
|
||||||
mobileDetails: { include: { simCards: true } },
|
mobileDetails: { include: { simCards: true } },
|
||||||
tvDetails: true,
|
tvDetails: true,
|
||||||
|
|
@ -403,11 +403,45 @@ export async function updateContract(
|
||||||
|
|
||||||
// Update type-specific details
|
// Update type-specific details
|
||||||
if (energyDetails) {
|
if (energyDetails) {
|
||||||
|
const existingEcd = await prisma.energyContractDetails.findUnique({
|
||||||
|
where: { contractId: id },
|
||||||
|
select: { id: true, meterId: true },
|
||||||
|
});
|
||||||
|
|
||||||
await prisma.energyContractDetails.upsert({
|
await prisma.energyContractDetails.upsert({
|
||||||
where: { contractId: id },
|
where: { contractId: id },
|
||||||
update: energyDetails,
|
update: energyDetails,
|
||||||
create: { contractId: id, ...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) {
|
if (internetDetails) {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,11 @@ export interface ReportedMeterReading {
|
||||||
customerNumber: string;
|
customerNumber: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
// Zugehöriger Vertrag
|
||||||
|
contract?: {
|
||||||
|
id: number;
|
||||||
|
contractNumber: string;
|
||||||
|
};
|
||||||
// Anbieter-Info für Quick-Login
|
// Anbieter-Info für Quick-Login
|
||||||
providerPortal?: {
|
providerPortal?: {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
|
|
@ -810,6 +815,7 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||||
contract: {
|
contract: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
contractNumber: true,
|
||||||
portalUsername: true,
|
portalUsername: true,
|
||||||
provider: {
|
provider: {
|
||||||
select: { id: true, name: true, portalUrl: true },
|
select: { id: true, name: true, portalUrl: true },
|
||||||
|
|
@ -847,6 +853,10 @@ async function getReportedMeterReadings(): Promise<ReportedMeterReading[]> {
|
||||||
customerNumber: r.meter.customer.customerNumber,
|
customerNumber: r.meter.customer.customerNumber,
|
||||||
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
name: `${r.meter.customer.firstName} ${r.meter.customer.lastName}`,
|
||||||
},
|
},
|
||||||
|
contract: contract ? {
|
||||||
|
id: contract.id,
|
||||||
|
contractNumber: contract.contractNumber,
|
||||||
|
} : undefined,
|
||||||
providerPortal: provider?.portalUrl ? {
|
providerPortal: provider?.portalUrl ? {
|
||||||
providerName: provider.name,
|
providerName: provider.name,
|
||||||
portalUrl: provider.portalUrl,
|
portalUrl: provider.portalUrl,
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,30 @@ export async function updateMeter(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteMeter(id: number) {
|
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 } });
|
return prisma.meter.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -451,14 +475,25 @@ export async function addMeterReading(
|
||||||
data: {
|
data: {
|
||||||
readingDate: Date;
|
readingDate: Date;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt?: number;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
notes?: 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({
|
return prisma.meterReading.create({
|
||||||
data: {
|
data: {
|
||||||
meterId,
|
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: {
|
data: {
|
||||||
readingDate?: Date;
|
readingDate?: Date;
|
||||||
value?: number;
|
value?: number;
|
||||||
|
valueNt?: number | null;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -489,12 +525,65 @@ export async function updateMeterReading(
|
||||||
throw new Error('Zählerstand nicht gefunden');
|
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({
|
return prisma.meterReading.update({
|
||||||
where: { id: readingId },
|
where: { id: readingId },
|
||||||
data,
|
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) {
|
export async function deleteMeterReading(meterId: number, readingId: number) {
|
||||||
// Verify the reading belongs to the meter
|
// Verify the reading belongs to the meter
|
||||||
const reading = await prisma.meterReading.findFirst({
|
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 { useState, useMemo, useEffect, useRef } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import { pushHistory } from '../../utils/navigation';
|
||||||
import { contractApi, meterApi } from '../../services/api';
|
import { contractApi, meterApi } from '../../services/api';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
import Badge from '../../components/ui/Badge';
|
import Badge from '../../components/ui/Badge';
|
||||||
|
|
@ -282,7 +283,7 @@ export default function ContractCockpit() {
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Link
|
<Link
|
||||||
to={`/contracts/${contract.id}`}
|
to={`/contracts/${contract.id}`}
|
||||||
state={{ from: '/contracts/cockpit' }}
|
state={pushHistory('/contracts/cockpit')}
|
||||||
className="font-medium hover:underline"
|
className="font-medium hover:underline"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
@ -399,7 +400,7 @@ export default function ContractCockpit() {
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/contracts/${contract.id}`}
|
to={`/contracts/${contract.id}`}
|
||||||
state={{ from: '/contracts/cockpit' }}
|
state={pushHistory('/contracts/cockpit')}
|
||||||
className="p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
className="p-2 hover:bg-white hover:bg-opacity-50 rounded"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
title="Zum Vertrag"
|
title="Zum Vertrag"
|
||||||
|
|
@ -610,6 +611,15 @@ export default function ContractCockpit() {
|
||||||
{reading.customer.name}
|
{reading.customer.name}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-xs text-gray-500">({reading.customer.customerNumber})</span>
|
<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>
|
</div>
|
||||||
<p className="text-xs text-gray-600">
|
<p className="text-xs text-gray-600">
|
||||||
Zähler {reading.meter.meterNumber} – <strong>{reading.value} {reading.unit}</strong> am{' '}
|
Zähler {reading.meter.meterNumber} – <strong>{reading.value} {reading.unit}</strong> am{' '}
|
||||||
|
|
@ -635,6 +645,7 @@ export default function ContractCockpit() {
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={async () => {
|
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);
|
await meterApi.markTransferred(reading.meter.id, reading.id);
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract-cockpit'] });
|
queryClient.invalidateQueries({ queryKey: ['contract-cockpit'] });
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { pushHistory, popHistory } from '../../utils/navigation';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi } from '../../services/api';
|
import { contractApi, uploadApi, meterApi, contractTaskApi, appSettingsApi, gdprApi } from '../../services/api';
|
||||||
import { ContractEmailsSection } from '../../components/email';
|
import { ContractEmailsSection } from '../../components/email';
|
||||||
|
|
@ -13,9 +14,9 @@ import Input from '../../components/ui/Input';
|
||||||
import Modal from '../../components/ui/Modal';
|
import Modal from '../../components/ui/Modal';
|
||||||
import FileUpload from '../../components/ui/FileUpload';
|
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 { 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 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> = {
|
const typeLabels: Record<ContractType, string> = {
|
||||||
ELECTRICITY: 'Strom',
|
ELECTRICITY: 'Strom',
|
||||||
|
|
@ -197,16 +198,19 @@ function SimCardDisplay({ simCard }: { simCard: SimCard }) {
|
||||||
function MeterReadingsSection({
|
function MeterReadingsSection({
|
||||||
meterId,
|
meterId,
|
||||||
meterType,
|
meterType,
|
||||||
|
tariffModel,
|
||||||
readings,
|
readings,
|
||||||
contractId,
|
|
||||||
canEdit,
|
canEdit,
|
||||||
|
label,
|
||||||
}: {
|
}: {
|
||||||
meterId: number;
|
meterId: number;
|
||||||
meterType: 'ELECTRICITY' | 'GAS';
|
meterType: 'ELECTRICITY' | 'GAS';
|
||||||
|
tariffModel?: string;
|
||||||
readings: MeterReading[];
|
readings: MeterReading[];
|
||||||
contractId: number;
|
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
|
label?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const isDualTariff = tariffModel === 'DUAL';
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
const [editingReading, setEditingReading] = useState<MeterReading | null>(null);
|
const [editingReading, setEditingReading] = useState<MeterReading | null>(null);
|
||||||
|
|
@ -215,7 +219,7 @@ function MeterReadingsSection({
|
||||||
const deleteReadingMutation = useMutation({
|
const deleteReadingMutation = useMutation({
|
||||||
mutationFn: (readingId: number) => meterApi.deleteReading(meterId, readingId),
|
mutationFn: (readingId: number) => meterApi.deleteReading(meterId, readingId),
|
||||||
onSuccess: () => {
|
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 justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Gauge className="w-4 h-4 text-gray-500" />
|
<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>
|
<Badge variant="default">{readings.length}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -274,8 +278,12 @@ function MeterReadingsSection({
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-mono flex items-center gap-1">
|
<span className="font-mono flex items-center gap-1">
|
||||||
{reading.value.toLocaleString('de-DE')} {reading.unit}
|
{isDualTariff && reading.valueNt != null ? (
|
||||||
<CopyButton value={reading.value.toString()} title="Nur Wert kopieren" />
|
<>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>
|
</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
||||||
|
|
@ -324,9 +332,9 @@ function MeterReadingsSection({
|
||||||
setEditingReading(null);
|
setEditingReading(null);
|
||||||
}}
|
}}
|
||||||
meterId={meterId}
|
meterId={meterId}
|
||||||
contractId={contractId}
|
|
||||||
reading={editingReading}
|
reading={editingReading}
|
||||||
defaultUnit={defaultUnit}
|
defaultUnit={defaultUnit}
|
||||||
|
tariffModel={tariffModel}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -334,22 +342,135 @@ function MeterReadingsSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Meter Reading Modal Component
|
// 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({
|
function MeterReadingModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
meterId,
|
meterId,
|
||||||
contractId,
|
|
||||||
reading,
|
reading,
|
||||||
defaultUnit,
|
defaultUnit,
|
||||||
|
tariffModel,
|
||||||
}: {
|
}: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
meterId: number;
|
meterId: number;
|
||||||
contractId: number;
|
|
||||||
reading?: MeterReading | null;
|
reading?: MeterReading | null;
|
||||||
defaultUnit: string;
|
defaultUnit: string;
|
||||||
|
tariffModel?: string;
|
||||||
}) {
|
}) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const isDualTariff = tariffModel === 'DUAL';
|
||||||
const isEditing = !!reading;
|
const isEditing = !!reading;
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
|
|
@ -357,33 +478,47 @@ function MeterReadingModal({
|
||||||
? new Date(reading.readingDate).toISOString().split('T')[0]
|
? new Date(reading.readingDate).toISOString().split('T')[0]
|
||||||
: new Date().toISOString().split('T')[0],
|
: new Date().toISOString().split('T')[0],
|
||||||
value: reading?.value?.toString() || '',
|
value: reading?.value?.toString() || '',
|
||||||
|
valueNt: reading?.valueNt?.toString() || '',
|
||||||
notes: reading?.notes || '',
|
notes: reading?.notes || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: Partial<MeterReading>) => meterApi.addReading(meterId, data),
|
mutationFn: (data: Partial<MeterReading>) => meterApi.addReading(meterId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||||
|
setError(null);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: Partial<MeterReading>) => meterApi.updateReading(meterId, reading!.id, data),
|
mutationFn: (data: Partial<MeterReading>) => meterApi.updateReading(meterId, reading!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract', contractId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['contract'] });
|
||||||
|
setError(null);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data: Record<string, unknown> = {
|
||||||
readingDate: new Date(formData.readingDate),
|
readingDate: new Date(formData.readingDate),
|
||||||
value: parseFloat(formData.value),
|
value: parseFloat(formData.value),
|
||||||
unit: defaultUnit,
|
unit: defaultUnit,
|
||||||
notes: formData.notes || undefined,
|
notes: formData.notes || undefined,
|
||||||
};
|
};
|
||||||
|
if (isDualTariff && formData.valueNt) {
|
||||||
|
data.valueNt = parseFloat(formData.valueNt);
|
||||||
|
}
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
updateMutation.mutate(data as unknown as Partial<MeterReading>);
|
updateMutation.mutate(data as unknown as Partial<MeterReading>);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -404,10 +539,10 @@ function MeterReadingModal({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className={`grid ${isDualTariff ? 'grid-cols-2' : 'grid-cols-3'} gap-4`}>
|
||||||
<div className="col-span-2">
|
<div className={isDualTariff ? '' : 'col-span-2'}>
|
||||||
<Input
|
<Input
|
||||||
label="Zählerstand"
|
label={isDualTariff ? 'HT-Stand (Hochtarif)' : 'Zählerstand'}
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.value}
|
value={formData.value}
|
||||||
|
|
@ -415,12 +550,26 @@ function MeterReadingModal({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{isDualTariff && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
<div>
|
||||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
<Input
|
||||||
{defaultUnit}
|
label="NT-Stand (Niedertarif)"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.valueNt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, valueNt: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -429,6 +578,12 @@ function MeterReadingModal({
|
||||||
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
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">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<Button type="button" variant="secondary" onClick={onClose}>
|
<Button type="button" variant="secondary" onClick={onClose}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
|
|
@ -450,24 +605,67 @@ function EnergyConsumptionCalculation({
|
||||||
endDate,
|
endDate,
|
||||||
basePrice,
|
basePrice,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
|
unitPriceNt,
|
||||||
bonus,
|
bonus,
|
||||||
|
hasMeter,
|
||||||
|
contractMeters,
|
||||||
}: {
|
}: {
|
||||||
contractType: 'ELECTRICITY' | 'GAS';
|
contractType: 'ELECTRICITY' | 'GAS';
|
||||||
readings: MeterReading[];
|
readings: MeterReading[];
|
||||||
startDate: string;
|
startDate?: string;
|
||||||
endDate: string;
|
endDate?: string;
|
||||||
basePrice?: number;
|
basePrice?: number;
|
||||||
unitPrice?: number;
|
unitPrice?: number;
|
||||||
bonus?: number;
|
bonus?: number;
|
||||||
|
unitPriceNt?: number;
|
||||||
|
hasMeter?: boolean;
|
||||||
|
contractMeters?: ContractMeter[];
|
||||||
}) {
|
}) {
|
||||||
// Berechnung durchführen
|
// Fehlende Felder ermitteln
|
||||||
const consumption = calculateConsumption(readings, startDate, endDate, contractType);
|
const missingFields: string[] = [];
|
||||||
const costs = consumption.consumptionKwh > 0
|
if (!startDate) missingFields.push('Vertragsbeginn');
|
||||||
? calculateCosts(consumption.consumptionKwh, basePrice, unitPrice, bonus)
|
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;
|
: null;
|
||||||
|
|
||||||
// Nichts anzeigen wenn keine Daten
|
const canCalculate = consumption && (consumption.type === 'exact' || consumption.type === 'projected');
|
||||||
if (consumption.type === 'none') return null;
|
|
||||||
|
// 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) =>
|
const formatNumber = (num: number, decimals: number = 2) =>
|
||||||
num.toLocaleString('de-DE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
num.toLocaleString('de-DE', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
|
||||||
|
|
@ -481,40 +679,61 @@ function EnergyConsumptionCalculation({
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<Calculator className="w-4 h-4 text-gray-500" />
|
<Calculator className="w-4 h-4 text-gray-500" />
|
||||||
<h4 className="text-sm font-medium text-gray-700">Verbrauch & Kosten</h4>
|
<h4 className="text-sm font-medium text-gray-700">Verbrauch & Kosten</h4>
|
||||||
{consumption.type === 'exact' && (
|
{canCalculate && consumption!.type === 'exact' && (
|
||||||
<Badge variant="success">Exakt</Badge>
|
<Badge variant="success">Exakt</Badge>
|
||||||
)}
|
)}
|
||||||
{consumption.type === 'projected' && (
|
{canCalculate && consumption!.type === 'projected' && (
|
||||||
<Badge variant="warning">Hochrechnung</Badge>
|
<Badge variant="warning">Hochrechnung</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Fall C: Unzureichende Daten */}
|
{/* Fehlende Daten */}
|
||||||
{consumption.type === 'insufficient' ? (
|
{!canCalculate ? (
|
||||||
<p className="text-sm text-gray-500 italic">{consumption.message}</p>
|
<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">
|
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
|
||||||
{/* Verbrauchsanzeige */}
|
{/* Verbrauchsanzeige */}
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-sm font-medium text-gray-600 mb-2">
|
<h5 className="text-sm font-medium text-gray-600 mb-2">
|
||||||
Berechneter Verbrauch
|
Berechneter Verbrauch
|
||||||
{consumption.type === 'projected' && ' (hochgerechnet)'}
|
{consumption!.type === 'projected' && ' (hochgerechnet)'}
|
||||||
</h5>
|
</h5>
|
||||||
<div className="text-lg font-semibold text-gray-900">
|
<div className="text-lg font-semibold text-gray-900">
|
||||||
{contractType === 'GAS' ? (
|
{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">
|
<span className="text-gray-500 text-sm ml-2">
|
||||||
= {formatNumber(consumption.consumptionKwh)} kWh
|
= {formatNumber(consumption!.consumptionKwh)} kWh
|
||||||
</span>
|
</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>
|
</div>
|
||||||
{consumption.startReading && consumption.endReading && (
|
{consumption!.startReading && consumption!.endReading && (
|
||||||
<p className="text-xs text-gray-400 mt-1">
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -533,15 +752,24 @@ function EnergyConsumptionCalculation({
|
||||||
<span className="font-mono">{formatNumber(costs.annualBaseCost)} €</span>
|
<span className="font-mono">{formatNumber(costs.annualBaseCost)} €</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Arbeitspreis */}
|
{/* Arbeitspreis HT */}
|
||||||
{unitPrice != null && unitPrice > 0 && (
|
{unitPrice != null && unitPrice > 0 && (
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-600">
|
<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>
|
||||||
<span className="font-mono">{formatNumber(costs.annualConsumptionCost)} €</span>
|
<span className="font-mono">{formatNumber(costs.annualConsumptionCost)} €</span>
|
||||||
</div>
|
</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 */}
|
{/* Trennlinie */}
|
||||||
<div className="border-t border-gray-300 pt-2">
|
<div className="border-t border-gray-300 pt-2">
|
||||||
<div className="flex justify-between font-medium">
|
<div className="flex justify-between font-medium">
|
||||||
|
|
@ -1212,7 +1440,7 @@ export default function ContractDetail() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const backTo = (location.state as any)?.from as string | undefined;
|
const currentPath = `/contracts/${id}`;
|
||||||
const { hasPermission, isCustomer, isCustomerPortal } = useAuth();
|
const { hasPermission, isCustomer, isCustomerPortal } = useAuth();
|
||||||
const contractId = parseInt(id!);
|
const contractId = parseInt(id!);
|
||||||
|
|
||||||
|
|
@ -1424,13 +1652,15 @@ export default function ContractDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const c = data.data;
|
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
|
// Consent-Sperrung: Vertrag nicht anzeigen wenn Kunde keine Einwilligung hat
|
||||||
if (!hasConsentApproval) {
|
if (!hasConsentApproval) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<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" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-2xl font-bold">Vertrag {c.contractNumber}</h1>
|
<h1 className="text-2xl font-bold">Vertrag {c.contractNumber}</h1>
|
||||||
|
|
@ -1465,7 +1695,7 @@ export default function ContractDetail() {
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -1499,7 +1729,7 @@ export default function ContractDetail() {
|
||||||
{c.customer && (
|
{c.customer && (
|
||||||
<p className="text-gray-500 ml-10">
|
<p className="text-gray-500 ml-10">
|
||||||
Kunde:{' '}
|
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}`}
|
{c.customer.companyName || `${c.customer.firstName} ${c.customer.lastName}`}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -1534,7 +1764,7 @@ export default function ContractDetail() {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{hasPermission('contracts:update') && (
|
{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">
|
<Button variant="secondary">
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
|
|
@ -2201,7 +2431,12 @@ export default function ContractDetail() {
|
||||||
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<dl className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{c.energyDetails.meter && (
|
{c.energyDetails.meter && (
|
||||||
<div>
|
<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">
|
<dd className="font-mono flex items-center gap-1">
|
||||||
{c.energyDetails.meter.meterNumber}
|
{c.energyDetails.meter.meterNumber}
|
||||||
<CopyButton value={c.energyDetails.meter.meterNumber} />
|
<CopyButton value={c.energyDetails.meter.meterNumber} />
|
||||||
|
|
@ -2242,12 +2477,22 @@ export default function ContractDetail() {
|
||||||
)}
|
)}
|
||||||
{c.energyDetails.unitPrice != null && (
|
{c.energyDetails.unitPrice != null && (
|
||||||
<div>
|
<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>
|
<dd>
|
||||||
{c.energyDetails.unitPrice.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 10 })} €/kWh
|
{c.energyDetails.unitPrice.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 10 })} €/kWh
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</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 && (
|
{c.energyDetails.bonus && (
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-sm text-gray-500">Bonus</dt>
|
<dt className="text-sm text-gray-500">Bonus</dt>
|
||||||
|
|
@ -2271,29 +2516,58 @@ export default function ContractDetail() {
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{/* Zählerstände */}
|
{/* Zählerstände - Multi-Zähler Support */}
|
||||||
{c.energyDetails.meter && (
|
{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
|
<MeterReadingsSection
|
||||||
meterId={c.energyDetails.meter.id}
|
meterId={c.energyDetails.meter.id}
|
||||||
meterType={c.energyDetails.meter.type}
|
meterType={c.energyDetails.meter.type}
|
||||||
|
tariffModel={c.energyDetails.meter.tariffModel}
|
||||||
readings={c.energyDetails.meter.readings || []}
|
readings={c.energyDetails.meter.readings || []}
|
||||||
contractId={contractId}
|
|
||||||
canEdit={hasPermission('contracts:update') && !isCustomer}
|
canEdit={hasPermission('contracts:update') && !isCustomer}
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{/* Verbrauchsberechnung & Kostenvorschau */}
|
{/* Verbrauchsberechnung & Kostenvorschau */}
|
||||||
{c.energyDetails.meter && c.startDate && c.endDate && (
|
<EnergyConsumptionCalculation
|
||||||
<EnergyConsumptionCalculation
|
contractType={c.type as 'ELECTRICITY' | 'GAS'}
|
||||||
contractType={c.type as 'ELECTRICITY' | 'GAS'}
|
readings={c.energyDetails.meter?.readings || []}
|
||||||
readings={c.energyDetails.meter.readings || []}
|
startDate={c.startDate}
|
||||||
startDate={c.startDate}
|
endDate={c.endDate}
|
||||||
endDate={c.endDate}
|
basePrice={c.energyDetails.basePrice}
|
||||||
basePrice={c.energyDetails.basePrice}
|
unitPrice={c.energyDetails.unitPrice}
|
||||||
unitPrice={c.energyDetails.unitPrice}
|
unitPriceNt={c.energyDetails.unitPriceNt}
|
||||||
bonus={c.energyDetails.bonus}
|
bonus={c.energyDetails.bonus}
|
||||||
/>
|
hasMeter={!!c.energyDetails.meter}
|
||||||
)}
|
contractMeters={c.energyDetails.contractMeters}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Rechnungen */}
|
{/* Rechnungen */}
|
||||||
<InvoicesSection
|
<InvoicesSection
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams, useLocation } from 'react-router-dom';
|
||||||
|
import { popHistory } from '../../utils/navigation';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
|
import { contractApi, customerApi, platformApi, cancellationPeriodApi, contractDurationApi, providerApi, contractCategoryApi } from '../../services/api';
|
||||||
|
|
@ -64,7 +65,7 @@ export default function ContractForm() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEdit = !!id;
|
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');
|
const preselectedCustomerId = searchParams.get('customerId');
|
||||||
|
|
||||||
|
|
@ -80,6 +81,7 @@ export default function ContractForm() {
|
||||||
const contractType = watch('type') as ContractType;
|
const contractType = watch('type') as ContractType;
|
||||||
const customerId = watch('customerId');
|
const customerId = watch('customerId');
|
||||||
const previousContractId = watch('previousContractId');
|
const previousContractId = watch('previousContractId');
|
||||||
|
const selectedMeterId = watch('meterId');
|
||||||
|
|
||||||
// Fetch existing contract for edit
|
// Fetch existing contract for edit
|
||||||
const { data: contract } = useQuery({
|
const { data: contract } = useQuery({
|
||||||
|
|
@ -276,6 +278,7 @@ export default function ContractForm() {
|
||||||
annualConsumptionKwh: c.energyDetails?.annualConsumptionKwh || '',
|
annualConsumptionKwh: c.energyDetails?.annualConsumptionKwh || '',
|
||||||
basePrice: c.energyDetails?.basePrice || '',
|
basePrice: c.energyDetails?.basePrice || '',
|
||||||
unitPrice: c.energyDetails?.unitPrice || '',
|
unitPrice: c.energyDetails?.unitPrice || '',
|
||||||
|
unitPriceNt: c.energyDetails?.unitPriceNt || '',
|
||||||
bonus: c.energyDetails?.bonus || '',
|
bonus: c.energyDetails?.bonus || '',
|
||||||
// Internet details
|
// Internet details
|
||||||
downloadSpeed: c.internetDetails?.downloadSpeed || '',
|
downloadSpeed: c.internetDetails?.downloadSpeed || '',
|
||||||
|
|
@ -512,6 +515,7 @@ export default function ContractForm() {
|
||||||
annualConsumptionKwh: data.annualConsumptionKwh ? parseFloat(data.annualConsumptionKwh) : null,
|
annualConsumptionKwh: data.annualConsumptionKwh ? parseFloat(data.annualConsumptionKwh) : null,
|
||||||
basePrice: data.basePrice ? parseFloat(data.basePrice) : null,
|
basePrice: data.basePrice ? parseFloat(data.basePrice) : null,
|
||||||
unitPrice: data.unitPrice ? parseFloat(data.unitPrice) : null,
|
unitPrice: data.unitPrice ? parseFloat(data.unitPrice) : null,
|
||||||
|
unitPriceNt: data.unitPriceNt ? parseFloat(data.unitPriceNt) : null,
|
||||||
bonus: data.bonus ? parseFloat(data.bonus) : null,
|
bonus: data.bonus ? parseFloat(data.bonus) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -605,7 +609,8 @@ export default function ContractForm() {
|
||||||
const addresses = customer?.addresses || [];
|
const addresses = customer?.addresses || [];
|
||||||
const bankCards = customer?.bankCards?.filter((c) => c.isActive) || [];
|
const bankCards = customer?.bankCards?.filter((c) => c.isActive) || [];
|
||||||
const documents = customer?.identityDocuments?.filter((d) => d.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 stressfreiEmails = customer?.stressfreiEmails?.filter((e: { isActive: boolean }) => e.isActive) || [];
|
||||||
const platforms = platformsData?.data || [];
|
const platforms = platformsData?.data || [];
|
||||||
const cancellationPeriods = cancellationPeriodsData?.data || [];
|
const cancellationPeriods = cancellationPeriodsData?.data || [];
|
||||||
|
|
@ -658,7 +663,7 @@ export default function ContractForm() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-4 mb-6">
|
<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" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
|
|
@ -951,10 +956,10 @@ export default function ContractForm() {
|
||||||
label="Zähler"
|
label="Zähler"
|
||||||
{...register('meterId')}
|
{...register('meterId')}
|
||||||
options={meters
|
options={meters
|
||||||
.filter((m) => m.type === contractType)
|
.filter((m) => m.type === contractType && (m.isActive || m.id.toString() === watch('meterId')))
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
value: m.id,
|
value: m.id,
|
||||||
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}`,
|
label: `${m.meterNumber}${m.location ? ` (${m.location})` : ''}${!m.isActive ? ' (deaktiviert)' : ''}`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -975,11 +980,19 @@ export default function ContractForm() {
|
||||||
)}
|
)}
|
||||||
<Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} />
|
<Input label="Grundpreis (€/Monat)" type="number" step="any" {...register('basePrice')} />
|
||||||
<Input
|
<Input
|
||||||
label="Arbeitspreis (€/kWh)"
|
label={selectedMeter?.tariffModel === 'DUAL' ? 'HT-Arbeitspreis (€/kWh)' : 'Arbeitspreis (€/kWh)'}
|
||||||
type="number"
|
type="number"
|
||||||
step="any"
|
step="any"
|
||||||
{...register('unitPrice')}
|
{...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')} />
|
<Input label="Bonus (€)" type="number" step="0.01" {...register('bonus')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1409,7 +1422,7 @@ export default function ContractForm() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<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
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isLoading}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient, useQueries } from '@tanstack/react-query';
|
||||||
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
import { Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { pushHistory } from '../../utils/navigation';
|
||||||
import { contractApi, ContractTreeNode } from '../../services/api';
|
import { contractApi, ContractTreeNode } from '../../services/api';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
|
|
@ -271,7 +272,7 @@ export default function ContractList() {
|
||||||
<div className="w-6" /> // Platzhalter für Ausrichtung
|
<div className="w-6" /> // Platzhalter für Ausrichtung
|
||||||
) : null}
|
) : 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}
|
{contract.contractNumber}
|
||||||
<CopyButton value={contract.contractNumber} />
|
<CopyButton value={contract.contractNumber} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -456,7 +457,7 @@ export default function ContractList() {
|
||||||
{data.data.map((contract) => (
|
{data.data.map((contract) => (
|
||||||
<tr key={contract.id} className="border-b hover:bg-gray-50">
|
<tr key={contract.id} className="border-b hover:bg-gray-50">
|
||||||
<td className="py-3 px-4 font-mono text-sm">
|
<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}
|
{contract.contractNumber}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -465,7 +466,7 @@ export default function ContractList() {
|
||||||
{contract.customer && (
|
{contract.customer && (
|
||||||
<Link
|
<Link
|
||||||
to={`/customers/${contract.customer.id}`}
|
to={`/customers/${contract.customer.id}`}
|
||||||
state={{ from: '/contracts' }}
|
state={pushHistory('/contracts')}
|
||||||
className="text-blue-600 hover:underline"
|
className="text-blue-600 hover:underline"
|
||||||
>
|
>
|
||||||
{contract.customer.companyName ||
|
{contract.customer.companyName ||
|
||||||
|
|
@ -505,7 +506,7 @@ export default function ContractList() {
|
||||||
<Eye className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{hasPermission('contracts:update') && !isCustomer && (
|
{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">
|
<Button variant="ghost" size="sm">
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, Link, useNavigate, useSearchParams, useLocation } from 'react-router-dom';
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, gdprApi, StressfreiEmail, ContractTreeNode } from '../../services/api';
|
import { customerApi, addressApi, bankCardApi, documentApi, meterApi, uploadApi, contractApi, stressfreiEmailApi, emailProviderApi, gdprApi, StressfreiEmail, ContractTreeNode } from '../../services/api';
|
||||||
import { EmailClientTab } from '../../components/email';
|
import { EmailClientTab } from '../../components/email';
|
||||||
|
|
@ -22,7 +23,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { hasPermission, isCustomerPortal } = useAuth();
|
const { hasPermission, isCustomerPortal } = useAuth();
|
||||||
const location = useLocation();
|
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 [searchParams, setSearchParams] = useSearchParams();
|
||||||
const customerId = portalCustomerId || parseInt(id!);
|
const customerId = portalCustomerId || parseInt(id!);
|
||||||
const defaultTab = searchParams.get('tab') || 'addresses';
|
const defaultTab = searchParams.get('tab') || 'addresses';
|
||||||
|
|
@ -204,7 +205,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<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" />
|
<ArrowLeft className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -221,7 +222,7 @@ export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{hasPermission('customers:update') && (
|
{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">
|
<Button variant="secondary">
|
||||||
<Edit className="w-4 h-4 mr-2" />
|
<Edit className="w-4 h-4 mr-2" />
|
||||||
Bearbeiten
|
Bearbeiten
|
||||||
|
|
@ -649,7 +650,7 @@ function AddressesTab({
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: addressApi.delete,
|
mutationFn: addressApi.delete,
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -746,18 +747,18 @@ function BankCardsTab({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<BankCard> }) =>
|
mutationFn: ({ id, data }: { id: number; data: Partial<BankCard> }) =>
|
||||||
bankCardApi.update(id, data),
|
bankCardApi.update(id, data),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: bankCardApi.delete,
|
mutationFn: bankCardApi.delete,
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDocumentUpload = async (cardId: number, file: File) => {
|
const handleDocumentUpload = async (cardId: number, file: File) => {
|
||||||
try {
|
try {
|
||||||
await uploadApi.uploadBankCardDocument(cardId, file);
|
await uploadApi.uploadBankCardDocument(cardId, file);
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload fehlgeschlagen:', error);
|
console.error('Upload fehlgeschlagen:', error);
|
||||||
alert('Upload fehlgeschlagen');
|
alert('Upload fehlgeschlagen');
|
||||||
|
|
@ -768,7 +769,7 @@ function BankCardsTab({
|
||||||
if (!confirm('Dokument wirklich löschen?')) return;
|
if (!confirm('Dokument wirklich löschen?')) return;
|
||||||
try {
|
try {
|
||||||
await uploadApi.deleteBankCardDocument(cardId);
|
await uploadApi.deleteBankCardDocument(cardId);
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Löschen fehlgeschlagen:', error);
|
console.error('Löschen fehlgeschlagen:', error);
|
||||||
alert('Löschen fehlgeschlagen');
|
alert('Löschen fehlgeschlagen');
|
||||||
|
|
@ -972,18 +973,18 @@ function DocumentsTab({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<IdentityDocument> }) =>
|
mutationFn: ({ id, data }: { id: number; data: Partial<IdentityDocument> }) =>
|
||||||
documentApi.update(id, data),
|
documentApi.update(id, data),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: documentApi.delete,
|
mutationFn: documentApi.delete,
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDocumentUpload = async (docId: number, file: File) => {
|
const handleDocumentUpload = async (docId: number, file: File) => {
|
||||||
try {
|
try {
|
||||||
await uploadApi.uploadIdentityDocument(docId, file);
|
await uploadApi.uploadIdentityDocument(docId, file);
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload fehlgeschlagen:', error);
|
console.error('Upload fehlgeschlagen:', error);
|
||||||
alert('Upload fehlgeschlagen');
|
alert('Upload fehlgeschlagen');
|
||||||
|
|
@ -994,7 +995,7 @@ function DocumentsTab({
|
||||||
if (!confirm('Dokument wirklich löschen?')) return;
|
if (!confirm('Dokument wirklich löschen?')) return;
|
||||||
try {
|
try {
|
||||||
await uploadApi.deleteIdentityDocument(docId);
|
await uploadApi.deleteIdentityDocument(docId);
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Löschen fehlgeschlagen:', error);
|
console.error('Löschen fehlgeschlagen:', error);
|
||||||
alert('Löschen fehlgeschlagen');
|
alert('Löschen fehlgeschlagen');
|
||||||
|
|
@ -1204,26 +1205,34 @@ function MetersTab({
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onEdit: (meter: Meter) => 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 [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 queryClient = useQueryClient();
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<Meter> }) =>
|
mutationFn: ({ id, data }: { id: number; data: Partial<Meter> }) =>
|
||||||
meterApi.update(id, data),
|
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({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: meterApi.delete,
|
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({
|
const deleteReadingMutation = useMutation({
|
||||||
mutationFn: ({ meterId, readingId }: { meterId: number; readingId: number }) =>
|
mutationFn: ({ meterId, readingId }: { meterId: number; readingId: number }) =>
|
||||||
meterApi.deleteReading(meterId, readingId),
|
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);
|
const filtered = showInactive ? meters : meters.filter((m) => m.isActive);
|
||||||
|
|
@ -1273,6 +1282,9 @@ function MetersTab({
|
||||||
<Badge variant={meter.type === 'ELECTRICITY' ? 'warning' : 'info'}>
|
<Badge variant={meter.type === 'ELECTRICITY' ? 'warning' : 'info'}>
|
||||||
{meter.type === 'ELECTRICITY' ? 'Strom' : 'Gas'}
|
{meter.type === 'ELECTRICITY' ? 'Strom' : 'Gas'}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
{meter.tariffModel === 'DUAL' && (
|
||||||
|
<Badge variant="default">HT/NT</Badge>
|
||||||
|
)}
|
||||||
{!meter.isActive && <Badge variant="danger">Inaktiv</Badge>}
|
{!meter.isActive && <Badge variant="danger">Inaktiv</Badge>}
|
||||||
</div>
|
</div>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
|
|
@ -1281,7 +1293,7 @@ function MetersTab({
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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"
|
title="Zählerstand hinzufügen"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
|
|
@ -1371,14 +1383,17 @@ function MetersTab({
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-mono flex items-center gap-1">
|
<span className="font-mono flex items-center gap-1">
|
||||||
{reading.value.toLocaleString('de-DE')} {reading.unit}
|
{reading.valueNt !== undefined && reading.valueNt !== null ? (
|
||||||
<CopyButton value={reading.value.toString()} title="Nur Wert kopieren" />
|
<>HT: {reading.value.toLocaleString('de-DE')} / NT: {reading.valueNt.toLocaleString('de-DE')} {reading.unit}</>
|
||||||
<CopyButton value={`${reading.value.toLocaleString('de-DE')} ${reading.unit}`} title="Mit Einheit kopieren" />
|
) : (
|
||||||
|
<>{reading.value.toLocaleString('de-DE')} {reading.unit}</>
|
||||||
|
)}
|
||||||
|
<CopyButton value={reading.value.toString()} title="Wert kopieren" />
|
||||||
</span>
|
</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
|
||||||
<button
|
<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"
|
className="text-gray-400 hover:text-blue-600"
|
||||||
title="Bearbeiten"
|
title="Bearbeiten"
|
||||||
>
|
>
|
||||||
|
|
@ -1417,6 +1432,7 @@ function MetersTab({
|
||||||
onClose={() => setShowReadingModal(null)}
|
onClose={() => setShowReadingModal(null)}
|
||||||
meterId={showReadingModal.meterId}
|
meterId={showReadingModal.meterId}
|
||||||
meterType={showReadingModal.meterType}
|
meterType={showReadingModal.meterType}
|
||||||
|
tariffModel={showReadingModal.tariffModel as any}
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1427,10 +1443,43 @@ function MetersTab({
|
||||||
onClose={() => setEditingReading(null)}
|
onClose={() => setEditingReading(null)}
|
||||||
meterId={editingReading.meterId}
|
meterId={editingReading.meterId}
|
||||||
meterType={editingReading.meterType}
|
meterType={editingReading.meterType}
|
||||||
|
tariffModel={editingReading.tariffModel as any}
|
||||||
customerId={customerId}
|
customerId={customerId}
|
||||||
reading={editingReading.reading}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1457,7 +1506,7 @@ function ContractsTab({
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: contractApi.delete,
|
mutationFn: contractApi.delete,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['customers'] });
|
queryClient.invalidateQueries({ queryKey: ['customers'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
queryClient.invalidateQueries({ queryKey: ['contracts'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['contract-tree', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['contract-tree', customerId] });
|
||||||
|
|
@ -1549,7 +1598,7 @@ function ContractsTab({
|
||||||
<div className="w-6" /> // Platzhalter für Ausrichtung
|
<div className="w-6" /> // Platzhalter für Ausrichtung
|
||||||
) : null}
|
) : 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}
|
{contract.contractNumber}
|
||||||
<CopyButton value={contract.contractNumber} />
|
<CopyButton value={contract.contractNumber} />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -2054,7 +2103,7 @@ function AddressModal({
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: typeof formData) => addressApi.create(customerId, data),
|
mutationFn: (data: typeof formData) => addressApi.create(customerId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({
|
setFormData({
|
||||||
type: 'DELIVERY_RESIDENCE',
|
type: 'DELIVERY_RESIDENCE',
|
||||||
|
|
@ -2071,7 +2120,7 @@ function AddressModal({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: typeof formData) => addressApi.update(address!.id, data),
|
mutationFn: (data: typeof formData) => addressApi.update(address!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -2201,7 +2250,7 @@ function BankCardModal({
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => bankCardApi.create(customerId, data),
|
mutationFn: (data: any) => bankCardApi.create(customerId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({ accountHolder: '', iban: '', bic: '', bankName: '', expiryDate: '', isActive: true });
|
setFormData({ accountHolder: '', iban: '', bic: '', bankName: '', expiryDate: '', isActive: true });
|
||||||
},
|
},
|
||||||
|
|
@ -2210,7 +2259,7 @@ function BankCardModal({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: any) => bankCardApi.update(bankCard!.id, data),
|
mutationFn: (data: any) => bankCardApi.update(bankCard!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -2333,7 +2382,7 @@ function DocumentModal({
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => documentApi.create(customerId, data),
|
mutationFn: (data: any) => documentApi.create(customerId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({
|
setFormData({
|
||||||
type: 'ID_CARD',
|
type: 'ID_CARD',
|
||||||
|
|
@ -2351,7 +2400,7 @@ function DocumentModal({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: any) => documentApi.update(document!.id, data),
|
mutationFn: (data: any) => documentApi.update(document!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -2498,6 +2547,7 @@ function MeterModal({
|
||||||
const getInitialFormData = () => ({
|
const getInitialFormData = () => ({
|
||||||
meterNumber: meter?.meterNumber || '',
|
meterNumber: meter?.meterNumber || '',
|
||||||
type: meter?.type || 'ELECTRICITY' as const,
|
type: meter?.type || 'ELECTRICITY' as const,
|
||||||
|
tariffModel: meter?.tariffModel || 'SINGLE' as const,
|
||||||
location: meter?.location || '',
|
location: meter?.location || '',
|
||||||
isActive: meter?.isActive ?? true,
|
isActive: meter?.isActive ?? true,
|
||||||
});
|
});
|
||||||
|
|
@ -2507,16 +2557,16 @@ function MeterModal({
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => meterApi.create(customerId, data),
|
mutationFn: (data: any) => meterApi.create(customerId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({ meterNumber: '', type: 'ELECTRICITY', location: '', isActive: true });
|
setFormData({ meterNumber: '', type: 'ELECTRICITY', tariffModel: 'SINGLE', location: '', isActive: true });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: any) => meterApi.update(meter!.id, data),
|
mutationFn: (data: any) => meterApi.update(meter!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
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
|
<Input
|
||||||
label="Standort"
|
label="Standort"
|
||||||
value={formData.location}
|
value={formData.location}
|
||||||
|
|
@ -2594,6 +2656,7 @@ function MeterReadingModal({
|
||||||
onClose,
|
onClose,
|
||||||
meterId,
|
meterId,
|
||||||
meterType,
|
meterType,
|
||||||
|
tariffModel,
|
||||||
customerId,
|
customerId,
|
||||||
reading,
|
reading,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -2601,47 +2664,63 @@ function MeterReadingModal({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
meterId: number;
|
meterId: number;
|
||||||
meterType: 'ELECTRICITY' | 'GAS';
|
meterType: 'ELECTRICITY' | 'GAS';
|
||||||
|
tariffModel?: 'SINGLE' | 'DUAL';
|
||||||
customerId: number;
|
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 queryClient = useQueryClient();
|
||||||
const isEditing = !!reading;
|
const isEditing = !!reading;
|
||||||
const defaultUnit = meterType === 'ELECTRICITY' ? 'kWh' : 'm³';
|
const defaultUnit = meterType === 'ELECTRICITY' ? 'kWh' : 'm³';
|
||||||
|
const isDualTariff = tariffModel === 'DUAL';
|
||||||
|
|
||||||
const getInitialFormData = () => ({
|
const getInitialFormData = () => ({
|
||||||
readingDate: reading?.readingDate
|
readingDate: reading?.readingDate
|
||||||
? new Date(reading.readingDate).toISOString().split('T')[0]
|
? new Date(reading.readingDate).toISOString().split('T')[0]
|
||||||
: new Date().toISOString().split('T')[0],
|
: new Date().toISOString().split('T')[0],
|
||||||
value: reading?.value?.toString() || '',
|
value: reading?.value?.toString() || '',
|
||||||
|
valueNt: reading?.valueNt?.toString() || '',
|
||||||
notes: reading?.notes || '',
|
notes: reading?.notes || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [formData, setFormData] = useState(getInitialFormData);
|
const [formData, setFormData] = useState(getInitialFormData);
|
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const createMutation = useMutation({
|
const createMutation = useMutation({
|
||||||
mutationFn: (data: any) => meterApi.addReading(meterId, data),
|
mutationFn: (data: any) => meterApi.addReading(meterId, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
|
setError(null);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (data: any) => meterApi.updateReading(meterId, reading!.id, data),
|
mutationFn: (data: any) => meterApi.updateReading(meterId, reading!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
|
setError(null);
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
setError(err instanceof Error ? err.message : 'Fehler beim Speichern');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const data = {
|
const data: Record<string, unknown> = {
|
||||||
readingDate: new Date(formData.readingDate),
|
readingDate: new Date(formData.readingDate),
|
||||||
value: parseFloat(formData.value),
|
value: parseFloat(formData.value),
|
||||||
unit: defaultUnit,
|
unit: defaultUnit,
|
||||||
notes: formData.notes || undefined,
|
notes: formData.notes || undefined,
|
||||||
};
|
};
|
||||||
|
if (isDualTariff && formData.valueNt) {
|
||||||
|
data.valueNt = parseFloat(formData.valueNt);
|
||||||
|
}
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
updateMutation.mutate(data);
|
updateMutation.mutate(data);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2667,10 +2746,10 @@ function MeterReadingModal({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className={`grid ${isDualTariff ? 'grid-cols-2' : 'grid-cols-3'} gap-4`}>
|
||||||
<div className="col-span-2">
|
<div className={isDualTariff ? '' : 'col-span-2'}>
|
||||||
<Input
|
<Input
|
||||||
label="Zählerstand"
|
label={isDualTariff ? 'HT-Stand (Hochtarif)' : 'Zählerstand'}
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={formData.value}
|
value={formData.value}
|
||||||
|
|
@ -2678,12 +2757,26 @@ function MeterReadingModal({
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{isDualTariff && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
|
<div>
|
||||||
<div className="h-10 flex items-center px-3 bg-gray-100 border border-gray-300 rounded-md text-gray-700">
|
<Input
|
||||||
{defaultUnit}
|
label="NT-Stand (Niedertarif)"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={formData.valueNt}
|
||||||
|
onChange={(e) => setFormData({ ...formData, valueNt: e.target.value })}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -2693,6 +2786,12 @@ function MeterReadingModal({
|
||||||
placeholder="Optionale Notizen..."
|
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">
|
<div className="flex justify-end gap-2">
|
||||||
<Button type="button" variant="secondary" onClick={onClose}>
|
<Button type="button" variant="secondary" onClick={onClose}>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
|
|
@ -2732,12 +2831,12 @@ function StressfreiEmailsTab({
|
||||||
const updateMutation = useMutation({
|
const updateMutation = useMutation({
|
||||||
mutationFn: ({ id, data }: { id: number; data: Partial<StressfreiEmail> }) =>
|
mutationFn: ({ id, data }: { id: number; data: Partial<StressfreiEmail> }) =>
|
||||||
stressfreiEmailApi.update(id, data),
|
stressfreiEmailApi.update(id, data),
|
||||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] }),
|
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['customer', customerId] }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: stressfreiEmailApi.delete,
|
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);
|
const filtered = showInactive ? emails : emails.filter((e) => e.isActive);
|
||||||
|
|
@ -3087,7 +3186,7 @@ function StressfreiEmailModal({
|
||||||
const result = await stressfreiEmailApi.enableMailbox(email.id);
|
const result = await stressfreiEmailApi.enableMailbox(email.id);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setMailboxEnabled(true);
|
setMailboxEnabled(true);
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
||||||
} else {
|
} else {
|
||||||
setProvisionError(result.error || 'Mailbox-Aktivierung fehlgeschlagen');
|
setProvisionError(result.error || 'Mailbox-Aktivierung fehlgeschlagen');
|
||||||
|
|
@ -3108,7 +3207,7 @@ function StressfreiEmailModal({
|
||||||
setMailboxEnabled(result.data.hasMailbox);
|
setMailboxEnabled(result.data.hasMailbox);
|
||||||
if (result.data.wasUpdated) {
|
if (result.data.wasUpdated) {
|
||||||
// DB wurde aktualisiert, Query invalidieren
|
// DB wurde aktualisiert, Query invalidieren
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -3199,7 +3298,7 @@ function StressfreiEmailModal({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['mailbox-accounts', customerId] });
|
||||||
setLocalPart('');
|
setLocalPart('');
|
||||||
setNotes('');
|
setNotes('');
|
||||||
|
|
@ -3216,7 +3315,7 @@ function StressfreiEmailModal({
|
||||||
mutationFn: (data: Partial<StressfreiEmail>) =>
|
mutationFn: (data: Partial<StressfreiEmail>) =>
|
||||||
stressfreiEmailApi.update(email!.id, data),
|
stressfreiEmailApi.update(email!.id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId.toString()] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -3664,6 +3763,7 @@ function ConsentTab({
|
||||||
try {
|
try {
|
||||||
await uploadApi.uploadPrivacyPolicy(customerId, file);
|
await uploadApi.uploadPrivacyPolicy(customerId, file);
|
||||||
onUpdate?.();
|
onUpdate?.();
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['customer-consents', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload fehlgeschlagen:', error);
|
console.error('Upload fehlgeschlagen:', error);
|
||||||
alert('Upload fehlgeschlagen');
|
alert('Upload fehlgeschlagen');
|
||||||
|
|
@ -3675,6 +3775,7 @@ function ConsentTab({
|
||||||
try {
|
try {
|
||||||
await uploadApi.deletePrivacyPolicy(customerId);
|
await uploadApi.deletePrivacyPolicy(customerId);
|
||||||
onUpdate?.();
|
onUpdate?.();
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['customer-consents', customerId] });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Löschen fehlgeschlagen:', error);
|
console.error('Löschen fehlgeschlagen:', error);
|
||||||
alert('Löschen fehlgeschlagen');
|
alert('Löschen fehlgeschlagen');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||||
|
import { popHistory } from '../../utils/navigation';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { customerApi } from '../../services/api';
|
import { customerApi } from '../../services/api';
|
||||||
|
|
@ -17,7 +18,7 @@ export default function CustomerForm() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEdit = !!id;
|
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 { register, handleSubmit, reset, watch, setValue, formState: { errors } } = useForm<CustomerFormData>();
|
||||||
const customerType = watch('type');
|
const customerType = watch('type');
|
||||||
|
|
@ -234,7 +235,7 @@ export default function CustomerForm() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="flex justify-end gap-4">
|
<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
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isLoading}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { pushHistory } from '../../utils/navigation';
|
||||||
import { customerApi } from '../../services/api';
|
import { customerApi } from '../../services/api';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
import Card from '../../components/ui/Card';
|
import Card from '../../components/ui/Card';
|
||||||
|
|
@ -78,12 +79,12 @@ export default function CustomerList() {
|
||||||
{data.data.map((customer) => (
|
{data.data.map((customer) => (
|
||||||
<tr key={customer.id} className="border-b hover:bg-gray-50">
|
<tr key={customer.id} className="border-b hover:bg-gray-50">
|
||||||
<td className="py-3 px-4 font-mono text-sm">
|
<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}
|
{customer.customerNumber}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<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.type === 'BUSINESS' && customer.companyName
|
||||||
? customer.companyName
|
? customer.companyName
|
||||||
: `${customer.firstName} ${customer.lastName}`}
|
: `${customer.firstName} ${customer.lastName}`}
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,15 @@ const RESOURCE_OPTIONS = [
|
||||||
{ value: 'Contract', label: 'Verträge' },
|
{ value: 'Contract', label: 'Verträge' },
|
||||||
{ value: 'User', label: 'Benutzer' },
|
{ value: 'User', label: 'Benutzer' },
|
||||||
{ value: 'BankCard', label: 'Bankdaten' },
|
{ value: 'BankCard', label: 'Bankdaten' },
|
||||||
{ value: 'IdentityDocument', label: 'Ausweisdokumente' },
|
{ value: 'IdentityDocument', label: 'Ausweise' },
|
||||||
{ value: 'Authentication', label: 'Authentifizierung' },
|
{ value: 'Address', label: 'Adressen' },
|
||||||
|
{ value: 'Meter', label: 'Zähler' },
|
||||||
|
{ value: 'ContractTask', label: 'Aufgaben' },
|
||||||
|
{ value: 'Authentication', label: 'Anmeldung' },
|
||||||
{ value: 'CustomerConsent', label: 'Einwilligungen' },
|
{ 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 {
|
function formatDate(date: string): string {
|
||||||
|
|
@ -354,11 +359,12 @@ export default function AuditLogs() {
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<div>{log.resourceType}</div>
|
{log.resourceLabel ? (
|
||||||
{log.resourceLabel && (
|
<div className="text-sm truncate max-w-[300px]" title={log.resourceLabel}>
|
||||||
<div className="text-xs text-gray-500 truncate max-w-[200px]" title={log.resourceLabel}>
|
|
||||||
{log.resourceLabel}
|
{log.resourceLabel}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500">{log.resourceType}</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 px-4 font-mono text-xs">{log.ipAddress}</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');
|
const res = await api.get<ApiResponse<import('../types').CockpitResult>>('/contracts/cockpit');
|
||||||
return res.data;
|
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: Vertrag zurückstellen
|
||||||
snooze: async (id: number, data: { nextReviewDate?: string; months?: number }) => {
|
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);
|
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;
|
licenseIssueDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MeterTariffModel = 'SINGLE' | 'DUAL';
|
||||||
|
|
||||||
export interface Meter {
|
export interface Meter {
|
||||||
id: number;
|
id: number;
|
||||||
customerId: number;
|
customerId: number;
|
||||||
meterNumber: string;
|
meterNumber: string;
|
||||||
type: 'ELECTRICITY' | 'GAS';
|
type: 'ELECTRICITY' | 'GAS';
|
||||||
|
tariffModel: MeterTariffModel;
|
||||||
location?: string;
|
location?: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
readings?: MeterReading[];
|
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 type MeterReadingStatus = 'RECORDED' | 'REPORTED' | 'TRANSFERRED';
|
||||||
|
|
||||||
export interface MeterReading {
|
export interface MeterReading {
|
||||||
|
|
@ -169,6 +183,7 @@ export interface MeterReading {
|
||||||
meterId: number;
|
meterId: number;
|
||||||
readingDate: string;
|
readingDate: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
valueNt?: number; // NT-Wert (nur bei Zweitarifzähler)
|
||||||
unit: string;
|
unit: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
reportedBy?: string;
|
reportedBy?: string;
|
||||||
|
|
@ -396,11 +411,13 @@ export interface EnergyContractDetails {
|
||||||
annualConsumption?: number; // kWh für Strom, m³ für Gas
|
annualConsumption?: number; // kWh für Strom, m³ für Gas
|
||||||
annualConsumptionKwh?: number; // kWh für Gas (zusätzlich zu m³)
|
annualConsumptionKwh?: number; // kWh für Gas (zusätzlich zu m³)
|
||||||
basePrice?: number; // €/Monat
|
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;
|
bonus?: number;
|
||||||
previousProviderName?: string;
|
previousProviderName?: string;
|
||||||
previousCustomerNumber?: string;
|
previousCustomerNumber?: string;
|
||||||
invoices?: Invoice[]; // Rechnungen
|
invoices?: Invoice[]; // Rechnungen
|
||||||
|
contractMeters?: ContractMeter[]; // Zähler-Zuordnungen (inkl. Folgezähler)
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternetContractDetails {
|
export interface InternetContractDetails {
|
||||||
|
|
@ -575,6 +592,7 @@ export interface ReportedMeterReading {
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
meter: { id: number; meterNumber: string; type: string };
|
meter: { id: number; meterNumber: string; type: string };
|
||||||
customer: { id: number; customerNumber: string; name: string };
|
customer: { id: number; customerNumber: string; name: string };
|
||||||
|
contract?: { id: number; contractNumber: string };
|
||||||
providerPortal?: {
|
providerPortal?: {
|
||||||
providerName: string;
|
providerName: string;
|
||||||
portalUrl: 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)
|
// Konstante für Gas → kWh Umrechnung (Standard Erdgas H)
|
||||||
export const GAS_TO_KWH_FACTOR = 10.5;
|
export const GAS_TO_KWH_FACTOR = 10.5;
|
||||||
|
|
@ -8,6 +8,8 @@ export interface ConsumptionCalculation {
|
||||||
type: 'exact' | 'projected' | 'insufficient' | 'none';
|
type: 'exact' | 'projected' | 'insufficient' | 'none';
|
||||||
consumptionM3?: number; // Nur bei Gas
|
consumptionM3?: number; // Nur bei Gas
|
||||||
consumptionKwh: number;
|
consumptionKwh: number;
|
||||||
|
consumptionHt?: number; // Nur bei HT/NT: HT-Verbrauch in kWh
|
||||||
|
consumptionNt?: number; // Nur bei HT/NT: NT-Verbrauch in kWh
|
||||||
startReading?: MeterReading;
|
startReading?: MeterReading;
|
||||||
endReading?: MeterReading;
|
endReading?: MeterReading;
|
||||||
projectedEndDate?: string; // Nur bei Hochrechnung
|
projectedEndDate?: string; // Nur bei Hochrechnung
|
||||||
|
|
@ -17,7 +19,8 @@ export interface ConsumptionCalculation {
|
||||||
// Ergebnis der Kostenberechnung
|
// Ergebnis der Kostenberechnung
|
||||||
export interface CostCalculation {
|
export interface CostCalculation {
|
||||||
annualBaseCost: number; // basePrice × 12
|
annualBaseCost: number; // basePrice × 12
|
||||||
annualConsumptionCost: number; // verbrauch × unitPrice
|
annualConsumptionCost: number; // verbrauch × unitPrice (HT bei Zweitarif)
|
||||||
|
annualConsumptionCostNt?: number; // NT-Verbrauch × unitPriceNt
|
||||||
annualTotalCost: number; // Summe
|
annualTotalCost: number; // Summe
|
||||||
monthlyPayment: number; // annualTotalCost / 12
|
monthlyPayment: number; // annualTotalCost / 12
|
||||||
bonus?: number;
|
bonus?: number;
|
||||||
|
|
@ -94,7 +97,6 @@ export function calculateConsumption(
|
||||||
(a, b) => new Date(a.readingDate).getTime() - new Date(b.readingDate).getTime()
|
(a, b) => new Date(a.readingDate).getTime() - new Date(b.readingDate).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstReading = sorted[0];
|
|
||||||
const lastReading = sorted[sorted.length - 1];
|
const lastReading = sorted[sorted.length - 1];
|
||||||
|
|
||||||
// Prüfen ob Endzählerstand am/nach Vertragsende liegt
|
// Prüfen ob Endzählerstand am/nach Vertragsende liegt
|
||||||
|
|
@ -103,16 +105,39 @@ export function calculateConsumption(
|
||||||
lastReadingDate.setHours(0, 0, 0, 0);
|
lastReadingDate.setHours(0, 0, 0, 0);
|
||||||
contractEndDate.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) {
|
if (lastReadingDate >= contractEndDate) {
|
||||||
const consumption = lastReading.value - firstReading.value;
|
const result = formatConsumptionResult('exact', totalConsumption, contractType, effectiveFirstReading, effectiveLastReading);
|
||||||
return formatConsumptionResult('exact', consumption, contractType, firstReading, lastReading);
|
if (hasNt) { result.consumptionHt = totalConsumption; result.consumptionNt = totalConsumptionNt; }
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall B: Hochrechnung erforderlich
|
// 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) {
|
if (daysBetweenReadings < 1) {
|
||||||
return {
|
return {
|
||||||
type: 'insufficient',
|
type: 'insufficient',
|
||||||
|
|
@ -122,17 +147,26 @@ export function calculateConsumption(
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalContractDays = daysDiff(startDate, endDate);
|
const totalContractDays = daysDiff(startDate, endDate);
|
||||||
const measuredConsumption = lastReading.value - firstReading.value;
|
const projectedConsumption = (totalConsumption / daysBetweenReadings) * totalContractDays;
|
||||||
const projectedConsumption = (measuredConsumption / daysBetweenReadings) * totalContractDays;
|
|
||||||
|
|
||||||
return formatConsumptionResult(
|
const result = formatConsumptionResult(
|
||||||
'projected',
|
'projected',
|
||||||
projectedConsumption,
|
projectedConsumption,
|
||||||
contractType,
|
contractType,
|
||||||
firstReading,
|
effectiveFirstReading,
|
||||||
lastReading,
|
effectiveLastReading,
|
||||||
endDate
|
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,
|
consumptionKwh: number,
|
||||||
basePrice?: number,
|
basePrice?: number,
|
||||||
unitPrice?: number,
|
unitPrice?: number,
|
||||||
bonus?: number
|
bonus?: number,
|
||||||
|
consumptionNtKwh?: number,
|
||||||
|
unitPriceNt?: number
|
||||||
): CostCalculation | null {
|
): CostCalculation | null {
|
||||||
// Mindestens ein Preis muss vorhanden sein
|
// Mindestens ein Preis muss vorhanden sein
|
||||||
if (basePrice == null && unitPrice == null) {
|
if (basePrice == null && unitPrice == null) {
|
||||||
|
|
@ -181,17 +217,78 @@ export function calculateCosts(
|
||||||
}
|
}
|
||||||
|
|
||||||
const annualBaseCost = (basePrice ?? 0) * 12;
|
const annualBaseCost = (basePrice ?? 0) * 12;
|
||||||
|
// Bei HT/NT: consumptionKwh ist nur HT, NT wird separat berechnet
|
||||||
const annualConsumptionCost = consumptionKwh * (unitPrice ?? 0);
|
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 effectiveAnnualCost = annualTotalCost - (bonus ?? 0);
|
||||||
const monthlyPayment = effectiveAnnualCost / 12;
|
const monthlyPayment = effectiveAnnualCost / 12;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
annualBaseCost,
|
annualBaseCost,
|
||||||
annualConsumptionCost,
|
annualConsumptionCost,
|
||||||
|
annualConsumptionCostNt: annualConsumptionCostNt > 0 ? annualConsumptionCostNt : undefined,
|
||||||
annualTotalCost,
|
annualTotalCost,
|
||||||
monthlyPayment,
|
monthlyPayment,
|
||||||
bonus: bonus ?? undefined,
|
bonus: bonus ?? undefined,
|
||||||
effectiveAnnualCost,
|
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