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?
Dokumentieren Sie alle Wohneinheiten und laden Sie die erforderlichen Dokumente hoch.
+Prüfen Sie die hochgeladenen Dokumente:
+ +Aktueller Termin: {{ formatDate(rescheduleModalData.currentDate, true) }}
+Möchten Sie diesen Auftrag wirklich abschließen und zur Prüfung einreichen?
+