diff --git a/addons/hr_holidays/__manifest__.py b/addons/hr_holidays/__manifest__.py index 905c452cad6..ee552118eab 100644 --- a/addons/hr_holidays/__manifest__.py +++ b/addons/hr_holidays/__manifest__.py @@ -67,12 +67,11 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p ], 'web.assets_backend': [ 'hr_holidays/static/src/js/time_off_calendar/*.js', - 'hr_holidays/static/src/js/radio_image.js', - 'hr_holidays/static/src/js/leave_stats_widget.js', 'hr_holidays/static/src/models/*/*.js', - 'hr_holidays/static/src/scss/time_off.scss', 'hr_holidays/static/src/components/*/*.scss', - 'hr_holidays/static/src/scss/accrual_plan_level.scss', + 'hr_holidays/static/src/dashboard/**/*.js', + 'hr_holidays/static/src/dashboard/**/*.scss', + 'hr_holidays/static/src/leave_stats/**/*.js', ], 'web.tests_assets': [ 'hr_holidays/static/tests/helpers/**/*', @@ -83,7 +82,9 @@ A synchronization with an internal agenda (Meetings of the CRM module) is also p ], 'web.assets_qweb': [ 'hr_holidays/static/src/components/*/*.xml', - 'hr_holidays/static/src/xml/*.xml', + 'hr_holidays/static/src/dashboard/**/*.xml', + 'hr_holidays/static/src/leave_stats/**/*.xml', + 'hr_holidays/static/src/xml/**/*.xml', ], }, 'license': 'LGPL-3', diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.js b/addons/hr_holidays/static/src/dashboard/time_off_card.js new file mode 100644 index 00000000000..5ccbcb2d66c --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_card.js @@ -0,0 +1,16 @@ +/* @odoo-module */ + +export class TimeOffCardPopover extends owl.Component {} + +TimeOffCardPopover.template = 'hr_holidays.TimeOffCardPopover'; +TimeOffCardPopover.props = ['allocated', 'approved', 'planned', 'left']; + +export class TimeOffCard extends owl.Component {} + +TimeOffCard.components = { TimeOffCardPopover }; +TimeOffCard.template = 'hr_holidays.TimeOffCard'; +TimeOffCard.props = ['name', 'id', 'data', 'requires_allocation']; + +export class TimeOffCardMobile extends TimeOffCard {} + +TimeOffCardMobile.template = 'hr_holidays.TimeOffCardMobile'; diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.scss b/addons/hr_holidays/static/src/dashboard/time_off_card.scss new file mode 100644 index 00000000000..cb0a63715a7 --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_card.scss @@ -0,0 +1,73 @@ +.o_timeoff_card { + color: $o-enterprise-color; + flex-direction: column; + flex-grow: 1; + display: flex; + text-align: center; + + &:not(:last-child) { + border-right: 2px solid $border-color; + } + + span { + line-height: 1; + } + + .o_timeoff_name { + color: $body-color; + font-size: 15px; + margin-bottom: 3px; + } + + .o_timeoff_duration { + font-size: 30px; + font-weight: bold; + + img { + height: 30px; + } + } + + .o_timeoff_validity { + font-size: 10px; + } + + .o_timeoff_info { + display: inline-block; + margin-right: -10px; + + span { + cursor: pointer; + padding-right: 10px; + } + } +} + +.o_timeoff_card_mobile { + .o_timeoff_purple { + color: $o-enterprise-color; + } + + .o_timeoff_green { + color: $o-enterprise-primary-color; + } +} + +.o_timeoff_popover { + font-size: 12px; + + ul { + list-style-type: none; + margin-bottom: 0; + padding: 0; + } + + li { + display: flex; + justify-content: space-between; + + span { + padding-left: 3px; + } + } +} diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.xml b/addons/hr_holidays/static/src/dashboard/time_off_card.xml new file mode 100644 index 00000000000..7ace8f2fde5 --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_card.xml @@ -0,0 +1,72 @@ + + +
+ + + + + + + + + + + +
+ hours + days + + available + + + taken + + + + +
+ + + ( hoursdays + valid until ) + + + (valid until ) + + +
+ + + + + + + + / HoursDays Available + + + HoursDays Taken + + + + + +
+ + + +
    +
  • Allocated:
  • +
  • Approved:
  • +
  • Planned:
  • +
  • Left:
  • +
+
+
+
+
+
diff --git a/addons/hr_holidays/static/src/dashboard/time_off_dashboard.js b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.js new file mode 100644 index 00000000000..75db5a2d62b --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.js @@ -0,0 +1,9 @@ +/* @odoo-module */ + +import { TimeOffCard } from './time_off_card'; + +export class TimeOffDashboard extends owl.Component {} + +TimeOffDashboard.components = { TimeOffCard }; +TimeOffDashboard.template = 'hr_holidays.TimeOffDashboard'; +TimeOffDashboard.props = ['holidays']; diff --git a/addons/hr_holidays/static/src/dashboard/time_off_dashboard.scss b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.scss new file mode 100644 index 00000000000..a32cb997a0c --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.scss @@ -0,0 +1,11 @@ +.o_timeoff_dashboard { + display: flex; + justify-content: space-between; + + height: auto; + box-shadow: inset 0 -1px 0 $border-color; + position: sticky; + top: 0; + z-index: 100; + background-color: $o-webclient-background-color; +} diff --git a/addons/hr_holidays/static/src/dashboard/time_off_dashboard.xml b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.xml new file mode 100644 index 00000000000..365ccd5eae0 --- /dev/null +++ b/addons/hr_holidays/static/src/dashboard/time_off_dashboard.xml @@ -0,0 +1,8 @@ + + +
+ + + +
+
diff --git a/addons/hr_holidays/static/src/js/leave_stats_widget.js b/addons/hr_holidays/static/src/js/leave_stats_widget.js deleted file mode 100644 index 2d7f5d648f2..00000000000 --- a/addons/hr_holidays/static/src/js/leave_stats_widget.js +++ /dev/null @@ -1,153 +0,0 @@ -odoo.define('hr_holidays.LeaveStatsWidget', function (require) { - "use strict"; - - var time = require('web.time'); - var Widget = require('web.Widget'); - var widget_registry = require('web.widget_registry'); - - var LeaveStatsWidget = Widget.extend({ - template: 'hr_holidays.leave_stats', - - /** - * @override - * @param {Widget|null} parent - * @param {Object} params - */ - init: function (parent, params) { - this._setState(params); - this._super(parent); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override to fetch data before rendering. - */ - willStart: function () { - return Promise.all([this._super(), this._fetchLeaveTypesData(), this._fetchDepartmentLeaves()]); - }, - - /** - * Fetch new data if needed (according to updated fields) and re-render the widget. - * Called by the basic renderer when the view changes. - * @param {Object} state - * @returns {Promise} - */ - updateState: function (state) { - var self = this; - var to_await = []; - var updatedFields = this._setState(state); - - if (_.intersection(updatedFields, ['employee', 'date']).length) { - to_await.push(this._fetchLeaveTypesData()); - } - if (_.intersection(updatedFields, ['department', 'date']).length) { - to_await.push(this._fetchDepartmentLeaves()); - } - return Promise.all(to_await).then(function () { - self.renderElement(); - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Update the state - * @param {Object} state - * @returns {String[]} list of updated fields - */ - _setState: function (state) { - var updatedFields = []; - if (state.data.employee_id.res_id !== (this.employee && this.employee.res_id)) { - updatedFields.push('employee'); - this.employee = state.data.employee_id; - } - if (state.data.department_id.res_id !== (this.department && this.department.res_id)) { - updatedFields.push('department'); - this.department = state.data.department_id; - } - if (state.data.date_from !== this.date) { - updatedFields.push('date'); - this.date = state.data.date_from; - } - return updatedFields; - }, - - /** - * Fetch leaves taken by members of ``this.department`` in the - * month of ``this.date``. - * Three fields are fetched for each leave, namely: employee_id, date_from - * and date_to. - * The resulting data is assigned to ``this.departmentLeaves`` - * @private - * @returns {Promise} - */ - _fetchDepartmentLeaves: function () { - if (!this.date || !this.department) { - this.departmentLeaves = null; - return Promise.resolve(); - } - var self = this; - var month_date_from = this.date.clone().startOf('month'); - var month_date_to = this.date.clone().endOf('month'); - return this._rpc({ - model: 'hr.leave', - method: 'search_read', - args: [ - [['department_id', '=', this.department.res_id], - ['state', '=', 'validate'], - ['holiday_type', '=', 'employee'], - ['date_from', '<=', month_date_to], - ['date_to', '>=', month_date_from]], - ['employee_id', 'date_from', 'date_to', 'number_of_days'], - ], - }).then(function (data) { - var dateFormat = time.getLangDateFormat(); - self.departmentLeaves = data.map(function (leave) { - // Format datetimes to date (in the user's format) - return _.extend(leave, { - date_from: moment(leave.date_from).format(dateFormat), - date_to: moment(leave.date_to).format(dateFormat), - number_of_days: leave.number_of_days, - }); - }); - }); - }, - - /** - * Fetch the number of leaves, grouped by leave type, taken by ``this.employee`` - * in the year of ``this.date``. - * The resulting data is assigned to ``this.leavesPerType`` - * @private - * @returns {Promise} - */ - _fetchLeaveTypesData: function () { - if (!this.date || !this.employee) { - this.leavesPerType = null; - return Promise.resolve(); - } - var self = this; - var year_date_from = this.date.clone().startOf('year'); - var year_date_to = this.date.clone().endOf('year'); - return this._rpc({ - model: 'hr.leave', - method: 'read_group', - kwargs: { - domain: [['employee_id', '=', this.employee.res_id], ['state', '=', 'validate'], ['date_from', '<=', year_date_to], ['date_to', '>=', year_date_from]], - fields: ['holiday_status_id', 'number_of_days:sum'], - groupby: ['holiday_status_id'], - }, - }).then(function (data) { - self.leavesPerType = data; - }); - } - }); - - widget_registry.add('hr_leave_stats', LeaveStatsWidget); - - return LeaveStatsWidget; -}); diff --git a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_renderer.js b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_renderer.js index 4060a1a7490..31bfd3adddf 100644 --- a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_renderer.js +++ b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_calendar_renderer.js @@ -1,58 +1,69 @@ /** @odoo-module **/ -import { qweb as QWeb } from 'web.core'; -import config from 'web.config'; - +import { ComponentWrapper } from 'web.OwlCompatibility'; import { TimeOffPopoverRenderer } from "./time_off_popover_renderer"; +import { TimeOffDashboard } from '@hr_holidays/dashboard/time_off_dashboard'; +import { TimeOffCardMobile } from '@hr_holidays/dashboard/time_off_card'; + +import config from 'web.config'; +import session from 'web.session'; + export const TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({ - async _render () { + async willStart() { await this._super(...arguments); - const result = await this._rpc({ - model: 'hr.leave.type', - method: 'get_days_all_request', - context: this.state.context, - }); - this.$el.parent().find('.o_calendar_mini').hide(); - this.$el.parent().find('.o_timeoff_container').remove(); + this.holidays = await this.loadDashboardData(); + }, - // Do not display header if there is no element to display - if (result.length > 0) { - if (config.device.isMobile) { - result.forEach((data) => { - const elem = QWeb.render('hr_holidays.dashboard_calendar_header_mobile', { - timeoff: data, - }); - this.$el.find('.o_calendar_filter_item[data-value=' + data[3] + '] .o_cw_filter_title').append(elem); - }); - } else { - const elem = QWeb.render('hr_holidays.dashboard_calendar_header', { - timeoffs: result, - }); - this.$el.before(elem); + async start() { + await this._super(...arguments); - //add popover to the information tags - [...this.$el.parent().find('.fa-question-circle-o')].forEach((popup) => { - $(popup).popover({ - trigger: 'hover', - html: true, - delay: {show: 300, hide: 0}, - content() { - const data = { - allocated: popup.dataset.allocated, - approved: popup.dataset.approved, - planned: popup.dataset.planned, - left: popup.dataset.left - }; - const elem_popover = QWeb.render('hr_holidays.dashboard_calendar_header_leave_type_popover', { - data: data, - }); - return elem_popover - }, - }); - }); - } + this.TimeOffDashboardWrapper = null; + + if (!config.device.isMobile) { + this.TimeOffDashboardWrapper = new ComponentWrapper(this, TimeOffDashboard, { holidays: this.holidays }); + await this.TimeOffDashboardWrapper.mount(this.el.parentElement, { position: 'first-child' }); } }, + + async _render() { + await this._super(...arguments); + + if (this.TimeOffDashboardWrapper) { + this.holidays = await this.loadDashboardData(); + this.TimeOffDashboardWrapper.update({ holidays: this.holidays }); + } + }, + + async loadDashboardData() { + return await this._rpc({ + model: 'hr.leave.type', + method: 'get_days_all_request', + context: session.user_context, + }); + }, + + async _renderFilters() { + await this._super(...arguments); + + if (!config.device.isMobile) { + return; + } + + this.holidays = await this.loadDashboardData(); + for (const holiday of this.holidays) { + const node = this.el.querySelector(`.o_calendar_filter_item[data-value="${holiday[3]}"] .o_cw_filter_title`); + + if (node) { + const mobileCard = new ComponentWrapper(this, TimeOffCardMobile, { + name: holiday[0], + data: holiday[1], + requires_allocation: holiday[2] === 'yes', + id: holiday[3] + }); + await mobileCard.mount(node); + } + } + } }); diff --git a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_popover_renderer.js b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_popover_renderer.js index 1465ad4409d..12115b6aeae 100644 --- a/addons/hr_holidays/static/src/js/time_off_calendar/time_off_popover_renderer.js +++ b/addons/hr_holidays/static/src/js/time_off_calendar/time_off_popover_renderer.js @@ -5,7 +5,9 @@ import config from 'web.config'; import CalendarRenderer from "web.CalendarRenderer"; import { TimeOffCalendarPopover } from "./time_off_calendar_popover"; -export const TimeOffPopoverRenderer = CalendarRenderer.extend({ +import { WidgetAdapterMixin } from 'web.OwlCompatibility'; + +export const TimeOffPopoverRenderer = CalendarRenderer.extend(WidgetAdapterMixin, { template: "TimeOff.CalendarView.extend", /** * We're overriding this to display the weeknumbers on the year view @@ -67,4 +69,22 @@ export const TimeOffPopoverRenderer = CalendarRenderer.extend({ } }); }, + + /* + * OWL Compatibility layer + */ + on_attach_callback() { + this._super(...arguments); + WidgetAdapterMixin.on_attach_callback.call(this); + }, + + on_detach_callback() { + this._super(...arguments); + WidgetAdapterMixin.on_detach_callback.call(this); + }, + + destroy() { + this._super(...arguments); + WidgetAdapterMixin.destroy.call(this); + }, }); diff --git a/addons/hr_holidays/static/src/leave_stats/leave_stats.js b/addons/hr_holidays/static/src/leave_stats/leave_stats.js new file mode 100644 index 00000000000..c67944daecf --- /dev/null +++ b/addons/hr_holidays/static/src/leave_stats/leave_stats.js @@ -0,0 +1,120 @@ +/* @odoo-module */ + +import { useService } from '@web/core/utils/hooks'; +import { getLangDateFormat } from 'web.time'; +import widgetRegistry from 'web.widgetRegistry'; + +const { Component, useState } = owl; + +export class LeaveStatsComponent extends Component { + setup() { + super.setup(...arguments); + this.rpc = useService('rpc'); + + this.state = useState({ + leaves: [], + departmentLeaves: [], + }); + } + + async willStart() { + await this.loadLeaves(this.date, this.employee); + await this.loadDepartmentLeaves(this.date, this.department, this.employee); + } + + async willUpdateProps(nextProps) { + const dateFrom = nextProps.record.data.date_from || moment(); + const dateChanged = this.date !== dateFrom; + const employee = nextProps.record.data.employee_id.data; + const department = nextProps.record.data.department_id.data; + + if (dateChanged || employee && (this.employee && this.employee.id) !== employee.id) { + await this.loadLeaves(dateFrom, employee); + } + + if (dateChanged || department && (this.department && this.department.id) !== department.id) { + await this.loadDepartmentLeaves(dateFrom, department, employee); + } + } + + get date() { + return this.props.record.data.date_from || moment(); + } + + get department() { + return this.props.record.data.department_id.data; + } + + get employee() { + return this.props.record.data.employee_id.data; + } + + get thisYear() { + return this.date.format('YYYY'); + } + + get thisMonth() { + return this.date.format('MMMM'); + } + + async loadDepartmentLeaves(date, department, employee) { + if (!(department && employee && date)) { + this.state.departmentLeaves = []; + return; + } + + const dateFrom = date.clone().startOf('month'); + const dateTo = date.clone().endOf('month'); + const dateFormat = getLangDateFormat(); + + const departmentLeaves = await this.rpc({ + model: 'hr.leave', + method: 'search_read', + args: [ + [ + ['department_id', '=', department.id], + ['state', '=', 'validate'], + ['holiday_type', '=', 'employee'], + ['date_from', '<=', dateTo], + ['date_to', '>=', dateFrom], + ], + ['employee_id', 'date_from', 'date_to', 'number_of_days'], + ] + }); + + this.state.departmentLeaves = departmentLeaves.map((leave) => { + return Object.assign({}, leave, { + dateFrom: moment(leave.date_from).format(dateFormat), + dateTo: moment(leave.date_to).format(dateFormat), + sameEmployee: leave.employee_id[0] === employee.id, + }); + }); + } + + async loadLeaves(date, employee) { + if (!(employee && date)) { + this.state.leaves = []; + return; + } + + const dateFrom = date.clone().startOf('year'); + const dateTo = date.clone().endOf('year'); + this.state.leaves = await this.rpc({ + model: 'hr.leave', + method: 'read_group', + kwargs: { + domain: [ + ['employee_id', '=', employee.id], + ['state', '=', 'validate'], + ['date_from', '<=', dateTo], + ['date_to', '>=', dateFrom] + ], + fields: ['holiday_status_id', 'number_of_days:sum'], + groupby: ['holiday_status_id'], + } + }); + } +} + +LeaveStatsComponent.template = 'hr_holidays.LeaveStatsComponent'; +widgetRegistry.add('hr_leave_stats', LeaveStatsComponent); diff --git a/addons/hr_holidays/static/src/leave_stats/leave_stats.xml b/addons/hr_holidays/static/src/leave_stats/leave_stats.xml new file mode 100644 index 00000000000..d0a7e65c400 --- /dev/null +++ b/addons/hr_holidays/static/src/leave_stats/leave_stats.xml @@ -0,0 +1,30 @@ + + +
+
+
+ in +
+
+ None +
+
+ + day(s) +
+
+ +
+
+ in +
+
+ None +
+
+ : day(s) + - +
+
+
+
diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss index ae63cf73347..00ce77d84c8 100644 --- a/addons/hr_holidays/static/src/scss/time_off.scss +++ b/addons/hr_holidays/static/src/scss/time_off.scss @@ -1,21 +1,4 @@ .o_timeoff_calendar .o_content { - .o_timeoff_container { - height: auto; - box-shadow: inset 0 -1px 0 $border-color; - position: sticky; - top: 0; - z-index: 100; - background-color: $o-webclient-background-color; - } - - .o_timeoff_card { - border-right: $border-color solid 2px; - text-align: center; - div { - line-height: 1; - } - } - .fc-dayGridYear-view { .fc-bgevent { border-radius: 25px; @@ -38,30 +21,6 @@ } } - .o_timeoff_card_last { - border-right: 0px; - } - - .o_timeoff_normal { - font-size: 10px; - } - - .o_timeoff_big { - font-size: 15px; - } - - .o_timeoff_huge { - font-size: 30px; - } - - .o_timeoff_purple { - color: $o-enterprise-color; - } - - .o_timeoff_green { - color: $o-enterprise-primary-color; - } - .o_calendar_filter { > h5 { font-weight: bold; @@ -86,17 +45,3 @@ } } } - -.o_timeoff_dashboard_popover{ - - hr{ - margin-top: 0px; - margin-bottom: 0px - } - - ul{ - list-style: none; - padding-left: 0; - margin:0px; - } -} diff --git a/addons/hr_holidays/static/src/xml/leave_stats_templates.xml b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml deleted file mode 100644 index 237ffba9e91..00000000000 --- a/addons/hr_holidays/static/src/xml/leave_stats_templates.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - -
-
in
-
None
day(s)
-
- - - - - - - - - - - - - - - - - - - - - -
-
in
-
None
: day(s) -
-
- -
- - - - - - - - -
- -
diff --git a/addons/hr_holidays/static/src/xml/time_off_calendar.xml b/addons/hr_holidays/static/src/xml/time_off_calendar.xml index 940682be93d..a74daf71925 100644 --- a/addons/hr_holidays/static/src/xml/time_off_calendar.xml +++ b/addons/hr_holidays/static/src/xml/time_off_calendar.xml @@ -1,72 +1,6 @@ - -
-
- - - - - - -
- -
- HOURSDAYS AVAILABLE - -
- -
- HOURSDAYS TAKEN -
-
- - - - - (VALID UNTIL ) - - - - - ( HOURSDAYS - VALID UNTIL ) - - - - -
-
-
- - -
-
    -
  • Allocated:
  • -
  • Approved:
  • -
  • Planned:
  • -
    -
  • Left:
  • -
-
-
- - - - - - - / - HoursDaysAvailable - - - HoursDaysTaken - - - - -