diff --git a/src/admin-facility-view/_facility-view.scss b/src/admin-facility-view/_facility-view.scss new file mode 100644 index 0000000..afd1d90 --- /dev/null +++ b/src/admin-facility-view/_facility-view.scss @@ -0,0 +1,24 @@ +nav.facility-edit { + + > ul { + + padding: 0; + + li.tab-pane { + padding: 1em; + } + + li a { + text-decoration: none; + } + } + + div.button-group { + margin-top: 8px; + max-width: 440px; + } +} + +.note-warning { + font-style: italic; +} diff --git a/src/admin-facility-view/admin-facility-view.routes.js b/src/admin-facility-view/admin-facility-view.routes.js new file mode 100644 index 0000000..fc5d902 --- /dev/null +++ b/src/admin-facility-view/admin-facility-view.routes.js @@ -0,0 +1,74 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. + */ + +(function() { + + 'use strict'; + + angular.module('admin-facility-view').config(routes); + + routes.$inject = ['$stateProvider', 'ADMINISTRATION_RIGHTS']; + + function routes($stateProvider, ADMINISTRATION_RIGHTS) { + + $stateProvider.state('openlmis.administration.facilities.edit', { + label: 'adminFacilityView.editFacility', + url: '/:id', + accessRights: [ADMINISTRATION_RIGHTS.FACILITIES_MANAGE], + views: { + '@openlmis': { + controller: 'FacilityViewController', + templateUrl: 'admin-facility-view/facility-view.html', + controllerAs: 'vm' + } + }, + resolve: { + facility: function(FacilityRepository, $stateParams) { + return new FacilityRepository().get($stateParams.id); + }, + facilityTypes: function(facilityTypeService) { + return facilityTypeService.query({ + active: true + }) + .then(function(response) { + return response.content; + }); + }, + geographicZones: function($q, geographicZoneService) { + var deferred = $q.defer(); + + geographicZoneService.getAll().then(function(response) { + deferred.resolve(response.content); + }, deferred.reject); + + return deferred.promise; + }, + facilityOperators: function(facilityOperatorService) { + return facilityOperatorService.getAll(); + }, + programs: function(programService) { + return programService.getAll(); + }, + wards: function(wardService, facility) { + return wardService.getWardsByFacility({ + facilityId: facility.id + }).then(function(response) { + return response.content; + }); + } + } + }); + } +})(); diff --git a/src/admin-facility-view/facility-view.controller.js b/src/admin-facility-view/facility-view.controller.js new file mode 100644 index 0000000..d8cb33f --- /dev/null +++ b/src/admin-facility-view/facility-view.controller.js @@ -0,0 +1,351 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. + */ + +(function() { + + 'use strict'; + + /** + * @ngdoc controller + * @name admin-facility-view.controller:FacilityViewController + * + * @description + * Controller for managing facility view screen. + */ + angular + .module('admin-facility-view') + .controller('FacilityViewController', controller); + + controller.$inject = [ + '$q', '$state', 'facility', 'facilityTypes', 'geographicZones', 'facilityOperators', + 'programs', 'FacilityRepository', 'loadingModalService', 'notificationService', + 'tzPeriodService', 'messageService', 'confirmService', 'wards', 'wardService' + ]; + + function controller($q, $state, facility, facilityTypes, geographicZones, facilityOperators, + programs, FacilityRepository, loadingModalService, notificationService, + tzPeriodService, messageService, confirmService, wards, wardService) { + + var vm = this; + + vm.$onInit = onInit; + vm.goToFacilityList = goToFacilityList; + vm.saveFacilityDetails = saveFacilityDetails; + vm.saveFacilityWithPrograms = saveFacilityWithPrograms; + vm.saveFacilityWards = saveFacilityWards; + vm.addProgram = addProgram; + vm.addWard = addWard; + vm.deleteProgramAssociate = deleteProgramAssociate; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name facility + * @type {Object} + * + * @description + * Contains facility object. + */ + vm.facility = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name facilityTypes + * @type {Array} + * + * @description + * Contains all facility types. + */ + vm.facilityTypes = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name geographicZones + * @type {Array} + * + * @description + * Contains all geographic zones. + */ + vm.geographicZones = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name facilityOperators + * @type {Array} + * + * @description + * Contains all facility operators. + */ + vm.facilityOperators = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name programs + * @type {Array} + * + * @description + * Contains all programs. + */ + vm.programs = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name wards + * @type {Array} + * + * @description + * Contains all wards. + */ + vm.wards = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name selectedTab + * @type {String} + * + * @description + * Contains currently selected tab. + */ + vm.selectedTab = undefined; + + /** + * @ngdoc property + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name newWard + * @type {Object} + * + * @description + * Contains new ward object. By default, active true. + */ + vm.newWard = { + disabled: false + }; + + /** + * @ngdoc method + * @propertyOf admin-facility-view.controller:FacilityViewController + * @name $onInit + * + * @description + * Method that is executed on initiating FacilityListController. + */ + function onInit() { + vm.originalFacilityName = facility.name; + vm.facility = angular.copy(facility); + vm.facilityWithPrograms = angular.copy(facility); + vm.facilityTypes = facilityTypes; + vm.geographicZones = geographicZones; + vm.facilityOperators = facilityOperators; + vm.programs = programs; + vm.wards = wards; + vm.selectedTab = 0; + vm.managedExternally = facility.isManagedExternally(); + + if (!vm.facilityWithPrograms.supportedPrograms) { + vm.facilityWithPrograms.supportedPrograms = []; + } + + vm.facilityWithPrograms.supportedPrograms.filter(function(supportedProgram) { + vm.programs = vm.programs.filter(function(program) { + return supportedProgram.id !== program.id; + }); + }); + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name goToFacilityList + * + * @description + * Redirects to facility list screen. + */ + function goToFacilityList() { + $state.go('openlmis.administration.facilities', {}, { + reload: true + }); + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name saveFacilityDetails + * + * @description + * Saves facility details and redirects to facility list screen. + */ + function saveFacilityDetails() { + doSave(vm.facility, + 'adminFacilityView.saveFacility.success', + 'adminFacilityView.saveFacility.fail'); + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name saveFacilityWithPrograms + * + * @description + * Saves facility with supported programs and redirects to facility list screen. + */ + function saveFacilityWithPrograms() { + doSave(vm.facilityWithPrograms, + 'adminFacilityView.saveFacility.success', + 'adminFacilityView.saveFacility.fail'); + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name addProgram + * + * @description + * Adds program to associated program list. + */ + function addProgram() { + var supportedProgram = angular.copy(vm.selectedProgram); + + vm.programs = vm.programs.filter(function(program) { + return supportedProgram.id !== program.id; + }); + + supportedProgram.supportStartDate = vm.selectedStartDate; + supportedProgram.supportActive = true; + + vm.selectedStartDate = null; + vm.selectedProgram = null; + + vm.facilityWithPrograms.supportedPrograms.push(supportedProgram); + + return $q.when(); + } + + function doSave(facility, successMessage, errorMessage) { + loadingModalService.open(); + return new FacilityRepository().update(facility) + .then(function(facility) { + notificationService.success(successMessage); + goToFacilityList(); + return $q.resolve(facility); + }) + .catch(function() { + notificationService.error(errorMessage); + loadingModalService.close(); + return $q.reject(); + }); + } + + function deleteProgramAssociate(program) { + var confirmMessage = messageService.get('adminFacilityView.question', { + period: program.name + }); + + confirmService.confirm(confirmMessage, + 'adminFacilityView.deleteAssociatedProgram').then(function() { + var loadingPromise = loadingModalService.open(); + tzPeriodService + .deleteProgramAssociate(program.id, vm.facility.id) + .then(function() { + loadingPromise.then(function() { + notificationService.success('adminFacilityView.deleteAssociatedPrograms.success'); + }); + $state.reload('openlmis.administration.facility.view'); + }) + .catch(function() { + loadingModalService.close(); + notificationService.error('adminFacilityView.deleteAssociatedPrograms.fail'); + }); + }); + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name addWard + * + * @description + * Adds ward to associated wards list. + */ + function addWard() { + var newWard = angular.copy(vm.newWard); + newWard.facility = { + id: vm.facility.id + }; + + return wardService.getAllWards().then(function(dbWards) { + var wardExistsInDb = wardExists(dbWards.content, newWard); + var wardExistsInLocalState = wardExists(vm.wards, newWard); + + if (wardExistsInDb || wardExistsInLocalState) { + return notifyAndReturn('adminFacilityView.wardExists'); + } + + vm.wards.push(newWard); + vm.newWard = { + disabled: false + }; + + return $q.when(); + }); + + function wardExists(wards, wardToCheck) { + return wards.some(function(ward) { + return wardToCheck.code === ward.code; + }); + } + + function notifyAndReturn(message) { + notificationService.error(message); + return $q.when(); + } + } + + /** + * @ngdoc method + * @methodOf admin-facility-view.controller:FacilityViewController + * @name saveFacilityWards + * + * @description + * Saves facility wards and redirects to facility list screen. + */ + function saveFacilityWards() { + var facilityWards = angular.copy(vm.wards); + + confirmService.confirm( + 'adminFacilityView.savingConfirmation', + 'adminFacilityView.save' + ).then(function() { + loadingModalService.open(); + return new wardService.saveFacilityWards(facilityWards) + .then(function(facilityWards) { + notificationService.success('adminFacilityView.saveWards.success'); + goToFacilityList(); + return $q.resolve(facilityWards); + }) + .catch(function() { + notificationService.error('adminFacilityView.saveWards.fail'); + loadingModalService.close(); + return $q.reject(); + }); + }); + } + } +})(); diff --git a/src/admin-facility-view/facility-view.controller.spec.js b/src/admin-facility-view/facility-view.controller.spec.js new file mode 100644 index 0000000..3acf5bb --- /dev/null +++ b/src/admin-facility-view/facility-view.controller.spec.js @@ -0,0 +1,382 @@ +/* + * This program is part of the OpenLMIS logistics management information system platform software. + * Copyright © 2017 VillageReach + * + * This program is free software: you can redistribute it and/or modify it under the terms + * of the GNU Affero General Public License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. You should have received a copy of + * the GNU Affero General Public License along with this program. If not, see + * http://www.gnu.org/licenses. For additional information contact info@OpenLMIS.org. + */ + +describe('FacilityViewController', function() { + + beforeEach(function() { + module('admin-facility-view'); + module('referencedata-period'); + + inject(function($injector) { + this.$q = $injector.get('$q'); + this.$rootScope = $injector.get('$rootScope'); + this.$controller = $injector.get('$controller'); + this.$state = $injector.get('$state'); + this.tzPeriodService = $injector.get('tzPeriodService'); + this.wardService = $injector.get('wardService'); + this.notificationService = $injector.get('notificationService'); + this.loadingModalService = $injector.get('loadingModalService'); + this.FacilityRepository = $injector.get('FacilityRepository'); + this.FacilityTypeDataBuilder = $injector.get('FacilityTypeDataBuilder'); + this.GeographicZoneDataBuilder = $injector.get('GeographicZoneDataBuilder'); + this.FacilityOperatorDataBuilder = $injector.get('FacilityOperatorDataBuilder'); + this.ProgramDataBuilder = $injector.get('ProgramDataBuilder'); + this.FacilityDataBuilder = $injector.get('FacilityDataBuilder'); + this.WardDataBuilder = $injector.get('WardDataBuilder'); + this.confirmService = $injector.get('confirmService'); + }); + + spyOn(this.FacilityRepository.prototype, 'update').andReturn(this.$q.when()); + spyOn(this.tzPeriodService, 'tzCreate').andReturn(this.$q.when()); + + this.confirmDeferred = this.$q.defer(); + spyOn(this.confirmService, 'confirm').andReturn(this.confirmDeferred.promise); + + this.saveFacilityDetailsDeferred = this.$q.defer(); + spyOn(this.wardService, 'saveFacilityWards').andReturn(this.saveFacilityDetailsDeferred.promise); + + var loadingModalPromise = this.$q.defer(); + spyOn(this.loadingModalService, 'close').andCallFake(loadingModalPromise.resolve); + spyOn(this.loadingModalService, 'open').andReturn(loadingModalPromise.promise); + + spyOn(this.notificationService, 'success').andReturn(true); + spyOn(this.notificationService, 'error').andReturn(true); + spyOn(this.$state, 'go').andCallFake(loadingModalPromise.resolve); + + this.facilityTypes = [ + new this.FacilityTypeDataBuilder().build(), + new this.FacilityTypeDataBuilder().build() + ]; + + this.geographicZones = [ + new this.GeographicZoneDataBuilder().build(), + new this.GeographicZoneDataBuilder().build() + ]; + + this.facilityOperators = [ + new this.FacilityOperatorDataBuilder().build(), + new this.FacilityOperatorDataBuilder().build() + ]; + + this.programs = [ + new this.ProgramDataBuilder().build(), + new this.ProgramDataBuilder().build() + ]; + + this.wards = [ + new this.WardDataBuilder().build(), + new this.WardDataBuilder().build() + ]; + + this.facility = new this.FacilityDataBuilder().withFacilityType(this.facilityTypes[0]) + .build(); + + this.vm = this.$controller('FacilityViewController', { + facility: this.facility, + facilityTypes: this.facilityTypes, + geographicZones: this.geographicZones, + facilityOperators: this.facilityOperators, + programs: this.programs, + wards: this.wards + }); + this.vm.$onInit(); + }); + + describe('onInit', function() { + + it('should expose goToFacilityList method', function() { + expect(angular.isFunction(this.vm.goToFacilityList)).toBe(true); + }); + + it('should expose saveFacility method', function() { + expect(angular.isFunction(this.vm.saveFacilityDetails)).toBe(true); + }); + + it('should expose addWard method', function() { + expect(angular.isFunction(this.vm.addWard)).toBe(true); + }); + + it('should expose saveFacilityWards method', function() { + expect(angular.isFunction(this.vm.saveFacilityWards)).toBe(true); + }); + + it('should expose facility', function() { + expect(this.vm.facility).toEqual(this.facility); + }); + + it('should expose facilityTypes list', function() { + expect(this.vm.facilityTypes).toEqual(this.facilityTypes); + }); + + it('should expose geographicZones list', function() { + expect(this.vm.geographicZones).toEqual(this.geographicZones); + }); + + it('should expose facilityOperators list', function() { + expect(this.vm.facilityOperators).toEqual(this.facilityOperators); + }); + + it('should expose program list', function() { + expect(this.vm.programs).toEqual(this.programs); + }); + + it('should expose ward list', function() { + expect(this.vm.wards).toEqual(this.wards); + }); + + it('should expose supported programs list', function() { + expect(this.vm.facilityWithPrograms.supportedPrograms).toEqual([]); + }); + + it('should expose supported programs list as empty list if undefined', function() { + this.vm.facility.supportedPrograms = undefined; + this.vm.$onInit(); + + expect(this.vm.facilityWithPrograms.supportedPrograms).toEqual([]); + }); + + it('should expose managedExternally flag', function() { + spyOn(this.facility, 'isManagedExternally').andReturn(true); + + this.vm.$onInit(); + + expect(this.vm.managedExternally).toBe(true); + }); + + it('should expose original facility name', function() { + this.vm.$onInit(); + + expect(this.vm.originalFacilityName).toEqual(this.facility.name); + + this.vm.facility.name += ' (Edited)'; + + expect(this.vm.originalFacilityName).not.toBe(this.vm.facility.name); + }); + }); + + describe('goToFacilityList', function() { + + it('should call state go with correct params', function() { + this.vm.goToFacilityList(); + + expect(this.$state.go).toHaveBeenCalledWith('openlmis.administration.facilities', {}, { + reload: true + }); + }); + }); + + describe('saveFacility', function() { + + it('should open loading modal', function() { + this.vm.saveFacilityDetails(); + this.$rootScope.$apply(); + + expect(this.loadingModalService.open).toHaveBeenCalled(); + }); + + it('should call this.FacilityRepository save method', function() { + this.vm.saveFacilityDetails(); + this.$rootScope.$apply(); + + expect(this.FacilityRepository.prototype.update).toHaveBeenCalledWith(this.vm.facility); + }); + + it('should close loading modal and show error notification after save fails', function() { + this.FacilityRepository.prototype.update.andReturn(this.$q.reject()); + this.vm.saveFacilityDetails(); + this.$rootScope.$apply(); + + expect(this.loadingModalService.close).toHaveBeenCalled(); + expect(this.notificationService.error).toHaveBeenCalledWith('adminFacilityView.saveFacility.fail'); + }); + + it('should go to facility list after successful save', function() { + this.vm.saveFacilityDetails(); + this.$rootScope.$apply(); + + expect(this.$state.go).toHaveBeenCalledWith('openlmis.administration.facilities', {}, { + reload: true + }); + }); + + it('should show success notification after successful save', function() { + this.vm.saveFacilityDetails(); + this.$rootScope.$apply(); + + expect(this.notificationService.success).toHaveBeenCalledWith('adminFacilityView.saveFacility.success'); + }); + }); + + describe('addProgram', function() { + + beforeEach(function() { + this.vm.facilityWithPrograms = {}; + this.vm.facilityWithPrograms.supportedPrograms = []; + }); + + it('should add supported program to the list', function() { + this.vm.selectedProgram = this.vm.programs[0]; + this.vm.selectedStartDate = new Date('08/10/2017'); + + var program = this.vm.selectedProgram; + + this.vm.addProgram(); + + expect(this.vm.facilityWithPrograms.supportedPrograms[0]) + .toEqual(angular.extend({}, program, { + supportStartDate: new Date('08/10/2017'), + supportActive: true + })); + }); + + it('should clear selections', function() { + this.vm.selectedProgram = this.vm.programs[0]; + this.vm.selectedStartDate = new Date('08/10/2017'); + + this.vm.addProgram(); + + expect(this.vm.selectedProgram).toBe(null); + expect(this.vm.selectedStartDate).toBe(null); + }); + + }); + + describe('addWard', function() { + + beforeEach(function() { + this.vm.wards = []; + this.vm.facility = { + id: 'facility-id' + }; + this.vm.newWard = { + code: 'ward-code', + disabled: false + }; + }); + + it('should add new ward to the list', function() { + var newWard = angular.copy(this.vm.newWard); + newWard.facility = { + id: this.vm.facility.id + }; + + spyOn(this.wardService, 'getAllWards').andReturn(this.$q.when({ + content: [] + })); + + this.vm.addWard(); + this.$rootScope.$apply(); + + expect(this.vm.wards[0]).toEqual(newWard); + }); + + it('should clear newWard', function() { + spyOn(this.wardService, 'getAllWards').andReturn(this.$q.when({ + content: [] + })); + + this.vm.addWard(); + this.$rootScope.$apply(); + + expect(this.vm.newWard).toEqual({ + disabled: false + }); + }); + + it('should not add new ward if it exists in db', function() { + var existingWard = angular.copy(this.vm.newWard); + existingWard.facility = { + id: this.vm.facility.id + }; + + spyOn(this.wardService, 'getAllWards').andReturn(this.$q.when({ + content: [existingWard] + })); + + this.vm.addWard(); + this.$rootScope.$apply(); + + expect(this.vm.wards.length).toBe(0); + }); + + it('should not add new ward if it exists in local state', function() { + var existingWard = angular.copy(this.vm.newWard); + existingWard.facility = { + id: this.vm.facility.id + }; + + this.vm.wards.push(existingWard); + + spyOn(this.wardService, 'getAllWards').andReturn(this.$q.when({ + content: [] + })); + + this.vm.addWard(); + this.$rootScope.$apply(); + + expect(this.vm.wards.length).toBe(1); + }); + }); + + describe('saveFacilityWards', function() { + + it('should prompt user for confirmation before saving', function() { + this.vm.saveFacilityWards(); + + expect(this.confirmService.confirm).toHaveBeenCalledWith( + 'adminFacilityView.savingConfirmation', + 'adminFacilityView.save' + ); + }); + + it('should open loading modal after user confirms', function() { + this.confirmDeferred.resolve(); + this.vm.saveFacilityWards(); + this.$rootScope.$apply(); + + expect(this.loadingModalService.open).toHaveBeenCalled(); + }); + + it('should call wardService.saveFacilityWards with correct parameters', function() { + this.confirmDeferred.resolve(); + this.vm.saveFacilityWards(); + this.$rootScope.$apply(); + + expect(this.wardService.saveFacilityWards).toHaveBeenCalledWith(this.vm.wards); + }); + + it('should show success notification and navigate to facility list if saved successfully', function() { + this.confirmDeferred.resolve(); + this.saveFacilityDetailsDeferred.resolve(this.wards); + this.vm.saveFacilityWards(); + this.$rootScope.$apply(); + + expect(this.notificationService.success).toHaveBeenCalledWith('adminFacilityView.saveWards.success'); + expect(this.$state.go).toHaveBeenCalledWith('openlmis.administration.facilities', {}, { + reload: true + }); + }); + + it('should show error notification and close loading modal if wards save has failed', function() { + this.confirmDeferred.resolve(); + this.saveFacilityDetailsDeferred.reject(); + this.vm.saveFacilityWards(); + this.$rootScope.$apply(); + + expect(this.notificationService.error).toHaveBeenCalledWith('adminFacilityView.saveWards.fail'); + expect(this.loadingModalService.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/admin-facility-view/facility-view.html b/src/admin-facility-view/facility-view.html new file mode 100644 index 0000000..3c0195b --- /dev/null +++ b/src/admin-facility-view/facility-view.html @@ -0,0 +1,28 @@ +