feat(fusion_planning): new module bridging fusion_clock with Odoo Planning
Adds a 'My Schedule' tab to the Fusion Clock portal that lists the current employee's published planning.slot records, grouped by day. Reuses the fusion_clock dark theme and reuses Odoo Planning's stock backend UI (Gantt, send wizard, recurrence) unchanged. - Controller /my/clock/schedule: pulls published slots in next 60 days - Portal template with next-shift hero card, summary stats, grouped list - Bottom-nav xpath inherits target the nav bar specifically (not the Recent Activity 'View All' link, which also linked to /my/clock/timesheets) - 4-tab nav fits via reduced padding and flex sizing Module depends on stock 'planning' (Enterprise) + fusion_clock. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2
fusion_planning/__init__.py
Normal file
2
fusion_planning/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import controllers
|
||||||
40
fusion_planning/__manifest__.py
Normal file
40
fusion_planning/__manifest__.py
Normal file
@@ -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,
|
||||||
|
}
|
||||||
2
fusion_planning/controllers/__init__.py
Normal file
2
fusion_planning/controllers/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from . import portal_schedule
|
||||||
97
fusion_planning/controllers/portal_schedule.py
Normal file
97
fusion_planning/controllers/portal_schedule.py
Normal file
@@ -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)
|
||||||
115
fusion_planning/static/src/css/portal_schedule.css
Normal file
115
fusion_planning/static/src/css/portal_schedule.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
48
fusion_planning/views/portal_nav_inherit.xml
Normal file
48
fusion_planning/views/portal_nav_inherit.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- Reusable Schedule nav button used by all three inherits -->
|
||||||
|
<template id="schedule_nav_button" name="Fusion Planning Schedule Nav Button">
|
||||||
|
<a href="/my/clock/schedule" class="fclk-nav-item">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
<line x1="8" y1="14" x2="10" y2="14"/>
|
||||||
|
<line x1="14" y1="14" x2="16" y2="14"/>
|
||||||
|
<line x1="8" y1="18" x2="10" y2="18"/>
|
||||||
|
<line x1="14" y1="18" x2="16" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
<span>Schedule</span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Inject "Schedule" between Timesheets and Reports on the Clock page -->
|
||||||
|
<template id="portal_clock_page_inherit_schedule_nav"
|
||||||
|
inherit_id="fusion_clock.portal_clock_page"
|
||||||
|
name="Fusion Planning: Add Schedule tab to Clock nav">
|
||||||
|
<xpath expr="//div[hasclass('fclk-nav-bar')]/a[@href='/my/clock/timesheets']" position="after">
|
||||||
|
<t t-call="fusion_planning.schedule_nav_button"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Inject "Schedule" between Timesheets and Reports on the Timesheets page -->
|
||||||
|
<template id="portal_timesheet_page_inherit_schedule_nav"
|
||||||
|
inherit_id="fusion_clock.portal_timesheet_page"
|
||||||
|
name="Fusion Planning: Add Schedule tab to Timesheets nav">
|
||||||
|
<xpath expr="//div[hasclass('fclk-nav-bar')]/a[@href='/my/clock/timesheets']" position="after">
|
||||||
|
<t t-call="fusion_planning.schedule_nav_button"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Inject "Schedule" between Timesheets and Reports on the Reports page -->
|
||||||
|
<template id="portal_report_page_inherit_schedule_nav"
|
||||||
|
inherit_id="fusion_clock.portal_report_page"
|
||||||
|
name="Fusion Planning: Add Schedule tab to Reports nav">
|
||||||
|
<xpath expr="//div[hasclass('fclk-nav-bar')]/a[@href='/my/clock/timesheets']" position="after">
|
||||||
|
<t t-call="fusion_planning.schedule_nav_button"/>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
165
fusion_planning/views/portal_schedule_templates.xml
Normal file
165
fusion_planning/views/portal_schedule_templates.xml
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<template id="portal_schedule_page" name="Fusion Clock My Schedule">
|
||||||
|
<t t-call="portal.portal_layout">
|
||||||
|
<t t-set="breadcrumbs_searchbar" t-value="False"/>
|
||||||
|
<t t-set="no_breadcrumbs" t-value="True"/>
|
||||||
|
<t t-set="no_header" t-value="True"/>
|
||||||
|
|
||||||
|
<div class="fclk-app">
|
||||||
|
<div class="fclk-container">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="fclk-header">
|
||||||
|
<div class="fclk-date">My Schedule</div>
|
||||||
|
<h1 class="fclk-greeting">Hello, <t t-esc="employee.name.split(' ')[0]"/></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Next Shift Card (if any upcoming) -->
|
||||||
|
<t t-if="next_slot">
|
||||||
|
<div class="fclk-status-card fpl-next-shift">
|
||||||
|
<div class="fpl-next-label">Next Shift</div>
|
||||||
|
<div class="fpl-next-date"><t t-esc="next_slot['date']"/></div>
|
||||||
|
<div class="fpl-next-time"><t t-esc="next_slot['time']"/></div>
|
||||||
|
<div class="fpl-next-role" t-if="next_slot['role']">
|
||||||
|
<t t-esc="next_slot['role']"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="fclk-status-card fpl-empty-card">
|
||||||
|
<div class="fpl-empty-icon">
|
||||||
|
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="1.5">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="fpl-empty-title">No upcoming shifts</div>
|
||||||
|
<div class="fpl-empty-sub">Your manager hasn't published any shifts yet.</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Summary Row -->
|
||||||
|
<div class="fclk-stats-row" t-if="slot_count">
|
||||||
|
<div class="fclk-stat-card">
|
||||||
|
<div class="fclk-stat-header">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
</svg>
|
||||||
|
<span>Upcoming</span>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-stat-value">
|
||||||
|
<t t-esc="slot_count"/>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-stat-target">shifts scheduled</div>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-stat-card">
|
||||||
|
<div class="fclk-stat-header">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12 6 12 12 16 14"/>
|
||||||
|
</svg>
|
||||||
|
<span>Total Hours</span>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-stat-value">
|
||||||
|
<t t-esc="'%.1f' % sum(s['duration_hours'] for items in groups.values() for s in items)"/>h
|
||||||
|
</div>
|
||||||
|
<div class="fclk-stat-target">across upcoming</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grouped Schedule List -->
|
||||||
|
<t t-foreach="groups.items()" t-as="group">
|
||||||
|
<div class="fpl-group">
|
||||||
|
<div class="fpl-group-title">
|
||||||
|
<t t-esc="group[0]"/>
|
||||||
|
</div>
|
||||||
|
<div class="fpl-list">
|
||||||
|
<t t-foreach="group[1]" t-as="item">
|
||||||
|
<div class="fclk-recent-item fpl-shift-item">
|
||||||
|
<div class="fclk-recent-date">
|
||||||
|
<div class="fclk-recent-day-name">
|
||||||
|
<t t-esc="item['day_label']"/>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-recent-day-num">
|
||||||
|
<t t-esc="item['day_num']"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-recent-info">
|
||||||
|
<div class="fclk-recent-location">
|
||||||
|
<t t-if="item['role_name']">
|
||||||
|
<t t-esc="item['role_name']"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
Shift
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-recent-times">
|
||||||
|
<t t-esc="item['time_range']"/>
|
||||||
|
</div>
|
||||||
|
<div class="fpl-shift-note" t-if="item['note']">
|
||||||
|
<t t-esc="item['note']"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fclk-recent-hours">
|
||||||
|
<t t-esc="'%.1f' % item['duration_hours']"/>h
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Navigation Bar -->
|
||||||
|
<div class="fclk-nav-bar">
|
||||||
|
<a href="/my/clock" class="fclk-nav-item">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12 6 12 12 16 14"/>
|
||||||
|
</svg>
|
||||||
|
<span>Clock</span>
|
||||||
|
</a>
|
||||||
|
<a href="/my/clock/timesheets" class="fclk-nav-item">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
</svg>
|
||||||
|
<span>Timesheets</span>
|
||||||
|
</a>
|
||||||
|
<a href="/my/clock/schedule" class="fclk-nav-item fclk-nav-active">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||||
|
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||||
|
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||||
|
<line x1="8" y1="14" x2="10" y2="14"/>
|
||||||
|
<line x1="14" y1="14" x2="16" y2="14"/>
|
||||||
|
<line x1="8" y1="18" x2="10" y2="18"/>
|
||||||
|
<line x1="14" y1="18" x2="16" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
<span>Schedule</span>
|
||||||
|
</a>
|
||||||
|
<a href="/my/clock/reports" class="fclk-nav-item">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||||
|
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
<span>Reports</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user