diff --git a/.bowerrc b/.bowerrc
index d12db12..3bf1675 100644
--- a/.bowerrc
+++ b/.bowerrc
@@ -1,3 +1,3 @@
{
- "directory": "accountant/frontend/static/bower_components"
+ "directory": "accountant-ui/bower_components"
}
diff --git a/Gruntfile.js b/Gruntfile.js
index 9d310ea..5bff1be 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -9,8 +9,8 @@ module.exports = function(grunt) {
accountant: {
frontend: {
app: require('./bower.json'),
- src: 'accountant/frontend/static',
- dist: 'dist'
+ src: 'accountant-ui',
+ dist: 'accountant-ui_dist'
}
},
diff --git a/accountant-ui/css/main.css b/accountant-ui/css/main.css
new file mode 100644
index 0000000..b71eb74
--- /dev/null
+++ b/accountant-ui/css/main.css
@@ -0,0 +1,7 @@
+.italic {
+ font-style: italic
+}
+
+.stroke {
+ text-decoration: line-through
+}
diff --git a/accountant-ui/index.html b/accountant-ui/index.html
new file mode 100644
index 0000000..0c36c2b
--- /dev/null
+++ b/accountant-ui/index.html
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+ Accountant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/accountant-ui/js/accounts.js b/accountant-ui/js/accounts.js
new file mode 100644
index 0000000..515211d
--- /dev/null
+++ b/accountant-ui/js/accounts.js
@@ -0,0 +1,159 @@
+/*
+ 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.
+
+ Accountant 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 .
+ */
+// vim: set tw=80 ts=2 sw=2 sts=2:
+'use strict';
+
+angular.module('accountant.accounts', [
+ 'ngResource',
+ 'ui-notification',
+ 'xeditable',
+ 'ngBootbox'
+])
+
+.config(['$resourceProvider', function($resourceProvider) {
+ // Keep trailing slashes to avoid redirect by flask..
+ $resourceProvider.defaults.stripTrailingSlashes = false;
+}])
+
+.factory('Account', ['$resource', function($resource) {
+ var Account = $resource(
+ '/api/account/:id', {
+ id: '@id'
+ }
+ );
+
+ Account.prototype.getSolds = function() {
+ var Solds = $resource('/api/account/:id/solds', {id: this.id});
+
+ this.solds = Solds.get();
+ };
+
+ Account.prototype.getBalance = function(begin, end) {
+ var Balance = $resource(
+ '/api/account/:id/balance', {
+ id: this.id,
+ begin: begin.format('YYYY-MM-DD'),
+ end: end.format('YYYY-MM-DD')
+ });
+
+ this.balance = Balance.get();
+ };
+
+ return Account;
+}])
+
+.controller(
+ 'AccountController', [
+ '$scope', '$ngBootbox', 'Account', 'Notification',
+ function($scope, $ngBootbox, Account, Notification) {
+
+ /*
+ * Return the class for an account current value compared to authorized
+ * overdraft.
+ */
+ $scope.rowClass = function(account) {
+ if(!account || !account.authorized_overdraft || !account.current) {
+ return;
+ }
+
+ if(account.current < account.authorized_overdraft) {
+ return 'danger';
+ } else if(account.current < 0) {
+ return 'warning';
+ }
+ };
+
+ /*
+ * Return the class for a value compared to account authorized overdraft.
+ */
+ $scope.valueClass = function(account, value) {
+ if(!account || !value) {
+ return;
+ }
+
+ if(value < account.authorized_overdraft) {
+ return 'text-danger';
+ } else if(value < 0) {
+ return 'text-warning';
+ }
+ };
+
+ /*
+ * Add an empty account.
+ */
+ $scope.add = function() {
+ var account = new Account({
+ authorized_overdraft: 0
+ });
+
+ // Insert account at the begining of the array.
+ $scope.accounts.splice(0, 0, account);
+ };
+
+ /*
+ * Cancel account edition. Remove it from array if a new one.
+ */
+ $scope.cancelEdit = function(rowform, account, $index) {
+ if(!account.id) {
+ // Account not saved, just remove it from array.
+ $scope.accounts.splice($index, 1);
+ } else {
+ rowform.$cancel();
+ }
+ };
+
+ /*
+ * Save account.
+ */
+ $scope.save = function(account) {
+ //var account = $scope.accounts[$index];
+
+ //account = angular.merge(account, $data);
+
+ return account.$save().then(function(data) {
+ Notification.success('Account #' + data.id + ' saved.');
+
+ // TODO Alexis Lahouze 2016-03-08 Update solds
+
+ return data;
+ });
+ };
+
+ /*
+ * Delete an account.
+ */
+ $scope.delete = function(account, $index) {
+ var id = account.id;
+
+ $ngBootbox.confirm(
+ 'Voulez-vous supprimer le compte \'' + account.name + '\' ?',
+ function(result) {
+ if(result) {
+ account.$delete().then(function() {
+ Notification.success('Account #' + id + ' deleted.');
+
+ // Remove account from array.
+ $scope.accounts.splice($index, 1);
+ });
+ }
+ }
+ );
+ };
+
+ // Load accounts.
+ $scope.accounts = Account.query();
+}]);
diff --git a/accountant-ui/js/app.js b/accountant-ui/js/app.js
new file mode 100644
index 0000000..628d9f2
--- /dev/null
+++ b/accountant-ui/js/app.js
@@ -0,0 +1,154 @@
+/*
+ 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.
+
+ Accountant 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 .
+ */
+// vim: set tw=80 ts=2 sw=2 sts=2:
+'use strict';
+
+angular.module('accountant', [
+ 'accountant.accounts',
+ 'accountant.operations',
+ 'accountant.scheduler',
+ 'ngRoute',
+ 'ngBootbox',
+ 'http-auth-interceptor',
+ 'Storage.Service'
+])
+
+.factory('sessionInjector', ['$storage', function($storage) {
+ var sessionInjector = {
+ request : function(config) {
+ var token = $storage.get('token');
+
+ if(token) {
+ var token_type = $storage.get('token_type');
+ var authorization = token_type + ' ' + token;
+ config.headers.Authorization = authorization;
+ }
+
+ return config;
+ }
+ };
+
+ return sessionInjector;
+}])
+
+.config(['$httpProvider', function($httpProvider) {
+ // Define interceptors.
+ $httpProvider.interceptors.push('sessionInjector');
+}])
+
+.config(['$routeProvider', function($routeProvider) {
+ // Defining template and controller in function of route.
+ $routeProvider
+ .when('/account/:accountId/operations', {
+ templateUrl: 'views/operations.html',
+ controller: 'OperationController',
+ controllerAs: 'operationsCtrl'
+ })
+ .when('/account/:accountId/scheduler', {
+ templateUrl: 'views/scheduler.html',
+ controller: 'SchedulerController',
+ controllerAs: 'schedulerCtrl'
+ })
+ .when('/accounts', {
+ templateUrl: 'views/accounts.html',
+ controller: 'AccountController',
+ controllerAs: 'accountsCtrl'
+ })
+ .otherwise({
+ redirectTo: '/accounts'
+ });
+
+}])
+
+.config(['$storageProvider', function($storageProvider) {
+ // Configure storage
+ // Set global prefix for stored keys
+ $storageProvider.setPrefix('accountant');
+
+ // Change the default storage engine
+ // Defaults to 'local'
+ $storageProvider.setDefaultStorageEngine('session');
+
+ // Change the enabled storage engines
+ // Defaults to ['memory', 'cookie', 'session', 'local']
+ $storageProvider.setEnabledStorageEngines(['local', 'session']);
+}])
+
+.run(function(editableOptions) {
+ editableOptions.theme = 'bs3'; // bootstrap3 theme. Can be also 'bs2', 'default'
+})
+
+.controller('MainController', [
+ '$scope', '$rootScope', '$http', 'authService', '$storage', '$ngBootbox',
+ function($scope, $rootScope, $http, authService, $storage, $ngBootbox) {
+ $scope.dialogShown = false;
+
+ $scope.showLoginForm = function() {
+ // First, if there are registered credentials, use them
+ if($scope.dialogShown) {
+ return;
+ }
+
+ $scope.dialogShown = true;
+
+ $storage.clear();
+
+ $ngBootbox.customDialog({
+ title: 'Authentification requise',
+ templateUrl: 'views/login.tmpl.html',
+ buttons: {
+ login: {
+ label: 'Login',
+ className: 'btn-primary',
+ callback: function() {
+ $scope.dialogShown = false;
+
+ var email = $('#email').val();
+ var password = $('#password').val();
+ $http.post(
+ '/api/user/login',
+ {
+ 'email': email,
+ 'password': password
+ }
+ ).success(function(result) {
+ // TODO Alexis Lahouze 2015-08-28 Handle callback.
+ // Call to /api/login to retrieve the token
+ $storage.set('token_type', result.token_type);
+ $storage.set('token', result.token);
+ $storage.set('expiration_date', result.expiration_date);
+
+ authService.loginConfirmed();
+ });
+ }
+ },
+ cancel: {
+ label: 'Annuler',
+ className: 'btn-default',
+ callback: function() {
+ authService.loginCancelled(null, 'Login cancelled by user action.');
+ $scope.dialogShown = false;
+ }
+ }
+ }
+ });
+ };
+
+ $rootScope.$on('event:auth-loginRequired', $scope.showLoginForm);
+}])
+
+;
diff --git a/accountant-ui/js/operations.js b/accountant-ui/js/operations.js
new file mode 100644
index 0000000..72b2f4b
--- /dev/null
+++ b/accountant-ui/js/operations.js
@@ -0,0 +1,466 @@
+/*
+ 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.
+
+ Accountant 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 .
+ */
+// vim: set tw=80 ts=2 sw=2 sts=2:
+'use strict';
+
+angular.module('accountant.operations', [
+ 'accountant.accounts',
+ 'ngRoute',
+ 'ngResource',
+ 'ngBootbox',
+ 'ui-notification',
+ 'mgcrea.ngStrap',
+ 'highcharts-ng',
+])
+
+.config(['$resourceProvider', function($resourceProvider) {
+ // Keep trailing slashes to avoid redirect by flask..
+ $resourceProvider.defaults.stripTrailingSlashes = false;
+}])
+
+.factory('Operation', [ '$resource', function($resource) {
+ return $resource(
+ '/api/operation/:id', {
+ id: '@id'
+ }
+ );
+}])
+
+.factory('OHLC', [ '$resource', '$routeParams',
+ function($resource, $routeParams) {
+ return $resource(
+ '/api/account/:account_id/ohlc', {
+ account_id: $routeParams.accountId
+ }
+ );
+}])
+
+.factory('Category', [ '$resource', '$routeParams',
+ function($resource, $routeParams) {
+ return $resource(
+ '/api/account/:account_id/category', {
+ account_id: $routeParams.accountId
+ }
+ );
+}])
+
+.factory('Balance', [ '$resource', '$routeParams',
+ function($resource, $routeParams) {
+ return $resource(
+ '/api/account/:account_id/balance', {
+ account_id: $routeParams.accountId
+ }
+ );
+}])
+
+/*
+ * Controller for category chart.
+ */
+.controller(
+ 'CategoryChartController', [
+ '$rootScope', '$scope', '$http', 'Category', 'Balance',
+ function($rootScope, $scope, $http, Category, Balance) {
+
+ var colors = Highcharts.getOptions().colors;
+ $scope.revenueColor = colors[2];
+ $scope.expenseColor = colors[3];
+
+ // Configure pie chart for categories.
+ $scope.config = {
+ options: {
+ chart: {
+ type: 'pie',
+ animation: {
+ duration: 500
+ }
+ },
+ plotOptions: {
+ pie: {
+ startAngle: -90
+ },
+ series: {
+ allowPointSelect: 0
+ }
+ },
+ tooltip: {
+ valueDecimals: 2,
+ valueSuffix: '€'
+ }
+ },
+ yAxis: {
+ title: {
+ text: 'Categories'
+ }
+ },
+ title: {
+ text: 'Répartition dépenses/recettes'
+ },
+ series: [{
+ name: 'Value',
+ data: [],
+ innerSize: '33%',
+ size: '60%',
+ dataLabels: {
+ formatter: function() {
+ return this.point.name;
+ },
+ distance: -40
+ }
+ }, {
+ name: 'Value',
+ data: [],
+ innerSize: '66%',
+ size: '60%',
+ dataLabels: {
+ formatter: function() {
+ return this.point.name !== null && this.percentage >= 2.5 ? this.point.name : null;
+ },
+ }
+ }]
+ };
+
+ $scope.brightenColor = function(color) {
+ var brightness = 0.2;
+
+ return Highcharts.Color(color).brighten(brightness).get();
+ };
+
+ // Load categories, mainly to populate the pie chart.
+ $scope.load = function(begin, end) {
+ $scope.config.loading = true;
+
+ Category.query({
+ begin: begin.format('YYYY-MM-DD'),
+ end: end.format('YYYY-MM-DD')
+ }, function(data) {
+ var expenses = [], revenues = [];
+
+ var expenseColor = $scope.brightenColor($scope.expenseColor);
+ var revenueColor = $scope.brightenColor($scope.revenueColor);
+
+ angular.forEach(angular.fromJson(data), function(category) {
+ expenses.push({
+ name: category.category,
+ y: -category.expenses,
+ color: expenseColor
+ });
+
+ revenues.push({
+ name: category.category,
+ y: category.revenues,
+ color: revenueColor
+ });
+ });
+
+ // Note: expenses and revenues must be in the same order than in series[0].
+ $scope.config.series[1].data = revenues.concat(expenses);
+
+ $scope.config.loading = false;
+ });
+ };
+
+ /*
+ * Get account balance.
+ */
+ $scope.getBalance = function(begin, end) {
+ Balance.get({
+ begin: begin.format('YYYY-MM-DD'),
+ end: end.format('YYYY-MM-DD')
+ }, function(balance) {
+ // Update pie chart subtitle with Balance.
+ $scope.config.subtitle = {
+ text: 'Balance: ' + balance.balance
+ };
+
+ $scope.config.series[0].data = [{
+ name: 'Revenues',
+ y: balance.revenues,
+ color: $scope.revenueColor
+ }, {
+ name: 'Expenses',
+ y: -balance.expenses,
+ color: $scope.expenseColor,
+ }];
+ });
+ };
+
+ // Reload categories and account status on range selection.
+ $rootScope.$on('rangeSelectedEvent', function(e, args) {
+ $scope.load(args.begin, args.end);
+ $scope.getBalance(args.begin, args.end);
+ });
+}])
+
+/*
+ * Controller for the sold chart.
+ */
+.controller(
+ 'SoldChartController', [
+ '$rootScope', '$scope', '$http', 'OHLC',
+ function($rootScope, $scope, $http, OHLC) {
+ // Configure chart for operations.
+ $scope.config = {
+ options: {
+ chart: {
+ zoomType: 'x'
+ },
+ rangeSelector: {
+ buttons: [{
+ type: 'month',
+ count: 1,
+ text: '1m'
+ }, {
+ type: 'month',
+ count: 3,
+ text: '3m'
+ }, {
+ type: 'month',
+ count: 6,
+ text: '6m'
+ }, {
+ type: 'year',
+ count: 1,
+ text: '1y'
+ }, {
+ type: 'all',
+ text: 'All'
+ }],
+ selected: 0,
+ },
+ navigator: {
+ enabled: true
+ },
+ tooltip: {
+ crosshairs: true,
+ shared: true,
+ valueDecimals: 2,
+ valueSuffix: '€'
+ },
+ scrollbar: {
+ liveRedraw: false
+ }
+ },
+ series: [{
+ type: 'ohlc',
+ name: 'Sold',
+ data: [],
+ dataGrouping : {
+ units : [[
+ 'week', // unit name
+ [1] // allowed multiples
+ ], [
+ 'month',
+ [1, 2, 3, 4, 6]
+ ]]
+ }
+ }],
+ title: {
+ text: 'Sold evolution'
+ },
+ xAxis: {
+ type: 'datetime',
+ dateTimeLabelFormats: {
+ month: '%e. %b',
+ year: '%Y'
+ },
+ minRange: 3600 * 1000 * 24 * 14, // 2 weeks
+ events: {
+ afterSetExtremes: function(e) {
+ $scope.$emit('rangeSelectedEvent', {
+ begin: moment.utc(e.min), end: moment.utc(e.max)
+ });
+ }
+ },
+ currentMin: moment.utc().startOf('month'),
+ currentMax: moment.utc().endOf('month')
+ },
+ yAxis: {
+ plotLines: [{
+ color: 'orange',
+ width: 2,
+ value: 0.0
+ }, {
+ color: 'red',
+ width: 2,
+ value: 0.0
+ }]
+ },
+ useHighStocks: true
+ };
+
+ $scope.loadSolds = function() {
+ $scope.config.loading = true;
+
+ OHLC.query({}, function(data) {
+ $scope.config.series[0].data = [];
+
+ angular.forEach(data, function(operation) {
+ $scope.config.series[0].data.push([
+ moment.utc(operation.operation_date).valueOf(),
+ operation.open, operation.high, operation.low, operation.close
+ ]);
+ });
+
+ $scope.$emit('rangeSelectedEvent', {
+ begin: $scope.config.xAxis.currentMin,
+ end: $scope.config.xAxis.currentMax
+ });
+
+ $scope.config.loading = false;
+ });
+ };
+
+ // Reload solds when an operation is saved.
+ $rootScope.$on('operationSavedEvent', function() {
+ $scope.loadSolds();
+ });
+
+ // Reload solds when an operation is deleted.
+ $rootScope.$on('operationDeletedEvent', function() {
+ $scope.loadSolds();
+ });
+
+ // Update authorized overdraft on account loading.
+ $rootScope.$on('accountLoadedEvent', function(e, account) {
+ $scope.config.yAxis.plotLines[1].value = account.authorized_overdraft;
+ });
+
+ // Select beginning and end of month.
+ $scope.loadSolds();
+}])
+
+/*
+ * Controller for the operations.
+ */
+.controller(
+ 'OperationController', [
+ '$scope', '$rootScope', '$routeParams', '$ngBootbox', 'Notification', 'Account', 'Operation',
+ function($scope, $rootScope, $routeParams, $ngBootbox, Notification, Account, Operation) {
+ // List of operations.
+ $scope.operations = [];
+
+ /*
+ * Add an empty operation.
+ */
+ $scope.add = function() {
+ var operation = new Operation({
+ account_id: $routeParams.accountId
+ });
+
+ $scope.operations.splice(0, 0, operation);
+ };
+
+ /*
+ * Load operations.
+ */
+ $scope.load = function(begin, end) {
+ $scope.operations = Operation.query({
+ account_id: $routeParams.accountId,
+ begin: begin.format('YYYY-MM-DD'),
+ end: end.format('YYYY-MM-DD')
+ });
+ };
+
+ /*
+ * Cancel edition.
+ */
+ $scope.cancelEdit = function(operation, rowform, $index) {
+ if(!operation.id) {
+ $scope.operations.splice($index, 1);
+ } else {
+ rowform.$cancel();
+ }
+ };
+
+ /*
+ * Toggle pointed indicator for an operation.
+ */
+ $scope.togglePointed = function(operation, rowform) {
+ operation.pointed = !operation.pointed;
+
+ // Save operation if not editing it.
+ if(!rowform.$visible) {
+ $scope.save(operation);
+ }
+ };
+
+ /*
+ * Toggle cancel indicator for an operation.
+ */
+ $scope.toggleCanceled = function(operation) {
+ operation.canceled = !operation.canceled;
+
+ $scope.save(operation);
+ };
+
+ /*
+ * Save an operation and emit operationSavedEvent.
+ */
+ $scope.save = function($data, $index) {
+ // Check if $data is already a resource.
+ var operation;
+
+ if($data.$save) {
+ operation = $data;
+ } else {
+ operation = $scope.operations[$index];
+ operation = angular.merge(operation, $data);
+ }
+
+ operation.confirmed = true;
+
+ return operation.$save().then(function(data) {
+ Notification.success('Operation #' + data.id + ' saved.');
+
+ $scope.$emit('operationSavedEvent', data);
+ });
+ };
+
+ /*
+ * Delete an operation and emit operationDeletedEvent.
+ */
+ $scope.delete = function(operation, $index) {
+ var id = operation.id;
+
+ $ngBootbox.confirm(
+ 'Voulez-vous supprimer l\'opération \\\'' + operation.label + '\\\' ?',
+ function(result) {
+ if(result) {
+ operation.$delete().then(function() {
+ Notification.success('Operation #' + id + ' deleted.');
+
+ // Remove operation from array.
+ $scope.operation.splice($index, 1);
+
+ $scope.$emit('operationDeletedEvent', operation);
+ });
+ }
+ }
+ );
+ };
+
+ $scope.account = Account.get({
+ id: $routeParams.accountId
+ });
+
+ /*
+ * Reload operations on rangeSelectedEvent.
+ */
+ $rootScope.$on('rangeSelectedEvent', function(e, args) {
+ $scope.load(args.begin, args.end);
+ });
+}]);
diff --git a/accountant-ui/js/scheduler.js b/accountant-ui/js/scheduler.js
new file mode 100644
index 0000000..510dd92
--- /dev/null
+++ b/accountant-ui/js/scheduler.js
@@ -0,0 +1,120 @@
+/*
+ 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.
+
+ Accountant 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 .
+ */
+// vim: set tw=80 ts=2 sw=2 sts=2:
+'use strict';
+
+angular.module('accountant.scheduler', [
+ 'ngRoute',
+ 'ngBootbox',
+ 'ui-notification',
+ 'mgcrea.ngStrap'
+])
+
+.config(['$resourceProvider', function($resourceProvider) {
+ // Keep trailing slashes to avoid redirect by flask..
+ $resourceProvider.defaults.stripTrailingSlashes = false;
+}])
+
+.factory('ScheduledOperation', ['$resource', function($resource) {
+ return $resource(
+ '/api/scheduled_operation/:id', {
+ id: '@id'
+ }
+ );
+}])
+
+.controller(
+ 'SchedulerController', [
+ '$scope', '$rootScope', '$routeParams', '$ngBootbox', 'Notification', 'ScheduledOperation',
+ function($scope, $rootScope, $routeParams, $ngBootbox, Notification, ScheduledOperation) {
+ // Operation store.
+ $scope.operations = [];
+
+ /*
+ * Add a new operation at the beginning of th array.
+ */
+ $scope.add = function() {
+ var operation = new ScheduledOperation({
+ account_id: $routeParams.accountId
+ });
+
+ // Insert new operation at the beginning of the array.
+ $scope.operations.splice(0, 0, operation);
+ };
+
+ /*
+ * Load operations.
+ */
+ $scope.load = function() {
+ $scope.operations = ScheduledOperation.query({
+ account_id: $routeParams.accountId
+ });
+ };
+
+ /*
+ * Save operation.
+ */
+ $scope.save = function($data, $index) {
+ var operation;
+
+ if($data.$save) {
+ operation = $data;
+ } else {
+ operation = $scope.operations[$index];
+ operation = angular.merge(operation, $data);
+ }
+
+ return operation.$save().then(function(data) {
+ Notification.success('Operation #' + data.id + ' saved.');
+ });
+ };
+
+ /*
+ * Cancel operation edition. Delete if new.
+ */
+ $scope.cancelEdit = function(operation, rowform, $index) {
+ if(!operation.id) {
+ $scope.operations.splice($index, 1);
+ } else {
+ rowform.$cancel();
+ }
+ };
+
+ /*
+ * Delete operation.
+ */
+ $scope.delete = function(operation, $index) {
+ var id = operation.id;
+
+ $ngBootbox.confirm(
+ 'Voulez-vous supprimer l\'operation planifiée \\\'' + operation.label + '\\\' ?',
+ function(result) {
+ if(result) {
+ operation.$delete().then(function() {
+ Notification.success('Operation #' + id + ' deleted.');
+
+ // Remove account from array.
+ $scope.operations.splice($index, 1);
+ });
+ }
+ }
+ );
+ };
+
+ // Load operations on controller initialization.
+ $scope.load();
+}]);
diff --git a/accountant-ui/views/accounts.html b/accountant-ui/views/accounts.html
new file mode 100644
index 0000000..9920458
--- /dev/null
+++ b/accountant-ui/views/accounts.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+ Nom du compte |
+ Solde courant |
+ Solde pointé |
+ Découvert autorisé |
+ Actions |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ {{ account.name }}
+
+ |
+
+
+
+ {{ account.solds.current | currency : "€" }}
+
+ |
+
+
+
+ {{ account.solds.pointed | currency : "€" }}
+
+ |
+
+
+
+ {{ account.authorized_overdraft | currency : "€" }}
+
+ |
+
+
+
+ |
+
+
+
+
diff --git a/accountant-ui/views/login.tmpl.html b/accountant-ui/views/login.tmpl.html
new file mode 100644
index 0000000..a381d06
--- /dev/null
+++ b/accountant-ui/views/login.tmpl.html
@@ -0,0 +1,17 @@
+
+
+
diff --git a/accountant-ui/views/operations.html b/accountant-ui/views/operations.html
new file mode 100644
index 0000000..fdf1fdd
--- /dev/null
+++ b/accountant-ui/views/operations.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+ Date d'op. |
+ Libellé de l'opération |
+ Montant |
+ Solde |
+ Catégorie |
+ Actions |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ {{ operation.operation_date | date:"yyyy-MM-dd" }}
+
+ |
+
+
+
+ {{ operation.label }}
+
+ |
+
+
+
+ {{ operation.value | currency:"€" }}
+
+ |
+
+
+ {{ operation.sold | currency:"€" }}
+ |
+
+
+
+ {{ operation.category }}
+
+ |
+
+
+
+ |
+
+
+
+
+
diff --git a/accountant-ui/views/scheduler.html b/accountant-ui/views/scheduler.html
new file mode 100644
index 0000000..2552387
--- /dev/null
+++ b/accountant-ui/views/scheduler.html
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+ Date de début |
+ Date de fin |
+ Jour |
+ Fréq. |
+ Libellé de l'opération |
+ Montant |
+ Catégorie |
+ Actions |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ {{ operation.start_date | date: "yyyy-MM-dd" }}
+
+ |
+
+
+
+ {{ operation.stop_date | date: "yyyy-MM-dd" }}
+
+ |
+
+
+
+ {{ operation.day }}
+
+ |
+
+
+
+ {{ operation.frequency }}
+
+ |
+
+
+
+ {{ operation.label }}
+
+ |
+
+
+
+ {{ operation.value | currency : "€" }}
+
+ |
+
+
+
+ {{ operation.category }}
+
+ |
+
+
+
+ |
+
+
+
+
diff --git a/bower.json b/bower.json
index 3127488..44eb582 100644
--- a/bower.json
+++ b/bower.json
@@ -6,8 +6,8 @@
],
"license": "AGPL",
"main": [
- "accountant/frontend/static/index.html",
- "accountant/frontend/static/js/app.js"
+ "accountant-ui/index.html",
+ "accountant-ui/js/app.js"
],
"ignore": [
"**/.*",