[IMP] system_dashboard_classic: automatic update
Auto-generated commit based on local changes.
This commit is contained in:
parent
fb0df50441
commit
21b86d27fc
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@
|
|||
|
||||
.genius-search-input {
|
||||
padding: 10px 38px 10px 40px !important;
|
||||
text-align: right !important;
|
||||
// text-align: right !important;
|
||||
}
|
||||
|
||||
.genius-search-clear {
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue