From 2f6053b6ab0d3e6a6eaeb448ce7707ac4c7ccdb3 Mon Sep 17 00:00:00 2001 From: Alex Swindler Date: Wed, 28 Feb 2024 14:05:36 -0700 Subject: [PATCH] Several AH bug fixes (#4546) Several AH fixes --- .../commands/create_default_user.py | 2 +- .../seed/js/controllers/admin_controller.js | 71 +- .../goal_editor_modal_controller.js | 263 ++- .../inventory_detail_controller.js | 2 +- .../new_member_modal_controller.js | 15 +- ..._access_level_instance_modal_controller.js | 23 +- .../portfolio_summary_controller.js | 1905 ++++++++--------- seed/static/seed/js/seed.js | 41 +- seed/static/seed/js/services/ah_service.js | 34 + .../seed/partials/inventory_detail.html | 4 +- seed/templates/seed/_header.html | 6 +- seed/templates/seed/_scripts.html | 1 + seed/utils/inventory_filter.py | 6 +- seed/views/v3/organization_users.py | 3 +- 14 files changed, 1178 insertions(+), 1198 deletions(-) create mode 100644 seed/static/seed/js/services/ah_service.js diff --git a/seed/management/commands/create_default_user.py b/seed/management/commands/create_default_user.py index eb8497f612..b60e56fd92 100644 --- a/seed/management/commands/create_default_user.py +++ b/seed/management/commands/create_default_user.py @@ -73,7 +73,7 @@ def handle(self, *args, **options): self.stdout.write( 'Org <%s> already exists, adding user' % options['organization'], ending='\n' ) - org.add_member(u, ROLE_OWNER) + org.add_member(u, org.root.id, ROLE_OWNER) else: self.stdout.write( 'Creating org <%s> ...' % options['organization'], diff --git a/seed/static/seed/js/controllers/admin_controller.js b/seed/static/seed/js/controllers/admin_controller.js index d17e944a59..959e68517f 100644 --- a/seed/static/seed/js/controllers/admin_controller.js +++ b/seed/static/seed/js/controllers/admin_controller.js @@ -10,6 +10,7 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ 'organization_service', 'column_mappings_service', 'uploader_service', + 'ah_service', 'auth_payload', 'organizations_payload', 'user_profile_payload', @@ -26,6 +27,7 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ organization_service, column_mappings_service, uploader_service, + ah_service, auth_payload, organizations_payload, user_profile_payload, @@ -66,16 +68,7 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ value: 'viewer' }]; - /* Build out access_level_instances_by_depth recursively */ let access_level_instances_by_depth = {}; - const calculate_access_level_instances_by_depth = (tree, depth = 1) => { - if (tree === undefined) return; - if (access_level_instances_by_depth[depth] === undefined) access_level_instances_by_depth[depth] = []; - for (const ali of tree) { - access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }); - calculate_access_level_instances_by_depth(ali.children, depth + 1); - } - }; $scope.change_selected_level_index = () => { const new_level_instance_depth = parseInt($scope.level_name_index, 10) + 1; @@ -89,6 +82,24 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ }; $scope.update_alert = update_alert; + const get_users = () => { + user_service.get_users().then((data) => { + $scope.org.users = data.users; + }); + }; + + const process_organizations = (data) => { + $scope.org_user.organizations = data.organizations; + _.forEach($scope.org_user.organizations, (org) => { + org.total_inventory = _.reduce(org.cycles, (sum, cycle) => sum + cycle.num_properties + cycle.num_taxlots, 0); + }); + }; + + const get_organizations = () => organization_service.get_organizations().then(process_organizations).catch((response) => { + $log.log({ message: 'error from data call', status: response.status, data: response.data }); + update_alert(false, `error getting organizations: ${response.data.message}`); + }); + $scope.org_form.new_org = () => { $scope.user.organization = undefined; $scope.user.access_level_instance_id = undefined; @@ -101,27 +112,27 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ organization_service.get_organization_access_level_tree($scope.user.organization.id).then((access_level_tree) => { $scope.level_names = access_level_tree.access_level_names; - access_level_instances_by_depth = {}; - calculate_access_level_instances_by_depth(access_level_tree.access_level_tree, 1); + access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth(access_level_tree.access_level_tree); }); }; $scope.org_form.reset = () => { + $scope.user = { role: $scope.roles[0].value }; $scope.org.user_email = ''; $scope.org.name = ''; }; $scope.org_form.add = (org) => { organization_service .add(org) - .then(() => { - get_organizations().then(() => { - $scope.$emit('organization_list_updated'); - }); + .then(async () => { + await get_organizations(); + $scope.$emit('organization_list_updated'); update_alert(true, `Organization ${org.name} created`); }) .catch((response) => { update_alert(false, `error creating organization: ${response.data.message}`); }); }; + $scope.user_form.add = (user) => { user_service .add(user) @@ -144,17 +155,17 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ update_alert(false, `error creating user: ${response.data.message}`); }); }; - $scope.org_form.not_ready = () => _.isUndefined($scope.org.email) || organization_exists($scope.org.name); - - var organization_exists = (name) => { + const organization_exists = (name) => { const orgs = _.map($scope.org_user.organizations, (org) => org.name.toLowerCase()); return orgs.includes(name.toLowerCase()); }; + $scope.org_form.not_ready = () => $scope.org.email === undefined || organization_exists($scope.org.name); + $scope.user_form.not_ready = () => !$scope.user.organization && !$scope.user.org_name; $scope.user_form.reset = () => { - $scope.user = { role: $scope.roles[1].value }; + $scope.user = { role: $scope.roles[0].value }; $scope.level_names = []; }; @@ -162,24 +173,6 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ $scope.user_form.reset(); - var get_users = () => { - user_service.get_users().then((data) => { - $scope.org.users = data.users; - }); - }; - - const process_organizations = (data) => { - $scope.org_user.organizations = data.organizations; - _.forEach($scope.org_user.organizations, (org) => { - org.total_inventory = _.reduce(org.cycles, (sum, cycle) => sum + cycle.num_properties + cycle.num_taxlots, 0); - }); - }; - - var get_organizations = () => organization_service.get_organizations().then(process_organizations, (response) => { - $log.log({ message: 'error from data call', status: response.status, data: response.data }); - update_alert(false, `error getting organizations: ${response.data.message}`); - }); - $scope.get_organizations_users = (org) => { if (org) { organization_service @@ -227,6 +220,7 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ }; $scope.confirm_column_mappings_delete = (org) => { + // eslint-disable-next-line no-restricted-globals,no-alert const yes = confirm(`Are you sure you want to delete the '${org.name}' column mappings? This will invalidate preexisting mapping review data`); if (yes) { $scope.delete_org_column_mappings(org); @@ -250,6 +244,7 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ * for an org's inventory. */ $scope.confirm_inventory_delete = (org) => { + // eslint-disable-next-line no-restricted-globals,no-alert const yes = confirm(`Are you sure you want to PERMANENTLY delete '${org.name}'s properties and tax lots?`); if (yes) { $scope.delete_org_inventory(org); @@ -280,8 +275,10 @@ angular.module('BE.seed.controller.admin', []).controller('admin_controller', [ }; $scope.confirm_org_delete = (org) => { + // eslint-disable-next-line no-restricted-globals const yes = confirm(`Are you sure you want to PERMANENTLY delete the entire '${org.name}' organization?`); if (yes) { + // eslint-disable-next-line no-restricted-globals const again = confirm(`Deleting an organization is permanent. Confirm again to delete '${org.name}'`); if (again) { $scope.delete_org(org); diff --git a/seed/static/seed/js/controllers/goal_editor_modal_controller.js b/seed/static/seed/js/controllers/goal_editor_modal_controller.js index 9c1d110fa9..0a52b5fa70 100644 --- a/seed/static/seed/js/controllers/goal_editor_modal_controller.js +++ b/seed/static/seed/js/controllers/goal_editor_modal_controller.js @@ -3,158 +3,135 @@ * See also https://github.com/seed-platform/seed/main/LICENSE.md */ angular.module('BE.seed.controller.goal_editor_modal', []) - .controller('goal_editor_modal_controller', [ - '$scope', - '$state', - '$stateParams', - '$uibModalInstance', - 'Notification', - 'goal_service', - 'access_level_tree', - 'area_columns', - 'auth_payload', - 'cycles', - 'eui_columns', - 'goal', - 'organization', - 'write_permission', - function ( - $scope, - $state, - $stateParams, - $uibModalInstance, - Notification, - goal_service, - access_level_tree, - area_columns, - auth_payload, - cycles, - eui_columns, - goal, - organization, - write_permission, - ) { - $scope.auth = auth_payload.auth; - $scope.organization = organization; - $scope.write_permission = write_permission; - $scope.goal = goal || {}; - $scope.access_level_tree = access_level_tree.access_level_tree; - $scope.all_level_names = [] - access_level_tree.access_level_names.forEach((level, i) => $scope.all_level_names.push({index: i, name: level})) - $scope.cycles = cycles; - $scope.area_columns = area_columns; - $scope.eui_columns = eui_columns; - // allow "none" as an option - if (!eui_columns.find(c => c.id === null && c.displayName === '')) { - $scope.eui_columns.unshift({ id: null, displayName: '' }); - } - $scope.valid = false; + .controller('goal_editor_modal_controller', [ + '$scope', + '$state', + '$stateParams', + '$uibModalInstance', + 'Notification', + 'ah_service', + 'goal_service', + 'access_level_tree', + 'area_columns', + 'auth_payload', + 'cycles', + 'eui_columns', + 'goal', + 'organization', + 'write_permission', + // eslint-disable-next-line func-names + function ( + $scope, + $state, + $stateParams, + $uibModalInstance, + Notification, + ah_service, + goal_service, + access_level_tree, + area_columns, + auth_payload, + cycles, + eui_columns, + goal, + organization, + write_permission + ) { + $scope.auth = auth_payload.auth; + $scope.organization = organization; + $scope.write_permission = write_permission; + $scope.goal = goal || {}; + $scope.access_level_tree = access_level_tree.access_level_tree; + $scope.level_names = access_level_tree.access_level_names.map((level, i) => ({ + index: i, + name: level + })); + $scope.cycles = cycles; + $scope.area_columns = area_columns; + $scope.eui_columns = eui_columns; + // allow "none" as an option + if (!eui_columns.find((col) => col.id === null && col.displayName === '')) { + $scope.eui_columns.unshift({ id: null, displayName: '' }); + } + $scope.valid = false; - const get_goals = () => { - goal_service.get_goals().then(result => { - $scope.goals = result.status == 'success' ? sort_goals(result.goals) : []; - }) - } - const sort_goals = (goals) => goals.sort((a, b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1) - get_goals() + const sort_goals = (goals) => goals.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); + const get_goals = () => { + goal_service.get_goals().then((result) => { + $scope.goals = result.status === 'success' ? sort_goals(result.goals) : []; + }); + }; + get_goals(); - $scope.$watch('goal', (cur, old) => { - $scope.goal_changed = cur != old; - }, true) + $scope.$watch('goal', (cur, old) => { + $scope.goal_changed = cur != old; + }, true); - // ACCESS LEVEL INSTANCES - // Users do not have permissions to create goals on levels above their own in the tree - const remove_restricted_level_names = (user_ali) => { - const path_keys = Object.keys(user_ali.data.path) - $scope.level_names = [] - const reversed_names = $scope.all_level_names.slice().reverse() - for (let index in reversed_names) { - $scope.level_names.push(reversed_names[index]) - if (path_keys.includes(reversed_names[index].name)) { - break - } - } - $scope.level_names.reverse() - } - - /* Build out access_level_instances_by_depth recurrsively */ - let access_level_instances_by_depth = {}; - const calculate_access_level_instances_by_depth = (tree, depth = 1) => { - if (tree == undefined) return; - if (access_level_instances_by_depth[depth] == undefined) access_level_instances_by_depth[depth] = []; - tree.forEach(ali => { - if (ali.id == window.BE.access_level_instance_id) remove_restricted_level_names(ali) - access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }); - calculate_access_level_instances_by_depth(ali.children, depth + 1); - }) - } - calculate_access_level_instances_by_depth($scope.access_level_tree, 1); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); - $scope.change_selected_level_index = () => { - new_level_instance_depth = parseInt($scope.goal.level_name_index) + 1 - $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; - } - $scope.change_selected_level_index() + $scope.change_selected_level_index = () => { + const new_level_instance_depth = parseInt($scope.goal.level_name_index, 10) + 1; + $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; + }; + $scope.change_selected_level_index(); - $scope.set_goal = (goal) => { - $scope.goal = goal; - $scope.change_selected_level_index();; - } + $scope.set_goal = (goal) => { + $scope.goal = goal; + $scope.change_selected_level_index(); + }; - $scope.save_goal = () => { - $scope.goal_changed = false; - const goal_fn = $scope.goal.id ? goal_service.update_goal : goal_service.create_goal - // if new goal, assign org id - $scope.goal.organization = $scope.goal.organization || $scope.organization.id - goal_fn($scope.goal).then(result => { - if (result.status === 200 || result.status === 201) { - Notification.success({ message: 'Goal saved', delay: 5000 }); - $scope.errors = null; - $scope.goal.id = $scope.goal.id || result.data.id; - get_goals() - $scope.set_goal($scope.goal) - } else { - $scope.errors = [`Unexpected response status: ${result.status}`]; - let result_errors = 'errors' in result.data ? result.data.errors : result.data - if (result_errors instanceof Object) { - for (let key in result_errors) { - let key_string = key == 'non_field_errors' ? 'Error' : key; - $scope.errors.push(`${key_string}: ${JSON.stringify(result_errors[key])}`) - } - } else { - $scope.errors = $scope.errors.push(result_errors) - } - }; - }); + $scope.save_goal = () => { + $scope.goal_changed = false; + const goal_fn = $scope.goal.id ? goal_service.update_goal : goal_service.create_goal; + // if new goal, assign org id + $scope.goal.organization = $scope.goal.organization || $scope.organization.id; + goal_fn($scope.goal).then((result) => { + if (result.status === 200 || result.status === 201) { + Notification.success({ message: 'Goal saved', delay: 5000 }); + $scope.errors = null; + $scope.goal.id = $scope.goal.id || result.data.id; + get_goals(); + $scope.set_goal($scope.goal); + } else { + $scope.errors = [`Unexpected response status: ${result.status}`]; + const result_errors = 'errors' in result.data ? result.data.errors : result.data; + if (result_errors instanceof Object) { + for (const key in result_errors) { + const key_string = key == 'non_field_errors' ? 'Error' : key; + $scope.errors.push(`${key_string}: ${JSON.stringify(result_errors[key])}`); + } + } else { + $scope.errors = $scope.errors.push(result_errors); } + } + }); + }; - $scope.delete_goal = (goal_id) => { - const goal = $scope.goals.find(goal => goal.id === goal_id) - if (!goal) return Notification.warning({ message: 'Unexpected Error', delay: 5000 }) + $scope.delete_goal = (goal_id) => { + const goal = $scope.goals.find((goal) => goal.id === goal_id); + if (!goal) return Notification.warning({ message: 'Unexpected Error', delay: 5000 }); - if (!confirm(`Are you sure you want to delete Goal "${goal.name}"`)) return + if (!confirm(`Are you sure you want to delete Goal "${goal.name}"`)) return; - goal_service.delete_goal(goal_id).then((response) => { - if (response.status === 204) { - Notification.success({ message: 'Goal deleted successfully', delay: 5000 }); - } else { - Notification.warning({ message: 'Unexpected Error', delay: 5000 }) - } - get_goals() - if (goal_id == $scope.goal.id) { - $scope.goal = null; - } - }) - } + goal_service.delete_goal(goal_id).then((response) => { + if (response.status === 204) { + Notification.success({ message: 'Goal deleted successfully', delay: 5000 }); + } else { + Notification.warning({ message: 'Unexpected Error', delay: 5000 }); + } + get_goals(); + if (goal_id == $scope.goal.id) { + $scope.goal = null; + } + }); + }; - $scope.new_goal = () => { - $scope.goal = {}; - } + $scope.new_goal = () => { + $scope.goal = {}; + }; - $scope.close = () => { - let goal_name = $scope.goal ? $scope.goal.name : null; - $uibModalInstance.close(goal_name) - } - } - ] -) + $scope.close = () => { + const goal_name = $scope.goal ? $scope.goal.name : null; + $uibModalInstance.close(goal_name); + }; + }]); diff --git a/seed/static/seed/js/controllers/inventory_detail_controller.js b/seed/static/seed/js/controllers/inventory_detail_controller.js index b752ac64b2..3527d90395 100644 --- a/seed/static/seed/js/controllers/inventory_detail_controller.js +++ b/seed/static/seed/js/controllers/inventory_detail_controller.js @@ -125,7 +125,7 @@ angular.module('BE.seed.controller.inventory_detail', []).controller('inventory_ inventory_payload.taxlot.access_level_instance; $scope.ali_path = {}; - if (typeof (ali) === 'object') { + if (typeof ali === 'object') { $scope.ali_path = ali.path; } diff --git a/seed/static/seed/js/controllers/new_member_modal_controller.js b/seed/static/seed/js/controllers/new_member_modal_controller.js index 16cfb1273c..af4e7daa26 100644 --- a/seed/static/seed/js/controllers/new_member_modal_controller.js +++ b/seed/static/seed/js/controllers/new_member_modal_controller.js @@ -6,30 +6,21 @@ angular.module('BE.seed.controller.new_member_modal', []).controller('new_member '$scope', '$uibModalInstance', 'organization', + 'ah_service', 'user_service', '$timeout', '$translate', 'access_level_tree', 'level_names', // eslint-disable-next-line func-names - function ($scope, $uibModalInstance, organization, user_service, $timeout, $translate, access_level_tree, level_names) { + function ($scope, $uibModalInstance, organization, ah_service, user_service, $timeout, $translate, access_level_tree, level_names) { $scope.access_level_tree = access_level_tree; $scope.level_names = level_names; $scope.level_name_index = null; $scope.potential_level_instances = []; $scope.error_message = null; - /* Build out access_level_instances_by_depth recursively */ - const access_level_instances_by_depth = {}; - const calculate_access_level_instances_by_depth = (tree, depth = 1) => { - if (tree === undefined) return; - if (access_level_instances_by_depth[depth] === undefined) access_level_instances_by_depth[depth] = []; - for (const ali of tree) { - access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }); - calculate_access_level_instances_by_depth(ali.children, depth + 1); - } - }; - calculate_access_level_instances_by_depth(access_level_tree, 1); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth(access_level_tree); $scope.change_selected_level_index = () => { const new_level_instance_depth = parseInt($scope.level_name_index, 10) + 1; diff --git a/seed/static/seed/js/controllers/organization_add_access_level_instance_modal_controller.js b/seed/static/seed/js/controllers/organization_add_access_level_instance_modal_controller.js index 00394a25ac..bd47439612 100644 --- a/seed/static/seed/js/controllers/organization_add_access_level_instance_modal_controller.js +++ b/seed/static/seed/js/controllers/organization_add_access_level_instance_modal_controller.js @@ -7,15 +7,18 @@ angular.module('BE.seed.controller.organization_add_access_level_instance_modal' '$scope', '$state', '$uibModalInstance', + 'ah_service', 'organization_service', 'org_id', 'level_names', 'access_level_tree', 'Notification', + // eslint-disable-next-line func-names function ( $scope, $state, $uibModalInstance, + ah_service, organization_service, org_id, level_names, @@ -28,19 +31,9 @@ angular.module('BE.seed.controller.organization_add_access_level_instance_modal' $scope.potential_parents = []; $scope.new_level_instance_name = ''; - /* Build out access_level_instances_by_depth recursively */ - const access_level_instances_by_depth = {}; - const calculate_access_level_instances_by_depth = function (tree, depth = 1) { - if (tree == undefined) return; - if (access_level_instances_by_depth[depth] == undefined) access_level_instances_by_depth[depth] = []; - tree.forEach((ali) => { - access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }); - calculate_access_level_instances_by_depth(ali.children, depth + 1); - }); - }; - calculate_access_level_instances_by_depth(access_level_tree, 1); + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth(access_level_tree); - $scope.change_selected_level_index = function () { + $scope.change_selected_level_index = () => { const new_level_instance_depth = parseInt($scope.selected_level_index, 10); $scope.potential_parents = access_level_instances_by_depth[new_level_instance_depth]; $scope.parent = null; @@ -52,13 +45,13 @@ angular.module('BE.seed.controller.organization_add_access_level_instance_modal' $scope.change_selected_level_index(); } - $scope.create_new_level_instance = function () { + $scope.create_new_level_instance = () => { organization_service.create_organization_access_level_instance(org_id, $scope.parent.id, $scope.new_level_instance_name) - .then((_) => $uibModalInstance.close()) + .then(() => $uibModalInstance.close()) .catch((err) => { Notification.error(err); }); }; - $scope.cancel = function () { + $scope.cancel = () => { $uibModalInstance.dismiss('cancel'); }; }]); diff --git a/seed/static/seed/js/controllers/portfolio_summary_controller.js b/seed/static/seed/js/controllers/portfolio_summary_controller.js index 7b7dbe9905..a0bf8ccc74 100644 --- a/seed/static/seed/js/controllers/portfolio_summary_controller.js +++ b/seed/static/seed/js/controllers/portfolio_summary_controller.js @@ -3,972 +3,967 @@ * See also https://github.com/seed-platform/seed/main/LICENSE.md */ angular.module('BE.seed.controller.portfolio_summary', []) - .controller('portfolio_summary_controller', [ - '$scope', - '$state', - '$stateParams', - '$uibModal', - 'urls', - 'inventory_service', - 'label_service', - 'goal_service', - 'cycles', - 'organization_payload', - 'access_level_tree', - 'auth_payload', - 'property_columns', - 'uiGridConstants', - 'gridUtil', - 'spinner_utility', - function ( - $scope, - $state, - $stateParams, - $uibModal, - urls, - inventory_service, - label_service, - goal_service, - cycles, - organization_payload, - access_level_tree, - auth_payload, - property_columns, - uiGridConstants, - gridUtil, - spinner_utility, - ) { - $scope.organization = organization_payload.organization; - $scope.write_permission = $scope.menu.user.is_ali_root || !$scope.menu.user.is_ali_leaf - // Ii there a better way to convert string units to displayUnits? - const area_units = $scope.organization.display_units_area.replace('**2', '²'); - const eui_units = $scope.organization.display_units_eui.replace('**2', '²'); - $scope.cycles = cycles.cycles; - $scope.access_level_tree = access_level_tree.access_level_tree; - $scope.level_names = access_level_tree.access_level_names; - const localStorageLabelKey = `grid.properties.labels`; - $scope.goal = {}; - $scope.columns = property_columns; - $scope.cycle_columns = []; - $scope.area_columns = []; - $scope.eui_columns = []; - let matching_column_names = []; - let table_column_ids = []; - - const initialize_columns = () => { - $scope.columns.forEach(c => { - const default_display = c.column_name == $scope.organization.property_display_field; - const matching = c.is_matching_criteria; - const area = c.data_type === 'area'; - const eui = c.data_type === 'eui'; - const other = ['property_name', 'property_type', 'year_built'].includes(c.column_name); - - if (default_display || matching || eui || area || other ) table_column_ids.push(c.id); - if (eui) $scope.eui_columns.push(c); - if (area) $scope.area_columns.push(c); - if (matching) matching_column_names.push(c.column_name); - }) - } - initialize_columns() - - // Can only sort based on baseline or current, not both. In the event of a conflict, use the more recent. - baseline_first = false - - // optionally pass a goal name to be set as $scope.goal - used on modal close - const get_goals = (goal_name=false) => { - goal_service.get_goals().then(result => { - $scope.goals = _.isEmpty(result.goals) ? [] : sort_goals(result.goals) - $scope.goal = goal_name ? - $scope.goals.find(goal => goal.name == goal_name) : - $scope.goals[0] - }) - } - const sort_goals = (goals) => goals.sort((a,b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1) - get_goals() - - // If goal changes, reset grid filters and repopulate ui-grids - $scope.$watch('goal', () => { - if ($scope.gridApi) $scope.reset_sorts_filters(); - $scope.data_valid = false; - if (_.isEmpty($scope.goal)) { - $scope.valid = false; - $scope.summary_valid = false; - } else { - reset_data(); - } - }) - - const reset_data = () => { - $scope.valid = true; - format_goal_details(); - $scope.refresh_data(); - } - - // selected goal details - const format_goal_details = () => { - $scope.change_selected_level_index() - const get_column_name = (column_id) => $scope.columns.find(c => c.id == column_id).displayName - const get_cycle_name = (cycle_id) => $scope.cycles.find(c => c.id == cycle_id).name - const level_name = $scope.level_names[$scope.goal.level_name_index] - const access_level_instance = $scope.potential_level_instances.find(level => level.id == $scope.goal.access_level_instance).name - - $scope.goal_details = [ - ['Baseline Cycle', get_cycle_name($scope.goal.baseline_cycle)], - ['Current Cycle', get_cycle_name($scope.goal.current_cycle)], - [level_name, access_level_instance], - ['Portfolio Target', `${$scope.goal.target_percentage} %`], - ['Area Column', get_column_name($scope.goal.area_column)], - ['Primary EUI', get_column_name($scope.goal.eui_column1)], - ] - if ($scope.goal.eui_column2) { - $scope.goal_details.push(['Secondary EUI', get_column_name($scope.goal.eui_column2)]) - } - if ($scope.goal.eui_column3) { - $scope.goal_details.push(['Tertiary EUI', get_column_name($scope.goal.eui_column3)]) - } - } - - // from inventory_list_controller - $scope.columnDisplayByName = {}; - for (const i in $scope.columns) { - $scope.columnDisplayByName[$scope.columns[i].name] = $scope.columns[i].displayName; - } - - // Build out access_level_instances_by_depth recurrsively - let access_level_instances_by_depth = {}; - const calculate_access_level_instances_by_depth = function (tree, depth = 1) { - if (tree == undefined) return; - if (access_level_instances_by_depth[depth] == undefined) access_level_instances_by_depth[depth] = []; - tree.forEach(ali => { - access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }) - calculate_access_level_instances_by_depth(ali.children, depth + 1); - }) - } - calculate_access_level_instances_by_depth($scope.access_level_tree, 1) - - $scope.change_selected_level_index = function () { - new_level_instance_depth = parseInt($scope.goal.level_name_index) + 1 - $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth] - } - - // GOAL EDITOR MODAL - $scope.open_goal_editor_modal = () => { - const modalInstance = $uibModal.open({ - templateUrl: `${urls.static_url}seed/partials/goal_editor_modal.html`, - controller: 'goal_editor_modal_controller', - size: 'lg', - backdrop: 'static', - resolve: { - access_level_tree: () => access_level_tree, - area_columns: () => $scope.area_columns, - auth_payload: () => auth_payload, - cycles: () => $scope.cycles, - eui_columns: () => $scope.eui_columns, - goal: () => $scope.goal, - organization: () => $scope.organization, - write_permission: () => $scope.write_permission, - }, - }); - - // on modal close - modalInstance.result.then((goal_name) => { - get_goals(goal_name) - }) + .controller('portfolio_summary_controller', [ + '$scope', + '$state', + '$stateParams', + '$uibModal', + 'urls', + 'ah_service', + 'inventory_service', + 'label_service', + 'goal_service', + 'cycles', + 'organization_payload', + 'access_level_tree', + 'auth_payload', + 'property_columns', + 'uiGridConstants', + 'gridUtil', + 'spinner_utility', + // eslint-disable-next-line func-names + function ( + $scope, + $state, + $stateParams, + $uibModal, + urls, + ah_service, + inventory_service, + label_service, + goal_service, + cycles, + organization_payload, + access_level_tree, + auth_payload, + property_columns, + uiGridConstants, + gridUtil, + spinner_utility + ) { + $scope.organization = organization_payload.organization; + $scope.write_permission = $scope.menu.user.is_ali_root || !$scope.menu.user.is_ali_leaf; + // Ii there a better way to convert string units to displayUnits? + const area_units = $scope.organization.display_units_area.replace('**2', '²'); + const eui_units = $scope.organization.display_units_eui.replace('**2', '²'); + $scope.cycles = cycles.cycles; + $scope.access_level_tree = access_level_tree.access_level_tree; + $scope.level_names = access_level_tree.access_level_names; + const localStorageLabelKey = 'grid.properties.labels'; + $scope.goal = {}; + $scope.columns = property_columns; + $scope.cycle_columns = []; + $scope.area_columns = []; + $scope.eui_columns = []; + const matching_column_names = []; + const table_column_ids = []; + + const initialize_columns = () => { + $scope.columns.forEach((col) => { + const default_display = col.column_name === $scope.organization.property_display_field; + const matching = col.is_matching_criteria; + const area = col.data_type === 'area'; + const eui = col.data_type === 'eui'; + const other = ['property_name', 'property_type', 'year_built'].includes(col.column_name); + + if (default_display || matching || eui || area || other) table_column_ids.push(col.id); + if (eui) $scope.eui_columns.push(col); + if (area) $scope.area_columns.push(col); + if (matching) matching_column_names.push(col.column_name); + }); + }; + initialize_columns(); + + // Can only sort based on baseline or current, not both. In the event of a conflict, use the more recent. + let baseline_first = false; + + const sort_goals = (goals) => goals.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)); + // optionally pass a goal name to be set as $scope.goal - used on modal close + const get_goals = (goal_name = false) => { + goal_service.get_goals().then((result) => { + $scope.goals = _.isEmpty(result.goals) ? [] : sort_goals(result.goals); + $scope.goal = goal_name ? + $scope.goals.find((goal) => goal.name === goal_name) : + $scope.goals[0]; + }); + }; + get_goals(); + + const reset_data = () => { + $scope.valid = true; + format_goal_details(); + $scope.refresh_data(); + }; + + // If goal changes, reset grid filters and repopulate ui-grids + $scope.$watch('goal', () => { + if ($scope.gridApi) $scope.reset_sorts_filters(); + $scope.data_valid = false; + if (_.isEmpty($scope.goal)) { + $scope.valid = false; + $scope.summary_valid = false; + } else { + reset_data(); + } + }); + + // selected goal details + const format_goal_details = () => { + $scope.change_selected_level_index(); + const get_column_name = (column_id) => $scope.columns.find((col) => col.id === column_id).displayName; + const get_cycle_name = (cycle_id) => $scope.cycles.find((col) => col.id === cycle_id).name; + const level_name = $scope.level_names[$scope.goal.level_name_index]; + const access_level_instance = $scope.potential_level_instances.find((level) => level.id === $scope.goal.access_level_instance).name; + + $scope.goal_details = [ + ['Baseline Cycle', get_cycle_name($scope.goal.baseline_cycle)], + ['Current Cycle', get_cycle_name($scope.goal.current_cycle)], + [level_name, access_level_instance], + ['Portfolio Target', `${$scope.goal.target_percentage} %`], + ['Area Column', get_column_name($scope.goal.area_column)], + ['Primary EUI', get_column_name($scope.goal.eui_column1)] + ]; + if ($scope.goal.eui_column2) { + $scope.goal_details.push(['Secondary EUI', get_column_name($scope.goal.eui_column2)]); + } + if ($scope.goal.eui_column3) { + $scope.goal_details.push(['Tertiary EUI', get_column_name($scope.goal.eui_column3)]); + } + }; + + // from inventory_list_controller + $scope.columnDisplayByName = {}; + for (const i in $scope.columns) { + $scope.columnDisplayByName[$scope.columns[i].name] = $scope.columns[i].displayName; + } + + const access_level_instances_by_depth = ah_service.calculate_access_level_instances_by_depth($scope.access_level_tree); + + $scope.change_selected_level_index = () => { + const new_level_instance_depth = parseInt($scope.goal.level_name_index, 10) + 1; + $scope.potential_level_instances = access_level_instances_by_depth[new_level_instance_depth]; + }; + + // GOAL EDITOR MODAL + $scope.open_goal_editor_modal = () => { + const modalInstance = $uibModal.open({ + templateUrl: `${urls.static_url}seed/partials/goal_editor_modal.html`, + controller: 'goal_editor_modal_controller', + size: 'lg', + backdrop: 'static', + resolve: { + access_level_tree: () => access_level_tree, + area_columns: () => $scope.area_columns, + auth_payload: () => auth_payload, + cycles: () => $scope.cycles, + eui_columns: () => $scope.eui_columns, + goal: () => $scope.goal, + organization: () => $scope.organization, + write_permission: () => $scope.write_permission + } + }); + + // on modal close + modalInstance.result.then((goal_name) => { + get_goals(goal_name); + }); + }; + + $scope.refresh_data = () => { + $scope.summary_loading = true; + load_summary(); + $scope.load_inventory(1); + }; + + const load_summary = () => { + $scope.show_access_level_instances = true; + $scope.summary_valid = false; + + goal_service.get_portfolio_summary($scope.goal.id).then((result) => { + const summary = result.data; + set_summary_grid_options(summary); + }).then(() => { + $scope.summary_loading = false; + $scope.summary_valid = true; + }); + }; + + $scope.page_change = (page) => { + spinner_utility.show(); + $scope.load_inventory(page); + }; + $scope.load_inventory = (page) => { + $scope.data_loading = true; + + const access_level_instance_id = $scope.goal.access_level_instance; + const combined_result = {}; + const per_page = 50; + const current_cycle = { id: $scope.goal.current_cycle }; + const baseline_cycle = { id: $scope.goal.baseline_cycle }; + // order of cycle property filter is dynamic based on column_sorts + const cycle_priority = baseline_first ? [baseline_cycle, current_cycle] : [current_cycle, baseline_cycle]; + + get_paginated_properties(page, per_page, cycle_priority[0], access_level_instance_id, true, null, $scope.goal.id).then((result0) => { + $scope.inventory_pagination = result0.pagination; + let properties = result0.results; + combined_result[cycle_priority[0].id] = properties; + const property_ids = properties.map((p) => p.id); + + get_paginated_properties(page, per_page, cycle_priority[1], access_level_instance_id, false, property_ids).then((result1) => { + properties = result1.results; + combined_result[cycle_priority[1].id] = properties; + get_all_labels(); + set_grid_options(combined_result); + }).then(() => { + $scope.data_loading = false; + $scope.data_valid = true; + }); + }); + }; + + const get_paginated_properties = (page, chunk, cycle, access_level_instance_id, include_filters_sorts, include_property_ids = null, goal_id = null) => { + const fn = inventory_service.get_properties; + const [filters, sorts] = include_filters_sorts ? [$scope.column_filters, $scope.column_sorts] : [[], []]; + + return fn( + page, + chunk, + cycle, + undefined, // profile_id + undefined, // include_view_ids + undefined, // exclude_view_ids + true, // save_last_cycle + $scope.organization.id, + true, // include_related + filters, + sorts, + false, // ids_only + table_column_ids.join(), + access_level_instance_id, + include_property_ids, + goal_id // optional param to retrieve goal note details + ); + }; + + const percentage = (a, b) => { + if (!a || b == null) return null; + const value = Math.round(((a - b) / a) * 100); + return Number.isNaN(value) ? null : value; + }; + + // -------------- LABEL LOGIC ------------- + + $scope.max_label_width = 750; + $scope.get_label_column_width = (labels_col, key) => { + if (!$scope.show_full_labels[key]) { + return 30; + } + let maxWidth = 0; + const renderContainer = document.body.getElementsByClassName('ui-grid-render-container-body')[1]; + const col = $scope.gridApi.grid.getColumn(labels_col); + const cells = renderContainer.querySelectorAll(`.${uiGridConstants.COL_CLASS_PREFIX}${col.uid} .ui-grid-cell-contents`); + Array.prototype.forEach.call(cells, (cell) => { + gridUtil.fakeElement(cell, {}, (newElm) => { + const e = angular.element(newElm); + e.attr('style', 'float: left;'); + const width = gridUtil.elementWidth(e); + if (width > maxWidth) { + maxWidth = width; } - - $scope.refresh_data = () => { - $scope.summary_loading = true; - load_summary(); - $scope.load_inventory(1); + }); + }); + return maxWidth > $scope.max_label_width ? $scope.max_label_width : maxWidth + 2; + }; + + // Expand or contract labels col + $scope.show_full_labels = { baseline: false, current: false }; + $scope.toggle_labels = (labels_col, key) => { + $scope.show_full_labels[key] = !$scope.show_full_labels[key]; + setTimeout(() => { + $scope.gridApi.grid.getColumn(labels_col).width = $scope.get_label_column_width(labels_col, key); + const icon = document.getElementById(`label-header-icon-${key}`); + icon.classList.add($scope.show_full_labels[key] ? 'fa-chevron-circle-left' : 'fa-chevron-circle-right'); + icon.classList.remove($scope.show_full_labels[key] ? 'fa-chevron-circle-right' : 'fa-chevron-circle-left'); + $scope.gridApi.grid.refresh(); + }, 0); + }; + + // retrieve labels for cycle + const get_labels = (key) => { + const cycle = key === 'baseline' ? $scope.goal.baseline_cycle : $scope.goal.current_cycle; + + label_service.get_labels('properties', undefined, cycle).then((current_labels) => { + const labels = _.filter(current_labels, (label) => !_.isEmpty(label.is_applied)); + + // load saved label filter + const ids = inventory_service.loadSelectedLabels(localStorageLabelKey); + // $scope.selected_labels = _.filter(labels, (label) => _.includes(ids, label.id)); + + if (key === 'baseline') { + $scope.baseline_labels = labels; + $scope.build_labels(key, $scope.baseline_labels); + } else { + $scope.current_labels = labels; + $scope.build_labels(key, $scope.current_labels); + } + }); + }; + const get_all_labels = () => { + get_labels('baseline'); + get_labels('current'); + }; + + // Find labels that should be displayed and organize by applied inventory id + $scope.show_labels_by_inventory_id = { baseline: {}, current: {} }; + $scope.build_labels = (key, labels) => { + $scope.show_labels_by_inventory_id[key] = {}; + for (const n in labels) { + const label = labels[n]; + if (label.show_in_list) { + for (const m in label.is_applied) { + const id = label.is_applied[m]; + const property_id = $scope.property_lookup[id]; + if (!$scope.show_labels_by_inventory_id[key][property_id]) { + $scope.show_labels_by_inventory_id[key][property_id] = []; + } + $scope.show_labels_by_inventory_id[key][property_id].push(label); } + } + } + }; + + // Builds the html to display labels associated with this row entity + $scope.display_labels = (entity, key) => { + const id = entity.id; + const labels = []; + const titles = []; + if ($scope.show_labels_by_inventory_id[key][id]) { + for (const i in $scope.show_labels_by_inventory_id[key][id]) { + const label = $scope.show_labels_by_inventory_id[key][id][i]; + labels.push('', $scope.show_full_labels[key] ? label.text : '', ''); + titles.push(label.text); + } + } + return ['', labels.join(''), ''].join(''); + }; + + // Build column defs for baseline or current labels + const build_label_col_def = (labels_col, key) => { + const header_cell_template = ``; + const cell_template = `
`; + const width_fn = $scope.gridApi ? $scope.get_label_column_width(labels_col, key) : 30; + + return { + name: labels_col, + displayName: '', + headerCellTemplate: header_cell_template, + cellTemplate: cell_template, + enableColumnMenu: false, + enableColumnMoving: false, + enableColumnResizing: false, + enableFiltering: false, + enableHiding: false, + enableSorting: false, + exporterSuppressExport: true, + // pinnedLeft: true, + visible: true, + width: width_fn, + maxWidth: $scope.max_label_width + }; + }; + + // ------------ DATA TABLE LOGIC --------- + + const set_eui_goal = (baseline, current, property, preferred_columns) => { + // only check defined columns + for (const col of preferred_columns.filter((c) => c)) { + if (baseline && property.baseline_eui == undefined) { + property.baseline_eui = baseline[col.name]; + } + if (current && property.current_eui == undefined) { + property.current_eui = current[col.name]; + } + } - const load_summary = () => { - $scope.show_access_level_instances = true; - $scope.summary_valid = false; - - goal_service.get_portfolio_summary($scope.goal.id).then(result => { - summary = result.data; - set_summary_grid_options(summary); - }).then(() => { - $scope.summary_loading = false; - $scope.summary_valid = true; - }) - } + property.baseline_kbtu = Math.round(property.baseline_sqft * property.baseline_eui) || undefined; + property.current_kbtu = Math.round(property.current_sqft * property.current_eui) || undefined; + property.eui_change = percentage(property.baseline_eui, property.current_eui); + }; + + const format_properties = (properties) => { + const area = $scope.columns.find((c) => c.id === $scope.goal.area_column); + const preferred_columns = [$scope.columns.find((c) => c.id === $scope.goal.eui_column1)]; + if ($scope.goal.eui_column2) preferred_columns.push($scope.columns.find((c) => c.id === $scope.goal.eui_column2)); + if ($scope.goal.eui_column3) preferred_columns.push($scope.columns.find((c) => c.id === $scope.goal.eui_column3)); + + const baseline_cycle_name = $scope.cycles.find((c) => c.id === $scope.goal.baseline_cycle).name; + const current_cycle_name = $scope.cycles.find((c) => c.id === $scope.goal.current_cycle).name; + // some fields span cycles (id, name, type) + // and others are cycle specific (source EUI, sqft) + const current_properties = properties[$scope.goal.current_cycle]; + const baseline_properties = properties[$scope.goal.baseline_cycle]; + const flat_properties = baseline_first ? + [baseline_properties, current_properties].flat() : + [current_properties, baseline_properties].flat(); + + // labels are related to property views, but cross cycles displays based on property + // create a lookup between property_view.id to property.id + $scope.property_lookup = {}; + flat_properties.forEach((p) => $scope.property_lookup[p.property_view_id] = p.id); + const unique_ids = [...new Set(flat_properties.map((property) => property.id))]; + const combined_properties = []; + unique_ids.forEach((id) => { + // find matching properties + const baseline = baseline_properties.find((p) => p.id === id) || {}; + const current = current_properties.find((p) => p.id === id) || {}; + // set accumulator + const property = combine_properties(current, baseline); + // add baseline stats + if (baseline) { + property.baseline_cycle = baseline_cycle_name; + property.baseline_sqft = baseline[area.name]; + } + // add current stats + if (current) { + property.current_cycle = current_cycle_name; + property.current_sqft = current[area.name]; + } + // comparison stats + property.sqft_change = percentage(property.current_sqft, property.baseline_sqft); + set_eui_goal(baseline, current, property, preferred_columns); + combined_properties.push(property); + }); + return combined_properties; + }; + + const combine_properties = (current, baseline) => { + // Given 2 properties, find non null values and combine into a single property + const [a, b] = baseline_first ? [baseline, current] : [current, baseline]; + const c = {}; + Object.keys(a).forEach((key) => c[key] = a[key] !== null ? a[key] : b[key]); + return c; + }; + + const apply_defaults = (cols, ...defaults) => { + _.map(cols, (col) => _.defaults(col, ...defaults)); + }; + + const property_column_names = [...new Set( + [ + $scope.organization.property_display_field, + ...matching_column_names, + 'property_name', + 'property_type', + 'year_built' + ] + )]; + + $scope.question_options = [ + { id: 0, value: null }, + { id: 1, value: 'Is this a new construction or acquisition?' }, + { id: 2, value: 'Do you have data to report?' }, + { id: 3, value: 'Is this value correct?' }, + { id: 4, value: 'Are these values correct?' }, + { id: 5, value: 'Other or multiple flags; explain in Additional Notes field' } + ]; + // handle cycle specific columns + const selected_columns = () => { + let cols = property_column_names.map((name) => $scope.columns.find((col) => col.column_name === name)); + const default_baseline = { headerCellClass: 'portfolio-summary-baseline-header', cellClass: 'portfolio-summary-baseline-cell' }; + const default_current = { headerCellClass: 'portfolio-summary-current-header', cellClass: 'portfolio-summary-current-cell' }; + const default_styles = { headerCellFilter: 'translate', minWidth: 75, width: 150 }; + + const baseline_cols = [ + { field: 'baseline_cycle', displayName: 'Cycle' }, + { field: 'baseline_sqft', displayName: `Area (${area_units})`, cellFilter: 'number' }, + { field: 'baseline_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number' }, + // ktbu acts as a derived column. Disable sorting filtering + { + field: 'baseline_kbtu', + displayName: 'kBTU', + cellFilter: 'number', + enableFiltering: false, + enableSorting: false, + headerCellClass: 'derived-column-display-name portfolio-summary-baseline-header' + }, + build_label_col_def('baseline-labels', 'baseline') + ]; + const current_cols = [ + { field: 'current_cycle', displayName: 'Cycle' }, + { field: 'current_sqft', displayName: `Area (${area_units})`, cellFilter: 'number' }, + { field: 'current_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number' }, + { + field: 'current_kbtu', + displayName: 'kBTU', + cellFilter: 'number', + enableFiltering: false, + enableSorting: false, + headerCellClass: 'derived-column-display-name portfolio-summary-current-header' + }, + build_label_col_def('current-labels', 'current') + ]; + const summary_cols = [ + { + field: 'sqft_change', displayName: 'Sq Ft % Change', enableFiltering: false, enableSorting: false, headerCellClass: 'derived-column-display-name' + }, + { + field: 'eui_change', displayName: 'EUI % Improvement', enableFiltering: false, enableSorting: false, headerCellClass: 'derived-column-display-name' + } + ]; + + const goal_note_cols = [ + { + field: 'goal_note.question', + displayName: 'Question', + enableFiltering: false, + enableSorting: false, + editableCellTemplate: 'ui-grid/dropdownEditor', + editDropdownOptionsArray: $scope.question_options, + editDropdownIdLabel: 'value', + enableCellEdit: $scope.write_permission, + cellClass: () => $scope.write_permission && 'cell-dropdown', + // if user has write permission show a dropdown indicator + width: 350, + cellTemplate: ` +
+ + {{row.entity.goal_note.question}} + + +
+ ` + }, + { + field: 'goal_note.resolution', + displayName: 'Resolution', + enableFiltering: false, + enableSorting: false, + enableCellEdit: true, + editableCellTemplate: 'ui-grid/cellTitleValidator', + cellClass: 'cell-edit', + width: 300 + }, + { + field: 'historical_note.text', + displayName: 'Historical Notes', + enableFiltering: false, + enableSorting: false, + enableCellEdit: true, + editableCellTemplate: 'ui-grid/cellTitleValidator', + cellClass: 'cell-edit', + width: 300 + }, + { + field: 'goal_note.passed_checks', + displayName: 'Passed Checks', + enableFiltering: false, + enableSorting: false, + editableCellTemplate: 'ui-grid/dropdownEditor', + editDropdownOptionsArray: [{ id: 1, value: true }, { id: 2, value: false }], + editDropdownIdLabel: 'value', + enableCellEdit: $scope.write_permission, + cellClass: () => $scope.write_permission && 'cell-dropdown', + // if user has write permission show a dropdown indicator + cellTemplate: ` +
+ {{row.entity.goal_note.passed_checks}} + + +
+ ` + }, + { + field: 'goal_note.new_or_acquired', + displayName: 'New Build or Acquired', + enableFiltering: false, + enableSorting: false, + editableCellTemplate: 'ui-grid/dropdownEditor', + editDropdownOptionsArray: [{ id: 1, value: true }, { id: 2, value: false }], + editDropdownIdLabel: 'value', + enableCellEdit: $scope.write_permission, + cellClass: () => $scope.write_permission && 'cell-dropdown', + // if user has write permission show a dropdown indicator + cellTemplate: ` +
+ {{row.entity.goal_note.new_or_acquired}} + + +
+ ` + } + + ]; + + apply_defaults(baseline_cols, default_baseline); + apply_defaults(current_cols, default_current); + cols = [...cols, ...baseline_cols, ...current_cols, ...summary_cols, ...goal_note_cols]; + + // Apply filters + // from inventory_list_controller + _.map(cols, (col) => { + const options = {}; + if (col.pinnedLeft) { + col.pinnedLeft = false; + } + // not an ideal solution. How is this done on the inventory list + if (col.column_name === 'pm_property_id') { + col.type = 'number'; + } + if (col.data_type === 'datetime') { + options.cellFilter = 'date:\'yyyy-MM-dd h:mm a\''; + options.filter = inventory_service.dateFilter(); + } else if (['area', 'eui', 'float', 'number'].includes(col.data_type)) { + options.cellFilter = `number: ${$scope.organization.display_decimal_places}`; + options.filter = inventory_service.combinedFilter(); + } else { + options.filter = inventory_service.combinedFilter(); + } + return _.defaults(col, options, default_styles); + }); + + apply_cycle_sorts_and_filters(cols); + add_access_level_names(cols); + return cols; + }; + + const apply_cycle_sorts_and_filters = (columns) => { + // Cycle specific columns filters and sorts must be set manually + const cycle_columns = ['baseline_cycle', 'baseline_sqft', 'baseline_eui', 'baseline_kbtu', 'current_cycle', 'current_sqft', 'current_eui', 'current_kbtu', 'sqft_change', 'eui_change']; + + for (const column of columns) { + if (cycle_columns.includes(column.field)) { + const cycle_column = $scope.cycle_columns.find((col) => col.name === column.field); + column.sort = cycle_column ? cycle_column.sort : {}; + column.filter.term = cycle_column ? cycle_column.filters[0].term : null; + } + } + }; + + const add_access_level_names = (cols) => { + $scope.organization.access_level_names.slice(1).reverse().forEach((level) => { + cols.unshift({ + name: level, + displayName: level, + group: 'access_level_instance', + enableColumnMenu: true, + enableColumnMoving: false, + enableColumnResizing: true, + enableFiltering: true, + enableHiding: true, + enableSorting: true, + enablePinning: false, + exporterSuppressExport: true, + pinnedLeft: true, + visible: true, + width: 100, + cellClass: 'ali-cell', + headerCellClass: 'ali-header' + }); + }); + }; + + $scope.toggle_access_level_instances = () => { + $scope.show_access_level_instances = !$scope.show_access_level_instances; + $scope.gridOptions.columnDefs.forEach((col) => { + if (col.group === 'access_level_instance') { + col.visible = $scope.show_access_level_instances; + } + }); + $scope.gridApi.core.refresh(); + }; + + const format_cycle_columns = (columns) => { + /* filtering is based on existing db columns. + ** The PortfolioSummary uses cycle specific columns that do not exist elsewhere ('baseline_eui', 'current_sqft') + ** To sort on these columns, override the column name to the canonical column, and set the cycle filter order + ** ex: if sort = {name: 'baseline_sqft'}, set {name: 'gross_floor_area_##'} and filter for baseline properties first. + + ** NOTE: + ** cant filter on cycle - cycle is not a column + ** cant filter on kbtu, sqft_change, eui_change - not real columns. calc'ed from eui and sqft. (similar to derived columns) + */ + const eui_column = $scope.columns.find((col) => col.id === $scope.goal.eui_column1); + const area_column = $scope.columns.find((col) => col.id === $scope.goal.area_column); + + const cycle_column_lookup = { + baseline_eui: eui_column.name, + baseline_sqft: area_column.name, + current_eui: eui_column.name, + current_sqft: area_column.name + }; + $scope.cycle_columns = []; + + for (const column of columns) { + if (cycle_column_lookup[column.name]) { + $scope.cycle_columns.push({ ...column }); + column.name = cycle_column_lookup[column.name]; + } + } - $scope.page_change = (page) => { - spinner_utility.show() - $scope.load_inventory(page) - } - $scope.load_inventory = (page) => { - $scope.data_loading = true; - - let access_level_instance_id = $scope.goal.access_level_instance - let combined_result = {} - let per_page = 50 - let current_cycle = {id: $scope.goal.current_cycle} - let baseline_cycle = {id: $scope.goal.baseline_cycle} - // order of cycle property filter is dynamic based on column_sorts - let cycle_priority = baseline_first ? [baseline_cycle, current_cycle]: [current_cycle, baseline_cycle] - - get_paginated_properties(page, per_page, cycle_priority[0], access_level_instance_id, true, null, $scope.goal.id).then(result0 => { - $scope.inventory_pagination = result0.pagination - properties = result0.results - combined_result[cycle_priority[0].id] = properties; - property_ids = properties.map(p => p.id) - - get_paginated_properties(page, per_page, cycle_priority[1], access_level_instance_id, false, property_ids).then(result1 => { - properties = result1.results - combined_result[cycle_priority[1].id] = properties; - get_all_labels() - set_grid_options(combined_result) - - }).then(() => { - $scope.data_loading = false; - $scope.data_valid = true - }) - }) - } + return columns; + }; + + const remove_conflict_columns = (grid_columns) => { + // Properties are returned from 2 different get requests. One for the current, one for the baseline + // The second filter is solely based on the property ids from the first + // Filtering on the first and second will result in unrepresentative data + // Remove the conflict to allow sorting/filtering on either baseline or current. + + const column_names = grid_columns.map((c) => c.name); + const includes_baseline = column_names.some((name) => name.includes('baseline')); + const includes_current = column_names.some((name) => name.includes('current')); + const conflict = includes_baseline && includes_current; + + if (conflict) { + baseline_first = !baseline_first; + const excluded_name = baseline_first ? 'current' : 'baseline'; + grid_columns = grid_columns.filter((column) => !column.name.includes(excluded_name)); + } else if (includes_baseline) { + baseline_first = true; + } else if (includes_current) { + baseline_first = false; + } - const get_paginated_properties = (page, chunk, cycle, access_level_instance_id, include_filters_sorts, include_property_ids=null, goal_id=null) => { - fn = inventory_service.get_properties; - const [filters, sorts] = include_filters_sorts ? [$scope.column_filters, $scope.column_sorts] : [[],[]] - - return fn( - page, - chunk, - cycle, - undefined, // profile_id - undefined, // include_view_ids - undefined, // exclude_view_ids - true, // save_last_cycle - $scope.organization.id, - true, // include_related - filters, - sorts, - false, // ids_only - table_column_ids.join(), - access_level_instance_id, - include_property_ids, - goal_id, // optional param to retrieve goal note details - ); - }; - - const percentage = (a, b) => { - if (!a || b == null) return null; - const value = Math.round((a - b) / a * 100); - return isNaN(value) ? null : value; + return grid_columns; + }; + + // from inventory_list_controller + const updateColumnFilterSort = () => { + let grid_columns = _.filter($scope.gridApi.saveState.save().columns, (col) => _.keys(col.sort).filter((key) => key !== 'ignoreSort').length + (_.get(col, 'filters[0].term', '') || '').length > 0); + // check filter/sort columns. Cannot filter on both baseline and current. choose the more recent filter/sort + grid_columns = remove_conflict_columns(grid_columns); + // convert cycle columns to canonical columns + const formatted_columns = format_cycle_columns(grid_columns); + + // inventory_service.saveGridSettings(`${localStorageKey}.sort`, { + // columns + // }); + $scope.column_filters = []; + // parse the filters and sorts + for (const column of formatted_columns) { + // format column if cycle specific + const { name, filters, sort } = column; + // remove the column id at the end of the name + const column_name = name.split('_').slice(0, -1).join('_'); + + for (const filter of filters) { + if (_.isEmpty(filter)) { + // eslint-disable-next-line no-continue + continue; } - // -------------- LABEL LOGIC ------------- - - $scope.max_label_width = 750; - $scope.get_label_column_width = (labels_col, key) => { - if (!$scope.show_full_labels[key]) { - return 30; - } - let maxWidth = 0; - const renderContainer = document.body.getElementsByClassName('ui-grid-render-container-body')[1]; - const col = $scope.gridApi.grid.getColumn(labels_col); - const cells = renderContainer.querySelectorAll(`.${uiGridConstants.COL_CLASS_PREFIX}${col.uid} .ui-grid-cell-contents`); - Array.prototype.forEach.call(cells, (cell) => { - gridUtil.fakeElement(cell, {}, (newElm) => { - const e = angular.element(newElm); - e.attr('style', 'float: left;'); - const width = gridUtil.elementWidth(e); - if (width > maxWidth) { - maxWidth = width; - } - }); - }); - return maxWidth > $scope.max_label_width ? $scope.max_label_width : maxWidth + 2; - }; - - // Expand or contract labels col - $scope.show_full_labels = { baseline: false, current: false } - $scope.toggle_labels = (labels_col, key) => { - $scope.show_full_labels[key] = !$scope.show_full_labels[key]; - setTimeout(() => { - $scope.gridApi.grid.getColumn(labels_col).width = $scope.get_label_column_width(labels_col, key); - const icon = document.getElementById(`label-header-icon-${key}`); - icon.classList.add($scope.show_full_labels[key] ? 'fa-chevron-circle-left' : 'fa-chevron-circle-right'); - icon.classList.remove($scope.show_full_labels[key] ? 'fa-chevron-circle-right' : 'fa-chevron-circle-left'); - $scope.gridApi.grid.refresh(); - }, 0); - }; - - // retreive labels for cycle - const get_all_labels = () => { - get_labels('baseline'); - get_labels('current'); - } - const get_labels = (key) => { - const cycle = key == 'baseline' ? $scope.goal.baseline_cycle : $scope.goal.current_cycle; - - label_service.get_labels('properties', undefined, cycle).then((current_labels) => { - let labels = _.filter(current_labels, (label) => !_.isEmpty(label.is_applied)); - - // load saved label filter - const ids = inventory_service.loadSelectedLabels(localStorageLabelKey); - // $scope.selected_labels = _.filter(labels, (label) => _.includes(ids, label.id)); - - if (key == 'baseline') { - $scope.baseline_labels = labels - $scope.build_labels(key, $scope.baseline_labels); - } else { - $scope.current_labels = labels - $scope.build_labels(key, $scope.current_labels); - } + // a filter can contain many comma-separated filters + const subFilters = _.map(_.split(filter.term, ','), _.trim); + for (const subFilter of subFilters) { + if (subFilter) { + const { string, operator, value } = parseFilter(subFilter); + const display = [$scope.columnDisplayByName[name], string, value].join(' '); + $scope.column_filters.push({ + name, + column_name, + operator, + value, + display }); - }; - - // Find labels that should be displayed and organize by applied inventory id - $scope.show_labels_by_inventory_id = {baseline: {}, current: {}}; - $scope.build_labels = (key, labels) => { - $scope.show_labels_by_inventory_id[key] = {}; - for (const n in labels) { - const label = labels[n]; - if (label.show_in_list) { - for (const m in label.is_applied) { - const id = label.is_applied[m]; - const property_id = $scope.property_lookup[id] - if (!$scope.show_labels_by_inventory_id[key][property_id]) { - $scope.show_labels_by_inventory_id[key][property_id] = []; - } - $scope.show_labels_by_inventory_id[key][property_id].push(label); - } - } - } - }; - - // Builds the html to display labels associated with this row entity - $scope.display_labels = (entity, key) => { - const id = entity.id; - const labels = []; - const titles = []; - if ($scope.show_labels_by_inventory_id[key][id]) { - for (const i in $scope.show_labels_by_inventory_id[key][id]) { - const label = $scope.show_labels_by_inventory_id[key][id][i]; - labels.push('', $scope.show_full_labels[key] ? label.text : '', ''); - titles.push(label.text); - } - } - return ['', labels.join(''), ''].join(''); - }; - - - // Build column defs for baseline or current labels - const build_label_col_def = (labels_col, key) => { - const header_cell_template = `` - const cell_template = `
` - const width_fn = $scope.gridApi ? $scope.get_label_column_width(labels_col, key) : 30 - - return { - name: labels_col, - displayName: '', - headerCellTemplate: header_cell_template, - cellTemplate: cell_template, - enableColumnMenu: false, - enableColumnMoving: false, - enableColumnResizing: false, - enableFiltering: false, - enableHiding: false, - enableSorting: false, - exporterSuppressExport: true, - // pinnedLeft: true, - visible: true, - width: width_fn, - maxWidth: $scope.max_label_width - } - } - - // ------------ DATA TABLE LOGIC --------- - - const set_eui_goal = (baseline, current, property, preferred_columns) => { - // only check defined columns - for (let col of preferred_columns.filter(c => c)) { - if (baseline && property.baseline_eui == undefined) { - property.baseline_eui = baseline[col.name] - } - if (current && property.current_eui == undefined) { - property.current_eui = current[col.name] - } - } - - property.baseline_kbtu = Math.round(property.baseline_sqft * property.baseline_eui) || undefined - property.current_kbtu = Math.round(property.current_sqft * property.current_eui) || undefined - property.eui_change = percentage(property.baseline_eui, property.current_eui) - } - - const format_properties = (properties) => { - const area = $scope.columns.find(c => c.id == $scope.goal.area_column) - const preferred_columns = [$scope.columns.find(c => c.id == $scope.goal.eui_column1)] - if ($scope.goal.eui_column2) preferred_columns.push($scope.columns.find(c => c.id == $scope.goal.eui_column2)) - if ($scope.goal.eui_column3) preferred_columns.push($scope.columns.find(c => c.id == $scope.goal.eui_column3)) - - const baseline_cycle_name = $scope.cycles.find(c => c.id == $scope.goal.baseline_cycle).name - const current_cycle_name = $scope.cycles.find(c => c.id == $scope.goal.current_cycle).name - // some fields span cycles (id, name, type) - // and others are cycle specific (source EUI, sqft) - let current_properties = properties[$scope.goal.current_cycle] - let baseline_properties = properties[$scope.goal.baseline_cycle] - let flat_properties = baseline_first ? - [baseline_properties, current_properties].flat() : - [current_properties, baseline_properties].flat() - - // labels are related to property views, but cross cycles displays based on property - // create a lookup between property_view.id to property.id - $scope.property_lookup = {} - flat_properties.forEach(p => $scope.property_lookup[p.property_view_id] = p.id) - let unique_ids = [...new Set(flat_properties.map(property => property.id))] - let combined_properties = [] - unique_ids.forEach(id => { - // find matching properties - let baseline = baseline_properties.find(p => p.id == id) || {} - let current = current_properties.find(p => p.id == id) || {} - // set accumulator - let property = combine_properties(current, baseline) - // add baseline stats - if (baseline) { - property.baseline_cycle = baseline_cycle_name - property.baseline_sqft = baseline[area.name] - } - // add current stats - if (current) { - property.current_cycle = current_cycle_name - property.current_sqft = current[area.name] - } - // comparison stats - property.sqft_change = percentage(property.current_sqft, property.baseline_sqft) - set_eui_goal(baseline, current, property, preferred_columns) - combined_properties.push(property) - }) - return combined_properties - } - - const combine_properties = (current, baseline) => { - // Given 2 properties, find non null values and combine into a single property - let [a, b] = baseline_first ? [baseline, current] : [current, baseline]; - let c = {}; - Object.keys(a).forEach(key => c[key] = a[key] !== null ? a[key] : b[key]) - return c - } - - const apply_defaults = (cols, ...defaults) => { _.map(cols, (col) => _.defaults(col, ...defaults)) } - - const property_column_names = [...new Set( - [ - $scope.organization.property_display_field, - ...matching_column_names, - 'property_name', - 'property_type', - 'year_built' - ] - )] - - $scope.question_options = [ - {id: 0, value: null}, - {id: 1, value: 'Is this a new construction or acquisition?'}, - {id: 2, value: 'Do you have data to report?'}, - {id: 3, value: 'Is this value correct?'}, - {id: 4, value: 'Are these values correct?'}, - {id: 5, value: 'Other or multiple flags; explain in Additional Notes field'}, - ] - // handle cycle specific columns - const selected_columns = () => { - let cols = property_column_names.map(name => $scope.columns.find(col => col.column_name === name)) - const default_baseline = { headerCellClass: 'portfolio-summary-baseline-header', cellClass: 'portfolio-summary-baseline-cell' } - const default_current = { headerCellClass: 'portfolio-summary-current-header', cellClass: 'portfolio-summary-current-cell' } - const default_styles = { headerCellFilter: 'translate', minWidth: 75, width: 150 } - - const baseline_cols = [ - { field: 'baseline_cycle', displayName: 'Cycle'}, - { field: 'baseline_sqft', displayName: `Area (${area_units})`, cellFilter: 'number'}, - { field: 'baseline_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number'}, - // ktbu acts as a derived column. Disable sorting filtering - { - field: 'baseline_kbtu', displayName: 'kBTU', cellFilter: 'number', - enableFiltering: false, enableSorting: false, - headerCellClass: 'derived-column-display-name portfolio-summary-baseline-header' - }, - build_label_col_def('baseline-labels', 'baseline') - ] - const current_cols = [ - { field: 'current_cycle', displayName: 'Cycle'}, - { field: 'current_sqft', displayName: `Area (${area_units})`, cellFilter: 'number'}, - { field: 'current_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number'}, - { - field: 'current_kbtu', displayName: 'kBTU', cellFilter: 'number', - enableFiltering: false, enableSorting: false, - headerCellClass: 'derived-column-display-name portfolio-summary-current-header' - }, - build_label_col_def('current-labels', 'current') - ] - const summary_cols = [ - { field: 'sqft_change', displayName: 'Sq Ft % Change', enableFiltering: false, enableSorting: false, headerCellClass: 'derived-column-display-name' }, - { field: 'eui_change', displayName: 'EUI % Improvement', enableFiltering: false, enableSorting: false, headerCellClass: 'derived-column-display-name' }, - ] - - const goal_note_cols = [ - { - field: 'goal_note.question', - displayName: 'Question', - enableFiltering: false, - enableSorting: false, - editableCellTemplate: "ui-grid/dropdownEditor", - editDropdownOptionsArray: $scope.question_options, - editDropdownIdLabel: 'value', - enableCellEdit: $scope.write_permission, - cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => $scope.write_permission && 'cell-dropdown', - // if user has write permission show a dropdown inidcator - width: 350, - cellTemplate: ` -
- - {{row.entity.goal_note.question}} - - -
- ` - }, - { - field: 'goal_note.resolution', - displayName: 'Resolution', - enableFiltering: false, - enableSorting: false, - enableCellEdit: true, - ediableCellTempalte: 'ui-grid/cellTitleValidator', - cellClass: 'cell-edit', - width: 300, - }, - { - field: 'historical_note.text', - displayName: 'Historical Notes', - enableFiltering: false, - enableSorting: false, - enableCellEdit: true, - ediableCellTempalte: 'ui-grid/cellTitleValidator', - cellClass: 'cell-edit', - width: 300, - }, - { - field: 'goal_note.passed_checks', - displayName: 'Passed Checks', - enableFiltering: false, - enableSorting: false, - editableCellTemplate: "ui-grid/dropdownEditor", - editDropdownOptionsArray: [{id: 1, value: true}, {id: 2, value: false}], - editDropdownIdLabel: 'value', - enableCellEdit: $scope.write_permission, - cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => $scope.write_permission && 'cell-dropdown', - // if user has write permission show a dropdown inidcator - cellTemplate: ` -
- {{row.entity.goal_note.passed_checks}} - - -
- ` - }, - { - field: 'goal_note.new_or_acquired', - displayName: 'New Build or Acquired', - enableFiltering: false, - enableSorting: false, - editableCellTemplate: "ui-grid/dropdownEditor", - editDropdownOptionsArray: [{id: 1, value: true}, {id: 2, value: false}], - editDropdownIdLabel: 'value', - enableCellEdit: $scope.write_permission, - cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => $scope.write_permission && 'cell-dropdown', - // if user has write permission show a dropdown inidcator - cellTemplate: ` -
- {{row.entity.goal_note.new_or_acquired}} - - -
- ` - }, - - ] - - apply_defaults(baseline_cols, default_baseline) - apply_defaults(current_cols, default_current) - cols = [...cols, ...baseline_cols, ...current_cols, ...summary_cols, ...goal_note_cols] - - // Apply filters - // from inventory_list_controller - _.map(cols, (col) => { - let options = {}; - if (col.pinnedLeft) { - col.pinnedLeft = false; - } - // not an ideal solution. How is this done on the inventory list - if (col.column_name == 'pm_property_id') { - col.type = 'number' - } - if (col.data_type === 'datetime') { - options.cellFilter = 'date:\'yyyy-MM-dd h:mm a\''; - options.filter = inventory_service.dateFilter(); - } else if (['area', 'eui', 'float', 'number'].includes(col.data_type)) { - options.cellFilter = 'number: ' + $scope.organization.display_decimal_places; - options.filter = inventory_service.combinedFilter(); - } else { - options.filter = inventory_service.combinedFilter(); - } - return _.defaults(col, options, default_styles); - }) - - apply_cycle_sorts_and_filters(cols) - add_access_level_names(cols) - return cols - - } - - const apply_cycle_sorts_and_filters = (columns) => { - // Cycle specific columns filters and sorts must be set manually - const cycle_columns = ['baseline_cycle', 'baseline_sqft', 'baseline_eui', 'baseline_kbtu', 'current_cycle', 'current_sqft', 'current_eui', 'current_kbtu', 'sqft_change', 'eui_change'] - - columns.forEach(column => { - if (cycle_columns.includes(column.field)) { - let cycle_column = $scope.cycle_columns.find(c => c.name == column.field) - column.sort = cycle_column ? cycle_column.sort : {} - column.filter.term = cycle_column ? cycle_column.filters[0].term : null - } - }) - } - - const add_access_level_names = (cols) => { - $scope.organization.access_level_names.slice(1).reverse().forEach((level) => { - cols.unshift({ - name: level, - displayName: level, - group: 'access_level_instance', - enableColumnMenu: true, - enableColumnMoving: false, - enableColumnResizing: true, - enableFiltering: true, - enableHiding: true, - enableSorting: true, - enablePinning: false, - exporterSuppressExport: true, - pinnedLeft: true, - visible: true, - width: 100, - cellClass: 'ali-cell', - headerCellClass: 'ali-header', - }) - }) - } - - $scope.toggle_access_level_instances = function () { - $scope.show_access_level_instances = !$scope.show_access_level_instances - $scope.gridOptions.columnDefs.forEach((col) => { - if (col.group == 'access_level_instance') { - col.visible = $scope.show_access_level_instances - } - }) - $scope.gridApi.core.refresh(); - } - - const format_cycle_columns = (columns) => { - /* filtering is based on existing db columns. - ** The PortfilioSummary uses cycle specific columns that do not exist elsewhere ('baseline_eui', 'current_sqft') - ** To sort on these columns, override the column name to the cannonical column, and set the cycle filter order - ** ex: if sort = {name: 'baseline_sqft'}, set {name: 'gross_floor_area_##'} and filter for baseline properties frist. - - ** NOTE: - ** cant fitler on cycle - cycle is not a column - ** cant filter on kbtu, sqft_change, eui_change - not real columns. calc'ed from eui and sqft. (similar to derived columns) - */ - let eui_column = $scope.columns.find(c => c.id == $scope.goal.eui_column1) - let area_column = $scope.columns.find(c => c.id == $scope.goal.area_column) - - const cycle_column_lookup = { - 'baseline_eui': eui_column.name, - 'baseline_sqft': area_column.name, - 'current_eui': eui_column.name, - 'current_sqft': area_column.name, - } - $scope.cycle_columns = [] - - columns.forEach(column => { - if (cycle_column_lookup[column.name]) { - $scope.cycle_columns.push({...column}) - column.name = cycle_column_lookup[column.name] - } - }) - - return columns - - } - - const remove_conflict_columns = (grid_columns) => { - // Property's are returned from 2 different get requests. One for the current, one for the baseline - // The second filter is solely based on the property ids from the first - // Filtering on the first and second will result in unrepresntative data - // Remove the conflict to allow sorting/filtering on either baseline or current. - - const column_names = grid_columns.map(c => c.name); - const includes_baseline = column_names.some(name => name.includes('baseline')); - const includes_current = column_names.some(name => name.includes('current')); - const conflict = includes_baseline && includes_current; - - if (conflict) { - baseline_first = !baseline_first; - const excluded_name = baseline_first ? 'current' : 'baseline'; - grid_columns = grid_columns.filter(column => !column.name.includes(excluded_name)); - } else if (includes_baseline) { - baseline_first = true; - } else if (includes_current) { - baseline_first = false; - } - - return grid_columns - } - - - // from inventory_list_controller - const updateColumnFilterSort = () => { - let grid_columns = _.filter($scope.gridApi.saveState.save().columns, (col) => _.keys(col.sort).filter((key) => key !== 'ignoreSort').length + (_.get(col, 'filters[0].term', '') || '').length > 0); - // check filter/sort columns. Cannot filter on both baseline and current. choose the more recent filter/sort - grid_columns = remove_conflict_columns(grid_columns) - // convert cycle columnss to cannonical columns - let formatted_columns = format_cycle_columns(grid_columns) - - // inventory_service.saveGridSettings(`${localStorageKey}.sort`, { - // columns - // }); - $scope.column_filters = []; - // parse the filters and sorts - for (let column of formatted_columns) { - // format column if cycle specific - const { name, filters, sort } = column; - // remove the column id at the end of the name - const column_name = name.split('_').slice(0, -1).join('_'); - - for (const filter of filters) { - if (_.isEmpty(filter)) { - continue; - } - - // a filter can contain many comma-separated filters - const subFilters = _.map(_.split(filter.term, ','), _.trim); - for (const subFilter of subFilters) { - if (subFilter) { - const { string, operator, value } = parseFilter(subFilter); - const index = $scope.columns.findIndex((p) => p.name === column_name); - const display = [$scope.columnDisplayByName[name], string, value].join(' '); - $scope.column_filters.push({ - name, - column_name, - operator, - value, - display - }); - } - } - } - - if (sort.direction) { - // remove the column id at the end of the name - const column_name = name.split('_').slice(0, -1).join('_'); - const display = [$scope.columnDisplayByName[name], sort.direction].join(' '); - $scope.column_sorts = [{ - name, - column_name, - direction: sort.direction, - display, - priority: sort.priority - }]; - // $scope.column_sorts.sort((a, b) => a.priority > b.priority); - } - } - // $scope.isModified(); - }; - - // from inventory_list_controller - // https://regexr.com/6cka2 - const combinedRegex = /^(!?)=\s*(-?\d+(?:\.\d+)?)$|^(!?)=?\s*"((?:[^"]|\\")*)"$|^(<=?|>=?)\s*((-?\d+(?:\.\d+)?)|(\d{4}-\d{2}-\d{2}))$/; - const parseFilter = (expression) => { - // parses an expression string into an object containing operator and value - const filterData = expression.match(combinedRegex); - if (filterData) { - if (!_.isUndefined(filterData[2])) { - // Numeric Equality - const operator = filterData[1]; - const value = Number(filterData[2].replace('\\.', '.')); - if (operator === '!') { - return { string: 'is not', operator: 'ne', value }; - } - return { string: 'is', operator: 'exact', value }; - } - if (!_.isUndefined(filterData[4])) { - // Text Equality - const operator = filterData[3]; - const value = filterData[4]; - if (operator === '!') { - return { string: 'is not', operator: 'ne', value }; - } - return { string: 'is', operator: 'exact', value }; - } - if (!_.isUndefined(filterData[7])) { - // Numeric Comparison - const operator = filterData[5]; - const value = Number(filterData[6].replace('\\.', '.')); - switch (operator) { - case '<': - return { string: '<', operator: 'lt', value }; - case '<=': - return { string: '<=', operator: 'lte', value }; - case '>': - return { string: '>', operator: 'gt', value }; - case '>=': - return { string: '>=', operator: 'gte', value }; - } - } else { - // Date Comparison - const operator = filterData[5]; - const value = filterData[8]; - switch (operator) { - case '<': - return { string: '<', operator: 'lt', value }; - case '<=': - return { string: '<=', operator: 'lte', value }; - case '>': - return { string: '>', operator: 'gt', value }; - case '>=': - return { string: '>=', operator: 'gte', value }; - } - } - } else { - // Case-insensitive Contains - return { string: 'contains', operator: 'icontains', value: expression }; - } - }; - - const set_grid_options = (result) => { - $scope.data = format_properties(result) - spinner_utility.hide() - $scope.gridOptions = { - data: 'data', - columnDefs: selected_columns(), - enableFiltering: true, - enableHorizontalScrollbar: 1, - cellWidth: 200, - enableGridMenu: true, - exporterMenuCsv: false, - exporterMenuExcel: false, - exporterMenuPdf: false, - gridMenuShowHideColumns: false, - gridMenuCustomItems: [{ - title: 'Export Page to CSV', - action: ($event) => $scope.gridApi.exporter.csvExport('all', 'all'), - }], - onRegisterApi: (gridApi) => { - $scope.gridApi = gridApi; - - gridApi.core.on.sortChanged($scope, () => { - spinner_utility.show() - _.debounce(() => { - updateColumnFilterSort(); - $scope.load_inventory(1); - }, 500)(); - }); - - gridApi.core.on.filterChanged($scope, _.debounce(() => { - spinner_utility.show() - updateColumnFilterSort(); - $scope.load_inventory(1); - }, 2000) - ); - - gridApi.edit.on.afterCellEdit($scope, (rowEntity, colDef, newValue, oldValue) => { - [model, field] = colDef.field.split('.') - - if (model == 'historical_note') { - goal_service.update_historical_note( rowEntity.id, rowEntity.historical_note.id, {[field]: newValue} ) - } else if (model == 'goal_note') { - goal_service.update_goal_note( rowEntity.id, rowEntity.goal_note.id, { [field]: newValue } ) - } - }) - } - } - } - - $scope.reset_sorts_filters = () => { - $scope.reset_sorts() - $scope.reset_filters() + } } - $scope.reset_sorts = () => { - $scope.column_sorts = [] - $scope.gridApi.core.refresh() - } - $scope.reset_filters = () => { - $scope.column_filters = [] - $scope.gridApi.grid.clearAllFilters() + } + + if (sort.direction) { + // remove the column id at the end of the name + const column_name = name.split('_').slice(0, -1).join('_'); + const display = [$scope.columnDisplayByName[name], sort.direction].join(' '); + $scope.column_sorts = [{ + name, + column_name, + direction: sort.direction, + display, + priority: sort.priority + }]; + // $scope.column_sorts.sort((a, b) => a.priority > b.priority); + } + } + // $scope.isModified(); + }; + + // from inventory_list_controller + // https://regexr.com/6cka2 + const combinedRegex = /^(!?)=\s*(-?\d+(?:\.\d+)?)$|^(!?)=?\s*"((?:[^"]|\\")*)"$|^(<=?|>=?)\s*((-?\d+(?:\.\d+)?)|(\d{4}-\d{2}-\d{2}))$/; + const parseFilter = (expression) => { + // parses an expression string into an object containing operator and value + const filterData = expression.match(combinedRegex); + if (filterData) { + if (!_.isUndefined(filterData[2])) { + // Numeric Equality + const operator = filterData[1]; + const value = Number(filterData[2].replace('\\.', '.')); + if (operator === '!') { + return { string: 'is not', operator: 'ne', value }; } - - - // -------- SUMMARY LOGIC ------------ - - const summary_selected_columns = () => { - const default_baseline = { headerCellClass: 'portfolio-summary-baseline-header', cellClass: 'portfolio-summary-baseline-cell' } - const default_current = { headerCellClass: 'portfolio-summary-current-header', cellClass: 'portfolio-summary-current-cell' } - const default_styles = { headerCellFilter: 'translate' } - - const baseline_cols = [ - { field: 'baseline_cycle', displayName: 'Cycle' }, - { field: 'baseline_total_sqft', displayName: `Total Area (${area_units})`, cellFilter: 'number'}, - { field: 'baseline_total_kbtu', displayName: 'Total kBTU', cellFilter: 'number'}, - { field: 'baseline_weighted_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number'}, - ] - const current_cols = [ - { field: 'current_cycle', displayName: 'Cycle' }, - { field: 'current_total_sqft', displayName: `Total Area (${area_units})`, cellFilter: 'number'}, - { field: 'current_total_kbtu', displayName: 'Total kBTU', cellFilter: 'number'}, - { field: 'current_weighted_eui', displayName: `EUI (${eui_units})` , cellFilter: 'number'}, - ] - const calc_cols = [ - { field: 'sqft_change', displayName: 'Area % Change' }, - { - field: 'eui_change', displayName: 'EUI % Improvement', cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => { - return row.entity.eui_change >= $scope.goal.target_percentage ? 'above-target' : 'below-target' - } - }, - ] - apply_defaults(baseline_cols, default_baseline, default_styles) - apply_defaults(current_cols, default_current, default_styles) - apply_defaults(calc_cols) - - return [...baseline_cols, ...current_cols, ...calc_cols] + return { string: 'is', operator: 'exact', value }; + } + if (!_.isUndefined(filterData[4])) { + // Text Equality + const operator = filterData[3]; + const value = filterData[4]; + if (operator === '!') { + return { string: 'is not', operator: 'ne', value }; } - - const format_summary = (summary) => { - const baseline = summary.baseline - const current = summary.current - return [{ - baseline_cycle: baseline.cycle_name, - baseline_total_sqft: baseline.total_sqft, - baseline_total_kbtu: baseline.total_kbtu, - baseline_weighted_eui: baseline.weighted_eui, - current_cycle: current.cycle_name, - current_total_sqft: current.total_sqft, - current_total_kbtu: current.total_kbtu, - current_weighted_eui: current.weighted_eui, - sqft_change: summary.sqft_change, - eui_change: summary.eui_change, - }] + return { string: 'is', operator: 'exact', value }; + } + if (!_.isUndefined(filterData[7])) { + // Numeric Comparison + const operator = filterData[5]; + const value = Number(filterData[6].replace('\\.', '.')); + switch (operator) { + case '<': + return { string: '<', operator: 'lt', value }; + case '<=': + return { string: '<=', operator: 'lte', value }; + case '>': + return { string: '>', operator: 'gt', value }; + case '>=': + return { string: '>=', operator: 'gte', value }; } - - const set_summary_grid_options = (summary) => { - $scope.summary_data = format_summary(summary) - $scope.summaryGridOptions = { - data: 'summary_data', - columnDefs: summary_selected_columns(), - enableSorting: false, - } + } else { + // Date Comparison + const operator = filterData[5]; + const value = filterData[8]; + switch (operator) { + case '<': + return { string: '<', operator: 'lt', value }; + case '<=': + return { string: '<=', operator: 'lte', value }; + case '>': + return { string: '>', operator: 'gt', value }; + case '>=': + return { string: '>=', operator: 'gte', value }; } - + } + } else { + // Case-insensitive Contains + return { string: 'contains', operator: 'icontains', value: expression }; } - ] -) + }; + + const set_grid_options = (result) => { + $scope.data = format_properties(result); + spinner_utility.hide(); + $scope.gridOptions = { + data: 'data', + columnDefs: selected_columns(), + enableFiltering: true, + enableHorizontalScrollbar: 1, + cellWidth: 200, + enableGridMenu: true, + exporterMenuCsv: false, + exporterMenuExcel: false, + exporterMenuPdf: false, + gridMenuShowHideColumns: false, + gridMenuCustomItems: [{ + title: 'Export Page to CSV', + action: () => $scope.gridApi.exporter.csvExport('all', 'all') + }], + onRegisterApi: (gridApi) => { + $scope.gridApi = gridApi; + + gridApi.core.on.sortChanged($scope, () => { + spinner_utility.show(); + _.debounce(() => { + updateColumnFilterSort(); + $scope.load_inventory(1); + }, 500)(); + }); + + gridApi.core.on.filterChanged($scope, _.debounce(() => { + spinner_utility.show(); + updateColumnFilterSort(); + $scope.load_inventory(1); + }, 2000)); + + gridApi.edit.on.afterCellEdit($scope, (rowEntity, colDef, newValue) => { + const [model, field] = colDef.field.split('.'); + + if (model === 'historical_note') { + goal_service.update_historical_note(rowEntity.id, rowEntity.historical_note.id, { [field]: newValue }); + } else if (model === 'goal_note') { + goal_service.update_goal_note(rowEntity.id, rowEntity.goal_note.id, { [field]: newValue }); + } + }); + } + }; + }; + + $scope.reset_sorts_filters = () => { + $scope.reset_sorts(); + $scope.reset_filters(); + }; + $scope.reset_sorts = () => { + $scope.column_sorts = []; + $scope.gridApi.core.refresh(); + }; + $scope.reset_filters = () => { + $scope.column_filters = []; + $scope.gridApi.grid.clearAllFilters(); + }; + + // -------- SUMMARY LOGIC ------------ + + const summary_selected_columns = () => { + const default_baseline = { headerCellClass: 'portfolio-summary-baseline-header', cellClass: 'portfolio-summary-baseline-cell' }; + const default_current = { headerCellClass: 'portfolio-summary-current-header', cellClass: 'portfolio-summary-current-cell' }; + const default_styles = { headerCellFilter: 'translate' }; + + const baseline_cols = [ + { field: 'baseline_cycle', displayName: 'Cycle' }, + { field: 'baseline_total_sqft', displayName: `Total Area (${area_units})`, cellFilter: 'number' }, + { field: 'baseline_total_kbtu', displayName: 'Total kBTU', cellFilter: 'number' }, + { field: 'baseline_weighted_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number' } + ]; + const current_cols = [ + { field: 'current_cycle', displayName: 'Cycle' }, + { field: 'current_total_sqft', displayName: `Total Area (${area_units})`, cellFilter: 'number' }, + { field: 'current_total_kbtu', displayName: 'Total kBTU', cellFilter: 'number' }, + { field: 'current_weighted_eui', displayName: `EUI (${eui_units})`, cellFilter: 'number' } + ]; + const calc_cols = [ + { field: 'sqft_change', displayName: 'Area % Change' }, + { + field: 'eui_change', + displayName: 'EUI % Improvement', + cellClass: (grid, row, col, rowRenderIndex, colRenderIndex) => (row.entity.eui_change >= $scope.goal.target_percentage ? 'above-target' : 'below-target') + } + ]; + apply_defaults(baseline_cols, default_baseline, default_styles); + apply_defaults(current_cols, default_current, default_styles); + apply_defaults(calc_cols); + + return [...baseline_cols, ...current_cols, ...calc_cols]; + }; + + const format_summary = (summary) => { + const baseline = summary.baseline; + const current = summary.current; + return [{ + baseline_cycle: baseline.cycle_name, + baseline_total_sqft: baseline.total_sqft, + baseline_total_kbtu: baseline.total_kbtu, + baseline_weighted_eui: baseline.weighted_eui, + current_cycle: current.cycle_name, + current_total_sqft: current.total_sqft, + current_total_kbtu: current.total_kbtu, + current_weighted_eui: current.weighted_eui, + sqft_change: summary.sqft_change, + eui_change: summary.eui_change + }]; + }; + + const set_summary_grid_options = (summary) => { + $scope.summary_data = format_summary(summary); + $scope.summaryGridOptions = { + data: 'summary_data', + columnDefs: summary_selected_columns(), + enableSorting: false + }; + }; + }]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index ded2e5087a..5c695e8cbe 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -74,10 +74,9 @@ angular.module('BE.seed.controllers', [ 'BE.seed.controller.export_report_modal', 'BE.seed.controller.export_to_audit_template_modal', 'BE.seed.controller.filter_group_modal', - 'BE.seed.controller.goal_editor_modal', 'BE.seed.controller.geocode_modal', + 'BE.seed.controller.goal_editor_modal', 'BE.seed.controller.green_button_upload_modal', - 'BE.seed.controller.organization_delete_access_level_instance_modal', 'BE.seed.controller.insights_program', 'BE.seed.controller.insights_property', 'BE.seed.controller.inventory_cycles', @@ -112,6 +111,7 @@ angular.module('BE.seed.controllers', [ 'BE.seed.controller.organization_access_level_tree', 'BE.seed.controller.organization_add_access_level_instance_modal', 'BE.seed.controller.organization_add_access_level_modal', + 'BE.seed.controller.organization_delete_access_level_instance_modal', 'BE.seed.controller.organization_edit_access_level_instance_modal', 'BE.seed.controller.organization_settings', 'BE.seed.controller.organization_sharing', @@ -122,13 +122,13 @@ angular.module('BE.seed.controllers', [ 'BE.seed.controller.profile', 'BE.seed.controller.program_setup', 'BE.seed.controller.record_match_merge_link_modal', - 'BE.seed.controller.set_update_to_now_modal', 'BE.seed.controller.rename_column_modal', 'BE.seed.controller.reset_modal', 'BE.seed.controller.sample_data_modal', 'BE.seed.controller.security', 'BE.seed.controller.sensor_readings_upload_modal', 'BE.seed.controller.sensors_upload_modal', + 'BE.seed.controller.set_update_to_now_modal', 'BE.seed.controller.settings_profile_modal', 'BE.seed.controller.show_populated_columns_modal', 'BE.seed.controller.ubid_admin', @@ -153,6 +153,7 @@ angular.module('BE.seed.directives', [ 'sdUploader' ]); angular.module('BE.seed.services', [ + 'BE.seed.service.ah', 'BE.seed.service.analyses', 'BE.seed.service.audit_template', 'BE.seed.service.auth', @@ -168,8 +169,8 @@ angular.module('BE.seed.services', [ 'BE.seed.service.event', 'BE.seed.service.filter_groups', 'BE.seed.service.flippers', - 'BE.seed.service.goal', 'BE.seed.service.geocode', + 'BE.seed.service.goal', 'BE.seed.service.httpParamSerializerSeed', 'BE.seed.service.inventory', 'BE.seed.service.inventory_reports', @@ -2834,32 +2835,24 @@ SEED_app.config([ .state({ name: 'portfolio_summary', url: '/insights/portfolio_summary', - templateUrl: static_url + 'seed/partials/portfolio_summary.html', + templateUrl: `${static_url}seed/partials/portfolio_summary.html`, controller: 'portfolio_summary_controller', resolve: { - valid_column_data_types: [function () { - return ['number', 'float', 'integer', 'area', 'eui', 'ghg', 'ghg_intensity']; - }], - property_columns: ['valid_column_data_types', '$stateParams', 'inventory_service', 'naturalSort', function (valid_column_data_types, $stateParams, inventory_service, naturalSort) { - return inventory_service.get_property_columns_for_org($stateParams.organization_id).then(function (columns) { - _.remove(columns, { table_name: 'TaxLotState' }); - return columns; - }); - }], - cycles: ['cycle_service', function (cycle_service) { - return cycle_service.get_cycles(); - }], - organization_payload: ['user_service', 'organization_service', function (user_service, organization_service) { - return organization_service.get_organization(user_service.get_organization().id); - }], - access_level_tree: ['organization_payload', 'organization_service', '$q', function (organization_payload, organization_service, $q) { - var organization_id = organization_payload.organization.id; + valid_column_data_types: [() => ['number', 'float', 'integer', 'area', 'eui', 'ghg', 'ghg_intensity']], + property_columns: ['valid_column_data_types', '$stateParams', 'inventory_service', (valid_column_data_types, $stateParams, inventory_service) => inventory_service.get_property_columns_for_org($stateParams.organization_id).then((columns) => { + _.remove(columns, { table_name: 'TaxLotState' }); + return columns; + })], + cycles: ['cycle_service', (cycle_service) => cycle_service.get_cycles()], + organization_payload: ['user_service', 'organization_service', (user_service, organization_service) => organization_service.get_organization(user_service.get_organization().id)], + access_level_tree: ['organization_payload', 'organization_service', (organization_payload, organization_service) => { + const organization_id = organization_payload.organization.id; return organization_service.get_organization_access_level_tree(organization_id); }], - auth_payload: ['auth_service', '$q', 'organization_payload', (auth_service, $q, organization_payload) => { + auth_payload: ['auth_service', 'organization_payload', (auth_service, organization_payload) => { const organization_id = organization_payload.organization.id; return auth_service.is_authorized(organization_id, ['requires_owner']); - }], + }] } }) .state({ diff --git a/seed/static/seed/js/services/ah_service.js b/seed/static/seed/js/services/ah_service.js new file mode 100644 index 0000000000..c65558398f --- /dev/null +++ b/seed/static/seed/js/services/ah_service.js @@ -0,0 +1,34 @@ +/** + * SEED Platform (TM), Copyright (c) Alliance for Sustainable Energy, LLC, and other contributors. + * See also https://github.com/seed-platform/seed/main/LICENSE.md + */ +angular.module('BE.seed.service.ah', []).factory('ah_service', [ + () => { + const { compare } = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); + + const recurse_access_levels = (tree, access_level_instances_by_depth, depth = 1) => { + if (!tree) return; + if (!access_level_instances_by_depth[depth]) access_level_instances_by_depth[depth] = []; + for (const ali of tree) { + access_level_instances_by_depth[depth].push({ id: ali.id, name: ali.data.name }); + recurse_access_levels(ali.children, access_level_instances_by_depth, depth + 1); + } + }; + + // Build out access_level_instances_by_depth recursively + const calculate_access_level_instances_by_depth = (tree) => { + const access_level_instances_by_depth = {}; + recurse_access_levels(tree, access_level_instances_by_depth); + + // Sort instances + for (const [depth, instances] of Object.entries(access_level_instances_by_depth)) { + access_level_instances_by_depth[depth] = instances.sort((a, b) => compare(a.name, b.name)); + } + return access_level_instances_by_depth; + }; + + return { + calculate_access_level_instances_by_depth + }; + } +]); diff --git a/seed/static/seed/partials/inventory_detail.html b/seed/static/seed/partials/inventory_detail.html index eb278e9cb9..36ba31846d 100644 --- a/seed/static/seed/partials/inventory_detail.html +++ b/seed/static/seed/partials/inventory_detail.html @@ -137,12 +137,12 @@

- + - +
{$:: 'Level' | translate $} {$ level+1 $} - {$ name $}{$:: 'Level' | translate $} {$ level+1 $} - {$:: name $}
{$ ali_path[name] $}{$:: ali_path[name] $}
diff --git a/seed/templates/seed/_header.html b/seed/templates/seed/_header.html index 27dd2acd3c..7f527ad17b 100644 --- a/seed/templates/seed/_header.html +++ b/seed/templates/seed/_header.html @@ -13,9 +13,9 @@
diff --git a/seed/templates/seed/_scripts.html b/seed/templates/seed/_scripts.html index aff5cbc2f3..2fc10da56c 100644 --- a/seed/templates/seed/_scripts.html +++ b/seed/templates/seed/_scripts.html @@ -138,6 +138,7 @@ + diff --git a/seed/utils/inventory_filter.py b/seed/utils/inventory_filter.py index eff3b9d9af..d4b08a8123 100644 --- a/seed/utils/inventory_filter.py +++ b/seed/utils/inventory_filter.py @@ -85,7 +85,7 @@ def get_filtered_results(request: Request, inventory_type: Literal['property', ' .filter( property__organization_id=org_id, cycle=cycle, # this is a m-to-1-to-1, so the joins not _that_ bad - # should it prove to be un-preformant, I think we can make it a "through" field + # should it prove to be un-performant, I think we can make it a "through" field property__access_level_instance__lft__gte=access_level_instance.lft, property__access_level_instance__rgt__lte=access_level_instance.rgt, ) @@ -96,7 +96,7 @@ def get_filtered_results(request: Request, inventory_type: Literal['property', ' .filter( taxlot__organization_id=org_id, cycle=cycle, # this is a m-to-1-to-1, so the joins not _that_ bad - # should it prove to be un-preformant, I think we can make it a "through" field + # should it prove to be un-performant, I think we can make it a "through" field taxlot__access_level_instance__lft__gte=access_level_instance.lft, taxlot__access_level_instance__rgt__lte=access_level_instance.rgt, ) @@ -130,7 +130,7 @@ def get_filtered_results(request: Request, inventory_type: Literal['property', ' except ValueError as e: return JsonResponse( { - 'stauts': 'error', + 'status': 'error', 'message': f'Error filtering: {str(e)}' }, status=status.HTTP_400_BAD_REQUEST diff --git a/seed/views/v3/organization_users.py b/seed/views/v3/organization_users.py index ff10e09267..8ad5aea5ce 100644 --- a/seed/views/v3/organization_users.py +++ b/seed/views/v3/organization_users.py @@ -70,8 +70,7 @@ def add(self, request, organization_pk, pk): user = User.objects.get(pk=pk) try: - _orguser, created = org.add_member(user, access_level_instance_id=org.root) - _orguser.save() + created = org.add_member(user, org.root.id) except IntegrityError as e: return JsonResponse({ 'status': 'error',