accountant/src/html/js/entries.js

679 lines
20 KiB
JavaScript
Raw Normal View History

// Entry object
2013-01-13 12:27:42 +01:00
function entry(){
this.id=ko.observable();
this.pointed=ko.observable();
2013-01-13 12:27:42 +01:00
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();
2013-01-07 18:42:02 +01:00
}
// Account object
function account() {
this.id=ko.observable();
this.name=ko.observable();
2013-01-24 20:44:09 +01:00
this.authorized_overdraft=ko.observable();
this.future=ko.observable();
this.current=ko.observable();
this.pointed=ko.observable();
}
// Month object
function month() {
this.year=ko.observable();
this.month=ko.observable();
}
2013-01-13 10:14:43 +01:00
// Util function to show a message in message placeholder.
2013-01-07 18:42:02 +01:00
function message(alertType, title, message) {
$(".alert").alert('close');
2013-01-13 20:57:48 +01:00
$("#message-placeholder").append('<div class="alert alert-' + alertType + '"><button type="button" class="close" data-dismiss="alert">&times;</button><h4>' + title + '</h4>' + message + '</div>');
2013-01-07 18:42:02 +01:00
}
// The ListViewModel used to instanciate viewmodel.
2013-01-07 18:42:02 +01:00
var ListViewModel = function() {
var self = this;
2013-01-13 10:14:43 +01:00
// Account store and selection
2013-01-07 18:42:02 +01:00
self.account = ko.observable();
self.accounts = ko.observableArray([]);
self.savedAccount = null;
self.editingAccount = ko.observable();
self.removedAccount = null;
2013-01-13 10:14:43 +01:00
// Month store and selection
2013-01-07 18:42:02 +01:00
self.months = ko.observableArray();
self.month = ko.observable();
2013-01-13 10:14:43 +01:00
// Entry store and selection
self.entries = ko.observableArray([]);
2013-01-07 18:42:02 +01:00
self.selectedItem = ko.observable();
2013-01-24 19:31:37 +01:00
self.newEntry = ko.observable(ko.mapping.fromJS({
id: null,
pointed: false,
2013-01-24 19:31:37 +01:00
operation_date: null,
label: null,
value: null,
sold: null,
pointedsold: null,
category: null,
account_id: null
}));
2013-01-13 10:14:43 +01:00
// Placeholder for saved value to cancel entry edition
self.savedItem = null;
2013-01-13 10:14:43 +01:00
// Placeholder for entry to remove to be available in modal function "yes" click callback
self.removedItem = null;
self.addAccount = function() {
self.editingAccount(ko.mapping.fromJS({
id: null,
2013-01-24 20:44:09 +01:00
name: null,
authorized_overdraft: null
}));
$("#edit-account").modal();
};
self.editAccount = function(account) {
self.editingAccount(account);
self.savedAccount = ko.toJS(account);
$("#edit-account").modal();
};
self.cancelEditAccount = function() {
if(self.editingAccount() && self.savedAccount) {
self.editingAccount().name(self.savedAccount.name);
2013-01-24 20:44:09 +01:00
self.editingAccount().authorized_overdraft(self.savedAccount.authorized_overdraft);
}
self.editingAccount(null);
self.savedAccount = null;
$("#edit-account").modal('hide');
};
self.saveAccount = function() {
var account = self.editingAccount();
// Ajax call to save the entry.
var type;
var url = "api/accounts";
if(account.id()) {
url += "/" + account.id();
}
$.ajax({
url: url,
type: "PUT",
data: ko.toJSON(account),
dataType: "json",
contentType: "application/json"
}).success(function(data) {
message("success", "Save", data);
self.editingAccount(null);
self.savedAccount = null;
$("#edit-account").modal('hide');
// Reload accounts to update solds.
self.loadAccounts();
});
};
self.removeAccount = function(account) {
// Cancel current editing.
self.removedAccount = account;
$('#remove-account-confirm').modal();
};
// Function to confirm the removal of an entry.
self.confirmAccountRemove = function() {
var account = self.removedAccount;
$.ajax("api/accounts/" + ko.utils.unwrapObservable(account.id), {type: "DELETE"}).success(function (data) {
message("success", "Save", data);
// Reload accounts to update solds.
self.loadAccounts();
}).complete(function (data) {
// Reset removed item to null and hide the modal dialog.
self.removedAccount = null;
$('#remove-account-confirm').modal('hide');
});
};
2013-01-07 18:42:02 +01:00
self.categories = ko.computed(function() {
var unwrap = ko.utils.unwrapObservable;
var entries=unwrap(self.entries);
var categories = ko.observableArray([]);
$.each(entries, function(index, entry) {
if(entry.category() && entry.category() != '' && categories.indexOf(entry.category()) == -1) {
categories.push(entry.category());
}
});
return categories();
});
2013-01-13 10:14:43 +01:00
// Returns the data for the categories by summing values with same category
self.expenseCategoriesChart = ko.computed(function() {
2013-01-13 13:32:23 +01:00
var unwrap = ko.utils.unwrapObservable;
var entries=unwrap(self.entries);
2013-01-13 10:14:43 +01:00
// First pass: get sum values for each category.
var chartValuesTmp = {};
$.each(entries, function(index, entry) {
2013-01-13 13:32:23 +01:00
var category = unwrap(entry.category);
var value = unwrap(entry.value) ? Number(unwrap(entry.value)) : null;
2013-01-13 10:14:43 +01:00
if(category && value && value < 0.0) {
var oldValue = 0.0;
if(chartValuesTmp[category]) {
oldValue = chartValuesTmp[category];
}
chartValuesTmp[category] = oldValue - value
}
});
2013-01-13 10:14:43 +01:00
// Second pass: transform to an array readable by jqplot.
var chartValues = [];
$.each(chartValuesTmp, function(key, value) {
chartValues.push([key, value]);
});
return chartValues;
});
2013-01-13 10:14:43 +01:00
// Return the data for the sold chart.
self.entriesChart = ko.computed(function() {
2013-01-13 13:32:23 +01:00
var unwrap = ko.utils.unwrapObservable;
2013-01-13 10:14:43 +01:00
// We assume that entries are sorted by value date descending.
2013-01-13 13:32:23 +01:00
var entries = unwrap(self.entries).slice().reverse();
2013-01-24 01:04:01 +01:00
// Transform to an array readable by jqplot Line renderer.
2013-01-13 10:14:43 +01:00
var chartValues = [];
2013-01-24 01:04:01 +01:00
$.each(entries, function(index, entry) {
if(unwrap(entry.operation_date) && unwrap(entry.sold)) {
chartValues.push([unwrap(entry.operation_date), Number(unwrap(entry.sold))]);
2013-01-24 14:58:09 +01:00
}
});
2013-01-24 20:44:09 +01:00
return {account: self.account(), entries: chartValues};
}, self);
2013-01-13 10:14:43 +01:00
// Function to load entries from server for a specific account and month.
self.loadEntries = function(account, month) {
// An account may not have any month (new account)
if(month) {
$.get("api/entries/" + account.id() + "/" + month.year() + "/" + month.month()).success(function(data) {
2013-01-13 10:14:43 +01:00
// Clean up selected entry.
2013-01-07 18:42:02 +01:00
self.selectedItem(null);
// Update entries
2013-01-24 19:31:37 +01:00
self.clearNewEntry()
var entries = [self.newEntry()].concat(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS));
self.entries(entries);
2013-01-24 21:19:07 +01:00
// Initialize date picker for operation date column.
$("#new_operation_date").datepicker().on('changeDate', function(ev){
self.newEntry().operation_date(ev.date.format(ev.currentTarget.dataset.dateFormat));
});
});
} else {
// If no month, just remove all entries.
self.entries.removeAll();
}
};
// Function to load accounts
self.loadAccounts = function() {
2013-01-13 20:57:48 +01:00
$.get("api/accounts").success(function (data) {
// Update accounts
2013-01-13 20:57:48 +01:00
self.accounts(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS));
2013-01-24 20:44:09 +01:00
var accountToSelect = null
// Reset selected account to the new instance corresponding to the old one.
if(self.account()) {
// Find the new instance of the previously selected account.
$.each(self.accounts(), function(index, account) {
2013-01-24 20:44:09 +01:00
if(account.id() == self.account().id()) {
accountToSelect = account;
}
});
}
// Set selected account to first one if not yet selected
2013-01-24 20:44:09 +01:00
if(!accountToSelect && self.accounts().length > 0){
accountToSelect = self.accounts()[0];
}
2013-01-24 20:44:09 +01:00
// Reset to account to select
self.account(accountToSelect);
// Load months if there is any account, or remove months.
if(self.account()) {
self.loadMonths(self.account());
} else {
self.months.removeAll();
}
});
};
2013-01-07 18:42:02 +01:00
// Function to load months
self.loadMonths = function(account){
2013-01-13 20:57:48 +01:00
$.get("api/accounts/" + account.id() + "/months").success(function (data) {
// Update months
2013-01-13 20:57:48 +01:00
self.months(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS));
2013-01-24 20:44:09 +01:00
var monthToSelect = null;
// Reset selected month to the new instance corresponding to the old one
if(self.month()) {
// Find the new instance of the previously selected month.
$.each(self.months(), function(index, month) {
2013-01-24 20:44:09 +01:00
if(month.year() === self.month().year() && month.month() === self.month().month()) {
monthToSelect = month;
}
});
}
// Set selected month to the last one if not yet selected.
2013-01-24 20:44:09 +01:00
if(!monthToSelect && self.months().length > 0) {
monthToSelect = self.months()[self.months().length - 1];
}
2013-01-24 20:44:09 +01:00
// Reset to month to select
self.month(monthToSelect);
// Load entries if there is a month or remove entries.
if(self.month) {
self.loadEntries(self.account(), self.month());
} else {
self.entries.removeAll();
}
2013-01-07 18:42:02 +01:00
});
};
2013-01-13 13:32:23 +01:00
// Function to select template in function of selected item.
2013-01-07 18:42:02 +01:00
self.templateToUse = function (item) {
2013-01-24 21:19:07 +01:00
return self.newEntry() === item ? 'newTmpl' : self.selectedItem() === item ? 'editTmpl' : 'itemsTmpl';
2013-01-07 18:42:02 +01:00
};
2013-01-13 13:32:23 +01:00
// Function to edit an item
2013-01-07 18:42:02 +01:00
self.edit = function(item) {
2013-01-13 13:32:23 +01:00
// Cancel previous editing.
if(self.savedItem) {
2013-01-07 18:42:02 +01:00
self.cancel();
}
2013-01-13 13:32:23 +01:00
// Save current item
self.savedItem=ko.toJS(item);
2013-01-07 18:42:02 +01:00
self.selectedItem(item);
2013-01-13 13:32:23 +01:00
// Initialize date picker for operation date column.
$("#operation_date").datepicker().on('changeDate', function(ev){
self.selectedItem().operation_date(ev.date.format(ev.currentTarget.dataset.dateFormat));
});
2013-01-07 18:42:02 +01:00
};
2013-01-24 19:31:37 +01:00
self.clearNewEntry = function() {
self.newEntry().id(null); // id should not change, but just in case...
self.newEntry().pointed(false)
2013-01-24 19:31:37 +01:00
self.newEntry().operation_date(null);
self.newEntry().label(null);
self.newEntry().value(null);
self.newEntry().account_id(self.account().id()); // account_id should not change, but just in case...
self.newEntry().category(null);
2013-01-24 19:31:37 +01:00
};
2013-01-13 13:32:23 +01:00
// Function to cancel current editing.
2013-01-24 19:31:37 +01:00
self.cancel = function(item) {
2013-01-13 13:32:23 +01:00
// Reset selected item fields to saved item ones.
2013-01-24 19:31:37 +01:00
if(item === self.selectedItem() && self.savedItem) {
self.selectedItem().id(self.savedItem.id); // id should not change, but just in case...
self.selectedItem().pointed(self.savedItem.pointed);
self.selectedItem().operation_date(self.savedItem.operation_date);
self.selectedItem().label(self.savedItem.label);
self.selectedItem().value(self.savedItem.value);
self.selectedItem().account_id(self.savedItem.account_id); // account_id should not change, but just in case...
self.selectedItem().category(self.savedItem.category);
2013-01-07 18:42:02 +01:00
}
2013-01-24 19:31:37 +01:00
// We are cancelling the new entry, just reset it.
if(item === self.newEntry()) {
self.clearNewEntry();
2013-01-07 18:42:02 +01:00
}
2013-01-13 13:32:23 +01:00
// Reset saved and selected items to null.
self.savedItem = null;
2013-01-07 18:42:02 +01:00
self.selectedItem(null);
};
self.pointEntry = function(entry) {
if(entry.pointed()) {
entry.pointed(false);
} else {
entry.pointed(true);
}
if(entry != self.newEntry()) {
self.save(entry);
}
};
2013-01-13 13:32:23 +01:00
// Function to save the current selected entry.
2013-01-24 19:31:37 +01:00
self.save = function(item) {
//var item = self.selectedItem();
if(item === self.newEntry()) {
item.account_id(self.account().id());
}
2013-01-07 18:42:02 +01:00
2013-01-13 13:32:23 +01:00
// Ajax call to save the entry.
2013-01-13 20:57:48 +01:00
var type;
var url = "api/entries";
2013-01-13 20:57:48 +01:00
if(item.id()) {
url += "/" + item.id();
2013-01-13 20:57:48 +01:00
}
$.ajax({
url: url,
type: "PUT",
contentType: "application/json",
data:ko.toJSON(item),
dataType: "json"
}).success(function(data) {
2013-01-13 20:57:48 +01:00
message("success", "Save", data);
2013-01-07 18:42:02 +01:00
2013-01-24 19:31:37 +01:00
self.clearNewEntry();
2013-01-07 18:42:02 +01:00
self.selectedItem(null);
2013-01-13 13:32:23 +01:00
self.savedItem = null;
// Reload accounts to update solds.
self.loadAccounts();
2013-01-07 18:42:02 +01:00
});
};
2013-01-13 13:32:23 +01:00
// Function to remove an entry.
2013-01-07 18:42:02 +01:00
self.remove = function (item) {
2013-01-13 13:32:23 +01:00
// Cancel current editing.
self.cancel();
2013-01-07 18:42:02 +01:00
if (item.id()) {
2013-01-13 13:32:23 +01:00
// This entry is saved in database, we show a modal dialog to confirm the removal.
self.removedItem = item;
$('#remove-confirm').modal();
2013-01-07 18:42:02 +01:00
} else {
2013-01-13 13:32:23 +01:00
// This entry was not saved in database yet, we just remove it from the list.
2013-01-07 18:42:02 +01:00
self.entries.remove(item);
}
};
2013-01-13 13:32:23 +01:00
// Function to confirm the removal of an entry.
self.confirmRemove = function() {
2013-01-13 13:32:23 +01:00
var item = self.removedItem;
2013-01-13 20:57:48 +01:00
$.ajax("api/entries/" + ko.utils.unwrapObservable(item.id), {type: "DELETE"}).success(function (result) {
2013-01-13 13:32:23 +01:00
// Reload accounts to update solds.
self.loadAccounts();
}).success(function (data) {
message("success", "Delete", data);
}).complete(function (data) {
2013-01-13 13:32:23 +01:00
// Reset removed item to null and hide the modal dialog.
self.removedItem = null;
$('#remove-confirm').modal('hide');
});
};
2013-01-13 13:32:23 +01:00
// Callback function to select a new month.
self.selectMonth = function(month) {
if(month) {
self.month(month);
self.loadEntries(self.account(), month);
}
2013-01-08 18:50:47 +01:00
};
2013-01-13 13:32:23 +01:00
// Callback function to select a new account.
self.selectAccount = function(account) {
if(account) {
2013-01-24 19:31:37 +01:00
self.newEntry().account_id(account.id());
self.account(account);
self.loadMonths(account);
}
2013-01-07 18:42:02 +01:00
};
};
2013-01-13 13:47:27 +01:00
// Function to draw the sold evolution chart.
2013-01-24 20:44:09 +01:00
drawChart = function(data, element) {
2013-01-13 13:47:27 +01:00
// Clear previous chart
$(element).html("");
2013-01-24 20:44:09 +01:00
var entries = data.entries;
if(entries && entries.length > 0) {
2013-01-13 13:47:27 +01:00
// Prepare for today vertical line.
var today = new Date();
today.setHours(0);
today.setMinutes(0);
2013-01-13 13:47:27 +01:00
// Find first and last days to set limits of the x axis.
2013-01-13 12:24:33 +01:00
var day = 24 * 60 * 60 * 1000;
var firstDate = new Date(Date.parse(entries[0][0]).valueOf() - day).format('yyyy-mm-dd');
var lastDate = new Date(Date.parse(entries[entries.length -1][0]).valueOf() + day).format('yyyy-mm-dd');
2013-01-13 13:47:27 +01:00
// Plot chart, and store it in a window parameter for resize callback (need to be done better than it...)
window.chart = $.jqplot(element.id, [entries], {
// Title of the chart
title: "&Eacute;volution du solde",
axes:{
2013-01-13 13:47:27 +01:00
// Parameters for the date axis
xaxis:{
2013-01-13 12:24:33 +01:00
autoscale: true,
2013-01-13 13:47:27 +01:00
// Date rendere for this axis
renderer:$.jqplot.DateAxisRenderer,
2013-01-13 13:47:27 +01:00
// Limits
2013-01-13 12:24:33 +01:00
min: firstDate,
max: lastDate,
2013-01-13 13:47:27 +01:00
// Tick options
tickOptions: {
// Format date for x axis.
formatString: "%F"
}
},
2013-01-13 13:47:27 +01:00
// Parameters for the value axis
yaxis: {
autoscale: true,
}
},
2013-01-13 13:47:27 +01:00
// Highlighter parameters
highlighter: {
2013-01-13 13:47:27 +01:00
show: true,
2013-01-24 01:04:01 +01:00
//yvalues: 4,
formatString: '<table class="jqplot-highlighter"><tr><td>date:</td><td>%s</td></tr><tr><td>sold:</td><td>%s</td></tr>'
},
2013-01-13 13:47:27 +01:00
// Series parameters
series: [{
2013-01-13 13:47:27 +01:00
// We use the OHLC (open, high, low, close) rendered.
2013-01-24 01:04:01 +01:00
//renderer:$.jqplot.OHLCRenderer,
color: "blue",
2013-01-24 01:04:01 +01:00
rendererOptions: {
smooth: true,
}
}],
2013-01-13 13:47:27 +01:00
// To display horizontal (0) and vertical (today) lines
canvasOverlay: {
show: true,
2013-01-13 13:47:27 +01:00
objects: [
2013-01-24 20:44:09 +01:00
// Orange horizontal line for 0 limit
2013-01-13 13:47:27 +01:00
{dashedHorizontalLine: {
name: "zero",
y: 0,
lineWidth: 1,
2013-01-24 20:44:09 +01:00
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
}},
2013-01-13 13:47:27 +01:00
// Gray vertical line for today
{ dashedVerticalLine: {
name: "today",
x: today,
lineWidth: 1,
color: "gray",
shadow: false
2013-01-13 13:47:27 +01:00
}}
]
}
});
2013-01-12 22:14:42 +01:00
} else {
2013-01-13 13:47:27 +01:00
// Reset chart to null to avoid redraw of a non existing chart in resize callback.
2013-01-12 22:14:42 +01:00
window.chart = null;
}
};
2013-01-13 13:47:27 +01:00
// Function to draw the expense category pie chart.
drawPieChart = function(entries, element) {
2013-01-13 13:47:27 +01:00
// Clear previous chart
$(element).html("");
if(entries && entries.length > 0) {
2013-01-13 13:47:27 +01:00
// Plot chart, and store it in a window parameter for resize callback (need to be done better than it...)
window.pieChart = $.jqplot(element.id, [entries], {
// Title of the chart
title: "D&eacute;penses",
2013-01-13 13:47:27 +01:00
// Series parameters.
seriesDefaults: {
2013-01-13 13:47:27 +01:00
// Pie chart renderer
renderer: $.jqplot.PieRenderer,
rendererOptions: {
2013-01-14 23:42:05 +01:00
showDataLabels: true,
}
},
2013-01-13 13:47:27 +01:00
// Legend parameters.
legend: {
show: true,
2013-01-14 23:42:05 +01:00
location: 'e',
rendererOptions: {
numberRows: 9,
numberColumns: 2
}
},
2013-01-13 13:47:27 +01:00
// Highlighter parameters;
highlighter: {
show: true,
formatString:'%s: %s',
tooltipLocation:'sw',
useAxesFormatters:false
}
});
2013-01-12 22:14:42 +01:00
} else {
2013-01-13 13:47:27 +01:00
// Reset chart to null to avoid redraw of a non existing chart in resize callback.
2013-01-12 22:14:42 +01:00
window.pieChart = null;
}
};
2013-01-13 13:47:27 +01:00
// Chart binding to redraw the chart on entries update.
ko.bindingHandlers.chart = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
// empty - left as placeholder if needed later
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var unwrap = ko.utils.unwrapObservable;
var dataSource = valueAccessor();
2013-01-24 20:44:09 +01:00
var data = dataSource ? unwrap(dataSource) : null;
drawChart(data, element);
}
};
2013-01-07 18:42:02 +01:00
2013-01-13 13:47:27 +01:00
// Pie chart binding to redraw expense chart on entries update.
ko.bindingHandlers.pieChart = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
// empty - left as placeholder if needed later
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var unwrap = ko.utils.unwrapObservable;
var dataSource = valueAccessor();
var entries = dataSource ? unwrap(dataSource) : null;
drawPieChart(entries, element);
}
};
2013-01-13 13:47:27 +01:00
// Default AJAX error handler.
$(document).ajaxError(function(event, xhr, settings) {
2013-01-13 20:57:48 +01:00
message("error", xhr.statusText, xhr.responseText);
});
// Bootstrap.Typeahead binding.
// Use like so: data-bind="typeahead: { source: namespaces }"
ko.bindingHandlers.typeahead = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
$(element).typeahead({
source: function() { return ko.utils.unwrapObservable(valueAccessor().source); },
onselect: function(value) { allBindingsAccessor().value(value); }
});
},
update: function(element, valueAccessor, allBindingsAccessor, viewModel) {
$(element).typeahead({
source: function() { ko.utils.unwrapObservable(valueAccessor().source); },
onselect: function(value) { allBindingsAccessor().value(value); }
});
}
};
2013-01-13 13:47:27 +01:00
// Resize callback.
$(window).resize(function() {
if(window.chart) {
window.chart.replot({resetAxes: true});
}
2013-01-12 22:16:05 +01:00
if(window.pieChart) {
window.pieChart.replot({resetAxes: true});
}
});
2013-01-13 13:47:27 +01:00
// ViewModal instanciation.
var viewModel = new ListViewModel();
ko.applyBindings(viewModel);
2013-01-13 13:47:27 +01:00
// Load accounts after page initialization.
$(viewModel.loadAccounts);
2013-01-07 18:42:02 +01:00