diff --git a/odex25_base/system_dashboard_classic/models/config.py b/odex25_base/system_dashboard_classic/models/config.py
index 1d24f164d..61857cf13 100644
--- a/odex25_base/system_dashboard_classic/models/config.py
+++ b/odex25_base/system_dashboard_classic/models/config.py
@@ -566,17 +566,23 @@ class DashboardConfigSettings(models.TransientModel):
('pie', 'Pie'),
], default='donut', string='Attendance Hours Chart',
config_parameter='system_dashboard_classic.attendance_hours_chart_type')
+
+ # Periodic Refresh Settings
+ dashboard_refresh_enabled = fields.Boolean(
+ string='Enable Auto Refresh',
+ default=False,
+ config_parameter='system_dashboard_classic.refresh_enabled',
+ help='Automatically refresh attendance status and approval count at regular intervals'
+ )
+
+ dashboard_refresh_interval = fields.Integer(
+ string='Refresh Interval (seconds)',
+ default=60,
+ config_parameter='system_dashboard_classic.refresh_interval',
+ help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds / 1 hour)'
+ )
+
- @api.model
- def get_dashboard_colors(self):
- """API method to get dashboard colors for JavaScript"""
- ICPSudo = self.env['ir.config_parameter'].sudo()
- return {
- 'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'),
- 'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
- 'success': ICPSudo.get_param('system_dashboard_classic.success_color', '#10b981'),
- 'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
- }
@api.model
def get_values(self):
diff --git a/odex25_base/system_dashboard_classic/models/models.py b/odex25_base/system_dashboard_classic/models/models.py
index c4d0a987e..84aba6f41 100644
--- a/odex25_base/system_dashboard_classic/models/models.py
+++ b/odex25_base/system_dashboard_classic/models/models.py
@@ -638,8 +638,29 @@ class SystemDashboard(models.Model):
except (json.JSONDecodeError, TypeError, AttributeError):
values['card_orders'] = {}
+ # Load periodic refresh settings
+ ICP = self.env['ir.config_parameter'].sudo()
+ values['refresh_settings'] = {
+ 'enabled': ICP.get_param('system_dashboard_classic.refresh_enabled', 'False') == 'True',
+ 'interval': int(ICP.get_param('system_dashboard_classic.refresh_interval', '60'))
+ }
+
return values
+ @api.model
+ def get_public_dashboard_colors(self):
+ """
+ Secure API method to get dashboard colors for Genius Enhancements JS.
+ Uses sudo() to bypass access rules for ir.config_parameter.
+ """
+ ICPSudo = self.env['ir.config_parameter'].sudo()
+ return {
+ 'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'),
+ 'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
+ 'success': ICPSudo.get_param('system_dashboard_classic.success_color', '#10b981'),
+ 'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
+ }
+
def _haversine_distance(self, lat1, lon1, lat2, lon2):
"""
Calculate the great-circle distance between two GPS points using Haversine formula.
@@ -879,3 +900,124 @@ class SystemDashboard(models.Model):
if order_type:
return all_orders.get(order_type, [])
return all_orders
+
+ @api.model
+ def get_refresh_data(self):
+ """
+ Lightweight method for periodic refresh.
+ Returns only attendance status and approval count.
+ Does NOT return full card data for performance.
+
+ @return: Dict with attendance status and approval count
+ """
+ result = {
+ 'attendance': [],
+ 'approval_count': 0
+ }
+
+ user = self.env['res.users'].browse(self.env.uid)
+ employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1)
+
+ if not employee:
+ return result
+
+ # ATTENDANCE LOGIC: Matches get_data exactly
+ # 1. Use correct model 'attendance.attendance' (Event-based)
+ # 2. Convert to User's Timezone
+ # 3. Determine status based on 'action' field
+
+ last_attendance = self.env['attendance.attendance'].sudo().search(
+ [('employee_id', '=', employee.id)],
+ order='name desc',
+ limit=1
+ )
+
+ if last_attendance:
+ is_attendance = (last_attendance.action == 'sign_in')
+
+ # Timezone Conversion
+ user_tz = pytz.timezone(self.env.context.get('tz', 'Asia/Riyadh') or self.env.user.tz or 'UTC')
+ time_str = ''
+
+ if last_attendance.name:
+ # 'name' is the Datetime field in UTC
+ time_object = fields.Datetime.from_string(last_attendance.name)
+ # Convert to user's timezone
+ time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
+ # Format to string for display (removing timezone offset info for cleanliness)
+ time_str = time_in_timezone.strftime('%Y-%m-%d %H:%M:%S')
+
+ result['attendance'] = [{
+ 'is_attendance': is_attendance,
+ 'time': time_str,
+ }]
+
+ # Calculate approval count
+ import ast
+
+ # Iterate over all dashboard lines (which represent "cards" or sections of cards)
+ # We process lines directly to be more efficient than iterating boards -> lines
+ config_lines = self.env['base.dashbord.line'].sudo().search([])
+ approval_count = 0
+
+ for line in config_lines:
+ # 1. User Access Check using the existing helper method
+ # This checks if the current user is in the allowed groups for this line
+ if not self.is_user(line.group_ids, user):
+ continue
+
+ # 2. Get the parent dashboard definition
+ dashboard = line.board_id
+ if not dashboard or not dashboard.model_id:
+ continue
+
+ # 3. Construct Domain (Replicating logic from get_data)
+ domain = []
+
+ # Add Action Domain (if configured)
+ if dashboard.action_domain:
+ try:
+ # Parse string domain to list
+ dom_list = ast.literal_eval(dashboard.action_domain)
+ # Filter out 'state' from action domain as per get_data logic
+ # This prevents conflict with the specific state/stage we are checking below
+ for item in dom_list:
+ if isinstance(item, (list, tuple)) and len(item) > 0 and item[0] != 'state':
+ domain.append(item)
+ except Exception:
+ # Invalid domain string, ignore action domain
+ pass
+
+ # Special Handling for HR Holidays Workflow
+ if dashboard.model_name == 'hr.holidays':
+ hr_holidays_workflow = self.env['ir.module.module'].sudo().search([('name', '=', 'hr_holidays_workflow')], limit=1)
+ if hr_holidays_workflow and hr_holidays_workflow.state == 'installed':
+ # Logic from get_data: OR condition for stage name matching state
+ domain.append('|')
+ domain.append(('stage_id.name', '=', line.state_id.state))
+
+ # Add State/Stage Filter
+ # Using the exact same logic as get_data to target the specific bucket
+ if line.state_id:
+ domain.append(('state', '=', line.state_id.state))
+ elif line.stage_id:
+ # stage_id in stage.stage is a Char field holding the ID, so we cast to int
+ # This matches get_data: ('stage_id', '=', int(line.stage_id.stage_id))
+ try:
+ target_stage_id = int(line.stage_id.stage_id)
+ domain.append(('stage_id', '=', target_stage_id))
+ except (ValueError, TypeError):
+ pass
+
+ # 4. Execute Count Query
+ try:
+ # Ensure the model exists in the registry
+ if dashboard.model_name in self.env:
+ line_count = self.env[dashboard.model_name].sudo().search_count(domain)
+ approval_count += line_count
+ except Exception:
+ # Failsafe: If domain is invalid or model doesn't exist, skip this line
+ pass
+
+ result['approval_count'] = approval_count
+ return result
diff --git a/odex25_base/system_dashboard_classic/static/src/js/genius_enhancements.js b/odex25_base/system_dashboard_classic/static/src/js/genius_enhancements.js
index 7aa72535f..dbb7b41f7 100644
--- a/odex25_base/system_dashboard_classic/static/src/js/genius_enhancements.js
+++ b/odex25_base/system_dashboard_classic/static/src/js/genius_enhancements.js
@@ -123,33 +123,40 @@ odoo.define('system_dashboard_classic.genius_enhancements', function(require) {
* Generates light/dark variants automatically for complete theming
*/
function loadCustomColors() {
- var ICP = 'ir.config_parameter';
- var prefix = 'system_dashboard_classic.';
+ var DashboardModel = 'system_dashboard_classic.dashboard';
// Define all color parameters with their CSS variable names
var colorParams = [
- { param: 'primary_color', cssVar: '--dash-primary', defaultColor: '#0891b2' },
- { param: 'secondary_color', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
- { param: 'success_color', cssVar: '--dash-success', defaultColor: '#10b981' },
- { param: 'warning_color', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
+ { param: 'primary', cssVar: '--dash-primary', defaultColor: '#0891b2' },
+ { param: 'secondary', cssVar: '--dash-secondary', defaultColor: '#1e293b' },
+ { param: 'success', cssVar: '--dash-success', defaultColor: '#10b981' },
+ { param: 'warning', cssVar: '--dash-warning', defaultColor: '#f59e0b' }
];
- // Load each color
- colorParams.forEach(function(item) {
- ajax.jsonRpc('/web/dataset/call_kw', 'call', {
- model: ICP,
- method: 'get_param',
- args: [prefix + item.param],
- kwargs: {}
- }).then(function(color) {
- if (color && color.match(/^#[0-9A-Fa-f]{6}$/)) {
- applyColorWithVariants(item.cssVar, color);
+ // Fetch valid dashboard colors from the secure public method
+ ajax.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: DashboardModel,
+ method: 'get_public_dashboard_colors',
+ args: [],
+ kwargs: {}
+ }).then(function(colors) {
+ if (!colors) return;
+
+ colorParams.forEach(function(item) {
+ var colorValue = colors[item.param];
+ // Use default if not returned or invalid
+ if (!colorValue) colorValue = item.defaultColor;
+
+ if (colorValue && colorValue.match(/^#[0-9A-Fa-f]{6}$/)) {
+ applyColorWithVariants(item.cssVar, colorValue);
// For primary color, also generate SVG icon filter
if (item.cssVar === '--dash-primary') {
- generateIconFilter(color);
+ generateIconFilter(colorValue);
}
}
- }).catch(function() {});
+ });
+ }).catch(function(err) {
+ console.warn('[Genius Enhancements] Failed to load custom colors:', err);
});
}
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 a951dff28..5d1e1fb31 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
@@ -202,6 +202,11 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
}, []).then(function(result) {
window.resultX = result;
+ // Load periodic refresh settings
+ var refreshEnabled = result.refresh_settings && result.refresh_settings.enabled;
+ var refreshInterval = (result.refresh_settings && result.refresh_settings.interval) || 60;
+ refreshInterval = Math.max(30, Math.min(3600, refreshInterval)) * 1000; // Clamp 30s-3600s, convert to ms
+
// IMMEDIATE CSS VARIABLE APPLICATION - Apply colors before any rendering
// This prevents the "flash of green/default" issue by setting CSS vars synchronously
if (result.chart_colors) {
@@ -836,6 +841,92 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
});
});
}
+
+ // ============================================================
+ // PERIODIC REFRESH - Updates attendance + approval count
+ // Uses Page Visibility API to pause when tab hidden
+ // ============================================================
+
+ self.periodicRefreshId = null;
+ var isRtl = $('body').hasClass('o_rtl');
+
+ function updateAttendanceStatus(attendance) {
+ if (!attendance || attendance.length === 0) return;
+
+ var att = attendance[0];
+ var $imgSection = $('.attendance-img-section');
+ var $infoSection = $('.last-checkin-info');
+
+ // Get localized titles
+ var checkin_title = isRtl ? 'وقت الدخول' : 'Check-in Time';
+ var checkout_title = isRtl ? 'وقت الخروج' : 'Check-out Time';
+
+ if (att.is_attendance) {
+ // User is checked IN - show logout icon with checkout title
+ var image = '';
+ $imgSection.html(image).removeClass('state-checkin').addClass('state-checkout');
+
+ // Show check-in info
+ var checkinInfoHtml = '