// 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(); } // Util function to show a message in message placeholder. function message(alertType, title, message) { $(".alert").alert('close'); $("#message-placeholder").append('

' + title + '

' + message + '
'); } 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 entries=ko.utils.unwrapObservable(self.entries); // First pass: get sum values for each category. var chartValuesTmp = {}; $.each(entries, function(index, entry) { var category = entry.category(); var value = entry.value() ? Number(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() { // We assume that entries are sorted by value date descending. var entries = ko.utils.unwrapObservable(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 = entry.value_date(); var value = entry.value ? Number(entry.value()) : null; if(date && value) { var values = {}; var sold = Number(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) { $.post("api/entry.php", {action: "get_entries", account: account.id, year: month.year, month: month.month}, function(data) { // Clean up current entries. self.entries.removeAll(); // Clean up selected entry. self.selectedItem(null); self.entries(ko.utils.arrayMap(data, ko.mapping.fromJS)); }); }; self.loadAccounts = function() { $.post("api/entry.php", {action: "get_accounts"}).success(function (result) { self.accounts(result); if(self.account()) { $.each(self.accounts(), function(index, account) { if(self.account().id == account.id) { self.account(account); } }); } if(!self.account()){ self.account(result[0]); } self.loadMonths(self.account()); }); }; self.loadMonths = function(account){ $.post("api/entry.php", {action: "get_months", account: account.id}).success(function (result) { self.months(result); if(self.month()) { $.each(self.months(), function(index, month) { if(self.month().year == month.year && self.month().month == month.month) { self.month(month); } }); } if(!self.month()) { self.month(result[result.length - 1]); } self.loadEntries(self.account(), self.month()); }); }; self.templateToUse = function (item) { return self.selectedItem() === item ? 'editTmpl' : 'itemsTmpl'; }; self.edit = function(item) { if(self.savedItem) { self.cancel(); } self.savedItem=ko.toJS(item); self.selectedItem(item); $("#value_date").datepicker().on('changeDate', function(ev){ self.selectedItem().value_date(ev.date.format(ev.currentTarget.dataset.dateFormat)); }); $("#operation_date").datepicker().on('changeDate', function(ev){ self.selectedItem().operation_date(ev.date.format(ev.currentTarget.dataset.dateFormat)); }); }; self.cancel = function() { 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. if(self.selectedItem() && !self.selectedItem().id()) { self.entries.remove(self.selectedItem()); } self.savedItem = null; self.selectedItem(null); }; self.add = function() { var newEntry = new entry(); newEntry.account_id(self.account().id); self.entries.unshift(newEntry); self.edit(newEntry); }; self.save = function() { var item = ko.toJS(self.selectedItem()); $.post("api/entry.php", {action: "save_entry", entry:item}).success(function(data) { message("success", "Save", data.message); self.selectedItem(null); self.loadAccounts(); }).error(function() { message("error", "Error.", "Unexpected error."); }); }; self.remove = function (item) { if (item.id()) { self.itemToRemove(item); $('#remove-confirm').modal(); } else { self.entries.remove(item); } }; self.confirmRemove = function() { var item = self.itemToRemove(); $.post("api/entry.php", {action: "remove_entry", entry:item}).success(function (result) { self.loadAccounts(); }).complete(function (result) { self.itemToRemove(null); $('#remove-confirm').modal('hide'); }); }; self.selectMonth = function(month) { if(month) { self.month(month); self.loadEntries(self.account(), month); } }; self.selectAccount = function(account) { if(account) { self.account(account); self.loadMonths(account); } }; }; drawChart = function(entries, element) { // clear previous chart $(element).html(""); if(entries && entries.length > 0) { var chartValues = [[], []]; chartValues[0] = entries; var today = new Date(); today.setHours(0); today.setMinutes(0); 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 window.chart = $.jqplot(element.id, chartValues, { title: "Évolution du solde", axes:{ xaxis:{ autoscale: true, renderer:$.jqplot.DateAxisRenderer, min: firstDate, max: lastDate, tickOptions: {formatString: "%F"} }, yaxis: { autoscale: true, } }, highlighter: { show:true, yvalues: 4, formatString:'
date:%s
open:%s
hi:%s
low:%s
close:%s
' }, series: [{ renderer:$.jqplot.OHLCRenderer, color: "blue", lineWidth: 3, rendererOptions:{} }], canvasOverlay: { show: true, objects: [{ dashedHorizontalLine: { name: "zero", y: 0, lineWidth: 1, color: "red", shadow: false }}, { dashedVerticalLine: { name: "today", x: today, lineWidth: 1, color: "gray", shadow: false }}] } }); } else { window.chart = null; } }; drawPieChart = function(entries, element) { // clear previous chart $(element).html(""); if(entries && entries.length > 0) { var chartValues = [[]]; chartValues[0] = entries; // plot chart window.pieChart = $.jqplot(element.id, chartValues, { title: "Dépenses", seriesDefaults: { renderer: $.jqplot.PieRenderer, rendererOptions: { showDataLabels: true } }, legend: { show: true, location: 'e' }, highlighter: { show: true, formatString:'%s: %s', tooltipLocation:'sw', useAxesFormatters:false } }); } else { window.pieChart = null; } }; 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; var entries = dataSource ? unwrap(dataSource) : null; drawChart(entries, element); } }; 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; var entries = dataSource ? unwrap(dataSource) : null; drawPieChart(entries, element); } }; $(document).ajaxError(function(event, xhr, settings) { message("error", "Error.", xhr.statusText); }); $(window).resize(function() { if(window.chart) { window.chart.replot({resetAxes: true}); } if(window.pieChart) { window.pieChart.replot({resetAxes: true}); } }); var viewModel = new ListViewModel(); ko.applyBindings(viewModel); $(viewModel.loadAccounts);