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);
|
||||
}
|
||||
});
|
||||
console.log('Expert Theme colors applied successfully!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying Expert Theme colors:', error);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
'base',
|
||||
],
|
||||
'data': [
|
||||
'data/system_parameters.xml',
|
||||
'views/res_config_settings.xml',
|
||||
],
|
||||
'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):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# Sidebar Menu Enable Setting
|
||||
sidebar_menu_enable = fields.Boolean(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_menu_enable',
|
||||
string='Enable Sidebar Menu',
|
||||
help='Enable or disable the sidebar menu in the backend'
|
||||
)
|
||||
|
||||
# Navigation Menu Section Disable Setting
|
||||
disable_nav_menu_section = fields.Boolean(
|
||||
config_parameter='odex_sidebar_backend_theme2.disable_nav_menu_section',
|
||||
string='Disable Navigation Menu Section',
|
||||
help='Enable or disable the top navigation bar menu section in the backend interface'
|
||||
)
|
||||
|
||||
# Sidebar Menu Icon Setting
|
||||
sidebar_menu_icon = fields.Binary(
|
||||
string="Sidebar Icon",
|
||||
help="Upload an icon for the sidebar menu.",
|
||||
)
|
||||
|
||||
# Uncollapsed Sidebar Overlay Setting
|
||||
uncollapsed_sidebar_overlay = fields.Boolean(
|
||||
config_parameter='odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay',
|
||||
string='Uncollapsed Sidebar Overlay',
|
||||
help='Enable overlay effect when sidebar is uncollapsed'
|
||||
)
|
||||
|
||||
# set default value for the setting
|
||||
# Sidebar Background Settings
|
||||
sidebar_background_type = fields.Selection(
|
||||
[('color', 'Color'), ('image', 'Image')],
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_background_type',
|
||||
string='Sidebar Background Type',
|
||||
default='color',
|
||||
help='Choose the type of background for the sidebar'
|
||||
)
|
||||
sidebar_background_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_background_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Background Color',
|
||||
help='Set the background color for the sidebar (hex code or color name)'
|
||||
)
|
||||
sidebar_background_image = fields.Binary(
|
||||
string='Sidebar Background Image',
|
||||
help='Upload an image to use as the sidebar background'
|
||||
)
|
||||
|
||||
# Sidebar Links Color Settings
|
||||
sidebar_links_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_color',
|
||||
default='#ffffff',
|
||||
string='Sidebar Links Color',
|
||||
help='Set the color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_links_hover_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_hover_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Links Hover Color',
|
||||
help='Set the hover color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_links_active_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_active_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Links Active Color',
|
||||
help='Set the active color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_links_bg_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_bg_color',
|
||||
default='#ffffff00',
|
||||
string='Sidebar Links Background Color',
|
||||
help='Set the background color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_links_hover_bg_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_hover_bg_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Links Hover Background Color',
|
||||
help='Set the hover background color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_links_active_bg_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_links_active_bg_color',
|
||||
default='#ffffff',
|
||||
string='Sidebar Links Active Background Color',
|
||||
help='Set the active background color for the sidebar links (hex code or color name)'
|
||||
)
|
||||
sidebar_scrollbar_track_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_track_color',
|
||||
default='#2f3542',
|
||||
string='Sidebar Scrollbar Track Color',
|
||||
help='Set the color for the sidebar scrollbar track (hex code or color name)'
|
||||
)
|
||||
sidebar_scrollbar_thumb_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Scrollbar Thumb Color',
|
||||
help='Set the color for the sidebar scrollbar thumb (hex code or color name)'
|
||||
)
|
||||
sidebar_scrollbar_thumb_hover_color = fields.Char(
|
||||
config_parameter='odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color',
|
||||
default='#151a2d',
|
||||
string='Sidebar Scrollbar Thumb Hover Color',
|
||||
help='Set the hover color for the sidebar scrollbar thumb (hex code or color name)'
|
||||
)
|
||||
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
sidebar_menu_enable = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable')
|
||||
disable_nav_menu_section = IrConfigParam.get_param('odex_sidebar_backend_theme2.disable_nav_menu_section')
|
||||
uncollapsed_sidebar_overlay = IrConfigParam.get_param('odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay')
|
||||
sidebar_background_type = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_type')
|
||||
sidebar_background_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_color')
|
||||
sidebar_background_image = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_background_image')
|
||||
sidebar_links_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_color')
|
||||
sidebar_links_hover_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_hover_color')
|
||||
sidebar_links_active_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_active_color')
|
||||
sidebar_links_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_bg_color')
|
||||
sidebar_links_hover_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_hover_bg_color')
|
||||
sidebar_links_active_bg_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_links_active_bg_color')
|
||||
sidebar_scrollbar_track_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_track_color')
|
||||
sidebar_scrollbar_thumb_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color')
|
||||
sidebar_scrollbar_thumb_hover_color = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color')
|
||||
|
||||
res.update(
|
||||
sidebar_menu_enable=sidebar_menu_enable == 'True',
|
||||
disable_nav_menu_section=disable_nav_menu_section == 'True',
|
||||
sidebar_menu_icon=IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon')
|
||||
sidebar_menu_icon=IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon'),
|
||||
uncollapsed_sidebar_overlay=uncollapsed_sidebar_overlay == 'True',
|
||||
sidebar_background_type=sidebar_background_type or 'color',
|
||||
sidebar_background_color=sidebar_background_color or '#151a2d',
|
||||
sidebar_background_image=sidebar_background_image or False,
|
||||
sidebar_links_color=sidebar_links_color or '#ffffff',
|
||||
sidebar_links_hover_color=sidebar_links_hover_color or '#151a2d',
|
||||
sidebar_links_active_color=sidebar_links_active_color or '#151a2d',
|
||||
sidebar_links_bg_color=sidebar_links_bg_color or '#ffffff00',
|
||||
sidebar_links_hover_bg_color=sidebar_links_hover_bg_color or '#151a2d',
|
||||
sidebar_links_active_bg_color=sidebar_links_active_bg_color or '#ffffff',
|
||||
sidebar_scrollbar_track_color=sidebar_scrollbar_track_color or '#2f3542',
|
||||
sidebar_scrollbar_thumb_color=sidebar_scrollbar_thumb_color or '#151a2d',
|
||||
sidebar_scrollbar_thumb_hover_color=sidebar_scrollbar_thumb_hover_color or '#151a2d',
|
||||
)
|
||||
return res
|
||||
|
||||
def _generate_sidebar_css(self):
|
||||
"""Generate CSS rules for sidebar menu state"""
|
||||
if self.disable_nav_menu_section:
|
||||
return """
|
||||
.o_main_navbar .o_menu_sections {
|
||||
display: none!important;
|
||||
visibility: hidden!important;
|
||||
}
|
||||
"""
|
||||
return ""
|
||||
|
||||
# save the setting value
|
||||
def set_values(self):
|
||||
super(ResConfigSettings, self).set_values()
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
|
|
@ -63,7 +162,60 @@ class ResConfigSettings(models.TransientModel):
|
|||
'odex_sidebar_backend_theme2.sidebar_menu_icon',
|
||||
self.sidebar_menu_icon or ''
|
||||
)
|
||||
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.uncollapsed_sidebar_overlay',
|
||||
str(self.uncollapsed_sidebar_overlay)
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_background_type',
|
||||
self.sidebar_background_type or 'color'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_background_color',
|
||||
self.sidebar_background_color or '#151a2d'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_background_image',
|
||||
self.sidebar_background_image or False
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_color',
|
||||
self.sidebar_links_color or '#ffffff'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_hover_color',
|
||||
self.sidebar_links_hover_color or '#151a2d'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_active_color',
|
||||
self.sidebar_links_active_color or '#151a2d'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_bg_color',
|
||||
self.sidebar_links_bg_color or '#ffffff00'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_hover_bg_color',
|
||||
self.sidebar_links_hover_bg_color or '#151a2d'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_links_active_bg_color',
|
||||
self.sidebar_links_active_bg_color or '#ffffff'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_scrollbar_track_color',
|
||||
self.sidebar_scrollbar_track_color or '#2f3542'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_color',
|
||||
self.sidebar_scrollbar_thumb_color or '#151a2d'
|
||||
)
|
||||
IrConfigParam.set_param(
|
||||
'odex_sidebar_backend_theme2.sidebar_scrollbar_thumb_hover_color',
|
||||
self.sidebar_scrollbar_thumb_hover_color or '#151a2d'
|
||||
)
|
||||
|
||||
# Store sidebar menu icon URL
|
||||
if self.sidebar_menu_icon:
|
||||
# Store the image URL in config parameter
|
||||
image_url = f"/web/image/res.config.settings/{self.id}/sidebar_menu_icon"
|
||||
|
|
@ -80,9 +232,42 @@ class ResConfigSettings(models.TransientModel):
|
|||
|
||||
return True
|
||||
|
||||
def _generate_sidebar_css(self):
|
||||
"""Generate CSS rules for sidebar menu state"""
|
||||
css = f"""
|
||||
/* Sidebar Menu State CSS */
|
||||
:root {{
|
||||
--ox-sidebar-bg-color: {self.sidebar_background_color};
|
||||
--ox-sidebar-links-color: {self.sidebar_links_color};
|
||||
--ox-sidebar-links-hover-color: {self.sidebar_links_hover_color};
|
||||
--ox-sidebar-links-active-color: {self.sidebar_links_active_color};
|
||||
--ox-sidebar-links-bg-color: {self.sidebar_links_bg_color};
|
||||
--ox-sidebar-links-hover-bg-color: {self.sidebar_links_hover_bg_color};
|
||||
--ox-sidebar-links-active-bg-color: {self.sidebar_links_active_bg_color};
|
||||
--ox-sidebar-scrollbar-track-color: {self.sidebar_scrollbar_track_color};
|
||||
--ox-sidebar-scrollbar-thumb-color: {self.sidebar_scrollbar_thumb_color};
|
||||
--ox-sidebar-scrollbar-thumb-hover-color: {self.sidebar_scrollbar_thumb_hover_color};
|
||||
}}
|
||||
"""
|
||||
if self.disable_nav_menu_section:
|
||||
css += """
|
||||
.o_main_navbar .o_menu_sections {
|
||||
display: none!important;
|
||||
visibility: hidden!important;
|
||||
}
|
||||
"""
|
||||
return css
|
||||
|
||||
@api.model
|
||||
def get_sidebar_setting(self):
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
sidebar_enabled = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_enable') == 'True'
|
||||
sidebar_icon_url = IrConfigParam.get_param('odex_sidebar_backend_theme2.sidebar_menu_icon_url')
|
||||
return {'sidebar_enabled': sidebar_enabled, 'sidebar_icon_url': sidebar_icon_url}
|
||||
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
|
||||
const uniqueId = menu.xmlid || menu.name || menu.id;
|
||||
localStorage.setItem('odex_sidebar_active_menu', uniqueId);
|
||||
console.log('Saved menu:', uniqueId, menu.name);
|
||||
} catch (e) {
|
||||
console.error('Storage error:', e);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ export function loadSidebarCSS() {
|
|||
kwargs: {},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('Fetched sidebar CSS:', css);
|
||||
|
||||
if (css && css.trim()) {
|
||||
// Create a style element and inject the CSS
|
||||
|
|
@ -29,7 +27,7 @@ export function loadSidebarCSS() {
|
|||
style.id = 'sidebar-dynamic-css';
|
||||
style.innerHTML = css;
|
||||
document.head.appendChild(style);
|
||||
console.log('Sidebar CSS injected successfully');
|
||||
console.error('Error loading sidebar CSS:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading sidebar CSS:', error);
|
||||
|
|
@ -45,9 +43,9 @@ export function loadSidebarCSS() {
|
|||
}
|
||||
|
||||
// Initialize on module load
|
||||
registry.category("web_tour.tours").add("sidebar_css_loader", {
|
||||
steps: [],
|
||||
});
|
||||
// registry.category("web_tour.tours").add("sidebar_css_loader", {
|
||||
// steps: [],
|
||||
// });
|
||||
|
||||
// Auto-load CSS on page load
|
||||
loadSidebarCSS();
|
||||
|
|
|
|||
|
|
@ -23,26 +23,37 @@ export class SidebarMenu extends Component {
|
|||
isCollapsed: false,
|
||||
sidebarEnabled: false,
|
||||
sidebarMenuIconUrl: null,
|
||||
overlayEnabled: false,
|
||||
supportTeamLink: null,
|
||||
});
|
||||
|
||||
this.loadSidebarSetting()
|
||||
|
||||
// =================== JavaScript Control Starts Here ===================
|
||||
|
||||
const applyLayoutChanges = (isOpen) => {
|
||||
const applyLayoutChanges = (isOpen,isCollapsed) => {
|
||||
const actionManager = document.querySelector('.o_action_manager');
|
||||
const mainNavbar = document.querySelector('.o_navbar');
|
||||
|
||||
// Determine sidebar width based on collapse state
|
||||
const collapsedWidth = "90px";
|
||||
let collapsedWidth = "270px";
|
||||
if (this.state.overlayEnabled) {
|
||||
collapsedWidth = '90px';
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
if (!isCollapsed) {
|
||||
if (actionManager) actionManager.style.marginInlineStart = collapsedWidth;
|
||||
if (mainNavbar) mainNavbar.style.marginInlineStart = collapsedWidth;
|
||||
} else {
|
||||
// If sidebar is hidden, remove margin
|
||||
if (actionManager) actionManager.style.marginInlineStart = '0';
|
||||
if (mainNavbar) mainNavbar.style.marginInlineStart = '0';
|
||||
if (actionManager) actionManager.style.marginInlineStart = '90px';
|
||||
if (mainNavbar) mainNavbar.style.marginInlineStart = '90px';
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
// If sidebar is closed, remove margin
|
||||
if (actionManager) actionManager.style.marginInlineStart = '0px';
|
||||
if (mainNavbar) mainNavbar.style.marginInlineStart = '0px';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -118,10 +129,16 @@ export class SidebarMenu extends Component {
|
|||
// Load sidebar menu icon URL
|
||||
this.state.sidebarMenuIconUrl = result.sidebar_icon_url || '/odex_sidebar_backend_theme2/static/src/img/logo.webp';
|
||||
|
||||
// Load overlay setting
|
||||
this.state.overlayEnabled = result.uncollapsed_sidebar_overlay === true || result.uncollapsed_sidebar_overlay === 'True';
|
||||
|
||||
// Load support team link
|
||||
this.state.supportTeamLink = result.support_team_link || 'https://odex.sa/support';
|
||||
} catch (error) {
|
||||
console.error('Error loading sidebar setting:', error);
|
||||
// Default to enabled if setting cannot be loaded
|
||||
this.state.sidebarEnabled = true;
|
||||
this.state.overlayEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +170,6 @@ export class SidebarMenu extends Component {
|
|||
|
||||
try {
|
||||
const activeId = localStorage.getItem('odex_sidebar_active_menu');
|
||||
console.log('Restored menu ID:', activeId);
|
||||
|
||||
let targetMenu = menuMapByXmlId.get(activeId) || menuMap.get(activeId);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,455 +2,456 @@
|
|||
1. Main Sidebar Design
|
||||
=================================== */
|
||||
.custom_sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 270px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background-color: #151a2d;
|
||||
z-index: 999;
|
||||
transform: translateX(0%);
|
||||
transition: all 0.4s ease-in-out;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 270px;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
background-color: var(--ox-sidebar-bg-color, #151a2d);
|
||||
z-index: 999;
|
||||
transform: translateX(0%);
|
||||
transition: all 0.4s ease-in-out;
|
||||
|
||||
&:not(.is-open) {
|
||||
transform: translateX(-100%) !important;
|
||||
/* Use !important to force hiding */
|
||||
}
|
||||
|
||||
/* =============== Add New Flyout =============== */
|
||||
|
||||
&.is-collapsed {
|
||||
overflow: visible;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
&:not(.is-open) {
|
||||
transform: translateX(-100%) !important;
|
||||
/* Use !important to force hiding */
|
||||
}
|
||||
|
||||
.flyout-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
min-width: 220px;
|
||||
background: #151a2d;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||
transform-origin: left top;
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px) scale(0.98);
|
||||
pointer-events: none;
|
||||
/* =============== Add New Flyout =============== */
|
||||
|
||||
&.is-collapsed {
|
||||
overflow: visible;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.flyout-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
min-width: 220px;
|
||||
background: var(--ox-sidebar-bg-color, #151a2d);
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||
transform-origin: left top;
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
opacity: 0;
|
||||
transform: translateX(-6px) scale(0.98);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
li:hover>.flyout-panel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.flyout-list {
|
||||
list-style: none;
|
||||
padding: 6px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.flyout-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flyout-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.flyout-item .icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flyout-item .label {
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Submenu flyout */
|
||||
.flyout-item.has-children {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flyout-subpanel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
min-width: 200px;
|
||||
background: #151a2d;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||
opacity: 0;
|
||||
transform: translateX(-6px) scale(0.98);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
pointer-events: none;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.flyout-item.has-children:hover>.flyout-subpanel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
li:hover > .flyout-panel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
&.is-collapsed {
|
||||
width: 90px;
|
||||
transform: translateX(0);
|
||||
|
||||
.sidebar-header {
|
||||
padding: 25px 10px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
.header-logo {
|
||||
img {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggler {
|
||||
position: static;
|
||||
width: 50%;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
.sidebar_menu_list {
|
||||
padding: 0 8px;
|
||||
|
||||
&.primary-nav {
|
||||
|
||||
.has-children,
|
||||
li:not(.has-children) {
|
||||
position: relative;
|
||||
|
||||
.menu-item-container {
|
||||
padding: 11px 8px;
|
||||
justify-content: center;
|
||||
|
||||
.menu-item-icon {
|
||||
margin: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.menu-item-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eef2ff;
|
||||
color: #151a2d;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #d9e1fd;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu_list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary-nav {
|
||||
width: auto;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 8px;
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
|
||||
.nav-link {
|
||||
padding: 11px 8px;
|
||||
justify-content: center;
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav_icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Flyout support for secondary-nav
|
||||
&:hover>.flyout-panel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flyout-list {
|
||||
list-style: none;
|
||||
padding: 6px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
li.open {
|
||||
>.menu-item-container {
|
||||
background-color: #fff;
|
||||
|
||||
.menu-item-link {
|
||||
color: #151a2d !important;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transform: rotate(90deg);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flyout-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
white-space: nowrap;
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.flyout-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--ox-sidebar-scrollbar-track-color, #0e1223);
|
||||
}
|
||||
|
||||
.flyout-item .icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--ox-sidebar-scrollbar-thumb-color, #151a2d);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.flyout-item .label {
|
||||
color: #fff;
|
||||
font-size: 0.95rem;
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--ox-sidebar-scrollbar-thumb-hover-color, #151a2d);
|
||||
}
|
||||
|
||||
/* Submenu flyout */
|
||||
.flyout-item.has-children {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flyout-subpanel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
min-width: 200px;
|
||||
background: #151a2d;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||
opacity: 0;
|
||||
transform: translateX(-6px) scale(0.98);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
pointer-events: none;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.flyout-item.has-children:hover > .flyout-subpanel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
width: 90px;
|
||||
transform: translateX(0);
|
||||
|
||||
.sidebar-header {
|
||||
padding: 25px 10px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 25px 20px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-logo {
|
||||
img {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
.header-logo {
|
||||
img {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggler {
|
||||
position: static;
|
||||
width: 50%;
|
||||
margin-top: 25px;
|
||||
}
|
||||
.sidebar-toggler {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
color: #151a2d;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background: #eef2ff;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: 0.4s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d9e1fd;
|
||||
}
|
||||
|
||||
span {
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
.sidebar_menu_list {
|
||||
padding: 0 8px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.primary-nav {
|
||||
|
||||
.has-children,
|
||||
li:not(.has-children) {
|
||||
position: relative;
|
||||
|
||||
.menu-item-container {
|
||||
padding: 11px 8px;
|
||||
justify-content: center;
|
||||
|
||||
.menu-item-icon {
|
||||
margin: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.menu-item-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #eef2ff;
|
||||
color: #151a2d;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #d9e1fd;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu_list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary-nav {
|
||||
width: auto;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 8px;
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
|
||||
.nav-link {
|
||||
padding: 11px 8px;
|
||||
justify-content: center;
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav_icon {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Flyout support for secondary-nav
|
||||
&:hover > .flyout-panel {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
transform: translateX(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li.open {
|
||||
> .menu-item-container {
|
||||
background-color: #fff;
|
||||
|
||||
.menu-item-link {
|
||||
color: #151a2d !important;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
transform: rotate(90deg);
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #2f3542;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #151a2d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #151a2d;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 25px 20px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-logo {
|
||||
img {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-toggler {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
color: #151a2d;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
background: #eef2ff;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: 0.4s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d9e1fd;
|
||||
}
|
||||
|
||||
span {
|
||||
transition: 0.4s ease;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sidebar_menu_list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 0 15px;
|
||||
flex-direction: column;
|
||||
transform: translateY(15px);
|
||||
transition: 0.4s ease;
|
||||
|
||||
&.primary-nav {
|
||||
.has-children,
|
||||
li:not(.has-children) {
|
||||
&:hover {
|
||||
> .menu-item-container {
|
||||
background-color: #eef2ff;
|
||||
|
||||
.menu-item-link {
|
||||
color: #151a2d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-container {
|
||||
.sidebar_menu_list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid #151a2d;
|
||||
text-decoration: none;
|
||||
padding: 11px 15px;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
// image icon style
|
||||
.menu-item-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 0 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// name style
|
||||
.menu-item-link {
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
// toggle icon style
|
||||
.toggle-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23b9d0ec' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu_list {
|
||||
list-style: none;
|
||||
padding: 5px 0;
|
||||
|
||||
li {
|
||||
.menu-item-container {
|
||||
margin-left: 8px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #eef2ff;
|
||||
border-color: #151a2d;
|
||||
|
||||
.menu-item-link {
|
||||
color: #151a2d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary-nav {
|
||||
margin-top: auto;
|
||||
// width: calc(100% - 30px);
|
||||
background: #151a2d;
|
||||
padding: 20px 15px;
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
// margin: 0 15px;
|
||||
|
||||
&:hover > .nav-link {
|
||||
color: #151a2d !important;
|
||||
background-color: #eef2ff;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #fff !important;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
white-space: nowrap;
|
||||
border-radius: 8px;
|
||||
padding: 11px 15px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
border: 1px solid #151a2d;
|
||||
gap: 4px;
|
||||
padding: 0 15px;
|
||||
flex-direction: column;
|
||||
transform: translateY(15px);
|
||||
transition: 0.4s ease;
|
||||
|
||||
.nav_icon {
|
||||
font-size: 24px;
|
||||
margin: 0 10px;
|
||||
&.primary-nav {
|
||||
|
||||
.has-children,
|
||||
li:not(.has-children) {
|
||||
&:hover {
|
||||
>.menu-item-container {
|
||||
background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff);
|
||||
|
||||
.menu-item-link {
|
||||
color: var(--ox-sidebar-links-hover-color, #151a2d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-container {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: 1px solid transparent;
|
||||
text-decoration: none;
|
||||
padding: 11px 15px;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
|
||||
// image icon style
|
||||
.menu-item-icon {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 0 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
// name style
|
||||
.menu-item-link {
|
||||
color: #fff;
|
||||
flex-grow: 1;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
// toggle icon style
|
||||
.toggle-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23b9d0ec' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submenu_list {
|
||||
list-style: none;
|
||||
padding: 5px 0;
|
||||
|
||||
li {
|
||||
.menu-item-container {
|
||||
margin-left: 8px;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff);
|
||||
// border-color: #151a2d;
|
||||
|
||||
.menu-item-link {
|
||||
color: var(--ox-sidebar-links-hover-color, #151a2d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 1rem;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
&.secondary-nav {
|
||||
margin-top: auto;
|
||||
// width: calc(100% - 30px);
|
||||
background: var(--ox-sidebar-bg-color, #151a2d);
|
||||
padding: 20px 15px;
|
||||
|
||||
.dropdown_menu {
|
||||
height: 0;
|
||||
overflow-y: hidden;
|
||||
list-style: none;
|
||||
padding-left: 15px;
|
||||
transition: height 0.4s ease;
|
||||
}
|
||||
.nav-item {
|
||||
position: relative;
|
||||
// margin: 0 15px;
|
||||
|
||||
&:hover>.nav-link {
|
||||
color: var(--ox-sidebar-links-hover-color, #151a2d) !important;
|
||||
background-color: var(--ox-sidebar-links-hover-bg-color, #eef2ff);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #fff !important;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
white-space: nowrap;
|
||||
border-radius: 8px;
|
||||
padding: 11px 15px;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.4s ease;
|
||||
|
||||
.nav_icon {
|
||||
font-size: 24px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 1rem;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_menu {
|
||||
height: 0;
|
||||
overflow-y: hidden;
|
||||
list-style: none;
|
||||
padding-left: 15px;
|
||||
transition: height 0.4s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
|
|
@ -458,32 +459,32 @@
|
|||
=================================== */
|
||||
// Hide default application icon from top bar
|
||||
.o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.o_web_client {
|
||||
margin-left: 0 !important;
|
||||
transition: all 0.3s ease-in-out;
|
||||
margin-left: 0 !important;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Adjust Top Bar and Main Content */
|
||||
.o_navbar,
|
||||
.o_action_manager {
|
||||
transition: all 0.3s ease-in-out;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Open Icon Design (New) */
|
||||
.sidebar_toggle_icon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
height: calc(var(--o-navbar-height) - 0px);
|
||||
border-radius: 0;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
||||
font-size: 1.2em;
|
||||
padding: 0 15px;
|
||||
border: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: auto;
|
||||
height: calc(var(--o-navbar-height) - 0px);
|
||||
border-radius: 0;
|
||||
user-select: none;
|
||||
background: transparent;
|
||||
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
||||
font-size: 1.2em;
|
||||
padding: 0 15px;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<!-- Secondary Bottom Nav -->
|
||||
<ul class="sidebar_menu_list secondary-nav">
|
||||
<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-label">Support</span>
|
||||
</a>
|
||||
|
|
@ -36,7 +36,7 @@
|
|||
<div class="icon">
|
||||
<i class="fa fa-question-circle"></i>
|
||||
</div>
|
||||
<a href="https://www.odex.sa" class="label">Support</a>
|
||||
<a target="_blank" t-att-href="state.supportTeamLink" class="label">Support</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<!-- Normal dropdown (when not collapsed) -->
|
||||
<ul class="dropdown_menu" t-if="!state.isCollapsed">
|
||||
<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>
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -12,13 +12,34 @@
|
|||
<setting help="Enable or disable the sidebar menu in the backend interface">
|
||||
<field name="sidebar_menu_enable" />
|
||||
</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" />
|
||||
</setting>
|
||||
<setting>
|
||||
<group>
|
||||
<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>
|
||||
</setting>
|
||||
</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