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 @@
-
-
-
+