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_org_chart/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_org_chart/static/src')
| -rw-r--r-- | addons/hr_org_chart/static/src/js/hr_org_chart.js | 196 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/scss/hr_org_chart.scss | 294 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/scss/variables.scss | 14 | ||||
| -rw-r--r-- | addons/hr_org_chart/static/src/xml/hr_org_chart.xml | 160 |
4 files changed, 664 insertions, 0 deletions
diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js new file mode 100644 index 00000000..085df1fa --- /dev/null +++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js @@ -0,0 +1,196 @@ +odoo.define('web.OrgChart', function (require) { +"use strict"; + +var AbstractField = require('web.AbstractField'); +var concurrency = require('web.concurrency'); +var core = require('web.core'); +var field_registry = require('web.field_registry'); +var session = require('web.session'); + +var QWeb = core.qweb; +var _t = core._t; + +var FieldOrgChart = AbstractField.extend({ + + events: { + "click .o_employee_redirect": "_onEmployeeRedirect", + "click .o_employee_sub_redirect": "_onEmployeeSubRedirect", + "click .o_employee_more_managers": "_onEmployeeMoreManager" + }, + /** + * @constructor + * @override + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.dm = new concurrency.DropMisordered(); + this.employee = null; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Get the chart data through a rpc call. + * + * @private + * @param {integer} employee_id + * @returns {Promise} + */ + _getOrgData: function () { + var self = this; + return this.dm.add(this._rpc({ + route: '/hr/get_org_chart', + params: { + employee_id: this.employee, + context: session.user_context, + }, + })).then(function (data) { + return data; + }); + }, + /** + * Get subordonates of an employee through a rpc call. + * + * @private + * @param {integer} employee_id + * @returns {Promise} + */ + _getSubordinatesData: function (employee_id, type) { + return this.dm.add(this._rpc({ + route: '/hr/get_subordinates', + params: { + employee_id: employee_id, + subordinates_type: type, + context: session.user_context, + }, + })); + }, + /** + * @override + * @private + */ + _render: function () { + if (!this.recordData.id) { + return this.$el.html(QWeb.render("hr_org_chart", { + managers: [], + children: [], + })); + } + else if (!this.employee) { + // the widget is either dispayed in the context of a hr.employee form or a res.users form + this.employee = this.recordData.employee_ids !== undefined ? this.recordData.employee_ids.res_ids[0] : this.recordData.id; + } + + var self = this; + return this._getOrgData().then(function (orgData) { + if (_.isEmpty(orgData)) { + orgData = { + managers: [], + children: [], + } + } + orgData.view_employee_id = self.recordData.id; + self.$el.html(QWeb.render("hr_org_chart", orgData)); + self.$('[data-toggle="popover"]').each(function () { + $(this).popover({ + html: true, + title: function () { + var $title = $(QWeb.render('hr_orgchart_emp_popover_title', { + employee: { + name: $(this).data('emp-name'), + id: $(this).data('emp-id'), + }, + })); + $title.on('click', + '.o_employee_redirect', _.bind(self._onEmployeeRedirect, self)); + return $title; + }, + container: this, + placement: 'left', + trigger: 'focus', + content: function () { + var $content = $(QWeb.render('hr_orgchart_emp_popover_content', { + employee: { + id: $(this).data('emp-id'), + name: $(this).data('emp-name'), + direct_sub_count: parseInt($(this).data('emp-dir-subs')), + indirect_sub_count: parseInt($(this).data('emp-ind-subs')), + }, + })); + $content.on('click', + '.o_employee_sub_redirect', _.bind(self._onEmployeeSubRedirect, self)); + return $content; + }, + template: QWeb.render('hr_orgchart_emp_popover', {}), + }); + }); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onEmployeeMoreManager: function(event) { + event.preventDefault(); + this.employee = parseInt($(event.currentTarget).data('employee-id')); + this._render(); + }, + /** + * Redirect to the employee form view. + * + * @private + * @param {MouseEvent} event + * @returns {Promise} action loaded + */ + _onEmployeeRedirect: function (event) { + var self = this; + event.preventDefault(); + var employee_id = parseInt($(event.currentTarget).data('employee-id')); + return this._rpc({ + model: 'hr.employee', + method: 'get_formview_action', + args: [employee_id], + }).then(function(action) { + return self.do_action(action); + }); + }, + /** + * Redirect to the sub employee form view. + * + * @private + * @param {MouseEvent} event + * @returns {Promise} action loaded + */ + _onEmployeeSubRedirect: function (event) { + event.preventDefault(); + var employee_id = parseInt($(event.currentTarget).data('employee-id')); + var employee_name = $(event.currentTarget).data('employee-name'); + var type = $(event.currentTarget).data('type') || 'direct'; + var self = this; + if (employee_id) { + this._getSubordinatesData(employee_id, type).then(function(data) { + var domain = [['id', 'in', data]]; + return self._rpc({ + model: 'hr.employee', + method: 'get_formview_action', + args: [employee_id], + }).then(function(action) { + action = _.extend(action, { + 'view_mode': 'kanban,list,form', + 'views': [[false, 'kanban'], [false, 'list'], [false, 'form']], + 'domain': domain, + }); + return self.do_action(action); + }); + }); + } + }, +}); + +field_registry.add('hr_org_chart', FieldOrgChart); + +return FieldOrgChart; + +}); diff --git a/addons/hr_org_chart/static/src/scss/hr_org_chart.scss b/addons/hr_org_chart/static/src/scss/hr_org_chart.scss new file mode 100644 index 00000000..8c4d9d89 --- /dev/null +++ b/addons/hr_org_chart/static/src/scss/hr_org_chart.scss @@ -0,0 +1,294 @@ +// MOBILE LAYOUT CUSTOMIZATIONS +@include media-breakpoint-down(sm) { + #o_employee_right { + .o_org_chart_title { + font-size: 20px; + padding: 5px 0; + border-bottom: 1px solid $o-hr-org-chart-border-color; + } + } +} + +// SMALL DESKTOP LAYOUT +@include media-breakpoint-up(md) { + #o_work_employee_container { + display: flex; + width: 100%; + } + #o_work_employee_main { + flex: 1 1 60%; + } + #o_employee_right { + flex: 0 1 35%; + margin-left: 2%; + padding-left: 2%; + border-left: 1px solid $o-hr-org-chart-border-color; + + .o_org_chart_title { + color: gray('600') + } + } +} + +// MEDIUM DESKTOP LAYOUT +@include media-breakpoint-up(lg) { + #o_employee_right { + flex: 0 1 33%; + } +} + +// LARGE DESKTOP LAYOUT +@include media-breakpoint-up(xl) { + #o_employee_right { + flex: 0 1 30%; + } +} + +#o_employee_right { + $tmp-gap-base: $o-hr-org-chart-entry-pic-size*0.7; + + // ORGANIGRAM LINES + .o_field_widget, .o_org_chart_group_up, .o_org_chart_group_down { + position: relative; + width: 100%; + } + + .o_org_chart_group_up { + &:before { + @include o-hr-org-chart-line; + border-left-width: $o-hr-org-chart-entry-line-w; + height: calc(100% + #{$o-hr-org-chart-entry-pic-size*0.5}) ; + @include o-position-absolute( + $top: $o-hr-org-chart-entry-pic-size*0.1 + 5px, + $left: $o-hr-org-chart-entry-pic-size*0.5 - $o-hr-org-chart-entry-line-w*0.5 + ); + } + .o_org_chart_entry:last-of-type { + &:before { + @include o-hr-org-chart-line; + border-width: 0 0 $o-hr-org-chart-entry-line-w $o-hr-org-chart-entry-line-w; + @include size(($o-hr-org-chart-entry-pic-size*0.5) - ($o-hr-org-chart-entry-v-gap*2), $o-hr-org-chart-entry-pic-size*0.5 + $o-hr-org-chart-entry-v-gap*2 ); + @include o-position-absolute( + $left: $o-hr-org-chart-entry-pic-size*0.5 - $o-hr-org-chart-entry-line-w*0.5, + $top: 100% + ); + } + } + } + + .o_org_chart_group_up + .o_org_chart_entry_self { + margin-left: $tmp-gap-base; + + & + .o_org_chart_group_down { + padding-left: $tmp-gap-base*2; + + &:before { + margin-left: $tmp-gap-base; + } + } + } + + .o_org_chart_group_down { + padding-left: $tmp-gap-base; + + &:before { + @include o-hr-org-chart-line; + border-left-width: $o-hr-org-chart-entry-line-w; + height: 100%; + @include o-position-absolute( + $top: $o-hr-org-chart-entry-v-gap*-1, + $left: $tmp-gap-base*0.5 + $o-hr-org-chart-entry-pic-size*0.1 + $o-hr-org-chart-entry-line-w*0.5 + ); + } + + .o_org_chart_entry { + &:before { + @include o-hr-org-chart-line; + border-top-width: $o-hr-org-chart-entry-line-w; + @include size($tmp-gap-base, 0); + @include o-position-absolute( + $left: $tmp-gap-base*-0.5 + $o-hr-org-chart-entry-pic-size*0.1 + $o-hr-org-chart-entry-line-w*0.5, + $top: $o-hr-org-chart-entry-pic-size*0.5 + ); + } + + &:last-of-type { + &:before { + height: 50%; + } + + } + + &.o_org_chart_more { + margin-top: $o-hr-org-chart-entry-v-gap; + + &:before { + top: 15px; + } + } + } + } + + // ORGANIGRAM DESIGN + .o_org_chart_entry { + margin-bottom: $o-hr-org-chart-entry-v-gap; + overflow: visible; + margin-top: 0; + + &, .o_media_left, .media-body { + position: relative; + } + + .o_media_left { + padding-right: 10px; + } + + .media-body { + vertical-align: middle; + + .badge { + float: right; + cursor: pointer; + margin-right: 5px; + color: gray('600'); + background: $o-hr-org-chart-bg; + border: 1px solid gray('600'); + &:hover { + color: $o-brand-primary; + border-color: $o-brand-primary; + } + &:focus { + outline: none; + } + } + + strong { + display: block; + line-height: 1.2; + font-size: 11px; + color: lighten(gray('600'), 15%); + } + } + + .o_media_object { + display: block; + width: $o-hr-org-chart-entry-pic-size*0.8; + height: $o-hr-org-chart-entry-pic-size*0.8; + margin: $o-hr-org-chart-entry-pic-size*0.1; + box-shadow: 0 0 0 $o-hr-org-chart-entry-line-w darken($o-hr-org-chart-bg, 20%); + background-size: cover; + background-position: center center; + background-color: $o-view-background-color; + + &.card { + height: 20px; + box-shadow: none; + border-color: transparent; + padding: 0; + position: relative; + color: $body-color; + + .o_org_chart_show_more { + line-height: 13px; + } + + &:hover { + border-color: $o-hr-org-chart-entry-border-color; + color:$o-brand-primary; + } + } + } + + &.o_org_chart_entry_manager, &.o_org_chart_entry_sub { + .o_media_left { + padding-right: 0; + } + .media-body > a { + padding-left: 10px; + max-width: 100%; + display: block; + + .o_media_heading { + color: lighten(gray('600'), 5%); + font-size: 13px; + } + } + + &:hover { + .o_media_object { + box-shadow: 0 0 0 $o-hr-org-chart-entry-line-w*2 rgba($o-brand-primary, 0.6); + } + .media-body > a { + .o_media_heading { + color: $o-brand-primary; + } + strong { + color: lighten(gray('600'), 5%); + } + } + } + } + + &.o_org_chart_entry_self { + &:not(:first-child) { + margin-top: $o-hr-org-chart-entry-v-gap*1.5; + } + + strong { + color: $text-muted; + } + + .o_media_object { + width: $o-hr-org-chart-entry-pic-size; + height: $o-hr-org-chart-entry-pic-size; + margin: 0; + border: $o-hr-org-chart-entry-line-w*2 solid $o-brand-primary; + box-shadow: inset 0 0 0 $o-hr-org-chart-entry-line-w*2 white; + } + + .media-body { + opacity: 1; + } + } + } +} + +// POP OVER +.o_org_chart_popup.popover { + max-width: 400px; + margin-right: 5px; + + .popover-header { + height: 47px; + line-height: 33px; + padding-right: 50px; + + > a { + @include o-position-absolute($right: 14px); + } + + span { + @include size(30px, 30px); + margin-right: 10px; + border-radius: 100%; + background-position: center; + background-size: cover; + float: left; + box-shadow: 0 1px 1px; + } + } + .table { + margin-bottom: 0; + } +} + +// Right to Left specific style to flip the popover arrow +.o_rtl { + .o_org_chart_popup.popover .arrow { + left: 100%; + -webkit-transform: matrix(-1, 0, 0, 1, 0, 0); + -moz-transform: matrix(-1, 0, 0, 1, 0, 0); + -o-transform: matrix(-1, 0, 0, 1, 0, 0); + transform: matrix(-1, 0, 0, 1, 0, 0); + } +} diff --git a/addons/hr_org_chart/static/src/scss/variables.scss b/addons/hr_org_chart/static/src/scss/variables.scss new file mode 100644 index 00000000..142ebf9a --- /dev/null +++ b/addons/hr_org_chart/static/src/scss/variables.scss @@ -0,0 +1,14 @@ +$o-hr-org-chart-bg: white; +$o-hr-org-chart-border-color: $o-brand-secondary; + +$o-hr-org-chart-entry-v-gap: 6px; +$o-hr-org-chart-entry-pic-size: 46px; +$o-hr-org-chart-entry-line-w: 1px; +$o-hr-org-chart-entry-border-color: darken($o-hr-org-chart-bg, 25%); + +// MIXINS +@mixin o-hr-org-chart-line { + content: ''; + background-color: $o-hr-org-chart-bg; + border: 0px solid $o-hr-org-chart-entry-border-color; +} diff --git a/addons/hr_org_chart/static/src/xml/hr_org_chart.xml b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml new file mode 100644 index 00000000..8c5160fc --- /dev/null +++ b/addons/hr_org_chart/static/src/xml/hr_org_chart.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + +<t t-name="hr_org_chart_employee"> + <div t-attf-class="o_org_chart_entry o_org_chart_entry_#{employee_type} media"> + <t t-set="is_self" t-value="employee.id == view_employee_id"/> + + <div class="o_media_left"> + <!-- NOTE: Since by the default on not squared images odoo add white borders, + use bg-images to get a clean and centred images --> + <a t-if="! is_self" + class="o_media_object rounded-circle o_employee_redirect" + t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/image_1024/\')'" + t-att-alt="employee.name" + t-att-data-employee-id="employee.id" + t-att-href="employee.link"/> + <div t-if="is_self" + class="o_media_object rounded-circle" + t-att-style="'background-image:url(\'/web/image/hr.employee.public/' + employee.id + '/image_1024/\')'"/> + </div> + + <div class="media-body"> + <span + t-if="employee.indirect_sub_count > 0" + class="badge badge-pill" + tabindex="0" + data-trigger="focus" + t-att-data-emp-name="employee.name" + t-att-data-emp-id="employee.id" + t-att-data-emp-dir-subs="employee.direct_sub_count" + t-att-data-emp-ind-subs="employee.indirect_sub_count" + data-toggle="popover"> + <t t-esc="employee.indirect_sub_count"/> + </span> + + <t t-if="!is_self"> + <a t-att-href="employee.link" class="o_employee_redirect" t-att-data-employee-id="employee.id"> + <h5 class="o_media_heading"><b><t t-esc="employee.name"/></b></h5> + <strong><t t-esc="employee.job_title"/></strong> + </a> + </t> + <t t-if="is_self"> + <h5 class="o_media_heading"><b><t t-esc="employee.name"/></b></h5> + <strong><t t-esc="employee.job_title"/></strong> + </t> + </div> + </div> +</t> + +<t t-name="hr_org_chart"> + <!-- NOTE: Desidered behaviour: + The maximun number of people is always 7 (including 'self'). Managers have priority over suburdinates + Eg. 1 Manager + 1 self = show just 5 subordinates (if availables) + Eg. 0 Manager + 1 self = show 6 subordinates (if available) + + --> + <t t-set="emp_count" t-value="0"/> + + <div t-if='managers.length > 0' class="o_org_chart_group_up"> + <t t-if='managers_more'> + <div class="o_org_chart_entry o_org_chart_more media"> + <div class="o_media_left"> + <a class="text-center o_employee_more_managers" + t-att-data-employee-id="managers[0].id"> + <i t-attf-class="fa fa-angle-double-up" role="img" aria-label="More managers" title="More managers"/> + </a> + </div> + </div> + </t> + + <t t-foreach="managers" t-as="employee"> + <t t-set="emp_count" t-value="emp_count + 1"/> + <t t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'manager'"/> + </t> + </t> + </div> + + <t t-if="children.length || managers.length" t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'self'"/> + <t t-set="employee" t-value="self"/> + </t> + + <t t-if="!children.length && !managers.length"> + <div class="alert alert-info" role="alert"> + <p><b>No hierarchy position.</b></p> + <p>This employee has no manager or subordinate.</p> + <p>In order to get an organigram, set a manager and save the record.</p> + </div> + </t> + + <div t-if="children.length" class="o_org_chart_group_down"> + <t t-foreach="children" t-as="employee"> + <t t-set="emp_count" t-value="emp_count + 1"/> + <t t-if="emp_count < 20"> + <t t-call="hr_org_chart_employee"> + <t t-set="employee_type" t-value="'sub'"/> + </t> + </t> + </t> + + <t t-if="(children.length + managers.length) > 19"> + <div class="o_org_chart_entry o_org_chart_more media"> + <div class="o_media_left"> + <a href="#" + t-att-data-employee-id="self.id" + t-att-data-employee-name="self.name" + class="o_org_chart_show_more text-center o_employee_sub_redirect">…</a> + </div> + </div> + </t> + </div> +</t> + +<t t-name="hr_orgchart_emp_popover"> + <div class="popover o_org_chart_popup" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div> +</t> + +<t t-name="hr_orgchart_emp_popover_content"> + <table class="table table-sm"> + <thead> + <td class="text-right"><t t-esc="employee.direct_sub_count"/></td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='direct' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + <b>Direct subordinates</b></a> + </td> + </thead> + <tbody> + <tr> + <td class="text-right"> + <t t-esc="employee.indirect_sub_count - employee.direct_sub_count"/> + </td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='indirect' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + Indirect subordinates</a> + </td> + </tr> + <tr> + <td class="text-right"><t t-esc="employee.indirect_sub_count"/></td> + <td> + <a href="#" class="o_employee_sub_redirect" data-type='total' + t-att-data-employee-name="employee.name" t-att-data-employee-id="employee.id"> + Total</a> + </td> + </tr> + </tbody> + </table> +</t> + +<t t-name="hr_orgchart_emp_popover_title"> + <div> + <span t-att-style='"background-image:url(\"/web/image/hr.employee.public/" + employee.id + "/image_1024/\")"'/> + <a href="#" class="float-right o_employee_redirect" t-att-data-employee-id="employee.id"><i class="fa fa-external-link" role="img" aria-label='Redirect' title="Redirect"></i></a> + <b><t t-esc="employee.name"/></b> + </div> +</t> + +</templates> |
