Compare commits
No commits in common. "feature/material" and "master" have entirely different histories.
feature/ma
...
master
147
.gitignore
vendored
147
.gitignore
vendored
@ -1,19 +1,100 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### Vim ###
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
.vimrc
|
||||
.vimtags
|
||||
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
|
||||
### grunt ###
|
||||
# Grunt usually compiles files inside this directory
|
||||
dist/
|
||||
|
||||
# Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
|
||||
.tmp/
|
||||
|
||||
|
||||
### Bower ###
|
||||
bower_components
|
||||
.bower-cache
|
||||
.bower-registry
|
||||
.bower-tmp
|
||||
|
||||
# Created by https://www.gitignore.io/api/vim,node
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
@ -21,64 +102,20 @@ lib-cov
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# Yarn Lock file
|
||||
yarn.lock
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
# Dependency directory
|
||||
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
|
||||
### Vim ###
|
||||
# swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
# session
|
||||
Session.vim
|
||||
# temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# auto-generated tag files
|
||||
tags
|
||||
|
||||
# End of https://www.gitignore.io/api/vim,node
|
||||
|
||||
/build
|
||||
### Local files ###
|
||||
config.cfg
|
||||
/flask_session
|
||||
|
35
.jscsrc
35
.jscsrc
@ -1,35 +0,0 @@
|
||||
{
|
||||
"preset": "google",
|
||||
"fileExtensions": [".js", "jscs"],
|
||||
|
||||
"requireSemicolons": true,
|
||||
"requireParenthesesAroundIIFE": true,
|
||||
"maximumLineLength": 120,
|
||||
"validateLineBreaks": "LF",
|
||||
"validateIndentation": 4,
|
||||
"disallowTrailingComma": true,
|
||||
"disallowUnusedParams": true,
|
||||
|
||||
"disallowSpacesInsideObjectBrackets": null,
|
||||
"disallowImplicitTypeConversion": ["string"],
|
||||
|
||||
"safeContextKeyword": "_this",
|
||||
|
||||
"jsDoc": {
|
||||
"checkAnnotations": "closurecompiler",
|
||||
"checkParamNames": true,
|
||||
"requireParamTypes": true,
|
||||
"checkRedundantParams": true,
|
||||
"checkReturnTypes": true,
|
||||
"checkRedundantReturns": true,
|
||||
"requireReturnTypes": true,
|
||||
"checkTypes": "capitalizedNativeCase",
|
||||
"checkRedundantAccess": true,
|
||||
"requireNewlineAfterDescription": true
|
||||
},
|
||||
|
||||
"excludeFiles": [
|
||||
"test/data/**",
|
||||
"patterns/*"
|
||||
]
|
||||
}
|
21
.jshintrc
Normal file
21
.jshintrc
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"bitwise": true,
|
||||
"browser": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"esnext": true,
|
||||
"latedef": true,
|
||||
"noarg": true,
|
||||
"node": true,
|
||||
"strict": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"quotmark": "single",
|
||||
"indent": 2,
|
||||
"jquery": true,
|
||||
"globals": {
|
||||
"angular": false,
|
||||
"moment": false,
|
||||
"Highcharts": false
|
||||
}
|
||||
}
|
75
Gruntfile.js
Normal file
75
Gruntfile.js
Normal file
@ -0,0 +1,75 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = function(grunt) {
|
||||
require('load-grunt-tasks')(grunt);
|
||||
require('time-grunt')(grunt);
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
accountant: {
|
||||
frontend: {
|
||||
app: require('./bower.json'),
|
||||
src: 'accountant-ui',
|
||||
dist: 'accountant-ui_dist'
|
||||
}
|
||||
},
|
||||
|
||||
config: {
|
||||
src: 'grunt-config/*.js'
|
||||
},
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %>\n'+
|
||||
'* Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %> */\n',
|
||||
};
|
||||
|
||||
var configs = require('load-grunt-configs')(grunt, options);
|
||||
grunt.initConfig(configs);
|
||||
|
||||
grunt.registerTask('dependencies', [
|
||||
'shell:npm_install',
|
||||
'shell:bower_install',
|
||||
'shell:pip_install',
|
||||
'wiredep:app',
|
||||
]);
|
||||
|
||||
grunt.registerTask('pydev', [
|
||||
'newer:flake8'
|
||||
]);
|
||||
|
||||
grunt.registerTask('jsdev', [
|
||||
'newer:jshint',
|
||||
'newer:jscs'
|
||||
]);
|
||||
|
||||
grunt.registerTask('htmldev', [
|
||||
'newer:htmllint'
|
||||
]);
|
||||
|
||||
grunt.registerTask('dev', [
|
||||
'dependencies',
|
||||
'pydev',
|
||||
'jsdev',
|
||||
'htmldev'
|
||||
]);
|
||||
|
||||
grunt.registerTask('serve', [
|
||||
'dev',
|
||||
'bgShell:runserver',
|
||||
'connect:livereload',
|
||||
'watch'
|
||||
]);
|
||||
|
||||
grunt.registerTask('dist', [
|
||||
'wiredep',
|
||||
'clean:dist',
|
||||
'useminPrepare',
|
||||
'copy:dist',
|
||||
'copy:styles',
|
||||
'cssmin:generated',
|
||||
'concat:generated',
|
||||
'ngAnnotate',
|
||||
'uglify:generated',
|
||||
'filerev',
|
||||
'usemin'
|
||||
]);
|
||||
};
|
7
accountant-ui/css/main.css
Normal file
7
accountant-ui/css/main.css
Normal file
@ -0,0 +1,7 @@
|
||||
.italic {
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
.stroke {
|
||||
text-decoration: line-through
|
||||
}
|
96
accountant-ui/index.html
Normal file
96
accountant-ui/index.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!--
|
||||
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: -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<!-- Title -->
|
||||
<title>Accountant</title>
|
||||
|
||||
<!-- build:css(accountant-ui) css/vendor.css -->
|
||||
<!-- bower:css -->
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
|
||||
<link rel="stylesheet" href="bower_components/bootstrap-additions/dist/bootstrap-additions.css" />
|
||||
<link rel="stylesheet" href="bower_components/angular-xeditable/dist/css/xeditable.css" />
|
||||
<link rel="stylesheet" href="bower_components/angular-ui-notification/dist/angular-ui-notification.css" />
|
||||
<link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.css" />
|
||||
<!-- endbower -->
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- Custom styles -->
|
||||
<!-- build:css(.tmp) css/main.css -->
|
||||
<!-- include: "type": "css", "files": "css/*.css" -->
|
||||
<link href="css/main.css" rel="stylesheet" type="text/css">
|
||||
<!-- /include -->
|
||||
<!-- endbuild -->
|
||||
</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"> Accountant</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid" ng-controller="MainController">
|
||||
<div ng-view></div>
|
||||
</div>
|
||||
|
||||
<!-- build:js(accountant-ui) js/vendor.js -->
|
||||
<!-- bower:js -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
<script src="bower_components/moment/moment.js"></script>
|
||||
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="bower_components/angular-strap/dist/angular-strap.js"></script>
|
||||
<script src="bower_components/angular-strap/dist/angular-strap.tpl.js"></script>
|
||||
<script src="bower_components/angular-xeditable/dist/js/xeditable.js"></script>
|
||||
<script src="bower_components/angular-ui-notification/dist/angular-ui-notification.js"></script>
|
||||
<script src="bower_components/highcharts-ng/dist/highcharts-ng.js"></script>
|
||||
<script src="bower_components/highstock-release/highstock.js"></script>
|
||||
<script src="bower_components/highstock-release/highcharts-more.js"></script>
|
||||
<script src="bower_components/highstock-release/modules/exporting.js"></script>
|
||||
<script src="bower_components/angular-http-auth/src/http-auth-interceptor.js"></script>
|
||||
<script src="bower_components/meanie-angular-storage/release/meanie-angular-storage.js"></script>
|
||||
<script src="bower_components/bootbox/bootbox.js"></script>
|
||||
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
|
||||
<script src="bower_components/ngBootbox/dist/ngBootbox.js"></script>
|
||||
<!-- endbower -->
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- Custom Javascript libraries -->
|
||||
<!-- build:js({.tmp,accountant-ui}) js/scripts.js -->
|
||||
<!-- include: "type": "js", "files": "js/*.js" -->
|
||||
<script src="js/accounts.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/operations.js"></script>
|
||||
<script src="js/scheduler.js"></script>
|
||||
<!-- /include -->
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
</html>
|
||||
|
211
accountant-ui/js/accounts.js
Normal file
211
accountant-ui/js/accounts.js
Normal file
@ -0,0 +1,211 @@
|
||||
/*
|
||||
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:
|
||||
'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();
|
||||
|
||||
}])
|
||||
|
||||
.directive(
|
||||
'accountFormDialog', function($ngBootbox) {
|
||||
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 = {};
|
||||
|
||||
element.on('click', function() {
|
||||
//angular.copy(scope.account, scope.form);
|
||||
|
||||
// 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: function() {
|
||||
// Validate form
|
||||
console.log(scope.form);
|
||||
|
||||
// Save account
|
||||
console.log(scope.account);
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
label: 'Cancel',
|
||||
className: 'btn-default',
|
||||
callback: true
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
154
accountant-ui/js/app.js
Normal file
154
accountant-ui/js/app.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// 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);
|
||||
}])
|
||||
|
||||
;
|
466
accountant-ui/js/operations.js
Normal file
466
accountant-ui/js/operations.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// 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);
|
||||
});
|
||||
}]);
|
120
accountant-ui/js/scheduler.js
Normal file
120
accountant-ui/js/scheduler.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// 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();
|
||||
}]);
|
22
accountant-ui/views/account.form.tmpl.html
Normal file
22
accountant-ui/views/account.form.tmpl.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
||||
<form class="form-horizontal" role="form" name="form">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label" for="name">Account name</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="name" class="form-control"
|
||||
name="name" ng-model="account.name"
|
||||
placeholder="Account name" type="text">
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label" for="authorized-overdraft">Authorized overdraft</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="authorized-overdraft" class="form-control" type="number"
|
||||
name="authorized_overdraft" ng-model="account.authorized_overdraft"
|
||||
placeholder="Authorized overdraft">
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
92
accountant-ui/views/accounts.html
Normal file
92
accountant-ui/views/accounts.html
Normal file
@ -0,0 +1,92 @@
|
||||
<!--
|
||||
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>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 btn-success"
|
||||
ng-click="add()">Ajouter</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="{{ account.id }}"
|
||||
class="form-inline" ng-class="rowClass(account)"
|
||||
ng-repeat="account in accounts | orderBy:'name'" ng-init="account.getSolds()">
|
||||
<td>
|
||||
<a href="#/account/{{ account.id }}/operations">{{ account.name }}</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span ng-class="valueClass(account, account.solds.current)">
|
||||
{{ account.solds.current | currency : "€" }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span ng-class="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="account">
|
||||
<span class="fa fa-pencil-square-o"></span>
|
||||
</button>
|
||||
|
||||
<!-- Cancel account edition. -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="cancelEdit(rowform, account, $index)">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
|
||||
<!-- Delete account, with confirm. -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="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
accountant-ui/views/login.tmpl.html
Normal file
18
accountant-ui/views/login.tmpl.html
Normal 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>
|
||||
|
152
accountant-ui/views/operations.html
Normal file
152
accountant-ui/views/operations.html
Normal file
@ -0,0 +1,152 @@
|
||||
<!--
|
||||
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>
|
||||
<!-- Chart row -->
|
||||
<div class="row">
|
||||
<!-- Sold evolution chart placeholder -->
|
||||
<div class="col-md-8" ng-controller="SoldChartController">
|
||||
<highchart id="sold-chart" config="config"></highchart>
|
||||
</div>
|
||||
|
||||
<!-- Category piechart -->
|
||||
<div class="col-md-4" ng-controller="CategoryChartController">
|
||||
<highchart id="categories-chart" config="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é de l'opération</th>
|
||||
<th class="col-md-1">Montant</th>
|
||||
<th class="col-md-1">Solde</th>
|
||||
<th class="col-md-2">Catégorie</th>
|
||||
<th class="col-md-2">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<button class="btn btn-success" ng-click="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 < account.authorized_overdraft}"
|
||||
ng-repeat="operation in operations | orderBy:['-operation_date', '-value', 'label']">
|
||||
<td>
|
||||
<span editable-text="operation.operation_date"
|
||||
e-data-date-format="yyyy-MM-dd" e-bs-datepicker
|
||||
e-timezone="UTC"
|
||||
e-class="input-sm" e-style="width: 100%"
|
||||
e-name="operation_date" e-form="rowform" e-required>
|
||||
{{ operation.operation_date | date:"yyyy-MM-dd" }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span editable-text="operation.label"
|
||||
e-placeholder="Libellé de l'opération"
|
||||
e-class="input-sm" e-style="width: 100%"
|
||||
e-name="label" e-form="rowform" e-required>
|
||||
{{ operation.label }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span editable-number="operation.value"
|
||||
e-class="input-sm" e-style="width: 100%"
|
||||
e-name="value" e-form="rowform" e-required>
|
||||
{{ operation.value | currency:"€" }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td ng-class="{'text-warning': operation.sold < 0, 'text-danger': operation.sold < account.authorized_overdraft}">
|
||||
{{ operation.sold | currency:"€" }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span editable-text="operation.category"
|
||||
e-placeholder="Catégorie"
|
||||
e-class="input-sm" e-style="width: 100%"
|
||||
e-name="category" e-form="rowform" e-required>
|
||||
{{ operation.category }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<form editable-form name="rowform"
|
||||
onbeforesave="save($data, $index)"
|
||||
shown="!operation.id">
|
||||
<div class="btn-group btn-group-xs">
|
||||
<!-- Save current operation, for editing and non-confirmed non-canceled operation. -->
|
||||
<button type="submit" class="btn btn-success"
|
||||
ng-if="!operation.canceled && (!operation.confirmed || rowform.$visible)"
|
||||
title="Save">
|
||||
<span class="fa fa-floppy-o"></span>
|
||||
</button>
|
||||
|
||||
<!-- Edit operation, for non-canceled and non-editing operation -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-if="!operation.canceled && !rowform.$visible"
|
||||
ng-click="rowform.$show()" title="edit">
|
||||
<span class="fa fa-pencil-square-o"></span>
|
||||
</button>
|
||||
|
||||
<!-- Cancel edition, for editing operation. -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-if="rowform.$visible"
|
||||
ng-click="cancelEdit(operation, rowform)">
|
||||
<span class="fa fa-times"></span>
|
||||
</button>
|
||||
|
||||
<!-- Toggle pointed operation, for non-canceled operations. -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-if="!operation.canceled"
|
||||
ng-click="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, for non-editing operations. -->
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="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="delete(operation, $index)">
|
||||
<span class="fa fa-trash-o"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
142
accountant-ui/views/scheduler.html
Normal file
142
accountant-ui/views/scheduler.html
Normal 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ébut</th>
|
||||
<th class="col-md-1">Date de fin</th>
|
||||
<th class="col-md-1">Jour</th>
|
||||
<th class="col-md-1">Fréq.</th>
|
||||
<th>Libellé de l'opération</th>
|
||||
<th class="col-md-1">Montant</th>
|
||||
<th class="col-md-2">Catégorie</th>
|
||||
<th class="col-md-1">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="8">
|
||||
<button class="btn btn-success" ng-click="add()">
|
||||
Ajouter
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr id="{{ operation.id }}" class="form-inline"
|
||||
ng-repeat="operation in 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="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="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="delete(operation, $index)"
|
||||
title="remove">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
54
bower.json
Normal file
54
bower.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "accountant",
|
||||
"version": "0.1.0",
|
||||
"authors": [
|
||||
"Alexis Lahouze <xals@lahouze.org>"
|
||||
],
|
||||
"license": "AGPL",
|
||||
"main": [
|
||||
"accountant-ui/index.html",
|
||||
"accountant-ui/js/app.js"
|
||||
],
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": "~2.2",
|
||||
"moment": "~2.12",
|
||||
"bootstrap": "~3.3.6",
|
||||
"bootstrap-additions": "~0.3.1",
|
||||
"angular": "~1.5",
|
||||
"angular-resource": "~1.5",
|
||||
"angular-route": "~1.5",
|
||||
"angular-strap": "~2.3.6",
|
||||
"angular-xeditable": "~0.1",
|
||||
"angular-ui-notification": "~0.2",
|
||||
"highcharts-ng": "~0.0.11",
|
||||
"highstock-release": "~4.2",
|
||||
"angular-http-auth": "~1.3",
|
||||
"meanie-angular-storage": "~1.1",
|
||||
"font-awesome": ">=4.5.0",
|
||||
"bootbox": "~4.4.0",
|
||||
"angular-bootstrap": "~1.3",
|
||||
"ngBootbox": "^0.1.3"
|
||||
},
|
||||
"overrides": {
|
||||
"bootstrap": {
|
||||
"main": [
|
||||
"less/bootstrap.less",
|
||||
"dist/css/bootstrap.css",
|
||||
"dist/js/bootstrap.js"
|
||||
]
|
||||
},
|
||||
"font-awesome": {
|
||||
"main": [
|
||||
"./css/font-awesome.css",
|
||||
"./fonts/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
7
grunt-config/bgShell.js
Normal file
7
grunt-config/bgShell.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
runserver: {
|
||||
cmd: 'python -m manage runserver -d -r',
|
||||
fail: true,
|
||||
bg: true
|
||||
}
|
||||
};
|
7
grunt-config/clean.js
Normal file
7
grunt-config/clean.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
dist: [
|
||||
'<%= accountant.frontend.dist %>'
|
||||
]
|
||||
};
|
40
grunt-config/connect.js
Normal file
40
grunt-config/connect.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
options: {
|
||||
port: 5001,
|
||||
hostname: 'localhost',
|
||||
base: '<%= accountant.frontend.src %>',
|
||||
apiUrl: 'http://localhost:5000/api/',
|
||||
swaggerUiUrl: 'http://localhost:5000/swaggerui/',
|
||||
livereload: 1337,
|
||||
},
|
||||
proxies: [{
|
||||
context: '/api',
|
||||
host: '127.0.0.1',
|
||||
port: 5000,
|
||||
https: false
|
||||
}, {
|
||||
contect: '/swaggerui',
|
||||
host: '127.0.0.1',
|
||||
port: 5000,
|
||||
https: false
|
||||
}],
|
||||
livereload: {
|
||||
options: {
|
||||
//open: true,
|
||||
middleware: function(connect, options, middlewares) {
|
||||
var connectLogger = require('connect-logger');
|
||||
var connectProxy = require('connect-proxy-layer');
|
||||
var apiProxy = connectProxy(options.apiUrl);
|
||||
var swaggerUiProxy = connectProxy(options.swaggerUiUrl);
|
||||
|
||||
return [
|
||||
connectLogger(),
|
||||
connect().use('/api', apiProxy),
|
||||
connect().use('/swaggerUi', swaggerUiProxy),
|
||||
].concat(middlewares);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
22
grunt-config/copy.js
Normal file
22
grunt-config/copy.js
Normal file
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
dist: {
|
||||
files: [{
|
||||
expand: true,
|
||||
dot: true,
|
||||
cwd: '<%= accountant.frontend.src %>',
|
||||
dest: '<%= accountant.frontend.dist %>',
|
||||
src :[
|
||||
'*.html',
|
||||
'views/*.html',
|
||||
]
|
||||
}]
|
||||
},
|
||||
styles: {
|
||||
expand: true,
|
||||
cwd: '<%= accountant.frontend.src %>/css',
|
||||
dest: '.tmp/css',
|
||||
src: '{,*/}*.css'
|
||||
}
|
||||
};
|
17
grunt-config/filerev.js
Normal file
17
grunt-config/filerev.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
options: {
|
||||
encoding: 'utf-8',
|
||||
algorithm: 'md5',
|
||||
length: 8
|
||||
},
|
||||
dist: {
|
||||
src: [
|
||||
'<%= accountant.frontend.dist %>/css/*.css',
|
||||
'<%= accountant.frontend.dist %>/js/*.js',
|
||||
'!<%= accountant.frontend.dist %>/css/*.map.css',
|
||||
'!<%= accountant.frontend.dist %>/js/*.map.js'
|
||||
]
|
||||
},
|
||||
};
|
7
grunt-config/flake8.js
Normal file
7
grunt-config/flake8.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
src: [
|
||||
'accountant/**/*.py'
|
||||
]
|
||||
};
|
16
grunt-config/htmllint.js
Normal file
16
grunt-config/htmllint.js
Normal file
@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
frontend: {
|
||||
options: {
|
||||
'attr-name-style': 'dash',
|
||||
'attr-req-value': false,
|
||||
'id-class-ignore-regex': '{{.*?}}',
|
||||
'id-class-style': 'dash',
|
||||
'indent-style': 'spaces',
|
||||
'indent-width': 2
|
||||
},
|
||||
src: [
|
||||
'<%= accountant.frontend.src %>/*.html',
|
||||
'<%= accountant.frontend.src %>/views/*.html'
|
||||
]
|
||||
}
|
||||
};
|
13
grunt-config/includeSource.js
Normal file
13
grunt-config/includeSource.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
options: {
|
||||
basePath: '<%= accountant.frontend.src %>',
|
||||
baseUrl: ''
|
||||
},
|
||||
index: {
|
||||
files: {
|
||||
'<%= accountant.frontend.src %>/index.html': '<%= accountant.frontend.src %>/index.html'
|
||||
}
|
||||
}
|
||||
};
|
13
grunt-config/jscs.js
Normal file
13
grunt-config/jscs.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
options: {
|
||||
config: '.jscsrc',
|
||||
verbose: true
|
||||
},
|
||||
frontend_js: [
|
||||
'<%= accountant.frontend.src %>/js/*.js'
|
||||
],
|
||||
toolchain: [
|
||||
'Gruntfile.js',
|
||||
'grunt-config/*.js'
|
||||
]
|
||||
};
|
13
grunt-config/jshint.js
Normal file
13
grunt-config/jshint.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
options: {
|
||||
jshintrc: '.jshintrc',
|
||||
reporter: require('jshint-stylish')
|
||||
},
|
||||
frontend_js: [
|
||||
'<%= accountant.frontend.src %>/js/*.js'
|
||||
],
|
||||
toolchain: [
|
||||
'Gruntfile.js',
|
||||
'grunt-config/*.js'
|
||||
]
|
||||
};
|
12
grunt-config/ngAnnotate.js
Normal file
12
grunt-config/ngAnnotate.js
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
dist: {
|
||||
files: [{
|
||||
expand: true,
|
||||
cwd: '.tmp/concat/js',
|
||||
src: '*.js',
|
||||
dest: '.tmp/concat/js'
|
||||
}]
|
||||
}
|
||||
};
|
11
grunt-config/shell.js
Normal file
11
grunt-config/shell.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
npm_install: {
|
||||
command: 'npm install'
|
||||
},
|
||||
bower_install: {
|
||||
command: 'bower install'
|
||||
},
|
||||
pip_install: {
|
||||
command: 'pip install --upgrade --requirement requirements.txt'
|
||||
}
|
||||
};
|
16
grunt-config/usemin.js
Normal file
16
grunt-config/usemin.js
Normal file
@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
html: ['<%= accountant.frontend.dist %>/{,*/}*.html'],
|
||||
css: ['<%= accountant.frontend.dist %>/css/{,*/}*.css'],
|
||||
js: ['<%= accountant.frontend.dist %>/js/{,*/}*.js'],
|
||||
options: {
|
||||
assetsDir: [
|
||||
'<%= accountant.frontend.dist %>',
|
||||
'<%= accountant.frontend.dist %>/css',
|
||||
],
|
||||
patterns: {
|
||||
js: [[/(images\/[^''""]*\.(png|jpg|jpeg|gif|webp|svg))/g, 'Replacing references to images']]
|
||||
}
|
||||
}
|
||||
};
|
17
grunt-config/useminPrepare.js
Normal file
17
grunt-config/useminPrepare.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
html: '<%= accountant.frontend.src %>/index.html ',
|
||||
options: {
|
||||
dest: '<%= accountant.frontend.dist %>',
|
||||
flow: {
|
||||
html: {
|
||||
steps: {
|
||||
js: ['concat', 'uglify'],
|
||||
css: ['cssmin']
|
||||
},
|
||||
post: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
43
grunt-config/watch.js
Normal file
43
grunt-config/watch.js
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
bower: {
|
||||
files: 'bower.json',
|
||||
tasks: ['wiredep']
|
||||
},
|
||||
js: {
|
||||
files: [
|
||||
'<%= accountant.frontend.src %>/js/*.js %>',
|
||||
'grunt-config/*.js'
|
||||
],
|
||||
tasks: ['jsdev']
|
||||
},
|
||||
py: {
|
||||
files: 'accountant/**/*.py',
|
||||
tasks: ['pydev', 'bgShell:runserver']
|
||||
},
|
||||
html: {
|
||||
files: [
|
||||
'<%= accountant.frontend.src %>/*.html',
|
||||
'<%= accountant.frontend.src %>/views/*.html'
|
||||
],
|
||||
tasks: ['htmldev']
|
||||
},
|
||||
gruntfile: {
|
||||
files: ['Gruntfile.js', 'grunt-config/*.js']
|
||||
},
|
||||
livereload: {
|
||||
options: {
|
||||
livereload: '<%= connect.options.livereload %>'
|
||||
},
|
||||
files: [
|
||||
'<%= accountant.frontend.src %>/{,*/}*.html',
|
||||
'<%= accountant.frontend.src %>/js/*.js',
|
||||
'<%= accountant.frontend.src %>/css/*.css'
|
||||
]
|
||||
},
|
||||
requirements: {
|
||||
files: ['requirements.txt'],
|
||||
tasks: ['shell:pip_install']
|
||||
}
|
||||
};
|
6
grunt-config/wiredep.js
Normal file
6
grunt-config/wiredep.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
app: {
|
||||
src: ['<%= accountant.frontend.src %>/index.html'],
|
||||
ignorePath: /\.\.\//
|
||||
}
|
||||
};
|
47
manage.py
Executable file
47
manage.py
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
from flask.ext.script import Manager
|
||||
from flask.ext.migrate import Migrate, MigrateCommand, stamp
|
||||
|
||||
from accountant import app, db
|
||||
|
||||
from accountant.api.models.users import User
|
||||
|
||||
manager = Manager(app)
|
||||
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
|
||||
@manager.command
|
||||
def initdb():
|
||||
""" Create the database ans stamp it. """
|
||||
|
||||
tables = db.engine.table_names()
|
||||
|
||||
if len(tables) > 1 and 'alembic_version' not in tables:
|
||||
exit("Database already initialized.")
|
||||
|
||||
db.metadata.create_all(bind=db.engine)
|
||||
stamp()
|
||||
print("Database created.")
|
||||
|
||||
user_manager = Manager(usage="Manage users.")
|
||||
|
||||
manager.add_command('user', user_manager)
|
||||
|
||||
|
||||
@user_manager.command
|
||||
def add(email, password):
|
||||
""" Add a new user. """
|
||||
user = User()
|
||||
user.email = email
|
||||
user.password = User.hash_password(password)
|
||||
|
||||
db.session.add(user)
|
||||
|
||||
print("User '%s' successfully added." % email)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
1
migrations/README
Normal file
1
migrations/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
migrations/alembic.ini
Normal file
45
migrations/alembic.ini
Normal file
@ -0,0 +1,45 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
73
migrations/env.py
Normal file
73
migrations/env.py
Normal file
@ -0,0 +1,73 @@
|
||||
from __future__ import with_statement
|
||||
from alembic import context
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from logging.config import fileConfig
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
from flask import current_app
|
||||
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
|
||||
target_metadata = current_app.extensions['migrate'].metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
engine = engine_from_config(
|
||||
config.get_section(config.config_ini_section),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool)
|
||||
|
||||
connection = engine.connect()
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata
|
||||
)
|
||||
|
||||
try:
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
|
22
migrations/script.py.mako
Normal file
22
migrations/script.py.mako
Normal file
@ -0,0 +1,22 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
34
migrations/versions/1232daf66ac_add_user_support.py
Normal file
34
migrations/versions/1232daf66ac_add_user_support.py
Normal file
@ -0,0 +1,34 @@
|
||||
"""Add user support.
|
||||
|
||||
Revision ID: 1232daf66ac
|
||||
Revises: 144929e0f5f
|
||||
Create Date: 2015-08-31 10:24:40.578432
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1232daf66ac'
|
||||
down_revision = '144929e0f5f'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('email', sa.String(length=200), nullable=False),
|
||||
sa.Column('password', sa.String(length=100), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_user_email'), table_name='user')
|
||||
op.drop_table('user')
|
||||
### end Alembic commands ###
|
142
migrations/versions/144929e0f5f_improve_operation_scheduling.py
Normal file
142
migrations/versions/144929e0f5f_improve_operation_scheduling.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""Improve operation scheduling.
|
||||
|
||||
Revision ID: 144929e0f5f
|
||||
Revises: None
|
||||
Create Date: 2015-07-17 15:04:01.002581
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '144929e0f5f'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from accountant.api.models.scheduled_operations import ScheduledOperation
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.get_bind().execute("DROP VIEW operation")
|
||||
op.rename_table('entry', 'operation')
|
||||
|
||||
# Add column "canceled" in table "entry"
|
||||
op.add_column(
|
||||
'operation',
|
||||
sa.Column(
|
||||
'canceled',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=False,
|
||||
server_default=sa.false()
|
||||
)
|
||||
)
|
||||
|
||||
# Add column "confirmed" in table "entry"
|
||||
op.add_column(
|
||||
'operation',
|
||||
sa.Column(
|
||||
'confirmed',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
default=True,
|
||||
server_default=sa.true()
|
||||
)
|
||||
)
|
||||
|
||||
# Drop unused table canceled_operation.
|
||||
op.drop_table('canceled_operation')
|
||||
|
||||
op.get_bind().execute(
|
||||
"alter sequence entry_id_seq rename to operation_id_seq"
|
||||
)
|
||||
|
||||
connection = op.get_bind()
|
||||
Session = sa.orm.sessionmaker()
|
||||
session = Session(bind=connection)
|
||||
|
||||
# Get all scheduled operations
|
||||
scheduled_operations = ScheduledOperation.query(session).all()
|
||||
|
||||
for scheduled_operation in scheduled_operations:
|
||||
scheduled_operation.reschedule(session)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
op.create_table(
|
||||
"canceled_operation",
|
||||
sa.Column("id", sa.Integer, primary_key=True),
|
||||
sa.Column(
|
||||
"scheduled_operation_id", sa.Integer(),
|
||||
sa.ForeignKey("scheduled_operation.id")),
|
||||
sa.Column("operation_date", sa.Date, nullable=False)
|
||||
)
|
||||
|
||||
op.drop_column('operation', 'canceled')
|
||||
op.drop_column('operation', 'confirmed')
|
||||
|
||||
op.get_bind().execute(
|
||||
"alter sequence operation_id_seq rename to entry_id_seq"
|
||||
)
|
||||
op.rename_table('operation', 'entry')
|
||||
|
||||
op.get_bind().execute(
|
||||
"""
|
||||
CREATE VIEW operation AS
|
||||
SELECT entry.id,
|
||||
entry.operation_date,
|
||||
entry.label,
|
||||
entry.value,
|
||||
entry.account_id,
|
||||
entry.category,
|
||||
entry.pointed,
|
||||
entry.scheduled_operation_id,
|
||||
false AS canceled
|
||||
FROM entry
|
||||
UNION
|
||||
SELECT NULL::bigint AS id,
|
||||
scheduled_operation.operation_date,
|
||||
scheduled_operation.label,
|
||||
scheduled_operation.value,
|
||||
scheduled_operation.account_id,
|
||||
scheduled_operation.category,
|
||||
false AS pointed,
|
||||
scheduled_operation.id AS scheduled_operation_id,
|
||||
false AS canceled
|
||||
FROM (
|
||||
SELECT scheduled_operation_1.id,
|
||||
scheduled_operation_1.start_date,
|
||||
scheduled_operation_1.stop_date,
|
||||
scheduled_operation_1.day,
|
||||
scheduled_operation_1.frequency,
|
||||
scheduled_operation_1.label,
|
||||
scheduled_operation_1.value,
|
||||
scheduled_operation_1.account_id,
|
||||
scheduled_operation_1.category,
|
||||
generate_series(scheduled_operation_1.start_date::timestamp without time zone, scheduled_operation_1.stop_date::timestamp without time zone, scheduled_operation_1.frequency::double precision * '1 mon'::interval) AS operation_date
|
||||
FROM scheduled_operation scheduled_operation_1) scheduled_operation
|
||||
LEFT JOIN (
|
||||
SELECT entry.scheduled_operation_id,
|
||||
date_trunc('MONTH'::text, entry.operation_date::timestamp with time zone) AS operation_date
|
||||
FROM entry
|
||||
UNION
|
||||
SELECT canceled_operation.scheduled_operation_id,
|
||||
date_trunc('MONTH'::text, canceled_operation.operation_date::timestamp with time zone) AS operation_date
|
||||
FROM canceled_operation
|
||||
) saved_operation ON saved_operation.scheduled_operation_id = scheduled_operation.id AND saved_operation.operation_date = date_trunc('MONTH'::text, scheduled_operation.operation_date)
|
||||
WHERE saved_operation.scheduled_operation_id IS NULL
|
||||
UNION
|
||||
SELECT NULL::bigint AS id,
|
||||
canceled_operation.operation_date,
|
||||
scheduled_operation.label,
|
||||
scheduled_operation.value,
|
||||
scheduled_operation.account_id,
|
||||
scheduled_operation.category,
|
||||
false AS pointed,
|
||||
scheduled_operation.id AS scheduled_operation_id,
|
||||
true AS canceled
|
||||
FROM scheduled_operation
|
||||
JOIN canceled_operation ON canceled_operation.scheduled_operation_id = scheduled_operation.id;
|
||||
"""
|
||||
)
|
103
package.json
103
package.json
@ -4,82 +4,31 @@
|
||||
"repository": "https://git.lahouze.org/xals/accountant",
|
||||
"license": "AGPL-1.0",
|
||||
"devDependencies": {
|
||||
"@types/requirejs": "^2.1.31",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
"awesome-typescript-loader": "^3.2.3",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.0.1",
|
||||
"babel-loader": "^7.1.2",
|
||||
"codelyzer": "^3.2.0",
|
||||
"css-loader": "^0.28.7",
|
||||
"ejs-loader": "^0.3.0",
|
||||
"eslint": "^4.10.0",
|
||||
"eslint-config-angular": "^0.5.0",
|
||||
"eslint-config-webpack": "^1.2.5",
|
||||
"eslint-loader": "^1.9.0",
|
||||
"eslint-plugin-angular": "^3.1.1",
|
||||
"eslint-plugin-html": "^3.2.2",
|
||||
"eslint-plugin-jquery": "^1.2.1",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"eslint-plugin-security": "^1.4.0",
|
||||
"eslint-plugin-this": "^0.2.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"file-loader": "^1.1.5",
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"htmllint-loader": "^2.1.4",
|
||||
"imports-loader": "^0.7.1",
|
||||
"less": "^3.0.0-alpha.3",
|
||||
"less-loader": "^4.0.5",
|
||||
"loglevel": "^1.5.1",
|
||||
"ngtemplate-loader": "^2.0.1",
|
||||
"node-sass": "^4.5.3",
|
||||
"resolve-url-loader": "^2.1.0",
|
||||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.19.0",
|
||||
"ts-loader": "^3.1.0",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-config-prettier": "^1.5.0",
|
||||
"tslint-loader": "^3.5.3",
|
||||
"typescript": "^2.5.2",
|
||||
"url-loader": "^0.6.2",
|
||||
"webpack": "^3.8.1",
|
||||
"webpack-dev-server": "^2.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^4.4.6",
|
||||
"@angular/cdk": "^2.0.0-beta.10",
|
||||
"@angular/common": "^4.4.6",
|
||||
"@angular/compiler": "^4.4.6",
|
||||
"@angular/core": "^4.4.6",
|
||||
"@angular/flex-layout": "^2.0.0-beta.9",
|
||||
"@angular/forms": "^4.4.6",
|
||||
"@angular/http": "^4.4.6",
|
||||
"@angular/material": "^2.0.0-beta.10",
|
||||
"@angular/platform-browser": "^4.4.6",
|
||||
"@angular/platform-browser-dynamic": "^4.4.6",
|
||||
"@angular/router": "^4.4.6",
|
||||
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.2",
|
||||
"@nsalaun/ng-logger": "^2.0.2",
|
||||
"@types/c3": "^0.4.45",
|
||||
"@types/jquery": "^3.2.12",
|
||||
"@types/node": "^8.0.47",
|
||||
"angular2-text-mask": "^8.0.4",
|
||||
"base64util": "^1.0.2",
|
||||
"bootstrap": "^4.0.0-beta",
|
||||
"c3": "^0.4.18",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jquery": "^3.2.1",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.19.1",
|
||||
"ngx-toastr": "^6.5.0",
|
||||
"reflect-metadata": "^0.1.10",
|
||||
"roboto-fontface": "^0.8.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"zone.js": "^0.8.17"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"dev": "webpack-dev-server --debug --devtool eval --config webpack.config.js --progress --colors --hot --content-base build"
|
||||
"connect-logger": "0.0.1",
|
||||
"connect-proxy-layer": "^0.1.2",
|
||||
"grunt": "~1.0",
|
||||
"grunt-bg-shell": "^2.3.1",
|
||||
"grunt-contrib-clean": "~1.0",
|
||||
"grunt-contrib-concat": "~1.0",
|
||||
"grunt-contrib-connect": "~1.0",
|
||||
"grunt-contrib-cssmin": "~1.0",
|
||||
"grunt-contrib-jshint": "~1.0",
|
||||
"grunt-contrib-uglify": "~1.0",
|
||||
"grunt-contrib-watch": "~1.0",
|
||||
"grunt-copy": "^0.1.0",
|
||||
"grunt-filerev": "^2.3.1",
|
||||
"grunt-flake8": "^0.1.3",
|
||||
"grunt-htmllint": "^0.2.7",
|
||||
"grunt-include-source": "^0.7.1",
|
||||
"grunt-jscs": "^2.7.0",
|
||||
"grunt-newer": "^1.1.1",
|
||||
"grunt-ng-annotate": "~2.0",
|
||||
"grunt-shell": "^1.1.2",
|
||||
"grunt-usemin": "^3.1.1",
|
||||
"grunt-wiredep": "~3.0",
|
||||
"jshint-stylish": "^2.1.0",
|
||||
"load-grunt-configs": "^0.4.3",
|
||||
"load-grunt-tasks": "^3.2.0",
|
||||
"time-grunt": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
178
sql/install/01_init.sql
Normal file
178
sql/install/01_init.sql
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump
|
||||
--
|
||||
|
||||
SET statement_timeout = 0;
|
||||
SET client_encoding = 'UTF8';
|
||||
SET standard_conforming_strings = on;
|
||||
SET check_function_bodies = false;
|
||||
SET client_min_messages = warning;
|
||||
|
||||
--
|
||||
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
|
||||
|
||||
|
||||
--
|
||||
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
|
||||
|
||||
|
||||
SET search_path = public, pg_catalog;
|
||||
|
||||
SET default_tablespace = '';
|
||||
|
||||
SET default_with_oids = false;
|
||||
|
||||
--
|
||||
-- Name: account; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
CREATE TABLE account (
|
||||
id integer NOT NULL,
|
||||
name character varying(200) NOT NULL,
|
||||
authorized_overdraft integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: account_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE account_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: account_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE account_id_seq OWNED BY account.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry; Type: TABLE; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
CREATE TABLE entry (
|
||||
id bigint NOT NULL,
|
||||
operation_date date,
|
||||
label character varying(500) NOT NULL,
|
||||
comment character varying(500),
|
||||
value numeric(15,2) NOT NULL,
|
||||
account_id integer NOT NULL,
|
||||
category character varying(100),
|
||||
pointed boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_id_seq; Type: SEQUENCE; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE SEQUENCE entry_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER SEQUENCE entry_id_seq OWNED BY entry.id;
|
||||
|
||||
|
||||
--
|
||||
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY account ALTER COLUMN id SET DEFAULT nextval('account_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: id; Type: DEFAULT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY entry ALTER COLUMN id SET DEFAULT nextval('entry_id_seq'::regclass);
|
||||
|
||||
|
||||
--
|
||||
-- Name: account_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY account
|
||||
ADD CONSTRAINT account_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY entry
|
||||
ADD CONSTRAINT entry_pkey PRIMARY KEY (id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_account_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
CREATE INDEX entry_account_id_idx ON entry USING btree (account_id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_operation_date_idx; Type: INDEX; Schema: public; Owner: -; Tablespace:
|
||||
--
|
||||
|
||||
CREATE INDEX entry_operation_date_idx ON entry USING btree (operation_date);
|
||||
|
||||
|
||||
--
|
||||
-- Name: entry_account_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY entry
|
||||
ADD CONSTRAINT entry_account_id_fkey FOREIGN KEY (account_id) REFERENCES account(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: public; Type: ACL; Schema: -; Owner: -
|
||||
--
|
||||
|
||||
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
||||
REVOKE ALL ON SCHEMA public FROM postgres;
|
||||
GRANT ALL ON SCHEMA public TO postgres;
|
||||
GRANT ALL ON SCHEMA public TO PUBLIC;
|
||||
|
||||
|
||||
--
|
||||
-- PostgreSQL database dump complete
|
||||
--
|
||||
|
18
sql/update/0.1-0.2/001_authorized_overdraft.sql
Normal file
18
sql/update/0.1-0.2/001_authorized_overdraft.sql
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
ALTER TABLE account ADD COLUMN authorized_overdraft INTEGER NOT NULL DEFAULT 0;
|
||||
|
26
sql/update/0.1-0.2/002_pointed.sql
Normal file
26
sql/update/0.1-0.2/002_pointed.sql
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
ALTER TABLE entry ADD COLUMN pointed BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
UPDATE entry SET pointed = operation_date IS NOT NULL;
|
||||
|
||||
UPDATE entry SET operation_date = value_date;
|
||||
|
||||
ALTER TABLE entry DROP COLUMN value_date;
|
||||
|
||||
CREATE INDEX entry_operation_date_idx ON entry USING btree (operation_date);
|
||||
|
16
sql/update/0.2-0.3/000_placeholder.sql
Normal file
16
sql/update/0.2-0.3/000_placeholder.sql
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
16
sql/update/0.2-0.3/001_scheduled_operations.sql
Normal file
16
sql/update/0.2-0.3/001_scheduled_operations.sql
Normal file
@ -0,0 +1,16 @@
|
||||
create table scheduled_operation(
|
||||
id serial primary key,
|
||||
start_date date not null,
|
||||
stop_date date not null,
|
||||
day integer not null check (day > 0 and day <= 31),
|
||||
frequency integer not null check (frequency > 0),
|
||||
label varchar(500) not null,
|
||||
value numeric(15,2) not null,
|
||||
account_id integer not null references account(id),
|
||||
category varchar(100)
|
||||
);
|
||||
|
||||
create index scheduled_operation_account_id_idx on scheduled_operation(account_id);
|
||||
|
||||
alter table entry add column scheduled_operation_id integer references scheduled_operation(id);
|
||||
|
2
sql/update/0.2-0.3/002_operation_date_cleanup.sql
Normal file
2
sql/update/0.2-0.3/002_operation_date_cleanup.sql
Normal file
@ -0,0 +1,2 @@
|
||||
alter table entry alter column operation_date set not null;
|
||||
alter table entry drop column comment;
|
@ -1,36 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Account } from './account';
|
||||
import { AccountBalances } from './accountBalances';
|
||||
import { AccountBalancesService } from './accountBalances.service';
|
||||
import { AccountService } from './account.service';
|
||||
|
||||
@Injectable()
|
||||
export class AccountDataSource extends DataSource<Account> {
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private accountBalancesService: AccountBalancesService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<Account[]> {
|
||||
return this.accountService.query().map((accounts: Account[]) => {
|
||||
for(let account of accounts) {
|
||||
this.accountBalancesService
|
||||
.get(account.id)
|
||||
.subscribe((accountBalances: AccountBalances) => {
|
||||
account.balances = accountBalances;
|
||||
})
|
||||
}
|
||||
|
||||
return accounts;
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdIconModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
MdTableModule,
|
||||
} from '@angular/material';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
import { AccountBalancesService } from './accountBalances.service';
|
||||
import { AccountListComponent } from './accountList.component';
|
||||
import { AccountDataSource } from './account.dataSource';
|
||||
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
|
||||
import { AccountEditModalComponent } from './accountEditModal.component';
|
||||
import { AccountFormComponent } from './accountForm.component';
|
||||
import { DailyBalanceService } from './dailyBalance.service';
|
||||
import { AccountListState } from './account.states'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule.forChild([
|
||||
AccountListState
|
||||
]),
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdIconModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
MdTableModule,
|
||||
NgLoggerModule,
|
||||
ToastrModule,
|
||||
NgbModule,
|
||||
],
|
||||
providers: [
|
||||
AccountService,
|
||||
AccountDataSource,
|
||||
AccountBalancesService,
|
||||
DailyBalanceService,
|
||||
],
|
||||
declarations: [
|
||||
AccountListComponent,
|
||||
AccountDeleteModalComponent,
|
||||
AccountEditModalComponent,
|
||||
AccountFormComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
AccountListComponent,
|
||||
AccountDeleteModalComponent,
|
||||
AccountEditModalComponent,
|
||||
]
|
||||
})
|
||||
export class AccountModule {}
|
@ -1,42 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Account } from './account';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
private url(id?: Number): string {
|
||||
if(id) {
|
||||
return `/api/account/${id}`;
|
||||
}
|
||||
|
||||
return `/api/account`;
|
||||
}
|
||||
|
||||
query(): Observable<Account[]> {
|
||||
return this.http.get<Account[]>(this.url());
|
||||
}
|
||||
|
||||
get(id: number): Observable<Account> {
|
||||
return this.http.get<Account>(this.url(id));
|
||||
}
|
||||
|
||||
create(account: Account): Observable<Account> {
|
||||
return this.http.post<Account>(this.url(), account);
|
||||
}
|
||||
|
||||
update(account: Account): Observable<Account> {
|
||||
return this.http.post<Account>(this.url(account.id), account);
|
||||
}
|
||||
|
||||
delete(account: Account): Observable<Account> {
|
||||
return this.http.delete<Account>(this.url(account.id));
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { AccountListComponent } from './accountList.component';
|
||||
|
||||
export const AccountListState = {
|
||||
path: 'accounts',
|
||||
component: AccountListComponent
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { AccountBalances } from './accountBalances';
|
||||
|
||||
export class Account {
|
||||
id: number;
|
||||
name: string;
|
||||
authorized_overdraft: number;
|
||||
|
||||
balances: AccountBalances;
|
||||
|
||||
constructor() {
|
||||
this.authorized_overdraft = 0;
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
|
||||
import { AccountBalances } from './accountBalances';
|
||||
|
||||
@Injectable()
|
||||
export class AccountBalancesService {
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
get(id: number): Observable<AccountBalances> {
|
||||
return this.http.get<AccountBalances>(`/api/account/${id}/balances`);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
export class AccountBalances {
|
||||
current: number;
|
||||
pointed: number;
|
||||
future: number;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MD_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'account-delete-modal',
|
||||
template: `
|
||||
<h3 md-dialog-title>Delete account #{{ data.account.id }}</h3>
|
||||
|
||||
<md-dialog-content>
|
||||
Do you really want to delete account #{{ data.account.id }} with name:<br/>
|
||||
{{ data.account.name }}
|
||||
</md-dialog-content>
|
||||
|
||||
<md-dialog-actions>
|
||||
<button md-raised-button color="warn" [md-dialog-close]="data.account">
|
||||
Yes
|
||||
</button>
|
||||
|
||||
<button md-raised-button md-dialog-close>
|
||||
No
|
||||
</button>
|
||||
</md-dialog-actions>
|
||||
`
|
||||
})
|
||||
export class AccountDeleteModalComponent {
|
||||
constructor(
|
||||
@Inject(MD_DIALOG_DATA) private data: any
|
||||
) {}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Inject, ViewChild } from '@angular/core';
|
||||
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { Account } from './account';
|
||||
import { AccountFormComponent } from './accountForm.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-edit-modal',
|
||||
template: `
|
||||
<h3 md-dialog-title>{{ title() }}</h3>
|
||||
|
||||
<md-dialog-content>
|
||||
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm">
|
||||
</account-form>
|
||||
</md-dialog-content>
|
||||
|
||||
<md-dialog-actions>
|
||||
<button md-raised-button color="primary" [disabled]="!accountForm?.form.valid" (click)="submit()">
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button md-raised-button color="warn" md-dialog-close>
|
||||
Cancel
|
||||
</button>
|
||||
</md-dialog-actions>
|
||||
`
|
||||
})
|
||||
export class AccountEditModalComponent {
|
||||
private account: Account;
|
||||
@ViewChild('accountForm') accountForm: AccountFormComponent;
|
||||
|
||||
constructor(
|
||||
@Inject(MD_DIALOG_DATA) public data: any,
|
||||
public dialogRef: MdDialogRef<AccountEditModalComponent>,
|
||||
) {
|
||||
this.account = data.account;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
if(this.account && this.account.id) {
|
||||
return "Account #" + this.account.id;
|
||||
} else {
|
||||
return "New account";
|
||||
}
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
let formModel = this.accountForm.form.value;
|
||||
let account = Object.assign({}, this.account);
|
||||
|
||||
account.id = this.account.id;
|
||||
account.name = formModel.name;
|
||||
account.authorized_overdraft = -formModel.authorizedOverdraft;
|
||||
|
||||
this.dialogRef.close(account);
|
||||
}
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { Account } from './account';
|
||||
|
||||
@Component({
|
||||
selector: 'account-form',
|
||||
exportAs: 'accountForm',
|
||||
template: `
|
||||
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
|
||||
<md-list>
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="name" placeholder="Account name">
|
||||
|
||||
<md-error *ngIf="name.errors?.required">The account name is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<span mdPrefix>-</span>
|
||||
<input mdInput formControlName="authorizedOverdraft"
|
||||
placeholder="Authorized overdraft">
|
||||
<span mdSuffix>.00€</span>
|
||||
|
||||
<md-error *ngIf="authorizedOverdraft.errors?.required">
|
||||
The authorized overdraft is required.
|
||||
</md-error>
|
||||
|
||||
<md-error *ngIf="authorizedOverdraft.errors?.min">
|
||||
The authorized overdraft must be less than or equal to 0.
|
||||
</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class AccountFormComponent implements OnInit {
|
||||
public form: FormGroup;
|
||||
@Input() account: Account;
|
||||
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
name: ['', Validators.required],
|
||||
authorizedOverdraft: ['', [Validators.required, Validators.min(0)]],
|
||||
});
|
||||
|
||||
this.form.patchValue({
|
||||
name: this.account.name,
|
||||
authorizedOverdraft: -this.account.authorized_overdraft
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
if(this.form.valid) {
|
||||
this.submitEventEmitter.emit();
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.form.get('name');
|
||||
}
|
||||
|
||||
get authorizedOverdraft() {
|
||||
return this.form.get('authorizedOverdraft');
|
||||
}
|
||||
}
|
@ -1,191 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component } from '@angular/core';
|
||||
import { MdDialog } from '@angular/material';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
import { Account } from './account';
|
||||
import { AccountBalances } from './accountBalances';
|
||||
import { AccountDataSource } from './account.dataSource';
|
||||
import { AccountService } from './account.service';
|
||||
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
|
||||
import { AccountEditModalComponent } from './accountEditModal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'account-list',
|
||||
template: `
|
||||
<div class="containerX">
|
||||
<div class="container">
|
||||
<button md-fab color="primary" (click)="add()">
|
||||
<md-icon>add</md-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<md-table #table [dataSource]="accounts">
|
||||
<ng-container mdColumnDef="name">
|
||||
<md-header-cell *mdHeaderCellDef>Nom du compte</md-header-cell>
|
||||
<md-cell *mdCellDef="let account">
|
||||
<a [routerLink]="['/account', account.id, 'operations']">
|
||||
{{ account.name }}
|
||||
</a>
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="current">
|
||||
<md-header-cell *mdHeaderCellDef>Solde courant</md-header-cell>
|
||||
<md-cell *mdCellDef="let account">
|
||||
<span
|
||||
[class.warning]="account.authorized_overdraft < 0 && account.balances?.current < 0"
|
||||
[class.error]="account.balances?.current < account.authorized_overdraft">
|
||||
{{ account.balances?.current | currency:"EUR":true }}
|
||||
</span>
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="pointed">
|
||||
<md-header-cell *mdHeaderCellDef>Solde pointé</md-header-cell>
|
||||
<md-cell *mdCellDef="let account">
|
||||
<span
|
||||
[class.warning]="account.authorized_overdraft < 0 && account.balances?.pointed < 0"
|
||||
[class.error]="account.balances?.pointed < account.authorized_overdraft">
|
||||
{{ account.balances?.pointed | currency:"EUR":true }}
|
||||
</span>
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="authorizedOverdraft">
|
||||
<md-header-cell *mdHeaderCellDef>Découvert autorisé</md-header-cell>
|
||||
<md-cell *mdCellDef="let account">
|
||||
{{ account.authorized_overdraft | currency:"EUR":true }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="actions">
|
||||
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
|
||||
<md-cell *mdCellDef="let account">
|
||||
<!-- Edit account. -->
|
||||
<button md-mini-fab color="primary"
|
||||
(click)="modify(account)">
|
||||
<md-icon>mode_edit</md-icon>
|
||||
</button>
|
||||
|
||||
<!-- Delete account, with confirm. -->
|
||||
<button md-mini-fab color="warn"
|
||||
(click)="confirmDelete(account)">
|
||||
<md-icon>delete_forever</md-icon>
|
||||
</button>
|
||||
|
||||
<!-- Open account scheduler. -->
|
||||
<button md-mini-fab
|
||||
[hidden]="!account.id"
|
||||
[routerLink]="['/account', account.id, 'scheduler']">
|
||||
<md-icon>event</md-icon>
|
||||
</button>
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
|
||||
<md-row *mdRowDef="let row; columns: displayedColumns;">
|
||||
</md-row>
|
||||
</md-table>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class AccountListComponent {
|
||||
displayedColumns: String[] = [
|
||||
'name', 'current', 'pointed', 'authorizedOverdraft', 'actions'
|
||||
];
|
||||
|
||||
constructor(
|
||||
private accounts: AccountDataSource,
|
||||
private accountService: AccountService,
|
||||
private toastrService: ToastrService,
|
||||
private logger: Logger,
|
||||
private mdDialog: MdDialog,
|
||||
) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an empty account.
|
||||
*/
|
||||
add() {
|
||||
this.modify(new Account());
|
||||
};
|
||||
|
||||
/*
|
||||
* Modify an account.
|
||||
*/
|
||||
modify(account: Account) {
|
||||
let dialogRef = this.mdDialog.open(AccountEditModalComponent, {
|
||||
data: {
|
||||
account: account,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((account: Account) => {
|
||||
if(account) {
|
||||
this.logger.log("Modal closed => save account", account);
|
||||
this.save(account);
|
||||
} else {
|
||||
this.logger.log("Modal dismissed");
|
||||
}
|
||||
}, (reason) => function(reason) {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Save account.
|
||||
*/
|
||||
save(account) {
|
||||
this.accountService.create(account).subscribe(account => {
|
||||
this.toastrService.success('Account #' + account.id + ' saved.');
|
||||
}, result => {
|
||||
this.logger.error('Error while saving account', account, result);
|
||||
|
||||
this.toastrService.error(
|
||||
'Error while saving account: ' + result.message
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Show a dialog to confirm account deletion.
|
||||
*/
|
||||
confirmDelete(account: Account) {
|
||||
let dialogRef = this.mdDialog.open(AccountDeleteModalComponent, {
|
||||
data: {
|
||||
account: account,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((account: Account) => {
|
||||
if(account) {
|
||||
this.delete(account);
|
||||
}
|
||||
}, reason => {
|
||||
this.logger.error("Delete dialog failed", reason);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Delete an account.
|
||||
*/
|
||||
delete(account: Account) {
|
||||
var id = account.id;
|
||||
|
||||
this.accountService.delete(account).subscribe(account => {
|
||||
this.toastrService.success('account #' + id + ' deleted.');
|
||||
|
||||
// FIXME Alexis Lahouze 2017-09-17 Remove from array.
|
||||
}, function(result) {
|
||||
this.toastrService.error(
|
||||
'An error occurred while trying to delete account #' +
|
||||
id + ':<br />' + result
|
||||
);
|
||||
});
|
||||
};
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { DailyBalance } from './dailyBalance';
|
||||
|
||||
@Injectable()
|
||||
export class DailyBalanceService {
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
query(id: number): Observable<DailyBalance[]> {
|
||||
return this.http.get<DailyBalance[]>(`/api/account/${id}/daily_balances`);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
export class DailyBalance {
|
||||
operation_date: string;
|
||||
balance: number;
|
||||
expenses: number;
|
||||
revenues: number;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'accountant',
|
||||
styles: [ require('./main.scss').toString() ],
|
||||
template: `
|
||||
<md-toolbar class="mat-elevation-z6" color="primary">
|
||||
<div class="acc-toolbar">
|
||||
<a md-button style="text-transform: uppercase"
|
||||
routerLink="/accounts">
|
||||
Accountant
|
||||
</a>
|
||||
</div>
|
||||
</md-toolbar>
|
||||
|
||||
<div class="acc-content">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AppComponent { }
|
@ -1,5 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Level } from '@nsalaun/ng-logger';
|
||||
|
||||
export const ApiBaseURL = "http://localhost:8080/api";
|
||||
export const LogLevel = Level.LOG;
|
@ -1,63 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import 'zone.js';
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import {
|
||||
MdButtonModule,
|
||||
MdToolbarModule,
|
||||
MdSidenavModule
|
||||
} from '@angular/material';
|
||||
import { FlexLayoutModule } from '@angular/flex-layout';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { NgLoggerModule } from '@nsalaun/ng-logger';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { LoginModule } from './login/login.module';
|
||||
import { AccountModule } from './accounts/account.module';
|
||||
import { ScheduleModule } from './scheduler/schedule.module';
|
||||
import { OperationModule } from './operations/operation.module';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import { ApiBaseURL, LogLevel } from './app.config';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
RouterModule.forRoot([
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/accounts',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
], {
|
||||
enableTracing: true,
|
||||
useHash: true
|
||||
}),
|
||||
FlexLayoutModule,
|
||||
MdButtonModule,
|
||||
MdToolbarModule,
|
||||
MdSidenavModule,
|
||||
LoginModule,
|
||||
NgLoggerModule.forRoot(LogLevel),
|
||||
ToastrModule.forRoot(),
|
||||
NgbModule.forRoot(),
|
||||
AccountModule,
|
||||
ScheduleModule,
|
||||
OperationModule,
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
bootstrap: [ AppComponent ]
|
||||
})
|
||||
|
||||
export class AppModule {
|
||||
constructor() {}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<base href="/">
|
||||
|
||||
<!-- Title -->
|
||||
<title><% htmlWebpackPlugin.options.title %></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<!-- htmllint attr-bans="false" -->
|
||||
<body style="padding-bottom: 50px; padding-top: 70px">
|
||||
<!-- htmllint attr-bans="$previous" -->
|
||||
<accountant></accountant>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,92 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { Injectable, Injector } from '@angular/core';
|
||||
import {
|
||||
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
|
||||
import { Observable} from 'rxjs/Rx';
|
||||
import 'rxjs/add/operator/catch';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
|
||||
import { LoginService } from './login.service';
|
||||
import { Token } from './token';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
private observable: Observable<Token>;
|
||||
|
||||
constructor(
|
||||
private logger: Logger,
|
||||
private injector: Injector,
|
||||
) {}
|
||||
|
||||
injectAuthorizationHeader(request: HttpRequest<any>, accessToken: string) {
|
||||
this.logger.log('Injecting Authorization header');
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
intercept(
|
||||
request: HttpRequest<any>,
|
||||
next: HttpHandler,
|
||||
pass?: number
|
||||
): Observable<HttpEvent<any>> {
|
||||
if(!pass) {
|
||||
pass = 1;
|
||||
}
|
||||
|
||||
let loginService = this.injector.get(LoginService);
|
||||
|
||||
if(request.url == loginService.url) {
|
||||
this.logger.log("Login URL, do not handle.");
|
||||
|
||||
return next.handle(request);
|
||||
}
|
||||
|
||||
this.logger.log(`Intercepted request, pass #${pass}`, request, next);
|
||||
|
||||
let accessToken = loginService.accessToken;
|
||||
|
||||
if(accessToken){
|
||||
request = request.clone({
|
||||
headers: request.headers.set('Authorization', `Bearer ${accessToken}`)
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.log('Request', request);
|
||||
|
||||
let observable: Observable<any> = next.handle(request);
|
||||
|
||||
return observable.catch(
|
||||
(error, caught): Observable<any> => {
|
||||
this.logger.error("Error", error, caught);
|
||||
|
||||
if(!(error instanceof HttpErrorResponse) || error.status != 401) {
|
||||
return Observable.throw(error);
|
||||
}
|
||||
|
||||
this.logger.log('Unauthorized', error);
|
||||
|
||||
if(pass === 3) {
|
||||
return Observable.throw(error);
|
||||
}
|
||||
|
||||
if(!this.observable) {
|
||||
this.logger.log("No current login observable.")
|
||||
this.observable = loginService.login();
|
||||
}
|
||||
|
||||
return this.observable.flatMap((token: Token): Observable<HttpEvent<any>> => {
|
||||
this.logger.log("Logged in, access_token:", token.access_token);
|
||||
this.observable = null;
|
||||
return this.intercept(request, next, ++pass);
|
||||
}).catch((error) => {
|
||||
this.observable = null;
|
||||
return Observable.throw(error);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
module.exports = function($httpProvider, $storageProvider) {
|
||||
// Define interceptors.
|
||||
$httpProvider.interceptors.push(function($storage) {
|
||||
return {
|
||||
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;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 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']);
|
||||
};
|
@ -1,49 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import {
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
} from '@angular/material';
|
||||
|
||||
import { NgLoggerModule } from '@nsalaun/ng-logger';
|
||||
|
||||
import { AuthInterceptor } from './authInterceptor';
|
||||
import { LoginService } from './login.service';
|
||||
import { LoginFormComponent } from './loginForm.component';
|
||||
import { LoginModalComponent } from './loginModal.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
NgLoggerModule,
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
],
|
||||
providers: [
|
||||
LoginService,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
}
|
||||
],
|
||||
declarations: [
|
||||
LoginModalComponent,
|
||||
LoginFormComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
LoginModalComponent,
|
||||
]
|
||||
})
|
||||
export class LoginModule {};
|
@ -1,65 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { MdDialog } from '@angular/material';
|
||||
|
||||
import { Observable} from 'rxjs/Rx';
|
||||
|
||||
import * as base64 from 'base64util';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
|
||||
import { Token } from './token';
|
||||
import { LoginModalComponent } from './loginModal.component';
|
||||
import { Login } from './login';
|
||||
|
||||
@Injectable()
|
||||
export class LoginService {
|
||||
|
||||
constructor(
|
||||
private httpClient: HttpClient,
|
||||
private logger: Logger,
|
||||
private mdDialog: MdDialog,
|
||||
) {}
|
||||
|
||||
public readonly url: string = '/api/user/login';
|
||||
|
||||
login(): Observable<Token> {
|
||||
let dialogRef = this.mdDialog.open(LoginModalComponent);
|
||||
|
||||
sessionStorage.clear();
|
||||
|
||||
return dialogRef.afterClosed().flatMap((login: Login) =>
|
||||
this.doLogin(login)
|
||||
).map((token: Token): Token => {
|
||||
this.accessToken = token.access_token;
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
logout() {
|
||||
sessionStorage.clear();
|
||||
}
|
||||
|
||||
doLogin(login: Login): Observable<any> {
|
||||
var authdata = base64.encode(
|
||||
`${login.email}:${login.password}`
|
||||
);
|
||||
|
||||
let headers = new HttpHeaders()
|
||||
headers = headers.set('Authorization', `Basic ${authdata}`);
|
||||
|
||||
this.logger.log("Headers", headers);
|
||||
|
||||
return this.httpClient.post(this.url, {}, {
|
||||
headers: headers
|
||||
});
|
||||
}
|
||||
|
||||
get accessToken(): string {
|
||||
return sessionStorage.getItem('access_token');
|
||||
}
|
||||
|
||||
set accessToken(token: string) {
|
||||
sessionStorage.setItem('access_token', token);
|
||||
}
|
||||
};
|
@ -1,38 +0,0 @@
|
||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
||||
<div class="modal top am-fade" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">Authentification requise</h3>
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" ng-submit="$login()">
|
||||
<div class="modal-body" id="modal-body">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<input class="btn btn-primary" type="submit" value="OK"/>
|
||||
|
||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,6 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
export class Login {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { Login } from './login';
|
||||
|
||||
@Component({
|
||||
selector: 'login-form',
|
||||
exportAs: 'loginForm',
|
||||
template: `
|
||||
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
|
||||
<md-list>
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput type="text"
|
||||
formControlName="email" placeholder="Nom d'utilisateur">
|
||||
|
||||
<md-error *ngIf="email.errors?.required">The email is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput type="password"
|
||||
formControlName="password" placeholder="Mot de passe">
|
||||
|
||||
<md-error *ngIf="password.errors?.required">The password is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
|
||||
export class LoginFormComponent {
|
||||
public form: FormGroup;
|
||||
@Input('login-form') private login: Login
|
||||
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(private formBuilder : FormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
email: ['', Validators.required],
|
||||
password: ['', Validators.required]
|
||||
})
|
||||
}
|
||||
|
||||
submit() {
|
||||
if(this.form.valid) {
|
||||
this.submitEventEmitter.emit();
|
||||
}
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.form.get('email');
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.form.get('password');
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { MdDialogRef } from '@angular/material';
|
||||
|
||||
import { Login } from './login';
|
||||
import { LoginFormComponent } from './loginForm.component';
|
||||
|
||||
@Component({
|
||||
selector: 'login-modal',
|
||||
template: `
|
||||
<h2 md-dialog-title>Authentification requise</h2>
|
||||
|
||||
<md-dialog-content>
|
||||
<login-form (submit)="submit()" #loginForm="loginForm"></login-form>
|
||||
</md-dialog-content>
|
||||
|
||||
<md-dialog-actions>
|
||||
<button md-button [disabled]="!loginForm.form.valid" (click)="submit()">
|
||||
Login
|
||||
</button>
|
||||
|
||||
<button md-button md-dialog-close>
|
||||
Cancel
|
||||
</button>
|
||||
</md-dialog-actions>
|
||||
`
|
||||
})
|
||||
export class LoginModalComponent {
|
||||
@ViewChild('loginForm') loginForm: LoginFormComponent;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MdDialogRef<LoginModalComponent>,
|
||||
) {}
|
||||
|
||||
submit(): void {
|
||||
let formModel = this.loginForm.form.value;
|
||||
let login: Login = new Login();
|
||||
|
||||
login.email = formModel.email;
|
||||
login.password = formModel.password;
|
||||
|
||||
this.dialogRef.close(login);
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
export class Token {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
$fa-font-path: '~font-awesome/fonts';
|
||||
|
||||
@import '~font-awesome/scss/font-awesome';
|
||||
|
||||
@import '~c3/c3';
|
||||
|
||||
@import '~ngx-toastr/toastr';
|
||||
|
||||
$roboto-font-path: '~roboto-fontface/fonts/roboto/';
|
||||
@import '~@angular/material/_theming';
|
||||
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
|
||||
@import '~material-design-icons/iconfont/material-icons.css';
|
||||
|
||||
/*.fixed-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Roboto, 'Helvetica Neue', sans-serif;
|
||||
|
||||
// Helps fonts on OSX looks more consistent with other systems
|
||||
// Isn't currently in button styles due to performance concerns
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.acc-content {
|
||||
padding: 32px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mat-toolbar {
|
||||
.acc-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
md-dialog > form {
|
||||
overflow: visible;
|
||||
}*/
|
||||
|
||||
/*
|
||||
// stretch to screen size in fullscreen mode
|
||||
.acc-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*/
|
||||
|
||||
.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.stroke {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.c3-ygrid-line.zeroline line {
|
||||
stroke: orange;
|
||||
}
|
||||
|
||||
.c3-ygrid-line.overdraft line {
|
||||
stroke: #FF0000;
|
||||
}
|
||||
|
||||
span.warning {
|
||||
color: mat-color($mat-amber, A700);
|
||||
}
|
||||
|
||||
.mat-row.warning {
|
||||
background-color: mat-color($mat-amber, 100);
|
||||
}
|
||||
|
||||
span.error {
|
||||
color: mat-color($mat-red, A700);
|
||||
}
|
||||
|
||||
.mat-row.error {
|
||||
background-color: mat-color($mat-red, 100);
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||
|
||||
platformBrowserDynamic().bootstrapModule(AppModule);
|
@ -1,177 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
import * as moment from 'moment';
|
||||
import * as c3 from 'c3';
|
||||
|
||||
import {
|
||||
Component, ElementRef,
|
||||
Inject, Input, Output, EventEmitter,
|
||||
OnInit, OnChanges
|
||||
} from '@angular/core';
|
||||
|
||||
import { Account } from '../accounts/account';
|
||||
import { DailyBalanceService } from '../accounts/dailyBalance.service';
|
||||
|
||||
class DateRange {
|
||||
minDate: Date;
|
||||
maxDate: Date;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'balance-chart',
|
||||
template: '<div></div>'
|
||||
})
|
||||
export class BalanceChartComponent implements OnInit, OnChanges {
|
||||
@Input() account: Account;
|
||||
@Output() onUpdate: EventEmitter<DateRange> = new EventEmitter<DateRange>();
|
||||
|
||||
private chart: c3.ChartAPI;
|
||||
private balances: number[];
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private dailyBalanceService: DailyBalanceService,
|
||||
) {
|
||||
}
|
||||
|
||||
loadData(account: Account) {
|
||||
this.dailyBalanceService.query(
|
||||
account.id
|
||||
).subscribe((results) => {
|
||||
var headers: any[][] = [['date', 'balances', 'expenses', 'revenues']];
|
||||
|
||||
var rows = results.map(function(result) {
|
||||
return [
|
||||
result.operation_date,
|
||||
result.balance,
|
||||
result.expenses,
|
||||
result.revenues
|
||||
];
|
||||
});
|
||||
|
||||
this.chart.unload();
|
||||
|
||||
this.chart.load({
|
||||
rows: headers.concat(rows)
|
||||
});
|
||||
|
||||
var x: any;
|
||||
|
||||
x = this.chart.x();
|
||||
|
||||
var balances = x.balances;
|
||||
|
||||
this.onUpdate.emit({
|
||||
minDate: balances[0],
|
||||
maxDate: balances[balances.length - 1]
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
var tomorrow = moment().endOf('day').valueOf();
|
||||
|
||||
this.chart = c3.generate({
|
||||
bindto: this.elementRef.nativeElement.children[0],
|
||||
size: {
|
||||
height: 450,
|
||||
},
|
||||
data: {
|
||||
x: 'date',
|
||||
rows: [],
|
||||
axes: {
|
||||
expenses: 'y2',
|
||||
revenues: 'y2'
|
||||
},
|
||||
type: 'bar',
|
||||
types: {
|
||||
balances: 'area'
|
||||
},
|
||||
groups: [
|
||||
['expenses', 'revenues']
|
||||
],
|
||||
// Disable for the moment because there is an issue when
|
||||
// using subchart line is not refreshed after subset
|
||||
// selection.
|
||||
//regions: {
|
||||
// balances: [{
|
||||
// start: tomorrow,
|
||||
// style: 'dashed'
|
||||
// }]
|
||||
//}
|
||||
},
|
||||
regions: [{
|
||||
start: tomorrow,
|
||||
}],
|
||||
axis: {
|
||||
x: {
|
||||
type: 'timeseries',
|
||||
tick: {
|
||||
format: '%Y-%m-%d',
|
||||
rotate: 50,
|
||||
}
|
||||
},
|
||||
y: {
|
||||
label: {
|
||||
text: 'Amount',
|
||||
position: 'outer-middle'
|
||||
}
|
||||
},
|
||||
y2: {
|
||||
show: true,
|
||||
label: {
|
||||
text: 'Amount',
|
||||
position: 'outer-middle'
|
||||
}
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
x: {
|
||||
show: true,
|
||||
},
|
||||
y: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
format: {
|
||||
value: function(value, ratio, id, index) {
|
||||
return value + '€';
|
||||
}
|
||||
}
|
||||
},
|
||||
subchart: {
|
||||
show: true,
|
||||
onbrush: (domain) => {
|
||||
this.onUpdate.emit({minDate: domain[0], maxDate: domain[1]});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
setLines(account: Account) {
|
||||
if(this.chart) {
|
||||
this.chart.ygrids([
|
||||
{ value: 0, axis: 'y2' },
|
||||
{ value: 0, axis: 'y', class: 'zeroline'},
|
||||
]);
|
||||
|
||||
if(account) {
|
||||
this.chart.ygrids.add({
|
||||
value: account.authorized_overdraft,
|
||||
axis: 'y',
|
||||
class: 'overdraft'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ngOnChanges(changes) {
|
||||
if('account' in changes && changes.account.currentValue) {
|
||||
this.loadData(changes.account.currentValue);
|
||||
this.setLines(changes.account.currentValue);
|
||||
} else {
|
||||
this.setLines(this.account);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
import * as moment from 'moment';
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { HttpClient, HttpParams } from "@angular/common/http";
|
||||
|
||||
import { Category } from './category';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
formatDate(date: Date|string) {
|
||||
if(date instanceof Date) {
|
||||
return moment(date).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
query(id: number, minDate: Date = null, maxDate: Date = null): Observable<Category[]> {
|
||||
let params: HttpParams = new HttpParams();
|
||||
|
||||
if(minDate) {
|
||||
params = params.set('begin', this.formatDate(minDate));
|
||||
}
|
||||
|
||||
if(maxDate) {
|
||||
params = params.set('end', this.formatDate(maxDate));
|
||||
}
|
||||
|
||||
return this.http.get<Category[]>(`/api/account/${id}/category`, { params: params});
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
export class Category {
|
||||
category: string;
|
||||
expenses: number;
|
||||
revenues: number;
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import * as c3 from 'c3';
|
||||
|
||||
import {
|
||||
Component, ElementRef,
|
||||
Inject, Input, Output,
|
||||
OnInit, OnChanges
|
||||
} from '@angular/core';
|
||||
|
||||
import { Account } from '../accounts/account';
|
||||
import { CategoryService } from './category.service';
|
||||
|
||||
@Component({
|
||||
selector: 'category-chart',
|
||||
template: '<div></div>'
|
||||
})
|
||||
export class CategoryChartComponent implements OnInit, OnChanges {
|
||||
@Input() minDate: Date;
|
||||
@Input() maxDate: Date;
|
||||
@Input() account: Account;
|
||||
|
||||
chart: c3.ChartAPI;
|
||||
|
||||
constructor(
|
||||
private elementRef: ElementRef,
|
||||
private categoryService: CategoryService,
|
||||
) {}
|
||||
|
||||
loadData(account: Account) {
|
||||
this.categoryService.query(
|
||||
account.id,
|
||||
this.minDate,
|
||||
this.maxDate
|
||||
).subscribe((results) => {
|
||||
var expenses=[],
|
||||
revenues=[],
|
||||
colors={},
|
||||
names={};
|
||||
|
||||
var revenuesColor = 'green',
|
||||
expensesColor = 'orange';
|
||||
|
||||
for(let result of results) {
|
||||
if(result.revenues > 0) {
|
||||
var revenuesName = 'revenues-' + result.category;
|
||||
|
||||
revenues.push([revenuesName, result.revenues]);
|
||||
names[revenuesName] = result.category;
|
||||
colors[revenuesName] = revenuesColor;
|
||||
}
|
||||
|
||||
if(result.expenses < 0) {
|
||||
var expensesName = 'expenses-' + result.category;
|
||||
|
||||
expenses.splice(0, 0, [expensesName, -result.expenses]);
|
||||
names[expensesName] = result.category;
|
||||
colors[expensesName] = expensesColor;
|
||||
}
|
||||
};
|
||||
|
||||
this.chart.unload();
|
||||
|
||||
this.chart.load({
|
||||
columns: revenues.concat(expenses),
|
||||
names: names,
|
||||
colors: colors
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
this.chart = c3.generate({
|
||||
bindto: this.elementRef.nativeElement.children[0],
|
||||
data: {
|
||||
columns: [],
|
||||
type: 'donut',
|
||||
order: null,
|
||||
},
|
||||
tooltip: {
|
||||
format: {
|
||||
value: function(value, ratio, id, index) {
|
||||
return value + '€';
|
||||
}
|
||||
}
|
||||
},
|
||||
donut: {
|
||||
label: {
|
||||
format: function(value) {
|
||||
return value + '€';
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ngOnChanges(changes) {
|
||||
if('account' in changes && changes.account.currentValue) {
|
||||
this.loadData(changes.account.currentValue);
|
||||
} else if (this.account) {
|
||||
this.loadData(this.account);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { TextMaskModule } from 'angular2-text-mask';
|
||||
|
||||
import { BalanceChartComponent } from './balanceChart.component';
|
||||
import { CategoryChartComponent } from './categoryChart.component';
|
||||
import { OperationRowComponent } from './operationRow.component';
|
||||
import { CategoryService } from './category.service';
|
||||
import { OperationService } from './operation.service';
|
||||
import { OperationListComponent } from './operationList.component';
|
||||
import { OperationDeleteModalComponent } from './operationDeleteModal.component';
|
||||
import { OperationFormComponent } from './operationForm.component';
|
||||
import { OperationEditModalComponent } from './operationEditModal.component'
|
||||
import { OperationListState } from './operation.states'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule.forChild([
|
||||
OperationListState
|
||||
]),
|
||||
NgLoggerModule,
|
||||
ToastrModule,
|
||||
NgbModule,
|
||||
TextMaskModule
|
||||
],
|
||||
providers: [
|
||||
CategoryService,
|
||||
OperationService,
|
||||
],
|
||||
declarations: [
|
||||
BalanceChartComponent,
|
||||
CategoryChartComponent,
|
||||
OperationRowComponent,
|
||||
OperationListComponent,
|
||||
OperationDeleteModalComponent,
|
||||
OperationFormComponent,
|
||||
OperationEditModalComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
OperationDeleteModalComponent,
|
||||
OperationEditModalComponent,
|
||||
OperationListComponent,
|
||||
]
|
||||
})
|
||||
export class OperationModule {}
|
@ -1,71 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import * as moment from 'moment';
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Operation } from './operation';
|
||||
|
||||
@Injectable()
|
||||
export class OperationService {
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
) {}
|
||||
|
||||
formatDate(date: Date|string) {
|
||||
if(date instanceof Date) {
|
||||
return moment(date).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
private url(id?: Number): string {
|
||||
if(id) {
|
||||
return `/api/operation/${id}`;
|
||||
}
|
||||
|
||||
return `/api/operation`;
|
||||
}
|
||||
|
||||
query(
|
||||
accountId: number,
|
||||
minDate: Date|string = null,
|
||||
maxDate: Date|string = null
|
||||
): Observable<Operation[]> {
|
||||
let params = new HttpParams();
|
||||
|
||||
params = params.set('account_id', `${accountId}`);
|
||||
|
||||
if(minDate) {
|
||||
params = params.set('begin', this.formatDate(minDate));
|
||||
}
|
||||
|
||||
if(maxDate) {
|
||||
params = params.set('end', this.formatDate(maxDate));
|
||||
}
|
||||
|
||||
return this.http.get<Operation[]>(this.url(), {
|
||||
params: params
|
||||
});
|
||||
}
|
||||
|
||||
get(id: number): Observable<Operation> {
|
||||
return this.http.get<Operation>(this.url(id));
|
||||
}
|
||||
|
||||
create(operation: Operation): Observable<Operation> {
|
||||
return this.http.post<Operation>(this.url(), operation);
|
||||
}
|
||||
|
||||
update(operation: Operation): Observable<Operation> {
|
||||
return this.http.post<Operation>(this.url(operation.id), operation);
|
||||
}
|
||||
|
||||
delete(operation: Operation): Observable<Operation> {
|
||||
return this.http.delete<Operation>(this.url(operation.id));
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { OperationListComponent } from './operationList.component';
|
||||
|
||||
export const OperationListState = {
|
||||
path: 'account/:accountId/operations',
|
||||
component: OperationListComponent
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
export class Operation {
|
||||
id: number;
|
||||
operation_date: string;
|
||||
label: string;
|
||||
value: number;
|
||||
category: string;
|
||||
scheduled_operation_id: number;
|
||||
account_id: number;
|
||||
balance: number;
|
||||
confirmed: boolean;
|
||||
pointed: boolean;
|
||||
cancelled: boolean
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Input } from '@angular/core';
|
||||
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { Operation } from './operation';
|
||||
|
||||
@Component({
|
||||
selector: 'operation-delete-modal',
|
||||
template: `
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">Delete Operation #{{ operation.id }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" id="modal-body">
|
||||
<p>
|
||||
Do you really want to delete operation #{{ operation.id }} with label:<br/>
|
||||
{{ operation.label }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-danger" (click)="submit()">
|
||||
Yes
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" (click)="cancel()">
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class OperationDeleteModalComponent {
|
||||
@Input() operation: Operation
|
||||
|
||||
constructor(private activeModal: NgbActiveModal) {}
|
||||
|
||||
submit(): void {
|
||||
this.activeModal.close(this.operation);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.activeModal.dismiss("closed");
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { Operation } from './operation';
|
||||
import { OperationFormComponent } from './operationForm.component';
|
||||
|
||||
@Component({
|
||||
selector: 'operation-edit-modal',
|
||||
template: `
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" id="modal-body">
|
||||
<operation-form [operation]="operation" (submit)="submit()" #operationForm="operationForm"></operation-form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" [disabled]="!operationForm.form.valid" (click)="submit()">
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" (click)="cancel()">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class OperationEditModalComponent {
|
||||
@Input() operation: Operation;
|
||||
@ViewChild('operationForm') operationForm: OperationFormComponent;
|
||||
|
||||
valid: boolean = false;
|
||||
|
||||
constructor(private activeModal: NgbActiveModal) {}
|
||||
|
||||
title(): string {
|
||||
if(this.operation.id) {
|
||||
return "Operation #" + this.operation.id;
|
||||
} else {
|
||||
return "New operation";
|
||||
}
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
let formModel = this.operationForm.form.value;
|
||||
let operation = Object.assign({}, this.operation);
|
||||
|
||||
operation.id = this.operation.id;
|
||||
operation.operation_date = formModel.operationDate;
|
||||
operation.label = formModel.label;
|
||||
operation.value = formModel.value;
|
||||
operation.category = formModel.category;
|
||||
|
||||
this.activeModal.close(operation);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.activeModal.dismiss("closed");
|
||||
}
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, OnInit, OnChanges, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { Operation } from './operation';
|
||||
|
||||
@Component({
|
||||
selector: 'operation-form',
|
||||
exportAs: 'operationForm',
|
||||
template: `
|
||||
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 control-label" for="operation-date">Date</label>
|
||||
|
||||
<div class="col-sm-8"
|
||||
[class.has-danger]="operationDate.errors">
|
||||
<input class="form-control"
|
||||
id="operation-date" formControlName="operationDate"
|
||||
[textMask]="{mask: dateMask}"
|
||||
placeholder="Operation date">
|
||||
|
||||
<div class="help-block text-danger" *ngIf="operationDate.errors">
|
||||
<p *ngIf="operationDate.errors.required">The operation date is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 control-label" for="label">Label</label>
|
||||
|
||||
<div class="col-sm-8"
|
||||
[class.has-danger]="label.errors">
|
||||
<input class="form-control"
|
||||
id="label" formControlName="label"
|
||||
placeholder="Label">
|
||||
|
||||
<div class="help-block text-danger" *ngIf="label.errors">
|
||||
<p *ngIf="label.errors.required">The operation label is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 control-label" for="value">Montant</label>
|
||||
|
||||
<div class="col-sm-8"
|
||||
[class.has-errors]="value.errors">
|
||||
<input class="form-control"
|
||||
id="value" formControlName="value"
|
||||
type="number" placeholder="Value">
|
||||
|
||||
<div class="help-block text-danger" *ngIf="value.errors">
|
||||
<p *ngIf="value.errors.required">The operation value is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 control-label" for="category">Catégorie</label>
|
||||
|
||||
<div class="col-sm-8"
|
||||
[class.has-errors]="category.errors">
|
||||
<input class="form-control"
|
||||
id="category" formControlName="category"
|
||||
placeholder="Category">
|
||||
|
||||
<div class="help-block text-danger" *ngIf="category.errors">
|
||||
<p *ngIf="category.errors.required">The operation category is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class OperationFormComponent implements OnInit {
|
||||
public form: FormGroup;
|
||||
@Input() operation: Operation;
|
||||
@Output() submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
//dateMask = [/\d{4}/, '-', /0[1-9]|1[0-2]/, '-', /[0-2]\d|3[0-1]/];
|
||||
dateMask = ['2', '0', /\d/, /\d/, '-', /[0-1]/, /\d/, '-', /[0-3]/, /\d/];
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
operationDate: ['', Validators.required],
|
||||
label: ['', Validators.required],
|
||||
value: ['', Validators.required],
|
||||
category: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.form.patchValue({
|
||||
operationDate: this.operation.operation_date,
|
||||
label: this.operation.label,
|
||||
value: this.operation.value,
|
||||
category: this.operation.category,
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
if(this.form.valid) {
|
||||
this.submitEventEmitter.emit();
|
||||
}
|
||||
}
|
||||
|
||||
get operationDate() {
|
||||
return this.form.get('operationDate');
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.form.get('label');
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.form.get('value');
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.form.get('category');
|
||||
}
|
||||
}
|
@ -1,150 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
import { Account } from '../accounts/account';
|
||||
import { AccountService } from '../accounts/account.service';
|
||||
import { Operation } from './operation';
|
||||
import { OperationService } from './operation.service';
|
||||
import { OperationEditModalComponent } from './operationEditModal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'operation-list',
|
||||
template: `
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<balance-chart (onUpdate)="onUpdate($event)"
|
||||
[account]="account"></balance-chart>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<category-chart
|
||||
[minDate]="minDate"
|
||||
[maxDate]="maxDate"
|
||||
[account]="account"></category-chart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<table class="table table-striped table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Date d'op.</th>
|
||||
<th>Libellé de l'opération</th>
|
||||
<th>Montant</th>
|
||||
<th>Solde</th>
|
||||
<th>Catégorie</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<button class="btn btn-success" (click)="add()">
|
||||
Ajouter
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr [operation-row]="operation"
|
||||
[account]="account"
|
||||
(needsReload)="load(minDate, maxDate)"
|
||||
*ngFor="let operation of operations">
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class OperationListComponent implements OnInit {
|
||||
private account: Account;
|
||||
private minDate: Date;
|
||||
private maxDate: Date;
|
||||
private operations: Operation[];
|
||||
|
||||
constructor(
|
||||
private toastrService: ToastrService,
|
||||
private operationService: OperationService,
|
||||
private accountService: AccountService,
|
||||
private logger: Logger,
|
||||
private ngbModal: NgbModal,
|
||||
private route: ActivatedRoute
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.accountService.get(
|
||||
+this.route.snapshot.paramMap.get('accountId')
|
||||
).subscribe(account => {
|
||||
this.account = account
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Add an empty operation.
|
||||
*/
|
||||
add() {
|
||||
var operation = new Operation();
|
||||
operation.account_id = this.account.id;
|
||||
|
||||
// FIXME Alexis Lahouze 2017-06-15 i18n
|
||||
const modal = this.ngbModal.open(OperationEditModalComponent, {
|
||||
size: 'lg'
|
||||
});
|
||||
|
||||
modal.componentInstance.operation = operation;
|
||||
|
||||
modal.result.then((operation: Operation) => {
|
||||
this.save(operation);
|
||||
}, (reason) => {
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Load operations.
|
||||
*/
|
||||
load(minDate, maxDate) {
|
||||
this.minDate = minDate;
|
||||
this.maxDate = maxDate;
|
||||
|
||||
return this.operationService.query(
|
||||
this.account.id,
|
||||
minDate,
|
||||
maxDate
|
||||
).subscribe((operations: Operation[]) => {
|
||||
this.operations = operations.reverse();
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Save an operation and return a promise.
|
||||
*/
|
||||
save(operation) {
|
||||
operation.confirmed = true;
|
||||
|
||||
return this.operationService.create(operation).subscribe(
|
||||
(operation) => {
|
||||
this.toastrService.success('Operation #' + operation.id + ' saved.');
|
||||
|
||||
this.load(this.minDate, this.maxDate);
|
||||
}, (result) => {
|
||||
this.toastrService.error(
|
||||
'Error while saving operation: ' + result.message
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onUpdate(dateRange) {
|
||||
this.load(dateRange.minDate, dateRange.maxDate);
|
||||
};
|
||||
};
|
@ -1,153 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, Inject, Input, Output, EventEmitter } from '@angular/core';
|
||||
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
import { Account } from '../accounts/account';
|
||||
import { Operation } from './operation';
|
||||
import { OperationService } from './operation.service';
|
||||
import { OperationDeleteModalComponent } from './operationDeleteModal.component';
|
||||
import { OperationEditModalComponent } from './operationEditModal.component';
|
||||
|
||||
@Component({
|
||||
selector: 'tr[operation-row]',
|
||||
host: {
|
||||
"[id]": "operation.id",
|
||||
"[class.stroke]": "operation.canceled",
|
||||
"[class.italic]": "!operation.confirmed",
|
||||
"[class.warning]": "operation.balance < 0",
|
||||
"[class.danger]": "operation.balance < account.authorized_overdraft"
|
||||
},
|
||||
template: `
|
||||
<td>{{ operation.id }}</td>
|
||||
|
||||
<td>{{ operation.operation_date | date:"yyyy-MM-dd" }}</td>
|
||||
|
||||
<td>{{ operation.label }}</td>
|
||||
|
||||
<td>{{ operation.value | currency:'EUR':true }}</td>
|
||||
|
||||
<td [class.text-warning]="operation.balance < 0"
|
||||
[class.text-danger]="operation.balance < account.authorized_overdraft">
|
||||
{{ operation.balance | currency:'EUR':true }}
|
||||
</td>
|
||||
|
||||
<td>{{ operation.category }}</td>
|
||||
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<!-- Edit operation, for non-canceled operation. -->
|
||||
<button type="button" class="btn btn-success"
|
||||
*ngIf="!operation.canceled"
|
||||
(click)="modify(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-secondary"
|
||||
*ngIf="!operation.canceled"
|
||||
(click)="togglePointed(operation)"
|
||||
[class.active]="operation.pointed" title="point">
|
||||
<span class="fa" [class.fa-check-square-o]="operation.pointed"
|
||||
[class.fa-square-o]="!operation.pointed"></span>
|
||||
</button>
|
||||
|
||||
<!-- Toggle canceled operation. -->
|
||||
<button type="button" class="btn btn-warning"
|
||||
(click)="toggleCanceled(operation)"
|
||||
*ngIf="operation.scheduled_operation_id"
|
||||
[class.active]="operation.canceled" title="cancel">
|
||||
<span class="fa fa-remove"></span>
|
||||
</button>
|
||||
|
||||
<!-- Delete operation, with confirm. -->
|
||||
<button type="button" class="btn btn-danger"
|
||||
(click)="confirmDelete(operation)"
|
||||
*ngIf="operation.id && !operation.scheduled_operation_id">
|
||||
<span class="fa fa-trash-o"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
`
|
||||
})
|
||||
export class OperationRowComponent {
|
||||
@Input('operation-row') operation: Operation;
|
||||
@Input() account: Account;
|
||||
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
constructor(
|
||||
private operationService: OperationService,
|
||||
private toastrService: ToastrService,
|
||||
private ngbModal: NgbModal,
|
||||
) {}
|
||||
|
||||
togglePointed(operation, rowform) {
|
||||
operation.pointed = !operation.pointed;
|
||||
|
||||
this.save(operation);
|
||||
};
|
||||
|
||||
toggleCanceled(operation) {
|
||||
operation.canceled = !operation.canceled;
|
||||
|
||||
this.save(operation);
|
||||
};
|
||||
|
||||
save(operation) {
|
||||
operation.confirmed = true;
|
||||
|
||||
return this.operationService.update(operation).subscribe((operation) => {
|
||||
this.toastrService.success('Operation #' + operation.id + ' saved.');
|
||||
|
||||
this.needsReload.emit();
|
||||
}, (result) => {
|
||||
this.toastrService.error(
|
||||
'Error while saving operation: ' + result.message
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
confirmDelete(operation) {
|
||||
const modal = this.ngbModal.open(OperationDeleteModalComponent);
|
||||
|
||||
modal.componentInstance.operation = this.operation;
|
||||
|
||||
var id = operation.id;
|
||||
|
||||
modal.result.then((operation: Operation) => {
|
||||
this.delete(operation);
|
||||
}, (reason) => {
|
||||
})
|
||||
};
|
||||
|
||||
delete(operation) {
|
||||
var id = operation.id;
|
||||
|
||||
return this.operationService.delete(operation).subscribe(() => {
|
||||
this.toastrService.success('Operation #' + id + ' deleted.');
|
||||
|
||||
this.needsReload.emit();
|
||||
}, (result) => {
|
||||
this.toastrService.error(
|
||||
'An error occurred while trying to delete operation #' +
|
||||
id + ':<br />' + result
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
modify(operation) {
|
||||
// FIXME Alexis Lahouze 2017-06-15 i18n
|
||||
const modal = this.ngbModal.open(OperationEditModalComponent, {
|
||||
size: 'lg'
|
||||
});
|
||||
|
||||
modal.componentInstance.operation = operation;
|
||||
|
||||
modal.result.then((operation: Operation) => {
|
||||
this.save(operation);
|
||||
}, (reason) => {
|
||||
});
|
||||
};
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
|
||||
import { Schedule } from './schedule';
|
||||
import { ScheduleService } from './schedule.service';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleDataSource extends DataSource<Schedule> {
|
||||
private subject: BehaviorSubject<number> = new BehaviorSubject<number>(null);
|
||||
|
||||
constructor(
|
||||
private scheduleService: ScheduleService,
|
||||
private logger: Logger,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
load(accountId: number): void {
|
||||
this.logger.log("In load", accountId);
|
||||
this.subject.next(accountId);
|
||||
}
|
||||
|
||||
connect(): Observable<Schedule[]> {
|
||||
return this.subject.asObservable().concatMap((accountId: number) => {
|
||||
this.logger.log("In connect", accountId);
|
||||
|
||||
if(accountId) {
|
||||
return this.scheduleService.query(accountId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import {
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdIconModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
MdTableModule,
|
||||
} from '@angular/material';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { TextMaskModule } from 'angular2-text-mask';
|
||||
|
||||
import { ScheduleService } from './schedule.service';
|
||||
import { ScheduleDataSource } from './schedule.dataSource';
|
||||
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
|
||||
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
|
||||
import { ScheduleFormComponent } from './scheduleForm.component';
|
||||
import { ScheduleListComponent } from './scheduleList.component';
|
||||
import { ScheduleListState } from './schedule.states';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
HttpClientModule,
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule.forChild([
|
||||
ScheduleListState
|
||||
]),
|
||||
MdButtonModule,
|
||||
MdDialogModule,
|
||||
MdIconModule,
|
||||
MdInputModule,
|
||||
MdListModule,
|
||||
MdTableModule,
|
||||
NgLoggerModule,
|
||||
ToastrModule,
|
||||
NgbModule,
|
||||
TextMaskModule
|
||||
],
|
||||
providers: [
|
||||
ScheduleService,
|
||||
ScheduleDataSource,
|
||||
],
|
||||
declarations: [
|
||||
ScheduleDeleteModalComponent,
|
||||
ScheduleEditModalComponent,
|
||||
ScheduleFormComponent,
|
||||
ScheduleListComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
ScheduleDeleteModalComponent,
|
||||
ScheduleEditModalComponent,
|
||||
ScheduleListComponent,
|
||||
]
|
||||
})
|
||||
export class ScheduleModule {}
|
@ -1,47 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Schedule } from './schedule';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
constructor(
|
||||
private http: HttpClient
|
||||
) {}
|
||||
|
||||
private url(id?: number): string {
|
||||
if(id) {
|
||||
return `/api/scheduled_operation/${id}`;
|
||||
}
|
||||
|
||||
return `/api/scheduled_operation`;
|
||||
}
|
||||
|
||||
query(accountId: number): Observable<Schedule[]> {
|
||||
let params = new HttpParams().set('account_id', `${accountId}`);
|
||||
|
||||
return this.http.get<Schedule[]>(this.url(), { params: params });
|
||||
}
|
||||
|
||||
get(accountId: number, id: number): Observable<Schedule> {
|
||||
let params = new HttpParams().set('account_id', `${accountId}`);
|
||||
|
||||
return this.http.get<Schedule>(this.url(id), { params: params });
|
||||
}
|
||||
|
||||
create(schedule: Schedule): Observable<Schedule> {
|
||||
return this.http.post<Schedule>(this.url(), schedule);
|
||||
}
|
||||
|
||||
update(schedule: Schedule): Observable<Schedule> {
|
||||
return this.http.post<Schedule>(this.url(schedule.id), schedule);
|
||||
}
|
||||
|
||||
delete(schedule: Schedule): Observable<Schedule> {
|
||||
return this.http.delete<Schedule>(this.url(schedule.id));
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
|
||||
import { ScheduleListComponent } from './scheduleList.component';
|
||||
|
||||
export const ScheduleListState = {
|
||||
path: 'account/:accountId/scheduler',
|
||||
component: ScheduleListComponent,
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
|
||||
export class Schedule {
|
||||
id: number;
|
||||
start_date: string;
|
||||
stop_date: string;
|
||||
day: number;
|
||||
frequency: number;
|
||||
label: string;
|
||||
value: number;
|
||||
category: string;
|
||||
account_id: number
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MD_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'schedule-delete-modal',
|
||||
template: `
|
||||
<h3 md-dialog-title>{{ title() }}</h3>
|
||||
|
||||
<md-dialog-content>
|
||||
Do you really want to delete schedule #{{ data.schedule.id }} with label:<br/>
|
||||
{{ data.schedule.label }}
|
||||
</md-dialog-content>
|
||||
|
||||
<md-dialog-actions>
|
||||
<button md-raised-button color="warn" [md-dialog-close]="data.schedule">
|
||||
Yes
|
||||
</button>
|
||||
|
||||
<button md-raised-button md-dialog-close>
|
||||
No
|
||||
</button>
|
||||
</md-dialog-actions>
|
||||
`
|
||||
})
|
||||
export class ScheduleDeleteModalComponent {
|
||||
constructor(
|
||||
@Inject(MD_DIALOG_DATA) private data: any
|
||||
) {}
|
||||
|
||||
title(): string {
|
||||
return "Delete schedule #" + this.data.schedule.id;
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||
import { Component, Inject, ViewChild } from '@angular/core';
|
||||
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
|
||||
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { Schedule } from './schedule';
|
||||
import { ScheduleFormComponent } from './scheduleForm.component';
|
||||
|
||||
@Component({
|
||||
selector: 'schedule-edit-modal',
|
||||
template: `
|
||||
<h3 md-dialog-title>{{ title() }}</h3>
|
||||
|
||||
<md-dialog-content>
|
||||
<schedule-form [schedule]="schedule" (submit)="submit()" #scheduleForm="scheduleForm"></schedule-form>
|
||||
</md-dialog-content>
|
||||
|
||||
<md-dialog-actions>
|
||||
<button md-raised-button color="primary" [disabled]="!scheduleForm?.form.valid" (click)="submit()">
|
||||
Save
|
||||
</button>
|
||||
|
||||
<button md-raised-button color="warn" md-dialog-close>
|
||||
Cancel
|
||||
</button>
|
||||
</md-dialog-actions>
|
||||
`
|
||||
})
|
||||
export class ScheduleEditModalComponent {
|
||||
private schedule: Schedule;
|
||||
@ViewChild('scheduleForm') scheduleForm: ScheduleFormComponent;
|
||||
|
||||
constructor(
|
||||
@Inject(MD_DIALOG_DATA) public data: any,
|
||||
public dialogRef: MdDialogRef<ScheduleEditModalComponent>,
|
||||
) {
|
||||
this.schedule = data.schedule;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
if(this.schedule.id) {
|
||||
return "Schedule #" + this.schedule.id;
|
||||
} else {
|
||||
return "New schedule";
|
||||
}
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
let formModel = this.scheduleForm.form.value;
|
||||
let schedule = Object.assign({}, this.schedule);
|
||||
|
||||
schedule.id = this.schedule.id;
|
||||
schedule.start_date = formModel.startDate;
|
||||
schedule.stop_date = formModel.stopDate;
|
||||
schedule.day = formModel.day;
|
||||
schedule.frequency = formModel.frequency;
|
||||
schedule.label = formModel.label;
|
||||
schedule.value = formModel.value;
|
||||
schedule.category = formModel.category;
|
||||
|
||||
this.dialogRef.close(schedule);
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
|
||||
import { Schedule } from './schedule';
|
||||
|
||||
@Component({
|
||||
selector: 'schedule-form',
|
||||
exportAs: 'scheduleForm',
|
||||
template: `
|
||||
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
|
||||
<md-list>
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="startDate"
|
||||
[textMask]="{mask: dateMask}"
|
||||
placeholder="Schedule start date">
|
||||
|
||||
<md-error *ngIf="startDate.errors?.required">The start date is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="stopDate"
|
||||
[textMask]="{mask: dateMask}"
|
||||
placeholder="Schedule stop date">
|
||||
|
||||
<md-error *ngIf="stopDate.errors?.required">The stop date is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="day"
|
||||
type="number" placeholder="Day">
|
||||
|
||||
<md-error *ngIf="day.errors?.required">The day is required.</md-error>
|
||||
<md-error *ngIf="day.errors?.min">The day must be greater than 0.</md-error>
|
||||
<md-error *ngIf="day.errors?.max">The day must be less than or equal to 31.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="frequency"
|
||||
type="number" placeholder="Frequency">
|
||||
|
||||
<md-error *ngIf="frequency.errors?.required">The frequency is required.</md-error>
|
||||
<md-error *ngIf="frequency.errors?.min">The frequency must be positive.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="label" placeholder="Label">
|
||||
|
||||
<md-error *ngIf="label.errors?.required">The label is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="value" type="number" placeholder="Value">
|
||||
|
||||
<md-error *ngIf="value.errors?.required">The value is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
|
||||
<md-list-item>
|
||||
<md-form-field>
|
||||
<input mdInput formControlName="category"
|
||||
placeholder="Category">
|
||||
|
||||
<md-error *ngIf="category.errors?.required">The category is required.</md-error>
|
||||
</md-form-field>
|
||||
</md-list-item>
|
||||
</md-list>
|
||||
</form>
|
||||
`
|
||||
})
|
||||
export class ScheduleFormComponent implements OnInit {
|
||||
public form: FormGroup;
|
||||
@Input() schedule: Schedule;
|
||||
@Output('submit') submitEventEmitter: EventEmitter<void> = new EventEmitter<void>();
|
||||
|
||||
//dateMask = [/\d{4}/, '-', /0[1-9]|1[0-2]/, '-', /[0-2]\d|3[0-1]/];
|
||||
dateMask = ['2', '0', /\d/, /\d/, '-', /[0-1]/, /\d/, '-', /[0-3]/, /\d/];
|
||||
|
||||
constructor(private formBuilder: FormBuilder) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = this.formBuilder.group({
|
||||
startDate: ['', Validators.required],
|
||||
stopDate: ['', Validators.required],
|
||||
day: ['', [Validators.required, Validators.min(1), Validators.max(31)]],
|
||||
frequency: ['', [Validators.required, Validators.min(0)]],
|
||||
label: ['', Validators.required],
|
||||
value: ['', Validators.required],
|
||||
category: ['', Validators.required],
|
||||
});
|
||||
|
||||
this.form.patchValue({
|
||||
startDate: this.schedule.start_date,
|
||||
stopDate: this.schedule.stop_date,
|
||||
day: this.schedule.day,
|
||||
frequency: this.schedule.frequency,
|
||||
label: this.schedule.label,
|
||||
value: this.schedule.value,
|
||||
category: this.schedule.category,
|
||||
});
|
||||
}
|
||||
|
||||
submit() {
|
||||
if(this.form.valid) {
|
||||
this.submitEventEmitter.emit();
|
||||
}
|
||||
}
|
||||
|
||||
get startDate() {
|
||||
return this.form.get('startDate');
|
||||
}
|
||||
|
||||
get stopDate() {
|
||||
return this.form.get('stopDate');
|
||||
}
|
||||
|
||||
get day() {
|
||||
return this.form.get('day');
|
||||
}
|
||||
|
||||
get frequency() {
|
||||
return this.form.get('frequency');
|
||||
}
|
||||
|
||||
get label() {
|
||||
return this.form.get('label');
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.form.get('value');
|
||||
}
|
||||
|
||||
get category() {
|
||||
return this.form.get('category');
|
||||
}
|
||||
}
|
@ -1,201 +0,0 @@
|
||||
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { MdDialog } from '@angular/material';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { Observable } from 'rxjs/Rx';
|
||||
|
||||
import { Logger } from '@nsalaun/ng-logger';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
import { ScheduleDataSource } from './schedule.dataSource';
|
||||
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
|
||||
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
|
||||
import { ScheduleService } from './schedule.service';
|
||||
import { Schedule } from './schedule';
|
||||
|
||||
@Component({
|
||||
selector: 'schedule-list',
|
||||
template: `
|
||||
<div class="containerX">
|
||||
<div class="container">
|
||||
<button md-fab color="primary" (click)="add()">
|
||||
<md-icon>add</md-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<md-table #table [dataSource]="schedules">
|
||||
<ng-container mdColumnDef="start_date">
|
||||
<md-header-cell *mdHeaderCellDef>Date de début</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.start_date | date: "yyyy-MM-dd" }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="stop_date">
|
||||
<md-header-cell *mdHeaderCellDef>Date de fin</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.stop_date | date: "yyyy-MM-dd" }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="day">
|
||||
<md-header-cell *mdHeaderCellDef>Jour</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.day }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="frequency">
|
||||
<md-header-cell *mdHeaderCellDef>Fréq.</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.frequency }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="label">
|
||||
<md-header-cell *mdHeaderCellDef>Libellé de l'opération</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.label }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="value">
|
||||
<md-header-cell *mdHeaderCellDef>Montant</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.value | currency: "EUR":true }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="category">
|
||||
<md-header-cell *mdHeaderCellDef>Catégorie</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
{{ schedule.category }}
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container mdColumnDef="actions">
|
||||
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
|
||||
<md-cell *mdCellDef="let schedule">
|
||||
<!-- Edit operation. -->
|
||||
<button md-mini-fab color="primary" (click)="modify(schedule)">
|
||||
<md-icon>mode_edit</md-icon>
|
||||
</button>
|
||||
|
||||
<!-- Remove operation. -->
|
||||
<button md-mini-fab color="warn" [hidden]="!schedule.id"
|
||||
(click)="confirmDelete(schedule)">
|
||||
<md-icon>delete_forever</md-icon>
|
||||
</button>
|
||||
</md-cell>
|
||||
</ng-container>
|
||||
|
||||
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
|
||||
<md-row *mdRowDef="let row; columns: displayedColumns;">
|
||||
</md-row>
|
||||
</md-table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ScheduleListComponent implements OnInit {
|
||||
private accountId: number;
|
||||
|
||||
private displayedColumns: String[] = [
|
||||
'start_date', 'stop_date', 'day', 'frequency',
|
||||
'label', 'value', 'category', 'actions'
|
||||
];
|
||||
|
||||
constructor(
|
||||
private toastrService: ToastrService,
|
||||
private scheduleService: ScheduleService,
|
||||
private logger: Logger,
|
||||
private ngbModal: NgbModal,
|
||||
private route: ActivatedRoute,
|
||||
private schedules: ScheduleDataSource,
|
||||
private mdDialog: MdDialog,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.logger.log("ngOnInit");
|
||||
this.accountId = +this.route.snapshot.paramMap.get('accountId')
|
||||
// Load operations on controller initialization.
|
||||
this.load();
|
||||
}
|
||||
|
||||
load() {
|
||||
this.logger.log("Loading schedules for accountId", this.accountId);
|
||||
if(!this.accountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.schedules.load(this.accountId);
|
||||
}
|
||||
|
||||
/*
|
||||
* Add a new operation at the beginning of th array.
|
||||
*/
|
||||
add() {
|
||||
this.modify(new Schedule());
|
||||
};
|
||||
|
||||
modify(schedule: Schedule) {
|
||||
let dialogRef = this.mdDialog.open(ScheduleEditModalComponent, {
|
||||
data: {
|
||||
schedule: schedule,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
|
||||
if(schedule) {
|
||||
this.save(schedule);
|
||||
}
|
||||
}, (reason) => function(reason) {
|
||||
});
|
||||
}
|
||||
|
||||
save(schedule: Schedule) {
|
||||
return this.scheduleService.create(schedule).subscribe((schedule: Schedule) => {
|
||||
this.toastrService.success('Schedule #' + schedule.id + ' saved.');
|
||||
|
||||
this.load();
|
||||
}, (result) => {
|
||||
this.toastrService.error(
|
||||
'Error while saving schedule: ' + result.message
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
confirmDelete(schedule: Schedule) {
|
||||
let dialogRef = this.mdDialog.open(ScheduleDeleteModalComponent, {
|
||||
data: {
|
||||
schedule: schedule,
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
|
||||
if(schedule) {
|
||||
this.delete(schedule);
|
||||
}
|
||||
}, (reason) => function(reason) {
|
||||
this.logger.error("Delete dialog failed", reason);
|
||||
});
|
||||
}
|
||||
|
||||
delete(schedule: Schedule) {
|
||||
var id = schedule.id;
|
||||
|
||||
return this.scheduleService.delete(schedule).subscribe(() => {
|
||||
this.toastrService.success('Schedule #' + id + ' deleted.');
|
||||
|
||||
this.load();
|
||||
}, result => {
|
||||
this.toastrService.error(
|
||||
'An error occurred while trying to delete schedule #' + id + ':<br />'
|
||||
+ result.message
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"noImplicitAny": false,
|
||||
"suppressImplicitAnyIndexErrors": true
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
/* jshint esversion: 6 */
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
context: path.resolve(__dirname, 'src'),
|
||||
entry: {
|
||||
"main": [
|
||||
'./main.ts'
|
||||
],
|
||||
"styles": [
|
||||
'./main.scss'
|
||||
]
|
||||
},
|
||||
devtool: 'source-map',
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.html'],
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
enforce: 'pre',
|
||||
test: /webpack\.config\.js$/,
|
||||
include: path.resolve(__dirname),
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
useEslintrc: false,
|
||||
emitWarning: true,
|
||||
emitError: true,
|
||||
failOnWarning: true,
|
||||
failOnError: true,
|
||||
baseConfig: 'webpack',
|
||||
rules: {
|
||||
indent: ['error', 4]
|
||||
},
|
||||
},
|
||||
}, {
|
||||
// Javascript
|
||||
enforce: 'pre',
|
||||
test: /\.js$/,
|
||||
//include: path.resolve(__dirname, 'src'),
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
useEslintrc: false,
|
||||
emitWarning: false,
|
||||
emitError: true,
|
||||
failOnWarning: false,
|
||||
failOnError: true,
|
||||
baseConfig: 'angular',
|
||||
rules: {
|
||||
indent: ['error', 4]
|
||||
},
|
||||
plugins: [
|
||||
'angular',
|
||||
'html',
|
||||
'security',
|
||||
'this',
|
||||
'jquery',
|
||||
'promise'
|
||||
]
|
||||
},
|
||||
}, {
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader'
|
||||
}, {
|
||||
// Typescript linting
|
||||
enforce: 'pre',
|
||||
test: /\.ts$/,
|
||||
loader: 'tslint-loader',
|
||||
options: {
|
||||
configuration: {
|
||||
extends: [
|
||||
"tslint:latest",
|
||||
"codelyzer"
|
||||
],
|
||||
rules: {
|
||||
//quotemark: [true, 'single']
|
||||
}
|
||||
},
|
||||
configFile: 'tslint-custom.json',
|
||||
emitErrors: true,
|
||||
failOnHint: true,
|
||||
typeCheck: true,
|
||||
tsConfigFile: 'tsconfig.json',
|
||||
formatter: 'verbose',
|
||||
formattersDirectory: 'node_modules/tslint/lib/formatters/',
|
||||
}
|
||||
}, {
|
||||
test: /\.ts$/,
|
||||
exclude: /node_modules/,
|
||||
loaders: ['awesome-typescript-loader', 'angular2-template-loader?keepUrl=true']
|
||||
}, {
|
||||
test: /\.html$/,
|
||||
loader: 'raw-loader'
|
||||
}, {
|
||||
test: /\.css$/,
|
||||
loaders: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
]
|
||||
}, {
|
||||
test: /\.scss$/,
|
||||
loaders: [
|
||||
'style-loader',
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
'resolve-url-loader',
|
||||
'sass-loader?sourceMap'
|
||||
]
|
||||
}, {
|
||||
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
|
||||
loader: 'url-loader?limit=100000'
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.ProvidePlugin({
|
||||
"window.jQuery": "jquery"
|
||||
}),
|
||||
new webpack.ContextReplacementPlugin(
|
||||
/angular(\\|\/)core(\\|\/)@angular/,
|
||||
path.resolve(__dirname, './')
|
||||
),
|
||||
new HtmlWebpackPlugin({
|
||||
title: 'Accountant',
|
||||
template: 'index.ejs',
|
||||
hash: false,
|
||||
inject: true,
|
||||
compile: true,
|
||||
minify: false,
|
||||
chunks: 'all'
|
||||
})
|
||||
],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'build'),
|
||||
filename: '[name].bundle.js',
|
||||
chunkFilename: '[name].chunk.js'
|
||||
//publicPath: 'js'
|
||||
},
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5000',
|
||||
secure: false
|
||||
}
|
||||
},
|
||||
hot: true,
|
||||
noInfo: false,
|
||||
quiet: false,
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user