Merge pull request #5869 from expsa/14.0-feat-system_dashboard_classic-auto-20251229_114535
[FIX] system_dashboard_classic tour_genius: enhance data models and use...
This commit is contained in:
commit
68e3e9688a
|
|
@ -582,6 +582,14 @@ class DashboardConfigSettings(models.TransientModel):
|
||||||
help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds / 1 hour)'
|
help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds / 1 hour)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Work Timer Settings
|
||||||
|
dashboard_show_work_timer = fields.Boolean(
|
||||||
|
string='Show Work Timer',
|
||||||
|
default=False,
|
||||||
|
help='Display live work hour countdown showing remaining time until end of planned shift. Updates every second and shows progress bar.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|
@ -607,9 +615,11 @@ class DashboardConfigSettings(models.TransientModel):
|
||||||
dashboard_show_attendance_hours=get_bool_param('system_dashboard_classic.show_attendance_hours'),
|
dashboard_show_attendance_hours=get_bool_param('system_dashboard_classic.show_attendance_hours'),
|
||||||
dashboard_show_attendance_section=get_bool_param('system_dashboard_classic.show_attendance_section'),
|
dashboard_show_attendance_section=get_bool_param('system_dashboard_classic.show_attendance_section'),
|
||||||
dashboard_enable_attendance_button=get_bool_param('system_dashboard_classic.enable_attendance_button', 'False'),
|
dashboard_enable_attendance_button=get_bool_param('system_dashboard_classic.enable_attendance_button', 'False'),
|
||||||
|
dashboard_show_work_timer=get_bool_param('system_dashboard_classic.show_work_timer'),
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def set_values(self):
|
def set_values(self):
|
||||||
"""Override to explicitly store Boolean visibility settings as strings
|
"""Override to explicitly store Boolean visibility settings as strings
|
||||||
|
|
||||||
|
|
@ -632,9 +642,12 @@ class DashboardConfigSettings(models.TransientModel):
|
||||||
'True' if self.dashboard_show_attendance_section else 'False')
|
'True' if self.dashboard_show_attendance_section else 'False')
|
||||||
ICPSudo.set_param('system_dashboard_classic.enable_attendance_button',
|
ICPSudo.set_param('system_dashboard_classic.enable_attendance_button',
|
||||||
'True' if self.dashboard_enable_attendance_button else 'False')
|
'True' if self.dashboard_enable_attendance_button else 'False')
|
||||||
|
ICPSudo.set_param('system_dashboard_classic.show_work_timer',
|
||||||
|
'True' if self.dashboard_show_work_timer else 'False')
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_stats_visibility(self):
|
def get_stats_visibility(self):
|
||||||
"""API method to get statistics visibility settings for JavaScript
|
"""API method to get statistics visibility settings for JavaScript
|
||||||
|
|
|
||||||
|
|
@ -1021,3 +1021,77 @@ class SystemDashboard(models.Model):
|
||||||
|
|
||||||
result['approval_count'] = approval_count
|
result['approval_count'] = approval_count
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_work_timer_data(self):
|
||||||
|
"""
|
||||||
|
Returns live work timer data for countdown display
|
||||||
|
Leverages existing attendance.attendance model and logic
|
||||||
|
"""
|
||||||
|
employee = self.env.user.employee_id
|
||||||
|
|
||||||
|
if not employee:
|
||||||
|
return {'enabled': False, 'reason': 'no_employee'}
|
||||||
|
|
||||||
|
# Check if feature is enabled in settings
|
||||||
|
ICP = self.env['ir.config_parameter'].sudo()
|
||||||
|
|
||||||
|
is_enabled = ICP.get_param('system_dashboard_classic.show_work_timer', 'True') == 'True'
|
||||||
|
|
||||||
|
if not is_enabled:
|
||||||
|
return {'enabled': False, 'reason': 'disabled_in_settings'}
|
||||||
|
|
||||||
|
today = fields.Date.today()
|
||||||
|
|
||||||
|
# Get last attendance record today using same model as attendance_duration
|
||||||
|
last_att = self.env['attendance.attendance'].sudo().search([
|
||||||
|
('employee_id', '=', employee.id),
|
||||||
|
('action_date', '=', today)
|
||||||
|
], order='name DESC', limit=1)
|
||||||
|
|
||||||
|
if not last_att:
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'status': 'not_checked_in',
|
||||||
|
'message': _('لم تسجل دخول بعد')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get planned hours from resource calendar
|
||||||
|
calendar = employee.resource_calendar_id
|
||||||
|
planned_hours = 8.0 # Default fallback
|
||||||
|
|
||||||
|
if calendar:
|
||||||
|
day_of_week = today.weekday()
|
||||||
|
attendance_lines = calendar.attendance_ids.filtered(
|
||||||
|
lambda a: int(a.dayofweek) == day_of_week
|
||||||
|
)
|
||||||
|
if attendance_lines:
|
||||||
|
planned_hours = sum(line.hour_to - line.hour_from for line in attendance_lines)
|
||||||
|
|
||||||
|
# If sign_out - use attendance_duration directly (existing logic)
|
||||||
|
if last_att.action == 'sign_out':
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'status': 'checked_out',
|
||||||
|
'hours_worked': last_att.attendance_duration,
|
||||||
|
'hours_worked_formatted': last_att.attendance_duration_hhmmss,
|
||||||
|
'planned_hours': planned_hours
|
||||||
|
}
|
||||||
|
|
||||||
|
# If sign_in - return start time for client-side countdown
|
||||||
|
# Use same timezone logic as get_refresh_data
|
||||||
|
import pytz
|
||||||
|
user_tz = pytz.timezone(self.env.context.get('tz', 'Asia/Riyadh') or self.env.user.tz or 'UTC')
|
||||||
|
|
||||||
|
# Convert UTC datetime to user timezone
|
||||||
|
sign_in_utc = last_att.name
|
||||||
|
sign_in_local = pytz.utc.localize(sign_in_utc).astimezone(user_tz)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'enabled': True,
|
||||||
|
'status': 'checked_in',
|
||||||
|
'sign_in_time': sign_in_local.isoformat(), # ISO format for JS Date parsing
|
||||||
|
'planned_hours': planned_hours,
|
||||||
|
'planned_seconds': int(planned_hours * 3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
if (type === 'birthday') {
|
if (type === 'birthday') {
|
||||||
emoji = '🎂';
|
emoji = '🎂';
|
||||||
badgeClass = 'birthday-mode';
|
badgeClass = 'birthday-mode';
|
||||||
title = isRtl ? 'عيد ميلاد سعيد!' : 'Happy Birthday!';
|
title = isRtl ? 'يوم ميلاد سعيد!' : 'Happy Birthday!';
|
||||||
// Gendered pronouns: لك (male) / لكِ (female)
|
// Gendered pronouns: لك (male) / لكِ (female)
|
||||||
var genderInfo = window.dashboardGenderInfo || {pronoun_you: 'ك'};
|
var genderInfo = window.dashboardGenderInfo || {pronoun_you: 'ك'};
|
||||||
var forYou = 'ل' + genderInfo.pronoun_you; // لك or لكِ
|
var forYou = 'ل' + genderInfo.pronoun_you; // لك or لكِ
|
||||||
|
|
@ -309,7 +309,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
var greetingHtml = '<span class="greeting-text">' + greetingWithHonorific + '</span>';
|
var greetingHtml = '<span class="greeting-text">' + greetingWithHonorific + '</span>';
|
||||||
|
|
||||||
if (result.celebration && result.celebration.is_birthday) {
|
if (result.celebration && result.celebration.is_birthday) {
|
||||||
var birthdayText = isRtl ? 'عيد ميلاد سعيد!' : 'Happy Birthday!';
|
var birthdayText = isRtl ? 'يوم ميلاد سعيد!' : 'Happy Birthday!';
|
||||||
greetingHtml = '<span class="greeting-text"><span class="greeting-emoji">🎂</span> <span class="greeting-shimmer">' + birthdayText + '</span></span>';
|
greetingHtml = '<span class="greeting-text"><span class="greeting-emoji">🎂</span> <span class="greeting-shimmer">' + birthdayText + '</span></span>';
|
||||||
} else if (result.celebration && result.celebration.is_anniversary) {
|
} else if (result.celebration && result.celebration.is_anniversary) {
|
||||||
var anniversaryText = isRtl ? 'شكراً لعطائك!' : 'Thank You!';
|
var anniversaryText = isRtl ? 'شكراً لعطائك!' : 'Thank You!';
|
||||||
|
|
@ -562,7 +562,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
if ($approveContainer.length === 0 || $trackContainer.length === 0) {
|
if ($approveContainer.length === 0 || $trackContainer.length === 0) {
|
||||||
// DOM not ready - retry after 100ms
|
// DOM not ready - retry after 100ms
|
||||||
if (retryCount < maxRetries) {
|
if (retryCount < maxRetries) {
|
||||||
console.log('[Dashboard] Waiting for DOM... attempt ' + (retryCount + 1));
|
// console.log('[Dashboard] Waiting for DOM... attempt ' + (retryCount + 1));
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
buildApprovalCards(resultData, retryCount + 1);
|
buildApprovalCards(resultData, retryCount + 1);
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
@ -621,9 +621,8 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
view_type: 'list',
|
view_type: 'list',
|
||||||
views: [[list_view, 'list'], [form_view, 'form']],
|
views: [[list_view, 'list'], [form_view, 'form']],
|
||||||
domain: domain,
|
domain: domain,
|
||||||
target: 'main',
|
|
||||||
flags: { reload: true }
|
|
||||||
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update pending count badge - Sum ACTUAL pending requests from data
|
// Update pending count badge - Sum ACTUAL pending requests from data
|
||||||
|
|
@ -680,6 +679,23 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
// Initial build of approval cards (pass full result object)
|
// Initial build of approval cards (pass full result object)
|
||||||
buildApprovalCards(result);
|
buildApprovalCards(result);
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ACTIVE TAB HANDLING - Activate correct tab from context
|
||||||
|
// Used when returning from approval record view via breadcrumb
|
||||||
|
// Note: 'context' here is actually the action object
|
||||||
|
// ============================================================
|
||||||
|
var activeTab = (context && context.params && context.params.active_tab) ||
|
||||||
|
(context && context.context && context.context.active_tab);
|
||||||
|
if (activeTab === 'to_approve') {
|
||||||
|
setTimeout(function() {
|
||||||
|
$('a[href="#to_approve"]').tab('show');
|
||||||
|
}, 100);
|
||||||
|
} else if (activeTab === 'to_track') {
|
||||||
|
setTimeout(function() {
|
||||||
|
$('a[href="#to_track"]').tab('show');
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// TAB CLICK REFRESH - Refresh data when clicking approval tabs
|
// TAB CLICK REFRESH - Refresh data when clicking approval tabs
|
||||||
// Only refreshes when coming FROM Self Services TO approval tabs
|
// Only refreshes when coming FROM Self Services TO approval tabs
|
||||||
|
|
@ -705,14 +721,14 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
// Only refresh when coming from Self Services tab
|
// Only refresh when coming from Self Services tab
|
||||||
// Not when switching between To Approve ↔ To Track
|
// Not when switching between To Approve ↔ To Track
|
||||||
if (lastActiveTab === 'approval') {
|
if (lastActiveTab === 'approval') {
|
||||||
console.log('[Dashboard] Already in approval section, no refresh needed');
|
// console.log('[Dashboard] Already in approval section, no refresh needed');
|
||||||
lastActiveTab = 'approval'; // Keep it as approval
|
lastActiveTab = 'approval'; // Keep it as approval
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent double refresh
|
// Prevent double refresh
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
console.log('[Dashboard] Refresh already in progress, skipping');
|
// console.log('[Dashboard] Refresh already in progress, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
|
|
@ -789,27 +805,30 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
var model = $(this).attr('data-model');
|
var model = $(this).attr('data-model');
|
||||||
var name = $(this).attr('data-name');
|
var name = $(this).attr('data-name');
|
||||||
var domain = $(this).attr('data-domain');
|
var domain = $(this).attr('data-domain');
|
||||||
var form_view = parseInt($(this).attr('data-form-view')) || false;
|
var form_view = parseInt($(this).attr('form-view')) || false;
|
||||||
var list_view = parseInt($(this).attr('data-list-view')) || false;
|
var list_view = parseInt($(this).attr('list-view')) || false;
|
||||||
var count = parseInt($(this).attr('data-count')) || 0;
|
|
||||||
var context = $(this).attr('data-context') || '{}';
|
|
||||||
|
|
||||||
try { context = JSON.parse(context.replace(/'/g, '"')); }
|
if (domain === undefined || domain === '') {
|
||||||
catch(e) { context = {}; }
|
domain = false;
|
||||||
|
} else {
|
||||||
|
try { domain = JSON.parse(domain); }
|
||||||
|
catch(e) { domain = false; }
|
||||||
|
}
|
||||||
|
if (isNaN(form_view)) form_view = false;
|
||||||
|
if (isNaN(list_view)) list_view = false;
|
||||||
|
|
||||||
if (count > 0 && domain) {
|
if (domain) {
|
||||||
return self.do_action({
|
return self.do_action({
|
||||||
name: _t(name),
|
name: _t(name),
|
||||||
type: 'ir.actions.act_window',
|
type: 'ir.actions.act_window',
|
||||||
res_model: model,
|
res_model: model,
|
||||||
view_mode: 'tree,form',
|
view_mode: 'tree,form',
|
||||||
|
view_type: 'list',
|
||||||
views: [[list_view, 'list'], [form_view, 'form']],
|
views: [[list_view, 'list'], [form_view, 'form']],
|
||||||
context: context,
|
domain: domain,
|
||||||
domain: JSON.parse(domain.replace(/'/g, '"')),
|
|
||||||
target: 'current',
|
|
||||||
flags: { reload: true }
|
|
||||||
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
|
@ -831,7 +850,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
$('.pending-count-badge').hide();
|
$('.pending-count-badge').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Dashboard] Approval cards refreshed on tab click');
|
// console.log('[Dashboard] Approval cards refreshed on tab click');
|
||||||
|
|
||||||
// Reset flag after a short delay to allow for any animation
|
// Reset flag after a short delay to allow for any animation
|
||||||
setTimeout(function() { isRefreshing = false; }, 500);
|
setTimeout(function() { isRefreshing = false; }, 500);
|
||||||
|
|
@ -897,16 +916,16 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
|
|
||||||
function startPeriodicRefresh() {
|
function startPeriodicRefresh() {
|
||||||
if (!refreshEnabled || refreshInterval <= 0) {
|
if (!refreshEnabled || refreshInterval <= 0) {
|
||||||
console.log('[Dashboard] Periodic refresh disabled');
|
// console.log('[Dashboard] Periodic refresh disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Dashboard] Starting periodic refresh (interval: ' + (refreshInterval/1000) + 's)');
|
// console.log('[Dashboard] Starting periodic refresh (interval: ' + (refreshInterval/1000) + 's)');
|
||||||
|
|
||||||
self.periodicRefreshId = setInterval(function() {
|
self.periodicRefreshId = setInterval(function() {
|
||||||
// Skip if page is hidden (browser tab switch)
|
// Skip if page is hidden (browser tab switch)
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
console.log('[Dashboard] Skipping refresh - tab hidden');
|
// console.log('[Dashboard] Skipping refresh - tab hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -917,7 +936,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
}, []).then(function(data) {
|
}, []).then(function(data) {
|
||||||
updateAttendanceStatus(data.attendance);
|
updateAttendanceStatus(data.attendance);
|
||||||
updateApprovalCount(data.approval_count);
|
updateApprovalCount(data.approval_count);
|
||||||
console.log('[Dashboard] Periodic refresh completed - Count: ' + data.approval_count);
|
// console.log('[Dashboard] Periodic refresh completed - Count: ' + data.approval_count);
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
console.warn('[Dashboard] Refresh failed:', err);
|
console.warn('[Dashboard] Refresh failed:', err);
|
||||||
});
|
});
|
||||||
|
|
@ -927,6 +946,194 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
// Start polling
|
// Start polling
|
||||||
startPeriodicRefresh();
|
startPeriodicRefresh();
|
||||||
|
|
||||||
|
// ===== WORK TIMER WIDGET =====
|
||||||
|
// Live countdown timer for remaining work hours
|
||||||
|
var WorkTimer = {
|
||||||
|
enabled: false,
|
||||||
|
intervalId: null,
|
||||||
|
signInTime: null,
|
||||||
|
plannedSeconds: 0,
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
var widget = this;
|
||||||
|
|
||||||
|
// console.log('[WorkTimer] ===== INITIALIZATION START =====');
|
||||||
|
// console.log('[WorkTimer] self object available:', typeof self !== 'undefined');
|
||||||
|
// console.log('[WorkTimer] self._rpc available:', typeof self._rpc);
|
||||||
|
// console.log('[WorkTimer] DOM container exists:', $('.attendance-section-body').length);
|
||||||
|
|
||||||
|
self._rpc({
|
||||||
|
model: 'system_dashboard_classic.dashboard',
|
||||||
|
method: 'get_work_timer_data',
|
||||||
|
}).then(function(data) {
|
||||||
|
// console.log('[WorkTimer] RPC Response received:', data);
|
||||||
|
|
||||||
|
if (!data.enabled) {
|
||||||
|
// console.log('[WorkTimer] Feature disabled -', data.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.enabled = true;
|
||||||
|
// console.log('[WorkTimer] Feature enabled, status:', data.status);
|
||||||
|
|
||||||
|
if (data.status === 'not_checked_in') {
|
||||||
|
// console.log('[WorkTimer] Rendering: Not Checked In');
|
||||||
|
widget.renderNotCheckedIn(data.message);
|
||||||
|
} else if (data.status === 'checked_out') {
|
||||||
|
// console.log('[WorkTimer] Rendering: Checked Out');
|
||||||
|
widget.renderCheckedOut(data);
|
||||||
|
} else if (data.status === 'checked_in') {
|
||||||
|
// console.log('[WorkTimer] Rendering: Live Countdown');
|
||||||
|
// Parse ISO datetime to JavaScript Date
|
||||||
|
widget.signInTime = new Date(data.sign_in_time);
|
||||||
|
widget.plannedSeconds = data.planned_seconds;
|
||||||
|
// console.log('[WorkTimer] Sign-in time:', widget.signInTime);
|
||||||
|
// console.log('[WorkTimer] Planned seconds:', widget.plannedSeconds);
|
||||||
|
widget.startCountdown();
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('[WorkTimer] !!!!! RPC FAILED !!!!!');
|
||||||
|
console.error('[WorkTimer] Error details:', err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
startCountdown: function() {
|
||||||
|
var widget = this;
|
||||||
|
|
||||||
|
// Clear existing interval
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update immediately
|
||||||
|
this.tick();
|
||||||
|
|
||||||
|
// Update every second with page visibility check
|
||||||
|
this.intervalId = setInterval(function() {
|
||||||
|
if (!document.hidden) { // Best practice: Page Visibility API
|
||||||
|
widget.tick();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
tick: function() {
|
||||||
|
var now = new Date();
|
||||||
|
var elapsedSeconds = Math.floor((now - this.signInTime) / 1000);
|
||||||
|
var remainingSeconds = Math.max(0, this.plannedSeconds - elapsedSeconds);
|
||||||
|
|
||||||
|
var progress = Math.min(100, (elapsedSeconds / this.plannedSeconds) * 100);
|
||||||
|
var isOvertime = elapsedSeconds > this.plannedSeconds;
|
||||||
|
|
||||||
|
this.updateUI({
|
||||||
|
elapsed: this.formatTime(elapsedSeconds),
|
||||||
|
remaining: this.formatTime(remainingSeconds),
|
||||||
|
progress: progress.toFixed(1),
|
||||||
|
isOvertime: isOvertime
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime: function(seconds) {
|
||||||
|
var h = Math.floor(seconds / 3600);
|
||||||
|
var m = Math.floor((seconds % 3600) / 60);
|
||||||
|
var s = Math.floor(seconds % 60);
|
||||||
|
return h.toString().padStart(2, '0') + ':' +
|
||||||
|
m.toString().padStart(2, '0') + ':' +
|
||||||
|
s.toString().padStart(2, '0');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUI: function(data) {
|
||||||
|
var $container = $('.work-timer-compact');
|
||||||
|
|
||||||
|
if ($container.length === 0) {
|
||||||
|
this.renderCountdown(data);
|
||||||
|
} else {
|
||||||
|
$container.find('.timer-value').text(data.remaining);
|
||||||
|
$container.find('.timer-elapsed').text(($ ('body').hasClass('o_rtl') ? 'منقضي' : 'Elapsed') + ': ' + data.elapsed);
|
||||||
|
$container.find('.timer-progress-fill').css('width', data.progress + '%');
|
||||||
|
$container.toggleClass('overtime', data.isOvertime);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCountdown: function(data) {
|
||||||
|
var isRtl = $('body').hasClass('o_rtl');
|
||||||
|
|
||||||
|
// console.log('[WorkTimer] Rendering countdown widget...');
|
||||||
|
// console.log('[WorkTimer] Data:', data);
|
||||||
|
// console.log('[WorkTimer] RTL mode:', isRtl);
|
||||||
|
|
||||||
|
// Compact inline design - integrated with attendance info
|
||||||
|
var html = `
|
||||||
|
<div class="work-timer-compact ${data.isOvertime ? 'overtime' : ''}">
|
||||||
|
<div class="timer-row">
|
||||||
|
<span class="timer-icon"><i class="fa fa-hourglass-half"></i></span>
|
||||||
|
<div class="timer-content">
|
||||||
|
<div class="timer-value">${data.remaining}</div>
|
||||||
|
<div class="timer-label">${isRtl ? 'متبقي' : 'remaining'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timer-progress-bar">
|
||||||
|
<div class="timer-progress-fill" style="width: ${data.progress}%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="timer-elapsed">${isRtl ? 'منقضي' : 'Elapsed'}: ${data.elapsed}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Insert after attendance title
|
||||||
|
var $target = $('.attendance-title');
|
||||||
|
// console.log('[WorkTimer] Target (title) found:', $target.length);
|
||||||
|
|
||||||
|
if ($target.length > 0) {
|
||||||
|
$target.after(html);
|
||||||
|
// console.log('[WorkTimer] ✅ Widget HTML injected after title!');
|
||||||
|
} else {
|
||||||
|
console.error('[WorkTimer] ❌ Attendance title NOT FOUND');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderNotCheckedIn: function(message) {
|
||||||
|
var html = `
|
||||||
|
<div class="work-timer-compact inactive">
|
||||||
|
<div class="timer-info-msg">
|
||||||
|
<i class="fa fa-info-circle"></i>
|
||||||
|
<span>${message}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$('.attendance-title').after(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCheckedOut: function(data) {
|
||||||
|
var isRtl = $('body').hasClass('o_rtl');
|
||||||
|
var html = `
|
||||||
|
<div class="work-timer-compact completed">
|
||||||
|
<div class="timer-row">
|
||||||
|
<span class="timer-icon"><i class="fa fa-check-circle"></i></span>
|
||||||
|
<div class="timer-content">
|
||||||
|
<div class="timer-value">${data.hours_worked_formatted}</div>
|
||||||
|
<div class="timer-label">${isRtl ? 'إجمالي ساعات العمل' : 'Total Hours'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$('.attendance-title').after(html);
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function() {
|
||||||
|
// Best practice: Clear interval on destroy
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
$('.work-timer-compact').remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize work timer after short delay
|
||||||
|
setTimeout(function() {
|
||||||
|
WorkTimer.init();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
|
||||||
// Chart Settings
|
// Chart Settings
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window.check = false;
|
window.check = false;
|
||||||
|
|
@ -1077,6 +1284,16 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
setTimeout(function() { $notification.remove(); }, 400);
|
setTimeout(function() { $notification.remove(); }, 400);
|
||||||
}, 5500);
|
}, 5500);
|
||||||
|
|
||||||
|
// ===== REFRESH WORK TIMER AFTER CHECK-IN/OUT =====
|
||||||
|
// console.log('[WorkTimer] Attendance changed - will refresh widget...');
|
||||||
|
setTimeout(function() {
|
||||||
|
if (typeof WorkTimer !== 'undefined' && WorkTimer.destroy && WorkTimer.init) {
|
||||||
|
// console.log('[WorkTimer] Refreshing after attendance change...');
|
||||||
|
WorkTimer.destroy();
|
||||||
|
WorkTimer.init();
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
}).guardedCatch(function(error) {
|
}).guardedCatch(function(error) {
|
||||||
// Handle unexpected server errors only
|
// Handle unexpected server errors only
|
||||||
var errorMsg = isRtl ? 'حدث خطأ غير متوقع. حاول مرة أخرى' : 'An unexpected error occurred. Please try again.';
|
var errorMsg = isRtl ? 'حدث خطأ غير متوقع. حاول مرة أخرى' : 'An unexpected error occurred. Please try again.';
|
||||||
|
|
@ -1642,8 +1859,19 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
$(".o_control_panel").addClass("o_hidden");
|
$(".o_control_panel").addClass("o_hidden");
|
||||||
var self = this;
|
var self = this;
|
||||||
},
|
},
|
||||||
reload: function() {
|
reload: function(options) {
|
||||||
window.location.href = this.href;
|
// Properly reload dashboard by triggering the action again
|
||||||
|
// options.active_tab can be passed to specify which tab to open
|
||||||
|
var activeTab = (options && options.active_tab) || 'to_approve';
|
||||||
|
this.do_action({
|
||||||
|
name: _t('Self-Service'),
|
||||||
|
type: 'ir.actions.client',
|
||||||
|
tag: 'system_dashboard_classic.dashboard_self_services',
|
||||||
|
target: 'main',
|
||||||
|
params: {
|
||||||
|
active_tab: activeTab
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Cleanup on widget destruction - prevents memory leak
|
* Cleanup on widget destruction - prevents memory leak
|
||||||
|
|
@ -1659,7 +1887,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
||||||
if (this.periodicRefreshId) {
|
if (this.periodicRefreshId) {
|
||||||
clearInterval(this.periodicRefreshId);
|
clearInterval(this.periodicRefreshId);
|
||||||
this.periodicRefreshId = null;
|
this.periodicRefreshId = null;
|
||||||
console.log('[Dashboard] Periodic refresh stopped');
|
// console.log('[Dashboard] Periodic refresh stopped');
|
||||||
}
|
}
|
||||||
// Call parent destroy
|
// Call parent destroy
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
|
|
|
||||||
|
|
@ -724,3 +724,321 @@ $border-color_1: #2eac96;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ====================================
|
||||||
|
WORK TIMER WIDGET
|
||||||
|
Compact, professional design using dashboard colors
|
||||||
|
==================================== */
|
||||||
|
|
||||||
|
.work-timer-widget {
|
||||||
|
// Use dashboard primary color with intelligent gradient
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-primary, #667eea) 0%,
|
||||||
|
var(--dash-primary-dark, #5a67d8) 100%
|
||||||
|
);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.overtime {
|
||||||
|
// Use warning color for overtime
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-warning, #f59e0b) 0%,
|
||||||
|
#dc2626 100%
|
||||||
|
);
|
||||||
|
animation: pulse-timer-warning 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive, &.completed {
|
||||||
|
// Use secondary color for inactive states
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-secondary, #64748b) 0%,
|
||||||
|
var(--dash-secondary-dark, #475569) 100%
|
||||||
|
);
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.95;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-timer-warning {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 2px 6px rgba(245, 158, 11, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 2px 12px rgba(245, 158, 11, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-main {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.timer-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-label {
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.85;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-progress-bar {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 2px 0;
|
||||||
|
|
||||||
|
.timer-progress-fill {
|
||||||
|
// Use success color for progress
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--dash-success, #10b981),
|
||||||
|
var(--dash-success-light, #34d399)
|
||||||
|
);
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-widget.overtime .timer-progress-fill {
|
||||||
|
// Use warning gradient for overtime
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--dash-warning, #fbbf24),
|
||||||
|
#f97316
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-info {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-summary {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label {
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.9;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL Support */
|
||||||
|
body.o_rtl {
|
||||||
|
.timer-header {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.work-timer-widget {
|
||||||
|
padding: 8px 10px;
|
||||||
|
|
||||||
|
.timer-value {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ====================================
|
||||||
|
WORK TIMER - COMPACT INLINE DESIGN
|
||||||
|
Integrated genius layout within attendance section
|
||||||
|
==================================== */
|
||||||
|
|
||||||
|
.work-timer-compact {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-primary, #667eea) 0%,
|
||||||
|
var(--dash-primary-dark, #5a67d8) 100%
|
||||||
|
);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin: 8px 0 12px 0;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&.overtime {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-warning, #f59e0b) 0%,
|
||||||
|
#dc2626 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-success, #10b981) 0%,
|
||||||
|
var(--dash-success-dark, #059669) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
var(--dash-secondary, #64748b) 0%,
|
||||||
|
var(--dash-secondary-dark, #475569) 100%
|
||||||
|
);
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.timer-info-msg {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact .timer-value {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact .timer-label {
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.85;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact .timer-progress-bar {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact .timer-progress-fill {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--dash-success, #10b981),
|
||||||
|
var(--dash-success-light, #34d399)
|
||||||
|
);
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact.overtime .timer-progress-fill {
|
||||||
|
background: linear-gradient(90deg,
|
||||||
|
var(--dash-warning, #fbbf24),
|
||||||
|
#f97316
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-timer-compact .timer-elapsed {
|
||||||
|
font-size: 9px;
|
||||||
|
opacity: 0.85;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* RTL Support */
|
||||||
|
body.o_rtl {
|
||||||
|
.timer-row {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,23 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Work Timer Settings -->
|
||||||
|
<h2 class="mt32">Work Timer Settings</h2>
|
||||||
|
<div class="row mt16 o_settings_container">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<div class="o_setting_left_pane">
|
||||||
|
<field name="dashboard_show_work_timer" widget="boolean_toggle"/>
|
||||||
|
</div>
|
||||||
|
<div class="o_setting_right_pane">
|
||||||
|
<label for="dashboard_show_work_timer" string="Show Work Timer"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Display live work hour countdown timer showing remaining time until end of shift.
|
||||||
|
<br/><em>Updates every second with real-time progress bar.</em>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
var tourId = $(ev.currentTarget).data('tour-id');
|
var tourId = $(ev.currentTarget).data('tour-id');
|
||||||
var tourName = 'genius_tour_' + tourId;
|
var tourName = 'genius_tour_' + tourId;
|
||||||
|
|
||||||
console.log('[Tour Genius] Running tour:', tourName);
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PHASE 1: Complete cleanup of ALL tour state
|
// PHASE 1: Complete cleanup of ALL tour state
|
||||||
|
|
@ -140,7 +139,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Tour Genius] Error cleaning tooltip:', e);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
tour.active_tooltips = {};
|
tour.active_tooltips = {};
|
||||||
|
|
@ -183,7 +181,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Tour Genius] Cleanup complete, removed', keysToRemove.length, 'localStorage items');
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PHASE 2: Activate the requested tour with forced reload
|
// PHASE 2: Activate the requested tour with forced reload
|
||||||
|
|
@ -230,7 +227,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
activateTour();
|
activateTour();
|
||||||
} else {
|
} else {
|
||||||
// Tour not found - try reloading tour definitions
|
// Tour not found - try reloading tour definitions
|
||||||
console.log('[Tour Genius] Tour not found, reloading definitions...');
|
|
||||||
|
|
||||||
var tourLoader = require('tour_genius.tour_loader');
|
var tourLoader = require('tour_genius.tour_loader');
|
||||||
if (tourLoader && tourLoader.registerAllTours) {
|
if (tourLoader && tourLoader.registerAllTours) {
|
||||||
|
|
@ -393,7 +389,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
// Find tour data
|
// Find tour data
|
||||||
var tour = this.tours.find(function(t) { return t.id === tourId; });
|
var tour = this.tours.find(function(t) { return t.id === tourId; });
|
||||||
if (!tour || !tour.quiz_id) {
|
if (!tour || !tour.quiz_id) {
|
||||||
console.warn('[Tour Genius] No quiz found for tour:', tourId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -409,7 +404,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
});
|
});
|
||||||
|
|
||||||
popup.appendTo(document.body).then(function() {
|
popup.appendTo(document.body).then(function() {
|
||||||
console.log('[Tour Genius] Quiz popup opened for tour:', tour.name);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -426,7 +420,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
// Find tour data
|
// Find tour data
|
||||||
var tour = this.tours.find(function(t) { return t.id === tourId; });
|
var tour = this.tours.find(function(t) { return t.id === tourId; });
|
||||||
if (!tour || !tour.quiz_id) {
|
if (!tour || !tour.quiz_id) {
|
||||||
console.warn('[Tour Genius] No quiz found for tour:', tourId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -442,7 +435,6 @@ odoo.define('tour_genius.Dashboard', function (require) {
|
||||||
});
|
});
|
||||||
|
|
||||||
popup.appendTo(document.body).then(function() {
|
popup.appendTo(document.body).then(function() {
|
||||||
console.log('[Tour Genius] Certificate view opened for tour:', tour.name);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,6 @@ odoo.define('tour_genius.GeniusCelebration', function (require) {
|
||||||
// Open PDF in new tab
|
// Open PDF in new tab
|
||||||
window.open('/tour_genius/certificate/view/' + data.attempt_id, '_blank');
|
window.open('/tour_genius/certificate/view/' + data.attempt_id, '_blank');
|
||||||
} else {
|
} else {
|
||||||
console.warn('[GeniusCelebration] No certificate found');
|
|
||||||
}
|
}
|
||||||
// Close the celebration
|
// Close the celebration
|
||||||
self.destroy();
|
self.destroy();
|
||||||
|
|
|
||||||
|
|
@ -81,18 +81,13 @@ odoo.define('tour_genius.GeniusQuizPopup', function (require) {
|
||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
console.log('[GeniusQuizPopup] Start called. Template:', this.template);
|
|
||||||
console.log('[GeniusQuizPopup] $el:', this.$el);
|
|
||||||
|
|
||||||
return this._super.apply(this, arguments).then(function () {
|
return this._super.apply(this, arguments).then(function () {
|
||||||
console.log('🚀 ANTIGRAVITY: GeniusQuizPopup Loaded v2 - Ghost Code Exorcised');
|
|
||||||
if (self.showCertificate) {
|
if (self.showCertificate) {
|
||||||
// console.log('[GeniusQuizPopup] Showing certificate');
|
|
||||||
// self._renderCertificate(); // REMOVED: Ghost code causing legacy UI
|
// self._renderCertificate(); // REMOVED: Ghost code causing legacy UI
|
||||||
// Fallback to results if certificate requested but method missing
|
// Fallback to results if certificate requested but method missing
|
||||||
self._renderResults();
|
self._renderResults();
|
||||||
} else {
|
} else {
|
||||||
console.log('[GeniusQuizPopup] Rendering Question');
|
|
||||||
self._renderQuestion();
|
self._renderQuestion();
|
||||||
self._startTimer();
|
self._startTimer();
|
||||||
}
|
}
|
||||||
|
|
@ -835,7 +830,6 @@ odoo.define('tour_genius.GeniusQuizPopup', function (require) {
|
||||||
var quizId = recordData.id;
|
var quizId = recordData.id;
|
||||||
var quizName = recordData.name;
|
var quizName = recordData.name;
|
||||||
|
|
||||||
console.log('[GeniusButton] Starting Test Mode for Quiz:', quizId);
|
|
||||||
|
|
||||||
// Instantiate Popup directly
|
// Instantiate Popup directly
|
||||||
var popup = new GeniusQuizPopup(self, {
|
var popup = new GeniusQuizPopup(self, {
|
||||||
|
|
@ -845,7 +839,6 @@ odoo.define('tour_genius.GeniusQuizPopup', function (require) {
|
||||||
});
|
});
|
||||||
|
|
||||||
popup.appendTo(document.body).then(function() {
|
popup.appendTo(document.body).then(function() {
|
||||||
console.log('[GeniusButton] Popup opened successfully');
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,6 @@ var GeniusTip = Tip.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hand remains visible - point stored for cleanup by next step
|
// Hand remains visible - point stored for cleanup by next step
|
||||||
console.log('[GeniusTip] UI hidden, hand remains. Waiting for user action on target.');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_onSkipTour: function (ev) {
|
_onSkipTour: function (ev) {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
this.dragOffsetX = 0;
|
this.dragOffsetX = 0;
|
||||||
this.dragOffsetY = 0;
|
this.dragOffsetY = 0;
|
||||||
|
|
||||||
console.log('[Recorder] Init - isNewTour:', this.isNewTour, 'topicId:', this.topicId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
|
|
@ -72,7 +71,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
self._loadModulesDropdown();
|
self._loadModulesDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Recorder] Started - isNewTour:', self.isNewTour);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -115,10 +113,8 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
var selectedText = $(this).val();
|
var selectedText = $(this).val();
|
||||||
var moduleId = self._moduleMap[selectedText] || '';
|
var moduleId = self._moduleMap[selectedText] || '';
|
||||||
$hiddenSelect.val(moduleId);
|
$hiddenSelect.val(moduleId);
|
||||||
console.log('[Recorder] Module input:', selectedText, '-> ID:', moduleId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Recorder] Loaded', modules.length, 'modules for search');
|
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
console.error('[Recorder] Module load error:', err);
|
console.error('[Recorder] Module load error:', err);
|
||||||
});
|
});
|
||||||
|
|
@ -190,7 +186,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
},
|
},
|
||||||
|
|
||||||
_startTracking: function () {
|
_startTracking: function () {
|
||||||
console.log('[Recorder] Start Tracking');
|
|
||||||
this.isTracking = true;
|
this.isTracking = true;
|
||||||
this.$('.btn-track-on').addClass('active').html('<i class="fa fa-stop"></i> Stop Tracking');
|
this.$('.btn-track-on').addClass('active').html('<i class="fa fa-stop"></i> Stop Tracking');
|
||||||
|
|
||||||
|
|
@ -213,7 +208,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
},
|
},
|
||||||
|
|
||||||
_stopTracking: function () {
|
_stopTracking: function () {
|
||||||
console.log('[Recorder] Stop Tracking');
|
|
||||||
this.isTracking = false;
|
this.isTracking = false;
|
||||||
this.$('.btn-track-on').removeClass('active').html('<i class="fa fa-crosshairs"></i> Track On');
|
this.$('.btn-track-on').removeClass('active').html('<i class="fa fa-crosshairs"></i> Track On');
|
||||||
|
|
||||||
|
|
@ -265,7 +259,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Recorder] Element Clicked:', target);
|
|
||||||
|
|
||||||
// Stop event!
|
// Stop event!
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
@ -273,7 +266,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
|
|
||||||
// Generate CSS selector
|
// Generate CSS selector
|
||||||
var selector = this._generateCSSSelector(target);
|
var selector = this._generateCSSSelector(target);
|
||||||
console.log('[Recorder] Generated Selector:', selector);
|
|
||||||
|
|
||||||
// Update current step
|
// Update current step
|
||||||
this.currentStep.css_selector = selector;
|
this.currentStep.css_selector = selector;
|
||||||
|
|
@ -545,7 +537,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
|
|
||||||
_onSaveStep: function () {
|
_onSaveStep: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
console.log('[Recorder] Save Step Clicked');
|
|
||||||
|
|
||||||
// Validate via UI values, NOT stored values (to allow manual edits)
|
// Validate via UI values, NOT stored values (to allow manual edits)
|
||||||
var selector = this.$('.css-selector-input').val() || '';
|
var selector = this.$('.css-selector-input').val() || '';
|
||||||
|
|
@ -581,8 +572,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
var moduleId = this.$('.tour-module-select').val();
|
var moduleId = this.$('.tour-module-select').val();
|
||||||
|
|
||||||
// DEBUG: Log module selection details
|
// DEBUG: Log module selection details
|
||||||
console.log('[Recorder] Tour Name:', tourName);
|
|
||||||
console.log('[Recorder] Module ID retrieved:', moduleId);
|
|
||||||
|
|
||||||
// Validate Tour Name
|
// Validate Tour Name
|
||||||
if (!tourName || tourName.trim() === '') {
|
if (!tourName || tourName.trim() === '') {
|
||||||
|
|
@ -600,12 +589,9 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
// Only add module_id if it's a valid number
|
// Only add module_id if it's a valid number
|
||||||
if (moduleId && moduleId !== '' && !isNaN(parseInt(moduleId))) {
|
if (moduleId && moduleId !== '' && !isNaN(parseInt(moduleId))) {
|
||||||
tourVals.module_id = parseInt(moduleId);
|
tourVals.module_id = parseInt(moduleId);
|
||||||
console.log('[Recorder] Module ID added to vals:', tourVals.module_id);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('[Recorder] No valid module_id to add');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Recorder] Creating Tour with vals:', JSON.stringify(tourVals));
|
|
||||||
|
|
||||||
createTourPromise = ajax.jsonRpc('/web/dataset/call_kw/genius.topic/create', 'call', {
|
createTourPromise = ajax.jsonRpc('/web/dataset/call_kw/genius.topic/create', 'call', {
|
||||||
model: 'genius.topic',
|
model: 'genius.topic',
|
||||||
|
|
@ -613,7 +599,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
args: [tourVals],
|
args: [tourVals],
|
||||||
kwargs: { context: session.user_context }
|
kwargs: { context: session.user_context }
|
||||||
}).then(function (newTopicId) {
|
}).then(function (newTopicId) {
|
||||||
console.log('[Recorder] Tour Created:', newTopicId);
|
|
||||||
self.topicId = newTopicId;
|
self.topicId = newTopicId;
|
||||||
self.topicName = tourName.trim();
|
self.topicName = tourName.trim();
|
||||||
self.isNewTour = false; // No longer new
|
self.isNewTour = false; // No longer new
|
||||||
|
|
@ -644,7 +629,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
sequence: (self.steps.length + 1) * 10,
|
sequence: (self.steps.length + 1) * 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[Recorder] Saving Step:', stepData);
|
|
||||||
|
|
||||||
return ajax.jsonRpc('/web/dataset/call_kw/genius.topic.step/create', 'call', {
|
return ajax.jsonRpc('/web/dataset/call_kw/genius.topic.step/create', 'call', {
|
||||||
model: 'genius.topic.step',
|
model: 'genius.topic.step',
|
||||||
|
|
@ -652,7 +636,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
args: [stepData],
|
args: [stepData],
|
||||||
kwargs: { context: session.user_context }
|
kwargs: { context: session.user_context }
|
||||||
}).then(function (stepId) {
|
}).then(function (stepId) {
|
||||||
console.log('[Recorder] Step Saved ID:', stepId);
|
|
||||||
|
|
||||||
// Update local steps array
|
// Update local steps array
|
||||||
stepData.id = stepId;
|
stepData.id = stepId;
|
||||||
|
|
@ -691,7 +674,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Recorder] Captured starting_url:', relativeUrl);
|
|
||||||
|
|
||||||
var modelName = '';
|
var modelName = '';
|
||||||
|
|
||||||
|
|
@ -730,7 +712,6 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) { console.warn('[Recorder] Context detection failed', e); }
|
|
||||||
|
|
||||||
var updateVals = { 'starting_url': relativeUrl };
|
var updateVals = { 'starting_url': relativeUrl };
|
||||||
if (modelName) {
|
if (modelName) {
|
||||||
|
|
@ -745,8 +726,8 @@ odoo.define('tour_genius.RecorderPanel', function (require) {
|
||||||
args: [[parseInt(self.topicId)], updateVals],
|
args: [[parseInt(self.topicId)], updateVals],
|
||||||
kwargs: { context: session.user_context }
|
kwargs: { context: session.user_context }
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
console.log('[Recorder] Topic updated with starting_url:', relativeUrl);
|
|
||||||
});
|
});
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
self._updateStepCounter();
|
self._updateStepCounter();
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ odoo.define('tour_genius.smart_systray', function (require) {
|
||||||
self.badge_count = result.new_count || 0;
|
self.badge_count = result.new_count || 0;
|
||||||
self._updateBadge();
|
self._updateBadge();
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
// console.warn('[Tour Genius] Failed to fetch contextual training', err);
|
|
||||||
self.tours = [];
|
self.tours = [];
|
||||||
self.badge_count = 0;
|
self.badge_count = 0;
|
||||||
self._updateBadge();
|
self._updateBadge();
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ odoo.define('tour_genius.client_action', function (require) {
|
||||||
return Promise.reject('No tour_name provided');
|
return Promise.reject('No tour_name provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Tour Genius] Running tour via client action:', tourName);
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// COMPREHENSIVE CLEANUP
|
// COMPREHENSIVE CLEANUP
|
||||||
|
|
@ -85,7 +84,6 @@ odoo.define('tour_genius.client_action', function (require) {
|
||||||
|
|
||||||
if (testMode && tourData && tourData.steps) {
|
if (testMode && tourData && tourData.steps) {
|
||||||
// Dynamic registration for test mode (draft tours)
|
// Dynamic registration for test mode (draft tours)
|
||||||
console.log('[Tour Genius] Test Mode: Storing tour data for post-redirect registration:', tourName);
|
|
||||||
|
|
||||||
// Store tour data in localStorage for post-redirect registration
|
// Store tour data in localStorage for post-redirect registration
|
||||||
// This is needed because draft tours aren't in get_tours_for_registration()
|
// This is needed because draft tours aren't in get_tours_for_registration()
|
||||||
|
|
@ -102,7 +100,6 @@ odoo.define('tour_genius.client_action', function (require) {
|
||||||
wait_for: Promise.resolve(),
|
wait_for: Promise.resolve(),
|
||||||
}, tourData.steps);
|
}, tourData.steps);
|
||||||
|
|
||||||
console.log('[Tour Genius] Tour registered dynamically:', tourName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetTour = tour.tours && tour.tours[tourName];
|
var targetTour = tour.tours && tour.tours[tourName];
|
||||||
|
|
@ -130,7 +127,6 @@ odoo.define('tour_genius.client_action', function (require) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
console.error('[Tour Genius] Tour not found or has no steps:', tourName);
|
console.error('[Tour Genius] Tour not found or has no steps:', tourName);
|
||||||
console.log('[Tour Genius] Available tours:', Object.keys(tour.tours || {}));
|
|
||||||
return Promise.reject('Tour not found: ' + tourName);
|
return Promise.reject('Tour not found: ' + tourName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
var geniusToursData = {};
|
var geniusToursData = {};
|
||||||
|
|
||||||
function registerAllTours() {
|
function registerAllTours() {
|
||||||
console.log('[Tour Genius] Starting tour registration...');
|
|
||||||
|
|
||||||
// GENIUS FIX: Pre-emptive check for pending genius tours to prevent overlap
|
// GENIUS FIX: Pre-emptive check for pending genius tours to prevent overlap
|
||||||
// This prevents standard tours from flashing while we fetch our tours
|
// This prevents standard tours from flashing while we fetch our tours
|
||||||
|
|
@ -52,13 +51,11 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGeniusPending) {
|
if (isGeniusPending) {
|
||||||
console.log('[Tour Genius] Pending genius tour detected. Aggressively clearing others.');
|
|
||||||
deactivateNonGeniusTours();
|
deactivateNonGeniusTours();
|
||||||
// Repeat deactivation every 100ms until RPC returns to catch any stragglers
|
// Repeat deactivation every 100ms until RPC returns to catch any stragglers
|
||||||
antiFlashInterval = setInterval(deactivateNonGeniusTours, 100);
|
antiFlashInterval = setInterval(deactivateNonGeniusTours, 100);
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.warn('[Tour Genius] Pre-emptive check failed:', e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc.query({
|
rpc.query({
|
||||||
|
|
@ -66,7 +63,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
method: 'get_tours_for_registration',
|
method: 'get_tours_for_registration',
|
||||||
args: [],
|
args: [],
|
||||||
}).catch(function(err) {
|
}).catch(function(err) {
|
||||||
console.warn('[Tour Genius] Failed to fetch tours (likely public page/access denied):', err);
|
|
||||||
return []; // Return empty array to allow chain to continue (e.g. for test tours)
|
return []; // Return empty array to allow chain to continue (e.g. for test tours)
|
||||||
}).then(function(topics) {
|
}).then(function(topics) {
|
||||||
// Stop the aggressive clearing once we have data
|
// Stop the aggressive clearing once we have data
|
||||||
|
|
@ -84,17 +80,13 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
var storedData = window.localStorage.getItem(key);
|
var storedData = window.localStorage.getItem(key);
|
||||||
if (storedData) {
|
if (storedData) {
|
||||||
var testTourData = JSON.parse(storedData);
|
var testTourData = JSON.parse(storedData);
|
||||||
console.log('[Tour Genius] Found stored test tour data:', tourName);
|
|
||||||
|
|
||||||
// Register the test tour dynamically
|
// Register the test tour dynamically
|
||||||
if (testTourData.steps && testTourData.steps.length > 0) {
|
if (testTourData.steps && testTourData.steps.length > 0) {
|
||||||
console.log('[Tour Genius] Registering test tour with steps:', testTourData.steps);
|
|
||||||
console.log('[Tour Genius] Step 0 Content CHECK:', testTourData.steps[0].content);
|
|
||||||
tour.register(tourName, {
|
tour.register(tourName, {
|
||||||
url: testTourData.url || '/web',
|
url: testTourData.url || '/web',
|
||||||
wait_for: Promise.resolve(),
|
wait_for: Promise.resolve(),
|
||||||
}, testTourData.steps);
|
}, testTourData.steps);
|
||||||
console.log('[Tour Genius] Registered test tour from localStorage:', tourName);
|
|
||||||
|
|
||||||
// CRITICAL: Add to registeredTours so it goes through activation!
|
// CRITICAL: Add to registeredTours so it goes through activation!
|
||||||
registeredTours.push(tourName);
|
registeredTours.push(tourName);
|
||||||
|
|
@ -108,15 +100,7 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Tour Genius] Error processing stored test tour data:', e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!topics || !topics.length) {
|
|
||||||
console.log('[Tour Genius] No published tours to register (test tours may still work)');
|
|
||||||
} else {
|
|
||||||
console.log('[Tour Genius] Registering', topics.length, 'tours');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safely iterate over topics (may be empty array or null)
|
// Safely iterate over topics (may be empty array or null)
|
||||||
(topics || []).forEach(function(topic) {
|
(topics || []).forEach(function(topic) {
|
||||||
var tourName = 'genius_tour_' + topic.id;
|
var tourName = 'genius_tour_' + topic.id;
|
||||||
|
|
@ -126,7 +110,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
|
|
||||||
// Skip if already registered (e.g. by test mode loader above)
|
// Skip if already registered (e.g. by test mode loader above)
|
||||||
if (tour.tours && tour.tours[tourName]) {
|
if (tour.tours && tour.tours[tourName]) {
|
||||||
console.log('[Tour Genius] Tour already registered (likely test mode):', tourName);
|
|
||||||
// Only add to registeredTours if NOT already there to prevent double activation
|
// Only add to registeredTours if NOT already there to prevent double activation
|
||||||
if (registeredTours.indexOf(tourName) === -1) {
|
if (registeredTours.indexOf(tourName) === -1) {
|
||||||
registeredTours.push(tourName);
|
registeredTours.push(tourName);
|
||||||
|
|
@ -136,7 +119,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
|
|
||||||
// Skip if no steps
|
// Skip if no steps
|
||||||
if (!topic.steps || !topic.steps.length) {
|
if (!topic.steps || !topic.steps.length) {
|
||||||
console.log('[Tour Genius] Skipping tour with no steps:', tourName);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +134,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
// Check if showAppsMenuItem is not already the first step (avoid dupes)
|
// Check if showAppsMenuItem is not already the first step (avoid dupes)
|
||||||
var firstStepIsApps = firstStep && firstStep.content === "Home Menu"; // Odoo standard name
|
var firstStepIsApps = firstStep && firstStep.content === "Home Menu"; // Odoo standard name
|
||||||
if (!firstStepIsApps) {
|
if (!firstStepIsApps) {
|
||||||
console.log('[Tour Genius] Injecting showAppsMenuItem for tour:', tourName);
|
|
||||||
stepsToRegister.unshift(tour.stepUtils.showAppsMenuItem());
|
stepsToRegister.unshift(tour.stepUtils.showAppsMenuItem());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +149,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
|
|
||||||
registeredTours.push(tourName);
|
registeredTours.push(tourName);
|
||||||
|
|
||||||
console.log('[Tour Genius] Registered tour:', tourName, 'with', topic.steps.length, 'steps');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// CRITICAL FIX: For tours with debugging flags, we need to properly call _register()
|
// CRITICAL FIX: For tours with debugging flags, we need to properly call _register()
|
||||||
|
|
@ -188,11 +168,9 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
var debuggingKey = 'debugging_tour_' + tourName;
|
var debuggingKey = 'debugging_tour_' + tourName;
|
||||||
var isDebugging = localStorage.getItem(debuggingKey);
|
var isDebugging = localStorage.getItem(debuggingKey);
|
||||||
|
|
||||||
console.log('[Tour Genius] Checking activation for:', tourName, 'debugging:', !!isDebugging);
|
|
||||||
|
|
||||||
if (isDebugging) {
|
if (isDebugging) {
|
||||||
// Tour was started via reset() - call _register to properly initialize it
|
// Tour was started via reset() - call _register to properly initialize it
|
||||||
console.log('[Tour Genius] Activating tour from debugging flag:', tourName);
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// DEEP CLEANUP: Force Fresh Start
|
// DEEP CLEANUP: Force Fresh Start
|
||||||
|
|
@ -209,7 +187,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
consumed.splice(idx, 1);
|
consumed.splice(idx, 1);
|
||||||
window.localStorage.setItem(consumedKey, JSON.stringify(consumed));
|
window.localStorage.setItem(consumedKey, JSON.stringify(consumed));
|
||||||
console.log('[Tour Genius] Removed from localStorage consumed list:', tourName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|
@ -218,7 +195,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
var idx = tour.consumed_tours.indexOf(tourName);
|
var idx = tour.consumed_tours.indexOf(tourName);
|
||||||
if (idx > -1) {
|
if (idx > -1) {
|
||||||
tour.consumed_tours.splice(idx, 1);
|
tour.consumed_tours.splice(idx, 1);
|
||||||
console.log('[Tour Genius] Removed from memory consumed list:', tourName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +205,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
var stepKey = 'tour_' + tourName + '_current_step';
|
var stepKey = 'tour_' + tourName + '_current_step';
|
||||||
if (window.localStorage.getItem(stepKey) !== null) {
|
if (window.localStorage.getItem(stepKey) !== null) {
|
||||||
window.localStorage.removeItem(stepKey);
|
window.localStorage.removeItem(stepKey);
|
||||||
console.log('[Tour Genius] Wiped step persistence key:', stepKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also clean any other patterns just in case
|
// Also clean any other patterns just in case
|
||||||
|
|
@ -244,14 +219,12 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
if (key === tourName ||
|
if (key === tourName ||
|
||||||
key.indexOf(tourName) >= 0 && (key.indexOf('step') >= 0 || key.indexOf('current') >= 0)) {
|
key.indexOf(tourName) >= 0 && (key.indexOf('step') >= 0 || key.indexOf('current') >= 0)) {
|
||||||
window.localStorage.removeItem(key);
|
window.localStorage.removeItem(key);
|
||||||
console.log('[Tour Genius] Wiped additional persistence key:', key);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Destroy Zombie Tooltips
|
// 2. Destroy Zombie Tooltips
|
||||||
// If a tooltip exists from a previous page load, it might be detached. Kill it.
|
// If a tooltip exists from a previous page load, it might be detached. Kill it.
|
||||||
if (tour.active_tooltips[tourName]) {
|
if (tour.active_tooltips[tourName]) {
|
||||||
console.log('[Tour Genius] Destroying zombie tooltip before activation');
|
|
||||||
try {
|
try {
|
||||||
if (tour.active_tooltips[tourName].destroy)
|
if (tour.active_tooltips[tourName].destroy)
|
||||||
tour.active_tooltips[tourName].destroy();
|
tour.active_tooltips[tourName].destroy();
|
||||||
|
|
@ -271,7 +244,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
// Call _register with do_update=true
|
// Call _register with do_update=true
|
||||||
// This will check the debugging flag and call _to_next_step
|
// This will check the debugging flag and call _to_next_step
|
||||||
tour._register(true, registeredTour, tourName).then(function() {
|
tour._register(true, registeredTour, tourName).then(function() {
|
||||||
console.log('[Tour Genius] Tour registered promise resolved:', tourName);
|
|
||||||
|
|
||||||
// CRITICAL: Deactivate ALL non-genius tours
|
// CRITICAL: Deactivate ALL non-genius tours
|
||||||
deactivateNonGeniusTours();
|
deactivateNonGeniusTours();
|
||||||
|
|
@ -287,31 +259,25 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
method: 'action_track_start',
|
method: 'action_track_start',
|
||||||
args: [[tourId]],
|
args: [[tourId]],
|
||||||
}).catch(function(e) {
|
}).catch(function(e) {
|
||||||
console.warn('[Tour Genius] Failed to track start time:', e);
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('[Tour Genius] Test mode - skipping progress tracking start');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE ACTIVATION CHECK
|
// FORCE ACTIVATION CHECK
|
||||||
// Sometimes Odoo's _register doesn't trigger _to_next_step if timing is off
|
// Sometimes Odoo's _register doesn't trigger _to_next_step if timing is off
|
||||||
if (!tour.active_tooltips[tourName]) {
|
if (!tour.active_tooltips[tourName]) {
|
||||||
console.warn('[Tour Genius] Tooltip missing after register. Forcing activation of Step 0.');
|
|
||||||
|
|
||||||
if (tour.tours[tourName]) {
|
if (tour.tours[tourName]) {
|
||||||
tour.tours[tourName].current_step = 0;
|
tour.tours[tourName].current_step = 0;
|
||||||
// Manually trigger internal method to stage the first tooltip
|
// Manually trigger internal method to stage the first tooltip
|
||||||
if (tour._to_next_step) {
|
if (tour._to_next_step) {
|
||||||
tour._to_next_step(tourName, 0);
|
tour._to_next_step(tourName, 0);
|
||||||
console.log('[Tour Genius] Forced _to_next_step(0) success.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// After registration, call update to display the tooltip
|
// After registration, call update to display the tooltip
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
console.log('[Tour Genius] Calling tour.update() for:', tourName);
|
|
||||||
console.log('[Tour Genius] Active Tooltip State:', tour.active_tooltips[tourName]);
|
|
||||||
tour.update(tourName);
|
tour.update(tourName);
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
@ -319,7 +285,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
// For non-debugging tours, do nothing - they don't need activation
|
// For non-debugging tours, do nothing - they don't need activation
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Tour Genius] All tours registered and activation checks initiated');
|
|
||||||
|
|
||||||
// CRITICAL: Override Odoo's _consume_tour to show our GeniusCelebration
|
// CRITICAL: Override Odoo's _consume_tour to show our GeniusCelebration
|
||||||
patchConsumeTour(registeredTours);
|
patchConsumeTour(registeredTours);
|
||||||
|
|
@ -366,7 +331,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
isTestMode = window.localStorage.getItem('genius_test_mode_' + tour_name) === 'true';
|
isTestMode = window.localStorage.getItem('genius_test_mode_' + tour_name) === 'true';
|
||||||
|
|
||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
console.log('[Tour Genius] Test mode - performing manual cleanup to skip Odoo effects');
|
|
||||||
|
|
||||||
// 1. UNCONDITIONAL DELETE: This is critical.
|
// 1. UNCONDITIONAL DELETE: This is critical.
|
||||||
// _to_next_step sets the value to undefined but keeps the key.
|
// _to_next_step sets the value to undefined but keeps the key.
|
||||||
|
|
@ -420,7 +384,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
// Show quiz button if quiz has a valid ID
|
// Show quiz button if quiz has a valid ID
|
||||||
var hasQuiz = geniusQuiz && geniusQuiz.id && geniusQuiz.id !== false;
|
var hasQuiz = geniusQuiz && geniusQuiz.id && geniusQuiz.id !== false;
|
||||||
|
|
||||||
console.log('[Tour Genius] Completed tour:', tourId, 'Has Quiz:', hasQuiz);
|
|
||||||
|
|
||||||
// Mark as consumed
|
// Mark as consumed
|
||||||
rpc.query({
|
rpc.query({
|
||||||
|
|
@ -453,7 +416,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
};
|
};
|
||||||
|
|
||||||
tour._consume_tour_patched = true;
|
tour._consume_tour_patched = true;
|
||||||
console.log('[Tour Genius] Patched _consume_tour for GeniusCelebration');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -470,7 +432,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
try {
|
try {
|
||||||
GeniusTip = require('tour_genius.GeniusTip');
|
GeniusTip = require('tour_genius.GeniusTip');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Tour Genius] GeniusTip not available, using standard tooltips');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tour._activate_tip = function(tip, tour_name, $anchor, $alt_trigger) {
|
tour._activate_tip = function(tip, tour_name, $anchor, $alt_trigger) {
|
||||||
|
|
@ -519,7 +480,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
|
|
||||||
// Handle genius_skip_tour event
|
// Handle genius_skip_tour event
|
||||||
tip.widget.on('genius_skip_tour', this, function(data) {
|
tip.widget.on('genius_skip_tour', this, function(data) {
|
||||||
console.log('[Tour Genius] Skip tour triggered:', data.tourName);
|
|
||||||
this._deactivate_tip(tip);
|
this._deactivate_tip(tip);
|
||||||
this._consume_tour(data.tourName);
|
this._consume_tour(data.tourName);
|
||||||
});
|
});
|
||||||
|
|
@ -529,11 +489,9 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
this._to_next_running_step.bind(this, tip, tour_name)
|
this._to_next_running_step.bind(this, tip, tour_name)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('[Tour Genius] Activated GeniusTip for step', stepIndex + 1, 'of', totalSteps);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tour._activate_tip_patched = true;
|
tour._activate_tip_patched = true;
|
||||||
console.log('[Tour Genius] Patched _activate_tip for GeniusTip');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -541,7 +499,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
* Called when a genius tour is being activated
|
* Called when a genius tour is being activated
|
||||||
*/
|
*/
|
||||||
function deactivateNonGeniusTours() {
|
function deactivateNonGeniusTours() {
|
||||||
console.log('[Tour Genius] Deactivating non-genius tours...');
|
|
||||||
|
|
||||||
if (!tour.active_tooltips) return;
|
if (!tour.active_tooltips) return;
|
||||||
|
|
||||||
|
|
@ -558,7 +515,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
try {
|
try {
|
||||||
tip.widget.destroy();
|
tip.widget.destroy();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Tour Genius] Error destroying tip widget:', e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -566,10 +522,8 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
delete tour.active_tooltips[tourName];
|
delete tour.active_tooltips[tourName];
|
||||||
deactivatedCount++;
|
deactivatedCount++;
|
||||||
|
|
||||||
console.log('[Tour Genius] Deactivated tour:', tourName);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Tour Genius] Deactivated', deactivatedCount, 'non-genius tours');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -601,7 +555,6 @@ odoo.define('tour_genius.tour_loader', function (require) {
|
||||||
// NOTE: Tour completion/skip handling is done in patchConsumeTour
|
// NOTE: Tour completion/skip handling is done in patchConsumeTour
|
||||||
// - Completion shows GeniusCelebration and marks consumed
|
// - Completion shows GeniusCelebration and marks consumed
|
||||||
// - Skip just marks as skipped (no celebration)
|
// - Skip just marks as skipped (no celebration)
|
||||||
console.log('[Tour Genius] Tour consumed (completion listener):', tourName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track that debugging is active
|
// Track that debugging is active
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue