diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_holidays/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_holidays/static/src')
16 files changed, 832 insertions, 0 deletions
diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.js b/addons/hr_holidays/static/src/bugfix/bugfix.js new file mode 100644 index 00000000..41d3e558 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.js @@ -0,0 +1,88 @@ +/** + * This file allows introducing new JS modules without contaminating other files. + * This is useful when bug fixing requires adding such JS modules in stable + * versions of Odoo. Any module that is defined in this file should be isolated + * in its own file in master. + */ +odoo.define('hr_holidays/static/src/bugfix/bugfix.js', function (require) { +'use strict'; + +}); + +// FIXME move me in hr_holidays/static/src/models/partner/partner.js +odoo.define('hr_holidays/static/src/models/partner/partner.js', function (require) { +'use strict'; + +const { + registerClassPatchModel, + registerFieldPatchModel, + registerInstancePatchModel, +} = require('mail/static/src/model/model_core.js'); +const { attr, one2one } = require('mail/static/src/model/model_field.js'); +const { clear } = require('mail/static/src/model/model_field_command.js'); + +const { str_to_datetime } = require('web.time'); + +registerClassPatchModel('mail.partner', 'hr_holidays/static/src/models/partner/partner.js', { + /** + * @override + */ + convertData(data) { + const data2 = this._super(data); + if ('out_of_office_date_end' in data) { + data2.outOfOfficeDateEnd = data.out_of_office_date_end ? data.out_of_office_date_end : clear(); + } + return data2; + }, +}); + +registerInstancePatchModel('mail.partner', 'hr_holidays/static/src/models/partner/partner.js', { + /** + * @private + */ + _computeOutOfOfficeText() { + if (!this.outOfOfficeDateEnd) { + return clear(); + } + if (!this.env.messaging.locale.language) { + return clear(); + } + const currentDate = new Date(); + const date = str_to_datetime(this.outOfOfficeDateEnd); + const options = { day: 'numeric', month: 'short' }; + if (currentDate.getFullYear() !== date.getFullYear()) { + options.year = 'numeric'; + } + const localeCode = this.env.messaging.locale.language.replace(/_/g,'-'); + const formattedDate = date.toLocaleDateString(localeCode, options); + return _.str.sprintf(this.env._t("Out of office until %s"), formattedDate); + }, + +}); + +registerFieldPatchModel('mail.partner', 'hr/static/src/models/partner/partner.js', { + /** + * Serves as compute dependency. + */ + locale: one2one('mail.locale', { + related: 'messaging.locale', + }), + /** + * Date of end of the out of office period of the partner as string. + * String is expected to use Odoo's datetime string format + * (examples: '2011-12-01 15:12:35.832' or '2011-12-01 15:12:35'). + */ + outOfOfficeDateEnd: attr(), + /** + * Text shown when partner is out of office. + */ + outOfOfficeText: attr({ + compute: '_computeOutOfOfficeText', + dependencies: [ + 'locale', + 'outOfOfficeDateEnd', + ], + }), +}); + +}); diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.scss b/addons/hr_holidays/static/src/bugfix/bugfix.scss new file mode 100644 index 00000000..c4272e52 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.scss @@ -0,0 +1,6 @@ +/** +* This file allows introducing new styles without contaminating other files. +* This is useful when bug fixing requires adding new components for instance in +* stable versions of Odoo. Any style that is defined in this file should be isolated +* in its own file in master. +*/ diff --git a/addons/hr_holidays/static/src/bugfix/bugfix.xml b/addons/hr_holidays/static/src/bugfix/bugfix.xml new file mode 100644 index 00000000..c17906f7 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + +<!-- + This file allows introducing new static templates without contaminating other files. + This is useful when bug fixing requires adding new components for instance in stable + versions of Odoo. Any template that is defined in this file should be isolated + in its own file in master. +--> + +</templates> diff --git a/addons/hr_holidays/static/src/bugfix/bugfix_tests.js b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js new file mode 100644 index 00000000..6f655603 --- /dev/null +++ b/addons/hr_holidays/static/src/bugfix/bugfix_tests.js @@ -0,0 +1,110 @@ +odoo.define('hr_holidays/static/src/bugfix/bugfix_tests.js', function (require) { +'use strict'; + +/** + * This file allows introducing new QUnit test modules without contaminating + * other test files. This is useful when bug fixing requires adding new + * components for instance in stable versions of Odoo. Any test that is defined + * in this file should be isolated in its own file in master. + */ +QUnit.module('hr_holidays', {}, function () { +QUnit.module('bugfix', {}, function () { +QUnit.module('bugfix_tests.js', { + +}); +}); +}); + +}); + +// FIXME move me in hr_holidays/static/src/components/thread_view/thread_view_tests.js +odoo.define('hr_holidays/static/src/components/thread_view/thread_view_tests.js', function (require) { +'use strict'; + +const components = { + ThreadView: require('mail/static/src/components/thread_view/thread_view.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + start, +} = require('mail/static/src/utils/test_utils.js'); + +QUnit.module('hr_holidays', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('thread_view', {}, function () { +QUnit.module('thread_view_tests.js', { + beforeEach() { + beforeEach(this); + + /** + * @param {mail.thread_view} threadView + * @param {Object} [otherProps={}] + */ + this.createThreadViewComponent = async (threadView, otherProps = {}) => { + const target = this.widget.el; + const props = Object.assign({ threadViewLocalId: threadView.localId }, otherProps); + await createRootComponent(this, components.ThreadView, { props, target }); + }; + + this.start = async params => { + const { afterEvent, env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.afterEvent = afterEvent; + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('out of office message on direct chat with out of office partner', async function (assert) { + assert.expect(2); + + // Returning date of the out of office partner, simulates he'll be back in a month + const returningDate = moment.utc().add(1, 'month'); + // Needed partner & user to allow simulation of message reception + this.data['res.partner'].records.push({ + id: 11, + name: "Foreigner partner", + out_of_office_date_end: returningDate.format("YYYY-MM-DD HH:mm:ss"), + }); + this.data['mail.channel'].records = [{ + channel_type: 'chat', + id: 20, + members: [this.data.currentPartnerId, 11], + }]; + await this.start(); + const thread = this.env.models['mail.thread'].findFromIdentifyingData({ + id: 20, + model: 'mail.channel' + }); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['link', thread]], + }); + await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true }); + assert.containsOnce( + document.body, + '.o_ThreadView_outOfOffice', + "should have an out of office alert on thread view" + ); + const formattedDate = returningDate.toDate().toLocaleDateString( + this.env.messaging.locale.language.replace(/_/g,'-'), + { day: 'numeric', month: 'short' } + ); + assert.ok( + document.querySelector('.o_ThreadView_outOfOffice').textContent.includes(formattedDate), + "out of office message should mention the returning date" + ); +}); + +}); +}); +}); + +}); diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss new file mode 100644 index 00000000..e981e30f --- /dev/null +++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.scss @@ -0,0 +1,12 @@ +// ----------------------------------------------------------------------------- +// Style +// ----------------------------------------------------------------------------- + +.o_PartnerImStatusIcon_icon { + &.o-leave-online { + color: $o-enterprise-primary-color; + } + &.o-leave-offline { + color: theme-color('warning'); + } +} diff --git a/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml new file mode 100644 index 00000000..c07377d3 --- /dev/null +++ b/addons/hr_holidays/static/src/components/partner_im_status_icon/partner_im_status_icon.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-inherit="mail.PartnerImStatusIcon" t-inherit-mode="extension"> + <xpath expr="//*[@name='rootCondition']" position="inside"> + <t t-if="partner.im_status === 'leave_online'"> + <i class="o_PartnerImStatusIcon_icon o-leave-online fa fa-plane fa-stack-1x" title="Online" role="img" aria-label="User is online"/> + </t> + <t t-if="partner.im_status === 'leave_offline'"> + <i class="o_PartnerImStatusIcon_icon o-leave-offline fa fa-plane fa-stack-1x" title="Out of office" role="img" aria-label="User is out of office"/> + </t> + </xpath> + </t> +</templates> diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss new file mode 100644 index 00000000..4caabb3b --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.scss @@ -0,0 +1,11 @@ +// ------------------------------------------------------------------ +// Style +// ------------------------------------------------------------------ + +.o_ThreadIcon_leaveOnline { + color: $o-enterprise-primary-color; +} + +.o_ThreadIcon_leaveOffline { + color: theme-color('warning'); +} diff --git a/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml new file mode 100644 index 00000000..f8a728a9 --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_icon/thread_icon.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-inherit="mail.ThreadIcon" t-inherit-mode="extension"> + <xpath expr="//*[@name='noImStatusCondition']" position="before"> + <t t-elif="thread.correspondent.im_status === 'leave_online'"> + <div class="o_ThreadIcon_leaveOnline fa fa-plane" title="Online"/> + </t> + <t t-elif="thread.correspondent.im_status === 'leave_offline'"> + <div class="o_ThreadIcon_leaveOffline fa fa-plane" title="Out of office"/> + </t> + </xpath> + </t> +</templates> diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.js b/addons/hr_holidays/static/src/components/thread_view/thread_view.js new file mode 100644 index 00000000..73ca28d4 --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.js @@ -0,0 +1,28 @@ +odoo.define('hr_holidays/static/src/components/thread_view/thread_view.js', function (require) { +'use strict'; + +const components = { + ThreadView: require('mail/static/src/components/thread_view/thread_view.js'), +}; + +const { patch } = require('web.utils'); + +patch(components.ThreadView, 'hr_holidays/static/src/components/thread_view/thread_view.js', { + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _useStoreSelector(props) { + const res = this._super(...arguments); + const thread = res.thread; + const correspondent = thread && thread.correspondent; + return Object.assign({}, res, { + correspondentOutOfOfficeText: correspondent && correspondent.outOfOfficeText, + }); + }, +}); + +}); diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.scss b/addons/hr_holidays/static/src/components/thread_view/thread_view.scss new file mode 100644 index 00000000..9cecd24e --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.scss @@ -0,0 +1,8 @@ +// ----------------------------------------------------------------------------- +// Layout +// ----------------------------------------------------------------------------- + +.o_ThreadView_outOfOffice { + margin-top: 0; + margin-bottom: 0; +} diff --git a/addons/hr_holidays/static/src/components/thread_view/thread_view.xml b/addons/hr_holidays/static/src/components/thread_view/thread_view.xml new file mode 100644 index 00000000..796dcfba --- /dev/null +++ b/addons/hr_holidays/static/src/components/thread_view/thread_view.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + <t t-inherit="mail.ThreadView" t-inherit-mode="extension"> + <xpath expr="//*[@name='loadingCondition']" position="before"> + <t t-if="threadView.thread.correspondent and threadView.thread.correspondent.outOfOfficeText"> + <div class="o_ThreadView_outOfOffice alert alert-primary" t-esc="threadView.thread.correspondent.outOfOfficeText" role="alert"/> + </t> + </xpath> + </t> +</templates> diff --git a/addons/hr_holidays/static/src/js/leave_stats_widget.js b/addons/hr_holidays/static/src/js/leave_stats_widget.js new file mode 100644 index 00000000..2d7f5d64 --- /dev/null +++ b/addons/hr_holidays/static/src/js/leave_stats_widget.js @@ -0,0 +1,153 @@ +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.js b/addons/hr_holidays/static/src/js/time_off_calendar.js new file mode 100644 index 00000000..ef9f262d --- /dev/null +++ b/addons/hr_holidays/static/src/js/time_off_calendar.js @@ -0,0 +1,182 @@ +odoo.define('hr_holidays.dashboard.view_custo', function(require) { + 'use strict'; + + var core = require('web.core'); + var CalendarPopover = require('web.CalendarPopover'); + var CalendarController = require("web.CalendarController"); + var CalendarRenderer = require("web.CalendarRenderer"); + var CalendarView = require("web.CalendarView"); + var viewRegistry = require('web.view_registry'); + + var _t = core._t; + var QWeb = core.qweb; + + var TimeOffCalendarPopover = CalendarPopover.extend({ + template: 'hr_holidays.calendar.popover', + + init: function (parent, eventInfo) { + this._super.apply(this, arguments); + const state = this.event.extendedProps.record.state; + this.canDelete = state && ['validate', 'refuse'].indexOf(state) === -1; + this.canEdit = state !== undefined; + this.displayFields = []; + + if (this.modelName === "hr.leave.report.calendar") { + const duration = this.event.extendedProps.record.display_name.split(':').slice(-1); + this.display_name = _.str.sprintf(_t("Time Off : %s"), duration); + } else { + this.display_name = this.event.extendedProps.record.display_name; + } + }, + }); + + var TimeOffCalendarController = CalendarController.extend({ + events: _.extend({}, CalendarController.prototype.events, { + 'click .btn-time-off': '_onNewTimeOff', + 'click .btn-allocation': '_onNewAllocation', + }), + + /** + * @override + */ + start: function () { + this.$el.addClass('o_timeoff_calendar'); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Render the buttons and add new button about + * time off and allocations request + * + * @override + */ + + renderButtons: function ($node) { + this._super.apply(this, arguments); + + $(QWeb.render('hr_holidays.dashboard.calendar.button', { + time_off: _t('New Time Off'), + request: _t('New Allocation'), + })).appendTo(this.$buttons); + + if ($node) { + this.$buttons.appendTo($node); + } else { + this.$('.o_calendar_buttons').replaceWith(this.$buttons); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Action: create a new time off request + * + * @private + */ + _onNewTimeOff: function () { + var self = this; + + this.do_action('hr_holidays.hr_leave_action_my_request', { + on_close: function () { + self.reload(); + } + }); + }, + + /** + * Action: create a new allocation request + * + * @private + */ + _onNewAllocation: function () { + var self = this; + this.do_action({ + type: 'ir.actions.act_window', + res_model: 'hr.leave.allocation', + name: 'New Allocation Request', + views: [[false,'form']], + context: {'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard'}, + target: 'new', + }, { + on_close: function () { + self.reload(); + } + }); + }, + }); + + var TimeOffPopoverRenderer = CalendarRenderer.extend({ + config: _.extend({}, CalendarRenderer.prototype.config, { + CalendarPopover: TimeOffCalendarPopover, + }), + + _getPopoverParams: function (eventData) { + let params = this._super.apply(this, arguments); + let calendarIcon; + let state = eventData.extendedProps.record.state; + + if (state === 'validate') { + calendarIcon = 'fa-calendar-check-o'; + } else if (state === 'refuse') { + calendarIcon = 'fa-calendar-times-o'; + } else if(state) { + calendarIcon = 'fa-calendar-o'; + } + + params['title'] = eventData.extendedProps.record.display_name.split(':').slice(0, -1).join(':'); + params['template'] = QWeb.render('hr_holidays.calendar.popover.placeholder', {color: this.getColor(eventData.color_index), calendarIcon: calendarIcon}); + return params; + }, + + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.$el.parent().find('.o_calendar_mini').hide(); + }); + }, + }); + + var TimeOffCalendarRenderer = TimeOffPopoverRenderer.extend({ + _render: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + return self._rpc({ + model: 'hr.leave.type', + method: 'get_days_all_request', + context: self.context, + }); + }).then(function (result) { + self.$el.parent().find('.o_calendar_mini').hide(); + self.$el.parent().find('.o_timeoff_container').remove(); + var elem = QWeb.render('hr_holidays.dashboard_calendar_header', { + timeoffs: result, + }); + self.$el.before(elem); + }); + }, + }); + var TimeOffCalendarView = CalendarView.extend({ + config: _.extend({}, CalendarView.prototype.config, { + Controller: TimeOffCalendarController, + Renderer: TimeOffCalendarRenderer, + }), + }); + + /** + * Calendar shown in the "Everyone" menu + */ + var TimeOffCalendarAllView = CalendarView.extend({ + config: _.extend({}, CalendarView.prototype.config, { + Renderer: TimeOffPopoverRenderer, + }), + }); + + viewRegistry.add('time_off_calendar', TimeOffCalendarView); + viewRegistry.add('time_off_calendar_all', TimeOffCalendarAllView); +}); diff --git a/addons/hr_holidays/static/src/scss/time_off.scss b/addons/hr_holidays/static/src/scss/time_off.scss new file mode 100644 index 00000000..f37b89ca --- /dev/null +++ b/addons/hr_holidays/static/src/scss/time_off.scss @@ -0,0 +1,44 @@ +.o_timeoff_calendar .o_content { + .o_timeoff_container { + height: 6rem; + } + + .o_calendar_container { + height: calc(100% - 6rem); + } + + @include media-breakpoint-down(sm) { + .o_timeoff_container { + height: 15%; + } + + .o_calendar_container { + height: 85%; + } + } + + .o_timeoff_card { + border-right: #adb5bd solid 2px; + text-align: center; + margin-top: 4px; + div { + line-height: 1; + } + } + + .o_timeoff_card_last { + border-right: 0px; + } + + .o_timeoff_big { + font-size: 20px; + } + + .o_timeoff_purple { + color: $o-enterprise-color; + } + + .o_timeoff_green { + color: $o-enterprise-primary-color; + } +} diff --git a/addons/hr_holidays/static/src/xml/leave_stats_templates.xml b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml new file mode 100644 index 00000000..237ffba9 --- /dev/null +++ b/addons/hr_holidays/static/src/xml/leave_stats_templates.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates id="template" xml:space="preserve"> + <t t-name="hr_holidays.leave_per_type"> + <table class="o_group o_inner_group table-striped"> + <thead> + <tr> + <td colspan="2"> + <div class="o_horizontal_separator"><t t-esc="widget.employee.data.display_name"/> in <t t-esc="widget.date.format('YYYY')"/></div> + </td> + </tr> + </thead> + <tbody> + <t t-if="widget.leavesPerType.length === 0"> + <tr> + <td>None</td> + </tr> + </t> + <t t-foreach="widget.leavesPerType" t-as="leave_type"> + <tr> + <td><t t-esc="leave_type.holiday_status_id[1]"/></td> + <td class="w-50"><t t-esc="leave_type.number_of_days"/> day(s)</td> + </tr> + </t> + </tbody> + </table> + </t> + + <t t-name="hr_holidays.department_leave"> + <table class="o_group o_inner_group table-striped"> + <thead> + <tr> + <td colspan="2"> + <div class="o_horizontal_separator"><t t-esc="widget.department.data.display_name"/> in <t t-esc="widget.date.format('MMMM')"/></div> + </td> + </tr> + </thead> + <tbody> + <t t-if="widget.departmentLeaves.length === 0"> + <tr> + <td>None</td> + </tr> + </t> + <t t-foreach="widget.departmentLeaves" t-as="leave"> + <tr t-attf-class="{{leave.employee_id[0] === widget.employee.res_id ? 'font-weight-bold' : ''}}"> + <td><t t-esc="leave.employee_id[1]"/>: <t t-esc="leave.number_of_days"/> day(s) </td> + <td class="w-50"><t t-esc="leave.date_from"/> - <t t-esc="leave.date_to"/></td> + </tr> + </t> + </tbody> + </table> + </t> + + <div t-name="hr_holidays.leave_stats" class="o_leave_stats"> + <t t-if="widget.employee"> + <t t-if="widget.leavesPerType"> + <t t-call="hr_holidays.leave_per_type"/> + </t> + <t t-if="widget.departmentLeaves"> + <t t-call="hr_holidays.department_leave"/> + </t> + </t> + </div> + +</templates> diff --git a/addons/hr_holidays/static/src/xml/time_off_calendar.xml b/addons/hr_holidays/static/src/xml/time_off_calendar.xml new file mode 100644 index 00000000..b041222d --- /dev/null +++ b/addons/hr_holidays/static/src/xml/time_off_calendar.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates id="template" xml:space="preserve"> + <t t-name="hr_holidays.dashboard_calendar_header"> + <div class="o_timeoff_container d-flex"> + <div t-foreach="timeoffs" t-as="timeoff" t-attf-class="o_timeoff_card flex-grow-1 d-flex flex-column {{ timeoff_last ? 'o_timeoff_card_last' : '' }}"> + <t t-set="need_allocation" t-value="timeoff[2] !== 'no'"/> + <t t-set="cl" t-value="'text-muted'"/> + + <t t-if="need_allocation && timeoff[1]['virtual_remaining_leaves'] > 0"> + <t t-set="cl" t-value="'o_timeoff_green'"/> + </t> + + <div class="mt-2"> + <t t-if="need_allocation"> + <span t-esc="timeoff[1]['virtual_remaining_leaves']" class="o_timeoff_big o_timeoff_purple"/> / <span t-esc="timeoff[1]['max_leaves']"/> <t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t> + </t> + <t t-else=""> + <span t-esc="timeoff[1]['virtual_leaves_taken']" class="o_timeoff_big o_timeoff_purple"/> <t t-if="timeoff[1]['request_unit'] == 'hour'">Hours</t><t t-else="">Days</t> + </t> + </div> + + <b><span t-esc="timeoff[0]" class="o_timeoff_name"/></b> + + <span class="mb-4" t-if="need_allocation"> + <span t-attf-class="mr-1 font-weight-bold {{ cl }}" t-esc="timeoff[1]['virtual_leaves_taken']"/><span>taken</span> + <t t-if="timeoff[3]"> (Expire on <span t-esc="moment(timeoff[3]).format('L')"/>)</t> + </span> + </div> + </div> + </t> + + <t t-name="hr_holidays.dashboard.calendar.button"> + <button class="btn btn-primary btn-time-off" type="button"> + <t t-esc="time_off"/> + </button> + <button class="btn btn-secondary btn-allocation" type="button"> + <t t-esc="request"/> + </button> + </t> + + <t t-name="hr_holidays.calendar.popover.placeholder"> + <div t-attf-class="o_cw_popover popover card shadow #{typeof color === 'number' ? _.str.sprintf('o_calendar_color_%s', color) : ''}" role="tooltip"> + <div class="arrow"/> + <div class="card-header d-flex justify-content-between py-2 pr-2"> + <h4 class="popover-header border-0 p-0 pt-1"/> + <div class="ml-4"> + <i t-if="calendarIcon" t-attf-class="fa {{calendarIcon}}"></i> + <span class="o_cw_popover_close ml-1"><i class="fa fa-close small"/></span> + </div> + </div> + <div class="o_cw_body"> + </div> + </div> + </t> + + <t t-name="hr_holidays.calendar.popover"> + <div class="o_cw_body"> + <ul class="list-group list-group-flush"> + <li t-if="!widget.hideDate and widget.eventDate.date" class="list-group-item"> + <b class="text-capitalize" t-esc="widget.eventDate.date"/> <small t-if="widget.eventDate.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventDate.duration)"/></small> + </li> + <li t-if="!widget.hideTime and widget.eventTime.time" class="list-group-item"> + <b t-esc="widget.eventTime.time"/> <small t-if="widget.eventTime.duration"><b t-esc="_.str.sprintf('(%s)', widget.eventTime.duration)"/></small> + </li> + </ul> + <ul class="list-group list-group-flush o_cw_popover_fields_secondary" t-if="widget.display_name"> + <li class="list-group-item"> + <span class="o_field_char o_field_widget" t-esc="widget.display_name" /> + </li> + </ul> + <div class="card-footer border-top" t-if="widget.canEdit or widget.canDelete"> + <a t-if="widget.canEdit" href="#" class="btn btn-primary o_cw_popover_edit">Edit</a> + <a t-if="widget.canDelete" href="#" class="btn btn-secondary o_cw_popover_delete ml-2">Delete</a> + </div> + </div> + </t> +</templates> |
