[IMP] system_dashboard_classic: automatic update
Auto-generated commit based on local changes.
This commit is contained in:
parent
dc65593634
commit
c99b324c9b
|
|
@ -581,6 +581,14 @@ class DashboardConfigSettings(models.TransientModel):
|
|||
config_parameter='system_dashboard_classic.refresh_interval',
|
||||
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.'
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -607,9 +615,11 @@ class DashboardConfigSettings(models.TransientModel):
|
|||
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_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
|
||||
|
||||
|
||||
def set_values(self):
|
||||
"""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')
|
||||
ICPSudo.set_param('system_dashboard_classic.enable_attendance_button',
|
||||
'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
|
||||
|
||||
|
||||
@api.model
|
||||
def get_stats_visibility(self):
|
||||
"""API method to get statistics visibility settings for JavaScript
|
||||
|
|
|
|||
|
|
@ -1021,3 +1021,77 @@ class SystemDashboard(models.Model):
|
|||
|
||||
result['approval_count'] = approval_count
|
||||
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') {
|
||||
emoji = '🎂';
|
||||
badgeClass = 'birthday-mode';
|
||||
title = isRtl ? 'عيد ميلاد سعيد!' : 'Happy Birthday!';
|
||||
title = isRtl ? 'يوم ميلاد سعيد!' : 'Happy Birthday!';
|
||||
// Gendered pronouns: لك (male) / لكِ (female)
|
||||
var genderInfo = window.dashboardGenderInfo || {pronoun_you: 'ك'};
|
||||
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>';
|
||||
|
||||
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>';
|
||||
} else if (result.celebration && result.celebration.is_anniversary) {
|
||||
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) {
|
||||
// DOM not ready - retry after 100ms
|
||||
if (retryCount < maxRetries) {
|
||||
console.log('[Dashboard] Waiting for DOM... attempt ' + (retryCount + 1));
|
||||
// console.log('[Dashboard] Waiting for DOM... attempt ' + (retryCount + 1));
|
||||
setTimeout(function() {
|
||||
buildApprovalCards(resultData, retryCount + 1);
|
||||
}, 100);
|
||||
|
|
@ -621,9 +621,8 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
view_type: 'list',
|
||||
views: [[list_view, 'list'], [form_view, 'form']],
|
||||
domain: domain,
|
||||
target: 'main',
|
||||
flags: { reload: true }
|
||||
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
||||
|
||||
});
|
||||
|
||||
// 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)
|
||||
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
|
||||
// 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
|
||||
// Not when switching between To Approve ↔ To Track
|
||||
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
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent double refresh
|
||||
if (isRefreshing) {
|
||||
console.log('[Dashboard] Refresh already in progress, skipping');
|
||||
// console.log('[Dashboard] Refresh already in progress, skipping');
|
||||
return;
|
||||
}
|
||||
isRefreshing = true;
|
||||
|
|
@ -789,27 +805,30 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
var model = $(this).attr('data-model');
|
||||
var name = $(this).attr('data-name');
|
||||
var domain = $(this).attr('data-domain');
|
||||
var form_view = parseInt($(this).attr('data-form-view')) || false;
|
||||
var list_view = parseInt($(this).attr('data-list-view')) || false;
|
||||
var count = parseInt($(this).attr('data-count')) || 0;
|
||||
var context = $(this).attr('data-context') || '{}';
|
||||
var form_view = parseInt($(this).attr('form-view')) || false;
|
||||
var list_view = parseInt($(this).attr('list-view')) || false;
|
||||
|
||||
try { context = JSON.parse(context.replace(/'/g, '"')); }
|
||||
catch(e) { context = {}; }
|
||||
if (domain === undefined || domain === '') {
|
||||
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({
|
||||
name: _t(name),
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: model,
|
||||
view_mode: 'tree,form',
|
||||
view_type: 'list',
|
||||
views: [[list_view, 'list'], [form_view, 'form']],
|
||||
context: context,
|
||||
domain: JSON.parse(domain.replace(/'/g, '"')),
|
||||
target: 'current',
|
||||
flags: { reload: true }
|
||||
domain: domain,
|
||||
}, { on_reverse_breadcrumb: function() { return self.reload(); } });
|
||||
}
|
||||
|
||||
});
|
||||
}, 100);
|
||||
|
||||
|
|
@ -831,7 +850,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
$('.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
|
||||
setTimeout(function() { isRefreshing = false; }, 500);
|
||||
|
|
@ -897,16 +916,16 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
|
||||
function startPeriodicRefresh() {
|
||||
if (!refreshEnabled || refreshInterval <= 0) {
|
||||
console.log('[Dashboard] Periodic refresh disabled');
|
||||
// console.log('[Dashboard] Periodic refresh disabled');
|
||||
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() {
|
||||
// Skip if page is hidden (browser tab switch)
|
||||
if (document.hidden) {
|
||||
console.log('[Dashboard] Skipping refresh - tab hidden');
|
||||
// console.log('[Dashboard] Skipping refresh - tab hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -917,7 +936,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
}, []).then(function(data) {
|
||||
updateAttendanceStatus(data.attendance);
|
||||
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) {
|
||||
console.warn('[Dashboard] Refresh failed:', err);
|
||||
});
|
||||
|
|
@ -927,6 +946,194 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
// Start polling
|
||||
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
|
||||
setTimeout(function() {
|
||||
window.check = false;
|
||||
|
|
@ -1077,6 +1284,16 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
setTimeout(function() { $notification.remove(); }, 400);
|
||||
}, 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) {
|
||||
// Handle unexpected server errors only
|
||||
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");
|
||||
var self = this;
|
||||
},
|
||||
reload: function() {
|
||||
window.location.href = this.href;
|
||||
reload: function(options) {
|
||||
// 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
|
||||
|
|
@ -1659,7 +1887,7 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require
|
|||
if (this.periodicRefreshId) {
|
||||
clearInterval(this.periodicRefreshId);
|
||||
this.periodicRefreshId = null;
|
||||
console.log('[Dashboard] Periodic refresh stopped');
|
||||
// console.log('[Dashboard] Periodic refresh stopped');
|
||||
}
|
||||
// Call parent destroy
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
</xpath>
|
||||
</field>
|
||||
|
|
|
|||
Loading…
Reference in New Issue