// Entry object function entry(){ this.id=ko.observable(); this.value_date=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(); } // Account object function account() { this.id=ko.observable(); this.name=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(); } // Util function to show a message in message placeholder. function message(alertType, title, message) { $(".alert").alert('close'); $("#message-placeholder").append('

' + title + '

' + message + '
'); } // The ListViewModel used to instanciate viewmodel. var ListViewModel = function() { var self = this; // Account store and selection self.account = ko.observable(); self.accounts = ko.observableArray([]); // Month store and selection self.months = ko.observableArray(); self.month = ko.observable(); // Entry store and selection self.entries = ko.observableArray([]); self.selectedItem = ko.observable(); // Placeholder for saved value to cancel entry edition self.savedItem = null; // Placeholder for entry to remove to be available in modal function "yes" click callback self.itemToRemove = ko.observable(); // Returns the data for the categories by summing values with same category self.expenseCategoriesChart = ko.computed(function() { var unwrap = ko.utils.unwrapObservable; var entries=unwrap(self.entries); // First pass: get sum values for each category. var chartValuesTmp = {}; $.each(entries, function(index, entry) { var category = unwrap(entry.category); var value = unwrap(entry.value) ? Number(unwrap(entry.value)) : null; if(category && value && value < 0.0) { var oldValue = 0.0; if(chartValuesTmp[category]) { oldValue = chartValuesTmp[category]; } chartValuesTmp[category] = oldValue - value } }); // Second pass: transform to an array readable by jqplot. var chartValues = []; $.each(chartValuesTmp, function(key, value) { chartValues.push([key, value]); }); return chartValues; }); // Return the data for the sold chart. self.entriesChart = ko.computed(function() { var unwrap = ko.utils.unwrapObservable; // We assume that entries are sorted by value date descending. var entries = unwrap(self.entries).slice().reverse(); // First pass: get open, high, low and close values for each day. var chartValuesTmp = {}; $.each(entries, function(index, entry) { //var date = entry.value_date() ? entry.value_date().toString() : null; var date = unwrap(entry.value_date); var value = unwrap(entry.value) ? Number(unwrap(entry.value())) : null; if(date && value) { var values = {}; var sold = Number(unwrap(entry.sold)); var open = Number((sold - value).toFixed(2)); values['open'] = open; values['high'] = sold > open ? sold : open; values['low'] = sold < open ? sold : open; values['close'] = sold; if(chartValuesTmp[date]) { var oldValues = chartValuesTmp[date]; if(oldValues['high'] > values['high']) { values['high'] = oldValues['high']; } if(oldValues['low'] < values['low']) { values['low'] = oldValues['low']; } values['open'] = oldValues['open']; } chartValuesTmp[date] = values; } }); // Second pass: transform to an array readable by jqplot OHLC renderer. var chartValues = []; $.each(chartValuesTmp, function(key, value) { chartValues.push([key, value['open'], value['high'], value['low'], value['close']]); }); return chartValues; }, self); // Function to load entries from server for a specific account and month. self.loadEntries = function(account, month) { $.get("api/entries/" + account.id() + "/" + month.year() + "/" + month.month()).success(function(data) { // Clean up selected entry. self.selectedItem(null); // Update entries self.entries(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS)); }); }; // Function to load accounts self.loadAccounts = function() { $.get("api/accounts").success(function (data) { // Update accounts self.accounts(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS)); // Reset selected account to the new instance corresponding to the old one. if(self.account()) { var oldId = self.account().id(); // Reset to null self.account(null); // Find the new instance of the previously selected account. $.each(self.accounts(), function(index, account) { if(account.id() == oldId) { self.account(account); } }); } // Set selected account to first one if not yet selected if(!self.account()){ self.account(self.accounts()[0]); } // Load months self.loadMonths(self.account()); }); }; // Function to load months self.loadMonths = function(account){ $.get("api/accounts/" + account.id() + "/months").success(function (data) { // Update months self.months(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS)); // Reset selected month to the new instance corresponding to the old one if(self.month()) { var oldYear = self.month().year(); var oldMonth = self.month().month(); // Reset to null self.month(null); // Find the new instance of the previously selected month. $.each(self.months(), function(index, month) { if(month.year() == oldYear && month.month() == oldMonth) { self.month(month); } }); } // Set selected month to the last one if not yet selected. if(!self.month()) { self.month(self.months()[self.months().length - 1]); } // Load entries self.loadEntries(self.account(), self.month()); }); }; // Function to select template in function of selected item. self.templateToUse = function (item) { return self.selectedItem() === item ? 'editTmpl' : 'itemsTmpl'; }; // Function to edit an item self.edit = function(item) { // Cancel previous editing. if(self.savedItem) { self.cancel(); } // Save current item self.savedItem=ko.toJS(item); self.selectedItem(item); // Initialize date picker for value date column. $("#value_date").datepicker().on('changeDate', function(ev){ self.selectedItem().value_date(ev.date.format(ev.currentTarget.dataset.dateFormat)); }); // 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)); }); }; // Function to cancel current editing. self.cancel = function() { // Reset selected item fields to saved item ones. if(self.selectedItem() && self.savedItem) { self.selectedItem().id(self.savedItem.id); // id should not change, but just in case... self.selectedItem().operation_date(self.savedItem.operation_date); self.selectedItem().value_date(self.savedItem.value_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... } // This item was just added: remove it from the entries array. if(self.selectedItem() && !self.selectedItem().id()) { self.entries.remove(self.selectedItem()); } // Reset saved and selected items to null. self.savedItem = null; self.selectedItem(null); }; // Function to add a new entry. self.add = function() { self.entries.unshift(ko.mapping.fromJS({ id: null, value_date: null, operation_date: null, label: null, value: null, sold: null, pointedsold: null, category: null, account_id: self.account().id() })); self.edit(self.entries()[0]); }; // Function to save the current selected entry. self.save = function() { var item = self.selectedItem(); // Ajax call to save the entry. var type; var url = "api/entries/"; if(item.id()) { type = "PUT"; url += "save/" + item.id(); } else { type = "POST"; url += "add"; } $.ajax({url: url, type: type, data:ko.toJSON(item)}).success(function(data) { message("success", "Save", data); self.selectedItem(null); self.savedItem = null; // Reload accounts to update solds. self.loadAccounts(); }); }; // Function to remove an entry. self.remove = function (item) { // Cancel current editing. self.cancel(); if (item.id()) { // This entry is saved in database, we show a modal dialog to confirm the removal. self.removedItem = item; $('#remove-confirm').modal(); } else { // This entry was not saved in database yet, we just remove it from the list. self.entries.remove(item); } }; // Function to confirm the removal of an entry. self.confirmRemove = function() { var item = self.removedItem; $.ajax("api/entries/" + ko.utils.unwrapObservable(item.id), {type: "DELETE"}).success(function (result) { // Reload accounts to update solds. self.loadAccounts(); }).complete(function (result) { // Reset removed item to null and hide the modal dialog. self.removedItem = null; $('#remove-confirm').modal('hide'); }); }; // Callback function to select a new month. self.selectMonth = function(month) { if(month) { self.month(month); self.loadEntries(self.account(), month); } }; // Callback function to select a new account. self.selectAccount = function(account) { if(account) { self.account(account); self.loadMonths(account); } }; }; // Function to draw the sold evolution chart. drawChart = function(entries, element) { // Clear previous chart $(element).html(""); if(entries && entries.length > 0) { // 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 = 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'); // 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: "É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: '
date:%s
open:%s
hi:%s
low:%s
close:%s
' }, // Series parameters series: [{ // We use the OHLC (open, high, low, close) rendered. renderer:$.jqplot.OHLCRenderer, color: "blue", }], // To display horizontal (0) and vertical (today) lines canvasOverlay: { show: true, objects: [ // Red horizontal line for 0 limit {dashedHorizontalLine: { name: "zero", y: 0, 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. drawPieChart = function(entries, element) { // Clear previous chart $(element).html(""); if(entries && entries.length > 0) { // 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épenses", // Series parameters. seriesDefaults: { // Pie chart renderer renderer: $.jqplot.PieRenderer, rendererOptions: { showDataLabels: true } }, // Legend parameters. legend: { show: true, location: 'e' }, // 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; } }; // 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(); var entries = dataSource ? unwrap(dataSource) : null; drawChart(entries, element); } }; // 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); } }; // Default AJAX error handler. $(document).ajaxError(function(event, xhr, settings) { message("error", xhr.statusText, xhr.responseText); }); // Resize callback. $(window).resize(function() { if(window.chart) { window.chart.replot({resetAxes: true}); } if(window.pieChart) { window.pieChart.replot({resetAxes: true}); } }); // ViewModal instanciation. var viewModel = new ListViewModel(); ko.applyBindings(viewModel); // Load accounts after page initialization. $(viewModel.loadAccounts);