diff --git a/fusion_planning/__init__.py b/fusion_planning/__init__.py new file mode 100644 index 00000000..153a9e31 --- /dev/null +++ b/fusion_planning/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import controllers diff --git a/fusion_planning/__manifest__.py b/fusion_planning/__manifest__.py new file mode 100644 index 00000000..154bd309 --- /dev/null +++ b/fusion_planning/__manifest__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# Part of the Fusion Clock product family. + +{ + 'name': 'Fusion Planning', + 'version': '19.0.1.0.0', + 'category': 'Human Resources/Planning', + 'summary': 'Fusion Clock bridge to Odoo Planning - employee schedule on the portal', + 'description': """ +Fusion Planning +=============== + +Adds Odoo Planning to the Fusion Clock product family: + +* Adds a "My Schedule" tab to the Fusion Clock portal +* Reuses Odoo Planning's exact backend UI (Gantt, send wizard, recurrence) +* Bridges planning.slot with fusion_clock attendance and leave +""", + 'author': 'Nexa Systems Inc.', + 'website': 'https://nexasystems.io', + 'license': 'OPL-1', + 'depends': [ + 'planning', + 'fusion_clock', + ], + 'data': [ + 'views/portal_schedule_templates.xml', + 'views/portal_nav_inherit.xml', + ], + 'assets': { + 'web.assets_frontend': [ + 'fusion_planning/static/src/css/portal_schedule.css', + ], + }, + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/fusion_planning/controllers/__init__.py b/fusion_planning/controllers/__init__.py new file mode 100644 index 00000000..2305124b --- /dev/null +++ b/fusion_planning/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import portal_schedule diff --git a/fusion_planning/controllers/portal_schedule.py b/fusion_planning/controllers/portal_schedule.py new file mode 100644 index 00000000..84c98eba --- /dev/null +++ b/fusion_planning/controllers/portal_schedule.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) + +import logging +from collections import OrderedDict +from datetime import timedelta + +import pytz + +from odoo import http, fields +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class FusionPlanningPortal(http.Controller): + """Portal controller exposing the employee's published Planning shifts.""" + + @http.route('/my/clock/schedule', type='http', auth='user', website=True) + def portal_schedule(self, **kw): + employee = request.env.user.employee_id + if not employee: + return request.redirect('/my') + + tz_name = employee.tz or request.env.user.tz or 'UTC' + try: + local_tz = pytz.timezone(tz_name) + except pytz.UnknownTimeZoneError: + local_tz = pytz.UTC + + now_utc = fields.Datetime.now() + horizon_utc = now_utc + timedelta(days=60) + + Slot = request.env['planning.slot'].sudo() + domain = [ + ('state', '=', 'published'), + ('end_datetime', '>=', now_utc), + ('start_datetime', '<=', horizon_utc), + ] + if employee.resource_id: + domain.append(('resource_id', '=', employee.resource_id.id)) + else: + domain.append(('resource_id', '=', -1)) + + slots = Slot.search(domain, order='start_datetime asc', limit=200) + + groups = OrderedDict() + today_local = fields.Datetime.context_timestamp( + request.env.user, now_utc + ).date() + for slot in slots: + local_start = pytz.UTC.localize(slot.start_datetime).astimezone(local_tz) + local_end = pytz.UTC.localize(slot.end_datetime).astimezone(local_tz) + day = local_start.date() + delta_days = (day - today_local).days + if delta_days == 0: + bucket_key = 'Today' + elif delta_days == 1: + bucket_key = 'Tomorrow' + elif 0 <= delta_days <= 6: + bucket_key = local_start.strftime('%A') + else: + bucket_key = local_start.strftime('%b %d') + groups.setdefault(bucket_key, []).append({ + 'slot': slot, + 'day_label': local_start.strftime('%a').upper(), + 'day_num': local_start.strftime('%d'), + 'date_full': local_start.strftime('%b %d, %Y'), + 'time_range': '%s - %s' % ( + local_start.strftime('%I:%M %p').lstrip('0'), + local_end.strftime('%I:%M %p').lstrip('0'), + ), + 'duration_hours': round(slot.allocated_hours or 0.0, 1), + 'role_name': slot.role_id.name if slot.role_id else '', + 'role_color': slot.role_id.color if slot.role_id else 0, + 'note': slot.name or '', + }) + + next_slot_data = None + if slots: + next_slot = slots[0] + local_start = pytz.UTC.localize(next_slot.start_datetime).astimezone(local_tz) + next_slot_data = { + 'date': local_start.strftime('%a, %b %d'), + 'time': local_start.strftime('%I:%M %p').lstrip('0'), + 'role': next_slot.role_id.name if next_slot.role_id else '', + } + + values = { + 'employee': employee, + 'groups': groups, + 'slot_count': len(slots), + 'next_slot': next_slot_data, + 'page_name': 'fusion_clock_schedule', + } + return request.render('fusion_planning.portal_schedule_page', values) diff --git a/fusion_planning/static/src/css/portal_schedule.css b/fusion_planning/static/src/css/portal_schedule.css new file mode 100644 index 00000000..08b5d0c7 --- /dev/null +++ b/fusion_planning/static/src/css/portal_schedule.css @@ -0,0 +1,115 @@ +/* Fusion Planning - Portal Schedule + * Inherits Fusion Clock dark-theme tokens (--fclk-card, --fclk-green, etc.) + */ + +/* ---- 4-tab nav fit ---- */ +.fclk-nav-bar { + justify-content: space-around !important; +} + +.fclk-nav-item { + padding: 8px 12px !important; + flex: 1; + max-width: 96px; +} + +/* ---- Next Shift hero card ---- */ +.fpl-next-shift { + text-align: center; + padding: 20px 16px; +} + +.fpl-next-label { + font-size: 11px; + color: var(--fclk-text-dim); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 6px; +} + +.fpl-next-date { + font-size: 18px; + color: var(--fclk-text); + font-weight: 600; + margin-bottom: 4px; +} + +.fpl-next-time { + font-size: 32px; + color: var(--fclk-green); + font-weight: 700; + letter-spacing: 0.02em; + margin-bottom: 6px; +} + +.fpl-next-role { + display: inline-block; + font-size: 12px; + color: var(--fclk-text-dim); + background: rgba(16, 185, 129, 0.08); + border: 1px solid rgba(16, 185, 129, 0.18); + padding: 4px 12px; + border-radius: 12px; +} + +/* ---- Empty state ---- */ +.fpl-empty-card { + text-align: center; + padding: 28px 16px; +} + +.fpl-empty-icon { + margin-bottom: 12px; + opacity: 0.7; +} + +.fpl-empty-title { + font-size: 16px; + color: var(--fclk-text); + font-weight: 600; + margin-bottom: 6px; +} + +.fpl-empty-sub { + font-size: 13px; + color: var(--fclk-text-dim); +} + +/* ---- Group headers ---- */ +.fpl-group { + margin-bottom: 18px; +} + +.fpl-group-title { + font-size: 13px; + color: var(--fclk-text-dim); + text-transform: uppercase; + letter-spacing: 0.08em; + font-weight: 600; + margin: 12px 16px 8px; +} + +.fpl-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +/* ---- Shift item polish ---- */ +.fpl-shift-item .fclk-recent-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.fpl-shift-note { + font-size: 11px; + color: var(--fclk-text-dim); + margin-top: 2px; + font-style: italic; +} + +/* ---- Bottom padding so nav doesn't cover last shift ---- */ +.fclk-container { + padding-bottom: 80px; +} diff --git a/fusion_planning/views/portal_nav_inherit.xml b/fusion_planning/views/portal_nav_inherit.xml new file mode 100644 index 00000000..f24a5917 --- /dev/null +++ b/fusion_planning/views/portal_nav_inherit.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + Schedule + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fusion_planning/views/portal_schedule_templates.xml b/fusion_planning/views/portal_schedule_templates.xml new file mode 100644 index 00000000..da23efed --- /dev/null +++ b/fusion_planning/views/portal_schedule_templates.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + My Schedule + Hello, + + + + + + Next Shift + + + + + + + + + + + + + + + + + + No upcoming shifts + Your manager hasn't published any shifts yet. + + + + + + + + + + + + + Upcoming + + + + + shifts scheduled + + + + + + + + Total Hours + + + h + + across upcoming + + + + + + + + + + + + + + + + + + + + + + + + + + + Shift + + + + + + + + + + + h + + + + + + + + + + + + + + + Clock + + + + + + + + + Timesheets + + + + + + + + + + + + + Schedule + + + + + + + + + Reports + + + + + + + + +