Merge branch 'feature/angular2' into develop
This commit is contained in:
commit
9805e8b2e6
78
package.json
78
package.json
@ -6,50 +6,62 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.25.0",
|
"babel-core": "^6.25.0",
|
||||||
"babel-eslint": "^7.2.3",
|
"babel-eslint": "^7.2.3",
|
||||||
"babel-loader": "^7.0.0",
|
"babel-loader": "^7.1.1",
|
||||||
"bootstrap-webpack": "^0.0.6",
|
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
"eslint": "^4.1.1",
|
"eslint": "^4.4.1",
|
||||||
"eslint-config-angular": "^0.5",
|
"eslint-config-angular": "^0.5.0",
|
||||||
"eslint-config-webpack": "^1.2.3",
|
"eslint-config-webpack": "^1.2.5",
|
||||||
"eslint-loader": "^1.7.1",
|
"eslint-loader": "^1.9.0",
|
||||||
"eslint-plugin-angular": "^3.0.0",
|
"eslint-plugin-angular": "^3.0.0",
|
||||||
"eslint-plugin-html": "^3.0.0",
|
"eslint-plugin-html": "^3.2.0",
|
||||||
"eslint-plugin-jquery": "^1.2",
|
"eslint-plugin-jquery": "^1.2.1",
|
||||||
"eslint-plugin-promise": "^3.5",
|
"eslint-plugin-promise": "^3.5.0",
|
||||||
"eslint-plugin-security": "^1.3",
|
"eslint-plugin-security": "^1.4.0",
|
||||||
"eslint-plugin-this": "^0.2",
|
"eslint-plugin-this": "^0.2.2",
|
||||||
"extract-text-webpack-plugin": "^2.1.2",
|
"extract-text-webpack-plugin": "^3.0.0",
|
||||||
"file-loader": "^0.11.2",
|
"file-loader": "^0.11.2",
|
||||||
"html-loader": "^0.4.5",
|
"html-loader": "^0.5.1",
|
||||||
"html-webpack-plugin": "^2.28.0",
|
"html-webpack-plugin": "^2.30.1",
|
||||||
"htmllint-loader": "^1.3.8",
|
"htmllint-loader": "^1.3.8",
|
||||||
"imports-loader": "^0.7.1",
|
"imports-loader": "^0.7.1",
|
||||||
"less": "^2.7.2",
|
"less": "^2.7.2",
|
||||||
"less-loader": "^4.0.4",
|
"less-loader": "^4.0.5",
|
||||||
"ngtemplate-loader": "^2.0.0",
|
"loglevel": "^1.4.1",
|
||||||
|
"ngtemplate-loader": "^2.0.1",
|
||||||
|
"node-sass": "^4.5.3",
|
||||||
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"url-loader": "^0.5.8",
|
"ts-loader": "^2.3.2",
|
||||||
"webpack": "^3.1.0",
|
"typescript": "^2.4.2",
|
||||||
"webpack-dev-server": "^2.5.1"
|
"url-loader": "^0.5.9",
|
||||||
|
"webpack": "^3.5.4",
|
||||||
|
"webpack-dev-server": "^2.7.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"angular": "^1.6",
|
"@angular/animations": "^4.3.4",
|
||||||
"angular-http-auth": "^1.5",
|
"@angular/common": "^4.3.4",
|
||||||
"angular-messages": "^1.6",
|
"@angular/compiler": "^4.3.4",
|
||||||
"angular-resource": "^1.6",
|
"@angular/core": "^4.3.4",
|
||||||
"angular-route": "^1.6.5",
|
"@angular/forms": "^4.3.4",
|
||||||
"angular-strap": "^2.3.12",
|
"@angular/http": "^4.3.4",
|
||||||
"angular-ui-notification": "^0.3",
|
"@angular/platform-browser": "^4.3.4",
|
||||||
|
"@angular/platform-browser-dynamic": "^4.3.4",
|
||||||
|
"@angular/router": "^4.3.4",
|
||||||
|
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.1",
|
||||||
|
"@nsalaun/ng-logger": "^2.0.1",
|
||||||
|
"@types/c3": "^0.4.44",
|
||||||
|
"@types/node": "^8.0.20",
|
||||||
|
"angular2-text-mask": "^8.0.3",
|
||||||
"base64util": "^1.0.2",
|
"base64util": "^1.0.2",
|
||||||
"bootbox": "^4.4.0",
|
"bootstrap": "4.0.0-beta",
|
||||||
"bootstrap": "^3.3.7",
|
"c3": "^0.4.15",
|
||||||
"bootstrap-additions": "^0.3.1",
|
|
||||||
"c3": "^0.4.13",
|
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"jquery": "^3.2",
|
"jquery": "^3.2.1",
|
||||||
"meanie-angular-storage": "^1.3.1",
|
"moment": "^2.18.1",
|
||||||
"moment": "^2.18"
|
"ngx-toastr": "^6.0.1",
|
||||||
|
"reflect-metadata": "^0.1.10",
|
||||||
|
"rxjs": "^5.4.3",
|
||||||
|
"zone.js": "^0.8.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = function($resourceProvider) {
|
|
||||||
// Keep trailing slashes to avoid redirect by flask.
|
|
||||||
$resourceProvider.defaults.stripTrailingSlashes = false;
|
|
||||||
};
|
|
@ -1,156 +0,0 @@
|
|||||||
var accountFormTmpl = require('./account.form.tmpl.html'),
|
|
||||||
accountDeleteTmpl = require('./account.delete.tmpl.html');
|
|
||||||
|
|
||||||
module.exports = function(Account, AccountBalances, Notification, $log, $modal) {
|
|
||||||
var vm = this;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the class for an account current value compared to authorized
|
|
||||||
* overdraft.
|
|
||||||
*/
|
|
||||||
vm.rowClass = function(account) {
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
if (!account || !account.authorized_overdraft || !account.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
if (account.current < account.authorized_overdraft) {
|
|
||||||
return 'danger';
|
|
||||||
} else if (account.current < 0) {
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Return the class for a value compared to account authorized overdraft.
|
|
||||||
*/
|
|
||||||
vm.valueClass = function(account, value) {
|
|
||||||
if (!account || !value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
if (value < account.authorized_overdraft) {
|
|
||||||
return 'text-danger';
|
|
||||||
} else if (value < 0) {
|
|
||||||
return 'text-warning';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.load = function() {
|
|
||||||
vm.accounts = Account.query({}, function(result) {
|
|
||||||
return result.map(function(item) {
|
|
||||||
item.balances = AccountBalances.get({id: item.id});
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add an empty account.
|
|
||||||
*/
|
|
||||||
vm.add = function() {
|
|
||||||
var account = new Account({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
authorized_overdraft: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Insert account at the begining of the array.
|
|
||||||
return vm.modify(account);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Save account.
|
|
||||||
*/
|
|
||||||
vm.save = function(account) {
|
|
||||||
return account.$save().then(function(data) {
|
|
||||||
Notification.success('Account #' + data.id + ' saved.');
|
|
||||||
|
|
||||||
vm.load();
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}, function(result){
|
|
||||||
$log.error('Error while saving account', account, result);
|
|
||||||
|
|
||||||
Notification.error(
|
|
||||||
'Error while saving account: ' + result.message
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.confirmDelete = function(account) {
|
|
||||||
var title = "Delete account #" + account.id;
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: accountDeleteTmpl,
|
|
||||||
controller: function($scope, title, account, $delete) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.account = account;
|
|
||||||
$scope.$delete = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$delete($scope.account);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
account: account,
|
|
||||||
$delete: vm.delete
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delete an account.
|
|
||||||
*/
|
|
||||||
vm.delete = function(account) {
|
|
||||||
var id = account.id;
|
|
||||||
|
|
||||||
return account.$delete().then(function() {
|
|
||||||
Notification.success('account #' + id + ' deleted.');
|
|
||||||
|
|
||||||
vm.load();
|
|
||||||
|
|
||||||
return account;
|
|
||||||
}, function(result) {
|
|
||||||
Notification.error(
|
|
||||||
'An error occurred while trying to delete account #' +
|
|
||||||
id + ':<br />' + result
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Open the popup to modify the account, save it on confirm.
|
|
||||||
*/
|
|
||||||
vm.modify = function(account) {
|
|
||||||
// FIXME Alexis Lahouze 2017-06-15 i18n
|
|
||||||
var title = "Account";
|
|
||||||
|
|
||||||
if (account.id) {
|
|
||||||
title = title + " #" + account.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: accountFormTmpl,
|
|
||||||
controller: function($scope, title, account, $save) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.account = account;
|
|
||||||
$scope.account.authorized_overdraft *= -1;
|
|
||||||
$scope.$save = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$scope.account.authorized_overdraft *= -1;
|
|
||||||
$save($scope.account);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
account: account,
|
|
||||||
$save: vm.save
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load accounts.
|
|
||||||
vm.load();
|
|
||||||
};
|
|
@ -1,23 +0,0 @@
|
|||||||
<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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<p>Voulez-vous supprimer le compte #{{ account.id }} ayant pour nom :<br/>{{ account.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-danger" type="button" ng-click="$delete()">
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/account/:id', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,72 +0,0 @@
|
|||||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
|
||||||
<!--
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<div class="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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form name="form" class="form-horizontal simple-form" novalidate
|
|
||||||
ng-submit="$save()">
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<div class="form-group" ng-class="{ 'has-error' : form.name.$invalid && !form.name.$pristine }">
|
|
||||||
<label class="col-sm-4 control-label" for="name">Account name</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="name" name="name"
|
|
||||||
ng-model="account.name"
|
|
||||||
type="text" placeholder="Account name"
|
|
||||||
required />
|
|
||||||
|
|
||||||
<div class="help-block" ng-messages="form.name.$error" ng-if="form.name.$invalid">
|
|
||||||
<p ng-message="required">The account name is required.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" ng-class="{ 'has-error' : form.authorized_overdraft.$invalid && !form.authorized_overdraft.$pristine }">
|
|
||||||
<label class="col-sm-4 control-label" for="authorized-overdraft">Authorized overdraft</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-addon">-</div>
|
|
||||||
<input class="form-control" id="authorized-overdraft"
|
|
||||||
name="authorized_overdraft"
|
|
||||||
ng-model="account.authorized_overdraft"
|
|
||||||
type="number" placeholder="Authorized overdraft"
|
|
||||||
required min="0"/>
|
|
||||||
<div class="input-group-addon">.00€</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="help-block" ng-messages="form.authorized_overdraft.$error" ng-if="form.authorized_overdraft.$invalid">
|
|
||||||
<p ng-message="required">The authorized overdraft is required.</p>
|
|
||||||
<p ng-message="min">The authorized overdraft must be equal or greater than 0.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<input class="btn btn-primary" type="submit" ng-disabled="form.$invalid"/>
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
53
src/accounts/account.module.ts
Normal file
53
src/accounts/account.module.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// 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 { AccountService } from './account.service';
|
||||||
|
import { AccountBalancesService } from './accountBalances.service';
|
||||||
|
import { AccountListComponent } from './accountList.component';
|
||||||
|
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
|
||||||
|
import { AccountEditModalComponent } from './accountEditModal.component';
|
||||||
|
import { AccountFormComponent } from './accountForm.component';
|
||||||
|
import { AccountRowComponent } from './accountRow.component';
|
||||||
|
import { DailyBalanceService } from './dailyBalance.service';
|
||||||
|
import { AccountListState } from './account.states'
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientModule,
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule.forChild([
|
||||||
|
AccountListState
|
||||||
|
]),
|
||||||
|
NgLoggerModule,
|
||||||
|
ToastrModule,
|
||||||
|
NgbModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AccountService,
|
||||||
|
AccountBalancesService,
|
||||||
|
DailyBalanceService,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AccountListComponent,
|
||||||
|
AccountDeleteModalComponent,
|
||||||
|
AccountEditModalComponent,
|
||||||
|
AccountFormComponent,
|
||||||
|
AccountRowComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
AccountListComponent,
|
||||||
|
AccountDeleteModalComponent,
|
||||||
|
AccountEditModalComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class AccountModule {}
|
42
src/accounts/account.service.ts
Normal file
42
src/accounts/account.service.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
8
src/accounts/account.states.ts
Normal file
8
src/accounts/account.states.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
import { AccountListComponent } from './accountList.component';
|
||||||
|
|
||||||
|
export const AccountListState = {
|
||||||
|
path: 'accounts',
|
||||||
|
component: AccountListComponent
|
||||||
|
}
|
15
src/accounts/account.ts
Normal file
15
src/accounts/account.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 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,7 +0,0 @@
|
|||||||
module.exports = function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/account/:id/balances', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
18
src/accounts/accountBalances.service.ts
Normal file
18
src/accounts/accountBalances.service.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// 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`);
|
||||||
|
}
|
||||||
|
}
|
6
src/accounts/accountBalances.ts
Normal file
6
src/accounts/accountBalances.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
export class AccountBalances {
|
||||||
|
current: number;
|
||||||
|
pointed: number;
|
||||||
|
future: number;
|
||||||
|
}
|
53
src/accounts/accountDeleteModal.component.ts
Normal file
53
src/accounts/accountDeleteModal.component.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { Account } from './account';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'account-delete-modal',
|
||||||
|
template: `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="modal-body">
|
||||||
|
<p>
|
||||||
|
Do you really want to delete account #{{ account.id }} with name:<br/>
|
||||||
|
{{ account.name }}
|
||||||
|
</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 AccountDeleteModalComponent {
|
||||||
|
@Input() account: Account
|
||||||
|
|
||||||
|
constructor(public activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
|
title(): string {
|
||||||
|
if(this.account.id) {
|
||||||
|
return "Account #" + this.account.id;
|
||||||
|
} else {
|
||||||
|
return "New account";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
this.activeModal.close(this.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.activeModal.dismiss("closed");
|
||||||
|
}
|
||||||
|
}
|
59
src/accounts/accountEditModal.component.ts
Normal file
59
src/accounts/accountEditModal.component.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// 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 { Account } from './account';
|
||||||
|
import { AccountFormComponent } from './accountForm.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'account-edit-modal',
|
||||||
|
template: `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="modal-body">
|
||||||
|
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm"></account-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary" [disabled]="!accountForm.form.valid" (click)="submit()">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" (click)="cancel()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class AccountEditModalComponent {
|
||||||
|
@Input() account: Account;
|
||||||
|
@ViewChild('accountForm') accountForm: AccountFormComponent;
|
||||||
|
|
||||||
|
constructor(private activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
|
title(): string {
|
||||||
|
if(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.activeModal.close(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.activeModal.dismiss("closed");
|
||||||
|
}
|
||||||
|
}
|
91
src/accounts/accountForm.component.ts
Normal file
91
src/accounts/accountForm.component.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
// 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">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="name">
|
||||||
|
Account name
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="name.errors">
|
||||||
|
<input class="form-control"
|
||||||
|
id="name" formControlName="name"
|
||||||
|
placeholder="Account name">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="name.errors">
|
||||||
|
<p *ngIf="name.errors.required">The account name is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="authorized-overdraft">
|
||||||
|
Authorized overdraft
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="authorizedOverdraft.errors">
|
||||||
|
<div class="input-group">
|
||||||
|
<input class="form-control"
|
||||||
|
id="authorized-overdraft" formControlName="authorizedOverdraft"
|
||||||
|
placeholder="Authorized overdraft">
|
||||||
|
|
||||||
|
<div class="input-group-addon">.00€</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="authorizedOverdraft.errors">
|
||||||
|
<p *ngIf="authorizedOverdraft.errors.required">
|
||||||
|
The authorized overdraft is required.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p *ngIf="authorizedOverdraft.errors.max">
|
||||||
|
The authorized overdraft must be less than or equal to 0.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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.max(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');
|
||||||
|
}
|
||||||
|
}
|
102
src/accounts/accountList.component.ts
Normal file
102
src/accounts/accountList.component.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
|
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 './account';
|
||||||
|
import { AccountBalances } from './accountBalances';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
import { AccountEditModalComponent } from './accountEditModal.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'account-list',
|
||||||
|
template: `
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-sm table-striped table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nom du compte</th>
|
||||||
|
<th>Solde courant</th>
|
||||||
|
<th>Solde pointé</th>
|
||||||
|
<th>Découvert autorisé</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
<button class="btn btn-success" (click)="add()">
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr *ngFor="let account of accounts"
|
||||||
|
[account-row]="account" (needsReload)="load()">
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class AccountListComponent implements OnInit {
|
||||||
|
accounts: Account[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private toastrService: ToastrService,
|
||||||
|
private logger: Logger,
|
||||||
|
private ngbModal: NgbModal
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Load accounts.
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.logger.log("Load accounts.");
|
||||||
|
this.accountService.query().subscribe(accounts => {
|
||||||
|
this.accounts = accounts;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add an empty account.
|
||||||
|
*/
|
||||||
|
add() {
|
||||||
|
const modal = this.ngbModal.open(AccountEditModalComponent, {
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.componentInstance.account = new Account();
|
||||||
|
|
||||||
|
modal.result.then((account: Account) => {
|
||||||
|
this.logger.log("Modal closed => save account", account);
|
||||||
|
this.save(account);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Save account.
|
||||||
|
*/
|
||||||
|
save(account) {
|
||||||
|
this.accountService.create(account).subscribe(account => {
|
||||||
|
this.toastrService.success('Account #' + account.id + ' saved.');
|
||||||
|
|
||||||
|
this.load();
|
||||||
|
}, result => {
|
||||||
|
this.logger.error('Error while saving account', account, result);
|
||||||
|
|
||||||
|
this.toastrService.error(
|
||||||
|
'Error while saving account: ' + result.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
176
src/accounts/accountRow.component.ts
Normal file
176
src/accounts/accountRow.component.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
import { CurrencyPipe } from '@angular/common';
|
||||||
|
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
|
import { Account } from './account';
|
||||||
|
import { AccountBalances } from './accountBalances';
|
||||||
|
import { AccountBalancesService } from './accountBalances.service';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
|
||||||
|
import { AccountEditModalComponent } from './accountEditModal.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tr[account-row]',
|
||||||
|
host: {
|
||||||
|
"[id]": "account.id",
|
||||||
|
"[class.warning]": "warning",
|
||||||
|
"[class.danger]": "danger"
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<td>
|
||||||
|
<a [routerLink]="['/account', account.id, 'operations']">{{ account.name }}</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span (ngClass)="valueClass(account, accountBalances?.current)">
|
||||||
|
{{ accountBalances?.current | currency:"EUR":true }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span (ngClass)="valueClass(account, accountBalances?.pointed)">
|
||||||
|
{{ accountBalances?.pointed | currency:"EUR":true }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{{ account.authorized_overdraft | currency:"EUR":true }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<!-- Edit account. -->
|
||||||
|
<button type="button" class="btn btn-success"
|
||||||
|
(click)="modify()">
|
||||||
|
<span class="fa fa-pencil-square-o"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Delete account, with confirm. -->
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
(click)="confirmDelete()">
|
||||||
|
<span class="fa fa-trash-o"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Open account scheduler. -->
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
[hidden]="!account.id"
|
||||||
|
[routerLink]="['/account', account.id, 'scheduler']">
|
||||||
|
<span class="fa fa-clock-o"></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class AccountRowComponent implements OnInit {
|
||||||
|
@Input('account-row') account: Account;
|
||||||
|
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
|
private accountBalances: AccountBalances;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private accountBalancesService: AccountBalancesService,
|
||||||
|
private toastrService: ToastrService,
|
||||||
|
private logger: Logger,
|
||||||
|
private ngbModal: NgbModal
|
||||||
|
) {
|
||||||
|
this.logger.log("AccountRowComponent constructor");
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.logger.log(this.account);
|
||||||
|
this.accountBalancesService
|
||||||
|
.get(this.account.id)
|
||||||
|
.subscribe((accountBalances: AccountBalances) => {
|
||||||
|
this.accountBalances = accountBalances;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get warning() {
|
||||||
|
return this.account && this.accountBalances
|
||||||
|
&& this.account.authorized_overdraft < this.accountBalances.current
|
||||||
|
&& this.accountBalances.current < 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
get error() {
|
||||||
|
return this.account && this.accountBalances
|
||||||
|
&& this.accountBalances.current < this.account.authorized_overdraft;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Return the class for a value compared to account authorized overdraft.
|
||||||
|
*/
|
||||||
|
valueClass(value: number) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < this.account.authorized_overdraft) {
|
||||||
|
return 'text-danger';
|
||||||
|
} else if (value < 0) {
|
||||||
|
return 'text-warning';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmDelete() {
|
||||||
|
const modal = this.ngbModal.open(AccountDeleteModalComponent);
|
||||||
|
|
||||||
|
modal.componentInstance.account = this.account;
|
||||||
|
|
||||||
|
modal.result.then((account: Account) => {
|
||||||
|
this.delete(account);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Delete an account.
|
||||||
|
*/
|
||||||
|
delete(account: Account) {
|
||||||
|
var id = account.id;
|
||||||
|
|
||||||
|
this.accountService.delete(account).subscribe(account => {
|
||||||
|
this.toastrService.success('account #' + id + ' deleted.');
|
||||||
|
|
||||||
|
this.needsReload.emit();
|
||||||
|
}, function(result) {
|
||||||
|
this.toastrService.error(
|
||||||
|
'An error occurred while trying to delete account #' +
|
||||||
|
id + ':<br />' + result
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Open the popup to modify the account, save it on confirm.
|
||||||
|
*/
|
||||||
|
modify() {
|
||||||
|
const modal = this.ngbModal.open(AccountEditModalComponent, {
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.componentInstance.account = this.account;
|
||||||
|
|
||||||
|
modal.result.then((account: Account) => {
|
||||||
|
this.logger.log("Modal closed => save account", account);
|
||||||
|
this.save(account);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
save(account: Account) {
|
||||||
|
this.accountService.update(account).subscribe((account: Account) => {
|
||||||
|
this.toastrService.success('Account #' + account.id + ' saved.');
|
||||||
|
|
||||||
|
this.needsReload.emit();
|
||||||
|
}, result => {
|
||||||
|
this.logger.error('Error while saving account', account, result);
|
||||||
|
|
||||||
|
this.toastrService.error(
|
||||||
|
'Error while saving account: ' + result.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
@ -1,89 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
|
||||||
<!--
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<div class="row">
|
|
||||||
<table class="table table-striped table-condensed table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Nom du compte</th>
|
|
||||||
<th class="col-md-1">Solde courant</th>
|
|
||||||
<th class="col-md-1">Solde pointé</th>
|
|
||||||
<th class="col-md-1">Découvert autorisé</th>
|
|
||||||
<th class="col-md-1">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td colspan="5">
|
|
||||||
<button class="btn btn-success" ng-click="accountsCtrl.add()">
|
|
||||||
Ajouter
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr id="{{ account.id }}"
|
|
||||||
class="form-inline" ng-class="rowClass(account)"
|
|
||||||
ng-repeat="account in accountsCtrl.accounts | orderBy:'name'">
|
|
||||||
<td>
|
|
||||||
<a href="#!/account/{{ account.id }}/operations">{{ account.name }}</a>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span ng-class="accountsCtrl.valueClass(account, account.balances.current)">
|
|
||||||
{{ account.balances.current | currency : "€" }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span ng-class="accountsCtrl.valueClass(account, account.balancess.pointed)">
|
|
||||||
{{ account.balances.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"
|
|
||||||
ng-model="accountsCtrl.account"
|
|
||||||
ng-click="accountsCtrl.modify(account)">
|
|
||||||
<span class="fa fa-pencil-square-o"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Delete account, with confirm. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-click="accountsCtrl.confirmDelete(account)">
|
|
||||||
<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>
|
|
19
src/accounts/dailyBalance.service.ts
Normal file
19
src/accounts/dailyBalance.service.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// 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`);
|
||||||
|
}
|
||||||
|
}
|
8
src/accounts/dailyBalance.ts
Normal file
8
src/accounts/dailyBalance.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
export class DailyBalance {
|
||||||
|
operation_date: string;
|
||||||
|
balance: number;
|
||||||
|
expenses: number;
|
||||||
|
revenues: number;
|
||||||
|
}
|
@ -1,48 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngResource = require('angular-resource'),
|
|
||||||
ngMessages = require('angular-messages'),
|
|
||||||
ngUiNotification = require('angular-ui-notification'),
|
|
||||||
ngStrap = require('angular-strap');
|
|
||||||
|
|
||||||
var AccountFactory = require('./account.factory.js');
|
|
||||||
var AccountBalancesFactory = require('./accountBalances.factory.js');
|
|
||||||
var AccountConfig = require('./account.config.js');
|
|
||||||
var AccountController = require('./account.controller.js');
|
|
||||||
|
|
||||||
module.exports = angular.module('accountant.accounts', [
|
|
||||||
ngResource,
|
|
||||||
ngMessages,
|
|
||||||
ngUiNotification,
|
|
||||||
ngStrap,
|
|
||||||
])
|
|
||||||
|
|
||||||
.config(AccountConfig)
|
|
||||||
|
|
||||||
.factory('AccountBalances', AccountBalancesFactory)
|
|
||||||
|
|
||||||
.factory('Account', AccountFactory)
|
|
||||||
|
|
||||||
.controller('AccountController', AccountController)
|
|
||||||
|
|
||||||
.name;
|
|
18
src/app.component.ts
Normal file
18
src/app.component.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'accountant',
|
||||||
|
template: `
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="navbar fixed-top navbar-dark bg-dark">
|
||||||
|
<a class="navbar-brand" routerLink="/accounts"> Accountant</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class AppComponent { }
|
@ -1,26 +0,0 @@
|
|||||||
var operationsTmpl = require('./operations/operations.html');
|
|
||||||
var accountsTmpl = require('./accounts/accounts.html');
|
|
||||||
var schedulerTmpl = require('./scheduler/scheduler.html');
|
|
||||||
|
|
||||||
module.exports = function($routeProvider) {
|
|
||||||
// Defining template and controller in function of route.
|
|
||||||
$routeProvider
|
|
||||||
.when('/account/:accountId/operations', {
|
|
||||||
templateUrl: operationsTmpl,
|
|
||||||
controller: 'OperationController',
|
|
||||||
controllerAs: 'operationsCtrl'
|
|
||||||
})
|
|
||||||
.when('/account/:accountId/scheduler', {
|
|
||||||
templateUrl: schedulerTmpl,
|
|
||||||
controller: 'SchedulerController',
|
|
||||||
controllerAs: 'schedulerCtrl'
|
|
||||||
})
|
|
||||||
.when('/accounts', {
|
|
||||||
templateUrl: accountsTmpl,
|
|
||||||
controller: 'AccountController',
|
|
||||||
controllerAs: 'accountsCtrl'
|
|
||||||
})
|
|
||||||
.otherwise({
|
|
||||||
redirectTo: '/accounts'
|
|
||||||
});
|
|
||||||
};
|
|
5
src/app.config.ts
Normal file
5
src/app.config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// 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;
|
40
src/app.js
40
src/app.js
@ -1,40 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngRoute = require('angular-route');
|
|
||||||
|
|
||||||
var accountModule = require('./accounts'),
|
|
||||||
loginModule = require('./login'),
|
|
||||||
operationModule = require('./operations'),
|
|
||||||
schedulerModule = require('./scheduler');
|
|
||||||
|
|
||||||
var routing = require('./app.config');
|
|
||||||
|
|
||||||
require('bootstrap-webpack!./bootstrap.config.js');
|
|
||||||
|
|
||||||
angular.module('accountant', [
|
|
||||||
ngRoute,
|
|
||||||
accountModule,
|
|
||||||
loginModule,
|
|
||||||
operationModule,
|
|
||||||
schedulerModule,
|
|
||||||
]).config(routing);
|
|
55
src/app.module.ts
Normal file
55
src/app.module.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||||
|
import 'zone.js';
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
require('./main.scss');
|
||||||
|
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
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
|
||||||
|
}),
|
||||||
|
LoginModule,
|
||||||
|
NgLoggerModule.forRoot(LogLevel),
|
||||||
|
ToastrModule.forRoot(),
|
||||||
|
NgbModule.forRoot(),
|
||||||
|
AccountModule,
|
||||||
|
ScheduleModule,
|
||||||
|
OperationModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
bootstrap: [ AppComponent ]
|
||||||
|
})
|
||||||
|
|
||||||
|
export class AppModule {
|
||||||
|
constructor() {}
|
||||||
|
}
|
64
src/bootstrap.config.js
vendored
64
src/bootstrap.config.js
vendored
@ -1,64 +0,0 @@
|
|||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
scripts: {
|
|
||||||
'transition': true,
|
|
||||||
'alert': true,
|
|
||||||
'button': true,
|
|
||||||
'carousel': true,
|
|
||||||
'collapse': true,
|
|
||||||
'dropdown': true,
|
|
||||||
'modal': true,
|
|
||||||
'tooltip': true,
|
|
||||||
'popover': true,
|
|
||||||
'scrollspy': true,
|
|
||||||
'tab': true,
|
|
||||||
'affix': true
|
|
||||||
},
|
|
||||||
|
|
||||||
styles: {
|
|
||||||
'mixins': true,
|
|
||||||
|
|
||||||
'normalize': true,
|
|
||||||
'print': true,
|
|
||||||
|
|
||||||
'scaffolding': true,
|
|
||||||
'type': true,
|
|
||||||
'code': true,
|
|
||||||
'grid': true,
|
|
||||||
'tables': true,
|
|
||||||
'forms': true,
|
|
||||||
'buttons': true,
|
|
||||||
|
|
||||||
'component-animations': true,
|
|
||||||
'glyphicons': true,
|
|
||||||
'dropdowns': true,
|
|
||||||
'button-groups': true,
|
|
||||||
'input-groups': true,
|
|
||||||
'navs': true,
|
|
||||||
'navbar': true,
|
|
||||||
'breadcrumbs': true,
|
|
||||||
'pagination': true,
|
|
||||||
'pager': true,
|
|
||||||
'labels': true,
|
|
||||||
'badges': true,
|
|
||||||
'jumbotron': true,
|
|
||||||
'thumbnails': true,
|
|
||||||
'alerts': true,
|
|
||||||
'progress-bars': true,
|
|
||||||
'media': true,
|
|
||||||
'list-group': true,
|
|
||||||
'panels': true,
|
|
||||||
'wells': true,
|
|
||||||
'close': true,
|
|
||||||
|
|
||||||
'modals': true,
|
|
||||||
'tooltip': true,
|
|
||||||
'popovers': true,
|
|
||||||
'carousel': true,
|
|
||||||
|
|
||||||
'utilities': true,
|
|
||||||
'responsive-utilities': true
|
|
||||||
}
|
|
||||||
};
|
|
3
src/bootstrap.config.less
vendored
3
src/bootstrap.config.less
vendored
@ -1,3 +0,0 @@
|
|||||||
@pre-border-color: @pre-bg; // hide the border.
|
|
||||||
|
|
||||||
@import "./main.less";
|
|
@ -19,30 +19,16 @@
|
|||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<title><% htmlWebpackPlugin.options.title %></title>
|
<title><% htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!-- htmllint attr-bans="false" -->
|
<!-- htmllint attr-bans="false" -->
|
||||||
<body style="padding-bottom: 50px; padding-top: 70px" ng-app="accountant">
|
<body style="padding-bottom: 50px; padding-top: 70px">
|
||||||
<!-- htmllint attr-bans="$previous" -->
|
<!-- htmllint attr-bans="$previous" -->
|
||||||
<!-- Navbar -->
|
<accountant></accountant>
|
||||||
<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">
|
|
||||||
<div ng-view></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom Javascript libraries -->
|
|
||||||
<script src="<% htmlWebpackPlugin.files.js[0] %>"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
92
src/login/authInterceptor.ts
Normal file
92
src/login/authInterceptor.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// 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,50 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngStorage = require('meanie-angular-storage'),
|
|
||||||
ngHttpAuth = require('angular-http-auth'),
|
|
||||||
ngStrap = require('angular-strap');
|
|
||||||
|
|
||||||
// Note: ngHttpAuth seems to have no module.exports.
|
|
||||||
ngHttpAuth = 'http-auth-interceptor';
|
|
||||||
|
|
||||||
var LoginService = require('./login.service');
|
|
||||||
var LoginConfig = require('./login.config');
|
|
||||||
|
|
||||||
module.exports = angular.module('accountant.login', [
|
|
||||||
ngHttpAuth,
|
|
||||||
ngStorage
|
|
||||||
])
|
|
||||||
|
|
||||||
.service('LoginService', LoginService)
|
|
||||||
|
|
||||||
.config(LoginConfig)
|
|
||||||
|
|
||||||
.run(function($rootScope, LoginService) {
|
|
||||||
var onAuthLoginRequired = $rootScope.$on('event:auth-loginRequired', LoginService.loginModal);
|
|
||||||
|
|
||||||
$rootScope.$on('$destroy', function() {
|
|
||||||
onAuthLoginRequired = angular.noop();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
.name;
|
|
39
src/login/login.module.ts
Normal file
39
src/login/login.module.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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 { 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,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
LoginService,
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: AuthInterceptor,
|
||||||
|
multi: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
LoginModalComponent,
|
||||||
|
LoginFormComponent,
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
LoginModalComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class LoginModule {};
|
@ -1,45 +0,0 @@
|
|||||||
var base64 = require('base64util');
|
|
||||||
|
|
||||||
var loginTmpl = require('./login.tmpl.html');
|
|
||||||
|
|
||||||
module.exports = function($storage, $http, authService, $modal) {
|
|
||||||
var login = function(email, password) {
|
|
||||||
// Encode authentication data.
|
|
||||||
var authdata = base64.encode(email + ':' + password);
|
|
||||||
|
|
||||||
return $http.post('/api/user/login', {}, {
|
|
||||||
ignoreAuthModule: true,
|
|
||||||
headers: {
|
|
||||||
'authorization': 'Basic ' + authdata
|
|
||||||
}
|
|
||||||
}).then(function (result) {
|
|
||||||
$storage.session.set('refresh_token', result.data.refresh_token);
|
|
||||||
$storage.session.set('access_token', result.data.access_token);
|
|
||||||
|
|
||||||
authService.loginConfirmed();
|
|
||||||
}, function(result) {
|
|
||||||
loginModal();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var loginModal = function () {
|
|
||||||
$storage.session.clear();
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: loginTmpl,
|
|
||||||
controller: function($scope, $login) {
|
|
||||||
$scope.$login = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$login($scope.email, $scope.password);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
$login: login,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
'loginModal': loginModal,
|
|
||||||
};
|
|
||||||
};
|
|
67
src/login/login.service.ts
Normal file
67
src/login/login.service.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Observable} from 'rxjs/Rx';
|
||||||
|
|
||||||
|
import * as base64 from 'base64util';
|
||||||
|
|
||||||
|
import { Logger } from '@nsalaun/ng-logger';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
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 ngbModal: NgbModal,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public readonly url: string = '/api/user/login';
|
||||||
|
|
||||||
|
login(): Observable<Token> {
|
||||||
|
let modal = this.ngbModal.open(LoginModalComponent);
|
||||||
|
|
||||||
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
let observable: Observable<any> = Observable.fromPromise(modal.result);
|
||||||
|
|
||||||
|
return observable.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);
|
||||||
|
}
|
||||||
|
};
|
6
src/login/login.ts
Normal file
6
src/login/login.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||||
|
|
||||||
|
export class Login {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
70
src/login/loginForm.component.ts
Normal file
70
src/login/loginForm.component.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
// 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">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="email" class="col-sm-4 control-label">Adresse email</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="email.errors">
|
||||||
|
<input type="text" class="form-control" id="email"
|
||||||
|
formControlName="email" placeholder="Nom d'utilisateur">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="email.errors">
|
||||||
|
<p *ngIf="email.errors.required">The email is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label for="password" class="col-sm-4 control-label">Mot de passe</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="password.errors">
|
||||||
|
<input type="password" class="form-control" id="password"
|
||||||
|
formControlName="password" placeholder="Mot de passe">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="password.errors">
|
||||||
|
<p *ngIf="password.errors.required">The password is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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');
|
||||||
|
}
|
||||||
|
}
|
51
src/login/loginModal.component.ts
Normal file
51
src/login/loginModal.component.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 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 { Login } from './login';
|
||||||
|
import { LoginFormComponent } from './loginForm.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'login-modal',
|
||||||
|
template: `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="modal-title">Authentification requise</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="modal-body">
|
||||||
|
<login-form (submit)="submit()" #loginForm="loginForm"></login-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary" [disabled]="!loginForm.form.valid" (click)="submit()">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" (click)="cancel()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class LoginModalComponent {
|
||||||
|
@ViewChild('loginForm') loginForm: LoginFormComponent;
|
||||||
|
|
||||||
|
constructor(private activeModal: NgbActiveModal) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
let formModel = this.loginForm.form.value;
|
||||||
|
let login: Login = new Login();
|
||||||
|
|
||||||
|
login.email = formModel.email;
|
||||||
|
login.password = formModel.password;
|
||||||
|
|
||||||
|
this.activeModal.close(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.activeModal.dismiss("closed");
|
||||||
|
}
|
||||||
|
}
|
6
src/login/token.ts
Normal file
6
src/login/token.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
export class Token {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
}
|
@ -1,41 +0,0 @@
|
|||||||
@import '~font-awesome/less/font-awesome';
|
|
||||||
|
|
||||||
@import '~angular-ui-notification/src/angular-ui-notification';
|
|
||||||
|
|
||||||
@import (inline) '~c3/c3.css';
|
|
||||||
|
|
||||||
@import (inline) '~bootstrap-additions/dist/bootstrap-additions.css';
|
|
||||||
|
|
||||||
.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stroke {
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c3-ygrid-line.zeroline line {
|
|
||||||
stroke: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c3-ygrid-line.overdraft line {
|
|
||||||
stroke: #FF0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Needed for modal backdrop opacity.
|
|
||||||
.modal-backdrop.am-fade {
|
|
||||||
opacity: .5;
|
|
||||||
transition: opacity .15s linear;
|
|
||||||
&.ng-enter {
|
|
||||||
opacity: 0;
|
|
||||||
&.ng-enter-active {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.ng-leave {
|
|
||||||
opacity: .5;
|
|
||||||
&.ng-leave-active {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
25
src/main.scss
Normal file
25
src/main.scss
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
$fa-font-path: '~font-awesome/fonts';
|
||||||
|
|
||||||
|
@import '~font-awesome/scss/font-awesome';
|
||||||
|
|
||||||
|
@import '~bootstrap/scss/bootstrap';
|
||||||
|
|
||||||
|
@import '~c3/c3';
|
||||||
|
|
||||||
|
@import '~ngx-toastr/toastr';
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stroke {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c3-ygrid-line.zeroline line {
|
||||||
|
stroke: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c3-ygrid-line.overdraft line {
|
||||||
|
stroke: #FF0000;
|
||||||
|
}
|
7
src/main.ts
Normal file
7
src/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// 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,190 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var moment = require('moment'),
|
|
||||||
c3 = require('c3');
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngResource = require('angular-resource');
|
|
||||||
|
|
||||||
module.exports = angular.module('balanceChartModule', [
|
|
||||||
ngResource
|
|
||||||
])
|
|
||||||
|
|
||||||
.component('balanceChart', {
|
|
||||||
template: '<div></div>',
|
|
||||||
bindings: {
|
|
||||||
account: '<',
|
|
||||||
onUpdate: '&'
|
|
||||||
},
|
|
||||||
controller: function($routeParams, Balances, $element) {
|
|
||||||
var vm = this;
|
|
||||||
|
|
||||||
vm.loadData = function() {
|
|
||||||
Balances.query({
|
|
||||||
id: $routeParams.accountId
|
|
||||||
}, function(results) {
|
|
||||||
var headers = [['date', 'balances', 'expenses', 'revenues']];
|
|
||||||
|
|
||||||
var rows = results.map(function(result) {
|
|
||||||
return [
|
|
||||||
result.operation_date,
|
|
||||||
result.balance,
|
|
||||||
result.expenses,
|
|
||||||
result.revenues
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
vm.chart.unload();
|
|
||||||
|
|
||||||
vm.chart.load({
|
|
||||||
rows: headers.concat(rows)
|
|
||||||
});
|
|
||||||
|
|
||||||
var x = vm.chart.x();
|
|
||||||
var balances = x.balances;
|
|
||||||
|
|
||||||
vm.onUpdate(balances[0], balances[balances.length - 1]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.$onInit = function() {
|
|
||||||
var tomorrow = moment().endOf('day').valueOf();
|
|
||||||
|
|
||||||
vm.chart = c3.generate({
|
|
||||||
bindto: $element[0].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: function(domain) {
|
|
||||||
vm.onUpdate({minDate: domain[0], maxDate: domain[1]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vm.loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.setLines = function(account) {
|
|
||||||
if(vm.chart) {
|
|
||||||
vm.chart.ygrids([
|
|
||||||
{ value: 0, axis: 'y2' },
|
|
||||||
{ value: 0, axis: 'y', class: 'zeroline'},
|
|
||||||
]);
|
|
||||||
|
|
||||||
vm.chart.ygrids.add({
|
|
||||||
value: account.authorized_overdraft,
|
|
||||||
axis: 'y',
|
|
||||||
class: 'overdraft'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.$onChanges = function(changes) {
|
|
||||||
if('account' in changes) {
|
|
||||||
if('$promise' in vm.account && vm.account.$resolved === false) {
|
|
||||||
vm.account.$promise.then(function(account) {
|
|
||||||
vm.setLines(account);
|
|
||||||
return account;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
vm.setLines(vm.account);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.factory('Balances', function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/account/:id/daily_balances', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
.name;
|
|
177
src/operations/balanceChart.component.ts
Normal file
177
src/operations/balanceChart.component.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
// 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,137 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var moment = require('moment'),
|
|
||||||
c3 = require('c3');
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngResource = require('angular-resource');
|
|
||||||
|
|
||||||
module.exports = angular.module('categoryChartModule', [
|
|
||||||
ngResource
|
|
||||||
])
|
|
||||||
|
|
||||||
.component('categoryChart', {
|
|
||||||
template: '<div></div>',
|
|
||||||
bindings: {
|
|
||||||
minDate: '<',
|
|
||||||
maxDate: '<'
|
|
||||||
},
|
|
||||||
controller: function($routeParams, $element, Categories, Incomes) {
|
|
||||||
var vm = this;
|
|
||||||
|
|
||||||
vm.loadData = function() {
|
|
||||||
Categories.query({
|
|
||||||
id: $routeParams.accountId,
|
|
||||||
begin: vm.minDate ? moment(vm.minDate).format('YYYY-MM-DD') : null,
|
|
||||||
end: vm.maxDate ? moment(vm.maxDate).format('YYYY-MM-DD') : null
|
|
||||||
}, function(results) {
|
|
||||||
var expenses=[],
|
|
||||||
revenues=[],
|
|
||||||
colors={},
|
|
||||||
names={};
|
|
||||||
|
|
||||||
var revenuesColor = 'green',
|
|
||||||
expensesColor = 'orange';
|
|
||||||
|
|
||||||
angular.forEach(results, function(result) {
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
vm.chart.unload();
|
|
||||||
|
|
||||||
vm.chart.load({
|
|
||||||
columns: revenues.concat(expenses),
|
|
||||||
names: names,
|
|
||||||
colors: colors
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.$onInit = function() {
|
|
||||||
vm.chart = c3.generate({
|
|
||||||
bindto: $element[0].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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//vm.loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.$onChanges = function() {
|
|
||||||
vm.loadData();
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
.factory('Categories', function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/account/:id/category', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
.factory('Incomes', function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/account/:id/income', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
.name;
|
|
39
src/operations/category.service.ts
Normal file
39
src/operations/category.service.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// 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});
|
||||||
|
}
|
||||||
|
}
|
7
src/operations/category.ts
Normal file
7
src/operations/category.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
export class Category {
|
||||||
|
category: string;
|
||||||
|
expenses: number;
|
||||||
|
revenues: number;
|
||||||
|
}
|
107
src/operations/categoryChart.component.ts
Normal file
107
src/operations/categoryChart.component.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// 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,54 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var moment = require('moment');
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngResource = require('angular-resource'),
|
|
||||||
ngMessages = require('angular-messages'),
|
|
||||||
ngUiNotification = require('angular-ui-notification'),
|
|
||||||
ngStrap = require('angular-strap');
|
|
||||||
|
|
||||||
var balanceChartModule = require('./balance-chart.component.js'),
|
|
||||||
categoryChartModule = require('./category-chart.component.js'),
|
|
||||||
accountModule = require('../accounts');
|
|
||||||
|
|
||||||
var OperationFactory = require('./operation.factory');
|
|
||||||
var OperationConfig = require('./operation.config');
|
|
||||||
var OperationController = require('./operation.controller');
|
|
||||||
|
|
||||||
module.exports = angular.module('accountant.operations', [
|
|
||||||
ngResource,
|
|
||||||
ngMessages,
|
|
||||||
ngUiNotification,
|
|
||||||
ngStrap,
|
|
||||||
accountModule,
|
|
||||||
balanceChartModule,
|
|
||||||
categoryChartModule
|
|
||||||
])
|
|
||||||
|
|
||||||
.config(OperationConfig)
|
|
||||||
|
|
||||||
.factory('Operation', OperationFactory)
|
|
||||||
|
|
||||||
.controller('OperationController', OperationController)
|
|
||||||
|
|
||||||
.name;
|
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = function($resourceProvider) {
|
|
||||||
// Keep trailing slashes to avoid redirect by flask..
|
|
||||||
$resourceProvider.defaults.stripTrailingSlashes = false;
|
|
||||||
};
|
|
@ -1,149 +0,0 @@
|
|||||||
var operationFormTmpl = require('./operation.form.tmpl.html'),
|
|
||||||
operationDeleteTmpl = require('./operation.delete.tmpl.html');
|
|
||||||
|
|
||||||
module.exports = function($routeParams, $modal, Notification, Operation,
|
|
||||||
Account) {
|
|
||||||
|
|
||||||
var vm = this;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add an empty operation.
|
|
||||||
*/
|
|
||||||
vm.add = function() {
|
|
||||||
var operation = new Operation({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
account_id: $routeParams.accountId
|
|
||||||
});
|
|
||||||
|
|
||||||
return vm.modify(operation);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load operations.
|
|
||||||
*/
|
|
||||||
vm.load = function(minDate, maxDate) {
|
|
||||||
vm.minDate = minDate;
|
|
||||||
vm.maxDate = maxDate;
|
|
||||||
|
|
||||||
return Operation.query({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
account_id: $routeParams.accountId,
|
|
||||||
begin: minDate ? moment(minDate).format('YYYY-MM-DD') : null,
|
|
||||||
end: maxDate ? moment(maxDate).format('YYYY-MM-DD') : null
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Toggle pointed indicator for an operation.
|
|
||||||
*/
|
|
||||||
vm.togglePointed = function(operation, rowform) {
|
|
||||||
operation.pointed = !operation.pointed;
|
|
||||||
|
|
||||||
vm.save(operation);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Toggle cancel indicator for an operation.
|
|
||||||
*/
|
|
||||||
vm.toggleCanceled = function(operation) {
|
|
||||||
operation.canceled = !operation.canceled;
|
|
||||||
|
|
||||||
vm.save(operation);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Save an operation and return a promise.
|
|
||||||
*/
|
|
||||||
vm.save = function(operation) {
|
|
||||||
operation.confirmed = true;
|
|
||||||
|
|
||||||
return operation.$save().then(function(operation) {
|
|
||||||
Notification.success('Operation #' + operation.id + ' saved.');
|
|
||||||
|
|
||||||
vm.operations = vm.load();
|
|
||||||
|
|
||||||
return operation;
|
|
||||||
}, function(result){
|
|
||||||
Notification.error(
|
|
||||||
'Error while saving operation: ' + result.message
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delete an operation and return a promise.
|
|
||||||
*/
|
|
||||||
vm.confirmDelete = function(operation) {
|
|
||||||
var title = "Delete operation #" + operation.id;
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: operationDeleteTmpl,
|
|
||||||
controller: function($scope, title, operation, $delete) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.operation = operation;
|
|
||||||
$scope.$delete = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$delete($scope.operation);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
operation: operation,
|
|
||||||
$delete: vm.delete
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.delete = function(operation) {
|
|
||||||
var id = operation.id;
|
|
||||||
|
|
||||||
return operation.$delete().then(function() {
|
|
||||||
Notification.success('Operation #' + id + ' deleted.');
|
|
||||||
|
|
||||||
vm.operations = vm.load();
|
|
||||||
|
|
||||||
return operation;
|
|
||||||
}, function(result) {
|
|
||||||
Notification.error(
|
|
||||||
'An error occurred while trying to delete operation #' +
|
|
||||||
id + ':<br />' + result
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Open the popup to modify the operation, save it on confirm.
|
|
||||||
* @returns a promise.
|
|
||||||
*/
|
|
||||||
vm.modify = function(operation) {
|
|
||||||
// FIXME Alexis Lahouze 2017-06-15 i18n
|
|
||||||
var title = "Operation";
|
|
||||||
|
|
||||||
if (operation.id) {
|
|
||||||
title = title + " #" + operation.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: operationFormTmpl,
|
|
||||||
controller: function($scope, title, operation, $save) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.operation = operation;
|
|
||||||
$scope.$save = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$save($scope.operation);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
operation: operation,
|
|
||||||
$save: vm.save
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.onUpdate = function(minDate, maxDate) {
|
|
||||||
vm.operations = vm.load(minDate, maxDate);
|
|
||||||
};
|
|
||||||
|
|
||||||
vm.account = Account.get({id: $routeParams.accountId});
|
|
||||||
};
|
|
@ -1,25 +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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<p>Voulez-vous supprimer l'opération #{{ operation.id }} ayant pour libellé :<br/>{{ operation.label }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-danger" type="button" ng-click="$delete()">
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/operation/:id', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,77 +0,0 @@
|
|||||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
|
||||||
<!-- kate: space-indent on; indent-width 2; mixedindent off; -->
|
|
||||||
<!--
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<div class="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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<form class="form-horizontal simple-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="operation-date">Date</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="operation-date" name="operation_date"
|
|
||||||
type="text" ng-model="operation.operation_date"
|
|
||||||
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
|
|
||||||
placeholder="Operation date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="label">Label</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="label" name="label"
|
|
||||||
ng-model="operation.label" type="text" placeholder="Label">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="value">Montant</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="value" name="value"
|
|
||||||
ng-model="operation.value" type="number" placeholder="Value">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="category">Catégorie</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="category" name="category"
|
|
||||||
ng-model="operation.category" type="text" placeholder="Category">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-primary" type="button" ng-click="$save()">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
57
src/operations/operation.module.ts
Normal file
57
src/operations/operation.module.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// 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 {}
|
71
src/operations/operation.service.ts
Normal file
71
src/operations/operation.service.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
8
src/operations/operation.states.ts
Normal file
8
src/operations/operation.states.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
import { OperationListComponent } from './operationList.component';
|
||||||
|
|
||||||
|
export const OperationListState = {
|
||||||
|
path: 'account/:accountId/operations',
|
||||||
|
component: OperationListComponent
|
||||||
|
}
|
15
src/operations/operation.ts
Normal file
15
src/operations/operation.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// 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
|
||||||
|
}
|
45
src/operations/operationDeleteModal.component.ts
Normal file
45
src/operations/operationDeleteModal.component.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
63
src/operations/operationEditModal.component.ts
Normal file
63
src/operations/operationEditModal.component.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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");
|
||||||
|
}
|
||||||
|
}
|
122
src/operations/operationForm.component.ts
Normal file
122
src/operations/operationForm.component.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
// 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');
|
||||||
|
}
|
||||||
|
}
|
150
src/operations/operationList.component.ts
Normal file
150
src/operations/operationList.component.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// 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);
|
||||||
|
};
|
||||||
|
};
|
153
src/operations/operationRow.component.ts
Normal file
153
src/operations/operationRow.component.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
// 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,115 +0,0 @@
|
|||||||
<!-- vim: set tw=80 ts=2 sw=2 sts=2: -->
|
|
||||||
<!--
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
<div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-9">
|
|
||||||
<balance-chart on-update="operationsCtrl.onUpdate(minDate, maxDate)"
|
|
||||||
account="operationsCtrl.account"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<category-chart
|
|
||||||
min-date="operationsCtrl.minDate"
|
|
||||||
max-date="operationsCtrl.maxDate"/>
|
|
||||||
</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="operationsCtrl.add()">
|
|
||||||
Ajouter
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr id="{{ operation.id }}" class="form-inline"
|
|
||||||
ng-class="{stroke: operation.canceled, italic: !operation.confirmed,
|
|
||||||
warning: operation.balance < 0, danger: operation.balance < operationsCtrl.account.authorized_overdraft}"
|
|
||||||
ng-repeat="operation in operationsCtrl.operations | orderBy:'+':true">
|
|
||||||
<td>
|
|
||||||
{{ operation.operation_date | date:"yyyy-MM-dd" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.label }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.value | currency:"€" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td ng-class="{'text-warning': operation.balance < 0, 'text-danger':
|
|
||||||
operation.balance < operationsCtrl.account.authorized_overdraft}">
|
|
||||||
{{ operation.balance | currency:"€" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.category }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-xs">
|
|
||||||
<!-- Edit operation, for non-canceled operation. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-if="!operation.canceled"
|
|
||||||
ng-click="operationsCtrl.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-default"
|
|
||||||
ng-if="!operation.canceled"
|
|
||||||
ng-click="operationsCtrl.togglePointed(operation)"
|
|
||||||
ng-class="{active: operation.pointed}" title="point">
|
|
||||||
<span ng-class="{'fa fa-check-square-o': operation.pointed, 'fa fa-square-o': !operation.pointed}"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Toggle canceled operation. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-click="operationsCtrl.toggleCanceled(operation)"
|
|
||||||
ng-if="operation.scheduled_operation_id && !rowform.$visible"
|
|
||||||
ng-class="{active: operation.canceled}" title="cancel">
|
|
||||||
<span class="fa fa-remove"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Delete operation, with confirm. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-if="operation.id && !operation.scheduled_operation_id"
|
|
||||||
ng-click="operationsCtrl.confirmDelete(operation)">
|
|
||||||
<span class="fa fa-trash-o"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,43 +0,0 @@
|
|||||||
// vim: set tw=80 ts=4 sw=4 sts=4:
|
|
||||||
/*
|
|
||||||
This file is part of Accountant.
|
|
||||||
|
|
||||||
Accountant is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
Accountant is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with Accountant. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
/* jshint node: true */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var angular = require('angular');
|
|
||||||
|
|
||||||
var ngMessages = require('angular-messages'),
|
|
||||||
ngUiNotification = require('angular-ui-notification'),
|
|
||||||
ngStrap = require('angular-strap');
|
|
||||||
|
|
||||||
var ScheduleConfig = require('./schedule.config.js');
|
|
||||||
var ScheduleController = require('./schedule.controller.js');
|
|
||||||
var ScheduleFactory = require('./schedule.factory.js');
|
|
||||||
|
|
||||||
module.exports = angular.module('accountant.scheduler', [
|
|
||||||
ngMessages,
|
|
||||||
ngUiNotification,
|
|
||||||
ngStrap
|
|
||||||
])
|
|
||||||
|
|
||||||
.config(ScheduleConfig)
|
|
||||||
|
|
||||||
.factory('ScheduledOperation', ScheduleFactory)
|
|
||||||
|
|
||||||
.controller('SchedulerController', ScheduleController)
|
|
||||||
|
|
||||||
.name;
|
|
@ -1,4 +0,0 @@
|
|||||||
module.exports = function($resourceProvider) {
|
|
||||||
// Keep trailing slashes to avoid redirect by flask..
|
|
||||||
$resourceProvider.defaults.stripTrailingSlashes = false;
|
|
||||||
};
|
|
@ -1,127 +0,0 @@
|
|||||||
var scheduleFormTmpl = require('./schedule.form.tmpl.html'),
|
|
||||||
scheduleDeleteTmpl = require('./schedule.delete.tmpl.html');
|
|
||||||
|
|
||||||
module.exports= function($rootScope, $routeParams, Notification, ScheduledOperation, $log, $modal) {
|
|
||||||
var vm = this;
|
|
||||||
|
|
||||||
// Operation store.
|
|
||||||
vm.operations = [];
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Add a new operation at the beginning of th array.
|
|
||||||
*/
|
|
||||||
vm.add = function() {
|
|
||||||
var operation = new ScheduledOperation({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
account_id: $routeParams.accountId
|
|
||||||
});
|
|
||||||
|
|
||||||
return vm.modify(operation);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Load operations.
|
|
||||||
*/
|
|
||||||
vm.load = function() {
|
|
||||||
return ScheduledOperation.query({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
account_id: $routeParams.accountId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Save operation.
|
|
||||||
*/
|
|
||||||
vm.save = function(operation) {
|
|
||||||
return operation.$save().then(function(operation) {
|
|
||||||
Notification.success('Scheduled operation #' + operation.id + ' saved.');
|
|
||||||
|
|
||||||
vm.operations = vm.load();
|
|
||||||
|
|
||||||
return operation;
|
|
||||||
}, function(result){
|
|
||||||
$log.error('Error while saving scheduled operation', operation, result);
|
|
||||||
|
|
||||||
Notification.error(
|
|
||||||
'Error while saving scheduled operation: ' + result.message
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delete an operation and return a promise.
|
|
||||||
*/
|
|
||||||
vm.confirmDelete = function(operation) {
|
|
||||||
var title = "Delete operation #" + operation.id;
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: scheduleDeleteTmpl,
|
|
||||||
controller: function($scope, title, operation, $delete) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.operation = operation;
|
|
||||||
$scope.$delete = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$delete($scope.operation);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
operation: operation,
|
|
||||||
$delete: vm.delete
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Delete operation.
|
|
||||||
*/
|
|
||||||
vm.delete = function(operation) {
|
|
||||||
var id = operation.id;
|
|
||||||
|
|
||||||
return operation.$delete().then(function() {
|
|
||||||
Notification.success('Scheduled operation #' + id + ' deleted.');
|
|
||||||
|
|
||||||
vm.operations = vm.load();
|
|
||||||
|
|
||||||
return operation;
|
|
||||||
}, function(result) {
|
|
||||||
Notification.error(
|
|
||||||
'An error occurred while trying to delete scheduled operation #' +
|
|
||||||
id + ':<br />' + result
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Open the popup to modify the operation, save it on confirm.
|
|
||||||
* @returns a promise.
|
|
||||||
*/
|
|
||||||
vm.modify = function(operation) {
|
|
||||||
// FIXME Alexis Lahouze 2017-06-15 i18n
|
|
||||||
var title = "Operation";
|
|
||||||
|
|
||||||
if (operation.id) {
|
|
||||||
title = title + " #" + operation.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal({
|
|
||||||
templateUrl: scheduleFormTmpl,
|
|
||||||
controller: function($scope, title, operation, $save) {
|
|
||||||
$scope.title = title;
|
|
||||||
$scope.operation = operation;
|
|
||||||
$scope.$save = function() {
|
|
||||||
$scope.$hide();
|
|
||||||
$save($scope.operation);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
locals: {
|
|
||||||
title: title,
|
|
||||||
operation: operation,
|
|
||||||
$save: vm.save
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load operations on controller initialization.
|
|
||||||
vm.operations = vm.load();
|
|
||||||
};
|
|
@ -1,25 +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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<p>Voulez-vous supprimer l'opération #{{ operation.id }} ayant pour libellé :<br/>{{ operation.label }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-danger" type="button" ng-click="$delete()">
|
|
||||||
Supprimer
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,7 +0,0 @@
|
|||||||
module.exports = function($resource) {
|
|
||||||
return $resource(
|
|
||||||
'/api/scheduled_operation/:id', {
|
|
||||||
id: '@id'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,89 +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">{{ title }}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-body" id="modal-body">
|
|
||||||
<form class="form-horizontal simple-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="start-date">Date de début</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="start-date" name="start_date"
|
|
||||||
type="text" ng-model="operation.start_date"
|
|
||||||
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
|
|
||||||
placeholder="Scheduled operation start date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="stop-date">Date de fin</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="stop-date" name="stop_date"
|
|
||||||
type="text" ng-model="operation.stop_date"
|
|
||||||
bs-datepicker data-date-format="yyyy-MM-dd" data-timezone="UTC"
|
|
||||||
placeholder="Scheduled operation stop date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="day">Jour</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="day" name="day"
|
|
||||||
ng-model="operation.day" type="number" placeholder="Day">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="frequency">Fréquence</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="frequency" name="frequency"
|
|
||||||
ng-model="operation.frequency" type="number" placeholder="Frequency">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="label">Label</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="label" name="label"
|
|
||||||
ng-model="operation.label" type="text" placeholder="Label">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="value">Montant</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="value" name="value"
|
|
||||||
ng-model="operation.value" type="number" placeholder="Value">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="col-sm-4 control-label" for="category">Catégorie</label>
|
|
||||||
<div class="col-sm-8">
|
|
||||||
<input class="form-control" id="category" name="category"
|
|
||||||
ng-model="operation.category" type="text" placeholder="Category">
|
|
||||||
</input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button class="btn btn-primary" type="button" ng-click="$save()">
|
|
||||||
OK
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="btn btn-default" type="button" ng-click="$hide()">
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
51
src/scheduler/schedule.module.ts
Normal file
51
src/scheduler/schedule.module.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// 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 { ScheduleService } from './schedule.service';
|
||||||
|
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
|
||||||
|
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
|
||||||
|
import { ScheduleFormComponent } from './scheduleForm.component';
|
||||||
|
import { ScheduleRowComponent } from './scheduleRow.component';
|
||||||
|
import { ScheduleListComponent } from './scheduleList.component';
|
||||||
|
import { ScheduleListState } from './schedule.states';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
HttpClientModule,
|
||||||
|
CommonModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
RouterModule.forChild([
|
||||||
|
ScheduleListState
|
||||||
|
]),
|
||||||
|
NgLoggerModule,
|
||||||
|
ToastrModule,
|
||||||
|
NgbModule,
|
||||||
|
TextMaskModule
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
ScheduleService,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
ScheduleDeleteModalComponent,
|
||||||
|
ScheduleEditModalComponent,
|
||||||
|
ScheduleFormComponent,
|
||||||
|
ScheduleListComponent,
|
||||||
|
ScheduleRowComponent
|
||||||
|
],
|
||||||
|
entryComponents: [
|
||||||
|
ScheduleDeleteModalComponent,
|
||||||
|
ScheduleEditModalComponent,
|
||||||
|
ScheduleListComponent,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ScheduleModule {}
|
47
src/scheduler/schedule.service.ts
Normal file
47
src/scheduler/schedule.service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
8
src/scheduler/schedule.states.ts
Normal file
8
src/scheduler/schedule.states.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2 :
|
||||||
|
|
||||||
|
import { ScheduleListComponent } from './scheduleList.component';
|
||||||
|
|
||||||
|
export const ScheduleListState = {
|
||||||
|
path: 'account/:accountId/scheduler',
|
||||||
|
component: ScheduleListComponent,
|
||||||
|
}
|
13
src/scheduler/schedule.ts
Normal file
13
src/scheduler/schedule.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// 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
|
||||||
|
}
|
49
src/scheduler/scheduleDeleteModal.component.ts
Normal file
49
src/scheduler/scheduleDeleteModal.component.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// vim: set tw=80 ts=2 sw=2 sts=2:
|
||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
|
import { Schedule } from './schedule';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'schedule-delete-modal',
|
||||||
|
template: `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="modal-body">
|
||||||
|
<p>
|
||||||
|
Do you really want to delete schedule #{{ schedule.id }} with label:<br/>
|
||||||
|
{{ schedule.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 ScheduleDeleteModalComponent {
|
||||||
|
@Input() schedule: Schedule
|
||||||
|
|
||||||
|
constructor(public activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
|
title(): string {
|
||||||
|
return "Delete schedule #" + this.schedule.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(): void {
|
||||||
|
this.activeModal.close(this.schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.activeModal.dismiss("closed");
|
||||||
|
}
|
||||||
|
}
|
66
src/scheduler/scheduleEditModal.component.ts
Normal file
66
src/scheduler/scheduleEditModal.component.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// 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 { Schedule } from './schedule';
|
||||||
|
import { ScheduleFormComponent } from './scheduleForm.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'schedule-edit-modal',
|
||||||
|
template: `
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body" id="modal-body">
|
||||||
|
<schedule-form [schedule]="schedule" (submit)="submit()" #scheduleForm="scheduleForm"></schedule-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-primary" [disabled]="!scheduleForm.form.valid" (click)="submit()">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" (click)="cancel()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ScheduleEditModalComponent {
|
||||||
|
@Input() schedule: Schedule;
|
||||||
|
@ViewChild('scheduleForm') scheduleForm: ScheduleFormComponent;
|
||||||
|
|
||||||
|
valid: boolean = false;
|
||||||
|
|
||||||
|
constructor(private activeModal: NgbActiveModal) {}
|
||||||
|
|
||||||
|
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.activeModal.close(schedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(): void {
|
||||||
|
this.activeModal.dismiss("closed");
|
||||||
|
}
|
||||||
|
}
|
189
src/scheduler/scheduleForm.component.ts
Normal file
189
src/scheduler/scheduleForm.component.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// 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">
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="start-date">Date de début (AAAA-MM-JJ)</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="startDate.errors">
|
||||||
|
<input class="form-control"
|
||||||
|
id="start-date" formControlName="startDate"
|
||||||
|
[textMask]="{mask: dateMask}"
|
||||||
|
placeholder="Schedule start date">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="startDate.errors">
|
||||||
|
<p *ngIf="startDate.errors.required">The start date is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="stop-date">Date de fin (AAAA-MM-JJ)</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="stopDate.errors">
|
||||||
|
<input class="form-control"
|
||||||
|
id="stop-date" formControlName="stopDate"
|
||||||
|
[textMask]="{mask: dateMask}"
|
||||||
|
placeholder="Schedule stop date">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="stopDate.errors">
|
||||||
|
<p *ngIf="stopDate.errors.required">The stop date is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="day">Jour</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="day.errors">
|
||||||
|
<input class="form-control"
|
||||||
|
id="day" formControlName="day"
|
||||||
|
type="number" placeholder="Day">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="day.errors">
|
||||||
|
<p *ngIf="day.errors.required">The day is required.</p>
|
||||||
|
<p *ngIf="day.errors.min">The day must be greater than 0.</p>
|
||||||
|
<p *ngIf="day.errors.max">The day must be less than or equal to 31.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group row">
|
||||||
|
<label class="col-sm-4 control-label" for="frequency">Fréquence</label>
|
||||||
|
|
||||||
|
<div class="col-sm-8"
|
||||||
|
[class.has-danger]="frequency.errors">
|
||||||
|
<input class="form-control"
|
||||||
|
id="frequency" formControlName="frequency"
|
||||||
|
type="number" placeholder="Frequency">
|
||||||
|
|
||||||
|
<div class="help-block text-danger" *ngIf="frequency.errors">
|
||||||
|
<p *ngIf="frequency.errors.required">The frequency is required.</p>
|
||||||
|
<p *ngIf="frequency.errors.min">The frequency must be positive.</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 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-danger]="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 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-danger]="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 category is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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');
|
||||||
|
}
|
||||||
|
}
|
115
src/scheduler/scheduleList.component.ts
Normal file
115
src/scheduler/scheduleList.component.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 { ScheduleEditModalComponent } from './scheduleEditModal.component';
|
||||||
|
import { ScheduleService } from './schedule.service';
|
||||||
|
import { Schedule } from './schedule';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'schedule-list',
|
||||||
|
template: `
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-sm table-striped table-condensed table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date de début</th>
|
||||||
|
<th>Date de fin</th>
|
||||||
|
<th>Jour</th>
|
||||||
|
<th>Fréq.</th>
|
||||||
|
<th>Libellé de l'opération</th>
|
||||||
|
<th>Montant</th>
|
||||||
|
<th>Catégorie</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colspan="8">
|
||||||
|
<button class="btn btn-success" (click)="add()">
|
||||||
|
Ajouter
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr *ngFor="let schedule of schedules"
|
||||||
|
[schedule-row]="schedule" (needsReload)="load()">
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ScheduleListComponent implements OnInit {
|
||||||
|
accountId: number;
|
||||||
|
schedules = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private toastrService: ToastrService,
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private logger: Logger,
|
||||||
|
private ngbModal: NgbModal,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.logger.log("ngOnInit");
|
||||||
|
this.accountId = +this.route.snapshot.paramMap.get('accountId')
|
||||||
|
// Load operations on controller initialization.
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Add a new operation at the beginning of th array.
|
||||||
|
*/
|
||||||
|
add() {
|
||||||
|
var schedule = new Schedule();
|
||||||
|
schedule.account_id = this.accountId;
|
||||||
|
|
||||||
|
const modal = this.ngbModal.open(ScheduleEditModalComponent, {
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.componentInstance.schedule = schedule;
|
||||||
|
|
||||||
|
modal.result.then((schedule: Schedule) => {
|
||||||
|
this.save(schedule);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.logger.log("Loading schedules for accountId", this.accountId);
|
||||||
|
if(!this.accountId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleService.query(this.accountId)
|
||||||
|
.subscribe((schedules: Schedule[]) => {
|
||||||
|
this.logger.log("Schedules loaded.", schedules);
|
||||||
|
this.schedules = schedules;
|
||||||
|
}, (reason) => {
|
||||||
|
this.logger.log("Got error", 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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
115
src/scheduler/scheduleRow.component.ts
Normal file
115
src/scheduler/scheduleRow.component.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 { Logger } from '@nsalaun/ng-logger';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
|
||||||
|
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
|
||||||
|
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
|
||||||
|
import { ScheduleService } from './schedule.service';
|
||||||
|
import { Schedule } from './schedule';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'tr[schedule-row]',
|
||||||
|
host: {
|
||||||
|
"[id]": "schedule.id",
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<td>{{ schedule.start_date | date: "yyyy-MM-dd" }}</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td>{{ schedule.stop_date | date: "yyyy-MM-dd" }}</td>
|
||||||
|
|
||||||
|
<td>{{ schedule.day }}</td>
|
||||||
|
|
||||||
|
<td>{{ schedule.frequency }}</td>
|
||||||
|
|
||||||
|
<td>{{ schedule.label }}</td>
|
||||||
|
|
||||||
|
<td>{{ schedule.value | currency:"EUR":true }}</td>
|
||||||
|
|
||||||
|
<td>{{ schedule.category }}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="btn-group btn-group-sm">
|
||||||
|
<!-- Edit operation. -->
|
||||||
|
<button type="button" class="btn btn-success"
|
||||||
|
(click)="modify()" title="edit">
|
||||||
|
<span class="fa fa-pencil-square-o"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Remove operation. -->
|
||||||
|
<button type="button" class="btn btn-danger"
|
||||||
|
[hidden]="!schedule.id"
|
||||||
|
(click)="confirmDelete()"
|
||||||
|
title="remove">
|
||||||
|
<span class="fa fa-trash"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class ScheduleRowComponent {
|
||||||
|
@Input('schedule-row') schedule: Schedule;
|
||||||
|
@Output() needsReload: EventEmitter<void> = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private scheduleService: ScheduleService,
|
||||||
|
private logger: Logger,
|
||||||
|
private toastrService: ToastrService,
|
||||||
|
private ngbModal: NgbModal
|
||||||
|
) {}
|
||||||
|
|
||||||
|
save(schedule: Schedule) {
|
||||||
|
return this.scheduleService.update(schedule).subscribe((schedule: Schedule) => {
|
||||||
|
this.toastrService.success('Schedule #' + schedule.id + ' saved.');
|
||||||
|
|
||||||
|
this.needsReload.emit();
|
||||||
|
}, result => {
|
||||||
|
this.toastrService.error(
|
||||||
|
'Error while saving schedule: ' + result.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmDelete() {
|
||||||
|
const modal = this.ngbModal.open(ScheduleDeleteModalComponent);
|
||||||
|
|
||||||
|
modal.componentInstance.schedule = this.schedule;
|
||||||
|
|
||||||
|
modal.result.then((schedule: Schedule) => {
|
||||||
|
this.delete(schedule);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(schedule: Schedule) {
|
||||||
|
var id = schedule.id;
|
||||||
|
|
||||||
|
return this.scheduleService.delete(schedule).subscribe(() => {
|
||||||
|
this.toastrService.success('Schedule #' + id + ' deleted.');
|
||||||
|
|
||||||
|
this.needsReload.emit();
|
||||||
|
}, result => {
|
||||||
|
this.toastrService.error(
|
||||||
|
'An error occurred while trying to delete schedule #' + id + ':<br />'
|
||||||
|
+ result.message
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
modify() {
|
||||||
|
const modal = this.ngbModal.open(ScheduleEditModalComponent, {
|
||||||
|
size: 'lg'
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.componentInstance.schedule = this.schedule;
|
||||||
|
|
||||||
|
modal.result.then((schedule: Schedule) => {
|
||||||
|
this.save(schedule);
|
||||||
|
}, (reason) => function(reason) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,92 +0,0 @@
|
|||||||
<!--
|
|
||||||
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="schedulerCtrl.add()">
|
|
||||||
Ajouter
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr id="{{ operation.id }}" class="form-inline"
|
|
||||||
ng-repeat="operation in schedulerCtrl.operations">
|
|
||||||
<td class="col-md-1">
|
|
||||||
{{ operation.start_date | date: "yyyy-MM-dd" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.stop_date | date: "yyyy-MM-dd" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.day }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.frequency }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.label }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.value | currency : "€" }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
{{ operation.category }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<div class="btn-group btn-group-xs">
|
|
||||||
<!-- Edit operation. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-click="schedulerCtrl.modify(operation)" title="edit">
|
|
||||||
<span class="fa fa-pencil-square-o"></span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Remove operation. -->
|
|
||||||
<button type="button" class="btn btn-default"
|
|
||||||
ng-if="operation.id"
|
|
||||||
ng-click="schedulerCtrl.confirmDelete(operation)"
|
|
||||||
title="remove">
|
|
||||||
<span class="fa fa-trash"></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"sourceMap": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"lib": [ "es2015", "dom" ],
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"suppressImplicitAnyIndexErrors": true
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,11 @@ const webpack = require('webpack');
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
context: path.resolve(__dirname, 'src'),
|
context: path.resolve(__dirname, 'src'),
|
||||||
entry: './app.js',
|
entry: './main.ts',
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx', '.html'],
|
||||||
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [{
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
@ -29,7 +32,6 @@ module.exports = {
|
|||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
//include: path.resolve(__dirname, 'src'),
|
//include: path.resolve(__dirname, 'src'),
|
||||||
exclude: /(node_modules|bootstrap)/,
|
|
||||||
loader: 'eslint-loader',
|
loader: 'eslint-loader',
|
||||||
options: {
|
options: {
|
||||||
useEslintrc: false,
|
useEslintrc: false,
|
||||||
@ -54,6 +56,10 @@ module.exports = {
|
|||||||
test: /\.jsx?$/,
|
test: /\.jsx?$/,
|
||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
loader: 'babel-loader'
|
loader: 'babel-loader'
|
||||||
|
}, {
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
loader: 'ts-loader'
|
||||||
}, {
|
}, {
|
||||||
test: /\.html$/,
|
test: /\.html$/,
|
||||||
use: [
|
use: [
|
||||||
@ -73,6 +79,13 @@ module.exports = {
|
|||||||
'style-loader',
|
'style-loader',
|
||||||
'css-loader',
|
'css-loader',
|
||||||
]
|
]
|
||||||
|
}, {
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader',
|
||||||
|
]
|
||||||
}, {
|
}, {
|
||||||
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
|
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
|
||||||
loader: 'url-loader?limit=100000'
|
loader: 'url-loader?limit=100000'
|
||||||
@ -86,6 +99,10 @@ module.exports = {
|
|||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
"window.jQuery": "jquery"
|
"window.jQuery": "jquery"
|
||||||
}),
|
}),
|
||||||
|
new webpack.ContextReplacementPlugin(
|
||||||
|
/angular(\\|\/)core(\\|\/)@angular/,
|
||||||
|
path.resolve(__dirname, './')
|
||||||
|
)
|
||||||
],
|
],
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, 'build'),
|
path: path.resolve(__dirname, 'build'),
|
||||||
|
Loading…
Reference in New Issue
Block a user