diff --git a/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js index d3bffc1..a9e86d9 100644 --- a/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js +++ b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js @@ -67,7 +67,6 @@ window.expertThemeApplyColors = async function() { root.style.setProperty(property, value); } }); - console.log('Expert Theme colors applied successfully!'); } } catch (error) { console.error('Error applying Expert Theme colors:', error); diff --git a/odex30_base/odex_sidebar_backend_theme2/__manifest__.py b/odex30_base/odex_sidebar_backend_theme2/__manifest__.py index 9fff9d6..94401df 100644 --- a/odex30_base/odex_sidebar_backend_theme2/__manifest__.py +++ b/odex30_base/odex_sidebar_backend_theme2/__manifest__.py @@ -31,6 +31,7 @@ 'base', ], 'data': [ + 'data/system_parameters.xml', 'views/res_config_settings.xml', ], 'assets': { diff --git a/odex30_base/odex_sidebar_backend_theme2/data/system_parameters.xml b/odex30_base/odex_sidebar_backend_theme2/data/system_parameters.xml new file mode 100644 index 0000000..0b514c6 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/data/system_parameters.xml @@ -0,0 +1,7 @@ + + + + odex_sidebar_backend_theme2.odex_support_team_link + https://odex.sa/support + + diff --git a/odex30_base/odex_sidebar_backend_theme2/models/res_config_settings.py b/odex30_base/odex_sidebar_backend_theme2/models/res_config_settings.py index bd336c5..f88a6d9 100644 --- a/odex30_base/odex_sidebar_backend_theme2/models/res_config_settings.py +++ b/odex30_base/odex_sidebar_backend_theme2/models/res_config_settings.py @@ -6,48 +6,147 @@ from odoo import fields,api, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' + # Sidebar Menu Enable Setting sidebar_menu_enable = fields.Boolean( config_parameter='odex_sidebar_backend_theme2.sidebar_menu_enable', string='Enable Sidebar Menu', help='Enable or disable the sidebar menu in the backend' ) + # Navigation Menu Section Disable Setting disable_nav_menu_section = fields.Boolean( config_parameter='odex_sidebar_backend_theme2.disable_nav_menu_section', string='Disable Navigation Menu Section', help='Enable or disable the top navigation bar menu section in the backend interface' ) + # Sidebar Menu Icon Setting sidebar_menu_icon = fields.Binary( string="Sidebar Icon", help="Upload an icon for the sidebar menu.", ) + + # Uncollapsed Sidebar Overlay Setting + uncollapsed_sidebar_overlay = fields.Boolean( + config_parameter='odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay', + string='Uncollapsed Sidebar Overlay', + help='Enable overlay effect when sidebar is uncollapsed' + ) - # set default value for the setting + # Sidebar Background Settings + sidebar_background_type = fields.Selection( + [('color', 'Color'), ('image', 'Image')], + config_parameter='odex_sidebar_backend_theme2.sidebar_background_type', + string='Sidebar Background Type', + default='color', + help='Choose the type of background for the sidebar' + ) + sidebar_background_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_background_color', + default='#151a2d', + string='Sidebar Background Color', + help='Set the background color for the sidebar (hex code or color name)' + ) + sidebar_background_image = fields.Binary( + string='Sidebar Background Image', + help='Upload an image to use as the sidebar background' + ) + + # Sidebar Links Color Settings + sidebar_links_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_color', + default='#ffffff', + string='Sidebar Links Color', + help='Set the color for the sidebar links (hex code or color name)' + ) + sidebar_links_hover_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_hover_color', + default='#151a2d', + string='Sidebar Links Hover Color', + help='Set the hover color for the sidebar links (hex code or color name)' + ) + sidebar_links_active_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_active_color', + default='#151a2d', + string='Sidebar Links Active Color', + help='Set the active color for the sidebar links (hex code or color name)' + ) + sidebar_links_bg_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_bg_color', + default='#ffffff00', + string='Sidebar Links Background Color', + help='Set the background color for the sidebar links (hex code or color name)' + ) + sidebar_links_hover_bg_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_hover_bg_color', + default='#151a2d', + string='Sidebar Links Hover Background Color', + help='Set the hover background color for the sidebar links (hex code or color name)' + ) + sidebar_links_active_bg_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_links_active_bg_color', + default='#ffffff', + string='Sidebar Links Active Background Color', + help='Set the active background color for the sidebar links (hex code or color name)' + ) + sidebar_scrollbar_track_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_track_color', + default='#2f3542', + string='Sidebar Scrollbar Track Color', + help='Set the color for the sidebar scrollbar track (hex code or color name)' + ) + sidebar_scrollbar_thumb_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color', + default='#151a2d', + string='Sidebar Scrollbar Thumb Color', + help='Set the color for the sidebar scrollbar thumb (hex code or color name)' + ) + sidebar_scrollbar_thumb_hover_color = fields.Char( + config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color', + default='#151a2d', + string='Sidebar Scrollbar Thumb Hover Color', + help='Set the hover color for the sidebar scrollbar thumb (hex code or color name)' + ) + def get_values(self): res = super(ResConfigSettings, self).get_values() IrConfigParam = self.env['ir.config_parameter'].sudo() sidebar_menu_enable = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable') disable_nav_menu_section = IrConfigParam.get_param('odex_sidebar_backend_theme2.disable_nav_menu_section') + uncollapsed_sidebar_overlay = IrConfigParam.get_param('odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay') + sidebar_background_type = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_type') + sidebar_background_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_color') + sidebar_background_image = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_image') + sidebar_links_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_color') + sidebar_links_hover_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_hover_color') + sidebar_links_active_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_active_color') + sidebar_links_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_bg_color') + sidebar_links_hover_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_hover_bg_color') + sidebar_links_active_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_active_bg_color') + sidebar_scrollbar_track_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_track_color') + sidebar_scrollbar_thumb_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color') + sidebar_scrollbar_thumb_hover_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color') + res.update( sidebar_menu_enable=sidebar_menu_enable == 'True', disable_nav_menu_section=disable_nav_menu_section == 'True', - sidebar_menu_icon=IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon') + sidebar_menu_icon=IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon'), + uncollapsed_sidebar_overlay=uncollapsed_sidebar_overlay == 'True', + sidebar_background_type=sidebar_background_type or 'color', + sidebar_background_color=sidebar_background_color or '#151a2d', + sidebar_background_image=sidebar_background_image or False, + sidebar_links_color=sidebar_links_color or '#ffffff', + sidebar_links_hover_color=sidebar_links_hover_color or '#151a2d', + sidebar_links_active_color=sidebar_links_active_color or '#151a2d', + sidebar_links_bg_color=sidebar_links_bg_color or '#ffffff00', + sidebar_links_hover_bg_color=sidebar_links_hover_bg_color or '#151a2d', + sidebar_links_active_bg_color=sidebar_links_active_bg_color or '#ffffff', + sidebar_scrollbar_track_color=sidebar_scrollbar_track_color or '#2f3542', + sidebar_scrollbar_thumb_color=sidebar_scrollbar_thumb_color or '#151a2d', + sidebar_scrollbar_thumb_hover_color=sidebar_scrollbar_thumb_hover_color or '#151a2d', ) return res - def _generate_sidebar_css(self): - """Generate CSS rules for sidebar menu state""" - if self.disable_nav_menu_section: - return """ -.o_main_navbar .o_menu_sections { - display: none!important; - visibility: hidden!important; -} -""" - return "" - - # save the setting value def set_values(self): super(ResConfigSettings, self).set_values() IrConfigParam = self.env['ir.config_parameter'].sudo() @@ -63,7 +162,60 @@ class ResConfigSettings(models.TransientModel): 'odex_sidebar_backend_theme2.sidebar_menu_icon', self.sidebar_menu_icon or '' ) - + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay', + str(self.uncollapsed_sidebar_overlay) + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_background_type', + self.sidebar_background_type or 'color' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_background_color', + self.sidebar_background_color or '#151a2d' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_background_image', + self.sidebar_background_image or False + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_color', + self.sidebar_links_color or '#ffffff' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_hover_color', + self.sidebar_links_hover_color or '#151a2d' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_active_color', + self.sidebar_links_active_color or '#151a2d' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_bg_color', + self.sidebar_links_bg_color or '#ffffff00' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_hover_bg_color', + self.sidebar_links_hover_bg_color or '#151a2d' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_links_active_bg_color', + self.sidebar_links_active_bg_color or '#ffffff' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_scrollbar_track_color', + self.sidebar_scrollbar_track_color or '#2f3542' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color', + self.sidebar_scrollbar_thumb_color or '#151a2d' + ) + IrConfigParam.set_param( + 'odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color', + self.sidebar_scrollbar_thumb_hover_color or '#151a2d' + ) + + # Store sidebar menu icon URL if self.sidebar_menu_icon: # Store the image URL in config parameter image_url = f"/web/image/res.config.settings/{self.id}/sidebar_menu_icon" @@ -80,9 +232,42 @@ class ResConfigSettings(models.TransientModel): return True + def _generate_sidebar_css(self): + """Generate CSS rules for sidebar menu state""" + css = f""" +/* Sidebar Menu State CSS */ +:root {{ + --ox-sidebar-bg-color: {self.sidebar_background_color}; + --ox-sidebar-links-color: {self.sidebar_links_color}; + --ox-sidebar-links-hover-color: {self.sidebar_links_hover_color}; + --ox-sidebar-links-active-color: {self.sidebar_links_active_color}; + --ox-sidebar-links-bg-color: {self.sidebar_links_bg_color}; + --ox-sidebar-links-hover-bg-color: {self.sidebar_links_hover_bg_color}; + --ox-sidebar-links-active-bg-color: {self.sidebar_links_active_bg_color}; + --ox-sidebar-scrollbar-track-color: {self.sidebar_scrollbar_track_color}; + --ox-sidebar-scrollbar-thumb-color: {self.sidebar_scrollbar_thumb_color}; + --ox-sidebar-scrollbar-thumb-hover-color: {self.sidebar_scrollbar_thumb_hover_color}; +}} +""" + if self.disable_nav_menu_section: + css += """ +.o_main_navbar .o_menu_sections { + display: none!important; + visibility: hidden!important; +} +""" + return css + @api.model def get_sidebar_setting(self): IrConfigParam = self.env['ir.config_parameter'].sudo() sidebar_enabled = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable') == 'True' sidebar_icon_url = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon_url') - return {'sidebar_enabled': sidebar_enabled, 'sidebar_icon_url': sidebar_icon_url} \ No newline at end of file + uncollapsed_sidebar_overlay = IrConfigParam.get_param('odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay') == 'True' + support_team_link = IrConfigParam.get_param('odex_sidebar_backend_theme2.odex_support_team_link') or 'https://odex.sa/support' + + return {'sidebar_enabled': sidebar_enabled, + 'sidebar_icon_url': sidebar_icon_url, + 'uncollapsed_sidebar_overlay': uncollapsed_sidebar_overlay, + 'support_team_link': support_team_link + } \ No newline at end of file diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js index 017214d..05d5bbb 100644 --- a/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js @@ -41,7 +41,6 @@ export class MenuItem extends Component { // Use xmlid if available, otherwise use name as a stable identifier const uniqueId = menu.xmlid || menu.name || menu.id; localStorage.setItem('odex_sidebar_active_menu', uniqueId); - console.log('Saved menu:', uniqueId, menu.name); } catch (e) { console.error('Storage error:', e); } diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_css_loader.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_css_loader.js index d48afe9..5a3e89f 100644 --- a/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_css_loader.js +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_css_loader.js @@ -19,8 +19,6 @@ export function loadSidebarCSS() { kwargs: {}, } ); - - console.log('Fetched sidebar CSS:', css); if (css && css.trim()) { // Create a style element and inject the CSS @@ -29,7 +27,7 @@ export function loadSidebarCSS() { style.id = 'sidebar-dynamic-css'; style.innerHTML = css; document.head.appendChild(style); - console.log('Sidebar CSS injected successfully'); + console.error('Error loading sidebar CSS:', error); } } catch (error) { console.error('Error loading sidebar CSS:', error); @@ -45,9 +43,9 @@ export function loadSidebarCSS() { } // Initialize on module load -registry.category("web_tour.tours").add("sidebar_css_loader", { - steps: [], -}); +// registry.category("web_tour.tours").add("sidebar_css_loader", { +// steps: [], +// }); // Auto-load CSS on page load loadSidebarCSS(); diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js index 2d01942..cca769f 100644 --- a/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js @@ -23,26 +23,37 @@ export class SidebarMenu extends Component { isCollapsed: false, sidebarEnabled: false, sidebarMenuIconUrl: null, + overlayEnabled: false, + supportTeamLink: null, }); this.loadSidebarSetting() // =================== JavaScript Control Starts Here =================== - const applyLayoutChanges = (isOpen) => { + const applyLayoutChanges = (isOpen,isCollapsed) => { const actionManager = document.querySelector('.o_action_manager'); const mainNavbar = document.querySelector('.o_navbar'); // Determine sidebar width based on collapse state - const collapsedWidth = "90px"; + let collapsedWidth = "270px"; + if (this.state.overlayEnabled) { + collapsedWidth = '90px'; + } - if (isOpen) { + if (!isCollapsed) { if (actionManager) actionManager.style.marginInlineStart = collapsedWidth; if (mainNavbar) mainNavbar.style.marginInlineStart = collapsedWidth; } else { // If sidebar is hidden, remove margin - if (actionManager) actionManager.style.marginInlineStart = '0'; - if (mainNavbar) mainNavbar.style.marginInlineStart = '0'; + if (actionManager) actionManager.style.marginInlineStart = '90px'; + if (mainNavbar) mainNavbar.style.marginInlineStart = '90px'; + } + + if (!isOpen) { + // If sidebar is closed, remove margin + if (actionManager) actionManager.style.marginInlineStart = '0px'; + if (mainNavbar) mainNavbar.style.marginInlineStart = '0px'; } }; @@ -118,10 +129,16 @@ export class SidebarMenu extends Component { // Load sidebar menu icon URL this.state.sidebarMenuIconUrl = result.sidebar_icon_url || '/odex_sidebar_backend_theme2/static/src/img/logo.webp'; + // Load overlay setting + this.state.overlayEnabled = result.uncollapsed_sidebar_overlay === true || result.uncollapsed_sidebar_overlay === 'True'; + + // Load support team link + this.state.supportTeamLink = result.support_team_link || 'https://odex.sa/support'; } catch (error) { console.error('Error loading sidebar setting:', error); // Default to enabled if setting cannot be loaded this.state.sidebarEnabled = true; + this.state.overlayEnabled = false; } } @@ -153,7 +170,6 @@ export class SidebarMenu extends Component { try { const activeId = localStorage.getItem('odex_sidebar_active_menu'); - console.log('Restored menu ID:', activeId); let targetMenu = menuMapByXmlId.get(activeId) || menuMap.get(activeId); diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss b/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss index e5fd256..f0bc36b 100644 --- a/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss @@ -2,455 +2,456 @@ 1. Main Sidebar Design =================================== */ .custom_sidebar { - display: flex; - flex-direction: column; - position: fixed; - top: 0; - left: 0; - width: 270px; - height: 100vh; - overflow: auto; - background-color: #151a2d; - z-index: 999; - transform: translateX(0%); - transition: all 0.4s ease-in-out; + display: flex; + flex-direction: column; + position: fixed; + top: 0; + left: 0; + width: 270px; + height: 100vh; + overflow: auto; + background-color: var(--ox-sidebar-bg-color, #151a2d); + z-index: 999; + transform: translateX(0%); + transition: all 0.4s ease-in-out; - &:not(.is-open) { - transform: translateX(-100%) !important; - /* Use !important to force hiding */ - } - - /* =============== Add New Flyout =============== */ - - &.is-collapsed { - overflow: visible; - - li { - position: relative; - z-index: 1000; + &:not(.is-open) { + transform: translateX(-100%) !important; + /* Use !important to force hiding */ } - .flyout-panel { - display: none; - position: absolute; - left: 100%; - top: 0; - min-width: 220px; - background: #151a2d; - color: #fff; - border-radius: 10px; - padding: 10px; - box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6); - transform-origin: left top; - transition: opacity 180ms ease, transform 180ms ease; - opacity: 0; - transform: translateX(-6px) scale(0.98); - pointer-events: none; + /* =============== Add New Flyout =============== */ + + &.is-collapsed { + overflow: visible; + + li { + position: relative; + z-index: 1000; + } + + .flyout-panel { + display: none; + position: absolute; + left: 100%; + top: 0; + min-width: 220px; + background: var(--ox-sidebar-bg-color, #151a2d); + color: #fff; + border-radius: 10px; + padding: 10px; + box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6); + transform-origin: left top; + transition: opacity 180ms ease, transform 180ms ease; + opacity: 0; + transform: translateX(-6px) scale(0.98); + pointer-events: none; + } + + li:hover>.flyout-panel { + display: block; + opacity: 1; + transform: translateX(0) scale(1); + pointer-events: auto; + } + + .flyout-list { + list-style: none; + padding: 6px 4px; + display: flex; + flex-direction: column; + gap: 6px; + } + + .flyout-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 8px; + cursor: pointer; + transition: background 150ms ease; + white-space: nowrap; + } + + .flyout-item:hover { + background: rgba(255, 255, 255, 0.05); + } + + .flyout-item .icon { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + } + + .flyout-item .label { + color: #fff; + font-size: 0.95rem; + } + + /* Submenu flyout */ + .flyout-item.has-children { + position: relative; + } + + .flyout-subpanel { + display: none; + position: absolute; + left: 100%; + top: 0; + min-width: 200px; + background: #151a2d; + border-radius: 10px; + padding: 10px; + box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6); + opacity: 0; + transform: translateX(-6px) scale(0.98); + transition: opacity 180ms ease, transform 180ms ease; + pointer-events: none; + z-index: 1200; + } + + .flyout-item.has-children:hover>.flyout-subpanel { + display: block; + opacity: 1; + transform: translateX(0) scale(1); + pointer-events: auto; + } } - li:hover > .flyout-panel { - display: block; - opacity: 1; - transform: translateX(0) scale(1); - pointer-events: auto; + &.is-collapsed { + width: 90px; + transform: translateX(0); + + .sidebar-header { + padding: 25px 10px; + justify-content: center; + flex-direction: column; + + .header-logo { + img { + width: 46px; + height: 46px; + } + } + + .sidebar-toggler { + position: static; + width: 50%; + margin-top: 25px; + } + } + + .sidebar-nav { + .sidebar_menu_list { + padding: 0 8px; + + &.primary-nav { + + .has-children, + li:not(.has-children) { + position: relative; + + .menu-item-container { + padding: 11px 8px; + justify-content: center; + + .menu-item-icon { + margin: 0; + width: 30px; + height: 30px; + } + + .menu-item-link { + display: none; + } + + .toggle-icon { + display: none; + } + + &:hover { + background-color: #eef2ff; + color: #151a2d; + } + + &:active { + background-color: #d9e1fd; + transform: scale(0.95); + } + } + } + + .submenu_list { + display: none; + } + } + + &.secondary-nav { + width: auto; + bottom: 20px; + left: 0; + right: 0; + padding: 0 8px; + + .nav-item { + position: relative; + z-index: 1000; + + .nav-link { + padding: 11px 8px; + justify-content: center; + + .nav-label { + display: none; + } + + .nav_icon { + margin: 0; + } + } + + .dropdown_menu { + display: none; + } + + // Flyout support for secondary-nav + &:hover>.flyout-panel { + display: block; + opacity: 1; + transform: translateX(0) scale(1); + pointer-events: auto; + } + } + } + } + } } - .flyout-list { - list-style: none; - padding: 6px 4px; - display: flex; - flex-direction: column; - gap: 6px; + li.open { + >.menu-item-container { + background-color: #fff; + + .menu-item-link { + color: #151a2d !important; + } + + .toggle-icon { + transform: rotate(90deg); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + } + } } - .flyout-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 10px; - border-radius: 8px; - cursor: pointer; - transition: background 150ms ease; - white-space: nowrap; + &::-webkit-scrollbar { + width: 8px; } - .flyout-item:hover { - background: rgba(255, 255, 255, 0.05); + &::-webkit-scrollbar-track { + background: var(--ox-sidebar-scrollbar-track-color, #0e1223); } - .flyout-item .icon { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; + &::-webkit-scrollbar-thumb { + background-color: var(--ox-sidebar-scrollbar-thumb-color, #151a2d); + border-radius: 4px; } - .flyout-item .label { - color: #fff; - font-size: 0.95rem; + &::-webkit-scrollbar-thumb:hover { + background-color: var(--ox-sidebar-scrollbar-thumb-hover-color, #151a2d); } - /* Submenu flyout */ - .flyout-item.has-children { - position: relative; - } - - .flyout-subpanel { - display: none; - position: absolute; - left: 100%; - top: 0; - min-width: 200px; - background: #151a2d; - border-radius: 10px; - padding: 10px; - box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6); - opacity: 0; - transform: translateX(-6px) scale(0.98); - transition: opacity 180ms ease, transform 180ms ease; - pointer-events: none; - z-index: 1200; - } - - .flyout-item.has-children:hover > .flyout-subpanel { - display: block; - opacity: 1; - transform: translateX(0) scale(1); - pointer-events: auto; - } - } - - &.is-collapsed { - width: 90px; - transform: translateX(0); - .sidebar-header { - padding: 25px 10px; - justify-content: center; - flex-direction: column; + display: flex; + position: relative; + padding: 25px 20px; + align-items: center; + justify-content: space-between; - .header-logo { - img { - width: 46px; - height: 46px; + .header-logo { + img { + width: 46px; + height: 46px; + display: block; + object-fit: contain; + border-radius: 50%; + } } - } - .sidebar-toggler { - position: static; - width: 50%; - margin-top: 25px; - } + .sidebar-toggler { + position: absolute; + right: 20px; + height: 35px; + width: 35px; + color: #151a2d; + border: none; + cursor: pointer; + display: flex; + background: #eef2ff; + align-items: center; + justify-content: center; + border-radius: 8px; + transition: 0.4s ease; + + &:hover { + background: #d9e1fd; + } + + span { + transition: 0.4s ease; + } + + i { + font-size: 18px; + transition: transform 0.4s ease; + } + } } .sidebar-nav { - .sidebar_menu_list { - padding: 0 8px; + flex: 1; + display: flex; + flex-direction: column; - &.primary-nav { - - .has-children, - li:not(.has-children) { - position: relative; - - .menu-item-container { - padding: 11px 8px; - justify-content: center; - - .menu-item-icon { - margin: 0; - width: 30px; - height: 30px; - } - - .menu-item-link { - display: none; - } - - .toggle-icon { - display: none; - } - - &:hover { - background-color: #eef2ff; - color: #151a2d; - } - - &:active { - background-color: #d9e1fd; - transform: scale(0.95); - } - } - } - - .submenu_list { - display: none; - } - } - - &.secondary-nav { - width: auto; - bottom: 20px; - left: 0; - right: 0; - padding: 0 8px; - - .nav-item { - position: relative; - z-index: 1000; - - .nav-link { - padding: 11px 8px; - justify-content: center; - - .nav-label { - display: none; - } - - .nav_icon { - margin: 0; - } - } - - .dropdown_menu { - display: none; - } - - // Flyout support for secondary-nav - &:hover > .flyout-panel { - display: block; - opacity: 1; - transform: translateX(0) scale(1); - pointer-events: auto; - } - } - } - } - } - } - - li.open { - > .menu-item-container { - background-color: #fff; - - .menu-item-link { - color: #151a2d !important; - } - - .toggle-icon { - transform: rotate(90deg); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); - } - } - } - - &::-webkit-scrollbar { - width: 8px; - } - - &::-webkit-scrollbar-track { - background: #2f3542; - } - - &::-webkit-scrollbar-thumb { - background-color: #151a2d; - border-radius: 4px; - } - - &::-webkit-scrollbar-thumb:hover { - background-color: #151a2d; - } - - .sidebar-header { - display: flex; - position: relative; - padding: 25px 20px; - align-items: center; - justify-content: space-between; - - .header-logo { - img { - width: 46px; - height: 46px; - display: block; - object-fit: contain; - border-radius: 50%; - } - } - - .sidebar-toggler { - position: absolute; - right: 20px; - height: 35px; - width: 35px; - color: #151a2d; - border: none; - cursor: pointer; - display: flex; - background: #eef2ff; - align-items: center; - justify-content: center; - border-radius: 8px; - transition: 0.4s ease; - - &:hover { - background: #d9e1fd; - } - - span { - transition: 0.4s ease; - } - - i { - font-size: 18px; - transition: transform 0.4s ease; - } - } - } - - .sidebar-nav { - flex: 1; - display: flex; - flex-direction: column; - - .sidebar_menu_list { - list-style: none; - display: flex; - gap: 4px; - padding: 0 15px; - flex-direction: column; - transform: translateY(15px); - transition: 0.4s ease; - - &.primary-nav { - .has-children, - li:not(.has-children) { - &:hover { - > .menu-item-container { - background-color: #eef2ff; - - .menu-item-link { - color: #151a2d; - } - } - } - - .menu-item-container { + .sidebar_menu_list { + list-style: none; display: flex; - gap: 12px; - align-items: center; - justify-content: space-between; - cursor: pointer; - transition: all 0.3s; - border: 1px solid #151a2d; - text-decoration: none; - padding: 11px 15px; - border-radius: 8px; - white-space: nowrap; - - // image icon style - .menu-item-icon { - width: 25px; - height: 25px; - margin: 0 10px; - object-fit: contain; - } - - // name style - .menu-item-link { - color: #fff; - flex-grow: 1; - font-size: 1rem; - text-decoration: none; - transition: opacity 0.3s ease; - } - - // toggle icon style - .toggle-icon { - width: 20px; - height: 20px; - margin-right: 10px; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23b9d0ec' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: center; - transition: transform 0.2s ease-in-out; - } - } - } - - .submenu_list { - list-style: none; - padding: 5px 0; - - li { - .menu-item-container { - margin-left: 8px; - border: 1px solid transparent; - transition: all 0.2s ease; - - &:hover { - background-color: #eef2ff; - border-color: #151a2d; - - .menu-item-link { - color: #151a2d; - } - } - } - } - } - } - - &.secondary-nav { - margin-top: auto; - // width: calc(100% - 30px); - background: #151a2d; - padding: 20px 15px; - - .nav-item { - position: relative; - // margin: 0 15px; - - &:hover > .nav-link { - color: #151a2d !important; - background-color: #eef2ff; - } - - .nav-link { - color: #fff !important; - display: flex; - gap: 12px; - white-space: nowrap; - border-radius: 8px; - padding: 11px 15px; - align-items: center; - text-decoration: none; - border: 1px solid #151a2d; + gap: 4px; + padding: 0 15px; + flex-direction: column; + transform: translateY(15px); transition: 0.4s ease; - .nav_icon { - font-size: 24px; - margin: 0 10px; + &.primary-nav { + + .has-children, + li:not(.has-children) { + &:hover { + >.menu-item-container { + background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff); + + .menu-item-link { + color: var(--ox-sidebar-links-hover-color, #151a2d); + } + } + } + + .menu-item-container { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + cursor: pointer; + transition: all 0.3s; + border: 1px solid transparent; + text-decoration: none; + padding: 11px 15px; + border-radius: 8px; + white-space: nowrap; + + // image icon style + .menu-item-icon { + width: 25px; + height: 25px; + margin: 0 10px; + object-fit: contain; + } + + // name style + .menu-item-link { + color: #fff; + flex-grow: 1; + font-size: 1rem; + text-decoration: none; + transition: opacity 0.3s ease; + } + + // toggle icon style + .toggle-icon { + width: 20px; + height: 20px; + margin-right: 10px; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23b9d0ec' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: center; + transition: transform 0.2s ease-in-out; + } + } + } + + .submenu_list { + list-style: none; + padding: 5px 0; + + li { + .menu-item-container { + margin-left: 8px; + border: 1px solid transparent; + transition: all 0.2s ease; + + &:hover { + background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff); + // border-color: #151a2d; + + .menu-item-link { + color: var(--ox-sidebar-links-hover-color, #151a2d); + } + } + } + } + } } - .nav-label { - font-size: 1rem; - transition: opacity 0.3s ease; - } - } + &.secondary-nav { + margin-top: auto; + // width: calc(100% - 30px); + background: var(--ox-sidebar-bg-color, #151a2d); + padding: 20px 15px; - .dropdown_menu { - height: 0; - overflow-y: hidden; - list-style: none; - padding-left: 15px; - transition: height 0.4s ease; - } + .nav-item { + position: relative; + // margin: 0 15px; + + &:hover>.nav-link { + color: var(--ox-sidebar-links-hover-color, #151a2d) !important; + background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff); + } + + .nav-link { + color: #fff !important; + display: flex; + gap: 12px; + white-space: nowrap; + border-radius: 8px; + padding: 11px 15px; + align-items: center; + text-decoration: none; + border: 1px solid transparent; + transition: 0.4s ease; + + .nav_icon { + font-size: 24px; + margin: 0 10px; + } + + .nav-label { + font-size: 1rem; + transition: opacity 0.3s ease; + } + } + + .dropdown_menu { + height: 0; + overflow-y: hidden; + list-style: none; + padding-left: 15px; + transition: height 0.4s ease; + } + } + } } - } } - } } /* =================================== @@ -458,32 +459,32 @@ =================================== */ // Hide default application icon from top bar .o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown { - display: none !important; + display: none !important; } .o_web_client { - margin-left: 0 !important; - transition: all 0.3s ease-in-out; + margin-left: 0 !important; + transition: all 0.3s ease-in-out; } /* Adjust Top Bar and Main Content */ .o_navbar, .o_action_manager { - transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; } /* Open Icon Design (New) */ .sidebar_toggle_icon { - position: relative; - display: flex; - align-items: center; - width: auto; - height: calc(var(--o-navbar-height) - 0px); - border-radius: 0; - user-select: none; - background: transparent; - color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9)); - font-size: 1.2em; - padding: 0 15px; - border: none; + position: relative; + display: flex; + align-items: center; + width: auto; + height: calc(var(--o-navbar-height) - 0px); + border-radius: 0; + user-select: none; + background: transparent; + color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9)); + font-size: 1.2em; + padding: 0 15px; + border: none; } \ No newline at end of file diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml index a91ffa1..79343a9 100644 --- a/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml @@ -23,7 +23,7 @@ @@ -45,7 +45,7 @@ diff --git a/odex30_base/odex_sidebar_backend_theme2/views/res_config_settings.xml b/odex30_base/odex_sidebar_backend_theme2/views/res_config_settings.xml index aced0d6..410e513 100644 --- a/odex30_base/odex_sidebar_backend_theme2/views/res_config_settings.xml +++ b/odex30_base/odex_sidebar_backend_theme2/views/res_config_settings.xml @@ -12,13 +12,34 @@ - + + options="{'size': [128, 128]}" /> + + + + + + + + + + + + + + + + + + + diff --git a/odex30_base/system_dashboard_classic/__init__.py b/odex30_base/system_dashboard_classic/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/odex30_base/system_dashboard_classic/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/odex30_base/system_dashboard_classic/__manifest__.py b/odex30_base/system_dashboard_classic/__manifest__.py new file mode 100644 index 0000000..dc50089 --- /dev/null +++ b/odex30_base/system_dashboard_classic/__manifest__.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +{ + 'name': "System Dashboard", + 'summary': "Configurable dashboard for employee self-service and manager approvals", + 'description': """ +System Dashboard Classic (Odoo 18) +================================== + +A comprehensive dashboard module that provides: + +* **Self-Service Portal**: Employees can view and manage their own requests + (leaves, expenses, timesheets, etc.) + +* **Manager Dashboard**: Managers can see pending approvals with state/stage + filtering based on user groups + +* **Configurable Services**: Add any Odoo model as a dashboard service card + with custom actions and views + +* **Attendance Integration**: Built-in check-in/check-out functionality with + geolocation zone validation + +* **Theme Customization**: Configurable colors and visibility settings + +Key Features: +------------- +- Dynamic state/stage loading from any model +- Group-based visibility for approval cards +- Self-service mode for employee-facing services +- Real-time attendance with confetti celebration +- Responsive design with RTL support +- OWL-based modern frontend architecture + """, + + 'author': "Expert Co. Ltd., Sudan Team", + 'category': 'Human Resources/Dashboard', + 'version': '18.0.1.0.0', + 'license': 'LGPL-3', + 'application': True, + + # Required Dependencies - Odoo 18 compatible + 'depends': [ + 'base', # Core Odoo + 'hr_holidays_public', # Work calendar for attendance hours + 'web', # Dashboard assets & OWL framework + 'employee_requests', # HR base for employee data + 'hr_timesheet', + 'exp_official_mission', + + ], + + # Optional Dependencies (checked at runtime for graceful fallback): + # - hr_attendance: For check-in/check-out functionality + # - hr_holidays: For leave management integration + # - hr_payroll: For payslip integration + # - hr_timesheet: For timesheet integration (account.analytic.line) + # - odoo_dynamic_workflow: For dynamic workflow states integration + + # Data files + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'views/system_dashboard.xml', + 'views/config.xml', + 'views/dashboard_settings.xml', + ], + + # Odoo 18 Assets Configuration + 'assets': { + 'web.assets_backend': [ + # External libraries + 'system_dashboard_classic/static/src/lib/donut_chart.js', # ✅ CSS-based chart renderer + 'system_dashboard_classic/static/src/lib/confetti.min.js', + + # SCSS styles - Variables MUST be first + ('prepend', 'system_dashboard_classic/static/src/scss/variables.scss'), # ✅ أضف prepend + 'system_dashboard_classic/static/src/scss/core.scss', + 'system_dashboard_classic/static/src/scss/cards.scss', + 'system_dashboard_classic/static/src/scss/pluscharts.scss', + 'system_dashboard_classic/static/src/scss/genius-enhancements.scss', + # OWL Components + 'system_dashboard_classic/static/src/components/**/*.js', + 'system_dashboard_classic/static/src/components/**/*.xml', + 'system_dashboard_classic/static/src/components/**/*.scss', + ], + + }, + + 'installable': True, + 'auto_install': False, +} diff --git a/odex30_base/system_dashboard_classic/i18n/ar_001.po b/odex30_base/system_dashboard_classic/i18n/ar_001.po new file mode 100644 index 0000000..bdecbb9 --- /dev/null +++ b/odex30_base/system_dashboard_classic/i18n/ar_001.po @@ -0,0 +1,980 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * system_dashboard_classic +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-19 20:45+0000\n" +"PO-Revision-Date: 2026-01-19 20:45+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid " Browse Icons" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "" +" Click the button below to detect available " +"states/stages for the selected model." +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__action_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__action_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__action_id +msgid "Action" +msgstr "الإجراء" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__action_context +msgid "Action Context" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__action_domain +msgid "Action Domain" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Add New" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Advanced View Settings" +msgstr "إعدادات العرض المتقدمة" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/dashboard.py:0 +msgid "All Records" +msgstr "كل السجلات" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Allow employees to check in/out from dashboard" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Amazing Years!" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Annual Leave" +msgstr "الاجازة السنوية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_annual_leave_chart_type +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Annual Leave Chart" +msgstr "مخطط الإجازة السنوية" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Are you sure you want to remove all loaded states?" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_attendance_hours_chart_type +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Attendance Hours Chart" +msgstr "مخطط ساعات الحضور" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Attendance Settings" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Auto Refresh" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_refresh_enabled +msgid "" +"Automatically refresh attendance status and approval count at regular " +"intervals" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Automatically refresh dashboard data" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__card_image +msgid "Card Image" +msgstr "صورة النموذج" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__name +msgid "Card name" +msgstr "اسم النموذج" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Chart Types" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Chart style for annual leave card" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Chart style for attendance hours card" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Chart style for salary slips card" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Chart style for timesheet card" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Check In" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Check Out" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Checked in successfully!" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Checked out successfully!" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Color for headers and dark elements" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Color for success/online status" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Color for warnings and remaining balances" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_res_config_settings +msgid "Config Settings" +msgstr "تهيئة الإعدادات " + +#. module: system_dashboard_classic +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_config +#: model:res.groups,name:system_dashboard_classic.group_dashboard_config +msgid "Configuration" +msgstr "الإعدادات" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__create_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__create_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__create_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__create_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__create_uid +msgid "Created by" +msgstr "أنشئ بواسطة" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__create_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__create_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__create_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__create_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__create_date +msgid "Created on" +msgstr "أنشئ في" + +#. module: system_dashboard_classic +#: model:ir.actions.client,name:system_dashboard_classic.action_dashboard_client +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__board_id +#: model:ir.module.category,name:system_dashboard_classic.module_category_dashboard +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_root +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Dashboard" +msgstr "لوحة المعلومات" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_base_dashbord +msgid "Dashboard Builder" +msgstr "تصميم الداشبورد" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_base_dashbord_line +msgid "Dashboard Builder Line" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "Dashboard Card" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_users__dashboard_card_orders +msgid "Dashboard Card Orders" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.actions.act_window,name:system_dashboard_classic.action_base_dashbord +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_cards +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashbord_tree +msgid "Dashboard Cards" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_node_state +msgid "Dashboard Node State" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Dashboard Service Configuration" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.actions.act_window,name:system_dashboard_classic.action_dashboard_settings +msgid "Dashboard Settings" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_stage_stage +msgid "Dashboard Stage" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__display_name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__display_name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__display_name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__display_name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Display Options" +msgstr "خيارات العرض" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Display countdown timer for remaining work hours" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_work_timer +msgid "" +"Display live work hour countdown showing remaining time until end of planned" +" shift" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_annual_leave_chart_type__donut +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_attendance_hours_chart_type__donut +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_salary_slips_chart_type__donut +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_timesheet_chart_type__donut +msgid "Donut" +msgstr "دونات" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Employee Filter Configuration" +msgstr "إعدادات فلترة الموظف" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_refresh_enabled +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Enable Auto Refresh" +msgstr "تفعيل التحديث التلقائي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_enable_attendance_button +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Enable Check-in/out Button" +msgstr "تفعيل زر تسجيل الدخول/الخروج" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Enable for employee self-service cards" +msgstr "تفعيل لبطاقات الخدمة الذاتية للموظف" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_enable_attendance_button +msgid "Enable/Disable the Check-in/Check-out button functionality" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Failed to load dashboard data" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.js:0 +msgid "Failed to record attendance" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__field_id +msgid "Fields" +msgstr "الحقول" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_base_dashbord__icon_name +msgid "" +"FontAwesome class (e.g., 'fa-plane', 'fa-users'). See " +"https://fontawesome.com/v4/icons/" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__form_view_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__form_view_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__form_view_id +msgid "Form View" +msgstr "نموذج العرض" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__group_ids +msgid "Groups" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Happy Birthday! Wishing you a wonderful day!" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_refresh_interval +msgid "How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds)" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__id +msgid "ID" +msgstr "المرجع" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__icon_name +msgid "Icon Class" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__base_dashbord__icon_type__icon +msgid "Icon Library" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__icon_type +msgid "Icon Type" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__icon_preview_html +msgid "Icon/Image Preview" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__base_dashbord__icon_type__image +msgid "Image" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_button +msgid "Is Button" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_double +msgid "Is Double" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__is_holiday_workflow +msgid "Is Holiday Workflow" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_stage +msgid "Is Stage" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_state +msgid "Is State" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__is_workflow +msgid "Is Workflow" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_users__dashboard_card_orders +msgid "JSON storage for drag-and-drop card ordering preferences" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__write_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__write_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__write_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__write_uid +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__write_uid +msgid "Last Updated by" +msgstr "اخر تحديث بواسطة" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__write_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__write_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__write_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__write_date +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__write_date +msgid "Last Updated on" +msgstr "اخر تحديث على" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__line_ids +msgid "Lines" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__list_view_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__list_view_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__list_view_id +msgid "List View" +msgstr "عرض القائمة" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Load Model States" +msgstr "تحميل حالات النموذج" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Lower numbers appear first" +msgstr "الأرقام الأصغر تظهر أولاً" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Main accent color for the dashboard" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_primary_color +msgid "Main accent color for the dashboard (e.g., #0891b2)" +msgstr "" + +#. module: system_dashboard_classic +#: model:res.groups,name:system_dashboard_classic.group_dashboard_manager +msgid "Manager" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Mark if this service has no financial impact" +msgstr "حدد إذا كانت هذه الخدمة بدون تأثير مالي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__model_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__model_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__model_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__model_id +msgid "Model" +msgstr "المديول" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Model Configuration" +msgstr "إعدادات النموذج" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__model_name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__model_name +msgid "Model Name" +msgstr "اسم المديول" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Monthly Attendance" +msgstr "الحضور الشهري" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__name +#: model:ir.model.fields,field_description:system_dashboard_classic.field_system_dashboard_classic_dashboard__name +msgid "Name" +msgstr "الإسم" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Optional: Custom form view for this service" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Optional: Custom list view for this service" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_annual_leave_chart_type__pie +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_attendance_hours_chart_type__pie +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_salary_slips_chart_type__pie +#: model:ir.model.fields.selection,name:system_dashboard_classic.selection__res_config_settings__dashboard_timesheet_chart_type__pie +msgid "Pie" +msgstr "دائري" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "Please select a valid model first." +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_primary_color +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Primary Color" +msgstr "اللون الأساسي" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Refresh Interval" +msgstr "فاصل التحديث" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_refresh_interval +msgid "Refresh Interval (seconds)" +msgstr "فاصل التحديث (بالثواني)" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Refresh States" +msgstr "تحديث الحالات" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__relation +msgid "Relation" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Remove All States" +msgstr "حذف جميع الحالات" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Salary Slips" +msgstr "قسائم الراتب" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_salary_slips_chart_type +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Salary Slips Chart" +msgstr "مخطط قسائم الراتب" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__search_field +msgid "Search Field" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_secondary_color +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Secondary Color" +msgstr "اللون الثانوي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_secondary_color +msgid "Secondary color for headers and dark elements (e.g., #1e293b)" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Select user groups..." +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_self_service +msgid "Self Service" +msgstr "الخدمة الذاتية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_self_service +msgid "Self Service?" +msgstr "الخدمة الذاتية؟" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Self Services" +msgstr "الخدمة الذاتية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__sequence +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__sequence +msgid "Sequence" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Service Name" +msgstr "اسم الخدمة" + +#. module: system_dashboard_classic +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_settings +msgid "Settings" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_annual_leave +msgid "Show Annual Leave" +msgstr "إظهار الإجازة السنوية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_attendance_hours +msgid "Show Attendance Hours" +msgstr "إظهار ساعات الحضور" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_attendance_section +msgid "Show Attendance Section" +msgstr "إظهار قسم الحضور والانصراف" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_salary_slips +msgid "Show Salary Slips" +msgstr "إظهار قسائم الراتب" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_timesheet +msgid "Show Weekly Timesheet" +msgstr "إظهار الجدول الزمني الأسبوعي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_show_work_timer +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Show Work Timer" +msgstr "إظهار مؤقت العمل" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Show or hide individual statistics cards" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_annual_leave +msgid "Show/Hide Annual Leave statistics card in dashboard header" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_attendance_hours +msgid "Show/Hide Attendance Hours statistics card in dashboard header" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_salary_slips +msgid "Show/Hide Salary Slips statistics card in dashboard header" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_timesheet +msgid "Show/Hide Weekly Timesheet statistics card in dashboard header" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_show_attendance_section +msgid "Show/Hide the Attendance Check-in/out section in dashboard header" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__stage_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__stage_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__stage_id +msgid "Stage" +msgstr "المرحلة" + +#. module: system_dashboard_classic +#: model:ir.actions.act_window,name:system_dashboard_classic.action_stage_stage +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_stage_stage_tree +msgid "Stages" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__state_id +#: model:ir.model.fields,field_description:system_dashboard_classic.field_node_state__state +msgid "State" +msgstr "الحالة" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "State/Stage Configuration" +msgstr "إعدادات الحالات/المراحل" + +#. module: system_dashboard_classic +#: model:ir.actions.act_window,name:system_dashboard_classic.action_node_state +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_node_state_tree +msgid "States" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Statistics Visibility" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_success_color +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Success Color" +msgstr "لون النجاح/الاتصال" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_success_color +msgid "Success/Online status color (e.g., #10b981)" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_system_dashboard_classic_dashboard +msgid "System Dashboard" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Thank You for" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "The action to open when clicking this card" +msgstr "الإجراء الذي يفتح عند النقر على هذه البطاقة" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "" +"The field path used to filter records for current user.\n" +"\n" +"Examples:\n" +"• 'employee_id.user_id' - For HR models (hr.leave, hr.expense, etc.)\n" +"• 'user_id' - For models with direct user reference (purchase.order, etc.)\n" +"• 'create_uid' - For records created by the user" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Theme Colors" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "There is already a record for this action." +msgstr "" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "This model has no states nor stages." +msgstr "هذا المديول ليس له مراحل موافقات." + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "This stage is already selected." +msgstr "" + +#. module: system_dashboard_classic +#. odoo-python +#: code:addons/system_dashboard_classic/models/config.py:0 +msgid "This state is already selected." +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Time between refreshes (30-3600 seconds)" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Timesheet Chart" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "To Approve" +msgstr "للموافقة والتعميد" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "To Track" +msgstr "للمتابعة والإطلاع" + +#. module: system_dashboard_classic +#: model:ir.model,name:system_dashboard_classic.model_res_users +msgid "User" +msgstr "المستخدم" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_stage_stage__value +msgid "Value" +msgstr "" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_warning_color +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.res_config_settings_view_form +msgid "Warning Color" +msgstr "لون التنبيه/المتبقي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,help:system_dashboard_classic.field_res_config_settings__dashboard_warning_color +msgid "Warning/Remaining balance color (e.g., #f59e0b)" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "Weekly Timesheet" +msgstr "الجداول الأسبوعية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_timesheet_chart_type +msgid "Weekly Timesheet Chart" +msgstr "مخطط الجدول الزمني الأسبوعي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__is_financial_impact +msgid "Without Financial Impact?" +msgstr "بدون تأثير مالي؟" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "days" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "days left" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "days total" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "e.g., employee_id.user_id or user_id" +msgstr "" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "fa-plane" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "h done" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "h left" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "h planned" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "h worked" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "hours" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "received" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "remaining" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "slips" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "لم تسجل خروج بعد" +msgstr "" + +#. module: system_dashboard_classic +#. odoo-javascript +#. odoo-python +#: code:addons/system_dashboard_classic/models/dashboard.py:0 +#: code:addons/system_dashboard_classic/static/src/components/dashboard/dashboard.xml:0 +msgid "لم تسجل دخول بعد" +msgstr "" \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/models/__init__.py b/odex30_base/system_dashboard_classic/models/__init__.py new file mode 100644 index 0000000..e33d369 --- /dev/null +++ b/odex30_base/system_dashboard_classic/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from . import dashboard +from . import config +from . import res_users +from . import res_config_settings diff --git a/odex30_base/system_dashboard_classic/models/config.py b/odex30_base/system_dashboard_classic/models/config.py new file mode 100644 index 0000000..5e26bff --- /dev/null +++ b/odex30_base/system_dashboard_classic/models/config.py @@ -0,0 +1,439 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models, api, _ +from odoo.exceptions import ValidationError + + +class BaseDashboard(models.Model): + _name = 'base.dashbord' + _description = 'Dashboard Builder' + _order = 'sequence' + + sequence = fields.Integer() + + name = fields.Char( + string='Card name', + translate=True + ) + + model_name = fields.Char(string='Model Name') + + model_id = fields.Many2one( + string='Model', + comodel_name='ir.model' + ) + + line_ids = fields.One2many( + string='Lines', + comodel_name='base.dashbord.line', + inverse_name='board_id' + ) + + icon_type = fields.Selection( + [('image', 'Image'), ('icon', 'Icon Library')], + string='Icon Type', + default='image', + required=True + ) + + icon_name = fields.Char( + string='Icon Class', + help="FontAwesome class (e.g., 'fa-plane', 'fa-users'). See https://fontawesome.com/v4/icons/" + ) + + icon_preview_html = fields.Html( + compute='_compute_icon_preview', + string="Icon/Image Preview" + ) + + card_image = fields.Binary(string='Card Image') + + is_self_service = fields.Boolean(string='Self Service?') + is_financial_impact = fields.Boolean(string='Without Financial Impact?') + + form_view_id = fields.Many2one('ir.ui.view', string='Form View') + list_view_id = fields.Many2one('ir.ui.view', string='List View') + action_id = fields.Many2one('ir.actions.act_window', string='Action') + field_id = fields.Many2one('ir.model.fields', string='Fields') + + is_button = fields.Boolean(string='Is Button') + is_stage = fields.Boolean(string='Is Stage', compute='_compute_field', store=True) + is_double = fields.Boolean(string='Is Double', compute='_compute_field', store=True) + is_state = fields.Boolean(string='Is State', compute='_compute_field', store=True) + + action_domain = fields.Char( + string='Action Domain', + compute='_compute_action_domain', + store=True + ) + action_context = fields.Char( + string='Action Context', + compute='_compute_action_domain', + store=True + ) + + relation = fields.Char(string='Relation') + search_field = fields.Char( + string='Search Field', + required=True, + default='employee_id.user_id' + ) + + @api.depends('icon_type', 'icon_name', 'card_image') + def _compute_icon_preview(self): + for record in self: + if record.icon_type == 'icon' and record.icon_name: + record.icon_preview_html = f'
' + elif record.icon_type == 'image' and record.card_image: + try: + img_rec = record.with_context(bin_size=False) + image_data = img_rec.card_image.decode('utf-8') if isinstance(img_rec.card_image, bytes) else img_rec.card_image + record.icon_preview_html = f'
' + except Exception: + record.icon_preview_html = '
' + else: + record.icon_preview_html = '
' + + def unlink_nodes(self): + for rec in self: + rec.is_button = False + nodes = self.env['node.state'].sudo().search([ + ('model_id', '=', rec.model_id.id), + ('is_workflow', '=', False) + ]) + nodes.sudo().unlink() + + def unlink(self): + for rec in self: + rec.unlink_nodes() + return super(BaseDashboard, self).unlink() + + @api.constrains('action_id') + def _check_action_id(self): + for record in self: + is_record = self.sudo().search_count([('action_id', '=', record.action_id.id)]) + if is_record > 1: + raise ValidationError(_('There is already a record for this action.')) + + @api.depends('action_id') + def _compute_action_domain(self): + for record in self: + record.action_domain = record.action_id.domain if record.action_id else False + record.action_context = record.action_id.context if record.action_id else False + + @api.onchange('model_id') + def _get_stage_value(self): + for rec in self: + if rec.model_id: + rec.model_name = rec.model_id.model + + @api.depends('model_name') + def _compute_field(self): + for rec in self: + rec.is_stage = False + rec.is_double = False + rec.is_state = False + + if rec.model_id and rec.model_name and rec.model_name in self.env: + model = self.env[rec.model_name] + # hr.holidays has special case (can have both state and stage) + if rec.model_name in ('hr.holidays', 'hr.leave'): + rec.is_double = True + elif 'state' in model._fields: + rec.is_state = True + elif 'stage_id' in model._fields: + rec.is_stage = True + + @api.depends('name', 'model_id') + def _compute_display_name(self): + for record in self: + if record.name: + record.display_name = record.name + elif record.model_id: + record.display_name = record.model_id.name + else: + record.display_name = _('Dashboard Card') + + def _get_stage(self, rel): + """Get stages from relation model and create them in intermediate table""" + for rec in self: + current_model = self.env['stage.stage'].sudo().search([('model_id', '=', rec.model_id.id)]) + stage_ids = self.env[rel].sudo().search([]) + + if not current_model: + for stage in stage_ids: + value = stage.with_context(lang=self.env.user.lang).name + self.env['stage.stage'].sudo().create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'stage_id': stage.id, + 'name': stage.name, + 'value': value + }) + else: + self.update_selection() + + def update_selection(self): + """Update states/stages when dynamic workflow changes""" + odoo_dynamic_workflow = self.env['ir.module.module'].sudo().search([ + ('name', '=', 'odoo_dynamic_workflow') + ]) + + for rec in self: + if odoo_dynamic_workflow and odoo_dynamic_workflow.state == 'installed': + if rec.model_name and rec.model_name in self.env: + model = self.env[rec.model_name] + + work_folow_active = self.env['odoo.workflow'].sudo().search([ + ('model_id', '=', rec.model_id.id), + ('active', '=', True) + ]) + + state = self.env['node.state'].sudo().search([('is_workflow', '=', True)]) + work_folow_name = work_folow_active.node_ids.filtered( + lambda r: r.code_node == False and r.active == True + ).mapped("node_name") + state_name = state.mapped('state') + + if not rec.is_stage and rec.model_name not in ('hr.holidays', 'hr.leave'): + for line in work_folow_active.node_ids: + if not line.code_node and line.active: + if not self.env['node.state'].sudo().search([('state', '=', line.node_name)]): + self.env['node.state'].create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'action_id': rec.action_id.id, + 'state': line.node_name, + 'name': line.name, + 'is_workflow': True + }) + + diffs = list(set(state_name) - set(work_folow_name)) + self.env['node.state'].sudo().search([('state', 'in', diffs)]).unlink() + + # Handle stage updates + if rec.is_stage: + rel = self.env['ir.model.fields'].sudo().search([ + ('model_id', '=', rec.model_id.id), + ('name', '=', 'stage_id') + ]) + current_model = self.env['stage.stage'].sudo().search([('model_id', '=', rec.model_id.id)]).ids + + if rel: + rel_ids = self.env[rel.relation].sudo().search([]) + for r in rel_ids: + if r.id not in current_model: + self.env['stage.stage'].create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'stage_id': r.id, + 'name': r.name + }) + + def compute_selection(self): + """Compute states or stages depending on chosen model""" + for rec in self: + rec.is_button = True + + if not rec.model_name or rec.model_name not in self.env: + raise ValidationError(_('Please select a valid model first.')) + + model = self.env[rec.model_name] + current_model = self.env['node.state'].sudo().search([('model_id', '=', rec.model_id.id)]) + + # Handle hr.holidays/hr.leave (can have both states and stages) + if rec.model_name in ('hr.holidays', 'hr.leave'): + rec.is_double = True + if not current_model: + if 'state' in model._fields: + nodes = model._fields.get('state')._description_selection(self.env) + for node in nodes: + self.env['node.state'].create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'action_id': rec.action_id.id, + 'state': node[0], + 'name': node[1] + }) + + rel = self.env['ir.model.fields'].sudo().search([ + ('model_id', '=', rec.model_id.id), + ('name', '=', 'stage_id') + ]) + if rel: + rel_ids = self.env[rel.relation].sudo().search([]) + for r in rel_ids: + if hasattr(r, 'state') and r.state == 'approved': + self.env['node.state'].create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'action_id': rec.action_id.id, + 'stage_id': r.id, + 'name': r.name, + 'is_holiday_workflow': True + }) + else: + self.update_selection() + + # Handle state-based models + elif 'state' in model._fields: + if not current_model: + nodes = model._fields.get('state')._description_selection(self.env) + for node in nodes: + self.env['node.state'].create({ + 'model_id': rec.model_id.id, + 'form_view_id': rec.form_view_id.id, + 'list_view_id': rec.list_view_id.id, + 'action_id': rec.action_id.id, + 'state': node[0], + 'name': node[1] + }) + else: + self.update_selection() + + # Handle stage-based models + elif 'stage_id' in model._fields: + rel = self.env['ir.model.fields'].sudo().search([ + ('model_id', '=', rec.model_id.id), + ('name', '=', 'stage_id') + ]) + if rel: + self._get_stage(rel.relation) + + else: + raise ValidationError(_('This model has no states nor stages.')) + + +class BaseDashboardLine(models.Model): + _name = 'base.dashbord.line' + _description = 'Dashboard Builder Line' + + name = fields.Char(string='Name') + + group_ids = fields.Many2many( + string='Groups', + comodel_name='res.groups' + ) + + board_id = fields.Many2one( + string='Dashboard', + comodel_name='base.dashbord', + ondelete='cascade' + ) + + state_id = fields.Many2one( + string='State', + comodel_name='node.state' + ) + + stage_id = fields.Many2one( + string='Stage', + comodel_name='stage.stage' + ) + + model_id = fields.Many2one( + string='Model', + comodel_name='ir.model' + ) + + model_name = fields.Char(string='Model Name') + sequence = fields.Integer(string='Sequence') + + @api.onchange('state_id') + def onchange_state_id(self): + if self.state_id: + state_ids = [stat.state_id.id for stat in self.board_id.line_ids] + if state_ids.count(self.state_id.id) > 2: + raise ValidationError(_('This state is already selected.')) + + @api.onchange('stage_id') + def onchange_stage_id(self): + if self.stage_id: + stage_ids = [stat.stage_id.id for stat in self.board_id.line_ids] + if stage_ids.count(self.stage_id.id) > 2: + raise ValidationError(_('This stage is already selected.')) + + +class NodeState(models.Model): + _name = 'node.state' + _description = 'Dashboard Node State' + + name = fields.Char(string='Name', translate=True) + state = fields.Char(string='State', translate=True) + stage_id = fields.Char(string='Stage', readonly=True) + + is_workflow = fields.Boolean(string='Is Workflow', readonly=True) + is_holiday_workflow = fields.Boolean(string='Is Holiday Workflow', readonly=True) + + model_id = fields.Many2one( + string='Model', + comodel_name='ir.model', + readonly=True + ) + + form_view_id = fields.Many2one( + 'ir.ui.view', + string='Form View', + readonly=True + ) + list_view_id = fields.Many2one( + 'ir.ui.view', + string='List View', + readonly=True + ) + action_id = fields.Many2one( + 'ir.actions.act_window', + string='Action', + readonly=True + ) + + @api.depends('name', 'state') + def _compute_display_name(self): + for record in self: + if self.env.user.lang == 'en_US': + record.display_name = record.state or record.name or '' + else: + record.display_name = record.name or record.state or '' + + +class StageStage(models.Model): + _name = 'stage.stage' + _description = 'Dashboard Stage' + + name = fields.Char(string='Name', translate=True, readonly=True) + stage_id = fields.Char(string='Stage', readonly=True) + value = fields.Char(string='Value', readonly=True) + + model_id = fields.Many2one( + string='Model', + comodel_name='ir.model', + readonly=True + ) + + form_view_id = fields.Many2one( + 'ir.ui.view', + string='Form View', + readonly=True + ) + list_view_id = fields.Many2one( + 'ir.ui.view', + string='List View', + readonly=True + ) + action_id = fields.Many2one( + 'ir.actions.act_window', + string='Action', + readonly=True + ) + + @api.depends('name', 'value') + def _compute_display_name(self): + for record in self: + if self.env.user.lang == 'en_US': + record.display_name = record.name or '' + else: + record.display_name = record.value or record.name or '' diff --git a/odex30_base/system_dashboard_classic/models/dashboard.py b/odex30_base/system_dashboard_classic/models/dashboard.py new file mode 100644 index 0000000..60f1c60 --- /dev/null +++ b/odex30_base/system_dashboard_classic/models/dashboard.py @@ -0,0 +1,1066 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import AccessError, UserError +import ast +import json +from datetime import datetime, date +from dateutil.relativedelta import relativedelta, SA, SU, MO +from math import radians, sin, cos, sqrt, asin +import logging + +_logger = logging.getLogger(__name__) + + +class SystemDashboard(models.Model): + _name = 'system_dashboard_classic.dashboard' + _description = 'System Dashboard' + + name = fields.Char("") + + # ====================================================================== + # HELPER METHODS + # ====================================================================== + + def is_user(self, groups, user): + """Check whether current user is in a certain group""" + for group in groups: + if user.id in group.users.ids: + return True + return False + + def _get_bool_param(self, key, default=True): + """ + Helper to safely get boolean value from ir.config_parameter. + Odoo 18 saves boolean config_parameter as 'True'/'False' strings. + """ + ICP = self.env['ir.config_parameter'].sudo() + value = ICP.get_param(key, None) + # If never set, use default + if value is None or value == '': + return default + # Handle string 'False' and 'True' + if isinstance(value, str): + return value.lower() not in ('false', '0', 'no') + # Handle actual boolean + return bool(value) + + def _check_module_installed(self, module_name): + """Check if a module is installed""" + module = self.env['ir.module.module'].sudo().search( + [('name', '=', module_name)], limit=1 + ) + return bool(module and module.state == 'installed') + + def _get_user_timezone(self): + """Get user's timezone with fallback""" + tz = self.env.context.get('tz') or self.env.user.tz or 'UTC' + try: + import pytz + return pytz.timezone(tz) + except Exception: + import pytz + return pytz.UTC + + # ====================================================================== + # MAIN DATA METHOD (Called by JavaScript) + # ====================================================================== + + @api.model + def get_data(self): + """ + Main RPC method called from JavaScript on dashboard load. + Returns user data, employee info, statistics, and card configurations. + """ + # Initialize return data structure + values = { + 'user': [], + 'employee': [], + 'timesheet': [], + 'leaves': [], + 'payroll': [], + 'attendance': [], + 'attendance_hours': [], + 'cards': [], + 'chart_types': {}, + 'chart_colors': {}, + 'card_orders': {}, + 'stats_visibility': {}, + 'celebration': {}, + 'gender_info': {}, + 'refresh_settings': {}, + 'enable_attendance_button': False, + 'job_english': '', + } + + ICP = self.env['ir.config_parameter'].sudo() + + # Load chart type settings + values['chart_types'] = { + 'annual_leave': ICP.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'), + 'salary_slips': ICP.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'), + 'timesheet': ICP.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'), + 'attendance_hours': ICP.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'), + } + + # Load chart colors from settings + values['chart_colors'] = { + 'primary': ICP.get_param('system_dashboard_classic.primary_color', '#0891b2'), + 'warning': ICP.get_param('system_dashboard_classic.warning_color', '#f59e0b'), + 'secondary': ICP.get_param('system_dashboard_classic.secondary_color', '#1e293b'), + 'success': ICP.get_param('system_dashboard_classic.success_color', '#10b981'), + } + + # Load attendance button setting + values['enable_attendance_button'] = self._get_bool_param( + 'system_dashboard_classic.enable_attendance_button', 'False' + ) + + # Load stats visibility settings + values['stats_visibility'] = { + 'show_annual_leave': self._get_bool_param('system_dashboard_classic.show_annual_leave'), + 'show_salary_slips': self._get_bool_param('system_dashboard_classic.show_salary_slips'), + 'show_timesheet': self._get_bool_param('system_dashboard_classic.show_timesheet'), + 'show_attendance_hours': self._get_bool_param('system_dashboard_classic.show_attendance_hours'), + 'show_attendance_section': self._get_bool_param('system_dashboard_classic.show_attendance_section'), + } + + # Initialize celebration data (birthday and work anniversary) + values['celebration'] = { + 'is_birthday': False, + 'is_anniversary': False, + 'anniversary_years': 0, + } + + # Initialize gender-aware data for personalized experience + values['gender_info'] = { + 'gender': 'male', + 'honorific': 'أستاذ', + 'pronoun_you': 'ك', + 'verb_suffix': 'تَ', + } + + # Get current user and employee + user = self.env.user + user_id = self.env['res.users'].sudo().search_read( + [('id', '=', user.id)], limit=1 + ) + + employee_id = self.env['hr.employee'].sudo().search_read( + [('user_id', '=', user.id)], limit=1 + ) + + employee_object = self.env['hr.employee'].sudo().search( + [('user_id', '=', user.id)], limit=1 + ) + + # Get job title in English + if employee_object and employee_object.job_id: + if hasattr(employee_object.job_id, 'english_name') and employee_object.job_id.english_name: + job_english = employee_object.job_id.english_name + else: + job_english = employee_object.job_id.name or '' + else: + job_english = '' + + values['job_english'] = job_english + + # ========================================== + # CELEBRATION & GENDER DETECTION + # ========================================== + today = date.today() + + if employee_object: + # Gender-aware personalization + employee_gender = getattr(employee_object, 'gender', 'male') or 'male' + if employee_gender == 'female': + values['gender_info'] = { + 'gender': 'female', + 'honorific': 'أستاذة', + 'pronoun_you': 'كِ', + 'verb_suffix': 'تِ', + } + + # Check for BIRTHDAY + birthday = getattr(employee_object, 'birthday', None) + if birthday: + if birthday.month == today.month and birthday.day == today.day: + values['celebration']['is_birthday'] = True + + # Check for WORK ANNIVERSARY + joining_date = None + if employee_object.contract_id and employee_object.contract_id.date_start: + joining_date = employee_object.contract_id.date_start + + if joining_date: + if joining_date.month == today.month and joining_date.day == today.day: + years = today.year - joining_date.year + if years > 0: + values['celebration']['is_anniversary'] = True + values['celebration']['anniversary_years'] = years + + # ========================================== + # ATTENDANCE DATA + # ========================================== + attendance_date = {'is_attendance': False, 'time': False} + + if self._check_module_installed('hr_attendance'): + self._load_attendance_data(employee_object, attendance_date) + + # ========================================== + # LEAVES DATA + # ========================================== + leaves_data = {'taken': 0, 'remaining_leaves': 0, 'is_module_installed': False} + + if self._check_module_installed('hr_holidays'): + self._load_leaves_data(employee_object, leaves_data) + + # ========================================== + # PAYROLL DATA + # ========================================== + payroll_data = {'taken': 0, 'payslip_remaining': 0, 'is_module_installed': False} + + if self._check_module_installed('hr_payroll'): + self._load_payroll_data(employee_object, payroll_data) + + # ========================================== + # TIMESHEET DATA + # ========================================== + timesheet_data = {'taken': 0, 'timesheet_remaining': 0, 'is_module_installed': False} + + if self._check_module_installed('hr_timesheet'): + self._load_timesheet_data(employee_object, timesheet_data) + + # ========================================== + # ATTENDANCE HOURS DATA + # ========================================== + attendance_hours_data = {'plan_hours': 0, 'official_hours': 0, 'is_module_installed': False} + self._load_attendance_hours_data(employee_object, attendance_hours_data, today) + + # ========================================== + # DASHBOARD CARDS + # ========================================== + self._load_dashboard_cards(user, values) + + # ========================================== + # FINALIZE RETURN DATA + # ========================================== + values['user'].append(user_id) + values['employee'].append(employee_id) + values['leaves'].append(leaves_data) + values['payroll'].append(payroll_data) + values['attendance'].append(attendance_date) + values['timesheet'].append(timesheet_data) + values['attendance_hours'].append(attendance_hours_data) + + # Load user's saved card order preferences + try: + card_orders_json = self.env.user.dashboard_card_orders or '{}' + values['card_orders'] = json.loads(card_orders_json) + except (json.JSONDecodeError, TypeError, AttributeError): + values['card_orders'] = {} + + # Load periodic refresh settings + values['refresh_settings'] = { + 'enabled': self._get_bool_param('system_dashboard_classic.refresh_enabled', 'False'), + 'interval': int(ICP.get_param('system_dashboard_classic.refresh_interval', '60')) + } + + return values + + # ====================================================================== + # DATA LOADING HELPER METHODS + # ====================================================================== + + def _load_attendance_data(self, employee, data): + """Load attendance check-in/out status""" + import pytz + + if not employee: + return + + try: + # Try custom attendance model first (attendance.attendance) + if 'attendance.attendance' in self.env: + last_attendance = self.env['attendance.attendance'].sudo().search( + [('employee_id', '=', employee.id)], limit=1, order="name desc" + ) + + if last_attendance: + data['is_attendance'] = (last_attendance.action == 'sign_in') + user_tz = self._get_user_timezone() + + # Odoo 18: Use to_datetime instead of from_string + if last_attendance.name: + time_object = fields.Datetime.to_datetime(last_attendance.name) + if time_object: + time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + data['time'] = time_in_timezone + + # Fallback to standard hr.attendance model + elif 'hr.attendance' in self.env: + last_attendance = self.env['hr.attendance'].sudo().search( + [('employee_id', '=', employee.id)], limit=1, order="check_in desc" + ) + + if last_attendance: + data['is_attendance'] = not last_attendance.check_out + user_tz = self._get_user_timezone() + + check_time = last_attendance.check_out or last_attendance.check_in + if check_time: + time_object = fields.Datetime.to_datetime(check_time) + if time_object: + time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + data['time'] = time_in_timezone + + except Exception as e: + _logger.warning(f"Error loading attendance data: {e}") + + def _load_leaves_data(self, employee, data): + """Load annual leave statistics""" + if not employee: + return + + try: + data['is_module_installed'] = True + + allocations = self.env['hr.holidays'].sudo().search( + [('employee_id', '=', employee.id), ('holiday_status_id.leave_type', '=', 'annual'), + ('type', '=', 'add'),('check_allocation_view', '=', 'balance') ]) + + + if allocations: + taken = sum(alloc.leaves_taken for alloc in allocations) + remaining = sum(alloc.remaining_leaves for alloc in allocations) + data['taken'] = taken + data['remaining_leaves'] = remaining + + except Exception as e: + _logger.warning(f"Error loading leaves data: {e}") + + def _load_payroll_data(self, employee, data): + """Load payslip statistics for current year""" + if not employee: + return + + try: + data['is_module_installed'] = True + + first_day = date(date.today().year, 1, 1) + last_day = date(date.today().year, 12, 31) + + payslip_count = self.env['hr.payslip'].sudo().search_count([ + ('employee_id', '=', employee.id), + ('date_from', '>=', first_day), + ('date_to', '<=', last_day) + ]) + + # Calculate expected slips based on contract start date + contract = self.env['hr.contract'].sudo().search([ + ('employee_id', '=', employee.id), + ('state', '=', 'open') + ], limit=1) + + expected_slips = 12 + if contract and contract.date_start and contract.date_start.year == date.today().year: + expected_slips = 12 - contract.date_start.month + 1 + + remaining_slips = max(0, expected_slips - payslip_count) + + data['taken'] = payslip_count + data['payslip_remaining'] = remaining_slips + + except Exception as e: + _logger.warning(f"Error loading payroll data: {e}") + + def _load_timesheet_data(self, employee, data): + """Load weekly timesheet statistics""" + if not employee: + return + + try: + data['is_module_installed'] = True + + # Determine week boundaries + calendar = employee.resource_calendar_id + today = date.today() + + # Default week start (Saturday) + start_date = today + relativedelta(weeks=-1, days=1, weekday=SA) + end_date = start_date + relativedelta(days=6) + + # Calculate total working hours + total_working_hours = 0 + if calendar: + total_working_hours = calendar.get_work_hours_count( + datetime.combine(start_date, datetime.min.time()), + datetime.combine(end_date, datetime.max.time()), + compute_leaves=True + ) + else: + total_working_hours = 40.0 # Default fallback + + # Get actual timesheet hours + timesheet = self.env['account.analytic.line'].sudo().search([ + ('employee_id', '=', employee.id), + ('date', '>=', start_date), + ('date', '<=', end_date) + ]) + + done_hours = sum(sheet.unit_amount for sheet in timesheet) + + data['taken'] = done_hours + data['timesheet_remaining'] = max(0, total_working_hours - done_hours) + + except Exception as e: + _logger.warning(f"Error loading timesheet data: {e}") + + def _load_attendance_hours_data(self, employee, data, today): + """Load monthly attendance hours statistics""" + if not employee: + return + + try: + # Try to use hr.attendance.transaction if available + if 'hr.attendance.transaction' in self.env: + first_day_of_month = date(today.year, today.month, 1) + + attendance_txns = self.env['hr.attendance.transaction'].sudo().search([ + ('employee_id', '=', employee.id), + ('date', '>=', first_day_of_month), + ('date', '<=', today) + ]) + + if employee.resource_calendar_id: + plan_hours_total = employee.resource_calendar_id.get_work_hours_count( + datetime.combine(first_day_of_month, datetime.min.time()), + datetime.combine(today, datetime.max.time()), + compute_leaves=True + ) + else: + plan_hours_total = sum(txn.plan_hours for txn in attendance_txns) + + official_hours_total = sum(txn.official_hours for txn in attendance_txns) + + data['plan_hours'] = round(plan_hours_total, 2) + data['official_hours'] = round(official_hours_total, 2) + data['is_module_installed'] = True + + # Fallback to standard hr.attendance + elif 'hr.attendance' in self.env: + first_day_of_month = date(today.year, today.month, 1) + + attendances = self.env['hr.attendance'].sudo().search([ + ('employee_id', '=', employee.id), + ('check_in', '>=', datetime.combine(first_day_of_month, datetime.min.time())), + ('check_in', '<=', datetime.combine(today, datetime.max.time())) + ]) + + if employee.resource_calendar_id: + plan_hours_total = employee.resource_calendar_id.get_work_hours_count( + datetime.combine(first_day_of_month, datetime.min.time()), + datetime.combine(today, datetime.max.time()), + compute_leaves=True + ) + else: + plan_hours_total = 8 * 22 # Default 8 hours * 22 work days + + official_hours_total = sum(att.worked_hours for att in attendances if att.worked_hours) + + data['plan_hours'] = round(plan_hours_total, 2) + data['official_hours'] = round(official_hours_total, 2) + data['is_module_installed'] = True + + except Exception as e: + _logger.warning(f"Error loading attendance hours data: {e}") + + def _load_dashboard_cards(self, user, values): + """Load dashboard card configurations""" + base = self.env['base.dashbord'].search([]) + + for models in base: + for model in models: + try: + model.with_user(user).check_access('read') + + mod = { + 'name': '', + 'model': '', + 'icon': '', + 'lines': [], + 'type': 'approve', + 'domain_to_follow': [] + } + + for line in model.line_ids: + if self.is_user(line.group_ids, user): + # Get card names with translations + card_name_ar = model.with_context(lang='ar_001').name or model.name + card_name_en = model.with_context(lang='en_US').name or model.name + + if not card_name_ar: + card_name_ar = model.model_id.with_context(lang='ar_001').name or model.model_id.name + if not card_name_en: + card_name_en = model.model_id.with_context(lang='en_US').name or model.model_id.name + + mod['name'] = card_name_ar if self.env.user.lang in ['ar_001', 'ar_SY'] else card_name_en + mod['name_arabic'] = card_name_ar + mod['name_english'] = card_name_en + mod['model'] = model.model_name + mod['image'] = model.card_image + mod['icon_type'] = model.icon_type + mod['icon_name'] = model.icon_name + + # Default Icon Logic + if not mod['image'] and not mod['icon_name']: + mod['icon_type'] = 'icon' + mod['icon_name'] = 'fa-th-large' + + # Build domain and state/stage info + self._build_card_line_data(model, line, mod, user) + + mod['lines'] = sorted(mod['lines'], key=lambda i: i['id']) + + if mod['name']: + values['cards'].append(mod) + + # Add self-service cards + if model.is_self_service: + self._add_self_service_card(model, user, values) + + except AccessError: + continue + + def _build_card_line_data(self, model, line, mod, user): + """Build domain and count data for a card line""" + state_or_stage = 'state' if line.state_id else 'stage_id' + state_click = line.state_id.state if line.state_id else int(line.stage_id.stage_id) + state_follow = line.state_id.state if line.state_id else int(line.stage_id.stage_id) + + action_domain_click = [] + action_domain_follow = [] + + if model.action_domain: + try: + dom_act = ast.literal_eval(model.action_domain) + for i in dom_act: + if i[0] != 'state': + action_domain_click.append(i) + action_domain_follow.append(i) + except (ValueError, SyntaxError): + pass + + # Handle hr.holidays workflow special case + if model.model_name == 'hr.holidays': + if self._check_module_installed('hr_holidays_workflow'): + action_domain_click.append('|') + action_domain_click.append(('stage_id.name', '=', line.state_id.state)) + + action_domain_click.append( + ('state', '=', line.state_id.state) if line.state_id else + ('stage_id', '=', int(line.stage_id.stage_id)) + ) + + domain_click = str(action_domain_click).replace('(', '[').replace(')', ']') + domain_click = ast.literal_eval(domain_click) + + domain_follow = str(action_domain_follow).replace('(', '[').replace(')', ']') + domain_follow = ast.literal_eval(domain_follow) + + try: + state_to_click = self.env[model.model_name].search_count(action_domain_click) + except Exception: + state_to_click = 0 + + mod['domain_to_follow'].append(state_follow) + action_domain_follow.append((state_or_stage, 'not in', mod['domain_to_follow'])) + + try: + state_to_follow = self.env[model.model_name].search_count(action_domain_follow) + except Exception: + state_to_follow = 0 + + domain_follow_js = str(action_domain_follow).replace('(', '[').replace(')', ']') + domain_follow_js = ast.literal_eval(domain_follow_js) + + # State name translation + state = "" + if line.state_id: + state = line.state_id.state if self.env.user.lang == 'en_US' else line.state_id.name + elif line.stage_id: + state = line.stage_id.name if self.env.user.lang == 'en_US' else line.stage_id.value + + mod['lines'].append({ + 'id': line.state_id.id if line.state_id else line.stage_id.id, + 'count_state_click': state_to_click, + 'count_state_follow': state_to_follow, + 'state_approval': _('') + '' + _(line.state_id.name) if line.state_id else '', + 'state_folow': _('All Records'), + 'domain_to_follow': state_follow, + 'form_view': model.form_view_id.id, + 'list_view': model.list_view_id.id, + 'domain_click': domain_click, + 'domain_follow': domain_follow_js, + 'context': model.action_context, + }) + + def _add_self_service_card(self, model, user, values): + """Add a self-service card to the values""" + card_name = model.name or model.model_id.name + service_action_domain = [] + + if model.action_domain: + try: + service_action = ast.literal_eval(model.action_domain) + for i in service_action: + if i[0] != 'state': + service_action_domain.append(i) + except (ValueError, SyntaxError): + pass + + if model.search_field: + service_action_domain.append((model.search_field, '=', user.id)) + + # Icon configuration + if not model.card_image and not model.icon_name: + model_icon_type = 'icon' + model_icon_name = 'fa-th-large' + else: + model_icon_type = model.icon_type + model_icon_name = model.icon_name + + try: + state_count = self.env[model.model_name].search_count(service_action_domain) + except Exception: + state_count = 0 + + values['cards'].append({ + 'type': 'selfs', + 'name': card_name, + 'name_english': card_name, + 'name_arabic': card_name, + 'model': model.model_name, + 'state_count': state_count, + 'image': model.card_image, + 'icon_type': model_icon_type, + 'icon_name': model_icon_name, + 'form_view': model.form_view_id.id, + 'list_view': model.list_view_id.id, + 'js_domain': service_action_domain, + 'context': model.action_context or {}, + }) + + # ====================================================================== + # PUBLIC API METHODS + # ====================================================================== + + @api.model + def get_public_dashboard_colors(self): + """Get dashboard colors for JavaScript theming""" + ICP = self.env['ir.config_parameter'].sudo() + return { + 'primary': ICP.get_param('system_dashboard_classic.primary_color', '#0891b2'), + 'secondary': ICP.get_param('system_dashboard_classic.secondary_color', '#1e293b'), + 'success': ICP.get_param('system_dashboard_classic.success_color', '#10b981'), + 'warning': ICP.get_param('system_dashboard_classic.warning_color', '#f59e0b'), + } + + # ====================================================================== + # GEOLOCATION HELPER + # ====================================================================== + + def _haversine_distance(self, lat1, lon1, lat2, lon2): + """ + Calculate the great-circle distance between two GPS points using Haversine formula. + Returns distance in meters. + """ + R = 6371000 # Earth's radius in meters + + lat1_rad, lon1_rad = radians(lat1), radians(lon1) + lat2_rad, lon2_rad = radians(lat2), radians(lon2) + + dlat = lat2_rad - lat1_rad + dlon = lon2_rad - lon1_rad + + a = sin(dlat / 2) ** 2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2) ** 2 + c = 2 * asin(sqrt(a)) + + return R * c + + # ====================================================================== + # CHECK-IN/CHECK-OUT + # ====================================================================== + + @api.model + def checkin_checkout(self, latitude=None, longitude=None): + """ + Check-in or Check-out with zone-based geolocation validation. + """ + import pytz + _logger = logging.getLogger(__name__) + _logger.info("=== checkin_checkout called ===") + _logger.info(f"User: {self.env.user.name}") + _logger.info(f"Lat/Long: {latitude}, {longitude}") + + user = self.env.user + employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1) + + if not employee: + is_arabic = 'ar' in self._context.get('lang', 'en_US') + msg = ("عذراً، لم يتم العثور على ملف وظيفي مرتبط بحسابك.\nيرجى مراجعة إدارة الموارد البشرية." + if is_arabic else + "Sorry, no employee profile found linked to your account.\nPlease contact HR department.") + return {'error': True, 'message': msg} + + # Zone validation (if attendance.zone model exists) + if 'attendance.zone' in self.env: + validation_result = self._validate_attendance_zone(employee, latitude, longitude) + if validation_result.get('error'): + return validation_result + + # Perform attendance action + return self._perform_attendance_action(employee, latitude, longitude) + + def _validate_attendance_zone(self, employee, latitude, longitude): + """Validate if employee is within allowed attendance zone""" + AttendanceZone = self.env['attendance.zone'].sudo() + is_arabic = 'ar' in self._context.get('lang', 'en_US') + + employee_zones = AttendanceZone.search([('employee_ids', 'in', employee.id)]) + general_zone = employee_zones.filtered(lambda z: z.general) + + if general_zone: + return {} # General zone allows from anywhere + + if not employee_zones: + msg = ("لم يتم تعيين منطقة حضور خاصة بك.\nيرجى التواصل مع إدارة النظام." + if is_arabic else + "No specific attendance zone assigned to you.\nPlease contact system administration.") + return {'error': True, 'message': msg} + + if not latitude or not longitude: + msg = ("يتطلب النظام الوصول إلى موقعك الجغرافي لتسجيل الحضور.\nيرجى تفعيل صلاحية الموقع في المتصفح والمحاولة مرة أخرى." + if is_arabic else + "System requires access to your location for attendance.\nPlease enable location permission in your browser and try again.") + return {'error': True, 'message': msg} + + # Check if within any zone + is_in_zone = False + closest_distance = float('inf') + allowed_range = 0 + valid_zones = 0 + + for zone in employee_zones: + if not zone.latitude or not zone.longitude or not zone.allowed_range: + continue + + try: + zone_lat = float(zone.latitude) + zone_lon = float(zone.longitude) + zone_range = float(zone.allowed_range) + except (ValueError, TypeError): + continue + + valid_zones += 1 + distance = self._haversine_distance(latitude, longitude, zone_lat, zone_lon) + + if distance <= zone_range: + is_in_zone = True + break + + if distance < closest_distance: + closest_distance = distance + allowed_range = zone_range + + if not is_in_zone: + if valid_zones == 0: + msg = ("بيانات منطقة الحضور غير مكتملة.\nيرجى التواصل مع إدارة النظام لتحديث الإحداثيات." + if is_arabic else + "Attendance zone data is incomplete.\nPlease contact system administration to update coordinates.") + return {'error': True, 'message': msg} + + distance_outside = int(closest_distance - allowed_range) + if distance_outside >= 1000: + distance_km = round(distance_outside / 1000, 1) + dist_str = f"{distance_km} كيلو متر" if is_arabic else f"{distance_km} kilometers" + else: + dist_str = f"{distance_outside} متر" if is_arabic else f"{distance_outside} meters" + + gender = getattr(employee, 'gender', 'male') or 'male' + if is_arabic: + if gender == 'female': + msg = f"عذراً، أنتِ تتواجدين خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {dist_str} تقريباً أو أكثر لتتمكني من التسجيل." + else: + msg = f"عذراً، أنت تتواجد خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {dist_str} تقريباً أو أكثر لتتمكن من التسجيل." + else: + msg = f"Sorry, you are outside the allowed attendance zone.\nPlease move approximately {dist_str} or more closer to be able to check in." + + return {'error': True, 'message': msg} + + return {} + + def _perform_attendance_action(self, employee, latitude, longitude): + """Perform the actual attendance check-in/out action""" + import pytz + + user_tz = self._get_user_timezone() + + # Try custom attendance model first + if 'attendance.attendance' in self.env: + Attendance = self.env['attendance.attendance'] + + last_attendance = Attendance.sudo().search( + [('employee_id', '=', employee.id)], limit=1, order="name desc" + ) + + vals = { + 'employee_id': employee.id, + 'action_type': 'manual', + 'latitude': str(latitude) if latitude else False, + 'longitude': str(longitude) if longitude else False, + } + + if last_attendance and last_attendance.action == 'sign_in': + vals['action'] = 'sign_out' + is_attendance = False + else: + vals['action'] = 'sign_in' + is_attendance = True + + Attendance.create(vals) + + # Fetch updated record + last_attendance = Attendance.sudo().search( + [('employee_id', '=', employee.id)], limit=1, order="name desc" + ) + + time_in_timezone = False + if last_attendance and last_attendance.name: + time_object = fields.Datetime.to_datetime(last_attendance.name) + if time_object: + time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + + return {'is_attendance': is_attendance, 'time': time_in_timezone} + + # Fallback to standard hr.attendance + elif 'hr.attendance' in self.env: + HrAttendance = self.env['hr.attendance'] + + last_attendance = HrAttendance.sudo().search( + [('employee_id', '=', employee.id)], limit=1, order="check_in desc" + ) + + if last_attendance and not last_attendance.check_out: + # Check out + last_attendance.write({'check_out': fields.Datetime.now()}) + is_attendance = False + check_time = last_attendance.check_out + else: + # Check in + new_attendance = HrAttendance.create({'employee_id': employee.id}) + is_attendance = True + check_time = new_attendance.check_in + + time_in_timezone = False + if check_time: + time_object = fields.Datetime.to_datetime(check_time) + if time_object: + time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + + return {'is_attendance': is_attendance, 'time': time_in_timezone} + + return {'error': True, 'message': 'Attendance module not configured'} + + # ====================================================================== + # CARD ORDER MANAGEMENT + # ====================================================================== + + @api.model + def save_card_order(self, order_type, card_ids): + """Save card order preference for current user""" + user = self.env.user + + try: + current_orders = json.loads(user.dashboard_card_orders or '{}') + except (json.JSONDecodeError, TypeError): + current_orders = {} + + current_orders[order_type] = card_ids + + user.sudo().write({ + 'dashboard_card_orders': json.dumps(current_orders) + }) + + return True + + @api.model + def get_card_order(self, order_type=None): + """Get card order preferences for current user""" + user = self.env.user + + try: + all_orders = json.loads(user.dashboard_card_orders or '{}') + except (json.JSONDecodeError, TypeError): + all_orders = {} + + if order_type: + return all_orders.get(order_type, []) + return all_orders + + # ====================================================================== + # REFRESH DATA METHOD (Lightweight) + # ====================================================================== + + @api.model + def get_refresh_data(self): + """ + Lightweight method for periodic refresh. + Returns only attendance status and approval count. + """ + import pytz + + result = {'attendance': [], 'approval_count': 0} + + user = self.env.user + employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1) + + if not employee: + return result + + # Attendance status + if 'attendance.attendance' in self.env: + last_attendance = self.env['attendance.attendance'].sudo().search( + [('employee_id', '=', employee.id)], order='name desc', limit=1 + ) + + if last_attendance: + is_attendance = (last_attendance.action == 'sign_in') + user_tz = self._get_user_timezone() + time_str = '' + + if last_attendance.name: + time_object = fields.Datetime.to_datetime(last_attendance.name) + if time_object: + time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + time_str = time_in_timezone.strftime('%Y-%m-%d %H:%M:%S') + + result['attendance'] = [{'is_attendance': is_attendance, 'time': time_str}] + + # Calculate approval count + config_lines = self.env['base.dashbord.line'].sudo().search([]) + approval_count = 0 + + for line in config_lines: + if not self.is_user(line.group_ids, user): + continue + + dashboard = line.board_id + if not dashboard or not dashboard.model_id: + continue + + domain = [] + + if dashboard.action_domain: + try: + dom_list = ast.literal_eval(dashboard.action_domain) + for item in dom_list: + if isinstance(item, (list, tuple)) and len(item) > 0 and item[0] != 'state': + domain.append(item) + except Exception: + pass + + if line.state_id: + domain.append(('state', '=', line.state_id.state)) + elif line.stage_id: + try: + target_stage_id = int(line.stage_id.stage_id) + domain.append(('stage_id', '=', target_stage_id)) + except (ValueError, TypeError): + pass + + try: + if dashboard.model_name in self.env: + line_count = self.env[dashboard.model_name].sudo().search_count(domain) + approval_count += line_count + except Exception: + pass + + result['approval_count'] = approval_count + return result + + # ====================================================================== + # WORK TIMER DATA + # ====================================================================== + + @api.model + def get_work_timer_data(self): + """Returns live work timer data for countdown display""" + employee = self.env.user.employee_id + + if not employee: + return {'enabled': False, 'reason': 'no_employee'} + + if not self._get_bool_param('system_dashboard_classic.show_work_timer', 'True'): + return {'enabled': False, 'reason': 'disabled_in_settings'} + + today = fields.Date.today() + + # Get calendar for planned hours + calendar = employee.resource_calendar_id + if calendar: + if hasattr(calendar, 'working_hours') and calendar.working_hours: + planned_hours = calendar.working_hours + elif hasattr(calendar, 'hours_per_day') and calendar.hours_per_day: + planned_hours = calendar.hours_per_day + else: + planned_hours = 8.0 + else: + planned_hours = 8.0 + + # Check attendance status + if 'attendance.attendance' in self.env: + 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': _('لم تسجل دخول بعد') + } + + if last_att.action == 'sign_out': + return { + 'enabled': True, + 'status': 'checked_out', + 'hours_worked': getattr(last_att, 'attendance_duration', 0), + 'hours_worked_formatted': getattr(last_att, 'attendance_duration_hhmmss', '00:00:00'), + 'planned_hours': planned_hours + } + + # Currently checked in + last_sign_out = self.env['attendance.attendance'].sudo().search([ + ('employee_id', '=', employee.id), + ('action_date', '=', today), + ('action', '=', 'sign_out'), + ('id', '<', last_att.id) + ], order='name DESC', limit=1) + + previous_duration = getattr(last_sign_out, 'attendance_duration', 0) if last_sign_out else 0 + + sign_in_utc = last_att.name + sign_in_local = fields.Datetime.context_timestamp(self, sign_in_utc) + + return { + 'enabled': True, + 'status': 'checked_in', + 'sign_in_time': sign_in_local.isoformat(), + 'planned_hours': planned_hours, + 'planned_seconds': int(planned_hours * 3600), + 'previous_duration': previous_duration, + 'previous_seconds': int(previous_duration * 3600) + } + + return {'enabled': False, 'reason': 'attendance_module_not_available'} diff --git a/odex30_base/system_dashboard_classic/models/res_config_settings.py b/odex30_base/system_dashboard_classic/models/res_config_settings.py new file mode 100644 index 0000000..5a7e033 --- /dev/null +++ b/odex30_base/system_dashboard_classic/models/res_config_settings.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models, api + + +class DashboardConfigSettings(models.TransientModel): + """Dashboard Theme Settings - Configurable colors from UI""" + _inherit = 'res.config.settings' + + # ========================================================================= + # THEME COLORS + # ========================================================================= + # Odoo 18: Use config_parameter directly - no need for get_values/set_values + + dashboard_primary_color = fields.Char( + string='Primary Color', + config_parameter='system_dashboard_classic.primary_color', + default='#0891b2', + help='Main accent color for the dashboard (e.g., #0891b2)' + ) + + dashboard_secondary_color = fields.Char( + string='Secondary Color', + config_parameter='system_dashboard_classic.secondary_color', + default='#1e293b', + help='Secondary color for headers and dark elements (e.g., #1e293b)' + ) + + dashboard_success_color = fields.Char( + string='Success Color', + config_parameter='system_dashboard_classic.success_color', + default='#10b981', + help='Success/Online status color (e.g., #10b981)' + ) + + dashboard_warning_color = fields.Char( + string='Warning Color', + config_parameter='system_dashboard_classic.warning_color', + default='#f59e0b', + help='Warning/Remaining balance color (e.g., #f59e0b)' + ) + + # ========================================================================= + # STATISTICS VISIBILITY + # ========================================================================= + # Odoo 18: Boolean fields with config_parameter work correctly now + + dashboard_show_annual_leave = fields.Boolean( + string='Show Annual Leave', + config_parameter='system_dashboard_classic.show_annual_leave', + default=True, + help='Show/Hide Annual Leave statistics card in dashboard header' + ) + + dashboard_show_salary_slips = fields.Boolean( + string='Show Salary Slips', + config_parameter='system_dashboard_classic.show_salary_slips', + default=True, + help='Show/Hide Salary Slips statistics card in dashboard header' + ) + + dashboard_show_timesheet = fields.Boolean( + string='Show Weekly Timesheet', + config_parameter='system_dashboard_classic.show_timesheet', + default=True, + help='Show/Hide Weekly Timesheet statistics card in dashboard header' + ) + + dashboard_show_attendance_hours = fields.Boolean( + string='Show Attendance Hours', + config_parameter='system_dashboard_classic.show_attendance_hours', + default=True, + help='Show/Hide Attendance Hours statistics card in dashboard header' + ) + + dashboard_show_attendance_section = fields.Boolean( + string='Show Attendance Section', + config_parameter='system_dashboard_classic.show_attendance_section', + default=True, + help='Show/Hide the Attendance Check-in/out section in dashboard header' + ) + + # ========================================================================= + # ATTENDANCE SETTINGS + # ========================================================================= + + dashboard_enable_attendance_button = fields.Boolean( + string='Enable Check-in/out Button', + config_parameter='system_dashboard_classic.enable_attendance_button', + default=False, + help='Enable/Disable the Check-in/Check-out button functionality' + ) + + dashboard_show_work_timer = fields.Boolean( + string='Show Work Timer', + config_parameter='system_dashboard_classic.show_work_timer', + default=False, + help='Display live work hour countdown showing remaining time until end of planned shift' + ) + + # ========================================================================= + # CHART TYPE SETTINGS + # ========================================================================= + + dashboard_annual_leave_chart_type = fields.Selection( + [('donut', 'Donut'), ('pie', 'Pie')], + default='donut', + string='Annual Leave Chart', + config_parameter='system_dashboard_classic.annual_leave_chart_type' + ) + + dashboard_salary_slips_chart_type = fields.Selection( + [('donut', 'Donut'), ('pie', 'Pie')], + default='donut', + string='Salary Slips Chart', + config_parameter='system_dashboard_classic.salary_slips_chart_type' + ) + + dashboard_timesheet_chart_type = fields.Selection( + [('donut', 'Donut'), ('pie', 'Pie')], + default='donut', + string='Weekly Timesheet Chart', + config_parameter='system_dashboard_classic.timesheet_chart_type' + ) + + dashboard_attendance_hours_chart_type = fields.Selection( + [('donut', 'Donut'), ('pie', 'Pie')], + default='donut', + string='Attendance Hours Chart', + config_parameter='system_dashboard_classic.attendance_hours_chart_type' + ) + + # ========================================================================= + # AUTO-REFRESH SETTINGS + # ========================================================================= + + dashboard_refresh_enabled = fields.Boolean( + string='Enable Auto Refresh', + config_parameter='system_dashboard_classic.refresh_enabled', + default=False, + help='Automatically refresh attendance status and approval count at regular intervals' + ) + + dashboard_refresh_interval = fields.Integer( + string='Refresh Interval (seconds)', + config_parameter='system_dashboard_classic.refresh_interval', + default=60, + help='How often to refresh data (minimum: 30 seconds, maximum: 3600 seconds)' + ) + + # ========================================================================= + # API METHODS FOR JAVASCRIPT + # ========================================================================= + + @api.model + def get_stats_visibility(self): + """API method to get statistics visibility settings for JavaScript""" + ICP = self.env['ir.config_parameter'].sudo() + + def get_bool_param(key): + """ + Properly read boolean from ir.config_parameter. + In Odoo 18, Boolean fields with config_parameter save: + - 'True' when checkbox is checked + - 'False' when checkbox is unchecked + - None/empty when never saved (should default to True for visibility) + """ + value = ICP.get_param(key, None) + # If never set, default to True (show cards by default) + if value is None or value == '': + return True + # Handle string 'False' and 'True' + if isinstance(value, str): + return value.lower() not in ('false', '0', 'no') + # Handle actual boolean + return bool(value) + + return { + 'show_annual_leave': get_bool_param('system_dashboard_classic.show_annual_leave'), + 'show_salary_slips': get_bool_param('system_dashboard_classic.show_salary_slips'), + 'show_timesheet': get_bool_param('system_dashboard_classic.show_timesheet'), + 'show_attendance_hours': get_bool_param('system_dashboard_classic.show_attendance_hours'), + 'show_attendance_section': get_bool_param('system_dashboard_classic.show_attendance_section'), + } + + @api.model + def get_chart_types(self): + """API method to get chart type settings for JavaScript""" + ICP = self.env['ir.config_parameter'].sudo() + + return { + 'annual_leave_chart': ICP.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'), + 'salary_slips_chart': ICP.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'), + 'timesheet_chart': ICP.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'), + 'attendance_hours_chart': ICP.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'), + } diff --git a/odex30_base/system_dashboard_classic/models/res_users.py b/odex30_base/system_dashboard_classic/models/res_users.py new file mode 100644 index 0000000..ef74508 --- /dev/null +++ b/odex30_base/system_dashboard_classic/models/res_users.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + dashboard_card_orders = fields.Text( + string='Dashboard Card Orders', + help='JSON storage for drag-and-drop card ordering preferences' + ) diff --git a/odex30_base/system_dashboard_classic/security/ir.model.access.csv b/odex30_base/system_dashboard_classic/security/ir.model.access.csv new file mode 100644 index 0000000..7a63e7e --- /dev/null +++ b/odex30_base/system_dashboard_classic/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_dashboard_user,system_dashboard_classic.dashboard user,model_system_dashboard_classic_dashboard,base.group_user,1,0,0,0 +access_dashboard_manager,system_dashboard_classic.dashboard manager,model_system_dashboard_classic_dashboard,system_dashboard_classic.group_dashboard_manager,1,1,1,1 +access_base_dashbord_user,base.dashbord user,model_base_dashbord,base.group_user,1,0,0,0 +access_base_dashbord_manager,base.dashbord manager,model_base_dashbord,system_dashboard_classic.group_dashboard_config,1,1,1,1 +access_base_dashbord_line_user,base.dashbord.line user,model_base_dashbord_line,base.group_user,1,0,0,0 +access_base_dashbord_line_manager,base.dashbord.line manager,model_base_dashbord_line,system_dashboard_classic.group_dashboard_config,1,1,1,1 +access_node_state_user,node.state user,model_node_state,base.group_user,1,0,0,0 +access_node_state_manager,node.state manager,model_node_state,system_dashboard_classic.group_dashboard_config,1,1,1,1 +access_stage_stage_user,stage.stage user,model_stage_stage,base.group_user,1,0,0,0 +access_stage_stage_manager,stage.stage manager,model_stage_stage,system_dashboard_classic.group_dashboard_config,1,1,1,1 diff --git a/odex30_base/system_dashboard_classic/security/security.xml b/odex30_base/system_dashboard_classic/security/security.xml new file mode 100644 index 0000000..1865e54 --- /dev/null +++ b/odex30_base/system_dashboard_classic/security/security.xml @@ -0,0 +1,24 @@ + + + + + + Dashboard + 150 + + + + + Manager + + + + + + + Configuration + + + + + diff --git a/odex30_base/system_dashboard_classic/static/description/icon.png b/odex30_base/system_dashboard_classic/static/description/icon.png new file mode 100644 index 0000000..52fb3c4 Binary files /dev/null and b/odex30_base/system_dashboard_classic/static/description/icon.png differ diff --git a/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.js b/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.js new file mode 100644 index 0000000..631c1d7 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.js @@ -0,0 +1,875 @@ +/** @odoo-module **/ + +import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { drawDonutChart } from "@system_dashboard_classic/lib/donut_chart"; +import { _t } from "@web/core/l10n/translation"; +import { session } from "@web/session"; + +/** + * System Dashboard Component + * + * Main OWL component for the employee self-service dashboard. + * Handles: + * - User profile display + * - Statistics cards (Leave, Payroll, Timesheet, Attendance) + * - Attendance check-in/out with geolocation + * - Service and approval cards with drag-drop + * - Theme customization + * - Auto-refresh + */ +export class SystemDashboard extends Component { + static template = "system_dashboard_classic.Dashboard"; + + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + this.notification = useService("notification"); + + this.sessionUserId = session.user_id; + this.sessionUserName = session.name; + this.sessionUserContext = session.user_context; + + this.state = useState({ + loading: true, + userData: null, + employeeData: null, + statistics: { + leaves: { taken: 0, remaining: 0, installed: false }, + payroll: { taken: 0, remaining: 0, installed: false }, + timesheet: { taken: 0, remaining: 0, installed: false }, + attendance_hours: { plan: 0, official: 0, installed: false }, + }, + attendance: { + isCheckedIn: false, + time: null, + }, + cards: { + service: [], + approve: [], + track: [], + }, + settings: { + colors: {}, + visibility: { + show_annual_leave: true, + show_salary_slips: true, + show_timesheet: true, + show_attendance_hours: true, + show_attendance_section: true, + }, + chartTypes: {}, + enableAttendance: false, + }, + celebration: { + isBirthday: false, + isAnniversary: false, + years: 0, + }, + genderInfo: {}, + activeTab: 'self_services', + refreshInterval: null, + clockTick: 0, + }); + + onWillStart(async () => { + await this.loadDashboardData(); + }); + + onMounted(() => { + this.applyThemeColors(); + this.startAutoRefresh(); + this.checkCelebration(); + this.animateCounters(); + this.renderCharts(); + this.startClockUpdate(); + }); + + onWillUnmount(() => { + this.stopAutoRefresh(); + this.stopClockUpdate(); + }); + } + + + + // ========================================================================= + // DATA LOADING + // ========================================================================= + + async loadDashboardData() { + try { + this.state.loading = true; + + const result = await this.orm.call( + "system_dashboard_classic.dashboard", // model + "get_data", // method + [], // args (positional arguments) + {} // kwargs (keyword arguments) + ); + + this.processData(result); + } catch (error) { + console.error("Error loading dashboard data:", error); + this.notification.add(_t("Failed to load dashboard data"), { + type: "danger", + }); + } finally { + this.state.loading = false; + } + } + + + processData(result) { + + + // User and Employee data + if (result.user && result.user[0]) { + this.state.userData = result.user[0][0]; + } + + if (result.employee && result.employee[0]) { + this.state.employeeData = result.employee[0][0]; + } + + // User and Employee data + if (result.user && result.user[0]) { + this.state.userData = result.user[0][0]; + } + + if (result.employee && result.employee[0]) { + this.state.employeeData = result.employee[0][0]; + } + + // Statistics + if (result.leaves && result.leaves[0]) { + const leaves = result.leaves[0]; + this.state.statistics.leaves = { + taken: leaves.taken || 0, + remaining: leaves.remaining_leaves || 0, + installed: leaves.is_module_installed || false, + }; + } + + if (result.payroll && result.payroll[0]) { + const payroll = result.payroll[0]; + this.state.statistics.payroll = { + taken: payroll.taken || 0, + remaining: payroll.payslip_remaining || 0, + installed: payroll.is_module_installed || false, + }; + } + + if (result.timesheet && result.timesheet[0]) { + const timesheet = result.timesheet[0]; + this.state.statistics.timesheet = { + taken: timesheet.taken || 0, + remaining: timesheet.timesheet_remaining || 0, + installed: timesheet.is_module_installed || false, + }; + } + + if (result.attendance_hours && result.attendance_hours[0]) { + const hours = result.attendance_hours[0]; + this.state.statistics.attendance_hours = { + plan: hours.plan_hours || 0, + official: hours.official_hours || 0, + installed: hours.is_module_installed || false, + }; + } + + // Attendance status + if (result.attendance && result.attendance[0]) { + const att = result.attendance[0]; + this.state.attendance = { + isCheckedIn: att.is_attendance || false, + time: att.time || null, + }; + } + + // Cards + const cards = result.cards || []; + + // Process approve cards (for "To Approve" tab) + const approveCards = cards.filter(c => c.type === 'approve').flatMap(c => + c.lines.map(l => ({ + ...c, + state_approval: l.state_approval, + count_state_click: l.count_state_click, + domain_click: l.domain_click, + cardId: `${c.model}-approve-${l.id}`, + tabType: 'approve' + })) + ); + + // Process track cards (for "To Track" tab) - same cards but different fields + const trackCards = cards.filter(c => c.type === 'approve').flatMap(c => + c.lines.map(l => ({ + ...c, + state_folow: l.state_folow, + count_state_follow: l.count_state_follow, + domain_follow: l.domain_follow, + cardId: `${c.model}-track-${l.id}`, + tabType: 'track' + })) + ); + + this.state.cards = { + service: cards.filter(c => c.type === 'selfs'), + approve: approveCards, + track: trackCards, + }; + + // Settings + this.state.settings = { + colors: result.chart_colors || {}, + visibility: result.stats_visibility || {}, + chartTypes: result.chart_types || {}, + enableAttendance: result.enable_attendance_button || false, + refreshSettings: result.refresh_settings || {}, + }; + + // Celebration + if (result.celebration) { + this.state.celebration = { + isBirthday: result.celebration.is_birthday || false, + isAnniversary: result.celebration.is_anniversary || false, + years: result.celebration.anniversary_years || 0, + }; + } + + // Gender info + this.state.genderInfo = result.gender_info || {}; + this.state.jobEnglish = result.job_english || ''; + } + + // ========================================================================= + // THEME & STYLING + // ========================================================================= + + applyThemeColors() { + const colors = this.state.settings.colors; + const root = document.documentElement; + + if (colors.primary) { + root.style.setProperty('--dash-primary', colors.primary); + root.style.setProperty('--dash-primary-light', this.lightenColor(colors.primary, 20)); + root.style.setProperty('--dash-primary-dark', this.darkenColor(colors.primary, 20)); + } + + if (colors.secondary) { + root.style.setProperty('--dash-secondary', colors.secondary); + } + + if (colors.warning) { + root.style.setProperty('--dash-warning', colors.warning); + } + + // Cache colors for next page load + try { + localStorage.setItem('dashboard_colors', JSON.stringify(colors)); + } catch (e) { + // Ignore storage errors + } + } + + lightenColor(hex, percent) { + return this.adjustColor(hex, percent); + } + + darkenColor(hex, percent) { + return this.adjustColor(hex, -percent); + } + + adjustColor(hex, percent) { + if (!hex) return hex; + hex = hex.replace('#', ''); + const num = parseInt(hex, 16); + const amt = Math.round(2.55 * percent); + const R = Math.min(255, Math.max(0, (num >> 16) + amt)); + const G = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amt)); + const B = Math.min(255, Math.max(0, (num & 0x0000FF) + amt)); + return `#${(0x1000000 + R * 0x10000 + G * 0x100 + B).toString(16).slice(1)}`; + } + + // ========================================================================= + // CELEBRATION + // ========================================================================= + + checkCelebration() { + if (this.state.celebration.isBirthday || this.state.celebration.isAnniversary) { + // Trigger confetti + if (typeof confetti !== 'undefined') { + confetti({ + particleCount: 100, + spread: 70, + origin: { y: 0.6 } + }); + } + } + } + + // ========================================================================= + // AUTO-REFRESH + // ========================================================================= + + startAutoRefresh() { + const settings = this.state.settings.refreshSettings || {}; + if (settings.enabled && settings.interval) { + const interval = Math.max(30, Math.min(3600, settings.interval)) * 1000; + this.refreshIntervalId = setInterval(() => { + this.refreshData(); + }, interval); + } + } + + stopAutoRefresh() { + if (this.refreshIntervalId) { + clearInterval(this.refreshIntervalId); + this.refreshIntervalId = null; + } + } + + // Clock update for attendance section + startClockUpdate() { + // Update immediately + this.updateClock(); + // Then update every second + this.clockIntervalId = setInterval(() => { + this.updateClock(); + }, 1000); + } + + stopClockUpdate() { + if (this.clockIntervalId) { + clearInterval(this.clockIntervalId); + this.clockIntervalId = null; + } + } + + updateClock() { + // Force re-render by updating a dummy state variable + // OWL will automatically call currentDateTime getter + this.state.clockTick = Date.now(); + } + + async refreshData() { + try { + const result = await this.orm.call( + "system_dashboard_classic.dashboard", + "get_refresh_data", + [] + ); + + if (result.attendance && result.attendance[0]) { + const att = result.attendance[0]; + this.state.attendance = { + isCheckedIn: att.is_attendance || false, + time: att.time || null, + }; + } + } catch (error) { + console.error("Error refreshing data:", error); + } + } + + // ========================================================================= + // ATTENDANCE + // ========================================================================= + + async onAttendanceClick() { + if (!this.state.settings.enableAttendance) { + return; + } + + // Get geolocation + let latitude = null; + let longitude = null; + + try { + const position = await this.getGeolocation(); + latitude = position.coords.latitude; + longitude = position.coords.longitude; + } catch (error) { + console.warn("Geolocation error:", error); + // Continue without location - server will validate + } + + try { + const result = await this.orm.call( + "system_dashboard_classic.dashboard", + "checkin_checkout", + [latitude, longitude] + ); + + if (result.error) { + this.notification.add(result.message, { type: "danger" }); + } else { + this.state.attendance = { + isCheckedIn: result.is_attendance, + time: result.time, + }; + + // Celebration on check-in + if (result.is_attendance && typeof confetti !== 'undefined') { + confetti({ + particleCount: 50, + spread: 60, + origin: { y: 0.7 } + }); + } + + const message = result.is_attendance ? + _t("Checked in successfully!") : + _t("Checked out successfully!"); + this.notification.add(message, { type: "success" }); + } + } catch (error) { + console.error("Attendance error:", error); + this.notification.add(_t("Failed to record attendance"), { type: "danger" }); + } + } + + getGeolocation() { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error("Geolocation not supported")); + return; + } + + navigator.geolocation.getCurrentPosition( + resolve, + reject, + { enableHighAccuracy: true, timeout: 10000 } + ); + }); + } + + // ========================================================================= + // CARD ACTIONS + // ========================================================================= + + onCardClick = (card) => { + + + if (!card?.model || !this.action) { + console.warn("Card or action unavailable"); + return; + } + + let context = card.context || {}; + + if (typeof context === "string" && context.includes('active_id')) { + console.warn("Skipping unsafe context:", context); + context = {}; + } + + + const domain = card.tabType === 'track' + ? (card.domain_follow || []) + : (card.js_domain || card.domain_click || []); + + const action = { + type: 'ir.actions.act_window', + name: card.name || card.model, + res_model: card.model, + view_mode: 'list,form', + views: [[false, 'list'], [false, 'form']], + domain: domain, + context: context, + target: 'current', + }; + + this.action.doAction(action); + }; + + + onCreateNewClick = (card) => { + + if (!card?.model || !this.action) { + console.warn("Card or action unavailable"); + return; + } + + let context = card.context || {}; + + if (typeof context === "string" && context.includes('active_id')) { + console.warn("Skipping unsafe context:", context); + context = {}; + } + + const action = { + type: 'ir.actions.act_window', + name: card.name || card.model, + res_model: card.model, + view_mode: 'form', + views: [[false, 'form']], + context: context, + target: 'current', + }; + + this.action.doAction(action); + }; + + // ========================================================================= + // TAB HANDLING + // ========================================================================= + + setActiveTab = (tab) => { + if (this.state) { + this.state.activeTab = tab; + } + } + + + + + // ========================================================================= + // COMPUTED GETTERS + // ========================================================================= + + get greeting() { + const hour = new Date().getHours(); + const isRtl = document.dir === 'rtl'; + + if (hour < 12) { + return isRtl ? 'صباح الخير' : 'Good Morning'; + } else if (hour < 18) { + return isRtl ? 'مساء الخير' : 'Good Afternoon'; + } else { + return isRtl ? 'مساء الخير' : 'Good Evening'; + } + } + + /** + * Get personalized greeting with gender-based honorific + * @returns {string} Greeting with honorific (e.g., "صباح الخير أستاذ") + */ + getPersonalizedGreeting() { + const greeting = this.greeting; + const genderInfo = this.state.genderInfo || {}; + const honorific = genderInfo.honorific || 'أستاذ'; + const isRtl = document.body.classList.contains('o_rtl'); + + // For RTL (Arabic), add honorific after greeting + // For LTR (English), greeting already includes title + return isRtl ? `${greeting} ${honorific}` : greeting; + } + + get userName() { + const greeting = this.getPersonalizedGreeting(); + const name = this.state.employeeData?.name || this.state.userData?.name || ''; + + // Return greeting on first line, name on second line + return name ? `${greeting}\n${name}` : greeting; + } + + get userJob() { + if (this.state.employeeData) { + return this.state.employeeData.job_id?.[1] || this.state.jobEnglish || ''; + } + return ''; + } + + get userEmployeeId() { + if (this.state.employeeData) { + // Use emp_no, pin, barcode, or fallback to ID (same as Odoo 14) + return this.state.employeeData.emp_no || + this.state.employeeData.pin || + this.state.employeeData.barcode || + this.state.employeeData.id || ''; + } else if (this.state.userData) { + return this.state.userData.id || ''; + } + return ''; + } + + get userImage() { + if (this.state.employeeData?.image_128) { + return `data:image/png;base64,${this.state.employeeData.image_128}`; + } + return '/web/static/img/placeholder.png'; + } + + get attendanceButtonText() { + return this.state.attendance.isCheckedIn ? + _t("Check Out") : + _t("Check In"); + } + + // Live clock - current date and time + get currentDateTime() { + const _ = this.state.clockTick; // Dependency for reactivity + const now = new Date(); + const isRtl = document.body.classList.contains('o_rtl'); + + const dayNames = isRtl + ? ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'] + : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + const monthNames = isRtl + ? ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + const dayName = dayNames[now.getDay()]; + const day = now.getDate(); + const month = monthNames[now.getMonth()]; + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + const seconds = String(now.getSeconds()).padStart(2, '0'); + + const dateStr = isRtl ? `${dayName}، ${day} ${month}` : `${dayName}, ${day} ${month}`; + const timeStr = `${hours}:${minutes}:${seconds}`; + + return { date: dateStr, time: timeStr }; + } + + // Last check-in/out time and label + get lastCheckInInfo() { + const isRtl = document.body.classList.contains('o_rtl'); + + if (this.state.attendance.isCheckedIn) { + // User is checked in - show last check-in time + const label = isRtl ? 'وقت تسجيل اخر دخول' : 'Last check in'; + return { + label: label, + time: this.state.attendance.time || '' + }; + } else { + // User is checked out - show last check-out time + const label = isRtl ? 'وقت تسجيل اخر خروج' : 'Last check out'; + return { + label: label, + time: this.state.attendance.time || '' + }; + } + } + + get attendanceTime() { + if (this.state.attendance.time) { + const date = new Date(this.state.attendance.time); + return date.toLocaleTimeString(); + } + return ''; + } + + get showSelfServices() { + return this.state.activeTab === 'self_services'; + } + + get showApprovals() { + return this.state.activeTab === 'to_approve'; + } + + get showTracking() { + return this.state.activeTab === 'to_track'; + } + + get hasApprovalCards() { + return this.state.cards.approve.length > 0; + } + + get approvalCount() { + return this.state.cards.approve.reduce((sum, card) => sum + (card.count_state_click || 0), 0); + } + + // ========================================================================= + // ANIMATED COUNTERS + // ========================================================================= + + /** + * Animate a number from 0 to target value with smooth easing + * @param {Element} element - The DOM element to animate + * @param {number} targetValue - Final number to display + * @param {number} duration - Animation duration in ms (default 1200) + * @param {string} suffix - Optional suffix like 'days', 'hours' (default '') + * @param {number} decimals - Decimal places to show (default 0) + */ + animateCounter(element, targetValue, duration = 1200, suffix = '', decimals = 0) { + if (!element || isNaN(targetValue)) return; + + const startTime = performance.now(); + const startValue = 0; + + // Easing function - easeOutQuad for smooth deceleration + const easeOutQuad = (t) => t * (2 - t); + + const animate = (currentTime) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + const easedProgress = easeOutQuad(progress); + const currentValue = startValue + (targetValue - startValue) * easedProgress; + + // Format the number + const displayValue = decimals > 0 ? currentValue.toFixed(decimals) : Math.round(currentValue); + element.textContent = displayValue + (suffix ? ' ' + suffix : ''); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); + } + + /** + * Animate all statistics counters on the dashboard + */ + animateCounters() { + // Wait for DOM to be ready + setTimeout(() => { + // Leaves counter + const leavesEl = this.root?.querySelector('.leave-center-value'); + if (leavesEl && this.state.statistics.leaves.installed) { + this.animateCounter(leavesEl, this.state.statistics.leaves.remaining, 1200, '', 0); + } + + // Payroll counter + const payrollEl = this.root?.querySelector('.payroll-center-value'); + if (payrollEl && this.state.statistics.payroll.installed) { + this.animateCounter(payrollEl, this.state.statistics.payroll.taken, 1200, '', 0); + } + + // Timesheet counter + const timesheetEl = this.root?.querySelector('.timesheet-center-value'); + if (timesheetEl && this.state.statistics.timesheet.installed) { + this.animateCounter(timesheetEl, this.state.statistics.timesheet.taken, 1200, '', 1); + } + + // Attendance hours counter + const attendanceEl = this.root?.querySelector('.attendance-center-value'); + if (attendanceEl && this.state.statistics.attendance_hours.installed) { + this.animateCounter(attendanceEl, this.state.statistics.attendance_hours.official, 1200, '', 1); + } + }, 100); + } + + // ========================================================================= + // CHART RENDERING + // ========================================================================= + + /** + * Render pie/donut charts for statistics cards using pluscharts library + */ + renderCharts() { + // Wait for DOM to be ready and pluscharts to be loaded + setTimeout(() => { + + + const colors = this.state.settings.colors || {}; + const primaryColor = colors.primary || '#0891b2'; + const secondaryColor = colors.secondary || '#1e293b'; + const warningColor = colors.warning || '#f59e0b'; + + // Render Leaves Chart (always render, gray if not installed) + this.renderLeaveChart(primaryColor, warningColor); + + // Render Payroll Chart (always render, gray if not installed) + this.renderPayrollChart(primaryColor, warningColor); + + // Render Timesheet Chart (always render, gray if not installed) + this.renderTimesheetChart(primaryColor, warningColor); + + // Render Attendance Hours Chart (always render, gray if not installed) + this.renderAttendanceChart(primaryColor, warningColor); + }, 200); + } + + renderLeaveChart(primaryColor, warningColor, pluschartsLib = window.pluscharts) { + const taken = this.state.statistics.leaves.taken || 0; + const remaining = this.state.statistics.leaves.remaining || 0; + const total = taken + remaining; + const installed = this.state.statistics.leaves.installed; + + // Use gray colors for empty state or not installed + const colors = (total === 0 || !installed) ? ['#d1d5db', '#d1d5db'] : [warningColor, primaryColor]; + + // Pass actual values, not percentages - donut_chart.js will calculate percentages + drawDonutChart( + '#chartContainer', + [ + { label: 'Used', value: taken }, + { label: 'Left', value: remaining } + ], + colors, + 'donut', + 140, + 140, + 15 + ); + } + + renderPayrollChart(primaryColor, warningColor) { + const taken = this.state.statistics.payroll.taken || 0; + const remaining = this.state.statistics.payroll.remaining || 0; + const total = taken + remaining; + const installed = this.state.statistics.payroll.installed; + + // Use gray colors for empty state or not installed + const colors = (total === 0 || !installed) ? ['#d1d5db', '#d1d5db'] : [primaryColor, warningColor]; + + // Pass actual values, not percentages + drawDonutChart( + '#chartPaylips', + [ + { label: 'Received', value: taken }, + { label: 'Remaining', value: remaining } + ], + colors, + 'donut', + 140, + 140, + 15 + ); + } + + renderTimesheetChart(primaryColor, warningColor) { + const taken = this.state.statistics.timesheet.taken || 0; + const remaining = this.state.statistics.timesheet.remaining || 0; + const total = taken + remaining; + const installed = this.state.statistics.timesheet.installed; + + // Use gray colors for empty state or not installed + const colors = (total === 0 || !installed) ? ['#d1d5db', '#d1d5db'] : [primaryColor, warningColor]; + + // Pass actual values, not percentages + drawDonutChart( + '#chartTimesheet', + [ + { label: 'Done', value: taken }, + { label: 'Left', value: remaining } + ], + colors, + 'donut', + 140, + 140, + 15 + ); + } + + renderAttendanceChart(primaryColor, warningColor) { + const plan = this.state.statistics.attendance_hours.plan || 0; + const official = this.state.statistics.attendance_hours.official || 0; + const installed = this.state.statistics.attendance_hours.installed; + + // Use gray colors for empty state or not installed + const colors = (plan === 0 || !installed) ? ['#d1d5db', '#d1d5db'] : [primaryColor, warningColor]; + + // Pass actual values, not percentages + drawDonutChart( + '#chartAttendanceHours', + [ + { label: 'Worked', value: official }, + { label: 'Remaining', value: Math.max(0, plan - official) } + ], + colors, + 'donut', + 140, + 140, + 15 + ); + } +} + +// Register the component as a client action +registry.category("actions").add("system_dashboard_classic.Dashboard", SystemDashboard); diff --git a/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.xml b/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.xml new file mode 100644 index 0000000..10c9276 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/components/dashboard/dashboard.xml @@ -0,0 +1,333 @@ + + + + +
+ + +
+
+ + 🎂 + Happy Birthday! Wishing you a wonderful day! + + + + Thank You for Amazing Years! + +
+
+
+ +
+
+ +
+
+
+
+
+
+

+

+

+
+
+
+ + +
+
+ + +
+
+
+
+ +
+ + +
+
+

+ + days total + +

+

+ + days left + +

+
+
+
+ + days +
+
+

Annual Leave

+
+
+
+ + + +
+
+

+ + received + +

+

+ + remaining + +

+
+
+
+ + slips +
+
+

Salary Slips

+
+
+
+ + + +
+
+

+ + h done + +

+

+ + h left + +

+
+
+
+ + hours +
+
+

Weekly Timesheet

+
+
+
+ + + +
+
+

+ + h planned + +

+

+ + h worked + +

+
+
+
+ + hours +
+
+

Monthly Attendance

+
+
+
+
+
+ + + +
+
+ +

+ + +

+ +
+ +
+ +

+ +

+
+ + +
+ + لم تسجل خروج بعد + لم تسجل دخول بعد +
+
+

+ + + + + +
+
+
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ + + + + + + + + +

+

+
+
+ + Add New +
+
+
+
+
+
+ + +
+
+ +
+
+
+ + + + + + +

+ +

+
+
+ + + + + +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+ + + + + + +

+ +

+
+
+ + + + + +
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/odex30_base/system_dashboard_classic/static/src/icons/login.svg b/odex30_base/system_dashboard_classic/static/src/icons/login.svg new file mode 100644 index 0000000..1446fc8 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/icons/login.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/odex30_base/system_dashboard_classic/static/src/icons/logout.svg b/odex30_base/system_dashboard_classic/static/src/icons/logout.svg new file mode 100644 index 0000000..6fbda2d --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/icons/logout.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/odex30_base/system_dashboard_classic/static/src/icons/sad.svg b/odex30_base/system_dashboard_classic/static/src/icons/sad.svg new file mode 100644 index 0000000..29ab1f8 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/icons/sad.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/icons/smile.svg b/odex30_base/system_dashboard_classic/static/src/icons/smile.svg new file mode 100644 index 0000000..dd553ba --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/icons/smile.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/lib/confetti.min.js b/odex30_base/system_dashboard_classic/static/src/lib/confetti.min.js new file mode 100644 index 0000000..ca0ef96 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/lib/confetti.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v5.37.0. + * Original file: /npm/canvas-confetti@1.9.3/dist/confetti.browser.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(t,e){!function t(e,a,n,r){var o=!!(e.Worker&&e.Blob&&e.Promise&&e.OffscreenCanvas&&e.OffscreenCanvasRenderingContext2D&&e.HTMLCanvasElement&&e.HTMLCanvasElement.prototype.transferControlToOffscreen&&e.URL&&e.URL.createObjectURL),i="function"==typeof Path2D&&"function"==typeof DOMMatrix,l=function(){if(!e.OffscreenCanvas)return!1;var t=new OffscreenCanvas(1,1),a=t.getContext("2d");a.fillRect(0,0,1,1);var n=t.transferToImageBitmap();try{a.createPattern(n,"no-repeat")}catch(t){return!1}return!0}();function s(){}function c(t){var n=a.exports.Promise,r=void 0!==n?n:e.Promise;return"function"==typeof r?new r(t):(t(s,s),null)}var h,f,u,d,m,g,p,b,M,v,y,w=(h=l,f=new Map,{transform:function(t){if(h)return t;if(f.has(t))return f.get(t);var e=new OffscreenCanvas(t.width,t.height);return e.getContext("2d").drawImage(t,0,0),f.set(t,e),e},clear:function(){f.clear()}}),x=(m=Math.floor(1e3/60),g={},p=0,"function"==typeof requestAnimationFrame&&"function"==typeof cancelAnimationFrame?(u=function(t){var e=Math.random();return g[e]=requestAnimationFrame((function a(n){p===n||p+m-1 sum + item.value, 0); + + + // Extract labels and actual values from data + const label1 = data[0]?.label || 'Segment 1'; + const label2 = data[1]?.label || 'Segment 2'; + const actualValue1 = data[0]?.value || 0; + const actualValue2 = data[1]?.value || 0; + + // If total is 0, show empty state with full circle + let percentage1, percentage2; + // Store display percentages (what to show in tooltip) + let displayPercent1, displayPercent2; + + if (total === 0) { + percentage1 = 100; // Show full circle (color will be gray from caller) + percentage2 = 0; + displayPercent1 = 0; // Display 0% in tooltip for empty state + displayPercent2 = 0; + + } else { + percentage1 = Math.round((data[0].value / total) * 100); + percentage2 = 100 - percentage1; + displayPercent1 = percentage1; + displayPercent2 = percentage2; + } + + // Create wrapper + const wrapper = document.createElement('div'); + wrapper.className = 'css-donut-wrapper'; + wrapper.style.position = 'relative'; + wrapper.style.width = `${width}px`; + wrapper.style.height = `${height}px`; + wrapper.style.margin = '0 auto'; + + // Create CSS-based circular progress + const ring = document.createElement('div'); + ring.className = 'css-donut-chart'; + ring.style.width = `${width}px`; + ring.style.height = `${height}px`; + ring.style.setProperty('--percentage', percentage1); + ring.style.setProperty('--color1', colors[0]); + ring.style.setProperty('--color2', colors[1] || '#e0e0e0'); + ring.style.cursor = 'pointer'; + + // Add tooltip (hidden by default) + const tooltip = document.createElement('div'); + tooltip.className = 'chart-tooltip'; + tooltip.style.display = 'none'; + + // Add single hover overlay for entire ring + const hoverRing = document.createElement('div'); + hoverRing.style.position = 'absolute'; + hoverRing.style.top = '0'; + hoverRing.style.left = '0'; + hoverRing.style.width = '100%'; + hoverRing.style.height = '100%'; + hoverRing.style.borderRadius = '50%'; + hoverRing.style.cursor = 'pointer'; + hoverRing.style.zIndex = '2'; + + // Calculate which segment is being hovered + hoverRing.addEventListener('mousemove', (e) => { + const rect = hoverRing.getBoundingClientRect(); + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + const mouseX = e.clientX - centerX; + const mouseY = e.clientY - centerY; + + // Calculate distance from center + const distance = Math.sqrt(mouseX * mouseX + mouseY * mouseY); + const outerRadius = rect.width / 2; + const innerRadius = outerRadius * 0.85; + + // Only show tooltip if hovering on ring (not center) + if (distance >= innerRadius && distance <= outerRadius) { + if (total === 0) { + tooltip.textContent = `0% ${label1}`; + } else { + // Calculate angle (0° = top/12 o'clock, clockwise) + let angle = (Math.atan2(mouseY, mouseX) * (180 / Math.PI) + 90 + 360) % 360; + + // Determine which segment based on angle + const segmentAngle = percentage1 * 3.6; // Convert percentage to degrees + + if (angle <= segmentAngle) { + // First segment (color1) + tooltip.textContent = `${displayPercent1}% ${label1}`; + } else { + // Second segment (color2) + tooltip.textContent = `${displayPercent2}% ${label2}`; + } + } + tooltip.style.display = 'block'; + } else { + tooltip.style.display = 'none'; + } + }); + + hoverRing.addEventListener('mouseleave', () => { + tooltip.style.display = 'none'; + }); + + wrapper.appendChild(ring); + wrapper.appendChild(hoverRing); + wrapper.appendChild(tooltip); + container.appendChild(wrapper); + + +} diff --git a/odex30_base/system_dashboard_classic/static/src/scss/attendance.scss b/odex30_base/system_dashboard_classic/static/src/scss/attendance.scss new file mode 100644 index 0000000..7c5c34b --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/attendance.scss @@ -0,0 +1,40 @@ +// =========================================================================== +// ATTENDANCE SECTION STYLES +// =========================================================================== + + +.work-timer-container { + margin-top: var(--dash-spacing-md); + padding: var(--dash-spacing-md); + background: rgba(255, 255, 255, 0.1); + border-radius: var(--dash-radius-md); + + .work-timer-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.7; + margin-bottom: var(--dash-spacing-xs); + } + + .work-timer-value { + font-size: 1.25rem; + font-weight: 700; + font-family: monospace; + } + + .work-timer-progress { + height: 4px; + background: rgba(255, 255, 255, 0.2); + border-radius: var(--dash-radius-full); + margin-top: var(--dash-spacing-sm); + overflow: hidden; + + .progress-bar { + height: 100%; + background: var(--dash-success); + border-radius: var(--dash-radius-full); + transition: width 1s linear; + } + } +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/cards.scss b/odex30_base/system_dashboard_classic/static/src/scss/cards.scss new file mode 100644 index 0000000..503ddb0 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/cards.scss @@ -0,0 +1,560 @@ +#main-cards { + display: none; +} + +/*CARD (1)*/ +.card { + padding-left: 0; + margin-top: 20px; + + &.mini { + .card-body { + border: 1px solid #eee; + height: 130px !important; + padding: 0; + + .box-1 { + height: 100%; + background: #ee6414; + text-align: center; + vertical-align: middle; + line-height: 130px; + } + + .box-2 { + padding-right: 10px; + padding-left: 10px; + height: 130px; + overflow: hidden; + } + } + } + + .card-body { + border: 1px solid #eee; + height: 160px; + padding: 0; + + .box-1 { + height: 100%; + background: #ee6414; + text-align: center; + vertical-align: middle; + line-height: 160px; + + &.red { + background: #ee0c21; + } + + &.blue { + background: #2bb0ee; + } + + &.green { + background: #08bf17; + } + + &.dark-blue { + background: #3e5d7f; + } + + i { + font-size: 60px; + color: #fff; + } + } + + .box-2 { + padding-right: 10px; + padding-left: 10px; + height: 160px; + overflow: hidden; + + &:hover { + overflow: visible; + + .btn-group { + bottom: 10px; + } + } + + h4 { + margin-bottom: 0; + font-size: 20px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + h2 { + margin-top: 5px; + margin-bottom: 0px; + font-weight: bold; + font-size: 44px; + color: #404040; + } + + button { + .btn-primary { + position: absolute; + bottom: 10px; + } + } + + p { + margin-bottom: 10px; + color: #9a9a9a; + } + + .btn-group { + position: absolute; + bottom: -50px; + right: 10px; + transition: all .4s; + } + } + + } + + +} + +/*CARD (2) - Approval Cards - Enhanced */ +.card2 { + padding-left: 0; + margin-bottom: 20px; + + .card-container { + border: none; + border-radius: var(--dashboard-border-radius, 16px); + padding: 0; + overflow: hidden; + box-shadow: var(--dashboard-shadow-md, 0 4px 20px rgba(0, 0, 0, 0.08)); + transition: all var(--dashboard-transition-normal, 0.3s ease); + position: relative; + background: #ffffff; + + /* Top accent bar on hover */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, + var(--dashboard-accent, #667eea), + var(--dashboard-primary, #2ead97)); + opacity: 0; + transition: opacity var(--dashboard-transition-normal, 0.3s ease); + z-index: 10; + } + + &:hover { + transform: translateY(-6px); + box-shadow: var(--dashboard-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.12)); + } + + &:hover::before { + opacity: 1; + } + + .card-header { + background: linear-gradient(135deg, + var(--dashboard-gradient-start, #0e3e34) 0%, + var(--dashboard-gradient-end, #00887e) 100%); + height: 56px; + vertical-align: middle; + padding: 0 16px !important; + display: flex; + align-items: center; + position: relative; + overflow: hidden; + + /* Decorative glow */ + &::after { + content: ''; + position: absolute; + top: 50%; + right: -20%; + width: 150px; + height: 150px; + border-radius: 50%; + background: radial-gradient(circle, + rgba(255, 255, 255, 0.12) 0%, + transparent 70%); + pointer-events: none; + } + + + img { + height: 38px; + width: 38px; + margin-right: 12px; + padding: 6px; + background: rgba(255, 255, 255, 0.15); + border-radius: 10px; + transition: transform var(--dashboard-transition-normal, 0.3s ease); + } + + + &:hover img { + transform: scale(1.1) rotate(-5deg); + } + + h4 { + line-height: 1.4; + color: #ffffff; + font-weight: 700; + padding: 0 !important; + margin: 0; + font-size: 16px; + flex: 1; + display: flex; + align-items: center; + gap: 10px; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + letter-spacing: 0.3px; + + i { + font-size: 24px; + opacity: 0.9; + } + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + &.red { + background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); + } + + &.blue { + background: linear-gradient(135deg, #2193b0 0%, #6dd5ed 100%); + } + + &.green { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + } + } + + .card-body { + padding: 0; + height: 180px; + overflow-y: auto; + background: #fff; + + /* Custom scrollbar */ + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: #f1f1f1; + } + + &::-webkit-scrollbar-thumb { + background: var(--dashboard-primary, #2ead97); + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--dashboard-primary-dark, #084e41); + } + + table { + margin: 0; + + tr { + border-top: none; + border-bottom: 1px solid rgba(0, 48, 86, 0.08); + transition: all var(--dashboard-transition-fast, 0.2s ease); + cursor: pointer; + + &:hover { + background: linear-gradient(90deg, + rgba(46, 173, 150, 0.08), + rgba(102, 126, 234, 0.05)); + } + + &:nth-child(2n) { + background: rgba(237, 246, 253, 0.5); + } + + &:nth-child(2n):hover { + background: linear-gradient(90deg, + rgba(46, 173, 150, 0.12), + rgba(102, 126, 234, 0.08)); + } + + &:last-child { + border-bottom: none; + } + + td { + border-top: none; + line-height: 1.6; + font-size: 14px; + padding: 12px 14px; + color: var(--dashboard-text-primary, #2d3748); + font-weight: 500; + + &:last-child { + text-align: right; + + div { + background: linear-gradient(135deg, + var(--dashboard-gradient-start, #0e3e34), + var(--dashboard-gradient-end, #00887e)); + height: 28px; + width: 28px; + text-align: center; + border-radius: 50%; + line-height: 28px; + font-size: 12px; + font-weight: 700; + float: right; + color: #fff; + transition: all var(--dashboard-transition-normal, 0.3s ease); + box-shadow: 0 2px 8px rgba(0, 136, 126, 0.3); + } + + i { + transition: color var(--dashboard-transition-fast, 0.2s ease); + + &:hover { + cursor: pointer; + color: var(--dashboard-primary, #2ead97); + } + } + } + } + } + + tr:hover td:last-child div { + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(0, 136, 126, 0.45); + } + } + } + } +} + +/*CARD (3) - Self Service Cards - Enhanced */ +.card3 { + padding: 0 8px; + margin-bottom: 24px; + + .card-body { + padding: 0; + border-radius: var(--dashboard-border-radius, 16px); + overflow: hidden; + box-shadow: var(--dashboard-shadow-md, 0 4px 20px rgba(0, 0, 0, 0.08)); + transition: all var(--dashboard-transition-normal, 0.3s ease); + position: relative; + + /* Top accent bar on hover */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 55; + right: 0; + height: 4px; + background: linear-gradient(90deg, + var(--dashboard-accent, #667eea), + var(--dashboard-primary, #2ead97)); + opacity: 0; + transition: opacity var(--dashboard-transition-normal, 0.3s ease); + z-index: 10; + } + + &:hover { + transform: translateY(-8px); + box-shadow: var(--dashboard-shadow-lg, 0 12px 40px rgba(0, 0, 0, 0.12)); + } + + &:hover::before { + opacity: 1; + } + + .box-1 { + height: 250px; + background: linear-gradient(145deg, + #ffffff 0%, + var(--dashboard-card-bg, #f4fefe) 100%); + text-align: center; + vertical-align: middle; + transition: all var(--dashboard-transition-normal, 0.3s ease); + display: flex; + flex-direction: column; + cursor: pointer; + align-content: center; + align-items: center; + justify-content: center; + border: none; + position: relative; + overflow: hidden; + + /* Decorative background circle */ + &::before { + content: ''; + position: absolute; + top: -30%; + right: -30%; + width: 200px; + height: 200px; + border-radius: 50%; + background: radial-gradient(circle, + rgba(46, 173, 150, 0.06) 0%, + transparent 70%); + pointer-events: none; + transition: transform var(--dashboard-transition-slow, 0.5s ease); + } + + &:hover::before { + transform: scale(1.3); + } + + &.red { + background: linear-gradient(145deg, #fee2e2, #fecaca); + } + + &.blue { + background: linear-gradient(145deg, #dbeafe, #bfdbfe); + } + + &.green { + background: linear-gradient(145deg, #dcfce7, #bbf7d0); + } + + i { + font-size: 60px; + color: #fff; + } + + img { + height: 70px; + width: auto; + margin-bottom: 8px; + transition: all var(--dashboard-transition-normal, 0.3s ease); + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)); + } + + &:hover img { + transform: scale(1.15) rotate(-5deg); + filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.15)); + } + + h3 { + margin-bottom: 0; + margin-top: 8px; + font-size: 52px; + font-weight: 800; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + background: linear-gradient(135deg, + var(--dashboard-secondary, #003056), + var(--dashboard-primary, #2ead97)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + transition: transform var(--dashboard-transition-normal, 0.3s ease); + } + + &:hover h3 { + transform: scale(1.05); + } + + h4 { + margin-bottom: 0; + font-size: 16px; + font-weight: 600; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: var(--dashboard-text-secondary, #718096); + padding-top: 10px !important; + padding-bottom: 0 !important; + max-width: 100%; + } + } + + .box-2 { + background: linear-gradient(135deg, + var(--dashboard-gradient-start, #0e3e34) 0%, + var(--dashboard-gradient-end, #00887e) 100%); + transition: all var(--dashboard-transition-normal, 0.3s ease); + cursor: pointer; + height: 56px; + line-height: 56px; + overflow: hidden; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + padding: 0 20px; + position: relative; + + /* Subtle shine effect */ + &::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, + transparent, + rgba(255, 255, 255, 0.1), + transparent); + transition: left 0.5s ease; + } + + &:hover::before { + left: 100%; + } + + i { + font-size: 28px; + color: #fff; + transition: all var(--dashboard-transition-normal, 0.3s ease); + margin-left: 10px; + } + + span { + font-size: 15px; + font-weight: 600; + color: rgba(255, 255, 255, 0.95); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + h4 { + margin-bottom: 0; + font-size: 20px; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + color: #fff; + } + + &:hover { + background: linear-gradient(135deg, + var(--dashboard-primary-dark, #084e41) 0%, + var(--dashboard-gradient-start, #0e3e34) 100%); + + i { + transform: rotate(90deg) scale(1.1); + } + + span { + color: #ffffff; + } + } + } + } +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/charts.scss b/odex30_base/system_dashboard_classic/static/src/scss/charts.scss new file mode 100644 index 0000000..75197db --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/charts.scss @@ -0,0 +1,105 @@ +// =========================================================================== +// CHART STYLES (D3.js) +// =========================================================================== + + +.chart-wrapper { + position: relative; + width: 100%; + max-width: 120px; + margin: 0 auto; + + svg { + width: 100%; + height: auto; + } +} + +.chart-center-text { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + pointer-events: none; + + .chart-center-value { + display: block; + font-size: 1.25rem; + font-weight: 700; + color: var(--dash-gray-800); + } + + .chart-center-unit { + display: block; + font-size: 0.75rem; + color: var(--dash-gray-500); + } +} + +// D3 Arc styling +.arc path { + stroke: var(--dash-white); + stroke-width: 2px; + transition: opacity var(--dash-transition-fast); + + &:hover { + opacity: 0.8; + } +} + +// Tooltip - Modern Premium Design with Dynamic Colors +.chart-tooltip { + position: absolute; + + /* Modern Gradient Background using Dashboard Primary Color */ + background: linear-gradient(135deg, + var(--dash-primary, #0891b2) 0%, + var(--dash-primary-dark, #0e7490) 100%); + + color: var(--dash-white); + padding: 8px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.5px; + pointer-events: none; + z-index: 1000; + white-space: nowrap; + + /* Premium Multi-Layer Shadow */ + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.2), + 0 10px 15px -3px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.15) inset; + + /* Smooth Entrance Animation */ + animation: tooltipSlideIn 0.2s ease-out; + + /* Glossy Effect Overlay */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient(180deg, + rgba(255, 255, 255, 0.2) 0%, + transparent 100%); + border-radius: 6px 6px 0 0; + pointer-events: none; + } +} + +@keyframes tooltipSlideIn { + 0% { + opacity: 0; + transform: translateY(-5px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/core.scss b/odex30_base/system_dashboard_classic/static/src/scss/core.scss new file mode 100644 index 0000000..bbe341e --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/core.scss @@ -0,0 +1,1305 @@ +$color_1: #888; +$color_2: #7fb8e6; +$color_3: #fff; +$color_4: #0B2E59; +$color_5: #2eac96; +$color_6: linear-gradient(270deg, rgb(14, 62, 52) 0%, rgb(0, 136, 126) 75%); +$color_7: #8a8a8a; +$color_8: #06211a; +$color_9: #ffffff; +$color_10: #9a9aa8; +$background-color_1: transparent; +$border-color_1: #2eac96; + +// /* Secondary Colors */ +// --dash-secondary: #1e293b; +// /* Slate-800 - Headers/Dark */ +// --dash-secondary-light: #334155; +// + +@-webkit-keyframes AnimationName { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +@-moz-keyframes AnimationName { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +@keyframes AnimationName { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +@keyframes lds-roller { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.o_content { + background: #fff; +} + +.dashboard-container { + max-height: 100%; + overflow: auto; + + .dashboard-header { + padding: 0; + border-bottom: 1px solid #d0d0d0; + overflow: hidden; + border-bottom: none; + box-shadow: 0 0 15px 0 #d7d7d7 !important; + display: flex; + flex-wrap: wrap; + + .dashboard-user-data-section { + background: $bg_user_section; + height: 280px; + display: flex; + align-items: center; + } + + .dashboard-user-statistics-section { + background: $bg_user_statistics; + height: 280px; + display: flex; + align-items: center; + padding: 0; + + .dashboard-charts-section { + height: 280px; + display: flex; + align-items: center; + } + + .dashboard-attendance-section { + height: 240px; + display: flex; + align-items: center; + border-left: 1px solid #d0d0d0; + + .attendance-section-body { + text-align: center; + padding: 0; + + p { + margin-bottom: 3px; + } + + h3 { + margin-top: 0; + font-weight: bold; + } + + .last-checkin-section { + font-size: 14px; + color: $color_1; + } + + .attendance-img-section { + text-align: center; + + img { + height: 60px; + } + } + + .attendance-button-section { + button { + background: transparent; // بدون خلفية + border: none; + padding: 10px; + + i { + font-size: 48px; // ضعف الحجم (24px * 2) + color: #10b981; // لون أخضر لتسجيل الدخول + } + + &:hover { + background: rgba(16, 185, 129, 0.1); // خلفية خضراء خفيفة عند hover + + i { + color: #059669; // أخضر غامق عند hover + } + } + } + + button.checkout-btn { + background: transparent; + + i { + color: #ef4444; // لون أحمر لتسجيل الخروج + } + + &:hover { + background: rgba(239, 68, 68, 0.1); // خلفية حمراء خفيفة عند hover + + i { + color: #dc2626; // أحمر غامق عند hover + } + } + } + } + } + } + } + } + + .dashboard-body { + padding-top: 20px; + padding-bottom: 20px; + + .dashboard-nav-buttons { + margin-bottom: 20px; + padding: 0; + + button { + background: $bg_dashboard_nav; + border: none; + + &:hover { + background: $bg_dashboard_nav_hover; + } + } + + hr { + border-color: $divd_border_color; + width: 10%; + text-align: left; + margin: 5px 0; + } + + .nav-tabs { + border-bottom: none; + + >li { + margin-right: 5px; + + >a { + color: $color_nav; + margin: 0; + } + } + + >li.active { + >a { + border: none; + background: $color_5; + color: $color_3; + } + } + } + } + } + + .tabs-container.dashboard-body { + .dashboard-nav-buttons { + hr { + width: 60%; + } + } + } +} + +.dashboard-module-charts { + display: flex; + flex-wrap: wrap; +} + +.profile-container { + padding: 0; + margin: auto; + + .pp-image-section { + padding: 0; + + .img-box { + background: #eeee; + border-radius: 100%; + padding: 10px; + height: 120px; + width: 120px; + background-size: 120%; + background-position: center; + border: 3px solid #fff; + margin: auto; + } + } + + .info-section { + padding: 10px; + + p { + margin: 0; + font-weight: bold; + color: $color_2; + text-align: center; + } + + p.fn-section { + font-size: 18px; + margin-bottom: 5px; + color: $color_3; + } + + p.fn-job { + font-size: 14px; + margin-bottom: 10px; + } + + p.fn-company { + font-size: 13px; + } + + p.fn-last-login { + border-top: 1px solid #eee; + margin-top: 5px; + + kbd { + font-size: 10px; + margin-right: 5px; + } + } + } +} + +.row-container { + padding-top: 40px; + padding-bottom: 20px; +} + +.p0 { + padding: 0; +} + +.nav-tabs { + >li.active { + >a { + border-radius: 0; + border-top: 2px solid #3e5d7f; + + &:focus { + border-radius: 0; + border-top: 2px solid #3e5d7f; + } + + &:hover { + border-radius: 0; + border-top: 2px solid #3e5d7f; + } + } + } +} + +.charts-over-layer { + height: 100%; + width: 100%; + background: #f8fcff; + position: absolute; + top: 0; + left: 0; + z-index: 9; + display: flex; + align-items: center; +} + +.module-box { + padding: 0 !important; + + .module-box-container { + padding: 0; + border-radius: 30px; + + #chartContainer { + display: flex; + height: 200px; + width: 100%; + } + + #chartPaylips { + display: flex; + height: 200px; + width: 100%; + } + + #chartTimesheet { + display: flex; + height: 200px; + width: 100%; + } + + svg { + height: 200px; + margin: auto; + } + + h4.leave-data-percent { + text-align: center; + transform: translate(0, -85px); + font-size: 30px; + font-weight: bold; + margin-top: -60px; + color: $color_5; + padding: 0 !important; + } + + h4.payroll-data-percent { + text-align: center; + transform: translate(0, -85px); + font-size: 30px; + font-weight: bold; + margin-top: -60px; + color: $color_5; + padding: 0 !important; + } + + h4.timesheet-data-percent { + text-align: center; + transform: translate(0, -85px); + font-size: 30px; + font-weight: bold; + margin-top: -60px; + color: $color_5; + padding: 0 !important; + } + + p { + text-align: center; + margin-bottom: 5px; + + >span { + font-size: 14px; + } + + &:nth-child(1) { + span { + i { + color: $color_5; + font-size: 14px; + } + + span { + color: $color_5; + font-size: 14px; + } + } + } + + &:nth-child(2) { + margin-bottom: 10px; + + span { + i { + color: $color_6; + color: $color_6; + font-size: 14px; + } + + span { + color: $color_6; + color: $color_6; + font-size: 14px; + } + } + } + } + + h3 { + text-align: center; + text-transform: uppercase; + margin-top: -20px; + font-size: 18px; + font-weight: normal; + } + + .module-body { + padding: 10px 15px 10px 10px; + + a { + background-color: $background-color_1; + + &:hover { + text-decoration: none; + } + + h3 { + margin: 0; + transition: all .3s ease-in-out; + color: $color_7; + font-size: 16px; + text-align: center; + padding-top: 5px; + padding-bottom: 5px; + font-size: 20px; + } + } + + h2 { + text-align: center; + font-size: 26px; + color: $color_8; + margin-top: 5px; + + span.unit { + font-size: 18px; + color: $color_1; + } + } + + .module-icon { + background: #2bb0ee; + text-align: center; + height: 80px; + width: 80px; + line-height: 80px; + border-radius: 50%; + position: absolute; + top: -40px; + left: -20px; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), 0 6px 10px 0 rgba(0, 0, 0, .14), 0 1px 18px 0 rgba(0, 0, 0, .12) !important; + + i { + font-size: 45px; + color: $color_9; + } + } + + .module-icon.red { + background: #ee451f; + } + + .module-icon.green { + background: #14bf1f; + } + + .module-icon.yellow { + background: #f9d700; + } + } + + .module-footer { + border-top: 1px solid #eee; + position: absolute; + bottom: 0; + padding-top: 5px; + padding-bottom: 4px; + + p { + margin: 0; + padding: 0; + + span { + color: $color_10; + } + } + } + } + + &:nth-child(2) { + .module-box-container { + .module-body { + .module-icon { + i { + font-size: 45px; + color: $color_9; + line-height: 70px; + } + } + } + } + } + + .module-box-container.theme-1 { + background: #e2f8e3; + + .module-icon { + background: #449a1b; + } + } + + .module-box-container.theme-2 { + background: #f9efe6; + + .module-icon { + background: #bb581f; + } + } + + .module-box-container.theme-3 { + background: #f7eaf4; + + .module-icon { + background: #994377; + } + } +} + +.o_web_client { + >.o_action_manager { + overflow: visible !important; + } +} + +.fn-department { + display: none; +} + +.img-logout { + transform: rotateZ(180deg); +} + +/* Odoo 18: Section visibility controlled by OWL template - removed display:none rules */ + +/* RTL Support - Essential rules that rtlcss cannot handle automatically */ +.o_rtl { + .canvasjs-chart-container { + direction: rtl; + /*rtl:ignore*/ + text-align: right !important; + /*rtl:ignore*/ + } + + .dashboard-container { + .dashboard-header { + .dashboard-user-statistics-section { + .dashboard-attendance-section { + border-left: 1px solid #d0d0d0; + /*rtl:ignore*/ + border-right: none !important; + /*rtl:ignore*/ + } + } + } + + .dashboard-body { + .dashboard-nav-buttons { + .nav-tabs { + >li { + >a { + border-radius: 0; + } + + margin-left: 5px; + /*rtl:ignore*/ + margin-right: 0; + /*rtl:ignore*/ + } + } + } + } + } + + .module-box { + .module-box-container { + p { + >span { + margin-right: 0; + /*rtl:ignore*/ + } + } + } + } + + .card2 { + padding-right: 0; + /*rtl:ignore*/ + } +} + +.lds-roller { + display: inline-block; + position: relative; + width: 80px; + height: 80px; + margin: auto; + + div { + animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + transform-origin: 40px 40px; + + &:after { + content: " "; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + background: linear-gradient(270deg, rgb(156, 167, 179) 0%, #607992 75%); + margin: -4px 0 0 -4px; + } + + &:nth-child(1) { + animation-delay: -0.036s; + + &:after { + top: 63px; + left: 63px; + } + } + + &:nth-child(2) { + animation-delay: -0.072s; + + &:after { + top: 68px; + left: 56px; + } + } + + &:nth-child(3) { + animation-delay: -0.108s; + + &:after { + top: 71px; + left: 48px; + } + } + + &:nth-child(4) { + animation-delay: -0.144s; + + &:after { + top: 72px; + left: 40px; + } + } + + &:nth-child(5) { + animation-delay: -0.18s; + + &:after { + top: 71px; + left: 32px; + } + } + + &:nth-child(6) { + animation-delay: -0.216s; + + &:after { + top: 68px; + left: 24px; + } + } + + &:nth-child(7) { + animation-delay: -0.252s; + + &:after { + top: 63px; + left: 17px; + } + } + + &:nth-child(8) { + animation-delay: -0.288s; + + &:after { + top: 56px; + left: 12px; + } + } + } +} + +@media (max-width: 575.98px) { + .dashboard-container { + .dashboard-header { + .dashboard-user-statistics-section { + height: auto !important; + + .dashboard-charts-section { + height: auto !important; + padding: 0; + } + + .dashboard-attendance-section { + border-left: none !important; + } + } + } + } + + .module-box { + .module-box-container { + padding: 15px 0; + } + + border-bottom: 1px solid #eee; + padding-bottom: 30px; + padding-top: 30px; + } + + .card2 { + padding: 0; + margin-bottom: 20px; + } + + .canvasjs-chart-canvas { + position: relative !important; + } + + /* RTL responsive override */ + .o_rtl { + .card3 { + padding: 0; + /*rtl:ignore*/ + } + } +} + +@media (max-width: 767px) { + .dashboard-container { + .dashboard-header { + .dashboard-user-statistics-section { + height: auto !important; + + .dashboard-charts-section { + height: auto !important; + } + + .dashboard-attendance-section { + border-left: none !important; + } + } + } + } + + .module-box { + border-bottom: 1px solid #eee; + padding-bottom: 30px; + padding-top: 30px; + } + + .card2 { + padding: 0; + margin-bottom: 20px; + } + + .canvasjs-chart-canvas { + position: relative !important; + } + + .card3 { + padding: 0; + } +} + +@media (min-width: 767px) and (max-width: 980px) { + .dashboard-container { + .dashboard-header { + .dashboard-user-statistics-section { + height: auto !important; + + .dashboard-charts-section { + height: auto !important; + } + + .dashboard-attendance-section { + display: none; + border-left: none !important; + } + } + } + } + + .module-box { + border-bottom: 1px solid #eee; + padding: 30px 0; + } + + .card2 { + margin-bottom: 20px; + + &:nth-child(2n) { + padding-left: 15px; + padding-right: 0; + } + + &:nth-child(3n) { + padding-right: 15px; + padding-left: 0; + } + } + + .canvasjs-chart-canvas { + position: relative !important; + } + + /* RTL responsive padding for card2 alternating columns */ + .o_rtl { + .card2 { + &:nth-child(2n) { + padding-right: 15px; + /*rtl:ignore*/ + padding-left: 0; + /*rtl:ignore*/ + } + + &:nth-child(3n) { + padding-left: 15px; + /*rtl:ignore*/ + padding-right: 0; + /*rtl:ignore*/ + } + } + } +} + +/* ==================================== + 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; + } +} + +/* ========================================================================= */ +/* CSS-based Donut Charts (conic-gradient) - Interactive */ +/* ========================================================================= */ +.css-donut-wrapper { + position: relative; +} + +.css-donut-chart { + position: relative; + border-radius: 50%; + background: conic-gradient(var(--color1) 0% calc(var(--percentage) * 1%), + var(--color2) calc(var(--percentage) * 1%) 100%); + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; +} + +/* Thinner donut hole - 85% instead of 70% */ +.css-donut-chart::before { + content: ''; + position: absolute; + width: 85%; + height: 85%; + background: white; + border-radius: 50%; + z-index: 1; +} + +/* Hover segments - invisible overlays */ +.chart-segment { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 50%; + cursor: pointer; + z-index: 2; +} + +.chart-segment.segment-1 { + clip-path: polygon(50% 50%, + 50% 0%, + calc(50% + 50% * sin(calc(var(--segment-percentage) * 3.6deg))) calc(50% - 50% * cos(calc(var(--segment-percentage) * 3.6deg)))); +} + +.chart-segment.segment-2 { + clip-path: polygon(50% 50%, + calc(50% + 50% * sin(calc(var(--segment-percentage) * 3.6deg))) calc(50% - 50% * cos(calc(var(--segment-percentage) * 3.6deg))), + 100% 50%, + 50% 100%, + 0% 50%, + 50% 0%); +} + +/* Tooltip - Modern Premium Design with Dynamic Dashboard Colors */ +.chart-tooltip { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + margin-top: 8px; + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-secondary-light) 100%) !important; + + color: white; + padding: 8px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.5px; + pointer-events: none; + z-index: 1000; + white-space: nowrap; + + /* Premium Multi-Layer Shadow */ + box-shadow: + 0 4px 6px -1px rgba(0, 0, 0, 0.2), + 0 10px 15px -3px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.15) inset; + + /* Smooth Entrance Animation */ + animation: tooltipSlideIn 0.2s ease-out; + + /* Glossy Effect Overlay */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient(180deg, + rgba(255, 255, 255, 0.2) 0%, + transparent 100%); + border-radius: 6px 6px 0 0; + pointer-events: none; + } +} + +/* Smooth Slide-in Animation */ +@keyframes tooltipSlideIn { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(-5px); + } + + 100% { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/dashboard.scss b/odex30_base/system_dashboard_classic/static/src/scss/dashboard.scss new file mode 100644 index 0000000..0d61a79 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/dashboard.scss @@ -0,0 +1,99 @@ +// =========================================================================== +// DASHBOARD MAIN STYLES (Combined) +// =========================================================================== + + +// Additional dashboard-specific styles not covered in core.scss + +.dashboard-main { + animation: fadeIn 0.3s ease-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +// Celebration overlay +.celebration-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + + .celebration-modal { + background: var(--dash-white); + border-radius: var(--dash-radius-xl); + padding: var(--dash-spacing-xl); + text-align: center; + max-width: 400px; + animation: bounceIn 0.5s ease-out; + } + + .celebration-icon { + font-size: 4rem; + margin-bottom: var(--dash-spacing-md); + } + + .celebration-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--dash-gray-800); + margin-bottom: var(--dash-spacing-sm); + } + + .celebration-message { + color: var(--dash-gray-600); + margin-bottom: var(--dash-spacing-lg); + } +} + +@keyframes bounceIn { + 0% { + opacity: 0; + transform: scale(0.8); + } + + 50% { + transform: scale(1.05); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +// Search bar (if added) +.dashboard-search { + margin-bottom: var(--dash-spacing-lg); + + .search-input { + width: 100%; + max-width: 400px; + padding: var(--dash-spacing-sm) var(--dash-spacing-md); + border: 1px solid var(--dash-gray-200); + border-radius: var(--dash-radius-lg); + font-size: 0.9375rem; + transition: all var(--dash-transition-fast); + + &:focus { + outline: none; + border-color: var(--dash-primary); + box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.1); + } + } +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss b/odex30_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss new file mode 100644 index 0000000..7f35fc3 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss @@ -0,0 +1,4125 @@ +/* ============================================================ + System Dashboard Classic - GENIUS PREMIUM REDESIGN + ============================================================ + + A complete professional redesign with: + - Premium stat cards instead of plain circles + - Enhanced employee profile with status badge + - Real-time search bar for services + - Modern check-in button + - Configurable 2-3 color palette + + ============================================================ */ + +/* === THEME COLORS (Configurable via CSS Variables) === */ +/* Default colors match Odoo settings - will be overridden by JS */ +:root { + /* Primary Brand Colors - DEFAULT CYAN (Odoo typical, will be overridden by JS) */ + --dash-primary: #0891b2; + /* Cyan-600 - Main accent */ + --dash-primary-light: #22d3ee; + /* Cyan-400 light */ + --dash-primary-dark: #0e7490; + /* Cyan-700 dark */ + + /* Secondary Colors */ + --dash-secondary: #1e293b; + /* Slate-800 - Headers/Dark */ + --dash-secondary-light: #334155; + + + /* Status Colors */ + --dash-success: #10b981; + /* Emerald-500 */ + --dash-warning: #f59e0b; + /* Amber-500 */ + --dash-danger: #ef4444; + /* Red-500 */ + + /* Neutral Colors */ + --dash-bg: #f1f5f9; + /* Slate-100 */ + --dash-white: #ffffff; + --dash-border: #e2e8f0; + /* Slate-200 */ + --dash-text: #475569; + /* Slate-600 */ + --dash-text-light: #94a3b8; + /* Slate-400 */ + + /* Shadows */ + --dash-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --dash-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --dash-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --dash-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + + /* Transitions */ + --dash-transition: 0.2s ease-in-out; +} + +/* ============================================================ + HEADER SECTION - Premium Dark Gradient + ============================================================ */ + +.dashboard-container .dashboard-header { + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-secondary-light) 100%) !important; + border-radius: 0 0 28px 28px !important; + box-shadow: var(--dash-shadow-xl) !important; + padding: 30px 20px !important; + min-height: auto !important; + position: relative !important; + overflow: hidden !important; + border-bottom: none !important; +} + +/* Subtle decorative gradient overlay - uses primary color */ +.dashboard-container .dashboard-header::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + right: 0 !important; + width: 50% !important; + height: 100% !important; + background: linear-gradient(135deg, transparent 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) 100%) !important; + pointer-events: none !important; +} + +.dashboard-container .dashboard-header::after { + display: none !important; +} + +/* ============================================================ + APPRECIATION RIBBON - Birthday & Anniversary Celebrations + ============================================================ */ + +.appreciation-ribbon { + position: fixed !important; + top: 48px !important; + /* Below Odoo navbar */ + left: 0 !important; + right: 0 !important; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; + color: white !important; + padding: 14px 20px !important; + text-align: center !important; + font-size: 18px !important; + font-weight: 600 !important; + z-index: 999 !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + animation: slideDown 0.5s ease-out !important; +} + +.appreciation-ribbon .ribbon-content { + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 12px !important; +} + +.appreciation-ribbon .ribbon-emoji { + font-size: 28px !important; + animation: bounce 1s ease-in-out infinite !important; +} + +.appreciation-ribbon .ribbon-text { + font-size: 18px !important; + font-weight: 600 !important; + letter-spacing: 0.3px !important; +} + +@keyframes slideDown { + from { + transform: translateY(-100%); + opacity: 0; + } + + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes bounce { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-8px); + } +} + +/* Adjust dashboard container when ribbon is visible */ +.dashboard-container:has(.appreciation-ribbon) { + margin-top: 60px !important; +} + +/* ============================================================ + EMPLOYEE PROFILE - Premium Card Design + ============================================================ */ + +.dashboard-user-data-section { + display: flex !important; + align-items: center !important; + padding: 0 !important; + background: transparent !important; + /* Remove the dark box - the header gradient behind will show through */ +} + +/* Profile container - VISIBLE Glassmorphism Card */ +/* Enhanced for better visibility on dark header */ +.profile-container { + background: rgba(255, 255, 255, 0.15) !important; + -webkit-backdrop-filter: blur(20px) saturate(180%) !important; + border-radius: 20px 0px 0px 20px !important; + padding: 10px 5px !important; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + margin: 10px !important; + text-align: center !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + position: relative !important; +} + +/* Subtle shine effect on top */ +.profile-container::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 10% !important; + right: 10% !important; + height: 1px !important; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent) !important; + pointer-events: none !important; +} + +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; + margin-bottom: 0px !important; +} + +/* Online status indicator removed by user request */ +/* .profile-container .pp-image-section::after { ... } */ + +.profile-container .pp-image-section::before { + display: none !important; +} + +.profile-container .pp-image-section .img-box { + border: 4px solid rgba(255, 255, 255, 0.2) !important; + height: 130px !important; + width: 130px !important; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3) !important; + transition: all var(--dash-transition) !important; +} + +.profile-container .pp-image-section .img-box:hover { + transform: scale(1.05) !important; + border-color: var(--dash-primary) !important; +} + +.profile-container .info-section { + padding: 0 !important; + margin-top: 0 !important; +} + +.profile-container .info-section::before { + display: none !important; +} + +.profile-container .info-section p { + margin: 0 !important; + color: rgba(255, 255, 255, 0.9) !important; +} + +.profile-container .info-section p.fn-section { + font-size: 18px !important; + font-weight: 700 !important; + color: #fff !important; + margin-bottom: 2px !important; + letter-spacing: 0.3px !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; + background: none !important; + animation: none !important; +} + +.profile-container .info-section p.fn-job { + font-size: 12px !important; + font-weight: 500 !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; + margin-bottom: 5px !important; + -webkit-text-fill-color: unset !important; + text-shadow: none !important; +} + +/* Employee ID - Styled Badge */ +.profile-container .info-section p.fn-id { + font-size: 14px !important; + color: rgba(255, 255, 255, 0.9) !important; + background: rgba(255, 255, 255, 0.15) !important; + padding: 0px 12px !important; + border-radius: 20px !important; + margin-top: 0px !important; + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; +} + +/* Hide fn-id only when it contains "/" or is empty - via class added by JS */ +p.fn-id.genius-hidden, +.fn-id.genius-hidden, +.profile-container .info-section p.fn-id.genius-hidden { + display: none !important; +} + +.emp-code-badge { + color: #fff !important; + font-weight: 600 !important; + letter-spacing: 0.5px !important; +} + +/* Dynamic Greeting Text */ +.greeting-text { + font-weight: 400 !important; + font-size: 14px !important; + opacity: 0.9; +} + +/* Pending Requests Counter Badge - Uses Warning Color from Settings */ +.pending-count-badge { + background: var(--dash-warning, #f59e0b) !important; + color: white !important; + border-radius: 50% !important; + padding: 2px 8px !important; + font-size: 11px !important; + margin-inline-start: 8px !important; + font-weight: bold !important; + min-width: 20px !important; + text-align: center !important; + display: inline-block !important; + animation: pulse-badge 2s ease-in-out infinite !important; +} + +@keyframes pulse-badge { + + 0%, + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); + } + + 50% { + transform: scale(1.1); + box-shadow: 0 0 0 8px rgba(245, 158, 11, 0); + } +} + +/* Attendance Title - Uses Secondary Color from Settings */ +.attendance-title { + color: var(--dash-secondary, #1e293b) !important; + font-weight: 600 !important; + font-size: 16px !important; + margin-bottom: 8px !important; +} + +/* Attendance Date & Time - Uses Primary Color from Settings */ +/* Date and time displayed on single line */ +.last-checkin-section { + text-align: center !important; + margin-bottom: 5px !important; +} + +.attendance-date { + color: var(--dash-primary, #0891b2) !important; + font-size: 14px !important; + font-weight: 600 !important; + display: inline !important; +} + +.attendance-time { + color: var(--dash-primary, #0891b2) !important; + font-size: 14px !important; + font-weight: 600 !important; + // font-family: 'SF Mono', 'Roboto Mono', 'Consolas', monospace !important; // Removed to match date font + letter-spacing: 0.5px !important; + display: inline !important; + margin-left: 5px !important; +} + +/* Last Check-in/out Info - Displayed below icon on single line */ +.last-checkin-info { + text-align: center !important; + margin-top: 4px !important; +} + +.last-checkin-info .checkin-label { + color: var(--dash-secondary, #64748b) !important; + font-size: 10px !important; + font-weight: 500 !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.last-checkin-info .checkin-time { + color: var(--dash-warning, #f59e0b) !important; + font-size: 11px !important; + font-weight: 600 !important; + font-family: 'SF Mono', 'Roboto Mono', monospace !important; + display: inline !important; + margin-left: 5px !important; +} + +/* Clickable Profile Elements */ +.clickable-profile { + cursor: pointer !important; + transition: all 0.3s ease !important; +} + +.clickable-profile:hover { + opacity: 0.85 !important; +} + +.pp-image-section .img-box.clickable-profile:hover { + transform: scale(1.08) !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4) !important; +} + +p.fn-section.clickable-profile:hover { + text-decoration: underline !important; + text-underline-offset: 3px !important; +} + +/* ============================================================ + STATISTICS CARDS - Modern Premium Design + ============================================================ */ + +.dashboard-user-statistics-section { + display: flex !important; + /* ✅ Ensure visibility on all screen sizes */ + padding: 0 15px !important; + border-radius: 5px !important; + /* No overflow-x here - only in mobile media query */ +} + +.dashboard-charts-section { + display: flex !important; + /* ✅ Ensure proper layout */ + height: auto !important; + padding: 0 !important; +} + +.dashboard-module-charts { + display: flex !important; + flex-wrap: nowrap !important; + gap: 12px !important; + padding: 10px 0 !important; + justify-content: stretch !important; + align-items: stretch !important; + width: 100% !important; +} + +/* Statistics cards - flex grow to fill space equally */ +.module-box { + flex: 1 1 0 !important; + /* grow, shrink, basis=0 for equal distribution */ + min-width: 0 !important; + /* allow shrinking below content size */ + max-width: none !important; + padding: 0 !important; +} + +/* Hidden cards should not take space */ +.module-box.genius-hidden { + display: none !important; + flex: 0 0 0 !important; +} + +/* Stat Card Container */ +.module-box .module-box-container { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + border-radius: 16px !important; + padding: 16px !important; + border: 1px solid rgba(255, 255, 255, 0.3) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important; + transition: all var(--dash-transition) !important; + position: relative !important; + overflow: hidden !important; + height: 100% !important; +} + +.module-box .module-box-container::before { + display: none !important; +} + +.module-box .module-box-container:hover { + transform: translateY(-4px) !important; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important; +} + +/* Stat Card Title - ANNUAL LEAVE, SALARY SLIPS, etc */ +.module-box .module-box-container h3 { + font-size: 13px !important; + font-weight: 700 !important; + color: var(--dash-secondary) !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; + margin: 12px 0 0 0 !important; + text-align: center !important; + text-shadow: none !important; + opacity: 1 !important; +} + +/* Stat Values - Big Numbers */ +.module-box .module-box-container h4.leave-data-percent, +.module-box .module-box-container h4.payroll-data-percent, +.module-box .module-box-container h4.timesheet-data-percent, +.module-box .module-box-container h4.attendance-hours-percent { + font-size: 28px !important; + font-weight: 800 !important; + color: var(--dash-primary) !important; + text-align: center !important; + margin: 8px 0 !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; + animation: none !important; + transform: none !important; +} + +/* Stat Labels - GENIUS MINIMAL DESIGN */ +/* Clean inline labels with subtle number highlighting */ +.module-box .module-box-container p { + margin: 3px 0 !important; + text-align: center !important; + line-height: 1.4 !important; +} + +.module-box .module-box-container p span { + font-size: 11px !important; + font-weight: 500 !important; + color: var(--dash-text-light) !important; + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; + background: none !important; + padding: 0 !important; + border-radius: 0 !important; +} + +/* Colored dots - smaller and subtle */ +.module-box .module-box-container p span i.fa-circle { + font-size: 5px !important; + opacity: 0.9 !important; +} + +/* First row dot (Total) - Primary teal */ +.module-box .module-box-container p:first-of-type span i { + color: var(--dash-primary) !important; +} + +/* Second row dot (Remaining) - Warm amber */ +.module-box .module-box-container p:nth-of-type(2) span i { + color: var(--dash-warning) !important; +} + +/* GENIUS NUMBER HIGHLIGHTING */ +/* The actual values get bold treatment with primary color */ +.module-box .module-box-container .leave-total-amount, +.module-box .module-box-container .payroll-total-amount, +.module-box .module-box-container .timesheet-total-amount, +.module-box .module-box-container .attendance-plan-hours { + font-weight: 800 !important; + font-size: 14px !important; + color: var(--dash-primary) !important; + padding: 1px 6px !important; + background: rgba(8, 145, 178, 0.08) !important; + border-radius: 4px !important; + margin-left: 2px !important; +} + +/* Remaining amounts - Amber highlight */ +.module-box .module-box-container .leave-left-amount, +.module-box .module-box-container .payroll-left-amount, +.module-box .module-box-container .timesheet-left-amount, +.module-box .module-box-container .attendance-official-hours { + font-weight: 800 !important; + font-size: 14px !important; + color: var(--dash-warning) !important; + padding: 1px 6px !important; + background: rgba(245, 158, 11, 0.08) !important; + border-radius: 4px !important; + margin-left: 2px !important; +} + +/* Hide the D3 chart container - CSS only approach */ +.module-box #chartContainer, +.module-box #chartPaylips, +.module-box #chartTimesheet, +.module-box #chartAttendanceHours { + height: 140px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +/* Style the SVG charts */ +.module-box svg { + max-height: 140px !important; + margin: 0 auto !important; +} + +/* Chart wrapper for center text positioning */ +.chart-wrapper { + position: relative !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 140px !important; +} + +/* Center text inside donut chart */ +.chart-center-text { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + text-align: center !important; + pointer-events: none !important; + z-index: 10 !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; +} + +.chart-center-value { + font-size: 2rem !important; + font-weight: 800 !important; + color: var(--dash-secondary, #1e293b); + /* No !important - JS sets dynamically */ + line-height: 1 !important; + transition: all 0.3s ease !important; +} + +/* Hover effect on center text */ +.chart-wrapper:hover .chart-center-value { + transform: scale(1.1) !important; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; +} + +.chart-center-unit { + font-size: 1.3rem !important; + font-weight: 600 !important; + color: var(--dash-secondary) !important; + text-transform: lowercase !important; + margin-top: 4px !important; + letter-spacing: 0.5px !important; + transition: all 0.3s ease !important; +} + +.chart-wrapper:hover .chart-center-unit { + color: var(--dash-secondary) !important; +} + +/* ============================================================ + ATTENDANCE SECTION - Modern Button Design + ============================================================ */ + +.dashboard-attendance-section { + border-left: 1px solid rgba(255, 255, 255, 0.1) !important; + display: flex !important; + align-items: center !important; +} + +.attendance-section-body { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + border-radius: 20px !important; + padding: 10px 4px 4px 4px !important; + margin: 4px !important; + text-align: center !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08) !important; + width: 100% !important; + border: 1px solid rgba(0, 0, 0, 0.05) !important; +} + +.attendance-section-body h3 { + font-size: 13px !important; + font-weight: 600 !important; + color: var(--dash-secondary) !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; + margin-bottom: 4px !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; +} + +/* Welcome text - Make sure no extra background */ +.attendance-section-body p.last-checkin-section { + font-size: 12px !important; + color: var(--dash-text) !important; + margin-bottom: 8px !important; + font-weight: 500 !important; + background: none !important; + padding: 0 !important; +} + +/* Check-in/out Arrow Button Container - FORCED VISIBILITY */ +.attendance-section-body .attendance-img-section { + display: flex !important; + justify-content: center !important; + align-items: center !important; + margin-top: 5px !important; + background: transparent !important; + // min-height: 70px !important; +} + +/* Check-in/out Arrow Button - DYNAMIC COLOR using CSS Mask */ +/* The container uses mask-image technique for reliable color matching */ +.attendance-section-body .attendance-img-section { + position: relative !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + cursor: pointer !important; +} + +/* Hide the original img completely - pseudo-element handles display */ +.attendance-section-body .attendance-img-section img, +.attendance-section-body .attendance-img-section img.img-logout, +.attendance-section-body .attendance-img-section img.img-login, +.attendance-section-body .attendance-img-section img.attendance-icon, +.attendance-section-body p.attendance-img-section img { + /* Hide img completely - CSS mask pseudo-element handles the visual */ + display: none !important; +} + +/* Create the visible icon using mask-image on a pseudo-element */ +/* Default state - Check-in (smile icon with success color) */ +.attendance-section-body .attendance-img-section::before { + content: '' !important; + display: block !important; + width: 70px !important; + height: 70px !important; + /* Default: Success color for check-in */ + background-color: var(--dash-success, #10b981) !important; + /* Default: smile icon */ + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/smile.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/smile.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; + transition: all 0.3s ease !important; + pointer-events: none !important; +} + +/* Check-in state - Green success color with smile icon */ +.attendance-section-body .attendance-img-section.state-checkin::before { + background-color: var(--dash-success, #10b981) !important; + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/smile.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/smile.svg') !important; +} + +/* Check-out state - Warning color with sad icon */ +.attendance-section-body .attendance-img-section.state-checkout::before { + background-color: var(--dash-warning, #f59e0b) !important; + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/sad.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/sad.svg') !important; +} + +/* Disabled state - only change icon color, no opacity */ +.attendance-section-body .attendance-img-section.disabled::before { + background-color: #9ca3af !important; + /* Remove opacity to prevent gray rectangle background */ +} + +.attendance-section-body .attendance-img-section.disabled { + cursor: not-allowed !important; + opacity: 0.6 !important; +} + +/* Hover effects */ +.attendance-section-body .attendance-img-section:not(.disabled):hover::before { + transform: scale(1.1) !important; +} + +.attendance-section-body .attendance-img-section.state-checkin:not(.disabled):hover::before { + background-color: var(--dash-success-dark, #059669) !important; +} + +.attendance-section-body .attendance-img-section.state-checkout:not(.disabled):hover::before { + background-color: var(--dash-warning-dark, #d97706) !important; +} + +/* Check-in/out Button - Modern Style */ + + +/* ============================================================ + DASHBOARD BODY - Clean Background + ============================================================ */ + +.dashboard-container .dashboard-body { + background: var(--dash-bg) !important; + padding: 30px !important; +} + +/* ============================================================ + SELF SERVICES HEADER - With Search Bar + ============================================================ */ + +.dashboard-nav-buttons { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + flex-wrap: wrap !important; + gap: 15px !important; + margin-bottom: 25px !important; + padding: 0 0 20px 0 !important; + position: relative !important; +} + +/* Full-width separator line under Self Services & Search */ +.dashboard-nav-buttons::after { + content: '' !important; + position: absolute !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + height: 2px !important; + background: linear-gradient(90deg, + var(--dash-primary-light) 0%, + var(--dash-primary) 50%, + var(--dash-primary-light) 100%) !important; + opacity: 0.6 !important; + border-radius: 2px !important; +} + +.dashboard-nav-buttons hr { + display: none !important; +} + +/* ============================================================ + TAB NAVIGATION - ELEGANT UNDERLINE DESIGN + Modern, minimal, professional - NOT button-like + ============================================================ */ + +.dashboard-nav-buttons .nav-tabs { + background: transparent !important; + border: none !important; + border-radius: 0 !important; + padding: 0 !important; + box-shadow: none !important; + display: inline-flex !important; + gap: 0 !important; + margin: 0 !important; + position: relative !important; +} + +/* Remove all ::before pseudo elements */ +.dashboard-nav-buttons .nav-tabs::before { + display: none !important; +} + +.dashboard-nav-buttons .nav-tabs li { + margin: 0 !important; + position: relative !important; +} + +/* Tab Links - Clean Text Style */ +.dashboard-nav-buttons .nav-tabs li a { + border-radius: 0 !important; + padding: 16px 32px !important; + font-weight: 600 !important; + font-size: 15px !important; + color: var(--dash-text-light, #94a3b8) !important; + background: transparent !important; + border: none !important; + transition: all 0.35s ease !important; + white-space: nowrap !important; + position: relative !important; + letter-spacing: 0.3px !important; + text-decoration: none !important; +} + +/* Animated underline indicator - subtle line always visible */ +.dashboard-nav-buttons .nav-tabs li a::after { + content: '' !important; + position: absolute !important; + bottom: 0 !important; + left: 10% !important; + width: 80% !important; + height: 2px !important; + background: rgba(0, 0, 0, 0.06) !important; + border-radius: 2px 2px 0 0 !important; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +/* Hover - Text color change + underline becomes primary */ +.dashboard-nav-buttons .nav-tabs li a:hover:not(.active) { + color: var(--dash-primary) !important; + background: transparent !important; + transform: none !important; +} + +.dashboard-nav-buttons .nav-tabs li a:hover:not(.active)::after { + left: 5% !important; + width: 90% !important; + background: linear-gradient(90deg, var(--dash-primary-light) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.3) 100%) !important; +} + +/* Active Tab - Full underline with gradient */ +.dashboard-nav-buttons .nav-tabs li.active a, +.dashboard-nav-buttons .nav-tabs li a.active { + color: var(--dash-primary) !important; + font-weight: 700 !important; + background: transparent !important; + box-shadow: none !important; + transform: none !important; +} + +.dashboard-nav-buttons .nav-tabs li.active a::after, +.dashboard-nav-buttons .nav-tabs li a.active::after { + left: 0 !important; + width: 100% !important; + height: 3px !important; + background: linear-gradient(90deg, + var(--dash-primary) 0%, + var(--dash-primary-light) 50%, + var(--dash-primary) 100%) !important; + box-shadow: 0 2px 8px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35) !important; +} + +/* Remove shine animation from this design */ +.dashboard-nav-buttons .nav-tabs li.active a::before, +.dashboard-nav-buttons .nav-tabs li a.active::before { + display: none !important; +} + +/* Icon inside tab (if any) */ +.dashboard-nav-buttons .nav-tabs li a i { + margin-right: 10px !important; + font-size: 22px !important; + opacity: 0.7 !important; + transition: all 0.3s ease !important; +} + +.dashboard-nav-buttons .nav-tabs li a:hover i, +.dashboard-nav-buttons .nav-tabs li a.active i { + opacity: 1 !important; + color: var(--dash-primary) !important; +} + +/* Loading/Refreshing state for cards */ +.card-section-approve.refreshing, +.card-section-track.refreshing { + opacity: 0.5 !important; + pointer-events: none !important; + position: relative !important; +} + +.card-section-approve.refreshing::after, +.card-section-track.refreshing::after { + content: '' !important; + position: absolute !important; + top: 50% !important; + left: 50% !important; + width: 40px !important; + height: 40px !important; + margin: -20px 0 0 -20px !important; + border: 3px solid var(--dash-primary-light) !important; + border-top-color: var(--dash-primary) !important; + border-radius: 50% !important; + animation: tabRefreshSpin 0.8s linear infinite !important; +} + +@keyframes tabRefreshSpin { + to { + transform: rotate(360deg); + } +} + +/* Search Container (will be added via JS) */ +.dashboard-search-container { + display: flex !important; + align-items: center !important; + gap: 10px !important; +} + +.dashboard-search-input { + background: var(--dash-white) !important; + border: 1px solid var(--dash-border) !important; + border-radius: 10px !important; + padding: 10px 16px !important; + font-size: 14px !important; + width: 220px !important; + color: var(--dash-text) !important; + transition: all var(--dash-transition) !important; +} + +.dashboard-search-input::placeholder { + color: var(--dash-text-light) !important; +} + +.dashboard-search-input:focus { + outline: none !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.1) !important; +} + +/* ============================================================ + APPROVAL CARDS (Card2) - GENIUS PREMIUM DESIGN + Uses dynamic colors from settings via CSS variables + ============================================================ */ + +.card2 { + margin-bottom: 24px !important; + padding: 0 10px !important; +} + +.card2 .card-container { + background: var(--dash-white) !important; + border: none !important; + border-radius: 20px !important; + overflow: hidden !important; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.04) !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + position: relative !important; +} + +/* Top accent line on hover */ +.card2 .card-container::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 55 !important; + height: 3px !important; + background: linear-gradient(90deg, var(--dash-primary) 0%, var(--dash-primary-light) 100%) !important; + opacity: 0 !important; + transition: opacity 0.3s ease !important; + z-index: 10 !important; +} + +.card2 .card-container:hover::before { + opacity: 1 !important; +} + +.card2 .card-container:hover { + transform: translateY(-6px) !important; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.1), + 0 4px 12px rgba(0, 0, 0, 0.06) !important; +} + +.card2 .card-container .card-header { + display: flex !important; + justify-content: flex-start !important; + /* تغيير من space-between */ + align-items: center !important; + padding: 20px !important; + gap: 16px !important; + /* مسافة ثابتة بين الصورة والنص */ +} + + +/* Decorative glow in header */ +// .card2 .card-container .card-header::before { +// content: '' !important; +// position: absolute !important; +// top: -50% !important; +// right: -80% !important; +// width: 120px !important; +// height: 120px !important; +// background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%) !important; +// pointer-events: none !important; +// } + +.card2 .card-container .card-header::after { + display: none !important; +} + +// .card2 .card-container .card-header img { +// height: 48px !important; +// width: 48px !important; +// padding: 80px !important; +// background: rgba(255, 255, 255, 0.25) !important; +// border-radius: 12px !important; +// margin-right: 20px !important; +// margin-left: 44px !important; +// transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +// filter: brightness(1.2) !important; +// } + +.card2 .card-container .card-header img { + height: 52px !important; + /* زيادة الحجم */ + width: 52px !important; + padding: 10px !important; + background: rgba(255, 255, 255, 0.25) !important; + border-radius: 12px !important; + margin: 0 !important; + flex-shrink: 0 !important; + /* منع تصغير الصورة */ + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + filter: brightness(1.2) !important; +} + + +/* للأيقونة بدلاً من الصورة */ +.card2 .card-container .card-header i { + font-size: 32px !important; + /* حجم كبير للأيقونة */ + color: rgba(255, 255, 255, 0.95) !important; + flex-shrink: 0 !important; + width: 52px !important; + height: 52px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: rgba(255, 255, 255, 0.15) !important; + border-radius: 12px !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + + +.card2 .card-container:hover .card-header i { + transform: scale(1.12) rotate(-5deg) !important; + background: rgba(255, 255, 255, 0.25) !important; +} + + + +.card2 .card-container:hover .card-header img { + transform: scale(1.12) rotate(-5deg) !important; + background: rgba(255, 255, 255, 0.3) !important; +} + +.card2 .card-container .card-header h4 { + color: white !important; + font-size: 16px !important; + /* حجم مناسب للنص */ + font-weight: 700 !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) !important; + margin: 0 !important; + flex: 1 !important; + letter-spacing: 0.3px !important; + text-align: center !important; + /* النص في المنتصف */ +} + +// .card2 .card-container .card-header h4 { +// color: white !important; +// font-size: 18px !important; +// font-weight: 700 !important; +// text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) !important; +// margin: 0 !important; +// flex: 1 !important; +// letter-spacing: 0.3px !important; +// } + +.card2 .card-container .card-body { + height: 180px !important; + padding: 0 !important; + overflow-y: auto !important; + background: linear-gradient(180deg, #ffffff 0%, #fafafa 100%) !important; +} + +/* Custom scrollbar for card body */ +.card2 .card-container .card-body::-webkit-scrollbar { + width: 5px !important; +} + +.card2 .card-container .card-body::-webkit-scrollbar-track { + background: #f1f1f1 !important; + border-radius: 3px !important; +} + +.card2 .card-container .card-body::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; + border-radius: 3px !important; +} + +.card2 .card-container .card-body table { + margin: 0 !important; + width: 100% !important; +} + +.card2 .card-container .card-body table tr { + border-bottom: 1px solid rgba(0, 0, 0, 0.04) !important; + transition: all 0.25s ease !important; + cursor: pointer !important; +} + +.card2 .card-container .card-body table tr:last-child { + border-bottom: none !important; +} + +.card2 .card-container .card-body table tr:hover { + background: linear-gradient(90deg, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.04) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.08) 100%) !important; +} + +.card2 .card-container .card-body table tr td { + padding: 14px 18px !important; + font-weight: 500 !important; + color: var(--dash-secondary) !important; + font-size: 13px !important; + vertical-align: middle !important; + transition: color 0.2s ease !important; +} + +.card2 .card-container .card-body table tr:hover td { + color: var(--dash-primary-dark) !important; +} + +.card2 .card-container .card-body table tr td:last-child { + text-align: right !important; +} + +/* Count Badge - Uses PRIMARY color from settings */ +// .card2 .card-container .card-body table tr td:last-child div { +// background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-secondary-dark) 100%) !important; +// height: 30px !important; +// width: 30px !important; +// line-height: 30px !important; +// font-size: 12px !important; +// font-weight: 700 !important; +// color: white !important; +// border-radius: 50% !important; +// display: inline-block !important; +// text-align: center !important; +// box-shadow: 0 3px 10px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35) !important; +// transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +// } +/* Count Badge - Uses PRIMARY color from settings */ +.card2 .card-container .card-body table tr td:last-child div { + background: linear-gradient(135deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; + /* تغيير من dash-secondary إلى dash-primary */ + height: 32px !important; + width: 32px !important; + line-height: 32px !important; + /* يجب أن تكون مساوية للـ height */ + font-size: 13px !important; + font-weight: 700 !important; + color: white !important; + border-radius: 50% !important; + display: inline-flex !important; + /* تغيير من inline-block إلى inline-flex */ + align-items: center !important; + /* محاذاة رأسية مثالية */ + justify-content: center !important; + /* محاذاة أفقية مثالية */ + text-align: center !important; + box-shadow: 0 3px 10px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.card2 .card-container .card-body table tr:hover td:last-child div { + transform: scale(1.15) !important; + box-shadow: 0 5px 15px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.45) !important; +} + + + +/* ============================================================ + SELF-SERVICE CARDS (Card3) - Premium Design + ============================================================ */ + +.card3 { + padding: 0 8px !important; + margin-bottom: 20px !important; +} + +.card3 .card-body { + background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%) !important; + border: 2px solid transparent !important; + border-radius: 20px !important; + overflow: hidden !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06), + 0 1px 4px rgba(0, 0, 0, 0.04) !important; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; + position: relative !important; +} + +/* Morphing gradient border on hover */ +.card3 .card-body::before { + content: '' !important; + position: absolute !important; + inset: -2px !important; + background: linear-gradient(135deg, + var(--dash-primary), + var(--dash-primary-light), + var(--dash-secondary-light), + var(--dash-primary)) !important; + background-size: 200% 200% !important; + border-radius: 22px !important; + z-index: -1 !important; + opacity: 0 !important; + transition: opacity 0.4s ease !important; +} + +.card3 .card-body:hover::before { + opacity: 1 !important; + animation: cardGradientBorder 3s ease-in-out infinite !important; +} + +@keyframes cardGradientBorder { + + 0%, + 100% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } +} + +.card3 .card-body:hover { + transform: translateY(-8px) scale(1.02) !important; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12), + 0 8px 16px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) !important; +} + + +.card3 .card-body .box-1 img, +.card3 .card-body .box-1 .service-icon { + height: 60px !important; + width: 60px !important; + margin-bottom: 4px !important; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)) !important; + animation: none !important; + transform-style: preserve-3d !important; + backface-visibility: hidden !important; + position: relative !important; + z-index: 2 !important; +} + +.card3 .card-body:hover .box-1 img, +.card3 .card-body:hover .box-1 .service-icon { + transform: scale(1.18) translateZ(35px) rotate(-5deg) !important; + filter: drop-shadow(0 12px 25px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35)) !important; + color: var(--dash-primary) !important; +} + +/* Genius 3D Tilt Effect on Service Cards */ +.card3 .card-body { + transform-style: preserve-3d !important; + perspective: 1000px !important; +} + +.card3 .card-body:hover .box-1 { + transform: translateZ(20px) !important; +} + + + +.card3 .card-body .box-1 { + height: 220px !important; + background: var(--dash-white) !important; + border: none !important; + padding: 24px 16px !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; +} + +.card3 .card-body .box-1::before { + display: none !important; +} + +/* Icon with glow effect */ +// .card3 .card-body .box-1 img, +// .card3 .card-body .box-1 .service-icon { +// height: 60px !important; +// width: 60px !important; +// /* Ensure width is also consistent */ +// display: inline-block !important; +// /* For icon */ +// font-size: 60px !important; +// /* For icon font size to match height */ +// line-height: 60px !important; +// /* Center vertically */ +// margin-bottom: 4px !important; +// transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; +// filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)) !important; +// animation: none !important; +// } + +.card3 .card-body:hover .box-1 img, +.card3 .card-body:hover .box-1 .service-icon { + transform: scale(1.18) rotate(-5deg) !important; + color: var(--dash-secondary) !important; + /* Ensure icon keeps color but gets effect */ + filter: drop-shadow(0 8px 20px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.3)) !important; +} + +/* Count Number - Gradient text with SECONDARY/PRIMARY */ +.card3 .card-body .box-1 h3 { + font-size: 48px !important; + font-weight: 900 !important; + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-primary) 100%) !important; + -webkit-background-clip: text !important; + -webkit-text-fill-color: transparent !important; + background-clip: text !important; + margin: 8px 0 !important; + text-shadow: none !important; + transition: all 0.3s ease !important; + letter-spacing: -1px !important; +} + +.card3 .card-body:hover .box-1 h3 { + transform: scale(1.08) !important; + filter: brightness(1.1) !important; +} + +/* Service Name - Uses SECONDARY color from settings */ +.card3 .card-body .box-1 h4 { + font-size: 15px !important; + font-weight: 700 !important; + color: var(--dash-secondary) !important; + padding: 8px 16px !important; + margin: 8px 0 0 0 !important; + -webkit-text-fill-color: unset !important; + text-align: center !important; + background: linear-gradient(135deg, rgba(var(--dash-secondary-rgb, 30, 41, 59), 0.04) 0%, rgba(var(--dash-secondary-rgb, 30, 41, 59), 0.08) 100%) !important; + border-radius: 8px !important; + letter-spacing: 0.3px !important; + transition: all 0.3s ease !important; + max-width: 100% !important; +} + + +.card3 .card-body:hover .box-1 h4 { + background: linear-gradient(135deg, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.08) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) 100%) !important; + color: var(--dash-primary-dark) !important; +} + +.card3 .card-body .box-2 { + background: var(--dash-primary) !important; + height: 50px !important; + line-height: 50px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + cursor: pointer !important; +} + +.card3 .card-body .box-2::before { + display: none !important; +} + +.card3 .card-body .box-2 span { + font-size: 14px !important; + font-weight: 700 !important; + color: white !important; + letter-spacing: 0.5px !important; + transition: all 0.4s ease !important; +} + +/* BIGGER + ICON */ +.card3 .card-body .box-2 i { + font-size: 24px !important; + font-weight: 700 !important; + color: white !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; +} + +/* Genius hover for Add New button */ +.card3 .card-body .box-2:hover { + background: linear-gradient(135deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; + box-shadow: 0 6px 20px rgba(var(--dash-primary-rgb, 75, 155, 75), 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; +} + +.card3 .card-body .box-2:hover span { + letter-spacing: 1.5px !important; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2) !important; +} + +.card3 .card-body .box-2:hover i { + transform: rotate(180deg) scale(1.2) !important; + text-shadow: 0 0 15px rgba(255, 255, 255, 0.5) !important; +} + +/* ============================================================ + CARD ENTRY ANIMATIONS + ============================================================ */ + +.card-section1 .card2, +.card-section .card2, +.card-section1 .card3, +.card-section .card3 { + animation: cardFadeIn 0.35s ease-out backwards !important; +} + +@keyframes cardFadeIn { + from { + opacity: 0; + transform: translateY(12px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.card-section1 .card2:nth-child(1), +.card-section1 .card3:nth-child(1) { + animation-delay: 0.04s !important; +} + +.card-section1 .card2:nth-child(2), +.card-section1 .card3:nth-child(2) { + animation-delay: 0.08s !important; +} + +.card-section1 .card2:nth-child(3), +.card-section1 .card3:nth-child(3) { + animation-delay: 0.12s !important; +} + +.card-section1 .card2:nth-child(4), +.card-section1 .card3:nth-child(4) { + animation-delay: 0.16s !important; +} + +.card-section1 .card2:nth-child(5), +.card-section1 .card3:nth-child(5) { + animation-delay: 0.20s !important; +} + +.card-section1 .card2:nth-child(6), +.card-section1 .card3:nth-child(6) { + animation-delay: 0.24s !important; +} + +/* ============================================================ + LOADING SPINNER + ============================================================ */ + +.charts-over-layer { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(5px) !important; + border-radius: 16px !important; +} + +.lds-roller div::after { + background: var(--dash-primary) !important; +} + +/* ============================================================ + RESPONSIVE ADJUSTMENTS - COMPREHENSIVE + ============================================================ */ + +/* Large tablets and small desktops (992px and below) */ +@media (max-width: 992px) { + .module-box { + min-width: 140px !important; + padding: 15px 12px !important; + } + + .dashboard-search-input, + .genius-search-input { + width: 200px !important; + } + + .card3 .card-body .box-1 { + height: 180px !important; + padding: 20px 12px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 32px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 60px !important; + height: 60px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 60px !important; + height: 60px !important; + } +} + +/* Tablets (768px and below) */ +@media (max-width: 768px) { + .dashboard-container .dashboard-header { + padding: 20px 15px !important; + border-radius: 0 0 20px 20px !important; + } + + .dashboard-nav-buttons { + flex-direction: column !important; + align-items: stretch !important; + gap: 12px !important; + } + + .dashboard-nav-buttons::after { + display: none !important; + } + + .dashboard-search-container, + .genius-search-container { + width: 100% !important; + } + + .dashboard-search-input, + .genius-search-input { + width: 100% !important; + } + + .profile-container { + padding: 16px 14px !important; + margin: 8px !important; + border-radius: 16px !important; + } + + .profile-container .pp-image-section img { + height: 70px !important; + width: 70px !important; + } + + .profile-container .info-section h2 { + font-size: 16px !important; + } + + .module-box { + min-width: 120px !important; + padding: 12px 10px !important; + } + + .module-box h3 { + font-size: 11px !important; + } + + .module-box h4 { + font-size: 20px !important; + } + + .card2 .card-container { + min-width: auto !important; + } + + .card3 .card-body .box-1 { + height: 160px !important; + padding: 16px 10px !important; + } + + .card3 .card-body .box-1 img { + height: 45px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 28px !important; + } + + .card3 .card-body .box-1 h4 { + font-size: 12px !important; + } + + .card3 .card-body .box-2 { + height: 45px !important; + line-height: 45px !important; + } + + .card3 .card-body .box-2 span { + font-size: 12px !important; + } + + .card3 .card-body .box-2 i { + font-size: 20px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 55px !important; + height: 55px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 55px !important; + height: 55px !important; + } +} + +/* Mobile devices (576px and below) */ +@media (max-width: 576px) { + + /* ============================================ + HEADER LAYOUT - BLOCK DISPLAY FOR STACKING + Key fix: Use display:block to force normal + document flow instead of flex + ============================================ */ + .dashboard-container .dashboard-header { + display: block !important; + /* KEY: Block forces stacking */ + padding: 10px !important; + border-radius: 0 0 16px 16px !important; + height: auto !important; + min-height: auto !important; + overflow: visible !important; + padding-bottom: 60px !important; + /* MASSIVE padding to fix overflow */ + position: relative !important; + } + + /* Ensure gradient overlay covers extended height */ + .dashboard-container .dashboard-header::before { + height: 100% !important; + min-height: 100% !important; + bottom: 0 !important; + } + + /* ============================================ + PROFILE SECTION - Full width block + ============================================ */ + .dashboard-user-data-section { + display: block !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + padding: 10px !important; + float: none !important; + } + + /* Single Row Tabs for Mobile */ + .dashboard-nav-buttons .nav-tabs { + flex-wrap: nowrap !important; + /* Force single row */ + overflow-x: auto !important; + /* scroll if needed */ + justify-content: flex-start !important; + width: 100% !important; + padding: 5px 0 !important; + } + + .dashboard-nav-buttons .nav-tabs li .nav-link { + padding: 6px 10px !important; + /* Smaller padding */ + font-size: 13px !important; + /* Smaller text */ + white-space: nowrap !important; + } + + /* ... rest of profile section styles ... */ + + + .profile-container { + padding: 15px !important; + margin: 0 auto 10px auto !important; + border-radius: 16px !important; + width: 100% !important; + // max-width: 250px !important; + } + + .profile-container .pp-image-section img, + .profile-container .pp-image-section .img-box { + height: 80px !important; + width: 80px !important; + } + + .profile-container .info-section h2 { + font-size: 16px !important; + } + + .profile-container .info-section p { + font-size: 11px !important; + } + + /* ============================================ + STATISTICS SECTION - CSS Grid Layout (2 columns) + Key fix: Replace horizontal scroll with grid + ============================================ */ + .dashboard-user-statistics-section { + display: block !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + padding: 28px 10px 40px 10px !important; + overflow: visible !important; + /* No more horizontal scroll */ + float: none !important; + } + + .dashboard-charts-section { + display: block !important; + width: 100% !important; + height: auto !important; + padding: 0 !important; + } + + .dashboard-module-charts { + display: grid !important; + grid-template-columns: repeat(2, 1fr) !important; + /* 2 columns */ + gap: 10px !important; + width: 100% !important; + padding: 5px !important; + } + + /* Override Bootstrap col classes for grid children */ + .dashboard-module-charts .module-box, + .dashboard-module-charts .module-box.col-12, + .dashboard-module-charts .module-box.col-sm-6 { + width: 100% !important; + max-width: 100% !important; + min-width: unset !important; + flex: unset !important; + padding: 0 !important; + } + + .module-box .module-box-container { + padding: 12px 10px !important; + border-radius: 12px !important; + height: 100% !important; + min-height: 120px !important; + } + + .module-box h3 { + font-size: 9px !important; + letter-spacing: 0.5px !important; + margin: 6px 0 0 0 !important; + } + + .module-box h4 { + font-size: 20px !important; + margin: 6px 0 !important; + } + + .module-box p { + font-size: 9px !important; + margin: 4px 0 !important; + } + + .module-box p span { + font-size: 9px !important; + gap: 4px !important; + } + + .module-box #chartContainer, + .module-box #chartPaylips, + .module-box #chartTimesheet { + height: 55px !important; + } + + .module-box svg { + // max-height: 55px !important; + } + + /* ============================================ + ATTENDANCE SECTION - Full width below cards + ============================================ */ + .dashboard-attendance-section { + width: 100% !important; + max-width: 100% !important; + min-width: unset !important; + flex: unset !important; + padding: 10px 0 !important; + border-left: none !important; + border-top: 1px solid rgba(255, 255, 255, 0.1) !important; + margin-top: 10px !important; + } + + .attendance-section-body { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + // padding: 15px !important; + margin: 0 5px !important; + border-radius: 12px !important; + gap: 12px !important; + } + + .attendance-section-body h3 { + font-size: 12px !important; + margin-bottom: 8px !important; + text-align: center !important; + } + + .attendance-section-body p.last-checkin-section { + font-size: 11px !important; + margin-bottom: 8px !important; + text-align: center !important; + } + + .attendance-img-section { + margin-bottom: 8px !important; + } + + .attendance-details-section { + text-align: center !important; + width: 100% !important; + } + + + + /* ============================================ + BODY SECTION + ============================================ */ + .dashboard-container .dashboard-body { + padding: 10px !important; + } + + .card3 { + padding: 0 4px !important; + margin-bottom: 15px !important; + } + + .card3 .card-body { + border-radius: 12px !important; + } + + .card3 .card-body .box-1 { + height: 140px !important; + padding: 12px 8px !important; + } + + .card3 .card-body .box-1 img { + height: 40px !important; + margin-bottom: 8px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 24px !important; + margin: 4px 0 !important; + } + + .card3 .card-body .box-1 h4 { + font-size: 11px !important; + } + + .card3 .card-body .box-2 { + height: 40px !important; + line-height: 40px !important; + gap: 6px !important; + } + + .card3 .card-body .box-2 span { + font-size: 11px !important; + } + + .card3 .card-body .box-2 i { + font-size: 18px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 50px !important; + height: 50px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 50px !important; + height: 50px !important; + } + + .dashboard-nav-buttons { + flex-direction: column !important; + gap: 15px !important; + padding-bottom: 15px !important; + align-items: center !important; + } + + /* Single Row Tabs for Mobile */ + .dashboard-nav-buttons .nav-tabs { + flex-wrap: nowrap !important; + /* Force single row */ + overflow-x: auto !important; + /* scroll */ + justify-content: flex-start !important; + /* Start align for scroll */ + width: 100% !important; + padding: 5px 0 !important; + display: flex !important; + -webkit-overflow-scrolling: touch !important; + } + + .dashboard-nav-buttons .nav-tabs li { + flex: 0 0 auto !important; + display: block !important; + } + + /* CRITICAL: Keep approval tabs hidden until JS shows them */ + .dashboard-nav-buttons .nav-tabs li.approval-tab-item[style*="display:none"], + .dashboard-nav-buttons .nav-tabs li.approval-tab-item[style*="display: none"] { + display: none !important; + } + + .dashboard-nav-buttons .nav-tabs li .nav-link { + padding: 8px 14px !important; + font-size: 12px !important; + } + + /* Fix Search Bar Alignment */ + .genius-search-container { + width: 100% !important; + margin: 0 !important; + justify-content: center !important; + position: relative !important; + } + + .genius-search-wrapper { + // width: 100% !important; + max-width: 100% !important; + position: relative !important; + } + + /* Center search input text */ + .genius-search-input { + width: 100% !important; + text-align: center !important; + padding-left: 40px !important; + padding-right: 40px !important; + box-sizing: border-box !important; + } + + /* Position search icon inside input - RTL SAFE */ + .genius-search-icon { + position: absolute !important; + right: 40px !important; + /* Moved DEEP inside */ + left: auto !important; + top: 50% !important; + transform: translateY(-50%) !important; + pointer-events: none !important; + z-index: 1000 !important; + /* Very high z-index */ + color: var(--dash-primary) !important; + } +} + +/* Extra small devices (400px and below) */ +@media (max-width: 400px) { + + /* Fix Overlap Issue: Add margin to attendance section */ + .dashboard-attendance-section { + margin: 20px auto 0 auto !important; + /* Centering with auto margins */ + padding-top: 10px !important; + border-top: 1px solid rgba(0, 0, 0, 0.05) !important; + width: 90% !important; + /* Contain width */ + max-width: 350px !important; + } + + .attendance-section-body { + width: 100% !important; + margin: 0 !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05) !important; + } + + .profile-container .pp-image-section img, + .profile-container .pp-image-section .img-box { + height: 60px !important; + width: 60px !important; + } + + .profile-container .info-section h2 { + font-size: 14px !important; + } + + .profile-container .info-section p { + font-size: 10px !important; + } + + /* Single column cards on very small screens */ + .dashboard-module-charts { + grid-template-columns: 1fr !important; + /* Single column */ + gap: 8px !important; + } + + .module-box .module-box-container { + min-height: 100px !important; + } + + .module-box h4 { + font-size: 18px !important; + } + + .module-box h3 { + font-size: 8px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 20px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 45px !important; + height: 45px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 45px !important; + height: 45px !important; + } +} + +/* ============================================================ + HIDDEN CLASS FOR SEARCH FILTER + ============================================================ */ + +.card-hidden, +.genius-hidden { + display: none !important; +} + +/* ============================================================ + GENIUS SEARCH BAR - Premium Design + ============================================================ */ + +.genius-search-container { + display: flex !important; + align-items: center !important; + margin-left: auto !important; +} + +.genius-search-wrapper { + position: relative !important; + display: flex !important; + align-items: center !important; +} + +.genius-search-icon { + position: absolute !important; + left: 14px !important; + color: var(--dash-text-light) !important; + font-size: 14px !important; + pointer-events: none !important; + z-index: 1 !important; +} + +.genius-search-input { + background: var(--dash-white) !important; + border: 1px solid var(--dash-border) !important; + border-radius: 25px !important; + padding: 10px 40px 10px 38px !important; + font-size: 14px !important; + width: 300px !important; + color: var(--dash-text) !important; + transition: all var(--dash-transition) !important; + box-shadow: var(--dash-shadow-sm) !important; +} + +.genius-search-input::placeholder { + color: var(--dash-text-light) !important; +} + +.genius-search-input:focus { + outline: none !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.15) !important; +} + +.genius-search-clear { + position: absolute !important; + right: 14px !important; + color: var(--dash-text-light) !important; + font-size: 18px !important; + cursor: pointer !important; + line-height: 1 !important; + transition: color var(--dash-transition) !important; +} + +.genius-search-clear:hover { + color: var(--dash-danger) !important; +} + +/* ============================================================ + SELF SERVICES - Title Styling (Not Button) + ============================================================ */ + +/* Style Self Services tab as a prominent title when it's the only tab */ +.dashboard-nav-buttons .nav-tabs li.genius-single-tab { + background: none !important; + box-shadow: none !important; + border: none !important; +} + +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a, +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a.genius-title-styled { + background: transparent !important; + color: var(--dash-secondary) !important; + font-size: 18px !important; + font-weight: 700 !important; + padding: 8px 4px 12px 4px !important; + box-shadow: none !important; + border: none !important; + border-bottom: 3px solid var(--dash-primary) !important; + cursor: default !important; + text-transform: none !important; + letter-spacing: 0.3px !important; + border-radius: 0 !important; +} + +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a:hover, +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a.genius-title-styled:hover { + background: transparent !important; + color: var(--dash-secondary) !important; + transform: none !important; +} + +/* When there's only one tab, make the nav-tabs transparent */ +.dashboard-nav-buttons .nav-tabs:has(.genius-single-tab) { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; +} + +/* ============================================================ + CSS-ONLY SINGLE TAB DETECTION + When nav-tabs has only one li, style it as a title, not a button + ============================================================ */ + +/* CSS fallback for single tab - using :only-child selector */ +.dashboard-nav-buttons .nav-tabs li:only-child { + background: none !important; + box-shadow: none !important; + border: none !important; +} + +.dashboard-nav-buttons .nav-tabs li:only-child a, +.dashboard-nav-buttons .nav-tabs li:only-child a.nav-link, +.dashboard-nav-buttons .nav-tabs li:only-child a.active { + background: transparent !important; + color: var(--dash-secondary) !important; + font-size: 20px !important; + font-weight: 700 !important; + padding: 8px 0 12px 0 !important; + box-shadow: none !important; + border: none !important; + border-bottom: 3px solid var(--dash-primary) !important; + cursor: default !important; + text-transform: none !important; + letter-spacing: -0.3px !important; + border-radius: 0 !important; +} + +.dashboard-nav-buttons .nav-tabs li:only-child a:hover, +.dashboard-nav-buttons .nav-tabs li:only-child a.active:hover { + background: transparent !important; + color: var(--dash-secondary) !important; + transform: none !important; +} + +/* When nav-tabs has only one child, make parent transparent */ +.dashboard-nav-buttons .nav-tabs:has(li:only-child) { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; + display: block !important; +} + +/* ============================================================ + SERVICE CARDS (Card3) - GENIUS SUBTLE ENHANCEMENTS + ============================================================ */ + +/* Card3 number styling - subtle genius effect */ +.card3 .card-body .box-1 h3 { + position: relative !important; + color: var(--dash-primary) !important; +} + +/* Card3 title - elegant styling */ +.card3 .card-body .box-2 h4 { + color: var(--dash-secondary) !important; + font-weight: 600 !important; + letter-spacing: -0.2px !important; + transition: color var(--dash-transition) !important; +} + +.card3 .card-body:hover .box-2 h4 { + color: var(--dash-primary) !important; +} + +/* Card3 icon subtle glow on hover */ +.card3 .card-body .box-2 i { + color: var(--dash-text-light) !important; + transition: all var(--dash-transition) !important; +} + +.card3 .card-body:hover .box-2 i { + color: var(--dash-primary) !important; + text-shadow: 0 0 8px rgba(8, 145, 178, 0.3) !important; +} + +/* ============================================================ + GREEN DOT - FINAL POSITION FIX + ============================================================ */ + +/* Ensure green dot is in correct position */ +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; +} + + + +/* ============================================================ + RTL SUPPORT - Essential Overrides + ============================================================ + These are rules that Odoo's rtlcss cannot handle automatically. + Only use .o_rtl class (added by Odoo to body for RTL languages). + + Rules that rtlcss DOES handle automatically: + - left ↔ right + - margin-left ↔ margin-right + - padding-left ↔ padding-right + - float: left ↔ float: right + - text-align: left ↔ text-align: right + + Only add manual overrides when rtlcss produces incorrect results + or when specific design requires different behavior. + ============================================================ */ + +.o_rtl { + + /* === Profile Section RTL === */ + .profile-container { + .pp-info-section { + border-left: none; + border-right: 1px solid #9f9f9f; + /*rtl:ignore*/ + } + + /* Green status dot - keep on left side in RTL */ + .pp-image-section::after { + right: auto !important; + left: 8px !important; + /*rtl:ignore*/ + } + } + + /* === Search Bar RTL === */ + .genius-search-icon { + left: auto !important; + right: 14px !important; + /*rtl:ignore*/ + } + + .genius-search-input { + padding: 10px 38px 10px 40px !important; + /*rtl:ignore*/ + } + + .genius-search-clear { + right: auto !important; + left: 14px !important; + /*rtl:ignore*/ + } + + /* === Card2 (Approval Cards) RTL === */ + .card2 { + .card-container .card-header { + flex-direction: row-reverse !important; + /*rtl:ignore*/ + + img { + margin-right: 0 !important; + margin-left: 12px !important; + /*rtl:ignore*/ + } + } + + .card-container .card-body table tr td { + &:last-child { + div { + float: left !important; + /*rtl:ignore*/ + } + } + } + } + + /* === Card3 (Service Cards) RTL === */ + .card3 .card-body .box-2 { + flex-direction: row-reverse !important; + /*rtl:ignore*/ + + i { + margin-left: 0 !important; + margin-right: 8px !important; + /*rtl:ignore*/ + } + } + + /* === Attendance Section RTL === */ + .dashboard-attendance-section { + border-left: none !important; + border-right: 1px solid rgba(255, 255, 255, 0.1) !important; + /*rtl:ignore*/ + } +} + +/* ============================================================ + CRITICAL OVERRIDES - MUST LOAD LAST + These rules override conflicting styles from core.scss + ============================================================ */ + + + +/* 2. FIX: Remove ALL colored theme backgrounds from stat boxes */ +.module-box-container, +.module-box .module-box-container, +.module-box-container.theme-1, +.module-box-container.theme-2, +.module-box-container.theme-3, +.module-box .module-box-container.theme-1, +.module-box .module-box-container.theme-2, +.module-box .module-box-container.theme-3 { + background: var(--dash-white, #ffffff) !important; + background-color: var(--dash-white, #ffffff) !important; +} + +/* Also remove colored backgrounds from module-icon inside themed containers */ +.module-box-container.theme-1 .module-icon, +.module-box-container.theme-2 .module-icon, +.module-box-container.theme-3 .module-icon { + background: var(--dash-primary) !important; +} + +/* 3. FIX: COMPLETE OVERRIDE - Remove ALL borders from nav-tabs (core.scss adds them) */ + + +/* 4. FIX: Correct green dot position - removed by user request */ +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; +} + + + +/* 5. FIX: Round corners on attendance/statistics white box */ +.attendance-section-body, +.dashboard-attendance-section .attendance-section-body, +.module-box-container, +.module-box .module-box-container { + border-radius: 10px !important; +} + +/* 6. FIX: Ensure animated border doesn't appear */ +.dashboard-nav-buttons .nav-tabs::before, +.dashboard-nav-buttons .nav-tabs::after, +.nav-tabs::before, +.nav-tabs::after { + display: none !important; + content: none !important; +} + +/* 7. FIX: Remove any focus/active border styling that might cause green border */ +.dashboard-nav-buttons .nav-tabs li a:focus, +.dashboard-nav-buttons .nav-tabs li a:active, +.nav-tabs li a:focus, +.nav-tabs li a:active { + outline: none !important; + border: none !important; + box-shadow: none !important; +} + +/* 8. FIX: Remove any animated transitions on initial load */ +.dashboard-container *, +.system-dashboard * { + animation-delay: 0s !important; +} + +/* Disable card entrance animations that cause "reload" glitch */ +@keyframes none-animation { + from { + opacity: 1; + transform: none; + } + + to { + opacity: 1; + transform: none; + } +} + +.card-section .card, +.card-section1 .card-line, +.dashboard-module-charts .module-box { + animation: none !important; + opacity: 1 !important; + transform: none !important; +} + +/* ============================================================ + CHART TOOLTIP - Premium Styling + ============================================================ */ + +/* Override the ugly black tooltip */ +.pc-tooltip { + background: var(--dash-secondary) !important; + color: white !important; + font-size: 12px !important; + font-weight: 500 !important; + padding: 8px 14px !important; + border-radius: 8px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + backdrop-filter: blur(8px) !important; + -webkit-backdrop-filter: blur(8px) !important; + line-height: 1.4 !important; + max-width: 200px !important; + text-align: center !important; +} + +/* ============================================================ + CHART HALF-CIRCLES - Subtle Enhancements + ============================================================ */ + +/* Chart container subtle glow */ +#chartContainer, +#chartPaylips, +#chartTimesheet { + position: relative !important; +} + +#chartContainer svg, +#chartPaylips svg, +#chartTimesheet svg { + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08)) !important; +} + +/* Chart paths - subtle gradient effect */ +.module-box svg path { + transition: opacity var(--dash-transition) !important; +} + +.module-box:hover svg path { + opacity: 0.9 !important; +} + + +/* ============================================================ + ADD NEW BUTTON - Enhanced Visibility and Hover + ============================================================ */ + +/* Ensure + icon is always visible */ +.card3 .card-body .box-2 i.mdi-plus, +.card3 .card-body .box-2 i[class*="plus"] { + color: white !important; + font-size: 20px !important; + font-weight: bold !important; + opacity: 1 !important; + visibility: visible !important; +} + +/* Add New button - base state */ +.card3 .card-body .box-2 { + background: var(--dash-primary) !important; + transition: all 0.25s ease !important; + cursor: pointer !important; + position: relative !important; + overflow: hidden !important; +} + +/* Add subtle shine overlay */ +.card3 .card-body .box-2::after { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: -100% !important; + width: 100% !important; + height: 100% !important; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; + transition: left 0.5s ease !important; +} + +/* Special hover for Add New button area */ +.card3 .card-body .box-2:hover { + background: var(--dash-primary-dark) !important; + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.4) !important; +} + +.card3 .card-body .box-2:hover::after { + left: 100% !important; +} + +.card3 .card-body .box-2:hover i { + transform: rotate(90deg) scale(1.1) !important; +} + +.card3 .card-body .box-2:hover span { + letter-spacing: 1px !important; +} + +/* ============================================================ + CHECK-IN BUTTON - Ensure Theme Variable Usage + ============================================================ */ + +/* Re-enforce theme variable on check-in button */ + + +/* Check-in/out button styles now defined earlier in file */ +/* Removed duplicate rule that conflicted with white bg approach */ + +/* ============================================================ + THEME VARIABLES - Complete Element Binding + ============================================================ */ + +/* Card2 header gradient - use secondary */ +.card2 .card-container .card-header { + background: linear-gradient(135deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; +} + +/* Card2 hover accent */ +.card2 .card-container:hover { + border-color: var(--dash-primary-light) !important; +} + +/* Action buttons in cards */ +.card2 .card-body table tr td:last-child div, +.card2 .card-body .btn-action { + background: var(--dash-primary) !important; +} + +/* Search input focus */ +.genius-search-input:focus, +.dashboard-search-input:focus { + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) !important; +} + +/* Single tab underline */ +.dashboard-nav-buttons .nav-tabs li:only-child a { + border-bottom-color: var(--dash-primary) !important; +} + +/* Scrollbar thumb */ +.card2 .card-container .card-body::-webkit-scrollbar-thumb { + background: var(--dash-primary) !important; +} + +/* Status indicators */ +.attendance-section-body .attendance-status.online, +.status-online { + color: var(--dash-success) !important; + background: rgba(var(--dash-success-rgb, 16, 185, 129), 0.1) !important; +} + +.attendance-section-body .attendance-status.offline, +.status-offline { + color: var(--dash-danger, #ef4444) !important; +} + +/* ============================================================ + CRITICAL: ATTENDANCE SECTION OVERRIDES + ============================================================ */ + +/* Ensure all text inside attendance has no extra backgrounds */ +.attendance-section-body p, +.attendance-section-body span, +.attendance-section-body .last-checkin-section { + background: none !important; + background-color: transparent !important; +} + + + +/* ============================================================ + CRITICAL: PROFILE SECTION VISIBILITY + ============================================================ */ + +/* Ensure profile text is visible on glassmorphism background */ +.profile-container .info-section .fn-section, +.profile-container .info-section .fn-job, +.profile-container .info-section .fn-id { + color: white !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; +} + +.profile-container .info-section .fn-section { + font-size: 16px !important; + font-weight: 700 !important; +} + +.profile-container .info-section .fn-job { + font-size: 11px !important; + font-weight: 500 !important; + opacity: 0.85 !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + + +/* ============================================================ + ENHANCED CELEBRATION MODE - Persistent Birthday & Anniversary + ============================================================ */ + +/* Dashboard-wide celebration mode class */ +.dashboard-container.celebration-mode { + position: relative; +} + +/* Festive header gradient animation for birthday */ +.dashboard-container.birthday-mode .dashboard-header { + background: linear-gradient(135deg, #667eea 0%, #f093fb 50%, #f5576c 100%) !important; + background-size: 200% 200% !important; + animation: festiveGradient 5s ease infinite !important; +} + +/* Festive header gradient animation for anniversary */ +.dashboard-container.anniversary-mode .dashboard-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #667eea 100%) !important; + background-size: 200% 200% !important; + animation: festiveGradient 5s ease infinite !important; +} + +@keyframes festiveGradient { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +/* Golden glow ring around employee photo */ +.img-box.celebration-glow { + animation: photoGlow 2s ease-in-out infinite; + box-shadow: 0 0 0 4px rgba(255, 215, 0, 0.6), + 0 0 20px rgba(255, 215, 0, 0.4), + 0 0 40px rgba(255, 215, 0, 0.2) !important; +} + +.birthday-mode .img-box.celebration-glow { + box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.7), + 0 0 20px rgba(255, 105, 180, 0.5), + 0 0 40px rgba(255, 105, 180, 0.3) !important; +} + +.anniversary-mode .img-box.celebration-glow { + box-shadow: 0 0 0 4px rgba(79, 172, 254, 0.7), + 0 0 20px rgba(79, 172, 254, 0.5), + 0 0 40px rgba(79, 172, 254, 0.3) !important; +} + +@keyframes photoGlow { + + 0%, + 100% { + transform: scale(1); + filter: brightness(1); + } + + 50% { + transform: scale(1.02); + filter: brightness(1.1); + } +} + +/* ==================================== + WORK TIMER WIDGET - Bottom Placement + Minimal, professional design + ==================================== */ + +.work-timer-widget, +.work-timer-compact { + background: transparent !important; + border-top: 1px solid rgba(0, 0, 0, 0.05) !important; + border-radius: 0 !important; + padding: 8px 0 0 0 !important; + margin-top: 8px !important; + margin-bottom: 0 !important; + color: var(--dash-text) !important; + box-shadow: none !important; + width: 100% !important; + text-align: center !important; + + &.overtime { + + // Subtle warning text for overtime + .timer-value { + color: var(--dash-warning, #f59e0b) !important; + } + + animation: none !important; + } + + &.inactive, + &.completed { + background: transparent !important; + padding: 8px 0 0 0 !important; + + i { + display: none !important; // Hide icon for cleaner look + } + + p, + .timer-label { + margin: 0 !important; + opacity: 0.8 !important; + font-size: 11px !important; + color: var(--dash-text-light, #94a3b8) !important; + } + + .timer-info-msg { + display: flex !important; + justify-content: center !important; + align-items: center !important; + gap: 5px !important; + font-size: 11px !important; + color: var(--dash-text-light, #94a3b8) !important; + + i { + display: block !important; + font-size: 12px !important; + margin: 0 !important; + color: var(--dash-text-light, #94a3b8) !important; + } + } + } +} + +// Remove pulse animation +@keyframes pulse-timer-warning { + // Empty +} + +.timer-header { + display: none !important; // Hide header/icon for minimal look +} + +// Timer Value (The big numbers) +.timer-value { + font-size: 16px !important; + font-weight: 700 !important; + color: var(--dash-primary, #0891b2) !important; + font-family: 'SF Mono', 'Roboto Mono', 'Consolas', monospace !important; + letter-spacing: 0.5px !important; + line-height: 1.2 !important; +} + +// Timer Label (Elapsed / Total Hours) +.timer-label { + font-size: 10px !important; + font-weight: 500 !important; + color: var(--dash-text-light, #94a3b8) !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; + margin-top: 2px !important; +} + +/* Progress Bar - Visible & Professional Design */ +.timer-progress-bg { + width: auto !important; + background-color: #e2e8f0 !important; + /* Light gray-blue track - more visible */ + border-radius: 6px !important; + height: 12px !important; + /* Thicker for visibility */ + overflow: hidden !important; + margin: 5px 8px !important; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1) !important; + position: relative !important; + border: 1px solid rgba(0, 0, 0, 0.05) !important; +} + +.timer-progress-fill { + height: 100% !important; + border-radius: 6px !important; + /* Uses the Warning color from module settings */ + background: var(--dash-warning, #f59e0b) !important; + box-shadow: + inset 0 2px 4px rgba(255, 255, 255, 0.3), + 0 2px 6px rgba(0, 0, 0, 0.15) !important; + transition: width 1s ease-out !important; + position: relative !important; + min-width: 2px !important; + /* Always show a bit */ + + /* Animated Shimmer Effect */ + &::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + background: linear-gradient(90deg, + transparent 0%, + rgba(255, 255, 255, 0.4) 50%, + transparent 100%) !important; + animation: shimmer 2s infinite !important; + } + + /* Overtime State - Warm Amber/Orange */ + &.overtime { + background: linear-gradient(135deg, #ea580c 0%, #f59e0b 50%, #fbbf24 100%) !important; + box-shadow: + inset 0 2px 4px rgba(255, 255, 255, 0.3), + 0 2px 6px rgba(245, 158, 11, 0.4) !important; + } +} + +/* Shimmer Animation */ +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +// Layout for checked-out state (Compact row) +.timer-row { + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + align-items: center !important; + + .timer-icon { + display: none !important; + } + + .timer-content { + text-align: center !important; + } +} + +/* Celebration badge near employee name */ +.celebration-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + margin-bottom: 8px; + animation: badgePop 0.5s ease-out, badgeGlow 2s ease-in-out infinite; +} + +.celebration-badge.birthday { + background: linear-gradient(135deg, #f093fb, #f5576c); + color: white; + box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4); +} + +.celebration-badge.anniversary { + background: linear-gradient(135deg, #4facfe, #00f2fe); + color: white; + box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4); +} + +.badge-emoji { + font-size: 16px; + animation: bounce 1s ease infinite; +} + +.badge-text { + letter-spacing: 0.3px; +} + +@keyframes badgePop { + 0% { + transform: scale(0); + opacity: 0; + } + + 50% { + transform: scale(1.2); + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes badgeGlow { + + 0%, + 100% { + box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4); + } + + 50% { + box-shadow: 0 4px 25px rgba(245, 87, 108, 0.6); + } +} + +/* Floating celebration message */ +.celebration-floating-msg { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + margin-top: 12px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + animation: floatIn 0.6s ease-out, floatBounce 3s ease-in-out infinite; + position: relative; + overflow: hidden; +} + +.celebration-floating-msg::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + animation: shimmer 3s ease-in-out infinite; +} + +.celebration-floating-msg.birthday { + background: linear-gradient(135deg, rgba(240, 147, 251, 0.15), rgba(245, 87, 108, 0.15)); + border: 1px solid rgba(245, 87, 108, 0.3); + color: #fff; +} + +.celebration-floating-msg.anniversary { + background: linear-gradient(135deg, rgba(79, 172, 254, 0.15), rgba(0, 242, 254, 0.15)); + border: 1px solid rgba(79, 172, 254, 0.3); + color: #fff; +} + +.floating-emoji { + font-size: 20px; + animation: bounce 1.5s ease infinite; +} + +.floating-text { + flex: 1; +} + +@keyframes floatIn { + 0% { + opacity: 0; + transform: translateY(20px); + } + + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes floatBounce { + + 0%, + 100% { + transform: translateY(0); + } + + 50% { + transform: translateY(-3px); + } +} + +@keyframes shimmer { + 0% { + left: -100%; + } + + 100% { + left: 100%; + } +} + +@keyframes bounce { + + 0%, + 20%, + 50%, + 80%, + 100% { + transform: translateY(0); + } + + 40% { + transform: translateY(-10px); + } + + 60% { + transform: translateY(-5px); + } +} + + +/* ============================================================ + ANIMATED COUNTERS - Smooth number transitions + ============================================================ */ + +/* Ensure counter elements have proper number styling */ +.chart-center-value, +.leave-center-value, +.leave-total-amount span, +.leave-left-amount span, +.timesheet-center-value, +.attendance-center-value { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; +} + +/* Counter value pulse on load */ +.counter-animated { + animation: counterPop 0.3s ease-out; +} + +@keyframes counterPop { + 0% { + transform: scale(0.8); + opacity: 0; + } + + 50% { + transform: scale(1.05); + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + + +/* ============================================================ + ENHANCED CELEBRATION DECORATIONS - More Festive Effects + ============================================================ */ + +/* Shimmer effect on greeting TEXT only (not emoji) - NO visible background */ +.celebration-mode .greeting-text { + font-weight: 700 !important; + font-size: 14px !important; +} + +/* Emoji stays normal */ +.celebration-mode .greeting-emoji { + font-size: 16px; + margin-inline-end: 4px; +} + +/* Shimmer effect on text portion only */ +.celebration-mode .greeting-shimmer { + background: linear-gradient(90deg, #ffd700, #ff69b4, #ffd700); + background-size: 200% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: textShimmer 2s linear infinite; +} + +.birthday-mode .greeting-shimmer { + background: linear-gradient(90deg, #f093fb, #f5576c, #ffd700, #f093fb); + background-size: 300% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.anniversary-mode .greeting-shimmer, +.anniversary-mode .anniversary-shimmer { + background: linear-gradient(90deg, #ffd700, #ff8c00, #ffa500, #ffd700); + background-size: 300% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +@keyframes textShimmer { + 0% { + background-position: 0% center; + } + + 100% { + background-position: 200% center; + } +} + +/* Appreciation ribbon for anniversary - positioned at bottom of header */ +.appreciation-ribbon { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, rgba(255, 215, 0, 0.95), rgba(255, 140, 0, 0.95)); + color: #333; + padding: 8px 8px; + border-radius: 25px; + font-size: 12px; + font-weight: 600; + box-shadow: 0 4px 15px rgba(255, 140, 0, 0.4); + animation: ribbonSlideIn 0.6s ease-out, ribbonGlow 2s ease-in-out infinite; + z-index: 100; + white-space: nowrap; + max-width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} + +.appreciation-ribbon .ribbon-text { + display: inline-block; +} + +@keyframes ribbonSlideIn { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(20px); + } + + 100% { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@keyframes ribbonGlow { + + 0%, + 100% { + box-shadow: 0 4px 15px rgba(255, 140, 0, 0.4); + } + + 50% { + box-shadow: 0 4px 25px rgba(255, 140, 0, 0.6); + } +} + +/* Festive glow on main stat cards */ +.celebration-mode .card2 { + position: relative; + overflow: visible; +} + +.celebration-mode .card2::after { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: inherit; + background: linear-gradient(45deg, rgba(255, 215, 0, 0.3), transparent, rgba(255, 105, 180, 0.3)); + z-index: -1; + animation: cardGlow 3s ease-in-out infinite; + pointer-events: none; +} + +.birthday-mode .card2::after { + background: linear-gradient(45deg, rgba(240, 147, 251, 0.4), transparent, rgba(245, 87, 108, 0.4)); +} + +.anniversary-mode .card2::after { + background: linear-gradient(45deg, rgba(79, 172, 254, 0.4), transparent, rgba(0, 242, 254, 0.4)); +} + +@keyframes cardGlow { + + 0%, + 100% { + opacity: 0.5; + transform: scale(1); + } + + 50% { + opacity: 1; + transform: scale(1.02); + } +} + +/* Celebration stars decoration on corners (using pseudo-elements, no layout shift) */ +.celebration-mode .dashboard-user-data-section::before { + content: '✨'; + position: absolute; + top: 25px; + right: 25px; + font-size: 20px; + animation: starTwinkle 1.5s ease-in-out infinite; + pointer-events: none; + z-index: 10; +} + +.celebration-mode .dashboard-user-data-section::after { + content: '⭐'; + position: absolute; + bottom: 25px; + left: 25px; + font-size: 16px; + animation: starTwinkle 2s ease-in-out infinite 0.5s; + pointer-events: none; + z-index: 10; +} + +.o_rtl .celebration-mode .dashboard-user-data-section::before { + right: auto; + left: 25px; +} + +.o_rtl .celebration-mode .dashboard-user-data-section::after { + left: auto; + right: 25px; +} + +@keyframes starTwinkle { + + 0%, + 100% { + opacity: 0.6; + transform: scale(1) rotate(0deg); + } + + 50% { + opacity: 1; + transform: scale(1.2) rotate(10deg); + } +} + +/* Festive employee code badge */ +.celebration-mode .emp-code-badge { + background: linear-gradient(135deg, #ffd700, #ffb347) !important; + color: #333 !important; + box-shadow: 0 2px 10px rgba(255, 215, 0, 0.5) !important; + animation: badgeShine 2s ease-in-out infinite; +} + +@keyframes badgeShine { + + 0%, + 100% { + box-shadow: 0 2px 10px rgba(255, 215, 0, 0.5); + } + + 50% { + box-shadow: 0 2px 20px rgba(255, 215, 0, 0.8); + } +} + +/* Celebration effect on nav-buttons separator line */ +.celebration-mode .dashboard-nav-buttons::after { + height: 4px !important; + /* Slightly thicker to show effect */ + opacity: 1 !important; + /* Stronger gradient with more stops for visible movement */ + background: linear-gradient(90deg, + #ffd700, #ff69b4, #00f2fe, #ffd700, #ff69b4) !important; + background-size: 200% 100% !important; + /* Smaller size = faster visual movement */ + animation: rainbowBorder 1.5s linear infinite !important; + /* Faster animation */ + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); +} + +.birthday-mode .dashboard-nav-buttons::after { + background: linear-gradient(90deg, + #f093fb, #f5576c, #ffd700, #00f2fe, #f093fb) !important; + background-size: 200% 100% !important; +} + +.anniversary-mode .dashboard-nav-buttons::after { + /* High contrast gold/orange/white for shine */ + background: linear-gradient(90deg, + #ffd700, #ff8c00, #ffffff, #ff8c00, #ffd700) !important; + background-size: 200% 100% !important; +} + +@keyframes rainbowBorder { + 0% { + background-position: 0% 0; + } + + 100% { + background-position: 200% 0; + } +} + +/* Floating emoji decorations on charts */ +.celebration-mode .chart-section::before { + content: '🎈'; + position: absolute; + top: 5px; + right: 5px; + font-size: 18px; + animation: floatEmoji 3s ease-in-out infinite; + pointer-events: none; + opacity: 0.8; +} + +.birthday-mode .chart-section::before { + content: '🎂'; +} + +.anniversary-mode .chart-section::before { + content: '🎉'; +} + +@keyframes floatEmoji { + + 0%, + 100% { + transform: translateY(0) rotate(-5deg); + } + + 50% { + transform: translateY(-8px) rotate(5deg); + } +} + +/* Make sure decorations don't affect layout */ +.celebration-mode .dashboard-user-data-section, +.celebration-mode .chart-section { + position: relative; +} + +/* ═══════════════════════════════════════════════════════════ + PREMIUM CHECK-IN/OUT NOTIFICATION - GENIUS DESIGN + Centered glass-morphism modal with warm professional messaging + ═══════════════════════════════════════════════════════════ */ + +/* Full-screen overlay with backdrop blur */ +.attendance-notification-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.2); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + &.show { + opacity: 1; + visibility: visible; + + .attendance-notification { + transform: scale(1) translateY(0); + opacity: 1; + } + } +} + +/* Premium notification card */ +.attendance-notification { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.95) 0%, rgba(248, 250, 252, 0.98) 100%); + border-radius: 24px; + padding: 40px 50px; + text-align: center; + box-shadow: + 0 25px 80px rgba(0, 0, 0, 0.25), + 0 10px 30px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + transform: scale(0.9) translateY(20px); + opacity: 0; + transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + max-width: 420px; + width: 90vw; + position: relative; + overflow: hidden; + + /* Decorative gradient line at top */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 5px; + border-radius: 24px 24px 0 0; + } + + /* Check-in gradient (warm gold/orange) */ + &.notification-checkin::before { + background: linear-gradient(90deg, #f59e0b, #f97316, #fbbf24); + } + + /* Check-out gradient (cool blue/teal) */ + &.notification-checkout::before { + background: linear-gradient(90deg, #0891b2, #06b6d4, #22d3ee); + } +} + +/* Icon wrapper with glow effect */ +.notification-icon-wrapper { + width: 90px; + height: 90px; + margin: 0 auto 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + animation: iconBounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + + .notification-icon { + font-size: 56px; + line-height: 1; + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1)); + } +} + +/* Check-in icon background */ +.notification-checkin .notification-icon-wrapper { + background: linear-gradient(135deg, rgba(251, 191, 36, 0.2) 0%, rgba(245, 158, 11, 0.1) 100%); + box-shadow: 0 0 40px rgba(245, 158, 11, 0.3); +} + +/* Check-out icon background */ +.notification-checkout .notification-icon-wrapper { + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2) 0%, rgba(8, 145, 178, 0.1) 100%); + box-shadow: 0 0 40px rgba(6, 182, 212, 0.3); +} + +@keyframes iconBounce { + 0% { + transform: scale(0) rotate(-10deg); + } + + 60% { + transform: scale(1.2) rotate(5deg); + } + + 100% { + transform: scale(1) rotate(0); + } +} + +/* Content styles */ +.notification-content { + .notification-title { + font-size: 28px; + font-weight: 700; + color: #1e293b; + margin: 0 0 10px; + letter-spacing: -0.5px; + animation: fadeSlideUp 0.5s 0.2s ease both; + } + + .notification-subtitle { + font-size: 16px; + font-weight: 400; + color: #64748b; + margin: 0 0 20px; + line-height: 1.6; + animation: fadeSlideUp 0.5s 0.3s ease both; + } + + .notification-time { + display: inline-flex; + align-items: center; + gap: 8px; + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); + padding: 10px 20px; + border-radius: 50px; + animation: fadeSlideUp 0.5s 0.4s ease both; + + .time-icon { + font-size: 16px; + } + + .time-value { + font-size: 15px; + font-weight: 600; + color: #334155; + letter-spacing: 0.5px; + } + } +} + +@keyframes fadeSlideUp { + from { + opacity: 0; + transform: translateY(15px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Subtle Icon Pulse Animation on Check-in/out */ +.attendance-success-pulse { + animation: attendancePulse 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes attendancePulse { + 0% { + transform: scale(1); + } + + 30% { + transform: scale(1.2); + } + + 60% { + transform: scale(0.95); + } + + 100% { + transform: scale(1); + } +} + +/* Mobile Responsive */ +@media (max-width: 576px) { + .attendance-notification { + padding: 30px 25px; + border-radius: 20px; + + &::before { + height: 4px; + } + } + + .notification-icon-wrapper { + width: 70px; + height: 70px; + margin-bottom: 18px; + + .notification-icon { + font-size: 42px; + } + } + + .notification-content { + .notification-title { + font-size: 22px; + } + + .notification-subtitle { + font-size: 14px; + margin-bottom: 16px; + } + + .notification-time { + padding: 8px 16px; + + .time-value { + font-size: 14px; + } + } + } +} + +/* RTL Support - Notification (Odoo Standard: .o_rtl on body) */ +.o_rtl .attendance-notification, +.attendance-notification.rtl-notification { + direction: rtl; + + .notification-content { + direction: rtl; + + .notification-title, + .notification-subtitle { + direction: rtl; + text-align: center; + /* Keep centered but with RTL direction */ + } + } +} + +/* ═══════════════════════════════════════════════════════════ + ATTENDANCE ERROR TOAST - Zone/Location Errors + ═══════════════════════════════════════════════════════════ */ + +.attendance-error-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.3); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &.show { + opacity: 1; + visibility: visible; + + .attendance-error-toast { + transform: scale(1) translateY(0); + opacity: 1; + } + } +} + +.attendance-error-toast { + background: #ffffff; + border-radius: 16px; + padding: 30px 35px; + text-align: center; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.2), + 0 8px 20px rgba(0, 0, 0, 0.1); + transform: scale(0.9) translateY(20px); + opacity: 0; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + max-width: 380px; + width: 90vw; + border-top: 4px solid #f59e0b; + + .error-icon { + font-size: 48px; + margin-bottom: 16px; + animation: errorShake 0.5s ease; + } + + .error-message { + font-size: 15px; + font-weight: 500; + color: #334155; + line-height: 1.7; + margin-bottom: 24px; + white-space: pre-line; + } + + .error-ok-btn { + background: var(--dash-secondary) !important; + color: #ffffff; + border: none; + padding: 12px 40px; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + filter: brightness(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + } + + &:active { + transform: translateY(0); + } + } +} + +@keyframes errorShake { + + 0%, + 100% { + transform: translateX(0); + } + + 20% { + transform: translateX(-8px); + } + + 40% { + transform: translateX(8px); + } + + 60% { + transform: translateX(-4px); + } + + 80% { + transform: translateX(4px); + } +} + +/* RTL Support for Error Toast (Odoo Standard: .o_rtl on body) */ +.o_rtl .attendance-error-toast, +.attendance-error-toast.rtl-toast { + direction: rtl; + + .error-message { + direction: rtl; + text-align: center; + /* Keep centered but with RTL direction */ + unicode-bidi: plaintext; + /* Ensures proper Unicode bidirectional handling */ + } +} + +/* Mobile Responsive */ +@media (max-width: 576px) { + .attendance-error-toast { + padding: 24px 20px; + + .error-icon { + font-size: 40px; + } + + .error-message { + font-size: 14px; + } + + .error-ok-btn { + padding: 10px 30px; + font-size: 14px; + } + } +} + +/* ═══════════════════════════════════════════════════════════ + DRAG AND DROP SERVICE CARDS REORDERING + ═══════════════════════════════════════════════════════════ */ + +.card3.draggable-card, +.card2.draggable-card { + cursor: grab; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; + position: relative; + + &:active { + cursor: grabbing; + } + + /* Dragging state - reduce opacity */ + &.dragging { + opacity: 0.5; + transform: scale(0.95); + z-index: 1000; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + } + + /* Drop target highlight */ + &.drag-over { + transform: scale(1.03); + + .card-body, + .card-container { + border: 2px dashed var(--dash-primary, #0891b2) !important; + background: rgba(8, 145, 178, 0.05); + } + } +} + +/* Subtle drag handle hint on hover */ +.card3.draggable-card .card-body::after { + content: '\22EE\22EE'; + /* ⋮⋮ - Unicode for vertical ellipsis */ + position: absolute; + top: 6px; + left: 6px; + font-size: 10px; + color: rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.card3.draggable-card:hover .card-body::after { + opacity: 1; +} + +/* RTL Support for drag handle */ +.o_rtl .card3.draggable-card .card-body::after { + left: auto; + right: 6px; + /*rtl:ignore*/ +} + +/* Disable drag on mobile - touch devices use different gestures */ +@media (max-width: 768px) { + + .card3.draggable-card, + .module-box.draggable-card { + cursor: default; + + .card-body::after, + .module-box-container::after { + display: none; + } + } +} + +/* ═══════════════════════════════════════════════════════════ + DRAG AND DROP FOR STATS MODULE-BOXES (HEADER CARDS) + ═══════════════════════════════════════════════════════════ */ + +.module-box.draggable-card { + cursor: grab; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; + position: relative; + + &:active { + cursor: grabbing; + } + + /* Dragging state */ + &.dragging { + opacity: 0.5; + transform: scale(0.95); + z-index: 1000; + + .module-box-container { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); + } + } + + /* Drop target highlight */ + &.drag-over { + transform: scale(1.02); + + .module-box-container { + border: 2px dashed var(--dash-primary, #0891b2) !important; + background: rgba(8, 145, 178, 0.08); + } + } +} + +/* Subtle drag handle hint on hover for stats cards */ +.module-box.draggable-card .module-box-container::after { + content: '⋮⋮'; + position: absolute; + top: 4px; + left: 4px; + font-size: 10px; + color: rgba(255, 255, 255, 0.4); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: 10; +} + +.module-box.draggable-card:hover .module-box-container::after { + opacity: 1; +} + +/* RTL Support for stats module-box drag handle */ +.o_rtl .module-box.draggable-card .module-box-container::after { + left: auto; + right: 4px; + /*rtl:ignore*/ +} + +/* Service Icon Uniformity and Hover Effects */ +.service-icon { + /* Base styles setup in JS (height/width/font-size), + but we ensure the container allows transition here */ + display: inline-block; + vertical-align: middle; + /* Ensure icon is centered */ + text-align: center; +} + +/* Apply hover effect to the parent card so it triggers on card hover */ +.card-section1 .card3:hover .service-icon { + transform: scale(1.15); + /* 15% zoom on hover */ +} + +/* Also support direct usage if needed */ +.service-icon:hover { + transform: scale(1.15); +} + +/* Approval Card Header Styling - MOVED FROM JS */ +.card2 .card-container .card-header { + background-color: var(--dash-primary) !important; +} + +// .card-header h4, .card-header i.fa { +// color: white !important; +// } + +/* Missing Wrapper Class from JS Refactor */ +.service-icon-wrapper { + text-align: center; + margin-bottom: 10px; +} + +/* Approval Card Header Icon Styling */ +.approval-header-icon { + margin: 0 8px; + /* Replaces margin-left: 8px; margin-right: 8px; */ + vertical-align: middle; +} + +img.approval-header-icon { + height: 30px; + width: 30px; + object-fit: contain; +} + +i.approval-header-icon { + font-size: 30px; + color: var(--dash-secondary); +} + +/* FORCE SUPER SPECIFIC OVERRIDE for Approval Card Header */ +/* User reported conflicts, so we use max specificity */ +.o_content .dashboard-container .card2 .card-container .card-header { + background-color: var(--dash-primary, #0891b2) !important; + /* Fallback to Cyan */ + color: #ffffff !important; +} + +.o_content .dashboard-container .card2 .card-container .card-header h4, +.o_content .dashboard-container .card2 .card-container .card-header i { + color: #ffffff !important; +} + +/* ULTRA SPECIFIC OVERRIDE - Fix Icon & Text Positioning */ +.o_content .dashboard-container .card2 .card-container .card-header img { + height: 48px !important; + width: 48px !important; + padding: 8px !important; + background: rgba(255, 255, 255, 0.25) !important; + border-radius: 12px !important; + margin-right: 20px !important; + margin-left: 0 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + filter: brightness(1.2) !important; +} + +.o_content .dashboard-container .card2 .card-container .card-header h4 { + font-size: 16px !important; + font-weight: 700 !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.15) !important; + letter-spacing: 0.3px !important; +} + +.o_content .dashboard-container .card2 .card-container .card-header i.fa { + font-size: 48px !important; + margin-right: 20px !important; + margin-left: 0 !important; +} + +/* ============================================== + DASHBOARD CONFIG PREVIEW STYLES + ============================================== */ + +/* Base container for preview */ +.dashboard-icon-preview-box { + display: flex; + align-items: center; + justify-content: center; + background: #f1f1f1; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + color: #666; + margin: 0; + /* Align nicely */ +} + +.dashboard-icon-preview-box img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* --- LIST VIEW (TREE) CONTEXT --- */ +/*.o_list_view .dashboard-icon-preview-box, */ +/* Standard Odoo list uses o_data_cell classes, but we are inside a widget=html */ +.o_list_view .dashboard-icon-preview-box { + width: 40px; + height: 40px; +} + +.o_list_view .dashboard-icon-preview-box i { + font-size: 18px; + /* Normal icon size for list */ +} + +/* --- FORM VIEW CONTEXT --- */ +/* Standard Odoo form avatar section */ +.o_form_view .oe_avatar .dashboard-icon-preview-box, +/* Fallback if oe_avatar class is missing or structure differs */ +.o_form_view .dashboard-icon-preview-box { + width: 90px; + height: 90px; + background: #f9f9f9; +} + +.o_form_view .dashboard-icon-preview-box i { + font-size: 3em; + /* roughly fa-3x ~ fa-4x equivalent */ +} + +/* Special Case for Error/Default */ +.dashboard-icon-preview-box.error-mode { + color: #ef4444; + /* Red for error */ + background: #fee2e2; +} + +/* ============================================================ + GENDER-AWARE PROFILE FRAME STYLING + ============================================================ + Subtle, professional differentiation for male/female employees + ============================================================ */ + +/* Male: Professional dark/cyan accent - HIGH SPECIFICITY to override base .img-box */ +.profile-container .pp-image-section .img-box.gender-male, +.img-box.gender-male { + border: 4px solid var(--dash-secondary) !important; + box-shadow: + 0 0 0 3px rgba(30, 41, 59, 0.15), + 0 8px 20px rgba(0, 0, 0, 0.25) !important; +} + +.profile-container .pp-image-section .img-box.gender-male:hover, +.img-box.gender-male:hover { + border-color: var(--dash-primary) !important; + box-shadow: + 0 0 0 4px rgba(8, 145, 178, 0.2), + 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +/* Female: Elegant rose gold/warm accent - HIGH SPECIFICITY to override base .img-box */ +.profile-container .pp-image-section .img-box.gender-female, +.img-box.gender-female { + border: 4px solid #be847c !important; + /* Rose gold */ + box-shadow: + 0 0 0 3px rgba(190, 132, 124, 0.2), + 0 8px 20px rgba(0, 0, 0, 0.2) !important; +} + +.profile-container .pp-image-section .img-box.gender-female:hover, +.img-box.gender-female:hover { + border-color: #d4a59a !important; + /* Lighter rose gold on hover */ + box-shadow: + 0 0 0 4px rgba(212, 165, 154, 0.25), + 0 10px 25px rgba(0, 0, 0, 0.25) !important; +} + +/* Transition for smooth hover effect */ +.profile-container .pp-image-section .img-box.gender-male, +.profile-container .pp-image-section .img-box.gender-female, +.img-box.gender-male, +.img-box.gender-female { + transition: border-color 0.3s ease, box-shadow 0.3s ease !important; +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/pluscharts.scss b/odex30_base/system_dashboard_classic/static/src/scss/pluscharts.scss new file mode 100644 index 0000000..f7ea0c2 --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/pluscharts.scss @@ -0,0 +1,44 @@ +.pc-chart-wrapper { + box-sizing: border-box; + font-family: inherit; +} + +.pc-tooltip { + position: absolute; + pointer-events: none; + background: #121212; + color: #ffffff; + font-size: 13px; + padding: 5px 15px; + border-radius: 3px; + line-height: 1; + font-family: inherit; + z-index: 2; +} + +text { + pointer-events: none; +} + +.pc-expand { + transform: scale(1.5, 1.5); +} + +.pc-legend-text { + text-transform: capitalize; +} + +.pc-y-axis path, +.pc-x-axis path { + stroke: #e5e5e5; +} + +.pc-y-axis line, +.pc-x-axis line { + display: none; +} + +.pc-y-axis text, +.pc-x-axis text { + color: #001737; +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/static/src/scss/variables.scss b/odex30_base/system_dashboard_classic/static/src/scss/variables.scss new file mode 100644 index 0000000..322c81a --- /dev/null +++ b/odex30_base/system_dashboard_classic/static/src/scss/variables.scss @@ -0,0 +1,60 @@ +/* ============================================================ + System Dashboard Classic - Theme Variables + ============================================================ + + MINIMAL COLOR SCHEME: + - Primary: Teal (#0d9488) + - Secondary: Dark Blue (#1e293b) + + To customize, override these CSS variables in your theme: + :root { + --theme-primary: #YOUR_COLOR; + --theme-secondary: #YOUR_COLOR; + } + ============================================================ */ + +/* === THEME CONFIGURATION (Edit these to change the entire look) === */ +:root { + /* Primary Colors - Main brand color */ + --theme-primary: #0d9488; + --theme-primary-light: #14b8a6; + --theme-primary-dark: #0f766e; + + /* Secondary Colors - For headers and dark elements */ + --theme-secondary: #1e293b; + --theme-secondary-light: #334155; + + /* Neutral Colors */ + --theme-bg-light: #f8fafc; + --theme-bg-white: #ffffff; + --theme-border: #e2e8f0; + --theme-text: #475569; + --theme-text-light: #94a3b8; + + /* Shadows */ + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.1); + + /* Transitions */ + --transition: 0.25s ease; +} + +/* === SCSS Variables (Backward Compatibility) === */ +$bg_user_section: linear-gradient(135deg, #1e293b 0%, #334155 100%) !default; +$bg_user_statistics: #FFFFFF !default; +$bg_checkin_btn: #0d9488 !default; +$bg_checkin_btn_hover: #0f766e !default; +$bg_checkout_btn: #0d9488 !default; +$bg_checkout_btn_hover: #0f766e !default; +$divd_border_color: #0d9488 !default; +$bg_dashboard_nav: #0d9488 !default; +$bg_dashboard_nav_hover: #0f766e !default; +$color_nav: #1e293b !default; +$bg_card: #f8fafc !default; +$bg_card_button: linear-gradient(135deg, #1e293b 0%, #334155 100%) !default; +$bg_card_header: linear-gradient(135deg, #1e293b 0%, #334155 100%) !default; + +@if variable-exists(sidebar_bg){ + $bg_user_section: $sidebar_bg; +} \ No newline at end of file diff --git a/odex30_base/system_dashboard_classic/views/config.xml b/odex30_base/system_dashboard_classic/views/config.xml new file mode 100644 index 0000000..37081c0 --- /dev/null +++ b/odex30_base/system_dashboard_classic/views/config.xml @@ -0,0 +1,226 @@ + + + + + + Dashboard Builder + base.dashbord + +
+ + + +
+
+ + + + + + + +
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+ + + + + + + + + + + +
+
+
+ +
+
+ + + + base.dashbord.tree + base.dashbord + + + + + + + + + + + + + Dashboard Cards + base.dashbord + list,form + + + + + + + + node.state.tree + node.state + + + + + + + + + + + + + States + node.state + list + + + + + + + + stage.stage.tree + stage.stage + + + + + + + + + + + + Stages + stage.stage + list + + + + +
+
diff --git a/odex30_base/system_dashboard_classic/views/dashboard_settings.xml b/odex30_base/system_dashboard_classic/views/dashboard_settings.xml new file mode 100644 index 0000000..3d0a8a7 --- /dev/null +++ b/odex30_base/system_dashboard_classic/views/dashboard_settings.xml @@ -0,0 +1,150 @@ + + + + + + res.config.settings.view.form.inherit.dashboard + res.config.settings + 99 + + + + + + +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ + + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ + + +
+ +
+
+ +
+ +
+
+
+ + + +
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+ + + + Dashboard Settings + ir.actions.act_window + res.config.settings + form + inline + {'module': 'system_dashboard_classic'} + + + + + +
+
diff --git a/odex30_base/system_dashboard_classic/views/system_dashboard.xml b/odex30_base/system_dashboard_classic/views/system_dashboard.xml new file mode 100644 index 0000000..47cffac --- /dev/null +++ b/odex30_base/system_dashboard_classic/views/system_dashboard.xml @@ -0,0 +1,41 @@ + + + + + + Dashboard + system_dashboard_classic.Dashboard + current + + + + + + + + + + + + + +