diff --git a/src/accounts/dailyBalance.service.ts b/src/accounts/dailyBalance.service.ts
new file mode 100644
index 0000000..774c8ea
--- /dev/null
+++ b/src/accounts/dailyBalance.service.ts
@@ -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 {
+ return this.http.get(`/api/account/${id}/daily_balances`);
+ }
+}
diff --git a/src/accounts/dailyBalance.ts b/src/accounts/dailyBalance.ts
new file mode 100644
index 0000000..68c3669
--- /dev/null
+++ b/src/accounts/dailyBalance.ts
@@ -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;
+}
diff --git a/src/accounts/index.js b/src/accounts/index.js
deleted file mode 100644
index 75e3bc0..0000000
--- a/src/accounts/index.js
+++ /dev/null
@@ -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 .
- */
-/* 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;
diff --git a/src/app.component.ts b/src/app.component.ts
new file mode 100644
index 0000000..9d53703
--- /dev/null
+++ b/src/app.component.ts
@@ -0,0 +1,18 @@
+// vim: set tw=80 ts=2 sw=2 sts=2 :
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'accountant',
+ template: `
+
+
+
+
+
+
+ `
+})
+export class AppComponent { }
diff --git a/src/app.config.js b/src/app.config.js
deleted file mode 100644
index b3400dd..0000000
--- a/src/app.config.js
+++ /dev/null
@@ -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'
- });
-};
diff --git a/src/app.config.ts b/src/app.config.ts
new file mode 100644
index 0000000..482e7b4
--- /dev/null
+++ b/src/app.config.ts
@@ -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;
diff --git a/src/app.js b/src/app.js
deleted file mode 100644
index b4b2d47..0000000
--- a/src/app.js
+++ /dev/null
@@ -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 .
- */
-/* 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);
diff --git a/src/app.module.ts b/src/app.module.ts
new file mode 100644
index 0000000..48d4d72
--- /dev/null
+++ b/src/app.module.ts
@@ -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() {}
+}
diff --git a/src/bootstrap.config.js b/src/bootstrap.config.js
deleted file mode 100644
index bcd24eb..0000000
--- a/src/bootstrap.config.js
+++ /dev/null
@@ -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
- }
-};
diff --git a/src/bootstrap.config.less b/src/bootstrap.config.less
deleted file mode 100644
index f2d357b..0000000
--- a/src/bootstrap.config.less
+++ /dev/null
@@ -1,3 +0,0 @@
-@pre-border-color: @pre-bg; // hide the border.
-
-@import "./main.less";
diff --git a/src/index.ejs b/src/index.ejs
index 888c8b5..e69bd09 100644
--- a/src/index.ejs
+++ b/src/index.ejs
@@ -19,30 +19,16 @@
+ <% htmlWebpackPlugin.options.title %>
-
+
-
-
-
-
-
-
-
-
-
+
diff --git a/src/login/authInterceptor.ts b/src/login/authInterceptor.ts
new file mode 100644
index 0000000..bd3c984
--- /dev/null
+++ b/src/login/authInterceptor.ts
@@ -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;
+
+ constructor(
+ private logger: Logger,
+ private injector: Injector,
+ ) {}
+
+ injectAuthorizationHeader(request: HttpRequest, accessToken: string) {
+ this.logger.log('Injecting Authorization header');
+
+ return request;
+ }
+
+ intercept(
+ request: HttpRequest,
+ next: HttpHandler,
+ pass?: number
+ ): Observable> {
+ 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 = next.handle(request);
+
+ return observable.catch(
+ (error, caught): Observable => {
+ 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> => {
+ 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);
+ });
+ }
+ );
+ }
+}
diff --git a/src/login/index.js b/src/login/index.js
deleted file mode 100644
index ad71013..0000000
--- a/src/login/index.js
+++ /dev/null
@@ -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 .
- */
-/* 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;
diff --git a/src/login/login.config.js b/src/login/login.config.ts
similarity index 100%
rename from src/login/login.config.js
rename to src/login/login.config.ts
diff --git a/src/login/login.module.ts b/src/login/login.module.ts
new file mode 100644
index 0000000..8e4f4b9
--- /dev/null
+++ b/src/login/login.module.ts
@@ -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 {};
diff --git a/src/login/login.service.js b/src/login/login.service.js
deleted file mode 100644
index 8140b79..0000000
--- a/src/login/login.service.js
+++ /dev/null
@@ -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,
- };
-};
diff --git a/src/login/login.service.ts b/src/login/login.service.ts
new file mode 100644
index 0000000..7418807
--- /dev/null
+++ b/src/login/login.service.ts
@@ -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 {
+ let modal = this.ngbModal.open(LoginModalComponent);
+
+ sessionStorage.clear();
+
+ let observable: Observable = 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 {
+ 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);
+ }
+};
diff --git a/src/login/login.ts b/src/login/login.ts
new file mode 100644
index 0000000..3424c59
--- /dev/null
+++ b/src/login/login.ts
@@ -0,0 +1,6 @@
+// vim: set tw=80 ts=2 sw=2 sts=2:
+
+export class Login {
+ email: string;
+ password: string;
+}
diff --git a/src/login/loginForm.component.ts b/src/login/loginForm.component.ts
new file mode 100644
index 0000000..1dc2f3a
--- /dev/null
+++ b/src/login/loginForm.component.ts
@@ -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: `
+
+ `
+})
+
+export class LoginFormComponent {
+ public form: FormGroup;
+ @Input('login-form') private login: Login
+ @Output('submit') submitEventEmitter: EventEmitter = new EventEmitter();
+
+ 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');
+ }
+}
diff --git a/src/login/loginModal.component.ts b/src/login/loginModal.component.ts
new file mode 100644
index 0000000..8ff7abd
--- /dev/null
+++ b/src/login/loginModal.component.ts
@@ -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: `
+
+
Authentification requise
+
+
+
+
+
+
+
+ `
+})
+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");
+ }
+}
diff --git a/src/login/token.ts b/src/login/token.ts
new file mode 100644
index 0000000..bd6902d
--- /dev/null
+++ b/src/login/token.ts
@@ -0,0 +1,6 @@
+// vim: set tw=80 ts=2 sw=2 sts=2 :
+
+export class Token {
+ access_token: string;
+ refresh_token: string;
+}
diff --git a/src/main.less b/src/main.less
deleted file mode 100644
index 8fe1882..0000000
--- a/src/main.less
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/main.scss b/src/main.scss
new file mode 100644
index 0000000..abef40c
--- /dev/null
+++ b/src/main.scss
@@ -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;
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..07b1480
--- /dev/null
+++ b/src/main.ts
@@ -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);
diff --git a/src/operations/balance-chart.component.js b/src/operations/balance-chart.component.js
deleted file mode 100644
index 6ab3fb0..0000000
--- a/src/operations/balance-chart.component.js
+++ /dev/null
@@ -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 .
- */
-/* 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: '',
- 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;
diff --git a/src/operations/balanceChart.component.ts b/src/operations/balanceChart.component.ts
new file mode 100644
index 0000000..3773418
--- /dev/null
+++ b/src/operations/balanceChart.component.ts
@@ -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: ''
+})
+export class BalanceChartComponent implements OnInit, OnChanges {
+ @Input() account: Account;
+ @Output() onUpdate: EventEmitter = new EventEmitter();
+
+ 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);
+ }
+ };
+}
diff --git a/src/operations/category-chart.component.js b/src/operations/category-chart.component.js
deleted file mode 100644
index 05e6413..0000000
--- a/src/operations/category-chart.component.js
+++ /dev/null
@@ -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 .
- */
-/* 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: '',
- 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;
diff --git a/src/operations/category.service.ts b/src/operations/category.service.ts
new file mode 100644
index 0000000..c7559df
--- /dev/null
+++ b/src/operations/category.service.ts
@@ -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 {
+ 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(`/api/account/${id}/category`, { params: params});
+ }
+}
diff --git a/src/operations/category.ts b/src/operations/category.ts
new file mode 100644
index 0000000..3e523f9
--- /dev/null
+++ b/src/operations/category.ts
@@ -0,0 +1,7 @@
+// vim: set tw=80 ts=2 sw=2 sts=2 :
+
+export class Category {
+ category: string;
+ expenses: number;
+ revenues: number;
+}
diff --git a/src/operations/categoryChart.component.ts b/src/operations/categoryChart.component.ts
new file mode 100644
index 0000000..5b0bd2b
--- /dev/null
+++ b/src/operations/categoryChart.component.ts
@@ -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: ''
+})
+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);
+ }
+ };
+}
diff --git a/src/operations/index.js b/src/operations/index.js
deleted file mode 100644
index 1b97b02..0000000
--- a/src/operations/index.js
+++ /dev/null
@@ -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 .
- */
-/* 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;
diff --git a/src/operations/operation.config.js b/src/operations/operation.config.js
deleted file mode 100644
index 0617c65..0000000
--- a/src/operations/operation.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-module.exports = function($resourceProvider) {
- // Keep trailing slashes to avoid redirect by flask..
- $resourceProvider.defaults.stripTrailingSlashes = false;
-};
diff --git a/src/operations/operation.controller.js b/src/operations/operation.controller.js
deleted file mode 100644
index 1830434..0000000
--- a/src/operations/operation.controller.js
+++ /dev/null
@@ -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 + ': ' + 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});
-};
diff --git a/src/operations/operation.delete.tmpl.html b/src/operations/operation.delete.tmpl.html
deleted file mode 100644
index 904ae6d..0000000
--- a/src/operations/operation.delete.tmpl.html
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
{{ title }}
-
-
-
-
Voulez-vous supprimer l'opération #{{ operation.id }} ayant pour libellé : {{ operation.label }}
-