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 @@
+
+
+
+
+ {{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