diff --git a/application/WorkorderMph/WorkorderMphModel.php b/application/WorkorderMph/WorkorderMphModel.php new file mode 100644 index 000000000..5f5688f8e --- /dev/null +++ b/application/WorkorderMph/WorkorderMphModel.php @@ -0,0 +1,21 @@ + '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.']); + } +} diff --git a/application/WorkorderMphBase/WorkorderMphBaseController.php b/application/WorkorderMphBase/WorkorderMphBaseController.php new file mode 100644 index 000000000..03b3811b0 --- /dev/null +++ b/application/WorkorderMphBase/WorkorderMphBaseController.php @@ -0,0 +1,304 @@ + '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' => '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'], + ['value' => 'archived', 'text' => 'Archiviert', 'icon' => 'fas fa-archive text-muted'], + ]] + ]; + + protected array $additionalJS = ["js/pages/WorkorderMphBase/WorkorderMphBase.js"]; + protected array $additionalHead = [""]; + + // Wohneinheit status options + protected array $wohneinheitStatuses = [ + ['value' => 1, 'text' => '10 - new'], + ['value' => 12, 'text' => '241 - BEP installed (MD)'], + ['value' => 13, 'text' => '242 - Inhouse cabling finished'], + ['value' => 18, 'text' => '243 - Cable in stairwell'], + ['value' => 14, 'text' => '244 - BEP installed (SD)'], + ['value' => 15, 'text' => '245 - Installation Approved'], + ['value' => 16, 'text' => '300 - ONT installed'], + ]; + + protected function getStatusText(string $statusKey): string + { + $statusMap = array_column($this->statusColumn['table']['filterOptions'] ?? [], 'text', 'value'); + return $statusMap[$statusKey] ?? ucfirst(str_replace('_', ' ', $statusKey)); + } + + protected function getWohneinheitStatusText(int $statusValue): string + { + $statusMap = array_column($this->wohneinheitStatuses, 'text', 'value'); + return $statusMap[$statusValue] ?? "Status $statusValue"; + } + + //region SHARED ACTIONS + /** + * Fetches documentation and journal entries for a given workorder. + */ + protected function getDocumentationAction() + { + if (empty($this->request->workorderMphId)) self::sendError("Arbeitsauftrags-ID fehlt."); + + $docs = WorkorderMphDocumentationModel::getAll(['workorderMphId' => intval($this->request->workorderMphId)], null, 0, ['key' => 'create', 'order' => 'ASC']); + $journals = WorkorderMphJournalModel::getAll(['workorderMphId' => intval($this->request->workorderMphId)], null, 0, ['key' => 'create', 'order' => 'DESC']); + + $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); + $newFilename = "{$documentTypeKey}_{$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['workorderMphId']) || empty(trim($post['text']))) self::sendError("Arbeitsauftrags-ID und Text sind erforderlich."); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $post['workorderMphId'], + 'text' => $post['text'], + 'createBy' => $this->user->id, + 'create' => time() + ]); + + $journals = WorkorderMphJournalModel::getAll(['workorderMphId' => intval($post['workorderMphId'])], 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['workorderMphId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderMphModel::get($post['workorderMphId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $oldInfo = $workorder->additionalInfo; + $newInfo = $post['additionalInfo'] ?? null; + $workorder->additionalInfo = $newInfo; + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $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]); + } + + /** + * Get all Wohneinheiten for a specific workorder with their statuses and notes + */ + protected function getWohneinheitenAction() + { + if (empty($this->request->workorderMphId)) self::sendError("Arbeitsauftrags-ID fehlt."); + + $workorderMphId = intval($this->request->workorderMphId); + $workorder = WorkorderMphModel::get($workorderMphId); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + // Get all Wohneinheiten for this Hausnummer from addressdb + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + $hausnummerId = $db->escape($workorder->hausnummerId); + + $sql = "SELECT w.id, w.bezeichner, w.contact + FROM Wohneinheit w + WHERE w.hausnummer_id = $hausnummerId + ORDER BY w.bezeichner"; + $result = $db->query($sql); + $wohneinheiten = $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + + // Get existing WorkorderMphWohneinheit records + $existingRecords = WorkorderMphWohneinheitModel::getAll(['workorderMphId' => $workorderMphId]); + $recordsMap = []; + foreach ($existingRecords as $record) { + $recordsMap[$record->wohneinheitId] = $record; + } + + // Merge data + $response = []; + foreach ($wohneinheiten as $we) { + $record = $recordsMap[$we['id']] ?? null; + $response[] = [ + 'wohneinheitId' => intval($we['id']), + 'bezeichner' => $we['bezeichner'], + 'contact' => $we['contact'], + 'status' => $record ? $record->status : 1, + 'note' => $record ? $record->note : null, + 'recordId' => $record ? $record->id : null, + ]; + } + + self::returnJson(['wohneinheiten' => $response, 'statusOptions' => $this->wohneinheitStatuses]); + } + + /** + * Update status and note for a specific Wohneinheit + */ + protected function updateWohneinheitAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderMphId']) || empty($post['wohneinheitId'])) { + self::sendError("Arbeitsauftrags-ID und Wohneinheit-ID sind erforderlich."); + } + + $workorderMphId = intval($post['workorderMphId']); + $wohneinheitId = intval($post['wohneinheitId']); + $status = intval($post['status'] ?? 1); + $note = $post['note'] ?? null; + + // Check if record exists + $existing = WorkorderMphWohneinheitModel::getFirst([ + 'workorderMphId' => $workorderMphId, + 'wohneinheitId' => $wohneinheitId + ]); + + $oldStatus = $existing ? $existing->status : 1; + $oldNote = $existing ? $existing->note : null; + + if ($existing) { + $existing->status = $status; + $existing->note = $note; + $existing->edit = time(); + $existing->editBy = $this->user->id; + WorkorderMphWohneinheitModel::update((array)$existing); + } else { + WorkorderMphWohneinheitModel::create([ + 'workorderMphId' => $workorderMphId, + 'wohneinheitId' => $wohneinheitId, + 'status' => $status, + 'note' => $note, + 'create' => time(), + 'createBy' => $this->user->id + ]); + } + + // Add journal entry if status or note changed + if ($oldStatus !== $status || $oldNote !== $note) { + $changes = []; + if ($oldStatus !== $status) { + $changes[] = "Status: " . $this->getWohneinheitStatusText($oldStatus) . " → " . $this->getWohneinheitStatusText($status); + } + if ($oldNote !== $note) { + $changes[] = "Notiz aktualisiert"; + } + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorderMphId, + 'text' => "Wohneinheit $wohneinheitId: " . implode(', ', $changes), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + + // If status is 241 (BEP MD) or 300 (ONT installed), set statusflag 200 on Wohneinheit + if (in_array($status, [12, 16])) { // 12=241 BEP MD, 16=300 ONT + $this->setWohneinheitStatusflag($wohneinheitId, 200); + } + } + + self::returnJson(['success' => true, 'message' => 'Wohneinheit aktualisiert.']); + } + + /** + * Set statusflag on Wohneinheit in addressdb + */ + private function setWohneinheitStatusflag(int $wohneinheitId, int $statusflagId) + { + $db = FronkDB::singleton(ADDRESSDB_DBHOST, ADDRESSDB_DBUSER, ADDRESSDB_DBPASS, ADDRESSDB_DBNAME); + $weId = $db->escape($wohneinheitId); + $sfId = $db->escape($statusflagId); + + // Check if statusflag already exists + $checkSql = "SELECT COUNT(*) as count FROM WohneinheitStatusflagValue WHERE wohneinheit_id = $weId AND statusflag_id = $sfId"; + $result = $db->query($checkSql); + $exists = $result->fetch_assoc()['count'] > 0; + + if (!$exists) { + $insertSql = "INSERT INTO WohneinheitStatusflagValue (wohneinheit_id, statusflag_id, create, createBy) + VALUES ($weId, $sfId, " . time() . ", " . $this->user->id . ")"; + $db->query($insertSql); + } + } + + /** + * Update checkbox documentation fields + */ + protected function updateCheckboxesAction() + { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderMphId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + + $workorder = WorkorderMphModel::get($post['workorderMphId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $changes = []; + $checkboxFields = ['easement', 'btb', 'fttxLocationSupplied', 'conduitToHuepLaid', 'huepMounted', 'dropCableAvailable']; + + foreach ($checkboxFields as $field) { + if (array_key_exists($field, $post)) { + $oldValue = $workorder->$field; + $newValue = $post[$field] ? 1 : 0; + if ($oldValue !== $newValue) { + $workorder->$field = $newValue; + $changes[] = "$field: " . ($newValue ? 'ja' : 'nein'); + } + } + } + + if (!empty($changes)) { + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorder->id, + 'text' => "Dokumentation aktualisiert:\n" . implode("\n", $changes), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + } + + self::returnJson(['success' => true, 'message' => 'Dokumentation aktualisiert.']); + } + //endregion +} diff --git a/application/WorkorderMphCompany/WorkorderMphCompanyController.php b/application/WorkorderMphCompany/WorkorderMphCompanyController.php new file mode 100644 index 000000000..6ba353c0a --- /dev/null +++ b/application/WorkorderMphCompany/WorkorderMphCompanyController.php @@ -0,0 +1,256 @@ + 'id', 'text' => 'Auftrags-Nr.', 'table' => ['sortable' => true]], + ['key' => 'hausnummerInfo', 'text' => 'Adresse', 'modal' => false, 'table' => ['sortable' => false]], + ['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 array $additionalJSVariables = ['COMPANY_ID' => '0', 'IS_COMPANY_VIEW' => true]; + + protected function prepareCrudConfig() + { + $hausnummerInfoColIdx = array_search('hausnummerInfo', array_column($this->columns, 'key')); + array_splice($this->columns, $hausnummerInfoColIdx + 1, 0, [$this->statusColumn]); + + $company = WorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); + $this->additionalJSVariables['COMPANY_ID'] = $company ? $company->id : 0; + } + + 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; + } + + $db = FronkDB::singleton(); + $fronkDbName = FRONKDB_DBNAME; + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + + $whereClauses = "WHERE w.companyId = " . intval($company->id); + + 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['deadlineDate'])) $whereClauses .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); + if (!empty($filters['appointmentDate'])) $whereClauses .= Helper::generateFilterCondition($filters['appointmentDate'], 'w.appointmentDate'); + if (!empty($filters['additionalInfo'])) $whereClauses .= Helper::generateFilterCondition($filters['additionalInfo'], 'w.additionalInfo'); + + $sql = " + SELECT + w.id, w.status, w.deadlineDate, w.appointmentDate, w.additionalInfo, + CONCAT_WS(' ', str.name, hn.hausnummer, hn.stiege) as hausnummerInfo, + str.name as street, hn.hausnummer, hn.stiege, plz.plz, ort.name as city, + (SELECT COUNT(*) FROM `$addressDbName`.`Wohneinheit` we WHERE we.hausnummer_id = hn.id) as wohneinheitCount + FROM `$fronkDbName`.`WorkorderMph` w + 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 + $whereClauses + "; + + $orderBy = ""; + if (!empty($order['key'])) { + $sortableColumns = ['id', 'status', 'deadlineDate', 'additionalInfo', 'appointmentDate']; + 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 `$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 + $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 + ] + ]); + } + + public function getWorkorderByIdAction() + { + if (empty($this->request->id)) self::sendError("ID fehlt"); + $workorder = WorkorderMphModel::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 = WorkorderMphModel::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!"); + + $oldStatus = $workorder->status; + $workorder->appointmentDate = $this->postData['appointmentDate']; + $workorder->status = 'scheduled'; + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorder->id, + 'text' => 'Termin festgelegt auf: ' . date('d.m.Y H:i', $this->postData['appointmentDate']), + 'statusChange' => $oldStatus !== 'scheduled' ? $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('scheduled') : null, + '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 = WorkorderMphModel::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']; + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $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 startWorkAction() + { + 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 = 'in_progress'; + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorder->id, + 'text' => 'Arbeit begonnen.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('in_progress'), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Arbeit wurde gestartet.']); + } + + protected function completeWorkorderAction() + { + if (empty($this->postData['workorderId'])) self::sendError("Arbeitsauftrags-ID fehlt."); + $workorder = WorkorderMphModel::get($this->postData['workorderId']); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + // Validate that all required Wohneinheiten have notes + $wohneinheiten = WorkorderMphWohneinheitModel::getAll(['workorderMphId' => $workorder->id]); + foreach ($wohneinheiten as $we) { + if (empty($we->note)) { + self::sendError("Bitte fügen Sie für jede Wohneinheit eine Notiz hinzu, bevor Sie den Auftrag abschließen."); + } + } + + $oldStatus = $workorder->status; + $workorder->status = 'documented'; + WorkorderMphModel::update((array)$workorder); + + WorkorderMphJournalModel::create([ + 'workorderMphId' => $workorder->id, + 'text' => 'Arbeitsauftrag abgeschlossen und dokumentiert.', + 'statusChange' => $this->getStatusText($oldStatus) . " -> " . $this->getStatusText('documented'), + 'create' => time(), + 'createBy' => $this->user->id, + ]); + self::returnJson(['success' => true, 'message' => 'Arbeitsauftrag erfolgreich abgeschlossen.']); + } + + protected function uploadDocumentationAction() + { + if (empty($_FILES['file']) || empty($_POST['workorderMphId'])) self::sendError("Datei und Arbeitsauftrags-ID sind erforderlich."); + + $workorderMphId = intval($_POST['workorderMphId']); + $workorder = WorkorderMphModel::get($workorderMphId); + if (!$workorder) self::sendError("Arbeitsauftrag nicht gefunden."); + + $documentType = $_POST['documentType'] ?? 'photo'; + $description = $_POST['description'] ?? null; + + // Upload file using mfUpload + $upload = new mfUpload($_FILES['file']); + if (!$upload->upload()) { + self::sendError("Datei-Upload fehlgeschlagen."); + } + + $file = $upload->getFile(); + + WorkorderMphDocumentationModel::create([ + 'workorderMphId' => $workorderMphId, + 'fileId' => $file->id, + 'description' => $description, + 'documentType' => $documentType, + 'create' => time(), + 'createBy' => $this->user->id + ]); + + self::returnJson(['success' => true, 'message' => 'Dokument erfolgreich hochgeladen.', 'fileId' => $file->id]); + } + + protected function deleteDocumentationAction() + { + if (empty($this->postData['documentationId'])) self::sendError("Dokumentations-ID fehlt."); + + $doc = WorkorderMphDocumentationModel::get($this->postData['documentationId']); + if (!$doc) self::sendError("Dokumentation nicht gefunden."); + + WorkorderMphDocumentationModel::delete($doc->id); + self::returnJson(['success' => true, 'message' => 'Dokumentation gelöscht.']); + } +} diff --git a/application/WorkorderMphDocumentation/WorkorderMphDocumentationModel.php b/application/WorkorderMphDocumentation/WorkorderMphDocumentationModel.php new file mode 100644 index 000000000..2e0c9460b --- /dev/null +++ b/application/WorkorderMphDocumentation/WorkorderMphDocumentationModel.php @@ -0,0 +1,12 @@ +getEnvironment() == "thetool") { + $table = $this->table("WorkerPermission"); + $table->addColumn("canWorkorderMphAdmin", "enum", [ + "null" => false, + "values" => ['false', 'true'], + "default" => "false", + "after" => "canWorkorderAdmin" + ]); + $table->update(); + + $workorderMph = $this->table('WorkorderMph', ['id' => 'id', 'primary_key' => 'id']); + $workorderMph + ->addColumn('hausnummerId', 'integer', ['null' => false]) + ->addColumn('companyId', 'integer', ['null' => true]) + ->addColumn('status', 'string', ['limit' => 50, 'null' => false, 'default' => 'new']) + ->addColumn('assignmentDate', 'integer', ['null' => true]) + ->addColumn('deadlineDate', 'integer', ['null' => true]) + ->addColumn('appointmentDate', 'integer', ['null' => true]) + ->addColumn('additionalInfo', 'text', ['null' => true]) + ->addColumn('easement', 'boolean', ['null' => true, 'default' => null, 'comment' => 'Leitungsrecht']) + ->addColumn('btb', 'boolean', ['null' => true, 'default' => null]) + ->addColumn('fttxLocationSupplied', 'boolean', ['null' => true, 'default' => null, 'comment' => 'FTTx Location mit Leerrohr versorgt']) + ->addColumn('conduitToHuepLaid', 'boolean', ['null' => true, 'default' => null, 'comment' => 'Leerrohr bis HÜP/HAK verlegt']) + ->addColumn('huepMounted', 'boolean', ['null' => true, 'default' => null, 'comment' => 'HÜP/HAK montiert']) + ->addColumn('dropCableAvailable', 'boolean', ['null' => true, 'default' => null, 'comment' => 'Dropkabel vorhanden']) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['hausnummerId'], ['name' => 'hausnummerId_idx']) + ->addIndex(['companyId'], ['name' => 'companyId_mph_idx']) + ->addIndex(['status'], ['name' => 'status_mph_idx']) + ->create(); + + $workorderMphWohneinheit = $this->table('WorkorderMphWohneinheit', ['id' => 'id', 'primary_key' => 'id']); + $workorderMphWohneinheit + ->addColumn('workorderMphId', 'integer', ['null' => false]) + ->addColumn('wohneinheitId', 'integer', ['null' => false]) + ->addColumn('status', 'integer', ['null' => false, 'default' => 1, 'comment' => '1=new, 12=241 BEP MD, 13=242 Inhouse, 18=243 Stairwell, 14=244 BEP SD, 15=245 Approved, 16=300 ONT']) + ->addColumn('note', 'text', ['null' => true]) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addColumn('edit', 'integer', ['null' => true]) + ->addColumn('editBy', 'integer', ['null' => true]) + ->addIndex(['workorderMphId'], ['name' => 'workorderMphId_idx']) + ->addIndex(['wohneinheitId'], ['name' => 'wohneinheitId_idx']) + ->addIndex(['workorderMphId', 'wohneinheitId'], ['unique' => true, 'name' => 'workorder_wohneinheit_unique']) + ->create(); + + $workorderMphJournal = $this->table('WorkorderMphJournal', ['id' => false, 'primary_key' => ['id']]); + $workorderMphJournal + ->addColumn('id', 'integer', ['identity' => true, 'signed' => true]) + ->addColumn('workorderMphId', 'integer', ['null' => false]) + ->addColumn('text', 'text', ['null' => true]) + ->addColumn('fileIds', 'json', ['null' => true]) + ->addColumn('statusChange', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['workorderMphId'], ['name' => 'workorderMphId_idx']) + ->create(); + + $workorderMphDocumentation = $this->table('WorkorderMphDocumentation', ['id' => 'id', 'primary_key' => 'id']); + $workorderMphDocumentation + ->addColumn('workorderMphId', 'integer', ['null' => false]) + ->addColumn('fileId', 'integer', ['null' => false]) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('documentType', 'string', ['limit' => 100, 'null' => false]) + ->addColumn('create', 'integer', ['null' => false]) + ->addColumn('createBy', 'integer', ['null' => false]) + ->addIndex(['workorderMphId'], ['name' => 'workorderMphId_idx']) + ->create(); + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $this->table('WorkorderMphDocumentation')->drop()->save(); + $this->table('WorkorderMphJournal')->drop()->save(); + $this->table('WorkorderMphWohneinheit')->drop()->save(); + $this->table('WorkorderMph')->drop()->save(); + + $table = $this->table("WorkerPermission"); + $table->removeColumn("canWorkorderMphAdmin"); + $table->save(); + } + } +} diff --git a/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js b/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js new file mode 100644 index 000000000..3d67c69d1 --- /dev/null +++ b/public/js/pages/WorkorderMphAdmin/WorkorderMphAdmin.js @@ -0,0 +1,237 @@ +// WorkorderMphAdmin.js +Vue.component('workorder-mph-admin', { + template: ` + + + + + + + + + + + + + + + + + + +

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

+ +
+
+ `, + data() { + return { + window, + editingWorkorderId: null, + editingDeadlineId: null, + editingAdditionalInfoId: null, + tempAdditionalInfo: '', + companies: [], + companiesLoading: false, + cancelWorkorderModalData: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + selectable: false, + expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled', 'archived'].includes(row.status)) return 'tt-mph-workorder-irrelevant'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-mph-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-mph-workorder-urgent'; + if (daysLeft <= 21) return 'tt-mph-workorder-medium'; + return 'tt-mph-workorder-ontrack'; + } + } + } + }, + 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 loadCompanies() { + if (this.companies.length > 0) return; + this.companiesLoading = true; + try { + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/getCompanies`); + this.companies = data; + } catch (e) { + window.notify('error', 'Firmenliste konnte nicht geladen werden.'); + } finally { + this.companiesLoading = false; + } + }, + async startCompanyEdit(row) { + await this.loadCompanies(); + this.editingWorkorderId = row.id; + }, + async assignCompany(workorder, companyId) { + if (!companyId) { + this.editingWorkorderId = null; + return; + } + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/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; + }, + async updateDeadline(workorder, newDate) { + if (!newDate) { + this.editingDeadlineId = null; + return; + } + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/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', 'Netzwerkfehler.'); + } finally { + this.editingDeadlineId = null; + } + }, + 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}/WorkorderMphAdmin/updateAdditionalInfo`, { + workorderMphId: 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.'); + } + } catch (e) { + window.notify('error', 'Netzwerkfehler.'); + } finally { + this.cancelEdit(); + } + }, + async cancelWorkorder() { + const { id, reason } = this.cancelWorkorderModalData; + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/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.'); + } + } + } +}); diff --git a/public/js/pages/WorkorderMphBase/WorkorderMphBase.css b/public/js/pages/WorkorderMphBase/WorkorderMphBase.css new file mode 100644 index 000000000..ef63a4ac6 --- /dev/null +++ b/public/js/pages/WorkorderMphBase/WorkorderMphBase.css @@ -0,0 +1,208 @@ +/* + * CSS for WorkorderMph Table Row Highlighting + */ + +/* Urgent: Deadline passed or less than 1 week away */ +.table-hover .tt-mph-workorder-urgent:hover, +.tt-mph-workorder-urgent { + background-color: #fbe9e7 !important; /* Soft Red */ +} + +/* Medium: Deadline less than 3 weeks away */ +.table-hover .tt-mph-workorder-medium:hover, +.tt-mph-workorder-medium { + background-color: #fff8e1 !important; /* Soft Yellow */ +} + +/* On Track: Deadline more than 3 weeks away */ +.table-hover .tt-mph-workorder-ontrack:hover, +.tt-mph-workorder-ontrack { + background-color: #e8f5e9 !important; /* Soft Green */ +} + +/* Irrelevant: No deadline or status makes it not applicable */ +.table-hover .tt-mph-workorder-irrelevant:hover, +.tt-mph-workorder-irrelevant { + background-color: #fafafa !important; /* Very light grey */ +} + +.table-hover .tt-mph-workorder-high:hover, +.tt-mph-workorder-high { + background-color: #f8d7da !important; /* A slightly more intense red for high priority issues */ +} + +/* + * Wohneinheit Manager - Dense Table Layout + */ +.wohneinheit-manager .we-table { + display: table; + width: 100%; + border-collapse: collapse; +} + +.wohneinheit-manager .we-row { + display: table-row; + border-bottom: 1px solid #e9ecef; + transition: background-color 0.15s ease; +} + +.wohneinheit-manager .we-row:hover { + background-color: #f8f9fa; +} + +.wohneinheit-manager .we-cell { + display: table-cell; + padding: 8px 12px; + vertical-align: middle; +} + +.wohneinheit-manager .we-bezeichner { + width: 25%; + font-size: 0.9rem; +} + +.wohneinheit-manager .we-status { + width: 20%; +} + +.wohneinheit-manager .we-note { + width: 40%; +} + +.wohneinheit-manager .we-actions { + width: 15%; + text-align: right; +} + +.contact-info { + font-size: 0.85rem; + margin-top: 4px; + padding-left: 4px; + border-left: 2px solid #007bff; +} + +.workorder-mph-button { + padding: 2px !important; +} + +/* + * Custom Checkboxes - Compact & Beautiful + */ +.custom-checkboxes-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 8px 16px; +} + +.custom-checkbox-item { + display: flex; + align-items: center; + padding: 8px 12px; + background: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + margin: 0; + user-select: none; +} + +.custom-checkbox-item:hover:not(.disabled) { + background: #e9ecef; + border-color: #007bff; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.custom-checkbox-item.disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.custom-checkbox-item input[type="checkbox"] { + position: absolute; + opacity: 0; + cursor: pointer; +} + +.custom-checkbox-item .checkmark { + position: relative; + height: 20px; + width: 20px; + background-color: #fff; + border: 2px solid #adb5bd; + border-radius: 4px; + margin-right: 10px; + flex-shrink: 0; + transition: all 0.2s ease; +} + +.custom-checkbox-item input[type="checkbox"]:checked ~ .checkmark { + background-color: #28a745; + border-color: #28a745; +} + +.custom-checkbox-item .checkmark:after { + content: ""; + position: absolute; + display: none; + left: 6px; + top: 2px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +.custom-checkbox-item input[type="checkbox"]:checked ~ .checkmark:after { + display: block; +} + +.custom-checkbox-item .checkbox-label { + font-size: 0.9rem; + font-weight: 500; + color: #495057; +} + +.custom-checkbox-item input[type="checkbox"]:checked ~ .checkbox-label { + color: #28a745; +} + +/* + * Required Documents Checklist + */ +.required-docs-checklist { + background: #f8f9fa; + border-radius: 6px; + padding: 8px; +} + +.doc-check-item { + display: flex; + align-items: center; + padding: 6px 10px; + margin-bottom: 4px; + background: white; + border-radius: 4px; + font-size: 0.9rem; + gap: 10px; +} + +.doc-check-item:last-child { + margin-bottom: 0; +} + +.doc-check-item i:first-child { + width: 20px; + text-align: center; +} + +.doc-check-item span { + flex: 1; + font-weight: 500; +} + +.doc-check-item .ml-auto { + margin-left: auto; +} diff --git a/public/js/pages/WorkorderMphBase/WorkorderMphBase.js b/public/js/pages/WorkorderMphBase/WorkorderMphBase.js new file mode 100644 index 000000000..f6ad3d113 --- /dev/null +++ b/public/js/pages/WorkorderMphBase/WorkorderMphBase.js @@ -0,0 +1,543 @@ +// WorkorderMphBase.js - Shared components for WorkorderMph module + +// Traffic light component (reused from WorkorderBase) +Vue.component('traffic-light-mph', { + 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: `` +}); + +// Wohneinheit Status Manager Component +Vue.component('wohneinheit-status-manager', { + props: { + workorderMphId: { type: Number, required: true }, + isAdmin: { type: Boolean, default: false } + }, + template: ` +
+
+
Wohneinheiten Status
+
+
+
+
+ Keine Wohneinheiten gefunden. +
+
+
+
+ {{ we.bezeichner }} +
+ {{ we.contact }} +
+
+ Keine Kontaktinfo +
+
+
+ +
+
+ +
+
+ + + + +
+
+
+
+
+ `, + data: () => ({ + loading: true, + wohneinheiten: [], + statusOptions: [] + }), + methods: { + async fetchWohneinheiten() { + this.loading = true; + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getWohneinheiten`, { + params: { workorderMphId: this.workorderMphId } + }); + this.wohneinheiten = data.wohneinheiten.map(we => ({ ...we, changed: false, saving: false })); + this.statusOptions = data.statusOptions || []; + } catch (e) { + window.notify('error', 'Wohneinheiten konnten nicht geladen werden.'); + console.error(e); + } finally { + this.loading = false; + } + }, + markAsChanged(we) { + we.changed = true; + }, + async saveWohneinheit(we) { + if (!we.note || !we.note.trim()) { + return window.notify('error', 'Bitte eine Notiz eingeben.'); + } + + we.saving = true; + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/updateWohneinheit`, { + workorderMphId: this.workorderMphId, + wohneinheitId: we.wohneinheitId, + status: we.status, + note: we.note + }); + if (data.success) { + window.notify('success', data.message); + we.changed = false; + this.$emit('wohneinheit-updated'); + } else { + window.notify('error', data.message || 'Speichern fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + we.saving = false; + } + }, + getStatusText(statusValue) { + const option = this.statusOptions.find(opt => opt.value === statusValue); + return option ? option.text : ''; + } + }, + async mounted() { + await this.fetchWohneinheiten(); + } +}); + +// Checkbox Documentation Component +Vue.component('checkbox-documentation', { + props: { + workorderMphId: { type: Number, required: true }, + isAdmin: { type: Boolean, default: false } + }, + template: ` +
+
+
Dokumentation Checkboxen
+
+
+
+ + + + + + + + + + + +
+
+ +
+
+
+
+ `, + data: () => ({ + loading: true, + saving: false, + checkboxes: { + easement: false, + btb: false, + fttxLocationSupplied: false, + conduitToHuepLaid: false, + huepMounted: false, + dropCableAvailable: false + } + }), + methods: { + async fetchCheckboxes() { + this.loading = true; + try { + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/getWorkorderById`, { + params: { id: this.workorderMphId } + }); + this.checkboxes = { + easement: !!data.easement, + btb: !!data.btb, + fttxLocationSupplied: !!data.fttxLocationSupplied, + conduitToHuepLaid: !!data.conduitToHuepLaid, + huepMounted: !!data.huepMounted, + dropCableAvailable: !!data.dropCableAvailable + }; + } catch (e) { + window.notify('error', 'Checkboxen konnten nicht geladen werden.'); + console.error(e); + } finally { + this.loading = false; + } + }, + async saveCheckboxes() { + this.saving = true; + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/updateCheckboxes`, { + workorderMphId: this.workorderMphId, + ...this.checkboxes + }); + if (data.success) { + window.notify('success', data.message); + this.$emit('checkboxes-updated'); + } else { + window.notify('error', data.message || 'Speichern fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.saving = false; + } + } + }, + async mounted() { + await this.fetchCheckboxes(); + } +}); + +// WorkorderMph Details Manager +Vue.component('workorder-mph-details-manager', { + props: { + workorderMphId: { type: String, required: true }, + isAdmin: { type: Boolean, default: false } + }, + data: () => ({ + loading: true, + docs: [], + journals: [], + newJournalMessage: '', + addingJournalEntry: false, + uploading: false, + completing: false, + showCompleteModal: false, + showAcceptModal: false, + uploadData: { files: [], documentType: '', description: '' }, + wohneinheitenWithNotes: true, + requiredDocs: [ + { key: 'huep_photo', label: 'HÜP/HAK Foto', icon: 'fas fa-camera', example: 'Foto der installierten HÜP/HAK' }, + { key: 'bep_md_photo', label: 'BEP MD Foto', icon: 'fas fa-camera', example: 'Foto der BEP (MD) Installation' }, + { key: 'ont_photo', label: 'ONT Foto', icon: 'fas fa-camera', example: 'Foto der ONT Installation' }, + { key: 'cable_routing', label: 'Kabelverlegung', icon: 'fas fa-route', example: 'Fotos der Kabelverlegung im Treppenhaus' }, + { key: 'fttx_location', label: 'FTTx Location', icon: 'fas fa-map-marker-alt', example: 'Foto/Dokument der FTTx Location' }, + { key: 'signature', label: 'Unterschrift', icon: 'fas fa-signature', example: 'Unterschriebenes Übergabeprotokoll' }, + { key: 'other', label: 'Sonstige Dokumentation', icon: 'fas fa-file', example: 'Weitere relevante Dokumente' } + ] + }), + template: ` +
+
+
+
+
+
+
Auftrag abschließen
+

Dokumentieren Sie alle Wohneinheiten und laden Sie die erforderlichen Dokumente hoch.

+
+ + + Bitte fügen Sie für jede Wohneinheit eine Notiz hinzu und laden Sie Dokumente hoch. + +
+ Auftrag bereits abgeschlossen oder storniert. +
+
+
+ +
+
+
Prüfung & Freigabe
+

Prüfen Sie die hochgeladenen Dokumente:

+ +
+
+ + {{ doc.label }} + + +
+
+ + + Stellen Sie sicher, dass alle relevanten Dokumente vorhanden sind. + + + +
+
+ +
+
Journal
+
+
    +
  • + {{ formatDate(log.create) }} ({{ log.createByName }}): +
    {{ log.text }}
    +
  • +
+
Keine Journaleinträge.
+
+ +
+
+ +
+
+
+
Neues Dokument hochladen
+ +
+ +
+ + + {{ getDocExample(uploadData.documentType) }} + +
+
+ +
+ +
+ +
+
+ +
+ +
+ + Erlaubt: Bilder (JPG, PNG) und PDF +
+
+ +
+ +
+
+
+ + + +
+
+ + + 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? + +
+ `, + computed: { + isReadOnly() { + return ['completed', 'cancelled'].includes(this.workorder?.status); + }, + canComplete() { + return this.wohneinheitenWithNotes && this.docs.length > 0; + } + }, + methods: { + formatDate(timestamp) { + return timestamp ? window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm') : '–'; + }, + hasDocType(docType) { + return this.docs.some(doc => doc.documentType === docType); + }, + getDocExample(docType) { + const doc = this.requiredDocs.find(d => d.key === docType); + return doc ? doc.example : ''; + }, + async fetchData() { + this.loading = true; + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}${basePath}/getDocumentation`, { + params: { workorderMphId: this.workorderMphId } + }); + this.docs = data.docs || []; + this.journals = data.journals || []; + } catch (e) { + window.notify('error', 'Details konnten nicht geladen werden.'); + this.docs = []; + this.journals = []; + } finally { + this.loading = false; + } + }, + async addJournalEntry() { + if (!this.newJournalMessage.trim()) return window.notify('error', 'Bitte eine Nachricht eingeben.'); + + this.addingJournalEntry = true; + try { + const basePath = this.isAdmin ? '/WorkorderMphAdmin' : '/WorkorderMphCompany'; + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}${basePath}/addJournal`, { + workorderMphId: this.workorderMphId, + text: this.newJournalMessage + }); + if (data.success) { + window.notify('success', data.message); + this.journals = data.journals || []; + this.newJournalMessage = ''; + } else { + window.notify('error', data.message || 'Eintrag fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.addingJournalEntry = 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('workorderMphId', this.workorderMphId); + formData.append('documentType', this.uploadData.documentType); + formData.append('description', this.uploadData.description); + for (const file of this.uploadData.files) { + formData.append('file', file); + } + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/uploadDocumentation`, formData); + if (data.success) { + window.notify('success', data.message); + this.$refs.fileInput.value = ''; + this.uploadData = { files: [], documentType: 'photo', description: '' }; + await this.fetchData(); + } else { + window.notify('error', data.error || 'Upload fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist beim Upload aufgetreten.'); + } finally { + this.uploading = false; + } + }, + async deleteDocumentation(file) { + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/deleteDocumentation`, { + documentationId: file.id + }); + if (data.success) { + window.notify('success', data.message); + await this.fetchData(); + } else { + window.notify('error', data.message || 'Löschen fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Netzwerkfehler beim Löschen.'); + } + }, + async completeWorkorder() { + this.completing = true; + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, { + workorderId: this.workorderMphId + }); + 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 acceptDocumentation() { + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphAdmin/acceptDocumentation`, { + workorderId: this.workorderMphId + }); + if (data.success) { + window.notify('success', data.message); + this.$emit('documentation-accepted'); + this.showAcceptModal = false; + } else { + window.notify('error', data.message || 'Akzeptieren fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } + } + }, + async mounted() { + await this.fetchData(); + } +}); diff --git a/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js b/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js new file mode 100644 index 000000000..039d9a8e6 --- /dev/null +++ b/public/js/pages/WorkorderMphCompany/WorkorderMphCompany.js @@ -0,0 +1,237 @@ +// WorkorderMphCompany.js +Vue.component('workorder-mph-company', { + template: ` + + + + + + + + + + + + + + + + +

Aktueller Termin: {{ formatDate(rescheduleModalData.currentDate, true) }}

+ + +
+ + +

Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?

+
+ + Bitte stellen Sie sicher, dass alle Wohneinheiten dokumentiert sind und alle erforderlichen Dokumente hochgeladen wurden. +
+
+
+ `, + data() { + return { + window, + editingAppointmentId: null, + rescheduleModalData: null, + completeModalData: null, + crudConfig: { + ...window.TT_CONFIG.CRUD_CONFIG, + selectable: false, + expandable: true, + customRowClass: (row) => { + if (['completed', 'new', 'cancelled'].includes(row.status)) return 'tt-mph-workorder-irrelevant'; + const deadlineDate = moment.unix(row.deadlineDate); + if (!deadlineDate.isValid()) return 'tt-mph-workorder-irrelevant'; + const daysLeft = deadlineDate.diff(moment(), 'days'); + if (daysLeft <= 7) return 'tt-mph-workorder-urgent'; + if (daysLeft <= 21) return 'tt-mph-workorder-medium'; + return 'tt-mph-workorder-ontrack'; + } + } + } + }, + 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'); + }, + canSchedule(row) { + return ['assigned', 'scheduled'].includes(row.status); + }, + async scheduleAppointment(row, newDate) { + if (!newDate) { + this.editingAppointmentId = null; + return; + } + + const hour = parseInt(moment.unix(newDate).format('H')); + if (hour >= 23 || hour < 1) { + window.notify('error', 'Bitte geben Sie eine Uhrzeit zwischen 01:00 und 22:59 an!'); + return; + } + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/scheduleAppointment`, { + workorderId: row.id, + appointmentDate: 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', 'Netzwerkfehler.'); + } finally { + this.editingAppointmentId = null; + } + }, + openRescheduleModal(row) { + this.rescheduleModalData = { + workorderId: row.id, + currentDate: row.appointmentDate, + newDate: null, + reason: '' + }; + }, + async rescheduleAppointment() { + if (!this.rescheduleModalData.newDate || !this.rescheduleModalData.reason) { + return window.notify('error', 'Bitte füllen Sie alle Felder aus.'); + } + + const hour = parseInt(moment.unix(this.rescheduleModalData.newDate).format('H')); + if (hour >= 23 || hour < 1) { + window.notify('error', 'Bitte geben Sie eine Uhrzeit zwischen 01:00 und 22:59 an!'); + return; + } + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/rescheduleAppointment`, { + workorderId: this.rescheduleModalData.workorderId, + appointmentDate: this.rescheduleModalData.newDate, + reason: this.rescheduleModalData.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.'); + } + } catch (e) { + window.notify('error', 'Netzwerkfehler.'); + } + }, + async startWork(row) { + if (!confirm(`Möchten Sie mit der Arbeit an Auftrag #${row.id} beginnen?`)) return; + + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/startWork`, { + workorderId: row.id + }); + 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', 'Netzwerkfehler.'); + } + }, + openCompleteModal(row) { + this.completeModalData = { workorderId: row.id }; + }, + async completeWorkorder() { + try { + const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/WorkorderMphCompany/completeWorkorder`, { + workorderId: this.completeModalData.workorderId + }); + if (data.success) { + window.notify('success', data.message); + this.$refs.table.$refs.table.refreshTable(); + this.completeModalData = null; + } else { + window.notify('error', data.message || 'Abschluss fehlgeschlagen.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } + }, + async checkAllWohneinheitenHaveNotes(workorderId) { + // This is called when a wohneinheit is updated + // Could be used to enable/disable the complete button + } + } +});