diff --git a/application/MobileApp/Modules/Workorder/Workorder/WorkorderHandler.php b/application/MobileApp/Modules/Workorder/Workorder/WorkorderHandler.php index 4a556bf79..574b2d060 100644 --- a/application/MobileApp/Modules/Workorder/Workorder/WorkorderHandler.php +++ b/application/MobileApp/Modules/Workorder/Workorder/WorkorderHandler.php @@ -244,7 +244,8 @@ class WorkorderHandler extends MobileAppBaseHandler { $technicalData = null; if ($tenantConfigData && !empty($tenantConfigData['showTechnicalData'])) { - $technicalData = $this->getTechnicalData($id); + RimoWorkorder::autoParseForWorkorder($id); + $technicalData = WorkorderModel::getTechnicalData($id); } self::returnJson([ @@ -900,51 +901,6 @@ class WorkorderHandler extends MobileAppBaseHandler { return true; } - /** - * Get technical data (patchposition and AHA Blatt) for a workorder - */ - private function getTechnicalData($workorderId) { - $workorder = WorkorderModel::get($workorderId); - if (!$workorder || !$workorder->preorderId) return null; - - $preorder = new Preorder($workorder->preorderId); - if (!$preorder->id || !$preorder->adb_wohneinheit_id) return null; - - $wohneinheit = $preorder->adb_wohneinheit; - if (!$wohneinheit) return null; - - $defaultCluster = ''; - if ($preorder->adb_hausnummer && $preorder->adb_hausnummer->netzgebiet) { - $defaultCluster = $preorder->adb_hausnummer->netzgebiet->extref ?? ''; - } - - $patchposition = [ - 'equipmentName' => $wohneinheit->getPatchEqString(), - 'equipmentPort' => $wohneinheit->patch_port, - 'cluster' => $wohneinheit->patch_cluster ?: $defaultCluster, - 'shelf' => $wohneinheit->patch_shelf, - 'module' => $wohneinheit->patch_module, - ]; - - $rimoWorkorders = []; - if (is_array($wohneinheit->rimo_workorders) && count($wohneinheit->rimo_workorders)) { - foreach ($wohneinheit->rimo_workorders as $wo) { - $rimoWorkorders[] = [ - 'id' => $wo->id, - 'rimoName' => $wo->rimo_name, - 'rimoId' => $wo->rimo_id, - 'rimoStatus' => $wo->rimo_status, - 'downloadUrl' => "/RimoWorkorder/downloadAha?id=" . $wo->id, - ]; - } - } - - return [ - 'patchposition' => $patchposition, - 'rimoWorkorders' => $rimoWorkorders, - ]; - } - /** * Get single workorder with full joined data (same structure as getCompanyWorkorders) */ diff --git a/application/RimoWorkorder/RimoWorkorder.php b/application/RimoWorkorder/RimoWorkorder.php index 31dcb8994..5c9eb0767 100644 --- a/application/RimoWorkorder/RimoWorkorder.php +++ b/application/RimoWorkorder/RimoWorkorder.php @@ -1,5 +1,7 @@ id || !$this->adb_wohneinheit_id) return ['success' => false, 'message' => 'Missing ID']; + $preorder = PreorderModel::getFirstActive(["adb_wohneinheit_id" => $this->adb_wohneinheit_id]); + if (!$preorder?->id) return ['success' => false, 'message' => 'No active Preorder']; + $workorder = WorkorderModel::getFirst(['preorderId' => $preorder->id]); + if (!$workorder || !($pdf = $this->getAha())) return ['success' => false, 'message' => 'No Workorder or PDF']; + + try { + $dropkabel = $this->parseDropkabelFromPdf($pdf); + $map = $this->extractMapFromPdf($pdf); + $meta = json_decode($workorder->metadata ?: '{}', true) ?: []; + $mapFileId = null; + + if ($map) { + if ($oldId = ($meta['dropcable']['map_file_id'] ?? null)) { + $old = new File($oldId); if ($old->id) try { $old->delete(); } catch (Exception $e) {} + } + $fn = 'aha_lageplan_' . $this->id . '_' . time() . '.png'; + $file = FileModel::create(['name' => 'AHA Lageplan ' . $this->rimo_name, 'filename' => $fn, + 'store_filename' => $fn, 'orig_filename' => 'AHA_Lageplan_' . $this->rimo_name . '.png', + 'mimetype' => 'image/png', 'subfolder' => 'aha_maps']); + if ($file->save()) { + $dir = MFUPLOAD_FILE_SAVE_PATH . '/aha_maps'; + if (!is_dir($dir)) mkdir($dir, 0755, true); + if (file_put_contents("$dir/$fn", $map)) $mapFileId = $file->id; + } + } + $meta['dropcable'] = ['rimo_workorder_id' => $this->id, 'rimo_name' => $this->rimo_name, + 'parsed_at' => time(), 'entries' => $dropkabel, 'map_file_id' => $mapFileId]; + $workorder->metadata = json_encode($meta); + WorkorderModel::update((array)$workorder); + return ['success' => true, 'dropkabel_count' => count($dropkabel), 'has_map' => (bool)$mapFileId]; + } catch (Exception $e) { + $this->log->error(__METHOD__ . ": " . $e->getMessage()); + return ['success' => false, 'message' => $e->getMessage()]; + } + } + + public static function autoParseForWorkorder(int $workorderId): void { + $wo = WorkorderModel::get($workorderId); + if (!$wo) return; + $meta = json_decode($wo->metadata ?? '{}', true); + if (empty($meta['dropcable']['parsed_at']) && $wo->preorderId) { + $pre = new Preorder($wo->preorderId); + $rimos = $pre->adb_wohneinheit_id ? RimoWorkorderModel::search(['adb_wohneinheit_id' => $pre->adb_wohneinheit_id]) : []; + if (!empty($rimos[0])) (new self($rimos[0]->id))->parseAha(); + } + } + + private function parseDropkabelFromPdf(string $pdf): array { + $result = []; + $text = (new Parser())->parseContent($pdf)->getPages()[0]?->getText() ?? ''; + if (!preg_match('/Dropkabel:\s*\n(.+?)(?:Lage:|$)/s', $text, $m)) return $result; + $started = false; + foreach (explode("\n", $m[1]) as $line) { + $line = trim($line); + if (!$line) continue; + if (stripos($line, 'ID') !== false && stripos($line, 'Type') !== false) { $started = true; continue; } + if ($started && preg_match('/^(F-[A-Z0-9\(\)-]+K\d+)\s+(.+)$/i', $line, $p)) { + $rest = $p[2]; $status = ''; + foreach (['Planfreigabe', 'Plan released', 'Grobplanung', 'Executed', 'Ausgeführt'] as $s) + if (preg_match('/\b' . preg_quote($s, '/') . '\s*$/i', $rest)) { + $status = $s; $rest = trim(preg_replace('/\b' . preg_quote($s, '/') . '\s*$/i', '', $rest)); break; + } + $lp = $li = ''; + if (preg_match_all('/(\d+)\s*m\b/', $rest, $lens, PREG_SET_ORDER)) { + $lp = ($lens[0][1] ?? '') . ' m'; $li = ($lens[1][1] ?? '') . ' m'; + $rest = preg_replace('/\d+\s*m\b/', '', $rest); + } + $result[] = ['cable_id' => trim($p[1]), 'type' => trim(preg_replace('/\s+/', ' ', $rest)), + 'laenge_plan' => $lp, 'laenge_ist' => $li, 'status' => $status]; + } + } + return $result; + } + + private function extractMapFromPdf(string $pdf): ?string { + $tmp = tempnam(sys_get_temp_dir(), 'aha_'); file_put_contents($tmp, $pdf); + $out = tempnam(sys_get_temp_dir(), 'aha_img_'); unlink($out); + exec(sprintf('pdftoppm -png -f 1 -l 1 -r 150 %s %s 2>&1', escapeshellarg($tmp), escapeshellarg($out)), $_, $ret); + @unlink($tmp); + $outFile = file_exists("$out-1.png") ? "$out-1.png" : "$out.png"; + if ($ret !== 0 || !file_exists($outFile)) return null; + $img = @imagecreatefromstring(file_get_contents($outFile)); @unlink($outFile); + if (!$img) return null; + $h = imagesy($img); $cropY = (int)($h * 0.42); $cropH = (int)($h * 0.84) - $cropY; + $cropped = imagecrop($img, ['x' => 60, 'y' => $cropY, 'width' => imagesx($img) - 90, 'height' => $cropH]); + imagedestroy($img); if (!$cropped) return null; + ob_start(); imagepng($cropped, null, 6); $content = ob_get_clean(); imagedestroy($cropped); + return $content; + } public function getProperty($name) { if($this->$name == null) { diff --git a/application/RimoWorkorder/RimoWorkorderController.php b/application/RimoWorkorder/RimoWorkorderController.php index 7f330599e..5fd097a28 100644 --- a/application/RimoWorkorder/RimoWorkorderController.php +++ b/application/RimoWorkorder/RimoWorkorderController.php @@ -44,4 +44,24 @@ class RimoWorkorderController extends mfBaseController { exit; } + protected function parseAhaAction() { + header('Content-Type: application/json'); + $post = json_decode(file_get_contents('php://input'), true); + $id = $post['id'] ?? $this->request->id ?? null; + + if (!$id || $id < 1) { + echo json_encode(['success' => false, 'message' => 'Invalid workorder id.']); + exit; + } + + $wo = new RimoWorkorder($id); + if (!$wo->id) { + echo json_encode(['success' => false, 'message' => 'RimoWorkorder nicht gefunden.']); + exit; + } + + echo json_encode($wo->parseAha()); + exit; + } + } \ No newline at end of file diff --git a/application/Workorder/WorkorderModel.php b/application/Workorder/WorkorderModel.php index b5762a147..680e51244 100644 --- a/application/Workorder/WorkorderModel.php +++ b/application/Workorder/WorkorderModel.php @@ -16,6 +16,7 @@ class WorkorderModel extends TTCrudBaseModel public ?string $additionalInfo; public ?string $cableLength; public ?string $cableType; + public ?string $metadata; public int $create; public int $createBy; @@ -199,4 +200,62 @@ class WorkorderModel extends TTCrudBaseModel $result = $db->query($sql); return $result ? $result->fetch_assoc()['count'] : 0; } + + public static function getTechnicalData(int $workorderId): ?array { + $workorder = self::get($workorderId); + if (!$workorder || !$workorder->preorderId) return null; + + $preorder = new Preorder($workorder->preorderId); + if (!$preorder->id || !$preorder->adb_wohneinheit_id) return null; + + $wohneinheit = $preorder->adb_wohneinheit; + if (!$wohneinheit) return null; + + $defaultCluster = ''; + if ($preorder->adb_hausnummer && $preorder->adb_hausnummer->netzgebiet) { + $defaultCluster = $preorder->adb_hausnummer->netzgebiet->extref ?? ''; + } + + $patchposition = [ + 'equipmentName' => $wohneinheit->getPatchEqString(), + 'equipmentPort' => $wohneinheit->patch_port, + 'cluster' => $wohneinheit->patch_cluster ?: $defaultCluster, + 'shelf' => $wohneinheit->patch_shelf, + 'module' => $wohneinheit->patch_module, + ]; + + // Get dropcable data from metadata + $dropkabelData = []; + $ahaParsed = null; + $mapFile = null; + if (!empty($workorder->metadata)) { + $metadata = json_decode($workorder->metadata, true); + if (!empty($metadata['dropcable'])) { + $ahaParsed = $metadata['dropcable']['parsed_at'] ?? null; + $dropkabelData = $metadata['dropcable']['entries'] ?? []; + if ($mapFileId = $metadata['dropcable']['map_file_id'] ?? null) { + $file = new File($mapFileId); + if ($file->id) { + $mapFile = ['id' => $file->id, 'name' => $file->name, 'download_url' => '/File/show?id=' . $file->id]; + } + } + } + } + + $rimoWorkorders = []; + if (is_array($wohneinheit->rimo_workorders) && count($wohneinheit->rimo_workorders)) { + foreach ($wohneinheit->rimo_workorders as $wo) { + $rimoWorkorders[] = [ + 'id' => $wo->id, 'rimoName' => $wo->rimo_name, 'rimoId' => $wo->rimo_id, + 'rimoStatus' => $wo->rimo_status, 'downloadUrl' => "/RimoWorkorder/downloadAha?id=" . $wo->id, + ]; + } + } + + return [ + 'patchposition' => $patchposition, + 'rimoWorkorders' => $rimoWorkorders, + 'dropcable' => ['parsed_at' => $ahaParsed, 'entries' => $dropkabelData, 'map_file' => $mapFile], + ]; + } } \ No newline at end of file diff --git a/application/WorkorderBase/WorkorderBaseController.php b/application/WorkorderBase/WorkorderBaseController.php index f8a9c759e..68e2066d0 100644 --- a/application/WorkorderBase/WorkorderBaseController.php +++ b/application/WorkorderBase/WorkorderBaseController.php @@ -60,6 +60,10 @@ class WorkorderBaseController extends TTCrud $translationMap = array_merge(['civil_engineering_photo' => 'Tiefbau_Foto'], $customMap); } + // Auto-parse AHA if enabled and not yet parsed + if ($tenantConfig?->showTechnicalData) { + RimoWorkorder::autoParseForWorkorder((int)$this->request->workorderId); + } $responseDocs = []; $typeCounts = []; @@ -145,45 +149,7 @@ class WorkorderBaseController extends TTCrud * Retrieves technical data (patchposition and AHA Blatt info) for a workorder. */ protected function getTechnicalData(int $workorderId): ?array { - $workorder = WorkorderModel::get($workorderId); - if (!$workorder || !$workorder->preorderId) return null; - - $preorder = new Preorder($workorder->preorderId); - if (!$preorder->id || !$preorder->adb_wohneinheit_id) return null; - - $wohneinheit = $preorder->adb_wohneinheit; - if (!$wohneinheit) return null; - - $defaultCluster = ''; - if ($preorder->adb_hausnummer && $preorder->adb_hausnummer->netzgebiet) { - $defaultCluster = $preorder->adb_hausnummer->netzgebiet->extref ?? ''; - } - - $patchposition = [ - 'equipmentName' => $wohneinheit->getPatchEqString(), - 'equipmentPort' => $wohneinheit->patch_port, - 'cluster' => $wohneinheit->patch_cluster ?: $defaultCluster, - 'shelf' => $wohneinheit->patch_shelf, - 'module' => $wohneinheit->patch_module, - ]; - - $rimoWorkorders = []; - if (is_array($wohneinheit->rimo_workorders) && count($wohneinheit->rimo_workorders)) { - foreach ($wohneinheit->rimo_workorders as $wo) { - $rimoWorkorders[] = [ - 'id' => $wo->id, - 'rimoName' => $wo->rimo_name, - 'rimoId' => $wo->rimo_id, - 'rimoStatus' => $wo->rimo_status, - 'downloadUrl' => "/RimoWorkorder/downloadAha?id=" . $wo->id, - ]; - } - } - - return [ - 'patchposition' => $patchposition, - 'rimoWorkorders' => $rimoWorkorders, - ]; + return WorkorderModel::getTechnicalData($workorderId); } //region BACKGROUND TASKS diff --git a/composer.json b/composer.json index 151670a4a..f4603b66e 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "stomp-php/stomp-php": "^5", "phpmailer/phpmailer": "^6.9", "pear2/net_routeros": "dev-develop@dev", - "matthiasmullie/minify": "^1.3" + "matthiasmullie/minify": "^1.3", + "smalot/pdfparser": "^2.0" } } diff --git a/db/migrations/20260119170000_add_metadata_to_workorder.php b/db/migrations/20260119170000_add_metadata_to_workorder.php new file mode 100644 index 000000000..a24126bef --- /dev/null +++ b/db/migrations/20260119170000_add_metadata_to_workorder.php @@ -0,0 +1,30 @@ +getEnvironment() == "thetool") { + $table = $this->table("Workorder"); + $table + ->addColumn("metadata", "json", [ + 'null' => true, + 'default' => null, + 'after' => 'cableType' + ]) + ->update(); + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table("Workorder") + ->removeColumn("metadata") + ->save(); + } + } +} diff --git a/public/js/pages/WorkorderBase/WorkorderBase.js b/public/js/pages/WorkorderBase/WorkorderBase.js index 0b7d2c244..9a4f914c5 100644 --- a/public/js/pages/WorkorderBase/WorkorderBase.js +++ b/public/js/pages/WorkorderBase/WorkorderBase.js @@ -275,38 +275,59 @@ Vue.component('workorder-details-manager', {
| Equipment Name: | -{{ technicalData.patchposition.equipmentName }} | -
|---|---|
| Equipment Port: | -{{ technicalData.patchposition.equipmentPort }} | -
| Kabel-ID | Typ | PLAN | IST | Status |
|---|---|---|---|---|
| {{ dk.cable_id }} | +{{ dk.type }} | +{{ dk.laenge_plan || '-' }} | +{{ dk.laenge_ist || '-' }} | +{{ dk.status || '-' }} | +
Pinch zum Zoomen • Doppeltippen für 2x
+