diff --git a/erpnext/hr/page/organizational_chart/organizational_chart.js b/erpnext/hr/page/organizational_chart/organizational_chart.js
index efb367ad44..0fe724c78e 100644
--- a/erpnext/hr/page/organizational_chart/organizational_chart.js
+++ b/erpnext/hr/page/organizational_chart/organizational_chart.js
@@ -5,927 +5,15 @@ frappe.pages['organizational-chart'].on_page_load = function(wrapper) {
single_column: true
});
- // let organizational_chart = undefined;
- // if (frappe.is_mobile()) {
- // organizational_chart = new OrgChartMobile(wrapper);
- // } else {
- // organizational_chart = new OrgChart(wrapper);
- // }
-
- let organizational_chart = new OrgChartMobile(wrapper);
- $(wrapper).bind('show', ()=> {
- organizational_chart.show();
- });
-};
-
-class OrgChart {
-
- 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();
- })
- }
-}
-
-
-class OrgChartMobile {
-
- 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,
- is_mobile: 1
- });
-
- node.parent.append(node_card);
- node.$link = $(`#${node.id}`);
- node.$link.addClass('mobile-node');
- }
-
- 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.$sibling_group)
- me.$sibling_group.remove();
-
- // setup sibling group wrapper
- me.$sibling_group = $(``);
- me.page.main.append(me.$sibling_group);
-
- 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) {
- const is_same_node = (this.selected_node && this.selected_node.id === node.id);
- this.set_selected_node(node);
- this.show_active_path(node);
-
- if (this.$sibling_group) {
- const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent');
- if (node.parent_id !== sibling_parent)
- this.$sibling_group.empty();
- }
-
- if (!is_same_node) {
- // since the previous/parent node collapses, all connections to that node need to be rebuilt
- // rebuild outgoing connections of parent
- this.refresh_connectors(node.parent_id, node.id);
-
- // rebuild incoming connections of parent
- let grandparent = $(`#${node.parent_id}`).attr('data-parent');
- this.refresh_connectors(grandparent, node.parent_id);
- }
-
- if (node.expandable && !node.expanded) {
- return this.load_children(node);
- }
- }
-
- collapse_node() {
- let node = this.selected_node;
- if (node.expandable) {
- node.$children.hide();
- node.expanded = false;
-
- // add a collapsed level to show the collapsed parent
- // and a button beside it to move to that level
- let node_parent = node.$link.parent();
- node_parent.prepend(
- ``
- );
-
- node_parent
- .find('.collapsed-level')
- .append(node.$link);
-
- frappe.run_serially([
- () => this.get_child_nodes(node.parent_id, node.id),
- (child_nodes) => this.get_node_group(child_nodes, node.parent_id),
- (node_group) => {
- node_parent.find('.collapsed-level')
- .append(node_group);
- },
- () => this.setup_node_group_action()
- ]);
- }
- }
-
- 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, exclude_node=null) {
- let me = this;
- return new Promise(resolve => {
- frappe.call({
- method: this.method,
- args: {
- parent: node_id,
- company: me.company,
- exclude_node: exclude_node
- },
- callback: (r) => {
- resolve(r.message);
- }
- });
- });
- }
-
- render_child_nodes(node, child_nodes) {
- if (!node.$children) {
- node.$children = $('')
- .hide()
- .appendTo(node.$link.parent());
-
- node.$children.empty();
-
- if (child_nodes) {
- $.each(child_nodes, (_i, data) => {
- this.add_node(node, data);
- $(`#${data.name}`).addClass('active-child');
-
- setTimeout(() => {
- this.add_connector(node.id, data.name);
- }, 250);
- });
- }
- }
-
- node.$children.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');
-
- let connector = undefined;
-
- if ($(`#${parent_id}`).hasClass('active')) {
- connector = this.get_connector_for_active_node(parent_node, child_node);
- } else if ($(`#${parent_id}`).hasClass('active-path')) {
- connector = this.get_connector_for_collapsed_node(parent_node, child_node);
- }
-
- path.setAttribute("d", connector);
- this.set_path_attributes(path, parent_id, child_id);
-
- $('#connectors').append(path);
- }
-
- get_connector_for_active_node(parent_node, child_node) {
- // we need to connect the bottom left of the parent to the left side of the child node
- let pos_parent_bottom = {
- x: parent_node.offsetLeft + 20,
- y: parent_node.offsetTop + parent_node.offsetHeight
- };
- let pos_child_left = {
- x: child_node.offsetLeft - 5,
- y: child_node.offsetTop + child_node.offsetHeight / 2
- };
-
- let connector =
- "M" +
- (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " +
- "L" +
- (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " +
- "L" +
- (pos_child_left.x) + "," + (pos_child_left.y);
-
- return connector;
- }
-
- get_connector_for_collapsed_node(parent_node, child_node) {
- // we need to connect the bottom left of the parent to the top left of the child node
- let pos_parent_bottom = {
- x: parent_node.offsetLeft + 20,
- y: parent_node.offsetTop + parent_node.offsetHeight
- };
- let pos_child_top = {
- x: child_node.offsetLeft + 20,
- y: child_node.offsetTop
- };
-
- let connector =
- "M" +
- (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " +
- "L" +
- (pos_child_top.x) + "," + (pos_child_top.y);
-
- return connector;
- }
-
- set_path_attributes(path, parent_id, child_id) {
- 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");
- }
- }
-
- 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');
- }
-
- setup_node_click_action(node) {
- let me = this;
- let node_element = $(`#${node.id}`);
-
- node_element.click(function() {
- if (node_element.is(':visible') && node_element.hasClass('active-path')) {
- me.remove_levels_after_node(node);
- me.remove_orphaned_connectors();
+ $(wrapper).bind('show', () => {
+ frappe.require('/assets/js/hierarchy-chart.min.js', () => {
+ let organizational_chart = undefined;
+ if (frappe.is_mobile()) {
+ organizational_chart = new erpnext.HierarchyChartMobile(wrapper);
} else {
- me.add_node_to_hierarchy(node, true);
- me.collapse_node();
+ organizational_chart = new erpnext.HierarchyChart(wrapper);
}
-
- me.expand_node(node);
+ organizational_chart.show();
});
- }
-
- setup_node_group_action() {
- let me = this;
-
- $('.node-group').on('click', function() {
- let parent = $(this).attr('data-parent');
- me.expand_sibling_group_node(parent);
- });
- }
-
- add_node_to_hierarchy(node) {
- this.$hierarchy.append(`
-
-
-
-
- `);
-
- node.$link.appendTo(this.$hierarchy.find('.level:last'));
- }
-
- get_node_group(nodes, parent, collapsed=true) {
- let limit = 2;
- const display_nodes = nodes.slice(0, limit);
- const extra_nodes = nodes.slice(limit);
-
- let html = display_nodes.map(node =>
- this.get_avatar(node)
- ).join('');
-
- if (extra_nodes.length === 1) {
- let node = extra_nodes[0];
- html += this.get_avatar(node);
- } else if (extra_nodes.length > 1) {
- html = `
- ${html}
-
-
- +${extra_nodes.length}
-
-
- `;
- }
-
- if (html) {
- const $node_group =
- $(``);
-
- if (collapsed)
- $node_group.addClass('collapsed');
-
- return $node_group;
- }
-
- return null;
- }
-
- get_avatar(node) {
- return `
-
- `
- }
-
- expand_sibling_group_node(parent) {
- let node_object = this.nodes[parent];
- let node = node_object.$link;
- node.removeClass('active-child active-path');
- node_object.expanded = 0;
- node_object.$children = undefined;
-
- // show parent's siblings and expand parent node
- frappe.run_serially([
- () => this.get_child_nodes(node_object.parent_id, node_object.id),
- (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false),
- (node_group) => {
- if (node_group)
- this.$sibling_group.empty().append(node_group);
- },
- () => this.setup_node_group_action(),
- () => {
- this.$hierarchy.empty().append(`
-
- `);
- this.$hierarchy.find('.level').append(node);
- $(`#connectors`).empty();
- this.expand_node(node_object);
- }
- ]);
- }
-
- remove_levels_after_node(node) {
- let level = $(`#${node.id}`).parent().parent();
-
- level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')');
- level.nextAll('li').remove();
-
- let current_node = level.find(`#${node.id}`);
- let node_object = this.nodes[node.id];
-
- current_node.removeClass('active-child active-path');
- node_object.expanded = 0;
- node_object.$children = undefined;
-
- level.empty().append(current_node);
- }
-
- 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();
- })
- }
-
- refresh_connectors(node_parent, node_id) {
- if (!node_parent) return;
-
- $(`path[data-parent="${node_parent}"]`).remove();
- this.add_connector(node_parent, node_id);
- }
-}
\ No newline at end of file
+ });
+};
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index d3ebcdf7e7..3c60e3ee50 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -4,7 +4,7 @@
"public/less/hub.less",
"public/scss/call_popup.scss",
"public/scss/point-of-sale.scss",
- "public/scss/organizational_chart.scss"
+ "public/scss/hierarchy_chart.scss"
],
"css/marketplace.css": [
"public/less/hub.less"
@@ -44,7 +44,8 @@
"public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js",
"public/js/telephony.js",
- "public/js/templates/call_link.html"
+ "public/js/templates/call_link.html",
+ "public/js/templates/node_card.html"
],
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
@@ -67,5 +68,9 @@
"public/js/bank_reconciliation_tool/data_table_manager.js",
"public/js/bank_reconciliation_tool/number_card.js",
"public/js/bank_reconciliation_tool/dialog_manager.js"
+ ],
+ "js/hierarchy-chart.min.js": [
+ "public/js/hierarchy_chart/hierarchy_chart_desktop.js",
+ "public/js/hierarchy_chart/hierarchy_chart_mobile.js"
]
}
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
new file mode 100644
index 0000000000..fd84d4ea5c
--- /dev/null
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -0,0 +1,396 @@
+erpnext.HierarchyChart = class {
+
+ 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();
+ })
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
new file mode 100644
index 0000000000..c705681438
--- /dev/null
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_mobile.js
@@ -0,0 +1,513 @@
+erpnext.HierarchyChartMobile = class {
+
+ 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,
+ is_mobile: 1
+ });
+
+ node.parent.append(node_card);
+ node.$link = $(`#${node.id}`);
+ node.$link.addClass('mobile-node');
+ }
+
+ 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.$sibling_group)
+ me.$sibling_group.remove();
+
+ // setup sibling group wrapper
+ me.$sibling_group = $(``);
+ me.page.main.append(me.$sibling_group);
+
+ 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) {
+ const is_same_node = (this.selected_node && this.selected_node.id === node.id);
+ this.set_selected_node(node);
+ this.show_active_path(node);
+
+ if (this.$sibling_group) {
+ const sibling_parent = this.$sibling_group.find('.node-group').attr('data-parent');
+ if (node.parent_id !== sibling_parent)
+ this.$sibling_group.empty();
+ }
+
+ if (!is_same_node) {
+ // since the previous/parent node collapses, all connections to that node need to be rebuilt
+ // rebuild outgoing connections of parent
+ this.refresh_connectors(node.parent_id, node.id);
+
+ // rebuild incoming connections of parent
+ let grandparent = $(`#${node.parent_id}`).attr('data-parent');
+ this.refresh_connectors(grandparent, node.parent_id);
+ }
+
+ if (node.expandable && !node.expanded) {
+ return this.load_children(node);
+ }
+ }
+
+ collapse_node() {
+ let node = this.selected_node;
+ if (node.expandable) {
+ node.$children.hide();
+ node.expanded = false;
+
+ // add a collapsed level to show the collapsed parent
+ // and a button beside it to move to that level
+ let node_parent = node.$link.parent();
+ node_parent.prepend(
+ ``
+ );
+
+ node_parent
+ .find('.collapsed-level')
+ .append(node.$link);
+
+ frappe.run_serially([
+ () => this.get_child_nodes(node.parent_id, node.id),
+ (child_nodes) => this.get_node_group(child_nodes, node.parent_id),
+ (node_group) => {
+ node_parent.find('.collapsed-level')
+ .append(node_group);
+ },
+ () => this.setup_node_group_action()
+ ]);
+ }
+ }
+
+ 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, exclude_node=null) {
+ let me = this;
+ return new Promise(resolve => {
+ frappe.call({
+ method: this.method,
+ args: {
+ parent: node_id,
+ company: me.company,
+ exclude_node: exclude_node
+ },
+ callback: (r) => {
+ resolve(r.message);
+ }
+ });
+ });
+ }
+
+ render_child_nodes(node, child_nodes) {
+ if (!node.$children) {
+ node.$children = $('')
+ .hide()
+ .appendTo(node.$link.parent());
+
+ node.$children.empty();
+
+ if (child_nodes) {
+ $.each(child_nodes, (_i, data) => {
+ this.add_node(node, data);
+ $(`#${data.name}`).addClass('active-child');
+
+ setTimeout(() => {
+ this.add_connector(node.id, data.name);
+ }, 250);
+ });
+ }
+ }
+
+ node.$children.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');
+
+ let connector = undefined;
+
+ if ($(`#${parent_id}`).hasClass('active')) {
+ connector = this.get_connector_for_active_node(parent_node, child_node);
+ } else if ($(`#${parent_id}`).hasClass('active-path')) {
+ connector = this.get_connector_for_collapsed_node(parent_node, child_node);
+ }
+
+ path.setAttribute("d", connector);
+ this.set_path_attributes(path, parent_id, child_id);
+
+ $('#connectors').append(path);
+ }
+
+ get_connector_for_active_node(parent_node, child_node) {
+ // we need to connect the bottom left of the parent to the left side of the child node
+ let pos_parent_bottom = {
+ x: parent_node.offsetLeft + 20,
+ y: parent_node.offsetTop + parent_node.offsetHeight
+ };
+ let pos_child_left = {
+ x: child_node.offsetLeft - 5,
+ y: child_node.offsetTop + child_node.offsetHeight / 2
+ };
+
+ let connector =
+ "M" +
+ (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " +
+ "L" +
+ (pos_parent_bottom.x) + "," + (pos_child_left.y) + " " +
+ "L" +
+ (pos_child_left.x) + "," + (pos_child_left.y);
+
+ return connector;
+ }
+
+ get_connector_for_collapsed_node(parent_node, child_node) {
+ // we need to connect the bottom left of the parent to the top left of the child node
+ let pos_parent_bottom = {
+ x: parent_node.offsetLeft + 20,
+ y: parent_node.offsetTop + parent_node.offsetHeight
+ };
+ let pos_child_top = {
+ x: child_node.offsetLeft + 20,
+ y: child_node.offsetTop
+ };
+
+ let connector =
+ "M" +
+ (pos_parent_bottom.x) + "," + (pos_parent_bottom.y) + " " +
+ "L" +
+ (pos_child_top.x) + "," + (pos_child_top.y);
+
+ return connector;
+ }
+
+ set_path_attributes(path, parent_id, child_id) {
+ 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");
+ }
+ }
+
+ 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');
+ }
+
+ setup_node_click_action(node) {
+ let me = this;
+ let node_element = $(`#${node.id}`);
+
+ node_element.click(function() {
+ if (node_element.is(':visible') && node_element.hasClass('active-path')) {
+ me.remove_levels_after_node(node);
+ me.remove_orphaned_connectors();
+ } else {
+ me.add_node_to_hierarchy(node, true);
+ me.collapse_node();
+ }
+
+ me.expand_node(node);
+ });
+ }
+
+ setup_node_group_action() {
+ let me = this;
+
+ $('.node-group').on('click', function() {
+ let parent = $(this).attr('data-parent');
+ me.expand_sibling_group_node(parent);
+ });
+ }
+
+ add_node_to_hierarchy(node) {
+ this.$hierarchy.append(`
+
+
+
+
+ `);
+
+ node.$link.appendTo(this.$hierarchy.find('.level:last'));
+ }
+
+ get_node_group(nodes, parent, collapsed=true) {
+ let limit = 2;
+ const display_nodes = nodes.slice(0, limit);
+ const extra_nodes = nodes.slice(limit);
+
+ let html = display_nodes.map(node =>
+ this.get_avatar(node)
+ ).join('');
+
+ if (extra_nodes.length === 1) {
+ let node = extra_nodes[0];
+ html += this.get_avatar(node);
+ } else if (extra_nodes.length > 1) {
+ html = `
+ ${html}
+
+
+ +${extra_nodes.length}
+
+
+ `;
+ }
+
+ if (html) {
+ const $node_group =
+ $(``);
+
+ if (collapsed)
+ $node_group.addClass('collapsed');
+
+ return $node_group;
+ }
+
+ return null;
+ }
+
+ get_avatar(node) {
+ return `
+
+ `
+ }
+
+ expand_sibling_group_node(parent) {
+ let node_object = this.nodes[parent];
+ let node = node_object.$link;
+ node.removeClass('active-child active-path');
+ node_object.expanded = 0;
+ node_object.$children = undefined;
+
+ // show parent's siblings and expand parent node
+ frappe.run_serially([
+ () => this.get_child_nodes(node_object.parent_id, node_object.id),
+ (child_nodes) => this.get_node_group(child_nodes, node_object.parent_id, false),
+ (node_group) => {
+ if (node_group)
+ this.$sibling_group.empty().append(node_group);
+ },
+ () => this.setup_node_group_action(),
+ () => {
+ this.$hierarchy.empty().append(`
+
+ `);
+ this.$hierarchy.find('.level').append(node);
+ $(`#connectors`).empty();
+ this.expand_node(node_object);
+ }
+ ]);
+ }
+
+ remove_levels_after_node(node) {
+ let level = $(`#${node.id}`).parent().parent();
+
+ level = $('.hierarchy-mobile > li:eq('+ (level.index()) + ')');
+ level.nextAll('li').remove();
+
+ let current_node = level.find(`#${node.id}`);
+ let node_object = this.nodes[node.id];
+
+ current_node.removeClass('active-child active-path');
+ node_object.expanded = 0;
+ node_object.$children = undefined;
+
+ level.empty().append(current_node);
+ }
+
+ 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();
+ })
+ }
+
+ refresh_connectors(node_parent, node_id) {
+ if (!node_parent) return;
+
+ $(`path[data-parent="${node_parent}"]`).remove();
+ this.add_connector(node_parent, node_id);
+ }
+}
\ No newline at end of file
diff --git a/erpnext/hr/page/organizational_chart/node_card.html b/erpnext/public/js/templates/node_card.html
similarity index 100%
rename from erpnext/hr/page/organizational_chart/node_card.html
rename to erpnext/public/js/templates/node_card.html
diff --git a/erpnext/public/scss/organizational_chart.scss b/erpnext/public/scss/hierarchy_chart.scss
similarity index 100%
rename from erpnext/public/scss/organizational_chart.scss
rename to erpnext/public/scss/hierarchy_chart.scss