Merge remote-tracking branch 'source_origin/dev_odex_base' into dev_odex30_base

This commit is contained in:
maltayyar2 2026-01-25 14:38:37 +03:00
commit 3fe8704696
40 changed files with 11693 additions and 467 deletions

View File

@ -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);

View File

@ -31,6 +31,7 @@
'base',
],
'data': [
'data/system_parameters.xml',
'views/res_config_settings.xml',
],
'assets': {

View File

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

View File

@ -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
}

View File

@ -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);
}

View File

@ -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();

View File

@ -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);

View File

@ -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;
}

View File

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

View File

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

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -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,
}

View File

@ -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 ""

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from . import dashboard
from . import config
from . import res_users
from . import res_config_settings

View File

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

View File

@ -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'),
}

View File

@ -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'
)

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_dashboard_user system_dashboard_classic.dashboard user model_system_dashboard_classic_dashboard base.group_user 1 0 0 0
3 access_dashboard_manager system_dashboard_classic.dashboard manager model_system_dashboard_classic_dashboard system_dashboard_classic.group_dashboard_manager 1 1 1 1
4 access_base_dashbord_user base.dashbord user model_base_dashbord base.group_user 1 0 0 0
5 access_base_dashbord_manager base.dashbord manager model_base_dashbord system_dashboard_classic.group_dashboard_config 1 1 1 1
6 access_base_dashbord_line_user base.dashbord.line user model_base_dashbord_line base.group_user 1 0 0 0
7 access_base_dashbord_line_manager base.dashbord.line manager model_base_dashbord_line system_dashboard_classic.group_dashboard_config 1 1 1 1
8 access_node_state_user node.state user model_node_state base.group_user 1 0 0 0
9 access_node_state_manager node.state manager model_node_state system_dashboard_classic.group_dashboard_config 1 1 1 1
10 access_stage_stage_user stage.stage user model_stage_stage base.group_user 1 0 0 0
11 access_stage_stage_manager stage.stage manager model_stage_stage system_dashboard_classic.group_dashboard_config 1 1 1 1

View File

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

View File

@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}
}

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.&#10;&#10;Examples:&#10;• 'employee_id.user_id' - For HR models (hr.leave, hr.expense, etc.)&#10;• 'user_id' - For models with direct user reference (purchase.order, etc.)&#10;• '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>

View File

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

View File

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