Rename accountant-ui dir to src.

This commit is contained in:
Alexis Lahouze
2017-06-10 20:24:51 +02:00
parent 3c4a67a952
commit 9fe38b2560
15 changed files with 3 additions and 3 deletions

51
src/index.ejs Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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 <http://www.gnu.org/licenses/>.
-->
<html lang="fr">
<head>
<meta charset="UTF-8">
<!-- Title -->
<title><% htmlWebpackPlugin.options.title %></title>
<!-- Custom styles -->
<link href="<% htmlWebpackPlugin.files.css[0] %>" rel="stylesheet" type="text/css">
</head>
<!-- htmllint attr-bans="false" -->
<body style="padding-bottom: 50px; padding-top: 70px" ng-app="accountant">
<!-- htmllint attr-bans="$previous" -->
<!-- Navbar -->
<nav class="navbar navbar-fixed-top navbar-inverse">
<div class="container-fluid">
<!-- Brand -->
<div class="navbar-header">
<a class="navbar-brand" href="#!/accounts">&nbsp;Accountant</a>
</div>
</div>
</nav>
<div class="container-fluid" ng-controller="MainController">
<div ng-view></div>
</div>
<!-- Custom Javascript libraries -->
<script src="<% htmlWebpackPlugin.files.js[0] %>"></script>
</body>
</html>

263
src/js/accounts.js Normal file
View File

@ -0,0 +1,263 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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 <http://www.gnu.org/licenses/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngResource = require('angular-resource'),
ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngBootbox = require('ngbootbox');
// Note: ngBootbox seems to have no module.exports.
ngBootbox = 'ngBootbox';
// FIXME Alexis Lahouze 2017-06-06 move into templates.js.
require('../views/account.form.tmpl.html');
var accountModule = angular.module('accountant.accounts', [
ngResource,
ngMessages,
ngUiNotification,
ngBootbox
])
.config(function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
})
.factory('Account', function($resource) {
var Account = $resource(
'/api/account/:id', {
id: '@id'
}
);
Account.prototype.getSolds = function() {
var Solds = $resource('/api/account/:id/balances', {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', function($ngBootbox, Account, Notification) {
var vm = this;
/*
* Return the class for an account current value compared to authorized
* overdraft.
*/
vm.rowClass = function(account) {
// eslint-disable-next-line camelcase
if (!account || !account.authorized_overdraft || !account.current) {
return;
}
// eslint-disable-next-line camelcase
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.
*/
vm.valueClass = function(account, value) {
if (!account || !value) {
return;
}
// eslint-disable-next-line camelcase
if (value < account.authorized_overdraft) {
return 'text-danger';
} else if (value < 0) {
return 'text-warning';
}
};
/*
* Add an empty account.
*/
vm.add = function() {
var account = new Account({
// eslint-disable-next-line camelcase
authorized_overdraft: 0
});
// Insert account at the begining of the array.
vm.accounts.splice(0, 0, account);
};
/*
* Cancel account edition. Remove it from array if a new one.
*/
vm.cancelEdit = function(rowform, account, $index) {
if (account.id) {
rowform.$cancel();
} else {
// Account not saved, just remove it from array.
vm.accounts.splice($index, 1);
}
};
/*
* Save account.
*/
vm.save = function(account) {
// var account = vm.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.
*/
vm.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.
vm.accounts.splice($index, 1);
});
}
}
);
};
// Load accounts.
vm.accounts = Account.query();
})
.directive('accountFormDialog', function(Account, $ngBootbox, Notification, $log) {
return {
restrict: 'A',
scope: {
account: '=ngModel'
},
link: function(scope, element) {
var title = 'Account';
if (scope.account && scope.account.id) {
title = title + ' #' + scope.account.id;
}
scope.form = {};
scope.submitForm = function() {
// check to make sure the form is completely valid
if (!scope.form.$valid) {
return false;
}
// Authorized overdraft is a positive integer but data is a negative integer.
// eslint-disable-next-line camelcase
scope.data.authorized_overdraft = -scope.data.authorized_overdraft;
angular.copy(scope.data, scope.account);
// Save account
$log.log(scope.account);
return scope.account.$save().then(
function(data) {
Notification.success('Account #' + data.id + ' saved.');
scope.account.getSolds();
return data;
},
function(data) {
Notification.error('Error while saving account #' + data.id);
scope.account.getSolds();
$log.log(data);
return false;
}
);
};
element.on('click', function() {
// Create new account if not passed in ng-model.
if (!scope.account) {
scope.account = new Account({
// eslint-disable-next-line camelcase
authorized_overdraft: 0
});
}
scope.data = {};
angular.copy(scope.account, scope.data);
// Authorized overdraft must be positive in form.
// eslint-disable-next-line camelcase
scope.data.authorized_overdraft = -scope.data.authorized_overdraft;
// Open dialog with form.
$ngBootbox.customDialog({
scope: scope,
title: title,
templateUrl: '/views/account.form.tmpl.html',
onEscape: true,
buttons: {
save: {
label: 'Save',
className: 'btn-success',
callback: scope.submitForm
},
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: true
}
}
});
});
}
};
});
module.exports = accountModule;

199
src/js/app.js Normal file
View File

@ -0,0 +1,199 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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 <http://www.gnu.org/licenses/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngRoute = require('angular-route'),
ngBootbox = require('ngbootbox'),
ngStorage = require('meanie-angular-storage'),
ngHttpAuth = require('angular-http-auth');
var base64 =require('base64util');
// Note: ngBootbox seems to have no module.exports.
ngBootbox = 'ngBootbox';
// Note: ngHttpAuth seems to have no module.exports.
ngHttpAuth = 'http-auth-interceptor';
var accountModule = require('./accounts'),
operationModule = require('./operations'),
schedulerModule = require('./scheduler');
require('bootstrap-webpack!./bootstrap.config.js');
require('../views/operations.html');
require('../views/accounts.html');
require('../views/scheduler.html');
require('../views/login.tmpl.html');
var app = angular.module('accountant', [
accountModule.name,
operationModule.name,
schedulerModule.name,
ngRoute,
ngBootbox,
ngHttpAuth,
ngStorage
])
.factory('sessionInjector', function($storage) {
var sessionInjector = {
request: function(config) {
var access_token = $storage.session.get('access_token');
if (access_token) {
//var tokenType = $storage.get('token_type');
var tokenType = 'Bearer';
var authorization = tokenType + ' ' + access_token;
config.headers.authorization = authorization;
}
return config;
}
};
return sessionInjector;
})
.config(function($httpProvider) {
// Define interceptors.
$httpProvider.interceptors.push('sessionInjector');
})
.config(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(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'
})
.factory('LoginService', function($http) {
var login = function(email, password) {
var authdata = base64.encode(email + ':' + password);
return $http.post('/api/user/login', {}, {
headers: {
'authorization': 'Basic ' + authdata
}
});
};
return {
'login': login
};
})
.controller('MainController', function($rootScope, LoginService, authService, $storage, $ngBootbox, $document) {
var vm = this;
vm.dialogShown = false;
vm.showLoginForm = function() {
// First, if there are registered credentials, use them
if (vm.dialogShown) {
return;
}
vm.dialogShown = true;
$storage.session.clear();
$ngBootbox.customDialog({
title: 'Authentification requise',
templateUrl: '/views/login.tmpl.html',
buttons: {
login: {
label: 'Login',
className: 'btn-primary',
callback: function() {
vm.dialogShown = false;
var email = angular.element($document.querySelector('#email')).val();
var password = angular.element($document.querySelector('#password')).val();
LoginService.login(
email, password
).then(function(result) {
// TODO Alexis Lahouze 2015-08-28 Handle callback.
// Call to /api/login to retrieve the token
$storage.session.set('refresh_token', result.data.refresh_token);
$storage.session.set('access_token', result.data.access_token);
authService.loginConfirmed();
});
}
},
cancel: {
label: 'Annuler',
className: 'btn-default',
callback: function() {
authService.loginCancelled(null, 'Login cancelled by user action.');
vm.dialogShown = false;
}
}
}
});
};
vm.onAuthLoginRequired = $rootScope.$on('event:auth-loginRequired', vm.showLoginForm);
$rootScope.$on('$destroy', function() {
vm.onAuthLoginRequired = angular.noop();
});
})
;
module.exports = app;

64
src/js/bootstrap.config.js vendored Normal file
View File

@ -0,0 +1,64 @@
/* jshint node: true */
'use strict';
module.exports = {
scripts: {
'transition': true,
'alert': true,
'button': true,
'carousel': true,
'collapse': true,
'dropdown': true,
'modal': true,
'tooltip': true,
'popover': true,
'scrollspy': true,
'tab': true,
'affix': true
},
styles: {
'mixins': true,
'normalize': true,
'print': true,
'scaffolding': true,
'type': true,
'code': true,
'grid': true,
'tables': true,
'forms': true,
'buttons': true,
'component-animations': true,
'glyphicons': true,
'dropdowns': true,
'button-groups': true,
'input-groups': true,
'navs': true,
'navbar': true,
'breadcrumbs': true,
'pagination': true,
'pager': true,
'labels': true,
'badges': true,
'jumbotron': true,
'thumbnails': true,
'alerts': true,
'progress-bars': true,
'media': true,
'list-group': true,
'panels': true,
'wells': true,
'close': true,
'modals': true,
'tooltip': true,
'popovers': true,
'carousel': true,
'utilities': true,
'responsive-utilities': true
}
};

2
src/js/bootstrap.config.less vendored Normal file
View File

@ -0,0 +1,2 @@
@pre-border-color: @pre-bg; // hide the border.
@import "../less/main.less";

561
src/js/operations.js Normal file
View File

@ -0,0 +1,561 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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 <http://www.gnu.org/licenses/>.
*/
/* jshint node: true */
'use strict';
var moment = require('moment'),
Highcharts = require('highstock-release');
var angular = require('angular');
require('../views/operation.form.tmpl.html');
var ngResource = require('angular-resource'),
ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngBootbox = require('ngbootbox'),
ngStrap = require('angular-strap');
// Note: ngBootbox seems to have no module.exports.
ngBootbox = 'ngBootbox';
var accountModule = require('./accounts');
var operationModule = angular.module('accountant.operations', [
accountModule.name,
ngResource,
ngMessages,
ngUiNotification,
ngBootbox,
ngStrap
])
.config(function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
})
.factory('Operation', function($resource) {
return $resource(
'/api/operation/:id', {
id: '@id'
}
);
})
.factory('OHLC', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/ohlc', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
.factory('Category', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/category', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
.factory('Balance', function($resource, $routeParams) {
return $resource(
'/api/account/:account_id/balance', {
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
}
);
})
/*
* Controller for category chart.
*/
.controller('CategoryChartController',
function($rootScope, Category, Balance) {
var vm = this;
var colors = Highcharts.getOptions().colors;
vm.revenueColor = colors[2];
vm.expenseColor = colors[3];
// Configure pie chart for categories.
vm.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() {
// eslint-disable-next-line angular/controller-as-vm
return this.point.name;
},
distance: -40
}
}, {
name: 'Value',
data: [],
innerSize: '66%',
size: '60%',
dataLabels: {
formatter: function() {
// eslint-disable-next-line angular/controller-as-vm
if (this.point.name !== null && this.percentage >= 2.5) {
// eslint-disable-next-line angular/controller-as-vm
return this.point.name;
}
return null;
}
}
}]
};
vm.brightenColor = function(color) {
var brightness = 0.2;
// eslint-disable-next-line new-cap
return Highcharts.Color(color).brighten(brightness).get();
};
// Load categories, mainly to populate the pie chart.
vm.load = function(begin, end) {
vm.config.loading = true;
Category.query({
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
}, function(data) {
var expenses = [];
var revenues = [];
var expenseColor = vm.brightenColor(vm.expenseColor);
var revenueColor = vm.brightenColor(vm.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].
vm.config.series[1].data = revenues.concat(expenses);
vm.config.loading = false;
});
};
/*
* Get account balance.
*/
vm.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.
vm.config.subtitle = {
text: 'Balance: ' + balance.balance
};
vm.config.series[0].data = [{
name: 'Revenues',
y: balance.revenues,
color: vm.revenueColor
}, {
name: 'Expenses',
y: -balance.expenses,
color: vm.expenseColor
}];
});
};
// Reload categories and account status on range selection.
vm.onRangeSelected = $rootScope.$on('rangeSelectedEvent', function(e, args) {
vm.load(args.begin, args.end);
vm.getBalance(args.begin, args.end);
});
$rootScope.$on('$destroy', function(){
vm.onRangeSelected = angular.noop();
});
}
)
/*
* Controller for the sold chart.
*/
.controller('SoldChartController', function($rootScope, $scope, OHLC) {
var vm = this;
// Configure chart for operations.
vm.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
};
vm.loadSolds = function() {
vm.config.loading = true;
OHLC.query({}, function(data) {
vm.config.series[0].data = [];
angular.forEach(data, function(operation) {
vm.config.series[0].data.push([
moment.utc(operation.operation_date).valueOf(),
operation.open, operation.high, operation.low, operation.close
]);
});
$scope.$emit('rangeSelectedEvent', {
begin: vm.config.xAxis.currentMin,
end: vm.config.xAxis.currentMax
});
vm.config.loading = false;
});
};
// Reload solds when an operation is saved.
vm.onOperationSaved = $rootScope.$on('operationSavedEvent', function() {
vm.loadSolds();
});
// Reload solds when an operation is deleted.
vm.onOperationDeleted = $rootScope.$on('operationDeletedEvent', function() {
vm.loadSolds();
});
// Update authorized overdraft on account loading.
vm.onAccountLoaded = $rootScope.$on('accountLoadedEvent', function(e, account) {
vm.config.yAxis.plotLines[1].value = account.authorized_overdraft;
});
$rootScope.$on('$destroy', function() {
vm.onOperationSaved = angular.noop();
vm.onOperationDeleted = angular.noop();
vm.onAccountLoaded = angular.noop();
});
// Select beginning and end of month.
vm.loadSolds();
})
/*
* Controller for the operations.
*/
.controller('OperationController', function($rootScope, $scope, $routeParams, $ngBootbox, Notification, Account, Operation) {
var vm = this;
// List of operations.
vm.operations = [];
/*
* Add an empty operation.
*/
vm.add = function() {
var operation = new Operation({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
vm.operations.splice(0, 0, operation);
};
/*
* Load operations.
*/
vm.load = function(begin, end) {
vm.operations = Operation.query({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId,
begin: begin.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD')
});
};
/*
* Cancel edition.
*/
vm.cancelEdit = function(operation, rowform, $index) {
if (operation.id) {
rowform.$cancel();
} else {
vm.operations.splice($index, 1);
}
};
/*
* Toggle pointed indicator for an operation.
*/
vm.togglePointed = function(operation, rowform) {
operation.pointed = !operation.pointed;
// Save operation if not editing it.
if (!rowform.$visible) {
vm.save(operation);
}
};
/*
* Toggle cancel indicator for an operation.
*/
vm.toggleCanceled = function(operation) {
operation.canceled = !operation.canceled;
vm.save(operation);
};
/*
* Save an operation and emit operationSavedEvent.
*/
vm.save = function($data, $index) {
// Check if $data is already a resource.
var operation;
if ($data.$save) {
operation = $data;
} else {
operation = vm.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.
*/
vm.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.
vm.operation.splice($index, 1);
$scope.$emit('operationDeletedEvent', operation);
});
}
}
);
};
vm.account = Account.get({
id: $routeParams.accountId
});
/*
* Reload operations on rangeSelectedEvent.
*/
vm.onRangeSelected = $rootScope.$on('rangeSelectedEvent', function(e, args) {
vm.load(args.begin, args.end);
});
$rootScope.$on('$destroy', function() {
vm.onRangeSelected = angular.noop;
});
})
.directive('operationFormDialog', function($log, $ngBootbox) {
return {
restrict: 'A',
scope: {
operation: '=ngModel'
},
link: function(scope, element) {
var title = 'Operation';
if (scope.operation && scope.operation.id) {
title = title + ' #' + scope.operation.id;
}
scope.form = {};
element.on('click', function() {
scope.data = {};
angular.copy(scope.operation, scope.data);
// Open dialog with form.
$ngBootbox.customDialog({
scope: scope,
title: title,
templateUrl: 'views/operation.form.tmpl.html',
onEscape: true,
buttons: {
save: {
label: 'Save',
className: 'btn-success',
callback: function() {
// Validate form
$log.log(scope.form);
// Save operation
$log.log(scope.operation);
// TODO Alexis Lahouze 2016-05-24 Save operation, handle return.
return false;
}
},
cancel: {
label: 'Cancel',
className: 'btn-default',
callback: true
}
}
});
});
}
};
});
module.exports = operationModule;

141
src/js/scheduler.js Normal file
View File

@ -0,0 +1,141 @@
// vim: set tw=80 ts=4 sw=4 sts=4:
/*
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 <http://www.gnu.org/licenses/>.
*/
/* jshint node: true */
'use strict';
var angular = require('angular');
var ngMessages = require('angular-messages'),
ngUiNotification = require('angular-ui-notification'),
ngBootbox = require('ngbootbox'),
ngStrap = require('angular-strap'),
ngXEditable = require('angular-xeditable');
// Note: ngBootbox seems to have no module.exports.
ngBootbox = 'ngBootbox';
// Note: angular-xeditable seems to have no module.exports.
ngXEditable = 'xeditable';
var schedulerModule = angular.module('accountant.scheduler', [
ngMessages,
ngUiNotification,
ngBootbox,
ngXEditable,
ngStrap
])
.config(function($resourceProvider) {
// Keep trailing slashes to avoid redirect by flask..
$resourceProvider.defaults.stripTrailingSlashes = false;
})
.factory('ScheduledOperation', function($resource) {
return $resource(
'/api/scheduled_operation/:id', {
id: '@id'
}
);
})
.controller('SchedulerController', function($rootScope, $routeParams, $ngBootbox, Notification, ScheduledOperation) {
var vm = this;
// Operation store.
vm.operations = [];
/*
* Add a new operation at the beginning of th array.
*/
vm.add = function() {
var operation = new ScheduledOperation({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
// Insert new operation at the beginning of the array.
vm.operations.splice(0, 0, operation);
};
/*
* Load operations.
*/
vm.load = function() {
vm.operations = ScheduledOperation.query({
// eslint-disable-next-line camelcase
account_id: $routeParams.accountId
});
};
/*
* Save operation.
*/
vm.save = function($data, $index) {
var operation;
if ($data.$save) {
operation = $data;
} else {
operation = vm.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.
*/
vm.cancelEdit = function(operation, rowform, $index) {
if (operation.id) {
rowform.$cancel();
} else {
vm.operations.splice($index, 1);
}
};
/*
* Delete operation.
*/
vm.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.
vm.operations.splice($index, 1);
});
}
}
);
};
// Load operations on controller initialization.
vm.load();
})
;
module.exports = schedulerModule;

17
src/less/main.less Normal file
View File

@ -0,0 +1,17 @@
@import '~bootstrap/less/bootstrap';
@import '~font-awesome/less/font-awesome';
@import '~angular-ui-notification/src/angular-ui-notification';
@import (inline) '~bootstrap-additions/dist/bootstrap-additions.css';
@import (inline) '~angular-xeditable/dist/css/xeditable.css';
.italic {
font-style: italic;
}
.stroke {
text-decoration: line-through;
}

View File

@ -0,0 +1,50 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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 <http://www.gnu.org/licenses/>.
-->
<form class="form-horizontal" ng-submit="submitForm()" name="form" novalidate>
<div class="form-group" ng-class="{ 'has-error' : form.name.$invalid && !form.name.$pristine }">
<label class="col-sm-4 control-label" for="name">Account name</label>
<div class="col-sm-8">
<input class="form-control" id="name" name="name"
ng-model="data.name"
type="text" placeholder="Account name"
required />
<div class="help-block" ng-messages="form.name.$error" ng-if="form.name.$invalid">
<p ng-message="required">The account name is required.</p>
</div>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error' : form.authorized_overdraft.$invalid && !form.authorized_overdraft.$pristine }">
<label class="col-sm-4 control-label" for="authorized-overdraft">Authorized overdraft</label>
<div class="col-sm-8">
<div class="input-group">
<div class="input-group-addon">-</div>
<input class="form-control" id="authorized-overdraft"
name="authorized_overdraft"
ng-model="data.authorized_overdraft"
type="number" placeholder="Authorized overdraft"
required min="0"/>
<div class="input-group-addon">.00€</div>
</div>
<div class="help-block" ng-messages="form.authorized_overdraft.$error" ng-if="form.authorized_overdraft.$invalid">
<p ng-message="required">The authorized overdraft is required.</p>
<p ng-message="min">The authorized overdraft must be equal or greater than 0.</p>
</div>
</div>
</div>
</form>

94
src/views/accounts.html Normal file
View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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 <http://www.gnu.org/licenses/>.
-->
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th>Nom du compte</th>
<th class="col-md-1">Solde courant</th>
<th class="col-md-1">Solde pointé</th>
<th class="col-md-1">Découvert autorisé</th>
<th class="col-md-1">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="5">
<button class="btn btn-success" account-form-dialog>
Ajouter
</button>
</td>
</tr>
<tr id="{{ account.id }}"
class="form-inline" ng-class="rowClass(account)"
ng-repeat="account in accountsCtrl.accounts | orderBy:'name'" ng-init="account.getSolds()">
<td>
<a href="#!/account/{{ account.id }}/operations">{{ account.name }}</a>
</td>
<td>
<span ng-class="accountsCtrl.valueClass(account, account.solds.current)">
{{ account.solds.current | currency : "€" }}
</span>
</td>
<td>
<span ng-class="accountsCtrl.valueClass(account, account.solds.pointed)">
{{ account.solds.pointed | currency : "€" }}
</span>
</td>
<td>
{{ account.authorized_overdraft | currency : "€" }}
</td>
<td>
<div class="btn-group btn-group-xs">
<!-- Edit account. -->
<button type="button" class="btn btn-success"
account-form-dialog ng-model="accountsCtrl.account">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Cancel account edition. -->
<button type="button" class="btn btn-default"
ng-click="accountsCtrl.cancelEdit(rowform, account, $index)">
<span class="fa fa-times"></span>
</button>
<!-- Delete account, with confirm. -->
<button type="button" class="btn btn-default"
ng-click="accountsCtrl.delete(account, $index)">
<span class="fa fa-trash-o"></span>
</button>
<!-- Open account scheduler. -->
<a class="btn btn-default"
ng-if="account.id"
href="#!/account/{{ account.id }}/scheduler">
<span class="fa fa-clock-o"></span>
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>

18
src/views/login.tmpl.html Normal file
View File

@ -0,0 +1,18 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<form class="form-horizontal">
<div class="form-group">
<label for="email" class="col-sm-4 control-label">Adresse email</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" ng-model="email"
placeholder="Nom d'utilisateur">
</div>
</div>
<div class="form-group">
<label for="password" class="col-sm-4 control-label">Mot de passe</label>
<div class="col-sm-8">
<input type="password" class="form-control" id="password" ng-model="password" placeholder="Mot de passe">
</div>
</div>
</form>

View File

@ -0,0 +1,58 @@
<!--
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 <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!-- kate: space-indent on; indent-width 2; mixedindent off; -->
<form class="form-horizontal" role="form" name="form">
<div class="form-group">
<label class="col-sm-4 control-label" for="operation-date">Date</label>
<div class="col-sm-8">
<input class="form-control" id="operation-date" name="operation_date" ng-model="data.operation_date"
type="text" bs-datepicker data-date-format="yyyy-MM-dd"
placeholder="Operation date">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="label">Label</label>
<div class="col-sm-8">
<input class="form-control" id="label" name="label"
ng-model="data.label"
type="text" placeholder="Label">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="value">Montant</label>
<div class="col-sm-8">
<input class="form-control" id="value" name="value"
ng-model="data.value"
type="number" placeholder="Value">
</input>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="category">Catégorie</label>
<div class="col-sm-8">
<input class="form-control" id="category" name="category"
ng-model="data.category"
type="text" placeholder="Category">
</input>
</div>
</div>
</form>

120
src/views/operations.html Normal file
View File

@ -0,0 +1,120 @@
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<!--
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 <http://www.gnu.org/licenses/>.
-->
<div>
<!-- Chart row -->
<div class="row">
<!-- Sold evolution chart placeholder -->
<div class="col-md-8"
ng-controller="SoldChartController as soldChartCtrl">
<!--<highchart id="sold-chart"
config="soldChartCtrl.config"></highchart>-->
</div>
<!-- Category piechart -->
<div class="col-md-4"
ng-controller="CategoryChartController as categoryChartCtrl">
<!--<highchart id="categories-chart"
config="categoryChartCtrl.config"></highchart>-->
</div>
</div>
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="col-md-1">Date d'op.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th class="col-md-1">Montant</th>
<th class="col-md-1">Solde</th>
<th class="col-md-2">Cat&eacute;gorie</th>
<th class="col-md-2">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6">
<button class="btn btn-success" ng-click="operationsCtrl.add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-class="{stroke: operation.canceled, italic: !operation.confirmed,
warning: operation.sold < 0, danger: operation.sold < operationsCtrl.account.authorized_overdraft}"
ng-repeat="operation in operationsCtrl.operations | orderBy:['-operation_date', '-value', 'label']">
<td>
{{ operation.operation_date | date:"yyyy-MM-dd" }}
</td>
<td>
{{ operation.label }}
</td>
<td>
{{ operation.value | currency:"€" }}
</td>
<td ng-class="{'text-warning': operation.sold < 0, 'text-danger':
operation.sold < operationsCtrl.account.authorized_overdraft}">
{{ operation.sold | currency:"€" }}
</td>
<td>
{{ operation.category }}
</td>
<td>
<div class="btn-group btn-group-xs">
<!-- Edit operation, for non-canceled operation. -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled"
operation-form-dialog ng-model="operation" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Toggle pointed operation, for non-canceled operations. -->
<button type="button" class="btn btn-default"
ng-if="!operation.canceled"
ng-click="operationsCtrl.togglePointed(operation, rowform)"
ng-class="{active: operation.pointed}" title="point">
<span ng-class="{'fa fa-check-square-o': operation.pointed, 'fa fa-square-o': !operation.pointed}"></span>
</button>
<!-- Toggle canceled operation. -->
<button type="button" class="btn btn-default"
ng-click="operationsCtrl.toggleCanceled(operation)"
ng-if="operation.scheduled_operation_id && !rowform.$visible"
ng-class="{active: operation.canceled}" title="cancel">
<span class="fa fa-remove"></span>
</button>
<!-- Delete operation, with confirm. -->
<button type="button" class="btn btn-default"
ng-if="operation.id && !operation.scheduled_operation_id"
ng-click="operationsCtrl.delete(operation, $index)">
<span class="fa fa-trash-o"></span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>

142
src/views/scheduler.html Normal file
View File

@ -0,0 +1,142 @@
<!--
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 <http://www.gnu.org/licenses/>.
-->
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
<div class="row">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th class="col-md-1">Date de d&eacute;but</th>
<th class="col-md-1">Date de fin</th>
<th class="col-md-1">Jour</th>
<th class="col-md-1">Fr&eacute;q.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th class="col-md-1">Montant</th>
<th class="col-md-2">Cat&eacute;gorie</th>
<th class="col-md-1">Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8">
<button class="btn btn-success" ng-click="schedulerCtrl.add()">
Ajouter
</button>
</td>
</tr>
<tr id="{{ operation.id }}" class="form-inline"
ng-repeat="operation in schedulerCtrl.operations">
<td class="col-md-1">
<span editable-text="operation.start_date"
e-style="width: 100%"
e-bs-datepicker e-data-date-format="yyyy-MM-dd"
e-name="start_date" e-form="rowform" e-required>
{{ operation.start_date | date: "yyyy-MM-dd" }}
</span>
</td>
<td>
<span editable-text="operation.stop_date"
e-style="width: 100%"
e-bs-datepicker e-data-date-format="yyyy-MM-dd"
e-name="stop_date" e-form="rowform" e-required>
{{ operation.stop_date | date: "yyyy-MM-dd" }}
</span>
</td>
<td>
<span editable-number="operation.day"
e-style="width: 100%"
e-name="day" e-form="rowform" e-required>
{{ operation.day }}
</span>
</td>
<td>
<span editable-number="operation.frequency"
e-style="width: 100%"
e-name="frequency" e-form="rowform" e-required>
{{ operation.frequency }}
</span>
</td>
<td>
<span editable-text="operation.label"
e-style="width: 100%"
e-placeholder="Libellé de l'opération"
e-name="label" e-form="rowform" e-required>
{{ operation.label }}
</span>
</td>
<td>
<span editable-number="operation.value"
e-style="width: 100%"
e-name="value" e-form="rowform" e-required>
{{ operation.value | currency : "€" }}
</span>
</td>
<td>
<span editable-text="operation.category"
e-style="width: 100%"
e-name="category" e-form="rowform">
{{ operation.category }}
</span>
</td>
<td>
<form editable-form name="rowform"
onbeforesave="schedulerCtrl.save($data, $index)"
shown="!operation.id">
<div class="btn-group btn-group-xs">
<!-- Save current operation -->
<button type="submit" class="btn btn-success"
ng-if="rowform.$visible" title="Save">
<span class="fa fa-floppy-o"></span>
</button>
<!-- Edit operation. -->
<button type="button" class="btn btn-default"
ng-if="!rowform.$visible"
ng-click="rowform.$show()" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Cancel edit. -->
<button type="button" class="btn btn-default"
ng-if="rowform.$visible"
ng-click="schedulerCtrl.cancelEdit(operation, rowform, $index)"
title="Cancel">
<span class="fa fa-times"></span>
</button>
<!-- Remove operation. -->
<button type="button" class="btn btn-default"
ng-if="operation.id"
ng-click="schedulerCtrl.delete(operation, $index)"
title="remove">
<span class="fa fa-trash"></span>
</button>
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>