no message
This commit is contained in:
+629
-11
@@ -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
|
||||
"<?php IPS_RequestAction(" . $this->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}
|
||||
</style>
|
||||
<form method='post' action='" . $this->h($webhookPath) . "' target='belevo_tariff_save_frame'>
|
||||
<input type='hidden' name='editor' value='tariffs'>
|
||||
<input type='hidden' name='payload' value=''>
|
||||
<table>
|
||||
<thead>
|
||||
@@ -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 = "
|
||||
<div class='belevo-users'>
|
||||
<style>
|
||||
.belevo-users{font-family:Arial,Helvetica,sans-serif;font-size:13px;color:#202124;max-width:1220px;margin-top:18px}
|
||||
.belevo-users table{width:100%;border-collapse:collapse;background:#fff}
|
||||
.belevo-users th{background:#f3f4f6;border:1px solid #d6d9de;text-align:left;padding:7px;font-weight:700}
|
||||
.belevo-users td{border:1px solid #e0e2e6;padding:5px;vertical-align:top}
|
||||
.belevo-users input,.belevo-users select{width:100%;box-sizing:border-box;border:1px solid #c7cbd1;border-radius:3px;padding:5px;font:inherit;background:#fff}
|
||||
.belevo-users option:disabled{color:#9aa0a6;background:#f1f3f4}
|
||||
.belevo-users .meter-tags{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
|
||||
.belevo-users .meter-tag{display:inline-flex;align-items:center;gap:5px;border:1px solid #c7cbd1;background:#f8f9fa;border-radius:3px;padding:3px 6px;max-width:100%}
|
||||
.belevo-users .meter-tag span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.belevo-users .meter-tag button{border:0;background:transparent;color:#9b1c1c;padding:0 2px;font-weight:700}
|
||||
.belevo-users .actions{display:flex;gap:8px;margin-top:10px;align-items:center}
|
||||
.belevo-users button{border:1px solid #9aa0a6;background:#f8f9fa;border-radius:3px;padding:6px 10px;font:inherit;cursor:pointer}
|
||||
.belevo-users button.primary{background:#1f4e79;border-color:#1f4e79;color:#fff;font-weight:700}
|
||||
.belevo-users button.danger{color:#9b1c1c;border-color:#d7a6a6;background:#fff5f5}
|
||||
.belevo-users .status{margin-left:8px;color:#4b5563}
|
||||
</style>
|
||||
<form method='post' action='" . $this->h($webhookPath) . "' target='belevo_user_save_frame'>
|
||||
<input type='hidden' name='editor' value='users'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style='width:18%'>Name</th>
|
||||
<th style='width:18%'>Adresse</th>
|
||||
<th style='width:18%'>Ort</th>
|
||||
<th style='width:19%'>Stromzaehler</th>
|
||||
<th style='width:19%'>Nebenzaehler</th>
|
||||
<th style='width:8%'>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>" . $rows . "</tbody>
|
||||
</table>
|
||||
<div class='actions'>
|
||||
<button type='button' data-action='add-user'>Zeile hinzufuegen</button>
|
||||
<button type='submit' class='primary'>Benutzer speichern</button>
|
||||
<span class='status'>" . $status . "</span>
|
||||
</div>
|
||||
</form>
|
||||
<iframe name='belevo_user_save_frame' style='display:none;width:0;height:0;border:0'></iframe>
|
||||
<template data-user-row-template>" . $template . "</template>
|
||||
<script>
|
||||
(function(){
|
||||
var root=document.currentScript.closest('.belevo-users');
|
||||
if(!root||root.dataset.bound==='1'){return;}
|
||||
root.dataset.bound='1';
|
||||
var tbody=root.querySelector('tbody');
|
||||
var status=root.querySelector('.status');
|
||||
var counter=0;
|
||||
window.addEventListener('message',function(event){
|
||||
if(event&&event.data&&event.data.type==='belevoUserSave'){
|
||||
status.textContent=event.data.message||'Gespeichert.';
|
||||
}
|
||||
});
|
||||
function bindDelete(row){
|
||||
var btn=row.querySelector('[data-action=delete-user]');
|
||||
if(btn){btn.onclick=function(){row.parentNode.removeChild(row);};}
|
||||
}
|
||||
function selectedByKind(){
|
||||
var selected={power:{},water:{}};
|
||||
Array.prototype.forEach.call(root.querySelectorAll('.meter-picker'),function(picker){
|
||||
var kind=picker.getAttribute('data-kind');
|
||||
Array.prototype.forEach.call(picker.querySelectorAll('.meter-tag'),function(tag){
|
||||
var value=tag.getAttribute('data-value');
|
||||
if(value!==''){selected[kind][value]=true;}
|
||||
});
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
function refreshPickers(){
|
||||
var selected=selectedByKind();
|
||||
Array.prototype.forEach.call(root.querySelectorAll('.meter-picker'),function(picker){
|
||||
var kind=picker.getAttribute('data-kind');
|
||||
var select=picker.querySelector('select[data-action=add-meter]');
|
||||
Array.prototype.forEach.call(select.options,function(option){
|
||||
if(option.value===''){return;}
|
||||
var base=option.getAttribute('data-label')||option.textContent;
|
||||
var assigned=!!selected[kind][option.value];
|
||||
option.disabled=assigned;
|
||||
option.textContent=assigned?base+' (vergeben)':base;
|
||||
});
|
||||
select.value='';
|
||||
});
|
||||
}
|
||||
function addMeterTag(picker,value,label){
|
||||
if(value===''){return;}
|
||||
if(picker.querySelector('.meter-tag[data-value=\"'+value+'\"]')){return;}
|
||||
var tag=document.createElement('span');
|
||||
tag.className='meter-tag';
|
||||
tag.setAttribute('data-value',value);
|
||||
var input=document.createElement('input');
|
||||
input.type='hidden';
|
||||
input.name=picker.getAttribute('data-field-name');
|
||||
input.value=value;
|
||||
var text=document.createElement('span');
|
||||
text.textContent=label;
|
||||
var remove=document.createElement('button');
|
||||
remove.type='button';
|
||||
remove.setAttribute('data-action','remove-meter');
|
||||
remove.textContent='x';
|
||||
tag.appendChild(input);
|
||||
tag.appendChild(text);
|
||||
tag.appendChild(remove);
|
||||
picker.querySelector('.meter-tags').appendChild(tag);
|
||||
refreshPickers();
|
||||
}
|
||||
Array.prototype.forEach.call(tbody.querySelectorAll('tr'),bindDelete);
|
||||
root.querySelector('[data-action=add-user]').onclick=function(){
|
||||
var key='new_'+Date.now()+'_'+(counter++);
|
||||
var tpl=root.querySelector('template[data-user-row-template]').innerHTML.replace(/__KEY__/g,key);
|
||||
var temp=document.createElement('tbody');
|
||||
temp.innerHTML=tpl;
|
||||
var row=temp.querySelector('tr');
|
||||
tbody.appendChild(row);
|
||||
bindDelete(row);
|
||||
refreshPickers();
|
||||
};
|
||||
root.addEventListener('change',function(event){
|
||||
var select=event.target;
|
||||
if(!select||select.getAttribute('data-action')!=='add-meter'){return;}
|
||||
var option=select.options[select.selectedIndex];
|
||||
if(!option||!option.value){return;}
|
||||
addMeterTag(select.closest('.meter-picker'),option.value,option.getAttribute('data-label')||option.textContent);
|
||||
});
|
||||
root.addEventListener('click',function(event){
|
||||
var btn=event.target;
|
||||
if(!btn||btn.getAttribute('data-action')!=='remove-meter'){return;}
|
||||
var tag=btn.closest('.meter-tag');
|
||||
if(tag&&tag.parentNode){tag.parentNode.removeChild(tag);}
|
||||
refreshPickers();
|
||||
});
|
||||
root.querySelector('form').onsubmit=function(){
|
||||
status.textContent='Speichern gesendet.';
|
||||
return true;
|
||||
};
|
||||
refreshPickers();
|
||||
})();
|
||||
</script>
|
||||
</div>";
|
||||
|
||||
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 '<tr>'
|
||||
. '<td><input type="hidden" name="user_id[]" value="' . $this->h($userId) . '"><input type="hidden" name="row_key[]" value="' . $this->h($rowKey) . '"><input name="user_name[]" value="' . $this->h($user['name'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_address[]" value="' . $this->h($user['address'] ?? '') . '"></td>'
|
||||
. '<td><input name="user_city[]" value="' . $this->h($user['city'] ?? '') . '"></td>'
|
||||
. '<td>' . $this->BuildMeterPicker('power', 'power_assign', $rowKey, $powerMeters, $powerSelected) . '</td>'
|
||||
. '<td>' . $this->BuildMeterPicker('water', 'water_assign', $rowKey, $waterMeters, $waterSelected) . '</td>'
|
||||
. '<td><button type="button" class="danger" data-action="delete-user">Loeschen</button></td>'
|
||||
. '</tr>';
|
||||
}
|
||||
|
||||
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 .= '<span class="meter-tag" data-value="' . $this->h($selectedIndex) . '">'
|
||||
. '<input type="hidden" name="' . $this->h($inputName) . '" value="' . $this->h($selectedIndex) . '">'
|
||||
. '<span>' . $this->h($label) . '</span>'
|
||||
. '<button type="button" data-action="remove-meter">x</button>'
|
||||
. '</span>';
|
||||
}
|
||||
|
||||
return '<div class="meter-picker" data-kind="' . $this->h($kind) . '" data-field-name="' . $this->h($inputName) . '">'
|
||||
. '<select data-action="add-meter">'
|
||||
. '<option value="">Zaehler waehlen...</option>'
|
||||
. $this->BuildMeterOptions($meters)
|
||||
. '</select>'
|
||||
. '<div class="meter-tags">' . $tags . '</div>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
private function BuildMeterOptions(array $meters)
|
||||
{
|
||||
if (empty($meters)) {
|
||||
return '<option value="" disabled>Keine Zaehler</option>';
|
||||
}
|
||||
|
||||
$options = '';
|
||||
foreach ($meters as $index => $meter) {
|
||||
$key = (string)$index;
|
||||
$label = $this->GetMeterLabel($meter, $index);
|
||||
$assigned = trim((string)($meter['user_id'] ?? '')) !== '';
|
||||
$options .= '<option value="' . $this->h($key) . '" data-label="' . $this->h($label) . '"' . ($assigned ? ' disabled' : '') . '>' . $this->h($assigned ? $label . ' (vergeben)' : $label) . '</option>';
|
||||
}
|
||||
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 '<!doctype html><html><body><script>try{parent.postMessage({type:"belevoTariffSave",message:' . json_encode($message) . '}, "*");}catch(e){}</script>' . $this->h($message) . '</body></html>';
|
||||
echo '<!doctype html><html><body><script>try{parent.postMessage({type:' . json_encode($postMessageType) . ',message:' . json_encode($message) . '}, "*");}catch(e){}</script>' . $this->h($message) . '</body></html>';
|
||||
}
|
||||
|
||||
// ====================== 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user