accountant-ui/src/html/js/entries.js

469 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.
Foobar 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/>.
*/
// Entry object
function entry(){
this.id=ko.observable();
this.pointed=ko.observable();
this.operation_date=ko.observable();
this.label=ko.observable();
this.value=ko.observable();
this.account_id=ko.observable();
this.sold=ko.observable();
this.pointedsold=ko.observable();
this.category=ko.observable();
}
// Util function to show a message in message placeholder.
function message(alertType, title, message) {
$(".alert").alert('close');
$("#message-placeholder").append('<div class="alert alert-' + alertType + '"><button type="button" class="close" data-dismiss="alert">&times;</button><h4>' + title + '</h4>' + message + '</div>');
}
// The ListViewModel used to instanciate viewmodel.
var EntryController = function($scope, $http, $rootScope, $filter) {
// Entry store and selection
$scope.entries = [];
$scope.selectedItem = null;
$scope.account = null;
// Placeholder for saved value to cancel entry edition
$scope.savedItem = null;
$scope.entriesLoaded = function(entries) {
var entriesReversed = entries.slice().reverse();
var categories = [];
var chartValues = [];
var pieChartValuesTmp = {};
var pieChartValues = [];
angular.forEach(entriesReversed, function(entry) {
var category = entry.category;
var value = entry.value ? Number(entry.value) : null;
var sold = entry.sold ? Number(entry.sold) : null;
var operation_date = entry.operation_date;
if(operation_date && sold) {
chartValues.push([entry.operation_date, sold]);
}
if(category && category != '') {
if(categories.indexOf(category) == -1) {
categories.push(category);
}
if(value && value < 0.0) {
var oldValue = 0.0;
if(pieChartValuesTmp[category]) {
oldValue = pieChartValuesTmp[category];
}
pieChartValuesTmp[category] = oldValue - value;
}
}
});
// Second pass: transform to an array readable by jqplot.
angular.forEach(pieChartValuesTmp, function(value, key) {
pieChartValues.push([key, value]);
});
$scope.categories = categories;
$scope.drawChart({account: $scope.account, entries: chartValues}, "#entries-chart-placeholder");
$scope.drawPieChart(pieChartValues, "#expense-categories-chart-placeholder");
};
// Function to load entries from server for a specific account and month.
$scope.loadEntries = function(account, month) {
if(account) {
$scope.account = account;
}
// Clean up selected entry.
$scope.selectedItem = null;
$scope.savedItem = null;
if(account && month) {
$http.get("api/entries/" + account.id + "/" + month.year + "/" + month.month).success($scope.loadEntries_success);
} else {
$scope.loadEntries_success(null);
}
};
// Load entries success callback
$scope.loadEntries_success = function(data) {
var entries = [{
id: null,
pointed: false,
operation_date: null,
label: null,
value: null,
sold: null,
pointedsold: null,
category: null,
account_id: null,
state: 'edit'
}];
if(data) {
entries = entries.concat(angular.fromJson(data));
}
$scope.entries = entries;
$scope.$emit("entriesLoadedEvent", {entries: entries});
};
// Returns the CSS class for a pointed entry.
$scope.pointedEntryClass = function(entry) {
if(entry.pointed) {
return "active";
}
};
// Returns the CSS class for an entry row.
$scope.entryRowClass = function(sold) {
if(sold < $scope.account.authorized_overdraft) {
return 'error';
} else if (sold < 0) {
return 'warning';
}
};
// Returns the CSS class for an entry sold.
$scope.entryValueClass = function(sold) {
if(sold < $scope.account.authorized_overdraft) {
return 'text-error';
} else if (sold < 0) {
return 'text-warning';
}
};
// Starts editing an entry
$scope.editEntry = function(entry) {
// Cancel previous editing.
if($scope.selectedItem) {
$scope.cancelEditEntry($scope.selectedItem);
}
// Save current entry values.
$scope.savedItem = angular.copy(entry);
$scope.selectedItem = entry;
// Enter edit state.
entry.state='edit';
};
// Returns true if the entry is a new one.
$scope.isNew = function(entry) {
return !entry.id;
};
// 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.id && (!entry.state || entry.state === 'display');
};
$scope.iconSaveClass = function(entry) {
if($scope.isNew(entry)) {
return "icon-plus";
} else if ($scope.isEditing(entry)) {
return "icon-ok";
}
};
$scope.iconCancelClass = function(entry) {
if($scope.isNew(entry)) {
return "icon-remove";
} else if ($scope.isEditing(entry)) {
return "icon-ban-circle";
}
};
// Cancel current editing entry or clears field if a new one.
$scope.cancelEditEntry = function(entry) {
if ($scope.isNew(entry)) {
// We are cancelling the new entry, just reset it.
entry.id = null; // id should not change, but just in case...
entry.pointed = false;
entry.operation_date = null;
entry.label = null;
entry.value = null;
entry.account_id = $scope.account.id; // account_id should not change, but just in case...
entry.category = null;
} else if($scope.isEditing(entry)) {
if($scope.savedItem) {
// Reset selected item fields to saved item ones.
entry.id = $scope.savedItem.id; // id should not change, but just in case...
entry.pointed = $scope.savedItem.pointed;
entry.operation_date = $scope.savedItem.operation_date;
entry.label = $scope.savedItem.label;
entry.value = $scope.savedItem.value;
entry.account_id = $scope.savedItem.account_id; // account_id should not change, but just in case...
entry.category = $scope.savedItem.category;
}
// Reset saved and selected items to null.
$scope.savedItem = null;
$scope.selectedItem = null;
// Enter display state.
entry.state = 'display';
}
};
// Points an entry.
$scope.pointEntry = function(entry) {
if(entry.pointed) {
entry.pointed = false;
} else {
entry.pointed = true;
}
// Save the entry if not new.
if(!$scope.isNew(entry)) {
$scope.saveEntry(entry);
}
};
// Saves an entry.
$scope.saveEntry = function(entry) {
// Affects the account ID if not (should never happen)
if(!entry.account_id) {
entry.account_id = $scope.account.id;
}
// Prepare the Ajax call to save the entry.
var type;
var url = "api/entries";
if(!$scope.isNew(entry)) {
url += "/" + entry.id;
}
// Ajax call to save an entry
$http.put(url, angular.toJson(entry)).success(function(data) {
message("success", "Save", data);
// $scope.savedItem = null;
// Send the "entry saved" event.
$scope.$emit("entrySavedEvent", entry);
});
};
// Removes an entry.
$scope.removeEntry = function(entry, modalScope) {
// Cancel current editing.
//$scope.cancelEditEntry(entry);
if (!$scope.isNew(entry)) {
$http.delete("api/entries/" + entry.id).success(function (result) {
message("success", "Delete", result);
// Send the "entry removed" event.
$scope.$emit("entryRemovedEvent", entry);
$scope.closeModal(modalScope);
}).error(function (data) {
message("error", "Delete", data);
$scope.closeModal(modalScope);
});
}
};
$scope.closeModal = function(modalScope) {
// Close the modal dialog
if(modalScope && modalScope.dismiss) {
modalScope.dismiss();
}
};
// Function to draw the sold evolution chart.
$scope.drawChart = function(data, elementId) {
// Clear previous chart
var element = angular.element(elementId);
element.html("");
element.css("height", "0px");
var entries = data.entries;
if(entries && entries.length > 1) {
// Prepare for today vertical line.
var today = new Date();
today.setHours(0);
today.setMinutes(0);
// Find first and last days to set limits of the x axis.
var day = 24 * 60 * 60 * 1000;
var firstDate = $filter('date')(new Date(Date.parse(entries[0][0]).valueOf() - day), 'yyyy-MM-dd');
var lastDate = $filter('date')(new Date(Date.parse(entries[entries.length -1][0]).valueOf() + day), 'yyyy-MM-dd');
// Plot chart, and store it in a window parameter for resize callback (need to be done better than it...)
window.chart = $.jqplot(element.prop("id"), [entries], {
// Title of the chart
title: "&Eacute;volution du solde",
axes:{
// Parameters for the date axis
xaxis:{
autoscale: true,
// Date rendere for this axis
renderer:$.jqplot.DateAxisRenderer,
// Limits
min: firstDate,
max: lastDate,
// Tick options
tickOptions: {
// Format date for x axis.
formatString: "%F"
}
},
// Parameters for the value axis
yaxis: {
autoscale: true,
}
},
// Highlighter parameters
highlighter: {
show: true,
//yvalues: 4,
formatString: '<table class="jqplot-highlighter"><tr><td>date:</td><td>%s</td></tr><tr><td>sold:</td><td>%s</td></tr>'
},
// Series parameters
series: [{
// We use the OHLC (open, high, low, close) rendered.
//renderer:$.jqplot.OHLCRenderer,
color: "blue",
rendererOptions: {
smooth: true,
}
}],
// To display horizontal (0) and vertical (today) lines
canvasOverlay: {
show: true,
objects: [
// Orange horizontal line for 0 limit
{dashedHorizontalLine: {
name: "zero",
y: 0,
lineWidth: 1,
color: "orange",
shadow: false
}},
// Red horizontal line for authorized overdraft limit
{dashedHorizontalLine: {
name: "overdraft",
y: data.account.authorized_overdraft,
lineWidth: 1,
color: "red",
shadow: false
}},
// Gray vertical line for today
{ dashedVerticalLine: {
name: "today",
x: today,
lineWidth: 1,
color: "gray",
shadow: false
}}
]
}
});
} else {
// Reset chart to null to avoid redraw of a non existing chart in resize callback.
window.chart = null;
}
};
// Function to draw the expense category pie chart.
$scope.drawPieChart = function(entries, elementId) {
// Clear previous chart
var element = angular.element(elementId);
element.html("");
element.css("height", "0px");
if(entries && entries.length > 1) {
// Plot chart, and store it in a window parameter for resize callback (need to be done better than it...)
window.pieChart = $.jqplot(element.attr("id"), [entries], {
// Title of the chart
title: "D&eacute;penses",
// Series parameters.
seriesDefaults: {
// Pie chart renderer
renderer: $.jqplot.PieRenderer,
rendererOptions: {
showDataLabels: true,
}
},
// Legend parameters.
legend: {
show: true,
location: 'e',
rendererOptions: {
numberRows: 9,
numberColumns: 2
}
},
// Highlighter parameters;
highlighter: {
show: true,
formatString:'%s: %s',
tooltipLocation:'sw',
useAxesFormatters:false
}
});
} else {
// Reset chart to null to avoid redraw of a non existing chart in resize callback.
window.pieChart = null;
}
};
$rootScope.$on("monthsLoadedEvent", function(event, args){
$scope.loadEntries(args.account, args.month);
});
$scope.$on("entriesLoadedEvent", function(event, args) {
$scope.entriesLoaded(args.entries);
});
};
// Default AJAX error handler.
//$(document).ajaxError(function(event, xhr, settings) {
// message("error", xhr.statusText, xhr.responseText);
//});