505 lines
14 KiB
JavaScript
505 lines
14 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/>.
|
|
*/
|
|
accountantApp
|
|
|
|
.factory("Entries", [ "$resource", function($resource) {
|
|
return $resource(
|
|
"/api/entries/:id", {
|
|
id: "@id"
|
|
}
|
|
);
|
|
}])
|
|
|
|
.controller(
|
|
"EntryController", [
|
|
"$scope", "$http", "$rootScope", "$filter", "$routeParams", "Entries",
|
|
function($scope, $http, $rootScope, $filter, $routeParams, Entries) {
|
|
// Range for entries.
|
|
$scope.begin = moment.utc().startOf('month');
|
|
$scope.end = moment.utc().endOf('month');
|
|
|
|
// Entry store and selection
|
|
$scope.entries = [];
|
|
|
|
$scope.categories = [];
|
|
|
|
$scope.account = null;
|
|
|
|
|
|
// Function to reset the new entry.
|
|
$scope.resetNewEntry = function() {
|
|
// The new entry.
|
|
$scope.newEntry = new Entries({});
|
|
};
|
|
|
|
$scope.resetNewEntry();
|
|
|
|
$scope.setExtremes = function(e) {
|
|
begin = moment.utc(e.min);
|
|
end = moment.utc(e.max);
|
|
|
|
$scope.selectRange(begin, end);
|
|
};
|
|
|
|
$scope.selectRange = function(begin, end) {
|
|
$scope.begin = begin;
|
|
$scope.end = end;
|
|
|
|
$scope.$emit("rangeSelectedEvent", {begin: begin, end: end});
|
|
};
|
|
|
|
// Configure pie chart for categories.
|
|
$scope.categoriesChartConfig = {
|
|
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;
|
|
},
|
|
}
|
|
}]
|
|
};
|
|
|
|
// Configure chart for entries.
|
|
$scope.soldChartConfig = {
|
|
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: $scope.setExtremes
|
|
}
|
|
},
|
|
yAxis: {
|
|
plotLines: [{
|
|
color: "orange",
|
|
width: 2,
|
|
value: 0.0
|
|
}, {
|
|
color: "red",
|
|
width: 2,
|
|
value: 0.0
|
|
}]
|
|
},
|
|
useHighStocks: true
|
|
};
|
|
|
|
// Load categories, mainly to populate the pie chart.
|
|
$scope.loadCategories = function() {
|
|
$scope.categoriesChartConfig.loading = true;
|
|
|
|
$http.get("/api/categories", {
|
|
params: {
|
|
account: $scope.account.id,
|
|
begin: $scope.begin.format('YYYY-MM-DD'),
|
|
end: $scope.end.format('YYYY-MM-DD')
|
|
}
|
|
}).success(function(data) {
|
|
var expenses = [], revenues = [];
|
|
var colors = Highcharts.getOptions().colors;
|
|
|
|
var config = $scope.categoriesChartConfig;
|
|
|
|
angular.forEach(angular.fromJson(data), function(category) {
|
|
brightness = 0.2;
|
|
|
|
expenses.push({
|
|
name: category.category,
|
|
y: -category.expenses,
|
|
color: Highcharts.Color(config.series[0].data[1].color).brighten(brightness).get()
|
|
});
|
|
|
|
revenues.push({
|
|
name: category.category,
|
|
y: category.revenues,
|
|
color: Highcharts.Color(config.series[0].data[0].color).brighten(brightness).get()
|
|
});
|
|
});
|
|
|
|
// Note: expenses and revenues must be in the same order than in series[0].
|
|
config.series[1].data = revenues.concat(expenses);
|
|
|
|
$scope.categoriesChartConfig.loading = false;
|
|
|
|
$scope.loadSolds();
|
|
});
|
|
};
|
|
|
|
$scope.loadSolds = function() {
|
|
$scope.soldChartConfig.loading = true;
|
|
|
|
$http.get("/api/solds", {
|
|
params: {
|
|
account: $scope.account.id,
|
|
}
|
|
}).success(function(data) {
|
|
var config = $scope.soldChartConfig;
|
|
|
|
config.series[0].data = [];
|
|
|
|
angular.forEach(data, function(entry) {
|
|
config.series[0].data.push([
|
|
moment.utc(entry.operation_date).valueOf(),
|
|
entry.open, entry.high, entry.low, entry.close
|
|
]);
|
|
});
|
|
|
|
$scope.soldChartConfig.loading = false;
|
|
|
|
$scope.loadEntries();
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Hook on account selected event to display account status.
|
|
*/
|
|
$rootScope.$on("accountSelectedEvent", function(event, args) {
|
|
$scope.getAccountStatus($routeParams.accountId);
|
|
});
|
|
|
|
$rootScope.$on("rangeSelectedEvent", function(event, args) {
|
|
$scope.getAccountStatus($routeParams.accountId);
|
|
});
|
|
|
|
$scope.getAccountStatus = function(accountId) {
|
|
$scope.categoriesChartConfig.loading = true;
|
|
|
|
$http.get("/api/accounts/" + accountId, {
|
|
params: {
|
|
begin: $scope.begin.format('YYYY-MM-DD'),
|
|
end: $scope.end.format('YYYY-MM-DD')
|
|
}
|
|
}).success(function(account) {
|
|
$scope.account = account;
|
|
$scope.categoriesChartConfig.loading = false;
|
|
|
|
$scope.$emit("accountLoadedEvent", account);
|
|
});
|
|
};
|
|
|
|
$rootScope.$on("accountLoadedEvent", $scope.loadCategories);
|
|
|
|
$rootScope.$on("accountLoadedEvent", function(e, account) {
|
|
var colors = Highcharts.getOptions().colors;
|
|
|
|
var config = $scope.categoriesChartConfig;
|
|
|
|
// Update pie chart subtitle with Balance.
|
|
config.subtitle = {
|
|
text: "Balance: " + account.balance
|
|
};
|
|
|
|
config.series[0].data = [{
|
|
name: "Revenues",
|
|
y: account.revenues,
|
|
color: colors[2]
|
|
}, {
|
|
name: "Expenses",
|
|
y: -account.expenses,
|
|
color: colors[3],
|
|
}];
|
|
|
|
$scope.soldChartConfig.yAxis.plotLines[1].value = account.authorized_overdraft;
|
|
});
|
|
|
|
// Function to load entries from server for a specific account and month.
|
|
$scope.loadEntries = function() {
|
|
// Clean up selected entry.
|
|
$scope.selectedItem = null;
|
|
$scope.savedItem = null;
|
|
|
|
$scope.entries = Entries.query({
|
|
account: $scope.account.id,
|
|
begin: $scope.begin.format('YYYY-MM-DD'),
|
|
end: $scope.end.format('YYYY-MM-DD')
|
|
}, function(data) {
|
|
$scope.$emit("entriesLoadedEvent", {entries: data});
|
|
});
|
|
};
|
|
|
|
// Returns the CSS class for an entry row.
|
|
$scope.entryRowClass = function(entry) {
|
|
if($scope.isSaved(entry)) {
|
|
cssclass="";
|
|
} else {
|
|
cssclass="italic";
|
|
}
|
|
|
|
if(entry.canceled) {
|
|
cssclass += " stroke";
|
|
}
|
|
|
|
if(entry.sold < $scope.account.authorized_overdraft) {
|
|
cssclass += " danger";
|
|
} else if (entry.sold < 0) {
|
|
cssclass += " warning";
|
|
}
|
|
|
|
return cssclass;
|
|
};
|
|
|
|
// Returns the CSS class for an entry sold.
|
|
$scope.entryValueClass = function(sold) {
|
|
if(sold && sold < $scope.account.authorized_overdraft) {
|
|
return 'text-danger';
|
|
} else if (sold && sold < 0) {
|
|
return 'text-warning';
|
|
}
|
|
};
|
|
|
|
// Starts editing an entry
|
|
$scope.editEntry = function(entry) {
|
|
// Enter edit state.
|
|
entry.confirmed=true;
|
|
entry.state='edit';
|
|
};
|
|
|
|
// Returns true if the entry is in editing state.
|
|
$scope.isEditing = function(entry) {
|
|
return entry.state === 'edit';
|
|
};
|
|
|
|
// Returns true if the entry is in displaying state.
|
|
$scope.isDisplaying = function(entry) {
|
|
return !entry.state || entry.state === 'display';
|
|
};
|
|
|
|
// Returns true if the entry is a scheduled one.
|
|
$scope.isSaved = function(entry) {
|
|
return entry.confirmed;
|
|
};
|
|
|
|
// Cancel current editing entry or clears field if a new one.
|
|
$scope.cancelEditEntry = function(entry) {
|
|
sold = entry.sold;
|
|
entry.$get(function(entry) {
|
|
entry.sold = sold;
|
|
});
|
|
};
|
|
|
|
// Points an entry.
|
|
$scope.pointEntry = function(entry) {
|
|
entry.confirmed = true;
|
|
entry.pointed = !entry.pointed;
|
|
|
|
$scope.saveEntry(entry);
|
|
};
|
|
|
|
// Confirm an entry.
|
|
$scope.confirmEntry = function(entry) {
|
|
entry.confirmed = true;
|
|
|
|
$scope.saveEntry(entry);
|
|
};
|
|
|
|
// Create an new entry.
|
|
$scope.createEntry = function(entry) {
|
|
entry.account_id = $scope.account.id;
|
|
|
|
// Ajax call to create an entry
|
|
$scope.newEntry.$save(function(data) {
|
|
$scope.resetNewEntry();
|
|
|
|
// Send the "entry saved" event.
|
|
$scope.$emit("entryCreatedEvent", entry);
|
|
});
|
|
};
|
|
|
|
$rootScope.$on("entryCreatedEvent", function(e, entry) {
|
|
new PNotify({
|
|
type: "success",
|
|
title: "Save",
|
|
text: "Entry #" + entry.id + " created."
|
|
});
|
|
|
|
});
|
|
|
|
// Saves an existing entry.
|
|
$scope.saveEntry = function(entry) {
|
|
|
|
if(!entry.account_id) {
|
|
entry.account_id = $scope.account.id;
|
|
}
|
|
|
|
sold = entry.sold;
|
|
|
|
// Ajax call to save an entry
|
|
entry.$save(function(data) {
|
|
data.sold = sold;
|
|
|
|
// Send the "entry saved" event.
|
|
$scope.$emit("entrySavedEvent", entry);
|
|
});
|
|
};
|
|
|
|
$rootScope.$on("entrySavedEvent", function(e, entry) {
|
|
$scope.getAccountStatus($routeParams.accountId);
|
|
});
|
|
|
|
$rootScope.$on("entrySavedEvent", function(e, entry) {
|
|
new PNotify({
|
|
type: "success",
|
|
title: "Save",
|
|
text: "Entry #" + entry.id + " saved."
|
|
});
|
|
});
|
|
|
|
$scope.removeEntry = function(entry) {
|
|
$scope.removingEntry = entry;
|
|
$("#remove_entry").modal({
|
|
keyboard: false,
|
|
});
|
|
};
|
|
|
|
$scope.hideRemoveEntryPopup = function() {
|
|
$scope.removingEntry = null;
|
|
$("#remove_entry").modal("hide");
|
|
};
|
|
|
|
// Removes an entry.
|
|
$scope.confirmRemoveEntry = function() {
|
|
// Cancel current editing.
|
|
if ($scope.removingEntry) {
|
|
$scope.removingEntry.$delete(function (result) {
|
|
// Send the "entry removed" event.
|
|
$scope.$emit("entryRemovedEvent", $scope.removingEntry);
|
|
|
|
$scope.hideRemoveEntryPopup();
|
|
});
|
|
}
|
|
};
|
|
|
|
$rootScope.$on("entryRemovedEvent", function(e, entry) {
|
|
new PNotify({
|
|
type: "success",
|
|
title: "Delete",
|
|
text: "Entry #" + entry.id + " deleted."
|
|
});
|
|
});
|
|
|
|
$scope.closeModal = function(modalScope) {
|
|
// Close the modal dialog
|
|
if(modalScope && modalScope.dismiss) {
|
|
modalScope.dismiss();
|
|
}
|
|
};
|
|
|
|
$scope.getAccountStatus($routeParams.accountId);
|
|
}]);
|