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 @@
+
+
+
+
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!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+