'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true, 'filter' => 'numberRange']], ['key' => 'hausnummerInfo', 'text' => 'Adresse', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => false]], ['key' => 'netzgebietName', 'text' => 'Netzgebiet', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], ['key' => 'companyName', 'text' => 'Firma', 'modal' => false, 'table' => ['filter' => 'search', 'sortable' => true]], ['key' => 'wohneinheitCount', 'text' => 'WE', 'modal' => false, 'table' => ['sortable' => false]], ['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 function prepareCrudConfig() { $hausnummerInfoColIdx = array_search('hausnummerInfo', array_column($this->columns, 'key')); array_splice($this->columns, $hausnummerInfoColIdx + 1, 0, [$this->statusColumn]); } public function indexAction() { $this->createWorkordersFromHausnummer(); parent::indexAction(); } protected function getAction() { $pagination = $this->postData['pagination'] ?? ['page' => 1, 'per_page' => 10]; $filters = $this->postData['filters'] ?? []; $order = $this->postData['order'] ?? []; $db = FronkDB::singleton(); $fronkDbName = FRONKDB_DBNAME; $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; $whereClauses = "WHERE 1=1"; if (empty($filters['status'])) { $whereClauses .= " AND w.status NOT IN ('completed', 'cancelled', 'archived')"; } else { $whereClauses .= Helper::generateFilterCondition($filters['status'], 'w.status', true); } if (!empty($filters['id'])) $whereClauses .= Helper::generateFilterCondition($filters['id'], 'w.id', true); if (!empty($filters['hausnummerInfo'])) { $searchColumns = "str.name|hn.hausnummer|hn.stiege|plz.plz|ort.name|w.additionalInfo"; $whereClauses .= Helper::generateFilterCondition($filters['hausnummerInfo'], $searchColumns); } if (!empty($filters['netzgebietName'])) $whereClauses .= Helper::generateFilterCondition($filters['netzgebietName'], 'ng.name'); if (!empty($filters['companyName'])) $whereClauses .= Helper::generateFilterCondition($filters['companyName'], 'c.name'); if (!empty($filters['deadlineDate'])) $whereClauses .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); if (!empty($filters['additionalInfo'])) $whereClauses .= Helper::generateFilterCondition($filters['additionalInfo'], 'w.additionalInfo'); $sql = " SELECT w.id, w.status, w.deadlineDate, w.appointmentDate, w.companyId, w.additionalInfo, IFNULL(c.name, 'Nicht zugewiesen') as companyName, CONCAT_WS(' ', str.name, hn.hausnummer, hn.stiege) as hausnummerInfo, str.name as street, hn.hausnummer, hn.stiege, plz.plz, ort.name as city, IFNULL(ng.name, '-') as netzgebietName, (SELECT COUNT(*) FROM `$addressDbName`.`Wohneinheit` we WHERE we.hausnummer_id = hn.id) as wohneinheitCount FROM `$fronkDbName`.`WorkorderMph` w LEFT JOIN `$fronkDbName`.`WorkorderCompany` c ON w.companyId = c.id LEFT JOIN `$addressDbName`.`Hausnummer` hn ON w.hausnummerId = hn.id LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id $whereClauses "; $orderBy = ""; if (!empty($order['key'])) { $sortableColumns = ['id', 'status', 'deadlineDate', 'companyName', 'additionalInfo', 'appointmentDate', 'netzgebietName']; if (in_array($order['key'], $sortableColumns)) { $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; $orderBy = " ORDER BY " . $db->escape($order['key']) . " " . $sortOrder; } } if (empty($orderBy)) $orderBy = " ORDER BY CASE WHEN w.deadlineDate IS NULL THEN 1 ELSE 0 END, w.deadlineDate ASC"; $sql .= $orderBy; // Get total count $countSql = "SELECT COUNT(*) as count FROM `$fronkDbName`.`WorkorderMph` w LEFT JOIN `$fronkDbName`.`WorkorderCompany` c ON w.companyId = c.id LEFT JOIN `$addressDbName`.`Hausnummer` hn ON w.hausnummerId = hn.id LEFT JOIN `$addressDbName`.`Strasse` str ON hn.strasse_id = str.id LEFT JOIN `$addressDbName`.`Plz` plz ON hn.plz_id = plz.id LEFT JOIN `$addressDbName`.`Ortschaft` ort ON hn.ortschaft_id = ort.id LEFT JOIN `$addressDbName`.`Netzgebiet` ng ON hn.netzgebiet_id = ng.id $whereClauses"; $totalCount = $db->query($countSql)->fetch_assoc()['count']; // Add pagination if ($pagination['per_page'] !== null) { $sql .= " LIMIT " . intval($pagination['per_page']) . " OFFSET " . intval(($pagination['page'] - 1) * $pagination['per_page']); } $result = $db->query($sql); $rows = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; 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() { $companies = WorkorderCompanyModel::getAll(); 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'); $workorder = WorkorderMphModel::get($this->postData['workorderId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $oldStatus = $workorder->status; $oldCompanyId = $workorder->companyId; $workorder->companyId = $this->postData['companyId']; $workorder->status = 'assigned'; $workorder->assignmentDate = time(); $workorder->deadlineDate = $deadline; WorkorderMphModel::update((array)$workorder); $company = WorkorderCompanyModel::get($this->postData['companyId']); $statusChange = $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('assigned'); WorkorderMphJournalModel::create([ 'workorderMphId' => $workorder->id, 'text' => "Arbeitsauftrag zugewiesen an: " . ($company ? $company->name : "Firma ID " . $this->postData['companyId']), 'statusChange' => $statusChange, 'create' => time(), 'createBy' => $this->user->id, ]); self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich zugewiesen.']); } protected function updateDeadlineAction() { if (empty($this->postData['workorderId']) || empty($this->postData['deadlineDate'])) self::sendError("Erforderliche Felder fehlen."); $workorder = WorkorderMphModel::get($this->postData['workorderId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $workorder->deadlineDate = $this->postData['deadlineDate']; WorkorderMphModel::update((array)$workorder); WorkorderMphJournalModel::create([ 'workorderMphId' => $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 = WorkorderMphModel::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."); $oldStatus = $workorder->status; $workorder->status = 'completed'; WorkorderMphModel::update((array)$workorder); WorkorderMphJournalModel::create([ 'workorderMphId' => $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.']); } /** * Background task: Creates WorkorderMph from Hausnummer with >2 Wohneinheiten * and RIMO state not in grossplaning/not2connect */ private function createWorkordersFromHausnummer() { $lockFile = TEMP_DIR . "/task_create_workorder_mph.lock"; if (file_exists($lockFile) && (time() - intval(file_get_contents($lockFile))) < 300) { return; // Run only every 5 minutes } $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); // Build netzgebiet filter $netzgebietIds = defined('TT_WORKORDER_MPH_NETZGEBIET_IDS') ? TT_WORKORDER_MPH_NETZGEBIET_IDS : []; $netzgebietFilter = ''; if (!empty($netzgebietIds)) { $escapedIds = array_map(fn($id) => $db->escape($id), $netzgebietIds); $netzgebietFilter = " AND hn.netzgebiet_id IN (" . implode(',', $escapedIds) . ")"; } // Find Hausnummer with >2 Wohneinheiten and state not in grossplaning/not2connect $sql = " SELECT hn.id, hn.netzgebiet_id, COUNT(we.id) as we_count FROM Hausnummer hn LEFT JOIN Wohneinheit we ON hn.id = we.hausnummer_id WHERE hn.rimo_ex_state NOT IN ('grossplaning', 'not2connect') AND hn.rimo_op_state NOT IN ('grossplaning', 'not2connect') $netzgebietFilter GROUP BY hn.id HAVING we_count > 2 "; $result = $db->query($sql); $hausnummern = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; // Get valid hausnummer IDs $validHausnummerIds = array_column($hausnummern, 'id'); foreach ($hausnummern as $hn) { // Check if WorkorderMph already exists $existing = WorkorderMphModel::getFirst(['hausnummerId' => $hn['id']]); if (!$existing) { // Create new WorkorderMph WorkorderMphModel::create([ 'hausnummerId' => $hn['id'], 'status' => 'new', 'create' => time(), 'createBy' => 1 // System user ]); } elseif ($existing->status === 'archived') { // Reactivate archived workorder $existing->status = 'new'; $existing->companyId = null; $existing->deadlineDate = null; $existing->appointmentDate = null; WorkorderMphModel::update((array)$existing); WorkorderMphJournalModel::create([ 'workorderMphId' => $existing->id, 'text' => 'Arbeitsauftrag wurde automatisch reaktiviert.', 'statusChange' => $this->getStatusText('archived') . " -> " . $this->getStatusText('new'), 'create' => time(), 'createBy' => 1, ]); } } // Archive workorders for Hausnummer that are no longer in allowed netzgebiete or don't meet criteria if (!empty($netzgebietIds)) { $allWorkorders = WorkorderMphModel::getAll(['status' => ['new', 'assigned', 'scheduled', 'in_progress']]); foreach ($allWorkorders as $workorder) { if (!in_array($workorder->hausnummerId, $validHausnummerIds)) { $workorder->status = 'archived'; WorkorderMphModel::update((array)$workorder); WorkorderMphJournalModel::create([ 'workorderMphId' => $workorder->id, 'text' => 'Arbeitsauftrag automatisch archiviert (Netzgebiet deaktiviert oder Kriterien nicht mehr erfüllt).', 'statusChange' => 'active -> archived', 'create' => time(), 'createBy' => 1, ]); } } } file_put_contents($lockFile, time()); } protected function cancelWorkorderAction() { if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); $workorder = WorkorderMphModel::get($this->postData['workorderId']); if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); $oldStatus = $workorder->status; $workorder->status = 'cancelled'; WorkorderMphModel::update((array)$workorder); $reason = !empty($this->postData['reason']) ? $this->postData['reason'] : 'Kein Grund angegeben'; WorkorderMphJournalModel::create([ 'workorderMphId' => $workorder->id, 'text' => "Arbeitsauftrag storniert. Grund: " . $reason, 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('cancelled'), 'create' => time(), 'createBy' => $this->user->id, ]); self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag wurde storniert.']); } }