From 918ca3aafa0c406a20db61af5b6a88e369552e2b Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 5 Aug 2025 07:05:33 +0000 Subject: [PATCH] Rml workorder/v2 need improvements --- .../RMLWorkorder/RMLWorkorderModel.php | 208 ++++++++- .../RMLWorkorderAdminController.php | 186 +++++--- .../RMLWorkorderCompanyController.php | 183 ++++++-- .../RMLWorkorderJournalModel.php | 11 + ...0250723204001_CreateRmlWorkorderTables.php | 14 + lib/Helper/Helper.php | 72 ++- .../RMLWorkorderAdmin/RMLWorkorderAdmin.css | 27 ++ .../RMLWorkorderAdmin/RMLWorkorderAdmin.js | 435 +++++++++++++----- .../RMLWorkorderCompany/RMLWorkorderAdmin.css | 30 ++ .../RMLWorkorderCompany.js | 272 +++++++---- .../vue/tt-components/css/tt-file-gallery.css | 44 +- .../vue/tt-components/css/tt-select.css | 198 ++++++++ .../vue/tt-components/tt-file-gallery.js | 203 ++++---- public/plugins/vue/tt-components/tt-modal.js | 1 - public/plugins/vue/tt-components/tt-select.js | 262 +++++------ .../vue/tt-components/tt-table-crud.js | 1 + 16 files changed, 1534 insertions(+), 613 deletions(-) create mode 100644 application/RMLWorkorderJournal/RMLWorkorderJournalModel.php create mode 100644 db/migrations/20250723204001_CreateRmlWorkorderTables.php create mode 100644 public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css create mode 100644 public/js/pages/RMLWorkorderCompany/RMLWorkorderAdmin.css create mode 100644 public/plugins/vue/tt-components/css/tt-select.css diff --git a/application/RMLWorkorder/RMLWorkorderModel.php b/application/RMLWorkorder/RMLWorkorderModel.php index e9924eeed..53e934eda 100644 --- a/application/RMLWorkorder/RMLWorkorderModel.php +++ b/application/RMLWorkorder/RMLWorkorderModel.php @@ -5,26 +5,20 @@ class RMLWorkorderModel extends TTCrudBaseModel { public int $id; public int $preorderId; public ?int $companyId; + public ?int $clusterId; public string $status; public ?int $assignmentDate; public ?int $deadlineDate; public ?int $appointmentDate; public int $create; public int $createBy; - - /** - * Finds work orders that are nearing their deadline or are overdue. - * This can be used for the traffic light system. - * - * @param string $urgency 'red', 'yellow', or 'green' - * @return array - */ + + // This method remains unchanged as requested. public static function getWorkordersByUrgency(string $urgency, ?int $companyId = null): array { $db = self::getDB(); $table = self::getFullyQualifiedTable(); - $now = time(); - $whereClause = "WHERE status IN ('assigned', 'scheduled')"; - + $whereClause = "WHERE status IN ('assigned', 'scheduled', 'correction_requested')"; + if ($companyId) { $whereClause .= " AND companyId = " . intval($companyId); } @@ -32,7 +26,7 @@ class RMLWorkorderModel extends TTCrudBaseModel { switch ($urgency) { case 'red': // Less than 1 week left or overdue $redDate = strtotime('+1 week'); - $whereClause .= " AND deadlineDate < $redDate"; + $whereClause .= " AND deadlineDate IS NOT NULL AND deadlineDate < $redDate"; break; case 'yellow': // Between 1 and 3 weeks left $yellowDateStart = strtotime('+1 week'); @@ -41,7 +35,7 @@ class RMLWorkorderModel extends TTCrudBaseModel { break; case 'green': // More than 3 weeks left $greenDate = strtotime('+3 weeks'); - $whereClause .= " AND deadlineDate > $greenDate"; + $whereClause .= " AND deadlineDate > $greenDate"; break; default: return []; @@ -56,4 +50,192 @@ class RMLWorkorderModel extends TTCrudBaseModel { return $orders; } + // --- REFACTORED METHODS --- + + private static function buildWhereClause(array $filters, array $allowedCampaignIds): string { + if (empty($allowedCampaignIds)) { + return " WHERE 1=0"; + } + + $sql = Helper::generateFilterCondition(array_map('intval', $allowedCampaignIds), 'p.preordercampaign_id'); + + if (!empty($filters['id'])) { + $sql .= Helper::generateFilterCondition($filters['id'], 'w.id', true); + } + if (!empty($filters['status'])) { + $sql .= Helper::generateFilterCondition($filters['status'], 'w.status', true); + } + if (!empty($filters['preordercampaign_id'])) { + $sql .= Helper::generateFilterCondition($filters['preordercampaign_id'], 'p.preordercampaign_id'); + } + if (!empty($filters['companyName'])) { + $sql .= Helper::generateFilterCondition($filters['companyName'], 'c.name'); + } + if (!empty($filters['deadlineDate'])) { + $sql .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); + } + if (!empty($filters['preorderInfo'])) { + $searchColumns = "p.firstname|p.lastname|p.company|p.oaid|p.street|p.housenumber|p.zip|p.city|str.name|ort.name"; + $sql .= Helper::generateFilterCondition($filters['preorderInfo'], $searchColumns); + } + + return "WHERE " . ltrim(trim($sql), 'AND'); + } + + public static function getAdminWorkorders(array $filters, ?int $limit, int $offset, array $order, array $allowedCampaignIds): array { + $db = self::getDB(); + $fronkDbName = FRONKDB_DBNAME; + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + + $sql = " + SELECT + w.id, w.status, w.deadlineDate, w.companyId, p.preordercampaign_id, + CONCAT_WS(' ', p.firstname, p.lastname) as customerName, + p.company as customerCompany, p.oaid, c.name as companyName, + str.name as street, hn.hausnummer, hn.stiege, we.bezeichner as apartment, plz.plz, ort.name as city + FROM `$fronkDbName`.`RMLWorkorder` w + JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id + LEFT JOIN `$fronkDbName`.`Preordercampaign` pc ON p.preordercampaign_id = pc.id + LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id + LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = 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`.`Wohneinheit` we ON p.adb_wohneinheit_id = we.id + "; + + $sql .= self::buildWhereClause($filters, $allowedCampaignIds); + + $sql .= " ORDER BY CASE WHEN w.deadlineDate IS NULL THEN 1 ELSE 0 END, w.deadlineDate ASC"; + + if (!empty($order['key'])) { + $sortableColumns = ['id', 'status', 'deadlineDate', 'companyName', 'clusterName']; + if (in_array($order['key'], $sortableColumns)) { + $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; + $sql .= ", " . $db->real_escape_string($order['key']) . " " . $sortOrder; + } + } + + if ($limit !== null) { + $sql .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset); + } + $result = $db->query($sql); + return $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + } + + public static function countAdminWorkorders(array $filters, array $allowedCampaignIds): int { + $db = self::getDB(); + $fronkDbName = FRONKDB_DBNAME; + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + + $sql = " + SELECT COUNT(w.id) as count + FROM `$fronkDbName`.`RMLWorkorder` w + JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id + LEFT JOIN `$fronkDbName`.`RMLWorkorderCompany` c ON w.companyId = c.id + LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = 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 + "; + + $sql .= self::buildWhereClause($filters, $allowedCampaignIds); + + $result = $db->query($sql); + if ($result === false) return 0; + + $row = $result->fetch_assoc(); + return $row['count'] ?? 0; + } + + private static function buildCompanyWhereClause(array $filters, int $companyId): string + { + $sql = "w.companyId = " . $companyId; + + if (!empty($filters['id'])) { + $sql .= Helper::generateFilterCondition($filters['id'], 'w.id', true); + } + if (!empty($filters['status'])) { + $sql .= Helper::generateFilterCondition($filters['status'], 'w.status'); + } + if (!empty($filters['deadlineDate'])) { + $sql .= Helper::generateFilterCondition($filters['deadlineDate'], 'w.deadlineDate'); + } + if (!empty($filters['appointmentDate'])) { + $sql .= Helper::generateFilterCondition($filters['appointmentDate'], 'w.appointmentDate'); + } + if (!empty($filters['preorderInfo'])) { + $searchColumns = "p.firstname|p.lastname|p.company|p.oaid|p.street|p.housenumber|p.zip|p.city|str.name|ort.name|p.phone|p.email"; + $sql .= Helper::generateFilterCondition($filters['preorderInfo'], $searchColumns); + } + + return "WHERE " . $sql; + } + + public static function getCompanyWorkorders(array $filters, ?int $limit, int $offset, array $order, int $companyId): array + { + $db = self::getDB(); + $fronkDbName = FRONKDB_DBNAME; + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + + $sql = " + SELECT + w.id, w.status, w.deadlineDate, w.appointmentDate, + CONCAT_WS(' ', p.firstname, p.lastname) as customerName, + p.company as customerCompany, p.oaid, p.phone, p.email, + str.name as street, hn.hausnummer, hn.stiege, we.bezeichner as apartment, plz.plz, ort.name as city + FROM `$fronkDbName`.`RMLWorkorder` w + JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id + LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = 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`.`Wohneinheit` we ON p.adb_wohneinheit_id = we.id + "; + + $sql .= self::buildCompanyWhereClause($filters, $companyId); + + $orderBy = " ORDER BY CASE WHEN w.deadlineDate IS NULL THEN 1 ELSE 0 END, w.deadlineDate ASC"; + if (!empty($order['key'])) { + $sortableColumns = ['id', 'status', 'deadlineDate', 'appointmentDate']; + if (in_array($order['key'], $sortableColumns)) { + $sortOrder = (strtoupper($order['order']) === 'DESC') ? 'DESC' : 'ASC'; + $orderBy = " ORDER BY " . $db->real_escape_string($order['key']) . " " . $sortOrder; + } + } + $sql .= $orderBy; + + if ($limit !== null) { + $sql .= " LIMIT " . intval($limit) . " OFFSET " . intval($offset); + } + + $result = $db->query($sql); + return $result ? $result->fetch_all(MYSQLI_ASSOC) : []; + } + + public static function countCompanyWorkorders(array $filters, int $companyId): int + { + $db = self::getDB(); + $fronkDbName = FRONKDB_DBNAME; + $addressDbName = defined('ADDRESSDB_DBNAME') ? ADDRESSDB_DBNAME : 'addressdb'; + + $sql = " + SELECT COUNT(w.id) as count + FROM `$fronkDbName`.`RMLWorkorder` w + JOIN `$fronkDbName`.`Preorder` p ON w.preorderId = p.id + LEFT JOIN `$addressDbName`.`Hausnummer` hn ON p.adb_hausnummer_id = 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 + "; + + $sql .= self::buildCompanyWhereClause($filters, $companyId); + + $result = $db->query($sql); + if ($result === false) { + return 0; + } + $row = $result->fetch_assoc(); + return $row['count'] ?? 0; + } } \ No newline at end of file diff --git a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php index e4a87014e..d60efe631 100644 --- a/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php +++ b/application/RMLWorkorderAdmin/RMLWorkorderAdminController.php @@ -10,21 +10,27 @@ class RMLWorkorderAdminController extends TTCrud protected array $columns = [ ['key' => 'id', 'text' => 'Auftrag-Nr.', 'table' => ['sortable' => true]], ['key' => 'preorderInfo', 'text' => 'Kunde / Projekt', 'modal' => false, 'table' => ['sortable' => false, 'filter' => 'search']], + ['key' => 'preordercampaign_id', 'text' => 'Cluster', 'modal' => false, 'table' => ['filter' => 'select']], ['key' => 'companyName', 'text' => 'Zuständige Firma', 'modal' => false, 'table' => ['filter' => 'search']], ['key' => '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' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'], + ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], ]]], ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']], - ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date']], - ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], ]; protected function indexAction() { + $campaigns = Helper::getPreorderCampaignFromUser($this->user, true); + $this->columns[array_search('preordercampaign_id', array_column($this->columns, 'key'))]['table']['filterOptions'] = array_map( + fn($c) => ['value' => $c->id, 'text' => $c->name], + $campaigns + ); + $this->createWorkordersFromPreorders(); Helper::renderVue($this, 'RMLWorkorderAdmin', $this->headerTitle, [ "CRUD_CONFIG" => $this->getCrudConfig(), @@ -32,62 +38,50 @@ class RMLWorkorderAdminController extends TTCrud ]); } - protected function getAction() - { + protected function getAction() { $json = json_decode(file_get_contents('php://input'), true); $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; $filters = $json['filters'] ?? []; - $order = $json['order'] ?? ['key' => 'id', 'order' => 'DESC']; + $order = $json['order'] ?? []; - // Custom filter logic for preorderInfo - if (!empty($filters['preorderInfo'])) { - $searchTerm = $filters['preorderInfo']; - unset($filters['preorderInfo']); - - // This is a simplified search. A more robust implementation might involve a full-text search or a more complex query. - $preorders = PreorderModel::getAll(['firstname|lastname|company|oaid' => $searchTerm]); - $preorderIds = array_map(fn($p) => $p->id, $preorders); + $allowedCampaignIds = Helper::getPreorderCampaignFromUser($this->user); - if (!empty($preorderIds)) { - $filters['preorderId'] = $preorderIds; - } else { - // No preorders found, so no workorders will be found - $filters['id'] = -1; - } + if (empty($allowedCampaignIds)) { + self::returnJson([ + 'rows' => [], + 'pagination' => array_merge($pagination, ['total_rows' => 0, 'total_pages' => 0, 'filtered_available' => 0]) + ]); + return; } - $workorders = RMLWorkorderModel::getAll($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order); - $totalCount = RMLWorkorderModel::count($filters); - - $rows = []; - foreach($workorders as $workorder) { + $limit = $pagination['per_page']; + $offset = ($pagination['page'] - 1) * $limit; + + $workorders = RMLWorkorderModel::getAdminWorkorders($filters, $limit, $offset, $order, $allowedCampaignIds); + $totalCount = RMLWorkorderModel::countAdminWorkorders($filters, $allowedCampaignIds); + + $rows = array_map(function($workorder) { $row = (array)$workorder; - - $preorder = new Preorder($workorder->preorderId); - $anschlussadresse = 'N/A'; - if ($preorder->adb_hausnummer_id) { - $hn = $preorder->adb_hausnummer; - $anschlussadresse = "{$hn->strasse->name} {$hn->hausnummer}"; - if ($hn->stiege) $anschlussadresse .= "/{$hn->stiege}"; - if ($preorder->adb_wohneinheit_id) $anschlussadresse .= " / WE: {$preorder->adb_wohneinheit->bezeichner}"; - $anschlussadresse .= ", {$hn->plz->plz} {$hn->ortschaft->name}"; - } - $kunde = ($preorder->company) ?: "{$preorder->firstname} {$preorder->lastname}"; - + $anschlussadresse = "{$row['street']} {$row['hausnummer']}"; + if ($row['stiege']) $anschlussadresse .= "/{$row['stiege']}"; + if ($row['apartment']) $anschlussadresse .= " / WE: {$row['apartment']}"; + $anschlussadresse .= ", {$row['plz']} {$row['city']}"; + + $kunde = $row['customerCompany'] ?: $row['customerName']; + $row['preorderInfo'] = "Kunde: {$kunde}
" . - "Anschluss: {$anschlussadresse}
" . - "OAID: {$preorder->oaid}"; + "Anschluss: {$anschlussadresse}
" . + "OAID: {$row['oaid']}"; - if($workorder->companyId) { - $company = RMLWorkorderCompanyModel::get($workorder->companyId); - $row['companyName'] = $company->name ?? 'N/A'; - } else { - $row['companyName'] = 'Nicht zugewiesen'; - } - - $rows[] = $row; - } + $row['companyName'] ??= 'Nicht zugewiesen'; + $row['deadlineDateFormatted'] = $row['deadlineDate'] ? date('d.m.Y', $row['deadlineDate']) : 'Keine Deadline'; + $row['daysUntilDeadline'] = $row['deadlineDate'] ? ceil(($row['deadlineDate'] - time()) / (60 * 60 * 24)) : null; + + unset($row['customerName'], $row['customerCompany'], $row['street'], $row['hausnummer'], $row['stiege'], $row['oaid'], $row['apartment'], $row['plz'], $row['city']); + + return $row; + }, $workorders); self::returnJson([ 'rows' => $rows, @@ -95,7 +89,7 @@ class RMLWorkorderAdminController extends TTCrud 'page' => $pagination['page'], 'per_page' => $pagination['per_page'], 'total_rows' => $totalCount, - 'total_pages' => ceil($totalCount / $pagination['per_page']), + 'total_pages' => ceil($totalCount / $limit), 'filtered_available' => $totalCount ] ]); @@ -109,6 +103,7 @@ class RMLWorkorderAdminController extends TTCrud if (!RMLWorkorderModel::getFirst(['preorderId' => $preorder->id])) { RMLWorkorderModel::create([ 'preorderId' => $preorder->id, + 'clusterId' => $preorder->preordercampaign_id, 'status' => 'new', 'create' => time(), 'createBy' => $this->user->id @@ -123,35 +118,112 @@ class RMLWorkorderAdminController extends TTCrud $workorder = RMLWorkorderModel::get($post['workorderId']); if (!$workorder) self::sendError("Workorder not found."); - + $workorder->companyId = $post['companyId']; $workorder->status = 'assigned'; $workorder->assignmentDate = time(); - $workorder->deadlineDate = strtotime('+6 weeks'); + $workorder->deadlineDate = $post['deadlineDate'] ?? strtotime('+6 weeks'); RMLWorkorderModel::update((array)$workorder); - + self::returnJson(['success' => true, 'message' => 'Auftrag erfolgreich zugewiesen.']); } protected function getDocumentationAction() { if(empty($this->request->workorderId)) self::sendError("Workorder ID missing."); - + $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); - $users = UserModel::search(['employee' => true]); - $userMap = array_reduce($users, fn($carry, $user) => $carry + [$user->id => $user->name], []); + $journals = RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']); + $users = UserModel::search(); foreach($docs as $doc) { $file = new File($doc->fileId); $doc->fileName = $file->orig_filename ?? $file->filename; - $doc->userName = $userMap[$doc->createBy] ?? 'Unbekannt'; + $doc->userName = UserModel::getOne($doc->createBy)->name ?? 'Unbekannt'; + $doc->mimetype = $file->mimetype ?? 'application/octet-stream'; } - self::returnJson($docs); + foreach($journals as $journal) { + $journal->createByName = UserModel::getOne($journal->createBy)->name ?? 'Unbekannt'; + } + self::returnJson(['docs' => $docs, 'journals' => $journals]); } - + + protected function massAssignWorkordersAction() { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderIds']) || empty($post['companyId'])) self::sendError("Required fields are missing."); + + $deadline = strtotime($post['deadlineDate'] ?? '+6 weeks'); + $count = 0; + foreach ($post['workorderIds'] as $workorderId) { + $workorder = RMLWorkorderModel::get($workorderId); + if ($workorder && $workorder->status === 'new') { + $workorder->companyId = $post['companyId']; + $workorder->status = 'assigned'; + $workorder->assignmentDate = time(); + $workorder->deadlineDate = $deadline; + RMLWorkorderModel::update((array)$workorder); + $count++; + } + } + self::returnJson(['success' => true, 'message' => "$count Aufträge erfolgreich zugewiesen."]); + } + + protected function requestCorrectionAction() { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['workorderId']) || empty($post['text'])) self::sendError("Required fields are missing."); + + $workorder = RMLWorkorderModel::get($post['workorderId']); + if (!$workorder) self::sendError("Workorder not found."); + + $oldStatus = $workorder->status; + $workorder->status = 'correction_requested'; + RMLWorkorderModel::update((array)$workorder); + + RMLWorkorderJournalModel::create([ + 'workorderId' => $workorder->id, + 'text' => $post['text'], + 'fileIds' => !empty($post['fileIds']) ? json_encode($post['fileIds']) : null, + 'statusChange' => "$oldStatus -> correction_requested", + 'create' => time(), + 'createBy' => $this->user->id, + ]); + + self::returnJson(['success' => true, 'message' => 'Korrektur wurde angefordert.']); + } + protected function getCompaniesAction() { $companies = RMLWorkorderCompanyModel::getAll([], null, 0, ['key' => 'name', 'order' => 'ASC']); $items = array_map(fn($c) => ['value' => $c->id, 'text' => $c->name], $companies); self::returnJson($items); } + + protected function addJournalAction() { + $post = json_decode(file_get_contents('php://input'), true); + + if (empty($post['workorderId']) || empty(trim($post['text']))) { + self::sendError("Workorder ID and text are required."); + } + + RMLWorkorderJournalModel::create([ + 'workorderId' => $post['workorderId'], + 'text' => $post['text'], + 'createBy' => $this->user->id, + 'create' => time(), + ]); + + $journals = array_map( + function ($j) { + $j->createByName = UserModel::getOne($j->createBy)->name ?? 'Unbekannt'; + return (array)$j; + }, + RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) + ); + + self::returnJson([ + 'success' => true, + 'message' => 'Journal-Eintrag hinzugefügt.', + 'journals' => $journals + ]); + } + } \ No newline at end of file diff --git a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php index 3829864ea..deee5d3e8 100644 --- a/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php +++ b/application/RMLWorkorderCompany/RMLWorkorderCompanyController.php @@ -13,12 +13,12 @@ class RMLWorkorderCompanyController extends TTCrud ['value' => 'new', 'text' => 'Neu', 'icon' => 'fas fa-star text-primary'], ['value' => 'assigned', 'text' => 'Zugewiesen', 'icon' => 'fas fa-user-check text-info'], ['value' => 'scheduled', 'text' => 'Terminiert', 'icon' => 'fas fa-calendar-check text-warning'], + ['value' => 'correction_requested', 'text' => 'Korrektur angefordert', 'icon' => 'fas fa-exclamation-triangle text-danger'], ['value' => 'documented', 'text' => 'Dokumentiert', 'icon' => 'fas fa-file-alt text-success'], ['value' => 'completed', 'text' => 'Abgeschlossen', 'icon' => 'fas fa-check-double text-secondary'], ]]], ['key' => 'deadlineDate', 'text' => 'Deadline', 'modal' => false, 'table' => ['filter' => 'date']], ['key' => 'appointmentDate', 'text' => 'Termin', 'modal' => false, 'table' => ['filter' => 'date']], - ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], ]; protected array $additionalJSVariables = ['COMPANY_ID' => '0']; @@ -46,40 +46,38 @@ class RMLWorkorderCompanyController extends TTCrud $json = json_decode(file_get_contents('php://input'), true); $pagination = $json['pagination'] ?? ['page' => 1, 'per_page' => 10]; $filters = $json['filters'] ?? []; - $order = $json['order'] ?? ['key' => 'id', 'order' => 'DESC']; + $order = $json['order'] ?? ['key' => 'deadlineDate', 'order' => 'ASC']; $company = RMLWorkorderCompanyModel::getFirst(['addressId' => $this->user->address_id]); - if(!$company) self::sendError("Company not found for user.", 403); - $filters['companyId'] = $company->id; - - if (!empty($filters['preorderInfo'])) { - $searchTerm = $filters['preorderInfo']; - - //todo: fix this preordermodel search shit - $preorders = PreorderModel::getAll(['firstname|lastname|company|oaid' => $searchTerm]); - $preorderIds = array_map(fn($p) => $p->id, $preorders); - - if (!empty($preorderIds)) { - $filters['preorderId'] = $preorderIds; - } else { - $filters['id'] = -1; - } + if (!$company) { + self::sendError("Company not found for user.", 403); } - unset($filters['preorderInfo']); - // only show workorders that are assigned to the company and have the status assigned or scheduled - $filters['status'] = ['assigned', 'scheduled']; - $filters['companyId'] = $company->id; + // Get paginated workorders and total count from the new model methods + $workorders = RMLWorkorderModel::getCompanyWorkorders($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order, $company->id); + $totalCount = RMLWorkorderModel::countCompanyWorkorders($filters, $company->id); - $workorders = RMLWorkorderModel::getAll($filters, $pagination['per_page'], ($pagination['page'] - 1) * $pagination['per_page'], $order); - $totalCount = RMLWorkorderModel::count($filters); - - $rows = []; - foreach($workorders as $workorder) { + // Format rows for the frontend + $rows = array_map(function($workorder) { $row = (array)$workorder; - $row['preorderInfo'] = $this->getPreorderInfoText($workorder->preorderId); - $rows[] = $row; - } + + $anschlussadresse = "{$row['street']} {$row['hausnummer']}"; + if ($row['stiege']) $anschlussadresse .= "/{$row['stiege']}"; + if ($row['apartment']) $anschlussadresse .= " / WE: {$row['apartment']}"; + $anschlussadresse .= ", {$row['plz']} {$row['city']}"; + + $kunde = $row['customerCompany'] ?: $row['customerName']; + + $row['preorderInfo'] = "Kunde: {$kunde}
" . + "Anschluss: {$anschlussadresse}
" . + "Kontakt: {$row['phone']} / {$row['email']}
" . + "OAID: {$row['oaid']}"; + + // Clean up unnecessary fields before sending + unset($row['customerName'], $row['customerCompany'], $row['street'], $row['hausnummer'], $row['stiege'], $row['oaid'], $row['apartment'], $row['plz'], $row['city'], $row['phone'], $row['email']); + + return $row; + }, $workorders); self::returnJson([ 'rows' => $rows, @@ -92,7 +90,6 @@ class RMLWorkorderCompanyController extends TTCrud ] ]); } - public function getWorkorderByIdAction() { $id = $this->request->id; if(!$id) self::sendError("ID missing"); @@ -173,22 +170,30 @@ class RMLWorkorderCompanyController extends TTCrud ]); $uploadCount++; } catch (Exception $e) { - var_dump($e->getMessage());exit; - // Log error but continue with other files error_log("File upload failed for $name: " . $e->getMessage()); } } } - self::returnJson(['success' => true, 'message' => "$uploadCount Datei(en) erfolgreich hochgeladen."]); + $workorder = RMLWorkorderModel::get($workorderId); + if ($workorder->status === 'correction_requested') { + $workorder->status = 'assigned'; + RMLWorkorderModel::update((array)$workorder); + $workorder = RMLWorkorderModel::get($workorderId); + } + + $formattedDocs = $this->getFormattedDocs($workorderId); + + self::returnJson([ + 'success' => true, + 'message' => "$uploadCount Datei(en) erfolgreich hochgeladen.", + 'docs' => $formattedDocs, + 'workorder' => (array)$workorder + ]); } - protected function getDocumentationAction() { - if(empty($this->request->workorderId)) self::sendError("Workorder ID missing."); - - // Order by creation date to ensure consistent numbering (_1, _2, etc.) - $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); - + private function getFormattedDocs($workorderId) { + $docs = RMLWorkorderDocumentationModel::getAll(['workorderId' => $workorderId], null, 0, ['key' => 'create', 'order' => 'ASC']); $responseDocs = []; $typeCounts = []; @@ -202,8 +207,6 @@ class RMLWorkorderCompanyController extends TTCrud foreach($docs as $doc) { $file = new File($doc->fileId); - - // Increment counter for the specific document type $documentTypeKey = $doc->documentType; if (!isset($typeCounts[$documentTypeKey])) { $typeCounts[$documentTypeKey] = 1; @@ -211,23 +214,37 @@ class RMLWorkorderCompanyController extends TTCrud $typeCounts[$documentTypeKey]++; } - // Construct the new filename using the original key $originalFilename = $file->orig_filename ?? $file->filename; $extension = pathinfo($originalFilename, PATHINFO_EXTENSION); $translatedType = $translationMap[$documentTypeKey] ?? $documentTypeKey; - $newFilename = "{$translatedType} {$typeCounts[$documentTypeKey]}." . strtolower($extension); + $newFilename = "{$translatedType}_{$typeCounts[$documentTypeKey]}." . strtolower($extension); - // Get the translated text, with a fallback to the original key - - // Build the response object with 'id' mapped from 'fileId' and the translated type $responseDocs[] = [ - 'id' => $doc->fileId, + 'id' => $doc->id, + 'fileId' => $doc->fileId, 'fileName' => $newFilename, 'documentType' => $documentTypeKey, 'mimetype' => $file->mimetype, ]; } - self::returnJson($responseDocs); + return $responseDocs; + } + + + protected function getDocumentationAction() { + if(empty($this->request->workorderId)) self::sendError("Workorder ID missing."); + + $docs = $this->getFormattedDocs($this->request->workorderId); + + $journals = array_map( + function ($j) { + $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); + return (array)$j; + }, + RMLWorkorderJournalModel::getAll(['workorderId' => $this->request->workorderId], null, 0, ['key' => 'create', 'order' => 'DESC']) + ); + + self::returnJson(['docs' => $docs, 'journals' => $journals]); } protected function completeWorkorderAction() { $post = json_decode(file_get_contents('php://input'), true); @@ -241,4 +258,74 @@ class RMLWorkorderCompanyController extends TTCrud self::returnJson(['success' => true, 'message' => 'Auftrag abgeschlossen.']); } + + protected function deleteDocumentationAction() { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['id'])) self::sendError("Document ID missing."); + + $doc = RMLWorkorderDocumentationModel::get($post['id']); + if (!$doc) self::sendError("Document not found."); + $workorderId = $doc->workorderId; + + RMLWorkorderDocumentationModel::delete($post['id']); + + $formattedDocs = $this->getFormattedDocs($workorderId); + + self::returnJson([ + 'success' => true, + 'message' => 'Dokument gelöscht.', + 'docs' => $formattedDocs + ]); + } + + protected function updateDocumentationAction() { + $post = json_decode(file_get_contents('php://input'), true); + if (empty($post['id'])) self::sendError("Document ID missing."); + + $doc = RMLWorkorderDocumentationModel::get($post['id']); + if (!$doc) self::sendError("Dokument nicht gefunden."); + + if (isset($post['documentType'])) { + $doc->documentType = $post['documentType']; + } + + RMLWorkorderDocumentationModel::update((array)$doc); + + $formattedDocs = $this->getFormattedDocs($doc->workorderId); + + self::returnJson([ + 'success' => true, + 'message' => 'Dokument aktualisiert.', + 'docs' => $formattedDocs + ]); + } + + protected function addJournalAction() { + $post = json_decode(file_get_contents('php://input'), true); + + if (empty($post['workorderId']) || empty(trim($post['text']))) { + self::sendError("Workorder ID and text are required."); + } + + RMLWorkorderJournalModel::create([ + 'workorderId' => $post['workorderId'], + 'text' => $post['text'], + 'createBy' => $this->user->id, + 'create' => time(), + ]); + + $journals = array_map( + function ($j) { + $j->createByName = UserModel::getOne($j->createBy)->getAbbrName(); + return (array)$j; + }, + RMLWorkorderJournalModel::getAll(['workorderId' => $post['workorderId']], null, 0, ['key' => 'create', 'order' => 'DESC']) + ); + + self::returnJson([ + 'success' => true, + 'message' => 'Journal-Eintrag hinzugefügt.', + 'journals' => $journals + ]); + } } \ No newline at end of file diff --git a/application/RMLWorkorderJournal/RMLWorkorderJournalModel.php b/application/RMLWorkorderJournal/RMLWorkorderJournalModel.php new file mode 100644 index 000000000..75a21b3b1 --- /dev/null +++ b/application/RMLWorkorderJournal/RMLWorkorderJournalModel.php @@ -0,0 +1,11 @@ += " . $filterValue['from'] . " AND `$columnName` <= " . $filterValue['to']; + $sql = " AND $quotedColumn >= " . $filterValue['from'] . " AND $quotedColumn <= " . $filterValue['to']; } elseif (isset($filterValue['from'])) { - $sql = " AND `$columnName` >= " . $filterValue['from']; + $sql = " AND $quotedColumn >= " . $filterValue['from']; } elseif (isset($filterValue['to'])) { - $sql = " AND `$columnName` <= " . $filterValue['to']; + $sql = " AND $quotedColumn <= " . $filterValue['to']; } else if (isset($filterValue['exact'])) { - $sql = " AND `$columnName` = " . "'{$filterValue['exact']}'"; + $sql = " AND $quotedColumn = " . "'{$filterValue['exact']}'"; } else if (!empty($filterValue)) { - $sql = " AND `$columnName` IN ('" . implode("','", $filterValue) . "')"; + $sql = " AND $quotedColumn IN ('" . implode("','", $filterValue) . "')"; } } else if ($filterValue === "0" || $filterValue === "1") { - $sql .= " AND `$columnName` = " . $filterValue; + $sql .= " AND $quotedColumn = " . $filterValue; } else if ($filterValue === null) { - $sql .= " AND `$columnName` IS NULL"; + $sql .= " AND $quotedColumn IS NULL"; } else if ($filterValue === '!NULL') { - $sql .= " AND `$columnName` IS NOT NULL"; + $sql .= " AND $quotedColumn IS NOT NULL"; } else if (!empty($filterValue)) { if ($exactMatch) { - $sql .= " AND `$columnName` = '" . $filterValue . "'"; + $sql .= " AND $quotedColumn = '" . $filterValue . "'"; } else if (strpos($columnName, "|") !== false) { - foreach (explode(" ", $filterValue) as $item) - $sql .= " AND CONCAT(" . join(",", explode("|", $columnName)) . ") LIKE '%" . $item . "%'"; + $columns = explode("|", $columnName); + +// Loop through each search term (e.g., "john", "doe") + foreach (explode(" ", $filterValue) as $item) { + // Skip if the item is empty + if (empty(trim($item))) { + continue; + } + + $escapedItem = addslashes($item); // Basic escaping + + // Build the list of OR conditions for the current item + $orConditions = []; + foreach ($columns as $column) { + // e.g., "first_name LIKE '%john%'" + $orConditions[] = "$column LIKE '%" . $escapedItem . "%'"; + } + + // Combine the OR conditions into a single block and add to the query + // e.g., "AND (first_name LIKE '%john%' OR last_name LIKE '%john%')" + if (!empty($orConditions)) { + $sql .= " AND (" . implode(" OR ", $orConditions) . ")"; + } + } } else if ($filterValue[0] === "%") { - $sql .= " AND `$columnName` LIKE '" . $filterValue . "'"; + $sql .= " AND $quotedColumn LIKE '" . addslashes($filterValue) . "'"; } else if ($filterValue[strlen($filterValue) - 1] === "%") { - $sql .= " AND `$columnName` LIKE '" . $filterValue . "'"; + $sql .= " AND $quotedColumn LIKE '" . addslashes($filterValue) . "'"; } else if ($filterValue[0] === "!") { - $sql .= " AND `$columnName` NOT LIKE '%" . substr($filterValue, 1) . "%'"; + $sql .= " AND $quotedColumn NOT LIKE '%" . addslashes(substr($filterValue, 1)) . "%'"; } else { $filterItems = explode(" ", $filterValue); foreach ($filterItems as $item) { - $sql .= " AND `$columnName` LIKE '%" . $item . "%'"; + $escapedItem = addslashes($item); // Basic escaping + $sql .= " AND $quotedColumn LIKE '%" . $escapedItem . "%'"; } } } else if ($filterValue === 0) { - $sql .= " AND `$columnName` = 0"; + $sql .= " AND $quotedColumn = 0"; } return $sql; } - /** * Validates an array of data based on a set of predefined rules. * @@ -175,4 +201,16 @@ class Helper { return number_format($number, $decimals, $decPoint, $thousandsSep); } + public static function getPreorderCampaignFromUser($user, bool $returnObject = false): array { + if ($user->isAdmin()) $campaigns = PreordercampaignModel::getAll(); + else { + $networkIDs = array_unique(array_merge( + array_column($user->myNetworks(["netowner", "salespartner"]), 'id'), + json_decode($user->getFlag("preorder_networks")->value() ?: '[]') + )); + $campaigns = PreordercampaignModel::search(['network_id' => $networkIDs]); + } + + return $returnObject ? $campaigns : array_column($campaigns, 'id'); + } } \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css new file mode 100644 index 000000000..dbd2f788f --- /dev/null +++ b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.css @@ -0,0 +1,27 @@ +/* + * CSS for Workorder Table Row Highlighting (Balanced Colors) + */ + +/* 🔴 Urgent: Deadline passed or less than 1 week away */ +.table-hover .tt-rml-workorder-urgent:hover, +.tt-rml-workorder-urgent { + background-color: #f8d7da !important; /* Balanced Red */ +} + +/* 🟡 Medium: Deadline less than 3 weeks away */ +.table-hover .tt-rml-workorder-medium:hover, +.tt-rml-workorder-medium { + background-color: #fff3cd !important; /* Balanced Yellow */ +} + +/* 🟢 On Track: Deadline more than 3 weeks away */ +.table-hover .tt-rml-workorder-ontrack:hover, +.tt-rml-workorder-ontrack { + background-color: #d4edda !important; /* Balanced Green */ +} + +/* ⚫ Irrelevant: No deadline or status makes it not applicable */ +.table-hover .tt-rml-workorder-irrelevant:hover, +.tt-rml-workorder-irrelevant { + background-color: #e9ecef !important; /* Balanced Grey */ +} \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js index c3b5891e6..2a04d5311 100644 --- a/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js +++ b/public/js/pages/RMLWorkorderAdmin/RMLWorkorderAdmin.js @@ -1,70 +1,151 @@ // RMLWorkorderAdmin.js Vue.component('r-m-l-workorder-admin', { template: ` - - +
+ {{ workordersToAssign.length }} Workorder(s) zuweisen: +
+ - - +
+ + + + + + + + + + + + + - - - - - - - - + + `, data() { return { - assignModalWorkorderId: null, - docsModalWorkorderId: null, + workordersToAssign: [], + editingWorkorderId: null, + companies: [], + massAssignCompanyId: null, + massAssignLoading: false, crudConfig: { ...window.TT_CONFIG.CRUD_CONFIG, - additionalActions: [ - { - "key": "assign", - "title": "Firma zuweisen", - "class": "fas fa-user-plus text-primary", - "condition": (row) => row.status === 'new', - }, - { - "key": "view_docs", - "title": "Dokumentation ansehen", - "class": "fas fa-folder-open text-info", - "condition": (row) => ['documented', 'completed'].includes(row.status), - }, - ] + selectable: false, + expandable: true, + customRowClass: (row) => { + const deadlineDate = moment.unix(row.deadlineDate); + + if (['completed', 'new'].includes(row.status) || !deadlineDate.isValid()) { + return 'tt-rml-workorder-irrelevant'; + } + + const daysLeft = deadlineDate.diff(moment(), 'days'); + + if (daysLeft <= 7) return 'tt-rml-workorder-urgent'; + if (daysLeft <= 21) return 'tt-rml-workorder-medium'; + + return 'tt-rml-workorder-ontrack'; + }, + additionalActions: [] } } }, + async mounted() { + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`); + this.companies = response.data; + }, methods: { + addToAssignList(row) { + if (!this.workordersToAssign.includes(row.id)) { + this.workordersToAssign.push(row.id); + } + }, + removeFromAssignList(row) { + this.workordersToAssign = this.workordersToAssign.filter(id => id !== row.id); + }, getStatusColumn(status) { const column = this.crudConfig.columns.find(c => c.key === 'status'); return column.table.filterOptions.find(opt => opt.value === status) || {}; @@ -72,6 +153,60 @@ Vue.component('r-m-l-workorder-admin', { formatDate(timestamp) { if (!timestamp) return '–'; return window.moment.unix(timestamp).format('DD.MM.YYYY'); + }, + async assignCompany(workorder, companyId) { + if (!companyId) { + this.editingWorkorderId = null; + return; + } + const payload = { + workorderId: workorder.id, + companyId: companyId + }; + try { + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/assignWorkorder`, payload); + if (response.data.success) { + window.notify('success', response.data.message); + this.$refs.table.$refs.table.refreshTable(); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.editingWorkorderId = null; + } + }, + async massAssignCompanies(companyId) { + if (!companyId) return; + + if (!confirm(`${this.workordersToAssign.length} Workorder(s) der ausgewählten Firma zuweisen?`)) { + this.massAssignCompanyId = null; // Reset select on cancel + return; + } + + this.massAssignLoading = true; + + const payload = { + companyId: companyId, + workorderIds: this.workordersToAssign + }; + + try { + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/massAssignWorkorders`, payload); + if (response.data.success) { + window.notify('success', response.data.message); + this.workordersToAssign = []; + this.$refs.table.$refs.table.refreshTable(); + } else { + window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.massAssignLoading = false; + this.massAssignCompanyId = null; + } } } }); @@ -80,104 +215,158 @@ Vue.component('traffic-light', { props: ['deadline', 'status'], computed: { lightInfo() { - if (['completed', 'new'].includes(this.status)) return { color: '#cccccc', title: 'Status irrelevant für Dringlichkeit' }; + if (['completed', 'new'].includes(this.status)) return {color: '#cccccc', title: 'Status irrelevant für Dringlichkeit'}; const now = moment(); const deadlineDate = moment.unix(this.deadline); - if (!deadlineDate.isValid()) return { color: '#cccccc', title: 'Keine Deadline gesetzt' }; + if (!deadlineDate.isValid()) return {color: '#cccccc', title: 'Keine Deadline gesetzt'}; - if (deadlineDate.isBefore(now)) return { color: '#dc3545', title: 'Deadline überschritten' }; + if (deadlineDate.isBefore(now)) return {color: '#dc3545', title: 'Deadline überschritten'}; const daysLeft = deadlineDate.diff(now, 'days'); - 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' }; + 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: `` }); -Vue.component('assign-company-modal', { +Vue.component('rml-documentation-viewer-admin', { props: ['workorderId'], template: ` - - - +
+
+
+
+ +
+
+
+
Korrektur anfordern
+
+

Wählen Sie die zu korrigierenden Dokumente aus der Galerie aus und geben Sie einen Grund an.

+ + +
+
+ + +
+
Dokumentation akzeptieren
+
+

Wenn die Dokumentation korrekt ist, können Sie sie hier akzeptieren.

+ +
+
+ +
+
Journal
+
+
    +
  • + {{ formatDate(log.create) }} ({{ log.createByName }}): +
    {{ log.text }}
    +
  • +
+
Keine Journaleinträge.
+
+ +
+
+
+
`, - data() { return { companies: [], selectedCompanyId: null } }, - async mounted() { - const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getCompanies`); - this.companies = response.data; + data() { + return { + loading: true, + correctionLoading: false, + docs: [], + journals: [], + selectedDocs: [], + correctionText: '', + newJournalMessage: '', + addingJournalEntry: false, + } }, methods: { - async submit() { - if (!this.selectedCompanyId) return window.notify('error', 'Bitte eine Firma auswählen.'); + async fetchData() { + this.loading = true; try { - const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/assignWorkorder`, { + const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getDocumentation`, { params: { workorderId: this.workorderId }}); + this.docs = response.data.docs; + this.journals = response.data.journals; + } catch(e) { + window.notify('error', 'Dokumentation konnte nicht geladen werden.'); + } finally { + this.loading = false; + } + }, + async requestCorrection() { + if (!this.correctionText) return window.notify('error', 'Bitte geben Sie einen Grund für die Korrektur an.'); + if (this.selectedDocs.length === 0) return window.notify('error', 'Bitte wählen Sie mindestens ein Dokument für die Korrektur aus.'); + + this.correctionLoading = true; + try { + const payload = { workorderId: this.workorderId, - companyId: this.selectedCompanyId - }); - if(response.data.success) { + text: this.correctionText, + fileIds: this.selectedDocs + }; + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/requestCorrection`, payload); + if (response.data.success) { window.notify('success', response.data.message); - this.$emit('close'); + this.correctionText = ''; + this.selectedDocs = []; + await this.fetchData(); + this.$emit('workorder-updated'); } else { - window.notify('error', response.data.message || 'Ein Fehler ist aufgetreten.'); + window.notify('error', response.data.message); } } catch (e) { window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); } - } - } -}); - -Vue.component('documentation-viewer-modal', { - props: ['workorderId'], - template: ` - -
-
-
Hochgeladene Dokumente
-
-
-
Keine Dokumente vorhanden.
-
    -
  • - - {{ doc.fileName }} - -
    - Typ: {{ getDocTypeText(doc.documentType) }}
    - Beschreibung: {{ doc.description || '-' }}
    - Hochgeladen von: {{ doc.userName }} am {{ formatDate(doc.create) }} -
    -
  • -
-
-
- `, - data() { - return { loading: false, docs: [] } - }, - methods: { - async fetchDocs() { - this.loading = true; - const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/getDocumentation`, { params: { workorderId: this.workorderId }}); - this.docs = response.data; - this.loading = false; + this.correctionLoading = false; + }, + async addJournalEntry() { + if (!this.newJournalMessage.trim()) { + return window.notify('error', 'Bitte geben Sie eine Nachricht ein.'); + } + this.addingJournalEntry = true; + try { + const response = await axios.post(`${window.TT_CONFIG.BASE_PATH}/RMLWorkorderAdmin/addJournal`, { + workorderId: this.workorderId, + text: this.newJournalMessage + }); + if (response.data.success) { + window.notify('success', response.data.message || 'Journal-Eintrag hinzugefügt.'); + this.newJournalMessage = ''; + this.journals = response.data.journals; + } else { + window.notify('error', response.data.message || 'Eintrag konnte nicht gespeichert werden.'); + } + } catch (e) { + window.notify('error', 'Ein Netzwerkfehler ist aufgetreten.'); + } finally { + this.addingJournalEntry = false; + } }, formatDate(timestamp) { return window.moment.unix(timestamp).format('DD.MM.YYYY HH:mm'); }, - getDocTypeText(type) { - const types = [ - { value: 'photo_before', text: 'Foto: Zustand vorher' }, - { value: 'photo_during', text: 'Foto: Während der Arbeit' }, - { value: 'photo_after', text: 'Foto: Zustand nachher' }, - { value: 'measurement_protocol', text: 'Messprotokoll (z.B. OTDR)' }, - { value: 'customer_signature', text: 'Unterschriebenes Arbeitsprotokoll' }, - ]; - return types.find(t => t.value === type)?.text || type; - } }, mounted() { - this.fetchDocs(); + this.fetchData(); } }); \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorderAdmin.css b/public/js/pages/RMLWorkorderCompany/RMLWorkorderAdmin.css new file mode 100644 index 000000000..ff603857e --- /dev/null +++ b/public/js/pages/RMLWorkorderCompany/RMLWorkorderAdmin.css @@ -0,0 +1,30 @@ +/* + * CSS for Workorder Table Row Highlighting + */ + +/* Urgent: Deadline passed or less than 1 week away */ +.table-hover .tt-rml-workorder-urgent:hover, +.tt-rml-workorder-urgent { + background-color: #fbe9e7 !important; /* Soft Red */ +} + +/* Medium: Deadline less than 3 weeks away */ +.table-hover .tt-rml-workorder-medium:hover, +.tt-rml-workorder-medium { + background-color: #fff8e1 !important; /* Soft Yellow */ +} + +/* On Track: Deadline more than 3 weeks away */ +.table-hover .tt-rml-workorder-ontrack:hover, +.tt-rml-workorder-ontrack { + background-color: #e8f5e9 !important; /* Soft Green */ +} + +/* Irrelevant: No deadline or status makes it not applicable */ +.table-hover .tt-rml-workorder-irrelevant:hover, +.tt-rml-workorder-irrelevant { + background-color: #fafafa !important; /* Very light grey */ +} +.tt-file-gallery-item.border.border-danger { + border: 4px solid #f1556c!important; +} \ No newline at end of file diff --git a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js b/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js index b2928cbf2..da58f3bf3 100644 --- a/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js +++ b/public/js/pages/RMLWorkorderCompany/RMLWorkorderCompany.js @@ -1,17 +1,9 @@ // RMLWorkorderCompany.js - Vue.component('r-m-l-workorder-company', { template: ` - -