From 8c54caad84f0e7678cee0bb055405e318ca0bd4c Mon Sep 17 00:00:00 2001 From: maltayyar2 Date: Sat, 27 Dec 2025 00:41:36 +0300 Subject: [PATCH] [I18N] other: automatic update Auto-generated commit based on local changes. --- .../system_dashboard_classic/__init__.py | 3 +- .../system_dashboard_classic/__manifest__.py | 83 +- .../controllers/__init__.py | 3 - .../controllers/controllers.py | 56 - .../data/dashboard_data.xml | 55 - .../system_dashboard_classic/demo/demo.xml | 27 +- .../system_dashboard_classic/i18n/ar_001.po | 174 +- .../models/__init__.py | 3 +- .../system_dashboard_classic/models/config.py | 240 +- .../system_dashboard_classic/models/models.py | 617 ++- .../models/res_users.py | 15 + .../security/secuirty.xml | 26 - .../static/description/icon.png | Bin 30189 -> 21683 bytes .../static/src/icons/login.svg | 15 + .../static/src/img/Attendence.png | Bin 69726 -> 0 bytes .../static/src/img/icon.png | Bin 24901 -> 0 bytes .../static/src/js/genius_enhancements.js | 351 ++ .../static/src/js/pluscharts.js | 2 +- .../static/src/js/system_dashboard.js | 609 --- .../src/js/system_dashboard_self_service.js | 1579 +++++++- .../static/src/js/utils.js | 151 - .../static/src/lib/confetti.min.js | 8 + .../static/src/scss/cards.scss | 396 +- .../static/src/scss/cards2.scss | 155 - .../static/src/scss/core.scss | 3 +- .../static/src/scss/genius-enhancements.scss | 3466 +++++++++++++++++ .../static/src/scss/rtl-cards.scss | 207 +- .../static/src/scss/variables.scss | 68 +- .../static/src/xml/self_service_dashboard.xml | 74 +- .../static/src/xml/system_dashboard.xml | 22 +- .../system_dashboard_classic/views/config.xml | 200 +- .../views/dashboard_settings.xml | 223 ++ .../views/system_dashboard.xml | 55 +- ...te_First Quotation Quiz_Mitchell Admin.pdf | Bin 0 -> 20690 bytes odex25_base/tour_genius/__init__.py | 4 + odex25_base/tour_genius/__manifest__.py | 75 + .../tour_genius/controllers/__init__.py | 3 + odex25_base/tour_genius/controllers/main.py | 91 + odex25_base/tour_genius/data/cron_data.xml | 48 + odex25_base/tour_genius/data/demo_data.xml | 50 + .../tour_genius/data/demo_plans_steps.xml | 380 ++ .../tour_genius/data/demo_tours_account.xml | 132 + .../tour_genius/data/demo_tours_hr.xml | 134 + .../tour_genius/data/demo_tours_purchase.xml | 127 + .../tour_genius/data/demo_tours_sales.xml | 150 + .../tour_genius/data/demo_tours_stock.xml | 127 + odex25_base/tour_genius/models/__init__.py | 30 + odex25_base/tour_genius/models/leaderboard.py | 339 ++ odex25_base/tour_genius/models/quiz.py | 1616 ++++++++ odex25_base/tour_genius/models/reminder.py | 278 ++ odex25_base/tour_genius/models/res_users.py | 67 + odex25_base/tour_genius/models/tour.py | 949 +++++ odex25_base/tour_genius/models/tour_mixin.py | 70 + odex25_base/tour_genius/models/tour_plan.py | 155 + .../tour_genius/models/tour_progress.py | 183 + odex25_base/tour_genius/models/tour_step.py | 135 + odex25_base/tour_genius/models/tour_tag.py | 38 + .../tour_genius/security/ir.model.access.csv | 26 + odex25_base/tour_genius/security/security.xml | 155 + .../tour_genius/static/description/icon.png | Bin 0 -> 514068 bytes .../static/src/css/tour_genius.css | 3117 +++++++++++++++ .../tour_genius/static/src/js/dashboard.js | 453 +++ .../static/src/js/genius_celebration.js | 156 + .../static/src/js/genius_quiz_popup.js | 866 ++++ .../tour_genius/static/src/js/genius_tip.js | 358 ++ .../static/src/js/recorder_panel.js | 900 +++++ .../static/src/js/smart_systray.js | 263 ++ .../static/src/js/tour_client_action.js | 144 + .../tour_genius/static/src/js/tour_loader.js | 631 +++ .../static/src/xml/dashboard_template.xml | 235 ++ .../static/src/xml/genius_celebration.xml | 130 + .../static/src/xml/genius_quiz_popup.xml | 325 ++ .../tour_genius/static/src/xml/genius_tip.xml | 41 + .../static/src/xml/recorder_template.xml | 125 + .../static/src/xml/systray_template.xml | 98 + odex25_base/tour_genius/tests/__init__.py | 6 + .../tour_genius/tests/test_advanced.py | 155 + odex25_base/tour_genius/tests/test_quiz.py | 304 ++ .../tour_genius/tests/test_security.py | 119 + .../tour_genius/tests/test_training.py | 198 + .../tour_genius/views/analytics_views.xml | 108 + odex25_base/tour_genius/views/assets.xml | 33 + .../tour_genius/views/dashboard_views.xml | 18 + .../tour_genius/views/leaderboard_views.xml | 63 + odex25_base/tour_genius/views/menu.xml | 30 + odex25_base/tour_genius/views/plan_views.xml | 191 + .../tour_genius/views/progress_views.xml | 117 + odex25_base/tour_genius/views/quiz_views.xml | 355 ++ .../tour_genius/views/reminder_views.xml | 82 + odex25_base/tour_genius/views/step_views.xml | 122 + odex25_base/tour_genius/views/topic_views.xml | 173 + 91 files changed, 22582 insertions(+), 1582 deletions(-) delete mode 100644 odex25_base/system_dashboard_classic/controllers/__init__.py delete mode 100644 odex25_base/system_dashboard_classic/controllers/controllers.py delete mode 100644 odex25_base/system_dashboard_classic/data/dashboard_data.xml create mode 100644 odex25_base/system_dashboard_classic/models/res_users.py delete mode 100644 odex25_base/system_dashboard_classic/security/secuirty.xml create mode 100644 odex25_base/system_dashboard_classic/static/src/icons/login.svg delete mode 100644 odex25_base/system_dashboard_classic/static/src/img/Attendence.png delete mode 100644 odex25_base/system_dashboard_classic/static/src/img/icon.png create mode 100644 odex25_base/system_dashboard_classic/static/src/js/genius_enhancements.js delete mode 100644 odex25_base/system_dashboard_classic/static/src/js/system_dashboard.js delete mode 100644 odex25_base/system_dashboard_classic/static/src/js/utils.js create mode 100644 odex25_base/system_dashboard_classic/static/src/lib/confetti.min.js delete mode 100644 odex25_base/system_dashboard_classic/static/src/scss/cards2.scss create mode 100644 odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss create mode 100644 odex25_base/system_dashboard_classic/views/dashboard_settings.xml create mode 100644 odex25_base/tour_genius/Certificate_First Quotation Quiz_Mitchell Admin.pdf create mode 100644 odex25_base/tour_genius/__init__.py create mode 100644 odex25_base/tour_genius/__manifest__.py create mode 100644 odex25_base/tour_genius/controllers/__init__.py create mode 100644 odex25_base/tour_genius/controllers/main.py create mode 100644 odex25_base/tour_genius/data/cron_data.xml create mode 100644 odex25_base/tour_genius/data/demo_data.xml create mode 100644 odex25_base/tour_genius/data/demo_plans_steps.xml create mode 100644 odex25_base/tour_genius/data/demo_tours_account.xml create mode 100644 odex25_base/tour_genius/data/demo_tours_hr.xml create mode 100644 odex25_base/tour_genius/data/demo_tours_purchase.xml create mode 100644 odex25_base/tour_genius/data/demo_tours_sales.xml create mode 100644 odex25_base/tour_genius/data/demo_tours_stock.xml create mode 100644 odex25_base/tour_genius/models/__init__.py create mode 100644 odex25_base/tour_genius/models/leaderboard.py create mode 100644 odex25_base/tour_genius/models/quiz.py create mode 100644 odex25_base/tour_genius/models/reminder.py create mode 100644 odex25_base/tour_genius/models/res_users.py create mode 100644 odex25_base/tour_genius/models/tour.py create mode 100644 odex25_base/tour_genius/models/tour_mixin.py create mode 100644 odex25_base/tour_genius/models/tour_plan.py create mode 100644 odex25_base/tour_genius/models/tour_progress.py create mode 100644 odex25_base/tour_genius/models/tour_step.py create mode 100644 odex25_base/tour_genius/models/tour_tag.py create mode 100644 odex25_base/tour_genius/security/ir.model.access.csv create mode 100644 odex25_base/tour_genius/security/security.xml create mode 100644 odex25_base/tour_genius/static/description/icon.png create mode 100644 odex25_base/tour_genius/static/src/css/tour_genius.css create mode 100644 odex25_base/tour_genius/static/src/js/dashboard.js create mode 100644 odex25_base/tour_genius/static/src/js/genius_celebration.js create mode 100644 odex25_base/tour_genius/static/src/js/genius_quiz_popup.js create mode 100644 odex25_base/tour_genius/static/src/js/genius_tip.js create mode 100644 odex25_base/tour_genius/static/src/js/recorder_panel.js create mode 100644 odex25_base/tour_genius/static/src/js/smart_systray.js create mode 100644 odex25_base/tour_genius/static/src/js/tour_client_action.js create mode 100644 odex25_base/tour_genius/static/src/js/tour_loader.js create mode 100644 odex25_base/tour_genius/static/src/xml/dashboard_template.xml create mode 100644 odex25_base/tour_genius/static/src/xml/genius_celebration.xml create mode 100644 odex25_base/tour_genius/static/src/xml/genius_quiz_popup.xml create mode 100644 odex25_base/tour_genius/static/src/xml/genius_tip.xml create mode 100644 odex25_base/tour_genius/static/src/xml/recorder_template.xml create mode 100644 odex25_base/tour_genius/static/src/xml/systray_template.xml create mode 100644 odex25_base/tour_genius/tests/__init__.py create mode 100644 odex25_base/tour_genius/tests/test_advanced.py create mode 100644 odex25_base/tour_genius/tests/test_quiz.py create mode 100644 odex25_base/tour_genius/tests/test_security.py create mode 100644 odex25_base/tour_genius/tests/test_training.py create mode 100644 odex25_base/tour_genius/views/analytics_views.xml create mode 100644 odex25_base/tour_genius/views/assets.xml create mode 100644 odex25_base/tour_genius/views/dashboard_views.xml create mode 100644 odex25_base/tour_genius/views/leaderboard_views.xml create mode 100644 odex25_base/tour_genius/views/menu.xml create mode 100644 odex25_base/tour_genius/views/plan_views.xml create mode 100644 odex25_base/tour_genius/views/progress_views.xml create mode 100644 odex25_base/tour_genius/views/quiz_views.xml create mode 100644 odex25_base/tour_genius/views/reminder_views.xml create mode 100644 odex25_base/tour_genius/views/step_views.xml create mode 100644 odex25_base/tour_genius/views/topic_views.xml diff --git a/odex25_base/system_dashboard_classic/__init__.py b/odex25_base/system_dashboard_classic/__init__.py index 3cbd8bc15..5305644df 100644 --- a/odex25_base/system_dashboard_classic/__init__.py +++ b/odex25_base/system_dashboard_classic/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- -#from . import controllers -from . import models \ No newline at end of file +from . import models \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/__manifest__.py b/odex25_base/system_dashboard_classic/__manifest__.py index d9b04a3c7..479899910 100644 --- a/odex25_base/system_dashboard_classic/__manifest__.py +++ b/odex25_base/system_dashboard_classic/__manifest__.py @@ -1,34 +1,77 @@ # -*- coding: utf-8 -*- { - 'name': "System Board", - - 'summary': """ - This module allows you to configure dashboard for different states or stages for specific model.""", - + 'name': "System Dashboard", + 'summary': "Configurable dashboard for employee self-service and manager approvals", 'description': """ - This module allows you to configure dashboard for different users - add state and its corresponding group which it will be visible , - depends on logged in user they can see records if it is on one state that they has access on - there also a self service screen which employees can see their request in any model.""", +System Dashboard Classic +======================== +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 + (requires 'attendances' module) + +* **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 + """, + 'author': "Expert Co. Ltd., Sudan Team", - # 'website': "http://www.yourcompany.com", - - 'category': 'Uncategorized', - 'version': '0.1', - 'application':True, - - # any module necessary for this one to work correctly - 'depends': ['base'], - - # always loaded + 'category': 'Human Resources/Dashboard', + 'version': '14.0.1.0.0', + 'license': 'LGPL-3', + 'application': True, + + # Required Dependencies + 'depends': [ + #'hr_base', MUST BE FIRST! exp_hr_payroll uses its groups but doesn't depend on it + 'base', # Core Odoo + 'resource', # Work calendar for attendance hours + 'web', # Dashboard assets & QWeb client actions + 'employee_requests', + 'hr_holidays_public', + 'hr_timesheet', + 'exp_official_mission', + 'odex_mobile', + ], + + # Optional Dependencies (soft - checked at runtime): + # - attendances: For check-in/check-out functionality (attendance.attendance model) + # - odoo_dynamic_workflow: For dynamic workflow states integration + # - hr_holidays: For leave management integration + # - hr_payroll: For payslip integration + # - hr_timesheet: For timesheet integration (account.analytic.line) + + # Data files 'data': [ 'security/ir.model.access.csv', 'security/security.xml', 'views/system_dashboard.xml', 'views/config.xml', + 'views/dashboard_settings.xml', ], + + # QWeb templates 'qweb': [ - "static/src/xml/*.xml", + 'static/src/xml/*.xml', ], + + # Assets are loaded via views/system_dashboard.xml + 'installable': True, + 'auto_install': False, } diff --git a/odex25_base/system_dashboard_classic/controllers/__init__.py b/odex25_base/system_dashboard_classic/controllers/__init__.py deleted file mode 100644 index 12bcb5b8f..000000000 --- a/odex25_base/system_dashboard_classic/controllers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -#from . import controllers \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/controllers/controllers.py b/odex25_base/system_dashboard_classic/controllers/controllers.py deleted file mode 100644 index 4b353396c..000000000 --- a/odex25_base/system_dashboard_classic/controllers/controllers.py +++ /dev/null @@ -1,56 +0,0 @@ -# # -*- coding: utf-8 -*- -# # Part of Odoo. See LICENSE file for full copyright and licensing details. - -# from odoo import http, _ -# from odoo.http import request -# from odoo.tools.safe_eval import safe_eval - - -# class BaseDashboard(http.Controller): - -# @http.route(['/system/dashboard'], type='http', auth="user", website=True) -# def system_dashboard(self, **kw): -# values = [] -# base = request.env['base.dashbord'].sudo().search([]) - -# for models in base: -# for model in models: -# for line in model.board_ids: -# if request.uid in line.group_id.users.ids: -# state_click = safe_eval(line.state) -# state_follow = safe_eval(line.state) - -# #get only state from list of list ex:[['state', '=', 'confrim']] return draft,so it become record to confrim -# state = state_click[-1][-1] -# #get state to follow ,which is not in curent state so replace = with != -# state_follow [-1][-2] = '!=' - -# state_to_click = request.env[line.model_name].sudo().search_count(state_click) -# state_to_follow = request.env[line.model_name].sudo().search_count(state_follow) -# values.append({ -# 'type':'user', # to differentiate between user has record to click on or employee has requests to see -# 'name':line.model_id.name, # name in card ex: Leave -# 'user':request.env.user, # user data on user card,use user.name to get his name -# 'model': line.model_name,# to be passed to js as field res_model -# 'count_state_click':state_to_click, #count of state to click on card -# 'count_state_follow':state_to_follow,#count of state to follow on card -# 'state': 'records to ' + '' + state, # title of card ex:records to confirm -# 'domain_to_click': state_click, # to be passed to js for his records on field called domain on action window -# 'domain_to_follow':state_follow , # to be passed to js for records to follow up on field called domain on action window -# }) - -# #if user has no record to click but needs to create new records or see his requests -# else: -# values.append({ -# 'type':'service', -# 'name':line.model_id.name, -# 'user':request.env.user, -# 'model': line.model_name, - -# }) - - -# print("values========================",values) -# return request.render("system_dashboard.portal_template", {'values':values}) - - \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/data/dashboard_data.xml b/odex25_base/system_dashboard_classic/data/dashboard_data.xml deleted file mode 100644 index d08d528a7..000000000 --- a/odex25_base/system_dashboard_classic/data/dashboard_data.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - diff --git a/odex25_base/system_dashboard_classic/demo/demo.xml b/odex25_base/system_dashboard_classic/demo/demo.xml index 457e0aef2..1edc1ea44 100644 --- a/odex25_base/system_dashboard_classic/demo/demo.xml +++ b/odex25_base/system_dashboard_classic/demo/demo.xml @@ -1,30 +1,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/i18n/ar_001.po b/odex25_base/system_dashboard_classic/i18n/ar_001.po index 2d1cf0d73..90214a7b5 100644 --- a/odex25_base/system_dashboard_classic/i18n/ar_001.po +++ b/odex25_base/system_dashboard_classic/i18n/ar_001.po @@ -14,7 +14,8 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" +"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n" "X-Generator: Poedit 3.3.2\n" #. module: system_dashboard_classic @@ -61,8 +62,8 @@ msgstr "الحضور والانصراف" #: model:ir.actions.act_window,name:system_dashboard_classic.base_dashboard_action #: model:ir.model,name:system_dashboard_classic.model_base_dashbord #: model:ir.ui.menu,name:system_dashboard_classic.base_dashboard -msgid "Base Dashboard" -msgstr "لوحة المعلومات الاساسية" +msgid "Dashboard Builder" +msgstr "تصميم الداشبورد" #. module: system_dashboard_classic #: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__board_id @@ -81,8 +82,8 @@ msgstr "اسم النموذج" #. module: system_dashboard_classic #: model:ir.ui.menu,name:system_dashboard_classic.base_dashboard_root -msgid "Configrutions" -msgstr "الاعدادات" +msgid "Configuration" +msgstr "الإعدادات" #. module: system_dashboard_classic #: model_terms:ir.actions.act_window,help:system_dashboard_classic.approval_screen @@ -227,11 +228,11 @@ msgstr "قسيمة راتب" #: code:addons/system_dashboard_classic/static/src/xml/system_dashboard.xml:0 #, python-format msgid "Salary Slips" -msgstr "قسيمة الراتب" +msgstr "قسائم الراتب" #. module: system_dashboard_classic #: model:ir.ui.menu,name:system_dashboard_classic.menu_self_service_service -msgid "Self Service Screen" +msgid "Self Service" msgstr "الخدمة الذاتية" #. module: system_dashboard_classic @@ -246,6 +247,7 @@ 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 "التسلسل" @@ -277,13 +279,9 @@ msgstr "الحالة" #: model:ir.actions.act_window,name:system_dashboard_classic.approval_screen #: model:ir.actions.act_window,name:system_dashboard_classic.self_service_dashboard #: model:ir.model,name:system_dashboard_classic.model_system_dashboard_classic_dashboard -msgid "System Dashboard" -msgstr "لوحة المعلومات" - -#. module: system_dashboard_classic #: model:ir.ui.menu,name:system_dashboard_classic.system_dashboard_classic_menu -msgid "System Dashboard" -msgstr "لوحة الخدمة الذاتية" +msgid "Dashboard" +msgstr "لوحة المعلومات" #. module: system_dashboard_classic #: code:addons/system_dashboard_classic/models/config.py:0 @@ -403,11 +401,6 @@ msgstr "" msgid "records to " msgstr "" -#. module: system_dashboard_classic -#: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord_line__sequence -msgid "sequence" -msgstr "التسلسل" - #. module: system_dashboard_classic #: model:ir.model,name:system_dashboard_classic.model_stage_stage msgid "stage.stage" @@ -437,4 +430,147 @@ msgstr "حذف المراحل" #. module: system_dashboard_classic #: model:ir.model.fields,field_description:system_dashboard_classic.field_base_dashbord__search_field msgid "Search field" -msgstr "حقل البحث" \ No newline at end of file +msgstr "حقل البحث" + +#. module: system_dashboard_classic +#. openerp-web +#: code:addons/system_dashboard_classic/static/src/xml/self_service_dashboard.xml:0 +#: code:addons/system_dashboard_classic/static/src/xml/system_dashboard.xml:0 +#, python-format +msgid "Monthly Attendance" +msgstr "الحضور الشهري" + +#. module: system_dashboard_classic +#. openerp-web +#: code:addons/system_dashboard_classic/static/src/xml/self_service_dashboard.xml:0 +#: code:addons/system_dashboard_classic/static/src/xml/system_dashboard.xml:0 +#, python-format +msgid "Timesheet" +msgstr "الجداول الزمنية" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_primary_color +msgid "Primary Color" +msgstr "اللون الأساسي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_secondary_color +msgid "Secondary Color" +msgstr "اللون الثانوي" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_success_color +msgid "Success Color" +msgstr "لون النجاح/الاتصال" + +#. module: system_dashboard_classic +#: model:ir.model.fields,field_description:system_dashboard_classic.field_res_config_settings__dashboard_warning_color +msgid "Warning Color" +msgstr "لون التنبيه/المتبقي" + +#. module: system_dashboard_classic +#. openerp-web +#: code:addons/system_dashboard_classic/static/src/js/system_dashboard.js:0 +#, python-format +msgid "Remaining" +msgstr "المتبقي" + +#. module: system_dashboard_classic +#. openerp-web +#: code:addons/system_dashboard_classic/static/src/js/system_dashboard.js:0 +#, python-format +msgid "Total" +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.actions.act_window,name:system_dashboard_classic.action_dashboard_configuration +#: model:ir.ui.menu,name:system_dashboard_classic.menu_dashboard_configuration +msgid "General Settings" +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_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.view_base_dashboard_form +msgid "Employee Filter Configuration" +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 +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "State/Stage Configuration" +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 "Refresh States" +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 +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "Service Name" +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.view_base_dashboard_form +msgid "Enable for employee self-service cards" +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_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 "Create your first dashboard service" +msgstr "أنشئ أول خدمة في الداشبورد" + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_form +msgid "" +"Configure which Odoo models appear as service cards on the employee " +"dashboard." +msgstr "حدد أي نماذج Odoo تظهر كبطاقات خدمات في لوحة تحكم الموظف." + +#. module: system_dashboard_classic +#: model_terms:ir.ui.view,arch_db:system_dashboard_classic.view_base_dashboard_tree +msgid "Dashboard Services" +msgstr "خدمات الداشبورد" diff --git a/odex25_base/system_dashboard_classic/models/__init__.py b/odex25_base/system_dashboard_classic/models/__init__.py index e47a205d9..d795b7333 100644 --- a/odex25_base/system_dashboard_classic/models/__init__.py +++ b/odex25_base/system_dashboard_classic/models/__init__.py @@ -1,2 +1,3 @@ from . import config -from . import models \ No newline at end of file +from . import models +from . import res_users \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/models/config.py b/odex25_base/system_dashboard_classic/models/config.py index c557f3da5..1d24f164d 100644 --- a/odex25_base/system_dashboard_classic/models/config.py +++ b/odex25_base/system_dashboard_classic/models/config.py @@ -3,7 +3,7 @@ from odoo.exceptions import ValidationError class BaseDashboard(models.Model): _name = 'base.dashbord' - _description = 'Base Dashboard' + _description = 'Dashboard Builder' _order = 'sequence' sequence = fields.Integer() @@ -29,6 +29,39 @@ class BaseDashboard(models.Model): ) + 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") + + @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: + # Dynamic Icon Preview - Class based sizing + record.icon_preview_html = f'
' + elif record.icon_type == 'image' and record.card_image: + # Actual Image Preview + # CRITICAL: Use bin_size=False to ensure we get DATA not SIZE TEXT (e.g. "25 KB") + try: + img_rec = record.with_context(bin_size=False) + image_data = img_rec.card_image.decode('utf-8') if isinstance(img_rec.card_image, bytes) else img_rec.card_image + record.icon_preview_html = f'
' + except Exception: + record.icon_preview_html = '
' + else: + # Default Placeholder + record.icon_preview_html = '
' + card_image = fields.Binary( string='Card Image', ) @@ -431,3 +464,208 @@ class StageStage(models.Model): 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) + + +class DashboardConfigSettings(models.TransientModel): + """Dashboard Theme Settings - Configurable colors from UI""" + _inherit = 'res.config.settings' + _description = 'Dashboard Configuration Settings' + + # Dashboard Theme Colors + 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)' + ) + + # Dashboard Statistics Visibility Settings + # NOTE: We DON'T use config_parameter here because Odoo's default behavior + # converts any non-empty string to True via bool(). Instead, we manually + # handle storage in get_values/set_values. + dashboard_show_annual_leave = fields.Boolean( + string='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', + default=True, + help='Show/Hide Salary Slips statistics card in dashboard header' + ) + + dashboard_show_timesheet = fields.Boolean( + string='Show Weekly Timesheet', + default=True, + help='Show/Hide Weekly Timesheet statistics card in dashboard header' + ) + + dashboard_show_attendance_hours = fields.Boolean( + string='Show Attendance Hours', + default=True, + help='Show/Hide Attendance Hours statistics card in dashboard header' + ) + + # Attendance Check-in/out Section + dashboard_show_attendance_section = fields.Boolean( + string='Show Attendance Section', + default=True, + help='Show/Hide the Attendance Check-in/out section in dashboard header. When hidden, statistics cards will expand to fill the space.' + ) + + # Enable/Disable Check-in/out Button + dashboard_enable_attendance_button = fields.Boolean( + string='Enable Check-in/out Button', + default=False, + help='Enable/Disable the Check-in/Check-out button functionality. When disabled, the button will appear but will not function.' + ) + + # Chart Type Selection Fields + 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') + + @api.model + def get_dashboard_colors(self): + """API method to get dashboard colors for JavaScript""" + ICPSudo = self.env['ir.config_parameter'].sudo() + return { + 'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'), + 'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'), + 'success': ICPSudo.get_param('system_dashboard_classic.success_color', '#10b981'), + 'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'), + } + + @api.model + def get_values(self): + """Override to properly load Boolean visibility settings from ir.config_parameter + + We store these as explicit strings ('True'/'False') and convert them back to + Python booleans here. + """ + res = super(DashboardConfigSettings, self).get_values() + + ICPSudo = self.env['ir.config_parameter'].sudo() + + # Helper to safely get boolean value from string storage + def get_bool_param(key, default='True'): + value = ICPSudo.get_param(key, default) + return value == 'True' + + res.update( + dashboard_show_annual_leave=get_bool_param('system_dashboard_classic.show_annual_leave'), + dashboard_show_salary_slips=get_bool_param('system_dashboard_classic.show_salary_slips'), + dashboard_show_timesheet=get_bool_param('system_dashboard_classic.show_timesheet'), + dashboard_show_attendance_hours=get_bool_param('system_dashboard_classic.show_attendance_hours'), + dashboard_show_attendance_section=get_bool_param('system_dashboard_classic.show_attendance_section'), + dashboard_enable_attendance_button=get_bool_param('system_dashboard_classic.enable_attendance_button', 'False'), + ) + return res + + def set_values(self): + """Override to explicitly store Boolean visibility settings as strings + + We store as 'True' or 'False' strings to prevent Odoo from deleting + the parameter when value is False. + """ + res = super(DashboardConfigSettings, self).set_values() + + # Explicitly store visibility settings as string values + ICPSudo = self.env['ir.config_parameter'].sudo() + ICPSudo.set_param('system_dashboard_classic.show_annual_leave', + 'True' if self.dashboard_show_annual_leave else 'False') + ICPSudo.set_param('system_dashboard_classic.show_salary_slips', + 'True' if self.dashboard_show_salary_slips else 'False') + ICPSudo.set_param('system_dashboard_classic.show_timesheet', + 'True' if self.dashboard_show_timesheet else 'False') + ICPSudo.set_param('system_dashboard_classic.show_attendance_hours', + 'True' if self.dashboard_show_attendance_hours else 'False') + ICPSudo.set_param('system_dashboard_classic.show_attendance_section', + 'True' if self.dashboard_show_attendance_section else 'False') + ICPSudo.set_param('system_dashboard_classic.enable_attendance_button', + 'True' if self.dashboard_enable_attendance_button else 'False') + + return res + + @api.model + def get_stats_visibility(self): + """API method to get statistics visibility settings for JavaScript + + Returns dict with boolean values for each visibility setting. + Default is True (show) if parameter doesn't exist. + """ + ICPSudo = self.env['ir.config_parameter'].sudo() + + # Helper to safely get boolean value + def get_bool_param(key, default='True'): + value = ICPSudo.get_param(key, default) + # Handle various possible values + if value in ('True', 'true', True, '1', 1): + return True + return False + + 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 + + Returns dict with chart type for each card. + Default is 'donut' if parameter doesn't exist. + """ + ICPSudo = self.env['ir.config_parameter'].sudo() + + return { + 'annual_leave_chart': ICPSudo.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'), + 'salary_slips_chart': ICPSudo.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'), + 'timesheet_chart': ICPSudo.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'), + 'attendance_hours_chart': ICPSudo.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'), + } diff --git a/odex25_base/system_dashboard_classic/models/models.py b/odex25_base/system_dashboard_classic/models/models.py index b4ebdc607..6ae3a517c 100644 --- a/odex25_base/system_dashboard_classic/models/models.py +++ b/odex25_base/system_dashboard_classic/models/models.py @@ -1,11 +1,10 @@ from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError , AccessError -from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import AccessError, UserError import ast from datetime import datetime, date from dateutil.relativedelta import relativedelta, SA, SU, MO -import calendar import pytz +from math import radians, sin, cos, sqrt, asin @@ -31,8 +30,56 @@ class SystemDashboard(models.Model): # employee and user date and vars defined to be used inside this method values = {'user': [], 'timesheet': [], 'leaves': [], 'payroll': [], 'attendance': [], 'employee': [], - 'cards': []} + 'cards': [], 'attendance_hours': [], 'chart_types': {}, 'card_orders': {}} base = self.env['base.dashbord'].search([]) + + # Load chart type settings + ICPSudo = self.env['ir.config_parameter'].sudo() + values['chart_types'] = { + 'annual_leave': ICPSudo.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'), + 'salary_slips': ICPSudo.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'), + 'timesheet': ICPSudo.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'), + 'attendance_hours': ICPSudo.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'), + } + + # Load chart colors from settings (match the totals styling) + # primary = Total/Remaining color (teal), warning = Left amount color (amber) + values['chart_colors'] = { + 'primary': ICPSudo.get_param('system_dashboard_classic.primary_color', '#0891b2'), + 'warning': ICPSudo.get_param('system_dashboard_classic.warning_color', '#f59e0b'), + 'secondary': ICPSudo.get_param('system_dashboard_classic.secondary_color', '#1e293b'), + } + + # Load attendance button setting (disabled by default) + values['enable_attendance_button'] = ICPSudo.get_param('system_dashboard_classic.enable_attendance_button', 'False') == 'True' + + # Load stats visibility settings (to prevent fadeIn from overriding hidden state) + def get_bool_param(key, default='True'): + value = ICPSudo.get_param(key, default) + return value in ('True', 'true', True, '1', 1) + + values['stats_visibility'] = { + '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'), + } + + # Initialize celebration data (birthday and work anniversary) + values['celebration'] = { + 'is_birthday': False, + 'is_anniversary': False, + 'anniversary_years': 0, + } + + # Initialize gender-aware data for personalized experience + values['gender_info'] = { + 'gender': 'male', # default + 'honorific': 'أستاذ', # default male + 'pronoun_you': 'ك', # default male (لديك) + 'verb_suffix': 'تَ', # default male (قمتَ) + } user = self.env.user user_id = self.env['res.users'].sudo().search_read( @@ -43,46 +90,116 @@ class SystemDashboard(models.Model): employee_object = self.env['hr.employee'].sudo().search( [('user_id', '=', user.id)], limit=1) - job_english = employee_object.job_id.english_name + if hasattr(employee_object.job_id, 'english_name') and employee_object.job_id.english_name: + job_english = employee_object.job_id.english_name + else: + job_english = employee_object.job_id.name or '' - t_date = date.today() + # ========================================== + # CELEBRATION DETECTION: Birthday & Anniversary + # ========================================== + today = date.today() + + if employee_object: + # ========================================== + # GENDER-AWARE PERSONALIZATION + # ========================================== + # hr.employee has 'gender' field with values: 'male', 'female', 'other' + employee_gender = getattr(employee_object, 'gender', 'male') or 'male' + if employee_gender == 'female': + values['gender_info'] = { + 'gender': 'female', + 'honorific': 'أستاذة', # Ms./Mrs. + 'pronoun_you': 'كِ', # لديكِ (feminine you) + 'verb_suffix': 'تِ', # قمتِ (feminine past tense) + } + else: + values['gender_info'] = { + 'gender': 'male', + 'honorific': 'أستاذ', # Mr. + 'pronoun_you': 'ك', # لديك (masculine you) + 'verb_suffix': 'تَ', # قمتَ (masculine past tense) + } + + # Check for BIRTHDAY (compare month and day only) + birthday = getattr(employee_object, 'birthday', None) + if birthday: + if birthday.month == today.month and birthday.day == today.day: + values['celebration']['is_birthday'] = True + + # Check for WORK ANNIVERSARY (first contract start date) + # Using contract_id.date_start from hr.contract + joining_date = None + if employee_object.contract_id and employee_object.contract_id.date_start: + joining_date = employee_object.contract_id.date_start + + if joining_date: + # Compare month and day + if joining_date.month == today.month and joining_date.day == today.day: + # Calculate years of service + years = today.year - joining_date.year + if years > 0: # Only celebrate if at least 1 year + values['celebration']['is_anniversary'] = True + values['celebration']['anniversary_years'] = years + + t_date = today # Use same date object attendance_date = {} leaves_data = {} payroll_data = {} timesheet_data = {} + attendance_hours_data = {} ################################################### # check whether last action sign in or out and its date is_hr_attendance_module = self.env['ir.module.module'].sudo().search([('name', '=', 'hr_attendance')]) if is_hr_attendance_module and is_hr_attendance_module.state == 'installed': - attendance = self.env['attendance.attendance'].sudo().search( - [('employee_id', '=', employee_object.id), ('action_date', '=', t_date)]) - is_attendance = True - if not attendance: - is_attendance = False - if attendance and attendance[-1].action == 'sign_out': - is_attendance = False + # ATTENDANCE LOGIC IMPROVEMENT: + # Fetch the absolute latest record regardless of date (e.g. yesterday, mobile app entry) + # This ensures we show the REAL status. + last_attendance = self.env['attendance.attendance'].sudo().search( + [('employee_id', '=', employee_object.id)], limit=1, order="name desc") + + is_attendance = False + if last_attendance and last_attendance.action == 'sign_in': + is_attendance = True + user_tz = pytz.timezone(self.env.context.get('tz', 'Asia/Riyadh') or self.env.user.tz) - if attendance: - time_object = fields.Datetime.from_string(attendance[-1].name) + time_in_timezone = False + + if last_attendance: + # Use 'name' (datetime) for the time display + time_object = fields.Datetime.from_string(last_attendance.name) time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + attendance_date.update({'is_attendance': is_attendance, - 'time': time_in_timezone if attendance else False + 'time': time_in_timezone }) # if noc is found case shoud be handeld ############################################### # compute leaves taken and remaing leaves is_leave_module = self.env['ir.module.module'].sudo().search([('name', '=', 'hr_holidays_community')]) - if is_leave_module and is_leave_module.state == 'installed': + is_leave_installed = bool(is_leave_module and is_leave_module.state == 'installed') + + # Always set default values and module status + leaves_data.update({ + 'taken': 0, + 'remaining_leaves': 0, + 'is_module_installed': is_leave_installed + }) + + if is_leave_installed: + # FIX: Removed limit=1 to consider ALL valid allocations (e.g. current year + carried over) leaves = self.env['hr.holidays'].sudo().search( [('employee_id', '=', employee_object.id), ('holiday_status_id.leave_type', '=', 'annual'), - ('type', '=', 'add'), ('check_allocation_view', '=', 'balance')], limit=1) - taken = leaves.leaves_taken - remaining_leaves = leaves.remaining_leaves + ('type', '=', 'add'), ('check_allocation_view', '=', 'balance')]) + + # Sum up valid allocations + taken = sum(l.leaves_taken for l in leaves) + remaining_leaves = sum(l.remaining_leaves for l in leaves) + leaves_data.update({'taken': taken, 'remaining_leaves': remaining_leaves - }) ################################################### @@ -91,17 +208,47 @@ class SystemDashboard(models.Model): last_day = date(date.today().year, 12, 31) is_payslip_module = self.env['ir.module.module'].sudo().search([('name', '=', 'exp_hr_payroll')]) - if is_payslip_module and is_payslip_module.state == 'installed': - payslip = self.env['hr.payslip'].sudo().search_count( + is_payslip_installed = bool(is_payslip_module and is_payslip_module.state == 'installed') + + # Always set default values and module status + payroll_data.update({ + 'taken': 0, + 'payslip_remaining': 0, + 'is_module_installed': is_payslip_installed + }) + + if is_payslip_installed: + payslip_count = self.env['hr.payslip'].sudo().search_count( [('employee_id', '=', employee_object.id), ('date_from', '>=', first_day), ('date_to', '<=', last_day)]) - payroll_data.update({'taken': payslip, - 'payslip_remaining': 12 - payslip + + # FIX: Calculate expected slips based on contract start date + # If joined mid-year, they shouldn't expect 12 slips + contract = self.env['hr.contract'].sudo().search([('employee_id', '=', employee_object.id), ('state', '=', 'open')], limit=1) + expected_slips = 12 + if contract and contract.date_start and contract.date_start.year == date.today().year: + # If joined this year, expected slips = months from start date to Dec + expected_slips = 12 - contract.date_start.month + 1 + + # Ensure we don't show negative remaining + remaining_slips = max(0, expected_slips - payslip_count) + + payroll_data.update({'taken': payslip_count, + 'payslip_remaining': remaining_slips }) ############################################## # compute timesheet taken and remaing timesheet is_analytic_module = self.env['ir.module.module'].sudo().search([('name', '=', 'analytic')]) - if is_analytic_module and is_analytic_module.state == 'installed': + is_timesheet_installed = bool(is_analytic_module and is_analytic_module.state == 'installed') + + # Always set default values and module status + timesheet_data.update({ + 'taken': 0, + 'timesheet_remaining': 0, + 'is_module_installed': is_timesheet_installed + }) + + if is_timesheet_installed: calender = employee_object.resource_calendar_id days_off_name = [] days_special_name = [] @@ -161,25 +308,47 @@ class SystemDashboard(models.Model): star_of_week = SA # calcultion of all working hours and return done working hours and remaining - lenght_days_off = len(days_off_name) - lenght_special_days_off = len(days_special_name) - lenght_work_days = (days_of_week - lenght_days_off) - lenght_special_days_off - total_wroking_hours = (working_hours * lenght_work_days) + sepcial_working_hours - domain = [('employee_id', '=', employee_object.id), '&', ( - 'date', '>=', (date.today() + relativedelta(weeks=-1, days=1, weekday=star_of_week)).strftime('%Y-%m-%d')), - ('date', '<=', (date.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d')), ] - timesheet = self.env['account.analytic.line'].sudo().search(domain) - day_name_list = ['saturday', 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', ] - done_hours = 0 - for sheet in timesheet: - day = datetime.strptime(str(sheet.date), '%Y-%m-%d').weekday() - day_name = day_name_list[day] - if day_name not in days_off_name: - done_hours = done_hours + sheet.unit_amount + # ATTENDANCE/TIMESHEET LOGIC IMPROVEMENT: + # Replaced manual calculation with standard Odoo calendar method + # This ensures Public Holidays (Global Leaves) are correctly handled + + # Determine start/end of week logic (kept from original code to maintain week start preference) + # star_of_week is determined above (SU, SA, MO) + + start_date = date.today() + relativedelta(weeks=-1, days=1, weekday=star_of_week) + end_date = start_date + relativedelta(days=6) # Ensure strictly 7 days week range + # Note: start_date is calculated based on star_of_week logic (e.g. last Saturday) + + total_wroking_hours = 0 + if employee_object.resource_calendar_id: + # Use standard Odoo method + total_wroking_hours = employee_object.resource_calendar_id.get_work_hours_count( + datetime.combine(start_date, datetime.min.time()), + datetime.combine(end_date, datetime.max.time()), + compute_leaves=True + ) + else: + # Fallback manual calculation (if calendar missing) + # Restore variables needed for calculation + lenght_days_off = len(days_off_name) + lenght_special_days_off = len(days_special_name) + lenght_work_days = (days_of_week - lenght_days_off) - lenght_special_days_off + total_wroking_hours = (working_hours * lenght_work_days) + sepcial_working_hours - don_hours = sum(item.unit_amount for item in timesheet) - timesheet_data.update({'taken': don_hours, - 'timesheet_remaining': total_wroking_hours - don_hours + domain = [('employee_id', '=', employee_object.id), + ('date', '>=', start_date), + ('date', '<=', end_date)] + + timesheet = self.env['account.analytic.line'].sudo().search(domain) + + # Filter days off manually? Odoo search already filters by date. + # Original code filtered 'day_name not in days_off_name'. + # If standard timesheet is used, entries on off-days are usually overtime or valid. + # We will sum ALL valid timesheet entries in the period. + done_hours = sum(sheet.unit_amount for sheet in timesheet) + + timesheet_data.update({'taken': done_hours, + 'timesheet_remaining': max(0, total_wroking_hours - done_hours) }) ############################################## @@ -198,23 +367,29 @@ class SystemDashboard(models.Model): if self.is_user(line.group_ids, user): # call method to return if user is in one of the groups in current line # static vars for the card - # TODO: find a way to fix translation, - # the filed is name is translateable, - # but its not loaded when change lang, - # so we handel it by searching on translation object. - model_name_to_serach = model.name if model.name else model.model_id.name - #value = self.env['ir.translation'].sudo().search([('source', '=', model_name_to_serach)], limit=1).value - value = "text" - card_name = model.name if model.name else model.model_id.name - # if self.env.user.lang == 'en_US': - # card_name = model.name if model.name else model.model_id.name - # else: - # card_name = value - mod['name'] = card_name - mod['name_arabic'] = card_name - mod['name_english'] = card_name + # FIX: Fetch explicit translations for bilingual support + # model.name returns current user lang, so we force context + card_name_ar = model.with_context(lang='ar_001').name or model.with_context(lang='ar_SY').name or model.name + card_name_en = model.with_context(lang='en_US').name or model.name + + # Fallback if model name is empty (use model description) + if not card_name_ar: + card_name_ar = model.model_id.with_context(lang='ar_001').name or model.model_id.name + if not card_name_en: + card_name_en = model.model_id.with_context(lang='en_US').name or model.model_id.name + + mod['name'] = card_name_ar if self.env.user.lang in ['ar_001', 'ar_SY'] else card_name_en + mod['name_arabic'] = card_name_ar + mod['name_english'] = card_name_en mod['model'] = model.model_name mod['image'] = model.card_image + mod['icon_type'] = model.icon_type + mod['icon_name'] = model.icon_name + + # Default Icon Logic + if not mod['image'] and not mod['icon_name']: + mod['icon_type'] = 'icon' + mod['icon_name'] = 'fa-th-large' # var used in domain to serach with either state or state state_or_stage = 'state' if line.state_id else 'stage_id' @@ -374,7 +549,14 @@ class SystemDashboard(models.Model): #if 'user_id' in mod._fields: # service_action_domain.append(('user_id', '=', user.id)) # service_action_domain.append(('employee_id.user_id','=',user.id)) - # value = self.env['ir.translation'].sudo().search([('source', '=', model.name)], limit=1).value + # Default Icon Logic + if not model.card_image and not model.icon_name: + model_icon_type = 'icon' + model_icon_name = 'fa-th-large' + else: + model_icon_type = model.icon_type + model_icon_name = model.icon_name + values['cards'].append({ 'type': 'selfs', 'name': card_name, @@ -383,6 +565,8 @@ class SystemDashboard(models.Model): 'model': model.model_name, 'state_count': self.env[model.model_name].search_count(service_action_domain), 'image': model.card_image, + 'icon_type': model_icon_type, + 'icon_name': model_icon_name, 'form_view': model.form_view_id.id, 'list_view': model.list_view_id.id, 'js_domain': service_action_domain, @@ -399,74 +583,291 @@ class SystemDashboard(models.Model): values['payroll'].append(payroll_data) values['attendance'].append(attendance_date) values['timesheet'].append(timesheet_data) + + # Compute attendance hours from hr.attendance.transaction + try: + AttendanceTransaction = self.env['hr.attendance.transaction'].sudo() + # Get current month's attendance transactions for the employee + first_day_of_month = date(t_date.year, t_date.month, 1) + attendance_txns = AttendanceTransaction.search([ + ('employee_id', '=', employee_object.id), + ('date', '>=', first_day_of_month), + ('date', '<=', t_date) + ]) + # ATTENDANCE LOGIC IMPROVEMENT: + # Calculate planned hours from the Resource Calendar (Work Schedule) + # This ensures we count days where the employee was ABSENT (no transaction) + # as missed hours, providing a true compliance percentage. + if employee_object.resource_calendar_id: + # Use standard Odoo method to get expected working hours for the period + # Use standard Odoo method to get expected working hours for the period + # from start of month to today (Month-to-Date) + # compute_related ensures we consider resource specificities if needed + # Important: Convert dates to datetime as required by get_work_hours_count + plan_hours_total = employee_object.resource_calendar_id.get_work_hours_count( + datetime.combine(first_day_of_month, datetime.min.time()), + datetime.combine(t_date, datetime.max.time()), + compute_leaves=True + ) + else: + # Fallback to sum of transactions if no calendar (though rare for active employees) + plan_hours_total = sum(txn.plan_hours for txn in attendance_txns) + + official_hours_total = sum(txn.official_hours for txn in attendance_txns) + attendance_hours_data.update({ + 'plan_hours': round(plan_hours_total, 2), + 'official_hours': round(official_hours_total, 2), + 'is_module_installed': True # Module is installed if we got here + }) + except Exception: + attendance_hours_data.update({ + 'plan_hours': 0, + 'official_hours': 0, + 'is_module_installed': False # Module not installed or error + }) + + values['attendance_hours'].append(attendance_hours_data) values['job_english'] = job_english + + # Load user's saved card order preferences + import json + try: + card_orders_json = self.env.user.dashboard_card_orders or '{}' + values['card_orders'] = json.loads(card_orders_json) + except (json.JSONDecodeError, TypeError, AttributeError): + values['card_orders'] = {} + return values + def _haversine_distance(self, lat1, lon1, lat2, lon2): + """ + Calculate the great-circle distance between two GPS points using Haversine formula. + + @param lat1, lon1: First point coordinates in degrees + @param lat2, lon2: Second point coordinates in degrees + @return: Distance in meters + """ + R = 6371000 # Earth's radius in meters + + # Convert to radians + lat1_rad, lon1_rad = radians(lat1), radians(lon1) + lat2_rad, lon2_rad = radians(lat2), radians(lon2) + + # Differences + dlat = lat2_rad - lat1_rad + dlon = lon2_rad - lon1_rad + + # Haversine formula + a = sin(dlat / 2) ** 2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2) ** 2 + c = 2 * asin(sqrt(a)) + + return R * c + @api.model - def checkin_checkout(self): + def checkin_checkout(self, latitude=None, longitude=None): + """ + Check-in or Check-out with zone-based geolocation validation. + + @param latitude: User's current latitude from browser geolocation + @param longitude: User's current longitude from browser geolocation + @return: Dict with is_attendance, time, and any error info + """ ctx = self._context - attendance_status = ctx.get('check', False) t_date = date.today() user = self.env.user - employee_object = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1) - - vals_in = {'employee_id': employee_object.id,'action': 'sign_in','action_type': 'manual'} - vals_out = {'employee_id': employee_object.id,'action': 'sign_out','action_type': 'manual'} + employee_object = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1) + + if not employee_object: + is_arabic = 'ar' in self._context.get('lang', 'en_US') + msg = "عذراً، لم يتم العثور على ملف وظيفي مرتبط بحسابك.\nيرجى مراجعة إدارة الموارد البشرية." if is_arabic else "Sorry, no employee profile found linked to your account.\nPlease contact HR department." + return {'error': True, 'message': msg} + + # ============================================================ + # ZONE VALIDATION - Check if employee is within allowed zone + # ============================================================ + AttendanceZone = self.env['attendance.zone'].sudo() + + # Find employee's assigned zones + employee_zones = AttendanceZone.search([('employee_ids', 'in', employee_object.id)]) + + # Check for general zone (applies to all employees) + general_zone = employee_zones.filtered(lambda z: z.general) + + # If employee has a general zone, allow from anywhere (no location check needed) + zone_validation_required = not bool(general_zone) + + if zone_validation_required: + is_arabic = 'ar' in self._context.get('lang', 'en_US') + + # Location is required for non-general zones + if not latitude or not longitude: + msg = "يتطلب النظام الوصول إلى موقعك الجغرافي لتسجيل الحضور.\nيرجى تفعيل صلاحية الموقع في المتصفح والمحاولة مرة أخرى." if is_arabic else "System requires access to your location for attendance.\nPlease enable location permission in your browser and try again." + return {'error': True, 'message': msg} + + # If no zones assigned at all + if not employee_zones: + msg = "لم يتم تعيين منطقة حضور خاصة بك.\nيرجى التواصل مع إدارة النظام." if is_arabic else "No specific attendance zone assigned to you.\nPlease contact system administration." + return {'error': True, 'message': msg} + + # Check if user is within any of their assigned zones + is_in_zone = False + closest_zone_distance = float('inf') + allowed_range = 0 + valid_zones_count = 0 # Track zones with valid coordinates + + for zone in employee_zones: + if not zone.latitude or not zone.longitude or not zone.allowed_range: + continue + + try: + zone_lat = float(zone.latitude) + zone_lon = float(zone.longitude) + zone_range = float(zone.allowed_range) + except (ValueError, TypeError): + continue + + valid_zones_count += 1 + + # Calculate distance from user to zone center + distance = self._haversine_distance(latitude, longitude, zone_lat, zone_lon) + + if distance <= zone_range: + is_in_zone = True + break + + # Track closest zone for error message + if distance < closest_zone_distance: + closest_zone_distance = distance + allowed_range = zone_range + + if not is_in_zone: + # Edge case: all zones have invalid coordinates + if valid_zones_count == 0: + msg = "بيانات منطقة الحضور غير مكتملة.\nيرجى التواصل مع إدارة النظام لتحديث الإحداثيات." if is_arabic else "Attendance zone data is incomplete.\nPlease contact system administration to update coordinates." + return {'error': True, 'message': msg} + + # Calculate how far outside the zone user is + distance_outside = int(closest_zone_distance - allowed_range) + + # Gender-aware Arabic message + # تتواجد (male) / تتواجدين (female) + # لتتمكن (male) / لتتمكني (female) + employee_gender = getattr(employee_object, 'gender', 'male') or 'male' + if employee_gender == 'female': + ar_msg = "عذراً، أنتِ تتواجدين خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة %s متر تقريباً أو أكثر لتتمكني من التسجيل." + else: + ar_msg = "عذراً، أنت تتواجد خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة %s متر تقريباً أو أكثر لتتمكن من التسجيل." + + msg_formats = { + 'ar': ar_msg, + 'en': "Sorry, you are outside the allowed attendance zone.\nPlease move approximately %s meters or more closer to be able to check in." + } + msg = (msg_formats['ar'] if is_arabic else msg_formats['en']) % distance_outside + return { + 'error': True, + 'message': msg + } + + # ============================================================ + # ATTENDANCE CREATION (Zone validation passed) + # ============================================================ + vals_in = { + 'employee_id': employee_object.id, + 'action': 'sign_in', + 'action_type': 'manual', + 'latitude': str(latitude) if latitude else False, + 'longitude': str(longitude) if longitude else False, + } + vals_out = { + 'employee_id': employee_object.id, + 'action': 'sign_out', + 'action_type': 'manual', + 'latitude': str(latitude) if latitude else False, + 'longitude': str(longitude) if longitude else False, + } - # check last attendance record before do any action Attendance = self.env['attendance.attendance'] - is_attendance = False - # get last attendance record + # Robust Logic: Always fetch the ABSOLUTE last record regardless of date last_attendance = self.env['attendance.attendance'].sudo().search( - [('employee_id', '=', employee_object.id), ('action_date', '=', t_date)], limit=1, order="id desc") + [('employee_id', '=', employee_object.id)], limit=1, order="name desc") + user_tz = pytz.timezone(self.env.context.get('tz', 'Asia/Riyadh') or self.env.user.tz) + + # Determine next action based on DB state + if last_attendance and last_attendance.action == 'sign_in': + # User is currently Checked In -> Action: Sign Out + result = Attendance.create(vals_out) + is_attendance = False + else: + # User is Checked Out (or no record) -> Action: Sign In + result = Attendance.create(vals_in) + is_attendance = True + + # Fetch the NEW last record to return accurate time + last_attendance = self.env['attendance.attendance'].sudo().search( + [('employee_id', '=', employee_object.id)], limit=1, order="name desc") + if last_attendance: time_object = fields.Datetime.from_string(last_attendance.name) time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) - - # check current attendance status - # (True == user already signed in --> create new record with signout action) - # (Fasle == user already signed out --> create new record with signin action) - if attendance_status: - is_attendance = False - # check last attendance record action - if last_attendance: - if last_attendance.action == 'sign_in': - result = Attendance.create(vals_out) # create signout record - last_attendance = self.env['attendance.attendance'].sudo().search( - [('employee_id', '=', employee_object.id), ('action_date', '=', t_date)], limit=1, order="id desc") - time_object = fields.Datetime.from_string(last_attendance.name) - time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) - elif last_attendance.action == 'sign_out': - is_attendance = False - else: - is_attendance = True else: - is_attendance = True - # check last attendance record action - if last_attendance: - if last_attendance.action == 'sign_out': - result = Attendance.create(vals_in) # create signin record - last_attendance = self.env['attendance.attendance'].sudo().search( - [('employee_id', '=', employee_object.id), ('action_date', '=', t_date)], limit=1, order="id desc") - time_object = fields.Datetime.from_string(last_attendance.name) - time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) - elif last_attendance.action == 'sign_in': - is_attendance = True - else: - is_attendance = False - else: - result = Attendance.create(vals_in) # create signin record - last_attendance = self.env['attendance.attendance'].sudo().search( - [('employee_id', '=', employee_object.id), ('action_date', '=', t_date)], limit=1, order="id desc") - time_object = fields.Datetime.from_string(last_attendance.name) - time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz) + time_in_timezone = False attendance_response = { 'is_attendance': is_attendance, - 'time': time_in_timezone if last_attendance else False + 'time': time_in_timezone } return attendance_response + + @api.model + def save_card_order(self, order_type, card_ids): + """ + Save card order preference for current user. + Stores in res.users.dashboard_card_orders as JSON. + + @param order_type: String key identifying which cards (e.g., 'service', 'approve', 'track', 'stats') + @param card_ids: List of card identifiers in desired order + @return: Boolean success status + """ + import json + user = self.env.user + + # Get current orders or initialize empty dict + try: + current_orders = json.loads(user.dashboard_card_orders or '{}') + except (json.JSONDecodeError, TypeError): + current_orders = {} + + # Update the specific order type + current_orders[order_type] = card_ids + + # Save back to user + user.sudo().write({ + 'dashboard_card_orders': json.dumps(current_orders) + }) + + return True + + @api.model + def get_card_order(self, order_type=None): + """ + Get card order preferences for current user. + + @param order_type: Optional string key for specific order type. + If None, returns all orders. + @return: List of card IDs for specific type, or dict of all orders + """ + import json + user = self.env.user + + try: + all_orders = json.loads(user.dashboard_card_orders or '{}') + except (json.JSONDecodeError, TypeError): + all_orders = {} + + if order_type: + return all_orders.get(order_type, []) + return all_orders diff --git a/odex25_base/system_dashboard_classic/models/res_users.py b/odex25_base/system_dashboard_classic/models/res_users.py new file mode 100644 index 000000000..7541717da --- /dev/null +++ b/odex25_base/system_dashboard_classic/models/res_users.py @@ -0,0 +1,15 @@ +from odoo import models, fields + + +class ResUsersCardOrder(models.Model): + """ + Extend res.users to store dashboard card order preferences. + This allows card ordering to persist across devices/sessions. + """ + _inherit = 'res.users' + + dashboard_card_orders = fields.Text( + string='Dashboard Card Orders', + default='{}', + help='JSON storage for dashboard card ordering preferences' + ) diff --git a/odex25_base/system_dashboard_classic/security/secuirty.xml b/odex25_base/system_dashboard_classic/security/secuirty.xml deleted file mode 100644 index 83db7be4f..000000000 --- a/odex25_base/system_dashboard_classic/security/secuirty.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - System Dashboard - User access level for System Board module - 20 - - - - - System Dashboard Manager - - - - - System Dashboard Configurations - - - - - \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/static/description/icon.png b/odex25_base/system_dashboard_classic/static/description/icon.png index 75990761ec0209dee620ddd4b103a66e8246ee29..52fb3c4cd86de18d203b389743eb645788456af9 100644 GIT binary patch literal 21683 zcmeFZXH?YPvN!nerjbTO8W9u(0g0l3f@Fb4lq3ieBnLr7a?Ys{0m&diBrB40&cOgE zS#oR;BuB})XE#3QoV#Z3r-B3CYd#R{C@Tl?%^{j{O^w011|_quL+Ie}KwW82m)zmGgir_rv$sy9 zJkRd<_%?13jo|xXuimes+kc!t*ru*mFhJltG!hbl{rzvsd1400VaQ(YpT99c%LFJ4ew?>3GY0B=+IyWEtN^~iK!s$d zt3imxa0ssCuaNQV@A445Q4GH<*eX*JjzrV(C%$F;`vnp@{AbVq+2~Y^|7%4zlTPot zQ8PkQ(y_q7?X!SCl>3hiS~)3C>^C#~nUZ6Pjgbrx`5Mwc+4iYfGiUv{>_We-L++>M zH}4wl@y9-O-$18_o_6p!wD)cGROl6Mx{%%}Qx{Go`t`WzPEW|jmAjKeM)jxCcn84^Mzs-*G2C#2_IH3S^vVS^WbPIO4Q)Jt zg7@Fz>D0y3X@#fr|0Yor*+@*hTu%2;`aQ`KO@~KWDa(7|J&{HqI)26`4jhF(b-`&_ zO!sP9(7){eX3t-GU3kpKK{&Owx~bdt#NwkTpGie;?nmVpcHx)dHcWsoM+D&ON){B{ zrTM8oH7VdRpYGJfNL_)mg@K%*rx_Muz-V!!_E_(In=Mkg&7gn#x`Q~33SJkg39D0?DfB+oGbM4kOk}DJ6{ivi&%ayhA8IID9-g}sw$vkV zL@3D1BtEt}6X#74&R!`_`p+VLqn%EuBsv@Pv)39NtX`qsVj zVeCW3O9**zzUj_tiB=?|afZ>NfrBtBj~=uBhNDl}gNYoEOol`BZ$5-;ZAKo92u<`A zuf9BS6+M_vgTUk$RvvoF1-*m)}Qe zpgeKOu=V;qQP`Wd7`m0gz$NiK)jy5mL^?ZuxNf`9dTy?^fo(5AIr_a$60*u9R)vN5 zrfs#uHZT1kH?PH+>K{UP_-Qqfh%dlHyq4WuvttB`r9jxI@O&%d9j>>X(933I7I)&> zaXjs{<|Ny~iOnxaJnRtUbeR>#40V%#wd47MNDv@=PviZH9$`+K!w~hyE(dnKR^7T<8ZHdvs5e6RDs zd|%>Hc6D`6{ybQ}nIr6e!sQh}nz}kvjJDbz6j-(Mdla%D9;nZsFz&U;ospDoC8)TS zjFCZpi^AWX%i!)5WI^U0JxhHJcaw-tD8;Mqlls&hemV@=_0ml2_$4aNkW00C*}Q*~ zTgWYW;mOO4flCck1F9cpt2%`qCo72Z_9Za;plN{(F4qNjZ$IvAV>w&BoZFMTO_gx3 z1v1>{mKj}CWxD~C{;wdMG zVt!vWSmJNXp12;T zoTA#}&i%-~#|N=?lXI88nSQ=`22P1WLmZeHBX;;Cffftn0Q}bQNFGrV&XUq@M!dEB z?Y6qGV<$mF@5c!}5woGTN447Y9{q|h=q@!uYt+zM%??ZTugN+H#^w&liG zZr(DIG(@V#<24VcbBq-;d@}mhu$aEKqH9@m^*d_yt=)w>C#bz{?$ToY(wyy>@`FwB z-B>q=&m=hQ%~Agwfh0!j4g`nJ*_cvkysoWL~S zGZNR4Uu5%9l<}pRMoOF^kuP+quXdT4dBt1STfO(JdRmnpXfk2f;UK+jzbi(U!MT{^ zqG{z~EXI*oI4`{D6lbglbB{bfQ*a^&;}I*uJFNk{ZDG0uP20)b!3|^qm`Ohc zGv&aV#2i0>ZY}^YV}Cl4A3w1HKM{Z#u2V3>ih(qCF2M>v0Wi~h3T8%uUbLNDgJSFf zn5jMmGx%#U3=Zur*g*ai%vgY%c=Iq`5H?g~Wbl6a(*)~6)!XxL`^<^iU#=Dv$xr`& z#!r8T><=E!Ery_tjBM^EC&#_98Bcx7|L{OFOSSUF=T}1UhP49?+FFK)Mu%~GtF`$S z+7?~T>+ddppPr^xkbf3gtTRB2*Ghf_%@vZ?xeTL{Buy%w1;OglnwlBO(CLejh4c53 zn5F9Xx;0V_bJeR4Bcbggv-{B7f!hNlFyuF1^cREycR77~{NTnSvkdwgBBjD<{=$zc z1}>53EK9&(eQ46ahgM-1WHO=e4vC5#$rBr4Hfzt92@MAGKYV@2GRGzRzVhqX-~kH5 z?6~ri?;Z2F_~Y*MAFreWSKGb5r(Hc6x|30hOY2K;>`Qu-#T%(S&4l9|Rx5Eue)B;q z6QX-cr=voX1PYGpox{g=ISjd>x;n{sa-I7&e$FXnjF9X~g8kf3sppXRhjHHxug*<` zfgXPvVO22I3l*)g@SjsUS_{7o4Q5c8G~Ngj4D(#Cs3_-J4oxi&>mv+wZh~=Ph{rTw z28CdVv3T$BSe!1Qo0NdUbrgv_7b&s{&T!&On9oT`1EGQfjKpwMGyPMwB^X?ZZ>yd)$x+wCcB$}85ViofqwU0+ z*HZCp`}J(X>@(#Z3LXN)h5HP@;3;6PhF43os}0t3WK<*=VxA|6-%4|BIMgp`b1RoF zjo*!I40M<3GVfR_Y`(9zM#FjQMGgrbF~{Q(^VB)a?aA<7FR!jjOPZu}LT1kw&RfbX z39anEUm`Tb!SZtNNJ*w#0|E1cN{*|vhtnj+5Xb6u5cIe^3}#x=2em}DP|Yg4I%=ne zT1g?`rBU4oynBiF2RL*FEIv`#TQI?wU1N9+ax>F@`*5jWS7GDixer>D91N=o`3K1R zC5nZ@C;R7huR6<_`RH{=s3-Z8`i?tpH2oXK;6ng`dI6J4yPN zRnh$^X*?Vw{wE5#;&@O=(uj2pGizNe(^a_ZD!5FCaWA?CkYzc+KM@D1h=($wF=y1x zuTi@?zVwY5!51v)_e&Ib$uOb*#u9*bHd&~4gf}$xUHgxvtUS`EsSNap>_=cT3)p`) z`$XJ`Jy107TyW40;l0;*##@UK9|Z91NRuQH7}OAi9~Tfat3kCK=YJTN7T3IAf)8bx z`Qe?U9NtG5gFpuG{{uc*6q~HDu$)9ymf=!>9y&a;|B($W#Dhn}_)zNWqhx#7y0iTY zW#NnrO*`JZg6zx3KJGv9A(0E-pOa(4_tGDFv|g;>P;r%c|HY>@%JI7mTa>N;Ol728 z6?Vz(AxC=ko%f=8sifR0jCF-m!)6jt?MLn2>&FdAFFM+;!6?@dh;OH~ojvtmN{kax zBH4al5?d8;5~RHqIUm-EX42O@f0NW7J$el$-J`|^{q{j4OhLxWn+m~cT8MewFn59;$mZ!i!{!? z4IH{mZcQs)rSrtXg45KpeBA%4Hb2~;hT?SbCr~ZPp0Jd7#z25ohfW&3zs18rFSEN& zrsHQZyCjXnn_Bi7Cyf_=XQ=n~5?TxwtJFA=eRt3mzy3xR={-9uMd!W_2XU$uKps!Q z$ytpPqx09L9Z0=2I4LHgFBWjW!tUI-(`WwztfyY-pCy7IREl?JTdH+q)6mtaR0P7 zLCDlTD0KALA=vW?mea!q>sa9-?-qwES|w~GSpO*GB3i?_XH|7Yz*0?r0-Z40IdYEp zS&1f{)7S^Ov@as5TSGI~AioYt$o!YgVh*bmkHi{RVv6(W& z(_|4RutjE?v{!$-&bpTg^6J=JcYiV4B;i?+D6;!Hi5OWWC!VmK$h=@U$f=6L;TT#Djg!O2-6 z3?@Wsc0(~_T$U>o;%`#GZhda0hO~3bY|f;3 znCDd!;TU%+cB4lhA`Eb!j_$HnYTR7d8C{!+XqZ{W@CjQson6y}W*Tg3E}TQ6SxPzR z+o&SZgT?-G7F8_2cP4-Ws>n?GtY&%zXX+c>>?H`dp1fLD z$y8vhwpYxsR)yOg-%U9~WTC%6i>u2yc@q05!=dkSTXff4d!RJb-VL%)FX=mzm|@nc zu~kiLzgqSOuznD2Wd_y_E>@VSrn#0UL1~_*EK7E`Q0@3wlJfrTJ3kP$+PGR3oPt8M z{#`BE@xQ!j53|*69C#oJB6LfXnC1xrf4z6uDxN>DPdmZsOWdo$q z_Fj-mK`$O$G)G_XgwJ1fSM1PTuPDf8gi6-hu%C~8NJf8)GQ1&$_XA`*JGYx#5^Sny_LsK>G)W-Bq*{1)83MEqF zfVtK_z|(N~EGEbIxFBo)#>sB~6R>2Whl^OM6QcG?%F*y;Vs8&TPT4(&x0TC?6uT|` zKE5r9>FnwE_yxJKq7c1qPrkcv*qN);)V+<3>+Kd8-+K~i${*=}Yl;=p&{ys9HKTPcmzhVldeCAWa772tAl zw6g^H`AKchiHm>JhTZrYzxR`b%HHhHluNn;J}Kuy9nOi}t0hTEQaoN;xeZqu#(HWP zNzJvJ2^n;M`}XN%uO)=zVqi3jvAqIHquTyMbS@hYKW#lIUfT-Iu=O3s{drZ9N^%*?oSSE_M3RQrQ`^K8sxM5CiV zKIPJU529Le`4GqIWvXLxgr|IWeyYi01AC#epyO}}*C~S2aZ$cz8i)bL_Z6X?g6Ta- zzVL(vNUyD85#D{r#RtqUvWfm769%bb+1A*gwG51hg^1(U0x(}U6y@$KkiWFB=pmQB z$(sdS%I836$k5}Co0|Ta-&Dqrjs-5R24;_GboqeUNxi~Xwm%K{d7QDt*6NBlt!CZ5 zPR{p$A--erHO2Y=Ob_vgN<(z>l0MsgYO|4*APRMHaQ0eXE;uG zUe6~G{PT@T{*-E9BQ$cjp-_>mw3_oO>6Luh%0}X3a3zkZsWI#r<&=}s!LWUa0}b7n+`p^BPy!vVtCEe z590(&abiqq@`E&#JnpbL!G<>ST6Lk0$U(DTN$Qu?V`{gM=o!%^nGE}%Q{~5JcDA?8 zrkgFI*;qfaBJJ%ZDePpH0tfr#0YtUt=|_{{`CLbHqX&Pmxe5L-o7)C6 zI%bR?KQLybhH6>vpcul)!9~cxLmH+_7{4#$!Y>E^G^?@jiUMjUXp}?6ZnyP9x`WY9 z5-GbnQ27DFWa5VS?{F5NA}^~LLA)$PVDM1=X9J1BYPWA3Z*_BRJ~N2WRIz2j)AK4s z(Ypf`MMVMcYv8rg1n=b1TG^Pc4IpTxO)V|^fHPlQUJl>&(#E5pUr4RXxftLP?l&mi zZ6$)aSRHAP)X2Z@*)ixgyHiaI+Z$NDJWQEx*ui>z=jDPWHTKTrbHJDidWj-X9|2R47zvp`siOCXM<%lChw`i#r zU1~CKKJ)$pITU|&XPfly#=VpgQAtBdu3^oZ>X9<@VZ&ZFIGA&Y#9Q+VFpb*7EYpO8 z5F3^sieb>}iLVh6HWovLkFmYDp~Wza6P3Zwk1j8KnV=UhjW6Ietb?%eXX*LjpVwRH z1Wq@?th+gPiITmvkMiJlFaGg8R!&HGXGk)-ZAP9d7>YGGeh6aOn1ZTPKk9Sv=uZAe zesD#9?fi7x?;3ZSptn>mW(jTl;=5V;GNNM}0-kpj?|JzyVPXQ-OeJtj!8w^Bz#vq5 zY+;X7t`@aI|MUXP)t!mWt0#80Lf&1>oMA=bQEmbNsRu6|mujHdV4aUuU)RF26Yc|p z`+BrqA->#XF#p6l@-3$g0-%mZg1@XI9hd;V#+$pJ#ErF|ow7Za^;0?6er_(C)X5NZ z;KJu=uGZ|mrI``l&+Z3io75whbw`ifeJVgYHuql^KI9SJuSCSLS_Nm*&IV9>8=B@f zUrEaP+}z7IfLf_xD7PF>vl!hYW_VF^|96{-scH95>CwAy&VfjsZ4l3D!x@Y;bVcBn z#DXYn=;t3bN)_FRTvumj^I0l%`2p)KuM-_k%^o8->|bd1XCh2vpho7svX$!0EyZ?X z`RC_*WAfv#ML=s zt1ly_A6-_LIAA8fZ04L)?sy{jmWAy6`6a9yeeg3b)&P3%)m0%a;E9L7pdj8--T=I! zfs&HPA2&G2Bs8!TI&_lp#dtksHUKPUS&TY9;JFm}%^&^nVd{Bt_xH8_Fii_2hH}_{ zbHst`lfx!|;!y{j3$c{GfB~`MPdflRkAE(Q*+TKkoN4-Ey+@ zKQHF&U3-Z@)ADaNGbaC;>lu8U_t0s0%**V&AF(`flrmj)Lq?Ztarq9YGtHQ~)wxnzY z(8~LTt9y8?=JpzYj#L;6t@2HVzeBu|#BUA;%&J-_Qx~l|;|CoAki5Z1iLtlIh*>MG zA#Ax!x}ZQTSZ(!L^+Nt8UmAMj+37HRehLhW63LZxY`>k4*^*^evW>IH9erTtO5~HA zREYW*iW<#|0H`LqHLSb(3x+5EZ#l?HWVs?T1| zZJUJ*9_g2MySi$+P9Kz_b~81~e^acl?mA_6%LAM=5pdiRNd%*FCer9}D&FX=7+Ga` zrbz2uG-g*?w!{{ZU!%nSvL#fM=JMs9Z{IF+)bTA8)TZKxsDD>l}G8;9Bfbeh&W#`c=jzv zM)}Owu5o@Qt$iOwwjWt~B^QmNNMn*ow5Wql7`fq_%#aqW3g2oyOP=yVQn{W42;%Ui>#eJlu1ZqM*?Hx}ZZ>oT`GA^d4fEB#-T+{irbR z#pD0k3ODAPHDbL)Y?VY#ZK!A4E@Upr^1RXEj_ydY?~}iKd_MC;NG{wEaAZK%*IOgcZc(W3}>tXILsm`ZphAmqCkl0-F-Ex&2Ld?{n3oR z33tzDIh*xIb=t5xV|Zadz4wElS(9J);u}sx`62Gh)g44SIF_63Z#1P_xdVuC7!2-ih&;rs-sZ>!aCcMX zwVsyf#;?0LtISpcF@Ha)Qk!W<1qFpBO0uQ10hnFWNkWq9QiIQTd%3+jq1Szd1fL|J zVCqqE_YW#ByxAZ`9~vrgRE?AvqmPfj@)i&zC2@VDq+H4l2S=f#@Zoio_LM$J_-&H56GMqY9eRqJ+I}q3j zN~Rok2$qRdpYch(OTbX^2_G_)S7vSaUWj-J$F7}V*Kp>jOcp%+5;(;g;85fiX!k3t z{luT?P%cbMr4+36E?Vi)RXi`cibR0CD&mWP*)9038y^h_u89&bTvr*%PpfPD{boYn z$AL@eC-$osf5+%2!)#umpciQ52z9KS}pN+vCIi0f0zmw99xQJxxvuTt4A(2DQ4(!6M3fOKGrxFT%~xA>H^1 z@;(}*F?{j5la~!Sr*V!P-{ey`?x~dPkybFcgX6J?->l2bcrJ##F9!xT;VTn))c{f9 zB#2hTG;M>Lqu+{g!1Ie;GY_BSABCM2+*Az#IsBlj_{|+)>#gwudyZvcd3^a%TaU@- zt*Vqf@{-?}3eS3@`|hB?!$}Cqvd* zr_iLTUQE?#Y9!5Ym=WK%)s_=ob2ukQX8w*=IxW;U?(VD$jaxH^0==!W`2dh(IUKM1 zH-Mg6fLtlnPD690X^;Oy2ENV8i9deEe(^Ep4i6gNUw(4fZ+zXW&1qM^V*2ILb*rC~ zqxI}FG&!9_|CCV#96_7FD6ez=NHgFxVe2XRQ16D~$oN4-VNXN`;2@G|@#q_G^!U+X zadxuSpNrf2&Z%SO^|^nZ`25GKfW#23lkF@_qW0$Z%6-O9^j(SBKu;obPNekH+Oy|@+9AZ@SZ?oy zt)BNS$5g$WN9hdb;jQC1Z3oY-zE;^!%8Q5JEjVt9rEd&{UqfNS(W`8vc)VW@co`Qw z9(~UX3l(`T@4Xd$fn6wXglCsk;YGI7dY*kpa2T8Sj)QT|pLa{{oR-Ukb0+iK@Ad*o znUFA~&(w(>K6W&1Vrd-e@pd4qCkGUU8ea=bdyT{$iUH2Ke(ww*);VVxFvC z)a<{RF}ZeAKdVYbq(P%@)Q=n*J-z}?G#&|CN7Z z?!0#~(pmpV)R-@t&r#oJO#V@wddPk#8O<9A!Qwm#(y=jCpb-7FThzWD6f?}^UvC{U zHNF!+lo~>yFogk6VhS=c_y#75iu4lvt1Fy_^3J)`N`-|P-?>fR)ndLHgwHQ7((XU2 zWf=Hu>R92a>ZXO&CyG`!i~{r7O>oCLpf&ged@y2PM4IbURzLUaT%PyQZDM?+C88~! z`gyiOomLJ)ya0V_kIg2Gg#-8i#|i}jL}{n5t#MJB6-sTxMes4 zG8T+)?~aMS3}YwhJs|>ptFSZYx+qusUwIF*3wS6t%D-M?xdm!%BD3z=Dhra{aF_}( z@7J-Kksw`rO?VwN9WykdzNoBzZ|{O}QV=-@X&^WIBm;jn3v`Xm>DwMn#Ho=bWZtF) z)mK5<;};Up(OWm>^LWxqt<~+MxYY;O#oKJy?zL%g2h?&O>8|K^D@|)Jr9E2=hJjY4 zY6Vb|-;jpj@N+`7AFRV>Noy7M7eLL2W@9TrULeWit)rt(T2?!GfR1?3BhaFmQe6j{ zAWmo8LGG@eo4>4?njaD5644FYtA3Q~SfR7E9(kNlD#0d0>`ee*bastDXsF!>B8JiU z3CK_6um0N2wAo<&s`>W4>)qXR+vWNn7TOtq+{CC!OEz+Lj+rivUob6Vgs4>8x&ol6 zF4JTtatsDyLRux&>W%xXJua`j{%Uwiv`N*~7nFol0n&OHc~l z?~epuM2w>_DF~k?Cg}6tA1(~$=;L|CnqyP?{a3=xVg>dlihaG(V*0m56$up9y3xHN zYI0@LV&W96uQA$vU%Y$!?*5^F&N+0rF#n-5{`ZQ$1V4fSSvOe(^w%!|9Au%~uj@`4 z0Dfc|{Vr*ZDpxn5bnJB{`$PRw$ZED+Y|T)dA4&TLvzu+P7Cjnmj&)!Lo|0xhgBg)> zVd_`d0|?FoKJZ-IN~e($lPOn+VBIuOdZIXf#i&jlGMAk5xtaG)&${goS+)0Ey=>m- z^h&jdPIQbdbOp{;qqFYPsr%}xv=ssnY(0c43Jnxe3a^xxeT{?S`}sXXw3-_@*T}!h z4zrfDn6l%Zgxsl<@a;SLbTi(e`E_GL#KSvILlZekEgRfD141*$=7j~e5{Wc#mWBo)%oGo!%z@MaBo{r0p*~@D;~w= zBo0z`agQKdU$>|m=1TdM5>*T09J-4n8VYj0Th|oQ-eraB<^Nc=iE9^oOfgJR8P3h1 z`DItpV4;(;l}g}1s?-GQ5%-9DlQIn2`$DCKf}>C@;A24Q>t@BWwaPAY=`f08MLE4x zGU?{>IIWZN^^lz3cR;%?J1O`f~Y-Zg8OV$c-HoAw%XAgz;?78RwqdyaU$ z;*bWW>k5E+2pK`pA*}}Nhu0DKIL_QZs44!G(9Rel0o{Yk(jd{3Yse^_HB$<8>Xucd zH_+b19~6XJZVf0MU21z@7knFurUvcFFl3a1L5sbt0GXoc*S*p+fh;Z;y5Bxp2=^;Z z8hYCs5%No{l^Vug!{;S{f~|MuC&(aDXystd!KW5_6~@wNi}Xd0UszwgvHuiT>B)Cj zXfv&$VWrSr$|`R34in*1P*C&={7d3BXh!MmE%3kt3R6P9zTbHG1t&L=xk__m?NxEr za4B}UH+KNV3>cock7yLHvAsr)v1KQ`>;oY%((H(P*Q~!zzP6-|&14__upivHV)ADn znIxTY590W8qV7X}%H4ga^}|Jjb8r-N^#cm&-DptsNzsySpj3OmmsN4ia-c}zv*#B{ z=tzM4zj6bN)+G5^Zxr&#@a4V1UG<$o9M&Yvv~{t4brzii+`u%LQgVas0*u{X83RNI zIlsQgfPkAhg}(gi{8>FccX~dOsEp`<8W8X6$qGE?e?;}~#dV4;WUzT&o)u)=mL> z4o|+j3!hBZafv{pT@u{G?m_OUOepvV)ZX3zcCpU1-O0ThTv_(d1TFZip)YiqfK?Rn0^n>0sQ(~*ohJqJ+vTD)8uMB* zr4z&;NOYMPa3$ysy!ZTn@?FVmktP$NNT%}0aM1z8m>*Tuf?$LT?fjL}|k-idb)nXDhQ^?nXSO4H}R7!=oC73G(fgc(xp~vVCGoL^y}n81oFs|-kfugHdx+Y``LYki&WKVZc7pf<@XJL zCyB`zzf_N=RnmKa0JMk_#sI+p-388|R{BfLU!eI_Rw}0Ynv&qW;I_x_ax@=zQmxr+ zpqF_3(kFYX>Fb{Na2I@l>Ule+K%enf22k&;SGkmJtX7inMHTr&=CKt*t|M_|zbVnX zRZrM62b9Z?EWIqi66_H@qMuzsOHnHdM2PL0gN} z{ z2r*EuQu#cNtx{ogwb$PDeSydA`8o06lS8dNCJ7`OmRamfHki;I9G z5aFhUAz_Ae8UM_J0jtvm1oa(!Umy3l+~? zyU!;-@k`{yd%s52es(XEY-67+6<2?WE9(>$3d;m$Hv^~lXLsj(4NOIq&)xlQYCwRw z+Tg8S`miT2T9UJ?&#)@8dM`Xz>|P~1=I@@mq<~G0nX=Rul6xez%O^IhzKl(A5RrD+ zt!&^XUkjR`z%YygNR{m}q;_Kp?cj8=Jj-~|Q(wrBcYGv9bM4%5$TN?}rtjyY0w^<{{xB ziF@tu#c6H--^bbuJE8lWTG&b49FbLh>1ctE8(BPfeq~`c=w)(qw^Z)yiD~Ub7fv3B zo3W&v<08G|6%M=}vToRj95YN!NJCBpTR)53P)9RB2(}n>tke25RVpmDd6sX?#mA=UQvo5ILeV+;%*jN>CsmG8 z+gXM20wLcGE_PbQGSi1A8qrbbEl#{9c=Pp16=R~;CUvytI;|de60f0#Mfa{c1bckQ z-ES#jGt(Xn;Sl=l={Y1a+&+ifEO_#j-cx7bh=MIK-P^8#+!WUqw`X;R5%SE8rl-JS zbx#r~Lw?g>0^CwAAdm=VB zswO#^fL5AdVSnVkF4rKjqSy}q_nNDdEudaD)YS#UHl46l+?k)T+i3OMn`v-52`%Ez zHruX2UyreGn~yrV zqu0H9PnJ-9@w=e#o#g)M_~N)^lC|8g^#kEtE6SsL>V0=ze0Y3AZ11GqbB$=W>*`ZF z>4eD|EcWl*GThuZX_G0-#j*)w*CPoFzOwfElZgUY6 zc-G(+Kv>=!_%`^@c>Hc;M~bm|kAEq`fC5Qi2K!H5(I}9Q```LLwwoHZ{DK`sYvXpPQ!#sN@FT+GIp`UAmyHp{Qt<+hAl=Cs?plSNGM@ zu!h^jlq2uwPo1|94OZ|kd$RAPq&=4#HZS6^^S^x^jtV#IA#8M@BRxMm!>VNj=Z^q! z00b8UU0L$qZQi=A--q1q92YUlUV;j!*3My^SV1KezfLPtcpGKk*j-)hWNau1WBAjl z5Gks1CeDHx%GY-qDiO<6zwqN;QLLXVTA9VGazu}TdfZtgy3ykU6{1m6iT%?Zo}NZU z7;Km53;vBZ>!ioNTf?z2PujOjfe^y{X&w^e|3Sn4j_}B>P3|i|e__ZYK5zRGHM`AE zwO(dVLHf|KC0Tq0jLgko< zW~!RTfz~2st3)}=x`Qv-$LCOx^*Grd6Oe29UZ6eR$-IvH6h_9Dgu%m zeYp5#m(1h-$1-SU`l9HZf?Du~FLT0E*+rDXdHr+Z;&^>V61l3*;VZ&KE%)x)z-c4~ z&55%9?QS|AS90>Rn#v9)I6baTzncEgNw!tvlB6f{TL@-19;fScmtpNDq`i-zZTWe- zg;QNDnN$2t1S*pb-fLOyyv6q}!7cf@>@2)+F+%@`~YI`oK%9wKR6EOU|(9w=x{7!A-`vlQd~mN3gSI)2Q%fK0e831zzuh=v4N~z# zAQk_r4CF|g!{+Z(0BrW6A4VT_skE9MVSrY4>-N1s=FJR3&Q}hkr(=F!u>N%70QS>* zg7Ss7JO9fPlRKxeu=5~bHW|CHCAo6)ka-|O2QKPEgIac zTwZ*l9>dg({y)=x$Vn=U4SwXtmWjszT<8-eh zrJjd3|1t-z=>Ro?;j;Kg3WZ6crNLZR-_y-bMjUL*dj63wSR?UY1m$Z1zBt;>wi8YU z_nUD$rjcp>Unq*jEa{b=Y?O$<(Dbf?a__EXSNyOkDEe=xYO$H9T`ewYc&aIZfLqV> z&IsZxG!a#dD0N_N3OgAgp(FiH$8I>ABu||_ z8mg7b&oANfJ3_s=f|tAWU%9iw25t0QI-VPQOt@}GAR+M7qaxzSh6TS9NSESwVxFW@ z_TAU7*;)|+3k5*V4-)EKR|rB&+p8JCmXGjTjsvX$>fnN)UBCj3p}X4D%0z*|-%u^v z&Nm;~XEA6UV2(cvdqaq6X?r+vWwq_`OipKj;SA{Z+c&E^&kz{OBb|#MGxB%L2J-&) zVS_X)56ro@rXCy97P>3j%38pZdl`NLhMiQdgxovb)CQ4WGOWBCoh)p8D==D`71FzX zL%m@8D)G@4`&py&$Zy_wT_pLdi@}&2oR-&qwy|{CqRgp=O#G75f0Hc;%I{?$`T1EG za1JQ!K?=_0>)Q=bKF$+Xh6yf>o4Ey=SVTvKW~0GKKK#gee6fc-ksa=PFT0REa1^(r z9H0taJ)xd##l1~1X3gfjNd*@JBgH<8UH~aRuNToPMLrA zzYHf}T3!FL#HBBKbtVwJE-$F;RWCSx#Z<@Cdhb06ydUo=Q-Rk5`x_$NEy4b4p{`|W zBY_j)SFk}n1Xpub*_kQeF>P1MCClSC+bjV%_3pw z^oemx*IW7P0a|17{ec(r3PU+F?Id9G4gNWib)6nX_%y>1xV%T~!x)lH`QH zE&&COOg`O9yWDCp!KaTL2!kT5b56nAp=%WyKtj{LF zRK!Nl<<&h3)l*SGwUfd~;Hjz8)pPkoELBtsW`Y^1sbRX=>FItn`4K&fv5f+Q1)AIG zri1ikFpWCP0M)HkZ}V(^)T}>x2*HnS(Dm?$sKtsbr>S#JjIBCyYS*WjZt~tnoFfj- zpq=}k!!2S~@{9I4=(m8JZ-{zqkAw;KJb0cLCxMbdc`N_u_Y=1qnE!tN)f3&iwDQtB zi|b5e${dvOUsbjZZrOE^TYvJ>t9a2bJ*}i+F7|ZIb#RVGs(62tc`j2B^OO{YjKnyx zH8vADzU*54aI1GfgG*Z1ZnV^1GfX*4HKbok2o=j%_~E(h(d#a+6MEL^Mkapv`RwZSvrALldXm6yM8=voTj9TdNU zFpWZIsoEy(M0X`P*y$_=MMLc}z&*p3`qEPwu}ZgwiV$ODqeB{Z1Lz>pN^%~Q!W zM3;WcRbc+-N6cPJ8^lL>_JED=Ues${=WWu1YTwdVv3I+Pq22b7-+?;^`=XE`jfJUw zDua1Kn>*b+G%Rwjq4oq~L0s#l>ikUtSxqLYPriSJf?&KgQ^=WFOGfCKW)YZz{q8@w#!$77y~X~{8=WmD+Kf1NW(}%n@+x7 zJLMZ~OQ)VQpj*GV$oyF*0e^rl>UBSD#tN)i8Dwi{%)8BUf&))ooH$zLt~Hs1p&YapM-&2D7N&ZSZzYqkS~A|Lrv_w_XVv8*LsUg<~uzu zv?1}iHXB84ui;?<=CW7!Niammd6#lUxpaHB@3Jvu)i5mKWfy)Eo?kkCJzB5$gm-Q% zCN~pAvks9OXa%Nc7LD1ff2>2|Z% z{v`TS<1pC?7EN({9PWI4KQ90)b zQ$d#po|{AFTMN~zi)KI{9+rK}m_;<2buxvWKS}>&WR|7b2MP&bPz1%Ekz8kdzMkE{ z8dLJH^Ov}bP}uOWvHs6H<8L=Q5eYZ4UM$_givRs74L#z@{H?UdPJ4BOy7jKLRE&K` zm!=oy@;tJq=jCLj`wR)Pzz+lf4ZFu??IqhOq_1f=&?VVLoBEoM8@jsG+o|0+N6 zTk_MTFUU{2W<+`JR(B=*LBBG~e7WGj0=fODwb)kcdQ^2#NxS8KhE=mo;1xn^y~G2=eJ_^|8XF=@Z* z|FJ;LQeB2>&$)&FM3?W%dsRCr?u;z6=v#XpaQtd8^_3K#EVU~=`!qdH-!gBJnEa+0VU7R$qdv(qCB?Wcn*XiJ$-9v;3YGU9`XAZ2Gx>&R?equAd3q zEB@ti5BK^!UD2-fZ~zGC0UKb~yg_ho5$h0uX- z5=lEiVbHEWObT&JPOHSiFknM#n5KS-bd5DX?YuF+e9= zax$M0@G`@5En|=na1|0Lov9n>xR>v|^j0%`f1u{y z)m6Ll?cc}(v+>jwujhQ6_x1D?J()PI%$Y&wE=^gzRCVR?iMzgh_-|DioVenGCvf8Z z3R|OQ_^r1dyPy6&AGlsSw=5>NM5a9WRH0c+>C4F3ul}C;YY^QFJRfUwQ;c|gP2sD1 zot1AzTMR(%b~r0&p|ijE1F%1*cQq!FQ-g!i!(noS61W6*P{;wc0B*RPWCy!bz=HwU zY+&?F>I9`wV4Q3KHjfg8Rpp_I8G&s{7L!g@Xd!t>4LFDEJEaDym<2eeCUik%8m!0z v9+VSLI-6;XQk`j{AUDBm=E8QZXpdj5XQqqldcc(N+*Wug8|9fZd z+&lBWA2VkTob&9x*Is+YZ^b5DMM)M1lL8ZhARM`uQfd%{2!2I`&{4tLsmJ&=ctd_C zA@>>`{P>`ohk?H_oL=g@gCN`v_zNMC1(y(|6+37NH?J@ znvQ{i+m-@K6Tf>uF>m+|VVeBNQwt-O8PND>8oDIiP`{ViT3Zbo{wiv*U*0{FzUi&giZU8}WO5b$ z?vm|_!X?6&|2|R+pbSnS5?^bk z?Q+VCsUV-YlS?el0STF-S|9*9;vPr+2({>iFDvt6w9Ff(;xBLKN_m+4)_I;(ZH>og zR9pO-%o@@~sC~jh2d9nDBJ}UBLzV^Fxx0Jkydm`n2eXQsm2|P#x1jBB=gVFW-w9VP zpg$3Rg@}V@deJ3-K<@iO%DuR=dDcjF)C*)8&wMRHH5ET9<3!qohc7~-dz!Q!^4Jju zY(hawPgb$PLJxM>AYXHoqV1gz)MnRNuc*az=ry#4+Ho4cAse^X5wDYy^kM2DZ3rQ` zMIx{K82GlCymezY{S1pzNS^s2L7L%1x}2GTK-_Ex zWPo+_%SYbJskg2be_Cmw9q-ze3+KC)!-JegPTc!uLS_XD$jQkd|>Cyr;6WuP~8vzu^XQ~&i1B+s zW695zspT{jG6X^bRSjWO6m`~3&e^aB*X(JeH=(yhN2afLpW_M&T1wjd-NV&@Z* zLQ+z%NtO`vSt!=SA0u4eRw%V;oqsKIgFTCL62bwhVIEY3W^AoNc}xHpwq3*%z&t_+ zJ1D($5O*)4zC{qvLil{T`*>F^D~5BK{0)4*>@qS7G_~ z;BTrEsQ=W8RuuAzK;53n|30I?XNDapL6NDocFiquCPCt8Y7S?}H910y;lqIVM(Rzm z99L%+J;5G)-PBtPJ(ic+pIDz^%hmhM$gQ zUsKsoQTY=RBReT)i@AWIk!t^Q#`8}oNVZu=1s<1|_U}5fefNHbCQP9s%ZvR_2~Dx+ zq#lZYL4|!%tDrIHzzTRXZ02JLXB?h7t@yJz$bbSJ?~SIc#K=0bh=bifmg0>`Wmm*T z5`DeV-1hK79R5q-=j`; zy9G9OQk*!g8A|w?aSR8f6%c1a(0|q{kZh~bF6w*$goj(N=8!Z$^~21kqnQn@|4^MB zcCxKwlV@BFh~7?Yd=Dq@$*K=379^QU!~wBu{GZ3r2O)B$Hp?%s|7($$1;xM^rr)oV zHS;9f_IXD*|A{&EHcGI7Yu>2r$SFTY&Dcj(CuLsje}>A-PvKXt8|E8Jt-r1#_|niP z7H=Z>!K(EiK4}X~p15MuskIVBOdkzI{5r4nSUyp@2lmo ztoEjQ);g&H>%q*l|NdvIBo8S0x?x1=XI}5T;(*F8EI`lj7o+}Gw8LgwC+}3;%ZKOW zO*>=KrkbC()_jk-2VZFy9a26=0%Aw9!*5K-OA)OS3`MCtwLE>kV>rD}`9@UV;Je%3B z5m&&5hV?(;e7do}AKi3!jnzHlv%)=@HsSqwcD3ZWFTp<}SVhq?P{?Zx*BUR+H@%f@ zkq8LObpzgUI?O&Z(K8Ah%pF*I=e?NxHHA3;xM}#|gXD09y5))h*qQ0+uYFBljdCS4 z#mg)H`G#*CjPs%i98mE;o0?toL_JR~ymGtE!pE(}%N@mskSx|{P{Ye4tx1$8o8X`M z(sSunYSTkJ^mdh{IyY>hfAsnJOyXN=$6GgX&Duc7k1v}Z{#@awnk8SOUfqCC+-zR{ z_bh}Cd2&6y!YT3tEdhz&RMc`4N&Y^9P(>6?{<#uDQd0H}eF*Add)l?u$4~c&MN3{- z{qv{9NSdWP))TMww2Z9*f4od(45puof$qR>>U^iu#94B~OpJfh6v zc}Sa7FU4aI{XUCaenBKhYi*_F415rv>t`sjKXH&Y-uZxe?91oRA$QI<`2MHN>;}KW zinPk;7#WdJu23MzlFZV>(=)?i!OLnqpR0|LVYo?>x1q67V=(1FkJF!K(R+(++d0jO zFd*m7YAZOwSgOWeS4s?_?ckzqk&38|^UXSek|TcDuJnwQaQ)adO+%{- z`MU*kH%Bo?NO!u=W+#PmGqSUhjW~3}lr8;P?U(O`$jHe0J+?`r(m3TVXLUzoD^Txm zsP^r+xEdZ9#$X0E*B7Z?7xx1-zZBr3iCiPH(BHn{yntMgouuBVkV2Rj$~&eA=}igi z=E5S@!b)kMYSbDS*mSEq+N2d!n*9P)uPZgXXm0QOTPI1g^qzJ0sjd_bpzHVabamgX zR@`b@JPv5Q>T}V9F;4Je6h@b0)nhTi~9w58*(%lnQTQ zg^1x&f+#K`=F?17*1(Ho(d#edMJFUAgfJs0cc*ixL^ zk(?LQ0B?mvFZ-hCrs6U=zoq+3&JheNpRo&oY&TdJ`TXez`qi`Y7`ln?j!P>NL|^n z!i%+u$v;qOQhHcci#&4?;Z&24T;Pc!koG4Wr@3Vt?uHD5moN1md`EsM zHL9;9*LrH1#+BSk%bbRhB>K1<;6BM2rH&?V}K3X_(CJ_3l`<_MPnP z3mei`zgwHV&UQx4zE%u?aD>wQl;fbIYRCkl$*5ll^!3 z;x0`lbb)l4v~}bi@8*_R&DVyX`t`TSLHjCA1Ik63guuDK?74Q`?O&a{_r1Ehy1d#- z8&_(7_~dTj{wC2D#(lSxX`!%p>5I4MdoNjATl?~G44EYTR?puSlGPaOJRL%yV;Gw( zlI4H+km?IzQF0mCu~v(Geha%PmvO(PykN${pK6Z($-Bc(E;I-)iK-EuJL! zY%cwJwJwHw-(2r?hPmi>f@8PvL&xj>@v1B|gjAd1#&7}0a)ID1GBi%;R*3FFtZ-~S zWToh^Kvm6$2eT|tENcJ2$i!u4TYR*^LJ^J1L^Rae*QduElcVB&bYFRyC401jcDT|4 z6WFfHH)jdBfcCV6;+oeOAqMVgSgo%b>it00wXuzPWGF5SGPccXt&C%<@O(`xFg=TT zRxq}WPuAzVA7G6GgYaoaD0U|uTXw3%GR*P@N8fQ1QU|<(7!ZU}iKuQ701|mf2g|=S z>sR)c$+AWAZ(LfcdN20oB8>3hLc3un)>y;RCh$H@+I=rb!*(HCi{14~rBF$HC zLn3-mIcdeo1DGHxP5#t~f4hG4$bishsecL!1O;Vp>z~-`(sgLcd==O7NZ@mxb9ylwptgZ&X=za^&A-u23u}Sf#@;+Qcsvf%EnQh^E87e+ z5Zc>({JH))SJ!b9u!`}pe{{^xF9gpzHoHtLR|_G}BCghb!%|2&XL|ncOA1~q=5aq~ zABNta8fpC;V>{&nqN@3aE#ejP0|;pTh3Ifnw4KVQH5)pF9whl+nkUZrdR{&p%nmQ< z!^9-)TNrmf%Ik$|Y+w~84p9u(J3Uqag~x90K)%Ifv)Z@quD565wet~^@4ttnrQL%9 z1G7}3R)25{H<=gh`>mWqeaD&PTA< z)zuHUs;bjx_0tXoV6@^=0~%IrN?Xtoeg|*y&6nMP8lztkkB-CJ#$Xq&VG$v~rY30T z1_g>~1pg+3s%|?5Z%7+sw0{CLP?n{+_%WkB2rJ-bFxz6avt)JbCh-BcOVX+@`L}2L zCrr;T(Shwc^=ja!DbP{h=RMur-Hq(_5-D|~P=j0C)A)AcnJwCpUpN<6ddzS&$*!#> z|Ni~ixRRu!q2VGkJKv4j{C#NX?Z=FNj0T8+^Ub4@n8m{B!R zyKUJ&?Kvp|)vX*UARwDxf#UX%CyyZ@Q0zB6dObA_hmTN!kC=gv$YJ$7Y9306gm7Wm z$Y8{4dRuZZq7y#iNiYaiL=v9iz%G*22wGax65vzN^iZJP-za9FFE1<0!{fetr+W{b zN2vGd>Bg7W*Yc5DTU(*&%lOvE2SQ%!oH8-w5x2KK+Z8NX>&H#@40CUfm>^(^|2P8C zJzhVe7x4L$emV`lFm2B*5qP_YOzLy?jAnn7Bqk95T7wy$VJUGSB0X-&r&#DBB+&Pi2wM$l@$15*dc#9sW75>&YnG^_#`>{xKQP}S?` z-fx2fSoGVC3oJ~`a>qlOl$QcAJeGuU2`(6Q&!6`U44^=@_4Vx&12{O8d=e@uoVvjw z^iO{=rlh873>om@!rsbVUg)K9joKyqfIWAaJVDB@xW@)=WV*&3k1&R|eTK*KV}eg} zKpht3^zL2ow|XH&BuaV#0dh9IV1{32d1zfL%N%4_0sm}XB%H}zX`QXDG~f8`J9ac! zD#X}vJ3FN{BG#6d!_KJ%jsKLd{5iWMVrFMwSuTS}ArioA^9@$(qh{sbV(KbR=-fP` zabLeaee}JojH98!m$*)C<}1JwDB>32V0E}2)m&FO5EbnB{+(=yCQ!LQ;YVSGo^}~P zQkuP=(EK4MH@7bLr80?|bL!W9FW7RAuHv`hO9Zp}0*RoAgw(HZfBysq`Um$U_vMo1 zE9rnIelYW2@W}~mDQ^!eLl{~T#QeEmfzvDws=|d`=)5H`#!VATrU8Pa*)j{ngtY>l zGjAwO5et|MtnSwPGS&ig&TjAl8vKK<*85i040O&d@BtG1gU;4wQy|XTZClDaa9Z9U zEHRaJEIZ0w{00EeY$hTR9TQVih@=q(B~-f~JOs4B+~ohLk@R4zUT-!NbD)6Ks(mi`dqQ~c`CWP|?a4;y^(2(XU$}<^f=QtHAG(A0^Ssn`D z>q1Kg0`GO$fPG!=k?WtPhy(^K14czn?R>o_9eJDF(=i9Jbjd)MzoI!hHo#SF2Hfc+grKmncXk z0t|fS=RbuCOIl!*uQhs57w-z^Z?3>)q>%S!l*-MUCc~upU!I9`udK%q>6=W)#w-UP zVpcK185|;T*%z1c>EXDagvZO^F&Lg1Ko6Ik6_se8;yd5|d*lsxB>r1G( zp%4Md;bs(Gmn}_6(a$({T`<6L&{IxlgUl^KDuC;;KQ0tkr=XzF4zNj>m>2OEc7u+I zBCU^caqmCeB3kiM04&!wut5F(eKbBXagb()Nm#_eWh|2W<$#fBs*=^0N+)lIXJ8RE zaW^;8$uhlA+Nsj6nrtk?);XXhiT zTN0H^nUmNoAbNNAoz#9}}XD04X$6(@5%xCBJtVv>cET1_fU%YtV z>KqXnS#Ej3zBki|B`PX~lssh(UwjoTwEKIqkzZ9#QOR?r6+GKt7Y~<`U2u46_K6Pog(ZaD ze>edB#XfDn=;K-`;Hvf3vg7)~<$QEsbWWX_F%q_Lp{B1H(ye9JLWZXN`cfeMbx~&u z62_mZRkDNx>)`936%{W+nUB|ke*bbNpo>HByYzc*`1SaMjy=-a^1brn;vzhpt}r{6 zaE|Ef<@7zFVHJLhNz^8ZKURKVREo6dxBc{U2NS09%@P?Y&3lSx9_mlRxvmh!_jmO{ zLO^k-xfHH^yZ%$9oGK2N^9Wx#)drq>7GHGdPDf+!BoVETK z46;z!q2qHk$J=|c6Trz+-4=BX4dL+^dP(o|K0O;{DQlgZ)7$6mkpMVnwEe@?N&n^p zXJEV0{Eun1Dmm&TDW31+Z17yt$>NN#|b+o4kyg{wNm#0fGF~kQsPd z1;pXYo13|APw$sE4`NUvN!tep*ZCdd9)KLu>u`|ANFJ#1hhDyV)%kgV5=2$36J@jk z98LPhYDguisnks=_ar-$C3$X7%}oG&$|r2}a*>p1bAD3s^~%dc!UO#m%Juz5dQz&~ z6o7mLd51k$H={YW{Kc0hRSDXYRugzjW?hj(1%8u(GFh#b?>0?(hD{1LO-gHV%1r^M zo_++G8_j{hAT-4PVdZ8MtzEU9)*rsHiEvAgV(n*9|0E%Wmh<`rvmSs%DuXwfgtenA$*DE|b-< z$5lx0St;)o7z@RflwwqFl0sZHtg)cF4|QW=SC^N|OOB0!+e3{3XS-7-3$xEaIMUR+ zE5%EJ)5fT4HoQ##XRbzU=$uL8A038<-&MT*`P=iNcI)af&)9SuhTH4BJ&gBikI97` zgt@s#3CJiT&5mWAw@0P*>)t*{evo{B$D4w4Dw(Lt}x2j^6z-P4O2f){K+!Y)gB$?wltP1-2QNB0_w^>?st<@gj%yeKTL{0lp4lL3G@RK8JQQ~ zrWr`6#cbhF=NBMaR2r5cCl+9{Uxf&C_y9g33A0+z z73k`>ub57Dd3AVk2t0Z6+&RVtB@O*|@C?YWNs(AB(Nj=FPy5n!T3C4abfWq=A4vtr zBFR_j_CNm^V94V4LM~Y|Uzw7U(j|;gOerah^~-NZq=#zpH8uPalI>1=+6^ix|F5tl zI9S#x?Ky!)7z(8N2o;y+sXjFC{+Nx3!+|7iv8+G6Rg(BHFsz>vlpr)R?*kA3aMKjR zY19A`;(K$hC4|Gk?F?tv%F0SOh-?c}Ll(QDD2FOH!t>SO=8IG3FOHMEv#V?8SbSc7 zDk?!TJcRcG>Bsg+9wSYmX!*v|UR&VJz^n}1Nbn)NK;*1Oe&ozr3%|CqBJYZ!EOVof zGJgy>yZw7{kp_#5?ftiJ-)0ML0|Cw@W!1$N5^~t1XSjpQ*N5vfq@&I+Sc*33RUm`) z<)m3+;n&%?QgB#*n5b+D#==vF!HWtak^Vy3rtT=Rv_K`M(<#ZM|87xUm`B)LVbF#= za>LFKU&Dst5JI9o2;n&ZP&_EyFt)XgREFI=K=Wtdh6kfaI5gbEknwmMY!v1BuS+v+0 zc^HX2Jw2h`=9Q_V!uLS!vQ)FW(6M60L-U!(^XYG6$>KE35LCFpF-n=o!3||36do2< zj1PFhz`z2J(!zQ$f!$(Y_#pw#^M8FcQ;-U&N%oC$(>{xZy(vCUZJAplK|d-h3&#h$ zx=;b?$ye{DeHHj&q*jiI*PbHi@vFe)tu3Y$vVvH!)wsBX1md;5mflDPj_mg49_tt18|3x_4;q2lK4}IQd`Q3kwSq5K~2UMSr6cc!*pwCzUA|pF3>D z`_w`q1YQZt{@B(gK_&8*{ASIx!rz|?#G|ruauu`I7=43-ZSU`#1e zvzjXvVVJ9yJlSNsL~rdl&XbgSC}(Z`1eaDcsMgn;fQ&41tvg1iBcLpHZ;#b(shVW2 z#va7SwY7A@na^gcZDSo4Ooxj-u@ZT*o5w7hWSB-<2gb4EHqAF|t~@cSv~IZRU>A;8 zplA~6>+NpVeZD_k?aBiX@3_+MhZCg+Xt8eF5lIINBUdA(!CfQKL)MScL!xr!GO12!G{k6!Bf-Q3Y|Cb!RA1`&X$~dcf5R zmYbiCzB_dn))`5*qPS`eMp5uPF1Em{uWeT+M^;}-$@y;OI$RD-63!4O;X*z_doW)Y zSXHEDdb}$Ave8#>-=$bnN9Tdt!E{WjP<4BEH(FMARC`dka{EHN6M5`djYOEa{LKU|p?{ST>Ob`Rm&qsbx)WA2Kgdf)BcWqhYB zmYJO`wV&{Z{_VSWT>gvtKf}>GLB*i;>SPO?#U5N|i{61G49GR2H+rA7#8A(rk74Y) zJ!S-`xGUCnD&2O=DvJ8<;gl6RT+lSvdR8RK1wEka^MhcKdiR1;JvB-kF$pQ$b9y}5eXeV3OEC7O}0Vx=!$*YkOJZbk}+ zS76p+1%$C`Iw;|8`oNx$`Zx9z53Rd97ol>9X?a|~mc=pBs`ajGnHwUB6q{!!zGxQ7 za%SYmbN`FweBwZ(c7h52Yv!W=(7gTAC&FUwCTvPSomk6UrS|N+G*zKT@!h~4vMK=q z*JL#cnV}t$c$4c%C1Y+@^dc2<9CzFa)jhLK*YxEWFEm1`LTXsvSvhUt%S363+?S-i zyaC++wFz;7k;yZ)AHN4jK6oyW{51yafe!Q7*K@D%`FZ{R>mfAl>jBd$buLdJ6+M|9 z-L##3JccbZm6qd58yUP5=?;uWyf{UHEz4i6@Ndx(zh1YF{N}}NICgXUP*X`XdoA|& z%hC`Bd+YCU7pmsO5Kta+TAsBBEmi?fN;4E+z0wdu`OhvSKqdipvf{0MzRwT%Zj*OH z##c>&%zG?Z*>i10ql4E<;JUI|(2pV@|JJ!&R_Ok#)t1qTfAbeZl`zmdU^=uxIqiLg zsx+?@I!Vlecnvx;q|DFZ!Z&5;iNWunuO*atU$AKxOKOiaoNH_Lb9)r|nNLws>L-SV z)YM@yX8gNAL3TlbYG3{+Lryh{4pO|PW%C^zTc0XGwGWiz%iO$9nc!Sx z-CwAr9D4OF^SW|z=U`$uSHVn5g!IGFAWq5KVSjQV0J(7U0|yaYkCwiVO#7daPChp5 zC{!nNVro{|KP{#Ku8^ArsIbT3Le0_sr_RFE<-Cd!x&|}JQ!1L6u)%0!%kCG=+o_R}fs{El@jXfagDy!OAjthf5LPBiL|mDTnyrzn~x zMsoZWg!zOK{a_8rgDU6Me1FJ@!~9;nuZ{Z_O2RZ~1_Ld8U*Bo%j~7RPbhDHc1SiGg zB9&kDwfU{IMfBIHwt(>cktR$0yvs*L`3}AG8+pf zju$p-Q3H^SA>MVP*Fw?5hLun`4Im&2oZ*!UW9+m^j5CTGiV~x1|BES1cw<6+J)X%z zvH~kIF}x8&akO;FzkS^DyNsLLXVvR5tAL);S4Zn7u?%!QL(INvonLP}P4sgo!{ZZrNA6OuGOz+)8gLdWSY+UNtwq7NizjXd!V=${Nn}KQUCILG*#wPXzyM*vP>T6_^#0-9SZk z!10$!+=ydy)vte0T0+f;Xaf(bv|2(#{0F28vzV$!GYKFbf8tXvM^ zfxeHe!k0eWMy~UPW`~#WJst__7qIU^#Rx}9YShfFt9Xdq7dOMUSDpp4&H3L7BGSgC z-z?A9RgypVVFpEsobJ-iy?`yzoj_o?vb!JmnOK?y#+nM!{TO5)6YI?e7NhwzH7v2sW*s7%Gh+`>0vG>rv4`l))OR@ykjX`JfAg^6glo zSEPVJYW*`j;iG1_$%N5gL}?*3*+`={Da?02YT651V^=2rpg(HkhstThpFR2jkCejt zKgb}##U#tp<-JUnsu1-LdTPLO)LKb<+SL;N z#{X6@Ie*(;&(!*!{LujcIpXrGnHLstF@8L-*h*UF$3-p@^0l+ghTAMl55;6-`MXnj zqTj1iJxHbp_h|&)2+;MJnYq;}Pf57nsZv)pu$Ft^VDMXF=Ztl}E-p}r|3|P{(=SHc zv-t&;p>`6`=Y(FoQrfX>sRu`B<13TffP0l%wt_tKLQ zoVb;GH=aYxq-&l;S}socTPI~~y?T^K7NepkHq2sdoQUEv#pw+2Wqwxx4ecL7%`|Y2 zUy^5&hAl9p2qt}4tN1P8dx=?UC?BX)#N$Qt$U^tw;&1WQRP18V(FPf3jnGZ!p$blx zMJI{-3sWrPUD$j!!pBC(?TC0Uahr&5@xiOh?9gcI-rin>RFQNrD<8ZRG317|&L zrWZ<4YDA`k!9W|#-qP7>%QHpHkJ{pIleK)##J1D55~|YD@0QzkCb$gu2>Sh*^G!jrATm|#^!-sm9oQ_yQpd)-zsj<_A!;S0&RUN22Rm#H4I-p$w8vd=kG@d&F=H8+TO+8xfcu%a6d!FdB>Lv)FN%yyA zS}9PgAg81Vdf$q(VrBU}5+luV3X}is38&COQt`)gND~Pi<>M0;D-F0Z#Shmhzl-uL zD4Ml?8K3w$bjeTfz0ntH37H*(sxj3R&Aj%Q?4s#77ZQ%zz0TTlV@3P1Hq$pKq6aj= zKW`;i6$DsV%ncwT$U{xDr4Vk+3T-Dc5We4Mr%9(Axr;nUvL)MV|BjQFT{g>425={r zt&yL8YDUliq`v<%$cncmv&q2e*%rH7j{X-CH{^gcs5qXa{HZpi{a+A5 z%{LWOr}pca&2nO0ps}Up;?^gFIw!QE&3);;&pf331CIHiUGVdxl+M3ITb!V0e(}#d z1xP!NEA<(JT&K3sPcmKqD$FN3bwAqJp>n(yY8`|#i$(vnu%6=Qbpp%L!ICRGE7@*O zQ5BjKBR$I;JAN{enl9x^;ll9JC$`J4Pf>o}{zb#qX+-(0^SARZN?DGc~v}Oc3V4T=~EfK`66cjEE&g1he+ql ztW~*JkC{jIugc<&S&+*ol45T7G*&?I`fu$Zn@deCP>0M}>g}{;{A{wgOX*zJBAll4 zEwJsAS_3ds3!h87&*p`z#zwvwxmp6Ipz^&XMYe+a-^aQAfeUTc1L5bGC4#&vAeToy z?~FV4T{~&p2BS~qBr)QiOVitR8j@Mi5mU&xiVnd}bR^y6n@(^ihjecU|NcL&2pkr9aghW%c&vfRZBy}KtJWqPEY z`NsEG?6}qizDjzMn%;=w%y`Q<;oM2M$koWV4`t)Q`-J(}6&hGSRLt zQts4NkweCTtIEvjP#eui-8w!#*;RRMHUDC8a*{O8evUC$mc!*>6sNS*L1w?10Rab* zV3-bHIffz>#Fu+8UoLQhvih&CkIhGB?Wua$!6~zyonk$w7rCP&ZIq`CgG@AaY-7EDcf?`S zi?(Br&Kpo9=X70jFejdGZx*UiZ4i@`9zXBwIC`V)v7Qj+bcm#~{@o6~9+}W;sqv7> z@vf4HZ=uH z=P5m2-VHv17o1k_nyzkMtba*Z&Pu|KxUk5_tFP)-wq##X^2@NwXNOFoJ^Z1avi{23 zhYXz!&$I^Z)-I=hwRcXa%CpY62vRqBxfiLT-iJ-bU2fKET>30(kv;=WV<)5~obE7F zpGrHDHim?IH&Nh7zAydII`+ff*j^8d^_KdLd4bT2hw%x1Q{`^xKyu3Ia@~GKfWBlm zQ$6hsA-7uBsE$yD>WT%vFB!cWm)$gr`(Zw(iIG-l-N}+I=xS*87Rs`8QF_WRs<=y|dHj zKu9GbKAtF7grBHSt5n^EbbYCry5&Q=8tJ^;)Z{(EsXYZV<7<9MO-u@$FEVs#F4u4b zT7HEnqmm1HK3JH@x*K(99onfrtd5Rmd;H3eG_v`QCi)w%MmFBN$yz!5&#Zdk)ZhdtR1CGC8hh zS=*A#Y#oBcFp3dJXSQYT6TVs<#Aum7F6CDpx6(^bfAFP)&deU#8hY3!^>&5yv*Ys) zb(2q^ba>S-;?kkZYspG?VQDwtk5-)w&Av%5RM`MxZblv zLh{9A`|*a!7Qhk9`fUh5@woka64tQiEjz_w?iKuDgC)Y7}Ayx!ZX)`RUJX z>fRrhf%XFLULTbBftFH_X(RIt?O(9zOrAOr#e{uTfb8p{E zX`I=(1VvRrrBI06%TOi+LY?|x7G=!tKZ5Cxd~E6zdSt#M%?-&d?HZMQB@+F5_dGLG zNFiEJ18)C8cD+RrweT(%ka{120TWt}t9c<0;I43w>PEXCi{n}CA$S3A1p)#n^F%4HF>)79$37jn_1KXu7&x9<0~?w zg17m~CMNVssRA*Qq33Hyt#<p%vs@lC|{9*6?Gu$q=$8A5_ zkI}@XTbAp-$np60r(EKuF3FnzV))S$MwRXXmumB-NiM`b<&;28yS`Ts;ddR+MM3}2 z57Wr0@1!v!*CSo|I#cQrd~g5!6zumqW||-Pd|^L2HHCLV%Kjp-MP}4g^D)OEyPLyO z!~ms#Yj}`ZgkiJy+V!*B(={9TYkGDzQ^QJt=5PVez|4!>-Ys3aNQKq#8Yx%wj(Bu* zy?A|yxi)lUx7uuL8dlB~7g+Gs!+W(t=Bymtg&9#Hf`pj}1MtejieKv_x-~sFp(ypH6rbXngzNu;GeA2A47{o+*Z9Qn_2n;OP3>p&SdRzd}=pMqVea=Wp&p3-ww- z!LX2R)RyI~NAPV9cNzTd8DJreAh?r4r%K&>WbrVUt_*$2=cj>gLtdVuY#9@1`Mj{K ztE)>A*+q)-e2YK$@arZD-*v4MjI}HFK@7i<{{tPJIL|SzvB8R2J)hu)lM6gw;El4V-xc7-T2=VLYp!^*wxh*nMtod zY(^|bc5GPCCJzD9{+qV1-Fz94plv@VZE|p1M)G{!^6>l8@xixy36QotowoUVTKv9d z@g|mGixCvo6n(v@Kz@t)(IU9Lk~Fcvi(3-W_?Boy!H*c{1S`NVgm)d>7}n6)hd6kx zH9+qU1QkqG=1^a%xA##VHi#))FB@rUhDc*Iwe)Z0>CS^bJg_?tlUeir?`H_|^6P%> z)fE-3XtZ})vvGZCv@gZqYQ`jJXTr10M2;(LKSdD8U7XUbKs5F#uL*-W^eY|IUdCZWYNz8vuf6 z`>$X3+DF3M6WqVd6ER3f**)>`{mB?$7{Uv`y{C~m0;S&Yf+hjbuK2ECy79%}yHmL# zMageHZfd3r?&pdOT~Xvg>P$r0UC%6-h+@pf!(IsZE8Tu55W|YK3OpuaQV6eFU4Nd^ z#CYzZ^qEQT<&3G|@JAUS8~O4SRrxrFU%Nz}o}2h3)uRcPTrQT9mPj0rBR>!9^95>d z_BUvxr=K_aeOD6bR^@V@j~DQu)3linkI$2z`K*&Wk=)NJ9*{@bW_uL@gbqpR9Dci9 zV-#ektJEP#Us=BUJ@r=58t^Zx6aI~l&g$+DmfTHe6EJ#)hT=)b=81`kmxHb2E6i?4 zt-0*&?BE`#t@VY+4b^07T@a6T@19|PAwui>OnPC^+gt^2j4b?gz7U+IBVY06MR9`T zeY;GQ)Y@aU18X$sw%Ux+@H-cEr?fP<5?2I@Ne!@G?AxYy-+m^YAyF9bsmBMcS-A@O z5K?l`Gdd#$Qud;cmHfM97`KS{;Z~*c`w>BI+o5??JpgDPUcD|D4-!EiG(POE zEIH!)CrgK)v)m1^9c(H=d?vQd>Ac4on)b(R`AccSv6OW?L!-RM=Tc#1N1u)0v2#if zd++z~vNAT7*vw)@AL%gCP)%EWziMZD4o-nhM3K4TwmYHC_cF6 z#lgoJ!3|K*pn%8!jlnWlVnxK7YWgAeMKKuN;a1e)2O`+BB4>Ct4htQpPl%$=`tO+o z$W;AkZy6y}1P{uVr%*SP4fPBQ4fWMWTB|>P zIma;lEEi3pz3_JWhL)6CB-)g5vHFse>%FVAqyY3sI}v`{{3VAHJFb-}BGY}RgOZe6 z$TV9B#Az{$Mfd|jUP0lw?2D0#SJk}%l!l0oFig>62`NeLWouFgsQY(BL&JNqwGkRz z54N{2r&u`T>s3{{hB(20ju3ksu@Ffw>3#pz$r)c%x87D`u^%KoPe(HipuN5EdnU7P z-D4~(I0&iVa9m8ee_A&rhPAPDu$y)J?brOknSWx~-{jzHW`D(xB~naGgfT099HNPY zEWE6Uar+&yX9iySgIO2^F>cj=w9CNN+-eQa%4`2`;XSNDZdU1Tm@n&-1Jy%29S6S& zcoqiP1x)+aAK(3T%;~tMqz>4CzGmKDHE~@OS)|``y`T%IgR*IjYJIi|_h!z%C1}S# z7Gt4Wt68_LbwG?nsOK+cqF^A_5Ws_D%Uv-7Y0b7uZ%oPQqHLxj*{#!Wf;A-dZr5_qeM|LqFTNXjI zYAGM)#VdYE@Qnv*#M^=AaTDKylI%U|+o2x*j_*dDHLz%^yM!Rc0FKRm%~jJh@E=Q} zj1CCAOwSRlP>iGd(}7hSr9p|JVubM+c>etmq}aiGa`vGp>xf>~t1Jp`oDyy3fAI10 z?HIz|F+M`l;)vMSFVd*T%@h)On$q{1sJmii%eBoJCWW^A2)a{+%`c6wBa)*oYoHqv z+*|HWmAqKpj&E}o6r06)W*Z?QCI;%V*@~`?7(c5<(qGw&jc}Hn`I;pT;qvi~+>8%U z-{0Pz^l_RSi*HNW!7&}@bU2F2b^*z|Gkw*P;HU1dO1eYoG~?nX*V zx};N(E=3yY5`jrGrG})W3ev5BAl=<1HA1?(yKCID_ulXK!}jUyAJ2K>_vE~77xxWt zmK{J#eK@~~=wL=5NS2hr3hSP6F$II)u}FentNwPLEKu=veke*5N#V;B55KKcO`2iS zcozD*1`)dqyr8E{$WGYDBc1QlBJ6Lb5#B&tHv1CrtF{;5S?c2mtF_Qf+eHrp6bq0> z0MNWe{!CUm>!ngzr9DZx?RaU2>+L1&GhfqVxP4sk8PUXWYu>wXif64)>yY(a+;9E; zWpq9)QACW^DLSjpP_glC(etJ?F_9tR=Jj>>LPOp}g5{2AWU$r!7lJkx(CW|36K4=V z!FPI0-^O*W6>9QU<@Zl>G=IH{QkZ)4%^m)mT^ylcL${&~A^@E?yagEQTZnpthw8iB zhQv&+x6+8x=DV2Kihdd~L~1fiFbfNd97;B8_SJ~|n^&)iIjpA0O+80F%S#tFCaSY? zN}8GU|5g7a<9Ldt!v1`RCmUT}g)#Wt#%@s-<8s3@kj4FQP)K$7>MS|7(RWiEo~|JX zPrjS?(J~EUE^N3M7;_c3ol1%1qh_P;ofog1ZnUoz8^$I?%}m0v3*>|KYM{-gXY@#+ z;s^cuK{sH^OR>MmM<4dGv#jBH3|3sI!<%Gw2b3&b{94B;g3%M6xi3CHB8yG9Q&aE> z2<(T!O*G@plvG6aadCR-z00`sNf*kw}&GH9UPUI$%unQ-#exnQ-oSL9*?z*Wg3 zhEx<|3{8$>BN_bl-DuA(nw;@{zH%wqQ)XsGfdn4Y!C^Y3nur6Go+K+4f#U>%@1c}TH4zym>RP;sh@2jiZDUloSVacF=@W{p!pWb5Q{-x7Et z1eViYthx>+Nhm%+g`=O@waq@*+0k{6e1cJr`;!soJ;FFII&b(=2^q?L>EwD1T?d@g z!LQWXc~=Nv6wWl%G+(@HO5t+GPNk19cg0>xvh_-rcHbFBDH*o1i2I}p5IhC+&3tQ! zRray&Sr5$J_HV|(0oz3z(`!5qR3T6b=vnTvT&UB9?;6EusM2b>^PcV6hkwr!f?paj z1l7CS0g*!Ph`_DX)1PCE;QbfDLbR-`-Gh%C_M?p7jjP$%l)S*EE!s7XN}L=LYBV|6 zM6XBxyD0`2P|Wk=4?aFFlb~_aQzI6?$oi9;|JemmyuSEl$@#YF zCpJt+*wCwHAJ^WgsTy0(s30!L$0qkg(sTEzg#X3v53zKu*LW6BEe@vC9{a#DU(sA7 z(u+*vIB9~b=GJ1E>*M>E+`k3iR+w_V1K91r>$;2^{O(^>OZGP?JF_%q{`eh^t4M0} zq%&tV^KO)W@mpRAL~yJoS^?WjY>?b;A$iW4Ni`&BXw&bkP%u9-okA4jy^*;+C|(C; z3@z(L+O>iTi(^2^_Vsn%5Ed>WWw*)?c2Gf^lEc^cHl$@*ByuSp6MraG-&z}KKAivj zn6s9P`<|N7JT2f*M0T%L(Vn4H5XjSH0rq6 z;*1M4FXn(*SgT3sTG^*&(HQd}zn=#WgP!T7(hYy-$%>39_rjE$1kXHo@_l1O*(u#P zu37bv&V~O9|BG4Wz=qXPhQ}!A*U`hGb;Tz%9sCa8Oi=hGwXRZAQ(n1F|5M^`tAtG7 z(llmj%dYb(ctab#2YBV>C^9%sX6>Xi6l3w5@yqPJB@QG#PCtRh&@3JWM(r>J0CptT zKOUn7<^H3dsI0?~GlP8FzV3-$Q|vtgh|p^J+ZH0c)^j%7T%&wl`GJH_9pM(y6_WW9 zZxEylebgdF{!sCyktFI@J`K>W@<1Wg;bJRsR=i)Hz{E-pkVFvR9Xtv4wm-N3n^9 zbHrs@U&U@Fft$r&eO4&a1oaQtOiarw*w*snoOa{3zgM_u6(VlFM6o<2r{SFIFy?$F zXDc7B;QR6ae#4ys{>lbwZ*R|8!wp`N2RmMUH&Q>J&8_5PVyd`W+|_AgIyC`Fpk~FN znw529%OwM!Z)H^Y9X(rkpi(ZY_`pK1`=y=d;phC9MAXKgL?OjgFnO z<}$UuSMNgjYyk`DV#&5)PKc+MDv_;~@ zWH)uW>*IVYo_%FW6S;Cz=mCa z&<1vhzv>4LBEQ>50?T=7#FTxEbwUX(RbQC|Iebgcvme z>9E(;e}5wX`xUTrlXZoi{Md(VITHuidyVti&Q9CEQvy3ad~fAAZ7qm%06xxbvcI5# zsaT43%+LSKSzEKBjf}yZRYldcK48V{6^1;OxEcMa{IXXRr?!UN>vi%X$}2Ua-(9OF zpfRs5<=8%66dqC%#a`5OKx71tOm4tDCT!sV6Ch~%Q%}OF4ABfNd943CR(v8eZ!j|n zIdke~_@ES{M2oHb_igC%Th4O2KNuBj9}mpbRpKA^g$Vl=@1|WGB`xj9`zHB8K27LTc$YF*-e|)+8bB2N)gl~i07KlonvxYwi>RL@kpJv3@9r949!+LdFDeGd< zx()sF0c=Aa9{EZy3ABv|C^M6U^<`@cIH*fFPS``d%-oAuMMb3xLK&c8#`3@Nx&X=l z7>PCtBCr9@(6y~HdZJ@=vq}w1i^xE-zwn&p-}d^MN&O1vG5N_cVIJrF?d!5*7hw2H z>Dpb~b2(v_PeTW@-i#WC!46x)bbtcue)%OPv44<~eTf`#fK)p}Y$@QfTMD_g1c972 zLi)a}EV+cPbFCdN3Hohpk8cdWX-BjW@=MfkAaMFTcKD6>)%>HQ$BDEY({}LS6{L zh{A&GzD?@8%DmmaQ-lFBsaN@h1XhGvpVzZO9@eQac(F5f(!baGM0O7i6FF<09i2M~^9p3BSeSq+>B!tCu(o1gqUur zS(O+nn<` zchaQkDgc^r_;5g-%4WPVf%z4?>`*(@_YqXf)_A(nbTaY?p#(M`j-gd7TnfHhvKd}& z5a)$x>3_@orWsy5$caW%^4Ljbl6b1|o=!I(67y_wVbrLi zI6az98tWm+xQ?i}EaNiZW>z<(pM-e3oE6tbTBUVhG-38#HXE%8;w@ZZ*4Ylyt>&p4 zg$b&fR_l)f@1`GTX8V=ag29Je`Es>i6u+usy;?jvX!gW~=Ba&i}GB(#e5fas~`rb9|%`}V> z#&F{F2lx5wHqo|9Dh-{IrdtUKuU+n#N(%&^4ShUo!B$NaJFooYXT!p}`M@JffbF&bR-d!JMoc(-0+Q7)3p z4XhPz4Qy%d4$6Qrgh-c~G^XIUMjnQPh^WcC-@vtqKA1{IqgjCY>B`)+jjg3Fa5gP1 zG9L?^L}=F#H{9R8<@C3Mkm%H3Rw3xg+llgMD7Q}QuY(_<&rmo`vHnvyvEAQ;P?^A3 zv3R*xH>ZTVs-#OI{50DE!*Y=O%HMYfGkCr3vRBmib822r?E&8ZGv9`Hm$&pvUi&EO$N?%2@K!3F(9gI}BKH(%V(^LXCsoK7OhD{>t#zv)3 zSZ*X`x!naP>d(M(?lSoP6Ofjqri#|kvMm2{_o!ZYYJ-$7>6hpyrdpz{eCndY6s4ZBqF?g9 z;CHzKlb`T`eX_C`Gcu1k@o8?))jy%RFl3;cIR=KoY-}=DE=dLGdn(m!q+R$J_j=qZ zqG`GX{X4kCo>b^PE<4=aZ94Kru2nfw4TY%kbh-ZWSDlL-;3=uAuj14OG)BR!HTzc% z>@u(Ni&`*1^-iTLie96*hVL^%`LtBTUBZx#)FWRxC^YyRlw74}Tp!Bgf~8FtXb+PG zH^W$@B(deSq^z{=P572WN#W@zHtFF4;IE}6ML@@k^dfW%)yjG;s_V}7(8+Zn09MXn zD$pABa~DtJvGKPR3K59UgkwY596a^O$K@e(MZ@eCM-)0cH_4^Fc1%byQUa3`|AhgF1=>m(ERC-`BAv;<>ij_UzyPjnTeH4AzmFo4dnf?V+R3ar_VMP-el=27 z&g;?=NH2Jz{yg2D;R>KZCJSq5~m+W!;z29+* zWVG+y>Acq%VIH5jww$*L<`6>_nYF9ji4(`NsS06&<&sa%_w{m3e%q5XEXBKH zro(g4&z?Pd#!pcz@ouDy=}7Z79`aW@s>kmrT{N0!UMgKMF%o^}?#5<;f+jR%Ot|D5 zoDz$7KD}L%i_5h6N+WO8VMLb|DW`YwjQa`<#-d4PTQ?>i)cF+P=Z6OPR`6kKOqnqDv27fuO+MrmS(f6E z3=MLlUynn_$Fb4N<1nbbq+H8}@1O&>mt_9v@b|sW1`o`A5#GwapDrVQL~JTs(IhR5A9+aJ96xwpFF$y9 zEnuPk7_wWN-TScKf=SQxtZ)BF5#U6G$I!Ew^>Ew`-Q)$kwMlxqk6tdaYeSzS`uo2G z>?pWJDmE(hpUjW_IVDsE{Odx$dFW+`Pjhlh)vvV8Vw!Iwi-Q@5r*4!&Y^Ce<<>r;P zL;fp*ibudqmkQf6HeNazlGLGjX^X}ME%>6}_H+R*R{~FV-Slll1Y394bBm7F#pT&O zpw&nb2aC@WW;-Yj+)w|3f}mVG;;Q*@lqTPFYiT5 zszk;7a_)TRI_P7^Yo&eY>X5W`LgrB@*o+Q@E^II_Q8b(JEh^i)%n#XQ+8YzZxPT+^86c_G)_wsDaFM;jU!@QB2OI27D~QcVGS1o0+9<@R0<{ zCS1wCNK zCZVC1cY7@4x$KwKA8cJRY~vzC^dU?Ut@$QKk(4~YJm+dy*lJEy??a=lsKs}2QiMOW zAD%yVbC11Um&#*ht6n`Kl<853MG!{Q`ZT9j$wyKQZ!yHm5S2Ny0a#hU;et)@^mJ#g zNMMDjT~7kh-Tgcm1FpaFulrgh7^JZMXQHH}6LMei-9zyEP;~OuU!zvGtep zATNpn;Bu4DiM6@c9jeq-J>_E!jF7EmXj7Bc?-TEbYRGs3?c+UVC;{!tnOfN1WXa}m z>!O5zUA~St-}qlm8X|o zu`2I38fP&BfG85Ysop(F+?vyI>es5rt$m=)t5-zQpDs7YRuWy?mm{%puNLU9WoN5_ zF2)5{t}w#Lt8SCsh?ruiL!UT{;-hytcjBC8uc)lONzHWvoMqWd6@(`jg@)mRMTHwqbazx#)cS-dJ54FX zfEsA`aWZU8Am>V`aJ$g~#tvHTFV-zU*qNBVtkX-Y?u-M|SulY~rErz~Lc>}wVW1uo`Q^T zFu-1a2c^F;KJ7>pUGr)*5_H66ka-jeBfJj4nX(Jw3aeV_4#WKZu?}{o6rf%ZSQ5w{ z5*&QIf5zwD9-Pf&LGn*K4 zS%$x(ID>yJaUyzmZpuI0t<0?}OAWZzw6ns zkD+C)-adohKXUCyFNP~z^RX7{K5>*he>E?!UsRjN;;vRH2GKg~o2G6}!SWVG26q9{ zabSK8%~yR@J^q~c^GN_h8$Pqdy}=qqEp18TBYR8cT|qlA7l2#|`5yt+HIuL*JL|y% z>-vKjNxzn-EqQMBX&Sy)JTfWgB`1Ce148G4#L~C7=8uP7os{FX$FT+pUtOOqZHqtr zCenV0dF#OVFr=qef8tA#PceYhH42@IZ8ens z9TZtg5S5S!&Cm4t;{N~_pl@VTGp+vY8Y_bYR%_*{&vEQe45r2eWh!*wOPR^=I)lXH z?sJ~82w%i1$}FlT+-gXgeO4%#9solNbInuwgUi^Wkhzw)gcb3;)|@$U7og&4x6s zVlML(QNgFduPLiHo8s+?7Tl5)W;VT|ulvZBrQfsz{-|1d6@DGAb|wrq_AHN=PgXGe zDziRZ1pA^nVw6QBEvA_E_#ur>=BB3B|9}Q)x>QRnSqP6gDUtEjtajWYpl>?TvU_+w z#P0b(M?h=dDw2kKD{{4aouJ7~?e0mrYRznTAd|p1Ye87(BzNfe8HaQ~k z0#95t@DHzp!Ysmif1cUQ=|lel)NUuL{66+A2f0fNy6+TbEy@AO&yPFb0^dqJ8_nPP zcrY$3b=~naGXpitC)O;g`Od%LYJ20%9sRR#0oTX7Ap@gUNHiRNvAeeAdKKN`sEo(& zZYe>iY1mccq24H*w_0#-@1chD(f=`cVoa`WRfAq%?D9jL7!GODHC(<1Xg%z*J3_C! z75$CCi}_0+Nb4G}x*GOfCy-Ck_;kdhUt+j<=04+e+;82|dY!4Of4Eu;2->$`yia!y7)aQ_DFU@66V<48aWp8?i=4B2kTMsagW{V|% zKKgQPr3Wy1`Lq?+7Lg5f8Pu zk|H88*`3O68xvEc3`_q$YzeO%uW;LGW%ge!`E>{4Sx1g>>9TjVZ}AasoJJOz8kGC* zSsn&Jm?HIWHmV6Rx|)Num5~=q*1&+AD=sITeA9q#zb$F#3OMlOs(^Z87%WH!?fgV@$Di@lh5x=?bap?lwyv)q%)AnE-x_4`oKSnq`&h*;Q();~+T!Cb zwsnq|X0y{cBMDv4U7B`pdyCmAYY0j^`m_9Bp5YgDhF)*a=0@ip#@UE-Gjus2;~zeL z6OY@@-C5oJu!GZlhV$Q>pwFMT7k^SdV5F%(f>9k7lxnIq-}eH;tV~X6)f2g`4pMa2 z@O?P{s5ge*A784Fr0AD)WXb}yq-UkE24`cxba5i0YUsSce#ABR(zHmj+Mkgf2Y-=M zz+^i}@=GpVMQIwzd)*vj+8KOm0E4r{a3RQG4ze>qy;ONJeVicl#wXVwWMSl;>aA1q#Sd zG*gL}s;HxBMVazmC;$alq@O(;h3?5Rod4E}8N<@q4|fNw12n~*f9A`*(b=_W^E9u7s3&}J=?(OPY5Pzb z1Tp;4>9-O^Wk|*HcpQ?30;0(#f79K+Bt-qN%sqBVSNZ8#wOSsk^V;e@gX>WK}Xtq8%0l`6(M7UXRydDVa}x zOZRb`N%891UHOM&K;Y0Bb)`Hi!Zf>^tvkD~*jVpW9k!{- zHM#MXyr6Q-i(wo7Ed5B5skzV*#K+CTUOT;;{Z9q|=iX_lGNx;1-=#ibu9{#a?zX@# zZiQQvC&?3R4N>xaC!}T3qfVA18!@N(o%K5^dam+jJe*)+b@5PjVODgN&%m2av@}>a zHEMoKkNvHuDs7(j6UC)#cdM^Q4(Y56PLqJ_;SB;nS zKefV{vo1Xhlom!9Lo+j90!p{YFyQOFlzM;1BvrBTOM#nib?AO6NAMK>z8)g~Y~F_} z`FsfD%}NXkGtBo5mP(tbzjj;l{I_}H z#-0!dN3~smuO~S%#o*7{{NtMeg`JVYz=_V2{;H3Sb!P=PY&e!!NV?!igvXJn866x|6W%zjP``TyLcgTp!OxgKKU1N6^Ig0W%?_y*Lr8B;Z+0 zA2TI#(6^Gd*SZOPQBd&pED%><{-E?JQeX=?G2p`>=Ci9}lYjO*qEp1AA1xR(+&5Cg3B@36|Lpw?QBXP2vWcK{S zekx8V0?^S`+er+DWmcJg%WA<4NqB}v4Zxe;h&Gi|VC;TbcK*=YCvH5L8$_ZRT&b(u zECCpsZ*wI(@){4X#xCiZr&8!o6I7ij{&v=0Y>s0q)VZn9<~-dslxAT*^#3HY1fQ`K zTql!K4B*Zd0&w1k5bz(cHEnHXT_vCP{KS-Q9cE|b2 zyfh=9B})6b zv;ThOm8u=xOvW?TLz40r0;9>AGid>`hP;eF^r8^UKo0R5H7D4OntM_!ddFTf%YI6)Fm$O{5V(^+8~xmaju+T8aS97EW+z(^=U)8H;QIF!Z-E= zRTq-})|~%z(w5yl`>fz8|D;MYD$msFd2{3QJnh<^^KCK%N8aoFlv*dgpFLVXe@}0F zeqzX&v+H?}sSZTZ;{|h>GgufqblV!TU-;hL9vcghx`=4yr%Gdh3(9a|fjVe69Kwoae+nB?4jJ&NDR*wGwNI{G5 zg$^(6e7~+r?(n;QxFk}anv=QTavitqWRVzS$^lPPNJ*n&q~dIQ)F%ttvpbXQj@N4{ zys`Xo%V$13-LFYFlvgx=eH;0u?M+^g8WK7RItz^8ZCi3?4IJ3*Hkm$;{G)soQ=Zqs zdBI?=G1t!%J9MI$9xC4UPRisFB?9TCm?z<<;j$#*Yc8|tc12=;RUy1IZ^XiI?m%W4SMpCb2lLji*&p`Z}1 ztGlQ7u2Nixshg*D97tC1Efy*x>J$PmJFrS!G!MK>q;i(1Ds1;UknXV!NbR4jVs61Q z$BF84Ka$Pb;Q_MirM4?OlrD5cPJ+kZth;@M+$8}ks(&YQ)-UUC))cl<``FeJ)aysw zf>%e9v=O)Dxp|t&YOUu$#{4t}2yq0!&XGRxy=J&Xp#Ddhtq+k!iT_!|P zrl4c5>kYvjra9Ub`h)swwVQGNWgk2*LaB+YcspOj>8q6II9G~LJxyDHbH+O>Z)|tW z%+ouf^GDz;=IL#eNK3P!EeXSidSOyM-p|B+>opLn8LfQt1Y6V5D^WsEyp?yU_p3>M znWaxnsn!a{VaJ>OeB)7MHP(kY*ah@EXwB5_6nEg->Xu@j7p?^kTO|4YLv0{U>fzGu z5b8$Xi`(z(NyE+``u=k0k>z0k$n9^CNL{Hb_<4RFcBVZYb*CKv8K+NJq$%PwusPWq zV0W;|ZJv>brybJz!S^GHIkE;NUe60kU ziXCQJd)30Ht%vtkQP}v{TjF*qE55>*P)ZMV&G+N?k^@?x;g52>*pJyfL&?Yn*aGr# z-(KYQ(CksQH}G|vuX$$Q6w}q{Sqz7k@Bj|^_#U>sP znhbori@b=J^jrMEjtbXGxM-32hD?RU4Get zolJ*xtuXRE!rQ%g_rO1a)(DAg+3lxtxw?!T=q>2`(3FDfb9J@N6%?5VOVY;6&F_E_ z-d1VmA?gFH+uGs@ZrxEm`ab-8=V|L3&8{~qe2gZyYRKe1=POfuUzFgRHJkPqUM0;{ zbZdwE3VRe653Xu@s(|dA%mTj0;~su|yu^d&EXgwQvS&d0$kKoGsi1F*m2(eB=LfWG zCf`w?wfCfb?(S-fLA8!bMttAfyUfkxY%_fqz_tOSK7) z3q_NxyNQUI_jV_oWqa3X88DmRA8#OQHk2=W{m?;}ao{%v4twyk3K$32Opk6Fh8cK5 z1>ty0+_=!wy;Ze-IQsXkuNI2P!@qNY4~78Ee!RK0cm44MG7otq1Vgc@Ft2}^T4ary z+ve(ej?rGzs8p@41NMJw@{gqZUA5`&QctufF|$|w2C~j;Lw_A-n)br=I>pfDqw*;f zi?fm7rr`^Cv9nTTgamNQTmWNs?*R3=y$YW)3gmQawEUZb*OFB8qtqV4%-8y_e?Yfl4@%)zh$ z`B`A=s)LH2Ba*Cz*a(y+JhVX!1fc1KJYJzB5~$f&9NGQDE}-+Aics%2s^Sbv#vM-bL?*y4E^gE0es|;Rq+|d4Zq*R>p-1$Z8I-AGdNcb{i z<3y)mPSJe#El-$H%^~Ai!okMW#@(}LoRyyp^Uv^I_`W(^$e}38J$!qX8xP)EGol?? z+zm`qHGD@TE!jKDSC8$dEoDV}ox7Ypot*~6r6l_*sxgsE{Sl?h`7M^^J(LT+0*@Ed zt7IXyiXLeQh;YSdog8vv$eFA9(`B|cA{#KJbjHG2emc(&;;i1$P+I1C3rB8@MFF%|3t&D~3m($~N$#dq$ z7q;-CQCL2NXfWvcaD$a{_NIXcvGLLTL1~5ouH(Hs3|h-RSxeN53c#BEj;d{3a=eq~ z$LA{_$(9TDMmD*RQgzfgY+p(*^IDKjnu8qotQWpqF z(92_U!vb$5;B==!ua+e&L|%nnP!6P=J_Flaex_Zmf!0!}V*G z#mlI)-7}Q*?q`7Ybimm%86QfQenw)#FZ;kPCabL@ zQ!W|#+t7=^cInWfuF4afHRyQX^a%4DrBHcksgMgrAKVI7*OqeQq5CGm z_1`lApJ6r#KI&~ekFLtLG6@beku{F)MzD53$OM!^x$r$QjuKyyfcg&IqrGyY40VJ= z1w>=LuvJB?ExF*qc+h3dd@Napn?786iDdbLHa$#j!4Z;oq4pirUCG2@vJ~{koZcMA1f1 Xur-eyGY9^f6{M=D^|D0X;`9Fi8t<_; diff --git a/odex25_base/system_dashboard_classic/static/src/icons/login.svg b/odex25_base/system_dashboard_classic/static/src/icons/login.svg new file mode 100644 index 000000000..1446fc8f9 --- /dev/null +++ b/odex25_base/system_dashboard_classic/static/src/icons/login.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/odex25_base/system_dashboard_classic/static/src/img/Attendence.png b/odex25_base/system_dashboard_classic/static/src/img/Attendence.png deleted file mode 100644 index bb1e4bf85fcfe89177c65136fbf21cc4843ec3c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69726 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 By#xRN diff --git a/odex25_base/system_dashboard_classic/static/src/img/icon.png b/odex25_base/system_dashboard_classic/static/src/img/icon.png deleted file mode 100644 index 689ebc71e5cb8b688670ce576374cba558ca98cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24901 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 5) { + clearInterval(checkInterval); + } + }, 1000); + }); + + function initGeniusEnhancements() { + fixEmptyEmployeeCode(); + // Search bar is now handled by SystemDashboardView widget in self_service.js + transformSelfServicesTab(); + loadCustomColors(); + loadStatsVisibility(); + } + + /** + * Fix empty employee code display - runs multiple times + * Uses genius-hidden class instead of hide() for CSS priority + */ + function fixEmptyEmployeeCode() { + $('p.fn-id').each(function() { + var el = $(this); + var idText = el.text().trim(); + + // Hide if empty, just "/", "False", "false", undefined, etc. + if (!idText || + idText === '' || + idText === '/' || + idText === 'undefined' || + idText === 'null' || + idText === 'false' || + idText === 'False' || + idText === 'None') { + el.addClass('genius-hidden'); + } else { + el.removeClass('genius-hidden'); + } + }); + } + + + /** + * Add search bar + */ + function addSearchBar() { + var navButtons = $('.dashboard-nav-buttons'); + + if ($('.genius-search-container').length > 0 || navButtons.length === 0) { + return; + } + + // RTL/LTR placeholder translation + var searchPlaceholder = $('body').hasClass('o_rtl') + ? 'ابحث عن خدمة...' + : 'Search for a service...'; + + var searchHtml = '
' + + '
' + + '' + + '' + + '' + + '
'; + + navButtons.append(searchHtml); + + $('#geniusServiceSearch').on('input', function() { + var term = $(this).val().toLowerCase().trim(); + $('#geniusClearSearch').toggle(term.length > 0); + filterCards(term); + }); + + $('#geniusClearSearch').on('click', function() { + $('#geniusServiceSearch').val(''); + $(this).hide(); + filterCards(''); + }); + } + + function filterCards(term) { + var cards = $('.card3, .card2'); + if (term === '') { + cards.removeClass('genius-hidden'); + return; + } + cards.each(function() { + var card = $(this); + var text = card.find('h4, h3, td').text().toLowerCase(); + card.toggleClass('genius-hidden', !text.includes(term)); + }); + } + + function transformSelfServicesTab() { + var tabs = $('.dashboard-nav-buttons .nav-tabs li'); + if (tabs.length === 1) { + tabs.first().addClass('genius-single-tab'); + } + } + + /** + * Load ALL custom colors from database and apply as CSS variables + * Generates light/dark variants automatically for complete theming + */ + function loadCustomColors() { + var ICP = 'ir.config_parameter'; + var prefix = 'system_dashboard_classic.'; + + // Define all color parameters with their CSS variable names + var colorParams = [ + { param: 'primary_color', cssVar: '--dash-primary', defaultColor: '#0891b2' }, + { param: 'secondary_color', cssVar: '--dash-secondary', defaultColor: '#1e293b' }, + { param: 'success_color', cssVar: '--dash-success', defaultColor: '#10b981' }, + { param: 'warning_color', cssVar: '--dash-warning', defaultColor: '#f59e0b' } + ]; + + // Load each color + colorParams.forEach(function(item) { + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: ICP, + method: 'get_param', + args: [prefix + item.param], + kwargs: {} + }).then(function(color) { + if (color && color.match(/^#[0-9A-Fa-f]{6}$/)) { + applyColorWithVariants(item.cssVar, color); + // For primary color, also generate SVG icon filter + if (item.cssVar === '--dash-primary') { + generateIconFilter(color); + } + } + }).catch(function() {}); + }); + } + + /** + * Generate CSS filter to colorize SVG icons to match target color + * Algorithm: First convert to black, then use filter combination + * Based on: https://stackoverflow.com/questions/42966641/ + * @param {string} hexColor - The target color in hex format + */ + function generateIconFilter(hexColor) { + var rgb = hexToRgb(hexColor); + if (!rgb) return; + + // Calculate HSL from RGB + var r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h = 0, s = 0, l = (max + min) / 2; + + if (max !== min) { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; + else if (max === g) h = ((b - r) / d + 2) / 6; + else h = ((r - g) / d + 4) / 6; + } + + // Convert to degrees and percentages + var hue = Math.round(h * 360); + var saturation = Math.round(s * 100); + var lightness = Math.round(l * 100); + + // Build filter: first make it black, then colorize + // invert(1) makes black -> white, then hue-rotate to target hue + // Or we can use sepia(1) to get yellow base, then hue-rotate + + // Calculate filter values + var invert = lightness < 50 ? 0 : 1; + var sepia = 1; + var saturate = saturation / 10; // Scale to reasonable value + var hueRotate = hue; + var brightness = lightness / 50; // Normal is 1 (50%) + + // Generate filter string - start with brightness(0) saturate(100%) to get black + var filterValue = 'brightness(0) saturate(100%) invert(' + (lightness > 50 ? '1' : '0.5') + ') ' + + 'sepia(1) saturate(' + Math.max(2, saturate).toFixed(0) + ') ' + + 'hue-rotate(' + hueRotate + 'deg) ' + + 'brightness(' + brightness.toFixed(2) + ')'; + + // Apply as CSS variable + document.documentElement.style.setProperty('--dash-icon-filter', filterValue); + } + + /** + * Apply a color and generate light/dark/RGB variants automatically + * @param {string} cssVarName - CSS variable name (e.g., '--dash-primary') + * @param {string} hexColor - Hex color value (e.g., '#0891b2') + */ + function applyColorWithVariants(cssVarName, hexColor) { + var root = document.documentElement.style; + + // Apply base color + root.setProperty(cssVarName, hexColor); + + // Generate RGB values for rgba() usage in CSS + var rgb = hexToRgb(hexColor); + if (rgb) { + root.setProperty(cssVarName + '-rgb', rgb.r + ', ' + rgb.g + ', ' + rgb.b); + } + + // Generate and apply light variant (+25% lighter) + var lightColor = adjustColor(hexColor, 25); + root.setProperty(cssVarName + '-light', lightColor); + + // Generate and apply dark variant (-20% darker) + var darkColor = adjustColor(hexColor, -20); + root.setProperty(cssVarName + '-dark', darkColor); + + // For secondary color, also update header gradient + if (cssVarName === '--dash-secondary') { + root.setProperty('--dash-secondary-light', lightColor); + // Update header background directly + updateHeaderGradient(hexColor, lightColor); + } + } + + /** + * Convert hex color to RGB object + * @param {string} hex - Hex color + * @returns {Object} RGB object with r, g, b properties + */ + function hexToRgb(hex) { + hex = hex.replace(/^#/, ''); + if (hex.length !== 6) return null; + + return { + r: parseInt(hex.substring(0, 2), 16), + g: parseInt(hex.substring(2, 4), 16), + b: parseInt(hex.substring(4, 6), 16) + }; + } + + /** + * Adjust color brightness + * @param {string} hex - Hex color + * @param {number} percent - Percentage to adjust (-100 to +100) + * @returns {string} Adjusted hex color + */ + function adjustColor(hex, percent) { + // Remove # if present + hex = hex.replace(/^#/, ''); + + // Parse RGB values + var r = parseInt(hex.substring(0, 2), 16); + var g = parseInt(hex.substring(2, 4), 16); + var b = parseInt(hex.substring(4, 6), 16); + + // Adjust brightness + r = Math.min(255, Math.max(0, r + Math.round(r * percent / 100))); + g = Math.min(255, Math.max(0, g + Math.round(g * percent / 100))); + b = Math.min(255, Math.max(0, b + Math.round(b * percent / 100))); + + // Convert back to hex + return '#' + [r, g, b].map(function(x) { + var hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + /** + * Update header gradient with new colors + */ + function updateHeaderGradient(baseColor, lightColor) { + var header = document.querySelector('.dashboard-container .dashboard-header'); + if (header) { + header.style.background = 'linear-gradient(135deg, ' + baseColor + ' 0%, ' + lightColor + ' 100%)'; + } + } + + /** + * Load statistics visibility settings from database and apply + * Hides/shows the 3 stats cards based on configuration + */ + function loadStatsVisibility() { + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'res.config.settings', + method: 'get_stats_visibility', + args: [], + kwargs: {} + }).then(function(visibility) { + // Annual Leave card + if (!visibility.show_annual_leave) { + $('#leave-section').addClass('genius-hidden'); + } else { + $('#leave-section').removeClass('genius-hidden'); + } + + // Salary Slips card + if (!visibility.show_salary_slips) { + $('#salary-section').addClass('genius-hidden'); + } else { + $('#salary-section').removeClass('genius-hidden'); + } + + // Weekly Timesheet card + if (!visibility.show_timesheet) { + $('#timesheet-section').addClass('genius-hidden'); + } else { + $('#timesheet-section').removeClass('genius-hidden'); + } + + // Attendance Hours card + if (!visibility.show_attendance_hours) { + $('#attendance-hours-section').addClass('genius-hidden'); + } else { + $('#attendance-hours-section').removeClass('genius-hidden'); + } + + // Attendance Check-in/out Section (right side panel) + if (!visibility.show_attendance_section) { + // Hide the attendance section + $('.dashboard-attendance-section').addClass('genius-hidden'); + // Expand the charts section to full width (from col-md-10 to col-md-12) + $('.dashboard-charts-section').removeClass('col-md-10').addClass('col-md-12'); + // Add class for extra CSS control + $('.dashboard-user-statistics-section').addClass('attendance-hidden'); + } else { + // Show attendance section + $('.dashboard-attendance-section').removeClass('genius-hidden'); + // Restore charts section width + $('.dashboard-charts-section').removeClass('col-md-12').addClass('col-md-10'); + $('.dashboard-user-statistics-section').removeClass('attendance-hidden'); + } + }).catch(function() { + // Silently handle visibility settings error + }); + } + + return { init: initGeniusEnhancements }; +}); diff --git a/odex25_base/system_dashboard_classic/static/src/js/pluscharts.js b/odex25_base/system_dashboard_classic/static/src/js/pluscharts.js index 0e11776d6..e75d80038 100644 --- a/odex25_base/system_dashboard_classic/static/src/js/pluscharts.js +++ b/odex25_base/system_dashboard_classic/static/src/js/pluscharts.js @@ -1224,7 +1224,7 @@ __webpack_require__.r(__webpack_exports__); var initTooltip = (bindedElement, data, i) => { var div = bindedElement.append("g").attr("class", "pc-tooltip").html(function () { - var tooltipContent = "" + data[i].label + "," + data[i].value; + var tooltipContent = "" + data[i].label; return tooltipContent; }); positionTooltip(bindedElement); diff --git a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard.js b/odex25_base/system_dashboard_classic/static/src/js/system_dashboard.js deleted file mode 100644 index 3c9e68cd5..000000000 --- a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard.js +++ /dev/null @@ -1,609 +0,0 @@ -odoo.define('system_dashboard_classic.dashboard', function(require) { - "use strict"; - - var core = require('web.core'); - var session = require('web.session'); - var ajax = require('web.ajax'); - var ActionManager = require('web.ActionManager'); - var view_registry = require('web.view_registry'); - var Widget = require('web.Widget'); - var AbstractAction = require('web.AbstractAction'); - - var QWeb = core.qweb; - - var _t = core._t; - var _lt = core._lt; - var functions = []; - window.click_actions = []; - - var SystemDashboardView = AbstractAction.extend({ - template: 'system_dashboard_classic.dashboard', - init: function(parent, context) { - this._super(parent, context); - var data = []; - var self = this; - window.selfOb = this; - if (context.tag == 'system_dashboard_classic.dashboard') { - self._rpc({ - model: 'system_dashboard_classic.dashboard', - method: 'get_data', - }, []).then(function(result) { - console.log(result) - window.resultX = result; - if (result.employee !== undefined && result.employee !== '' && result.employee[0].length !== 0) { - $('#main-cards').css('display', 'flex'); - $('.fn-id,.fn-department,.fn-job').show(); - // SET EMPLOYEE DATA - if (result.employee[0][0] !== undefined) { - $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.employee[0][0].image_128 + ')') - $("p.fn-section").html(result.employee[0][0].name); - $("p.fn-id").html(result.employee[0][0].emp_no); - if (result.employee[0][0].job_id.length !== 0) { - $("p.fn-job").html(result.employee[0][0].job_id[1]); - } - } else { - $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.user[0][0].image_128 + ')') - $("p.fn-section").html(result.user[0][0].display_name); - $("p.fn-id").html(result.user[0][0].id); - } - // SET MAIN CARD DATA (LEAVES - TIMESHEET - PAYSLIPS) - $(".leaves_count span.value").html(parseFloat(result['employee'][0][0].remaining_leaves).toFixed(2)); - $(".timesheet_count span.value").html(result['employee'][0][0].timesheet_count); - $(".paylips_count span.value").html(result['employee'][0][0].payslip_count); - // LEAVES DO ACTION - $("#leaves_btn").on('click', function(event) { - event.stopPropagation(); - event.preventDefault(); - return self.do_action({ - name: _t("Leaves"), - type: 'ir.actions.act_window', - res_model: 'hr.holidays', - src_model: 'hr.employee', - view_mode: 'tree,form', - view_type: 'form', - views: [ - [false, 'list'], - [false, 'form'] - ], - context: { - 'search_default_employee_id': [result['employee'][0][0].id], - 'default_employee_id': result['employee'][0][0].id, - }, - domain: [ - ['holiday_type', '=', 'employee'], - ['holiday_status_id.limit', '=', false], - ['state', '!=', 'refuse'] - ], - target: 'main', - flags:{ - reload: true, - } - }, { on_reverse_breadcrumb: self.on_reverse_breadcrumb }) - }); - - // TIMESHEET DO ACTION - $("#timesheet_btn").on('click', function(event) { - event.stopPropagation(); - event.preventDefault(); - return self.do_action({ - name: _t("Timesheets"), - type: 'ir.actions.act_window', - res_model: 'account.analytic.line', - view_mode: 'tree,form', - view_type: 'form', - views: [ - [false, 'list'], - [false, 'form'] - ], - context: { - 'search_default_employee_id': [result['employee'][0][0].id], - }, - domain: [ - ['user_id', '=', result['user'][0]] - ], - target: 'main', - flags:{ - reload: true, - } - }, { on_reverse_breadcrumb: self.on_reverse_breadcrumb }) - }); - - // PAYSLIPS DO ACTION - $("#paylips_btn").on('click', function(event) { - event.stopPropagation(); - event.preventDefault(); - return self.do_action({ - name: _t("Payslips"), - type: 'ir.actions.act_window', - res_model: 'hr.payslip', - view_mode: 'tree,form', - view_type: 'form', - views: [ - [false, 'list'], - [false, 'form'] - ], - context: { - 'search_default_employee_id': [result['employee'][0][0].id], - 'default_employee_id': result['employee'][0][0].id, - }, - target: 'main', - flags:{ - reload: true, - } - }, { on_reverse_breadcrumb: self.on_reverse_breadcrumb }) - }); - } else { - if (result.user !== undefined && result.user !== '' && result.user[0].length !== 0) { - // SET EMPLOYEE DATA - $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.user[0][0].image_128 + ')') - $("p.fn-section").html(result.user[0][0].name); - $("p.fn-id").html('ID: ' + result.user[0][0].id); - } - } - - // Dynamic Cards (DO ACTIONS) - if (result.cards !== undefined && result.cards.length > 0) { - $.each(result.cards, function(index) { - if (result.cards[index].type == "approve") { - window.card_data = result.cards[index]; - var card_title = (card_data.name_english.replace(/\s/g, '').toLocaleLowerCase()); - var btn = 'click button#' + (card_title + index); - // BUILD APPROVE CARD - window.card = buildCard(result.cards[index], index, 'table1'); - $(card).find('.card-header h4 span').html(_t(result.cards[index].name)); - // BUILD FOLLOW CARD - window.card2 = buildCard(result.cards[index], index, 'table2'); - $(card2).find('.card-header h4 span').html(_t(result.cards[index].name)); - // APPENDING APPROVE CARD TO APPROVE TAB/SECTION - self.$el.find('div.card-section1').append(card); - // APPENDING FOLLOW CARD TO FOLLOW TAB/SECTION - self.$el.find('div#details .card-section').append(card2); - } - }); - - // TRIGGERING BUTTONS IN THE APPROVE & FOLLOW CARD TO SHOW DETAILS - self.$el.find('tr[data-target="record-button"]').on('click', function(event) { - event.stopPropagation(); - event.preventDefault(); - // GET DATA OF BUTTON TO HANDEL IT - var model = $(this).attr('data-model'); - var name = $(this).attr('data-name'); - var domain = $(this).attr('data-domain'); - var form_view = parseInt($(this).attr('form-view')); - var list_view = parseInt($(this).attr('list-view')); - var type = $(this).attr('data-type'); - - // CHECK DOMAIN - if (domain === undefined || domain === '') { - domain = false; - } else { - domain = JSON.parse(domain); - } - // CHECK FORM VIEW VALUE - if (isNaN(form_view)) { - form_view = false; - } - // CHECK LIST VIEW VALUE - if (isNaN(list_view)) { - list_view = false; - } - - //FIRING DO ACTION - return self.do_action({ - name: _t(name), - type: 'ir.actions.act_window', - res_model: model, - view_mode: 'tree,form', - view_type: 'list', - views: [ - [list_view, 'list'], - [form_view, 'form'] - ], - domain: domain, - target: 'main', - flags:{ - reload: true, - } - }, { on_reverse_breadcrumb: function() { return self.reload(); } }) - }); - - } - // Chart Settings - setTimeout(function() { - window.check = false; - //result.attendance[0].is_attendance = true; - var name = ""; - if (result.employee[0][0] !== undefined) { - name = result.employee[0][0].name; - } else { - name = result.user[0][0].display_name; - } - if (result.attendance !== undefined) { - setupAttendanceArea(result.attendance[0], name); - // check button submission - $('button#check_button').on('click', function(event) { - //var check = result.attendance[0].is_attendance; //false or true - self._rpc({ - model: 'system_dashboard_classic.dashboard', - method: 'checkin_checkout', - context: { 'check': check }, - }, []).then(function(result) { - $('.last-checkin-section').html(''); - $('.attendance-img-section').html(''); - $('#check_button').html(''); - setupAttendanceArea(result, resultX.employee[0][0].name); - }); - }); - } else { - $('.attendance-section-body').hide(); - } - - // Charts Labels - if ($('body').hasClass('o_rtl') === true) { - var total_leaves_title = "الرصيد الكلي "; - var remaining_leaves_title = "رصيد الأيام المتبقية "; - var taken_leaves_title = "رصيد الأيام المستنفذة "; - - var total_payroll_title = "إجمالي القسائم السنوية"; - var remaining_payroll_title = "قسائم الراتب المتبقية "; - var taken_payroll_title = "القسائم المستنفذة "; - - var total_timesheet_title = "إجمالي ساعات الأسبوع "; - var remaining_timesheet_title = "الساعات المتبقية "; - var taken_timesheet_title = "الساعات المنجزة "; - } else { - var total_leaves_title = "Total Balance "; - var remaining_leaves_title = "Left Balance "; - var taken_leaves_title = "Total remaining days "; - - var total_payroll_title = "Total annual slips"; - var remaining_payroll_title = "Remaining salary slips "; - var taken_payroll_title = "Left salary slips "; - - var total_timesheet_title = "Total hours of week"; - var remaining_timesheet_title = "Remaining hours "; - var taken_timesheet_title = "Left hours "; - } - - if (result.leaves !== undefined) { - if (result.leaves[0].taken > 0 || result.leaves[0].remaining_leaves > 0) { - var total_leaves = (result.leaves[0].taken + result.leaves[0].remaining_leaves); - var remaining_leaves_percent = ((result.leaves[0].remaining_leaves / total_leaves) * 100).toFixed(2); - var taken_leaves_percent = ((result.leaves[0].taken / total_leaves) * 100).toFixed(2); - //set data - $('.leave-total-amount').html(total_leaves_title + " " + Math.round(parseFloat(result.leaves[0].taken + result.leaves[0].remaining_leaves))); - $('.leave-left-amount').html(remaining_leaves_title + " " + Math.round(parseFloat(result.leaves[0].remaining_leaves))); - if ($('body').hasClass('o_rtl') === true) { - $('.leave-data-percent').html('%' + taken_leaves_percent); - } else { - $('.leave-data-percent').html(taken_leaves_percent + '%'); - } - $('#chartContainer').html(''); - pluscharts.draw({ - drawOn: "#chartContainer", - type: "donut", - dataset: { - data: [{ - label: remaining_leaves_title, - value: ((result.leaves[0].taken / total_leaves) * 100).toFixed(2) - }, - { - label: taken_leaves_title, - value: ((result.leaves[0].remaining_leaves / total_leaves) * 100).toFixed(2) - } - ], - backgroundColor: ["#003056", "#2ead97"], - borderColor: "#ffffff", - borderWidth: 0, - }, - options: { - width: 15, - text: { - display: false, - color: "#f6f6f6" - }, - legends: { - display: false, - width: 20, - height: 20 - }, - size: { - width: '140', //give 'container' if you want width and height of initiated container - height: '140' - } - } - }); - // display section - $('#leave-section').fadeIn(); - } - } - - if (result.payroll !== undefined) { - if (result.payroll[0].taken > 0 || result.payroll[0].payslip_remaining > 0) { - // Configure Salary Slips Chart - var total_payroll = (result.payroll[0].taken + result.payroll[0].payslip_remaining); - var remaining_payroll_percent = ((result.payroll[0].payslip_remaining / total_payroll) * 100).toFixed(2); - var taken_payroll_percent = ((result.payroll[0].taken / total_payroll) * 100).toFixed(2); - $('.payroll-total-amount').html(total_payroll_title + " " + parseFloat(total_payroll)); - $('.payroll-left-amount').html(remaining_payroll_title + " " + parseFloat(result.payroll[0].payslip_remaining)); - if ($('body').hasClass('o_rtl') === true) { - $('.payroll-data-percent').html('%' + taken_payroll_percent); - } else { - $('.payroll-data-percent').html(taken_payroll_percent + '%'); - } - $('#chartPaylips').html(''); - pluscharts.draw({ - drawOn: "#chartPaylips", - type: "donut", - dataset: { - data: [{ - label: remaining_payroll_title, - value: ((result.payroll[0].payslip_remaining / total_payroll) * 100).toFixed(2) - }, - { - label: taken_payroll_title, - value: ((result.payroll[0].taken / total_payroll) * 100).toFixed(2) - } - ], - backgroundColor: ["#003056", "#2ead97"], - borderColor: "#ffffff", - borderWidth: 0, - }, - options: { - width: 15, - text: { - display: false, - color: "#f6f6f6" - }, - legends: { - display: false, - width: 20, - height: 20 - }, - size: { - width: '140', //give 'container' if you want width and height of initiated container - height: '140' - } - } - }); - // display section - $('#salary-section').fadeIn(); - } - } - - if (result.timesheet !== undefined) { - if (result.timesheet[0].taken > 0 || result.timesheet[0].timesheet_remaining > 0) { - // Configure Weekly Timesheet Chart - var total_timesheet = (result.timesheet[0].taken + result.timesheet[0].timesheet_remaining); - var remaining_timesheet_percent = ((result.timesheet[0].timesheet_remaining / total_timesheet) * 100).toFixed(2); - var taken_timesheet_percent = ((result.timesheet[0].taken / total_timesheet) * 100).toFixed(2); - $('.timesheet-total-amount').html(total_timesheet_title + " " + parseFloat(total_timesheet)); - $('.timesheet-left-amount').html(remaining_timesheet_title + " " + parseFloat(result.timesheet[0].timesheet_remaining)); - $('.timesheet-data-percent').html(taken_timesheet_percent + '%'); - if ($('body').hasClass('o_rtl') === true) { - $('.timesheet-data-percent').html('%' + taken_timesheet_percent); - } else { - $('.timesheet-data-percent').html(taken_timesheet_percent + '%'); - } - $('#chartTimesheet').html(''); - pluscharts.draw({ - drawOn: "#chartTimesheet", - type: "donut", - dataset: { - data: [{ - label: taken_timesheet_title, - value: ((result.timesheet[0].taken / total_timesheet) * 100).toFixed(2) - }, - { - label: remaining_timesheet_title, - value: ((result.timesheet[0].timesheet_remaining / total_timesheet)).toFixed(2) - } - ], - backgroundColor: ["#003056", "#2ead97"], - borderColor: "#ffffff", - borderWidth: 0, - }, - options: { - width: 15, - text: { - display: false, - color: "#f6f6f6" - }, - legends: { - display: false, - width: 20, - height: 20 - }, - size: { - width: '140', //give 'container' if you want width and height of initiated container - height: '140' - } - } - }); - - // display section - $('#timesheet-section').fadeIn(); - } - } - // hide charts loader - $('.charts-over-layer').fadeOut(); - }, 1000); - - }) - // .done(function() { - // self.render(); - // self.href = window.location.href; - // }); - } - var attrs = [{ - value: 50, - label: 'Total Amount 30', - color: '#003056' - }, - { - value: 50, - label: 'Left Amount', - color: '#2ead97' - }, - ]; - - - function explodePie(e) { - if (typeof(e.dataSeries.dataPoints[e.dataPointIndex].exploded) === "undefined" || !e.dataSeries.dataPoints[e.dataPointIndex].exploded) { - e.dataSeries.dataPoints[e.dataPointIndex].exploded = true; - } else { - e.dataSeries.dataPoints[e.dataPointIndex].exploded = false; - } - e.chart.render(); - } - }, - willStart: function() { - return $.when(ajax.loadLibs(this), this._super()); - }, - start: function() { - var self = this; - return this._super(); - }, - render: function() { - var super_render = this._super; - $(".o_control_panel").addClass("o_hidden"); - var self = this; - }, - reload: function() { - window.location.href = this.href; - } - }); - core.action_registry.add('system_dashboard_classic.dashboard', SystemDashboardView); - return SystemDashboardView -}); - -window.text = [{ - 'btn': { - 'ar': 'مشاهدة التفاصيل ', - 'en': 'Show Details ' - } -}]; - -/* - NAME: buildCard - DESC: Using to create (APPROVE/FOLLOW) card base on sent data and it return DOM OBJECT - RETURN: DOM OBJECT -*/ -function buildCard(data, index, card_type) { - var card = document.createElement('div'); - var card_container = document.createElement('div'); - var card_header = document.createElement('div'); - var card_body = document.createElement('div'); - - $(card).addClass('col-md-3 col-sm-6 col-xs-12 card2'); - $(card_container).addClass('col-md-12 col-sm-12 col-xs-12 card-container'); - - //card header - var h4 = document.createElement('h4'); - $(card_header).addClass('col-md-12 col-sm-12 col-xs-12 card-header'); - $(h4).html(' ' + data.name + '').attr('title', data.name); - $(card_header).append(h4); - $(card_container).append(card_header); - - // card body - $(card_body).addClass('col-md-12 col-sm-12 col-xs-12 card-body'); - if (card_type == "table1") { - var table = buildTableApprove(data); - } else if (card_type == "table2") { - var table = buildTableFollow(data); - } - $(card_body).append(table); - $(card_container).append(card_body); - - // card - $(card).append(card_container); - return card; -} - -/* - NAME: buildTableApprove - DESC: Using to create (APPROVE) rows/buttons base on sent data and it return DOM OBJECT - RETURN: DOM OBJECT -*/ -function buildTableApprove(data) { - var table = document.createElement('table'); - var tbody = document.createElement('tbody'); - $(table).addClass('table'); - if (data.lines.length !== undefined && data.lines.length > 0) { - $.each(data.lines, function(index) { - var tr = document.createElement('tr'); - var td1 = document.createElement('td'); - var td2 = document.createElement('td'); - $(td1).html(data.lines[index].state_approval); - $(td2).html('
' + data.lines[index].count_state_click + '
'); - $(tr).attr('data-target', 'record-button').attr('data-model', data.model).attr('data-name', data.name). - attr('data-domain', JSON.stringify(data.lines[index].domain_click)).attr('form-view', data.lines[index].form_view). - attr('list-view', data.lines[index].list_view).attr('data-action', JSON.stringify(data.lines[index].action_domain)); - $(tr).append(td1).append(td2); - $(tbody).append(tr); - }); - } - $(table).append(tbody); - return table; -} - -/* - NAME: buildTableFollow - DESC: Using to create (FOLLOW) rows/buttons base on sent data and it return DOM OBJECT - RETURN: DOM OBJECT -*/ -function buildTableFollow(data) { - var table = document.createElement('table'); - var tbody = document.createElement('tbody'); - $(table).addClass('table'); - if (data.lines.length !== undefined && data.lines.length > 0) { - $.each(data.lines, function(index) { - var tr = document.createElement('tr'); - var td1 = document.createElement('td'); - var td2 = document.createElement('td'); - $(td1).html(data.lines[index].state_folow); - $(td2).html('
' + data.lines[index].count_state_follow + '
'); - $(tr).attr('data-target', 'record-button').attr('data-type', 'follow').attr('data-model', data.model).attr('data-name', data.name). - attr('data-domain', JSON.stringify(data.lines[index].domain_follow)).attr('form-view', data.lines[index].form_view). - attr('list-view', data.lines[index].list_view) - $(tr).append(td1).append(td2); - // to append - //$(tbody).append(tr); - // to override - $(tbody).html(tr) - - }); - } - // to append - $(table).append(tbody); - return table; -} - -function setupAttendanceArea(data, name) { - window.check = data.is_attendance; - var button = ''; - if ($('body').hasClass('o_rtl')) { - var checkout_title = "زمن تسجيل اخر دخول" - var checkin_title = ",مرحباً بك" - var checkin_button = "تسجيل الحضور"; - var checkout_button = "تسجيل الخروج"; - } else { - var checkout_title = "Last check in" - var checkin_title = "Welcome"; - var checkin_button = "Check In" - var checkout_button = "Check Out"; - } - if (data.is_attendance === true) { - var check = checkout_title + " " + data.time; - var image = ''; - $('#check_button').html(checkout_button).removeClass('checkin-btn').addClass('checkout-btn'); - $('.last-checkin-section').html(check); - $('.attendance-img-section').html(image); - } else { - var check = checkin_title + " " + name + ","; - var image = ''; - $('#check_button').html(checkin_button).removeClass('checkout-btn').addClass('checkin-btn'); - $('.last-checkin-section').html(check); - $('.attendance-img-section').html(image); - } -} diff --git a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js b/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js index 4e691c555..787473eaf 100644 --- a/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js +++ b/odex25_base/system_dashboard_classic/static/src/js/system_dashboard_self_service.js @@ -16,6 +16,176 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require var functions = []; window.click_actions = []; + /** + * Convert hex color to RGB components + * @param {string} hex - Color in hex format (#RRGGBB) + * @returns {object|null} RGB components {r, g, b} + */ + function hexToRgbComponents(hex) { + if (!hex || typeof hex !== 'string') return null; + var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; + } + + /** + * Lighten or darken a hex color + * @param {string} hex - Color in hex format + * @param {number} percent - Positive = lighter, Negative = darker + * @returns {string} Adjusted color in hex format + */ + function adjustColor(hex, percent) { + if (!hex || typeof hex !== 'string') return hex; + // Remove # if present + hex = hex.replace(/^\s*#|\s*$/g, ''); + // Convert 3-char hex to 6-char + if (hex.length === 3) { + hex = hex.replace(/(.)/g, '$1$1'); + } + var r = parseInt(hex.substr(0, 2), 16); + var g = parseInt(hex.substr(2, 2), 16); + var b = parseInt(hex.substr(4, 2), 16); + + r = Math.round(Math.min(255, Math.max(0, r + (r * percent / 100)))); + g = Math.round(Math.min(255, Math.max(0, g + (g * percent / 100)))); + b = Math.round(Math.min(255, Math.max(0, b + (b * percent / 100)))); + + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); + } + + /** + * Animate a number from 0 to target value with smooth easing + * @param {jQuery|Element} element - The element to animate + * @param {number} targetValue - Final number to display + * @param {number} duration - Animation duration in ms (default 1000) + * @param {string} suffix - Optional suffix like 'days', 'hours' (default '') + * @param {number} decimals - Decimal places to show (default 0) + */ + function animateCounter(element, targetValue, duration, suffix, decimals) { + duration = duration || 1000; + suffix = suffix || ''; + decimals = decimals || 0; + + var $el = $(element); + if (!$el.length || isNaN(targetValue)) return; + + var startTime = null; + var startValue = 0; + + // Easing function - easeOutQuad for smooth deceleration + function easeOutQuad(t) { + return t * (2 - t); + } + + function animate(currentTime) { + if (!startTime) startTime = currentTime; + var elapsed = currentTime - startTime; + var progress = Math.min(elapsed / duration, 1); + var easedProgress = easeOutQuad(progress); + var currentValue = startValue + (targetValue - startValue) * easedProgress; + + // Format the number + var displayValue = decimals > 0 ? currentValue.toFixed(decimals) : Math.round(currentValue); + $el.text(displayValue + (suffix ? ' ' + suffix : '')); + + if (progress < 1) { + requestAnimationFrame(animate); + } + } + + requestAnimationFrame(animate); + } + + /** + * Show celebration modal/banner for birthday or work anniversary + * @param {string} type - 'birthday' or 'anniversary' + * @param {number} years - Years of service (for anniversary) + */ + function showCelebration(type, years) { + var isRtl = $('body').hasClass('o_rtl'); + var title, message, emoji, badgeClass; + + if (type === 'birthday') { + emoji = '🎂'; + badgeClass = 'birthday-mode'; + title = isRtl ? 'عيد ميلاد سعيد!' : 'Happy Birthday!'; + // Gendered pronouns: لك (male) / لكِ (female) + var genderInfo = window.dashboardGenderInfo || {pronoun_you: 'ك'}; + var forYou = 'ل' + genderInfo.pronoun_you; // لك or لكِ + message = isRtl ? ('نتمنى ' + forYou + ' عاماً جديداً مليئاً بالنجاح والسعادة ✨') : 'Wishing you a wonderful year ahead! ✨'; + } else if (type === 'anniversary') { + emoji = '⭐'; + badgeClass = 'anniversary-mode'; + title = isRtl ? 'شكراً لعطائك!' : 'Thank You!'; + message = isRtl + ? 'نقدر إخلاصك وتفانيك في العمل' + : 'We appreciate your dedication'; + } else { + return; + } + + // Add celebration mode class to entire dashboard (persistent) + $('.dashboard-container').addClass('celebration-mode ' + badgeClass); + + // Add golden glow ring around employee photo + $('.img-box').addClass('celebration-glow'); + + // Add appreciation ribbon at bottom of header for anniversary + if (type === 'anniversary' && $('.appreciation-ribbon').length === 0) { + var ribbonMessage = isRtl + ? 'بمناسبة إتمامك عامًا جديدًا معنا، نثمّن عطائك ونتطلع للمزيد ⭐' + : '⭐ Celebrating another year with us. We value your contribution and look forward to more!'; + var $ribbon = $('
' + + '' + ribbonMessage + '' + + '
'); + setTimeout(function() { + $('.dashboard-header').append($ribbon); + }, 1000); + } + + // Trigger initial confetti burst + if (window.confetti) { + setTimeout(function() { + confetti({ + particleCount: 150, + spread: 100, + origin: { y: 0.3, x: 0.5 } + }); + }, 500); + + // Subtle continuous confetti every 30 seconds (non-intrusive) + setInterval(function() { + if ($('.dashboard-container').hasClass('celebration-mode')) { + confetti({ + particleCount: 30, + spread: 60, + origin: { y: 0.2, x: Math.random() }, + colors: type === 'birthday' ? ['#ff69b4', '#ff1493', '#ffd700'] : ['#4facfe', '#00f2fe', '#ffd700'] + }); + } + }, 30000); + } + } + + /** + * Get time-based greeting message + * @returns {string} Greeting in Arabic or English based on RTL + */ + function getGreeting() { + var hour = new Date().getHours(); + var isRtl = $('body').hasClass('o_rtl'); + if (hour >= 6 && hour < 12) { + return isRtl ? 'صباح الخير' : 'Good Morning'; + } else if (hour >= 12 && hour < 18) { + return isRtl ? 'مساء الخير' : 'Good Afternoon'; + } else { + return isRtl ? 'مرحباً' : 'Hello'; + } + } + var SystemDashboardView = AbstractAction.extend({ template: 'system_dashboard_classic.self_service_dashboard', init: function(parent, context) { @@ -23,38 +193,145 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require var data = []; var self = this; window.selfOb = this; + // Store interval ID for cleanup on destroy + this.pollingIntervalId = null; if (context.tag == 'system_dashboard_classic.dashboard_self_services') { self._rpc({ model: 'system_dashboard_classic.dashboard', method: 'get_data', }, []).then(function(result) { window.resultX = result; + + // IMMEDIATE CSS VARIABLE APPLICATION - Apply colors before any rendering + // This prevents the "flash of green/default" issue by setting CSS vars synchronously + if (result.chart_colors) { + var root = document.documentElement; + var primary = result.chart_colors.primary || '#0891b2'; + var secondary = result.chart_colors.secondary || '#1e293b'; + var warning = result.chart_colors.warning || '#f59e0b'; + var success = result.chart_colors.success || '#10b981'; + + // Apply primary color and variants + root.style.setProperty('--dash-primary', primary); + root.style.setProperty('--dash-primary-light', adjustColor(primary, 30)); + root.style.setProperty('--dash-primary-dark', adjustColor(primary, -20)); + + // Apply secondary color + root.style.setProperty('--dash-secondary', secondary); + root.style.setProperty('--dash-secondary-light', adjustColor(secondary, 20)); + + // Apply status colors + root.style.setProperty('--dash-success', success); + root.style.setProperty('--dash-warning', warning); + + // Generate RGB values for rgba() usage + var rgb = hexToRgbComponents(primary); + if (rgb) { + root.style.setProperty('--dash-primary-rgb', rgb.r + ', ' + rgb.g + ', ' + rgb.b); + } + + // Cache colors in localStorage for instant loading on next page load + // The inline script in system_dashboard.xml reads this BEFORE CSS loads + try { + localStorage.setItem('dashboard_colors', JSON.stringify({ + primary: primary, + primaryLight: adjustColor(primary, 30), + primaryDark: adjustColor(primary, -20), + secondary: secondary, + warning: warning, + success: success + })); + } catch(e) {} + } + + // ========================================== + // CELEBRATION TRIGGER: Birthday & Anniversary + // ========================================== + if (result.celebration) { + // Show birthday celebration first (if applicable) + if (result.celebration.is_birthday) { + showCelebration('birthday'); + } + // Show anniversary celebration (with delay if both apply) + if (result.celebration.is_anniversary && result.celebration.anniversary_years > 0) { + var delay = result.celebration.is_birthday ? 2000 : 0; + setTimeout(function() { + showCelebration('anniversary', result.celebration.anniversary_years); + }, delay); + } + } + if (result.employee !== undefined && result.employee !== '' && result.employee[0].length !== 0) { $('#main-cards').css('display', 'flex'); $('.fn-id,.fn-department,.fn-job').show(); // SET EMPLOYEE DATA if (result.employee[0][0] !== undefined) { + var employeeId = result.employee[0][0].id; + // Make ONLY photo clickable to HR record $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.employee[0][0].image_128 + ')') - $("p.fn-section").html(result.employee[0][0].name); - $("p.fn-id").html(result.employee[0][0].emp_no); - if (result.employee[0][0].job_id.length !== 0) { - $("p.fn-job").html(result.employee[0][0].job_id[1]); + .addClass('clickable-profile') + .off('click').on('click', function() { + self.do_action({ + type: 'ir.actions.act_window', + res_model: 'hr.employee', + res_id: employeeId, + views: [[false, 'form']], + target: 'current' + }); + }); + // Employee name with greeting above (use celebration greeting if active) + var greetingText = getGreeting(); + + // Get gender info for personalized greeting + var genderInfo = result.gender_info || {gender: 'male', honorific: 'أستاذ'}; + var honorific = genderInfo.honorific || 'أستاذ'; + var genderClass = genderInfo.gender === 'female' ? 'gender-female' : 'gender-male'; + + // Add gender class to profile image container for styled frame + $(".img-box").addClass(genderClass); + + // Store gender info globally for use in other messages + window.dashboardGenderInfo = genderInfo; + + // Build greeting with honorific on same line (for Arabic/RTL) + var employeeName = result.employee[0][0].name; + var isRtl = $('body').hasClass('o_rtl'); + + // Format: "مساء الخير يا أستاذ" then "أحمد" on new line + var greetingWithHonorific = isRtl + ? (greetingText + ' يا ' + honorific) + : greetingText; + var greetingHtml = '' + greetingWithHonorific + ''; + + if (result.celebration && result.celebration.is_birthday) { + var birthdayText = isRtl ? 'عيد ميلاد سعيد!' : 'Happy Birthday!'; + greetingHtml = '🎂 ' + birthdayText + ''; + } else if (result.celebration && result.celebration.is_anniversary) { + var anniversaryText = isRtl ? 'شكراً لعطائك!' : 'Thank You!'; + greetingHtml = ' ' + anniversaryText + ''; } + $("p.fn-section").html(greetingHtml + '
' + employeeName); + // Employee code with styled badge + // Employee code with styled badge (Fallback to ID if emp_no missing) + var empCode = result.employee[0][0].emp_no || result.employee[0][0].pin || result.employee[0][0].barcode || result.employee[0][0].id; + $("p.fn-id").html('' + empCode + ''); + + // Job Title (Use safe multilingual variable) + $("p.fn-job").html(result.job_english || result.employee[0][0].job_id[1] || ''); } else { $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.user[0][0].image_128 + ')') $("p.fn-section").html(result.user[0][0].display_name); - $("p.fn-id").html(result.user[0][0].id); + $("p.fn-id").html('' + result.user[0][0].id + ''); } - // SET MAIN CARD DATA (LEAVES - TIMESHEET - PAYSLIPS) - $(".leaves_count span.value").html(parseFloat(result['employee'][0][0].remaining_leaves).toFixed(2)); - $(".timesheet_count span.value").html(result['employee'][0][0].timesheet_count); - $(".paylips_count span.value").html(result['employee'][0][0].payslip_count); + // SET MAIN CARD DATA (LEAVES - TIMESHEET - PAYSLIPS) with animated counters + animateCounter(".leaves_count span.value", parseFloat(result['employee'][0][0].remaining_leaves) || 0, 1200, '', 2); + animateCounter(".timesheet_count span.value", parseInt(result['employee'][0][0].timesheet_count) || 0, 1200); + animateCounter(".paylips_count span.value", parseInt(result['employee'][0][0].payslip_count) || 0, 1200); // LEAVES DO ACTION $("#leaves_btn").on('click', function(event) { - console.log(this); event.stopPropagation(); event.preventDefault(); return self.do_action({ @@ -86,7 +363,6 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require // TIMESHEET DO ACTION $("#timesheet_btn").on('click', function(event) { - console.log(this); event.stopPropagation(); event.preventDefault(); return self.do_action({ @@ -114,7 +390,6 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require //PAYSLIPS DO ACTION $("#paylips_btn").on('click', function(event) { - console.log(this); event.stopPropagation(); event.preventDefault(); return self.do_action({ @@ -142,7 +417,8 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require // SET EMPLOYEE DATA $(".img-box").css('background-image', 'url(data:image/png;base64,' + result.user[0][0].image_128 + ')') $("p.fn-section").html(result.user[0][0].name); - $("p.fn-id").html('ID: ' + result.user[0][0].id); + $("p.fn-job").html(result.job_english || ''); + $("p.fn-id").html(result.user[0][0].id); } } @@ -158,6 +434,20 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require self.$el.find('div.card-section1').append(card); } }); + + // Apply animated counters to all service cards after they're added to DOM + setTimeout(function() { + self.$el.find('.service-card-count').each(function() { + var $el = $(this); + var targetCount = parseInt($el.attr('data-target-count')) || 0; + animateCounter($el, targetCount, 1200); + }); + + // Initialize drag and drop for service cards reordering + // Pass server-prefetched order and RPC context for cross-device persistence + var serverServiceOrder = result.card_orders ? result.card_orders['dashboard_card_order'] : null; + initServiceCardsDragDrop('div.card-section1', self._rpc.bind(self), serverServiceOrder); + }, 100); // triggering click event in the main card self.$el.find('div.card3 .box-1').on('click', function(event) { @@ -244,6 +534,190 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require } }, { on_reverse_breadcrumb: self.on_reverse_breadcrumb }) }); + + // ============================================================ + // APPROVAL CARDS HANDLING (Merged from system_dashboard.js) + // This code handles cards of type 'approve' for managers + // ============================================================ + + // Function to build/rebuild approval cards + // IMPORTANT: Receives full result object to access card_orders for drag-drop + function buildApprovalCards(resultData) { + var cards = resultData.cards; + var cardOrders = resultData.card_orders; + + // Clear existing cards + self.$el.find('div.card-section-approve').empty(); + self.$el.find('div.card-section-track').empty(); + + $.each(cards, function(index, cardData) { + if (cardData.type == "approve") { + // Show approval tabs + $('.approval-tab-item').show(); + + // BUILD APPROVE CARD + var card = buildCardApprove(cardData, index, 'table1'); + $(card).find('.card-header h4 span').html(_t(cardData.name)); + // BUILD FOLLOW CARD + var card2 = buildCardApprove(cardData, index, 'table2'); + $(card2).find('.card-header h4 span').html(_t(cardData.name)); + // APPENDING CARDS + self.$el.find('div.card-section-approve').append(card); + self.$el.find('div.card-section-track').append(card2); + } + }); + + // Rebind click handlers for the new cards + self.$el.find('tr[data-target="record-button"]').off('click').on('click', function(event) { + event.stopPropagation(); + event.preventDefault(); + var model = $(this).attr('data-model'); + var name = $(this).attr('data-name'); + var domain = $(this).attr('data-domain'); + var form_view = parseInt($(this).attr('form-view')); + var list_view = parseInt($(this).attr('list-view')); + + if (domain === undefined || domain === '') { + domain = false; + } else { + domain = JSON.parse(domain); + } + if (isNaN(form_view)) form_view = false; + if (isNaN(list_view)) list_view = false; + + return self.do_action({ + name: _t(name), + type: 'ir.actions.act_window', + res_model: model, + view_mode: 'tree,form', + view_type: 'list', + views: [[list_view, 'list'], [form_view, 'form']], + domain: domain, + target: 'main', + flags: { reload: true } + }, { on_reverse_breadcrumb: function() { return self.reload(); } }); + }); + + // Update pending count badge - Sum ACTUAL pending requests from data + var totalPendingCount = 0; + $.each(cards, function(index, cardData) { + if (cardData.type == 'approve' && cardData.lines && cardData.lines.length > 0) { + $.each(cardData.lines, function(lineIdx, line) { + if (line.count_state_click) { + totalPendingCount += parseInt(line.count_state_click) || 0; + } + }); + } + }); + + if (totalPendingCount > 0) { + $('.pending-count-badge').text(totalPendingCount).show(); + } else { + $('.pending-count-badge').hide(); + } + + // Initialize drag and drop for approval cards ONLY + // Track tab will use the SAME saved order but without its own drag-drop handlers + setTimeout(function() { + // Use cardOrders from the received resultData (MUST be fresh, not closure) + var serverApprovalOrder = cardOrders ? cardOrders['dashboard_approval_order'] : null; + + // Enable drag-drop ONLY on approve tab + initDragDropSortable({ + containerSelector: 'div.card-section-approve', + cardSelector: '.card2', + storageKey: 'dashboard_approval_order', + rpcContext: self._rpc.bind(self), + serverOrder: serverApprovalOrder, + linkedContainerSelector: 'div.card-section-track' // Sync DOM to track tab on drag end + }); + + // Apply same order to track tab (no drag-drop, just reorder DOM) + var $trackContainer = $('div.card-section-track'); + if ($trackContainer.length > 0 && serverApprovalOrder && serverApprovalOrder.length > 0) { + var $trackCards = $trackContainer.find('.card2'); + serverApprovalOrder.forEach(function(cardId) { + var $matchingCard = $trackCards.filter(function() { + var model = $(this).find('.box-1').attr('data-model') || $(this).attr('data-model'); + return model === cardId; + }).first(); + if ($matchingCard.length > 0) { + $trackContainer.append($matchingCard); + } + }); + } + }, 150); + } + + // Initial build of approval cards (pass full result object) + buildApprovalCards(result); + + // ============================================================ + // TAB CLICK - Cards are already built, no need to refresh on tab switch + // Polling handles data freshness. Drag-drop order is preserved. + // ============================================================ + + // ============================================================ + // POLLING FOR NEW APPROVAL REQUESTS (Feature 7) + // Checks every 60 seconds for new requests and plays notification + // ============================================================ + var lastApproveCount = self.$el.find('div.card-section-approve .card3').length; + var notificationSound = null; + var audioUnlocked = false; + + // Simple notification sound as base64 (short beep) + var soundDataUri = 'data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YVoGAACBhYqFbF1fdH2AgYB4cWpvdXt/fn55dHFydnl8fHt6eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7enl4d3d4eXp7e3t6eXh3d3h5ent7e3p5eHd3eHl6e3t7'; + + // Unlock audio on first user interaction (browser policy) + $(document).one('click keydown', function() { + try { + notificationSound = new Audio(soundDataUri); + notificationSound.volume = 0.5; + notificationSound.load(); + audioUnlocked = true; + } catch (e) { + // Audio notification not supported + } + }); + + // Poll for new approval requests every 60 seconds + self.pollingIntervalId = setInterval(function() { + self._rpc({ + model: 'system_dashboard_classic.dashboard', + method: 'get_data', + }, []).then(function(freshResult) { + // Count ACTUAL pending requests from fresh data + var newApproveCount = 0; + $.each(freshResult.cards, function(index, cardData) { + if (cardData.type == 'approve' && cardData.lines && cardData.lines.length > 0) { + $.each(cardData.lines, function(lineIdx, line) { + if (line.count_state_click) { + newApproveCount += parseInt(line.count_state_click) || 0; + } + }); + } + }); + + // Check if there are NEW requests + if (newApproveCount > lastApproveCount) { + // Play notification sound + if (audioUnlocked && notificationSound) { + notificationSound.currentTime = 0; + notificationSound.play().catch(function(e) { + // Ignore errors silently + }); + } + + // Rebuild cards and update badge + buildApprovalCards(freshResult.cards); + } + + // Update count for next comparison + lastApproveCount = newApproveCount; + }).catch(function(e) { + // Silently ignore polling errors + }); + }, 60000); // Check every 60 seconds } // Chart Settings setTimeout(function() { @@ -257,20 +731,198 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require } if (result.attendance !== undefined) { setupAttendanceArea(result.attendance[0], name); - // check button submission - $('button#check_button').on('click', function(event) { - self._rpc({ - model: 'system_dashboard_classic.dashboard', - method: 'checkin_checkout', - context: { 'check': check }, - }, []).then(function(result) { - console.log(result); - console.log(resultX.employee); - $('.last-checkin-section').html(''); - $('.attendance-img-section').html(''); - $('#check_button').html(''); - setupAttendanceArea(result, resultX.employee[0][0].name); - }); + + // Check if attendance is enabled from settings + var isAttendanceEnabled = result.enable_attendance_button !== false; + + // Icon-based check-in/out click handler + if (!isAttendanceEnabled) { + // Disable icon visually but keep it visible + $('.attendance-img-section').addClass('disabled') + .attr('title', $('body').hasClass('o_rtl') + ? 'تسجيل الحضور معطّل من قبل المسؤول' + : 'Check-in/out disabled by administrator'); + } + + // Icon click handler for check-in/out + $('.attendance-img-section').off('click').on('click', function(event) { + // Check if attendance is enabled + if (!isAttendanceEnabled) { + return false; + } + + var $attendanceIcon = $(this); + var isRtl = $('body').hasClass('o_rtl'); + + // Helper: Show error toast with OK button + function showErrorToast(message) { + $('.attendance-error-overlay').remove(); + var $errorToast = $( + '
' + + '
' + + '
⚠️
' + + '
' + message + '
' + + '' + + '
' + + '
' + ); + $('body').append($errorToast); + setTimeout(function() { $errorToast.addClass('show'); }, 50); + + $errorToast.find('.error-ok-btn').on('click', function() { + $errorToast.removeClass('show'); + setTimeout(function() { $errorToast.remove(); }, 300); + }); + } + + // Helper: Perform check-in/out RPC + function performCheckinCheckout(lat, lng) { + self._rpc({ + model: 'system_dashboard_classic.dashboard', + method: 'checkin_checkout', + args: [lat, lng], + }).then(function(result) { + // Check for zone validation errors returned from server + if (result && result.error) { + showErrorToast(result.message); + return; + } + + $('.last-checkin-section').html(''); + $('.attendance-img-section').html(''); + setupAttendanceArea(result, resultX.employee[0][0].name); + + // === Premium Check-in/out Notification === + var isCheckIn = result.is_attendance; + var employeeName = resultX.employee[0][0].name || ''; + var firstName = employeeName.split(' ')[0]; + + var now = new Date(); + var hours = now.getHours(); + var minutes = now.getMinutes().toString().padStart(2, '0'); + var ampm = hours >= 12 ? (isRtl ? 'م' : 'PM') : (isRtl ? 'ص' : 'AM'); + hours = hours % 12 || 12; + var timeStr = hours + ':' + minutes + ' ' + ampm; + + var title, subtitle, icon, gradientClass; + // Gendered pronouns: لك (male) / لكِ (female) + var genderInfo = window.dashboardGenderInfo || {pronoun_you: 'ك'}; + var forYou = 'ل' + genderInfo.pronoun_you; // لك or لكِ + if (isCheckIn) { + icon = '☀️'; + gradientClass = 'notification-checkin'; + if (isRtl) { + title = 'أهلاً ' + firstName + '!'; + subtitle = 'نتمنى ' + forYou + ' يوماً مثمراً ومليئاً بالإنجازات'; + } else { + title = 'Welcome, ' + firstName + '!'; + subtitle = 'Wishing you a productive and successful day'; + } + } else { + icon = '🌙'; + gradientClass = 'notification-checkout'; + if (isRtl) { + title = 'مع السلامة ' + firstName + '!'; + subtitle = 'شكراً لجهودك اليوم، نراك غداً بإذن الله'; + } else { + title = 'Goodbye, ' + firstName + '!'; + subtitle = 'Thank you for your hard work today. See you tomorrow!'; + } + } + + $('.attendance-notification-overlay').remove(); + var $notification = $( + '
' + + '
' + + '
' + + '' + icon + '' + + '
' + + '
' + + '

' + title + '

' + + '

' + subtitle + '

' + + '
' + + '🕐' + + '' + timeStr + '' + + '
' + + '
' + + '
' + + '
' + ); + + $('body').append($notification); + $('.attendance-img-section').addClass('attendance-success-pulse'); + setTimeout(function() { + $('.attendance-img-section').removeClass('attendance-success-pulse'); + }, 800); + setTimeout(function() { $notification.addClass('show'); }, 50); + setTimeout(function() { + $notification.removeClass('show'); + setTimeout(function() { $notification.remove(); }, 400); + }, 5500); + + }).guardedCatch(function(error) { + // Handle unexpected server errors only + var errorMsg = isRtl ? 'حدث خطأ غير متوقع. حاول مرة أخرى' : 'An unexpected error occurred. Please try again.'; + showErrorToast(errorMsg); + }); + } + + // ============================================================ + // GEOLOCATION: Get user's current position before check-in/out + // ============================================================ + if (navigator.geolocation) { + // Show loading indicator on attendance icon + $attendanceIcon.css('opacity', '0.6'); + + var geoOptions = { + enableHighAccuracy: true, + timeout: 10000, // 10 seconds timeout + maximumAge: 0 // Don't use cached position + }; + + navigator.geolocation.getCurrentPosition( + // Success: Got location + function(position) { + $attendanceIcon.css('opacity', '1'); + var lat = position.coords.latitude; + var lng = position.coords.longitude; + performCheckinCheckout(lat, lng); + }, + // Error: Location failed + function(error) { + $attendanceIcon.css('opacity', '1'); + var errorMessage; + switch(error.code) { + case error.PERMISSION_DENIED: + errorMessage = isRtl + ? 'يجب السماح بالوصول إلى موقعك الجغرافي لتسجيل الحضور.\nيرجى تفعيل صلاحية الموقع في إعدادات المتصفح.' + : 'Location access is required for attendance.\nPlease enable location permission in browser settings.'; + break; + case error.POSITION_UNAVAILABLE: + errorMessage = isRtl + ? 'تعذر تحديد موقعك الجغرافي.\nتأكد من تفعيل GPS على جهازك.' + : 'Unable to determine your location.\nMake sure GPS is enabled on your device.'; + break; + case error.TIMEOUT: + errorMessage = isRtl + ? 'انتهت المهلة المحددة لتحديد الموقع.\nحاول مرة أخرى.' + : 'Location request timed out.\nPlease try again.'; + break; + default: + errorMessage = isRtl + ? 'حدث خطأ في تحديد الموقع.' + : 'An error occurred while getting location.'; + } + showErrorToast(errorMessage); + }, + geoOptions + ); + } else { + // Browser doesn't support geolocation + showErrorToast(isRtl + ? 'متصفحك لا يدعم خدمة تحديد الموقع الجغرافي.' + : 'Your browser does not support geolocation.'); + } }); } else { $('.attendance-section-body').hide(); @@ -288,48 +940,83 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require var total_timesheet_title = "الساعات المنجزة"; var remaining_timesheet_title = "إجمالي ساعات الأسبوع "; var taken_timesheet_title = "الساعات المتبقية "; + + // Chart Tooltip Labels (Arabic) + var label_used = "مستنفذ"; + var label_left = "متبقي"; + var label_received = "مستلم"; + var label_remaining = "متبقي"; + var label_done = "منجز"; + var label_worked = "ساعات عمل"; } else { var total_leaves_title = "Total Balance "; - var remaining_leaves_title = "Left Balance "; - var taken_leaves_title = "Total remaining days "; + var remaining_leaves_title = "Days Left "; + var taken_leaves_title = "Days Used "; var total_payroll_title = "Total annual slips"; - var remaining_payroll_title = "Remaining salary slips "; - var taken_payroll_title = "Left salary slips "; + var remaining_payroll_title = "Slips Remaining "; + var taken_payroll_title = "Slips Received "; - var total_timesheet_title = "Total hours of week"; - var remaining_timesheet_title = "Remaining hours "; - var taken_timesheet_title = "Left hours "; + var total_timesheet_title = "Hours Done "; + var remaining_timesheet_title = "Hours Left "; + var taken_timesheet_title = "Hours Left "; + + // Chart Tooltip Labels (English) + var label_used = "Used"; + var label_left = "Left"; + var label_received = "Received"; + var label_remaining = "Remaining"; + var label_done = "Done"; + var label_worked = "Worked"; } - if (result.leaves !== undefined) { - if (result.leaves[0].taken > 0 || result.leaves[0].remaining_leaves > 0) { - var total_leaves = (result.leaves[0].taken + result.leaves[0].remaining_leaves); - var remaining_leaves_percent = ((result.leaves[0].remaining_leaves / total_leaves) * 100).toFixed(2); - var taken_leaves_percent = ((result.leaves[0].taken / total_leaves) * 100).toFixed(2); - //set data - $('.leave-total-amount').html(total_leaves_title + " " + Math.round(parseFloat(result.leaves[0].taken + result.leaves[0].remaining_leaves))); - $('.leave-left-amount').html(remaining_leaves_title + " " + Math.round(parseFloat(result.leaves[0].remaining_leaves))); - if ($('body').hasClass('o_rtl') === true) { - $('.leave-data-percent').html('%' + taken_leaves_percent); - } else { - $('.leave-data-percent').html(taken_leaves_percent + '%'); - } - $('#chartContainer').html(''); + // LEAVE CHART: Show if module is installed (regardless of balance) + if (result.leaves !== undefined && result.leaves[0] && result.leaves[0].is_module_installed) { + // Get colors from configuration + var primaryColor = result.chart_colors ? result.chart_colors.primary : '#0891b2'; + var warningColor = result.chart_colors ? result.chart_colors.warning : '#f59e0b'; + var secondaryColor = result.chart_colors ? result.chart_colors.secondary : '#1e293b'; + + var total_leaves = (result.leaves[0].taken + result.leaves[0].remaining_leaves); + var remaining_leaves = Math.round(result.leaves[0].remaining_leaves); + var taken_leaves = Math.round(result.leaves[0].taken); + + // Handle zero balance case gracefully + var remaining_leaves_percent = total_leaves > 0 ? Math.round((result.leaves[0].remaining_leaves / total_leaves) * 100) : 0; + var taken_leaves_percent = total_leaves > 0 ? Math.round((result.leaves[0].taken / total_leaves) * 100) : 0; + + // Genius Design: Show Left (primary) and Used (warning) with matching colors + if ($('body').hasClass('o_rtl') === true) { + $('.leave-total-amount').html('' + remaining_leaves + ' ' + remaining_leaves_title); + $('.leave-left-amount').html('' + taken_leaves + ' ' + taken_leaves_title); + $('.leave-center-value').css('color', secondaryColor); + animateCounter('.leave-center-value', total_leaves > 0 ? Math.round(total_leaves) : 0, 1200); + $('.leave-center-unit').html('يوم'); + } else { + $('.leave-total-amount').html('' + remaining_leaves + ' ' + remaining_leaves_title); + $('.leave-left-amount').html('' + taken_leaves + ' ' + taken_leaves_title); + $('.leave-center-value').css('color', secondaryColor); + animateCounter('.leave-center-value', total_leaves > 0 ? Math.round(total_leaves) : 0, 1200); + $('.leave-center-unit').html('days'); + } + $('#chartContainer').html(''); + + // Draw chart (show empty state if no balance) + if (total_leaves > 0) { pluscharts.draw({ drawOn: "#chartContainer", - type: "donut", + type: result.chart_types.annual_leave || "donut", dataset: { data: [{ - label: remaining_leaves_title, - value: ((result.leaves[0].taken / total_leaves) * 100) + label: taken_leaves_percent + "% " + label_used, + value: taken_leaves_percent }, { - label: taken_leaves_title, - value: ((result.leaves[0].remaining_leaves / total_leaves) * 100) + label: remaining_leaves_percent + "% " + label_left, + value: remaining_leaves_percent } ], - backgroundColor: ["#003056", "#2ead97"], + backgroundColor: [warningColor, primaryColor], borderColor: "#ffffff", borderWidth: 0, }, @@ -345,44 +1032,81 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require height: 20 }, size: { - width: '140', //give 'container' if you want width and height of initiated container + width: '140', height: '140' } } }); - // display section + } else { + // No balance - show empty donut + pluscharts.draw({ + drawOn: "#chartContainer", + type: result.chart_types.annual_leave || "donut", + dataset: { + data: [{ + label: "0% " + label_left, + value: 100 + }], + backgroundColor: ['#e5e7eb'], + borderColor: "#ffffff", + borderWidth: 0, + }, + options: { + width: 15, + text: { display: false }, + legends: { display: false }, + size: { width: '140', height: '140' } + } + }); + } + // display section (only if enabled in settings) + if (!result.stats_visibility || result.stats_visibility.show_annual_leave !== false) { $('#leave-section').fadeIn(); } } - if (result.payroll !== undefined) { - if (result.payroll[0].taken > 0 || result.payroll[0].payslip_remaining > 0) { - // Configure Salary Slips Chart + // PAYROLL CHART: Show if module is installed (regardless of balance) + if (result.payroll !== undefined && result.payroll[0] && result.payroll[0].is_module_installed) { + // Get colors from configuration + var primaryColor = result.chart_colors ? result.chart_colors.primary : '#0891b2'; + var warningColor = result.chart_colors ? result.chart_colors.warning : '#f59e0b'; + var secondaryColor = result.chart_colors ? result.chart_colors.secondary : '#1e293b'; + var total_payroll = (result.payroll[0].taken + result.payroll[0].payslip_remaining); - var remaining_payroll_percent = ((result.payroll[0].payslip_remaining / total_payroll) * 100).toFixed(2); - var taken_payroll_percent = ((result.payroll[0].taken / total_payroll) * 100).toFixed(2); - $('.payroll-total-amount').html(total_payroll_title + " " + parseFloat(total_payroll)); - $('.payroll-left-amount').html(remaining_payroll_title + " " + parseFloat(result.payroll[0].payslip_remaining)); + var remaining_payroll = Math.round(result.payroll[0].payslip_remaining); + var taken_payroll = Math.round(result.payroll[0].taken); + var remaining_payroll_percent = Math.round((result.payroll[0].payslip_remaining / total_payroll) * 100); + var taken_payroll_percent = Math.round((result.payroll[0].taken / total_payroll) * 100); + + // Genius Design: Show remaining (primary) and received (warning) with matching colors if ($('body').hasClass('o_rtl') === true) { - $('.payroll-data-percent').html('%' + taken_payroll_percent); + $('.payroll-total-amount').html('' + remaining_payroll + ' ' + remaining_payroll_title); + $('.payroll-left-amount').html('' + taken_payroll + ' ' + taken_payroll_title); + $('.payroll-center-value').css('color', secondaryColor); + animateCounter('.payroll-center-value', Math.round(total_payroll), 1200); + $('.payroll-center-unit').html('قسيمة'); } else { - $('.payroll-data-percent').html(taken_payroll_percent + '%'); + $('.payroll-total-amount').html('' + remaining_payroll + ' ' + remaining_payroll_title); + $('.payroll-left-amount').html('' + taken_payroll + ' ' + taken_payroll_title); + $('.payroll-center-value').css('color', secondaryColor); + animateCounter('.payroll-center-value', Math.round(total_payroll), 1200); + $('.payroll-center-unit').html('slips'); } $('#chartPaylips').html(''); pluscharts.draw({ drawOn: "#chartPaylips", - type: "donut", + type: result.chart_types.salary_slips || "donut", dataset: { data: [{ - label: remaining_payroll_title, - value: ((result.payroll[0].payslip_remaining / total_payroll) * 100) + label: taken_payroll_percent + "% " + label_received, + value: taken_payroll_percent }, { - label: taken_payroll_title, - value: ((result.payroll[0].taken / total_payroll) * 100) + label: remaining_payroll_percent + "% " + label_remaining, + value: remaining_payroll_percent } ], - backgroundColor: ["#003056", "#2ead97"], + backgroundColor: [warningColor, primaryColor], borderColor: "#ffffff", borderWidth: 0, }, @@ -398,45 +1122,59 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require height: 20 }, size: { - width: '140', //give 'container' if you want width and height of initiated container + width: '140', height: '140' } } }); - // display section - $('#salary-section').fadeIn(); - } + // display section (only if enabled in settings) + if (!result.stats_visibility || result.stats_visibility.show_salary_slips !== false) { + $('#salary-section').fadeIn(); + } } - if (result.timesheet !== undefined) { - if (result.timesheet[0].taken > 0 || result.timesheet[0].timesheet_remaining > 0) { - // Configure Weekly Timesheet Chart + // TIMESHEET CHART: Show if module is installed (regardless of balance) + if (result.timesheet !== undefined && result.timesheet[0] && result.timesheet[0].is_module_installed) { + // Get colors from configuration + var primaryColor = result.chart_colors ? result.chart_colors.primary : '#0891b2'; + var warningColor = result.chart_colors ? result.chart_colors.warning : '#f59e0b'; + var secondaryColor = result.chart_colors ? result.chart_colors.secondary : '#1e293b'; + var total_timesheet = (result.timesheet[0].taken + result.timesheet[0].timesheet_remaining); - var remaining_timesheet_percent = ((result.timesheet[0].timesheet_remaining / total_timesheet) * 100).toFixed(2); - var taken_timesheet_percent = ((result.timesheet[0].taken / total_timesheet) * 100).toFixed(2); - $('.timesheet-total-amount').html(total_timesheet_title + " " + parseFloat(total_timesheet)); - $('.timesheet-left-amount').html(remaining_timesheet_title + " " + parseFloat(result.timesheet[0].timesheet_remaining)); - $('.timesheet-data-percent').html(taken_timesheet_percent); + var done_hours = Math.round(result.timesheet[0].taken); + var remaining_hours = Math.round(result.timesheet[0].timesheet_remaining); + var done_hours_percent = Math.round((result.timesheet[0].taken / total_timesheet) * 100); + var remaining_hours_percent = Math.round((result.timesheet[0].timesheet_remaining / total_timesheet) * 100); + + // Genius Design: Show Remaining (primary) and Done (warning) - consistent with Leaves/Payroll if ($('body').hasClass('o_rtl') === true) { - $('.timesheet-data-percent').html('%' + taken_timesheet_percent); + $('.timesheet-total-amount').html('' + remaining_hours + ' ' + remaining_timesheet_title); + $('.timesheet-left-amount').html('' + done_hours + ' ' + total_timesheet_title); + $('.timesheet-center-value').css('color', secondaryColor); + animateCounter('.timesheet-center-value', Math.round(total_timesheet), 1200); + $('.timesheet-center-unit').html('ساعة'); } else { - $('.timesheet-data-percent').html(taken_timesheet_percent + '%'); + $('.timesheet-total-amount').html('' + remaining_hours + ' ' + remaining_timesheet_title); + $('.timesheet-left-amount').html('' + done_hours + ' ' + total_timesheet_title); + $('.timesheet-center-value').css('color', secondaryColor); + animateCounter('.timesheet-center-value', Math.round(total_timesheet), 1200); + $('.timesheet-center-unit').html('hrs'); } $('#chartTimesheet').html(''); pluscharts.draw({ drawOn: "#chartTimesheet", - type: "donut", + type: result.chart_types.timesheet || "donut", dataset: { data: [{ - label: taken_timesheet_title, - value: ((result.timesheet[0].taken / total_timesheet) * 100) + label: done_hours_percent + "% " + label_done, + value: done_hours_percent }, { - label: remaining_timesheet_title, - value: ((result.timesheet[0].timesheet_remaining / total_timesheet)) + label: remaining_hours_percent + "% " + label_left, + value: remaining_hours_percent } ], - backgroundColor: ["#003056", "#2ead97"], + backgroundColor: [warningColor, primaryColor], borderColor: "#ffffff", borderWidth: 0, }, @@ -452,18 +1190,100 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require height: 20 }, size: { - width: '140', //give 'container' if you want width and height of initiated container + width: '140', height: '140' } } }); - // display section - $('#timesheet-section').fadeIn(); - } + // display section (only if enabled in settings) + if (!result.stats_visibility || result.stats_visibility.show_timesheet !== false) { + $('#timesheet-section').fadeIn(); + } + } + + // ATTENDANCE HOURS CHART: Show if module is installed (regardless of balance) + if (result.attendance_hours !== undefined && result.attendance_hours[0] && result.attendance_hours[0].is_module_installed) { + // Get colors from configuration + var primaryColor = result.chart_colors ? result.chart_colors.primary : '#0891b2'; + var warningColor = result.chart_colors ? result.chart_colors.warning : '#f59e0b'; + var secondaryColor = result.chart_colors ? result.chart_colors.secondary : '#1e293b'; + + var total_attendance = result.attendance_hours[0].plan_hours; + var worked_hours = Math.round(result.attendance_hours[0].official_hours); + var remaining_hours = Math.round(total_attendance - result.attendance_hours[0].official_hours); + var worked_percent = total_attendance > 0 ? Math.round((result.attendance_hours[0].official_hours / total_attendance) * 100) : 0; + var remaining_percent = total_attendance > 0 ? (100 - worked_percent) : 0; + + // Genius Design: Show Total Planned (primary) and Worked (warning) - Goal vs Actual + if ($('body').hasClass('o_rtl') === true) { + $('.attendance-plan-hours').html('' + Math.round(total_attendance) + ' الساعات المخططة'); + $('.attendance-official-hours').html('' + worked_hours + ' ساعات الحضور'); + $('.attendance-center-value').css('color', secondaryColor); + animateCounter('.attendance-center-value', Math.round(total_attendance), 1200); + $('.attendance-center-unit').html('ساعة'); + } else { + $('.attendance-plan-hours').html('' + Math.round(total_attendance) + ' hrs Planned'); + $('.attendance-official-hours').html('' + worked_hours + ' hrs Worked'); + $('.attendance-center-value').css('color', secondaryColor); + animateCounter('.attendance-center-value', Math.round(total_attendance), 1200); + $('.attendance-center-unit').html('hrs'); + } + $('#chartAttendanceHours').html(''); + pluscharts.draw({ + drawOn: "#chartAttendanceHours", + type: result.chart_types.attendance_hours || "donut", + dataset: { + data: [{ + label: worked_percent + "% " + label_worked, + value: worked_percent + }, + { + label: remaining_percent + "% " + label_remaining, + value: remaining_percent + } + ], + backgroundColor: [warningColor, primaryColor], + borderColor: "#ffffff", + borderWidth: 0, + }, + options: { + width: 15, + text: { + display: false, + color: "#f6f6f6" + }, + legends: { + display: false, + width: 20, + height: 20 + }, + size: { + width: '140', + height: '140' + } + } + }); + // display section (only if enabled in settings) + if (!result.stats_visibility || result.stats_visibility.show_attendance_hours !== false) { + $('#attendance-hours-section').fadeIn(); + } } // hide charts loader $('.charts-over-layer').fadeOut(); + + // Initialize drag and drop for stats module-boxes in header + // Uses server-side persistence for cross-device sync + setTimeout(function() { + var serverStatsOrder = result.card_orders ? result.card_orders['dashboard_stats_order'] : null; + initDragDropSortable({ + containerSelector: '.dashboard-module-charts', + cardSelector: '.module-box', + storageKey: 'dashboard_stats_order', + rpcContext: self._rpc.bind(self), + serverOrder: serverStatsOrder + }); + }, 200); }, 1000); }) @@ -487,8 +1307,119 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require }, start: function() { var self = this; + // Inject search bar after template is attached to DOM + setTimeout(function() { + self._addSearchBar(); + }, 500); + // Load stats visibility settings FRESH on every start (fixes caching issue) + setTimeout(function() { + self._loadStatsVisibility(); + }, 600); return this._super(); }, + + /** + * Load statistics visibility settings from database and apply + * Called on every dashboard start to ensure settings are always fresh + */ + _loadStatsVisibility: function() { + var self = this; + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'res.config.settings', + method: 'get_stats_visibility', + args: [], + kwargs: {} + }).then(function(visibility) { + // Annual Leave card + if (!visibility.show_annual_leave) { + self.$('#leave-section').addClass('genius-hidden'); + } else { + self.$('#leave-section').removeClass('genius-hidden'); + } + + // Salary Slips card + if (!visibility.show_salary_slips) { + self.$('#salary-section').addClass('genius-hidden'); + } else { + self.$('#salary-section').removeClass('genius-hidden'); + } + + // Weekly Timesheet card + if (!visibility.show_timesheet) { + self.$('#timesheet-section').addClass('genius-hidden'); + } else { + self.$('#timesheet-section').removeClass('genius-hidden'); + } + + // Attendance Hours card + if (!visibility.show_attendance_hours) { + self.$('#attendance-hours-section').addClass('genius-hidden'); + } else { + self.$('#attendance-hours-section').removeClass('genius-hidden'); + } + + // Attendance Check-in/out Section (right side panel) + if (!visibility.show_attendance_section) { + self.$('.dashboard-attendance-section').addClass('genius-hidden'); + self.$('.dashboard-charts-section').removeClass('col-md-10').addClass('col-md-12'); + self.$('.dashboard-user-statistics-section').addClass('attendance-hidden'); + } else { + self.$('.dashboard-attendance-section').removeClass('genius-hidden'); + self.$('.dashboard-charts-section').removeClass('col-md-12').addClass('col-md-10'); + self.$('.dashboard-user-statistics-section').removeClass('attendance-hidden'); + } + }).catch(function() { + // Silently handle visibility settings error + }); + }, + + /** + * Add search bar to the dashboard nav buttons + * This is called from start() to ensure DOM is ready + */ + _addSearchBar: function() { + var navButtons = this.$('.dashboard-nav-buttons'); + + if (this.$('.genius-search-container').length > 0 || navButtons.length === 0) { + return; + } + + // RTL/LTR placeholder translation + var searchPlaceholder = $('body').hasClass('o_rtl') + ? 'ابحث عن خدمة...' + : 'Search for a service...'; + + var searchHtml = '
' + + '
' + + '' + + '' + + '' + + '
'; + + navButtons.append(searchHtml); + + this.$('#geniusServiceSearch').on('input', function() { + var term = $(this).val().toLowerCase().trim(); + $('#geniusClearSearch').toggle(term.length > 0); + // Filter cards + var cards = $('.card3, .card2'); + if (term === '') { + cards.removeClass('genius-hidden'); + return; + } + cards.each(function() { + var card = $(this); + var text = card.find('h4, h3, td').text().toLowerCase(); + card.toggleClass('genius-hidden', !text.includes(term)); + }); + }); + + this.$('#geniusClearSearch').on('click', function() { + $('#geniusServiceSearch').val(''); + $(this).hide(); + $('.card3, .card2').removeClass('genius-hidden'); + }); + }, render: function() { var super_render = this._super; $(".o_control_panel").addClass("o_hidden"); @@ -496,6 +1427,24 @@ odoo.define('system_dashboard_classic.dashboard_self_services', function(require }, reload: function() { window.location.href = this.href; + }, + /** + * Cleanup on widget destruction - prevents memory leak + * Clears the polling interval for new approval requests + */ + destroy: function() { + // Clear polling interval to prevent memory leak + if (this.pollingIntervalId) { + clearInterval(this.pollingIntervalId); + this.pollingIntervalId = null; + } + // Call parent destroy + this._super.apply(this, arguments); + // Clear attendance clock interval if exists + if (window.attendanceClockInterval) { + clearInterval(window.attendanceClockInterval); + window.attendanceClockInterval = null; + } } }); @@ -528,11 +1477,32 @@ function buildCardZ(data, index) { var h4 = document.createElement('h4'); var content = document.createElement('div'); - $(img).attr('src', 'data:image/png;base64,' + data.image); - $(h3).html(data.state_count); + if (data.icon_type === 'icon' && data.icon_name) { + // Render FontAwesome Icon + var iconDict = document.createElement('div'); + var iconElem = document.createElement('i'); + + $(iconDict).addClass('service-icon-wrapper'); + + // Add fa class and the specific icon class (e.g., fa-plane) + // Apply secondary color using CSS variable for dynamic theming + // Added 'service-icon' class for consistent sizing (64px) and hover effects + // CSS handling: .service-icon in genius-enhancements.scss + $(iconElem).addClass('fa ' + data.icon_name + ' service-icon').css('color', 'var(--dash-secondary)'); + + $(iconDict).append(iconElem); + $(content).append(iconDict); + } else { + // Render Image (Default) + // Add specific class for sizing if not already present in CSS + $(img).attr('src', 'data:image/png;base64,' + data.image).addClass('service-icon'); + $(content).append(img); + } + + $(h3).addClass('service-card-count').attr('data-target-count', data.state_count || 0); $(h4).html(data.name); $(content).addClass('col-md-12 col-sm-12 col-xs-12'); - $(content).append(img).append(h3).append(h4); + $(content).append(h3).append(h4); $(box1).append(content); //box 2 var box2_i = document.createElement('i'); @@ -552,31 +1522,414 @@ function buildCardZ(data, index) { return card; } +/** + * Generic Drag and Drop Sortable Function + * Works with any card type by accepting configuration options + * Uses HTML5 Drag and Drop API with SERVER-SIDE persistence (+ localStorage cache) + * + * @param {Object} options Configuration options + * @param {string} options.containerSelector - CSS selector for the container + * @param {string} options.cardSelector - CSS selector for cards within container + * @param {string} options.storageKey - Storage key for saving order (used for both localStorage cache and server) + * @param {string} options.idAttribute - Attribute name to identify cards (default: data-name) + * @param {function} options.idFinder - Optional function to get card ID + * @param {Array} options.serverOrder - Optional pre-fetched order from server + * @param {Object} options.rpcContext - RPC context object (self._rpc compatible) + */ +function initDragDropSortable(options) { + var containerSelector = options.containerSelector; + var cardSelector = options.cardSelector || '.card3'; + var storageKey = options.storageKey || 'dashboard_card_order'; + var idFinder = options.idFinder || null; + var serverOrder = options.serverOrder || null; + var rpcContext = options.rpcContext || null; + var linkedContainerSelector = options.linkedContainerSelector || null; // Cross-tab sync + + var $container = $(containerSelector); + if ($container.length === 0) return; + + var $cards = $container.find(cardSelector); + if ($cards.length < 2) return; // Need at least 2 cards to reorder + + // Helper function to get card ID + function getCardId($card) { + if (idFinder) { + return idFinder($card); + } + // Check for data-name in box-1 (service cards) + var boxName = $card.find('.box-1').attr('data-name'); + if (boxName) return boxName; + // Check for id attribute (module-box stats cards) + var elemId = $card.attr('id'); + if (elemId) return elemId; + // Fallback to data-model attribute (approval cards) + var model = $card.find('.box-1').attr('data-model') || $card.attr('data-model'); + if (model) return model; + // Ultimate fallback: index + return $card.index(); + } + + // Priority: serverOrder > localStorage cache + var savedOrder = serverOrder; + if (!savedOrder) { + try { + var savedData = localStorage.getItem(storageKey); + if (savedData) { + savedOrder = JSON.parse(savedData); + } + } catch(e) { + // Ignore localStorage errors + } + } + + // Apply saved order if exists + if (savedOrder && Array.isArray(savedOrder) && savedOrder.length > 0) { + var orderedCards = []; + // First add cards in saved order + savedOrder.forEach(function(cardId) { + var $matchingCard = $cards.filter(function() { + return getCardId($(this)) === cardId; + }).first(); + if ($matchingCard.length > 0) { + orderedCards.push($matchingCard[0]); + } + }); + // Then add any new cards not in saved order + $cards.each(function() { + if (orderedCards.indexOf(this) === -1) { + orderedCards.push(this); + } + }); + // Reorder in DOM + orderedCards.forEach(function(card) { + $container.append(card); + }); + // Refresh reference after reordering + $cards = $container.find(cardSelector); + } + + // Make cards draggable + $cards.attr('draggable', 'true').addClass('draggable-card'); + + var draggedCard = null; + + // Drag Start - store reference to dragged card + $cards.on('dragstart', function(e) { + draggedCard = this; + $(this).addClass('dragging'); + // Required for Firefox + e.originalEvent.dataTransfer.effectAllowed = 'move'; + e.originalEvent.dataTransfer.setData('text/plain', ''); + }); + + // Drag End - cleanup and save order (to both localStorage AND server) + $cards.on('dragend', function() { + $(this).removeClass('dragging'); + $container.find(cardSelector).removeClass('drag-over'); + draggedCard = null; + + // Collect new order + var newOrder = []; + $container.find(cardSelector).each(function() { + var cardId = getCardId($(this)); + if (cardId) { + newOrder.push(cardId); + } + }); + + // Save to localStorage (immediate cache) + try { + localStorage.setItem(storageKey, JSON.stringify(newOrder)); + } catch(e) { + // Ignore localStorage errors + } + + // Save to server (persistent, cross-device) + if (rpcContext) { + rpcContext({ + model: 'system_dashboard_classic.dashboard', + method: 'save_card_order', + args: [storageKey, newOrder], + }).then(function() { + // Saved successfully to server + }).guardedCatch(function(err) { + // Silently fail - localStorage still has it + console.log('Card order save to server failed, using local cache'); + }); + } + + // ============================================================ + // CROSS-TAB DOM SYNC: Reorder the linked container to match + // ============================================================ + if (linkedContainerSelector) { + var $linkedContainer = $(linkedContainerSelector); + if ($linkedContainer.length > 0) { + var $linkedCards = $linkedContainer.find(cardSelector); + if ($linkedCards.length > 0) { + // Build ordered array based on newOrder + newOrder.forEach(function(cardId) { + var $matchingCard = $linkedCards.filter(function() { + return getCardId($(this)) === cardId; + }).first(); + if ($matchingCard.length > 0) { + $linkedContainer.append($matchingCard); + } + }); + } + } + } + }); + + // Drag Over - allow drop + $cards.on('dragover', function(e) { + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'move'; + + if (this !== draggedCard) { + $(this).addClass('drag-over'); + } + }); + + // Drag Leave - remove highlight + $cards.on('dragleave', function() { + $(this).removeClass('drag-over'); + }); + + // Drop - reorder cards + $cards.on('drop', function(e) { + e.preventDefault(); + if (this !== draggedCard && draggedCard) { + var $this = $(this); + var $dragged = $(draggedCard); + + // Get positions to determine insert before or after + var thisRect = this.getBoundingClientRect(); + var draggedRect = draggedCard.getBoundingClientRect(); + + // If dragging from left to right, insert after + // If dragging from right to left, insert before + if (draggedRect.left < thisRect.left) { + $dragged.insertAfter($this); + } else { + $dragged.insertBefore($this); + } + } + $(this).removeClass('drag-over'); + }); +} + +// Backward compatibility wrapper for service cards +function initServiceCardsDragDrop(containerSelector, rpcContext, serverOrder) { + initDragDropSortable({ + containerSelector: containerSelector, + cardSelector: '.card3', + storageKey: 'dashboard_card_order', + rpcContext: rpcContext, + serverOrder: serverOrder + }); +} + + function setupAttendanceArea(data, name) { window.check = data.is_attendance; var button = ''; + + // Get current date info for display + var now = new Date(); + var dayNames = $('body').hasClass('o_rtl') + ? ['الأحد', 'الإثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'] + : ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + var monthNames = $('body').hasClass('o_rtl') + ? ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'] + : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + var dayName = dayNames[now.getDay()]; + var day = now.getDate(); + var month = monthNames[now.getMonth()]; + var dateDisplay = $('body').hasClass('o_rtl') + ? dayName + '، ' + day + ' ' + month + : dayName + ', ' + day + ' ' + month; + if ($('body').hasClass('o_rtl')) { - var checkout_title = "زمن تسجيل اخر دخول" - var checkin_title = ",مرحباً بك" + var checkout_title = "وقت تسجيل اخر دخول"; + var checkin_title = "وقت تسجيل اخر خروج"; var checkin_button = "تسجيل الحضور"; var checkout_button = "تسجيل الخروج"; } else { - var checkout_title = "Last check in" - var checkin_title = "Welcome"; - var checkin_button = "Check In" + var checkout_title = "Last check in"; + var checkin_title = "Last check out"; + var checkin_button = "Check In"; var checkout_button = "Check Out"; } + + // Always show live clock (date and time) + function updateClock() { + var now = new Date(); + var hours = now.getHours().toString().padStart(2, '0'); + var minutes = now.getMinutes().toString().padStart(2, '0'); + var seconds = now.getSeconds().toString().padStart(2, '0'); + var timeStr = hours + ':' + minutes + ':' + seconds; + // Date on one line, time on another line + var clockHtml = '
' + dateDisplay + '
' + + '
' + timeStr + '
'; + $('.last-checkin-section').html(clockHtml); + } + + // Initial display + updateClock(); + + // Clear any existing interval first + if (window.attendanceClockInterval) { + clearInterval(window.attendanceClockInterval); + } + // Update every second (client-side only, no server calls) + window.attendanceClockInterval = setInterval(updateClock, 1000); + if (data.is_attendance === true) { - var check = checkout_title + " " + data.time; - var image = ''; - $('#check_button').html(checkout_button).removeClass('checkin-btn').addClass('checkout-btn'); - $('.last-checkin-section').html(check); - $('.attendance-img-section').html(image); + // User is checked in - show logout icon (arrow out) with warning color + var image = ''; + $('.attendance-img-section').html(image).removeClass('state-checkin').addClass('state-checkout'); + + // Show last check-in info below icon (on two lines) + var checkinInfoHtml = '
' + checkout_title + '
' + + '
' + data.time + '
'; + $('.last-checkin-info').html(checkinInfoHtml).show(); } else { - var check = checkin_title + " " + name + ","; - var image = ''; - $('#check_button').html(checkin_button).removeClass('checkout-btn').addClass('checkin-btn'); - $('.last-checkin-section').html(check); - $('.attendance-img-section').html(image); + // User is NOT checked in - show login icon (arrow in) with success color + var image = ''; + $('.attendance-img-section').html(image).removeClass('state-checkout').addClass('state-checkin'); + + // Show last check-out info below icon (on two lines) if time exists + if (data.time) { + var checkoutInfoHtml = '
' + checkin_title + '
' + + '
' + data.time + '
'; + $('.last-checkin-info').html(checkoutInfoHtml).show(); + } else { + $('.last-checkin-info').html('').hide(); + } } } + +/* + ============================================================ + APPROVAL CARD BUILDER FUNCTIONS (Merged from system_dashboard.js) + These functions build the approval/follow cards for managers + ============================================================ +*/ + +/* + NAME: buildCardApprove + DESC: Using to create (APPROVE/FOLLOW) card base on sent data and it return DOM OBJECT + RETURN: DOM OBJECT +*/ +function buildCardApprove(data, index, card_type) { + var card = document.createElement('div'); + var card_container = document.createElement('div'); + var card_header = document.createElement('div'); + var card_body = document.createElement('div'); + + $(card).addClass('col-md-3 col-sm-6 col-xs-12 card2'); + // CRITICAL: Set data-model attribute for drag-drop order tracking + $(card).attr('data-model', data.model); + $(card_container).addClass('col-md-12 col-sm-12 col-xs-12 card-container'); + + //card header + //card header + var h4 = document.createElement('h4'); + // Changed header background color logic implies CSS update likely needed, + // but here we ensure the icon inside matches size. + // NOTE: The user requested Primary Color for header. + // Usually this is set in CSS, but we can enforce it here if needed or check CSS. + // Assuming .card-header has background color, we might need to override it. + $(card_header).addClass('col-md-12 col-sm-12 col-xs-12 card-header'); + // .css('background-color', 'var(--dash-primary)') REMOVED - Moved to SCSS + + var iconOrImgHtml = ''; + // Unify size for Approval cards too (typically smaller than self service) + // Let's use 30px for header icons to be clean. + var headerIconSize = '30px'; + + if (data.icon_type === 'icon' && data.icon_name) { + // Dynamic Icon - using SCSS class .approval-header-icon + iconOrImgHtml = ''; + } else { + // Default Image - using SCSS class .approval-header-icon + iconOrImgHtml = ''; + } + + $(h4).html(iconOrImgHtml + ' ' + data.name + '').attr('title', data.name); + $(card_header).append(h4); + $(card_container).append(card_header); + + // card body + $(card_body).addClass('col-md-12 col-sm-12 col-xs-12 card-body'); + if (card_type == "table1") { + var table = buildTableApprove(data); + } else if (card_type == "table2") { + var table = buildTableFollow(data); + } + $(card_body).append(table); + $(card_container).append(card_body); + + // card + $(card).append(card_container); + return card; +} + +/* + NAME: buildTableApprove + DESC: Using to create (APPROVE) rows/buttons base on sent data and it return DOM OBJECT + RETURN: DOM OBJECT +*/ +function buildTableApprove(data) { + var table = document.createElement('table'); + var tbody = document.createElement('tbody'); + $(table).addClass('table'); + if (data.lines.length !== undefined && data.lines.length > 0) { + $.each(data.lines, function(index) { + var tr = document.createElement('tr'); + var td1 = document.createElement('td'); + var td2 = document.createElement('td'); + $(td1).html(data.lines[index].state_approval); + $(td2).html('
' + data.lines[index].count_state_click + '
'); + $(tr).attr('data-target', 'record-button').attr('data-model', data.model).attr('data-name', data.name). + attr('data-domain', JSON.stringify(data.lines[index].domain_click)).attr('form-view', data.lines[index].form_view). + attr('list-view', data.lines[index].list_view).attr('data-action', JSON.stringify(data.lines[index].action_domain)); + $(tr).append(td1).append(td2); + $(tbody).append(tr); + }); + } + $(table).append(tbody); + return table; +} + +/* + NAME: buildTableFollow + DESC: Using to create (FOLLOW) rows/buttons base on sent data and it return DOM OBJECT + RETURN: DOM OBJECT +*/ +function buildTableFollow(data) { + var table = document.createElement('table'); + var tbody = document.createElement('tbody'); + $(table).addClass('table'); + if (data.lines.length !== undefined && data.lines.length > 0) { + $.each(data.lines, function(index) { + var tr = document.createElement('tr'); + var td1 = document.createElement('td'); + var td2 = document.createElement('td'); + $(td1).html(data.lines[index].state_folow); + $(td2).html('
' + data.lines[index].count_state_follow + '
'); + $(tr).attr('data-target', 'record-button').attr('data-type', 'follow').attr('data-model', data.model).attr('data-name', data.name). + attr('data-domain', JSON.stringify(data.lines[index].domain_follow)).attr('form-view', data.lines[index].form_view). + attr('list-view', data.lines[index].list_view) + $(tr).append(td1).append(td2); + // to append + //$(tbody).append(tr); + // to override + $(tbody).html(tr) + + }); + } + // to append + $(table).append(tbody); + return table; +} diff --git a/odex25_base/system_dashboard_classic/static/src/js/utils.js b/odex25_base/system_dashboard_classic/static/src/js/utils.js deleted file mode 100644 index 6500b4445..000000000 --- a/odex25_base/system_dashboard_classic/static/src/js/utils.js +++ /dev/null @@ -1,151 +0,0 @@ -'use strict'; - -(function(global) { - var Samples = global.Samples || (global.Samples = {}); - var Color = global.Color; - - function fallback(/* values ... */) { - var ilen = arguments.length; - var i = 0; - var v; - - for (; i < ilen; ++i) { - v = arguments[i]; - if (v !== undefined) { - return v; - } - } - } - - Samples.COLORS = [ - '#FF3784', - '#36A2EB', - '#4BC0C0', - '#F77825', - '#9966FF', - '#00A8C6', - '#379F7A', - '#CC2738', - '#8B628A', - '#8FBE00', - '#606060' - ]; - - // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ - Samples.srand = function(seed) { - this._seed = seed; - }; - - Samples.rand = function(min, max) { - var seed = this._seed; - min = min === undefined ? 0 : min; - max = max === undefined ? 1 : max; - this._seed = (seed * 9301 + 49297) % 233280; - return min + (this._seed / 233280) * (max - min); - }; - - Samples.numbers = function(config) { - var cfg = config || {}; - var min = fallback(cfg.min, 0); - var max = fallback(cfg.max, 1); - var from = fallback(cfg.from, []); - var count = fallback(cfg.count, 8); - var decimals = fallback(cfg.decimals, 8); - var continuity = fallback(cfg.continuity, 1); - var dfactor = Math.pow(10, decimals) || 0; - var data = []; - var i, value; - - for (i = 0; i < count; ++i) { - value = (from[i] || 0) + this.rand(min, max); - if (this.rand() <= continuity) { - data.push(Math.round(dfactor * value) / dfactor); - } else { - data.push(null); - } - } - - return data; - }; - - Samples.color = function(offset) { - var count = Samples.COLORS.length; - var index = offset === undefined ? ~~Samples.rand(0, count) : offset; - return Samples.COLORS[index % count]; - }; - - Samples.colors = function(config) { - var cfg = config || {}; - var color = cfg.color || Samples.color(0); - var count = cfg.count !== undefined ? cfg.count : 8; - var method = cfg.mode ? Color.prototype[cfg.mode] : null; - var values = []; - var i, f, v; - - for (i = 0; i < count; ++i) { - f = i / count; - - if (method) { - v = method.call(Color(color), f).rgbString(); - } else { - v = Samples.color(i); - } - - values.push(v); - } - - return values; - }; - - Samples.transparentize = function(color, opacity) { - var alpha = opacity === undefined ? 0.5 : 1 - opacity; - return Color(color).alpha(alpha).rgbString(); - }; - - // INITIALIZATION - - Samples.srand(Date.now()); - - var root = (function() { - var scripts = document.getElementsByTagName('script'); - var script = scripts[scripts.length - 1]; - var path = script.src; - return path.substr(0, path.lastIndexOf('/') + 1); - }()); - - window.addEventListener('DOMContentLoaded', function load() { - window.removeEventListener('DOMContentLoaded', load, true); - var header = document.getElementById('header'); - var info = global.SAMPLE_INFO; - - if (header && info) { - var group = info.group; - var name = info.name; - var desc = info.desc; - - document.title = name + (group ? ' / ' + group : '') + ' / chartjs-plugin-datalabels'; - header.innerHTML = - '
' + - '
' + - '' + group + ' / ' + - '' + name + '' + - (desc ? '
' + desc + '
' : '') + - '
'; - } - }, true); - - // Google Analytics - /* eslint-disable */ - if (['localhost', '127.0.0.1', ''].indexOf(document.location.hostname) === -1) { - (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); - ga('create', 'UA-99068522-2', 'auto'); - ga('send', 'pageview'); - } - /* eslint-enable */ - -}(this)); \ No newline at end of file diff --git a/odex25_base/system_dashboard_classic/static/src/lib/confetti.min.js b/odex25_base/system_dashboard_classic/static/src/lib/confetti.min.js new file mode 100644 index 000000000..ca0ef9652 --- /dev/null +++ b/odex25_base/system_dashboard_classic/static/src/lib/confetti.min.js @@ -0,0 +1,8 @@ +/** + * Minified by jsDelivr using Terser v5.37.0. + * Original file: /npm/canvas-confetti@1.9.3/dist/confetti.browser.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(t,e){!function t(e,a,n,r){var o=!!(e.Worker&&e.Blob&&e.Promise&&e.OffscreenCanvas&&e.OffscreenCanvasRenderingContext2D&&e.HTMLCanvasElement&&e.HTMLCanvasElement.prototype.transferControlToOffscreen&&e.URL&&e.URL.createObjectURL),i="function"==typeof Path2D&&"function"==typeof DOMMatrix,l=function(){if(!e.OffscreenCanvas)return!1;var t=new OffscreenCanvas(1,1),a=t.getContext("2d");a.fillRect(0,0,1,1);var n=t.transferToImageBitmap();try{a.createPattern(n,"no-repeat")}catch(t){return!1}return!0}();function s(){}function c(t){var n=a.exports.Promise,r=void 0!==n?n:e.Promise;return"function"==typeof r?new r(t):(t(s,s),null)}var h,f,u,d,m,g,p,b,M,v,y,w=(h=l,f=new Map,{transform:function(t){if(h)return t;if(f.has(t))return f.get(t);var e=new OffscreenCanvas(t.width,t.height);return e.getContext("2d").drawImage(t,0,0),f.set(t,e),e},clear:function(){f.clear()}}),x=(m=Math.floor(1e3/60),g={},p=0,"function"==typeof requestAnimationFrame&&"function"==typeof cancelAnimationFrame?(u=function(t){var e=Math.random();return g[e]=requestAnimationFrame((function a(n){p===n||p+m-1a { color: $color_nav; - border: 1px solid $color_5 !important; margin: 0; } } diff --git a/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss b/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss new file mode 100644 index 000000000..f50374a1d --- /dev/null +++ b/odex25_base/system_dashboard_classic/static/src/scss/genius-enhancements.scss @@ -0,0 +1,3466 @@ +/* ============================================================ + System Dashboard Classic - GENIUS PREMIUM REDESIGN + ============================================================ + + A complete professional redesign with: + - Premium stat cards instead of plain circles + - Enhanced employee profile with status badge + - Real-time search bar for services + - Modern check-in button + - Configurable 2-3 color palette + + ============================================================ */ + +/* === THEME COLORS (Configurable via CSS Variables) === */ +/* Default colors match Odoo settings - will be overridden by JS */ +:root { + /* Primary Brand Colors - DEFAULT CYAN (Odoo typical, will be overridden by JS) */ + --dash-primary: #0891b2; /* Cyan-600 - Main accent */ + --dash-primary-light: #22d3ee; /* Cyan-400 light */ + --dash-primary-dark: #0e7490; /* Cyan-700 dark */ + + /* Secondary Colors */ + --dash-secondary: #1e293b; /* Slate-800 - Headers/Dark */ + --dash-secondary-light: #334155; + + /* Status Colors */ + --dash-success: #10b981; /* Emerald-500 */ + --dash-warning: #f59e0b; /* Amber-500 */ + --dash-danger: #ef4444; /* Red-500 */ + + /* Neutral Colors */ + --dash-bg: #f1f5f9; /* Slate-100 */ + --dash-white: #ffffff; + --dash-border: #e2e8f0; /* Slate-200 */ + --dash-text: #475569; /* Slate-600 */ + --dash-text-light: #94a3b8; /* Slate-400 */ + + /* Shadows */ + --dash-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --dash-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --dash-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + --dash-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); + + /* Transitions */ + --dash-transition: 0.2s ease-in-out; +} + +/* ============================================================ + HEADER SECTION - Premium Dark Gradient + ============================================================ */ + +.dashboard-container .dashboard-header { + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-secondary-light) 100%) !important; + border-radius: 0 0 28px 28px !important; + box-shadow: var(--dash-shadow-xl) !important; + padding: 30px 20px !important; + min-height: auto !important; + position: relative !important; + overflow: hidden !important; + border-bottom: none !important; +} + +/* Subtle decorative gradient overlay - uses primary color */ +.dashboard-container .dashboard-header::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + right: 0 !important; + width: 50% !important; + height: 100% !important; + background: linear-gradient(135deg, transparent 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) 100%) !important; + pointer-events: none !important; +} + +.dashboard-container .dashboard-header::after { + display: none !important; +} + +/* ============================================================ + EMPLOYEE PROFILE - Premium Card Design + ============================================================ */ + +.dashboard-user-data-section { + display: flex !important; + align-items: center !important; + padding: 0 !important; + background: transparent !important; + /* Remove the dark box - the header gradient behind will show through */ +} + +/* Profile container - VISIBLE Glassmorphism Card */ +/* Enhanced for better visibility on dark header */ +.profile-container { + background: rgba(255, 255, 255, 0.15) !important; + -webkit-backdrop-filter: blur(20px) saturate(180%) !important; + border-radius: 20px 0px 0px 20px !important; + padding: 10px 5px !important; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + margin: 10px !important; + text-align: center !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; + position: relative !important; +} + +/* Subtle shine effect on top */ +.profile-container::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 10% !important; + right: 10% !important; + height: 1px !important; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent) !important; + pointer-events: none !important; +} + +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; + margin-bottom: 0px !important; +} + +/* Online status indicator removed by user request */ +/* .profile-container .pp-image-section::after { ... } */ + +.profile-container .pp-image-section::before { + display: none !important; +} + +.profile-container .pp-image-section .img-box { + border: 4px solid rgba(255, 255, 255, 0.2) !important; + height: 130px !important; + width: 130px !important; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3) !important; + transition: all var(--dash-transition) !important; +} + +.profile-container .pp-image-section .img-box:hover { + transform: scale(1.05) !important; + border-color: var(--dash-primary) !important; +} + +.profile-container .info-section { + padding: 0 !important; + margin-top: 0 !important; +} + +.profile-container .info-section::before { + display: none !important; +} + +.profile-container .info-section p { + margin: 0 !important; + color: rgba(255, 255, 255, 0.9) !important; +} + +.profile-container .info-section p.fn-section { + font-size: 18px !important; + font-weight: 700 !important; + color: #fff !important; + margin-bottom: 4px !important; + letter-spacing: 0.3px !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; + background: none !important; + animation: none !important; +} + +.profile-container .info-section p.fn-job { + font-size: 12px !important; + font-weight: 500 !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; + margin-bottom: 2px !important; + -webkit-text-fill-color: unset !important; + text-shadow: none !important; +} + +/* Employee ID - Styled Badge */ +.profile-container .info-section p.fn-id { + font-size: 14px !important; + color: rgba(255, 255, 255, 0.9) !important; + background: rgba(255, 255, 255, 0.15) !important; + padding: 0px 12px !important; + border-radius: 20px !important; + margin-top: 0px !important; + display: inline-block !important; + visibility: visible !important; + opacity: 1 !important; +} + +/* Hide fn-id only when it contains "/" or is empty - via class added by JS */ +p.fn-id.genius-hidden, +.fn-id.genius-hidden, +.profile-container .info-section p.fn-id.genius-hidden { + display: none !important; +} + +.emp-code-badge { + color: #fff !important; + font-weight: 600 !important; + letter-spacing: 0.5px !important; +} + +/* Dynamic Greeting Text */ +.greeting-text { + font-weight: 400 !important; + font-size: 14px !important; + opacity: 0.9; +} + +/* Pending Requests Counter Badge - Uses Warning Color from Settings */ +.pending-count-badge { + background: var(--dash-warning, #f59e0b) !important; + color: white !important; + border-radius: 50% !important; + padding: 2px 8px !important; + font-size: 11px !important; + margin-inline-start: 8px !important; + font-weight: bold !important; + min-width: 20px !important; + text-align: center !important; + display: inline-block !important; + animation: pulse-badge 2s ease-in-out infinite !important; +} + +@keyframes pulse-badge { + 0%, 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); + } + 50% { + transform: scale(1.1); + box-shadow: 0 0 0 8px rgba(245, 158, 11, 0); + } +} + +/* Attendance Title - Uses Secondary Color from Settings */ +.attendance-title { + color: var(--dash-secondary, #1e293b) !important; + font-weight: 600 !important; + font-size: 16px !important; + margin-bottom: 8px !important; +} + +/* Attendance Date & Time - Uses Primary Color from Settings */ +/* Date and time displayed on single line */ +.last-checkin-section { + text-align: center !important; + margin-bottom: 5px !important; +} + +.attendance-date { + color: var(--dash-primary, #0891b2) !important; + font-size: 12px !important; + font-weight: 500 !important; +} + +.attendance-time { + color: var(--dash-primary, #0891b2) !important; + font-size: 18px !important; + font-weight: 600 !important; + font-family: 'SF Mono', 'Roboto Mono', 'Consolas', monospace !important; + letter-spacing: 1px !important; + display: inline !important; +} + +/* Last Check-in/out Info - Displayed below icon on single line */ +.last-checkin-info { + text-align: center !important; + margin-top: 4px !important; +} + +.last-checkin-info .checkin-label { + color: var(--dash-secondary, #64748b) !important; + font-size: 10px !important; + font-weight: 500 !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + +.last-checkin-info .checkin-time { + color: var(--dash-warning, #f59e0b) !important; + font-size: 11px !important; + font-weight: 600 !important; + font-family: 'SF Mono', 'Roboto Mono', monospace !important; + display: inline !important; + margin-left: 5px !important; +} + +/* Clickable Profile Elements */ +.clickable-profile { + cursor: pointer !important; + transition: all 0.3s ease !important; +} + +.clickable-profile:hover { + opacity: 0.85 !important; +} + +.pp-image-section .img-box.clickable-profile:hover { + transform: scale(1.08) !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4) !important; +} + +p.fn-section.clickable-profile:hover { + text-decoration: underline !important; + text-underline-offset: 3px !important; +} + +/* ============================================================ + STATISTICS CARDS - Modern Premium Design + ============================================================ */ + +.dashboard-user-statistics-section { + padding: 0 15px !important; + border-radius: 5px !important; + /* No overflow-x here - only in mobile media query */ +} + +.dashboard-charts-section { + height: auto !important; + padding: 0 !important; +} + +.dashboard-module-charts { + display: flex !important; + flex-wrap: nowrap !important; + gap: 12px !important; + padding: 10px 0 !important; + justify-content: stretch !important; + align-items: stretch !important; + width: 100% !important; +} + +/* Statistics cards - flex grow to fill space equally */ +.module-box { + flex: 1 1 0 !important; /* grow, shrink, basis=0 for equal distribution */ + min-width: 0 !important; /* allow shrinking below content size */ + max-width: none !important; + padding: 0 !important; +} + +/* Hidden cards should not take space */ +.module-box.genius-hidden { + display: none !important; + flex: 0 0 0 !important; +} + +/* Stat Card Container */ +.module-box .module-box-container { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + border-radius: 16px !important; + padding: 16px !important; + border: 1px solid rgba(255, 255, 255, 0.3) !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1) !important; + transition: all var(--dash-transition) !important; + position: relative !important; + overflow: hidden !important; + height: 100% !important; +} + +.module-box .module-box-container::before { + display: none !important; +} + +.module-box .module-box-container:hover { + transform: translateY(-4px) !important; + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important; +} + +/* Stat Card Title - ANNUAL LEAVE, SALARY SLIPS, etc */ +.module-box .module-box-container h3 { + font-size: 13px !important; + font-weight: 700 !important; + color: var(--dash-secondary) !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; + margin: 12px 0 0 0 !important; + text-align: center !important; + text-shadow: none !important; + opacity: 1 !important; +} + +/* Stat Values - Big Numbers */ +.module-box .module-box-container h4.leave-data-percent, +.module-box .module-box-container h4.payroll-data-percent, +.module-box .module-box-container h4.timesheet-data-percent, +.module-box .module-box-container h4.attendance-hours-percent { + font-size: 28px !important; + font-weight: 800 !important; + color: var(--dash-primary) !important; + text-align: center !important; + margin: 8px 0 !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; + animation: none !important; + transform: none !important; +} + +/* Stat Labels - GENIUS MINIMAL DESIGN */ +/* Clean inline labels with subtle number highlighting */ +.module-box .module-box-container p { + margin: 3px 0 !important; + text-align: center !important; + line-height: 1.4 !important; +} + +.module-box .module-box-container p span { + font-size: 11px !important; + font-weight: 500 !important; + color: var(--dash-text-light) !important; + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; + background: none !important; + padding: 0 !important; + border-radius: 0 !important; +} + +/* Colored dots - smaller and subtle */ +.module-box .module-box-container p span i.fa-circle { + font-size: 5px !important; + opacity: 0.9 !important; +} + +/* First row dot (Total) - Primary teal */ +.module-box .module-box-container p:first-of-type span i { + color: var(--dash-primary) !important; +} + +/* Second row dot (Remaining) - Warm amber */ +.module-box .module-box-container p:nth-of-type(2) span i { + color: var(--dash-warning) !important; +} + +/* GENIUS NUMBER HIGHLIGHTING */ +/* The actual values get bold treatment with primary color */ +.module-box .module-box-container .leave-total-amount, +.module-box .module-box-container .payroll-total-amount, +.module-box .module-box-container .timesheet-total-amount, +.module-box .module-box-container .attendance-plan-hours { + font-weight: 800 !important; + font-size: 14px !important; + color: var(--dash-primary) !important; + padding: 1px 6px !important; + background: rgba(8, 145, 178, 0.08) !important; + border-radius: 4px !important; + margin-left: 2px !important; +} + +/* Remaining amounts - Amber highlight */ +.module-box .module-box-container .leave-left-amount, +.module-box .module-box-container .payroll-left-amount, +.module-box .module-box-container .timesheet-left-amount, +.module-box .module-box-container .attendance-official-hours { + font-weight: 800 !important; + font-size: 14px !important; + color: var(--dash-warning) !important; + padding: 1px 6px !important; + background: rgba(245, 158, 11, 0.08) !important; + border-radius: 4px !important; + margin-left: 2px !important; +} + +/* Hide the D3 chart container - CSS only approach */ +.module-box #chartContainer, +.module-box #chartPaylips, +.module-box #chartTimesheet, +.module-box #chartAttendanceHours { + height: 140px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; +} + +/* Style the SVG charts */ +.module-box svg { + max-height: 140px !important; + margin: 0 auto !important; +} + +/* Chart wrapper for center text positioning */ +.chart-wrapper { + position: relative !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + height: 140px !important; +} + +/* Center text inside donut chart */ +.chart-center-text { + position: absolute !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + text-align: center !important; + pointer-events: none !important; + z-index: 10 !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; +} + +.chart-center-value { + font-size: 2rem !important; + font-weight: 800 !important; + color: var(--dash-secondary, #1e293b); /* No !important - JS sets dynamically */ + line-height: 1 !important; + transition: all 0.3s ease !important; +} + +/* Hover effect on center text */ +.chart-wrapper:hover .chart-center-value { + transform: scale(1.1) !important; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important; +} + +.chart-center-unit { + font-size: 1.3rem !important; + font-weight: 600 !important; + color: var(--dash-secondary) !important; + text-transform: lowercase !important; + margin-top: 4px !important; + letter-spacing: 0.5px !important; + transition: all 0.3s ease !important; +} + +.chart-wrapper:hover .chart-center-unit { + color: var(--dash-secondary) !important; +} + +/* ============================================================ + ATTENDANCE SECTION - Modern Button Design + ============================================================ */ + +.dashboard-attendance-section { + border-left: 1px solid rgba(255, 255, 255, 0.1) !important; + display: flex !important; + align-items: center !important; +} + +.attendance-section-body { + background: rgba(255, 255, 255, 0.98) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + border-radius: 20px !important; + padding: 10px 4px 4px 4px !important; + margin: 4px !important; + text-align: center !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08) !important; + width: 100% !important; + border: 1px solid rgba(0, 0, 0, 0.05) !important; +} + +.attendance-section-body h3 { + font-size: 13px !important; + font-weight: 600 !important; + color: var(--dash-secondary) !important; + text-transform: uppercase !important; + letter-spacing: 1px !important; + margin-bottom: 4px !important; + text-shadow: none !important; + -webkit-text-fill-color: unset !important; +} + +/* Welcome text - Make sure no extra background */ +.attendance-section-body p.last-checkin-section { + font-size: 12px !important; + color: var(--dash-text) !important; + margin-bottom: 8px !important; + font-weight: 500 !important; + background: none !important; + padding: 0 !important; +} + +/* Check-in/out Arrow Button Container - FORCED VISIBILITY */ +.attendance-section-body .attendance-img-section { + display: flex !important; + justify-content: center !important; + align-items: center !important; + margin-top: 10px !important; + background: transparent !important; + min-height: 70px !important; +} + +/* Check-in/out Arrow Button - DYNAMIC COLOR using CSS Mask */ +/* The container uses mask-image technique for reliable color matching */ +.attendance-section-body .attendance-img-section { + position: relative !important; + display: flex !important; + justify-content: center !important; + align-items: center !important; + cursor: pointer !important; +} + +/* Hide the original img completely - pseudo-element handles display */ +.attendance-section-body .attendance-img-section img, +.attendance-section-body .attendance-img-section img.img-logout, +.attendance-section-body .attendance-img-section img.img-login, +.attendance-section-body .attendance-img-section img.attendance-icon, +.attendance-section-body p.attendance-img-section img { + /* Hide img completely - CSS mask pseudo-element handles the visual */ + display: none !important; +} + +/* Create the visible icon using mask-image on a pseudo-element */ +/* Default state - Check-in (login icon with success color) */ +.attendance-section-body .attendance-img-section::before { + content: '' !important; + display: block !important; + width: 70px !important; + height: 70px !important; + /* Default: Success color for check-in */ + background-color: var(--dash-success, #10b981) !important; + /* Default: login icon */ + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/login.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/login.svg') !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; + -webkit-mask-position: center !important; + mask-position: center !important; + transition: all 0.3s ease !important; + pointer-events: none !important; +} + +/* Check-in state - Green success color with login icon */ +.attendance-section-body .attendance-img-section.state-checkin::before { + background-color: var(--dash-success, #10b981) !important; + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/login.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/login.svg') !important; +} + +/* Check-out state - Warning color with logout icon */ +.attendance-section-body .attendance-img-section.state-checkout::before { + background-color: var(--dash-warning, #f59e0b) !important; + -webkit-mask-image: url('/system_dashboard_classic/static/src/icons/logout.svg') !important; + mask-image: url('/system_dashboard_classic/static/src/icons/logout.svg') !important; +} + +/* Disabled state - only change icon color, no opacity */ +.attendance-section-body .attendance-img-section.disabled::before { + background-color: #9ca3af !important; + /* Remove opacity to prevent gray rectangle background */ +} + +.attendance-section-body .attendance-img-section.disabled { + cursor: not-allowed !important; + opacity: 0.6 !important; +} + +/* Hover effects */ +.attendance-section-body .attendance-img-section:not(.disabled):hover::before { + transform: scale(1.1) !important; +} + +.attendance-section-body .attendance-img-section.state-checkin:not(.disabled):hover::before { + background-color: var(--dash-success-dark, #059669) !important; +} + +.attendance-section-body .attendance-img-section.state-checkout:not(.disabled):hover::before { + background-color: var(--dash-warning-dark, #d97706) !important; +} + +/* Check-in/out Button - Modern Style */ + + +/* ============================================================ + DASHBOARD BODY - Clean Background + ============================================================ */ + +.dashboard-container .dashboard-body { + background: var(--dash-bg) !important; + padding: 30px !important; +} + +/* ============================================================ + SELF SERVICES HEADER - With Search Bar + ============================================================ */ + +.dashboard-nav-buttons { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + flex-wrap: wrap !important; + gap: 15px !important; + margin-bottom: 25px !important; + padding: 0 0 20px 0 !important; + position: relative !important; +} + +/* Full-width separator line under Self Services & Search */ +.dashboard-nav-buttons::after { + content: '' !important; + position: absolute !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + height: 2px !important; + background: linear-gradient(90deg, + var(--dash-primary-light) 0%, + var(--dash-primary) 50%, + var(--dash-primary-light) 100%) !important; + opacity: 0.6 !important; + border-radius: 2px !important; +} + +.dashboard-nav-buttons hr { + display: none !important; +} + +/* ============================================================ + TAB NAVIGATION - ELEGANT UNDERLINE DESIGN + Modern, minimal, professional - NOT button-like + ============================================================ */ + +.dashboard-nav-buttons .nav-tabs { + background: transparent !important; + border: none !important; + border-radius: 0 !important; + padding: 0 !important; + box-shadow: none !important; + display: inline-flex !important; + gap: 0 !important; + margin: 0 !important; + position: relative !important; +} + +/* Remove all ::before pseudo elements */ +.dashboard-nav-buttons .nav-tabs::before { + display: none !important; +} + +.dashboard-nav-buttons .nav-tabs li { + margin: 0 !important; + position: relative !important; +} + +/* Tab Links - Clean Text Style */ +.dashboard-nav-buttons .nav-tabs li a { + border-radius: 0 !important; + padding: 16px 32px !important; + font-weight: 600 !important; + font-size: 15px !important; + color: var(--dash-text-light, #94a3b8) !important; + background: transparent !important; + border: none !important; + transition: all 0.35s ease !important; + white-space: nowrap !important; + position: relative !important; + letter-spacing: 0.3px !important; + text-decoration: none !important; +} + +/* Animated underline indicator - subtle line always visible */ +.dashboard-nav-buttons .nav-tabs li a::after { + content: '' !important; + position: absolute !important; + bottom: 0 !important; + left: 10% !important; + width: 80% !important; + height: 2px !important; + background: rgba(0, 0, 0, 0.06) !important; + border-radius: 2px 2px 0 0 !important; + transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +/* Hover - Text color change + underline becomes primary */ +.dashboard-nav-buttons .nav-tabs li a:hover:not(.active) { + color: var(--dash-primary) !important; + background: transparent !important; + transform: none !important; +} + +.dashboard-nav-buttons .nav-tabs li a:hover:not(.active)::after { + left: 5% !important; + width: 90% !important; + background: linear-gradient(90deg, var(--dash-primary-light) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.3) 100%) !important; +} + +/* Active Tab - Full underline with gradient */ +.dashboard-nav-buttons .nav-tabs li.active a, +.dashboard-nav-buttons .nav-tabs li a.active { + color: var(--dash-primary) !important; + font-weight: 700 !important; + background: transparent !important; + box-shadow: none !important; + transform: none !important; +} + +.dashboard-nav-buttons .nav-tabs li.active a::after, +.dashboard-nav-buttons .nav-tabs li a.active::after { + left: 0 !important; + width: 100% !important; + height: 3px !important; + background: linear-gradient(90deg, + var(--dash-primary) 0%, + var(--dash-primary-light) 50%, + var(--dash-primary) 100%) !important; + box-shadow: 0 2px 8px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35) !important; +} + +/* Remove shine animation from this design */ +.dashboard-nav-buttons .nav-tabs li.active a::before, +.dashboard-nav-buttons .nav-tabs li a.active::before { + display: none !important; +} + +/* Icon inside tab (if any) */ +.dashboard-nav-buttons .nav-tabs li a i { + margin-right: 10px !important; + font-size: 16px !important; + opacity: 0.7 !important; + transition: all 0.3s ease !important; +} + +.dashboard-nav-buttons .nav-tabs li a:hover i, +.dashboard-nav-buttons .nav-tabs li a.active i { + opacity: 1 !important; + color: var(--dash-primary) !important; +} + +/* Loading/Refreshing state for cards */ +.card-section-approve.refreshing, +.card-section-track.refreshing { + opacity: 0.5 !important; + pointer-events: none !important; + position: relative !important; +} + +.card-section-approve.refreshing::after, +.card-section-track.refreshing::after { + content: '' !important; + position: absolute !important; + top: 50% !important; + left: 50% !important; + width: 40px !important; + height: 40px !important; + margin: -20px 0 0 -20px !important; + border: 3px solid var(--dash-primary-light) !important; + border-top-color: var(--dash-primary) !important; + border-radius: 50% !important; + animation: tabRefreshSpin 0.8s linear infinite !important; +} + +@keyframes tabRefreshSpin { + to { transform: rotate(360deg); } +} + +/* Search Container (will be added via JS) */ +.dashboard-search-container { + display: flex !important; + align-items: center !important; + gap: 10px !important; +} + +.dashboard-search-input { + background: var(--dash-white) !important; + border: 1px solid var(--dash-border) !important; + border-radius: 10px !important; + padding: 10px 16px !important; + font-size: 14px !important; + width: 220px !important; + color: var(--dash-text) !important; + transition: all var(--dash-transition) !important; +} + +.dashboard-search-input::placeholder { + color: var(--dash-text-light) !important; +} + +.dashboard-search-input:focus { + outline: none !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.1) !important; +} + +/* ============================================================ + APPROVAL CARDS (Card2) - GENIUS PREMIUM DESIGN + Uses dynamic colors from settings via CSS variables + ============================================================ */ + +.card2 { + margin-bottom: 24px !important; + padding: 0 10px !important; +} + +.card2 .card-container { + background: var(--dash-white) !important; + border: none !important; + border-radius: 20px !important; + overflow: hidden !important; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06), + 0 1px 3px rgba(0, 0, 0, 0.04) !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + position: relative !important; +} + +/* Top accent line on hover */ +.card2 .card-container::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + height: 3px !important; + background: linear-gradient(90deg, var(--dash-primary) 0%, var(--dash-primary-light) 100%) !important; + opacity: 0 !important; + transition: opacity 0.3s ease !important; + z-index: 10 !important; +} + +.card2 .card-container:hover::before { + opacity: 1 !important; +} + +.card2 .card-container:hover { + transform: translateY(-6px) !important; + box-shadow: 0 16px 40px rgba(0, 0, 0, 0.1), + 0 4px 12px rgba(0, 0, 0, 0.06) !important; +} + + +/* Decorative glow in header */ +.card2 .card-container .card-header::before { + content: '' !important; + position: absolute !important; + top: -50% !important; + right: -20% !important; + width: 120px !important; + height: 120px !important; + background: radial-gradient(circle, rgba(255, 255, 255, 0.15) 0%, transparent 70%) !important; + pointer-events: none !important; +} + +.card2 .card-container .card-header::after { + display: none !important; +} + +.card2 .card-container .card-header img { + height: 36px !important; + width: 36px !important; + padding: 6px !important; + // background: rgba(255, 255, 255, 0.18) !important; + border-radius: 10px !important; + margin-right: 14px !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + filter: brightness(1.1) !important; +} + +.card2 .card-container:hover .card-header img { + transform: scale(1.12) rotate(-5deg) !important; + background: rgba(255, 255, 255, 0.25) !important; +} + +.card2 .card-container .card-header h4 { + color: white !important; + font-size: 15px !important; + font-weight: 700 !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important; + margin: 0 !important; + flex: 1 !important; + letter-spacing: 0.3px !important; +} + +.card2 .card-container .card-body { + height: 180px !important; + padding: 0 !important; + overflow-y: auto !important; + background: linear-gradient(180deg, #ffffff 0%, #fafafa 100%) !important; +} + +/* Custom scrollbar for card body */ +.card2 .card-container .card-body::-webkit-scrollbar { + width: 5px !important; +} + +.card2 .card-container .card-body::-webkit-scrollbar-track { + background: #f1f1f1 !important; + border-radius: 3px !important; +} + +.card2 .card-container .card-body::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; + border-radius: 3px !important; +} + +.card2 .card-container .card-body table { + margin: 0 !important; + width: 100% !important; +} + +.card2 .card-container .card-body table tr { + border-bottom: 1px solid rgba(0, 0, 0, 0.04) !important; + transition: all 0.25s ease !important; + cursor: pointer !important; +} + +.card2 .card-container .card-body table tr:last-child { + border-bottom: none !important; +} + +.card2 .card-container .card-body table tr:hover { + background: linear-gradient(90deg, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.04) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.08) 100%) !important; +} + +.card2 .card-container .card-body table tr td { + padding: 14px 18px !important; + font-weight: 500 !important; + color: var(--dash-secondary) !important; + font-size: 13px !important; + vertical-align: middle !important; + transition: color 0.2s ease !important; +} + +.card2 .card-container .card-body table tr:hover td { + color: var(--dash-primary-dark) !important; +} + +.card2 .card-container .card-body table tr td:last-child { + text-align: right !important; +} + +/* Count Badge - Uses PRIMARY color from settings */ +.card2 .card-container .card-body table tr td:last-child div { + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-secondary-dark) 100%) !important; + height: 30px !important; + width: 30px !important; + line-height: 30px !important; + font-size: 12px !important; + font-weight: 700 !important; + color: white !important; + border-radius: 50% !important; + display: inline-block !important; + text-align: center !important; + box-shadow: 0 3px 10px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.35) !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; +} + +.card2 .card-container .card-body table tr:hover td:last-child div { + transform: scale(1.15) !important; + box-shadow: 0 5px 15px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.45) !important; +} + +/* ============================================================ + SELF-SERVICE CARDS (Card3) - Premium Design + ============================================================ */ + +.card3 { + padding: 0 8px !important; + margin-bottom: 20px !important; +} + +.card3 .card-body { + background: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%) !important; + border: 2px solid transparent !important; + border-radius: 20px !important; + overflow: hidden !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06), + 0 1px 4px rgba(0, 0, 0, 0.04) !important; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; + position: relative !important; +} + +/* Morphing gradient border on hover */ +.card3 .card-body::before { + content: '' !important; + position: absolute !important; + inset: -2px !important; + background: linear-gradient(135deg, + var(--dash-primary), + var(--dash-primary-light), + var(--dash-secondary-light), + var(--dash-primary)) !important; + background-size: 200% 200% !important; + border-radius: 22px !important; + z-index: -1 !important; + opacity: 0 !important; + transition: opacity 0.4s ease !important; +} + +.card3 .card-body:hover::before { + opacity: 1 !important; + animation: cardGradientBorder 3s ease-in-out infinite !important; +} + +@keyframes cardGradientBorder { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +.card3 .card-body:hover { + transform: translateY(-8px) scale(1.02) !important; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.12), + 0 8px 16px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) !important; +} + +/* Genius 3D Tilt Effect on Service Cards */ +.card3 .card-body { + transform-style: preserve-3d !important; + perspective: 1000px !important; +} + +.card3 .card-body:hover .box-1 { + transform: translateZ(20px) !important; +} + +.card3 .card-body:hover .box-1 img, +.card3 .card-body:hover .box-1 .service-icon { + transform: scale(1.15) translateZ(30px) !important; + filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.15)) !important; +} + +.card3 .card-body .box-1 { + height: 220px !important; + background: var(--dash-white) !important; + border: none !important; + padding: 24px 16px !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; +} + +.card3 .card-body .box-1::before { + display: none !important; +} + +/* Icon with glow effect */ +.card3 .card-body .box-1 img, +.card3 .card-body .box-1 .service-icon { + height: 60px !important; + width: 60px !important; /* Ensure width is also consistent */ + display: inline-block !important; /* For icon */ + font-size: 60px !important; /* For icon font size to match height */ + line-height: 60px !important; /* Center vertically */ + margin-bottom: 4px !important; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) !important; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.1)) !important; + animation: none !important; +} + +.card3 .card-body:hover .box-1 img, +.card3 .card-body:hover .box-1 .service-icon { + transform: scale(1.18) rotate(-5deg) !important; + color: var(--dash-secondary) !important; /* Ensure icon keeps color but gets effect */ + filter: drop-shadow(0 8px 20px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.3)) !important; +} + +/* Count Number - Gradient text with SECONDARY/PRIMARY */ +.card3 .card-body .box-1 h3 { + font-size: 48px !important; + font-weight: 900 !important; + background: linear-gradient(135deg, var(--dash-secondary) 0%, var(--dash-primary) 100%) !important; + -webkit-background-clip: text !important; + -webkit-text-fill-color: transparent !important; + background-clip: text !important; + margin: 8px 0 !important; + text-shadow: none !important; + transition: all 0.3s ease !important; + letter-spacing: -1px !important; +} + +.card3 .card-body:hover .box-1 h3 { + transform: scale(1.08) !important; + filter: brightness(1.1) !important; +} + +/* Service Name - Uses SECONDARY color from settings */ +.card3 .card-body .box-1 h4 { + font-size: 15px !important; + font-weight: 700 !important; + color: var(--dash-secondary) !important; + padding: 8px 16px !important; + margin: 8px 0 0 0 !important; + -webkit-text-fill-color: unset !important; + text-align: center !important; + background: linear-gradient(135deg, rgba(var(--dash-secondary-rgb, 30, 41, 59), 0.04) 0%, rgba(var(--dash-secondary-rgb, 30, 41, 59), 0.08) 100%) !important; + border-radius: 8px !important; + letter-spacing: 0.3px !important; + transition: all 0.3s ease !important; + max-width: 100% !important; +} + +.card3 .card-body:hover .box-1 h4 { + background: linear-gradient(135deg, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.08) 0%, rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) 100%) !important; + color: var(--dash-primary-dark) !important; +} + +.card3 .card-body .box-2 { + background: var(--dash-primary) !important; + height: 50px !important; + line-height: 50px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + cursor: pointer !important; +} + +.card3 .card-body .box-2::before { + display: none !important; +} + +.card3 .card-body .box-2 span { + font-size: 14px !important; + font-weight: 700 !important; + color: white !important; + letter-spacing: 0.5px !important; + transition: all 0.4s ease !important; +} + +/* BIGGER + ICON */ +.card3 .card-body .box-2 i { + font-size: 24px !important; + font-weight: 700 !important; + color: white !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important; +} + +/* Genius hover for Add New button */ +.card3 .card-body .box-2:hover { + background: linear-gradient(135deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; + box-shadow: 0 6px 20px rgba(var(--dash-primary-rgb, 75, 155, 75), 0.4), + inset 0 1px 0 rgba(255, 255, 255, 0.2) !important; +} + +.card3 .card-body .box-2:hover span { + letter-spacing: 1.5px !important; + text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2) !important; +} + +.card3 .card-body .box-2:hover i { + transform: rotate(180deg) scale(1.2) !important; + text-shadow: 0 0 15px rgba(255, 255, 255, 0.5) !important; +} + +/* ============================================================ + CARD ENTRY ANIMATIONS + ============================================================ */ + +.card-section1 .card2, +.card-section .card2, +.card-section1 .card3, +.card-section .card3 { + animation: cardFadeIn 0.35s ease-out backwards !important; +} + +@keyframes cardFadeIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.card-section1 .card2:nth-child(1), +.card-section1 .card3:nth-child(1) { animation-delay: 0.04s !important; } +.card-section1 .card2:nth-child(2), +.card-section1 .card3:nth-child(2) { animation-delay: 0.08s !important; } +.card-section1 .card2:nth-child(3), +.card-section1 .card3:nth-child(3) { animation-delay: 0.12s !important; } +.card-section1 .card2:nth-child(4), +.card-section1 .card3:nth-child(4) { animation-delay: 0.16s !important; } +.card-section1 .card2:nth-child(5), +.card-section1 .card3:nth-child(5) { animation-delay: 0.20s !important; } +.card-section1 .card2:nth-child(6), +.card-section1 .card3:nth-child(6) { animation-delay: 0.24s !important; } + +/* ============================================================ + LOADING SPINNER + ============================================================ */ + +.charts-over-layer { + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(5px) !important; + border-radius: 16px !important; +} + +.lds-roller div::after { + background: var(--dash-primary) !important; +} + +/* ============================================================ + RESPONSIVE ADJUSTMENTS - COMPREHENSIVE + ============================================================ */ + +/* Large tablets and small desktops (992px and below) */ +@media (max-width: 992px) { + .module-box { + min-width: 140px !important; + padding: 15px 12px !important; + } + + .dashboard-search-input, + .genius-search-input { + width: 200px !important; + } + + .card3 .card-body .box-1 { + height: 180px !important; + padding: 20px 12px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 32px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 60px !important; + height: 60px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 60px !important; + height: 60px !important; + } +} + +/* Tablets (768px and below) */ +@media (max-width: 768px) { + .dashboard-container .dashboard-header { + padding: 20px 15px !important; + border-radius: 0 0 20px 20px !important; + } + + .dashboard-nav-buttons { + flex-direction: column !important; + align-items: stretch !important; + gap: 12px !important; + } + + .dashboard-nav-buttons::after { + display: none !important; + } + + .dashboard-search-container, + .genius-search-container { + width: 100% !important; + } + + .dashboard-search-input, + .genius-search-input { + width: 100% !important; + } + + .profile-container { + padding: 16px 14px !important; + margin: 8px !important; + border-radius: 16px !important; + } + + .profile-container .pp-image-section img { + height: 70px !important; + width: 70px !important; + } + + .profile-container .info-section h2 { + font-size: 16px !important; + } + + .module-box { + min-width: 120px !important; + padding: 12px 10px !important; + } + + .module-box h3 { + font-size: 11px !important; + } + + .module-box h4 { + font-size: 20px !important; + } + + .card2 .card-container { + min-width: auto !important; + } + + .card3 .card-body .box-1 { + height: 160px !important; + padding: 16px 10px !important; + } + + .card3 .card-body .box-1 img { + height: 45px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 28px !important; + } + + .card3 .card-body .box-1 h4 { + font-size: 12px !important; + } + + .card3 .card-body .box-2 { + height: 45px !important; + line-height: 45px !important; + } + + .card3 .card-body .box-2 span { + font-size: 12px !important; + } + + .card3 .card-body .box-2 i { + font-size: 20px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 55px !important; + height: 55px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 55px !important; + height: 55px !important; + } +} + +/* Mobile devices (576px and below) */ +@media (max-width: 576px) { + /* ============================================ + HEADER LAYOUT - BLOCK DISPLAY FOR STACKING + Key fix: Use display:block to force normal + document flow instead of flex + ============================================ */ + .dashboard-container .dashboard-header { + display: block !important; /* KEY: Block forces stacking */ + padding: 10px !important; + border-radius: 0 0 16px 16px !important; + height: auto !important; + min-height: auto !important; + overflow: visible !important; + padding-bottom: 60px !important; /* MASSIVE padding to fix overflow */ + position: relative !important; + } + + /* Ensure gradient overlay covers extended height */ + .dashboard-container .dashboard-header::before { + height: 100% !important; + min-height: 100% !important; + bottom: 0 !important; + } + + /* ============================================ + PROFILE SECTION - Full width block + ============================================ */ + .dashboard-user-data-section { + display: block !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + padding: 10px !important; + float: none !important; + } + + /* Single Row Tabs for Mobile */ + .dashboard-nav-buttons .nav-tabs { + flex-wrap: nowrap !important; /* Force single row */ + overflow-x: auto !important; /* scroll if needed */ + justify-content: flex-start !important; + width: 100% !important; + padding: 5px 0 !important; + } + + .dashboard-nav-buttons .nav-tabs li .nav-link { + padding: 6px 10px !important; /* Smaller padding */ + font-size: 11px !important; /* Smaller text */ + white-space: nowrap !important; + } + + /* ... rest of profile section styles ... */ + + + .profile-container { + padding: 15px !important; + margin: 0 auto 10px auto !important; + border-radius: 16px !important; + width: 100% !important; + // max-width: 250px !important; + } + + .profile-container .pp-image-section img, + .profile-container .pp-image-section .img-box { + height: 80px !important; + width: 80px !important; + } + + .profile-container .info-section h2 { + font-size: 16px !important; + } + + .profile-container .info-section p { + font-size: 11px !important; + } + + /* ============================================ + STATISTICS SECTION - CSS Grid Layout (2 columns) + Key fix: Replace horizontal scroll with grid + ============================================ */ + .dashboard-user-statistics-section { + display: block !important; + width: 100% !important; + max-width: 100% !important; + height: auto !important; + padding: 28px 10px 40px 10px !important; + overflow: visible !important; /* No more horizontal scroll */ + float: none !important; + } + + .dashboard-charts-section { + display: block !important; + width: 100% !important; + height: auto !important; + padding: 0 !important; + } + + .dashboard-module-charts { + display: grid !important; + grid-template-columns: repeat(2, 1fr) !important; /* 2 columns */ + gap: 10px !important; + width: 100% !important; + padding: 5px !important; + } + + /* Override Bootstrap col classes for grid children */ + .dashboard-module-charts .module-box, + .dashboard-module-charts .module-box.col-12, + .dashboard-module-charts .module-box.col-sm-6 { + width: 100% !important; + max-width: 100% !important; + min-width: unset !important; + flex: unset !important; + padding: 0 !important; + } + + .module-box .module-box-container { + padding: 12px 10px !important; + border-radius: 12px !important; + height: 100% !important; + min-height: 120px !important; + } + + .module-box h3 { + font-size: 9px !important; + letter-spacing: 0.5px !important; + margin: 6px 0 0 0 !important; + } + + .module-box h4 { + font-size: 20px !important; + margin: 6px 0 !important; + } + + .module-box p { + font-size: 9px !important; + margin: 4px 0 !important; + } + + .module-box p span { + font-size: 9px !important; + gap: 4px !important; + } + + .module-box #chartContainer, + .module-box #chartPaylips, + .module-box #chartTimesheet { + height: 55px !important; + } + + .module-box svg { + // max-height: 55px !important; + } + + /* ============================================ + ATTENDANCE SECTION - Full width below cards + ============================================ */ + .dashboard-attendance-section { + width: 100% !important; + max-width: 100% !important; + min-width: unset !important; + flex: unset !important; + padding: 10px 0 !important; + border-left: none !important; + border-top: 1px solid rgba(255,255,255,0.1) !important; + margin-top: 10px !important; + } + + .attendance-section-body { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + padding: 15px !important; + margin: 0 5px !important; + border-radius: 12px !important; + gap: 12px !important; + } + + .attendance-section-body h3 { + font-size: 12px !important; + margin-bottom: 8px !important; + text-align: center !important; + } + + .attendance-section-body p.last-checkin-section { + font-size: 11px !important; + margin-bottom: 8px !important; + text-align: center !important; + } + + .attendance-img-section { + margin-bottom: 8px !important; + } + + .attendance-details-section { + text-align: center !important; + width: 100% !important; + } + + + + /* ============================================ + BODY SECTION + ============================================ */ + .dashboard-container .dashboard-body { + padding: 10px !important; + } + + .card3 { + padding: 0 4px !important; + margin-bottom: 15px !important; + } + + .card3 .card-body { + border-radius: 12px !important; + } + + .card3 .card-body .box-1 { + height: 140px !important; + padding: 12px 8px !important; + } + + .card3 .card-body .box-1 img { + height: 40px !important; + margin-bottom: 8px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 24px !important; + margin: 4px 0 !important; + } + + .card3 .card-body .box-1 h4 { + font-size: 11px !important; + } + + .card3 .card-body .box-2 { + height: 40px !important; + line-height: 40px !important; + gap: 6px !important; + } + + .card3 .card-body .box-2 span { + font-size: 11px !important; + } + + .card3 .card-body .box-2 i { + font-size: 18px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 50px !important; + height: 50px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 50px !important; + height: 50px !important; + } + + .dashboard-nav-buttons { + flex-direction: column !important; + gap: 15px !important; + padding-bottom: 15px !important; + align-items: center !important; + } + + /* Single Row Tabs for Mobile */ + .dashboard-nav-buttons .nav-tabs { + flex-wrap: nowrap !important; /* Force single row */ + overflow-x: auto !important; /* scroll */ + justify-content: flex-start !important; /* Start align for scroll */ + width: 100% !important; + padding: 5px 0 !important; + display: flex !important; + -webkit-overflow-scrolling: touch !important; + } + + .dashboard-nav-buttons .nav-tabs li { + flex: 0 0 auto !important; + display: block !important; + } + + /* CRITICAL: Keep approval tabs hidden until JS shows them */ + .dashboard-nav-buttons .nav-tabs li.approval-tab-item[style*="display:none"], + .dashboard-nav-buttons .nav-tabs li.approval-tab-item[style*="display: none"] { + display: none !important; + } + + .dashboard-nav-buttons .nav-tabs li .nav-link { + padding: 8px 14px !important; + font-size: 12px !important; + } + + /* Fix Search Bar Alignment */ + .genius-search-container { + width: 100% !important; + margin: 0 !important; + justify-content: center !important; + position: relative !important; + } + + .genius-search-wrapper { + // width: 100% !important; + max-width: 100% !important; + position: relative !important; + } + + /* Center search input text */ + .genius-search-input { + width: 100% !important; + text-align: center !important; + padding-left: 40px !important; + padding-right: 40px !important; + box-sizing: border-box !important; + } + + /* Position search icon inside input - RTL SAFE */ + .genius-search-icon { + position: absolute !important; + right: 40px !important; /* Moved DEEP inside */ + left: auto !important; + top: 50% !important; + transform: translateY(-50%) !important; + pointer-events: none !important; + z-index: 1000 !important; /* Very high z-index */ + color: var(--dash-primary) !important; + } +} + +/* Extra small devices (400px and below) */ +@media (max-width: 400px) { + /* Fix Overlap Issue: Add margin to attendance section */ + .dashboard-attendance-section { + margin: 20px auto 0 auto !important; /* Centering with auto margins */ + padding-top: 10px !important; + border-top: 1px solid rgba(0,0,0,0.05) !important; + width: 90% !important; /* Contain width */ + max-width: 350px !important; + } + + .attendance-section-body { + width: 100% !important; + margin: 0 !important; + box-shadow: 0 4px 15px rgba(0,0,0,0.05) !important; + } + + .profile-container .pp-image-section img, + .profile-container .pp-image-section .img-box { + height: 60px !important; + width: 60px !important; + } + + .profile-container .info-section h2 { + font-size: 14px !important; + } + + .profile-container .info-section p { + font-size: 10px !important; + } + + /* Single column cards on very small screens */ + .dashboard-module-charts { + grid-template-columns: 1fr !important; /* Single column */ + gap: 8px !important; + } + + .module-box .module-box-container { + min-height: 100px !important; + } + + .module-box h4 { + font-size: 18px !important; + } + + .module-box h3 { + font-size: 8px !important; + } + + .card3 .card-body .box-1 h3 { + font-size: 20px !important; + } + + .attendance-section-body .attendance-img-section::before { + width: 45px !important; + height: 45px !important; + } + + .attendance-section-body .attendance-img-section img { + width: 45px !important; + height: 45px !important; + } +} + +/* ============================================================ + HIDDEN CLASS FOR SEARCH FILTER + ============================================================ */ + +.card-hidden, +.genius-hidden { + display: none !important; +} + +/* ============================================================ + GENIUS SEARCH BAR - Premium Design + ============================================================ */ + +.genius-search-container { + display: flex !important; + align-items: center !important; + margin-left: auto !important; +} + +.genius-search-wrapper { + position: relative !important; + display: flex !important; + align-items: center !important; +} + +.genius-search-icon { + position: absolute !important; + left: 14px !important; + color: var(--dash-text-light) !important; + font-size: 14px !important; + pointer-events: none !important; + z-index: 1 !important; +} + +.genius-search-input { + background: var(--dash-white) !important; + border: 1px solid var(--dash-border) !important; + border-radius: 25px !important; + padding: 10px 40px 10px 38px !important; + font-size: 14px !important; + width: 300px !important; + color: var(--dash-text) !important; + transition: all var(--dash-transition) !important; + box-shadow: var(--dash-shadow-sm) !important; +} + +.genius-search-input::placeholder { + color: var(--dash-text-light) !important; +} + +.genius-search-input:focus { + outline: none !important; + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(8, 145, 178, 0.15) !important; +} + +.genius-search-clear { + position: absolute !important; + right: 14px !important; + color: var(--dash-text-light) !important; + font-size: 18px !important; + cursor: pointer !important; + line-height: 1 !important; + transition: color var(--dash-transition) !important; +} + +.genius-search-clear:hover { + color: var(--dash-danger) !important; +} + +/* ============================================================ + SELF SERVICES - Title Styling (Not Button) + ============================================================ */ + +/* Style Self Services tab as a prominent title when it's the only tab */ +.dashboard-nav-buttons .nav-tabs li.genius-single-tab { + background: none !important; + box-shadow: none !important; + border: none !important; +} + +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a, +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a.genius-title-styled { + background: transparent !important; + color: var(--dash-secondary) !important; + font-size: 18px !important; + font-weight: 700 !important; + padding: 8px 4px 12px 4px !important; + box-shadow: none !important; + border: none !important; + border-bottom: 3px solid var(--dash-primary) !important; + cursor: default !important; + text-transform: none !important; + letter-spacing: 0.3px !important; + border-radius: 0 !important; +} + +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a:hover, +.dashboard-nav-buttons .nav-tabs li.genius-single-tab a.genius-title-styled:hover { + background: transparent !important; + color: var(--dash-secondary) !important; + transform: none !important; +} + +/* When there's only one tab, make the nav-tabs transparent */ +.dashboard-nav-buttons .nav-tabs:has(.genius-single-tab) { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; +} + +/* ============================================================ + CSS-ONLY SINGLE TAB DETECTION + When nav-tabs has only one li, style it as a title, not a button + ============================================================ */ + +/* CSS fallback for single tab - using :only-child selector */ +.dashboard-nav-buttons .nav-tabs li:only-child { + background: none !important; + box-shadow: none !important; + border: none !important; +} + +.dashboard-nav-buttons .nav-tabs li:only-child a, +.dashboard-nav-buttons .nav-tabs li:only-child a.nav-link, +.dashboard-nav-buttons .nav-tabs li:only-child a.active { + background: transparent !important; + color: var(--dash-secondary) !important; + font-size: 20px !important; + font-weight: 700 !important; + padding: 8px 0 12px 0 !important; + box-shadow: none !important; + border: none !important; + border-bottom: 3px solid var(--dash-primary) !important; + cursor: default !important; + text-transform: none !important; + letter-spacing: -0.3px !important; + border-radius: 0 !important; +} + +.dashboard-nav-buttons .nav-tabs li:only-child a:hover, +.dashboard-nav-buttons .nav-tabs li:only-child a.active:hover { + background: transparent !important; + color: var(--dash-secondary) !important; + transform: none !important; +} + +/* When nav-tabs has only one child, make parent transparent */ +.dashboard-nav-buttons .nav-tabs:has(li:only-child) { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 !important; + display: block !important; +} + +/* ============================================================ + SERVICE CARDS (Card3) - GENIUS SUBTLE ENHANCEMENTS + ============================================================ */ + +/* Card3 number styling - subtle genius effect */ +.card3 .card-body .box-1 h3 { + position: relative !important; + color: var(--dash-primary) !important; +} + +/* Card3 title - elegant styling */ +.card3 .card-body .box-2 h4 { + color: var(--dash-secondary) !important; + font-weight: 600 !important; + letter-spacing: -0.2px !important; + transition: color var(--dash-transition) !important; +} + +.card3 .card-body:hover .box-2 h4 { + color: var(--dash-primary) !important; +} + +/* Card3 icon subtle glow on hover */ +.card3 .card-body .box-2 i { + color: var(--dash-text-light) !important; + transition: all var(--dash-transition) !important; +} + +.card3 .card-body:hover .box-2 i { + color: var(--dash-primary) !important; + text-shadow: 0 0 8px rgba(8, 145, 178, 0.3) !important; +} + +/* ============================================================ + GREEN DOT - FINAL POSITION FIX + ============================================================ */ + +/* Ensure green dot is in correct position */ +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; +} + + + +/* ============================================================ + RTL SUPPORT FOR NEW ELEMENTS + ============================================================ */ + +.o_rtl { + .genius-search-container { + margin-left: 0 !important; + margin-right: auto !important; + } + + .genius-search-icon { + left: auto !important; + right: 14px !important; + } + + .genius-search-input { + padding: 10px 38px 10px 40px !important; + text-align: right !important; + } + + .genius-search-clear { + right: auto !important; + left: 14px !important; + } +} + +/* ============================================================ + CRITICAL OVERRIDES - MUST LOAD LAST + These rules override conflicting styles from core.scss + ============================================================ */ + + + +/* 2. FIX: Remove ALL colored theme backgrounds from stat boxes */ +.module-box-container, +.module-box .module-box-container, +.module-box-container.theme-1, +.module-box-container.theme-2, +.module-box-container.theme-3, +.module-box .module-box-container.theme-1, +.module-box .module-box-container.theme-2, +.module-box .module-box-container.theme-3 { + background: var(--dash-white, #ffffff) !important; + background-color: var(--dash-white, #ffffff) !important; +} + +/* Also remove colored backgrounds from module-icon inside themed containers */ +.module-box-container.theme-1 .module-icon, +.module-box-container.theme-2 .module-icon, +.module-box-container.theme-3 .module-icon { + background: var(--dash-primary) !important; +} + +/* 3. FIX: COMPLETE OVERRIDE - Remove ALL borders from nav-tabs (core.scss adds them) */ + + +/* 4. FIX: Correct green dot position - removed by user request */ +.profile-container .pp-image-section { + position: relative !important; + display: inline-block !important; +} + + + +/* 5. FIX: Round corners on attendance/statistics white box */ +.attendance-section-body, +.dashboard-attendance-section .attendance-section-body, +.module-box-container, +.module-box .module-box-container { + border-radius: 10px !important; +} + +/* 6. FIX: Ensure animated border doesn't appear */ +.dashboard-nav-buttons .nav-tabs::before, +.dashboard-nav-buttons .nav-tabs::after, +.nav-tabs::before, +.nav-tabs::after { + display: none !important; + content: none !important; +} + +/* 7. FIX: Remove any focus/active border styling that might cause green border */ +.dashboard-nav-buttons .nav-tabs li a:focus, +.dashboard-nav-buttons .nav-tabs li a:active, +.nav-tabs li a:focus, +.nav-tabs li a:active { + outline: none !important; + border: none !important; + box-shadow: none !important; +} + +/* 8. FIX: Remove any animated transitions on initial load */ +.dashboard-container *, +.system-dashboard * { + animation-delay: 0s !important; +} + +/* Disable card entrance animations that cause "reload" glitch */ +@keyframes none-animation { + from { opacity: 1; transform: none; } + to { opacity: 1; transform: none; } +} + +.card-section .card, +.card-section1 .card-line, +.dashboard-module-charts .module-box { + animation: none !important; + opacity: 1 !important; + transform: none !important; +} + +/* ============================================================ + CHART TOOLTIP - Premium Styling + ============================================================ */ + +/* Override the ugly black tooltip */ +.pc-tooltip { + background: var(--dash-secondary) !important; + color: white !important; + font-size: 12px !important; + font-weight: 500 !important; + padding: 8px 14px !important; + border-radius: 8px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + border: 1px solid rgba(255, 255, 255, 0.1) !important; + backdrop-filter: blur(8px) !important; + -webkit-backdrop-filter: blur(8px) !important; + line-height: 1.4 !important; + max-width: 200px !important; + text-align: center !important; +} + +/* ============================================================ + CHART HALF-CIRCLES - Subtle Enhancements + ============================================================ */ + +/* Chart container subtle glow */ +#chartContainer, +#chartPaylips, +#chartTimesheet { + position: relative !important; +} + +#chartContainer svg, +#chartPaylips svg, +#chartTimesheet svg { + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08)) !important; +} + +/* Chart paths - subtle gradient effect */ +.module-box svg path { + transition: opacity var(--dash-transition) !important; +} + +.module-box:hover svg path { + opacity: 0.9 !important; +} + + +/* ============================================================ + ADD NEW BUTTON - Enhanced Visibility and Hover + ============================================================ */ + +/* Ensure + icon is always visible */ +.card3 .card-body .box-2 i.mdi-plus, +.card3 .card-body .box-2 i[class*="plus"] { + color: white !important; + font-size: 20px !important; + font-weight: bold !important; + opacity: 1 !important; + visibility: visible !important; +} + +/* Add New button - base state */ +.card3 .card-body .box-2 { + background: var(--dash-primary) !important; + transition: all 0.25s ease !important; + cursor: pointer !important; + position: relative !important; + overflow: hidden !important; +} + +/* Add subtle shine overlay */ +.card3 .card-body .box-2::after { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: -100% !important; + width: 100% !important; + height: 100% !important; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent) !important; + transition: left 0.5s ease !important; +} + +/* Special hover for Add New button area */ +.card3 .card-body .box-2:hover { + background: var(--dash-primary-dark) !important; + transform: translateY(-2px) !important; + box-shadow: 0 6px 20px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.4) !important; +} + +.card3 .card-body .box-2:hover::after { + left: 100% !important; +} + +.card3 .card-body .box-2:hover i { + transform: rotate(90deg) scale(1.1) !important; +} + +.card3 .card-body .box-2:hover span { + letter-spacing: 1px !important; +} + +/* ============================================================ + CHECK-IN BUTTON - Ensure Theme Variable Usage + ============================================================ */ + +/* Re-enforce theme variable on check-in button */ + + +/* Check-in/out button styles now defined earlier in file */ +/* Removed duplicate rule that conflicted with white bg approach */ + +/* ============================================================ + THEME VARIABLES - Complete Element Binding + ============================================================ */ + +/* Card2 header gradient - use secondary */ +.card2 .card-container .card-header { + background: linear-gradient(135deg, var(--dash-primary) 0%, var(--dash-primary-dark) 100%) !important; +} + +/* Card2 hover accent */ +.card2 .card-container:hover { + border-color: var(--dash-primary-light) !important; +} + +/* Action buttons in cards */ +.card2 .card-body table tr td:last-child div, +.card2 .card-body .btn-action { + background: var(--dash-primary) !important; +} + +/* Search input focus */ +.genius-search-input:focus, +.dashboard-search-input:focus { + border-color: var(--dash-primary) !important; + box-shadow: 0 0 0 3px rgba(var(--dash-primary-rgb, 8, 145, 178), 0.15) !important; +} + +/* Single tab underline */ +.dashboard-nav-buttons .nav-tabs li:only-child a { + border-bottom-color: var(--dash-primary) !important; +} + +/* Scrollbar thumb */ +.card2 .card-container .card-body::-webkit-scrollbar-thumb { + background: var(--dash-primary) !important; +} + +/* Status indicators */ +.attendance-section-body .attendance-status.online, +.status-online { + color: var(--dash-success) !important; + background: rgba(var(--dash-success-rgb, 16, 185, 129), 0.1) !important; +} + +.attendance-section-body .attendance-status.offline, +.status-offline { + color: var(--dash-danger, #ef4444) !important; +} + +/* ============================================================ + CRITICAL: ATTENDANCE SECTION OVERRIDES + ============================================================ */ + +/* Ensure all text inside attendance has no extra backgrounds */ +.attendance-section-body p, +.attendance-section-body span, +.attendance-section-body .last-checkin-section { + background: none !important; + background-color: transparent !important; +} + + + +/* ============================================================ + CRITICAL: PROFILE SECTION VISIBILITY + ============================================================ */ + +/* Ensure profile text is visible on glassmorphism background */ +.profile-container .info-section .fn-section, +.profile-container .info-section .fn-job, +.profile-container .info-section .fn-id { + color: white !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3) !important; +} + +.profile-container .info-section .fn-section { + font-size: 16px !important; + font-weight: 700 !important; +} + +.profile-container .info-section .fn-job { + font-size: 11px !important; + font-weight: 500 !important; + opacity: 0.85 !important; + text-transform: uppercase !important; + letter-spacing: 0.5px !important; +} + + +/* ============================================================ + ENHANCED CELEBRATION MODE - Persistent Birthday & Anniversary + ============================================================ */ + +/* Dashboard-wide celebration mode class */ +.dashboard-container.celebration-mode { + position: relative; +} + +/* Festive header gradient animation for birthday */ +.dashboard-container.birthday-mode .dashboard-header { + background: linear-gradient(135deg, #667eea 0%, #f093fb 50%, #f5576c 100%) !important; + background-size: 200% 200% !important; + animation: festiveGradient 5s ease infinite !important; +} + +/* Festive header gradient animation for anniversary */ +.dashboard-container.anniversary-mode .dashboard-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 50%, #667eea 100%) !important; + background-size: 200% 200% !important; + animation: festiveGradient 5s ease infinite !important; +} + +@keyframes festiveGradient { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +/* Golden glow ring around employee photo */ +.img-box.celebration-glow { + animation: photoGlow 2s ease-in-out infinite; + box-shadow: 0 0 0 4px rgba(255, 215, 0, 0.6), + 0 0 20px rgba(255, 215, 0, 0.4), + 0 0 40px rgba(255, 215, 0, 0.2) !important; +} + +.birthday-mode .img-box.celebration-glow { + box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.7), + 0 0 20px rgba(255, 105, 180, 0.5), + 0 0 40px rgba(255, 105, 180, 0.3) !important; +} + +.anniversary-mode .img-box.celebration-glow { + box-shadow: 0 0 0 4px rgba(79, 172, 254, 0.7), + 0 0 20px rgba(79, 172, 254, 0.5), + 0 0 40px rgba(79, 172, 254, 0.3) !important; +} + +@keyframes photoGlow { + 0%, 100% { + transform: scale(1); + filter: brightness(1); + } + 50% { + transform: scale(1.02); + filter: brightness(1.1); + } +} + +/* Celebration badge near employee name */ +.celebration-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + margin-bottom: 8px; + animation: badgePop 0.5s ease-out, badgeGlow 2s ease-in-out infinite; +} + +.celebration-badge.birthday { + background: linear-gradient(135deg, #f093fb, #f5576c); + color: white; + box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4); +} + +.celebration-badge.anniversary { + background: linear-gradient(135deg, #4facfe, #00f2fe); + color: white; + box-shadow: 0 4px 15px rgba(79, 172, 254, 0.4); +} + +.badge-emoji { + font-size: 16px; + animation: bounce 1s ease infinite; +} + +.badge-text { + letter-spacing: 0.3px; +} + +@keyframes badgePop { + 0% { + transform: scale(0); + opacity: 0; + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + +@keyframes badgeGlow { + 0%, 100% { + box-shadow: 0 4px 15px rgba(245, 87, 108, 0.4); + } + 50% { + box-shadow: 0 4px 25px rgba(245, 87, 108, 0.6); + } +} + +/* Floating celebration message */ +.celebration-floating-msg { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + margin-top: 12px; + border-radius: 10px; + font-size: 13px; + font-weight: 500; + animation: floatIn 0.6s ease-out, floatBounce 3s ease-in-out infinite; + position: relative; + overflow: hidden; +} + +.celebration-floating-msg::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + animation: shimmer 3s ease-in-out infinite; +} + +.celebration-floating-msg.birthday { + background: linear-gradient(135deg, rgba(240, 147, 251, 0.15), rgba(245, 87, 108, 0.15)); + border: 1px solid rgba(245, 87, 108, 0.3); + color: #fff; +} + +.celebration-floating-msg.anniversary { + background: linear-gradient(135deg, rgba(79, 172, 254, 0.15), rgba(0, 242, 254, 0.15)); + border: 1px solid rgba(79, 172, 254, 0.3); + color: #fff; +} + +.floating-emoji { + font-size: 20px; + animation: bounce 1.5s ease infinite; +} + +.floating-text { + flex: 1; +} + +@keyframes floatIn { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes floatBounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-3px); + } +} + +@keyframes shimmer { + 0% { + left: -100%; + } + 100% { + left: 100%; + } +} + +@keyframes bounce { + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-10px); + } + 60% { + transform: translateY(-5px); + } +} + + +/* ============================================================ + ANIMATED COUNTERS - Smooth number transitions + ============================================================ */ + +/* Ensure counter elements have proper number styling */ +.chart-center-value, +.leave-center-value, +.leave-total-amount span, +.leave-left-amount span, +.timesheet-center-value, +.attendance-center-value { + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum"; +} + +/* Counter value pulse on load */ +.counter-animated { + animation: counterPop 0.3s ease-out; +} + +@keyframes counterPop { + 0% { + transform: scale(0.8); + opacity: 0; + } + 50% { + transform: scale(1.05); + } + 100% { + transform: scale(1); + opacity: 1; + } +} + + +/* ============================================================ + ENHANCED CELEBRATION DECORATIONS - More Festive Effects + ============================================================ */ + +/* Shimmer effect on greeting TEXT only (not emoji) - NO visible background */ +.celebration-mode .greeting-text { + font-weight: 700 !important; + font-size: 14px !important; +} + +/* Emoji stays normal */ +.celebration-mode .greeting-emoji { + font-size: 16px; + margin-inline-end: 4px; +} + +/* Shimmer effect on text portion only */ +.celebration-mode .greeting-shimmer { + background: linear-gradient(90deg, #ffd700, #ff69b4, #ffd700); + background-size: 200% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: textShimmer 2s linear infinite; +} + +.birthday-mode .greeting-shimmer { + background: linear-gradient(90deg, #f093fb, #f5576c, #ffd700, #f093fb); + background-size: 300% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.anniversary-mode .greeting-shimmer, +.anniversary-mode .anniversary-shimmer { + background: linear-gradient(90deg, #ffd700, #ff8c00, #ffa500, #ffd700); + background-size: 300% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +@keyframes textShimmer { + 0% { background-position: 0% center; } + 100% { background-position: 200% center; } +} + +/* Appreciation ribbon for anniversary - positioned at bottom of header */ +.appreciation-ribbon { + position: absolute; + bottom: 8px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, rgba(255, 215, 0, 0.95), rgba(255, 140, 0, 0.95)); + color: #333; + padding: 8px 8px; + border-radius: 25px; + font-size: 12px; + font-weight: 600; + box-shadow: 0 4px 15px rgba(255, 140, 0, 0.4); + animation: ribbonSlideIn 0.6s ease-out, ribbonGlow 2s ease-in-out infinite; + z-index: 100; + white-space: nowrap; + max-width: 90%; + overflow: hidden; + text-overflow: ellipsis; +} + +.appreciation-ribbon .ribbon-text { + display: inline-block; +} + +@keyframes ribbonSlideIn { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(20px); + } + 100% { + opacity: 1; + transform: translateX(-50%) translateY(0); + } +} + +@keyframes ribbonGlow { + 0%, 100% { + box-shadow: 0 4px 15px rgba(255, 140, 0, 0.4); + } + 50% { + box-shadow: 0 4px 25px rgba(255, 140, 0, 0.6); + } +} + +/* Festive glow on main stat cards */ +.celebration-mode .card2 { + position: relative; + overflow: visible; +} + +.celebration-mode .card2::after { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + border-radius: inherit; + background: linear-gradient(45deg, rgba(255,215,0,0.3), transparent, rgba(255,105,180,0.3)); + z-index: -1; + animation: cardGlow 3s ease-in-out infinite; + pointer-events: none; +} + +.birthday-mode .card2::after { + background: linear-gradient(45deg, rgba(240,147,251,0.4), transparent, rgba(245,87,108,0.4)); +} + +.anniversary-mode .card2::after { + background: linear-gradient(45deg, rgba(79,172,254,0.4), transparent, rgba(0,242,254,0.4)); +} + +@keyframes cardGlow { + 0%, 100% { opacity: 0.5; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.02); } +} + +/* Celebration stars decoration on corners (using pseudo-elements, no layout shift) */ +.celebration-mode .dashboard-user-data-section::before { + content: '✨'; + position: absolute; + top: 25px; + right: 25px; + font-size: 20px; + animation: starTwinkle 1.5s ease-in-out infinite; + pointer-events: none; + z-index: 10; +} + +.celebration-mode .dashboard-user-data-section::after { + content: '⭐'; + position: absolute; + bottom: 25px; + left: 25px; + font-size: 16px; + animation: starTwinkle 2s ease-in-out infinite 0.5s; + pointer-events: none; + z-index: 10; +} + +.o_rtl .celebration-mode .dashboard-user-data-section::before { + right: auto; + left: 25px; +} + +.o_rtl .celebration-mode .dashboard-user-data-section::after { + left: auto; + right: 25px; +} + +@keyframes starTwinkle { + 0%, 100% { opacity: 0.6; transform: scale(1) rotate(0deg); } + 50% { opacity: 1; transform: scale(1.2) rotate(10deg); } +} + +/* Festive employee code badge */ +.celebration-mode .emp-code-badge { + background: linear-gradient(135deg, #ffd700, #ffb347) !important; + color: #333 !important; + box-shadow: 0 2px 10px rgba(255, 215, 0, 0.5) !important; + animation: badgeShine 2s ease-in-out infinite; +} + +@keyframes badgeShine { + 0%, 100% { box-shadow: 0 2px 10px rgba(255, 215, 0, 0.5); } + 50% { box-shadow: 0 2px 20px rgba(255, 215, 0, 0.8); } +} + +/* Celebration effect on nav-buttons separator line */ +.celebration-mode .dashboard-nav-buttons::after { + height: 4px !important; /* Slightly thicker to show effect */ + opacity: 1 !important; + /* Stronger gradient with more stops for visible movement */ + background: linear-gradient(90deg, + #ffd700, #ff69b4, #00f2fe, #ffd700, #ff69b4 + ) !important; + background-size: 200% 100% !important; /* Smaller size = faster visual movement */ + animation: rainbowBorder 1.5s linear infinite !important; /* Faster animation */ + box-shadow: 0 1px 4px rgba(0,0,0,0.1); +} + +.birthday-mode .dashboard-nav-buttons::after { + background: linear-gradient(90deg, + #f093fb, #f5576c, #ffd700, #00f2fe, #f093fb + ) !important; + background-size: 200% 100% !important; +} + +.anniversary-mode .dashboard-nav-buttons::after { + /* High contrast gold/orange/white for shine */ + background: linear-gradient(90deg, + #ffd700, #ff8c00, #ffffff, #ff8c00, #ffd700 + ) !important; + background-size: 200% 100% !important; +} + +@keyframes rainbowBorder { + 0% { background-position: 0% 0; } + 100% { background-position: 200% 0; } +} + +/* Floating emoji decorations on charts */ +.celebration-mode .chart-section::before { + content: '🎈'; + position: absolute; + top: 5px; + right: 5px; + font-size: 18px; + animation: floatEmoji 3s ease-in-out infinite; + pointer-events: none; + opacity: 0.8; +} + +.birthday-mode .chart-section::before { + content: '🎂'; +} + +.anniversary-mode .chart-section::before { + content: '🎉'; +} + +@keyframes floatEmoji { + 0%, 100% { transform: translateY(0) rotate(-5deg); } + 50% { transform: translateY(-8px) rotate(5deg); } +} + +/* Make sure decorations don't affect layout */ +.celebration-mode .dashboard-user-data-section, +.celebration-mode .chart-section { + position: relative; +} + +/* ═══════════════════════════════════════════════════════════ + PREMIUM CHECK-IN/OUT NOTIFICATION - GENIUS DESIGN + Centered glass-morphism modal with warm professional messaging + ═══════════════════════════════════════════════════════════ */ + +/* Full-screen overlay with backdrop blur */ +.attendance-notification-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.2); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + + &.show { + opacity: 1; + visibility: visible; + + .attendance-notification { + transform: scale(1) translateY(0); + opacity: 1; + } + } +} + +/* Premium notification card */ +.attendance-notification { + background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(248,250,252,0.98) 100%); + border-radius: 24px; + padding: 40px 50px; + text-align: center; + box-shadow: + 0 25px 80px rgba(0, 0, 0, 0.25), + 0 10px 30px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.8); + transform: scale(0.9) translateY(20px); + opacity: 0; + transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1); + max-width: 420px; + width: 90vw; + position: relative; + overflow: hidden; + + /* Decorative gradient line at top */ + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 5px; + border-radius: 24px 24px 0 0; + } + + /* Check-in gradient (warm gold/orange) */ + &.notification-checkin::before { + background: linear-gradient(90deg, #f59e0b, #f97316, #fbbf24); + } + + /* Check-out gradient (cool blue/teal) */ + &.notification-checkout::before { + background: linear-gradient(90deg, #0891b2, #06b6d4, #22d3ee); + } +} + +/* Icon wrapper with glow effect */ +.notification-icon-wrapper { + width: 90px; + height: 90px; + margin: 0 auto 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + animation: iconBounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + + .notification-icon { + font-size: 56px; + line-height: 1; + filter: drop-shadow(0 4px 12px rgba(0,0,0,0.1)); + } +} + +/* Check-in icon background */ +.notification-checkin .notification-icon-wrapper { + background: linear-gradient(135deg, rgba(251, 191, 36, 0.2) 0%, rgba(245, 158, 11, 0.1) 100%); + box-shadow: 0 0 40px rgba(245, 158, 11, 0.3); +} + +/* Check-out icon background */ +.notification-checkout .notification-icon-wrapper { + background: linear-gradient(135deg, rgba(6, 182, 212, 0.2) 0%, rgba(8, 145, 178, 0.1) 100%); + box-shadow: 0 0 40px rgba(6, 182, 212, 0.3); +} + +@keyframes iconBounce { + 0% { transform: scale(0) rotate(-10deg); } + 60% { transform: scale(1.2) rotate(5deg); } + 100% { transform: scale(1) rotate(0); } +} + +/* Content styles */ +.notification-content { + .notification-title { + font-size: 28px; + font-weight: 700; + color: #1e293b; + margin: 0 0 10px; + letter-spacing: -0.5px; + animation: fadeSlideUp 0.5s 0.2s ease both; + } + + .notification-subtitle { + font-size: 16px; + font-weight: 400; + color: #64748b; + margin: 0 0 20px; + line-height: 1.6; + animation: fadeSlideUp 0.5s 0.3s ease both; + } + + .notification-time { + display: inline-flex; + align-items: center; + gap: 8px; + background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); + padding: 10px 20px; + border-radius: 50px; + animation: fadeSlideUp 0.5s 0.4s ease both; + + .time-icon { + font-size: 16px; + } + + .time-value { + font-size: 15px; + font-weight: 600; + color: #334155; + letter-spacing: 0.5px; + } + } +} + +@keyframes fadeSlideUp { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Subtle Icon Pulse Animation on Check-in/out */ +.attendance-success-pulse { + animation: attendancePulse 0.8s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes attendancePulse { + 0% { transform: scale(1); } + 30% { transform: scale(1.2); } + 60% { transform: scale(0.95); } + 100% { transform: scale(1); } +} + +/* Mobile Responsive */ +@media (max-width: 576px) { + .attendance-notification { + padding: 30px 25px; + border-radius: 20px; + + &::before { + height: 4px; + } + } + + .notification-icon-wrapper { + width: 70px; + height: 70px; + margin-bottom: 18px; + + .notification-icon { + font-size: 42px; + } + } + + .notification-content { + .notification-title { + font-size: 22px; + } + + .notification-subtitle { + font-size: 14px; + margin-bottom: 16px; + } + + .notification-time { + padding: 8px 16px; + + .time-value { + font-size: 14px; + } + } + } +} + +/* RTL Support */ +[dir="rtl"] .attendance-notification, +body.o_rtl .attendance-notification { + .notification-content { + .notification-subtitle { + direction: rtl; + } + } +} + +/* ═══════════════════════════════════════════════════════════ + ATTENDANCE ERROR TOAST - Zone/Location Errors + ═══════════════════════════════════════════════════════════ */ + +.attendance-error-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(15, 23, 42, 0.3); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + z-index: 99999; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + + &.show { + opacity: 1; + visibility: visible; + + .attendance-error-toast { + transform: scale(1) translateY(0); + opacity: 1; + } + } +} + +.attendance-error-toast { + background: #ffffff; + border-radius: 16px; + padding: 30px 35px; + text-align: center; + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.2), + 0 8px 20px rgba(0, 0, 0, 0.1); + transform: scale(0.9) translateY(20px); + opacity: 0; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); + max-width: 380px; + width: 90vw; + border-top: 4px solid #f59e0b; + + .error-icon { + font-size: 48px; + margin-bottom: 16px; + animation: errorShake 0.5s ease; + } + + .error-message { + font-size: 15px; + font-weight: 500; + color: #334155; + line-height: 1.7; + margin-bottom: 24px; + white-space: pre-line; + } + + .error-ok-btn { + background: var(--dash-secondary) !important; + color: #ffffff; + border: none; + padding: 12px 40px; + border-radius: 8px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-2px); + filter: brightness(1.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + } + + &:active { + transform: translateY(0); + } + } +} + +@keyframes errorShake { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-8px); } + 40% { transform: translateX(8px); } + 60% { transform: translateX(-4px); } + 80% { transform: translateX(4px); } +} + +/* RTL Support for Error Toast */ +[dir="rtl"] .attendance-error-toast, +body.o_rtl .attendance-error-toast { + .error-message { + direction: rtl; + } +} + +/* Mobile Responsive */ +@media (max-width: 576px) { + .attendance-error-toast { + padding: 24px 20px; + + .error-icon { + font-size: 40px; + } + + .error-message { + font-size: 14px; + } + + .error-ok-btn { + padding: 10px 30px; + font-size: 14px; + } + } +} + +/* ═══════════════════════════════════════════════════════════ + DRAG AND DROP SERVICE CARDS REORDERING + ═══════════════════════════════════════════════════════════ */ + +.card3.draggable-card, +.card2.draggable-card { + cursor: grab; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; + position: relative; + + &:active { + cursor: grabbing; + } + + /* Dragging state - reduce opacity */ + &.dragging { + opacity: 0.5; + transform: scale(0.95); + z-index: 1000; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + } + + /* Drop target highlight */ + &.drag-over { + transform: scale(1.03); + + .card-body, + .card-container { + border: 2px dashed var(--dash-primary, #0891b2) !important; + background: rgba(8, 145, 178, 0.05); + } + } +} + +/* Subtle drag handle hint on hover */ +.card3.draggable-card .card-body::after { + content: '⋮⋮'; + position: absolute; + top: 6px; + left: 6px; + font-size: 10px; + color: rgba(0, 0, 0, 0.15); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; +} + +.card3.draggable-card:hover .card-body::after { + opacity: 1; +} + +/* RTL Support for drag handle */ +[dir="rtl"] .card3.draggable-card .card-body::after, +body.o_rtl .card3.draggable-card .card-body::after { + left: auto; + right: 6px; +} + +/* Disable drag on mobile - touch devices use different gestures */ +@media (max-width: 768px) { + .card3.draggable-card, + .module-box.draggable-card { + cursor: default; + + .card-body::after, + .module-box-container::after { + display: none; + } + } +} + +/* ═══════════════════════════════════════════════════════════ + DRAG AND DROP FOR STATS MODULE-BOXES (HEADER CARDS) + ═══════════════════════════════════════════════════════════ */ + +.module-box.draggable-card { + cursor: grab; + transition: transform 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; + position: relative; + + &:active { + cursor: grabbing; + } + + /* Dragging state */ + &.dragging { + opacity: 0.5; + transform: scale(0.95); + z-index: 1000; + + .module-box-container { + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25); + } + } + + /* Drop target highlight */ + &.drag-over { + transform: scale(1.02); + + .module-box-container { + border: 2px dashed var(--dash-primary, #0891b2) !important; + background: rgba(8, 145, 178, 0.08); + } + } +} + +/* Subtle drag handle hint on hover for stats cards */ +.module-box.draggable-card .module-box-container::after { + content: '⋮⋮'; + position: absolute; + top: 4px; + left: 4px; + font-size: 10px; + color: rgba(255, 255, 255, 0.4); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: 10; +} + +.module-box.draggable-card:hover .module-box-container::after { + opacity: 1; +} + +/* RTL Support for stats module-box drag handle */ +[dir="rtl"] .module-box.draggable-card .module-box-container::after, +body.o_rtl .module-box.draggable-card .module-box-container::after { + left: auto; + right: 4px; +} + +/* Service Icon Uniformity and Hover Effects */ +.service-icon { + /* Base styles setup in JS (height/width/font-size), + but we ensure the container allows transition here */ + display: inline-block; + vertical-align: middle; + /* Ensure icon is centered */ + text-align: center; +} + +/* Apply hover effect to the parent card so it triggers on card hover */ +.card-section1 .card3:hover .service-icon { + transform: scale(1.15); /* 15% zoom on hover */ +} + +/* Also support direct usage if needed */ +.service-icon:hover { + transform: scale(1.15); +} + +/* Approval Card Header Styling - MOVED FROM JS */ +.card2 .card-container .card-header { + background-color: var(--dash-primary) !important; +} +// .card-header h4, .card-header i.fa { +// color: white !important; +// } + +/* Missing Wrapper Class from JS Refactor */ +.service-icon-wrapper { + text-align: center; + margin-bottom: 10px; +} + +/* Approval Card Header Icon Styling */ +.approval-header-icon { + margin: 0 8px; /* Replaces margin-left: 8px; margin-right: 8px; */ + vertical-align: middle; +} + +img.approval-header-icon { + height: 30px; + width: 30px; + object-fit: contain; +} + +i.approval-header-icon { + font-size: 30px; + color: var(--dash-secondary); +} + +/* FORCE SUPER SPECIFIC OVERRIDE for Approval Card Header */ +/* User reported conflicts, so we use max specificity */ +.o_content .dashboard-container .card2 .card-container .card-header { + background-color: var(--dash-primary, #0891b2) !important; /* Fallback to Cyan */ + color: #ffffff !important; +} + +.o_content .dashboard-container .card2 .card-container .card-header h4, +.o_content .dashboard-container .card2 .card-container .card-header i { + color: #ffffff !important; +} + +/* ============================================== + DASHBOARD CONFIG PREVIEW STYLES + ============================================== */ + +/* Base container for preview */ +.dashboard-icon-preview-box { + display: flex; + align-items: center; + justify-content: center; + background: #f1f1f1; + border: 1px solid #ddd; + border-radius: 4px; + overflow: hidden; + color: #666; + margin: 0; /* Align nicely */ +} + +.dashboard-icon-preview-box img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* --- LIST VIEW (TREE) CONTEXT --- */ +/*.o_list_view .dashboard-icon-preview-box, */ +/* Standard Odoo list uses o_data_cell classes, but we are inside a widget=html */ +.o_list_view .dashboard-icon-preview-box { + width: 40px; + height: 40px; +} + +.o_list_view .dashboard-icon-preview-box i { + font-size: 18px; /* Normal icon size for list */ +} + +/* --- FORM VIEW CONTEXT --- */ +/* Standard Odoo form avatar section */ +.o_form_view .oe_avatar .dashboard-icon-preview-box, +/* Fallback if oe_avatar class is missing or structure differs */ +.o_form_view .dashboard-icon-preview-box { + width: 90px; + height: 90px; + background: #f9f9f9; +} + +.o_form_view .dashboard-icon-preview-box i { + font-size: 3em; /* roughly fa-3x ~ fa-4x equivalent */ +} + +/* Special Case for Error/Default */ +.dashboard-icon-preview-box.error-mode { + color: #ef4444; /* Red for error */ + background: #fee2e2; +} + +/* ============================================================ + GENDER-AWARE PROFILE FRAME STYLING + ============================================================ + Subtle, professional differentiation for male/female employees + ============================================================ */ + +/* Male: Professional dark/cyan accent - HIGH SPECIFICITY to override base .img-box */ +.profile-container .pp-image-section .img-box.gender-male, +.img-box.gender-male { + border: 4px solid var(--dash-secondary) !important; + box-shadow: + 0 0 0 3px rgba(30, 41, 59, 0.15), + 0 8px 20px rgba(0, 0, 0, 0.25) !important; +} + +.profile-container .pp-image-section .img-box.gender-male:hover, +.img-box.gender-male:hover { + border-color: var(--dash-primary) !important; + box-shadow: + 0 0 0 4px rgba(8, 145, 178, 0.2), + 0 10px 25px rgba(0, 0, 0, 0.3) !important; +} + +/* Female: Elegant rose gold/warm accent - HIGH SPECIFICITY to override base .img-box */ +.profile-container .pp-image-section .img-box.gender-female, +.img-box.gender-female { + border: 4px solid #be847c !important; /* Rose gold */ + box-shadow: + 0 0 0 3px rgba(190, 132, 124, 0.2), + 0 8px 20px rgba(0, 0, 0, 0.2) !important; +} + +.profile-container .pp-image-section .img-box.gender-female:hover, +.img-box.gender-female:hover { + border-color: #d4a59a !important; /* Lighter rose gold on hover */ + box-shadow: + 0 0 0 4px rgba(212, 165, 154, 0.25), + 0 10px 25px rgba(0, 0, 0, 0.25) !important; +} + +/* Transition for smooth hover effect */ +.profile-container .pp-image-section .img-box.gender-male, +.profile-container .pp-image-section .img-box.gender-female, +.img-box.gender-male, +.img-box.gender-female { + transition: border-color 0.3s ease, box-shadow 0.3s ease !important; +} diff --git a/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss b/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss index 3c345471e..d2390d5df 100644 --- a/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss +++ b/odex25_base/system_dashboard_classic/static/src/scss/rtl-cards.scss @@ -1,77 +1,164 @@ -$color_1: linear-gradient(270deg, rgb(14, 62, 52) 0%, rgb(0, 136, 126) 75%); +/* ============================================================ + System Dashboard Classic - RTL Styles + ============================================================ + + This file contains RTL (Right-to-Left) overrides for Arabic/Hebrew + language support. Uses .o_rtl class added by Odoo. + + IMPORTANT: This file must load AFTER all LTR styles. + ============================================================ */ .o_rtl { - .card { - .card-body { - .box-2 { - .btn-group { - position: absolute; - bottom: -50px; - left: 10px; - right: auto; - transition: all .4s; + /* === Profile Section RTL === */ + .profile-container { + .pp-info-section { + border: none; + border-right: 1px solid #9f9f9f; + padding: 10px; + } + + /* Green status dot - flip to left side */ + .pp-image-section::after { + right: auto !important; + left: 8px !important; + } + } + + /* === Module Box / Statistics RTL === */ + .module-box { + .module-box-container { + .module-body { + padding: 10px 15px 10px 10px; + + a { + background-color: transparent; + + &:hover { + text-decoration: none; + } + + h3 { + margin: 0; + transition: all .3s ease-in-out; + color: #8a8a8a; + font-size: 16px; + text-align: center; + padding-top: 5px; + } } + + h2 { + text-align: center; + font-size: 26px; + color: #06211a; + margin-top: 5px; + } + + .module-icon { + text-align: center; + height: 80px; + width: 80px; + line-height: 80px; + border-radius: 50%; + position: absolute; + top: -40px; + right: -20px; + box-shadow: 0 3px 5px -1px rgba(0, 0, 0, .2), + 0 6px 10px 0 rgba(0, 0, 0, .14), + 0 1px 18px 0 rgba(0, 0, 0, .12) !important; + + i { + font-size: 45px; + color: #ffffff; + } + } + + .module-icon.red { background: #ee451f; } + .module-icon.green { background: #14bf1f; } + .module-icon.yellow { background: #f9d700; } + } + + /* Stat labels - reverse flex direction */ + p span { + flex-direction: row-reverse !important; } } } - .card3 { - padding: 0 5px; - .card-body { - .box-1 { - h4 { - margin-bottom: 0; - font-size: 16px; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - color: $color_1; - padding-top: 10px !important; - padding-bottom: 0 !important; - } + + /* === Card2 (Approval Cards) RTL === */ + .card2 { + padding-left: inherit; + padding-right: 0; + + .card-container .card-header { + flex-direction: row-reverse !important; + + img { + margin-right: 0 !important; + margin-left: 12px !important; } - .box-2 { - &:hover { - .btn-group { - bottom: 10px; - } - } - i { + + h4 { + text-align: right !important; + } + } + + .card-container .card-body table tr td { + text-align: right; + + &:last-child { + text-align: left; + + div { float: left; } - span { - float: right; - } } } } - .card2 { - .card-container { - .card-header { - img { - margin-right: unset; - margin-left: 10px; - } - } - .card-body { - table { - tr { - td { - &:last-child { - div { - float: left; - } - } - } - } - } - } + + /* === Card3 (Service Cards) RTL === */ + .card3 .card-body .box-2 { + flex-direction: row-reverse !important; + + i { + margin-left: 0 !important; + margin-right: 8px !important; } - padding-left: inherit; } - .profile-container { - padding-right: 0; + + /* === Tab Navigation RTL === */ + .dashboard-nav-buttons { + flex-direction: row-reverse !important; + + .nav-tabs { + flex-direction: row-reverse !important; + } } - .p0 { - padding: 0; + + /* === Search Container RTL === */ + .genius-search-container { + margin-left: 0 !important; + margin-right: auto !important; + } + + .genius-search-icon { + left: auto !important; + right: 14px !important; + } + + .genius-search-input { + padding: 10px 38px 10px 40px !important; + text-align: right !important; + } + + .genius-search-clear { + right: auto !important; + left: 14px !important; + } + + /* === Attendance Section RTL === */ + .dashboard-attendance-section { + border-left: none !important; + border-right: 1px solid rgba(255, 255, 255, 0.1) !important; } } diff --git a/odex25_base/system_dashboard_classic/static/src/scss/variables.scss b/odex25_base/system_dashboard_classic/static/src/scss/variables.scss index 557cf43c0..50b3c93a3 100644 --- a/odex25_base/system_dashboard_classic/static/src/scss/variables.scss +++ b/odex25_base/system_dashboard_classic/static/src/scss/variables.scss @@ -1,17 +1,59 @@ -$bg_user_section: linear-gradient(270deg, rgb(14, 62, 52) 0%, rgb(0, 136, 126) 75%) !default; -$bg_user_statistics: #FFFFFF !default; -$bg_checkin_btn: #2ead97 !default; -$bg_checkin_btn_hover: #084e41 !default; -$bg_checkout_btn: #2ead97 !default; -$bg_checkout_btn_hover: #084e41 !default; -$divd_border_color: #2eac96 !default; -$bg_dashboard_nav: #2ead97 !default; -$bg_dashboard_nav_hover: #084e41 !default; -$color_nav: #0B2E59 !default; -$bg_card: #f4fefe !default; -$bg_card_button: linear-gradient(270deg, #0e3e34 0%, #00887e 75%) !default; -$bg_card_header: linear-gradient(270deg, #0e3e34 0%, #00887e 75%) !default; +/* ============================================================ + 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; diff --git a/odex25_base/system_dashboard_classic/static/src/xml/self_service_dashboard.xml b/odex25_base/system_dashboard_classic/static/src/xml/self_service_dashboard.xml index 2f78135ed..270589f2e 100644 --- a/odex25_base/system_dashboard_classic/static/src/xml/self_service_dashboard.xml +++ b/odex25_base/system_dashboard_classic/static/src/xml/self_service_dashboard.xml @@ -23,7 +23,7 @@
-
+ -
+ -
+ + +
@@ -81,12 +119,10 @@
-

Attendance

+

Attendance

- +

@@ -96,6 +132,9 @@ @@ -104,6 +143,13 @@
+ +
+
+
+
+
+
diff --git a/odex25_base/system_dashboard_classic/static/src/xml/system_dashboard.xml b/odex25_base/system_dashboard_classic/static/src/xml/system_dashboard.xml index a4083dc5b..ce5b4d70a 100644 --- a/odex25_base/system_dashboard_classic/static/src/xml/system_dashboard.xml +++ b/odex25_base/system_dashboard_classic/static/src/xml/system_dashboard.xml @@ -74,6 +74,24 @@

Weekly Timesheet

+ +
+
+

+ + 0 + +

+

+ + 0 + +

+
+

+

Monthly Attendance

+
+
@@ -84,9 +102,7 @@

Attendance

- +

diff --git a/odex25_base/system_dashboard_classic/views/config.xml b/odex25_base/system_dashboard_classic/views/config.xml index f1ba10e3a..48e4ca276 100644 --- a/odex25_base/system_dashboard_classic/views/config.xml +++ b/odex25_base/system_dashboard_classic/views/config.xml @@ -1,84 +1,198 @@ + - Base Dashboard + Dashboard Builder base.dashbord -
+ + + +
+
+ + + + + + + +
+

+ +

+ +
+ + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - -