summaryrefslogtreecommitdiff
path: root/addons/hr_org_chart/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_org_chart/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js196
-rw-r--r--addons/hr_org_chart/static/src/scss/hr_org_chart.scss294
-rw-r--r--addons/hr_org_chart/static/src/scss/variables.scss14
-rw-r--r--addons/hr_org_chart/static/src/xml/hr_org_chart.xml160
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 &gt; 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 &gt; 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 &amp;&amp; !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 &lt; 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) &gt; 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">&#8230;</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>