// 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.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(); } // 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([]); self.savedAccount = null; self.editingAccount = ko.observable(); self.removedAccount = null; // Month store and selection self.months = ko.observableArray(); self.month = ko.observable(); // Entry store and selection self.entries = ko.observableArray([]); self.selectedItem = ko.observable(); self.newEntry = ko.observable(ko.mapping.fromJS({ id: null, value_date: null, operation_date: null, label: null, value: null, sold: null, pointedsold: null, category: null, account_id: null })); // 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.removedItem = null; self.addAccount = function() { self.editingAccount(ko.mapping.fromJS({ id: null, 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); 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'); }); }; 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(); }); // 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(); // Transform to an array readable by jqplot Line renderer. var chartValues = []; $.each(entries, function(index, entry) { if(unwrap(entry.value_date) && unwrap(entry.sold)) { chartValues.push([unwrap(entry.value_date), Number(unwrap(entry.sold))]); } }); return {account: self.account(), entries: chartValues}; }, self); // 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) { // Clean up selected entry. self.selectedItem(null); // Update entries self.clearNewEntry() var entries = [self.newEntry()].concat(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS)); self.entries(entries); // Initialize date picker for value date column. $("#new_value_date").datepicker().on('changeDate', function(ev){ self.newEntry().value_date(ev.date.format(ev.currentTarget.dataset.dateFormat)); }); // 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() { $.get("api/accounts").success(function (data) { // Update accounts self.accounts(ko.utils.arrayMap($.parseJSON(data), ko.mapping.fromJS)); 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) { if(account.id() == self.account().id()) { accountToSelect = account; } }); } // Set selected account to first one if not yet selected if(!accountToSelect && self.accounts().length > 0){ accountToSelect = self.accounts()[0]; } // 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(); } }); }; // 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)); 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) { if(month.year() === self.month().year() && month.month() === self.month().month()) { monthToSelect = month; } }); } // Set selected month to the last one if not yet selected. if(!monthToSelect && self.months().length > 0) { monthToSelect = self.months()[self.months().length - 1]; } // 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(); } }); }; // Function to select template in function of selected item. self.templateToUse = function (item) { return self.newEntry() === item ? 'newTmpl' : 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)); }); }; self.clearNewEntry = function() { self.newEntry().id(null); // id should not change, but just in case... self.newEntry().operation_date(null); self.newEntry().value_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... }; // Function to cancel current editing. self.cancel = function(item) { // Reset selected item fields to saved item ones. if(item === 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... } // We are cancelling the new entry, just reset it. if(item === self.newEntry()) { self.clearNewEntry(); } // Reset saved and selected items to null. self.savedItem = null; self.selectedItem(null); }; // Function to save the current selected entry. self.save = function(item) { //var item = self.selectedItem(); if(item === self.newEntry()) { item.account_id(self.account().id()); } // Ajax call to save the entry. var type; var url = "api/entries"; if(item.id()) { url += "/" + item.id(); } $.ajax({ url: url, type: "PUT", contentType: "application/json", data:ko.toJSON(item), dataType: "json" }).success(function(data) { message("success", "Save", data); self.clearNewEntry(); 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(); }).success(function (data) { message("success", "Delete", data); }).complete(function (data) { // 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.newEntry().account_id(account.id()); self.account(account); self.loadMonths(account); } }; }; // Function to draw the sold evolution chart. drawChart = function(data, element) { // Clear previous chart $(element).html(""); var entries = data.entries; 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: '' }, // 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. 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', 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; } }; // 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 data = dataSource ? unwrap(dataSource) : null; drawChart(data, 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); }); // 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); } }); } }; // 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);
date:%s
sold:%s