From 4080080cb39d50405a8708a4c622c5448f86556d Mon Sep 17 00:00:00 2001 From: febbweiss Date: Tue, 6 Oct 2015 08:42:21 +0000 Subject: [PATCH] Feature: add account entries listing --- public/account/account.controller.js | 172 +++++++++++++++ public/account/account.view.html | 96 +++++++++ public/css/cloudbudget.css | 6 + public/js/services/account.service.js | 58 ++++++ test/account.controller.spec.js | 287 ++++++++++++++++++++++++++ 5 files changed, 619 insertions(+) create mode 100644 public/account/account.controller.js create mode 100644 public/account/account.view.html create mode 100644 public/css/cloudbudget.css create mode 100644 public/js/services/account.service.js create mode 100644 test/account.controller.spec.js diff --git a/public/account/account.controller.js b/public/account/account.controller.js new file mode 100644 index 0000000..c8080f2 --- /dev/null +++ b/public/account/account.controller.js @@ -0,0 +1,172 @@ +(function(){ + 'use strict'; + + angular + .module('cloudbudget') + .filter('category', CategoryFilter) + .filter('sub_category', SubcategoryFilter) + .controller('AccountController', AccountController); + + function CategoryFilter() { + return function(input, categories) { + if( !input ) { + return ''; + } + var category = categories.filter(function(elt, idx) { + return elt._id === input; + }); + if( category.length > 0 ) { + return category[0].label; + } + return ''; + }; + } + + function SubcategoryFilter() { + return function(input, category_id, categories) { + if( !input || !category_id) { + return ''; + } + + var category = categories.filter(function(elt, idx) { + return elt._id === category_id; + })[0]; + + if( !category ) { + return ''; + } + + var res = category.sub_categories.filter( function(elt, idx) { + return elt._id === input; + }); + if( res.length === 1 ) { + return res[0].label; + } else { + return ''; + } + }; + } + + AccountController.$inject = ['$scope', '$location', '$routeParams', 'FlashService', 'AccountService']; + + function AccountController($scope, $location, $routeParams, FlashService, AccountService) { + var vm = this; + + $scope.calendar = { + opened: {}, + dateFormat: 'dd/MM/yyyy', + dateOptions: {}, + open: function($event, which) { + $event.preventDefault(); + $event.stopPropagation(); + $scope.calendar.opened[which] = true; + } + }; + + vm.dataLoading = false; + vm.entries = []; + vm.categories = []; + vm.sub_categories = []; + vm.account = undefined; + vm.create = create; + vm.drop = drop; + vm.edit = edit; + vm.updateSubCategory = updateSubCategory; + vm.updateSubCategoryEditForm = updateSubCategoryEditForm; + vm.disabledSubCategories = false; + vm.edit_sub_categories = []; + + (function init() { + vm.dataLoading = true; + AccountService.details($routeParams.account_id) + .then(function(response) { + if( response.success ) { + vm.account = response.account; + vm.categories = angular.copy(vm.account.categories); + vm.categories.unshift({_id: '', label: ''}); + } else { + FlashService.error(response.message); + } + vm.dataLoading = false; + }); + AccountService.list($routeParams.account_id) + .then(function(response) { + if( response.success ) { + vm.entries = response.data.entries; + } else { + FlashService.error(response.message); + } + }); + })(); + + function create() { + vm.dataLoading = true; + AccountService.create(vm.account, vm.entry) + .then( function(response) { + if( response.success) { + vm.entries = response.data.entries; + } else { + FlashService.error(response.message); + } + + vm.dataLoading = false; + }); + vm.entry = angular.copy({}); + $scope.form.$setPristine(); + }; + + function drop(entry) { + vm.dataLoading = true; + AccountService.drop(vm.account, entry) + .then(function(response) { + if( response.success ) { + vm.entries = response.data.entries; + } else { + FlashService.error( response.message ); + } + vm.dataLoading = false; + }); + }; + + function edit(altered, origin) { + vm.dataLoading = true; + return AccountService.edit(vm.account, origin._id, altered) + .then( function(response) { + vm.dataLoading = false; + if( response.success ) { + var index = vm.entries.map(function (item) { + return item._id; + }).indexOf(origin._id); + vm.entries[index] = response.data.entries[index]; + } else { + var index = vm.entries.map(function (item) { + return item._id; + }).indexOf(origin._id); + vm.entries[index] = origin; + FlashService.error( response.message ); + return false; + } + }) + }; + + function updateSubCategory() { + vm.sub_categories = getSubCategories(vm.entry.category); + }; + + function updateSubCategoryEditForm(category_id) { + vm.edit_sub_categories = getSubCategories(category_id); + vm.disabledSubCategories = !vm.edit_sub_categories || vm.edit_sub_categories.length === 0; + }; + + function getSubCategories(category_id) { + var categories = vm.categories.filter(function(elt, idx) { + return elt._id === category_id; + }); + if( categories.length === 0 ) { + return []; + } else { + return categories[0].sub_categories; + } + } + } +})(); \ No newline at end of file diff --git a/public/account/account.view.html b/public/account/account.view.html new file mode 100644 index 0000000..7764ad2 --- /dev/null +++ b/public/account/account.view.html @@ -0,0 +1,96 @@ +
+
+
+
+
+ + Date is required +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ + Amount is required +
+
+
+ + +
+
+
+
+
+ {{entry.date | date: "dd/MM/yyyy" }} +
+
+ {{entry.category | category:vm.categories}} +
+
+ {{entry.sub_category | sub_category:entry.category:vm.categories}} +
+
+ {{entry.label}} +
+
+ + {{entry.amount | currency }} + +
+
+
+ + + + + +
+ + + +
+
+
\ No newline at end of file diff --git a/public/css/cloudbudget.css b/public/css/cloudbudget.css new file mode 100644 index 0000000..ec07eb0 --- /dev/null +++ b/public/css/cloudbudget.css @@ -0,0 +1,6 @@ +div.div-striped div.row:nth-of-type(odd) { + background: #e0e0e0; +} +div.div-striped div.row:nth-of-type(even) { + background: #FFFFFF; +} diff --git a/public/js/services/account.service.js b/public/js/services/account.service.js new file mode 100644 index 0000000..b099d67 --- /dev/null +++ b/public/js/services/account.service.js @@ -0,0 +1,58 @@ +(function() { + 'use strict'; + + angular + .module('cloudbudget') + .factory('AccountService', AccountService); + + AccountService.$inject =['$http', 'apiRoutes']; + + function AccountService($http, apiRoute) { + + var service = {}; + service.details = details; + service.list = list; + service.create = create; + service.drop = drop; + service.edit = edit; + + return service; + + function details(account_id) { + return $http.get( apiRoute.accounts + account_id) + .then(function handleSuccess(response) { + return {success: true, account: response.data}; + }, handleError('Error during accounts listing')); + } + + function list(account_id) { + return $http.get( apiRoute.accounts + account_id + '/entries') + .then(handleSuccess, handleError('Error listing account entries')); + } + + function create(account, entry) { + return $http.post( apiRoute.accounts + account._id + '/entries', entry) + .then(handleSuccess, handleError('Error creating entry')); + } + + function drop(account, entry) { + return $http.delete(apiRoute.accounts + account._id + '/entries/' + entry._id) + .then(handleSuccess, handleError('Error deleting entry')); + } + + function edit(account, id, entry) { + return $http.put(apiRoute.accounts + account._id + '/entries/' + id, entry) + .then(handleSuccess, handleError('Error updating entry')); + } + + function handleSuccess(response) { + return {success: true, data: response.data}; + } + + function handleError(error) { + return function() { + return {success: false, message: error}; + }; + } + } +})(); \ No newline at end of file diff --git a/test/account.controller.spec.js b/test/account.controller.spec.js new file mode 100644 index 0000000..fdb4ef7 --- /dev/null +++ b/test/account.controller.spec.js @@ -0,0 +1,287 @@ +describe('AccountController', function() { + + var $location, + $rootScope, + $scope, + $timeout, + $httpBackend, + AccountService, + FlashService, + createController, + apiRoutes, + shouldPass, + DEFAULT_ACCOUNT = { + "name": "test", + "reference": "1234567890", + "user_id": "55b78934d2a706265ea28e9c", + "_id": "560aa0e79633cd7c1495ff21", + "categories": [{ + "key": "alimony_payments", + "label": "Alimony Payments", + "_id": "560a84058812ad8d0ff200ef", + "sub_categories": [] + }, { + "key": "automobile_expenses", + "label": "Automobile Expenses", + "_id": "560a84058812ad8d0ff200f0", + "sub_categories": [{ + "label": "Car Payment", + "key": "car_payment", + "_id": "560a84058812ad8d0ff200f3" + }, { + "label": "Gasoline", + "key": "gasoline", + "_id": "560a84058812ad8d0ff200f2" + }, { + "label": "Maintenance", + "key": "maintenance", + "_id": "560a84058812ad8d0ff200f1" + }] + }] + }, + DEFAULT_ENTRY = { + "_id": "561280789f3c83904adcf41b", + "account_id": "560a84058812ad8d0ff200ee", + "amount": 100, + "date": "2015-09-29T22:00:00.000Z", + "type": "DEPOSIT", + "category": "560a84058812ad8d0ff200f0", + "sub_category": "560a84058812ad8d0ff200f3" + }; + + beforeEach(module('cloudbudget')); + + beforeEach(inject(function ( _$rootScope_, _$httpBackend_, $controller, _$location_, $routeParams, _$timeout_, _AccountService_, _FlashService_, _apiRoutes_) { + $location = _$location_; + $httpBackend = $httpBackend; + $rootScope = _$rootScope_.$new(); + $scope = _$rootScope_.$new(); + $scope.form = { + $valid: true, + $setPristine: function() {} + }; + $timeout = _$timeout_; + AccountService = _AccountService_; + FlashService = _FlashService_; + apiRoutes = _apiRoutes_; + + createController = function() { + return $controller('AccountController', { + '$scope': $scope, + '$location': $location, + '$rootScope': $rootScope, + '$routeParams': {account_id: DEFAULT_ACCOUNT._id}, + FlashService: _FlashService_, + AccountService: _AccountService_, + }); + }; + })); + + describe('init()', function() { + it('should init successfully', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[DEFAULT_ENTRY], balance: 100}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + should.exist(accountController.account); + accountController.account._id.should.be.equal(DEFAULT_ACCOUNT._id); + accountController.entries.should.be.instanceof(Array).and.have.lengthOf(1); + })); + + it('should fail to init', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(400); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond(400); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + should.not.exist(accountController.account); + accountController.entries.should.be.instanceof(Array).and.have.lengthOf(0); + })); + }); + + describe('* create()', function() { + it('should create successfully', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[], balance: 0}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + $httpBackend.expect('POST', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: DEFAULT_ENTRY, entries:[DEFAULT_ENTRY], balance: 100}); + + accountController.entry = DEFAULT_ENTRY; + + accountController.create(); + $httpBackend.flush(); + $timeout.flush(); + + var entry = accountController.entries[0]; + entry.amount.should.be.equal(DEFAULT_ENTRY.amount); + entry.category.should.be.equal(DEFAULT_ENTRY.category); + entry.sub_category.should.be.equal(DEFAULT_ENTRY.sub_category); + entry.type.should.be.equal(DEFAULT_ENTRY.type); + should.exist(entry._id); + })); + + it('should fail to create entry', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[], balance: 0}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + $httpBackend.expect('POST', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond(400, [{"field":"amount","rule":"required","message":"Path `amount` is required."}]); + + + accountController.entry = { + date: DEFAULT_ENTRY.date + }; + + accountController.create(); + $httpBackend.flush(); + $timeout.flush(); + + accountController.entries.should.be.instanceof(Array).and.have.lengthOf(0); + })); + }); + + describe('* delete()', function() { + it('should delete successfully', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[DEFAULT_ENTRY], balance: 100}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + $httpBackend.expect('DELETE', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries/' + DEFAULT_ENTRY._id) + .respond(204, {entry: null, entries:[], balance: 0}); + + accountController.drop( accountController.entries[0] ); + + $httpBackend.flush(); + $timeout.flush(); + + accountController.entries.should.be.instanceof(Array).and.have.lengthOf(0); + })); + + it('should fail to delete unknown entry', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[DEFAULT_ACCOUNT], balance: 100}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + $httpBackend.expect('DELETE', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries/fake_id') + .respond(200, {entry: null, entries:[DEFAULT_ENTRY], balance: 100}); + + accountController.drop({_id: 'fake_id'}); + $httpBackend.flush(); + $timeout.flush(); + + accountController.entries.should.be.instanceof(Array).and.have.lengthOf(1); + accountController.entries[0]._id.should.be.equal(DEFAULT_ENTRY._id); + })); + }); + + describe('* edit()', function() { + it('should edit successfully', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[DEFAULT_ENTRY], balance: 100}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + var altered_entry = { + "_id": "561280789f3c83904adcf41b", + "account_id": "560a84058812ad8d0ff200ee", + "amount": 120, + "date": "2015-09-29T22:00:00.000Z", + "type": "DEPOSIT", + "category": "560a84058812ad8d0ff200f0", + "sub_category": "560a84058812ad8d0ff200f3" + }; + + $httpBackend.expect('PUT', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries/' + DEFAULT_ENTRY._id) + .respond({entry: altered_entry, entries:[altered_entry], balance: 120}); + + accountController.edit(altered_entry, DEFAULT_ENTRY); + $httpBackend.flush(); + $timeout.flush(); + + var entry = accountController.entries[0]; + entry.amount.should.be.equal(altered_entry.amount); + entry.category.should.be.equal(DEFAULT_ENTRY.category); + entry.sub_category.should.be.equal(DEFAULT_ENTRY.sub_category); + entry.type.should.be.equal(DEFAULT_ENTRY.type); + })); + + it('should fail to edit unknown entry', inject(function($httpBackend) { + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id) + .respond(DEFAULT_ACCOUNT); + + $httpBackend.expect('GET', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries') + .respond({entry: null, entries:[DEFAULT_ENTRY], balance: 100}); + + var accountController = createController(); + $httpBackend.flush(); + $timeout.flush(); + + var altered_entry = { + "_id": "561280789f3c83904adcf41b", + "account_id": "560a84058812ad8d0ff200ee", + "amount": 120, + "date": "2015-09-29T22:00:00.000Z", + "type": "DEPOSIT", + "category": "560a84058812ad8d0ff200f0", + "sub_category": "560a84058812ad8d0ff200f3" + }; + + $httpBackend.expect('PUT', apiRoutes.accounts + DEFAULT_ACCOUNT._id + '/entries/' + DEFAULT_ENTRY._id) + .respond(400, {entry: DEFAULT_ENTRY, entries:[DEFAULT_ENTRY], balance: 100}); + + accountController.edit(altered_entry, DEFAULT_ENTRY); + $httpBackend.flush(); + $timeout.flush(); + + var entry = accountController.entries[0]; + entry.amount.should.be.equal(DEFAULT_ENTRY.amount); + entry.category.should.be.equal(DEFAULT_ENTRY.category); + entry.sub_category.should.be.equal(DEFAULT_ENTRY.sub_category); + entry.type.should.be.equal(DEFAULT_ENTRY.type); + })); + }); + +}); \ No newline at end of file