From b173a6edc1bd132c552065ae9d964bb6cfbe8f69 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 2 Sep 2025 08:36:33 +0000 Subject: [PATCH] rmlworkorder major upgrade --- .../RMLWorkorderAdminController.php | 375 +---------- .../RMLWorkorderCompanyController.php | 353 +--------- .../RMLWorkorderTenantConfigModel.php | 12 - .../WorkorderModel.php} | 42 +- .../WorkorderAdminController.php | 296 +++++++++ .../WorkorderBase/WorkorderBaseController.php | 139 ++++ .../WorkorderCompanyController.php | 195 ++++++ .../WorkorderCompanyModel.php} | 4 +- .../WorkorderDocumentationModel.php} | 4 +- .../WorkorderJournalModel.php} | 3 +- .../WorkorderTenantConfigModel.php | 31 + .../20250901410000_workorder_rename.php | 81 +++ lib/Helper/Helper.php | 12 + .../RMLWorkorderAdmin/RMLWorkorderAdmin.js | 601 ------------------ .../RMLWorkorderCompany/RMLWorkorder.css | 40 -- .../RMLWorkorderCompany.js | 568 ----------------- .../RMLWorkorderCompanyDashboardView.js | 53 -- .../RMLWorkorderDashboardView.js | 61 -- .../js/pages/WorkorderAdmin/WorkorderAdmin.js | 310 +++++++++ .../WorkorderBase.css} | 0 .../js/pages/WorkorderBase/WorkorderBase.js | 471 ++++++++++++++ .../WorkorderCompany/WorkorderCompany.js | 176 +++++ public/plugins/vue/tt-components/tt-modal.js | 2 +- .../vue/tt-components/tt-number-range.js | 6 + 24 files changed, 1762 insertions(+), 2073 deletions(-) delete mode 100644 application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php rename application/{RMLWorkorder/RMLWorkorderModel.php => Workorder/WorkorderModel.php} (79%) create mode 100644 application/WorkorderAdmin/WorkorderAdminController.php create mode 100644 application/WorkorderBase/WorkorderBaseController.php create mode 100644 application/WorkorderCompany/WorkorderCompanyController.php rename application/{RMLWorkorderCompany/RMLWorkorderCompanyModel.php => WorkorderCompany/WorkorderCompanyModel.php} (65%) rename application/{RMLWorkorderDocumentation/RMLWorkorderDocumentationModel.php => WorkorderDocumentation/WorkorderDocumentationModel.php} (66%) rename application/{RMLWorkorderJournal/RMLWorkorderJournalModel.php => WorkorderJournal/WorkorderJournalModel.php} (70%) create mode 100644 application/WorkorderTenantConfig/WorkorderTenantConfigModel.php create mode 100644 db/migrations/20250901410000_workorder_rename.php delete mode 100644 public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js delete mode 100644 public/js/pages/RMLWorkorderCompany/RMLWorkorder.css delete mode 100644 public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js delete mode 100644 public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js delete mode 100644 public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js create mode 100644 public/js/pages/WorkorderAdmin/WorkorderAdmin.js rename public/js/pages/{RMLWorkorderAdmin/RMLWorkorderAdmin.css => WorkorderBase/WorkorderBase.css} (100%) create mode 100644 public/js/pages/WorkorderBase/WorkorderBase.js create mode 100644 public/js/pages/WorkorderCompany/WorkorderCompany.js diff --git a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php index f45a72db7..71a95fac2 100644 --- a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php +++ b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php @@ -1,375 +1,8 @@ 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], - ['key' => 'preordercampaign_id', 'text' => 'Kampagne', 'modal' => false, 'table' => ['filter' => 'select']], - ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'companyName', 'text' => 'Zugewiesene Firma', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', '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'], - ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], - ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], - ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], - ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], - ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], - ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], - ]]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ]; - - private function getStatusText(string $statusKey): string - { - foreach ($this->columns as $column) { - if ($column['key'] === 'status') { - foreach ($column['table']['filterOptions'] as $option) { - if ($option['value'] === $statusKey) { - return $option['text']; - } - } - } - } - return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback - } - - protected function indexAction() - { - $campaigns = Helper::getPreorderCampaignFromUser($this->user, true); - $this->columns[array_search('preordercampaign_id', array_column($this->columns, 'key'))]['table']['filterOptions'] = array_map( - fn($c) => ['value' => $c->id, 'text' => $c->name], - $campaigns - ); - - $this->createWorkordersFromPreorders(); - Helper::renderVue($this, 'RMLWorkorderAdmin', $this->headerTitle, [ - "CRUD_CONFIG" => $this->getCrudConfig(), - "TABLE_URL" => $this::getUrl("RMLWorkorderAdmin/get"), - ]); - } - - protected function getAction() - { - $json = json_decode(file_get_contents('php://input'), true); - $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; - $filters = $json['filters'] ?? []; - $order = $json['order'] ?? []; - - $allowedCampaignIds = Helper::getPreorderCampaignFromUser($this->user); - - if (empty($allowedCampaignIds)) { - self::returnJson([ - 'rows' => [], - 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0]) - ]); - return; - } - - $limit = $pagination['per_page']; - $offset = ($pagination['page'] - 1) * $limit; - - $workorders = RMLWorkorderModel::getAdminWorkorders($filters, $limit, $offset, $order, $allowedCampaignIds); - $totalCount = RMLWorkorderModel::countAdminWorkorders($filters, $allowedCampaignIds); - - $rows = array_map(function ($workorder) { - $row = (array)$workorder; - $row['companyName'] ??= 'Nicht zugewiesen'; - return $row; - }, $workorders); - - self::returnJson([ - 'rows' => $rows, - 'pagination' => [ - 'page' => $pagination['page'], - 'per_page' => $pagination['per_page'], - 'total_rows' => $totalCount, - 'total_pages' => ceil($totalCount / $limit), - 'filtered_available' => $totalCount - ] - ]); - } - - private function createWorkordersFromPreorders() - { - $configs = RMLWorkorderTenantConfigModel::getAll(); - foreach ($configs as $config) { - $filters = json_decode($config->workorderCreationFilters, true); - if (empty($filters)) continue; - - $networks = NetworkModel::getAll(['owner_id' => $config->addressId]); - if (empty($networks)) continue; - - $tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)])); - if (empty($tenantCampaigns)) continue; - - $filters['preordercampaign_id'] = $tenantCampaigns; - - $newPreorders = PreorderModel::searchActive($filters); - if (empty($newPreorders)) continue; - - foreach ($newPreorders as $preorder) { - if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) { - RMLWorkorderModel::create([ - 'preorderId' => $preorder->id, - 'clusterId' => $preorder->preordercampaign_id, - 'status' => 'new', - 'create' => time(), - 'createBy' => 0 // System User - ]); - } - } - } - } - - protected function getDocumentationAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); - $journals = RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); - - $translationMap = [ - 'photo_hup_mounted' => 'Foto_montierter_HÜP', 'photo_hup_open' => 'Foto_offener_HÜP', - 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', - 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', - 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', - 'measurement_protocol_otdr' => 'ODTR_Messung', 'other' => 'Sonstiges_Dokument' - ]; - - $responseDocs = []; - $typeCounts = []; - - foreach ($docs as $doc) { - $file = new File($doc->fileId); - $documentTypeKey = $doc->documentType; - $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; - $originalFilename = $file->orig_filename ?? $file->filename; - $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); - $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; - $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); - - $responseDocs[] = [ - 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, - 'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', - 'mimetype' => $file->mimetype ?? 'application/octet-stream', - ]; - } - - foreach ($journals as $journal) $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; - - self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); - } - - private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId) - { - $workorder = RMLWorkorderModel::get($workorderId); - if (!$workorder) return false; - $company = RMLWorkorderCompanyModel::get($companyId); - if (!$company) return false; - - $workorder->companyId = $companyId; - $workorder->status = 'assigned'; - $workorder->assignmentDate = time(); - $workorder->deadlineDate = $deadline; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Firma '{$company->name}' wurde zugewiesen.", - 'create' => time(), 'createBy' => $userId, - ]); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 10; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - return true; - } - - protected function assignWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['companyId'])) self::sendError("Erforderliche Felder fehlen."); - - $deadline = !empty($post['deadlineDate']) ? $post['deadlineDate'] : strtotime('+6 weeks'); - if ($this->assignSingleWorkorder($post['workorderId'], $post['companyId'], $deadline, $this->user->id)) { - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich zugewiesen.']); - } else self::sendError("Arbeitsauftrag konnte nicht zugewiesen werden. Er wurde möglicherweise bereits bearbeitet oder existiert nicht."); - } - - protected function massAssignWorkordersAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderIds']) || empty($post['companyId'])) self::sendError("Erforderliche Felder fehlen."); - - $deadline = strtotime($post['deadlineDate'] ?? '+6 weeks'); - $count = 0; - foreach ($post['workorderIds'] as $workorderId) if ($this->assignSingleWorkorder($workorderId, $post['companyId'], $deadline, $this->user->id)) $count++; - self::returnJson(['success' => true, 'message' => "$count Arbeitsaufträge erfolgreich zugewiesen."]); - } - - protected function requestCorrectionAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['text'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'correction_requested'; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Korrektur angefordert. Grund: " . $post['text'], - 'fileIds' => !empty($post['fileIds']) ? json_encode($post['fileIds']) : null, - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Korrektur wurde angefordert.']); - } - - protected function getCompaniesAction() - { - $tenantId = $this->request->tenantId ?? null; - $companies = RMLWorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']); - - if ($tenantId) { - $companies = array_filter($companies, function ($company) use ($tenantId) { - if ($company->addressId == 4807 && empty($company->visibleForAddressId)) return true; - $visibleFor = !empty($company->visibleForAddressId) ? json_decode($company->visibleForAddressId, true) : []; - return in_array($tenantId, $visibleFor); - }); - } - self::returnJson(array_values(array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies))); - } - - protected function addJournalAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - - RMLWorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->name ?? 'Unbekannt'; - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); - } - - protected function updateDeadlineAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['deadlineDate'])) self::sendError("Erforderliche Felder fehlen."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $workorder->deadlineDate = $post['deadlineDate']; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create(['workorderId' => $workorder->id, 'text' => 'Deadline geändert auf ' . date('d.m.Y', $post['deadlineDate']) . '.', 'create' => time(), 'createBy' => $this->user->id]); - self::returnJson(['success' => true, 'message' => 'Deadline erfolgreich aktualisiert.']); - } - - protected function acceptDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - if ($workorder->status !== 'documented') self::sendError("Die Dokumentation muss zuerst von der Firma als fertig markiert werden."); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 15; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - - $oldStatus = $workorder->status; - $workorder->status = 'completed'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.', - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']); - } - - protected function setToProblemSolvedAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - if ($workorder->status !== 'intervention_required') self::sendError("Der Arbeitsauftrag muss den Status 'Eingriff erforderlich' haben, um als gelöst markiert zu werden."); - - $oldStatus = $workorder->status; - $workorder->status = 'problem_solved'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Problem gelöst: " . $post['text'], - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als "Problem gelöst" markiert.']); - } - - protected function updateAdditionalInfoAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldInfo = $workorder->additionalInfo; - $workorder->additionalInfo = $post['additionalInfo'] ?? null; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$workorder->additionalInfo}'", - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.']); - } - - protected function cancelWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'cancelled'; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Arbeitsauftrag wurde storniert.' . (!empty($post['reason']) ? ' Grund: ' . $post['reason'] : ''), - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('cancelled'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - - $preorder = new Preorder($workorder->preorderId); - if ($preorder->id) { - $preorder->status_id = 99; - $preorder->edit_by = $this->user->id; - $preorder->save(); - } - - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag wurde storniert.']); +class RMLWorkorderAdminController extends mfBaseController { + protected function init() { + $this->needlogin = true; + $this->redirect("WorkorderAdmin"); } } \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php index 90d049526..3c83ff9b2 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php +++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php @@ -1,353 +1,8 @@ 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], - ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => false]], - ['key' => 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', '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'], - ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], - ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], - ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], - ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], - ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], - ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], - ]]], - ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], - ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], - ]; - - protected array $additionalJSVariables = ['COMPANY_ID' => '0']; - - private function getStatusText(string $statusKey): string - { - foreach ($this->columns as $column) { - if ($column['key'] === 'status') { - foreach ($column['table']['filterOptions'] as $option) { - if ($option['value'] === $statusKey) { - return $option['text']; - } - } - } - } - return ucfirst(str_replace('_', ' ', $statusKey)); // Fallback - } - - protected function prepareCrudConfig() - { - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if ($company) { - $this->additionalJSVariables['COMPANY_ID'] = $company->id; - } else { - $this->additionalJSVariables['COMPANY_ID'] = 0; - } - } - - protected function indexAction() - { - Helper::renderVue($this, 'RMLWorkorderCompany', $this->headerTitle, [ - "CRUD_CONFIG" => $this->getCrudConfig(), - "TABLE_URL" => $this::getUrl("RMLWorkorderCompany/get"), - "COMPANY_ID" => $this->additionalJSVariables['COMPANY_ID'], - ]); - } - - protected function getAction() - { - $json = json_decode(file_get_contents('php://input'), true); - $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; - $filters = $json['filters'] ?? []; - $order = $json['order'] ?? []; - - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if (!$company) { - self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); - return; - } - $companyId = $company->id; - - $workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $companyId); - $totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $companyId); - - $rows = array_map(function ($workorder) { - $row = (array)$workorder; - $row['preorderInfo'] = $this->getPreorderInfoTextByData($row); - unset($row['customerName'], $row['customerCompany'], $row['street'], $row['hausnummer'], $row['stiege'], $row['oaid'], $row['apartment'], $row['plz'], $row['city'], $row['phone'], $row['email']); - return $row; - }, $workorders); - - self::returnJson([ - 'rows' => $rows, - 'pagination' => [ - 'page' => $pagination['page'], 'per_page' => $pagination['per_page'], - 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), - 'filtered_available' => $totalCount - ] - ]); - } - - private function getPreorderInfoTextByData($data) - { - $anschlussadresse = "{$data['street']} {$data['hausnummer']}"; - if ($data['stiege']) $anschlussadresse .= "/{$data['stiege']}"; - if ($data['apartment']) $anschlussadresse .= " / WE: {$data['apartment']}"; - $anschlussadresse .= ", {$data['plz']} {$data['city']}"; - $kunde = $data['customerCompany'] ?: $data['customerName']; - return "Kunde: {$kunde}
" . "Anschluss: {$anschlussadresse}
" . "Kontakt: {$data['phone']} / {$data['email']}
" . "OAID: {$data['oaid']}"; - } - - public function getWorkorderByIdAction() - { - $id = $this->request->id; - if (!$id) self::sendError("ID fehlt"); - $workorder = RMLWorkorderModel::get($id); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); - $workorder->preorderInfo = $this->getPreorderInfoText($workorder->preorderId); - self::returnJson((array)$workorder); - } - - private function getPreorderInfoText($preorderId) - { - $preorder = new Preorder($preorderId); - $anschlussadresse = 'N/A'; - if ($preorder->adb_hausnummer_id) { - $hn = $preorder->adb_hausnummer; - $anschlussadresse = "{$hn->strasse->name} {$hn->hausnummer}"; - if ($hn->stiege) $anschlussadresse .= "/{$hn->stiege}"; - if ($preorder->adb_wohneinheit_id) $anschlussadresse .= " / WE: {$preorder->adb_wohneinheit->bezeichner}"; - $anschlussadresse .= ", {$hn->plz->plz} {$hn->ortschaft->name}"; - } - $kunde = ($preorder->company) ?: "{$preorder->firstname} {$preorder->lastname}"; - return "Kunde: {$kunde}
" . "Anschluss: {$anschlussadresse}
" . "Kontakt: {$preorder->phone} / {$preorder->email}
" . "OAID: {$preorder->oaid}"; - } - - protected function scheduleAppointmentAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['appointmentDate'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); - $hour = (int)date('H', $post['appointmentDate']); - if ($hour >= 23 || $hour < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); - - $workorder->appointmentDate = $post['appointmentDate']; - $workorder->status = 'scheduled'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $post['appointmentDate']), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']); - } - - protected function rescheduleAppointmentAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['appointmentDate']) || empty($post['reason'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $hour = (int)date('H', $post['appointmentDate']); - if ($hour >= 23 || $hour < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); - - $oldDateFormatted = $workorder->appointmentDate ? date('d.m.Y H:i', $workorder->appointmentDate) : 'N/A'; - $newDateFormatted = date('d.m.Y H:i', $post['appointmentDate']); - $workorder->appointmentDate = $post['appointmentDate']; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Termin verschoben von {$oldDateFormatted} auf {$newDateFormatted}. Grund: " . $post['reason'], - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Termin erfolgreich verschoben.']); - } - - protected function requestInterventionAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty($post['journalText'])) self::sendError("Erforderliche Felder fehlen."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - - $oldStatus = $workorder->status; - $workorder->status = 'intervention_required'; - RMLWorkorderModel::update((array)$workorder); - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Eingriff erforderlich: " . $post['journalText'], - 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'), - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']); - } - - protected function uploadDocumentationAction() - { - if (empty($_FILES['files']) || empty($_POST['workorderId'])) { - self::returnJson(['error' => 'Erforderliche Daten fehlen.']); - return; - } - $workorderId = $_POST['workorderId']; - $description = $_POST['description'] ?? ''; - $documentType = $_POST['documentType'] ?? 'general'; - $files = $_FILES['files']; - $uploadCount = 0; - - foreach ($files['name'] as $index => $name) { - if ($files['error'][$index] === UPLOAD_ERR_OK) { - $_FILES['file'] = ['name' => $files['name'][$index], 'type' => $files['type'][$index], 'tmp_name' => $files['tmp_name'][$index], 'error' => $files['error'][$index], 'size' => $files['size'][$index]]; - try { - $uploaded = mfUpload::handleFormUpload("file", false, "/RMLWorkorder"); - RMLWorkorderDocumentationModel::create(['workorderId' => $workorderId, 'fileId' => $uploaded->id, 'description' => $description, 'documentType' => $documentType, 'create' => time(), 'createBy' => $this->user->id]); - $uploadCount++; - } catch (Exception $e) { - error_log("Dateiupload für $name fehlgeschlagen: " . $e->getMessage()); - } - } - } - - $workorder = RMLWorkorderModel::get($workorderId); - if ($workorder->status === 'correction_requested' || $workorder->status === 'problem_solved') { - $workorder->status = 'assigned'; - RMLWorkorderModel::update((array)$workorder); - $workorder = RMLWorkorderModel::get($workorderId); - } - $formattedDocs = $this->getFormattedDocs($workorderId); - self::returnJson(['success' => true, 'message' => "$uploadCount Datei(en) erfolgreich hochgeladen.", 'docs' => $formattedDocs, 'workorder' => (array)$workorder]); - } - - private function getFormattedDocs($workorderId) - { - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); - $responseDocs = []; - $typeCounts = []; - $translationMap = [ - 'photo_hup_mounted' => 'Foto_montierter_HÜP', 'photo_hup_open' => 'Foto_offener_HÜP', - 'photo_splice_cassette_hup' => 'Foto_Spleißkassette_HÜP', 'photo_splice_cassette_fcp' => 'Foto_Spleißkassette_FCP', - 'photo_hup_closed_stickers' => 'Foto_geschlossener_HÜP_mit_Aufklebern', 'photo_fcp_labeled' => 'Foto_FCP_beschriftet', - 'photo_patch_position_osp' => 'Foto_Patch-Position_OSP-Seite', 'photo_patch_position_anb' => 'Foto_Patch-Position_ANB-Seite', - 'measurement_protocol_otdr' => 'ODTR_Messung', 'other' => 'Sonstiges_Dokument' - ]; - foreach ($docs as $doc) { - $file = new File($doc->fileId); - $documentTypeKey = $doc->documentType; - $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; - $originalFilename = $file->orig_filename ?? $file->filename; - $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); - $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; - $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); - $responseDocs[] = ['id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, 'documentType' => $documentTypeKey, 'mimetype' => $file->mimetype]; - } - return $responseDocs; - } - - protected function getDocumentationAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - $docs = $this->getFormattedDocs($this->request->workorderId); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['docs' => $docs, 'journals' => $journals]); - } - - protected function completeWorkorderAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $workorder->status = 'documented'; - RMLWorkorderModel::update((array)$workorder); - self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag abgeschlossen.']); - } - - protected function deleteDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['id'])) self::sendError("Dokumenten-ID fehlt."); - $doc = RMLWorkorderDocumentationModel::get($post['id']); - if (!$doc) self::sendError("Dokument nicht gefunden."); - $workorderId = $doc->workorderId; - RMLWorkorderDocumentationModel::delete($post['id']); - $formattedDocs = $this->getFormattedDocs($workorderId); - self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.', 'docs' => $formattedDocs]); - } - - protected function updateDocumentationAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['id'])) self::sendError("Dokumenten-ID fehlt."); - $doc = RMLWorkorderDocumentationModel::get($post['id']); - if (!$doc) self::sendError("Dokument nicht gefunden."); - if (isset($post['documentType'])) $doc->documentType = $post['documentType']; - RMLWorkorderDocumentationModel::update((array)$doc); - $formattedDocs = $this->getFormattedDocs($doc->workorderId); - self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.', 'docs' => $formattedDocs]); - } - - protected function addJournalAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); - RMLWorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); - $journals = array_map(function ($j) { - $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); - return (array)$j; - }, - RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) - ); - self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); - } - - protected function getTenantConfigAction() - { - if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); - $workorder = RMLWorkorderModel::get($this->request->workorderId); - if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); - $preorder = new Preorder($workorder->preorderId); - if (!$preorder->id) self::sendError("Vorbestellung nicht gefunden."); - $campaign = new Preordercampaign($preorder->preordercampaign_id); - if (!$campaign->id) self::sendError("Kampagne nicht gefunden."); - $network = NetworkModel::getOne($campaign->network_id); - if (!$network) self::sendError("Netzwerk nicht gefunden."); - $tenantId = $network->owner_id; - $tenantConfig = RMLWorkorderTenantConfigModel::getFirst(['addressId' => $tenantId]) ?? RMLWorkorderTenantConfigModel::getFirst(['addressId' => 4807]); - if (!$tenantConfig) { - self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']); - return; - } - self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true)]); - } - - protected function updateAdditionalInfoAction() - { - $post = json_decode(file_get_contents('php://input'), true); - if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); - - $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if (!$company) self::sendError("Firma nicht gefunden."); - - $workorder = RMLWorkorderModel::get($post['workorderId']); - if (!$workorder || $workorder->companyId !== $company->id) self::sendError("Arbeitsauftrag nicht gefunden oder nicht Ihrer Firma zugewiesen."); - - $oldInfo = $workorder->additionalInfo; - $workorder->additionalInfo = $post['additionalInfo'] ?? null; - RMLWorkorderModel::update((array)$workorder); - - RMLWorkorderJournalModel::create([ - 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$workorder->additionalInfo}'", - 'create' => time(), 'createBy' => $this->user->id, - ]); - self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.']); +class RMLWorkorderCompanyController extends mfBaseController { + protected function init() { + $this->needlogin = true; + $this->redirect("WorkorderCompany"); } } \ No newline at end of file diff --git a/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php b/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php deleted file mode 100644 index 56a7458d1..000000000 --- a/application/RMLWorkorderTenantConfig/RMLWorkorderTenantConfigModel.php +++ /dev/null @@ -1,12 +0,0 @@ - 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true, 'filter' => 'numberRange']], + ['key' => 'netOwnerId', 'text' => 'Netzeigentümer', 'modal' => false, 'table' => ['filter' => 'select'], 'required' => false], + ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], + ['key' => 'companyName', 'text' => 'Firma', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], + ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], + // Status column is now inherited via prepareCrudConfig + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ]; + + /** + * Prepares the CRUD configuration. + */ + protected function prepareCrudConfig() + { + $preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key')); + array_splice($this->columns, $preorderInfoColIdx + 1, 0, [$this->statusColumn]); + $netOwnerColIdx = array_search('netOwnerId', array_column($this->columns, 'key')); + + if ($netOwnerColIdx !== false) { + if ($this->user->isAdmin()) { + $netOwners = Helper::getPreorderCampaignNetworkOwners(); + $this->columns[$netOwnerColIdx]['table']['filterOptions'] = array_map(fn($o) => ['value' => $o->id, 'text' => $o->company], $netOwners); + } else { + $this->columns[$netOwnerColIdx]['table'] = false; + } + } + } + + //region ACTIONS + public function indexAction() + { + $this->createWorkordersFromPreorders(); + parent::indexAction(); + } + + /** + * Fetches workorders for the admin view. + */ + protected function getAction() + { + $pagination = $this->postData['pagination'] ?? ['page' => 1, 'per_page' => 10]; + $filters = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? []; + + $allowedCampaignIds = Helper::getPreorderCampaignFromUser($this->user); + if (empty($allowedCampaignIds)) { + self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); + return; + } + + $workorders = WorkorderModel::getAdminWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $allowedCampaignIds); + $totalCount = WorkorderModel::countAdminWorkorders($filters, $allowedCampaignIds); + + $rows = array_map(function ($workorder) { + $row = (array)$workorder; + $row['companyName'] ??= 'Nicht zugewiesen'; + return $row; + }, $workorders); + + self::returnJson([ + 'rows' => $rows, + 'pagination' => ['page' => $pagination['page'], 'per_page' => $pagination['per_page'], 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount] + ]); + } + + protected function getCompaniesAction() + { + $tenantId = $this->request->tenantId; + $companies = WorkorderCompanyModel::getAll(['visibleForAddressId' => "%$tenantId%"]); + self::returnJson(array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies)); + } + + protected function assignWorkorderAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['companyId'])) self::sendError("Erforderliche Felder fehlen."); + $deadline = !empty($this->postData['deadlineDate']) ? $this->postData['deadlineDate'] : strtotime('+6 weeks'); + + if ($this->assignSingleWorkorder($this->postData['workorderId'], $this->postData['companyId'], $deadline, $this->user->id)) { + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich zugewiesen.']); + } else { + self::sendError("Arbeitsauftrag konnte nicht zugewiesen werden."); + } + } + + protected function massAssignWorkordersAction() + { + if (empty($this->postData['workorderIds']) || empty($this->postData['companyId'])) self::sendError("Erforderliche Felder fehlen."); + + $deadline = strtotime($this->postData['deadlineDate'] ?? '+6 weeks'); + $count = 0; + foreach ($this->postData['workorderIds'] as $workorderId) { + if ($this->assignSingleWorkorder($workorderId, $this->postData['companyId'], $deadline, $this->user->id)) $count++; + } + self::returnJson(['success' => true, 'message' => "$count Arbeitsaufträge erfolgreich zugewiesen."]); + } + + protected function requestCorrectionAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'correction_requested'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Korrektur angefordert. Grund: " . $this->postData['text'], + 'fileIds' => !empty($this->postData['fileIds']) ? json_encode($this->postData['fileIds']) : null, + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('correction_requested'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Korrektur wurde angefordert.']); + } + + protected function updateDeadlineAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['deadlineDate'])) self::sendError("Erforderliche Felder fehlen."); + + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $workorder->deadlineDate = $this->postData['deadlineDate']; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create(['workorderId' => $workorder->id, 'text' => 'Deadline geändert auf ' . date('d.m.Y', $this->postData['deadlineDate']) . '.', 'create' => time(), 'createBy' => $this->user->id]); + self::returnJson(['success' => true, 'message' => 'Deadline erfolgreich aktualisiert.']); + } + + protected function acceptDocumentationAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ($workorder->status !== 'documented') self::sendError("Die Dokumentation muss zuerst von der Firma als fertig markiert werden."); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 15; + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + + $oldStatus = $workorder->status; + $workorder->status = 'completed'; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('completed'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']); + } + + protected function setToProblemSolvedAction() + { + if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ($workorder->status !== 'intervention_required') self::sendError("Der Arbeitsauftrag muss den Status 'Eingriff erforderlich' haben."); + + $oldStatus = $workorder->status; + $workorder->status = 'problem_solved'; + WorkorderModel::update((array)$workorder); + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Problem gelöst: " . $this->postData['text'], + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('problem_solved'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als "Problem gelöst" markiert.']); + } + + protected function cancelWorkorderAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'cancelled'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Arbeitsauftrag wurde storniert.' . (!empty($this->postData['reason']) ? ' Grund: ' . $this->postData['reason'] : ''), + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('cancelled'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 99; // Storniert + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag wurde storniert.']); + } + + protected function setCivilEngineeringRequiredAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + if (empty($this->postData['companyId'])) self::sendError("Bitte Tiefbaufirma auswählen."); + + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + $company = WorkorderCompanyModel::get($this->postData['companyId']); + if (!$company) self::sendError("Tiefbaufirma nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->civilEngineeringCompanyId = $company->id; + $workorder->originalCompanyId = $workorder->companyId; + $workorder->companyId = $company->id; + $workorder->status = 'civil_engineering_required'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Tiefbau wurde angefordert. Firma '{$company->name}' wurde zugewiesen.", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('civil_engineering_required'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Tiefbau wurde angefordert und Firma zugewiesen.']); + } + + //endregion + + //region PRIVATE HELPERS + private function assignSingleWorkorder($workorderId, $companyId, $deadline, $userId): bool + { + $workorder = WorkorderModel::get($workorderId); + if (!$workorder) return false; + if ($workorder->status === 'civil_engineering_required') { + $workorder->companyId = $companyId; + $workorder->civilEngineeringCompanyId = $companyId; + WorkorderModel::update((array)$workorder); + return true; + } + $company = WorkorderCompanyModel::get($companyId); + if (!$company) return false; + + $workorder->companyId = $companyId; + $workorder->status = 'assigned'; + $workorder->assignmentDate = time(); + $workorder->deadlineDate = $deadline; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Firma '{$company->name}' wurde zugewiesen.", 'create' => time(), 'createBy' => $userId, + ]); + + $preorder = new Preorder($workorder->preorderId); + if ($preorder->id) { + $preorder->status_id = 10; // In Ausführung + $preorder->edit_by = $this->user->id; + $preorder->save(); + } + return true; + } + + private function createWorkordersFromPreorders() + { + $configs = WorkorderTenantConfigModel::getAll(); + foreach ($configs as $config) { + $filters = json_decode($config->workorderCreationFilters, true); + if (empty($filters)) continue; + + $networks = NetworkModel::getAll(['owner_id' => $config->addressId]); + if (empty($networks)) continue; + + $tenantCampaigns = array_map(fn($n) => $n->id, PreordercampaignModel::getAll(['network_id' => array_map(fn($n) => $n->id, $networks)])); + if (empty($tenantCampaigns)) continue; + + $filters['preordercampaign_id'] = $tenantCampaigns; + $newPreorders = PreorderModel::searchActive($filters); + + foreach ($newPreorders as $preorder) { + if (!WorkorderModel::getFirst(['preorderId' => $preorder->id])) { + WorkorderModel::create([ + 'preorderId' => $preorder->id, 'clusterId' => $preorder->preordercampaign_id, + 'status' => 'new', 'create' => time(), 'createBy' => 0 // System User + ]); + } + } + } + } + //endregion +} \ No newline at end of file diff --git a/application/WorkorderBase/WorkorderBaseController.php b/application/WorkorderBase/WorkorderBaseController.php new file mode 100644 index 000000000..12c6c35cb --- /dev/null +++ b/application/WorkorderBase/WorkorderBaseController.php @@ -0,0 +1,139 @@ + 'status', 'text' => 'Status', 'modal' => false, 'table' => ['filter' => 'iconSelect', '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'], + ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], + ['value' => 'intervention_required', 'text' => 'Eingriff erforderlich', 'icon' => 'fas fa-times-circle text-danger'], + ['value' => 'civil_engineering_required', 'text' => 'Tiefbau benötigt', 'icon' => 'fas fa-hard-hat text-orange'], + ['value' => 'civil_engineering_completed', 'text' => 'Tiefbau abgeschlossen', 'icon' => 'fas fa-hard-hat text-success'], + ['value' => 'problem_solved', 'text' => 'Problem gelöst', 'icon' => 'fas fa-check-circle text-success'], + ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], + ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], + ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], + ]] + ]; + + protected array $additionalJS = ["js/pages/WorkorderBase/WorkorderBase.js"]; + protected array $additionalHead = [""]; + + /** + * Gets the display text for a given status key. + * @param string $statusKey + * @return string + */ + protected function getStatusText(string $statusKey): string + { + $statusMap = array_column($this->statusColumn['table']['filterOptions'] ?? [], 'text', 'value'); + return $statusMap[$statusKey] ?? ucfirst(str_replace('_', ' ', $statusKey)); + } + + //region SHARED ACTIONS + /** + * Fetches documentation and journal entries for a given workorder. + */ + protected function getDocumentationAction() + { + if (empty($this->request->workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); + + $docs = WorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); + $journals = WorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); + + $tenantConfig = $this->getTenantConfigFromWorkorder((int)$this->request->workorderId); + if ($tenantConfig && !empty($tenantConfig->documentationTypes)) { + $customTypes = json_decode($tenantConfig->documentationTypes, true); + $customMap = array_column($customTypes, 'text', 'value'); + $translationMap = array_merge(['civil_engineering_photo' => 'Tiefbau_Foto'], $customMap); + } + + + $responseDocs = []; + $typeCounts = []; + + foreach ($docs as $doc) { + $file = new File($doc->fileId); + $documentTypeKey = $doc->documentType; + $typeCounts[$documentTypeKey] = ($typeCounts[$documentTypeKey] ?? 0) + 1; + $originalFilename = $file->orig_filename ?? $file->filename; + $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); + $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; + $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); + + $responseDocs[] = [ + 'id' => $doc->id, 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'description' => $doc->description, + 'documentType' => $documentTypeKey, 'userName' => UserModel::getOne($doc->createBy)->name ?? 'Unbekannt', + 'mimetype' => $file->mimetype ?? 'application/octet-stream', 'create' => $doc->create + ]; + } + + foreach ($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + + self::returnJson(['docs' => $responseDocs, 'journals' => $journals]); + } + + /** + * Adds a new entry to a workorder's journal. + */ + protected function addJournalAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); + + WorkorderJournalModel::create(['workorderId' => $post['workorderId'], 'text' => $post['text'], 'createBy' => $this->user->id, 'create' => time()]); + + $journals = WorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']); + foreach ($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + self::returnJson(['success' => true, 'message' => 'Journaleintrag hinzugefügt.', 'journals' => $journals]); + } + + /** + * Updates the additional info field for a workorder. + */ + protected function updateAdditionalInfoAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($post['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldInfo = $workorder->additionalInfo; + $newInfo = $post['additionalInfo'] ?? null; + $workorder->additionalInfo = $newInfo; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Zusatzinfo geändert.\nAlt: '{$oldInfo}'\nNeu: '{$newInfo}'", + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Zusatzinfo aktualisiert.', 'newInfo' => $newInfo]); + } + //endregion + + protected function getTenantConfigFromWorkorder(int $workorderId) { + if (empty($workorderId)) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($workorderId); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + $preorder = new Preorder($workorder->preorderId); + if (!$preorder->id) self::sendError("Vorbestellung nicht gefunden."); + $campaign = new Preordercampaign($preorder->preordercampaign_id); + if (!$campaign->id) self::sendError("Kampagne nicht gefunden."); + $network = NetworkModel::getOne($campaign->network_id); + if (!$network) self::sendError("Netzwerk nicht gefunden."); + + return WorkorderTenantConfigModel::getFirst(['addressId' => $network->owner_id]) ?? null; + } +} \ No newline at end of file diff --git a/application/WorkorderCompany/WorkorderCompanyController.php b/application/WorkorderCompany/WorkorderCompanyController.php new file mode 100644 index 000000000..38273e7e9 --- /dev/null +++ b/application/WorkorderCompany/WorkorderCompanyController.php @@ -0,0 +1,195 @@ + 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], + ['key' => 'networkOwnerName', 'text' => 'Auftraggeber', 'table' => ['sortable' => false]], + ['key' => 'preorderInfo', 'text' => 'Kunde', 'modal' => false, 'table' => ['sortable' => false]], + ['key' => 'rimo_fcp_name', 'text' => 'FCP', 'modal' => false, 'table' => ['sortable' => false]], + // Status column is now inherited via prepareCrudConfig + ['key' => 'additionalInfo', 'text' => 'Notiz', 'modal' => false, 'table' => ['sortable' => true]], + ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date', 'sortable' => true]], + ]; + protected array $additionalJSVariables = ['COMPANY_ID' => '0']; + + protected function prepareCrudConfig() { + $preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key')); + array_splice($this->columns, $preorderInfoColIdx + 1, 0, [$this->statusColumn]); + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + $this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0; + } + + //region ACTIONS + public function indexAction() { + parent::indexAction(); + } + + protected function getAction() { + $pagination = $this->postData['pagination'] ?? ['page' => 1, 'per_page' => 10]; + $filters = $this->postData['filters'] ?? []; + $order = $this->postData['order'] ?? []; + + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + if (!$company) { + self::returnJson(['rows' => [], 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0])]); + return; + } + + $workorders = WorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $company->id); + $totalCount = WorkorderModel::countCompanyWorkorders($filters, $company->id); + + self::returnJson([ + 'rows' => $workorders, + 'pagination' => ['page' => $pagination['page'], 'per_page' => $pagination['per_page'], 'total_rows' => $totalCount, 'total_pages' => ceil($totalCount / $pagination['per_page']), 'filtered_available' => $totalCount] + ]); + } + + public function getWorkorderByIdAction() { + if (empty($this->request->id)) self::sendError("ID fehlt"); + $workorder = WorkorderModel::get($this->request->id); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); + self::returnJson((array)$workorder); + } + + protected function scheduleAppointmentAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['appointmentDate'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden"); + if ((int)date('H', $this->postData['appointmentDate']) >= 23 || (int)date('H', $this->postData['appointmentDate']) < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); + + $workorder->appointmentDate = $this->postData['appointmentDate']; + $workorder->status = 'scheduled'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $this->postData['appointmentDate']), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Termin erfolgreich gespeichert.']); + } + + protected function rescheduleAppointmentAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['appointmentDate']) || empty($this->postData['reason'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + if ((int)date('H', $this->postData['appointmentDate']) >= 23 || (int)date('H', $this->postData['appointmentDate']) < 1) self::sendError("Bitte geben Sie eine Uhrzeit an!"); + + $oldDateFormatted = $workorder->appointmentDate ? date('d.m.Y H:i', $workorder->appointmentDate) : 'N/A'; + $newDateFormatted = date('d.m.Y H:i', $this->postData['appointmentDate']); + $workorder->appointmentDate = $this->postData['appointmentDate']; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Termin verschoben von {$oldDateFormatted} auf {$newDateFormatted}. Grund: " . $this->postData['reason'], + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Termin erfolgreich verschoben.']); + } + + protected function requestInterventionAction() { + if (empty($this->postData['workorderId']) || empty($this->postData['journalText'])) self::sendError("Erforderliche Felder fehlen."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldStatus = $workorder->status; + $workorder->status = 'intervention_required'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Eingriff erforderlich: " . $this->postData['journalText'], + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('intervention_required'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Eingriff wurde angefordert.']); + } + + protected function completeWorkorderAction() { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $workorder->status = 'documented'; + WorkorderModel::update((array)$workorder); + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag zur Prüfung eingereicht.']); + } + + protected function getTenantConfigAction() { + $tenantConfig = $this->getTenantConfigFromWorkorder($this->request->workorderId); + + if (!$tenantConfig) { + self::returnJson(['success' => false, 'message' => 'Keine Mandantenkonfiguration gefunden.']); + return; + } + self::returnJson(['success' => true, 'documentationTypes' => json_decode($tenantConfig->documentationTypes, true), 'civilEngineeringDocsRequired' => $tenantConfig->civilEngineeringDocsRequired]); + } + + protected function uploadDocumentationAction() { + if (empty($_FILES['files']) || empty($_POST['workorderId'])) self::sendError('Erforderliche Daten fehlen.'); + $workorderId = $_POST['workorderId']; + + foreach ($_FILES['files']['name'] as $index => $name) { + if ($_FILES['files']['error'][$index] === UPLOAD_ERR_OK) { + $_FILES['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("file", false, "/Workorder"); + WorkorderDocumentationModel::create(['workorderId' => $workorderId, 'fileId' => $uploaded->id, 'description' => $_POST['description'] ?? '', 'documentType' => $_POST['documentType'] ?? 'general', 'create' => time(), 'createBy' => $this->user->id]); + } catch (Exception $e) { /* Log error if necessary */ + } + } + } + + $workorder = WorkorderModel::get($workorderId); + if (in_array($workorder->status, ['correction_requested', 'problem_solved', 'civil_engineering_completed'])) { + $workorder->status = 'assigned'; + WorkorderModel::update((array)$workorder); + } + + self::returnJson(['success' => true, 'message' => "Datei(en) erfolgreich hochgeladen."]); + } + + protected function deleteDocumentationAction() { + if (empty($this->postData['id'])) self::sendError("Dokumenten-ID fehlt."); + WorkorderDocumentationModel::delete($this->postData['id']); + self::returnJson(['success' => true, 'message' => 'Dokument gelöscht.']); + } + + protected function updateDocumentationAction() { + if (empty($this->postData['id'])) self::sendError("Dokumenten-ID fehlt."); + $doc = WorkorderDocumentationModel::get($this->postData['id']); + if (!$doc) self::sendError("Dokument nicht gefunden."); + if (isset($this->postData['documentType'])) $doc->documentType = $this->postData['documentType']; + WorkorderDocumentationModel::update((array)$doc); + self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.']); + } + + protected function completeCivilEngineeringAction() { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + // Re-assign to original company + if ($workorder->originalCompanyId) { + $workorder->companyId = $workorder->originalCompanyId; + $workorder->originalCompanyId = null; + } + + $oldStatus = $workorder->status; + $workorder->civilEngineeringCompanyId = null; + $workorder->status = 'civil_engineering_completed'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, 'text' => "Tiefbau abgeschlossen.", + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('civil_engineering_completed'), + 'create' => time(), 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']); + } + //endregion +} \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php b/application/WorkorderCompany/WorkorderCompanyModel.php similarity index 65% rename from application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php rename to application/WorkorderCompany/WorkorderCompanyModel.php index 318d44d77..b24193820 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyModel.php +++ b/application/WorkorderCompany/WorkorderCompanyModel.php @@ -1,7 +1,7 @@ preorderId)) return null; + + $db = self::getDB(); + $dbName = FRONKDB_DBNAME; + $tableWTC = self::getFullyQualifiedTable(); + $preorderId = $db->real_escape_string($workorder->preorderId); + + $sql = "SELECT wtc.* FROM $tableWTC wtc + JOIN `$dbName`.`Network` n ON wtc.addressId = n.owner_id + JOIN `$dbName`.`Preordercampaign` pc ON n.id = pc.network_id + JOIN `$dbName`.`Preorder` p ON pc.id = p.preordercampaign_id + WHERE p.id = '$preorderId' LIMIT 1"; + + $row = $db->query($sql)?->fetch_assoc(); + + return $row ? new self($row) : null; + }} \ No newline at end of file diff --git a/db/migrations/20250901410000_workorder_rename.php b/db/migrations/20250901410000_workorder_rename.php new file mode 100644 index 000000000..339448e8e --- /dev/null +++ b/db/migrations/20250901410000_workorder_rename.php @@ -0,0 +1,81 @@ +table('RMLWorkorder')->rename('Workorder'); + $this->table('RMLWorkorderCompany')->rename('WorkorderCompany'); + $this->table('RMLWorkorderDocumentation')->rename('WorkorderDocumentation'); + $this->table('RMLWorkorderJournal')->rename('WorkorderJournal'); + $this->table('RMLWorkorderTenantConfig')->rename('WorkorderTenantConfig'); + + $workorderTable = $this->table('Workorder'); + $workorderTable->addColumn('civilEngineeringCompanyId', 'integer', ['null' => true, 'default' => null, 'after' => 'companyId', 'comment' => 'Company assigned for civil engineering task']) + ->addColumn('originalCompanyId', 'integer', ['null' => true, 'default' => null, 'after' => 'civilEngineeringCompanyId', 'comment' => 'Stores the companyId before assigning to civil engineering']) + ->addIndex(['civilEngineeringCompanyId'], ['name' => 'civilEngineeringCompanyId_idx']) + ->addIndex(['originalCompanyId'], ['name' => 'originalCompanyId_idx']) + ->save(); + + $workorderTable->changeColumn('status', 'enum', [ + 'values' => [ + 'new', + 'assigned', + 'scheduled', + 'correction_requested', + 'intervention_required', + 'civil_engineering_required', + 'civil_engineering_completed', + 'problem_solved', + 'documented', + 'completed', + 'cancelled' + ], + 'default' => 'new', + 'null' => false + ])->save(); + + $tenantConfigTable = $this->table('WorkorderTenantConfig'); + $tenantConfigTable->addColumn('civilEngineeringDocsRequired', 'boolean', ['default' => false, 'null' => false, 'after' => 'workorderCreationFilters', 'comment' => 'If true, civil engineering company must upload docs to complete task']) + ->save(); + } + + public function down(): void + { + $tenantConfigTable = $this->table('WorkorderTenantConfig'); + $tenantConfigTable->removeColumn('civilEngineeringDocsRequired')->save(); + + $workorderTable = $this->table('Workorder'); + $workorderTable->changeColumn('status', 'enum', [ + 'values' => [ + 'new', + 'assigned', + 'scheduled', + 'correction_requested', + 'intervention_required', + 'problem_solved', + 'documented', + 'completed', + 'cancelled' + ], + 'default' => 'new', + 'null' => false + ])->save(); + + $workorderTable->removeColumn('civilEngineeringCompanyId') + ->removeColumn('originalCompanyId') + ->removeIndexByName('civilEngineeringCompanyId_idx') + ->removeIndexByName('originalCompanyId_idx') + ->save(); + + $this->table('Workorder')->rename('RMLWorkorder'); + $this->table('WorkorderCompany')->rename('RMLWorkorderCompany'); + $this->table('WorkorderDocumentation')->rename('RMLWorkorderDocumentation'); + $this->table('WorkorderJournal')->rename('RMLWorkorderJournal'); + $this->table('WorkorderTenantConfig')->rename('RMLWorkorderTenantConfig'); + } +} diff --git a/lib/Helper/Helper.php b/lib/Helper/Helper.php index e81644fc7..cf013a7ce 100644 --- a/lib/Helper/Helper.php +++ b/lib/Helper/Helper.php @@ -213,4 +213,16 @@ class Helper { return $returnObject ? $campaigns : array_column($campaigns, 'id'); } + + public static function getPreorderCampaignNetworkOwners() { + $sql = "SELECT a.id FROM Preordercampaign pc + LEFT JOIN Network n ON pc.network_id = n.id + LEFT JOIN Address a ON n.owner_id = a.id + GROUP BY a.id + ORDER BY a.company, a.lastname, a.firstname"; + + $results = FronkDB::singleton()->fetch_all_assoc(FronkDB::singleton()->query($sql)) ?? []; + + return array_map(fn($owner) => new Address($owner['id']), $results); + } } \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js deleted file mode 100644 index 06963ffb9..000000000 --- a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js +++ /dev/null @@ -1,601 +0,0 @@ -// RMLWorkorderAdmin.js -Vue.component('r-m-l-workorder-admin', { - template: ` - -
- {{ workordersToAssign.length }} Workorder(s) zuweisen: -
- -
-
- - - - - - - - - - - - - - - - - -
- `, - data() { - return { - window, - workordersToAssign: [], - editingWorkorderId: null, - editingDeadlineId: null, - editingAdditionalInfoId: null, - tempAdditionalInfo: '', - companiesByTenant: {}, - companiesLoading: false, - massAssignCompanyId: null, - massAssignLoading: false, - companiesForMassAssign: [], - crudConfig: { - ...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true, - customRowClass: (row) => { - if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; - if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; - const deadlineDate = moment.unix(row.deadlineDate); - if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; - if (daysLeft <= 21) return 'tt-rml-workorder-medium'; - return 'tt-rml-workorder-ontrack'; - }, - additionalActions: [] - } - } - }, - methods: { - addToAssignList(row) { - if (!this.workordersToAssign.includes(row.id)) this.workordersToAssign.push(row.id); - }, - removeFromAssignList(row) { - this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id); - }, - getStatusColumn(status) { - const column = this.crudConfig.columns.find(c => c.key === 'status'); - return column.table.filterOptions.find(opt => opt.value === status) || {}; - }, - formatDate(timestamp, withTime = false) { - if (!timestamp) return '–'; - return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); - }, - async getCompaniesForWorkorder(workorder) { - if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) return; - this.companiesLoading = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`, {params: {tenantId: workorder.tenantId}}); - this.$set(this.companiesByTenant, workorder.tenantId, data); - } catch (e) { - window.notify('error', 'Firmenliste konnte nicht geladen werden.'); - } finally { - this.companiesLoading = false; - } - }, - async startCompanyEdit(row) { - await this.getCompaniesForWorkorder(row); - this.editingWorkorderId = row.id; - }, - async assignCompany(workorder, companyId) { - if (!companyId) { - this.editingWorkorderId = null; - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/assignWorkorder`, { - workorderId: workorder.id, - companyId: companyId - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.editingWorkorderId = null; - } - }, - async massAssignCompanies(companyId) { - if (!companyId) return; - if (!confirm(`${this.workordersToAssign.length} Workorder(s) der ausgewählten Firma zuweisen?`)) { - this.massAssignCompanyId = null; - return; - } - this.massAssignLoading = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/massAssignWorkorders`, { - companyId: companyId, - workorderIds: this.workordersToAssign - }); - if (data.success) { - window.notify('success', data.message); - this.workordersToAssign = []; - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.massAssignLoading = false; - this.massAssignCompanyId = null; - } - }, - async updateDeadline(workorder, newDate) { - if (!newDate) { - this.editingDeadlineId = null; - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/updateDeadline`, { - workorderId: workorder.id, - deadlineDate: newDate - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.editingDeadlineId = null; - } - }, - async acceptDocumentation(workorderId) { - if (!confirm('Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden?')) return; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/acceptDocumentation`, {workorderId}); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - async setToProblemSolved(row) { - const text = prompt('Bitte geben Sie einen kurzen Text für den Journaleintrag ein:', ''); - if (text === null) return; - if (!text.trim()) { - window.notify('error', 'Bitte geben Sie einen Text ein.'); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/setToProblemSolved`, { - workorderId: row.id, - text: text - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - startAdditionalInfoEdit(row) { - this.editingAdditionalInfoId = row.id; - this.tempAdditionalInfo = row.additionalInfo || ''; - this.$nextTick(() => { - this.$refs.editTextarea?.$el.querySelector('textarea').focus(); - }); - }, - cancelEdit() { - this.editingAdditionalInfoId = null; - this.tempAdditionalInfo = ''; - }, - async updateAdditionalInfo(row, newInfo) { - if (row.additionalInfo === newInfo) { - this.cancelEdit(); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/updateAdditionalInfo`, { - workorderId: row.id, - additionalInfo: newInfo - }); - if (data.success) { - window.notify('success', data.message); - row.additionalInfo = newInfo; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Update.'); - } finally { - this.cancelEdit(); - } - }, - async cancelWorkorder(row) { - const reason = prompt('Bitte geben Sie einen Grund für die Stornierung an (optional):'); - if (reason === null) return; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/cancelWorkorder`, { - workorderId: row.id, - reason: reason - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Stornierung fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - } - }, - watch: { - workordersToAssign: { - async handler(newVal) { - if (newVal.length === 0) { - this.companiesForMassAssign = []; - return; - } - const firstWorkorder = this.$refs.table.$refs.table.rows.find(r => r.id === newVal[0]); - if (!firstWorkorder) return; - const firstTenantId = firstWorkorder.tenantId; - if (!newVal.every(id => { - const wo = this.$refs.table.$refs.table.rows.find(r => r.id === id); - return wo && wo.tenantId === firstTenantId; - })) { - window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.'); - this.workordersToAssign.pop(); - return; - } - await this.getCompaniesForWorkorder(firstWorkorder); - this.companiesForMassAssign = this.companiesByTenant[firstTenantId] || []; - }, - deep: true - } - } -}); - -Vue.component('traffic-light', { - props: ['deadline', 'status'], - computed: { - lightInfo() { - if (['completed', 'new', 'cancelled'].includes(this.status)) return { - color: '#cccccc', - title: 'Status irrelevant für Dringlichkeit' - }; - const deadlineDate = moment.unix(this.deadline); - if (!deadlineDate.isValid()) return {color: '#cccccc', title: 'Keine Deadline gesetzt'}; - if (deadlineDate.isBefore(moment())) return {color: '#dc3545', title: 'Deadline überschritten'}; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return {color: '#dc3545', title: 'Dringend: Weniger als 1 Woche'}; - if (daysLeft <= 21) return {color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen'}; - return {color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen'}; - } - }, - template: `` -}); - -Vue.component('rml-documentation-viewer-admin', { - props: ['workorderId'], - template: ` -
-
-
-
- -
-
-
-
Korrektur - anfordern
-
-

Wählen Sie die zu korrigierenden Dokumente aus der Galerie aus und geben Sie - einen Grund an.

- - -
-
-
-
Dokumentation - akzeptieren
-
-

Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.

-
-
    -
  • - - {{ docType.text }} -
  • -
-
- -
-
-
-
Journal
-
-
    -
  • - {{ formatDate(log.create) }} ({{ log.createByName }}): -
    {{ log.text }}
    -
  • -
-
Keine Journaleinträge.
-
- -
-
-
-
`, - data: () => ({ - loading: true, - loadingConfig: true, - correctionLoading: false, - docs: [], - journals: [], - selectedDocs: [], - correctionText: '', - newJournalMessage: '', - addingJournalEntry: false, - tenantDocTypes: null - }), - computed: { - requiredDocTypes() { - if (this.tenantDocTypes) return this.tenantDocTypes; - return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, { - value: 'photo_hup_open', - text: 'Foto von dem offenen HÜP' - }, { - value: 'photo_splice_cassette_hup', - text: 'Foto der Spleißkassette – HÜP' - }, { - value: 'photo_splice_cassette_fcp', - text: 'Foto der Spleißkassette - FCP' - }, { - value: 'photo_hup_closed_stickers', - text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' - }, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, { - value: 'photo_patch_position_osp', - text: 'Foto der Patch-Position - OSP-Seite' - }, { - value: 'photo_patch_position_anb', - text: 'Foto der Patch-Position - ANB-Seite' - }, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; - } - }, - methods: { - isUploaded(docType) { - return this.docs.some(doc => doc.documentType === docType); - }, - async fetchData() { - this.loading = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getDocumentation`, {params: {workorderId: this.workorderId}}); - this.docs = data.docs; - this.journals = data.journals; - } catch (e) { - window.notify('error', 'Dokumentation konnte nicht geladen werden.'); - } finally { - this.loading = false; - } - }, - async loadTenantConfig() { - this.loadingConfig = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); - if (data.success) this.tenantDocTypes = data.documentationTypes; - } catch (e) { - console.error("Konnte Mandantenkonfiguration nicht laden", e); - } finally { - this.loadingConfig = false; - } - }, - async requestCorrection() { - if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.'); - if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.'); - this.correctionLoading = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/requestCorrection`, { - workorderId: this.workorderId, - text: this.correctionText, - fileIds: this.selectedDocs - }); - if (data.success) { - window.notify('success', data.message); - this.correctionText = ''; - this.selectedDocs = []; - await this.fetchData(); - this.$emit('workorder-updated'); - } else window.notify('error', data.message); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - this.correctionLoading = false; - }, - async addJournalEntry() { - if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte geben Sie eine Nachricht ein.'); - this.addingJournalEntry = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/addJournal`, { - workorderId: this.workorderId, - text: this.newJournalMessage - }); - if (data.success) { - window.notify('success', data.message || 'Journal-Eintrag hinzugefügt.'); - this.newJournalMessage = ''; - this.journals = data.journals; - } else window.notify('error', data.message || 'Eintrag konnte nicht gespeichert werden.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.addingJournalEntry = false; - } - }, - formatDate(timestamp) { - return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm'); - }, - }, - async mounted() { - await this.loadTenantConfig(); - await this.fetchData(); - } -}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css b/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css deleted file mode 100644 index 76ca5510f..000000000 --- a/public/js/pages/RMLWorkorderCompany/RMLWorkorder.css +++ /dev/null @@ -1,40 +0,0 @@ -/* - * CSS for Workorder Table Row Highlighting - */ - -/* Urgent: Deadline passed or less than 1 week away */ -.table-hover .tt-rml-workorder-urgent:hover, -.tt-rml-workorder-urgent { - background-color: #fbe9e7 !important; /* Soft Red */ -} - -/* Medium: Deadline less than 3 weeks away */ -.table-hover .tt-rml-workorder-medium:hover, -.tt-rml-workorder-medium { - background-color: #fff8e1 !important; /* Soft Yellow */ -} - -/* On Track: Deadline more than 3 weeks away */ -.table-hover .tt-rml-workorder-ontrack:hover, -.tt-rml-workorder-ontrack { - background-color: #e8f5e9 !important; /* Soft Green */ -} - -/* Irrelevant: No deadline or status makes it not applicable */ -.table-hover .tt-rml-workorder-irrelevant:hover, -.tt-rml-workorder-irrelevant { - background-color: #fafafa !important; /* Very light grey */ -} - -.table-hover .tt-rml-workorder-high:hover, -.tt-rml-workorder-high { - background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */ -} - -.tt-file-gallery-item.border.border-danger { - border: 4px solid #f1556c!important; -} - -.RMLWorkorderCompany-table .modal-body { - overflow-y: hidden; -} \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js b/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js deleted file mode 100644 index fa0ffa657..000000000 --- a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js +++ /dev/null @@ -1,568 +0,0 @@ -// RMLWorkorderCompany.js -Vue.component('r-m-l-workorder-company', { - template: ` -
- - - - - - - - - - - - - - - - - - -

Auftrag: #{{ rescheduleData.workorder.id }}

- - -
-
- `, - data() { - return { - rescheduleData: null, editingAdditionalInfoId: null, tempAdditionalInfo: '', - crudConfig: { - ...window.TT_CONFIG.CRUD_CONFIG, expandable: true, - customRowClass: (row) => { - if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; - if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; - const deadlineDate = moment.unix(row.deadlineDate); - if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; - if (daysLeft <= 21) return 'tt-rml-workorder-medium'; - return 'tt-rml-workorder-ontrack'; - }, - additionalActions: [] - } - } - }, - methods: { - getStatusColumn(status) { - const column = this.crudConfig.columns.find(c => c.key === 'status'); - return column.table.filterOptions.find(opt => opt.value === status) || {}; - }, - formatDate(timestamp, withTime = false) { - if (!timestamp) return '–'; - return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); - }, - async setAppointment(workorder, date) { - if (!date) return; - const hour = moment.unix(date).hour(); - if (hour >= 23 || hour < 1) { - this.$refs.table.$refs.table.refreshTable(); - return window.notify('error', 'Bitte Uhrzeit angeben!'); - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/scheduleAppointment`, { - workorderId: workorder.id, - appointmentDate: date - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - openRescheduleModal(row) { - this.rescheduleData = {workorder: row, newDate: row.appointmentDate, reason: ''}; - }, - closeRescheduleModal() { - this.rescheduleData = null; - }, - async rescheduleAppointment() { - const {workorder, newDate, reason} = this.rescheduleData; - if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.'); - if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!'); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/rescheduleAppointment`, { - workorderId: workorder.id, - appointmentDate: newDate, - reason: reason - }); - if (data.success) { - window.notify('success', data.message); - this.$refs.table.$refs.table.refreshTable(); - this.closeRescheduleModal(); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', e.response?.data?.message || 'Ein Netzwerkfehler ist aufgetreten.'); - } - }, - startAdditionalInfoEdit(row) { - this.editingAdditionalInfoId = row.id; - this.tempAdditionalInfo = row.additionalInfo || ''; - this.$nextTick(() => { - this.$refs.editTextarea?.$el.querySelector('textarea').focus(); - }); - }, - cancelEdit() { - this.editingAdditionalInfoId = null; - this.tempAdditionalInfo = ''; - }, - async updateAdditionalInfo(row, newInfo) { - if (row.additionalInfo === newInfo) { - this.cancelEdit(); - return; - } - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/updateAdditionalInfo`, { - workorderId: row.id, - additionalInfo: newInfo - }); - if (data.success) { - window.notify('success', data.message); - row.additionalInfo = newInfo; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Update.'); - } finally { - this.cancelEdit(); - } - }, - } -}); - -Vue.component('traffic-light', { - props: ['deadline', 'status'], - computed: { - lightInfo() { - if (['completed', 'new', 'cancelled'].includes(this.status)) return { - color: '#cccccc', - title: 'Status irrelevant für Dringlichkeit' - }; - const deadlineDate = moment.unix(this.deadline); - if (!deadlineDate.isValid()) return {color: '#cccccc', title: 'Keine Deadline gesetzt'}; - if (deadlineDate.isBefore(moment())) return {color: '#dc3545', title: 'Deadline überschritten'}; - const daysLeft = deadlineDate.diff(moment(), 'days'); - if (daysLeft <= 7) return {color: '#dc3545', title: 'Dringend: Weniger als 1 Woche'}; - if (daysLeft <= 21) return {color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen'}; - return {color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen'}; - } - }, - template: `` -}); - -Vue.component('documentation-manager', { - props: ['workorderId'], - template: ` -
-
-
-
-
-
-
-
Benötigte Dokumente
-
    -
  • - - {{ docType.text }} -
  • -
-
- - - Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen. - -
- Auftrag zur Prüfung eingereicht oder storniert. -
-
-
-
-
Eingriff benötigt -
-
-

Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es - hier.

- -
-
-
-
Journal
-
-
    -
  • Keine Einträge - vorhanden. -
  • -
  • - {{ formatDate(log.create) }} ({{ log.createByName }}): -
    {{ log.text }}
    -
  • -
-
- -
-
-
-
-
-
-
Neues Dokument hochladen
- - -
- -
- -
-
- -
-
- - - -
-
- - -
- - -
-
-
`, - data: () => ({ - loadingWorkorder: true, - loadingConfig: true, - tenantDocTypes: null, - workorder: null, - uploading: false, - completing: false, - uploadedFiles: [], - journals: [], - newJournalMessage: '', - addingJournalEntry: false, - interventionData: null, - interventionTypes: [{value: 'stuck', text: 'Ab X Laufmeter stecken geblieben'}, { - value: 'stuck_fcp', - text: 'Vom FCP nach HÜP nach X Laufmetern stecken geblieben' - }, {value: 'stuck_hup', text: 'Vom HÜP nach FCP nach X Laufmetern stecken geblieben'}, { - value: 'no_air', - text: 'Keine Luftverbindung' - }, {value: 'other', text: 'Sonstiges'}], - uploadData: {files: [], documentType: 'photo_hup_mounted', description: ''} - }), - computed: { - requiredDocTypes() { - if (this.tenantDocTypes) return this.tenantDocTypes; - return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, { - value: 'photo_hup_open', - text: 'Foto von dem offenen HÜP' - }, { - value: 'photo_splice_cassette_hup', - text: 'Foto der Spleißkassette – HÜP' - }, { - value: 'photo_splice_cassette_fcp', - text: 'Foto der Spleißkassette - FCP' - }, { - value: 'photo_hup_closed_stickers', - text: 'Foto vom geschlossenen HÜP mit allen Aufklebern' - }, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, { - value: 'photo_patch_position_osp', - text: 'Foto der Patch-Position - OSP-Seite' - }, { - value: 'photo_patch_position_anb', - text: 'Foto der Patch-Position - ANB-Seite' - }, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; - }, - allDocTypes() { - return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; - }, - canComplete() { - return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); - }, - filesWithStatus() { - if (!this.journals?.length) return this.uploadedFiles; - const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested')); - if (!correctionJournal?.fileIds) return this.uploadedFiles; - try { - const incorrectFileIds = JSON.parse(correctionJournal.fileIds); - if (!Array.isArray(incorrectFileIds)) return this.uploadedFiles; - return this.uploadedFiles.map(file => incorrectFileIds.includes(file.id) ? { - ...file, - class: 'border border-danger' - } : file); - } catch (e) { - return this.uploadedFiles; - } - } - }, - methods: { - formatDate(timestamp) { - return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; - }, - async loadTenantConfig() { - this.loadingConfig = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); - if (data.success) this.tenantDocTypes = data.documentationTypes; - } catch (e) { - console.error("Konnte Mandantenkonfiguration nicht laden", e); - } finally { - this.loadingConfig = false; - } - }, - async loadWorkorder() { - this.loadingWorkorder = true; - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getWorkorderById`, {params: {id: this.workorderId}}); - this.workorder = data; - } catch (e) { - window.notify('error', 'Arbeitsauftragsdetails konnten nicht geladen werden.'); - } - this.loadingWorkorder = false; - }, - isUploaded(docType) { - return this.uploadedFiles.some(file => file.documentType === docType); - }, - async fetchDocs() { - try { - const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/getDocumentation`, {params: {workorderId: this.workorderId}}); - this.uploadedFiles = data.docs; - this.journals = data.journals; - } catch (e) { - window.notify('error', 'Dokumente konnten nicht geladen werden.'); - } - }, - handleFileUpload(event) { - this.uploadData.files = event.target.files; - }, - async uploadFiles() { - if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); - this.uploading = true; - const formData = new FormData(); - formData.append('workorderId', this.workorder.id); - formData.append('documentType', this.uploadData.documentType); - formData.append('description', this.uploadData.description); - for (const file of this.uploadData.files) formData.append('files[]', file); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/uploadDocumentation`, formData); - if (data.success) { - window.notify('success', data.message); - this.$refs.fileInput.value = ''; - this.uploadData.files = []; - this.uploadData.description = ''; - this.uploadedFiles = data.docs; - this.workorder = data.workorder; - } else window.notify('error', data.error || 'Upload fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); - } - this.uploading = false; - }, - async completeWorkorder() { - if (!confirm('Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?')) return; - this.completing = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/completeWorkorder`, {workorderId: this.workorder.id}); - if (data.success) { - window.notify('success', data.message); - this.$emit('workorder-completed'); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - this.completing = false; - }, - async deleteDocumentation(file) { - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/deleteDocumentation`, {id: file.id}); - if (data.success) { - window.notify('success', data.message); - this.uploadedFiles = data.docs; - } else window.notify('error', data.message || 'Löschen fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Löschen.'); - } - }, - async updateDocumentation(file) { - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/updateDocumentation`, { - id: file.id, - documentType: file.documentType - }); - if (data.success) { - window.notify('success', data.message); - this.uploadedFiles = data.docs; - } else window.notify('error', data.message || 'Update fehlgeschlagen.'); - } catch (e) { - window.notify('error', 'Netzwerkfehler beim Aktualisieren.'); - } - }, - async addJournalEntry() { - if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte geben Sie eine Nachricht ein.'); - this.addingJournalEntry = true; - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/addJournal`, { - workorderId: this.workorderId, - text: this.newJournalMessage - }); - if (data.success) { - window.notify('success', data.message || 'Journal-Eintrag hinzugefügt.'); - this.newJournalMessage = ''; - this.journals = data.journals; - } else window.notify('error', data.message || 'Eintrag konnte nicht gespeichert werden.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } finally { - this.addingJournalEntry = false; - } - }, - getInterventionLabel(type) { - return this.interventionTypes.find(t => t.value === type)?.text || type; - }, - openInterventionModal() { - this.interventionData = { - types: [], - details: { - stuck: {distance: ''}, - stuck_fcp: {distance: ''}, - stuck_hup: {distance: ''}, - other: {reason: ''} - } - }; - }, - async requestIntervention() { - const {types, details} = this.interventionData; - if (types.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Problem aus.'); - let journalParts = []; - types.sort(); - for (const type of types) { - let text = ''; - const problemText = this.interventionTypes.find(o => o.value === type)?.text || 'Unbekanntes Problem'; - if (['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)) { - const distance = details[type]?.distance; - if (!distance || isNaN(distance) || distance <= 0) return window.notify('error', `Bitte eine gültige Distanz für "${problemText}" eingeben.`); - text = problemText.replace('X', distance); - } else if (type === 'no_air') text = problemText; - else if (type === 'other') { - const reason = details.other?.reason; - if (!reason?.trim()) return window.notify('error', 'Bitte geben Sie einen Grund für "Sonstiges" an.'); - text = `Sonstiges: ${reason.trim()}`; - } - if (text) journalParts.push(text); - } - const journalText = journalParts.join('\\n'); - if (!journalText) return window.notify('error', 'Keine gültigen Problemdetails zum Senden gefunden.'); - try { - const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderCompany/requestIntervention`, { - workorderId: this.workorderId, - journalText - }); - if (data.success) { - window.notify('success', data.message); - this.interventionData = null; - this.$emit('workorder-completed'); - } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); - } catch (e) { - window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); - } - } - }, - async mounted() { - await this.loadWorkorder(); - await this.loadTenantConfig(); - await this.fetchDocs(); - } -}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js b/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js deleted file mode 100644 index 47f09d012..000000000 --- a/public/js/pages/RMLWorkorderCompanyDashboardView/RMLWorkorderCompanyDashboardView.js +++ /dev/null @@ -1,53 +0,0 @@ -// RMLWorkorderCompanyDashboardView.js -// This would be a separate file and view. - -Vue.component('rml-workorder-company-dashboard', { - template: ` -
-
-
-
-
-

Meine offenen Aufträge

-

{{ stats.assigned || 0 }}

-
-
-
-
-
-
-

Meine dringenden Aufträge

-

{{ stats.urgent || 0 }}

-
-
-
-
-
-
-

Meine terminierten Aufträge

-

{{ stats.scheduled || 0 }}

-
-
-
-
- -
-
- - - -
-
-
- `, - data() { - return { - stats: {} - } - }, - async mounted() { - // You would create a new controller action e.g., /RMLWorkorder/getCompanyDashboardStats - // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getCompanyDashboardStats`); - // this.stats = response.data; - } -}) \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js b/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js deleted file mode 100644 index 15cd84e01..000000000 --- a/public/js/pages/RMLWorkorderDashboardView/RMLWorkorderDashboardView.js +++ /dev/null @@ -1,61 +0,0 @@ -// RMLWorkorderAdminDashboardView.js -// This would be a separate file and view. - -Vue.component('rml-workorder-admin-dashboard', { - template: ` -
-
-
-
-
-

Neue Aufträge

-

{{ stats.new || 0 }}

-
-
-
-
-
-
-

In Arbeit

-

{{ stats.in_progress || 0 }}

-
-
-
-
-
-
-

Überfällig

-

{{ stats.overdue || 0 }}

-
-
-
-
-
-
-

Abgeschlossen (30T)

-

{{ stats.completed_30d || 0 }}

-
-
-
-
- -
-
- - - -
-
-
- `, - data() { - return { - stats: {} - } - }, - async mounted() { - // You would create a new controller action e.g., /RMLWorkorder/getDashboardStats - // const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorder/getDashboardStats`); - // this.stats = response.data; - } -}) \ No newline at end of file diff --git a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js new file mode 100644 index 000000000..2c3db0984 --- /dev/null +++ b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js @@ -0,0 +1,310 @@ +// WorkorderAdmin.js +Vue.component('workorder-admin', { + template: ` + +
+ {{ workordersToAssign.length }} Workorder(s) zuweisen: +
+ +
+
+ + + + + + + + + + + + + + + + + + +

Auftrag: #{{ civilEngineeringData.workorder.id }}

+ +
+ + +

Soll der Auftrag #{{ cancelWorkorderModalData.id }} wirklich storniert werden?

+ +
+ + +

Soll das Problem bei Auftrag #{{ problemSolvedModalData.id }} als gelöst markiert werden?

+ +
+ + +

Sollen {{ workordersToAssign.length }} Workorder(s) der Firma {{ massAssignModalData.companyName }} zugewiesen werden?

+ +
+ +
+ `, + data() { + return { + window, workordersToAssign: [], editingWorkorderId: null, editingDeadlineId: null, editingAdditionalInfoId: null, + civilEngineeringData: null, tempAdditionalInfo: '', companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null, + cancelWorkorderModalData: null, problemSolvedModalData: null, massAssignModalData: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; + if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; + if (daysLeft <= 21) return 'tt-rml-workorder-medium'; + return 'tt-rml-workorder-ontrack'; + }, + additionalActions: [] + } + } + }, + computed: { + companiesForMassAssign() { + if (this.workordersToAssign.length === 0) return []; + const firstWorkorder = this.$refs.table?.$refs.table.rows.find(r => r.id === this.workordersToAssign[0]); + return firstWorkorder ? this.companiesByTenant[firstWorkorder.tenantId] || [] : []; + } + }, + methods: { + async acceptDocumentation(workorderId) { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/acceptDocumentation`, { workorderId }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + openCivilEngineeringModal(row) { + this.getCompaniesForWorkorder(row); + this.civilEngineeringData = { workorder: row, companyId: null }; + }, + async assignCivilEngineering() { + if (!this.civilEngineeringData.companyId) return window.notify('error', 'Bitte eine Firma auswählen.'); + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setCivilEngineeringRequired`, { + workorderId: this.civilEngineeringData.workorder.id, companyId: this.civilEngineeringData.companyId + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.civilEngineeringData = null; + } else window.notify('error', data.message || 'Zuweisung fehlgeschlagen.'); + }, + addToAssignList(row) { if (!this.workordersToAssign.includes(row.id)) this.workordersToAssign.push(row.id); }, + removeFromAssignList(row) { this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id); }, + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp, withTime = false) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); + }, + async getCompaniesForWorkorder(workorder) { + if (!workorder.tenantId || this.companiesByTenant[workorder.tenantId]) return; + this.companiesLoading = true; + try { + const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/getCompanies`, {params: {tenantId: workorder.tenantId}}); + this.$set(this.companiesByTenant, workorder.tenantId, data); + } catch (e) { + window.notify('error', 'Firmenliste konnte nicht geladen werden.'); + } finally { + this.companiesLoading = false; + } + }, + async startCompanyEdit(row) { + await this.getCompaniesForWorkorder(row); + this.editingWorkorderId = row.id; + }, + async assignCompany(workorder, companyId) { + if (!companyId) { this.editingWorkorderId = null; return; } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/assignWorkorder`, { workorderId: workorder.id, companyId: companyId }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + this.editingWorkorderId = null; + }, + openMassAssignModal(companyId) { + if (!companyId) return; + const companyName = this.companiesForMassAssign.find(c => c.value === companyId)?.text; + this.massAssignModalData = { companyId, companyName, deadline: null }; + }, + async massAssignCompanies() { + try { + const { companyId, deadline } = this.massAssignModalData; + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/massAssignWorkorders`, { + companyId: companyId, workorderIds: this.workordersToAssign, deadlineDate: deadline + }); + if (data.success) { + window.notify('success', data.message); + this.workordersToAssign = []; + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + } catch (e) {} finally { + this.massAssignCompanyId = null; + this.massAssignModalData = null; + } + }, + async updateDeadline(workorder, newDate) { + if (!newDate) { this.editingDeadlineId = null; return; } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateDeadline`, { workorderId: workorder.id, deadlineDate: newDate }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + } catch (e) {} finally { + this.editingDeadlineId = null; + } + }, + async setToProblemSolved() { + const { id, text } = this.problemSolvedModalData; + if (!text || !text.trim()) return window.notify('error', 'Bitte geben Sie einen Text ein.'); + + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/setToProblemSolved`, { workorderId: id, text: text }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.problemSolvedModalData = null; + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + startAdditionalInfoEdit(row) { + this.editingAdditionalInfoId = row.id; + this.tempAdditionalInfo = row.additionalInfo || ''; + this.$nextTick(() => this.$refs.editTextarea?.$el.querySelector('textarea').focus()); + }, + cancelEdit() { + this.editingAdditionalInfoId = null; + this.tempAdditionalInfo = ''; + }, + async updateAdditionalInfo(row) { + if (row.additionalInfo === this.tempAdditionalInfo) { this.cancelEdit(); return; } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/updateAdditionalInfo`, { + workorderId: row.id, additionalInfo: this.tempAdditionalInfo + }); + if (data.success) { + window.notify('success', data.message); + row.additionalInfo = data.newInfo; // Update local data + } else window.notify('error', data.message || 'Update fehlgeschlagen.'); + } catch (e) { + } finally { + this.cancelEdit(); + } + }, + async cancelWorkorder() { + const { id, reason } = this.cancelWorkorderModalData; + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/cancelWorkorder`, { workorderId: id, reason: reason }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.cancelWorkorderModalData = null; + } else window.notify('error', data.message || 'Stornierung fehlgeschlagen.'); + } + }, + watch: { + workordersToAssign: { + async handler(newVal) { + if (newVal.length === 0) return; + const rows = this.$refs.table?.$refs.table.rows; + if (!rows) return; + + const firstWorkorder = rows.find(r => r.id === newVal[0]); + if (!firstWorkorder) return; + + const firstTenantId = firstWorkorder.tenantId; + const allSameTenant = newVal.every(id => { + const wo = rows.find(r => r.id === id); + return wo && wo.tenantId === firstTenantId; + }); + + if (!allSameTenant) { + window.notify('error', 'Massen-Zuweisung nur für Aufträge des gleichen Mandanten möglich.'); + this.workordersToAssign.pop(); + return; + } + await this.getCompaniesForWorkorder(firstWorkorder); + }, + deep: true + } + } +}); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css b/public/js/pages/WorkorderBase/WorkorderBase.css similarity index 100% rename from public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css rename to public/js/pages/WorkorderBase/WorkorderBase.css diff --git a/public/js/pages/WorkorderBase/WorkorderBase.js b/public/js/pages/WorkorderBase/WorkorderBase.js new file mode 100644 index 000000000..6f1b957f5 --- /dev/null +++ b/public/js/pages/WorkorderBase/WorkorderBase.js @@ -0,0 +1,471 @@ +// WorkorderCommon.js + +// A simple component to display a status light based on a deadline. +Vue.component('traffic-light', { + props: ['deadline', 'status'], + computed: { + lightInfo() { + const deadlineDate = moment.unix(this.deadline); + const daysLeft = deadlineDate.diff(moment(), 'days'); + + if (['completed', 'new', 'cancelled'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' }; + if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' }; + if (deadlineDate.isBefore(moment())) return { color: '#dc3545', title: 'Deadline überschritten' }; + if (daysLeft <= 7) return { color: '#dc3545', title: 'Dringend: Weniger als 1 Woche' }; + if (daysLeft <= 21) return { color: '#ffc107', title: 'Mittel: Weniger als 3 Wochen' }; + return { color: '#28a745', title: 'Im Plan: Mehr als 3 Wochen' }; + } + }, + template: `` +}); + +// A manager for civil engineering tasks, used when a workorder requires it. +Vue.component('civil-engineering-manager', { + props: ['workorderId', 'isAdmin'], + template: ` +
+
+
+
+
+
+
Tiefbau-Arbeiten
+

Schließen Sie den Tiefbau-Auftrag ab. Laden Sie Dokumente hoch, falls erforderlich.

+
+ + + Bitte laden Sie mindestens ein Dokument hoch, um den Auftrag abzuschließen. + +
+
+
+
+
+
+
Dokument hochladen
+ +
+ +
+ +
+
+ +
+
+ + +
Für diesen Auftraggeber ist keine Dokumentation für Tiefbau-Arbeiten erforderlich.
+
+
+ + Möchten Sie diese Tiefbau-Arbeiten wirklich als abgeschlossen markieren? + +
+ `, + data: () => ({ + loading: true, uploading: false, completing: false, docsRequired: false, + uploadedFiles: [], uploadData: { files: [], description: '' }, showCompleteModal: false + }), + computed: { + canComplete() { + return !this.docsRequired || this.uploadedFiles.length > 0; + } + }, + methods: { + async fetchInitialData() { + this.loading = true; + try { + const configRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, { params: { workorderId: this.workorderId } }); + this.docsRequired = configRes.data.civilEngineeringDocsRequired || false; + + if(this.docsRequired) { + const docRes = await axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, { params: { workorderId: this.workorderId } }); + this.uploadedFiles = docRes.data.docs || []; + } + } catch (e) { + window.notify('error', 'Konfiguration konnte nicht geladen werden.'); + console.error(e); + } finally { + this.loading = false; + } + }, + handleFileUpload(event) { this.uploadData.files = event.target.files; }, + async uploadFiles() { + if (!this.uploadData.files?.length) return window.notify('error', 'Bitte eine oder mehrere Dateien auswählen.'); + this.uploading = true; + const formData = new FormData(); + formData.append('workorderId', this.workorderId); + formData.append('documentType', 'civil_engineering_photo'); + formData.append('description', this.uploadData.description); + for (const file of this.uploadData.files) formData.append('files[]', file); + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', data.message); + this.$refs.fileInput.value = ''; + this.uploadData = { files: [], description: '' }; + await this.fetchInitialData(); + } else window.notify('error', data.error || 'Upload fehlgeschlagen.'); + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); + } + this.uploading = false; + }, + async deleteDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); + if (data.success) { + window.notify('success', data.message); + await this.fetchInitialData(); + } else window.notify('error', data.message || 'Löschen fehlgeschlagen.'); + } catch (e) { + window.notify('error', 'Netzwerkfehler beim Löschen.'); + } + }, + async completeTask() { + this.completing = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeCivilEngineering`, { workorderId: this.workorderId }); + if (data.success) { + window.notify('success', data.message); + this.$emit('workorder-completed'); + this.showCompleteModal = false; + } else { + window.notify('error', data.message || 'Abschluss fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.completing = false; + } + } + }, + async mounted() { + await this.fetchInitialData(); + } +}); + + +/** + * Unified component for viewing and managing Workorder Details, Documentation, and Journals. + * Adapts its UI and functionality based on the isAdmin prop. + */ +Vue.component('workorder-details-manager', { + props: { + workorderId: { type: String, required: true }, + isAdmin: { type: Boolean, default: false } + }, + template: ` +
+
+
+
+
+
+
Benötigte Dokumente
+
    +
  • + + {{ docType.text }} +
  • +
+
+ + + Bitte laden Sie alle benötigten Dokumente hoch, um den Auftrag abzuschließen. + +
+ Auftrag bereits abgeschlossen oder storniert. Keine Aktionen mehr möglich. +
+
+
+ +
+
+
Prüfung & Freigabe
+

Prüfen Sie, ob alle erforderlichen Dokumente vorhanden und korrekt sind.

+
    +
  • + + {{ docType.text }} +
  • +
+
+
+ +
+
+ +
+
Journal
+
+
    +
  • + {{ formatDate(log.create) }} ({{ log.createByName }}): +
    {{ log.text }}
    +
  • +
+
Keine Journaleinträge.
+
+ +
+
+ +
+
+
+
Neues Dokument hochladen
+ + +
+
+
+ +
+
+ +
+
Korrektur anfordern
+
+

Die ausgewählten Dokumente werden als fehlerhaft markiert. Bitte geben Sie einen Grund an.

+ + +
+
+ + + + + +
+
Eingriff benötigt
+
+

Falls ein Problem auftritt, das ein Eingreifen erfordert, melden Sie es hier.

+ +
+
+
+
+ + + +
+ + +
+
+ + Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen? + + + Soll die Dokumentation für diesen Arbeitsauftrag wirklich akzeptiert und der Auftrag abgeschlossen werden? + +
`, + data: () => ({ + loading: true, loadingConfig: true, workorder: null, docs: [], journals: [], tenantDocTypes: null, + newJournalMessage: '', addingJournalEntry: false, + // Company state + uploading: false, completing: false, showCompleteModal: false, + uploadData: { files: [], documentType: 'photo_hup_mounted', description: '' }, + interventionData: null, interventionTypes: [ + {value: 'stuck', text: 'Ab X Laufmeter stecken geblieben'}, + {value: 'stuck_fcp', text: 'Vom FCP nach HÜP nach X Laufmetern stecken geblieben'}, + {value: 'stuck_hup', text: 'Vom HÜP nach FCP nach X Laufmetern stecken geblieben'}, + {value: 'no_air', text: 'Keine Luftverbindung'}, {value: 'other', text: 'Sonstiges'} + ], + // Admin state + selectedDocs: [], correctionText: '', correctionLoading: false, showAcceptModal: false, + }), + computed: { + isReadOnly() { return ['documented', 'completed', 'cancelled'].includes(this.workorder?.status); }, + requiredDocTypes() { + if (this.tenantDocTypes) return this.tenantDocTypes; + // Default list as a fallback + return [{value: 'photo_hup_mounted', text: 'Foto vom montierten HÜP'}, {value: 'photo_hup_open', text: 'Foto von dem offenen HÜP'}, {value: 'photo_splice_cassette_hup', text: 'Foto der Spleißkassette – HÜP'}, {value: 'photo_splice_cassette_fcp', text: 'Foto der Spleißkassette - FCP'}, {value: 'photo_hup_closed_stickers', text: 'Foto vom geschlossenen HÜP mit allen Aufklebern'}, {value: 'photo_fcp_labeled', text: 'Foto vom FCP beschriftet'}, {value: 'photo_patch_position_osp', text: 'Foto der Patch-Position - OSP-Seite'}, {value: 'photo_patch_position_anb', text: 'Foto der Patch-Position - ANB-Seite'}, {value: 'measurement_protocol_otdr', text: 'ODTR – Messung (1310nm & 1550nm)'}]; + }, + allDocTypes() { return [...this.requiredDocTypes, {value: 'other', text: 'Sonstiges Dokument (optional)'}]; }, + canComplete() { return this.requiredDocTypes.every(docType => this.isUploaded(docType.value)); }, + docsWithStatus() { + if (!this.journals?.length) return this.docs; + const correctionJournal = [...this.journals].sort((a, b) => b.create - a.create).find(j => j.statusChange?.includes('correction_requested')); + if (!correctionJournal?.fileIds) return this.docs; + try { + const incorrectFileIds = JSON.parse(correctionJournal.fileIds); + if (!Array.isArray(incorrectFileIds)) return this.docs; + return this.docs.map(doc => incorrectFileIds.includes(doc.id) ? { ...doc, class: 'border border-danger' } : doc); + } catch (e) { return this.docs; } + } + }, + methods: { + formatDate(timestamp) { return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; }, + // FIX: Added a guard to prevent calling .some() on an undefined value + isUploaded(docType) { + return Array.isArray(this.docs) && this.docs.some(doc => doc.documentType === docType); + }, + async fetchData() { + this.loading = true; + try { + const [workorderRes, docsJournalsRes] = await Promise.all([ + axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getWorkorderById`, {params: {id: this.workorderId}}), + axios.get(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/getDocumentation`, {params: {workorderId: this.workorderId}}) + ]); + this.workorder = workorderRes.data; + // FIX: Ensure docs and journals are always arrays + this.docs = docsJournalsRes.data.docs || []; + this.journals = docsJournalsRes.data.journals || []; + } catch (e) { + window.notify('error', 'Details konnten nicht geladen werden.'); + this.docs = []; // Ensure it's an array on error + this.journals = []; + } finally { + this.loading = false; + } + }, + async loadTenantConfig() { + this.loadingConfig = true; + try { + const {data} = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/getTenantConfig`, {params: {workorderId: this.workorderId}}); + if (data.success) this.tenantDocTypes = data.documentationTypes; + } catch (e) { console.error("Mandantenkonfiguration nicht geladen", e); } + finally { this.loadingConfig = false; } + }, + async addJournalEntry() { + if (!this.newJournalMessage.trim()) return; + this.addingJournalEntry = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/${this.isAdmin ? 'WorkorderAdmin' : 'WorkorderCompany'}/addJournal`, { workorderId: this.workorderId, text: this.newJournalMessage }); + if (data.success) { + this.newJournalMessage = ''; + this.journals = data.journals; + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + finally { this.addingJournalEntry = false; } + }, + // Company Methods + handleFileUpload(event) { this.uploadData.files = event.target.files; }, + async uploadFiles() { + if (!this.uploadData.files?.length) return window.notify('error', 'Bitte Dateien auswählen.'); + this.uploading = true; + const formData = new FormData(); + formData.append('workorderId', this.workorderId); + formData.append('documentType', this.uploadData.documentType); + formData.append('description', this.uploadData.description); + for (const file of this.uploadData.files) formData.append('files[]', file); + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', data.message); + this.$refs.fileInput.value = ''; + this.uploadData.files = []; + await this.fetchData(); + this.$emit('workorder-updated'); + } else window.notify('error', data.error); + } catch (e) { window.notify('error', 'Upload-Fehler'); } + this.uploading = false; + }, + async deleteDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/deleteDocumentation`, {id: file.id}); + if (data.success) { + window.notify('success', data.message); + await this.fetchData(); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + async updateDocumentation(file) { + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateDocumentation`, { id: file.id, documentType: file.documentType }); + if (data.success) { + window.notify('success', data.message); + await this.fetchData(); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + openInterventionModal() { + this.interventionData = { types: [], details: { stuck: {}, stuck_fcp: {}, stuck_hup: {}, other: {} } }; + }, + async requestIntervention() { + const { types, details } = this.interventionData; + if (types.length === 0) return window.notify('error', 'Bitte Problem auswählen.'); + let journalParts = []; + for (const type of types.sort()) { + const problemText = this.interventionTypes.find(o => o.value === type)?.text || type; + if (['stuck', 'stuck_fcp', 'stuck_hup'].includes(type)) { + if (!details[type]?.distance > 0) return window.notify('error', `Bitte DisWtanz für "${problemText}" eingeben.`); + journalParts.push(problemText.replace('X', details[type].distance)); + } else if (type === 'other') { + if (!details.other?.reason?.trim()) return window.notify('error', `Bitte Grund für "Sonstiges" angeben.`); + journalParts.push(`Sonstiges: ${details.other.reason.trim()}`); + } else journalParts.push(problemText); + } + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/requestIntervention`, { workorderId: this.workorderId, journalText: journalParts.join('\\n') }); + if (data.success) { + window.notify('success', data.message); + this.interventionData = null; + this.$emit('workorder-completed'); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + }, + async completeWorkorder() { + this.completing = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/completeWorkorder`, {workorderId: this.workorderId}); + if (data.success) { + window.notify('success', data.message); + this.$emit('workorder-completed'); + this.showCompleteModal = false; + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + this.completing = false; + }, + // Admin Methods + async requestCorrection() { + if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund an.'); + if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte Dokumente für die Korrektur auswählen.'); + this.correctionLoading = true; + try { + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderAdmin/requestCorrection`, { + workorderId: this.workorderId, text: this.correctionText, fileIds: this.selectedDocs + }); + if (data.success) { + window.notify('success', data.message); + this.correctionText = ''; + this.selectedDocs = []; + await this.fetchData(); + this.$emit('workorder-completed'); + } else window.notify('error', data.message); + } catch (e) { window.notify('error', 'Netzwerkfehler'); } + this.correctionLoading = false; + }, + acceptDocumentation() { + this.$emit('accept-documentation', this.workorderId); + this.showAcceptModal = false; + }, + getInterventionLabel(type) { return this.interventionTypes.find(t => t.value === type)?.text || type; }, + }, + async mounted() { + await this.loadTenantConfig(); + await this.fetchData(); + } +}); \ No newline at end of file diff --git a/public/js/pages/WorkorderCompany/WorkorderCompany.js b/public/js/pages/WorkorderCompany/WorkorderCompany.js new file mode 100644 index 000000000..50b64058f --- /dev/null +++ b/public/js/pages/WorkorderCompany/WorkorderCompany.js @@ -0,0 +1,176 @@ +// WorkorderCompany.js +Vue.component('workorder-company', { + template: ` +
+ + + + + + + + + + + + + + + + + +

Auftrag: #{{ rescheduleModalData.workorder.id }}

+ + +
+
+ `, + data() { + return { + window, + rescheduleModalData: null, + editingAdditionalInfoId: null, + tempAdditionalInfo: '', + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-rml-workorder-irrelevant'; + if (['correction_requested', 'intervention_required'].includes(row.status)) return 'tt-rml-workorder-high'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-rml-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; + if (daysLeft <= 21) return 'tt-rml-workorder-medium'; + return 'tt-rml-workorder-ontrack'; + }, + additionalActions: [] + } + } + }, + methods: { + getStatusColumn(status) { + const column = this.crudConfig.columns.find(c => c.key === 'status'); + return column.table.filterOptions.find(opt => opt.value === status) || {}; + }, + formatDate(timestamp, withTime = false) { + if (!timestamp) return '–'; + return window.moment.unix(timestamp).format(withTime ? 'DD.MM.YYYY HH:mm' : 'DD.MM.YYYY'); + }, + async setAppointment(workorder, date) { + if (!date) return; + if (moment.unix(date).hour() >= 23 || moment.unix(date).hour() < 1) { + this.$refs.table.$refs.table.refreshTable(); + return window.notify('error', 'Bitte Uhrzeit angeben!'); + } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/scheduleAppointment`, { + workorderId: workorder.id, appointmentDate: date + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + openRescheduleModal(row) { + this.rescheduleModalData = { workorder: row, newDate: row.appointmentDate, reason: '' }; + }, + async rescheduleAppointment() { + const { workorder, newDate, reason } = this.rescheduleModalData; + if (!newDate || !reason) return window.notify('error', 'Bitte geben Sie ein neues Datum und einen Grund an.'); + if (moment.unix(newDate).hour() >= 23 || moment.unix(newDate).hour() < 1) return window.notify('error', 'Bitte Uhrzeit angeben!'); + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/rescheduleAppointment`, { + workorderId: workorder.id, appointmentDate: newDate, reason: reason + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.rescheduleModalData = null; + } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); + }, + startAdditionalInfoEdit(row) { + this.editingAdditionalInfoId = row.id; + this.tempAdditionalInfo = row.additionalInfo || ''; + this.$nextTick(() => this.$refs.editTextarea?.$el.querySelector('textarea').focus()); + }, + cancelEdit() { + this.editingAdditionalInfoId = null; + this.tempAdditionalInfo = ''; + }, + async updateAdditionalInfo(row) { + if (row.additionalInfo === this.tempAdditionalInfo) { + this.cancelEdit(); + return; + } + const {data} = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderCompany/updateAdditionalInfo`, { + workorderId: row.id, additionalInfo: this.tempAdditionalInfo + }); + if (data.success) { + window.notify('success', data.message); + row.additionalInfo = data.newInfo; + } else window.notify('error', data.message || 'Update fehlgeschlagen.'); + this.cancelEdit(); + }, + } +}); \ No newline at end of file diff --git a/public/plugins/vue/tt-components/tt-modal.js b/public/plugins/vue/tt-components/tt-modal.js index b37625c99..6a372e1ab 100644 --- a/public/plugins/vue/tt-components/tt-modal.js +++ b/public/plugins/vue/tt-components/tt-modal.js @@ -1,6 +1,6 @@ Vue.component('tt-modal', { props: { - show: {type: Boolean, default: false}, + show: {type: [Boolean, Object], default: false}, title: {type: String, default: 'Überschrift'}, delete: {type: Boolean, default: true}, deleteText: {type: String, default: 'Löschen'}, diff --git a/public/plugins/vue/tt-components/tt-number-range.js b/public/plugins/vue/tt-components/tt-number-range.js index 9b7065f55..32367747a 100644 --- a/public/plugins/vue/tt-components/tt-number-range.js +++ b/public/plugins/vue/tt-components/tt-number-range.js @@ -10,6 +10,12 @@ Vue.component('tt-number-range', { }; }, watch: { value(val) { + if (typeof val === 'undefined' || val === null || val === '') { + this.inputValueFrom = ''; + this.inputValueTo = ''; + return; + } + if (this.returnText !== true) { this.inputValueFrom = val.from; this.inputValueTo = val.to;