Merge remote-tracking branch 'source_origin/dev_odex_base' into dev_odex30_base
This commit is contained in:
commit
3fe8704696
|
|
@ -67,7 +67,6 @@ window.expertThemeApplyColors = async function() {
|
||||||
root.style.setProperty(property, value);
|
root.style.setProperty(property, value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log('Expert Theme colors applied successfully!');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error applying Expert Theme colors:', error);
|
console.error('Error applying Expert Theme colors:', error);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
'base',
|
'base',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
|
'data/system_parameters.xml',
|
||||||
'views/res_config_settings.xml',
|
'views/res_config_settings.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="odex_sidebar_backend_theme2_backend_odex_support_team_link" model="ir.config_parameter">
|
||||||
|
<field name="key">odex_sidebar_backend_theme2.odex_support_team_link</field>
|
||||||
|
<field name="value">https://odex.sa/support</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -6,48 +6,147 @@ from odoo import fields,api, models
|
||||||
class ResConfigSettings(models.TransientModel):
|
class ResConfigSettings(models.TransientModel):
|
||||||
_inherit = 'res.config.settings'
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
# Sidebar Menu Enable Setting
|
||||||
sidebar_menu_enable = fields.Boolean(
|
sidebar_menu_enable = fields.Boolean(
|
||||||
config_parameter='odex_sidebar_backend_theme2.sidebar_menu_enable',
|
config_parameter='odex_sidebar_backend_theme2.sidebar_menu_enable',
|
||||||
string='Enable Sidebar Menu',
|
string='Enable Sidebar Menu',
|
||||||
help='Enable or disable the sidebar menu in the backend'
|
help='Enable or disable the sidebar menu in the backend'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Navigation Menu Section Disable Setting
|
||||||
disable_nav_menu_section = fields.Boolean(
|
disable_nav_menu_section = fields.Boolean(
|
||||||
config_parameter='odex_sidebar_backend_theme2.disable_nav_menu_section',
|
config_parameter='odex_sidebar_backend_theme2.disable_nav_menu_section',
|
||||||
string='Disable Navigation Menu Section',
|
string='Disable Navigation Menu Section',
|
||||||
help='Enable or disable the top navigation bar menu section in the backend interface'
|
help='Enable or disable the top navigation bar menu section in the backend interface'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Sidebar Menu Icon Setting
|
||||||
sidebar_menu_icon = fields.Binary(
|
sidebar_menu_icon = fields.Binary(
|
||||||
string="Sidebar Icon",
|
string="Sidebar Icon",
|
||||||
help="Upload an icon for the sidebar menu.",
|
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):
|
def get_values(self):
|
||||||
res = super(ResConfigSettings, self).get_values()
|
res = super(ResConfigSettings, self).get_values()
|
||||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||||
sidebar_menu_enable = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable')
|
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')
|
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(
|
res.update(
|
||||||
sidebar_menu_enable=sidebar_menu_enable == 'True',
|
sidebar_menu_enable=sidebar_menu_enable == 'True',
|
||||||
disable_nav_menu_section=disable_nav_menu_section == '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
|
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):
|
def set_values(self):
|
||||||
super(ResConfigSettings, self).set_values()
|
super(ResConfigSettings, self).set_values()
|
||||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||||
|
|
@ -63,7 +162,60 @@ class ResConfigSettings(models.TransientModel):
|
||||||
'odex_sidebar_backend_theme2.sidebar_menu_icon',
|
'odex_sidebar_backend_theme2.sidebar_menu_icon',
|
||||||
self.sidebar_menu_icon or ''
|
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:
|
if self.sidebar_menu_icon:
|
||||||
# Store the image URL in config parameter
|
# Store the image URL in config parameter
|
||||||
image_url = f"/web/image/res.config.settings/{self.id}/sidebar_menu_icon"
|
image_url = f"/web/image/res.config.settings/{self.id}/sidebar_menu_icon"
|
||||||
|
|
@ -80,9 +232,42 @@ class ResConfigSettings(models.TransientModel):
|
||||||
|
|
||||||
return True
|
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
|
@api.model
|
||||||
def get_sidebar_setting(self):
|
def get_sidebar_setting(self):
|
||||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||||
sidebar_enabled = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable') == 'True'
|
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')
|
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}
|
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
|
||||||
|
}
|
||||||
|
|
@ -41,7 +41,6 @@ export class MenuItem extends Component {
|
||||||
// Use xmlid if available, otherwise use name as a stable identifier
|
// Use xmlid if available, otherwise use name as a stable identifier
|
||||||
const uniqueId = menu.xmlid || menu.name || menu.id;
|
const uniqueId = menu.xmlid || menu.name || menu.id;
|
||||||
localStorage.setItem('odex_sidebar_active_menu', uniqueId);
|
localStorage.setItem('odex_sidebar_active_menu', uniqueId);
|
||||||
console.log('Saved menu:', uniqueId, menu.name);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Storage error:', e);
|
console.error('Storage error:', e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,6 @@ export function loadSidebarCSS() {
|
||||||
kwargs: {},
|
kwargs: {},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('Fetched sidebar CSS:', css);
|
|
||||||
|
|
||||||
if (css && css.trim()) {
|
if (css && css.trim()) {
|
||||||
// Create a style element and inject the CSS
|
// Create a style element and inject the CSS
|
||||||
|
|
@ -29,7 +27,7 @@ export function loadSidebarCSS() {
|
||||||
style.id = 'sidebar-dynamic-css';
|
style.id = 'sidebar-dynamic-css';
|
||||||
style.innerHTML = css;
|
style.innerHTML = css;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
console.log('Sidebar CSS injected successfully');
|
console.error('Error loading sidebar CSS:', error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading sidebar CSS:', error);
|
console.error('Error loading sidebar CSS:', error);
|
||||||
|
|
@ -45,9 +43,9 @@ export function loadSidebarCSS() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on module load
|
// Initialize on module load
|
||||||
registry.category("web_tour.tours").add("sidebar_css_loader", {
|
// registry.category("web_tour.tours").add("sidebar_css_loader", {
|
||||||
steps: [],
|
// steps: [],
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Auto-load CSS on page load
|
// Auto-load CSS on page load
|
||||||
loadSidebarCSS();
|
loadSidebarCSS();
|
||||||
|
|
|
||||||
|
|
@ -23,26 +23,37 @@ export class SidebarMenu extends Component {
|
||||||
isCollapsed: false,
|
isCollapsed: false,
|
||||||
sidebarEnabled: false,
|
sidebarEnabled: false,
|
||||||
sidebarMenuIconUrl: null,
|
sidebarMenuIconUrl: null,
|
||||||
|
overlayEnabled: false,
|
||||||
|
supportTeamLink: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.loadSidebarSetting()
|
this.loadSidebarSetting()
|
||||||
|
|
||||||
// =================== JavaScript Control Starts Here ===================
|
// =================== JavaScript Control Starts Here ===================
|
||||||
|
|
||||||
const applyLayoutChanges = (isOpen) => {
|
const applyLayoutChanges = (isOpen,isCollapsed) => {
|
||||||
const actionManager = document.querySelector('.o_action_manager');
|
const actionManager = document.querySelector('.o_action_manager');
|
||||||
const mainNavbar = document.querySelector('.o_navbar');
|
const mainNavbar = document.querySelector('.o_navbar');
|
||||||
|
|
||||||
// Determine sidebar width based on collapse state
|
// 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 (actionManager) actionManager.style.marginInlineStart = collapsedWidth;
|
||||||
if (mainNavbar) mainNavbar.style.marginInlineStart = collapsedWidth;
|
if (mainNavbar) mainNavbar.style.marginInlineStart = collapsedWidth;
|
||||||
} else {
|
} else {
|
||||||
// If sidebar is hidden, remove margin
|
// If sidebar is hidden, remove margin
|
||||||
if (actionManager) actionManager.style.marginInlineStart = '0';
|
if (actionManager) actionManager.style.marginInlineStart = '90px';
|
||||||
if (mainNavbar) mainNavbar.style.marginInlineStart = '0';
|
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
|
// Load sidebar menu icon URL
|
||||||
this.state.sidebarMenuIconUrl = result.sidebar_icon_url || '/odex_sidebar_backend_theme2/static/src/img/logo.webp';
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading sidebar setting:', error);
|
console.error('Error loading sidebar setting:', error);
|
||||||
// Default to enabled if setting cannot be loaded
|
// Default to enabled if setting cannot be loaded
|
||||||
this.state.sidebarEnabled = true;
|
this.state.sidebarEnabled = true;
|
||||||
|
this.state.overlayEnabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,7 +170,6 @@ export class SidebarMenu extends Component {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const activeId = localStorage.getItem('odex_sidebar_active_menu');
|
const activeId = localStorage.getItem('odex_sidebar_active_menu');
|
||||||
console.log('Restored menu ID:', activeId);
|
|
||||||
|
|
||||||
let targetMenu = menuMapByXmlId.get(activeId) || menuMap.get(activeId);
|
let targetMenu = menuMapByXmlId.get(activeId) || menuMap.get(activeId);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,455 +2,456 @@
|
||||||
1. Main Sidebar Design
|
1. Main Sidebar Design
|
||||||
=================================== */
|
=================================== */
|
||||||
.custom_sidebar {
|
.custom_sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 270px;
|
width: 270px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: #151a2d;
|
background-color: var(--ox-sidebar-bg-color, #151a2d);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
transition: all 0.4s ease-in-out;
|
transition: all 0.4s ease-in-out;
|
||||||
|
|
||||||
&:not(.is-open) {
|
&:not(.is-open) {
|
||||||
transform: translateX(-100%) !important;
|
transform: translateX(-100%) !important;
|
||||||
/* Use !important to force hiding */
|
/* Use !important to force hiding */
|
||||||
}
|
|
||||||
|
|
||||||
/* =============== Add New Flyout =============== */
|
|
||||||
|
|
||||||
&.is-collapsed {
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
li {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flyout-panel {
|
/* =============== Add New Flyout =============== */
|
||||||
display: none;
|
|
||||||
position: absolute;
|
&.is-collapsed {
|
||||||
left: 100%;
|
overflow: visible;
|
||||||
top: 0;
|
|
||||||
min-width: 220px;
|
li {
|
||||||
background: #151a2d;
|
position: relative;
|
||||||
color: #fff;
|
z-index: 1000;
|
||||||
border-radius: 10px;
|
}
|
||||||
padding: 10px;
|
|
||||||
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
.flyout-panel {
|
||||||
transform-origin: left top;
|
display: none;
|
||||||
transition: opacity 180ms ease, transform 180ms ease;
|
position: absolute;
|
||||||
opacity: 0;
|
left: 100%;
|
||||||
transform: translateX(-6px) scale(0.98);
|
top: 0;
|
||||||
pointer-events: none;
|
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 {
|
&.is-collapsed {
|
||||||
display: block;
|
width: 90px;
|
||||||
opacity: 1;
|
transform: translateX(0);
|
||||||
transform: translateX(0) scale(1);
|
|
||||||
pointer-events: auto;
|
.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 {
|
li.open {
|
||||||
list-style: none;
|
>.menu-item-container {
|
||||||
padding: 6px 4px;
|
background-color: #fff;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
.menu-item-link {
|
||||||
gap: 6px;
|
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 {
|
&::-webkit-scrollbar {
|
||||||
display: flex;
|
width: 8px;
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 150ms ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flyout-item:hover {
|
&::-webkit-scrollbar-track {
|
||||||
background: rgba(255, 255, 255, 0.05);
|
background: var(--ox-sidebar-scrollbar-track-color, #0e1223);
|
||||||
}
|
}
|
||||||
|
|
||||||
.flyout-item .icon {
|
&::-webkit-scrollbar-thumb {
|
||||||
width: 28px;
|
background-color: var(--ox-sidebar-scrollbar-thumb-color, #151a2d);
|
||||||
height: 28px;
|
border-radius: 4px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.flyout-item .label {
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
color: #fff;
|
background-color: var(--ox-sidebar-scrollbar-thumb-hover-color, #151a2d);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-collapsed {
|
|
||||||
width: 90px;
|
|
||||||
transform: translateX(0);
|
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
padding: 25px 10px;
|
display: flex;
|
||||||
justify-content: center;
|
position: relative;
|
||||||
flex-direction: column;
|
padding: 25px 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
img {
|
img {
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-toggler {
|
.sidebar-toggler {
|
||||||
position: static;
|
position: absolute;
|
||||||
width: 50%;
|
right: 20px;
|
||||||
margin-top: 25px;
|
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-nav {
|
||||||
.sidebar_menu_list {
|
flex: 1;
|
||||||
padding: 0 8px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
&.primary-nav {
|
.sidebar_menu_list {
|
||||||
|
list-style: none;
|
||||||
.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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
align-items: center;
|
padding: 0 15px;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
transform: translateY(15px);
|
||||||
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;
|
|
||||||
transition: 0.4s ease;
|
transition: 0.4s ease;
|
||||||
|
|
||||||
.nav_icon {
|
&.primary-nav {
|
||||||
font-size: 24px;
|
|
||||||
margin: 0 10px;
|
.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 {
|
&.secondary-nav {
|
||||||
font-size: 1rem;
|
margin-top: auto;
|
||||||
transition: opacity 0.3s ease;
|
// width: calc(100% - 30px);
|
||||||
}
|
background: var(--ox-sidebar-bg-color, #151a2d);
|
||||||
}
|
padding: 20px 15px;
|
||||||
|
|
||||||
.dropdown_menu {
|
.nav-item {
|
||||||
height: 0;
|
position: relative;
|
||||||
overflow-y: hidden;
|
// margin: 0 15px;
|
||||||
list-style: none;
|
|
||||||
padding-left: 15px;
|
&:hover>.nav-link {
|
||||||
transition: height 0.4s ease;
|
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
|
// Hide default application icon from top bar
|
||||||
.o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown {
|
.o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.o_web_client {
|
.o_web_client {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust Top Bar and Main Content */
|
/* Adjust Top Bar and Main Content */
|
||||||
.o_navbar,
|
.o_navbar,
|
||||||
.o_action_manager {
|
.o_action_manager {
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Open Icon Design (New) */
|
/* Open Icon Design (New) */
|
||||||
.sidebar_toggle_icon {
|
.sidebar_toggle_icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: calc(var(--o-navbar-height) - 0px);
|
height: calc(var(--o-navbar-height) - 0px);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<!-- Secondary Bottom Nav -->
|
<!-- Secondary Bottom Nav -->
|
||||||
<ul class="sidebar_menu_list secondary-nav">
|
<ul class="sidebar_menu_list secondary-nav">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="#" class="nav-link" t-att-title="state.isCollapsed ? 'Support' : ''">
|
<a target="_blank" t-att-href="state.supportTeamLink" class="nav-link" t-att-title="state.isCollapsed ? 'Support' : ''">
|
||||||
<span class="nav_icon"><i class="fa fa-question-circle"></i></span>
|
<span class="nav_icon"><i class="fa fa-question-circle"></i></span>
|
||||||
<span class="nav-label">Support</span>
|
<span class="nav-label">Support</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<i class="fa fa-question-circle"></i>
|
<i class="fa fa-question-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://www.odex.sa" class="label">Support</a>
|
<a target="_blank" t-att-href="state.supportTeamLink" class="label">Support</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
<!-- Normal dropdown (when not collapsed) -->
|
<!-- Normal dropdown (when not collapsed) -->
|
||||||
<ul class="dropdown_menu" t-if="!state.isCollapsed">
|
<ul class="dropdown_menu" t-if="!state.isCollapsed">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a href="https://www.odex.sa" class="nav-link dropdown-title">Support</a>
|
<a target="_blank" t-att-href="state.supportTeamLink" class="nav-link dropdown-title">Support</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,34 @@
|
||||||
<setting help="Enable or disable the sidebar menu in the backend interface">
|
<setting help="Enable or disable the sidebar menu in the backend interface">
|
||||||
<field name="sidebar_menu_enable" />
|
<field name="sidebar_menu_enable" />
|
||||||
</setting>
|
</setting>
|
||||||
<setting help="Enable or disable the top navigation bar menu section in the backend interface">
|
<setting
|
||||||
|
help="Enable or disable the top navigation bar menu section in the backend interface">
|
||||||
<field name="disable_nav_menu_section" />
|
<field name="disable_nav_menu_section" />
|
||||||
</setting>
|
</setting>
|
||||||
<setting>
|
<setting>
|
||||||
<group>
|
<group>
|
||||||
<field name="sidebar_menu_icon" widget="image"
|
<field name="sidebar_menu_icon" widget="image"
|
||||||
options="{'size': [128, 128]}"/>
|
options="{'size': [128, 128]}" />
|
||||||
|
</group>
|
||||||
|
</setting>
|
||||||
|
<setting help="Enable overlay effect when sidebar is uncollapsed">
|
||||||
|
<field name="uncollapsed_sidebar_overlay" />
|
||||||
|
</setting>
|
||||||
|
<setting help="Color Schema Setting">
|
||||||
|
<group>
|
||||||
|
<field name="sidebar_background_type" />
|
||||||
|
<field name="sidebar_background_color" widget="color" />
|
||||||
|
<field name="sidebar_background_image" widget="image"
|
||||||
|
options="{'size': [128, 128]}" />
|
||||||
|
<field name="sidebar_links_color" widget="color" />
|
||||||
|
<field name="sidebar_links_hover_color" widget="color" />
|
||||||
|
<field name="sidebar_links_active_color" widget="color" />
|
||||||
|
<field name="sidebar_links_bg_color" widget="color" />
|
||||||
|
<field name="sidebar_links_hover_bg_color" widget="color" />
|
||||||
|
<field name="sidebar_links_active_bg_color" widget="color" />
|
||||||
|
<field name="sidebar_scrollbar_track_color" widget="color" />
|
||||||
|
<field name="sidebar_scrollbar_thumb_color" widget="color" />
|
||||||
|
<field name="sidebar_scrollbar_thumb_hover_color" widget="color" />
|
||||||
</group>
|
</group>
|
||||||
</setting>
|
</setting>
|
||||||
</block>
|
</block>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import models
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 "<i class=\"fa fa-external-link\"/> Browse Icons"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: system_dashboard_classic
|
||||||
|
#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form
|
||||||
|
msgid ""
|
||||||
|
"<i class=\"fa fa-info-circle\"/> 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 ""
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import dashboard
|
||||||
|
from . import config
|
||||||
|
from . import res_users
|
||||||
|
from . import res_config_settings
|
||||||
|
|
@ -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'<div class="dashboard-icon-preview-box icon-mode"><i class="fa {record.icon_name}"></i></div>'
|
||||||
|
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'<div class="dashboard-icon-preview-box image-mode"><img src="data:image/png;base64,{image_data}"/></div>'
|
||||||
|
except Exception:
|
||||||
|
record.icon_preview_html = '<div class="dashboard-icon-preview-box error-mode"><i class="fa fa-exclamation-triangle"></i></div>'
|
||||||
|
else:
|
||||||
|
record.icon_preview_html = '<div class="dashboard-icon-preview-box default-mode"><i class="fa fa-th-large"></i></div>'
|
||||||
|
|
||||||
|
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 ''
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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'),
|
||||||
|
}
|
||||||
|
|
@ -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'
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Dashboard Management Category -->
|
||||||
|
<record id="module_category_dashboard" model="ir.module.category">
|
||||||
|
<field name="name">Dashboard</field>
|
||||||
|
<field name="sequence">150</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Manager Group - Can view dashboard and approve -->
|
||||||
|
<record id="group_dashboard_manager" model="res.groups">
|
||||||
|
<field name="name">Manager</field>
|
||||||
|
<field name="category_id" ref="module_category_dashboard"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Configuration Group - Can configure dashboard cards -->
|
||||||
|
<record id="group_dashboard_config" model="res.groups">
|
||||||
|
<field name="name">Configuration</field>
|
||||||
|
<field name="category_id" ref="module_category_dashboard"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_dashboard_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,333 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="system_dashboard_classic.Dashboard">
|
||||||
|
<!-- Main Dashboard Container - Same structure as Odoo 14 -->
|
||||||
|
<div class="container-fluid dashboard-container">
|
||||||
|
<!-- Appreciation Ribbon for Celebrations -->
|
||||||
|
<t t-if="state.celebration.isBirthday or state.celebration.isAnniversary">
|
||||||
|
<div class="appreciation-ribbon">
|
||||||
|
<div class="ribbon-content">
|
||||||
|
<t t-if="state.celebration.isBirthday">
|
||||||
|
<span class="ribbon-emoji">🎂</span>
|
||||||
|
<span class="ribbon-text">Happy Birthday! Wishing you a wonderful day!</span>
|
||||||
|
</t>
|
||||||
|
<t t-elif="state.celebration.isAnniversary">
|
||||||
|
<span class="ribbon-emoji">⭐</span>
|
||||||
|
<span class="ribbon-text">Thank You for <t t-esc="state.celebration.years"/> Amazing Years!</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 dashboard-header">
|
||||||
|
<!-- User Profile Section -->
|
||||||
|
<div class="col-md-2 col-sm-12 col-12 dashboard-user-data-section">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 profile-container">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 pp-image-section">
|
||||||
|
<div class="img-box" t-att-style="'background-image: url(' + userImage + ')'"/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 info-section">
|
||||||
|
<p class="fn-section" style="white-space: pre-line;"><t t-esc="userName"/></p>
|
||||||
|
<p class="fn-job"><t t-esc="userJob"/></p>
|
||||||
|
<p class="fn-id"><t t-if="userEmployeeId"><span class="emp-code-badge"><t t-esc="userEmployeeId"/></span></t></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics Section -->
|
||||||
|
<div class="col-md-10 col-sm-12 col-12 dashboard-user-statistics-section" id="main-cards" t-att-style="state.loading ? 'display:none' : ''">
|
||||||
|
<div class="col-md-10 col-sm-12 col-12 dashboard-charts-section">
|
||||||
|
<!-- Loading Spinner -->
|
||||||
|
<t t-if="state.loading">
|
||||||
|
<div class="charts-over-layer">
|
||||||
|
<div class="lds-roller"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 dashboard-module-charts" style="padding:0;">
|
||||||
|
<!-- Annual Leave Card -->
|
||||||
|
<t t-if="state.settings.visibility.show_annual_leave">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12 module-box" id="leave-section" style="display:block;">
|
||||||
|
<div class="col-md-12 module-box-container">
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-primary, #0d9488);"/> <span class="leave-total-amount"><t t-esc="state.statistics.leaves.taken + state.statistics.leaves.remaining"/> days total</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-secondary, #1e293b);"/> <span class="leave-left-amount"><t t-esc="state.statistics.leaves.remaining"/> days left</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div id="chartContainer"></div>
|
||||||
|
<div class="chart-center-text">
|
||||||
|
<span class="chart-center-value leave-center-value"><t t-esc="state.statistics.leaves.remaining"/></span>
|
||||||
|
<span class="chart-center-unit leave-center-unit">days</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Annual Leave</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Salary Slips Card -->
|
||||||
|
<t t-if="state.settings.visibility.show_salary_slips">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12 module-box" id="salary-section" style="display:block;">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 module-box-container">
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-primary, #0d9488);"/> <span class="payroll-total-amount"><t t-esc="state.statistics.payroll.taken"/> received</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-secondary, #1e293b);"/> <span class="payroll-left-amount"><t t-esc="state.statistics.payroll.remaining"/> remaining</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div id="chartPaylips"></div>
|
||||||
|
<div class="chart-center-text">
|
||||||
|
<span class="chart-center-value payroll-center-value"><t t-esc="state.statistics.payroll.taken"/></span>
|
||||||
|
<span class="chart-center-unit payroll-center-unit">slips</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Salary Slips</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Weekly Timesheet Card -->
|
||||||
|
<t t-if="state.settings.visibility.show_timesheet">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12 module-box" id="timesheet-section" style="display:block;">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 module-box-container">
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-primary, #0d9488);"/> <span class="timesheet-total-amount"><t t-esc="state.statistics.timesheet.taken.toFixed ? state.statistics.timesheet.taken.toFixed(1) : state.statistics.timesheet.taken"/>h done</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-secondary, #1e293b);"/> <span class="timesheet-left-amount"><t t-esc="state.statistics.timesheet.remaining.toFixed ? state.statistics.timesheet.remaining.toFixed(1) : state.statistics.timesheet.remaining"/>h left</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div id="chartTimesheet"></div>
|
||||||
|
<div class="chart-center-text">
|
||||||
|
<span class="chart-center-value timesheet-center-value"><t t-esc="state.statistics.timesheet.taken.toFixed ? state.statistics.timesheet.taken.toFixed(1) : state.statistics.timesheet.taken"/></span>
|
||||||
|
<span class="chart-center-unit timesheet-center-unit">hours</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Weekly Timesheet</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Monthly Attendance Hours Card -->
|
||||||
|
<t t-if="state.settings.visibility.show_attendance_hours">
|
||||||
|
<div class="col-md-4 col-sm-6 col-12 module-box" id="attendance-hours-section" style="display:block;">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 module-box-container">
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-primary, #0d9488);"/> <span class="attendance-plan-hours"><t t-esc="state.statistics.attendance_hours.plan.toFixed ? state.statistics.attendance_hours.plan.toFixed(1) : state.statistics.attendance_hours.plan"/>h planned</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
<i class="fa fa-circle" style="color: var(--theme-secondary, #1e293b);"/> <span class="attendance-official-hours"><t t-esc="state.statistics.attendance_hours.official.toFixed ? state.statistics.attendance_hours.official.toFixed(1) : state.statistics.attendance_hours.official"/>h worked</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div class="chart-wrapper">
|
||||||
|
<div id="chartAttendanceHours"></div>
|
||||||
|
<div class="chart-center-text">
|
||||||
|
<span class="chart-center-value attendance-center-value"><t t-esc="state.statistics.attendance_hours.official.toFixed ? state.statistics.attendance_hours.official.toFixed(1) : state.statistics.attendance_hours.official"/></span>
|
||||||
|
<span class="chart-center-unit attendance-center-unit">hours</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h3>Monthly Attendance</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attendance Section -->
|
||||||
|
<t t-if="state.settings.visibility.show_attendance_section">
|
||||||
|
<div class="col-md-2 col-sm-12 col-12 dashboard-attendance-section">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 attendance-section-body">
|
||||||
|
|
||||||
|
<p class="last-checkin-section">
|
||||||
|
<span class="attendance-date"><t t-esc="currentDateTime.date"/></span>
|
||||||
|
<span class="attendance-time"><t t-esc="currentDateTime.time"/></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="attendance-button-section">
|
||||||
|
<button
|
||||||
|
t-att-class="'btn btn-lg ' + (state.attendance.isCheckedIn ? 'checkout-btn' : 'checkin-btn')"
|
||||||
|
t-att-disabled="!state.settings.enableAttendance"
|
||||||
|
t-on-click="onAttendanceClick">
|
||||||
|
<i t-att-class="'fa ' + (state.attendance.isCheckedIn ? 'fa-sign-out' : 'fa-sign-in')"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="last-checkin-info">
|
||||||
|
<t t-if="lastCheckInInfo.time">
|
||||||
|
<div class="checkin-label"><t t-esc="lastCheckInInfo.label"/></div>
|
||||||
|
<div class="checkin-time"><t t-esc="attendanceTime"/></div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="checkin-label">
|
||||||
|
<i class="fa fa-info-circle" style="margin-right: 5px;"/>
|
||||||
|
<t t-if="state.attendance.isCheckedIn">لم تسجل خروج بعد</t>
|
||||||
|
<t t-else="">لم تسجل دخول بعد</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</p>
|
||||||
|
<!-- <p t-att-class="'attendance-img-section ' + (state.attendance.isCheckedIn ? 'state-checkout' : 'state-checkin') + (!state.settings.enableAttendance ? ' disabled' : '')" t-on-click="state.settings.enableAttendance ? onAttendanceClick : null">-->
|
||||||
|
<!-- <img t-att-src="state.attendance.isCheckedIn ? '/system_dashboard_classic/static/src/icons/smile.svg' : '/system_dashboard_classic/static/src/icons/sad.svg'" class="attendance-icon" style="height: 60px;"/>-->
|
||||||
|
<!-- </p>-->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="work-timer-container"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dashboard Body - Cards Section -->
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 tabs-container dashboard-body">
|
||||||
|
<div class="col-md-12 col-sm-12 col-12 dashboard-nav-buttons">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li role="presentation" class="nav-item">
|
||||||
|
<a href="#self_services"
|
||||||
|
t-att-class="'nav-link ' + (state.activeTab === 'self_services' ? 'active' : '')"
|
||||||
|
aria-controls="self_services"
|
||||||
|
role="tab"
|
||||||
|
t-on-click.prevent="() => setActiveTab('self_services')">Self Services</a>
|
||||||
|
</li>
|
||||||
|
<t t-if="hasApprovalCards">
|
||||||
|
<li role="presentation" class="nav-item approval-tab-item">
|
||||||
|
<a href="#to_approve"
|
||||||
|
t-att-class="'nav-link ' + (state.activeTab === 'to_approve' ? 'active' : '')"
|
||||||
|
aria-controls="to_approve"
|
||||||
|
role="tab"
|
||||||
|
t-on-click.prevent="() => setActiveTab('to_approve')">
|
||||||
|
To Approve
|
||||||
|
<t t-if="approvalCount > 0">
|
||||||
|
<span class="pending-count-badge badge bg-danger"><t t-esc="approvalCount"/></span>
|
||||||
|
</t>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation" class="nav-item approval-tab-item">
|
||||||
|
<a href="#to_track"
|
||||||
|
t-att-class="'nav-link ' + (state.activeTab === 'to_track' ? 'active' : '')"
|
||||||
|
aria-controls="to_track"
|
||||||
|
role="tab"
|
||||||
|
t-on-click.prevent="() => setActiveTab('to_track')">To Track</a>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Panes -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<!-- Self Services Tab -->
|
||||||
|
<div role="tabpanel" t-att-class="'tab-pane fade ' + (state.activeTab === 'self_services' ? 'show active' : '')" id="self_services">
|
||||||
|
<div class="col-md-12 col-12 d-flex flex-wrap card-section1" style="padding: 0 15px;">
|
||||||
|
<t t-foreach="state.cards.service" t-as="card" t-key="card.model">
|
||||||
|
<div class="col-md-2 col-sm-4 col-6 card3" >
|
||||||
|
<div class="card-body" >
|
||||||
|
<div class="box-1" t-on-click="() => onCardClick(card)">
|
||||||
|
<t t-if="card.image">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + card.image"/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="card.icon_name">
|
||||||
|
<i t-att-class="'fa ' + card.icon_name" style="font-size: 60px; color: var(--theme-primary, #0d9488);"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i class="fa fa-th-large" style="font-size: 60px; color: var(--theme-primary, #0d9488);"/>
|
||||||
|
</t>
|
||||||
|
<h3><t t-esc="card.state_count || 0"/></h3>
|
||||||
|
<h4><t t-esc="card.name"/></h4>
|
||||||
|
</div>
|
||||||
|
<div class="box-2"
|
||||||
|
t-on-click="() => onCreateNewClick(card)">
|
||||||
|
<i class="fa fa-plus"/>
|
||||||
|
<span>Add New</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- To Approve Tab -->
|
||||||
|
<div role="tabpanel" t-att-class="'tab-pane fade ' + (state.activeTab === 'to_approve' ? 'show active' : '')" id="to_approve">
|
||||||
|
<div class="col-md-12 col-12 d-flex flex-wrap card-section-approve" style="padding: 0 15px;">
|
||||||
|
<t t-foreach="state.cards.approve" t-as="card" t-key="card.cardId">
|
||||||
|
<div class="col-md-3 col-sm-4 col-6 card2">
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-header">
|
||||||
|
<t t-if="card.image">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + card.image"/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="card.icon_name">
|
||||||
|
<i t-att-class="'fa ' + card.icon_name"/>
|
||||||
|
</t>
|
||||||
|
<h4>
|
||||||
|
<span><t t-esc="card.name"/></span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tr t-on-click="() => onCardClick(card)">
|
||||||
|
<td><t t-esc="card.state_approval || 'Pending'"/></td>
|
||||||
|
<td><div><t t-esc="card.count_state_click || 0"/></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- To Track Tab -->
|
||||||
|
<div role="tabpanel" t-att-class="'tab-pane fade ' + (state.activeTab === 'to_track' ? 'show active' : '')" id="to_track">
|
||||||
|
<div class="col-md-12 col-12 d-flex flex-wrap card-section-track" style="padding: 0 15px;">
|
||||||
|
<t t-foreach="state.cards.track" t-as="card" t-key="card.cardId">
|
||||||
|
<div class="col-md-3 col-sm-4 col-6 card2">
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-header">
|
||||||
|
<t t-if="card.image">
|
||||||
|
<img t-att-src="'data:image/png;base64,' + card.image"/>
|
||||||
|
</t>
|
||||||
|
<t t-elif="card.icon_name">
|
||||||
|
<i t-att-class="'fa ' + card.icon_name"/>
|
||||||
|
</t>
|
||||||
|
<h4>
|
||||||
|
<span><t t-esc="card.name"/></span>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table">
|
||||||
|
<tr t-on-click="() => onCardClick(card)">
|
||||||
|
<td><t t-esc="card.state_folow || 'All'"/></td>
|
||||||
|
<td><div><t t-esc="card.count_state_follow || 0"/></div></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Login Icon - Arrow pointing IN (opposite of logout) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 384 384" style="enable-background:new 0 0 384 384;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#003056;" d="M341.333,0H42.667C19.093,0,0,19.093,0,42.667V128h42.667V42.667h298.667v298.667H42.667V256H0v85.333
|
||||||
|
C0,364.907,19.093,384,42.667,384h298.667C364.907,384,384,364.907,384,341.333V42.667C384,19.093,364.907,0,341.333,0z"/>
|
||||||
|
<polygon style="fill:#003056;" points="232.853,115.52 202.667,85.333 96,192 202.667,298.667 232.853,268.48 177.707,213.333 384,213.333 384,170.667
|
||||||
|
177.707,170.667 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 792 B |
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 384 384" style="enable-background:new 0 0 384 384;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#003056;" d="M341.333,0H42.667C19.093,0,0,19.093,0,42.667V128h42.667V42.667h298.667v298.667H42.667V256H0v85.333
|
||||||
|
C0,364.907,19.093,384,42.667,384h298.667C364.907,384,384,364.907,384,341.333V42.667C384,19.093,364.907,0,341.333,0z"/>
|
||||||
|
<polygon style="fill:#003056;" points="151.147,268.48 181.333,298.667 288,192 181.333,85.333 151.147,115.52 206.293,170.667 0,170.667 0,213.333
|
||||||
|
206.293,213.333 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 958 B |
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
|
||||||
|
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 4 C 22.393 4 4 22.393 4 45 s 18.393 41 41 41 s 41 -18.393 41 -41 S 67.607 4 45 4 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
|
||||||
|
<circle cx="30.344" cy="33.274" r="5.864" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<circle cx="59.663999999999994" cy="33.274" r="5.864" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<path d="M 72.181 65.49 c -0.445 0 -0.893 -0.147 -1.265 -0.451 c -7.296 -5.961 -16.5 -9.244 -25.916 -9.244 c -9.417 0 -18.62 3.283 -25.916 9.244 c -0.854 0.7 -2.115 0.572 -2.814 -0.283 c -0.699 -0.855 -0.572 -2.115 0.283 -2.814 C 24.561 55.398 34.664 51.795 45 51.795 c 10.336 0 20.438 3.604 28.447 10.146 c 0.855 0.699 0.982 1.959 0.283 2.814 C 73.335 65.239 72.76 65.49 72.181 65.49 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve">
|
||||||
|
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)">
|
||||||
|
<path d="M 45 90 C 20.187 90 0 69.813 0 45 C 0 20.187 20.187 0 45 0 c 24.813 0 45 20.187 45 45 C 90 69.813 69.813 90 45 90 z M 45 4 C 22.393 4 4 22.393 4 45 s 18.393 41 41 41 s 41 -18.393 41 -41 S 67.607 4 45 4 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
|
||||||
|
<circle cx="31" cy="35.55" r="5" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<circle cx="59" cy="35.55" r="5" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) "/>
|
||||||
|
<path d="M 45 69.345 c -7.954 0 -15.337 -3.969 -19.751 -10.617 c -0.611 -0.92 -0.36 -2.162 0.56 -2.772 c 0.92 -0.613 2.162 -0.36 2.772 0.56 c 3.671 5.529 9.809 8.83 16.419 8.83 c 6.61 0 12.748 -3.301 16.419 -8.83 c 0.61 -0.921 1.85 -1.173 2.772 -0.56 c 0.92 0.61 1.171 1.853 0.56 2.772 C 60.337 65.376 52.953 69.345 45 69.345 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,128 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple CSS-based Progress Ring with Smart Hover Detection
|
||||||
|
* Detects hover on entire ring and shows correct percentage
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function drawDonutChart(selector, data, colors, type = 'donut', width = 140, height = 140, donutWidth = 15) {
|
||||||
|
const container = document.querySelector(selector);
|
||||||
|
if (!container) {
|
||||||
|
console.warn(`[Chart] Container ${selector} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear previous chart
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Calculate percentage (assume 2 items: used and remaining)
|
||||||
|
const total = data.reduce((sum, item) => 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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Dashboard Builder Form View -->
|
||||||
|
<record id="view_base_dashboard_form" model="ir.ui.view">
|
||||||
|
<field name="name">Dashboard Builder</field>
|
||||||
|
<field name="model">base.dashbord</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Dashboard Service Configuration">
|
||||||
|
<sheet>
|
||||||
|
<!-- Avatar / Icon Selection -->
|
||||||
|
<!-- Avatar / Icon Selection -->
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="icon_type" class="oe_edit_only"/>
|
||||||
|
<field name="icon_type" widget="radio" class="oe_edit_only" options="{'horizontal': true}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Widget (Visible if 'image') - NOT REQUIRED -->
|
||||||
|
<field name="card_image" widget="image" class="oe_avatar"
|
||||||
|
options="{'preview_image': 'card_image'}"
|
||||||
|
invisible="icon_type == 'icon'" />
|
||||||
|
|
||||||
|
<!-- Icon Preview Widget (Visible if 'icon') -->
|
||||||
|
<field name="icon_preview_html" widget="html" class="oe_avatar"
|
||||||
|
style="padding:0; border:none; background:transparent;"
|
||||||
|
invisible="icon_type == 'image'" />
|
||||||
|
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Service Name" required="True"/>
|
||||||
|
</h1>
|
||||||
|
<div invisible="icon_type == 'image'" class="o_row">
|
||||||
|
<!-- NOTE: Removed required attr to allow default fallback -->
|
||||||
|
<field name="icon_name" placeholder="fa-plane"/>
|
||||||
|
<a href="https://fontawesome.com/v4/icons/" target="_blank" class="btn btn-link" role="button">
|
||||||
|
<i class="fa fa-external-link"/> Browse Icons
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Configuration Groups -->
|
||||||
|
<group>
|
||||||
|
<group string="Model Configuration">
|
||||||
|
<field name="model_id"
|
||||||
|
options="{'no_create_edit': True}"
|
||||||
|
required="1"/>
|
||||||
|
<field name="model_name" invisible="1"/>
|
||||||
|
<field name="action_id"
|
||||||
|
options="{'no_create_edit': True}"
|
||||||
|
domain="[('res_model','=',model_name)]"
|
||||||
|
required="1"
|
||||||
|
help="The action to open when clicking this card"/>
|
||||||
|
</group>
|
||||||
|
<group string="Display Options">
|
||||||
|
<field name="sequence" help="Lower numbers appear first"/>
|
||||||
|
<field name="is_self_service"
|
||||||
|
help="Enable for employee self-service cards" widget="boolean_toggle"/>
|
||||||
|
<field name="is_financial_impact"
|
||||||
|
help="Mark if this service has no financial impact" widget="boolean_toggle"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="Employee Filter Configuration">
|
||||||
|
<field name="search_field"
|
||||||
|
placeholder="e.g., employee_id.user_id or user_id"
|
||||||
|
help="The field path used to filter records for current user. Examples: • 'employee_id.user_id' - For HR models (hr.leave, hr.expense, etc.) • 'user_id' - For models with direct user reference (purchase.order, etc.) • 'create_uid' - For records created by the user"/>
|
||||||
|
</group>
|
||||||
|
<group string="Advanced View Settings"
|
||||||
|
invisible="not model_id">
|
||||||
|
<field name="form_view_id"
|
||||||
|
options="{'no_create_edit': True}"
|
||||||
|
domain="[('type','=','form'),('model','=',model_name)]"
|
||||||
|
help="Optional: Custom form view for this service"/>
|
||||||
|
<field name="list_view_id"
|
||||||
|
options="{'no_create_edit': True}"
|
||||||
|
domain="[('type','=','list'),('model','=',model_name)]"
|
||||||
|
help="Optional: Custom list view for this service"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<!-- Hidden computed fields -->
|
||||||
|
<field name="action_domain" invisible="1"/>
|
||||||
|
<field name="action_context" invisible="1"/>
|
||||||
|
<field name="is_button" invisible="1"/>
|
||||||
|
<field name="is_stage" invisible="1"/>
|
||||||
|
<field name="is_double" invisible="1"/>
|
||||||
|
<field name="is_state" invisible="1"/>
|
||||||
|
|
||||||
|
<!-- State/Stage Configuration Notebook -->
|
||||||
|
<notebook>
|
||||||
|
<page name="state_config" string="State/Stage Configuration">
|
||||||
|
<!-- Initial state - show info and load button -->
|
||||||
|
<div invisible="is_button" >
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<i class="fa fa-info-circle"/> Click the button below to detect available states/stages for the selected model.
|
||||||
|
</div>
|
||||||
|
<button name="compute_selection"
|
||||||
|
string="Load Model States"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-download"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- After loading - show action buttons above the list -->
|
||||||
|
<div class="d-flex mb-3" invisible="not is_button">
|
||||||
|
<button name="update_selection"
|
||||||
|
string="Refresh States"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
icon="fa-refresh"/>
|
||||||
|
<button name="unlink_nodes"
|
||||||
|
string="Remove All States"
|
||||||
|
type="object"
|
||||||
|
class="btn-danger ml-2"
|
||||||
|
icon="fa-trash"
|
||||||
|
confirm="Are you sure you want to remove all loaded states?"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<field name="line_ids"
|
||||||
|
invisible="not is_button"
|
||||||
|
context="{'default_model_name':model_name,'default_model_id':model_id}">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="group_ids"
|
||||||
|
widget="many2many_tags"
|
||||||
|
options="{'no_quick_create': True, 'color_field': 'color'}"
|
||||||
|
required="1"
|
||||||
|
placeholder="Select user groups..."/>
|
||||||
|
<field name="model_name" invisible="1"/>
|
||||||
|
<field name="model_id" invisible="1"/>
|
||||||
|
<field name="state_id"
|
||||||
|
column_invisible="not parent.is_state and not parent.is_double"
|
||||||
|
options="{'no_create': True, 'no_create_edit': True}"
|
||||||
|
domain="[('model_id', '=', model_id)]"/>
|
||||||
|
<field name="stage_id"
|
||||||
|
column_invisible="not parent.is_stage"
|
||||||
|
options="{'no_create': True, 'no_create_edit': True}"
|
||||||
|
domain="[('model_id', '=', model_id)]"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Dashboard Builder Tree View -->
|
||||||
|
<record id="view_base_dashbord_tree" model="ir.ui.view">
|
||||||
|
<field name="name">base.dashbord.tree</field>
|
||||||
|
<field name="model">base.dashbord</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Dashboard Cards">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="model_id"/>
|
||||||
|
<field name="is_self_service"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Dashboard Builder Action -->
|
||||||
|
<record id="action_base_dashbord" model="ir.actions.act_window">
|
||||||
|
<field name="name">Dashboard Cards</field>
|
||||||
|
<field name="res_model">base.dashbord</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu for Dashboard Cards -->
|
||||||
|
<menuitem
|
||||||
|
id="menu_dashboard_cards"
|
||||||
|
name="Dashboard Cards"
|
||||||
|
parent="menu_dashboard_config"
|
||||||
|
action="action_base_dashbord"
|
||||||
|
sequence="10"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Node State Tree View -->
|
||||||
|
<record id="view_node_state_tree" model="ir.ui.view">
|
||||||
|
<field name="name">node.state.tree</field>
|
||||||
|
<field name="model">node.state</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="States" create="false" edit="false">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<field name="model_id"/>
|
||||||
|
<field name="is_workflow"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Node State Action -->
|
||||||
|
<record id="action_node_state" model="ir.actions.act_window">
|
||||||
|
<field name="name">States</field>
|
||||||
|
<field name="res_model">node.state</field>
|
||||||
|
<field name="view_mode">list</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu for States -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Stage Tree View -->
|
||||||
|
<record id="view_stage_stage_tree" model="ir.ui.view">
|
||||||
|
<field name="name">stage.stage.tree</field>
|
||||||
|
<field name="model">stage.stage</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Stages" create="false" edit="false">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="value"/>
|
||||||
|
<field name="model_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Stage Action -->
|
||||||
|
<record id="action_stage_stage" model="ir.actions.act_window">
|
||||||
|
<field name="name">Stages</field>
|
||||||
|
<field name="res_model">stage.stage</field>
|
||||||
|
<field name="view_mode">list</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu for Stages -->
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Dashboard Settings Page -->
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.dashboard</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority">99</field>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<app data-string="Dashboard" string="Dashboard" name="system_dashboard_classic" groups="system_dashboard_classic.group_dashboard_config">
|
||||||
|
<block title="Theme Colors" name="theme_colors">
|
||||||
|
<setting string="Primary Color" help="Main accent color for the dashboard">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="dashboard_primary_color" class="col-3 col-lg-3 o_light_label"/>
|
||||||
|
<field name="dashboard_primary_color" widget="color"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Secondary Color" help="Color for headers and dark elements">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="dashboard_secondary_color" class="col-3 col-lg-3 o_light_label"/>
|
||||||
|
<field name="dashboard_secondary_color" widget="color"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Success Color" help="Color for success/online status">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="dashboard_success_color" class="col-3 col-lg-3 o_light_label"/>
|
||||||
|
<field name="dashboard_success_color" widget="color"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Warning Color" help="Color for warnings and remaining balances">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="dashboard_warning_color" class="col-3 col-lg-3 o_light_label"/>
|
||||||
|
<field name="dashboard_warning_color" widget="color"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block title="Statistics Visibility" name="stats_visibility">
|
||||||
|
<setting help="Show or hide individual statistics cards">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row">
|
||||||
|
<field name="dashboard_show_annual_leave"/>
|
||||||
|
<label for="dashboard_show_annual_leave"/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<field name="dashboard_show_salary_slips"/>
|
||||||
|
<label for="dashboard_show_salary_slips"/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<field name="dashboard_show_timesheet"/>
|
||||||
|
<label for="dashboard_show_timesheet"/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<field name="dashboard_show_attendance_hours"/>
|
||||||
|
<label for="dashboard_show_attendance_hours"/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<field name="dashboard_show_attendance_section"/>
|
||||||
|
<label for="dashboard_show_attendance_section"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block title="Chart Types" name="chart_types">
|
||||||
|
<setting string="Annual Leave Chart" help="Chart style for annual leave card">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_annual_leave_chart_type" widget="radio"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Salary Slips Chart" help="Chart style for salary slips card">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_salary_slips_chart_type" widget="radio"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Timesheet Chart" help="Chart style for timesheet card">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_timesheet_chart_type" widget="radio"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Attendance Hours Chart" help="Chart style for attendance hours card">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_attendance_hours_chart_type" widget="radio"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block title="Attendance Settings" name="attendance_settings">
|
||||||
|
<setting string="Enable Check-in/out Button" help="Allow employees to check in/out from dashboard">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_enable_attendance_button"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Show Work Timer" help="Display countdown timer for remaining work hours">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_show_work_timer"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block title="Auto Refresh" name="auto_refresh">
|
||||||
|
<setting string="Enable Auto Refresh" help="Automatically refresh dashboard data">
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="dashboard_refresh_enabled"/>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Refresh Interval" help="Time between refreshes (30-3600 seconds)">
|
||||||
|
<div class="content-group" invisible="not dashboard_refresh_enabled">
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="dashboard_refresh_interval" class="col-3 col-lg-3 o_light_label"/>
|
||||||
|
<field name="dashboard_refresh_interval"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</app>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Settings Action -->
|
||||||
|
<record id="action_dashboard_settings" model="ir.actions.act_window">
|
||||||
|
<field name="name">Dashboard Settings</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">res.config.settings</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">inline</field>
|
||||||
|
<field name="context">{'module': 'system_dashboard_classic'}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Settings Menu -->
|
||||||
|
<menuitem id="menu_dashboard_settings"
|
||||||
|
name="Settings"
|
||||||
|
parent="menu_dashboard_config"
|
||||||
|
action="action_dashboard_settings"
|
||||||
|
groups="system_dashboard_classic.group_dashboard_config"
|
||||||
|
sequence="99"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Client Action for Dashboard -->
|
||||||
|
<record id="action_dashboard_client" model="ir.actions.client">
|
||||||
|
<field name="name">Dashboard</field>
|
||||||
|
<field name="tag">system_dashboard_classic.Dashboard</field>
|
||||||
|
<field name="target">current</field>
|
||||||
|
<field name="params" eval="{}"/>
|
||||||
|
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Main Menu -->
|
||||||
|
<menuitem
|
||||||
|
id="menu_dashboard_root"
|
||||||
|
name="Dashboard"
|
||||||
|
web_icon="system_dashboard_classic,static/description/icon.png"
|
||||||
|
groups="base.group_user"
|
||||||
|
sequence="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Self Service Menu -->
|
||||||
|
<menuitem
|
||||||
|
id="menu_dashboard_self_service"
|
||||||
|
name="Self Service"
|
||||||
|
parent="menu_dashboard_root"
|
||||||
|
action="action_dashboard_client"
|
||||||
|
groups="base.group_user"
|
||||||
|
sequence="1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Configuration Menu -->
|
||||||
|
<menuitem
|
||||||
|
id="menu_dashboard_config"
|
||||||
|
name="Configuration"
|
||||||
|
parent="menu_dashboard_root"
|
||||||
|
groups="system_dashboard_classic.group_dashboard_config"
|
||||||
|
sequence="99"
|
||||||
|
/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue