From 21b86d27fc203e49dade379c007f25d96cab1ee1 Mon Sep 17 00:00:00 2001 From: maltayyar2 Date: Sat, 27 Dec 2025 19:10:59 +0300 Subject: [PATCH] [IMP] system_dashboard_classic: automatic update Auto-generated commit based on local changes. --- .../system_dashboard_classic/models/models.py | 22 ++- .../src/js/system_dashboard_self_service.js | 185 +++++++++++++----- .../static/src/scss/core.scss | 29 +-- .../static/src/scss/genius-enhancements.scss | 102 +++++++--- .../static/src/scss/rtl-cards.scss | 2 +- .../views/system_dashboard.xml | 4 +- 6 files changed, 245 insertions(+), 99 deletions(-) diff --git a/odex25_base/system_dashboard_classic/models/models.py b/odex25_base/system_dashboard_classic/models/models.py index 6ae3a517c..c4d0a987e 100644 --- a/odex25_base/system_dashboard_classic/models/models.py +++ b/odex25_base/system_dashboard_classic/models/models.py @@ -750,20 +750,28 @@ class SystemDashboard(models.Model): # Calculate how far outside the zone user is distance_outside = int(closest_zone_distance - allowed_range) + # Smart distance formatting: use km for large distances, meters for small + if distance_outside >= 1000: + # Convert to km with 1 decimal place + distance_km = round(distance_outside / 1000, 1) + distance_str_ar = f"{distance_km} كيلو متر" + distance_str_en = f"{distance_km} kilometers" + else: + distance_str_ar = f"{distance_outside} متر" + distance_str_en = f"{distance_outside} meters" + # Gender-aware Arabic message # تتواجد (male) / تتواجدين (female) # لتتمكن (male) / لتتمكني (female) employee_gender = getattr(employee_object, 'gender', 'male') or 'male' if employee_gender == 'female': - ar_msg = "عذراً، أنتِ تتواجدين خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة %s متر تقريباً أو أكثر لتتمكني من التسجيل." + ar_msg = f"عذراً، أنتِ تتواجدين خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {distance_str_ar} تقريباً أو أكثر لتتمكني من التسجيل." else: - ar_msg = "عذراً، أنت تتواجد خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة %s متر تقريباً أو أكثر لتتمكن من التسجيل." + ar_msg = f"عذراً، أنت تتواجد خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {distance_str_ar} تقريباً أو أكثر لتتمكن من التسجيل." - msg_formats = { - 'ar': ar_msg, - 'en': "Sorry, you are outside the allowed attendance zone.\nPlease move approximately %s meters or more closer to be able to check in." - } - msg = (msg_formats['ar'] if is_arabic else msg_formats['en']) % distance_outside + en_msg = f"Sorry, you are outside the allowed attendance zone.\nPlease move approximately {distance_str_en} or more closer to be able to check in." + + msg = ar_msg if is_arabic else en_msg return { 'error': True, 'message': msg diff --git a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js b/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js index 3623c5c5f..5f0e8118a 100644 --- a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js +++ b/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js @@ -542,13 +542,36 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require // Function to build/rebuild approval cards // IMPORTANT: Receives full result object to access card_orders for drag-drop - function buildApprovalCards(resultData) { + // FIX: Added DOM-ready check with retry to handle race condition + function buildApprovalCards(resultData, retryCount) { + retryCount = retryCount || 0; + var maxRetries = 30; // 3 seconds max wait (30 * 100ms) + var cards = resultData.cards; var cardOrders = resultData.card_orders; + // Check if DOM elements exist (template may not be attached yet) + var $approveContainer = self.$el.find('div.card-section-approve'); + var $trackContainer = self.$el.find('div.card-section-track'); + + if ($approveContainer.length === 0 || $trackContainer.length === 0) { + // DOM not ready - retry after 100ms + if (retryCount < maxRetries) { + console.log('[Dashboard] Waiting for DOM... attempt ' + (retryCount + 1)); + setTimeout(function() { + buildApprovalCards(resultData, retryCount + 1); + }, 100); + return; + } else { + console.error('[Dashboard] DOM elements not found after ' + maxRetries + ' attempts'); + return; + } + } + + // DOM is ready - proceed with building cards // Clear existing cards - self.$el.find('div.card-section-approve').empty(); - self.$el.find('div.card-section-track').empty(); + $approveContainer.empty(); + $trackContainer.empty(); $.each(cards, function(index, cardData) { if (cardData.type == "approve") { @@ -562,8 +585,8 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require var card2 = buildCardApprove(cardData, index, 'table2'); $(card2).find('.card-header h4 span').html(_t(cardData.name)); // APPENDING CARDS - self.$el.find('div.card-section-approve').append(card); - self.$el.find('div.card-section-track').append(card2); + $approveContainer.append(card); + $trackContainer.append(card2); } }); @@ -653,71 +676,131 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require buildApprovalCards(result); // ============================================================ - // TAB CLICK - Cards are already built, no need to refresh on tab switch - // Polling handles data freshness. Drag-drop order is preserved. + // ============================================================ + // TAB CLICK REFRESH - Refresh data when clicking approval tabs + // Fetches fresh data while PRESERVING saved card order // ============================================================ - // ============================================================ - // POLLING FOR NEW APPROVAL REQUESTS (Feature 7) - // Checks every 60 seconds for new requests and plays notification - // ============================================================ - var lastApproveCount = self.$el.find('div.card-section-approve .card3').length; - var notificationSound = null; - var audioUnlocked = false; - - // Simple notification sound as base64 (short beep) - var soundDataUri = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YVoGAACBhYqFbF1fdH2AgYB4cWpvdXt/fn55dHFydnl8fHt6eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7'; - - // Unlock audio on first user interaction (browser policy) - $(document).one('click keydown', function() { - try { - notificationSound = new Audio(soundDataUri); - notificationSound.volume = 0.5; - notificationSound.load(); - audioUnlocked = true; - } catch (e) { - // Audio notification not supported - } - }); - - // Poll for new approval requests every 60 seconds - self.pollingIntervalId = setInterval(function() { + // Bind to approval tab clicks for data refresh + $('a[href="#to_approve"], a[href="#to_track"]').off('click.refresh').on('click.refresh', function(e) { + var $tab = $(this); + + // Show loading indicator (optional visual feedback) + var $approveContainer = self.$el.find('div.card-section-approve'); + var $trackContainer = self.$el.find('div.card-section-track'); + + // Fetch fresh data from server self._rpc({ model: 'system_dashboard_classic.dashboard', method: 'get_data', }, []).then(function(freshResult) { - // Count ACTUAL pending requests from fresh data - var newApproveCount = 0; + // Update global result reference + window.resultX = freshResult; + + // Get saved order from server (MUST BE FRESH from this request) + var freshCardOrders = freshResult.card_orders || {}; + var savedApprovalOrder = freshCardOrders['dashboard_approval_order']; + + // Clear existing cards + $approveContainer.empty(); + $trackContainer.empty(); + + // Rebuild cards from fresh data + $.each(freshResult.cards, function(index, cardData) { + if (cardData.type == "approve") { + // BUILD APPROVE CARD + var card = buildCardApprove(cardData, index, 'table1'); + $(card).find('.card-header h4 span').html(_t(cardData.name)); + // BUILD FOLLOW CARD + var card2 = buildCardApprove(cardData, index, 'table2'); + $(card2).find('.card-header h4 span').html(_t(cardData.name)); + // APPEND + $approveContainer.append(card); + $trackContainer.append(card2); + } + }); + + // CRITICAL: Apply saved order AFTER building cards + // This preserves user's drag-drop customization + setTimeout(function() { + // Re-initialize drag-drop with saved order from server + initDragDropSortable({ + containerSelector: 'div.card-section-approve', + cardSelector: '.card2', + storageKey: 'dashboard_approval_order', + rpcContext: self._rpc.bind(self), + serverOrder: savedApprovalOrder, + linkedContainerSelector: 'div.card-section-track' + }); + + // Apply same order to track tab + if (savedApprovalOrder && savedApprovalOrder.length > 0) { + var $trackCards = $trackContainer.find('.card2'); + savedApprovalOrder.forEach(function(cardId) { + var $matchingCard = $trackCards.filter(function() { + var model = $(this).find('.box-1').attr('data-model') || $(this).attr('data-model'); + return model === cardId; + }).first(); + if ($matchingCard.length > 0) { + $trackContainer.append($matchingCard); + } + }); + } + + // Rebind click handlers for new cards + self.$el.find('tr[data-target="record-button"]').off('click').on('click', function(event) { + event.stopPropagation(); + event.preventDefault(); + var model = $(this).attr('data-model'); + var name = $(this).attr('data-name'); + var domain = $(this).attr('data-domain'); + var form_view = parseInt($(this).attr('data-form-view')) || false; + var list_view = parseInt($(this).attr('data-list-view')) || false; + var count = parseInt($(this).attr('data-count')) || 0; + var context = $(this).attr('data-context') || '{}'; + + try { context = JSON.parse(context.replace(/'/g, '"')); } + catch(e) { context = {}; } + + if (count > 0 && domain) { + return self.do_action({ + name: _t(name), + type: 'ir.actions.act_window', + res_model: model, + view_mode: 'tree,form', + views: [[list_view, 'list'], [form_view, 'form']], + context: context, + domain: JSON.parse(domain.replace(/'/g, '"')), + target: 'current', + flags: { reload: true } + }, { on_reverse_breadcrumb: function() { return self.reload(); } }); + } + }); + }, 100); + + // Update pending count badge + var totalPendingCount = 0; $.each(freshResult.cards, function(index, cardData) { if (cardData.type == 'approve' && cardData.lines && cardData.lines.length > 0) { $.each(cardData.lines, function(lineIdx, line) { if (line.count_state_click) { - newApproveCount += parseInt(line.count_state_click) || 0; + totalPendingCount += parseInt(line.count_state_click) || 0; } }); } }); - // Check if there are NEW requests - if (newApproveCount > lastApproveCount) { - // Play notification sound - if (audioUnlocked && notificationSound) { - notificationSound.currentTime = 0; - notificationSound.play().catch(function(e) { - // Ignore errors silently - }); - } - - // Rebuild cards and update badge - buildApprovalCards(freshResult.cards); + if (totalPendingCount > 0) { + $('.pending-count-badge').text(totalPendingCount).show(); + } else { + $('.pending-count-badge').hide(); } - // Update count for next comparison - lastApproveCount = newApproveCount; + console.log('[Dashboard] Approval cards refreshed on tab click'); }).catch(function(e) { - // Silently ignore polling errors + console.error('[Dashboard] Error refreshing approval data:', e); }); - }, 60000); // Check every 60 seconds + }); } // Chart Settings setTimeout(function() { diff --git a/odex25_base/system_dashboard_classic/static/src/scss/core.scss b/odex25_base/system_dashboard_classic/static/src/scss/core.scss index b8560dc27..8099d4598 100644 --- a/odex25_base/system_dashboard_classic/static/src/scss/core.scss +++ b/odex25_base/system_dashboard_classic/static/src/scss/core.scss @@ -488,17 +488,18 @@ $border-color_1: #2eac96; #timesheet-section { display: none; } +/* RTL Support - Essential rules that rtlcss cannot handle automatically */ .o_rtl { .canvasjs-chart-container { - direction: rtl; - text-align: right !important; + direction: rtl; /*rtl:ignore*/ + text-align: right !important; /*rtl:ignore*/ } .dashboard-container { .dashboard-header { .dashboard-user-statistics-section { .dashboard-attendance-section { - border-left: 1px solid #d0d0d0; - border-right: none !important; + border-left: 1px solid #d0d0d0; /*rtl:ignore*/ + border-right: none !important; /*rtl:ignore*/ } } } @@ -509,8 +510,8 @@ $border-color_1: #2eac96; >a { border-radius: 0; } - margin-left: 5px; - margin-right: 0; + margin-left: 5px; /*rtl:ignore*/ + margin-right: 0; /*rtl:ignore*/ } } } @@ -520,13 +521,13 @@ $border-color_1: #2eac96; .module-box-container { p { >span { - margin-right: 0; + margin-right: 0; /*rtl:ignore*/ } } } } .card2 { - padding-right: 0; + padding-right: 0; /*rtl:ignore*/ } } .lds-roller { @@ -637,9 +638,10 @@ $border-color_1: #2eac96; .canvasjs-chart-canvas { position: relative !important; } + /* RTL responsive override */ .o_rtl { .card3 { - padding: 0; + padding: 0; /*rtl:ignore*/ } } } @@ -708,15 +710,16 @@ $border-color_1: #2eac96; .canvasjs-chart-canvas { position: relative !important; } + /* RTL responsive padding for card2 alternating columns */ .o_rtl { .card2 { &:nth-child(2n) { - padding-right: 15px; - padding-left: 0; + padding-right: 15px; /*rtl:ignore*/ + padding-left: 0; /*rtl:ignore*/ } &:nth-child(3n) { - padding-left: 15px; - padding-right: 0; + padding-left: 15px; /*rtl:ignore*/ + padding-right: 0; /*rtl:ignore*/ } } } diff --git a/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss b/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss index b5beca179..e0b4ccb18 100644 --- a/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss +++ b/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss @@ -2028,28 +2028,86 @@ p.fn-section.clickable-profile:hover { /* ============================================================ - RTL SUPPORT FOR NEW ELEMENTS + RTL SUPPORT - Essential Overrides + ============================================================ + These are rules that Odoo's rtlcss cannot handle automatically. + Only use .o_rtl class (added by Odoo to body for RTL languages). + + Rules that rtlcss DOES handle automatically: + - left ↔ right + - margin-left ↔ margin-right + - padding-left ↔ padding-right + - float: left ↔ float: right + - text-align: left ↔ text-align: right + + Only add manual overrides when rtlcss produces incorrect results + or when specific design requires different behavior. ============================================================ */ .o_rtl { - // .genius-search-container { - // margin-left: 0 !important; - // margin-right: auto !important; - // } - + /* === Profile Section RTL === */ + .profile-container { + .pp-info-section { + border-left: none; + border-right: 1px solid #9f9f9f; /*rtl:ignore*/ + } + + /* Green status dot - keep on left side in RTL */ + .pp-image-section::after { + right: auto !important; + left: 8px !important; /*rtl:ignore*/ + } + } + + /* === Search Bar RTL === */ .genius-search-icon { left: auto !important; - right: 14px !important; + right: 14px !important; /*rtl:ignore*/ } .genius-search-input { - padding: 10px 38px 10px 40px !important; - text-align: right !important; + padding: 10px 38px 10px 40px !important; /*rtl:ignore*/ } .genius-search-clear { right: auto !important; - left: 14px !important; + left: 14px !important; /*rtl:ignore*/ + } + + /* === Card2 (Approval Cards) RTL === */ + .card2 { + .card-container .card-header { + flex-direction: row-reverse !important; /*rtl:ignore*/ + + img { + margin-right: 0 !important; + margin-left: 12px !important; /*rtl:ignore*/ + } + } + + .card-container .card-body table tr td { + &:last-child { + div { + float: left !important; /*rtl:ignore*/ + } + } + } + } + + /* === Card3 (Service Cards) RTL === */ + .card3 .card-body .box-2 { + flex-direction: row-reverse !important; /*rtl:ignore*/ + + i { + margin-left: 0 !important; + margin-right: 8px !important; /*rtl:ignore*/ + } + } + + /* === Attendance Section RTL === */ + .dashboard-attendance-section { + border-left: none !important; + border-right: 1px solid rgba(255, 255, 255, 0.1) !important; /*rtl:ignore*/ } } @@ -3037,12 +3095,11 @@ p.fn-section.clickable-profile:hover { } } -/* RTL Support */ -[dir="rtl"] .attendance-notification, -body.o_rtl .attendance-notification { +/* RTL Support - Notification */ +.o_rtl .attendance-notification { .notification-content { .notification-subtitle { - direction: rtl; + direction: rtl; /*rtl:ignore*/ } } } @@ -3141,10 +3198,9 @@ body.o_rtl .attendance-notification { } /* RTL Support for Error Toast */ -[dir="rtl"] .attendance-error-toast, -body.o_rtl .attendance-error-toast { +.o_rtl .attendance-error-toast { .error-message { - direction: rtl; + direction: rtl; /*rtl:ignore*/ } } @@ -3204,7 +3260,7 @@ body.o_rtl .attendance-error-toast { /* Subtle drag handle hint on hover */ .card3.draggable-card .card-body::after { - content: '⋮⋮'; + content: '\22EE\22EE'; /* ⋮⋮ - Unicode for vertical ellipsis */ position: absolute; top: 6px; left: 6px; @@ -3220,10 +3276,9 @@ body.o_rtl .attendance-error-toast { } /* RTL Support for drag handle */ -[dir="rtl"] .card3.draggable-card .card-body::after, -body.o_rtl .card3.draggable-card .card-body::after { +.o_rtl .card3.draggable-card .card-body::after { left: auto; - right: 6px; + right: 6px; /*rtl:ignore*/ } /* Disable drag on mobile - touch devices use different gestures */ @@ -3293,10 +3348,9 @@ body.o_rtl .card3.draggable-card .card-body::after { } /* RTL Support for stats module-box drag handle */ -[dir="rtl"] .module-box.draggable-card .module-box-container::after, -body.o_rtl .module-box.draggable-card .module-box-container::after { +.o_rtl .module-box.draggable-card .module-box-container::after { left: auto; - right: 4px; + right: 4px; /*rtl:ignore*/ } /* Service Icon Uniformity and Hover Effects */ diff --git a/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss b/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss index d81c54dd7..3fc5b0f49 100644 --- a/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss +++ b/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss @@ -148,7 +148,7 @@ .genius-search-input { padding: 10px 38px 10px 40px !important; - text-align: right !important; + // text-align: right !important; } .genius-search-clear { diff --git a/odex25_base/system_dashboard_classic/views/system_dashboard.xml b/odex25_base/system_dashboard_classic/views/system_dashboard.xml index 4bfdd7144..b69b65e40 100644 --- a/odex25_base/system_dashboard_classic/views/system_dashboard.xml +++ b/odex25_base/system_dashboard_classic/views/system_dashboard.xml @@ -61,9 +61,7 @@ - - - +