diff --git a/odex30_base/expert_theme/README.md b/odex30_base/expert_theme/README.md new file mode 100644 index 0000000..b50cee3 --- /dev/null +++ b/odex30_base/expert_theme/README.md @@ -0,0 +1,30 @@ +# Expert Theme + +A simple and clean theme for Odoo 18 backend that provides a custom home page displaying only installed modules. + +## Features + +- **Custom Home Page**: Clean interface showing only your installed modules +- **Simple Navigation**: Easy access to all your applications +- **Responsive Design**: Works on all devices +- **Minimal Interface**: No clutter, just functionality + +## Installation + +1. Copy this module to your Odoo addons directory +2. Update the apps list in Odoo +3. Install the "Expert Theme" module +4. The custom home page will be available after installation + +## Usage + +After installation, when you log into Odoo, you'll see the new home page with all your installed modules displayed in a clean grid layout. Simply click on any module to access it. + +## Requirements + +- Odoo 18.0 +- No additional dependencies + +## Support + +This is a simple theme module designed for basic customization of the Odoo backend home page. diff --git a/odex30_base/expert_theme/__init__.py b/odex30_base/expert_theme/__init__.py new file mode 100644 index 0000000..c3d410e --- /dev/null +++ b/odex30_base/expert_theme/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/odex30_base/expert_theme/__manifest__.py b/odex30_base/expert_theme/__manifest__.py new file mode 100644 index 0000000..fd5b3b7 --- /dev/null +++ b/odex30_base/expert_theme/__manifest__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Expert Theme', + 'version': '18.0.1.0.0', + 'category': 'Theme/Backend', + 'summary': 'Custom backend theme with installed modules home page', + 'description': """ + Expert Theme + ============ + + A simple custom theme for Odoo 18 backend that provides: + - Custom home page displaying only installed modules + - Clean and minimal interface + - Easy navigation to installed applications + """, + 'author': 'Expert', + 'website': '', + 'depends': ['base', 'web'], + 'data': [ + 'security/ir.model.access.csv', + 'data/expert_login_template_data.xml', + 'views/login_templates.xml', + 'views/expert_login_template_views.xml', + 'views/expert_theme_config_views.xml', + 'views/expert_home_views.xml', + 'views/expert_menu_views.xml', + ], + 'assets': { + 'web.assets_backend': [ + 'expert_theme/static/src/css/expert_theme_config.css', + 'expert_theme/static/src/css/expert_theme.css', + 'expert_theme/static/src/js/expert_theme_dynamic.js', + 'expert_theme/static/src/js/expert_theme_config.js', + 'expert_theme/static/src/js/expert_home.js', + 'expert_theme/static/src/js/expert_login_template_list.js', + 'expert_theme/static/src/xml/expert_home.xml', + ], + 'web.assets_frontend': [ + 'expert_theme/static/src/css/expert_login.css', + 'expert_theme/static/src/css/login_modern.css', + 'expert_theme/static/src/css/login_minimal.css', + 'expert_theme/static/src/js/expert_login_template.js', + ], + }, + 'installable': True, + 'auto_install': False, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/odex30_base/expert_theme/controllers/__init__.py b/odex30_base/expert_theme/controllers/__init__.py new file mode 100644 index 0000000..2b4d723 --- /dev/null +++ b/odex30_base/expert_theme/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import expert_controller diff --git a/odex30_base/expert_theme/controllers/expert_controller.py b/odex30_base/expert_theme/controllers/expert_controller.py new file mode 100644 index 0000000..4f5b646 --- /dev/null +++ b/odex30_base/expert_theme/controllers/expert_controller.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- + +from odoo import http, fields +from odoo.http import request +import json +import logging + +_logger = logging.getLogger(__name__) + + +class ExpertController(http.Controller): + + @http.route('/expert_theme/test', type='http', auth='user') + def test_controller(self): + """Simple test endpoint to verify controller is working""" + return request.make_json_response({ + 'success': True, + 'message': 'Controller is working!', + 'timestamp': fields.Datetime.now() + }) + + @http.route('/expert_theme/get_css_variables', type='http', auth='user') + def get_css_variables(self): + """Get CSS variables for the active theme configuration""" + try: + config = request.env['expert.theme.config'].get_active_config() + css_variables = config.get_css_variables() + return request.make_json_response({ + 'success': True, + 'css_variables': css_variables + }) + except Exception as e: + return request.make_json_response({ + 'success': False, + 'error': str(e) + }) + + @http.route('/expert_theme/apply_theme', type='http', auth='user', methods=['POST']) + def apply_theme(self): + """Apply theme changes immediately""" + try: + # Get the active configuration + config = request.env['expert.theme.config'].get_active_config() + + # Apply the theme (this could trigger cache invalidation, etc.) + config.apply_theme() + + return request.make_json_response({ + 'success': True, + 'message': 'Theme applied successfully!' + }) + except Exception as e: + return request.make_json_response({ + 'success': False, + 'error': str(e) + }) + + @http.route('/web/login', type='http', auth='none', methods=['GET', 'POST'], csrf=False) + def web_login(self, redirect=None, **kw): + """Override Odoo login to use expert login templates when configured. + + - POST: delegate to standard Odoo login logic (authentication, redirects, etc.). + - GET: render the active expert login template if not 'default', otherwise use standard login. + """ + # Import Home from its new location in Odoo 18 + from odoo.addons.web.controllers.home import Home + + # Handle login submission via standard controller + if request.httprequest.method == 'POST': + home = Home() + return home.web_login(redirect=redirect, **kw) + + # GET: decide which template to render + try: + login_template = request.env['expert.login.template'].sudo().get_active_template() + template_name = login_template.get_template_name() + except Exception as e: + _logger.error("Error getting active login template: %s", e) + template_name = 'web.login' + + # For default template, just call the standard login + if template_name == 'web.login': + home = Home() + return home.web_login(redirect=redirect, **kw) + + # Try to render the selected expert template; fall back to default on error + try: + return request.render(template_name, { + 'redirect': redirect or '', + 'login': kw.get('login', ''), + 'login_template': login_template, + }) + except Exception as e: + _logger.error("Error rendering login template '%s': %s", template_name, e) + home = Home() + return home.web_login(redirect=redirect, **kw) + + @http.route('/expert_theme/get_login_template_styles', type='http', auth='public', methods=['GET']) + def get_login_template_styles(self): + """Get CSS styles for the active login template (public access for login page)""" + try: + template = request.env['expert.login.template'].sudo().get_active_template() + styles = template.get_template_styles() + return request.make_json_response({ + 'success': True, + 'styles': styles + }) + except Exception as e: + # Return default styles if error + return request.make_json_response({ + 'success': False, + 'error': str(e), + 'styles': {'background_color': '#FFFFFF'} + }) + + @http.route('/expert_theme/get_installed_modules_http', type='http', auth='user') + def get_installed_modules_http(self): + """Return list of installed modules for the home page via HTTP""" + try: + # Get all installed modules + installed_modules = request.env['ir.module.module'].search([ + ('state', '=', 'installed'), + ('name', '!=', 'expert_theme') # Exclude our own module + ]) + + # Get all top-level menu items that belong to installed modules + menu_items = request.env['ir.ui.menu'].search([ + ('parent_id', '=', False), # Top level menus only + ('active', '=', True), + ('name', 'not in', ['Expert Home', 'Dashboard']) # Exclude our own menus + ]) + + result = [] + installed_module_names = installed_modules.mapped('name') + + debug_info = { + 'total_installed_modules': len(installed_modules), + 'total_menu_items': len(menu_items) + } + + for menu in menu_items: + # Check if this menu belongs to an installed module + module_name = None + if menu.web_icon and ',' in menu.web_icon: + module_name = menu.web_icon.split(',')[0] + + # Include menu if it belongs to an installed module OR if it has no web_icon (base modules) + if (module_name and module_name in installed_module_names) or not menu.web_icon: + + # Create URL based on action type - use proper Odoo navigation + if menu.action and menu.action.type == 'ir.actions.act_window': + # For act_window, use the action ID + url = f'/web#action={menu.action.id}' + elif menu.action and menu.action.type == 'ir.actions.client': + # For client actions, use the action ID + url = f'/web#action={menu.action.id}' + elif menu.action and menu.action.type == 'ir.actions.server': + # For server actions, use the action ID + url = f'/web#action={menu.action.id}' + else: + # For menus without actions, use menu_id + url = f'/web#menu_id={menu.id}' + + result.append({ + 'id': menu.id, + 'name': menu.name, + 'web_icon': menu.web_icon or 'base,static/description/icon.png', + 'action': menu.action.id if menu.action else False, + 'url': url, + 'module_name': module_name + }) + + return request.make_json_response({ + 'success': True, + 'modules': result, + 'debug_info': debug_info + }) + except Exception as e: + return request.make_json_response({ + 'success': False, + 'error': str(e) + }) + + @http.route('/web/signup', type='http', auth='public', methods=['GET', 'POST'], website=True, csrf=False) + def web_auth_signup(self, redirect=None, **kw): + """Override Odoo signup to use expert signup templates when modern template is active. + + - POST: delegate to standard Odoo signup logic (authentication, redirects, etc.). + - GET: render the active expert signup template if modern template is active, otherwise use standard signup. + """ + # Import AuthSignupHome from auth_signup module + try: + from odoo.addons.auth_signup.controllers.main import AuthSignupHome + except ImportError: + # If auth_signup is not installed, return 404 + from werkzeug.exceptions import NotFound + raise NotFound() + + # Handle signup submission via standard controller + if request.httprequest.method == 'POST': + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_signup(redirect=redirect, **kw) + + # GET: decide which template to render + try: + login_template = request.env['expert.login.template'].sudo().get_active_template() + template_name = login_template.get_signup_template_name() + except Exception as e: + _logger.error("Error getting active signup template: %s", e) + template_name = 'auth_signup.signup' + + # For default template, just call the standard signup + if template_name == 'auth_signup.signup': + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_signup(redirect=redirect, **kw) + + # Try to render the selected expert template; fall back to default on error + try: + # Get signup context from auth_signup + auth_signup_home = AuthSignupHome() + qcontext = auth_signup_home.get_auth_signup_qcontext() + qcontext.update({ + 'redirect': redirect or '', + 'login': kw.get('login', ''), + 'login_template': login_template, + }) + response = request.render(template_name, qcontext) + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" + return response + except Exception as e: + _logger.error("Error rendering signup template '%s': %s", template_name, e) + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_signup(redirect=redirect, **kw) + + @http.route('/web/reset_password', type='http', auth='public', methods=['GET', 'POST'], website=True, csrf=False) + def web_auth_reset_password(self, redirect=None, **kw): + """Override Odoo reset password to use expert reset password templates when modern template is active. + + - POST: delegate to standard Odoo reset password logic (authentication, redirects, etc.). + - GET: render the active expert reset password template if modern template is active, otherwise use standard reset password. + """ + # Import AuthSignupHome from auth_signup module + try: + from odoo.addons.auth_signup.controllers.main import AuthSignupHome + except ImportError: + # If auth_signup is not installed, return 404 + from werkzeug.exceptions import NotFound + raise NotFound() + + # Handle reset password submission via standard controller + if request.httprequest.method == 'POST': + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw) + + # GET: decide which template to render + try: + login_template = request.env['expert.login.template'].sudo().get_active_template() + template_name = login_template.get_reset_password_template_name() + except Exception as e: + _logger.error("Error getting active reset password template: %s", e) + template_name = 'auth_signup.reset_password' + + # For default template, just call the standard reset password + if template_name == 'auth_signup.reset_password': + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw) + + # Try to render the selected expert template; fall back to default on error + try: + # Get reset password context from auth_signup + auth_signup_home = AuthSignupHome() + qcontext = auth_signup_home.get_auth_signup_qcontext() + qcontext.update({ + 'redirect': redirect or '', + 'login': kw.get('login', ''), + 'login_template': login_template, + }) + response = request.render(template_name, qcontext) + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + response.headers['Content-Security-Policy'] = "frame-ancestors 'self'" + return response + except Exception as e: + _logger.error("Error rendering reset password template '%s': %s", template_name, e) + auth_signup_home = AuthSignupHome() + return auth_signup_home.web_auth_reset_password(redirect=redirect, **kw) diff --git a/odex30_base/expert_theme/data/expert_login_template_data.xml b/odex30_base/expert_theme/data/expert_login_template_data.xml new file mode 100644 index 0000000..dfd3b38 --- /dev/null +++ b/odex30_base/expert_theme/data/expert_login_template_data.xml @@ -0,0 +1,51 @@ + + + + + + + + Default Template + True + 1 + default + #FFFFFF + True + Standard Odoo login page template + + + + + Modern Template + False + 2 + modern + #667eea + False + Modern card design with gradient background + + + + + Minimal Template + False + 3 + minimal + #F8F9FA + False + Clean minimal design template + + + + + Corporate Template + False + 4 + corporate + #1a1a1a + False + Professional corporate dark theme + + + + diff --git a/odex30_base/expert_theme/models/__init__.py b/odex30_base/expert_theme/models/__init__.py new file mode 100644 index 0000000..6878437 --- /dev/null +++ b/odex30_base/expert_theme/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import expert_theme_config +from . import expert_login_template diff --git a/odex30_base/expert_theme/models/expert_login_template.py b/odex30_base/expert_theme/models/expert_login_template.py new file mode 100644 index 0000000..a17dce2 --- /dev/null +++ b/odex30_base/expert_theme/models/expert_login_template.py @@ -0,0 +1,289 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class ExpertLoginTemplate(models.Model): + _name = 'expert.login.template' + _description = 'Expert Login Page Template' + _rec_name = 'name' + _order = 'sequence, name' + + name = fields.Char(string='Template Name', required=True, help='Name of the login page template') + active = fields.Boolean(string='Active', default=False, help='Only one template can be active at a time. Activate this template to use it on the login page.') + sequence = fields.Integer(string='Sequence', default=10, help='Order of display') + + # Template Design Selection + template_type = fields.Selection([ + ('default', 'Default Odoo Login'), + ('modern', 'Modern Card Design'), + ('minimal', 'Minimal Design'), + ('corporate', 'Corporate Design'), + ], string='Template Design', default='default', required=True, + help='Select the login page template design. Each design has a completely different HTML structure and layout.') + + # Template Settings (for backward compatibility and CSS customization) + background_color = fields.Char(string='Background Color', default='#FFFFFF', help='Background color for the login page') + is_default = fields.Boolean(string='Is Default Template', default=False, help='Default template to use') + + # Template Images + modern_template_image = fields.Binary(string='Modern Template Image', help='Image to display on the right side of Modern login, signup, and reset password pages') + minimal_template_image = fields.Binary(string='Minimal Template Image', help='Image to display on the right side of Minimal login, signup, and reset password pages') + corporate_template_image = fields.Binary(string='Corporate Template Image', help='Image to display on the right side of Corporate login, signup, and reset password pages') + + # Template Logos + modern_template_logo = fields.Binary(string='Modern Template Logo', help='Company logo to display on Modern login, signup, and reset password pages') + minimal_template_logo = fields.Binary(string='Minimal Template Logo', help='Company logo to display on Minimal login, signup, and reset password pages') + corporate_template_logo = fields.Binary(string='Corporate Template Logo', help='Company logo to display on Corporate login, signup, and reset password pages') + + # Modern Template Text Fields + modern_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', help='Title text for Modern template login page') + modern_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', help='Subtitle text for Modern template login page') + modern_login_button_text = fields.Char(string='Login Button Text', default='Sign In', help='Button text for Modern template login page') + modern_login_button_bg_color = fields.Char(string='Login Button Background', default='#007bff', help='Background color for login button') + modern_login_button_text_color = fields.Char(string='Login Button Text Color', default='#FFFFFF', help='Text color for login button') + modern_login_button_bg_hover = fields.Char(string='Login Button BG Hover', default='#0056b3', help='Background color for login button on hover') + modern_login_button_text_hover = fields.Char(string='Login Button Text Hover', default='#FFFFFF', help='Text color for login button on hover') + + modern_signup_title = fields.Char(string='Signup Title', default='Create an account', help='Title text for Modern template signup page') + modern_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', help='Subtitle text for Modern template signup page') + modern_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', help='Button text for Modern template signup page') + modern_signup_button_bg_color = fields.Char(string='Signup Button Background', default='#28a745', help='Background color for signup button') + modern_signup_button_text_color = fields.Char(string='Signup Button Text Color', default='#FFFFFF', help='Text color for signup button') + modern_signup_button_bg_hover = fields.Char(string='Signup Button BG Hover', default='#218838', help='Background color for signup button on hover') + modern_signup_button_text_hover = fields.Char(string='Signup Button Text Hover', default='#FFFFFF', help='Text color for signup button on hover') + + modern_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', help='Title text for Modern template reset password page') + modern_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', help='Subtitle text for Modern template reset password page') + + # Minimal Template Text Fields + minimal_login_title = fields.Char(string='Login Title', default='Welcome to Expert 👋', help='Title text for Minimal template login page') + minimal_login_subtitle = fields.Char(string='Login Subtitle', default='Kindly fill in your details below to sign in to your account', help='Subtitle text for Minimal template login page') + minimal_login_button_text = fields.Char(string='Login Button Text', default='Sign In', help='Button text for Minimal template login page') + minimal_login_button_bg_color = fields.Char(string='Login Button Background', default='#E5E5E5', help='Background color for login button') + minimal_login_button_text_color = fields.Char(string='Login Button Text Color', default='#000000', help='Text color for login button') + minimal_login_button_bg_hover = fields.Char(string='Login Button BG Hover', default='#D0D0D0', help='Background color for login button on hover') + minimal_login_button_text_hover = fields.Char(string='Login Button Text Hover', default='#000000', help='Text color for login button on hover') + + minimal_signup_title = fields.Char(string='Signup Title', default='Create an account', help='Title text for Minimal template signup page') + minimal_signup_subtitle = fields.Char(string='Signup Subtitle', default='Join us today and get started', help='Subtitle text for Minimal template signup page') + minimal_signup_button_text = fields.Char(string='Signup Button Text', default='Create an account', help='Button text for Minimal template signup page') + minimal_signup_button_bg_color = fields.Char(string='Signup Button Background', default='#000000', help='Background color for signup button') + minimal_signup_button_text_color = fields.Char(string='Signup Button Text Color', default='#FFFFFF', help='Text color for signup button') + minimal_signup_button_bg_hover = fields.Char(string='Signup Button BG Hover', default='#333333', help='Background color for signup button on hover') + minimal_signup_button_text_hover = fields.Char(string='Signup Button Text Hover', default='#FFFFFF', help='Text color for signup button on hover') + + minimal_reset_title = fields.Char(string='Reset Password Title', default='Reset your password', help='Title text for Minimal template reset password page') + minimal_reset_subtitle = fields.Char(string='Reset Password Subtitle', default='Enter your email to receive reset instructions', help='Subtitle text for Minimal template reset password page') + + # Description + description = fields.Text(string='Description', help='Description of this template') + + @api.model + def get_active_template(self): + """Get the active login template or return default""" + try: + template = self.search([('active', '=', True)], limit=1, order='sequence') + if not template: + # Create a default template if none exists + template = self.create({ + 'name': 'Default Template', + 'active': True, + 'template_type': 'default', + 'background_color': '#FFFFFF', + 'is_default': True, + 'sequence': 1 + }) + return template + except Exception as e: + # If there's an error, try to return any template or create a minimal one + import logging + _logger = logging.getLogger(__name__) + _logger.error(f"Error in get_active_template: {str(e)}") + # Try to get any template + template = self.search([], limit=1) + if template: + return template + # Last resort - return a recordset that won't cause errors + return self.browse([]) + + @api.model + def create(self, vals): + """Ensure only one template is active at a time""" + # If no active value is set, default to False (don't auto-activate new templates) + if 'active' not in vals: + vals['active'] = False + # Only deactivate others if this one is being set to active + if vals.get('active'): + # Deactivate all other templates (excluding the one being created) + self.search([('active', '=', True)]).write({'active': False}) + return super(ExpertLoginTemplate, self).create(vals) + + def write(self, vals): + """Ensure only one template is active at a time""" + # Track if we need to reload + need_reload = False + + # Check if we're activating a template + if 'active' in vals and vals.get('active'): + # Deactivate all other templates (excluding the current recordset) + other_templates = self.search([('active', '=', True), ('id', 'not in', self.ids)]) + if other_templates: + # Use super().write to avoid recursion + super(ExpertLoginTemplate, other_templates).write({'active': False}) + need_reload = True + + result = super(ExpertLoginTemplate, self).write(vals) + + # Update template views if active state changed + if 'active' in vals or 'template_type' in vals: + self._update_template_views() + + # If we're in a list view context and need reload, return reload action + if need_reload and self.env.context.get('from_list_view'): + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } + + return result + + @api.onchange('active') + def _onchange_active(self): + """When activating a template in the form view, show a warning about deactivating others""" + if self.active and not self._origin.active: + other_active = self.search([('active', '=', True), ('id', '!=', self.id)]) + if other_active: + return { + 'warning': { + 'title': 'Other Templates Will Be Deactivated', + 'message': f'Activating this template will automatically deactivate: {", ".join(other_active.mapped("name"))}' + } + } + + def get_template_styles(self): + """Get CSS styles for this template""" + return { + 'background_color': self.background_color or '#FFFFFF', + } + + def get_template_name(self): + """Get the QWeb template name to render""" + template_map = { + 'default': 'web.login', # Use standard Odoo login + 'modern': 'expert_theme.login_template_modern_page', + 'minimal': 'expert_theme.login_template_minimal_page', + 'corporate': 'expert_theme.login_template_corporate_page', + } + return template_map.get(self.template_type, 'web.login') + + def get_signup_template_name(self): + """Get the QWeb template name for signup page""" + template_map = { + 'default': 'auth_signup.signup', # Use standard Odoo signup + 'modern': 'expert_theme.signup_template_modern_page', + 'minimal': 'expert_theme.signup_template_minimal_page', + 'corporate': 'auth_signup.signup', # Use default for now + } + return template_map.get(self.template_type, 'auth_signup.signup') + + def get_reset_password_template_name(self): + """Get the QWeb template name for reset password page""" + template_map = { + 'default': 'auth_signup.reset_password', # Use standard Odoo reset password + 'modern': 'expert_theme.reset_password_template_modern_page', + 'minimal': 'expert_theme.reset_password_template_minimal_page', + 'corporate': 'auth_signup.reset_password', # Use default for now + } + return template_map.get(self.template_type, 'auth_signup.reset_password') + + def toggle_active(self): + """Toggle active state - ensures only one template is active""" + # Ensure we're working with exactly one record + self.ensure_one() + + # Get the specific record ID to ensure we activate the correct one + template_id = self.id + + # Deactivate all other templates first (including any that might be active) + self.env['expert.login.template'].search([ + ('id', '!=', template_id), + ('active', '=', True) + ]).write({'active': False}) + + # Activate this specific template by ID using browse to ensure we're working with the correct record + self.browse(template_id).write({'active': True}) + + # Update template view active states + self._update_template_views() + + # Return True instead of reload action to avoid potential issues + return True + + def _update_template_views(self): + """Update the active state of template views based on active template""" + try: + # Map template types to their view XML IDs + template_view_map = { + 'modern': 'expert_theme.login_template_modern_page', + 'minimal': 'expert_theme.login_template_minimal_page', + 'corporate': 'expert_theme.login_template_corporate_page', + } + + # Get the active template + active_template = self.search([('active', '=', True)], limit=1) + if not active_template: + # If no active template, deactivate all custom template views + for view_xmlid in template_view_map.values(): + try: + view = self.env.ref(view_xmlid, raise_if_not_found=False) + if view: + view.write({'active': False}) + except Exception: + pass # View doesn't exist, skip + return + + # Deactivate all template views first + for view_xmlid in template_view_map.values(): + try: + view = self.env.ref(view_xmlid, raise_if_not_found=False) + if view: + view.write({'active': False}) + except Exception: + pass # View doesn't exist, skip + + # Activate the view for the active template type (if not default) + if active_template.template_type != 'default': + view_xmlid = template_view_map.get(active_template.template_type) + if view_xmlid: + try: + view = self.env.ref(view_xmlid, raise_if_not_found=False) + if view: + view.write({'active': True}) + except Exception: + pass # View doesn't exist, skip + except Exception: + pass # Silently fail if views don't exist + + def action_duplicate(self): + """Duplicate the current template""" + self.ensure_one() + copy_vals = { + 'name': self.name + ' (Copy)', + 'active': False, # Don't activate the copy + 'sequence': self.sequence + 1, + 'template_type': self.template_type, + 'background_color': self.background_color, + 'description': self.description, + 'is_default': False, + } + return { + 'type': 'ir.actions.act_window', + 'name': 'Login Page Template', + 'res_model': 'expert.login.template', + 'view_mode': 'form', + 'target': 'current', + 'res_id': self.create(copy_vals).id, + 'context': {'default_name': copy_vals['name']}, + } + diff --git a/odex30_base/expert_theme/models/expert_theme_config.py b/odex30_base/expert_theme/models/expert_theme_config.py new file mode 100644 index 0000000..6d0060d --- /dev/null +++ b/odex30_base/expert_theme/models/expert_theme_config.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class ExpertThemeConfig(models.Model): + _name = 'expert.theme.config' + _description = 'Expert Theme Color Configuration' + _rec_name = 'name' + + name = fields.Char(string='Configuration Name', required=True, default='Default Theme') + active = fields.Boolean(string='Active', default=True) + + # Primary Colors + primary_color = fields.Char(string='Primary Color', default='#875A7B', help='Main brand color - affects navbar, primary buttons, and accents') + primary_hover = fields.Char(string='Primary Hover', default='#6B4C6B', help='Hover state for primary elements') + primary_light = fields.Char(string='Primary Light', default='#A67B9B', help='Light version of primary color') + + # Secondary Colors + secondary_color = fields.Char(string='Secondary Color', default='#00A09D', help='Secondary brand color - affects upgrade buttons and secondary elements') + secondary_hover = fields.Char(string='Secondary Hover', default='#008B8A', help='Hover state for secondary elements') + + # Button Colors + btn_primary = fields.Char(string='Primary Button', default='#875A7B', help='Primary buttons (like "Activate" buttons)') + btn_primary_hover = fields.Char(string='Primary Button Hover', default='#6B4C6B', help='Primary button hover state') + btn_secondary = fields.Char(string='Secondary Button', default='#00A09D', help='Secondary buttons (like "Upgrade" buttons)') + btn_secondary_hover = fields.Char(string='Secondary Button Hover', default='#008B8A', help='Secondary button hover state') + btn_light = fields.Char(string='Light Button', default='#F8F9FA', help='Light buttons (like "Learn More" buttons)') + btn_light_hover = fields.Char(string='Light Button Hover', default='#E9ECEF', help='Light button hover state') + btn_light_text = fields.Char(string='Light Button Text', default='#6C757D', help='Light button text color') + + # Background Colors + bg_primary = fields.Char(string='Primary Background', default='#875A7B', help='Navbar background') + bg_secondary = fields.Char(string='Secondary Background', default='#F8F9FA', help='Sidebar background') + bg_content = fields.Char(string='Content Background', default='#FFFFFF', help='Main content background') + bg_gradient_start = fields.Char(string='Gradient Start', default='#f5f7fa', help='Page gradient start color') + bg_gradient_end = fields.Char(string='Gradient End', default='#c3cfe2', help='Page gradient end color') + + # Text Colors + text_primary = fields.Char(string='Primary Text', default='#212529', help='Main text color (dark)') + text_secondary = fields.Char(string='Secondary Text', default='#6C757D', help='Secondary text color (gray)') + text_light = fields.Char(string='Light Text', default='#FFFFFF', help='Light text (for dark backgrounds)') + + # Border Colors + border_color = fields.Char(string='Border Color', default='#DEE2E6', help='Default border color') + border_light = fields.Char(string='Light Border', default='#E9ECEF', help='Light border color') + + # Status Colors + success_color = fields.Char(string='Success Color', default='#28A745', help='Success/Active state (green)') + warning_color = fields.Char(string='Warning Color', default='#FFC107', help='Warning state (yellow)') + danger_color = fields.Char(string='Danger Color', default='#DC3545', help='Danger/Error state (red)') + info_color = fields.Char(string='Info Color', default='#17A2B8', help='Info state (blue)') + + @api.model + def get_active_config(self): + """Get the active configuration or create a default one""" + config = self.search([('active', '=', True)], limit=1) + if not config: + config = self.create({ + 'name': 'Default Theme', + 'active': True + }) + return config + + @api.model + def get_css_variables(self): + """Get CSS variables for the active configuration""" + config = self.get_active_config() + return { + '--expert-primary-color': config.primary_color, + '--expert-primary-hover': config.primary_hover, + '--expert-primary-light': config.primary_light, + '--expert-secondary-color': config.secondary_color, + '--expert-secondary-hover': config.secondary_hover, + '--expert-btn-primary': config.btn_primary, + '--expert-btn-primary-hover': config.btn_primary_hover, + '--expert-btn-secondary': config.btn_secondary, + '--expert-btn-secondary-hover': config.btn_secondary_hover, + '--expert-btn-light': config.btn_light, + '--expert-btn-light-hover': config.btn_light_hover, + '--expert-btn-light-text': config.btn_light_text, + '--expert-bg-primary': config.bg_primary, + '--expert-bg-secondary': config.bg_secondary, + '--expert-bg-content': config.bg_content, + '--expert-bg-gradient-start': config.bg_gradient_start, + '--expert-bg-gradient-end': config.bg_gradient_end, + '--expert-text-primary': config.text_primary, + '--expert-text-secondary': config.text_secondary, + '--expert-text-light': config.text_light, + '--expert-border-color': config.border_color, + '--expert-border-light': config.border_light, + '--expert-success': config.success_color, + '--expert-warning': config.warning_color, + '--expert-danger': config.danger_color, + '--expert-info': config.info_color, + } + + def apply_theme(self): + """Apply the current theme configuration""" + # This method can be called to trigger theme updates + return True diff --git a/odex30_base/expert_theme/security/ir.model.access.csv b/odex30_base/expert_theme/security/ir.model.access.csv new file mode 100644 index 0000000..f0e95b4 --- /dev/null +++ b/odex30_base/expert_theme/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_expert_theme_config_user,expert.theme.config.user,model_expert_theme_config,base.group_user,1,1,1,1 +access_expert_theme_config_admin,expert.theme.config.admin,model_expert_theme_config,base.group_system,1,1,1,1 +access_expert_login_template_user,expert.login.template.user,model_expert_login_template,base.group_user,1,1,1,1 +access_expert_login_template_admin,expert.login.template.admin,model_expert_login_template,base.group_system,1,1,1,1 diff --git a/odex30_base/expert_theme/static/description/index.html b/odex30_base/expert_theme/static/description/index.html new file mode 100644 index 0000000..51a1105 --- /dev/null +++ b/odex30_base/expert_theme/static/description/index.html @@ -0,0 +1,65 @@ + + + + + Expert Theme + + + +
+
🎨
+

Expert Theme

+ +
+

Custom Home Page

+

A clean and modern home page that displays only your installed modules for easy navigation.

+
+ +
+

Simple Interface

+

Minimal design focused on functionality - no clutter, just your modules.

+
+ +
+

Responsive Design

+

Works perfectly on desktop, tablet, and mobile devices.

+
+ +
+

Easy Installation

+

Simply install the module and your new home page will be ready to use.

+
+
+ + diff --git a/odex30_base/expert_theme/static/src/css/expert_login.css b/odex30_base/expert_theme/static/src/css/expert_login.css new file mode 100644 index 0000000..0cc1dc6 --- /dev/null +++ b/odex30_base/expert_theme/static/src/css/expert_login.css @@ -0,0 +1,27 @@ +/* Expert Theme - Login Page Styles */ + +:root { + --expert-login-bg-color: #FFFFFF; +} + +/* Apply background color to login page */ +.expert_login_page { + background-color: var(--expert-login-bg-color) !important; +} + +/* Ensure the background applies to the entire login area */ +body.oe_login_layout, +body { + background-color: var(--expert-login-bg-color) !important; +} + +/* Apply to main login container */ +.oe_login_form { + background-color: var(--expert-login-bg-color) !important; +} + +/* Apply to html element as well */ +html { + background-color: var(--expert-login-bg-color) !important; +} + diff --git a/odex30_base/expert_theme/static/src/css/expert_theme.css b/odex30_base/expert_theme/static/src/css/expert_theme.css new file mode 100644 index 0000000..6f4b521 --- /dev/null +++ b/odex30_base/expert_theme/static/src/css/expert_theme.css @@ -0,0 +1,314 @@ +/* Expert Theme Styles */ + +/* CSS Variables for Customizable Colors */ +:root { + /* Primary Colors */ + --expert-primary-color: #875A7B; /* Main brand color (Odoo purple) */ + --expert-primary-hover: #6B4C6B; /* Primary hover state */ + --expert-primary-light: #A67B9B; /* Light primary */ + + /* Secondary Colors */ + --expert-secondary-color: #00A09D; /* Teal for upgrades */ + --expert-secondary-hover: #008B8A; /* Teal hover */ + + /* Button Colors */ + --expert-btn-primary: #875A7B; /* Primary button (Activate) */ + --expert-btn-primary-hover: #6B4C6B; /* Primary button hover */ + --expert-btn-secondary: #00A09D; /* Secondary button (Upgrade) */ + --expert-btn-secondary-hover: #008B8A; /* Secondary button hover */ + --expert-btn-light: #F8F9FA; /* Light button (Learn More) */ + --expert-btn-light-hover: #E9ECEF; /* Light button hover */ + --expert-btn-light-text: #6C757D; /* Light button text */ + + /* Background Colors */ + --expert-bg-primary: #875A7B; /* Navbar background */ + --expert-bg-secondary: #F8F9FA; /* Sidebar background */ + --expert-bg-content: #FFFFFF; /* Main content background */ + --expert-bg-gradient-start: #f5f7fa; /* Gradient start */ + --expert-bg-gradient-end: #c3cfe2; /* Gradient end */ + + /* Text Colors */ + --expert-text-primary: #212529; /* Primary text */ + --expert-text-secondary: #6C757D; /* Secondary text */ + --expert-text-light: #FFFFFF; /* Light text (on dark backgrounds) */ + + /* Border Colors */ + --expert-border-color: #DEE2E6; /* Default border */ + --expert-border-light: #E9ECEF; /* Light border */ + + /* Status Colors */ + --expert-success: #28A745; /* Success/Active state */ + --expert-warning: #FFC107; /* Warning state */ + --expert-danger: #DC3545; /* Danger/Error state */ + --expert-info: #17A2B8; /* Info state */ +} + +/* Apply gradient to the entire page background */ +body { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; + background: linear-gradient(135deg, var(--expert-bg-gradient-start, #f5f7fa) 0%, var(--expert-bg-gradient-end, #c3cfe2) 100%) !important; + min-height: 100vh !important; + margin: 0 !important; + padding: 0 !important; +} + +/* Ensure the gradient shows on the main content area */ +.o_action_manager, +.o_action_manager .o_view_controller, +.o_action_manager .o_view_controller .o_content { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; + background: linear-gradient(135deg, var(--expert-bg-gradient-start, #f5f7fa) 0%, var(--expert-bg-gradient-end, #c3cfe2) 100%) !important; + min-height: 100vh !important; +} + +/* Override any Odoo background colors that might interfere */ +.o_web_client { + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; + background: linear-gradient(135deg, var(--expert-bg-gradient-start, #f5f7fa) 0%, var(--expert-bg-gradient-end, #c3cfe2) 100%) !important; +} + +.expert-home-container { + padding: 20px; + max-width: 1200px; + margin: 0 auto; + background: transparent; +} + +.expert-theme-controls { + text-align: center; + margin-bottom: 30px; +} + +.expert-theme-btn { + background: linear-gradient(135deg, var(--expert-primary-color) 0%, var(--expert-primary-hover) 100%); + border: none; + color: white; + padding: 12px 24px; + border-radius: 25px; + font-size: 16px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.expert-theme-btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0,0,0,0.15); + background: linear-gradient(135deg, var(--expert-primary-hover) 0%, var(--expert-primary-color) 100%); +} + +.expert-home-header { + text-align: center; + margin-bottom: 40px; + padding: 20px; + background: linear-gradient(135deg, var(--expert-primary-color) 0%, var(--expert-primary-hover) 100%); + color: var(--expert-text-light); + border-radius: 10px; + box-shadow: 0 4px 15px rgba(0,0,0,0.1); +} + +.expert-home-header h1 { + margin: 0 0 10px 0; + font-size: 2.5rem; + font-weight: 300; +} + +.expert-home-header p { + margin: 0; + font-size: 1.1rem; + opacity: 0.9; +} + +.expert-modules-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.expert-module-card { + background: var(--expert-bg-content); + border-radius: 10px; + padding: 20px; + text-align: center; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + transition: all 0.3s ease; + cursor: pointer; + border: 1px solid var(--expert-border-light); +} + +.expert-module-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 25px rgba(0,0,0,0.15); + border-color: var(--expert-primary-color); +} + +.expert-module-icon { + margin-bottom: 15px; +} + +.module-icon { + width: 64px; + height: 64px; + object-fit: contain; + border-radius: 8px; +} + +.expert-module-info h3 { + margin: 0 0 10px 0; + color: var(--expert-text-primary); + font-size: 1.2rem; + font-weight: 500; +} + +.expert-module-info p { + margin: 0; + color: var(--expert-text-secondary); + font-size: 0.9rem; +} + +.expert-loading { + text-align: center; + padding: 40px; +} + +.spinner-border { + display: inline-block; + width: 2rem; + height: 2rem; + vertical-align: text-bottom; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: spinner-border 0.75s linear infinite; +} + +@keyframes spinner-border { + to { transform: rotate(360deg); } +} + +.expert-error, +.expert-no-modules { + text-align: center; + padding: 20px; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert-danger { + color: #721c24; + background-color: #f8d7da; + border-color: #f5c6cb; +} + +.alert-info { + color: #0c5460; + background-color: #d1ecf1; + border-color: #bee5eb; +} + +/* Override Odoo's default colors */ +.o_main_navbar { + background-color: var(--expert-bg-primary) !important; + border-bottom: 1px solid var(--expert-border-color) !important; +} + +.o_main_navbar .o_menu_item > a { + color: var(--expert-text-light) !important; +} + +.o_main_navbar .o_menu_item > a:hover { + background-color: var(--expert-primary-hover) !important; + color: var(--expert-text-light) !important; +} + +/* Button overrides */ +.btn-primary { + background-color: var(--expert-btn-primary) !important; + border-color: var(--expert-btn-primary) !important; +} + +.btn-primary:hover { + background-color: var(--expert-btn-primary-hover) !important; + border-color: var(--expert-btn-primary-hover) !important; +} + +.btn-secondary { + background-color: var(--expert-btn-secondary) !important; + border-color: var(--expert-btn-secondary) !important; +} + +.btn-secondary:hover { + background-color: var(--expert-btn-secondary-hover) !important; + border-color: var(--expert-btn-secondary-hover) !important; +} + +.btn-light { + background-color: var(--expert-btn-light) !important; + border-color: var(--expert-btn-light) !important; + color: var(--expert-btn-light-text) !important; +} + +.btn-light:hover { + background-color: var(--expert-btn-light-hover) !important; + border-color: var(--expert-btn-light-hover) !important; + color: var(--expert-btn-light-text) !important; +} + +/* Sidebar overrides */ +.o_main_navbar .o_menu_sections { + background-color: var(--expert-bg-secondary) !important; +} + +.o_main_navbar .o_menu_sections .o_menu_item > a { + color: var(--expert-text-primary) !important; +} + +.o_main_navbar .o_menu_sections .o_menu_item > a:hover { + background-color: var(--expert-primary-light) !important; + color: var(--expert-text-light) !important; +} + +/* Content area overrides */ +.o_action_manager { + background-color: var(--expert-bg-content) !important; +} + +/* Status colors */ +.text-success { + color: var(--expert-success) !important; +} + +.text-warning { + color: var(--expert-warning) !important; +} + +.text-danger { + color: var(--expert-danger) !important; +} + +.text-info { + color: var(--expert-info) !important; +} + +/* Responsive design */ +@media (max-width: 768px) { + .expert-modules-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 15px; + } + + .expert-home-header h1 { + font-size: 2rem; + } + + .expert-home-container { + padding: 15px; + } +} diff --git a/odex30_base/expert_theme/static/src/css/expert_theme_config.css b/odex30_base/expert_theme/static/src/css/expert_theme_config.css new file mode 100644 index 0000000..96c9168 --- /dev/null +++ b/odex30_base/expert_theme/static/src/css/expert_theme_config.css @@ -0,0 +1,134 @@ +/* Expert Theme Color Configuration */ +/* + * This file contains the color variables for the Expert Theme. + * You can easily customize the colors by changing the values below. + * + * Color Format: Use hex codes (e.g., #FF0000) or CSS color names (e.g., red, blue) + * + * To apply changes: + * 1. Modify the colors below + * 2. Save the file + * 3. Refresh your Odoo page + */ + +:root { + /* ===== PRIMARY COLORS ===== */ + /* Main brand color - affects navbar, primary buttons, and accents */ + --expert-primary-color: #875A7B; /* Default: Odoo Purple */ + --expert-primary-hover: #6B4C6B; /* Hover state for primary elements */ + --expert-primary-light: #A67B9B; /* Light version of primary */ + + /* ===== SECONDARY COLORS ===== */ + /* Secondary brand color - affects upgrade buttons and secondary elements */ + --expert-secondary-color: #00A09D; /* Default: Teal */ + --expert-secondary-hover: #008B8A; /* Hover state for secondary elements */ + + /* ===== BUTTON COLORS ===== */ + /* Primary buttons (like "Activate" buttons) */ + --expert-btn-primary: #875A7B; /* Same as primary color */ + --expert-btn-primary-hover: #6B4C6B; /* Hover state */ + + /* Secondary buttons (like "Upgrade" buttons) */ + --expert-btn-secondary: #00A09D; /* Same as secondary color */ + --expert-btn-secondary-hover: #008B8A; /* Hover state */ + + /* Light buttons (like "Learn More" buttons) */ + --expert-btn-light: #F8F9FA; /* Light gray background */ + --expert-btn-light-hover: #E9ECEF; /* Slightly darker on hover */ + --expert-btn-light-text: #6C757D; /* Dark gray text */ + + /* ===== BACKGROUND COLORS ===== */ + /* Main backgrounds */ + --expert-bg-primary: #875A7B; /* Navbar background */ + --expert-bg-secondary: #F8F9FA; /* Sidebar background */ + --expert-bg-content: #FFFFFF; /* Main content background */ + + /* Gradient background for the page */ + --expert-bg-gradient-start: #f5f7fa; /* Gradient start color */ + --expert-bg-gradient-end: #c3cfe2; /* Gradient end color */ + + /* ===== TEXT COLORS ===== */ + --expert-text-primary: #212529; /* Main text color (dark) */ + --expert-text-secondary: #6C757D; /* Secondary text color (gray) */ + --expert-text-light: #FFFFFF; /* Light text (for dark backgrounds) */ + + /* ===== BORDER COLORS ===== */ + --expert-border-color: #DEE2E6; /* Default border color */ + --expert-border-light: #E9ECEF; /* Light border color */ + + /* ===== STATUS COLORS ===== */ + --expert-success: #28A745; /* Success/Active state (green) */ + --expert-warning: #FFC107; /* Warning state (yellow) */ + --expert-danger: #DC3545; /* Danger/Error state (red) */ + --expert-info: #17A2B8; /* Info state (blue) */ +} + +/* ===== EXAMPLE COLOR SCHEMES ===== */ + +/* + * BLUE THEME EXAMPLE: + * Uncomment the section below to use a blue color scheme + */ +/* +:root { + --expert-primary-color: #007BFF; + --expert-primary-hover: #0056B3; + --expert-primary-light: #66B3FF; + --expert-secondary-color: #6F42C1; + --expert-secondary-hover: #5A32A3; + --expert-btn-primary: #007BFF; + --expert-btn-primary-hover: #0056B3; + --expert-btn-secondary: #6F42C1; + --expert-btn-secondary-hover: #5A32A3; + --expert-bg-primary: #007BFF; + --expert-bg-gradient-start: #E3F2FD; + --expert-bg-gradient-end: #BBDEFB; +} +*/ + +/* + * GREEN THEME EXAMPLE: + * Uncomment the section below to use a green color scheme + */ +/* +:root { + --expert-primary-color: #28A745; + --expert-primary-hover: #1E7E34; + --expert-primary-light: #5CB85C; + --expert-secondary-color: #20C997; + --expert-secondary-hover: #17A2B8; + --expert-btn-primary: #28A745; + --expert-btn-primary-hover: #1E7E34; + --expert-btn-secondary: #20C997; + --expert-btn-secondary-hover: #17A2B8; + --expert-bg-primary: #28A745; + --expert-bg-gradient-start: #E8F5E8; + --expert-bg-gradient-end: #C3E6C3; +} +*/ + +/* + * DARK THEME EXAMPLE: + * Uncomment the section below to use a dark color scheme + */ +/* +:root { + --expert-primary-color: #343A40; + --expert-primary-hover: #23272B; + --expert-primary-light: #495057; + --expert-secondary-color: #6C757D; + --expert-secondary-hover: #545B62; + --expert-btn-primary: #343A40; + --expert-btn-primary-hover: #23272B; + --expert-btn-secondary: #6C757D; + --expert-btn-secondary-hover: #545B62; + --expert-bg-primary: #343A40; + --expert-bg-secondary: #495057; + --expert-bg-content: #F8F9FA; + --expert-bg-gradient-start: #2C3E50; + --expert-bg-gradient-end: #34495E; + --expert-text-primary: #FFFFFF; + --expert-text-secondary: #E9ECEF; + --expert-text-light: #FFFFFF; +} +*/ diff --git a/odex30_base/expert_theme/static/src/css/login_minimal.css b/odex30_base/expert_theme/static/src/css/login_minimal.css new file mode 100644 index 0000000..df4cbc8 --- /dev/null +++ b/odex30_base/expert_theme/static/src/css/login_minimal.css @@ -0,0 +1,269 @@ +/* Expert Theme - Minimal Login Template Styles */ + +.expert-login-minimal { + background: #FFFFFF; + min-height: 100vh; + padding: 20px; +} + +.expert-login-minimal .container-fluid { + height: calc(100vh - 40px); +} + +.expert-login-minimal .row { + height: 100%; + margin: 0; +} + +/* Left Column: Login Form */ +.expert-login-minimal .expert-login-left { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px; + text-align: center; +} + +/* Logo Section */ +.expert-login-minimal .expert-login-logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40px; + width: 100%; +} + +.expert-login-minimal .expert-login-logo .expert-logo-icon { + width: 40px; + height: 40px; + background: #000000; + border-radius: 8px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +.expert-login-minimal .expert-login-logo .expert-logo-icon span { + color: white; + font-weight: bold; + font-size: 20px; +} + +.expert-login-minimal .expert-login-logo .expert-logo-text { + color: #000000; + font-size: 20px; + font-weight: 600; +} + +/* Welcome Section */ +.expert-login-minimal .expert-login-welcome { + width: 100%; + text-align: center; + margin-bottom: 40px; +} + +.expert-login-minimal .expert-login-welcome h1 { + color: #000000; + font-size: 32px; + font-weight: 700; + margin-bottom: 12px; + line-height: 1.2; + text-align: center; +} + +.expert-login-minimal .expert-login-welcome h1 .welcome-emoji { + font-size: 32px; +} + +.expert-login-minimal .expert-login-welcome p { + color: rgba(0, 0, 0, 0.7); + font-size: 16px; + margin-bottom: 0; + text-align: center; +} + +/* Form Section */ +.expert-login-minimal .expert-login-form { + max-width: 450px; + width: 100%; + margin: 0 auto; + background-color: transparent !important; +} + +.expert-login-minimal .expert-login-form .form-group { + margin-bottom: 24px; +} + +.expert-login-minimal .expert-login-form .form-group label { + font-weight: 500; + color: #000000; + display: block; + margin-bottom: 8px; + font-size: 14px; + text-align: start; +} + +.expert-login-minimal .expert-login-form .form-group input[type="text"], +.expert-login-minimal .expert-login-form .form-group input[type="password"] { + background: #FFFFFF !important; + border: 1px solid #000000 !important; + border-radius: 8px; + padding: 12px 16px; + width: 100%; + box-sizing: border-box; + color: #000000 !important; + font-size: 14px; +} + +.expert-login-minimal .expert-login-form .form-group input[type="text"]::placeholder, +.expert-login-minimal .expert-login-form .form-group input[type="password"]::placeholder { + color: rgba(0, 0, 0, 0.5) !important; +} + +.expert-login-minimal .expert-login-form .form-group input[type="text"]:focus, +.expert-login-minimal .expert-login-form .form-group input[type="password"]:focus { + background: #FFFFFF !important; + border-color: #000000 !important; + outline: none; + color: #000000 !important; +} + +.expert-login-minimal .expert-login-form .form-check { + margin-bottom: 32px; +} + +.expert-login-minimal .expert-login-form .form-check label { + color: #000000; + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: 5px; + font-size: 14px; + cursor: pointer; +} + +.expert-login-minimal .expert-login-form .form-check label input[type="checkbox"] { + margin-right: 8px; + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #000000; +} + +.expert-login-minimal .expert-login-form button[type="submit"] { + width: 100%; + background: #E5E5E5; + border: none; + border-radius: 8px; + padding: 14px; + font-weight: 600; + color: #000000; + font-size: 16px; + cursor: pointer; + transition: background 0.2s; +} + +.expert-login-minimal .expert-login-form button[type="submit"]:hover { + background: #D5D5D5; +} + +.expert-login-minimal .expert-login-form button[type="submit"]:active { + background: #CCCCCC; +} + +/* Login Link Section */ +.expert-login-minimal .expert-login-link { + margin-top: 24px; + max-width: 450px; + width: 100%; + text-align: center; +} + +.expert-login-minimal .expert-login-link p { + color: rgba(0, 0, 0, 0.7); + font-size: 14px; + margin: 0; +} + +.expert-login-minimal .expert-login-link p a { + color: #000000; + text-decoration: underline; + font-weight: 500; +} + +.expert-login-minimal .expert-login-link p a:hover { + text-decoration: none; +} + +/* Right Column: Image Section */ +.expert-login-minimal .expert-login-right { + padding: 0; + height: 100%; + overflow: hidden; + border-radius: 0; +} + +.expert-login-minimal .expert-login-right .expert-login-image { + width: 100%; + height: 100%; + background: #FFFFFF; + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.expert-login-minimal .expert-login-right .expert-login-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.expert-login-minimal .expert-minimal-illustration { + width: 600px !important; + height: 600px !important; + object-fit: contain; +} + +/* Alert Messages */ +.expert-login-minimal .alert { + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 20px; + text-align: start; +} + +.expert-login-minimal .alert-danger { + background-color: #F8D7DA; + border: 1px solid #F5C6CB; + color: #721C24; +} + +.expert-login-minimal .alert-success { + background-color: #D4EDDA; + border: 1px solid #C3E6CB; + color: #155724; +} + +/* Responsive adjustments */ +@media (max-width: 991.98px) { + .expert-login-minimal .expert-login-left { + padding: 30px 20px; + } + + .expert-login-minimal .expert-login-welcome h1 { + font-size: 28px; + } + + .expert-login-minimal .expert-login-welcome h1 .welcome-emoji { + font-size: 28px; + } + + .expert-login-minimal .expert-login-welcome p { + font-size: 14px; + } +} + diff --git a/odex30_base/expert_theme/static/src/css/login_modern.css b/odex30_base/expert_theme/static/src/css/login_modern.css new file mode 100644 index 0000000..8bd771b --- /dev/null +++ b/odex30_base/expert_theme/static/src/css/login_modern.css @@ -0,0 +1,234 @@ +/* Expert Theme - Modern Login Template Styles */ + +.expert-login-modern { + background: #19181F !important; + min-height: 100vh; + padding: 20px; +} + +.expert-login-modern .container-fluid { + height: calc(100vh - 40px); +} + +.expert-login-modern .row { + height: 100%; + margin: 0; +} + +/* Left Column: Login Form */ +.expert-login-modern .expert-login-left { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px; + text-align: center; +} + +/* Logo Section */ +.expert-login-modern .expert-login-logo { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 40px; + width: 100%; +} + +.expert-login-modern .expert-login-logo .expert-logo-icon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +.expert-login-modern .expert-login-logo .expert-logo-icon span { + color: white; + font-weight: bold; + font-size: 20px; +} + +.expert-login-modern .expert-login-logo .expert-logo-text { + color: white; + font-size: 20px; + font-weight: 600; +} + +/* Welcome Section */ +.expert-login-modern .expert-login-welcome { + width: 100%; + text-align: center; +} + +.expert-login-modern .expert-login-welcome h1 { + color: white; + font-size: 36px; + font-weight: 700; + margin-bottom: 12px; + line-height: 1.2; + text-align: center; +} + +.expert-login-modern .expert-login-welcome h1 .welcome-emoji { + font-size: 36px; +} + +.expert-login-modern .expert-login-welcome p { + color: rgba(255, 255, 255, 0.7); + font-size: 16px; + margin-bottom: 40px; + text-align: center; +} + +/* Form Section */ +.expert-login-modern .expert-login-form { + max-width: 450px; + width: 100%; + margin: 0 auto; + background-color: transparent !important; +} + +.expert-login-modern .expert-login-form .form-group { + margin-bottom: 24px; +} + +.expert-login-modern .expert-login-form .form-group label { + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + display: block; + margin-bottom: 8px; + font-size: 14px; + text-align: start; +} + +.expert-login-modern .expert-login-form .form-group input[type="text"], +.expert-login-modern .expert-login-form .form-group input[type="password"] { + background: rgba(255, 255, 255, 0.1) !important; + border: 1px solid rgba(255, 255, 255, 0.2) !important; + border-radius: 8px; + padding: 12px 16px; + width: 100%; + box-sizing: border-box; + color: white !important; + font-size: 14px; +} + +.expert-login-modern .expert-login-form .form-group input[type="text"]::placeholder, +.expert-login-modern .expert-login-form .form-group input[type="password"]::placeholder { + color: rgba(255, 255, 255, 0.5) !important; +} + +.expert-login-modern .expert-login-form .form-group input[type="text"]:focus, +.expert-login-modern .expert-login-form .form-group input[type="password"]:focus { + background: rgba(255, 255, 255, 0.15) !important; + border-color: rgba(255, 255, 255, 0.4) !important; + outline: none; + color: white !important; +} + +.expert-login-modern .expert-login-form .form-check { + margin-bottom: 32px; +} + +.expert-login-modern .expert-login-form .form-check label { + color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: flex-start; + font-size: 14px; + cursor: pointer; +} + +.expert-login-modern .expert-login-form .form-check label input[type="checkbox"] { + margin-right: 8px; + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #667eea; +} + +.expert-login-modern .expert-login-form button[type="submit"] { + width: 100%; + background: #764ba2; + border: none !important; + border-radius: 8px; + padding: 14px; + font-weight: 600; + color: white; + font-size: 16px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; +} + +.expert-login-modern .expert-login-form button[type="submit"]:hover { + transform: translateY(-2px); +} + +/* Login Link Section */ +.expert-login-modern .expert-login-link { + margin-top: 24px; + max-width: 450px; + width: 100%; + text-align: center; +} + +.expert-login-modern .expert-login-link p { + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + margin: 0; +} + +.expert-login-modern .expert-login-link p a { + color: #667eea; + text-decoration: none; + font-weight: 500; +} + +.expert-login-modern .expert-login-link p a:hover { + text-decoration: underline; +} + +/* Right Column: Image Section */ +.expert-login-modern .expert-login-right { + padding: 0; + height: 100%; + overflow: hidden; + border-radius: 20px; +} + +.expert-login-modern .expert-login-right .expert-login-image { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.expert-login-modern .expert-login-right .expert-login-image img { + width: 100% !important; + height: 100% !important; + object-fit: cover !important; +} + +/* Responsive adjustments */ +@media (max-width: 991.98px) { + .expert-login-modern .expert-login-left { + padding: 30px 20px; + } + + .expert-login-modern .expert-login-welcome h1 { + font-size: 28px; + } + + .expert-login-modern .expert-login-welcome h1 .welcome-emoji { + font-size: 28px; + } + + .expert-login-modern .expert-login-welcome p { + font-size: 14px; + } +} + diff --git a/odex30_base/expert_theme/static/src/img/minimal-login-img.png b/odex30_base/expert_theme/static/src/img/minimal-login-img.png new file mode 100644 index 0000000..bca7cd3 Binary files /dev/null and b/odex30_base/expert_theme/static/src/img/minimal-login-img.png differ diff --git a/odex30_base/expert_theme/static/src/img/modern-template-bg.png b/odex30_base/expert_theme/static/src/img/modern-template-bg.png new file mode 100644 index 0000000..eb6c4f9 Binary files /dev/null and b/odex30_base/expert_theme/static/src/img/modern-template-bg.png differ diff --git a/odex30_base/expert_theme/static/src/js/expert_home.js b/odex30_base/expert_theme/static/src/js/expert_home.js new file mode 100644 index 0000000..844fa5f --- /dev/null +++ b/odex30_base/expert_theme/static/src/js/expert_home.js @@ -0,0 +1,134 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Component, onMounted, useState } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +export class ExpertHome extends Component { + setup() { + this.state = useState({ + modules: [], + loading: true, + error: null + }); + + onMounted(() => { + this.loadModules(); + }); + } + + async loadModules() { + try { + this.state.loading = true; + + // Try to get modules dynamically from the controller + try { + const response = await fetch('/expert_theme/get_installed_modules_http'); + + // Check if response is HTML (error page) instead of JSON + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + throw new Error(`Server returned HTML (status: ${response.status}). Check if controller is working.`); + } + + const result = await response.json(); + + if (result.success && result.modules && result.modules.length > 0) { + this.state.modules = result.modules; + return; + } + } catch (httpError) { + // Don't set error state, try fallback instead + } + + // Fallback: Try to get modules from DOM + try { + // Try multiple selectors to find the sidebar menus + let sidebarMenus = document.querySelectorAll('.o_main_navbar .o_menu_item a'); + if (sidebarMenus.length === 0) { + sidebarMenus = document.querySelectorAll('.o_menu_item a'); + } + if (sidebarMenus.length === 0) { + sidebarMenus = document.querySelectorAll('nav a'); + } + if (sidebarMenus.length === 0) { + sidebarMenus = document.querySelectorAll('a[href*="/web#"]'); + } + + const modules = []; + + sidebarMenus.forEach((link, index) => { + const name = link.textContent.trim(); + const href = link.href; + + if (name && href && + !name.includes('Expert Home') && + !name.includes('Dashboard') && + name.length > 0 && + href.includes('/web#')) { + modules.push({ + id: index + 1, + name: name, + web_icon: 'base,static/description/icon.png', + action: true, + url: href + }); + } + }); + + if (modules.length > 0) { + this.state.modules = modules; + return; + } + } catch (domError) { + // DOM fallback failed + } + + // Fallback: Show empty state with message + this.state.modules = []; + + } catch (error) { + this.state.error = error.message; + } finally { + this.state.loading = false; + } + } + + getModuleIcon(webIcon) { + // Custom icon mapping for modules + const customIcons = { + 'Discuss': '/web/static/src/img/discuss-icon.png', + 'To-do': '/web/static/src/img/todo-icon.png', + 'Calendar': '/web/static/src/img/calendar-icon.png', + 'Contacts': '/web/static/src/img/contacts-icon.png', + 'CRM': '/web/static/src/img/crm-icon.png', + 'Project': '/web/static/src/img/project-icon.png', + 'Website': '/web/static/src/img/website-icon.png', + 'Email Marketing': '/web/static/src/img/email-icon.png', + 'Surveys': '/web/static/src/img/survey-icon.png', + 'Employees': '/web/static/src/img/employees-icon.png', + 'Link Tracker': '/web/static/src/img/link-icon.png', + 'Apps': '/web/static/src/img/apps-icon.png', + 'Settings': '/web/static/src/img/settings-icon.png' + }; + + // Try to get custom icon first, fallback to default + return customIcons[webIcon] || '/web/static/img/placeholder.png'; + } + + openModule(module) { + if (module.url) { + window.location.href = module.url; + } + } + + openThemeColors() { + // Navigate to the theme colors configuration + window.location.href = '/web#action=expert_theme.action_expert_theme_config'; + } +} + +ExpertHome.template = "expert_theme.ExpertHomeTemplate"; + +// Register the component as a client action +registry.category("actions").add("expert_home", ExpertHome); diff --git a/odex30_base/expert_theme/static/src/js/expert_login_template.js b/odex30_base/expert_theme/static/src/js/expert_login_template.js new file mode 100644 index 0000000..f3b3a9a --- /dev/null +++ b/odex30_base/expert_theme/static/src/js/expert_login_template.js @@ -0,0 +1,54 @@ +/** @odoo-module **/ + +// Load login template styles dynamically +// Temporarily disabled to fix login page access +/* +(function() { + function applyLoginStyles() { + fetch('/expert_theme/get_login_template_styles') + .then(response => response.json()) + .then(data => { + if (data.success && data.styles) { + const root = document.documentElement; + if (data.styles.background_color) { + // Set CSS variable + root.style.setProperty('--expert-login-bg-color', data.styles.background_color); + + // Apply background color to body (main background) + if (document.body) { + document.body.style.backgroundColor = data.styles.background_color; + } + + // Apply to login container if exists + const loginContainer = document.querySelector('.oe_login_form') || + document.querySelector('.o_database_list') || + document.querySelector('main') || + document.body; + if (loginContainer) { + loginContainer.style.backgroundColor = data.styles.background_color; + } + + // Also apply to html element + if (document.documentElement) { + document.documentElement.style.backgroundColor = data.styles.background_color; + } + } + } + }) + .catch(error => { + console.warn('Could not load login template styles:', error); + }); + } + + // Apply styles when DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', applyLoginStyles); + } else { + applyLoginStyles(); + } + + // Also apply after a short delay to ensure everything is loaded + setTimeout(applyLoginStyles, 100); +})(); +*/ + diff --git a/odex30_base/expert_theme/static/src/js/expert_login_template_list.js b/odex30_base/expert_theme/static/src/js/expert_login_template_list.js new file mode 100644 index 0000000..527e976 --- /dev/null +++ b/odex30_base/expert_theme/static/src/js/expert_login_template_list.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ + +console.log('expert_login_template_list.js loaded'); + +import { patch } from "@web/core/utils/patch"; +import { ListBooleanToggleField } from "@web/views/fields/boolean_toggle/list_boolean_toggle_field"; + +// Patch ListBooleanToggleField to reload after toggle +patch(ListBooleanToggleField.prototype, { + async onClick() { + const isActiveField = this.props.name === 'active' && this.props.record.resModel === 'expert.login.template'; + console.log('ListBooleanToggleField onClick - isActiveField:', isActiveField, 'name:', this.props.name, 'resModel:', this.props.record.resModel); + + // Call parent method first + await super.onClick(...arguments); + + // If this is the active field, reload the page + if (isActiveField) { + console.log('Active field toggled! Reloading page in 500ms...'); + setTimeout(() => { + console.log('Reloading now!'); + window.location.reload(); + }, 500); + } + }, +}); diff --git a/odex30_base/expert_theme/static/src/js/expert_theme_config.js b/odex30_base/expert_theme/static/src/js/expert_theme_config.js new file mode 100644 index 0000000..2508de0 --- /dev/null +++ b/odex30_base/expert_theme/static/src/js/expert_theme_config.js @@ -0,0 +1,120 @@ +/** @odoo-module **/ + +import { FormController } from "@web/views/form/form_controller"; +import { registry } from "@web/core/registry"; + +export class ExpertThemeConfigController extends FormController { + setup() { + super.setup(); + + // Listen for field changes to apply colors in real-time + this.env.bus.addEventListener('FIELD_CHANGED', this.onFieldChanged.bind(this)); + } + + onFieldChanged(event) { + // Check if the changed field is a color field + if (event.detail.fieldName && event.detail.fieldName.includes('color')) { + this.applyThemePreview(); + } + } + + async applyThemePreview() { + try { + // Get current form values + const formData = this.model.root; + const record = formData.data; + + // Create CSS variables object from form data + const cssVariables = { + '--expert-primary-color': record.primary_color || '#875A7B', + '--expert-primary-hover': record.primary_hover || '#6B4C6B', + '--expert-primary-light': record.primary_light || '#A67B9B', + '--expert-secondary-color': record.secondary_color || '#00A09D', + '--expert-secondary-hover': record.secondary_hover || '#008B8A', + '--expert-btn-primary': record.btn_primary || '#875A7B', + '--expert-btn-primary-hover': record.btn_primary_hover || '#6B4C6B', + '--expert-btn-secondary': record.btn_secondary || '#00A09D', + '--expert-btn-secondary-hover': record.btn_secondary_hover || '#008B8A', + '--expert-btn-light': record.btn_light || '#F8F9FA', + '--expert-btn-light-hover': record.btn_light_hover || '#E9ECEF', + '--expert-btn-light-text': record.btn_light_text || '#6C757D', + '--expert-bg-primary': record.bg_primary || '#875A7B', + '--expert-bg-secondary': record.bg_secondary || '#F8F9FA', + '--expert-bg-content': record.bg_content || '#FFFFFF', + '--expert-bg-gradient-start': record.bg_gradient_start || '#f5f7fa', + '--expert-bg-gradient-end': record.bg_gradient_end || '#c3cfe2', + '--expert-text-primary': record.text_primary || '#212529', + '--expert-text-secondary': record.text_secondary || '#6C757D', + '--expert-text-light': record.text_light || '#FFFFFF', + '--expert-border-color': record.border_color || '#DEE2E6', + '--expert-border-light': record.border_light || '#E9ECEF', + '--expert-success': record.success_color || '#28A745', + '--expert-warning': record.warning_color || '#FFC107', + '--expert-danger': record.danger_color || '#DC3545', + '--expert-info': record.info_color || '#17A2B8', + }; + + // Apply the CSS variables + this.applyCSSVariables(cssVariables); + + } catch (error) { + console.warn('Error applying theme preview:', error); + } + } + + applyCSSVariables(cssVariables) { + const root = document.documentElement; + + // Apply each CSS variable to the root element + Object.entries(cssVariables).forEach(([property, value]) => { + if (value) { + root.style.setProperty(property, value); + } + }); + } + + async save() { + const result = await super.save(); + + // After saving, apply the theme + if (result) { + await this.applyTheme(); + } + + return result; + } + + async applyTheme() { + try { + const response = await fetch('/expert_theme/apply_theme', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const result = await response.json(); + + if (result.success) { + // Show success message + this.env.services.notification.add('Theme applied successfully!', { + type: 'success', + }); + } else { + this.env.services.notification.add('Error applying theme: ' + result.error, { + type: 'danger', + }); + } + } catch (error) { + this.env.services.notification.add('Error applying theme: ' + error.message, { + type: 'danger', + }); + } + } +} + +// Register the controller for the expert.theme.config model +registry.category("views").add("expert_theme_config_form", { + ...registry.category("views").get("form"), + Controller: ExpertThemeConfigController, +}); diff --git a/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js new file mode 100644 index 0000000..d3bffc1 --- /dev/null +++ b/odex30_base/expert_theme/static/src/js/expert_theme_dynamic.js @@ -0,0 +1,80 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { Component, onMounted, onWillStart } from "@odoo/owl"; + +export class ExpertThemeDynamic extends Component { + setup() { + onWillStart(() => { + this.loadThemeColors(); + }); + + onMounted(() => { + this.loadThemeColors(); + }); + } + + async loadThemeColors() { + try { + const response = await fetch('/expert_theme/get_css_variables'); + const result = await response.json(); + + if (result.success && result.css_variables) { + this.applyCSSVariables(result.css_variables); + } + } catch (error) { + console.warn('Could not load dynamic theme colors:', error); + } + } + + applyCSSVariables(cssVariables) { + const root = document.documentElement; + + // Apply each CSS variable to the root element + Object.entries(cssVariables).forEach(([property, value]) => { + if (value) { + root.style.setProperty(property, value); + } + }); + } +} + +// Register the component to run on every page load +registry.category("services").add("expert_theme_dynamic", { + start() { + const component = new ExpertThemeDynamic(); + component.loadThemeColors(); + + // Also load colors when the page is refreshed or navigated + window.addEventListener('load', () => { + component.loadThemeColors(); + }); + + return component; + } +}); + +// Also create a standalone function that can be called manually +window.expertThemeApplyColors = async function() { + try { + const response = await fetch('/expert_theme/get_css_variables'); + const result = await response.json(); + + if (result.success && result.css_variables) { + const root = document.documentElement; + Object.entries(result.css_variables).forEach(([property, value]) => { + if (value) { + root.style.setProperty(property, value); + } + }); + console.log('Expert Theme colors applied successfully!'); + } + } catch (error) { + console.error('Error applying Expert Theme colors:', error); + } +}; + +// Auto-apply colors when the script loads +document.addEventListener('DOMContentLoaded', function() { + window.expertThemeApplyColors(); +}); diff --git a/odex30_base/expert_theme/static/src/scss/login_modern.scss b/odex30_base/expert_theme/static/src/scss/login_modern.scss new file mode 100644 index 0000000..976f9fd --- /dev/null +++ b/odex30_base/expert_theme/static/src/scss/login_modern.scss @@ -0,0 +1,258 @@ +/* Expert Theme - Modern Login Template Styles */ + +.expert-login-modern { + background: #19181F; + min-height: 100vh; + padding: 20px; + + .container-fluid { + height: calc(100vh - 40px); + } + + .row { + height: 100%; + margin: 0; + } + + // Left Column: Login Form + .expert-login-left { + display: flex; + flex-direction: column; + justify-content: center; + padding: 40px; + } + + // Logo Section + .expert-login-logo { + display: flex; + align-items: center; + margin-bottom: 40px; + + .expert-logo-icon { + width: 40px; + height: 40px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + margin-right: 12px; + display: flex; + align-items: center; + justify-content: center; + + span { + color: white; + font-weight: bold; + font-size: 20px; + } + } + + .expert-logo-text { + color: white; + font-size: 20px; + font-weight: 600; + } + } + + // Welcome Section + .expert-login-welcome { + h1 { + color: white; + font-size: 36px; + font-weight: 700; + margin-bottom: 12px; + line-height: 1.2; + + .welcome-emoji { + font-size: 36px; + } + } + + p { + color: rgba(255, 255, 255, 0.7); + font-size: 16px; + margin-bottom: 40px; + } + } + + // Form Section + .expert-login-form { + max-width: 450px; + + .form-group { + margin-bottom: 24px; + + label { + font-weight: 500; + color: rgba(255, 255, 255, 0.9); + display: block; + margin-bottom: 8px; + font-size: 14px; + } + + input[type="text"], + input[type="password"] { + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 12px 16px; + width: 100%; + box-sizing: border-box; + color: white; + font-size: 14px; + + &::placeholder { + color: rgba(255, 255, 255, 0.5); + } + + &:focus { + background: rgba(255, 255, 255, 0.15) !important; + border-color: rgba(255, 255, 255, 0.4) !important; + outline: none; + color: white; + } + } + } + + .form-check { + margin-bottom: 32px; + + label { + color: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + font-size: 14px; + cursor: pointer; + + input[type="checkbox"] { + margin-right: 8px; + width: 18px; + height: 18px; + cursor: pointer; + accent-color: #667eea; + } + } + } + + button[type="submit"] { + width: 100%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border: none; + border-radius: 8px; + padding: 14px; + font-weight: 600; + color: white; + font-size: 16px; + cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4); + } + + &:active { + transform: translateY(0); + } + } + } + + // Login Link Section + .expert-login-link { + margin-top: 24px; + max-width: 450px; + + p { + color: rgba(255, 255, 255, 0.7); + font-size: 14px; + margin: 0; + + a { + color: #667eea; + text-decoration: none; + font-weight: 500; + + &:hover { + text-decoration: underline; + } + } + } + } + + // Right Column: Image Section + .expert-login-right { + padding: 0; + height: 100%; + overflow: hidden; + border-radius: 20px 0 0 20px; + + .expert-login-image { + width: 100%; + height: 100%; + background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 25%, #667eea 75%, #764ba2 100%); + display: flex; + align-items: center; + justify-content: center; + position: relative; + + .expert-spiral-decoration { + width: 400px; + height: 400px; + position: relative; + opacity: 0.9; + + .spiral-circle { + position: absolute; + border-radius: 50%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + + &.circle-1 { + width: 100%; + height: 100%; + transform: rotate(45deg); + } + + &.circle-2 { + width: 80%; + height: 80%; + top: 10%; + left: 10%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.08) 0%, transparent 70%); + transform: rotate(-45deg); + } + + &.circle-3 { + width: 60%; + height: 60%; + top: 20%; + left: 20%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.06) 0%, transparent 70%); + transform: rotate(30deg); + } + } + } + } + } +} + +// Responsive adjustments +@media (max-width: 991.98px) { + .expert-login-modern { + .expert-login-left { + padding: 30px 20px; + } + + .expert-login-welcome { + h1 { + font-size: 28px; + + .welcome-emoji { + font-size: 28px; + } + } + + p { + font-size: 14px; + } + } + } +} + diff --git a/odex30_base/expert_theme/static/src/xml/expert_home.xml b/odex30_base/expert_theme/static/src/xml/expert_home.xml new file mode 100644 index 0000000..42b98f8 --- /dev/null +++ b/odex30_base/expert_theme/static/src/xml/expert_home.xml @@ -0,0 +1,58 @@ + + + +
+ + +
+ +
+ +
+
+ Loading... +
+

Loading your modules...

+
+ +
+
+ Error: +
+
+ +
+
+
+ +
+
+

+

Click to open

+

+
+
+ +
+
+ No installed modules found. +

This could mean:

+
    +
  • No modules are currently installed
  • +
  • The module detection is not working properly
  • +
  • Check the browser console for debugging information
  • +
+

Try refreshing the page or check if modules are properly installed in Odoo.

+
+
+
+
+
diff --git a/odex30_base/expert_theme/views/expert_home_views.xml b/odex30_base/expert_theme/views/expert_home_views.xml new file mode 100644 index 0000000..71944e0 --- /dev/null +++ b/odex30_base/expert_theme/views/expert_home_views.xml @@ -0,0 +1,32 @@ + + + + + + expert.theme.home + ir.ui.menu + qweb + + +
+
+

Welcome to Expert Theme

+

Your installed modules are listed below

+
+
+ +
+
+
+
+
+ + + + Expert Home + expert_home + current + + +
+
diff --git a/odex30_base/expert_theme/views/expert_login_template_views.xml b/odex30_base/expert_theme/views/expert_login_template_views.xml new file mode 100644 index 0000000..11c775e --- /dev/null +++ b/odex30_base/expert_theme/views/expert_login_template_views.xml @@ -0,0 +1,233 @@ + + + + + + expert.login.template.form + expert.login.template + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + expert.login.template.tree + expert.login.template + + + + + + + + + +