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 = '
' + checkout_title + '
' + + '
' + att.time + '
'; + $infoSection.html(checkinInfoHtml).show(); + } else { + // User is checked OUT - show login icon with checkin title + var image = ''; + $imgSection.html(image).removeClass('state-checkout').addClass('state-checkin'); + + // Show check-out info if time exists + if (att.time) { + var checkoutInfoHtml = '
' + checkin_title + '
' + + '
' + att.time + '
'; + $infoSection.html(checkoutInfoHtml).show(); + } else { + $infoSection.html('').hide(); + } + } + } + + function updateApprovalCount(count) { + var $badge = $('.pending-count-badge'); + if (count > 0) { + $badge.text(count).show(); + } else { + $badge.hide(); + } + } + + function startPeriodicRefresh() { + if (!refreshEnabled || refreshInterval <= 0) { + console.log('[Dashboard] Periodic refresh disabled'); + return; + } + + console.log('[Dashboard] Starting periodic refresh (interval: ' + (refreshInterval/1000) + 's)'); + + self.periodicRefreshId = setInterval(function() { + // Skip if page is hidden (browser tab switch) + if (document.hidden) { + console.log('[Dashboard] Skipping refresh - tab hidden'); + return; + } + + // Fetch lightweight data + self._rpc({ + model: 'system_dashboard_classic.dashboard', + method: 'get_refresh_data', + }, []).then(function(data) { + updateAttendanceStatus(data.attendance); + updateApprovalCount(data.approval_count); + console.log('[Dashboard] Periodic refresh completed - Count: ' + data.approval_count); + }).catch(function(err) { + console.warn('[Dashboard] Refresh failed:', err); + }); + }, refreshInterval); + } + + // Start polling + startPeriodicRefresh(); + // Chart Settings setTimeout(function() { window.check = false; @@ -1564,6 +1655,12 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require clearInterval(this.pollingIntervalId); this.pollingIntervalId = null; } + // Clear periodic refresh interval + if (this.periodicRefreshId) { + clearInterval(this.periodicRefreshId); + this.periodicRefreshId = null; + console.log('[Dashboard] Periodic refresh stopped'); + } // Call parent destroy this._super.apply(this, arguments); // Clear attendance clock interval if exists diff --git a/odex25_base/system_dashboard_classic/views/dashboard_settings.xml b/odex25_base/system_dashboard_classic/views/dashboard_settings.xml index 94e8f06fe..0ec8b3a66 100644 --- a/odex25_base/system_dashboard_classic/views/dashboard_settings.xml +++ b/odex25_base/system_dashboard_classic/views/dashboard_settings.xml @@ -200,6 +200,39 @@ + + +

Auto-Refresh Settings

+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
diff --git a/odex25_base/tour_genius/__manifest__.py b/odex25_base/tour_genius/__manifest__.py index dd6c788c1..ccf0f78f5 100644 --- a/odex25_base/tour_genius/__manifest__.py +++ b/odex25_base/tour_genius/__manifest__.py @@ -34,6 +34,7 @@ Author: Expert Development Team # Security 'security/security.xml', 'security/ir.model.access.csv', + 'security/rules.xml', # Data (Crons) 'data/cron_data.xml', # Assets (MUST load before views for JS to register) diff --git a/odex25_base/tour_genius/models/res_users.py b/odex25_base/tour_genius/models/res_users.py index 11c5888c8..560820d85 100644 --- a/odex25_base/tour_genius/models/res_users.py +++ b/odex25_base/tour_genius/models/res_users.py @@ -65,3 +65,5 @@ class ResUsers(models.Model): user.genius_completed_topics = completed user.genius_in_progress_topics = in_progress user.genius_total_time_hours = total_time / 60.0 if total_time else 0.0 + + diff --git a/odex25_base/tour_genius/security/ir.model.access.csv b/odex25_base/tour_genius/security/ir.model.access.csv index f48bc50d9..3db1d518a 100644 --- a/odex25_base/tour_genius/security/ir.model.access.csv +++ b/odex25_base/tour_genius/security/ir.model.access.csv @@ -2,22 +2,31 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_plan_user,genius.plan.user,model_genius_plan,group_genius_user,1,0,0,0 access_plan_instructor,genius.plan.instructor,model_genius_plan,group_genius_instructor,1,1,1,0 access_plan_admin,genius.plan.admin,model_genius_plan,group_genius_admin,1,1,1,1 +access_topic_standard_user,genius.topic.standard.user,model_genius_topic,base.group_user,1,0,0,0 access_topic_user,genius.topic.user,model_genius_topic,group_genius_user,1,0,0,0 access_topic_instructor,genius.topic.instructor,model_genius_topic,group_genius_instructor,1,1,1,1 +access_step_standard_user,genius.topic.step.standard.user,model_genius_topic_step,base.group_user,1,0,0,0 access_step_user,genius.topic.step.user,model_genius_topic_step,group_genius_user,1,0,0,0 access_step_instructor,genius.topic.step.instructor,model_genius_topic_step,group_genius_instructor,1,1,1,1 +access_progress_standard_user,genius.progress.user,model_genius_progress,base.group_user,1,1,1,0 access_progress_user,genius.progress.user,model_genius_progress,group_genius_user,1,1,1,0 access_progress_instructor,genius.progress.instructor,model_genius_progress,group_genius_instructor,1,1,1,1 +access_tag_standard_user,genius.tour.tag.standard.user,model_genius_tour_tag,base.group_user,1,0,0,0 access_tag_user,genius.tour.tag.user,model_genius_tour_tag,group_genius_user,1,0,0,0 access_tag_instructor,genius.tour.tag.instructor,model_genius_tour_tag,group_genius_instructor,1,1,1,1 +access_quiz_standard_user,genius.quiz.standard.user,model_genius_quiz,base.group_user,1,0,0,0 access_quiz_user,genius.quiz.user,model_genius_quiz,group_genius_user,1,0,0,0 access_quiz_instructor,genius.quiz.instructor,model_genius_quiz,group_genius_instructor,1,1,1,1 +access_question_standard_user,genius.quiz.question.standard.user,model_genius_quiz_question,base.group_user,1,0,0,0 access_question_user,genius.quiz.question.user,model_genius_quiz_question,group_genius_user,1,0,0,0 access_question_instructor,genius.quiz.question.instructor,model_genius_quiz_question,group_genius_instructor,1,1,1,1 +access_answer_standard_user,genius.quiz.answer.standard.user,model_genius_quiz_answer,base.group_user,1,0,0,0 access_answer_user,genius.quiz.answer.user,model_genius_quiz_answer,group_genius_user,1,0,0,0 access_answer_instructor,genius.quiz.answer.instructor,model_genius_quiz_answer,group_genius_instructor,1,1,1,1 +access_attempt_standard_user,genius.quiz.attempt.standard.user,model_genius_quiz_attempt,base.group_user,1,1,1,0 access_attempt_user,genius.quiz.attempt.user,model_genius_quiz_attempt,group_genius_user,1,1,1,0 access_attempt_instructor,genius.quiz.attempt.instructor,model_genius_quiz_attempt,group_genius_instructor,1,1,1,1 +access_response_standard_user,genius.quiz.response.user,model_genius_quiz_response,base.group_user,1,1,1,0 access_response_user,genius.quiz.response.user,model_genius_quiz_response,group_genius_user,1,1,1,0 access_response_instructor,genius.quiz.response.instructor,model_genius_quiz_response,group_genius_instructor,1,1,1,1 access_leaderboard_user,genius.leaderboard.user,model_genius_leaderboard,group_genius_user,1,0,0,0 diff --git a/odex25_base/tour_genius/security/rules.xml b/odex25_base/tour_genius/security/rules.xml new file mode 100644 index 000000000..664561382 --- /dev/null +++ b/odex25_base/tour_genius/security/rules.xml @@ -0,0 +1,42 @@ + + + + + + + Genius Topic: Published Only for Standard Users + + + [('state', '=', 'published'), ('active', '=', True)] + + + + + + + + + + + Genius Topic: All for Genius Users + + + [(1, '=', 1)] + + + + + + + +