diff --git a/application/Radius/RadiusController.php b/application/Radius/RadiusController.php index 95a5f4f63..12b12071a 100644 --- a/application/Radius/RadiusController.php +++ b/application/Radius/RadiusController.php @@ -16,7 +16,16 @@ 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"]); - Helper::renderVue($this, $this->mod, "Radius", ['CAN_BILLING' => $this->me->can("Billing"), 'HIDE_PAGE_TITLE' => true]); + + $allowedAcsUserIds = [9, 13, 25, 65, 135, 145, 178]; + $acsEnabled = in_array($this->me->id, $allowedAcsUserIds); + + Helper::renderVue($this, $this->mod, "Radius", [ + 'CAN_BILLING' => $this->me->can("Billing"), + 'HIDE_PAGE_TITLE' => true, + 'USER_ID' => $this->me->id, + 'ACS_ENABLED' => $acsEnabled + ]); } protected function proxyUnsecureHTTPRequestToRadiusAction() { @@ -35,6 +44,49 @@ class RadiusController extends mfBaseController { die(); } + /** + * Run speedtest via ACS proxy + */ + protected function genieacsRunSpeedtestAction() { + try { + $input = json_decode(file_get_contents('php://input'), true); + $ip = $input['ip'] ?? null; + + if (!$ip) { + self::sendError("IP address is required"); + } + + $url = "http://acs.xinon.at:5000/run-speedtest"; + $apiKey = "2H9zWrgxPEJL9MZ1yTGtWh16cPCu0AsQ"; + + $data = json_encode(['ip' => $ip]); + + $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 + ] + ]; + + $context = stream_context_create($opts); + $response = file_get_contents($url, false, $context); + + if ($response === false) { + self::sendError("Failed to connect to speedtest server"); + } + + header("Content-Type: application/json"); + echo $response; + die(); + } catch (Exception $e) { + error_log("GenieACS runSpeedtest error: " . $e->getMessage()); + self::sendError("Error running speedtest: " . $e->getMessage()); + } + } + protected function sendCustomerEmailAction() { $input = json_decode(file_get_contents('php://input'), true); if (!$input || !isset($input['username'], $input['year'], $input['month'], $input['monthlySummary'], $input['monthlyDetails'], $input['recipient'])) diff --git a/application/WorkorderMphBase/WorkorderMphBaseController.php b/application/WorkorderMphBase/WorkorderMphBaseController.php index 00943af3a..b78a02b08 100644 --- a/application/WorkorderMphBase/WorkorderMphBaseController.php +++ b/application/WorkorderMphBase/WorkorderMphBaseController.php @@ -65,6 +65,81 @@ class WorkorderMphBaseController extends TTCrud self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); } + /** + * Upload documentation for the Workorder itself (not Wohneinheit). + */ + protected function uploadDocumentationAction() + { + if (empty($_FILES['files']) && empty($_FILES['file'])) self::sendError('Erforderliche Daten fehlen.'); + if (empty($_POST['workorderMphId'])) self::sendError('Workorder ID fehlt.'); + + $workorderMphId = intval($_POST['workorderMphId']); + $uploadedCount = 0; + + // Handle multiple files (files[]) + if (!empty($_FILES['files'])) { + foreach ($_FILES['files']['name'] as $index => $name) { + if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) { + // Mock the $_FILES entry for handleFormUpload + $_FILES['single_upload_file'] = [ + 'name' => $name, + 'type' => $_FILES['files']['type'][$index], + 'tmp_name' => $_FILES['files']['tmp_name'][$index], + 'error' => $_FILES['files']['error'][$index], + 'size' => $_FILES['files']['size'][$index] + ]; + try { + $uploaded = mfUpload::handleFormUpload("single_upload_file", false, "/WorkorderMph"); + WorkorderMphDocumentationModel::create([ + 'workorderMphId' => $workorderMphId, + 'fileId' => $uploaded->id, + 'description' => $_POST['description'] ?? '', + 'documentType' => $_POST['documentType'] ?? 'other', + 'create' => time(), + 'createBy' => $this->user->id + ]); + $uploadedCount++; + } catch (Exception $e) { + // Log error + } + } + } + } + // Handle single file (file) - fallback or primary if JS sends single + elseif (!empty($_FILES['file'])) { + try { + $uploaded = mfUpload::handleFormUpload("file", false, "/WorkorderMph"); + WorkorderMphDocumentationModel::create([ + 'workorderMphId' => $workorderMphId, + 'fileId' => $uploaded->id, + 'description' => $_POST['description'] ?? '', + 'documentType' => $_POST['documentType'] ?? 'other', + 'create' => time(), + 'createBy' => $this->user->id + ]); + $uploadedCount++; + } catch (Exception $e) { + self::sendError("Upload fehlgeschlagen: " . $e->getMessage()); + } + } + + if ($uploadedCount > 0) { + self::returnJson(['success' => true, 'message' => "$uploadedCount Datei(en) erfolgreich hochgeladen."]); + } else { + self::sendError("Keine Dateien wurden hochgeladen."); + } + } + + /** + * Delete Workorder documentation + */ + protected function deleteDocumentationAction() + { + if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt."); + WorkorderMphDocumentationModel::delete($this->postData['documentationId']); + self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); + } + /** * Adds a new entry to a workorder's journal. */ @@ -139,14 +214,15 @@ class WorkorderMphBaseController extends TTCrud $sql = "SELECT w.id, w.zusatz, w.tuer, w.contact, w.oaid, w.note, w.status_id, w.splice_hak_completed FROM Wohneinheit w WHERE w.hausnummer_id = $hausnummerId - ORDER BY w.zusatz"; + ORDER BY w.oaid ASC"; $result = $db->query($sql); $wohneinheiten = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; // Get Preorders for this Hausnummer to fallback contact info $preorders = []; if (class_exists('PreorderModel')) { - $preorderList = PreorderModel::search(['adb_hausnummer_id' => $workorder->hausnummerId, 'deleted' => 0]); + // Use searchActive to filter out canceled preorders (status_code = 20) + $preorderList = PreorderModel::searchActive(['adb_hausnummer_id' => $workorder->hausnummerId]); foreach ($preorderList as $preorder) { if ($preorder->adb_wohneinheit_id) { $preorders[$preorder->adb_wohneinheit_id] = $preorder; @@ -161,21 +237,30 @@ class WorkorderMphBaseController extends TTCrud $contact = $we['contact']; $preorderContact = null; $preorderUcode = null; - + if (isset($preorders[$we['id']])) { $p = $preorders[$we['id']]; $preorderUcode = $p->ucode; $pContact = trim($p->firstname . ' ' . $p->lastname); if ($p->phone) $pContact .= ' (' . $p->phone . ')'; - + $preorderContact = $pContact; - + // If address contact is empty, use preorder contact if (empty($contact)) { $contact = $pContact; } } + // Get document count for this Wohneinheit + $docCountSql = "SELECT COUNT(*) as cnt FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($we['id']); + $docCountResult = $db->query($docCountSql); + $documentCount = 0; + if ($docCountResult) { + $docCountRow = $docCountResult->fetch_assoc(); + $documentCount = intval($docCountRow['cnt']); + } + $response[] = [ 'wohneinheitId' => intval($we['id']), 'zusatz' => $we['zusatz'], @@ -187,6 +272,7 @@ class WorkorderMphBaseController extends TTCrud 'status' => intval($we['status_id']), 'spliceCompleted' => intval($we['splice_hak_completed'] ?? 0), 'note' => $we['note'], + 'documentCount' => $documentCount, ]; } @@ -214,6 +300,11 @@ class WorkorderMphBaseController extends TTCrud $tuer = $post['tuer'] ?? null; $zusatz = $post['zusatz'] ?? null; + // Validate that "Tür" field is not empty if it's being set + if ($tuer !== null && trim($tuer) === '') { + self::sendError("Das Feld 'Tür' darf nicht leer sein."); + } + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); $escapedWohneinheitId = $db->escape($wohneinheitId); @@ -315,6 +406,93 @@ class WorkorderMphBaseController extends TTCrud } } + /** + * Get documents for a specific Wohneinheit + */ + protected function getWohneinheitDocumentsAction() + { + if (empty($this->request->wohneinheitId)) self::sendError("Wohneinheit-ID fehlt."); + + $wohneinheitId = intval($this->request->wohneinheitId); + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + + $sql = "SELECT * FROM WohneinheitDocumentation WHERE wohneinheit_id = " . $db->escape($wohneinheitId) . " ORDER BY `create` ASC"; + $result = $db->query($sql); + $docs = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + + $responseDocs = []; + foreach ($docs as $doc) { + $file = new File($doc['fileId']); + $responseDocs[] = [ + 'id' => $doc['id'], + 'fileId' => $doc['fileId'], + 'fileName' => $file->orig_filename ?? $file->filename, + 'description' => $doc['description'], + 'documentType' => $doc['documentType'], + 'userName' => UserModel::getOne($doc['createBy'])->name ?? 'Unbekannt', + 'mimetype' => $file->mimetype ?? 'application/octet-stream', + 'create' => $doc['create'] + ]; + } + + self::returnJson(['docs' => $responseDocs]); + } + + /** + * Upload document for a specific Wohneinheit + */ + protected function uploadWohneinheitDocumentAction() + { + if (empty($_FILES['file']) || empty($_POST['wohneinheitId'])) { + self::sendError("Datei und Wohneinheit-ID sind erforderlich."); + } + + $wohneinheitId = intval($_POST['wohneinheitId']); + $documentType = $_POST['documentType'] ?? 'photo'; + $description = $_POST['description'] ?? null; + + // Upload file using mfUpload handleFormUpload for proper handling + try { + $upload = mfUpload::handleFormUpload("file", false, "/WorkorderMph/Wohneinheit"); + $file = $upload; // handleFormUpload returns the File object + } catch (Exception $e) { + self::sendError("Datei-Upload fehlgeschlagen: " . $e->getMessage()); + return; + } + + // Insert into WohneinheitDocumentation table in addressdb + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + $escapedWohneinheitId = $db->escape($wohneinheitId); + $escapedFileId = $db->escape($file->id); + $escapedDescription = $description ? "'" . $db->escape($description) . "'" : "NULL"; + $escapedDocumentType = "'" . $db->escape($documentType) . "'"; + $escapedCreateBy = $db->escape($this->user->id); + $escapedCreate = time(); + + $sql = "INSERT INTO WohneinheitDocumentation (wohneinheit_id, fileId, description, documentType, `create`, createBy) + VALUES ($escapedWohneinheitId, $escapedFileId, $escapedDescription, $escapedDocumentType, $escapedCreate, $escapedCreateBy)"; + $db->query($sql); + + self::returnJson(['success' => true, 'message' => 'Dokument erfolgreich hochgeladen.', 'fileId' => $file->id]); + } + + /** + * Delete document for a specific Wohneinheit + */ + protected function deleteWohneinheitDocumentAction() + { + if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt."); + + $documentationId = intval($this->postData['documentationId']); + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + + $escapedId = $db->escape($documentationId); + $sql = "DELETE FROM WohneinheitDocumentation WHERE id = $escapedId"; + $db->query($sql); + + self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); + } + /** * Update checkbox documentation fields */ @@ -327,17 +505,25 @@ class WorkorderMphBaseController extends TTCrud if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $changes = []; - $checkboxFields = ['easement', 'btb', 'fttxLocationSupplied', 'conduitToHuepLaid', 'huepMounted', 'dropCableAvailable', 'spliceCompleted']; + $checkboxFields = [ + 'easement' => 'Leitungsrecht', + 'btb' => 'Bautechnische Begehung', + 'fttxLocationSupplied' => 'FTTx Location mit Leerrohr versorgt', + 'conduitToHuepLaid' => 'Leerrohr bis HÜP/HAK verlegt', + 'huepMounted' => 'HÜP/HAK montiert', + 'dropCableAvailable' => 'Dropkabel vorhanden', + 'spliceCompleted' => 'Spleiß abgeschlossen' + ]; $updateHausnummerStatus = false; - foreach ($checkboxFields as $field) { + foreach ($checkboxFields as $field => $fieldLabel) { if (array_key_exists($field, $post)) { $oldValue = $workorder->$field; $newValue = $post[$field] ? 1 : 0; if ($oldValue !== $newValue) { $workorder->$field = $newValue; - $changes[] = "$field: " . ($newValue ? 'ja' : 'nein'); + $changes[] = "$fieldLabel: " . ($newValue ? 'ja' : 'nein'); // Check for FTTx Location mit Leerrohr versorgt if ($field === 'fttxLocationSupplied' && $newValue === 1) { @@ -374,4 +560,4 @@ class WorkorderMphBaseController extends TTCrud self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']); } //endregion -} +} \ No newline at end of file diff --git a/db/migrations/20251203080600_add_wohneinheit_documentation.php b/db/migrations/20251203080600_add_wohneinheit_documentation.php new file mode 100644 index 000000000..cfe5c637d --- /dev/null +++ b/db/migrations/20251203080600_add_wohneinheit_documentation.php @@ -0,0 +1,38 @@ +getEnvironment() == "addressdb") { + $table = $this->table('WohneinheitDocumentation', ['id' => 'id', 'primary_key' => 'id']); + if (!$table->exists()) { + $table + ->addColumn('wohneinheit_id', 'integer', ['null' => false]) + ->addColumn('fileId', 'integer', ['null' => false]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('documentType', 'string', ['limit' => 100, 'null' => false, 'default' => 'photo']) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['wohneinheit_id'], ['name' => 'wohneinheit_id_idx']) + ->addIndex(['fileId'], ['name' => 'fileId_idx']) + ->create(); + } + } + } + + public function down(): void + { + // AddressDB modifications + if ($this->getEnvironment() == "addressdb") { + $table = $this->table('WohneinheitDocumentation'); + if ($table->exists()) { + $table->drop()->save(); + } + } + } +} diff --git a/public/js/pages/Radius/Radius.css b/public/js/pages/Radius/Radius.css index 93776214e..5fc4970fa 100644 --- a/public/js/pages/Radius/Radius.css +++ b/public/js/pages/Radius/Radius.css @@ -16,10 +16,12 @@ .radius-scope .inline-copy { display:flex; align-items:center; gap:8px; justify-content: flex-end; } .radius-scope .grid { display:grid; } .radius-scope .g-2 { gap: 8px; } +.radius-scope .g-3 { gap: 12px; } .radius-scope .g-4 { gap: 16px; } .radius-scope .g-6 { gap: 24px; } .radius-scope .cols-1 { grid-template-columns: 1fr; } .radius-scope .cols-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } +.radius-scope .cols-3 { grid-template-columns: repeat(3, minmax(0,1fr)); } .radius-scope .cols-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } @media (max-width: 900px){ .radius-scope .cols-4 { grid-template-columns: repeat(2, minmax(0,1fr)); } } @media (max-width: 600px){ .radius-scope .cols-4 { grid-template-columns: 1fr; } } diff --git a/public/js/pages/Radius/RadiusUsers.js b/public/js/pages/Radius/RadiusUsers.js index 0f995ff90..5058a1deb 100644 --- a/public/js/pages/Radius/RadiusUsers.js +++ b/public/js/pages/Radius/RadiusUsers.js @@ -57,7 +57,7 @@ Vue.component('radius-users', {
{{ routerDevice.deviceInfo.manufacturer || '—' }}{{ routerDevice.deviceInfo.productClass || '—' }}{{ routerDevice.deviceInfo.hardwareVersion || '—' }}{{ routerDevice.deviceInfo.softwareVersion || '—' }}{{ routerDevice.deviceInfo.serialNumber || '—' }}{{ routerDevice.deviceId || '—' }}
-
+ {{ routerDevice.deviceInfo.hardwareVersion || '—' }}{{ routerDevice.deviceInfo.softwareVersion || '—' }}{{ routerDevice.deviceInfo.serialNumber || '—' }}{{ routerDevice.deviceId || '—' }}
+
+ {{ routerDevice.ip || '—' }}
+
+ {{ routerDevice.managementIp || '—' }}
+
+ {{ routerDevice.ip || '—' }}
-
- {{ routerDevice.managementIp || '—' }}
-
- {{ managementUsername || '—' }}
-
-
-
- {{ pingResult.packetsTransmitted }}{{ pingResult.packetsReceived }}{{ pingResult.packetLoss }}%{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms{{ pingResult.packetsTransmitted }}{{ pingResult.packetsReceived }}{{ pingResult.packetLoss }}%{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms| # | +Bandbreite | +Übertragen | +Pakete | +
|---|---|---|---|
| {{ idx + 1 }} | +{{ row.bpsFormatted }} | +{{ row.bytesFormatted }} | +{{ row.packets }} | +
{{ remoteAccessResult.username }}
+
+ {{ remoteAccessResult.password }}
+
+ Dokumentieren Sie alle Wohneinheiten und laden Sie die erforderlichen Dokumente hoch.
-Prüfen Sie die hochgeladenen Dokumente:
- -