accountant-ui/accountant-ui/js/operations.js

542 lines
15 KiB
JavaScript
Raw Normal View History

2016-10-09 20:17:11 +02:00
// vim: set tw=80 ts=4 sw=4 sts=4:
2016-04-12 10:55:08 +02:00
/*
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/>.
*/
'use strict';
angular.module('accountant.operations', [
'accountant.accounts',
'ngRoute',
'ngResource',
'ngBootbox',
'ui-notification',
'mgcrea.ngStrap',
2016-10-09 20:17:11 +02:00
'highcharts-ng'
2016-04-12 10:55:08 +02:00
])
2016-10-14 08:19:23 +02:00
.config(function($resourceProvider) {
2016-04-12 10:55:08 +02:00
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
2016-10-14 08:19:23 +02:00
})
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
.factory('Operation', function($resource) {
2016-04-12 10:55:08 +02:00
return $resource(
'/api/operation/:id', {
id: '@id'
}
);
2016-10-14 08:19:23 +02:00
})
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
.factory('OHLC', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/ohlc', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
.factory('Category', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/category', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
.factory('Balance', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/balance', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
2016-04-12 10:55:08 +02:00
/*
* Controller for category chart.
*/
2016-10-14 08:19:23 +02:00
.controller('CategoryChartController',
2016-10-14 09:02:44 +02:00
function($rootScope, Category, Balance) {
2016-10-14 08:19:23 +02:00
var vm = this;
2016-10-09 20:17:11 +02:00
var colors = Highcharts.getOptions().colors;
2016-10-14 08:19:23 +02:00
vm.revenueColor = colors[2];
vm.expenseColor = colors[3];
2016-10-09 20:17:11 +02:00
// Configure pie chart for categories.
2016-10-14 08:19:23 +02:00
vm.config = {
2016-10-09 20:17:11 +02:00
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() {
2016-10-14 09:20:02 +02:00
// eslint-disable-next-line angular/controller-as-vm
2016-10-09 20:17:11 +02:00
return this.point.name;
},
distance: -40
}
}, {
name: 'Value',
data: [],
innerSize: '66%',
size: '60%',
dataLabels: {
formatter: function() {
2016-10-14 09:20:02 +02:00
// eslint-disable-next-line angular/controller-as-vm
if (this.point.name !== null && this.percentage >= 2.5) {
// eslint-disable-next-line angular/controller-as-vm
return this.point.name;
}
return null;
}
2016-10-09 20:17:11 +02:00
}
}]
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
vm.brightenColor = function(color) {
2016-10-09 20:17:11 +02:00
var brightness = 0.2;
2016-10-12 19:57:19 +02:00
// eslint-disable-next-line new-cap
2016-10-09 20:17:11 +02:00
return Highcharts.Color(color).brighten(brightness).get();
};
// Load categories, mainly to populate the pie chart.
2016-10-14 08:19:23 +02:00
vm.load = function(begin, end) {
vm.config.loading = true;
2016-10-09 20:17:11 +02:00
Category.query({
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
}, function(data) {
var expenses = [];
var revenues = [];
2016-10-14 08:19:23 +02:00
var expenseColor = vm.brightenColor(vm.expenseColor);
var revenueColor = vm.brightenColor(vm.revenueColor);
2016-10-09 20:17:11 +02:00
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].
2016-10-14 08:19:23 +02:00
vm.config.series[1].data = revenues.concat(expenses);
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
vm.config.loading = false;
2016-10-09 20:17:11 +02:00
});
};
/*
* Get account balance.
*/
2016-10-14 08:19:23 +02:00
vm.getBalance = function(begin, end) {
2016-10-09 20:17:11 +02:00
Balance.get({
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
}, function(balance) {
// Update pie chart subtitle with Balance.
2016-10-14 08:19:23 +02:00
vm.config.subtitle = {
2016-10-09 20:17:11 +02:00
text: 'Balance: ' + balance.balance
};
2016-10-14 08:19:23 +02:00
vm.config.series[0].data = [{
2016-10-09 20:17:11 +02:00
name: 'Revenues',
y: balance.revenues,
2016-10-14 08:19:23 +02:00
color: vm.revenueColor
2016-10-09 20:17:11 +02:00
}, {
name: 'Expenses',
y: -balance.expenses,
2016-10-14 08:19:23 +02:00
color: vm.expenseColor
2016-10-09 20:17:11 +02:00
}];
});
};
// Reload categories and account status on range selection.
2016-10-14 08:19:23 +02:00
vm.onRangeSelected = $rootScope.$on('rangeSelectedEvent', function(e, args) {
vm.load(args.begin, args.end);
vm.getBalance(args.begin, args.end);
});
2016-10-14 08:51:35 +02:00
$rootScope.$on('$destroy', function(){
2016-10-14 08:19:23 +02:00
vm.onRangeSelected = angular.noop();
2016-04-12 10:55:08 +02:00
});
2016-10-09 20:17:11 +02:00
}
2016-10-14 08:19:23 +02:00
)
2016-04-12 10:55:08 +02:00
/*
* Controller for the sold chart.
*/
2016-10-14 09:02:44 +02:00
.controller('SoldChartController', function($rootScope, $scope, OHLC) {
2016-10-14 08:19:23 +02:00
var vm = this;
// Configure chart for operations.
vm.config = {
options: {
chart: {
zoomType: 'x'
2016-10-09 20:17:11 +02:00
},
2016-10-14 08:19:23 +02:00
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
2016-10-09 20:17:11 +02:00
},
2016-10-14 08:19:23 +02:00
navigator: {
enabled: true
2016-10-09 20:17:11 +02:00
},
2016-10-14 08:19:23 +02:00
tooltip: {
crosshairs: true,
shared: true,
valueDecimals: 2,
valueSuffix: '€'
2016-10-09 20:17:11 +02:00
},
2016-10-14 08:19:23 +02:00
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) {
2016-10-14 08:51:35 +02:00
$scope.$emit('rangeSelectedEvent', {
2016-10-14 08:19:23 +02:00
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
};
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
vm.loadSolds = function() {
vm.config.loading = true;
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
OHLC.query({}, function(data) {
vm.config.series[0].data = [];
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
angular.forEach(data, function(operation) {
vm.config.series[0].data.push([
moment.utc(operation.operation_date).valueOf(),
operation.open, operation.high, operation.low, operation.close
]);
});
2016-10-09 20:17:11 +02:00
2016-10-14 08:51:35 +02:00
$scope.$emit('rangeSelectedEvent', {
2016-10-14 08:19:23 +02:00
begin: vm.config.xAxis.currentMin,
end: vm.config.xAxis.currentMax
2016-10-09 20:17:11 +02:00
});
2016-10-14 08:19:23 +02:00
vm.config.loading = false;
2016-10-09 20:17:11 +02:00
});
2016-10-14 08:19:23 +02:00
};
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
// Reload solds when an operation is saved.
vm.onOperationSaved = $rootScope.$on('operationSavedEvent', function() {
vm.loadSolds();
});
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
// Reload solds when an operation is deleted.
vm.onOperationDeleted = $rootScope.$on('operationDeletedEvent', function() {
vm.loadSolds();
});
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
// Update authorized overdraft on account loading.
vm.onAccountLoaded = $rootScope.$on('accountLoadedEvent', function(e, account) {
vm.config.yAxis.plotLines[1].value = account.authorized_overdraft;
});
2016-10-14 08:51:35 +02:00
$rootScope.$on('$destroy', function() {
2016-10-14 08:19:23 +02:00
vm.onOperationSaved = angular.noop();
vm.onOperationDeleted = angular.noop();
vm.onAccountLoaded = angular.noop();
});
// Select beginning and end of month.
vm.loadSolds();
})
2016-04-12 10:55:08 +02:00
/*
* Controller for the operations.
*/
2016-10-14 08:51:35 +02:00
.controller('OperationController', function($rootScope, $scope, $routeParams, $ngBootbox, Notification, Account, Operation) {
2016-10-14 08:19:23 +02:00
var vm = this;
// List of operations.
vm.operations = [];
/*
* Add an empty operation.
*/
vm.add = function() {
var operation = new Operation({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
vm.operations.splice(0, 0, operation);
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
/*
* Load operations.
*/
vm.load = function(begin, end) {
vm.operations = Operation.query({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId,
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
});
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
/*
* Cancel edition.
*/
vm.cancelEdit = function(operation, rowform, $index) {
if (operation.id) {
rowform.$cancel();
} else {
vm.operations.splice($index, 1);
}
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
/*
* Toggle pointed indicator for an operation.
*/
vm.togglePointed = function(operation, rowform) {
operation.pointed = !operation.pointed;
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
// Save operation if not editing it.
if (!rowform.$visible) {
vm.save(operation);
}
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
/*
* Toggle cancel indicator for an operation.
*/
vm.toggleCanceled = function(operation) {
operation.canceled = !operation.canceled;
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
vm.save(operation);
};
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
/*
* Save an operation and emit operationSavedEvent.
*/
vm.save = function($data, $index) {
// Check if $data is already a resource.
var operation;
if ($data.$save) {
operation = $data;
} else {
operation = vm.operations[$index];
operation = angular.merge(operation, $data);
}
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
operation.confirmed = true;
2016-04-12 10:55:08 +02:00
2016-10-14 08:19:23 +02:00
return operation.$save().then(function(data) {
Notification.success('Operation #' + data.id + ' saved.');
2016-05-24 08:49:55 +02:00
2016-10-14 08:51:35 +02:00
$scope.$emit('operationSavedEvent', data);
2016-10-14 08:19:23 +02:00
});
};
2016-05-24 08:49:55 +02:00
2016-10-14 08:19:23 +02:00
/*
* Delete an operation and emit operationDeletedEvent.
*/
vm.delete = function(operation, $index) {
var id = operation.id;
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
$ngBootbox.confirm(
'Voulez-vous supprimer l\'opération \\\'' + operation.label + '\\\' ?',
function(result) {
if (result) {
operation.$delete().then(function() {
Notification.success('Operation #' + id + ' deleted.');
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
// Remove operation from array.
vm.operation.splice($index, 1);
2016-10-09 20:17:11 +02:00
2016-10-14 08:51:35 +02:00
$scope.$emit('operationDeletedEvent', operation);
2016-10-14 08:19:23 +02:00
});
2016-05-24 08:49:55 +02:00
}
2016-10-14 08:19:23 +02:00
}
);
};
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
vm.account = Account.get({
id: $routeParams.accountId
});
2016-05-24 08:49:55 +02:00
2016-10-14 08:19:23 +02:00
/*
* Reload operations on rangeSelectedEvent.
*/
vm.onRangeSelected = $rootScope.$on('rangeSelectedEvent', function(e, args) {
vm.load(args.begin, args.end);
});
2016-10-14 08:51:35 +02:00
$rootScope.$on('$destroy', function() {
2016-10-14 08:19:23 +02:00
vm.onRangeSelected = angular.noop;
});
})
2016-10-09 20:17:11 +02:00
2016-10-14 08:19:23 +02:00
.directive('operationFormDialog', function($log, $ngBootbox) {
2016-10-09 20:17:11 +02:00
return {
restrict: 'A',
scope: {
operation: '=ngModel'
},
link: function(scope, element) {
var title = 'Operation';
if (scope.operation && scope.operation.id) {
title = title + ' #' + scope.operation.id;
}
scope.form = {};
element.on('click', function() {
scope.data = {};
angular.copy(scope.operation, scope.data);
// Open dialog with form.
$ngBootbox.customDialog({
scope: scope,
title: title,
templateUrl: 'views/operation.form.tmpl.html',
onEscape: true,
buttons: {
save: {
label: 'Save',
className: 'btn-success',
callback: function() {
// Validate form
2016-10-14 08:19:23 +02:00
$log.log(scope.form);
2016-10-09 20:17:11 +02:00
// Save operation
2016-10-14 08:19:23 +02:00
$log.log(scope.operation);
2016-10-09 20:17:11 +02:00
// TODO Alexis Lahouze 2016-05-24 Save operation, handle return.
return false;
2016-05-24 08:49:55 +02:00
}
2016-10-09 20:17:11 +02:00
},
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: true
2016-05-24 08:49:55 +02:00
}
2016-10-09 20:17:11 +02:00
}
2016-05-24 08:49:55 +02:00
});
2016-10-09 20:17:11 +02:00
});
}
};
2016-05-24 08:49:55 +02:00
});