From 6593e921ee1e3e170f0478bf27e25634e24ad556 Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Fri, 27 Feb 2026 15:35:17 +0100 Subject: [PATCH] feat: improve notiz column UI and fix memory exhaustion Notiz column now has max 5 lines with expand/collapse animation and auto-resizing edit textarea. Moved raw SQL query to WorkorderModel::getPreorderIdsByCampaigns() to fix OOM in archiveWorkorders(). Co-Authored-By: Claude Opus 4.6 --- application/Workorder/WorkorderModel.php | 11 ++++ .../WorkorderBase/WorkorderBaseController.php | 8 +-- .../js/pages/WorkorderAdmin/WorkorderAdmin.js | 30 +++++++--- .../js/pages/WorkorderBase/WorkorderBase.css | 56 ++++++++++++++++++- .../WorkorderCompany/WorkorderCompany.js | 32 ++++++++--- 5 files changed, 116 insertions(+), 21 deletions(-) diff --git a/application/Workorder/WorkorderModel.php b/application/Workorder/WorkorderModel.php index 680e51244..6f775e151 100644 --- a/application/Workorder/WorkorderModel.php +++ b/application/Workorder/WorkorderModel.php @@ -201,6 +201,17 @@ class WorkorderModel extends TTCrudBaseModel return $result ? $result->fetch_assoc()['count'] : 0; } + public static function getPreorderIdsByCampaigns(array $campaignIds): array { + if (empty($campaignIds)) return []; + $db = self::getDB(); + $campaignIdList = implode(',', array_map('intval', $campaignIds)); + $result = $db->query("SELECT id FROM `" . FRONKDB_DBNAME . "`.`Preorder` WHERE preordercampaign_id IN ($campaignIdList)"); + if (!$result || $result->num_rows === 0) return []; + $ids = array_column($result->fetch_all(MYSQLI_ASSOC), 'id'); + $result->free(); + return $ids; + } + public static function getTechnicalData(int $workorderId): ?array { $workorder = self::get($workorderId); if (!$workorder || !$workorder->preorderId) return null; diff --git a/application/WorkorderBase/WorkorderBaseController.php b/application/WorkorderBase/WorkorderBaseController.php index 1c967ce2e..551728d87 100644 --- a/application/WorkorderBase/WorkorderBaseController.php +++ b/application/WorkorderBase/WorkorderBaseController.php @@ -253,12 +253,8 @@ class WorkorderBaseController extends TTCrud $statusesToCheck = ['new', 'assigned', 'scheduled', 'in_progress', 'correction_requested', 'intervention_required', 'civil_engineering_required', 'civil_engineering_completed', 'problem_solved']; - // Get ALL preorders for tenant (including deleted/cancelled) to ensure their workorders get archived - // Note: Not passing 'deleted' filter means all preorders are returned regardless of deleted status - $allTenantPreorders = PreorderModel::search(['preordercampaign_id' => $tenantCampaignIds]); - if(empty($allTenantPreorders)) continue; - - $allTenantPreorderIds = array_map(fn($p) => $p->id, $allTenantPreorders); + $allTenantPreorderIds = WorkorderModel::getPreorderIdsByCampaigns($tenantCampaignIds); + if (empty($allTenantPreorderIds)) continue; $workordersToCheck = WorkorderModel::getAll([ 'status' => $statusesToCheck, diff --git a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js index 0cf9a40d4..6223ff0d1 100644 --- a/public/js/pages/WorkorderAdmin/WorkorderAdmin.js +++ b/public/js/pages/WorkorderAdmin/WorkorderAdmin.js @@ -64,16 +64,19 @@ Vue.component('workorder-admin', { @@ -122,7 +125,7 @@ Vue.component('workorder-admin', { data() { return { window, workordersToAssign: [], editingWorkorderId: null, editingDeadlineId: null, editingAdditionalInfoId: null, - civilEngineeringData: null, tempAdditionalInfo: '', companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null, + civilEngineeringData: null, tempAdditionalInfo: '', expandedNotes: [], companiesByTenant: {}, companiesLoading: false, massAssignCompanyId: null, cancelWorkorderModalData: null, problemSolvedModalData: null, massAssignModalData: null, crudConfig: { ...window.TT_CONFIG.CRUD_CONFIG, selectable: false, expandable: true, @@ -155,7 +158,11 @@ Vue.component('workorder-admin', { const firstWorkorder = rows.find(r => r.id === this.workordersToAssign[0]); if (!firstWorkorder) return []; return this.companiesByTenant[firstWorkorder.tenantId] || []; - } + }, + editTextareaRows() { + const lines = (this.tempAdditionalInfo || '').split('\n').length; + return String(Math.max(4, lines + 1)); + }, }, methods: { async chargeWorkorder(row) { @@ -273,6 +280,15 @@ Vue.component('workorder-admin', { this.problemSolvedModalData = null; } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); }, + isNoteLong(text) { + if (!text) return false; + return text.split('\n').length > 4 || text.length > 180; + }, + toggleNoteExpand(rowId) { + const idx = this.expandedNotes.indexOf(rowId); + if (idx === -1) this.expandedNotes.push(rowId); + else this.expandedNotes.splice(idx, 1); + }, startAdditionalInfoEdit(row) { this.editingAdditionalInfoId = row.id; this.tempAdditionalInfo = row.additionalInfo || ''; diff --git a/public/js/pages/WorkorderBase/WorkorderBase.css b/public/js/pages/WorkorderBase/WorkorderBase.css index 08c6beaeb..083b892ff 100644 --- a/public/js/pages/WorkorderBase/WorkorderBase.css +++ b/public/js/pages/WorkorderBase/WorkorderBase.css @@ -41,4 +41,58 @@ .workorder-button { padding: 2px !important; -} \ No newline at end of file +} + +.wo-notiz-text { + white-space: pre-wrap; + word-break: break-word; + max-width: 250px; + max-height: calc(5 * 1.5em); + overflow: hidden; + transition: max-height 0.3s ease; +} + +.wo-notiz-text.wo-notiz-expanded { + max-height: 2000px; +} + +.wo-notiz-actions { + display: flex; + align-items: center; + gap: 8px; + margin-top: 2px; +} + +.wo-notiz-toggle { + font-size: 0.8rem; + color: #007bff; + cursor: pointer; + padding: 0; + border: none; + background: none; + white-space: nowrap; +} + +.wo-notiz-toggle:hover { + color: #0056b3; + text-decoration: underline; +} + +.wo-notiz-edit-btn { + color: #007bff; + cursor: pointer; + padding: 0; +} + +.wo-notiz-edit-btn:hover { + color: #0056b3; +} + +.wo-notiz-edit { + min-width: 300px; +} + +.wo-notiz-edit textarea { + min-height: 80px; + resize: vertical; +} diff --git a/public/js/pages/WorkorderCompany/WorkorderCompany.js b/public/js/pages/WorkorderCompany/WorkorderCompany.js index 6e68e65a2..c4474af5e 100644 --- a/public/js/pages/WorkorderCompany/WorkorderCompany.js +++ b/public/js/pages/WorkorderCompany/WorkorderCompany.js @@ -35,18 +35,20 @@ Vue.component('workorder-company', { @@ -104,6 +106,7 @@ Vue.component('workorder-company', { rescheduleModalData: null, editingAdditionalInfoId: null, tempAdditionalInfo: '', + expandedNotes: [], crudConfig: { ...window.TT_CONFIG.CRUD_CONFIG, expandable: true, @@ -121,6 +124,12 @@ Vue.component('workorder-company', { } } }, + computed: { + editTextareaRows() { + const lines = (this.tempAdditionalInfo || '').split('\n').length; + return String(Math.max(4, lines + 1)); + }, + }, methods: { getStatusColumn(status) { const column = this.crudConfig.columns.find(c => c.key === 'status'); @@ -222,6 +231,15 @@ Vue.component('workorder-company', { this.$refs.table.$refs.table.refreshTable(); } else window.notify('error', data.message || 'Ein Fehler ist aufgetreten.'); }, + isNoteLong(text) { + if (!text) return false; + return text.split('\n').length > 4 || text.length > 180; + }, + toggleNoteExpand(rowId) { + const idx = this.expandedNotes.indexOf(rowId); + if (idx === -1) this.expandedNotes.push(rowId); + else this.expandedNotes.splice(idx, 1); + }, startAdditionalInfoEdit(row) { this.editingAdditionalInfoId = row.id; this.tempAdditionalInfo = row.additionalInfo || '';