Merge pull request #5842 from expsa/14.0-feat-system_dashboard_classic-auto-20251227_191059

[FIX] system_dashboard_classic: enhance data models and user interface
This commit is contained in:
Mohamed Eltayar 2025-12-27 19:12:00 +03:00 committed by GitHub
commit 169df0a739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 245 additions and 99 deletions

View File

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

View File

@ -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;
// 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);
// Simple notification sound as base64 (short beep)
var soundDataUri = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YVoGAACBhYqFbF1fdH2AgYB4cWpvdXt/fn55dHFydnl8fHt6eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7';
// Show loading indicator (optional visual feedback)
var $approveContainer = self.$el.find('div.card-section-approve');
var $trackContainer = self.$el.find('div.card-section-track');
// 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() {
// 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() {

View File

@ -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*/
}
}
}

View File

@ -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 */

View File

@ -148,7 +148,7 @@
.genius-search-input {
padding: 10px 38px 10px 40px !important;
text-align: right !important;
// text-align: right !important;
}
.genius-search-clear {

View File

@ -61,9 +61,7 @@
<link rel="stylesheet" type="text/scss" href="/system_dashboard_classic/static/src/scss/core.scss"/>
<link rel="stylesheet" type="text/scss" href="/system_dashboard_classic/static/src/scss/cards.scss"/>
<link rel="stylesheet" type="text/scss" href="/system_dashboard_classic/static/src/scss/genius-enhancements.scss"/>
<!--RTL-->
<link rel="stylesheet" type="text/scss" href="/system_dashboard_classic/static/src/scss/rtl-cards.scss"/>
<link rel="stylesheet" type="text/scss" href="/system_dashboard_classic/static/src/scss/rtl-core.scss"/>
<!--RTL: Handled by Odoo's rtlcss + essential overrides in genius-enhancements.scss-->
<!--JAVASCRIPT-->
<!--<script type="text/javascript" src="/system_dashboard_classic/static/src/js/canvasjs.min.js"></script>-->
<script type="text/javascript" src="/system_dashboard_classic/static/src/js/d3.v5.min.js"/>