diff --git a/application/Workorder/WorkorderModel.php b/application/Workorder/WorkorderModel.php index 0ef3c261c..6ec80a964 100644 --- a/application/Workorder/WorkorderModel.php +++ b/application/Workorder/WorkorderModel.php @@ -24,7 +24,7 @@ class WorkorderModel extends TTCrudBaseModel $sql = Helper::generateFilterCondition(array_map('intval', $allowedCampaignIds), 'p.preordercampaign_id'); if (empty($filters['status'])) { - $sql .= " AND w.status NOT IN ('completed', 'cancelled')"; + $sql .= " AND w.status NOT IN ('completed', 'cancelled', 'charged', 'archived')"; } else { $sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true); } @@ -114,12 +114,16 @@ class WorkorderModel extends TTCrudBaseModel private static function buildCompanyWhereClause(array $filters, int $companyId): string { - $sql = "(w.companyId = " . $companyId . " OR w.civilEngineeringCompanyId = " . $companyId . ")"; + $sql = "(w.companyId = " . $companyId . " OR w.civilEngineeringCompanyId = " . $companyId . ") AND w.status != 'charged'"; + + if (empty($filters['status'])) { + $sql .= " AND w.status NOT IN ('completed', 'cancelled', 'archived')"; + } else { + $sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true); + } - if (empty($filters['status'])) $sql .= " AND w.status NOT IN ('completed', 'cancelled')"; - else $sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true); if (!empty($filters['id'])) $sql .= Helper::generateFilterCondition($filters['id'], 'w.id', true); - if (!empty($filters['status'])) $sql .= Helper::generateFilterCondition($filters['status'], 'w.status'); + if (!empty($filters['preordercampaign_id'])) $sql .= Helper::generateFilterCondition($filters['preordercampaign_id'], 'p.preordercampaign_id'); if (!empty($filters['deadlineDate'])) $sql .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); if (!empty($filters['networkOwnerName'])) $sql .= Helper::generateFilterCondition($filters['networkOwnerName'], 'owner_addr.company'); if (!empty($filters['appointmentDate'])) $sql .= Helper::generateFilterCondition($filters['appointmentDate'], 'w.appointmentDate'); @@ -157,7 +161,7 @@ class WorkorderModel extends TTCrudBaseModel $orderBy = ""; if (!empty($order['key'])) { - $sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate', 'additionalInfo']; + $sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate', 'additionalInfo', 'preordercampaign_id']; if (in_array($order['key'], $sortableColumns)) { $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; $orderBy = " ORDER BY " . $db->real_escape_string($order['key']) . " " . $sortOrder; @@ -193,4 +197,4 @@ class WorkorderModel extends TTCrudBaseModel $result = $db->query($sql); return $result ? $result->fetch_assoc()['count'] : 0; } -} +} \ No newline at end of file diff --git a/application/WorkorderAdmin/WorkorderAdminController.php b/application/WorkorderAdmin/WorkorderAdminController.php index 72507b591..07f82e930 100644 --- a/application/WorkorderAdmin/WorkorderAdminController.php +++ b/application/WorkorderAdmin/WorkorderAdminController.php @@ -54,6 +54,7 @@ class WorkorderAdminController extends WorkorderBaseController public function indexAction() { $this->createWorkordersFromPreorders(); + $this->archiveWorkorders(); parent::indexAction(); } @@ -177,6 +178,40 @@ class WorkorderAdminController extends WorkorderBaseController self::returnJson(['success' => true, 'message' => 'Dokumentation akzeptiert und Arbeitsauftrag abgeschlossen.']); } + protected function chargeWorkorderAction() + { + if (!$this->user->can('RMLAdmin')) { + self::sendError("Keine Berechtigung."); + } + + 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 !== 'completed') { + self::sendError("Nur abgeschlossene Arbeitsaufträge können verrechnet werden."); + } + + $oldStatus = $workorder->status; + $workorder->status = 'charged'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, + 'text' => 'Arbeitsauftrag wurde als verrechnet markiert.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('charged'), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag als verrechnet markiert.']); + } + protected function setToProblemSolvedAction() { if (empty($this->postData['workorderId']) || empty($this->postData['text'])) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); @@ -276,32 +311,5 @@ class WorkorderAdminController extends WorkorderBaseController } return true; } - - private function createWorkordersFromPreorders() - { - $configs = WorkorderTenantConfigModel::getAll(); - foreach ($configs as $config) { - $filters = json_decode($config->workorderCreationFilters, true); - if (empty($filters)) continue; - - $networks = NetworkModel::search(['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 index 300007e08..281872390 100644 --- a/application/WorkorderBase/WorkorderBaseController.php +++ b/application/WorkorderBase/WorkorderBaseController.php @@ -20,7 +20,9 @@ class WorkorderBaseController extends TTCrud ['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' => 'charged', 'text' => 'Verrechnet', 'icon' => 'fas fa-euro-sign text-purple'], ['value' => 'cancelled', 'text' => 'Abgebrochen', 'icon' => 'fas fa-ban text-danger'], + ['value' => 'archived', 'text' => 'Archiviert', 'icon' => 'fas fa-archive text-muted'], ]] ]; @@ -50,6 +52,7 @@ class WorkorderBaseController extends TTCrud $journals = WorkorderJournalModel::getAll(['workorderId' => intval($this->request->workorderId)], null, 0, ['key' => 'create', 'order' => 'DESC']); $tenantConfig = $this->getTenantConfigFromWorkorder((int)$this->request->workorderId); + $translationMap = []; if ($tenantConfig && !empty($tenantConfig->documentationTypes)) { $customTypes = json_decode($tenantConfig->documentationTypes, true); $customMap = array_column($customTypes, 'text', 'value'); @@ -136,4 +139,107 @@ class WorkorderBaseController extends TTCrud return WorkorderTenantConfigModel::getFirst(['addressId' => $network->owner_id]) ?? null; } + + //region BACKGROUND TASKS + /** + * Creates new workorders from preorders based on tenant configurations. + * Runs at most once every 5 minutes to avoid performance issues. + */ + protected function createWorkordersFromPreorders() + { + $lockFile = TEMP_DIR . "/task_create_workorders.lock"; + if (file_exists($lockFile) && (time() - intval(file_get_contents($lockFile))) < 300) { + return; // Run only every 5 minutes + } + + $configs = WorkorderTenantConfigModel::getAll(); + foreach ($configs as $config) { + $filters = json_decode($config->workorderCreationFilters, true); + if (empty($filters)) continue; + + $networks = NetworkModel::search(['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 + ]); + } + } + } + file_put_contents($lockFile, time()); + } + + /** + * Archives workorders that are no longer considered active based on tenant configurations. + * Runs at most once every 5 minutes to avoid performance issues. + */ + protected function archiveWorkorders() + { + $lockFile = TEMP_DIR . "/task_archive_workorders.lock"; + if (file_exists($lockFile) && (time() - intval(file_get_contents($lockFile))) < 300) { + return; // Run only every 5 minutes + } + + $configs = WorkorderTenantConfigModel::getAll(); + foreach ($configs as $config) { + $activeFilters = json_decode($config->workorderActiveFilters, true); + if (empty($activeFilters)) { + continue; + } + + $networks = NetworkModel::search(['owner_id' => $config->addressId]); + if (empty($networks)) { + continue; + } + + $tenantCampaignIds = array_column(PreordercampaignModel::getAll(['network_id' => array_column($networks, 'id')]), 'id'); + if (empty($tenantCampaignIds)) { + continue; + } + + $activeFilters['preordercampaign_id'] = $tenantCampaignIds; + + $activePreorderIds = array_column(PreorderModel::searchActive($activeFilters), 'id'); + $activePreorderIdsSet = array_flip($activePreorderIds); + + $statusesToCheck = ['new', 'assigned', 'scheduled', 'correction_requested', 'intervention_required', 'civil_engineering_required', 'civil_engineering_completed', 'problem_solved']; + + $allTenantPreorders = PreorderModel::getAll(['preordercampaign_id' => $tenantCampaignIds]); + if(empty($allTenantPreorders)) continue; + + $allTenantPreorderIds = array_column($allTenantPreorders, 'id'); + + $workordersToCheck = WorkorderModel::getAll([ + 'status' => $statusesToCheck, + 'preorderId' => $allTenantPreorderIds + ]); + + foreach ($workordersToCheck as $workorder) { + if (!isset($activePreorderIdsSet[$workorder->preorderId])) { + $oldStatus = $workorder->status; + $workorder->status = 'archived'; + WorkorderModel::update((array)$workorder); + + WorkorderJournalModel::create([ + 'workorderId' => $workorder->id, + 'text' => 'Arbeitsauftrag wurde automatisch archiviert, da die zugehörige Vorbestellung nicht mehr den Aktiv-Kriterien entspricht.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('archived'), + 'create' => time(), + 'createBy' => 1, // System user + ]); + } + } + } + file_put_contents($lockFile, time()); + } + //endregion } \ No newline at end of file diff --git a/application/WorkorderCompany/WorkorderCompanyController.php b/application/WorkorderCompany/WorkorderCompanyController.php index 9b05d54cd..8a8a66891 100644 --- a/application/WorkorderCompany/WorkorderCompanyController.php +++ b/application/WorkorderCompany/WorkorderCompanyController.php @@ -16,7 +16,7 @@ class WorkorderCompanyController extends WorkorderBaseController { ['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 array $additionalJSVariables = ['COMPANY_ID' => '0', 'RML_COMPANY_MANAGER' => false]; protected function prepareCrudConfig() { $preorderInfoColIdx = array_search('preorderInfo', array_column($this->columns, 'key')); @@ -34,6 +34,14 @@ class WorkorderCompanyController extends WorkorderBaseController { $this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0; + if ($this->user->can('RMLCompanyManager')) { + $this->additionalJSVariables['RML_COMPANY_MANAGER'] = true; + } + } + + public function indexAction() { + $this->archiveWorkorders(); + parent::indexAction(); } protected function logout() { @@ -185,7 +193,7 @@ class WorkorderCompanyController extends WorkorderBaseController { $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); + WorkorderModel::update((array)$doc); self::returnJson(['success' => true, 'message' => 'Dokument aktualisiert.']); } @@ -213,4 +221,4 @@ class WorkorderCompanyController extends WorkorderBaseController { self::returnJson(['success' => true, 'message' => 'Tiefbau erfolgreich abgeschlossen.']); } //endregion -} +} \ No newline at end of file diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigController.php b/application/WorkorderTenantConfig/WorkorderTenantConfigController.php index 50027294f..8a7dc0099 100644 --- a/application/WorkorderTenantConfig/WorkorderTenantConfigController.php +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigController.php @@ -19,6 +19,7 @@ class WorkorderTenantConfigController extends TTCrud { $data['documentationTypes'] = json_encode($data['documentationTypes'] ?? []); $data['interventionTypes'] = json_encode($data['interventionTypes'] ?? []); $data['workorderCreationFilters'] ??= '{}'; + $data['workorderActiveFilters'] ??= '{}'; if (empty($data['id'])) { $data['create'] = time(); diff --git a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php index 112679fc9..862642472 100644 --- a/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php +++ b/application/WorkorderTenantConfig/WorkorderTenantConfigModel.php @@ -7,6 +7,7 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { public string $name; public string $documentationTypes; // JSON public string $workorderCreationFilters; // JSON + public ?string $workorderActiveFilters; // JSON public ?string $interventionTypes; // JSON public int $civilEngineeringDocsRequired; public int $create; @@ -30,4 +31,4 @@ class WorkorderTenantConfigModel extends TTCrudBaseModel { $row = $result ? $result->fetch_assoc() : null; return $row ? new self($row) : null; - }} + }} \ No newline at end of file diff --git a/db/migrations/20251008120000_workorder_add_statuses_and_config_and_permission.php b/db/migrations/20251008120000_workorder_add_statuses_and_config_and_permission.php new file mode 100644 index 000000000..dfe330ad4 --- /dev/null +++ b/db/migrations/20251008120000_workorder_add_statuses_and_config_and_permission.php @@ -0,0 +1,91 @@ +getEnvironment() !== 'thetool') { + return; + } + + $this->table('Workorder') + ->changeColumn('status', 'enum', [ + 'values' => [ + 'new', + 'assigned', + 'scheduled', + 'correction_requested', + 'intervention_required', + 'civil_engineering_required', + 'civil_engineering_completed', + 'problem_solved', + 'documented', + 'completed', + 'cancelled', + 'archived', + 'charged' + ], + 'default' => 'new', + 'null' => false, + ]) + ->save(); + + $workorderTenantConfigTable = $this->table('WorkorderTenantConfig'); + if (!$workorderTenantConfigTable->hasColumn('workorderActiveFilters')) + $workorderTenantConfigTable->addColumn('workorderActiveFilters', 'text', [ + 'null' => true, + 'default' => null, + 'limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG, + ]) + ->save(); + + $workerPermissionTable = $this->table('WorkerPermission'); + if (!$workerPermissionTable->hasColumn('canRMLCompanyManager')) + $workerPermissionTable->addColumn('canRMLCompanyManager', 'enum', [ + 'values' => ['false', 'true'], + 'null' => false, + 'default' => 'false', + ]) + ->save(); + } + + public function down(): void + { + if ($this->getEnvironment() !== 'thetool') { + return; + } + + $this->table('Workorder') + ->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(); + + $workorderTenantConfigTable = $this->table('WorkorderTenantConfig'); + if ($workorderTenantConfigTable->hasColumn('workorderActiveFilters')) + $workorderTenantConfigTable->removeColumn('workorderActiveFilters')->save(); + + $workerPermissionTable = $this->table('WorkerPermission'); + if ($workerPermissionTable->hasColumn('canRMLCompanyManager')) + $workerPermissionTable->removeColumn('canRMLCompanyManager')->save(); + } +} + diff --git a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js index 71675ea46..561f6f1ab 100644 --- a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js +++ b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js @@ -57,10 +57,12 @@ Vue.component('workorder-admin', { additional-class="btn-link workorder-button" title="Tiefbau benötigt" /> + - +