26 Commits

Author SHA1 Message Date
a41811356a Remove require. 2017-10-28 22:18:29 +02:00
73a43b6e2a Migrate schedule edit form to Material. 2017-10-28 22:16:55 +02:00
2287ca8ad3 Fix and cleanup account edit dialog. 2017-10-28 22:16:55 +02:00
877315d6e1 Migrate schedule edit modal to Material. 2017-10-28 22:16:55 +02:00
d5160a55fb Remove unused shedule row component. 2017-10-28 22:16:55 +02:00
73de67db73 Migrate schedule delete dialog to Material. 2017-10-28 22:16:55 +02:00
f632722916 Migrate schedule list to material. 2017-10-28 22:16:55 +02:00
048a2a7d08 Add Schedule DataSource. 2017-10-28 22:16:55 +02:00
6a93eac767 Migrate to material. 2017-10-28 22:16:55 +02:00
2f5538fc08 Upgrades. 2017-10-28 22:16:55 +02:00
4e21327d49 Fix design. 2017-10-28 22:13:54 +02:00
82670ce86b Finish migration of account list to material. 2017-10-28 22:13:54 +02:00
3c6ebea602 Upgrades. 2017-10-28 22:13:54 +02:00
3088ccc748 Disable theme customization. 2017-10-28 22:12:06 +02:00
2a07506005 Finish migration of account edit form to material. 2017-10-28 22:12:06 +02:00
b6d710ec67 Begin to migrato to angular/material. 2017-10-28 22:12:06 +02:00
ca9bccaf92 Try to improve global page template. 2017-10-28 22:12:06 +02:00
7067366ac1 Improve style. 2017-10-28 22:12:06 +02:00
c63c9c0b24 Add roboto font face. 2017-10-28 22:12:06 +02:00
30a1a69451 Add material theme in style. 2017-10-28 22:09:48 +02:00
a84a1f1c28 Migrate login module to material. 2017-10-28 22:09:48 +02:00
2236f317c6 Migrate app module and component to material. 2017-10-28 22:09:48 +02:00
93563aceae Install flex. 2017-10-28 22:09:48 +02:00
6800b1ee2d Improve webpack configuration. 2017-10-28 22:04:12 +02:00
04433bed3e Add TS linting. 2017-10-28 22:04:12 +02:00
3d5b3ae14b Fix SASS loading. 2017-10-28 22:02:26 +02:00
29 changed files with 808 additions and 806 deletions

View File

@ -4,20 +4,21 @@
"repository": "https://git.lahouze.org/xals/accountant",
"license": "AGPL-1.0",
"devDependencies": {
"@types/requirejs": "^2.1.31",
"angular2-template-loader": "^0.6.2",
"awesome-typescript-loader": "^3.2.3",
"babel-core": "^6.26.0",
"babel-eslint": "^8.0.1",
"babel-loader": "^7.1.2",
"codelyzer": "^3.1.2",
"css-loader": "^0.28.5",
"codelyzer": "^3.2.0",
"css-loader": "^0.28.7",
"ejs-loader": "^0.3.0",
"eslint": "^4.10.0",
"eslint-config-angular": "^0.5.0",
"eslint-config-webpack": "^1.2.5",
"eslint-loader": "^1.9.0",
"eslint-plugin-angular": "^3.1.0",
"eslint-plugin-html": "^3.2.0",
"eslint-plugin-angular": "^3.1.1",
"eslint-plugin-html": "^3.2.2",
"eslint-plugin-jquery": "^1.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-security": "^1.4.0",
@ -38,38 +39,42 @@
"style-loader": "^0.19.0",
"ts-loader": "^3.1.0",
"tslint": "^5.7.0",
"tslint-config-prettier": "^1.4.0",
"tslint-config-prettier": "^1.5.0",
"tslint-loader": "^3.5.3",
"typescript": "^2.4.2",
"typescript": "^2.5.2",
"url-loader": "^0.6.2",
"webpack": "^3.8.1",
"webpack-concat-plugin": "^1.4.1",
"webpack-dev-server": "^2.9.3"
},
"dependencies": {
"@angular/animations": "^4.4.6",
"@angular/cdk": "^2.0.0-beta.10",
"@angular/common": "^4.4.6",
"@angular/compiler": "^4.4.6",
"@angular/core": "^4.4.6",
"@angular/flex-layout": "^2.0.0-beta.9",
"@angular/forms": "^4.4.6",
"@angular/http": "^4.4.6",
"@angular/material": "^2.0.0-beta.10",
"@angular/platform-browser": "^4.4.6",
"@angular/platform-browser-dynamic": "^4.4.6",
"@angular/router": "^4.4.6",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.2",
"@nsalaun/ng-logger": "^2.0.2",
"@types/c3": "^0.4.45",
"@types/jquery": "^3.2.12",
"@types/node": "^8.0.47",
"angular2-text-mask": "^8.0.3",
"angular2-text-mask": "^8.0.4",
"base64util": "^1.0.2",
"bootstrap": "4.0.0-beta",
"c3": "^0.4.17",
"bootstrap": "^4.0.0-beta",
"c3": "^0.4.18",
"font-awesome": "^4.7.0",
"jquery": "^3.2.1",
"material-design-icons": "^3.0.1",
"moment": "^2.19.1",
"ng2-materialize": "^1.5.1",
"ngx-toastr": "^6.5.0",
"reflect-metadata": "^0.1.10",
"roboto-fontface": "^0.8.0",
"rxjs": "^5.5.2",
"zone.js": "^0.8.17"
},

View File

@ -0,0 +1,36 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Injectable } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Rx';
import { Account } from './account';
import { AccountBalances } from './accountBalances';
import { AccountBalancesService } from './accountBalances.service';
import { AccountService } from './account.service';
@Injectable()
export class AccountDataSource extends DataSource<Account> {
constructor(
private accountService: AccountService,
private accountBalancesService: AccountBalancesService,
) {
super();
}
connect(): Observable<Account[]> {
return this.accountService.query().map((accounts: Account[]) => {
for(let account of accounts) {
this.accountBalancesService
.get(account.id)
.subscribe((accountBalances: AccountBalances) => {
account.balances = accountBalances;
})
}
return accounts;
});
}
disconnect() {}
}

View File

@ -3,21 +3,28 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import {
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
} from '@angular/material';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
import { NgLoggerModule, Level } from '@nsalaun/ng-logger';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ToastrModule } from 'ngx-toastr';
import { MaterializeModule } from 'ng2-materialize';
import { AccountService } from './account.service';
import { AccountBalancesService } from './accountBalances.service';
import { AccountListComponent } from './accountList.component';
import { AccountDataSource } from './account.dataSource';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
import { AccountFormComponent } from './accountForm.component';
import { AccountRowComponent } from './accountRow.component';
import { DailyBalanceService } from './dailyBalance.service';
import { AccountListState } from './account.states'
@ -29,13 +36,19 @@ import { AccountListState } from './account.states'
RouterModule.forChild([
AccountListState
]),
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
NgLoggerModule,
ToastrModule,
NgbModule,
MaterializeModule,
],
providers: [
AccountService,
AccountDataSource,
AccountBalancesService,
DailyBalanceService,
],
@ -44,7 +57,6 @@ import { AccountListState } from './account.states'
AccountDeleteModalComponent,
AccountEditModalComponent,
AccountFormComponent,
AccountRowComponent
],
entryComponents: [
AccountListComponent,

View File

@ -1,53 +1,30 @@
// 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';
import { Component, Inject } from '@angular/core';
import { MD_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'account-delete-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<h3 md-dialog-title>Delete account #{{ data.account.id }}</h3>
<div class="modal-body" id="modal-body">
<p>
Do you really want to delete account #{{ account.id }} with name:<br/>
{{ account.name }}
</p>
</div>
<md-dialog-content>
Do you really want to delete account #{{ data.account.id }} with name:<br/>
{{ data.account.name }}
</md-dialog-content>
<div class="modal-footer">
<button class="btn btn-danger" (click)="submit()">
<md-dialog-actions>
<button md-raised-button color="warn" [md-dialog-close]="data.account">
Yes
</button>
<button class="btn btn-default" (click)="cancel()">
<button md-raised-button md-dialog-close>
No
</button>
</div>
</md-dialog-actions>
`
})
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");
}
constructor(
@Inject(MD_DIALOG_DATA) private data: any
) {}
}

View File

@ -1,7 +1,6 @@
// 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 { Component, Inject, ViewChild } from '@angular/core';
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
import { Account } from './account';
import { AccountFormComponent } from './accountForm.component';
@ -9,33 +8,37 @@ 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>
<h3 md-dialog-title>{{ title() }}</h3>
<div class="modal-body" id="modal-body">
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm"></account-form>
</div>
<md-dialog-content>
<account-form [account]="account" (submit)="submit()" #accountForm="accountForm">
</account-form>
</md-dialog-content>
<div class="modal-footer">
<button class="btn btn-primary" [disabled]="!accountForm.form.valid" (click)="submit()">
<md-dialog-actions>
<button md-raised-button color="primary" [disabled]="!accountForm?.form.valid" (click)="submit()">
Save
</button>
<button class="btn btn-default" (click)="cancel()">
<button md-raised-button color="warn" md-dialog-close>
Cancel
</button>
</div>
</md-dialog-actions>
`
})
export class AccountEditModalComponent {
@Input() account: Account;
private account: Account;
@ViewChild('accountForm') accountForm: AccountFormComponent;
constructor(private activeModal: NgbActiveModal) {}
constructor(
@Inject(MD_DIALOG_DATA) public data: any,
public dialogRef: MdDialogRef<AccountEditModalComponent>,
) {
this.account = data.account;
}
title(): string {
if(this.account.id) {
if(this.account && this.account.id) {
return "Account #" + this.account.id;
} else {
return "New account";
@ -48,12 +51,8 @@ export class AccountEditModalComponent {
account.id = this.account.id;
account.name = formModel.name;
account.authorized_overdraft = formModel.authorizedOverdraft;
account.authorized_overdraft = -formModel.authorizedOverdraft;
this.activeModal.close(account);
}
cancel(): void {
this.activeModal.dismiss("closed");
this.dialogRef.close(account);
}
}

View File

@ -8,51 +8,33 @@ import { Account } from './account';
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>
<form novalidate (keyup.enter)="submit()" [formGroup]="form">
<md-list>
<md-list-item>
<md-form-field>
<input mdInput formControlName="name" placeholder="Account name">
<div class="col-sm-8"
[class.has-danger]="name.errors">
<input class="form-control"
id="name" formControlName="name"
placeholder="Account name">
<md-error *ngIf="name.errors?.required">The account name is required.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<span mdPrefix>-</span>
<input mdInput formControlName="authorizedOverdraft"
placeholder="Authorized overdraft">
<span mdSuffix>.00€</span>
<div class="input-group-addon">.00€</div>
</div>
<div class="help-block text-danger" *ngIf="authorizedOverdraft.errors">
<p *ngIf="authorizedOverdraft.errors.required">
<md-error *ngIf="authorizedOverdraft.errors?.required">
The authorized overdraft is required.
</p>
</md-error>
<p *ngIf="authorizedOverdraft.errors.max">
<md-error *ngIf="authorizedOverdraft.errors?.min">
The authorized overdraft must be less than or equal to 0.
</p>
</div>
</div>
</div>
</md-error>
</md-form-field>
</md-list-item>
</md-list>
</form>
`
})
@ -66,12 +48,12 @@ export class AccountFormComponent implements OnInit {
ngOnInit() {
this.form = this.formBuilder.group({
name: ['', Validators.required],
authorizedOverdraft: ['', [Validators.required, Validators.max(0)]],
authorizedOverdraft: ['', [Validators.required, Validators.min(0)]],
});
this.form.patchValue({
name: this.account.name,
authorizedOverdraft: this.account.authorized_overdraft
authorizedOverdraft: -this.account.authorized_overdraft
});
}

View File

@ -1,89 +1,141 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { MdDialog } from '@angular/material';
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 { AccountDataSource } from './account.dataSource';
import { AccountService } from './account.service';
import { AccountDeleteModalComponent } from './accountDeleteModal.component';
import { AccountEditModalComponent } from './accountEditModal.component';
@Component({
selector: 'account-list',
template: `
<div class="row">
<div class="col s12">
<table class="bordered highlight responsive-table">
<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 mz-button class="green" (click)="add()">
Ajouter
<div class="containerX">
<div class="container">
<button md-fab color="primary" (click)="add()">
<md-icon>add</md-icon>
</button>
</td>
</tr>
</div>
<tr *ngFor="let account of accounts"
[account-row]="account" (needsReload)="load()">
</tr>
</tbody>
</table>
<div class="container">
<md-table #table [dataSource]="accounts">
<ng-container mdColumnDef="name">
<md-header-cell *mdHeaderCellDef>Nom du compte</md-header-cell>
<md-cell *mdCellDef="let account">
<a [routerLink]="['/account', account.id, 'operations']">
{{ account.name }}
</a>
</md-cell>
</ng-container>
<ng-container mdColumnDef="current">
<md-header-cell *mdHeaderCellDef>Solde courant</md-header-cell>
<md-cell *mdCellDef="let account">
<span
[class.warning]="account.authorized_overdraft < 0 && account.balances?.current < 0"
[class.error]="account.balances?.current < account.authorized_overdraft">
{{ account.balances?.current | currency:"EUR":true }}
</span>
</md-cell>
</ng-container>
<ng-container mdColumnDef="pointed">
<md-header-cell *mdHeaderCellDef>Solde pointé</md-header-cell>
<md-cell *mdCellDef="let account">
<span
[class.warning]="account.authorized_overdraft < 0 && account.balances?.pointed < 0"
[class.error]="account.balances?.pointed < account.authorized_overdraft">
{{ account.balances?.pointed | currency:"EUR":true }}
</span>
</md-cell>
</ng-container>
<ng-container mdColumnDef="authorizedOverdraft">
<md-header-cell *mdHeaderCellDef>Découvert autorisé</md-header-cell>
<md-cell *mdCellDef="let account">
{{ account.authorized_overdraft | currency:"EUR":true }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="actions">
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
<md-cell *mdCellDef="let account">
<!-- Edit account. -->
<button md-mini-fab color="primary"
(click)="modify(account)">
<md-icon>mode_edit</md-icon>
</button>
<!-- Delete account, with confirm. -->
<button md-mini-fab color="warn"
(click)="confirmDelete(account)">
<md-icon>delete_forever</md-icon>
</button>
<!-- Open account scheduler. -->
<button md-mini-fab
[hidden]="!account.id"
[routerLink]="['/account', account.id, 'scheduler']">
<md-icon>event</md-icon>
</button>
</md-cell>
</ng-container>
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
<md-row *mdRowDef="let row; columns: displayedColumns;">
</md-row>
</md-table>
</div>
</div>
`,
})
export class AccountListComponent implements OnInit {
accounts: Account[];
export class AccountListComponent {
displayedColumns: String[] = [
'name', 'current', 'pointed', 'authorizedOverdraft', 'actions'
];
constructor(
private accounts: AccountDataSource,
private accountService: AccountService,
private toastrService: ToastrService,
private logger: Logger,
private ngbModal: NgbModal
private mdDialog: MdDialog,
) {
}
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'
this.modify(new Account());
};
/*
* Modify an account.
*/
modify(account: Account) {
let dialogRef = this.mdDialog.open(AccountEditModalComponent, {
data: {
account: account,
}
});
modal.componentInstance.account = new Account();
modal.result.then((account: Account) => {
dialogRef.afterClosed().subscribe((account: Account) => {
if(account) {
this.logger.log("Modal closed => save account", account);
this.save(account);
} else {
this.logger.log("Modal dismissed");
}
}, (reason) => function(reason) {
});
};
}
/*
* Save account.
@ -91,8 +143,6 @@ export class AccountListComponent implements OnInit {
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);
@ -101,4 +151,41 @@ export class AccountListComponent implements OnInit {
);
});
};
/*
* Show a dialog to confirm account deletion.
*/
confirmDelete(account: Account) {
let dialogRef = this.mdDialog.open(AccountDeleteModalComponent, {
data: {
account: account,
}
});
dialogRef.afterClosed().subscribe((account: Account) => {
if(account) {
this.delete(account);
}
}, reason => {
this.logger.error("Delete dialog failed", reason);
});
};
/*
* Delete an account.
*/
delete(account: Account) {
var id = account.id;
this.accountService.delete(account).subscribe(account => {
this.toastrService.success('account #' + id + ' deleted.');
// FIXME Alexis Lahouze 2017-09-17 Remove from array.
}, function(result) {
this.toastrService.error(
'An error occurred while trying to delete account #' +
id + ':<br />' + result
);
});
};
};

View File

@ -1,153 +0,0 @@
// 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": "lighten-5",
"[class.orange]": "account.authorized_overdraft < 0 && accountBalances?.current < 0",
"[class.red]": "accountBalances?.current < account.authorized_overdraft",
},
template: `
<td>
<a [routerLink]="['/account', account.id, 'operations']">{{ account.name }}</a>
</td>
<td>
<span class="text-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && accountBalances?.current < 0"
[class.red-text]="accountBalances?.current < account.authorized_overdraft">
{{ accountBalances?.current | currency:"EUR":true }}
</span>
</td>
<td>
<span class="text-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && accountBalances?.pointed < 0"
[class.red-text]="accountBalances?.pointed < account.authorized_overdraft">
{{ accountBalances?.pointed | currency:"EUR":true }}
</span>
</td>
<td>{{ account.authorized_overdraft | currency:"EUR":true }}</td>
<td>
<!-- Edit account. -->
<button mz-button [float]="true" class="green"
(click)="modify()">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Delete account, with confirm. -->
<button mz-button [float]="true" class="red"
(click)="confirmDelete()">
<span class="fa fa-trash-o"></span>
</button>
<!-- Open account scheduler. -->
<button mz-button [float]="true"
[hidden]="!account.id"
[routerLink]="['/account', account.id, 'scheduler']">
<span class="fa fa-clock-o"></span>
</button>
</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;
})
}
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
);
});
};
}

View File

@ -4,15 +4,20 @@ import { Component } from '@angular/core';
@Component({
selector: 'accountant',
styles: [ require('./main.scss').toString() ],
template: `
<!-- Navbar -->
<div class="navbar-fixed">
<mz-navbar>
<a class="brand-logo" routerLink="/accounts">&nbsp;Accountant</a>
</mz-navbar>
<md-toolbar class="mat-elevation-z6" color="primary">
<div class="acc-toolbar">
<a md-button style="text-transform: uppercase"
routerLink="/accounts">
Accountant
</a>
</div>
</md-toolbar>
<div class="acc-content">
<router-outlet></router-outlet>
</div>
`
})
export class AppComponent { }

View File

@ -3,6 +3,12 @@ import 'zone.js';
import 'reflect-metadata';
import { NgModule } from '@angular/core';
import {
MdButtonModule,
MdToolbarModule,
MdSidenavModule
} from '@angular/material';
import { FlexLayoutModule } from '@angular/flex-layout';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
@ -10,7 +16,6 @@ import { RouterModule } from '@angular/router';
import { NgLoggerModule } from '@nsalaun/ng-logger';
import { ToastrModule } from 'ngx-toastr';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { MaterializeModule } from 'ng2-materialize'
import { LoginModule } from './login/login.module';
import { AccountModule } from './accounts/account.module';
@ -35,11 +40,14 @@ import { ApiBaseURL, LogLevel } from './app.config';
enableTracing: true,
useHash: true
}),
FlexLayoutModule,
MdButtonModule,
MdToolbarModule,
MdSidenavModule,
LoginModule,
NgLoggerModule.forRoot(LogLevel),
ToastrModule.forRoot(),
NgbModule.forRoot(),
MaterializeModule.forRoot(),
AccountModule,
ScheduleModule,
OperationModule,

View File

@ -9,7 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<!-- htmllint attr-bans="false" -->
<body style="padding-bottom: 50px; padding-top: 70px">
<!-- htmllint attr-bans="$previous" -->
<accountant></accountant>
</body>
</html>

31
src/login/login.config.ts Normal file
View File

@ -0,0 +1,31 @@
module.exports = function($httpProvider, $storageProvider) {
// Define interceptors.
$httpProvider.interceptors.push(function($storage) {
return {
request: function(config) {
var access_token = $storage.session.get('access_token');
if (access_token) {
//var tokenType = $storage.get('token_type');
var tokenType = 'Bearer';
var authorization = tokenType + ' ' + access_token;
config.headers.authorization = authorization;
}
return config;
},
};
});
// Configure storage
// Set global prefix for stored keys
$storageProvider.setPrefix('accountant');
// Change the default storage engine
// Defaults to 'local'
$storageProvider.setDefaultStorageEngine('session');
// Change the enabled storage engines
// Defaults to ['memory', 'cookie', 'session', 'local']
$storageProvider.setEnabledStorageEngines(['local', 'session']);
};

View File

@ -5,7 +5,12 @@ import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { MaterializeModule } from 'ng2-materialize';
import {
MdButtonModule,
MdDialogModule,
MdInputModule,
MdListModule,
} from '@angular/material';
import { NgLoggerModule } from '@nsalaun/ng-logger';
@ -19,8 +24,11 @@ import { LoginModalComponent } from './loginModal.component';
HttpClientModule,
CommonModule,
ReactiveFormsModule,
MaterializeModule,
NgLoggerModule,
MdButtonModule,
MdDialogModule,
MdInputModule,
MdListModule,
],
providers: [
LoginService,

View File

@ -1,12 +1,12 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { MdDialog } from '@angular/material';
import { Observable} from 'rxjs/Rx';
import * as base64 from 'base64util';
import { Logger } from '@nsalaun/ng-logger';
import { MzModalService } from 'ng2-materialize';
import { Token } from './token';
import { LoginModalComponent } from './loginModal.component';
@ -18,25 +18,22 @@ export class LoginService {
constructor(
private httpClient: HttpClient,
private logger: Logger,
private mzModalService: MzModalService,
private mdDialog: MdDialog,
) {}
public readonly url: string = '/api/user/login';
login(): Observable<Token> {
let modal = this.mzModalService.open(LoginModalComponent);
let dialogRef = this.mdDialog.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;
//});
return null;
return dialogRef.afterClosed().flatMap((login: Login) =>
this.doLogin(login)
).map((token: Token): Token => {
this.accessToken = token.access_token;
return token;
});
}
logout() {

View File

@ -9,33 +9,25 @@ import { Login } from './login';
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"
<md-list>
<md-list-item>
<md-form-field>
<input mdInput type="text"
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>
<md-error *ngIf="email.errors?.required">The email is required.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<input mdInput type="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>
<md-error *ngIf="password.errors?.required">The password is required.</md-error>
</md-form-field>
</md-list-item>
</md-list>
</form>
`
})

View File

@ -1,7 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { MzBaseModal } from 'ng2-materialize';
import { MdDialogRef } from '@angular/material';
import { Login } from './login';
import { LoginFormComponent } from './loginForm.component';
@ -9,41 +8,37 @@ import { LoginFormComponent } from './loginForm.component';
@Component({
selector: 'login-modal',
template: `
<mz-modal>
<mz-modal-header>
Authentification requise
</mz-modal-header>
<h2 md-dialog-title>Authentification requise</h2>
<mz-modal-content>
<md-dialog-content>
<login-form (submit)="submit()" #loginForm="loginForm"></login-form>
</mz-modal-content>
</md-dialog-content>
<mz-modal-footer>
<button class="btn btn-primary" [disabled]="!loginForm.form.valid" (click)="submit()">
<md-dialog-actions>
<button md-button [disabled]="!loginForm.form.valid" (click)="submit()">
Login
</button>
<button class="btn btn-default" (click)="cancel()">
<button md-button md-dialog-close>
Cancel
</button>
</mz-modal-footer>
</mz-modal>
</md-dialog-actions>
`
})
export class LoginModalComponent extends MzBaseModal {
export class LoginModalComponent {
@ViewChild('loginForm') loginForm: LoginFormComponent;
//submit(): void {
// let formModel = this.loginForm.form.value;
// let login: Login = new Login();
constructor(
public dialogRef: MdDialogRef<LoginModalComponent>,
) {}
// login.email = formModel.email;
// login.password = formModel.password;
submit(): void {
let formModel = this.loginForm.form.value;
let login: Login = new Login();
// this.activeModal.close(login);
//}
login.email = formModel.email;
login.password = formModel.password;
//cancel(): void {
// this.activeModal.dismiss("closed");
//}
this.dialogRef.close(login);
}
}

View File

@ -1,13 +1,69 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
$fa-font-path: '~font-awesome/fonts';
@import '~font-awesome/scss/font-awesome';
@import '~materialize-css/sass/materialize.scss';
@import '~c3/c3';
@import '~ngx-toastr/toastr';
$roboto-font-path: '~roboto-fontface/fonts/roboto/';
@import '~@angular/material/_theming';
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
@import '~material-design-icons/iconfont/material-icons.css';
/*.fixed-header {
position: fixed;
top: 0;
left: 0;
z-index: 2;
width: 100% !important;
}
body {
font-family: Roboto, 'Helvetica Neue', sans-serif;
// Helps fonts on OSX looks more consistent with other systems
// Isn't currently in button styles due to performance concerns
* {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.acc-content {
padding: 32px;
box-sizing: border-box;
}
.mat-toolbar {
.acc-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
}
h1 {
font-size: 20px;
}
}
md-dialog > form {
overflow: visible;
}*/
/*
// stretch to screen size in fullscreen mode
.acc-content {
width: 100%;
height: 100%;
box-sizing: border-box;
}
*/
.italic {
font-style: italic;
}
@ -23,3 +79,19 @@ $fa-font-path: '~font-awesome/fonts';
.c3-ygrid-line.overdraft line {
stroke: #FF0000;
}
span.warning {
color: mat-color($mat-amber, A700);
}
.mat-row.warning {
background-color: mat-color($mat-amber, 100);
}
span.error {
color: mat-color($mat-red, A700);
}
.mat-row.error {
background-color: mat-color($mat-red, 100);
}

View File

@ -10,7 +10,6 @@ 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 { MaterializeModule } from 'ng2-materialize';
import { BalanceChartComponent } from './balanceChart.component';
import { CategoryChartComponent } from './categoryChart.component';
@ -34,8 +33,7 @@ import { OperationListState } from './operation.states'
NgLoggerModule,
ToastrModule,
NgbModule,
TextMaskModule,
MaterializeModule,
TextMaskModule
],
providers: [
CategoryService,

View File

@ -19,12 +19,12 @@ import { OperationEditModalComponent } from './operationEditModal.component';
template: `
<div>
<div class="row">
<div class="col s9">
<div class="col-md-9">
<balance-chart (onUpdate)="onUpdate($event)"
[account]="account"></balance-chart>
</div>
<div class="col s3">
<div class="col-md-3">
<category-chart
[minDate]="minDate"
[maxDate]="maxDate"
@ -33,8 +33,7 @@ import { OperationEditModalComponent } from './operationEditModal.component';
</div>
<div class="row">
<div class="col s12">
<table class="bordered highlight responsive-table">
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th>#</th>
@ -49,8 +48,8 @@ import { OperationEditModalComponent } from './operationEditModal.component';
<tbody>
<tr>
<td colspan="7">
<button mz-button class="green" (click)="add()">
<td colspan="6">
<button class="btn btn-success" (click)="add()">
Ajouter
</button>
</td>
@ -64,7 +63,6 @@ import { OperationEditModalComponent } from './operationEditModal.component';
</tbody>
</table>
</div>
</div>
</div>
`
})

View File

@ -17,9 +17,8 @@ import { OperationEditModalComponent } from './operationEditModal.component';
"[id]": "operation.id",
"[class.stroke]": "operation.canceled",
"[class.italic]": "!operation.confirmed",
"class": "lighten-5",
"[class.orange]": "account.authorized_overdraft < 0 && operation.balance < 0",
"[class.red]": "operation.balance < account.authorized_overdraft"
"[class.warning]": "operation.balance < 0",
"[class.danger]": "operation.balance < account.authorized_overdraft"
},
template: `
<td>{{ operation.id }}</td>
@ -30,24 +29,24 @@ import { OperationEditModalComponent } from './operationEditModal.component';
<td>{{ operation.value | currency:'EUR':true }}</td>
<td class="test-lighten-2"
[class.orange-text]="account.authorized_overdraft < 0 && operation.balance < 0"
[class.red-text]="operation.balance < account.authorized_overdraft">
<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 mz-button [float]="true" class="green"
<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 mz-button [float]="true" class="blue"
<button type="button" class="btn btn-secondary"
*ngIf="!operation.canceled"
(click)="togglePointed(operation)"
[class.active]="operation.pointed" title="point">
@ -56,7 +55,7 @@ import { OperationEditModalComponent } from './operationEditModal.component';
</button>
<!-- Toggle canceled operation. -->
<button mz-button [float]="true" class="orange"
<button type="button" class="btn btn-warning"
(click)="toggleCanceled(operation)"
*ngIf="operation.scheduled_operation_id"
[class.active]="operation.canceled" title="cancel">
@ -64,11 +63,12 @@ import { OperationEditModalComponent } from './operationEditModal.component';
</button>
<!-- Delete operation, with confirm. -->
<button mz-button [float]="true" class="red"
<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>
`
})

View File

@ -0,0 +1,40 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Injectable } from '@angular/core';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { Logger } from '@nsalaun/ng-logger';
import { Schedule } from './schedule';
import { ScheduleService } from './schedule.service';
@Injectable()
export class ScheduleDataSource extends DataSource<Schedule> {
private subject: BehaviorSubject<number> = new BehaviorSubject<number>(null);
constructor(
private scheduleService: ScheduleService,
private logger: Logger,
) {
super();
}
load(accountId: number): void {
this.logger.log("In load", accountId);
this.subject.next(accountId);
}
connect(): Observable<Schedule[]> {
return this.subject.asObservable().concatMap((accountId: number) => {
this.logger.log("In connect", accountId);
if(accountId) {
return this.scheduleService.query(accountId);
}
});
}
disconnect() {}
}

View File

@ -3,6 +3,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule } from '@angular/forms';
import {
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
} from '@angular/material';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';
@ -10,13 +18,12 @@ 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 { MaterializeModule } from 'ng2-materialize';
import { ScheduleService } from './schedule.service';
import { ScheduleDataSource } from './schedule.dataSource';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleFormComponent } from './scheduleForm.component';
import { ScheduleRowComponent } from './scheduleRow.component';
import { ScheduleListComponent } from './scheduleList.component';
import { ScheduleListState } from './schedule.states';
@ -28,21 +35,26 @@ import { ScheduleListState } from './schedule.states';
RouterModule.forChild([
ScheduleListState
]),
MdButtonModule,
MdDialogModule,
MdIconModule,
MdInputModule,
MdListModule,
MdTableModule,
NgLoggerModule,
ToastrModule,
NgbModule,
TextMaskModule,
MaterializeModule,
TextMaskModule
],
providers: [
ScheduleService,
ScheduleDataSource,
],
declarations: [
ScheduleDeleteModalComponent,
ScheduleEditModalComponent,
ScheduleFormComponent,
ScheduleListComponent,
ScheduleRowComponent
],
entryComponents: [
ScheduleDeleteModalComponent,

View File

@ -1,49 +1,34 @@
// 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';
import { Component, Inject } from '@angular/core';
import { MD_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'schedule-delete-modal',
template: `
<div class="modal-header">
<h3 class="modal-title" id="modal-title">{{ title() }}</h3>
</div>
<h3 md-dialog-title>{{ title() }}</h3>
<div class="modal-body" id="modal-body">
<p>
Do you really want to delete schedule #{{ schedule.id }} with label:<br/>
{{ schedule.label }}
</p>
</div>
<md-dialog-content>
Do you really want to delete schedule #{{ data.schedule.id }} with label:<br/>
{{ data.schedule.label }}
</md-dialog-content>
<div class="modal-footer">
<button class="btn btn-danger" (click)="submit()">
<md-dialog-actions>
<button md-raised-button color="warn" [md-dialog-close]="data.schedule">
Yes
</button>
<button class="btn btn-default" (click)="cancel()">
<button md-raised-button md-dialog-close>
No
</button>
</div>
</md-dialog-actions>
`
})
export class ScheduleDeleteModalComponent {
@Input() schedule: Schedule
constructor(public activeModal: NgbActiveModal) {}
constructor(
@Inject(MD_DIALOG_DATA) private data: any
) {}
title(): string {
return "Delete schedule #" + this.schedule.id;
}
submit(): void {
this.activeModal.close(this.schedule);
}
cancel(): void {
this.activeModal.dismiss("closed");
return "Delete schedule #" + this.data.schedule.id;
}
}

View File

@ -1,5 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2:
import { Component, Input, ViewChild } from '@angular/core';
import { Component, Inject, ViewChild } from '@angular/core';
import { MdDialogRef, MD_DIALOG_DATA } from '@angular/material';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@ -9,32 +10,33 @@ 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>
<h3 md-dialog-title>{{ title() }}</h3>
<div class="modal-body" id="modal-body">
<md-dialog-content>
<schedule-form [schedule]="schedule" (submit)="submit()" #scheduleForm="scheduleForm"></schedule-form>
</div>
</md-dialog-content>
<div class="modal-footer">
<button class="btn btn-primary" [disabled]="!scheduleForm.form.valid" (click)="submit()">
<md-dialog-actions>
<button md-raised-button color="primary" [disabled]="!scheduleForm?.form.valid" (click)="submit()">
Save
</button>
<button class="btn btn-default" (click)="cancel()">
<button md-raised-button color="warn" md-dialog-close>
Cancel
</button>
</div>
</md-dialog-actions>
`
})
export class ScheduleEditModalComponent {
@Input() schedule: Schedule;
private schedule: Schedule;
@ViewChild('scheduleForm') scheduleForm: ScheduleFormComponent;
valid: boolean = false;
constructor(private activeModal: NgbActiveModal) {}
constructor(
@Inject(MD_DIALOG_DATA) public data: any,
public dialogRef: MdDialogRef<ScheduleEditModalComponent>,
) {
this.schedule = data.schedule;
}
title(): string {
if(this.schedule.id) {
@ -57,10 +59,6 @@ export class ScheduleEditModalComponent {
schedule.value = formModel.value;
schedule.category = formModel.category;
this.activeModal.close(schedule);
}
cancel(): void {
this.activeModal.dismiss("closed");
this.dialogRef.close(schedule);
}
}

View File

@ -9,115 +9,73 @@ import { Schedule } from './schedule';
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"
<md-list>
<md-list-item>
<md-form-field>
<input mdInput 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>
<md-error *ngIf="startDate.errors?.required">The start date is required.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<input mdInput 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>
<md-error *ngIf="stopDate.errors?.required">The stop date is required.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<input mdInput 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>
<md-error *ngIf="day.errors?.required">The day is required.</md-error>
<md-error *ngIf="day.errors?.min">The day must be greater than 0.</md-error>
<md-error *ngIf="day.errors?.max">The day must be less than or equal to 31.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<input mdInput 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>
<md-error *ngIf="frequency.errors?.required">The frequency is required.</md-error>
<md-error *ngIf="frequency.errors?.min">The frequency must be positive.</md-error>
</md-form-field>
</md-list-item>
<div class="form-group row">
<label class="col-sm-4 control-label" for="label">Label</label>
<md-list-item>
<md-form-field>
<input mdInput formControlName="label" placeholder="Label">
<div class="col-sm-8"
[class.has-danger]="label.errors">
<input class="form-control"
id="label" formControlName="label"
placeholder="Label">
<md-error *ngIf="label.errors?.required">The label is required.</md-error>
</md-form-field>
</md-list-item>
<div class="help-block text-danger" *ngIf="label.errors">
<p *ngIf="label.errors.required">The label is required.</p>
</div>
</div>
</div>
<md-list-item>
<md-form-field>
<input mdInput formControlName="value" type="number" placeholder="Value">
<div class="form-group row">
<label class="col-sm-4 control-label" for="value">Montant</label>
<md-error *ngIf="value.errors?.required">The value is required.</md-error>
</md-form-field>
</md-list-item>
<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"
<md-list-item>
<md-form-field>
<input mdInput 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>
<md-error *ngIf="category.errors?.required">The category is required.</md-error>
</md-form-field>
</md-list-item>
</md-list>
</form>
`
})

View File

@ -1,5 +1,6 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { Component, Inject, OnInit } from '@angular/core';
import { MdDialog } from '@angular/material';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Rx';
@ -8,6 +9,8 @@ import { Logger } from '@nsalaun/ng-logger';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { ScheduleDataSource } from './schedule.dataSource';
import { ScheduleDeleteModalComponent } from './scheduleDeleteModal.component';
import { ScheduleEditModalComponent } from './scheduleEditModal.component';
import { ScheduleService } from './schedule.service';
import { Schedule } from './schedule';
@ -15,41 +18,95 @@ import { Schedule } from './schedule';
@Component({
selector: 'schedule-list',
template: `
<div class="row">
<table class="bordered highlight responsive-table">
<thead>
<tr>
<th>Date de d&eacute;but</th>
<th>Date de fin</th>
<th>Jour</th>
<th>Fr&eacute;q.</th>
<th>Libell&eacute; de l'op&eacute;ration</th>
<th>Montant</th>
<th>Cat&eacute;gorie</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8">
<button mz-button class="green" (click)="add()">
Ajouter
<div class="containerX">
<div class="container">
<button md-fab color="primary" (click)="add()">
<md-icon>add</md-icon>
</button>
</td>
</tr>
</div>
<tr *ngFor="let schedule of schedules"
[schedule-row]="schedule" (needsReload)="load()">
</tr>
</tbody>
</table>
<div class="container">
<md-table #table [dataSource]="schedules">
<ng-container mdColumnDef="start_date">
<md-header-cell *mdHeaderCellDef>Date de d&eacute;but</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.start_date | date: "yyyy-MM-dd" }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="stop_date">
<md-header-cell *mdHeaderCellDef>Date de fin</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.stop_date | date: "yyyy-MM-dd" }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="day">
<md-header-cell *mdHeaderCellDef>Jour</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.day }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="frequency">
<md-header-cell *mdHeaderCellDef>Fr&eacute;q.</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.frequency }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="label">
<md-header-cell *mdHeaderCellDef>Libell&eacute; de l'op&eacute;ration</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.label }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="value">
<md-header-cell *mdHeaderCellDef>Montant</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.value | currency: "EUR":true }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="category">
<md-header-cell *mdHeaderCellDef>Cat&eacute;gorie</md-header-cell>
<md-cell *mdCellDef="let schedule">
{{ schedule.category }}
</md-cell>
</ng-container>
<ng-container mdColumnDef="actions">
<md-header-cell *mdHeaderCellDef>Actions</md-header-cell>
<md-cell *mdCellDef="let schedule">
<!-- Edit operation. -->
<button md-mini-fab color="primary" (click)="modify(schedule)">
<md-icon>mode_edit</md-icon>
</button>
<!-- Remove operation. -->
<button md-mini-fab color="warn" [hidden]="!schedule.id"
(click)="confirmDelete(schedule)">
<md-icon>delete_forever</md-icon>
</button>
</md-cell>
</ng-container>
<md-header-row *mdHeaderRowDef="displayedColumns"></md-header-row>
<md-row *mdRowDef="let row; columns: displayedColumns;">
</md-row>
</md-table>
</div>
</div>
`
})
export class ScheduleListComponent implements OnInit {
accountId: number;
schedules = [];
private accountId: number;
private displayedColumns: String[] = [
'start_date', 'stop_date', 'day', 'frequency',
'label', 'value', 'category', 'actions'
];
constructor(
private toastrService: ToastrService,
@ -57,6 +114,8 @@ export class ScheduleListComponent implements OnInit {
private logger: Logger,
private ngbModal: NgbModal,
private route: ActivatedRoute,
private schedules: ScheduleDataSource,
private mdDialog: MdDialog,
) {}
ngOnInit() {
@ -66,41 +125,37 @@ export class ScheduleListComponent implements OnInit {
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);
this.schedules.load(this.accountId);
}
);
/*
* Add a new operation at the beginning of th array.
*/
add() {
this.modify(new Schedule());
};
modify(schedule: Schedule) {
let dialogRef = this.mdDialog.open(ScheduleEditModalComponent, {
data: {
schedule: schedule,
}
});
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
if(schedule) {
this.save(schedule);
}
}, (reason) => function(reason) {
});
}
save(schedule: Schedule) {
return this.scheduleService.create(schedule).subscribe((schedule: Schedule) => {
this.toastrService.success('Schedule #' + schedule.id + ' saved.');
@ -112,4 +167,35 @@ export class ScheduleListComponent implements OnInit {
);
});
};
confirmDelete(schedule: Schedule) {
let dialogRef = this.mdDialog.open(ScheduleDeleteModalComponent, {
data: {
schedule: schedule,
}
});
dialogRef.afterClosed().subscribe((schedule: Schedule) => {
if(schedule) {
this.delete(schedule);
}
}, (reason) => function(reason) {
this.logger.error("Delete dialog failed", reason);
});
}
delete(schedule: Schedule) {
var id = schedule.id;
return this.scheduleService.delete(schedule).subscribe(() => {
this.toastrService.success('Schedule #' + id + ' deleted.');
this.load();
}, result => {
this.toastrService.error(
'An error occurred while trying to delete schedule #' + id + ':<br />'
+ result.message
);
});
}
};

View File

@ -1,113 +0,0 @@
// vim: set tw=80 ts=2 sw=2 sts=2 :
import { CurrencyPipe } from '@angular/common';
import { Component, Inject, Input, Output, EventEmitter } from '@angular/core';
import { 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>
<!-- Edit operation. -->
<button mz-button [float]="true" class="green"
(click)="modify()" title="edit">
<span class="fa fa-pencil-square-o"></span>
</button>
<!-- Remove operation. -->
<button mz-button [float]="true" class="red"
[hidden]="!schedule.id"
(click)="confirmDelete()"
title="remove">
<span class="fa fa-trash"></span>
</button>
</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) {
});
}
}

View File

@ -8,10 +8,6 @@
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": false,
"suppressImplicitAnyIndexErrors": true,
"types": [
"jquery",
"materialize-css"
]
"suppressImplicitAnyIndexErrors": true
}
}

View File

@ -6,15 +6,11 @@ const webpack = require('webpack');
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
main: [
"main": [
'./main.ts'
],
styles: [
"styles": [
'./main.scss'
],
vendor: [
'jquery',
'materialize-css'
]
},
devtool: 'source-map',
@ -118,15 +114,8 @@ module.exports = {
}]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor"
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery",
"window.jQuery": "jquery",
"window.Materialize": "materialize-css",
"Materialize": "materialize-css"
"window.jQuery": "jquery"
}),
new webpack.ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,