diff --git a/Layout/default/menu.php b/Layout/default/menu.php index afd636c90..a3dfc4035 100644 --- a/Layout/default/menu.php +++ b/Layout/default/menu.php @@ -144,6 +144,8 @@ can("RMLCompany")): ?>
  • "> Arbeitsaufträge
  • can("RMLAdmin")): ?>
  • "> Arbeitsaufträge-Management
  • + can("WorkorderMph")): ?>
  • "> MPH Arbeitsaufträge
  • + can("WorkorderMphAdmin")): ?>
  • "> MPH Arbeitsaufträge Verwaltung
  • diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 09ac74297..5af86bb65 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -17,14 +17,10 @@ class RadiusController extends mfBaseController { protected function indexAction() { $this->layout()->set('additionalJS', ["plugins/chart.js/chart.4.4.6.js", "plugins/chart.js/chartjs-adapter-moment.min.js"]); - $allowedAcsUserIds = [9, 13, 25, 65, 135, 145, 178]; - $acsEnabled = in_array($this->me->id, $allowedAcsUserIds); - Helper::renderVue3($this, $this->mod, "Radius", [ 'CAN_BILLING' => $this->me->can("Billing"), 'HIDE_PAGE_TITLE' => true, 'USER_ID' => $this->me->id, - 'ACS_ENABLED' => $acsEnabled ]); } @@ -286,13 +282,14 @@ class RadiusController extends mfBaseController { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; - $this->log->debug("genieacsRemoteAccessAction", ['deviceId' => $deviceId]); + $forceRecreate = $input['forceRecreate'] ?? false; + $this->log->debug("genieacsRemoteAccessAction", ['deviceId' => $deviceId, 'forceRecreate' => $forceRecreate]); if (!$deviceId) self::sendError("Device ID is required"); - + $acs = $this->getGenieACS(); - $result = $acs->createRemoteUser($deviceId); - + $result = $acs->createRemoteUser($deviceId, $forceRecreate); + if ($result) { self::returnJson(['success' => true] + $result); } else { @@ -304,19 +301,71 @@ class RadiusController extends mfBaseController { } } + protected function genieacsEventLogAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $deviceId = $input['deviceId'] ?? null; + $this->log->debug("genieacsEventLogAction", ['deviceId' => $deviceId]); + + if (!$deviceId) self::sendError("Device ID is required"); + + $acs = $this->getGenieACS(); + $creds = $acs->createRemoteUser($deviceId); + + if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); + + $url = "http://acs.xinon.at:5000/read-fritz-eventlog"; + $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + + $data = json_encode([ + 'fritz_ip' => $creds['ip'], + 'fritz_port' => "9090", + 'fritz_user' => $creds['username'], + 'fritz_pass' => $creds['password'] + ]); + + $opts = [ + "http" => [ + "method" => "POST", + "header" => "Content-Type: application/json\r\n" . + "X-API-Key: " . $apiKey . "\r\n" . + "Content-Length: " . strlen($data) . "\r\n", + "content" => $data, + "timeout" => 60 + ] + ]; + + $context = stream_context_create($opts); + $response = file_get_contents($url, false, $context); + + if ($response) { + $json = json_decode($response, true); + if ($json && isset($json['data'])) { + self::returnJson(['success' => true, 'events' => $json['data']]); + return; + } + } + + self::sendError("Failed to fetch event log"); + } catch (Exception $e) { + $this->log->debug("Event Log Error", ['error' => $e->getMessage()]); + self::sendError("Error: " . $e->getMessage()); + } + } + protected function genieacsNetworkStructureAction() { try { $input = json_decode(file_get_contents('php://input'), true); $deviceId = $input['deviceId'] ?? null; $this->log->debug("genieacsNetworkStructureAction", ['deviceId' => $deviceId]); - + if (!$deviceId) self::sendError("Device ID is required"); - + $acs = $this->getGenieACS(); $creds = $acs->createRemoteUser($deviceId); - + if (!$creds) self::sendError("Could not obtain credentials for FritzBox"); - + $url = "http://acs.xinon.at:5000/read-fritz"; $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; diff --git a/application/WorkorderMphAdmin/WorkorderMphAdminController.php b/application/WorkorderMphAdmin/WorkorderMphAdminController.php index 1e7571a11..416d01d6d 100644 --- a/application/WorkorderMphAdmin/WorkorderMphAdminController.php +++ b/application/WorkorderMphAdmin/WorkorderMphAdminController.php @@ -8,11 +8,13 @@ class WorkorderMphAdminController extends WorkorderMphBaseController protected array $columns = [ ['key' => 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true, 'filter' => 'numberRange']], + ['key' => 'netOwnerId', 'text' => 'Netzeigentümer', 'modal' => false, 'table' => ['filter' => 'select', 'sortable' => false], 'required' => false], ['key' => 'hausnummerInfo', 'text' => 'Adresse', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], - ['key' => 'netzgebietName', 'text' => 'Netzgebiet', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], - ['key' => 'companyName', 'text' => 'Firma', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], - ['key' => 'wohneinheitCount', 'text' => 'WE', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'netzgebietName', 'text' => 'Netzgebiet', 'modal' => false, 'table' => ['filter' => 'select', 'sortable' => false]], + ['key' => 'rimoFcpName', 'text' => 'FCP', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'companyName', 'text' => 'Firma', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'wohneinheitCount', 'text' => 'WE', 'modal' => false, 'table' => ['sortable' => true, 'filter' => 'numberRange']], + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => false]], ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], ]; @@ -21,11 +23,49 @@ class WorkorderMphAdminController extends WorkorderMphBaseController { $hausnummerInfoColIdx = array_search('hausnummerInfo', array_column($this->columns, 'key')); array_splice($this->columns, $hausnummerInfoColIdx + 1, 0, [$this->statusColumn]); + + // Handle netOwnerId column - only visible for admins + $netOwnerColIdx = array_search('netOwnerId', array_column($this->columns, 'key')); + if ($netOwnerColIdx !== false) { + if ($this->user->isAdmin()) { + $netOwners = Helper::getMphNetworkOwners(); + $this->columns[$netOwnerColIdx]['table']['filterOptions'] = array_map(fn($o) => ['value' => $o->id, 'text' => $o->company], $netOwners); + } else { + $this->columns[$netOwnerColIdx]['table'] = false; + } + } + + // Populate netzgebiet filter options + $netzgebietColIdx = array_search('netzgebietName', array_column($this->columns, 'key')); + if ($netzgebietColIdx !== false) { + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + + // Apply network ownership filtering + $netzgebietFilter = ""; + if (!$this->user->isAdmin()) { + $allowedNetzgebietIds = Helper::getADBNetworksFromUser($this->user); + if (!empty($allowedNetzgebietIds)) { + $escapedIds = array_map(fn($id) => $db->escape($id), $allowedNetzgebietIds); + $netzgebietFilter = " AND ng.id IN (" . implode(',', $escapedIds) . ")"; + } + } + + $fronkDbName = FRONKDB_DBNAME; + $sql = "SELECT DISTINCT ng.id, ng.name FROM Netzgebiet ng + INNER JOIN Hausnummer hn ON ng.id = hn.netzgebiet_id + INNER JOIN `$fronkDbName`.`WorkorderMph` wm ON wm.hausnummerId = hn.id + WHERE ng.name IS NOT NULL AND ng.name != '' + $netzgebietFilter + ORDER BY ng.name ASC"; + $result = $db->query($sql); + $netzgebiete = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + $this->columns[$netzgebietColIdx]['table']['filterOptions'] = array_map(fn($ng) => ['value' => $ng['id'], 'text' => $ng['name']], $netzgebiete); + } } public function indexAction() { - $this->createWorkordersFromHausnummer(); + // Note: Workorder creation is now handled by cronjob script: scripts/workorder-mph-create-from-hausnummer.php parent::indexAction(); } @@ -41,6 +81,18 @@ class WorkorderMphAdminController extends WorkorderMphBaseController $whereClauses = "WHERE 1=1"; + // Apply network ownership filtering (similar to WorkorderAdmin) + if (!$this->user->isAdmin()) { + $allowedNetzgebietIds = Helper::getADBNetworksFromUser($this->user); + if (!empty($allowedNetzgebietIds)) { + $escapedIds = array_map(fn($id) => $db->escape($id), $allowedNetzgebietIds); + $whereClauses .= " AND hn.netzgebiet_id IN (" . implode(',', $escapedIds) . ")"; + } else { + // User has no networks assigned, show no results + $whereClauses .= " AND 1=0"; + } + } + if (empty($filters['status'])) { $whereClauses .= " AND w.status NOT IN ('completed', 'cancelled', 'archived')"; } else { @@ -48,12 +100,15 @@ class WorkorderMphAdminController extends WorkorderMphBaseController } if (!empty($filters['id'])) $whereClauses .= Helper::generateFilterCondition($filters['id'], 'w.id', true); + if (!empty($filters['netOwnerId'])) $whereClauses .= Helper::generateFilterCondition($filters['netOwnerId'], 'n.owner_id'); if (!empty($filters['hausnummerInfo'])) { $searchColumns = "str.name|hn.hausnummer|hn.stiege|plz.plz|ort.name|w.additionalInfo"; $whereClauses .= Helper::generateFilterCondition($filters['hausnummerInfo'], $searchColumns); } - if (!empty($filters['netzgebietName'])) $whereClauses .= Helper::generateFilterCondition($filters['netzgebietName'], 'ng.name'); + if (!empty($filters['netzgebietName'])) $whereClauses .= Helper::generateFilterCondition($filters['netzgebietName'], 'ng.id'); + if (!empty($filters['rimoFcpName'])) $whereClauses .= Helper::generateFilterCondition($filters['rimoFcpName'], 'hn.rimo_fcp_name'); if (!empty($filters['companyName'])) $whereClauses .= Helper::generateFilterCondition($filters['companyName'], 'c.name'); + if (!empty($filters['wohneinheitCount'])) $whereClauses .= Helper::generateFilterCondition($filters['wohneinheitCount'], '(SELECT COUNT(*) FROM `' . $addressDbName . '`.`Wohneinheit` we WHERE we.hausnummer_id = hn.id)', true); if (!empty($filters['deadlineDate'])) $whereClauses .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); if (!empty($filters['additionalInfo'])) $whereClauses .= Helper::generateFilterCondition($filters['additionalInfo'], 'w.additionalInfo'); @@ -63,7 +118,9 @@ class WorkorderMphAdminController extends WorkorderMphBaseController IFNULL(c.name, 'Nicht zugewiesen') as companyName, CONCAT_WS(' ', str.name, hn.hausnummer, hn.stiege) as hausnummerInfo, str.name as street, hn.hausnummer, hn.stiege, plz.plz, ort.name as city, - IFNULL(ng.name, '-') as netzgebietName, + ng.id as netzgebietName, + n.owner_id as netOwnerId, + hn.rimo_fcp_name as rimoFcpName, (SELECT COUNT(*) FROM `$addressDbName`.`Wohneinheit` we WHERE we.hausnummer_id = hn.id) as wohneinheitCount FROM `$fronkDbName`.`WorkorderMph` w LEFT JOIN `$fronkDbName`.`WorkorderCompany` c ON w.companyId = c.id @@ -72,12 +129,13 @@ class WorkorderMphAdminController extends WorkorderMphBaseController LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id + LEFT JOIN `$fronkDbName`.`Network` n ON n.adb_netzgebiet_id = ng.id $whereClauses "; $orderBy = ""; if (!empty($order['key'])) { - $sortableColumns = ['id', 'status', 'deadlineDate', 'companyName', 'additionalInfo', 'appointmentDate', 'netzgebietName']; + $sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate', 'wohneinheitCount']; if (in_array($order['key'], $sortableColumns)) { $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; $orderBy = " ORDER BY " . $db->escape($order['key']) . " " . $sortOrder; @@ -95,8 +153,9 @@ class WorkorderMphAdminController extends WorkorderMphBaseController LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id + LEFT JOIN `$fronkDbName`.`Network` n ON n.adb_netzgebiet_id = ng.id $whereClauses"; - $totalCount = $db->query($countSql)->fetch_assoc()['count']; + $totalCount = (int)$db->query($countSql)->fetch_assoc()['count']; // Add pagination if ($pagination['per_page'] !== null) { @@ -109,10 +168,10 @@ class WorkorderMphAdminController extends WorkorderMphBaseController self::returnJson([ 'rows' => $rows, 'pagination' => [ - 'page' => $pagination['page'], - 'per_page' => $pagination['per_page'], + 'page' => (int)$pagination['page'], + 'per_page' => (int)$pagination['per_page'], 'total_rows' => $totalCount, - 'total_pages' => ceil($totalCount / $pagination['per_page']), + 'total_pages' => (int)ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount ] ]); diff --git a/application/WorkorderMphBase/WorkorderMphBaseController.php b/application/WorkorderMphBase/WorkorderMphBaseController.php index b78a02b08..0c9c7e25e 100644 --- a/application/WorkorderMphBase/WorkorderMphBaseController.php +++ b/application/WorkorderMphBase/WorkorderMphBaseController.php @@ -3,7 +3,7 @@ class WorkorderMphBaseController extends TTCrud { protected array $statusColumn = [ - 'key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'filterOptions' => [ + 'key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', 'sortable' => false, 'filterOptions' => [ ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], ['value' => 'scheduled', 'text' => 'Geplant', 'icon' => 'fas fa-calendar-check text-warning'], @@ -523,7 +523,10 @@ class WorkorderMphBaseController extends TTCrud $newValue = $post[$field] ? 1 : 0; if ($oldValue !== $newValue) { $workorder->$field = $newValue; - $changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein'); + // Only log changes where newValue is 'ja' or oldValue was 'ja' (changing from yes to no) + if ($newValue === 1 || $oldValue === 1) { + $changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein'); + } // Check for FTTx Location mit Leerrohr versorgt if ($field === 'fttxLocationSupplied' && $newValue === 1) { diff --git a/application/WorkorderMphCompany/WorkorderMphCompanyController.php b/application/WorkorderMphCompany/WorkorderMphCompanyController.php index 6ba353c0a..6e52b88ca 100644 --- a/application/WorkorderMphCompany/WorkorderMphCompanyController.php +++ b/application/WorkorderMphCompany/WorkorderMphCompanyController.php @@ -7,10 +7,11 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController protected array $permissionCheck = ['RMLCompany']; protected array $columns = [ - ['key' => 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], - ['key' => 'hausnummerInfo', 'text' => 'Adresse', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'wohneinheitCount', 'text' => 'WE', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true, 'filter' => 'numberRange']], + ['key' => 'hausnummerInfo', 'text' => 'Adresse', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'netzgebietName', 'text' => 'Netzgebiet', 'modal' => false, 'table' => ['filter' => 'select', 'sortable' => false]], + ['key' => 'rimoFcpName', 'text' => 'FCP', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => false]], ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], ]; @@ -23,6 +24,22 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); $this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0; + + // Populate netzgebiet filter options for this company's workorders + $netzgebietColIdx = array_search('netzgebietName', array_column($this->columns, 'key')); + if ($netzgebietColIdx !== false && $company) { + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + $fronkDbName = FRONKDB_DBNAME; + + $sql = "SELECT DISTINCT ng.id, ng.name FROM Netzgebiet ng + INNER JOIN Hausnummer hn ON ng.id = hn.netzgebiet_id + INNER JOIN `$fronkDbName`.`WorkorderMph` wm ON wm.hausnummerId = hn.id + WHERE ng.name IS NOT NULL AND ng.name != '' AND wm.companyId = " . intval($company->id) . " + ORDER BY ng.name ASC"; + $result = $db->query($sql); + $netzgebiete = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + $this->columns[$netzgebietColIdx]['table']['filterOptions'] = array_map(fn($ng) => ['value' => $ng['id'], 'text' => $ng['name']], $netzgebiete); + } } protected function getAction() @@ -54,6 +71,8 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController $searchColumns = "str.name|hn.hausnummer|hn.stiege|plz.plz|ort.name|w.additionalInfo"; $whereClauses .= Helper::generateFilterCondition($filters['hausnummerInfo'], $searchColumns); } + if (!empty($filters['netzgebietName'])) $whereClauses .= Helper::generateFilterCondition($filters['netzgebietName'], 'ng.id'); + if (!empty($filters['rimoFcpName'])) $whereClauses .= Helper::generateFilterCondition($filters['rimoFcpName'], 'hn.rimo_fcp_name'); if (!empty($filters['deadlineDate'])) $whereClauses .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); if (!empty($filters['appointmentDate'])) $whereClauses .= Helper::generateFilterCondition($filters['appointmentDate'], 'w.appointmentDate'); if (!empty($filters['additionalInfo'])) $whereClauses .= Helper::generateFilterCondition($filters['additionalInfo'], 'w.additionalInfo'); @@ -63,18 +82,21 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController w.id, w.status, w.deadlineDate, w.appointmentDate, w.additionalInfo, CONCAT_WS(' ', str.name, hn.hausnummer, hn.stiege) as hausnummerInfo, str.name as street, hn.hausnummer, hn.stiege, plz.plz, ort.name as city, + ng.id as netzgebietName, + hn.rimo_fcp_name as rimoFcpName, (SELECT COUNT(*) FROM `$addressDbName`.`Wohneinheit` we WHERE we.hausnummer_id = hn.id) as wohneinheitCount FROM `$fronkDbName`.`WorkorderMph` w LEFT JOIN `$addressDbName`.`Hausnummer` hn ON w.hausnummerId = hn.id LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id + LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id $whereClauses "; $orderBy = ""; if (!empty($order['key'])) { - $sortableColumns = ['id', 'status', 'deadlineDate', 'additionalInfo', 'appointmentDate']; + $sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate']; if (in_array($order['key'], $sortableColumns)) { $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; $orderBy = " ORDER BY " . $db->escape($order['key']) . " " . $sortOrder; @@ -90,8 +112,9 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id + LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id $whereClauses"; - $totalCount = $db->query($countSql)->fetch_assoc()['count']; + $totalCount = (int)$db->query($countSql)->fetch_assoc()['count']; // Add pagination if ($pagination['per_page'] !== null) { @@ -104,10 +127,10 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController self::returnJson([ 'rows' => $rows, 'pagination' => [ - 'page' => $pagination['page'], - 'per_page' => $pagination['per_page'], + 'page' => (int)$pagination['page'], + 'per_page' => (int)$pagination['per_page'], 'total_rows' => $totalCount, - 'total_pages' => ceil($totalCount / $pagination['per_page']), + 'total_pages' => (int)ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount ] ]); @@ -190,14 +213,6 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController $workorder = WorkorderMphModel::get($this->postData['workorderId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - // Validate that all required Wohneinheiten have notes - $wohneinheiten = WorkorderMphWohneinheitModel::getAll(['workorderMphId' => $workorder->id]); - foreach ($wohneinheiten as $we) { - if (empty($we->note)) { - self::sendError("Bitte fügen Sie für jede Wohneinheit eine Notiz hinzu, bevor Sie den Auftrag abschließen."); - } - } - $oldStatus = $workorder->status; $workorder->status = 'documented'; WorkorderMphModel::update((array)$workorder); @@ -253,4 +268,34 @@ class WorkorderMphCompanyController extends WorkorderMphBaseController WorkorderMphDocumentationModel::delete($doc->id); self::returnJson(['success' => true, 'message' => 'Dokumentation gelöscht.']); } + + protected function updateAdditionalInfoAction() + { + if (empty($this->postData['workorderMphId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + + $workorder = WorkorderMphModel::get($this->postData['workorderMphId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + // Verify company access + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + if (!$company || $workorder->companyId != $company->id) { + self::sendError("Keine Berechtigung für diesen Arbeitsauftrag."); + } + + $oldInfo = $workorder->additionalInfo; + $newInfo = $this->postData['additionalInfo'] ?? ''; + $workorder->additionalInfo = $newInfo; + WorkorderMphModel::update((array)$workorder); + + if ($oldInfo !== $newInfo) { + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorder->id, + 'text' => "Notiz geändert: " . ($newInfo ?: '(leer)'), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + } + + self::returnJson(['success' => true, 'message' => 'Notiz aktualisiert.', 'newInfo' => $newInfo]); + } } diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php index 6454d6502..bc917678b 100644 --- a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php @@ -12,6 +12,8 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { public int $civilEngineeringDocsRequired; public int $requireCableLength; public int $requireCableType; + public int $enableWorkorder; + public int $enableWorkorderMph; public int $create; public int $createBy; diff --git a/db/migrations/20251210120000_add_workorder_mph_permissions.php b/db/migrations/20251210120000_add_workorder_mph_permissions.php new file mode 100644 index 000000000..0e45ebc33 --- /dev/null +++ b/db/migrations/20251210120000_add_workorder_mph_permissions.php @@ -0,0 +1,33 @@ +getEnvironment() == "thetool") { + $table = $this->table("WorkerPermission"); + $table->addColumn("canWorkorderMphAdmin", "enum", ["values" => 'false,true', "default" => "false", "after" => "canRMLAdmin"]); + $table->addColumn("canWorkorderMph", "enum", ["values" => 'false,true', "default" => "false", "after" => "canWorkorderMphAdmin"]); + $table->update(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if($this->getEnvironment() == "thetool") { + $this->table("WorkerPermission")->removeColumn("canWorkorderMphAdmin")->save(); + $this->table("WorkerPermission")->removeColumn("canWorkorderMph")->save(); + } + + if($this->getEnvironment() == "addressdb") { + + } + } +} diff --git a/db/migrations/20251210130000_add_workorder_tenant_config_module_flags.php b/db/migrations/20251210130000_add_workorder_tenant_config_module_flags.php new file mode 100644 index 000000000..6933ce5d9 --- /dev/null +++ b/db/migrations/20251210130000_add_workorder_tenant_config_module_flags.php @@ -0,0 +1,40 @@ +getEnvironment() == "thetool") { + $table = $this->table('WorkorderTenantConfig'); + + $table->addColumn('enableWorkorder', 'boolean', [ + 'default' => true, + 'null' => false, + 'after' => 'requireCableType', + 'comment' => 'Enable Workorder module for this tenant' + ]); + + $table->addColumn('enableWorkorderMph', 'boolean', [ + 'default' => true, + 'null' => false, + 'after' => 'enableWorkorder', + 'comment' => 'Enable WorkorderMPH module for this tenant' + ]); + + $table->update(); + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table('WorkorderTenantConfig') + ->removeColumn('enableWorkorder') + ->removeColumn('enableWorkorderMph') + ->save(); + } + } +} \ No newline at end of file diff --git a/lib/GenieACS/GenieACS.php b/lib/GenieACS/GenieACS.php index 4f2b17ae8..540026e90 100644 --- a/lib/GenieACS/GenieACS.php +++ b/lib/GenieACS/GenieACS.php @@ -146,10 +146,10 @@ class GenieACS { return self::getParam($device, $param); } - public function createRemoteUser($deviceId) { - $this->log->debug("GenieACS: createRemoteUser called", ['deviceId' => $deviceId]); + public function createRemoteUser($deviceId, $forceRecreate = false) { + $this->log->debug("GenieACS: createRemoteUser called", ['deviceId' => $deviceId, 'forceRecreate' => $forceRecreate]); $cacheKey = "remote_user_" . $deviceId; - if ($cached = $this->getCache($cacheKey)) { + if (!$forceRecreate && $cached = $this->getCache($cacheKey)) { $this->log->debug("GenieACS: Using cached credentials"); return $cached; } diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index 8126e8990..1bc28fc37 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -252,4 +252,63 @@ class Helper { return array_map(fn($owner) => new Address($owner['id']), $results); } + + /** + * Get AddressDB Netzgebiet IDs that a user has access to based on their Network ownership + * @param User $user The user to get networks for + * @return array Array of addressdb netzgebiet IDs + */ + public static function getADBNetworksFromUser($user): array { + if ($user->isAdmin()) { + // Admin has access to all networks + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + $sql = "SELECT id FROM Netzgebiet WHERE id IS NOT NULL"; + $result = $db->query($sql); + $netzgebiete = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + return array_column($netzgebiete, 'id'); + } + + // Get networks where user's address is the owner + $networks = NetworkModel::search(['owner_id' => $user->address_id]); + + // Also check user flags for additional networks + $flagNetworkIds = json_decode($user->getFlag("workordermph_networks")->value() ?: '[]', true); + if (!empty($flagNetworkIds)) { + $additionalNetworks = NetworkModel::search(['id' => $flagNetworkIds]); + $networks = array_merge($networks, $additionalNetworks); + } + + // Extract adb_netzgebiet_id from networks + $netzgebietIds = []; + foreach ($networks as $network) { + if ($network->adb_netzgebiet_id) { + $netzgebietIds[] = $network->adb_netzgebiet_id; + } + } + + return array_unique(array_filter($netzgebietIds)); + } + + /** + * Get network owners that have WorkorderMph entries (based on Netzgebiet) + * @return array Array of Address objects representing network owners + */ + public static function getMphNetworkOwners(): array { + $db = FronkDB::singleton(); + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + $fronkDbName = FRONKDB_DBNAME; + + $sql = "SELECT DISTINCT a.id, a.company, a.lastname, a.firstname + FROM `$fronkDbName`.`WorkorderMph` wm + INNER JOIN `$addressDbName`.`Hausnummer` hn ON wm.hausnummerId = hn.id + INNER JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id + INNER JOIN `$fronkDbName`.`Network` n ON n.adb_netzgebiet_id = ng.id + INNER JOIN `$fronkDbName`.`Address` a ON n.owner_id = a.id + WHERE a.id IS NOT NULL + ORDER BY a.company, a.lastname, a.firstname"; + + $results = $db->fetch_all_assoc($db->query($sql)) ?? []; + + return array_map(fn($owner) => new Address($owner['id']), $results); + } } \ No newline at end of file diff --git a/public/js/pages/Radius/RadiusRouterManager.js b/public/js/pages/Radius/RadiusRouterManager.js index 04d63be4d..47289255a 100644 --- a/public/js/pages/Radius/RadiusRouterManager.js +++ b/public/js/pages/Radius/RadiusRouterManager.js @@ -71,6 +71,10 @@ const RadiusRouterManager = { Netzwerkstruktur + @@ -157,6 +161,12 @@ const RadiusRouterManager = { +
    + +
    Ein Fehler ist aufgetreten.
    @@ -173,6 +183,34 @@ const RadiusRouterManager = {
    Keine Daten verfügbar.
    + + + +
    +
    + + + + + + + + + + + + + + + + + +
    DatumUhrzeitGruppeNachricht
    {{ event.date }}{{ event.time }}{{ event.group }}{{ event.msg }}
    +
    +
    +
    Keine Ereignisse verfügbar.
    +
    + `, data: () => ({ @@ -197,7 +235,11 @@ const RadiusRouterManager = { showNetworkStructureModal: false, networkStructureLoading: false, - rootDevice: null + rootDevice: null, + + showEventLogModal: false, + eventLogLoading: false, + eventLogData: null }), watch: { show: { @@ -353,20 +395,24 @@ const RadiusRouterManager = { }; poll(); }, - async runRemoteAccess() { + async runRemoteAccess(forceRecreate = false) { if (!this.routerDevice || !this.routerDevice.deviceId) return; this.showRemoteAccessModal = true; this.remoteAccessLoading = true; - this.remoteAccessStep = 'Konfiguriere Zugriff...'; + this.remoteAccessStep = forceRecreate ? 'Erstelle neue Zugangsdaten...' : 'Konfiguriere Zugriff...'; this.remoteAccessResult = null; try { const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRemoteAccess`, { - deviceId: this.routerDevice.deviceId + deviceId: this.routerDevice.deviceId, + forceRecreate: forceRecreate }); if (data.success) { this.remoteAccessResult = data; + if (forceRecreate) { + window.notify('success', 'Neue Zugangsdaten erstellt'); + } } else { throw new Error(data.message || "Unbekannter Fehler"); } @@ -396,6 +442,29 @@ const RadiusRouterManager = { } finally { this.networkStructureLoading = false; } + }, + async openEventLog() { + if (!this.routerDevice || !this.routerDevice.deviceId) return; + this.showEventLogModal = true; + this.eventLogLoading = true; + this.eventLogData = null; + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsEventLog`, { + deviceId: this.routerDevice.deviceId + }); + + if (data.success && data.events) { + this.eventLogData = data.events; + } else { + throw new Error(data.message || "Keine Ereignisse gefunden"); + } + } catch (error) { + console.error(error); + window.notify('error', error.response?.data?.message || 'Fehler beim Laden des Ereignisprotokolls'); + } finally { + this.eventLogLoading = false; + } } } }; diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js index 9c91cce9b..bc3e5d96a 100644 --- a/public/js/pages/Radius/RadiusUsers.js +++ b/public/js/pages/Radius/RadiusUsers.js @@ -150,7 +150,7 @@ const RadiusUsers = { data-tooltip-align="left"> - diff --git a/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js b/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js index 44cff87af..72191b6e8 100644 --- a/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js +++ b/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js @@ -4,10 +4,7 @@ Vue.component('workorder-mph-admin', {