diff --git a/seed/static/seed/js/controllers/insights_program_controller.js b/seed/static/seed/js/controllers/insights_program_controller.js index 9f9cf1b912..6174c14261 100644 --- a/seed/static/seed/js/controllers/insights_program_controller.js +++ b/seed/static/seed/js/controllers/insights_program_controller.js @@ -12,16 +12,36 @@ angular.module('BE.seed.controller.insights_program', []).controller('insights_p 'compliance_metric_service', 'spinner_utility', 'organization_payload', + 'filter_groups', 'cycles', + 'property_columns', 'auth_payload', // eslint-disable-next-line func-names - function ($scope, $stateParams, $state, $uibModal, urls, compliance_metrics, compliance_metric_service, spinner_utility, organization_payload, cycles, auth_payload) { + function ( + $scope, + $stateParams, + $state, + $uibModal, + urls, + compliance_metrics, + compliance_metric_service, + spinner_utility, + organization_payload, + filter_groups, + cycles, + property_columns, + auth_payload + ) { $scope.id = $stateParams.id; $scope.cycles = cycles.cycles; $scope.organization = organization_payload.organization; $scope.initialize_chart = true; $scope.auth = auth_payload.auth; + // used by modal + $scope.filter_groups = filter_groups; + $scope.property_columns = property_columns; + // compliance metric $scope.compliance_metric = {}; $scope.compliance_metrics = compliance_metrics; @@ -44,6 +64,53 @@ angular.module('BE.seed.controller.insights_program', []).controller('insights_p // CHARTS const colors = { compliant: '#77CCCB', 'non-compliant': '#A94455', unknown: '#DDDDDD' }; + // Program Setup Modal + $scope.open_program_setup_modal = () => { + const modalInstance = $uibModal.open({ + templateUrl: `${urls.static_url}seed/partials/program_setup.html`, + controller: 'program_setup_controller', + size: 'lg', + backdrop: 'static', + resolve: { + cycles: () => $scope.cycles, + compliance_metrics: () => $scope.compliance_metrics, + organization_payload: () => $scope.organization, + filter_groups: () => $scope.filter_groups, + property_columns: () => $scope.property_columns, + id: () => $scope.selected_metric + } + }); + // on modal close + modalInstance.result.then((program) => { + // re-fetch compliance metrics + compliance_metric_service.get_compliance_metrics($scope.organization.id).then((data) => { + $scope.compliance_metrics = data; + // change selection to last selected in modal and reload + if ($scope.compliance_metrics.length > 0) { + if (program != null) { + $scope.compliance_metric = $scope.compliance_metrics.find((cm) => cm.id === program.id); + $scope.selected_metric = program.id; + } else { + // attempt to keep the selected metric + $scope.compliance_metric = $scope.compliance_metrics.find((cm) => cm.id === $scope.selected_metric); + if ($scope.compliance_metric == null) { + // load first metric b/c selected metric no longer exists + $scope.compliance_metric = $scope.compliance_metrics[0]; + $scope.selected_metric = $scope.compliance_metric.id; + } + } + } else { + // load nothing + $scope.compliance_metric = {}; + $scope.selected_metric = null; + $scope.data = null; + } + + $scope.updateSelectedMetric(); + }); + }); + }; + const _load_datasets = () => { // load data @@ -173,7 +240,10 @@ angular.module('BE.seed.controller.insights_program', []).controller('insights_p }; $scope.updateSelectedMetric = () => { - $scope.compliance_metric = _.find($scope.compliance_metrics, (o) => o.id === $scope.selected_metric); + $scope.compliance_metric = {}; + if ($scope.selected_metric != null) { + $scope.compliance_metric = _.find($scope.compliance_metrics, (o) => o.id === $scope.selected_metric); + } // reload data for selected metric _load_data(); diff --git a/seed/static/seed/js/controllers/insights_property_controller.js b/seed/static/seed/js/controllers/insights_property_controller.js index c2de5e8680..8345cd341a 100644 --- a/seed/static/seed/js/controllers/insights_property_controller.js +++ b/seed/static/seed/js/controllers/insights_property_controller.js @@ -11,20 +11,42 @@ angular.module('BE.seed.controller.insights_property', []).controller('insights_ 'compliance_metrics', 'compliance_metric_service', 'organization_payload', + 'filter_groups', + 'property_columns', + 'cycles', 'spinner_utility', 'auth_payload', // eslint-disable-next-line func-names - function ($scope, $state, $stateParams, $uibModal, urls, compliance_metrics, compliance_metric_service, organization_payload, spinner_utility, auth_payload) { + function ( + $scope, + $state, + $stateParams, + $uibModal, + urls, + compliance_metrics, + compliance_metric_service, + organization_payload, + filter_groups, + property_columns, + cycles, + spinner_utility, + auth_payload + ) { $scope.id = $stateParams.id; $scope.static_url = urls.static_url; $scope.organization = organization_payload.organization; $scope.auth = auth_payload.auth; + // used by modal + $scope.filter_groups = filter_groups; + $scope.property_columns = property_columns; + $scope.all_cycles = cycles.cycles; + // toggle help $scope.show_help = false; $scope.toggle_help = () => { $scope.show_help = !$scope.show_help; - } + }; // configs ($scope.configs set to saved_configs where still applies. // for example, if saved_configs.compliance_metric is 1, but 1 has been deleted, it does apply.) @@ -73,6 +95,53 @@ angular.module('BE.seed.controller.insights_property', []).controller('insights_ $scope.y_axis_options = []; $scope.x_categorical = false; + // Program Setup Modal + $scope.open_program_setup_modal = () => { + const modalInstance = $uibModal.open({ + templateUrl: `${urls.static_url}seed/partials/program_setup.html`, + controller: 'program_setup_controller', + size: 'lg', + backdrop: 'static', + resolve: { + cycles: () => $scope.all_cycles, + compliance_metrics: () => $scope.compliance_metrics, + organization_payload: () => $scope.organization, + filter_groups: () => $scope.filter_groups, + property_columns: () => $scope.property_columns, + id: () => $scope.selected_metric + } + }); + // on modal close + modalInstance.result.then((program) => { + // re-fetch compliance metrics + compliance_metric_service.get_compliance_metrics($scope.organization.id).then((data) => { + $scope.compliance_metrics = data; + // change selection to last selected in modal and reload + if ($scope.compliance_metrics.length > 0) { + if (program != null) { + $scope.configs.compliance_metric = $scope.compliance_metrics.find((cm) => cm.id === program.id); + $scope.selected_metric = program.id; + } else { + // attempt to keep the selected metric + $scope.configs.compliance_metric = $scope.compliance_metrics.find((cm) => cm.id === $scope.selected_metric); + if ($scope.configs.compliance_metric == null) { + // load first metric b/c selected metric no longer exists + $scope.configs.compliance_metric = $scope.compliance_metrics[0]; + $scope.selected_metric = $scope.configs.compliance_metric.id; + } + } + } else { + // load nothing + $scope.configs.compliance_metric = {}; + $scope.selected_metric = null; + $scope.data = null; + } + + $scope.update_metric(); + }); + }); + }; + $scope.$watch( 'configs', (new_configs) => { @@ -89,6 +158,7 @@ angular.module('BE.seed.controller.insights_property', []).controller('insights_ const _load_data = () => { if (_.isEmpty($scope.configs.compliance_metric)) { spinner_utility.hide(); + $scope.data = null; return; } spinner_utility.show(); @@ -189,7 +259,10 @@ angular.module('BE.seed.controller.insights_property', []).controller('insights_ spinner_utility.show(); // compliance metric - $scope.configs.compliance_metric = _.find($scope.compliance_metrics, (o) => o.id === $scope.selected_metric); + $scope.configs.compliance_metric = {}; + if ($scope.selected_metric != null) { + $scope.configs.compliance_metric = _.find($scope.compliance_metrics, (o) => o.id === $scope.selected_metric); + } // reload data for selected metric _load_data(); diff --git a/seed/static/seed/js/controllers/program_setup_controller.js b/seed/static/seed/js/controllers/program_setup_controller.js index f292b4679d..cc2c130986 100644 --- a/seed/static/seed/js/controllers/program_setup_controller.js +++ b/seed/static/seed/js/controllers/program_setup_controller.js @@ -5,39 +5,49 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup_controller', [ '$scope', '$state', - '$stateParams', - 'compliance_metrics', + '$uibModalInstance', 'compliance_metric_service', - 'filter_groups', 'Notification', + 'spinner_utility', + 'naturalSort', + 'cycles', + 'compliance_metrics', 'organization_payload', - 'cycles_payload', + 'filter_groups', 'property_columns', - 'spinner_utility', - 'x_axis_columns', + 'id', // eslint-disable-next-line func-names function ( $scope, $state, - $stateParams, - compliance_metrics, + $uibModalInstance, compliance_metric_service, - filter_groups, Notification, + spinner_utility, + naturalSort, + cycles, + compliance_metrics, organization_payload, - cycles_payload, + filter_groups, property_columns, - spinner_utility, - x_axis_columns + id ) { - spinner_utility.show(); + // spinner_utility.show(); $scope.state = $state.current; - $scope.id = $stateParams.id; - $scope.org = organization_payload.organization; - $scope.cycles = cycles_payload.cycles; + $scope.org = organization_payload; + $scope.cycles = cycles; + $scope.id = id; + $scope.filter_groups = filter_groups; // order cycles by start date - $scope.cycles = _.orderBy($scope.cycles, ['start'], - ['asc']); + $scope.cycles = _.orderBy($scope.cycles, ['start'], ['asc']); + $scope.filter_groups = filter_groups; + $scope.valid_column_data_types = ['number', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean']; + $scope.valid_x_axis_data_types = ['number', 'string', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean']; + + $scope.property_columns = _.reject(property_columns, (item) => item.related || !$scope.valid_column_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); + $scope.x_axis_columns = _.reject(property_columns, (item) => item.related || !$scope.valid_x_axis_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); + $scope.x_axis_selection = ''; + $scope.cycle_selection = ''; $scope.compliance_metrics_error = []; $scope.program_settings_not_changed = true; $scope.program_settings_changed = () => { @@ -45,12 +55,35 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup }; $scope.compliance_metrics = compliance_metrics; $scope.has_compliance_metrics = $scope.compliance_metrics.length > 0; + $scope.selected_compliance_metric = null; - if ($scope.id) { - $scope.selected_compliance_metric = $scope.compliance_metrics.find((item) => item.id === $scope.id); - } - $scope.property_columns = property_columns; - $scope.x_axis_columns = x_axis_columns; + // init_selected_compliance_metric (handle case where there are none) + $scope.init_selected_metric = (id) => { + $scope.has_compliance_metrics = $scope.compliance_metrics.length > 0; + $scope.selected_compliance_metric = null; + $scope.available_cycles = []; + $scope.available_x_axis_columns = []; + $scope.compliance_metrics_error = []; + $scope.program_settings_not_changed = true; + $scope.x_axis_selection = ''; + $scope.cycle_selection = ''; + $scope.available_x_axis_columns = () => []; + $scope.available_cycles = () => []; + + if (id === null) { + if ($scope.has_compliance_metrics) { + // this is after a delete. choose the first metric? + id = $scope.compliance_metrics[0].id; + } + } + if (id != null) { + $scope.selected_compliance_metric = $scope.compliance_metrics.find((item) => item.id === id); + $scope.available_x_axis_columns = () => $scope.x_axis_columns.filter(({ id }) => !$scope.selected_compliance_metric?.x_axis_columns.includes(id)); + $scope.available_cycles = () => $scope.cycles.filter(({ id }) => !$scope.selected_compliance_metric?.cycles.includes(id)); + } + }; + + $scope.init_selected_metric($scope.id); $scope.get_column_display = (id) => { const record = _.find($scope.property_columns, { id }); @@ -60,7 +93,6 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup }; // cycles - $scope.cycle_selection = ''; $scope.get_cycle_display = (id) => { const record = _.find($scope.cycles, { id }); if (record) { @@ -68,8 +100,6 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup } }; - $scope.available_cycles = () => $scope.cycles.filter(({ id }) => !$scope.selected_compliance_metric?.cycles.includes(id)); - $scope.select_cycle = () => { $scope.program_settings_changed(); const selection = $scope.cycle_selection; @@ -79,7 +109,6 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup } $scope.selected_compliance_metric.cycles.push(selection); $scope.order_selected_cycles(); - }; $scope.order_selected_cycles = () => { @@ -100,10 +129,6 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup } }; - $scope.available_x_axis_columns = () => $scope.x_axis_columns.filter(({ id }) => !$scope.selected_compliance_metric?.x_axis_columns.includes(id)); - - $scope.x_axis_selection = ''; - $scope.select_x_axis = () => { $scope.program_settings_changed(); const selection = $scope.x_axis_selection; @@ -120,7 +145,6 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup }; // Filter Groups - $scope.filter_groups = filter_groups; $scope.get_filter_group_display = (id) => { const record = _.find($scope.filter_groups, { id }); if (record) { @@ -128,8 +152,19 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup } }; + $scope.set_program = (id) => { + // check to ensure there are no unsaved changes + if ($scope.program_settings_not_changed) { + // switch it out / re-init + $scope.init_selected_metric(id); + } else { + // warn user to save first + Notification.warning({ message: 'You have unsaved changes to the current program. Save your changes first before selecting a different program to update.', delay: 5000 }); + } + }; + /** - * saves the updates settings + * saves the updated settings */ $scope.save_settings = () => { spinner_utility.show(); @@ -184,22 +219,23 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup $scope.compliance_metrics_error.push(`${key}: ${error}`); } } else { + // success. the ID would already be saved so this block seems unnecesary if (!$scope.selected_compliance_metric.id) { - window.location = `#/accounts/${$scope.org.id}/program_setup/${data.id}`; + $scope.selected_compliance_metric.id = data.id; } - - // replace data into compliance metric? needed? + // replace data into compliance metric const index = _.findIndex($scope.compliance_metrics, ['id', data.id]); - $scope.compliance_metrics[index] = data; - + if (index >= 0) { + $scope.compliance_metrics[index] = data; + } else { + $scope.compliance_metrics.push(data); + } $scope.selected_compliance_metric = data; - - window.location = `#/accounts/${$scope.org.id}/program_setup`; } }); // display messages - Notification.primary({ message: 'Click here to view your Program Overview', delay: 5000 }); + // Notification.primary({ message: 'Click here to view your Program Overview', delay: 5000 }); Notification.success({ message: 'Program Setup Saved!', delay: 5000 }); $scope.program_settings_not_changed = true; @@ -207,7 +243,7 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup }; $scope.click_new_compliance_metric = () => { - spinner_utility.show(); + //spinner_utility.show(); // create a new metric using api and then assign it to selected_compliance_metric that // way it will have an id @@ -224,16 +260,14 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup x_axis_columns: [] }; compliance_metric_service.new_compliance_metric(template_compliance_metric, $scope.org.id).then((data) => { - $scope.selected_compliance_metric = data; - window.location = `#/accounts/${$scope.org.id}/program_setup/${data.id}`; + $scope.compliance_metrics.push(data); + $scope.init_selected_metric(data.id); }); - $scope.program_settings_not_changed = true; - - spinner_utility.hide(); - }; + //spinner_utility.hide(); + } $scope.click_delete = (compliance_metric = null) => { - spinner_utility.show(); + // spinner_utility.show(); if (!compliance_metric) { compliance_metric = $scope.selected_compliance_metric; } @@ -243,12 +277,21 @@ angular.module('BE.seed.controller.program_setup', []).controller('program_setup if (data.status === 'success') { $scope.compliance_metrics = $scope.compliance_metrics.filter((compliance_metric) => compliance_metric.id !== delete_id); if ($scope.selected_compliance_metric.id === delete_id) { - window.location = `#/accounts/${$scope.org.id}/program_setup`; + // notification + Notification.success({ message: 'Compliance metric deleted successfully!', delay: 5000 }); + // reset selection + $scope.selected_compliance_metric = {}; + $scope.init_selected_metric(null); } } }); } - spinner_utility.hide(); + // spinner_utility.hide(); + }; + + $scope.close = () => { + // close and return selected compliance metric + $uibModalInstance.close($scope.selected_compliance_metric); }; } ]); diff --git a/seed/static/seed/js/seed.js b/seed/static/seed/js/seed.js index db17a6680d..34ba3e5dce 100644 --- a/seed/static/seed/js/seed.js +++ b/seed/static/seed/js/seed.js @@ -1235,148 +1235,6 @@ SEED_app.config([ ] } }) - .state({ - name: 'programs', - url: '/accounts/{organization_id:int}/program_setup', - templateUrl: `${static_url}seed/partials/program_setup.html`, - controller: 'program_setup_controller', - resolve: { - valid_column_data_types: [ - () => ['number', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean'] - ], - valid_x_axis_data_types: [ - () => ['number', 'string', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean'] - ], - compliance_metrics: [ - '$stateParams', - 'compliance_metric_service', - ($stateParams, compliance_metric_service) => compliance_metric_service.get_compliance_metrics($stateParams.organization_id) - ], - organization_payload: [ - 'organization_service', - '$stateParams', - (organization_service, $stateParams) => organization_service.get_organization($stateParams.organization_id) - ], - cycles_payload: [ - 'cycle_service', - '$stateParams', - (cycle_service, $stateParams) => cycle_service.get_cycles_for_org($stateParams.organization_id) - ], - property_columns: [ - 'valid_column_data_types', - '$stateParams', - 'inventory_service', - 'naturalSort', - (valid_column_data_types, $stateParams, inventory_service, naturalSort) => inventory_service.get_property_columns_for_org($stateParams.organization_id).then((columns) => { - columns = _.reject(columns, (item) => item.related || !valid_column_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); - return columns; - }) - ], - x_axis_columns: [ - 'valid_x_axis_data_types', - '$stateParams', - 'inventory_service', - 'naturalSort', - (valid_x_axis_data_types, $stateParams, inventory_service, naturalSort) => inventory_service.get_property_columns_for_org($stateParams.organization_id).then((columns) => { - columns = _.reject(columns, (item) => item.related || !valid_x_axis_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); - return columns; - }) - ], - filter_groups: [ - '$stateParams', - 'filter_groups_service', - ($stateParams, filter_groups_service) => { - const inventory_type = 'Property'; // just properties for now - return filter_groups_service.get_filter_groups(inventory_type, $stateParams.organization_id); - } - ], - auth_payload: [ - 'auth_service', - '$stateParams', - '$q', - (auth_service, $stateParams, $q) => auth_service.is_authorized($stateParams.organization_id, ['requires_member']).then( - (data) => { - if (data.auth.requires_member) { - return data; - } - return $q.reject('not authorized'); - }, - (data) => $q.reject(data.message) - ) - ] - } - }) - .state({ - name: 'program_setup', - url: '/accounts/{organization_id:int}/program_setup/{id:int}', - templateUrl: `${static_url}seed/partials/program_setup.html`, - controller: 'program_setup_controller', - resolve: { - valid_column_data_types: [ - () => ['number', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean'] - ], - valid_x_axis_data_types: [ - () => ['number', 'string', 'float', 'integer', 'ghg', 'ghg_intensity', 'area', 'eui', 'boolean'] - ], - compliance_metrics: [ - '$stateParams', - 'compliance_metric_service', - ($stateParams, compliance_metric_service) => compliance_metric_service.get_compliance_metrics($stateParams.organization_id) - ], - organization_payload: [ - 'organization_service', - '$stateParams', - (organization_service, $stateParams) => organization_service.get_organization($stateParams.organization_id) - ], - cycles_payload: [ - 'cycle_service', - '$stateParams', - (cycle_service, $stateParams) => cycle_service.get_cycles_for_org($stateParams.organization_id) - ], - property_columns: [ - 'valid_column_data_types', - '$stateParams', - 'inventory_service', - 'naturalSort', - (valid_column_data_types, $stateParams, inventory_service, naturalSort) => inventory_service.get_property_columns_for_org($stateParams.organization_id).then((columns) => { - columns = _.reject(columns, (item) => item.related || !valid_column_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); - return columns; - }) - ], - x_axis_columns: [ - 'valid_x_axis_data_types', - '$stateParams', - 'inventory_service', - 'naturalSort', - (valid_x_axis_data_types, $stateParams, inventory_service, naturalSort) => inventory_service.get_property_columns_for_org($stateParams.organization_id).then((columns) => { - columns = _.reject(columns, (item) => item.related || !valid_x_axis_data_types.includes(item.data_type)).sort((a, b) => naturalSort(a.displayName, b.displayName)); - return columns; - }) - ], - filter_groups: [ - '$stateParams', - 'filter_groups_service', - ($stateParams, filter_groups_service) => { - const inventory_type = 'Property'; // just properties for now - return filter_groups_service.get_filter_groups(inventory_type, $stateParams.organization_id); - } - ], - auth_payload: [ - 'auth_service', - '$stateParams', - '$q', - (auth_service, $stateParams, $q) => auth_service.is_authorized($stateParams.organization_id, ['requires_member']).then( - (data) => { - if (data.auth.requires_member) { - return data; - } - return $q.reject('not authorized'); - }, - (data) => $q.reject(data.message) - ) - ] - } - }) .state({ name: 'organization_column_settings', url: '/accounts/{organization_id:int}/column_settings/{inventory_type:properties|taxlots}', @@ -2646,10 +2504,26 @@ SEED_app.config([ 'cycle_service', (cycle_service) => cycle_service.get_cycles() ], + property_columns: [ + 'inventory_service', + 'user_service', + (inventory_service, user_service) => { + const organization_id = user_service.get_organization().id; + return inventory_service.get_property_columns_for_org(organization_id); + } + ], organization_payload: [ 'user_service', 'organization_service', (user_service, organization_service) => organization_service.get_organization(user_service.get_organization().id) + ], + filter_groups: [ + '$stateParams', + 'filter_groups_service', + ($stateParams, filter_groups_service) => { + const inventory_type = 'Property'; // just properties for now + return filter_groups_service.get_filter_groups(inventory_type, $stateParams.organization_id); + } ] } }) @@ -2675,6 +2549,26 @@ SEED_app.config([ 'user_service', 'organization_service', (user_service, organization_service) => organization_service.get_organization(user_service.get_organization().id) + ], + filter_groups: [ + '$stateParams', + 'filter_groups_service', + ($stateParams, filter_groups_service) => { + const inventory_type = 'Property'; // just properties for now + return filter_groups_service.get_filter_groups(inventory_type, $stateParams.organization_id); + } + ], + property_columns: [ + 'inventory_service', + 'user_service', + (inventory_service, user_service) => { + const organization_id = user_service.get_organization().id; + return inventory_service.get_property_columns_for_org(organization_id); + } + ], + cycles: [ + 'cycle_service', + (cycle_service) => cycle_service.get_cycles() ] } }) diff --git a/seed/static/seed/partials/accounts.html b/seed/static/seed/partials/accounts.html index 5c578d8fcb..a533bf6b01 100644 --- a/seed/static/seed/partials/accounts.html +++ b/seed/static/seed/partials/accounts.html @@ -42,7 +42,6 @@
If you are not seeing a chart on this page, visit the Program page to configure your program's metrics.
+Configure your program's metrics:
+Need to configure your Program? Program Configuration page.
+Need to configure your Program? + +
If you are not seeing a chart on this page, visit the Program page to configure your program's metrics.
+Configure your program's metrics:
+CONFIGURE_PROGRAM Program Configuration page.
+Need to configure your Program? + +