411 lines
13 KiB
JavaScript
411 lines
13 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/>.
|
|
*/
|
|
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");
|
|
};
|
|
|
|
$scope.getAccountStatus = function(account, month) {
|
|
if(account != null && month != null) {
|
|
$http.get("/api/accounts/" + account.id + "/" + month.year + "/" + month.month).
|
|
success($scope.getAccountStatus_success);
|
|
}
|
|
};
|
|
|
|
$scope.getAccountStatus_success = function(status) {
|
|
$scope.accountStatus = status;
|
|
};
|
|
|
|
// 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: 'new',
|
|
canceled: false,
|
|
scheduled_operation_id: null
|
|
}];
|
|
|
|
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(entry) {
|
|
if($scope.isSaved(entry)) {
|
|
cssclass="";
|
|
} else {
|
|
cssclass="italic";
|
|
}
|
|
|
|
if(entry.canceled) {
|
|
cssclass += " stroke";
|
|
}
|
|
|
|
if(entry.sold < $scope.account.authorized_overdraft) {
|
|
cssclass += " error";
|
|
} else if (entry.sold < 0) {
|
|
cssclass += " warning";
|
|
}
|
|
|
|
return cssclass;
|
|
};
|
|
|
|
// Returns the CSS class for an entry sold.
|
|
$scope.entryValueClass = function(sold) {
|
|
if(sold && sold < $scope.account.authorized_overdraft) {
|
|
return 'text-error';
|
|
} else if (sold && 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 !$scope.isSaved(entry) && (entry.state === 'edit' || entry.state === 'new');
|
|
};
|
|
|
|
// Returns true if the entry is in editing state.
|
|
$scope.isEditing = function(entry) {
|
|
return entry.state === 'edit' || entry.state === 'new';
|
|
};
|
|
|
|
// Returns true if the entry is in displaying state.
|
|
$scope.isDisplaying = function(entry) {
|
|
return !entry.state || entry.state === 'display';
|
|
};
|
|
|
|
// Returns true if the entry is a scheduled one.
|
|
$scope.isSaved = function(entry) {
|
|
return entry.id != null;
|
|
}
|
|
|
|
$scope.iconSaveClass = function(entry) {
|
|
if(!$scope.isSaved(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.savedItem == null) {
|
|
// 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;
|
|
entry.canceled = false;
|
|
entry.scheduled_operation_id = null;
|
|
|
|
// Reset state to new.
|
|
entry.state = 'new';
|
|
} else {
|
|
// 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;
|
|
entry.canceled = $scope.savedItem.canceled;
|
|
entry.scheduled_operation_id = $scope.savedItem.scheduled_operation_id;
|
|
|
|
// 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.isSaved(entry)) {
|
|
url += "/" + entry.id;
|
|
}
|
|
|
|
// Ajax call to save an entry
|
|
$http.put(url, angular.toJson(entry)).success(function(data) {
|
|
$.pnotify({
|
|
type: "success",
|
|
title: "Save",
|
|
text: data
|
|
});
|
|
|
|
// $scope.savedItem = null;
|
|
|
|
// Send the "entry saved" event.
|
|
$scope.$emit("entrySavedEvent", entry);
|
|
});
|
|
};
|
|
|
|
// Removes an entry.
|
|
$scope.removeEntry = function(entry, modalScope) {
|
|
// Cancel current editing.
|
|
if (!$scope.isNew(entry)) {
|
|
$http.delete("/api/entries/" + entry.id).success(function (result) {
|
|
$.pnotify({
|
|
type: "success",
|
|
title: "Delete",
|
|
text: result
|
|
});
|
|
|
|
// Send the "entry removed" event.
|
|
$scope.$emit("entryRemovedEvent", entry);
|
|
|
|
$scope.closeModal(modalScope);
|
|
}).error(function (data) {
|
|
$.pnotify({
|
|
type: "error",
|
|
title: "Delete",
|
|
text: 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');
|
|
//var firstDate = new Date(Date.parse(entries[0][0]).valueOf() - day);
|
|
//var lastDate = new Date(Date.parse(entries[entries.length -1][0]).valueOf() + day);
|
|
|
|
// Plot chart, and store it in a window parameter for resize callback (need to be done better than it...)
|
|
var chart = nv.models.lineChart()
|
|
.x(function(d) { return d3.time.format("%Y-%m-%d").parse(d[0]); })
|
|
.y(function(d) { return new Number(d[1]); });
|
|
|
|
chart.lines.interpolate("monotone");
|
|
|
|
chart.xAxis.axisLabel("Date").tickFormat(function(d) {
|
|
return d3.time.format("%Y-%m-%d")(new Date(d));
|
|
});
|
|
chart.xAxis.scale().range([firstDate, lastDate]);
|
|
|
|
chart.yAxis.axisLabel("Solde").tickFormat(d3.format('.02f'));
|
|
|
|
// FIXME add vertical line for today
|
|
graph = d3.select(elementId + " svg").datum([
|
|
{ color: "orange", key: "Zero", values:[
|
|
[firstDate, "0"],
|
|
[lastDate, "0"]
|
|
]},
|
|
{ color: "red", key: "Authorized overdraft", values : [
|
|
[firstDate, new Number($scope.account.authorized_overdraft)],
|
|
[lastDate, new Number($scope.account.authorized_overdraft)]
|
|
]},
|
|
{ color: "darkblue", key: "Sold evolution", values: entries},
|
|
]).transition().duration(1200).call(chart);
|
|
|
|
nv.utils.windowResize(chart.update);
|
|
};
|
|
|
|
// Function to draw the expense category pie chart.
|
|
$scope.drawPieChart = function(entries, elementId) {
|
|
//if(entries && entries.length > 1) {
|
|
var chart = nv.models.pieChart()
|
|
.x(function(d) { return d[0]; })
|
|
.y(function(d) { return d[1]; })
|
|
.showLabels(true);
|
|
|
|
d3.select(elementId + " svg").datum([{key: "Expenses", values: entries}]).transition().duration(1200).call(chart);
|
|
|
|
nv.utils.windowResize(chart.update);
|
|
//}
|
|
};
|
|
|
|
$rootScope.$on("monthsLoadedEvent", function(event, args){
|
|
$scope.loadEntries(args.account, args.month);
|
|
});
|
|
|
|
$rootScope.$on("monthsLoadedEvent", function(event, args){
|
|
$scope.getAccountStatus(args.account, args.month);
|
|
});
|
|
|
|
$scope.$on("entriesLoadedEvent", function(event, args) {
|
|
$scope.entriesLoaded(args.entries);
|
|
});
|
|
};
|
|
|