467 lines
11 KiB
JavaScript
467 lines
11 KiB
JavaScript
|
/*
|
||
|
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 <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
// 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);
|
||
|
});
|
||
|
}]);
|