diff --git a/Abrechnung/module.php b/Abrechnung/module.php index 4a2705b..40aee87 100644 --- a/Abrechnung/module.php +++ b/Abrechnung/module.php @@ -70,6 +70,8 @@ class Abrechnung extends IPSModule $this->RegisterVariableFloat('VisuTariffPrice', 'Tarif Rp/Einheit', 'BELEVO.TariffPrice', 14); $this->RegisterVariableString('VisuTariffStatus', 'Tarif Status', '', 15); $this->RegisterVariableString('VisuTariffPayload', 'Tarif Payload', '', 16); + $this->RegisterVariableString('VisuUserOverview', 'Benutzer bearbeiten', '~HTMLBox', 20); + $this->RegisterVariableString('VisuUserStatus', 'Benutzer Status', '', 21); $this->EnableAction('FromDate'); $this->EnableAction('ToDate'); $this->EnableAction('VisuTariffType'); @@ -84,6 +86,7 @@ class Abrechnung extends IPSModule "InstanceID . ", 'StartBilling', ''); ?>" ); $this->EnsureVisuTariffObjects(); + $this->EnsureVisuUserObjects(); $this->RegisterMediaDocument('InvoicePDF', 'Letzte Rechnung', 'pdf'); } @@ -93,8 +96,10 @@ class Abrechnung extends IPSModule $this->RegisterTariffVariableProfiles(); $this->RegisterTariffWebhook(); $this->EnsureVisuTariffObjects(); + $this->EnsureVisuUserObjects(); $this->InitializeVisuTariffDefaults(); $this->UpdateTariffOverview(); + $this->UpdateUserOverview(); } private function RegisterTariffWebhook() @@ -162,6 +167,13 @@ class Abrechnung extends IPSModule } } + private function EnsureVisuUserObjects() + { + $this->RegisterVariableString('VisuUserOverview', 'Benutzer bearbeiten', '~HTMLBox', 20); + $this->RegisterVariableString('VisuUserStatus', 'Benutzer Status', '', 21); + $this->HideObjectByIdent('VisuUserStatus'); + } + private function HideObjectByIdent($Ident) { $id = @IPS_GetObjectIDByIdent($Ident, $this->InstanceID); @@ -411,6 +423,7 @@ class Abrechnung extends IPSModule .belevo-tariffs .status{margin-left:8px;color:#4b5563}
+ @@ -573,6 +586,591 @@ form.onsubmit=function(){ return $this->SaveTariffTableFromPayload(json_encode($rows)); } + private function ReadUsers() + { + $users = json_decode($this->ReadPropertyString('Users'), true); + return is_array($users) ? array_values($users) : []; + } + + private function ReadPowerMeters() + { + $meters = json_decode($this->ReadPropertyString('PowerMeters'), true); + return is_array($meters) ? array_values($meters) : []; + } + + private function ReadWaterMeters() + { + $meters = json_decode($this->ReadPropertyString('WaterMeters'), true); + return is_array($meters) ? array_values($meters) : []; + } + + private function WriteUsersAndMeterAssignments(array $users, array $powerMeters, array $waterMeters) + { + IPS_SetProperty($this->InstanceID, 'Users', json_encode(array_values($users), JSON_UNESCAPED_UNICODE)); + IPS_SetProperty($this->InstanceID, 'PowerMeters', json_encode(array_values($powerMeters), JSON_UNESCAPED_UNICODE)); + IPS_SetProperty($this->InstanceID, 'WaterMeters', json_encode(array_values($waterMeters), JSON_UNESCAPED_UNICODE)); + IPS_ApplyChanges($this->InstanceID); + $this->UpdateUserOverview(); + } + + private function UpdateUserOverview() + { + $overviewID = @IPS_GetObjectIDByIdent('VisuUserOverview', $this->InstanceID); + if ($overviewID === false) { + return; + } + + $users = $this->ReadUsers(); + $powerMeters = $this->ReadPowerMeters(); + $waterMeters = $this->ReadWaterMeters(); + $rows = ''; + + foreach ($users as $index => $user) { + $rows .= $this->BuildUserEditorRow((string)$index, is_array($user) ? $user : [], $powerMeters, $waterMeters); + } + + if ($rows === '') { + $rows = $this->BuildUserEditorRow('0', [], $powerMeters, $waterMeters); + } + + $webhookPath = $this->GetTariffWebhookPath(); + $statusID = @IPS_GetObjectIDByIdent('VisuUserStatus', $this->InstanceID); + $status = $statusID !== false ? $this->h(@GetValue($statusID)) : ''; + $template = $this->BuildUserEditorRow('__KEY__', [], $powerMeters, $waterMeters); + + $html = " +
+ + + +
+ + + + + + + + + + +" . $rows . " +
NameAdresseOrtStromzaehlerNebenzaehlerAktion
+
+ + +" . $status . " +
+
+ + + +"; + + SetValue($overviewID, $html); + } + + private function BuildUserEditorRow($rowKey, array $user, array $powerMeters, array $waterMeters) + { + $userId = trim((string)($user['id'] ?? '')); + $powerSelected = $this->GetMeterIndexesForUser($powerMeters, $userId); + $waterSelected = $this->GetMeterIndexesForUser($waterMeters, $userId); + + return '' + . '' + . '' + . '' + . '' . $this->BuildMeterPicker('power', 'power_assign', $rowKey, $powerMeters, $powerSelected) . '' + . '' . $this->BuildMeterPicker('water', 'water_assign', $rowKey, $waterMeters, $waterSelected) . '' + . '' + . ''; + } + + private function GetMeterIndexesForUser(array $meters, $userId) + { + if ($userId === '') { + return []; + } + + $indexes = []; + foreach ($meters as $index => $meter) { + if ((string)($meter['user_id'] ?? '') === (string)$userId) { + $indexes[] = (string)$index; + } + } + return $indexes; + } + + private function BuildMeterPicker($kind, $fieldName, $rowKey, array $meters, array $selectedIndexes) + { + $inputName = $fieldName . '[' . $rowKey . '][]'; + $tags = ''; + + foreach ($selectedIndexes as $selectedIndex) { + if (!array_key_exists((int)$selectedIndex, $meters)) { + continue; + } + $label = $this->GetMeterLabel($meters[(int)$selectedIndex], (int)$selectedIndex); + $tags .= '' + . '' + . '' . $this->h($label) . '' + . '' + . ''; + } + + return '
' + . '' + . '
' . $tags . '
' + . '
'; + } + + private function BuildMeterOptions(array $meters) + { + if (empty($meters)) { + return ''; + } + + $options = ''; + foreach ($meters as $index => $meter) { + $key = (string)$index; + $label = $this->GetMeterLabel($meter, $index); + $assigned = trim((string)($meter['user_id'] ?? '')) !== ''; + $options .= ''; + } + return $options; + } + + private function GetMeterLabel(array $meter, $index) + { + $id = trim((string)($meter['id'] ?? '')); + $name = trim((string)($meter['name'] ?? '')); + $type = trim((string)($meter['meter_type'] ?? '')); + + if ($name === '' && $id === '') { + $label = 'Zaehler ' . ((int)$index + 1); + } elseif ($name === '') { + $label = $id; + } else { + $label = $name; + if ($id !== '' && $id !== $name) { + $label .= ' [' . $id . ']'; + } + } + + if ($type !== '') { + $label .= ' - ' . $type; + } + return $label; + } + + private function SaveUsersTableFromPost(array $post) + { + $ids = $post['user_id'] ?? []; + $names = $post['user_name'] ?? []; + $addresses = $post['user_address'] ?? []; + $cities = $post['user_city'] ?? []; + $rowKeys = $post['row_key'] ?? []; + $powerAssign = $post['power_assign'] ?? []; + $waterAssign = $post['water_assign'] ?? []; + + if (!is_array($ids) || !is_array($names) || !is_array($addresses) || !is_array($cities) || !is_array($rowKeys)) { + throw new Exception('Ungueltige Benutzerwerte.'); + } + if (!is_array($powerAssign)) { + $powerAssign = []; + } + if (!is_array($waterAssign)) { + $waterAssign = []; + } + + $existingUsers = []; + foreach ($this->ReadUsers() as $existingUser) { + if (!is_array($existingUser)) { + continue; + } + $existingId = (string)($existingUser['id'] ?? ''); + if ($existingId !== '') { + $existingUsers[$existingId] = $existingUser; + } + } + + $powerMeters = $this->ReadPowerMeters(); + $waterMeters = $this->ReadWaterMeters(); + foreach ($powerMeters as &$powerMeter) { + unset($powerMeter['user_id']); + } + unset($powerMeter); + foreach ($waterMeters as &$waterMeter) { + unset($waterMeter['user_id']); + } + unset($waterMeter); + + $users = []; + $seenUsers = []; + foreach (array_keys($existingUsers) as $existingId) { + $seenUsers[(string)$existingId] = false; + } + $assignedPower = []; + $assignedWater = []; + $count = max(count($ids), count($names), count($addresses), count($cities), count($rowKeys)); + + for ($i = 0; $i < $count; $i++) { + $rowKey = (string)($rowKeys[$i] ?? $i); + $id = trim((string)($ids[$i] ?? '')); + $name = trim((string)($names[$i] ?? '')); + $address = trim((string)($addresses[$i] ?? '')); + $city = trim((string)($cities[$i] ?? '')); + $selectedPower = $this->NormalizeMeterSelection($powerAssign[$rowKey] ?? []); + $selectedWater = $this->NormalizeMeterSelection($waterAssign[$rowKey] ?? []); + + if ($id === '' && $name === '' && $address === '' && $city === '' && empty($selectedPower) && empty($selectedWater)) { + continue; + } + if ($id === '') { + $id = $this->GenerateUniqueUserId($seenUsers); + } + if (isset($seenUsers[$id]) && $seenUsers[$id] === true) { + throw new Exception('Benutzer-ID doppelt: ' . $id); + } + $seenUsers[$id] = true; + + $user = $existingUsers[$id] ?? []; + $user['id'] = $id; + $user['name'] = $name; + $user['address'] = $address; + $user['city'] = $city; + $users[] = $user; + + foreach ($selectedPower as $meterIndex) { + if (!array_key_exists($meterIndex, $powerMeters)) { + throw new Exception('Stromzaehler nicht gefunden.'); + } + if (isset($assignedPower[$meterIndex])) { + throw new Exception('Stromzaehler mehrfach zugewiesen: ' . $this->GetMeterLabel($powerMeters[$meterIndex], $meterIndex)); + } + $powerMeters[$meterIndex]['user_id'] = $id; + $assignedPower[$meterIndex] = true; + } + + foreach ($selectedWater as $meterIndex) { + if (!array_key_exists($meterIndex, $waterMeters)) { + throw new Exception('Nebenzaehler nicht gefunden.'); + } + if (isset($assignedWater[$meterIndex])) { + throw new Exception('Nebenzaehler mehrfach zugewiesen: ' . $this->GetMeterLabel($waterMeters[$meterIndex], $meterIndex)); + } + $waterMeters[$meterIndex]['user_id'] = $id; + $assignedWater[$meterIndex] = true; + } + } + + $this->WriteUsersAndMeterAssignments($users, $powerMeters, $waterMeters); + return count($users) . ' Benutzer gespeichert.'; + } + + private function GenerateUniqueUserId(array $usedIds) + { + for ($i = 0; $i < 100; $i++) { + $id = (string)random_int(100000, 999999999); + if (!isset($usedIds[$id])) { + return $id; + } + } + + do { + $id = (string)time() . (string)random_int(1000, 9999); + } while (isset($usedIds[$id])); + + return $id; + } + + private function NormalizeMeterSelection($selection) + { + if (!is_array($selection)) { + $selection = [$selection]; + } + + $result = []; + foreach ($selection as $value) { + $value = trim((string)$value); + if ($value === '') { + continue; + } + if (!ctype_digit($value)) { + throw new Exception('Ungueltige Zaehlerauswahl.'); + } + $result[] = (int)$value; + } + return array_values(array_unique($result)); + } + + private function ValidateBillingConfiguration(array $users, array $powerMeters, array $waterMeters, array $tariffs, $from, $to) + { + $errors = []; + $from = (int)$from; + $to = (int)$to; + + if ($from <= 0 || $to <= 0 || $to <= $from) { + $errors[] = 'Abrechnungszeitraum ist ungueltig.'; + } + + $userIds = []; + foreach ($users as $index => $user) { + if (!is_array($user)) { + continue; + } + $id = trim((string)($user['id'] ?? '')); + if ($id === '') { + $errors[] = 'Benutzer ohne interne ID gefunden.'; + continue; + } + if (isset($userIds[$id])) { + $errors[] = 'Benutzer-ID doppelt: ' . $id; + } + $userIds[$id] = true; + } + + if (empty($userIds)) { + $errors[] = 'Keine Benutzer konfiguriert.'; + } + + $hasAnyMeter = false; + $hasPowerConsumption = false; + $hasPowerFeed = false; + foreach ($powerMeters as $index => $meter) { + if (!is_array($meter)) { + continue; + } + $hasAnyMeter = true; + $userId = trim((string)($meter['user_id'] ?? '')); + if ($userId === '' || !isset($userIds[$userId])) { + $errors[] = 'Stromzaehler nicht zugewiesen: ' . $this->GetMeterLabel($meter, $index); + } + if (!empty($meter['var_consumption'])) { + $hasPowerConsumption = true; + } + if (!empty($meter['var_feed'])) { + $hasPowerFeed = true; + } + } + + $requiredAdditionalTariffs = []; + foreach ($waterMeters as $index => $meter) { + if (!is_array($meter)) { + continue; + } + $hasAnyMeter = true; + $userId = trim((string)($meter['user_id'] ?? '')); + if ($userId === '' || !isset($userIds[$userId])) { + $errors[] = 'Nebenzaehler nicht zugewiesen: ' . $this->GetMeterLabel($meter, $index); + } + $type = trim((string)($meter['meter_type'] ?? 'Warmwasser')); + if ($type === '') { + $type = 'Warmwasser'; + } + $requiredAdditionalTariffs[$type] = true; + } + + if (!$hasAnyMeter) { + $errors[] = 'Keine Zaehler konfiguriert.'; + } + + if ($from > 0 && $to > $from) { + if ($hasPowerConsumption || $hasPowerFeed) { + $this->RequireTariffCoverage($tariffs, 'Netztarif', $from, $to, $errors); + } + if ($hasPowerFeed) { + $this->RequireTariffCoverage($tariffs, 'Solartarif', $from, $to, $errors); + $this->RequireTariffCoverage($tariffs, 'Einspeisetarif', $from, $to, $errors); + } + foreach (array_keys($requiredAdditionalTariffs) as $type) { + $this->RequireTariffCoverage($tariffs, $type, $from, $to, $errors); + } + } + + if (!empty($errors)) { + throw new Exception(implode(' ', array_values(array_unique($errors)))); + } + } + + private function RequireTariffCoverage(array $tariffs, $type, $from, $to, array &$errors) + { + $gap = $this->GetTariffCoverageGap($tariffs, $type, (int)$from, (int)$to); + if ($gap === null) { + return; + } + + $errors[] = 'Tarif fehlt durchgehend: ' . $type . ' (' . date('d.m.Y H:i', $gap[0]) . ' - ' . date('d.m.Y H:i', $gap[1]) . ').'; + } + + private function GetTariffCoverageGap(array $tariffs, $type, $from, $to) + { + $periodStart = (int)$from; + $periodEnd = (int)$to; + if ($periodEnd > $periodStart) { + $periodEnd--; + } + + if ($periodStart <= 0 || $periodEnd < $periodStart) { + return [$periodStart, $periodEnd]; + } + + $intervals = []; + foreach ($tariffs as $tariff) { + if (!is_array($tariff)) { + continue; + } + if (strtolower(trim((string)($tariff['unit_type'] ?? ''))) !== strtolower(trim((string)$type))) { + continue; + } + + $start = $this->toUnixTs($tariff['start'] ?? 0, false); + $end = $this->toUnixTs($tariff['end'] ?? 0, true); + if ($start === null || $end === null || $end < $periodStart || $start > $periodEnd) { + continue; + } + + $intervals[] = [ + max($periodStart, (int)$start), + min($periodEnd, (int)$end) + ]; + } + + usort($intervals, function ($a, $b) { + return $a[0] <=> $b[0]; + }); + + $cursor = $periodStart; + foreach ($intervals as $interval) { + if ($interval[0] > $cursor) { + return [$cursor, min($periodEnd, $interval[0] - 1)]; + } + if ($interval[1] >= $cursor) { + $cursor = $interval[1] + 1; + } + if ($cursor > $periodEnd) { + return null; + } + } + + return [$cursor, $periodEnd]; + } + private function FormatDateInput($timestamp) { return $timestamp ? date('Y-m-d', (int)$timestamp) : ''; @@ -655,6 +1253,7 @@ form.onsubmit=function(){ try { $pdfContent = $this->GenerateInvoices(); if (!$pdfContent) { + SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung Fehler'); echo 'Abrechnung Fehler'; return; } @@ -664,8 +1263,9 @@ form.onsubmit=function(){ SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung erfolgreich erstellt'); echo 'Abrechnung erfolgreich erstellt'; } catch (Throwable $e) { - SetValue($this->GetIDForIdent('LastResult'), 'Abrechnung Fehler'); - echo 'Abrechnung Fehler: ' . $e->getMessage(); + $message = 'Abrechnung Fehler: ' . $e->getMessage(); + SetValue($this->GetIDForIdent('LastResult'), $message); + echo $message; } break; } @@ -674,22 +1274,38 @@ form.onsubmit=function(){ public function ProcessHookData() { $message = ''; + $postMessageType = 'belevoTariffSave'; try { - $payload = $_POST['payload'] ?? ''; - if (trim((string)$payload) !== '') { - $message = $this->SaveTariffTableFromPayload($payload); + $editor = (string)($_POST['editor'] ?? 'tariffs'); + if ($editor === 'users') { + $postMessageType = 'belevoUserSave'; + $message = $this->SaveUsersTableFromPost($_POST); + SetValue($this->GetIDForIdent('VisuUserStatus'), $message); + $this->UpdateUserOverview(); } else { - $message = $this->SaveTariffTableFromPost($_POST); + $payload = $_POST['payload'] ?? ''; + if (trim((string)$payload) !== '') { + $message = $this->SaveTariffTableFromPayload($payload); + } else { + $message = $this->SaveTariffTableFromPost($_POST); + } + SetValue($this->GetIDForIdent('VisuTariffStatus'), $message); + $this->UpdateTariffOverview(); } - SetValue($this->GetIDForIdent('VisuTariffStatus'), $message); - $this->UpdateTariffOverview(); } catch (Throwable $e) { - $message = 'Fehler beim Speichern der Tarifliste: ' . $e->getMessage(); - SetValue($this->GetIDForIdent('VisuTariffStatus'), $message); + if ($postMessageType === 'belevoUserSave') { + $message = 'Fehler beim Speichern der Benutzerliste: ' . $e->getMessage(); + SetValue($this->GetIDForIdent('VisuUserStatus'), $message); + $this->UpdateUserOverview(); + } else { + $message = 'Fehler beim Speichern der Tarifliste: ' . $e->getMessage(); + SetValue($this->GetIDForIdent('VisuTariffStatus'), $message); + $this->UpdateTariffOverview(); + } } header('Content-Type: text/html; charset=utf-8'); - echo '' . $this->h($message) . ''; + echo '' . $this->h($message) . ''; } // ====================== PDF-Erstellung ====================== @@ -709,6 +1325,8 @@ public function GenerateInvoices() $water = is_array($water) ? $water : []; $tariffs = is_array($tariffs) ? $tariffs : []; + $this->ValidateBillingConfiguration($users, $power, $water, $tariffs, (int)$from, (int)$to); + if (!class_exists('TCPDF')) { return false; }