/* This file is part of Accountant. Accountant 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. Accountant 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 Accountant. If not, see . */ // vim: set tw=80 ts=2 sw=2 sts=2: 'use strict'; angular.module('accountant.operations', [ 'accountant.accounts', 'ngRoute', 'ngResource', 'ngBootbox', 'ui-notification', 'mgcrea.ngStrap', 'highcharts-ng', ]) .config(['$resourceProvider', function($resourceProvider) { // Keep trailing slashes to avoid redirect by flask.. $resourceProvider.defaults.stripTrailingSlashes = false; }]) .factory('Operation', [ '$resource', function($resource) { return $resource( '/api/operation/:id', { id: '@id' } ); }]) .factory('OHLC', [ '$resource', '$routeParams', function($resource, $routeParams) { return $resource( '/api/account/:account_id/ohlc', { account_id: $routeParams.accountId } ); }]) .factory('Category', [ '$resource', '$routeParams', function($resource, $routeParams) { return $resource( '/api/account/:account_id/category', { account_id: $routeParams.accountId } ); }]) .factory('Balance', [ '$resource', '$routeParams', function($resource, $routeParams) { return $resource( '/api/account/:account_id/balance', { account_id: $routeParams.accountId } ); }]) /* * Controller for category chart. */ .controller( 'CategoryChartController', [ '$rootScope', '$scope', '$http', 'Category', 'Balance', function($rootScope, $scope, $http, Category, Balance) { var colors = Highcharts.getOptions().colors; $scope.revenueColor = colors[2]; $scope.expenseColor = colors[3]; // Configure pie chart for categories. $scope.config = { options: { chart: { type: 'pie', animation: { duration: 500 } }, plotOptions: { pie: { startAngle: -90 }, series: { allowPointSelect: 0 } }, tooltip: { valueDecimals: 2, valueSuffix: '€' } }, yAxis: { title: { text: 'Categories' } }, title: { text: 'Répartition dépenses/recettes' }, series: [{ name: 'Value', data: [], innerSize: '33%', size: '60%', dataLabels: { formatter: function() { return this.point.name; }, distance: -40 } }, { name: 'Value', data: [], innerSize: '66%', size: '60%', dataLabels: { formatter: function() { return this.point.name !== null && this.percentage >= 2.5 ? this.point.name : null; }, } }] }; $scope.brightenColor = function(color) { var brightness = 0.2; return Highcharts.Color(color).brighten(brightness).get(); }; // Load categories, mainly to populate the pie chart. $scope.load = function(begin, end) { $scope.config.loading = true; Category.query({ begin: begin.format('YYYY-MM-DD'), end: end.format('YYYY-MM-DD') }, function(data) { var expenses = [], revenues = []; var expenseColor = $scope.brightenColor($scope.expenseColor); var revenueColor = $scope.brightenColor($scope.revenueColor); angular.forEach(angular.fromJson(data), function(category) { expenses.push({ name: category.category, y: -category.expenses, color: expenseColor }); revenues.push({ name: category.category, y: category.revenues, color: revenueColor }); }); // Note: expenses and revenues must be in the same order than in series[0]. $scope.config.series[1].data = revenues.concat(expenses); $scope.config.loading = false; }); }; /* * Get account balance. */ $scope.getBalance = function(begin, end) { Balance.get({ begin: begin.format('YYYY-MM-DD'), end: end.format('YYYY-MM-DD') }, function(balance) { // Update pie chart subtitle with Balance. $scope.config.subtitle = { text: 'Balance: ' + balance.balance }; $scope.config.series[0].data = [{ name: 'Revenues', y: balance.revenues, color: $scope.revenueColor }, { name: 'Expenses', y: -balance.expenses, color: $scope.expenseColor, }]; }); }; // Reload categories and account status on range selection. $rootScope.$on('rangeSelectedEvent', function(e, args) { $scope.load(args.begin, args.end); $scope.getBalance(args.begin, args.end); }); }]) /* * Controller for the sold chart. */ .controller( 'SoldChartController', [ '$rootScope', '$scope', '$http', 'OHLC', function($rootScope, $scope, $http, OHLC) { // Configure chart for operations. $scope.config = { options: { chart: { zoomType: 'x' }, rangeSelector: { buttons: [{ type: 'month', count: 1, text: '1m' }, { type: 'month', count: 3, text: '3m' }, { type: 'month', count: 6, text: '6m' }, { type: 'year', count: 1, text: '1y' }, { type: 'all', text: 'All' }], selected: 0, }, navigator: { enabled: true }, tooltip: { crosshairs: true, shared: true, valueDecimals: 2, valueSuffix: '€' }, scrollbar: { liveRedraw: false } }, series: [{ type: 'ohlc', name: 'Sold', data: [], dataGrouping : { units : [[ 'week', // unit name [1] // allowed multiples ], [ 'month', [1, 2, 3, 4, 6] ]] } }], title: { text: 'Sold evolution' }, xAxis: { type: 'datetime', dateTimeLabelFormats: { month: '%e. %b', year: '%Y' }, minRange: 3600 * 1000 * 24 * 14, // 2 weeks events: { afterSetExtremes: function(e) { $scope.$emit('rangeSelectedEvent', { begin: moment.utc(e.min), end: moment.utc(e.max) }); } }, currentMin: moment.utc().startOf('month'), currentMax: moment.utc().endOf('month') }, yAxis: { plotLines: [{ color: 'orange', width: 2, value: 0.0 }, { color: 'red', width: 2, value: 0.0 }] }, useHighStocks: true }; $scope.loadSolds = function() { $scope.config.loading = true; OHLC.query({}, function(data) { $scope.config.series[0].data = []; angular.forEach(data, function(operation) { $scope.config.series[0].data.push([ moment.utc(operation.operation_date).valueOf(), operation.open, operation.high, operation.low, operation.close ]); }); $scope.$emit('rangeSelectedEvent', { begin: $scope.config.xAxis.currentMin, end: $scope.config.xAxis.currentMax }); $scope.config.loading = false; }); }; // Reload solds when an operation is saved. $rootScope.$on('operationSavedEvent', function() { $scope.loadSolds(); }); // Reload solds when an operation is deleted. $rootScope.$on('operationDeletedEvent', function() { $scope.loadSolds(); }); // Update authorized overdraft on account loading. $rootScope.$on('accountLoadedEvent', function(e, account) { $scope.config.yAxis.plotLines[1].value = account.authorized_overdraft; }); // Select beginning and end of month. $scope.loadSolds(); }]) /* * Controller for the operations. */ .controller( 'OperationController', [ '$scope', '$rootScope', '$routeParams', '$ngBootbox', 'Notification', 'Account', 'Operation', function($scope, $rootScope, $routeParams, $ngBootbox, Notification, Account, Operation) { // List of operations. $scope.operations = []; /* * Add an empty operation. */ $scope.add = function() { var operation = new Operation({ account_id: $routeParams.accountId }); $scope.operations.splice(0, 0, operation); }; /* * Load operations. */ $scope.load = function(begin, end) { $scope.operations = Operation.query({ account_id: $routeParams.accountId, begin: begin.format('YYYY-MM-DD'), end: end.format('YYYY-MM-DD') }); }; /* * Cancel edition. */ $scope.cancelEdit = function(operation, rowform, $index) { if(!operation.id) { $scope.operations.splice($index, 1); } else { rowform.$cancel(); } }; /* * Toggle pointed indicator for an operation. */ $scope.togglePointed = function(operation, rowform) { operation.pointed = !operation.pointed; // Save operation if not editing it. if(!rowform.$visible) { $scope.save(operation); } }; /* * Toggle cancel indicator for an operation. */ $scope.toggleCanceled = function(operation) { operation.canceled = !operation.canceled; $scope.save(operation); }; /* * Save an operation and emit operationSavedEvent. */ $scope.save = function($data, $index) { // Check if $data is already a resource. var operation; if($data.$save) { operation = $data; } else { operation = $scope.operations[$index]; operation = angular.merge(operation, $data); } operation.confirmed = true; return operation.$save().then(function(data) { Notification.success('Operation #' + data.id + ' saved.'); $scope.$emit('operationSavedEvent', data); }); }; /* * Delete an operation and emit operationDeletedEvent. */ $scope.delete = function(operation, $index) { var id = operation.id; $ngBootbox.confirm( 'Voulez-vous supprimer l\'opération \\\'' + operation.label + '\\\' ?', function(result) { if(result) { operation.$delete().then(function() { Notification.success('Operation #' + id + ' deleted.'); // Remove operation from array. $scope.operation.splice($index, 1); $scope.$emit('operationDeletedEvent', operation); }); } } ); }; $scope.account = Account.get({ id: $routeParams.accountId }); /* * Reload operations on rangeSelectedEvent. */ $rootScope.$on('rangeSelectedEvent', function(e, args) { $scope.load(args.begin, args.end); }); }]);