'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' => 'in_progress', 'text' => 'In Bearbeitung', 'icon' => 'fas fa-cog 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' => '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'], ]] ]; 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' => intval($this->request->workorderId)], null, 0, ['key' => 'create', 'order' => 'ASC']); $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'); $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' => intval($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; } //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) { $existingWorkorder = (array) WorkorderModel::getFirst(['preorderId' => $preorder->id]); if ($existingWorkorder) { if ($existingWorkorder['status'] === 'archived') { $oldStatus = $existingWorkorder['status']; $new = (array) $existingWorkorder; $new['status'] = 'new'; $new['companyId'] = null; $new['civilEngineeringCompanyId'] = null; $new['deadlineDate'] = null; $new['appointmentDate'] = null; $new['clusterId'] = $preorder->preordercampaign_id; WorkorderModel::update($new); WorkorderJournalModel::create([ 'workorderId' => $existingWorkorder['id'], 'text' => 'Arbeitsauftrag wurde automatisch reaktiviert, da die zugehörige Vorbestellung wieder den Kriterien entspricht.', 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('new'), 'create' => time(), 'createBy' => 1, ]); } } else { WorkorderModel::create([ 'preorderId' => $preorder->id, 'clusterId' => $preorder->preordercampaign_id, 'status' => 'new', 'create' => time(), 'createBy' => 1 ]); } } } 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', 'in_progress', '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 }