diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index ed7d588434..7917e3abf5 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -476,13 +476,14 @@ def get_employee_emails(employee_list):
return employee_emails
@frappe.whitelist()
-def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
+def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False, fields=None):
filters = [['status', '!=', 'Left']]
if company and company != 'All Companies':
filters.append(['company', '=', company])
- fields = ['name as value', 'employee_name as title']
+ if not fields:
+ fields = ['name as value', 'employee_name as title']
if is_root:
parent = ''
diff --git a/erpnext/hr/page/organizational_chart/__init__.py b/erpnext/hr/page/organizational_chart/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/hr/page/organizational_chart/node_card.html b/erpnext/hr/page/organizational_chart/node_card.html
new file mode 100644
index 0000000000..057c45ee86
--- /dev/null
+++ b/erpnext/hr/page/organizational_chart/node_card.html
@@ -0,0 +1,27 @@
+
\ No newline at end of file
diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js
new file mode 100644
index 0000000000..04bd9422bd
--- /dev/null
+++ b/erpnext/hr/page/organizational_chart/organizational_chart.js
@@ -0,0 +1,409 @@
+frappe.pages['organizational-chart'].on_page_load = function(wrapper) {
+ frappe.ui.make_app_page({
+ parent: wrapper,
+ title: __('Organizational Chart'),
+ single_column: true
+ });
+
+ let organizational_chart = new OrganizationalChart(wrapper);
+ $(wrapper).bind('show', ()=> {
+ organizational_chart.show();
+ });
+};
+
+class OrganizationalChart {
+
+ constructor(wrapper) {
+ this.wrapper = $(wrapper);
+ this.page = wrapper.page;
+
+ this.page.main.css({
+ 'min-height': '300px',
+ 'max-height': '600px',
+ 'overflow': 'auto',
+ 'position': 'relative'
+ });
+ this.page.main.addClass('frappe-card');
+
+ this.nodes = {};
+ this.setup_node_class();
+ }
+
+ setup_node_class() {
+ let me = this;
+ this.Node = class {
+ constructor({
+ id, parent, parent_id, image, name, title, expandable, connections, is_root // eslint-disable-line
+ }) {
+ // to setup values passed via constructor
+ $.extend(this, arguments[0]);
+
+ this.expanded = 0;
+
+ me.nodes[this.id] = this;
+ me.make_node_element(this);
+ me.setup_node_click_action(this);
+ }
+ }
+ }
+
+ make_node_element(node) {
+ let node_card = frappe.render_template('node_card', {
+ id: node.id,
+ name: node.name,
+ title: node.title,
+ image: node.image,
+ parent: node.parent_id,
+ connections: node.connections
+ });
+
+ node.parent.append(node_card);
+ node.$link = $(`#${node.id}`);
+ }
+
+ show() {
+ frappe.breadcrumbs.add('HR');
+
+ let me = this;
+ let company = this.page.add_field({
+ fieldtype: 'Link',
+ options: 'Company',
+ fieldname: 'company',
+ placeholder: __('Select Company'),
+ default: frappe.defaults.get_default('company'),
+ only_select: true,
+ reqd: 1,
+ change: () => {
+ me.company = undefined;
+
+ if (company.get_value() && me.company != company.get_value()) {
+ me.company = company.get_value();
+
+ // svg for connectors
+ me.make_svg_markers()
+
+ if (me.$hierarchy)
+ me.$hierarchy.remove();
+
+ // setup hierarchy
+ me.$hierarchy = $(
+ ``);
+
+ me.page.main.append(me.$hierarchy);
+ me.render_root_node();
+ }
+ }
+ });
+
+ company.refresh();
+ $(`[data-fieldname="company"]`).trigger('change');
+ }
+
+ make_svg_markers() {
+ $('#arrows').remove();
+
+ this.page.main.prepend(`
+ `);
+ }
+
+ render_root_node() {
+ this.method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children';
+
+ let me = this;
+
+ frappe.call({
+ method: me.method,
+ args: {
+ company: me.company
+ },
+ callback: function(r) {
+ if (r.message.length) {
+ let data = r.message[0];
+
+ let root_node = new me.Node({
+ id: data.name,
+ parent: me.$hierarchy.find('.root-level'),
+ parent_id: undefined,
+ image: data.image,
+ name: data.employee_name,
+ title: data.designation,
+ expandable: true,
+ connections: data.connections,
+ is_root: true,
+ });
+
+ me.expand_node(root_node);
+ }
+ }
+ })
+ }
+
+ expand_node(node) {
+ let is_sibling = this.selected_node && this.selected_node.parent_id === node.parent_id;
+ this.set_selected_node(node);
+ this.show_active_path(node);
+ this.collapse_previous_level_nodes(node);
+
+ // since the previous node collapses, all connections to that node need to be rebuilt
+ // if a sibling node is clicked, connections don't need to be rebuilt
+ if (!is_sibling) {
+ // rebuild outgoing connections
+ this.refresh_connectors(node.parent_id);
+
+ // rebuild incoming connections
+ let grandparent = $(`#${node.parent_id}`).attr('data-parent');
+ this.refresh_connectors(grandparent)
+ }
+
+ if (node.expandable && !node.expanded) {
+ return this.load_children(node);
+ }
+ }
+
+ collapse_node() {
+ if (this.selected_node.expandable) {
+ this.selected_node.$children.hide();
+ $(`path[data-parent="${this.selected_node.id}"]`).hide();
+ this.selected_node.expanded = false;
+ }
+ }
+
+ show_active_path(node) {
+ // mark node parent on active path
+ $(`#${node.parent_id}`).addClass('active-path');
+ }
+
+ load_children(node) {
+ frappe.run_serially([
+ () => this.get_child_nodes(node.id),
+ (child_nodes) => this.render_child_nodes(node, child_nodes)
+ ]);
+ }
+
+ get_child_nodes(node_id) {
+ let me = this;
+ return new Promise(resolve => {
+ frappe.call({
+ method: this.method,
+ args: {
+ parent: node_id,
+ company: me.company
+ },
+ callback: (r) => {
+ resolve(r.message);
+ }
+ });
+ });
+ }
+
+ render_child_nodes(node, child_nodes) {
+ const last_level = this.$hierarchy.find('.level:last').index();
+ const current_level = $(`#${node.id}`).parent().parent().parent().index();
+
+ if (last_level === current_level) {
+ this.$hierarchy.append(`
+
+ `);
+ }
+
+ if (!node.$children) {
+ node.$children = $('')
+ .hide()
+ .appendTo(this.$hierarchy.find('.level:last'));
+
+ node.$children.empty();
+
+ if (child_nodes) {
+ $.each(child_nodes, (_i, data) => {
+ this.add_node(node, data);
+
+ setTimeout(() => {
+ this.add_connector(node.id, data.name);
+ }, 250);
+ });
+ }
+ }
+
+ node.$children.show();
+ $(`path[data-parent="${node.id}"]`).show();
+ node.expanded = true;
+ }
+
+ add_node(node, data) {
+ var $li = $('');
+
+ return new this.Node({
+ id: data.name,
+ parent: $li.appendTo(node.$children),
+ parent_id: node.id,
+ image: data.image,
+ name: data.employee_name,
+ title: data.designation,
+ expandable: data.expandable,
+ connections: data.connections,
+ children: undefined
+ });
+ }
+
+ add_connector(parent_id, child_id) {
+ let parent_node = document.querySelector(`#${parent_id}`);
+ let child_node = document.querySelector(`#${child_id}`);
+
+ // variable for the namespace
+ const svgns = 'http://www.w3.org/2000/svg';
+ let path = document.createElementNS(svgns, 'path');
+
+ // we need to connect right side of the parent to the left side of the child node
+ let pos_parent_right = {
+ x: parent_node.offsetLeft + parent_node.offsetWidth,
+ y: parent_node.offsetTop + parent_node.offsetHeight / 2
+ };
+ let pos_child_left = {
+ x: child_node.offsetLeft - 5,
+ y: child_node.offsetTop + child_node.offsetHeight / 2
+ };
+
+ let connector =
+ "M" +
+ (pos_parent_right.x) + "," + (pos_parent_right.y) + " " +
+ "C" +
+ (pos_parent_right.x + 100) + "," + (pos_parent_right.y) + " " +
+ (pos_child_left.x - 100) + "," + (pos_child_left.y) + " " +
+ (pos_child_left.x) + "," + (pos_child_left.y);
+
+ path.setAttribute("d", connector);
+ path.setAttribute("data-parent", parent_id);
+ path.setAttribute("data-child", child_id);
+
+ if ($(`#${parent_id}`).hasClass('active')) {
+ path.setAttribute("class", "active-connector");
+ path.setAttribute("marker-start", "url(#arrowstart-active)");
+ path.setAttribute("marker-end", "url(#arrowhead-active)");
+ } else if ($(`#${parent_id}`).hasClass('active-path')) {
+ path.setAttribute("class", "collapsed-connector");
+ path.setAttribute("marker-start", "url(#arrowstart-collapsed)");
+ path.setAttribute("marker-end", "url(#arrowhead-collapsed)");
+ }
+
+ $('#connectors').append(path);
+ }
+
+ set_selected_node(node) {
+ // remove .active class from the current node
+ $('.active').removeClass('active');
+
+ // add active class to the newly selected node
+ this.selected_node = node;
+ node.$link.addClass('active');
+ }
+
+ collapse_previous_level_nodes(node) {
+ let node_parent = $(`#${node.parent_id}`);
+
+ let previous_level_nodes = node_parent.parent().parent().children('li');
+ if (node_parent.parent().hasClass('root-level')) {
+ previous_level_nodes = node_parent.parent().children('li');
+ }
+
+ let node_card = undefined;
+
+ previous_level_nodes.each(function() {
+ node_card = $(this).find('.node-card');
+
+ if (!node_card.hasClass('active-path')) {
+ node_card.addClass('collapsed');
+ }
+ });
+ }
+
+ refresh_connectors(node_parent) {
+ if (!node_parent) return;
+
+ $(`path[data-parent="${node_parent}"]`).remove();
+
+ frappe.run_serially([
+ () => this.get_child_nodes(node_parent),
+ (child_nodes) => {
+ if (child_nodes) {
+ $.each(child_nodes, (_i, data) => {
+ this.add_connector(node_parent, data.name);
+ });
+ }
+ }
+ ]);
+ }
+
+ setup_node_click_action(node) {
+ let me = this;
+ let node_element = $(`#${node.id}`);
+
+ node_element.click(function() {
+ let is_sibling = me.selected_node.parent_id === node.parent_id;
+
+ if (is_sibling) {
+ me.collapse_node();
+ } else if (node_element.is(':visible')
+ && (node_element.hasClass('collapsed') || node_element.hasClass('active-path'))) {
+ me.remove_levels_after_node(node);
+ me.remove_orphaned_connectors();
+ }
+
+ me.expand_node(node);
+ });
+ }
+
+ remove_levels_after_node(node) {
+ let level = $(`#${node.id}`).parent().parent().parent();
+
+ if ($(`#${node.id}`).parent().hasClass('root-level')) {
+ level = $(`#${node.id}`).parent();
+ }
+
+ level = $('.hierarchy > li:eq('+ level.index() + ')');
+ level.nextAll('li').remove();
+
+ let nodes = level.find('.node-card');
+ let node_object = undefined;
+
+ $.each(nodes, (_i, element) => {
+ node_object = this.nodes[element.id];
+ node_object.expanded = 0;
+ node_object.$children = undefined;
+ });
+
+ nodes.removeClass('collapsed active-path');
+ }
+
+ remove_orphaned_connectors() {
+ let paths = $('#connectors > path');
+ $.each(paths, (_i, path) => {
+ let parent = $(path).data('parent');
+ let child = $(path).data('child');
+
+ if ($(parent).length || $(child).length)
+ return;
+
+ $(path).remove();
+ })
+ }
+}
diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.json b/erpnext/hr/page/organizational_chart/organizational_chart.json
new file mode 100644
index 0000000000..d802781320
--- /dev/null
+++ b/erpnext/hr/page/organizational_chart/organizational_chart.json
@@ -0,0 +1,26 @@
+{
+ "content": null,
+ "creation": "2021-05-25 10:53:10.107241",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2021-05-25 10:53:18.201931",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "organizational-chart",
+ "owner": "Administrator",
+ "page_name": "Organizational Chart",
+ "roles": [
+ {
+ "role": "HR User"
+ },
+ {
+ "role": "HR Manager"
+ }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Organizational Chart"
+}
\ No newline at end of file
diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.py b/erpnext/hr/page/organizational_chart/organizational_chart.py
new file mode 100644
index 0000000000..be2964530b
--- /dev/null
+++ b/erpnext/hr/page/organizational_chart/organizational_chart.py
@@ -0,0 +1,49 @@
+from __future__ import unicode_literals
+import frappe
+
+@frappe.whitelist()
+def get_children(parent=None, company=None, is_root=False, is_tree=False, fields=None):
+
+ filters = [['status', '!=', 'Left']]
+ if company and company != 'All Companies':
+ filters.append(['company', '=', company])
+
+ if not fields:
+ fields = ['employee_name', 'name', 'reports_to', 'image', 'designation']
+
+ if is_root:
+ parent = ''
+ if parent and company and parent!=company:
+ filters.append(['reports_to', '=', parent])
+ else:
+ filters.append(['reports_to', '=', ''])
+
+ employees = frappe.get_list('Employee', fields=fields,
+ filters=filters, order_by='name')
+
+ for employee in employees:
+ is_expandable = frappe.get_all('Employee', filters=[
+ ['reports_to', '=', employee.get('name')]
+ ])
+ employee.connections = get_connections(employee.name)
+ employee.expandable = 1 if is_expandable else 0
+
+ return employees
+
+
+def get_connections(employee):
+ num_connections = 0
+
+ connections = frappe.get_list('Employee', filters=[
+ ['reports_to', '=', employee]
+ ])
+ num_connections += len(connections)
+
+ while connections:
+ for entry in connections:
+ connections = frappe.get_list('Employee', filters=[
+ ['reports_to', '=', entry.name]
+ ])
+ num_connections += len(connections)
+
+ return num_connections
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 7a3cb838a9..d3ebcdf7e7 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -3,7 +3,8 @@
"public/less/erpnext.less",
"public/less/hub.less",
"public/scss/call_popup.scss",
- "public/scss/point-of-sale.scss"
+ "public/scss/point-of-sale.scss",
+ "public/scss/organizational_chart.scss"
],
"css/marketplace.css": [
"public/less/hub.less"
diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/organizational_chart.scss
new file mode 100644
index 0000000000..62f6ddcb6e
--- /dev/null
+++ b/erpnext/public/scss/organizational_chart.scss
@@ -0,0 +1,209 @@
+.node-card {
+ background: white;
+ stroke: 1px solid var(--gray-200);
+ box-shadow: var(--shadow-base);
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ margin-left: 3rem;
+ width: 18rem;
+
+ .btn-edit-node {
+ display: none;
+ }
+
+ .edit-chart-node {
+ display: none;
+ }
+
+ .node-edit-icon {
+ display: none;
+ }
+}
+
+.node-image {
+ width: 3.0rem;
+ height: 3.0rem;
+}
+
+.node-name {
+ font-size: 1rem;
+ line-height: 1.72;
+}
+
+.node-title {
+ font-size: 0.75rem;
+ line-height: 1.35;
+}
+
+.node-connections {
+ font-size: 0.75rem;
+ line-height: 1.35;
+}
+
+.node-card.active {
+ background: var(--blue-50);
+ border: 1px solid var(--blue-500);
+ box-shadow: var(--shadow-md);
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ width: 18rem;
+ height: 5rem;
+
+ .btn-edit-node {
+ display: flex;
+ background: var(--blue-100);
+ color: var(--blue-500);
+ padding: .25rem .5rem;
+ font-size: .75rem;
+ justify-content: center;
+ box-shadow: var(--shadow-sm);
+ }
+
+ .edit-chart-node {
+ display: block;
+ }
+
+ .node-edit-icon {
+ display: block;
+ }
+
+ .edit-chart-node {
+ margin-right: 0.25rem;
+ }
+
+ .node-edit-icon > .icon{
+ stroke: var(--blue-500);
+ }
+
+ .node-name {
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 2px;
+ }
+}
+
+.node-card.active-path {
+ background: var(--blue-100);
+ border: 1px solid var(--blue-300);
+ box-shadow: var(--shadow-sm);
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ width: 15rem;
+ height: 3.0rem;
+
+ .btn-edit-node {
+ display: none !important;
+ }
+
+ .edit-chart-node {
+ display: none;
+ }
+
+ .node-edit-icon {
+ display: none;
+ }
+
+ .node-info {
+ display: none;
+ }
+
+ .node-title {
+ display: none;
+ }
+
+ .node-connections {
+ display: none;
+ }
+
+ .node-name {
+ font-size: 0.85rem;
+ line-height: 1.35;
+ }
+
+ .node-image {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+
+ .node-meta {
+ align-items: baseline;
+ }
+}
+
+.node-card.collapsed {
+ background: white;
+ stroke: 1px solid var(--gray-200);
+ box-shadow: var(--shadow-sm);
+ border-radius: 0.5rem;
+ padding: 0.75rem;
+ width: 15rem;
+ height: 3.0rem;
+
+ .btn-edit-node {
+ display: none !important;
+ }
+
+ .edit-chart-node {
+ display: none;
+ }
+
+ .node-edit-icon {
+ display: none;
+ }
+
+ .node-info {
+ display: none;
+ }
+
+ .node-title {
+ display: none;
+ }
+
+ .node-connections {
+ display: none;
+ }
+
+ .node-name {
+ font-size: 0.85rem;
+ line-height: 1.35;
+ }
+
+ .node-image {
+ width: 1.5rem;
+ height: 1.5rem;
+ }
+
+ .node-meta {
+ align-items: baseline;
+ }
+}
+
+// horizontal hierarchy tree view
+.hierarchy {
+ display: flex;
+ padding-top: 30px;
+}
+
+.hierarchy li {
+ list-style-type: none;
+}
+
+.child-node {
+ margin: 0px 0px 16px 0px;
+}
+
+.level {
+ margin-right: 8px;
+}
+
+#arrows {
+ position: absolute;
+}
+
+.active-connector {
+ stroke: var(--blue-500);
+}
+
+.collapsed-connector {
+ stroke: var(--blue-300);
+}