You've already forked openaccounting-web
forked from cybercinch/openaccounting-web
initial commit
This commit is contained in:
86
src/app/transaction/advancededit.html
Normal file
86
src/app/transaction/advancededit.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Transaction</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form [formGroup]="form">
|
||||
<div class="form-group row">
|
||||
<label for="date" class="col-sm-4 col-form-label">Date</label>
|
||||
<div class="col-sm-8">
|
||||
<input formControlName="date" id="date" type="text" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="description" class="col-sm-4 col-form-label">Description</label>
|
||||
<div class="col-sm-8">
|
||||
<input formControlName="description" id="description" type="text" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
Account
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
Debit
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
Credit
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
</div>
|
||||
</div>
|
||||
<div *ngFor="let split of getSplitControls(); let i=index" [formGroup]="split" class="splits">
|
||||
<div *ngIf="debitVisible(i) || creditVisible(i)" class="row">
|
||||
<div class="col-sm-3"></div>
|
||||
<div *ngIf="!debitVisible(i)" class="col-sm-4"></div>
|
||||
<div *ngIf="debitVisible(i)" class="col-sm-2">{{getCurrency(split.value.accountId)}}</div>
|
||||
<div *ngIf="debitVisible(i)" class="col-sm-2">{{org.currency}}</div>
|
||||
<div *ngIf="!creditVisible(i)" class="col-sm-4"></div>
|
||||
<div *ngIf="creditVisible(i)" class="col-sm-2">{{org.currency}}</div>
|
||||
<div *ngIf="creditVisible(i)" class="col-sm-2">{{getCurrency(split.value.accountId)}}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-3 account">
|
||||
<select class="form-control" formControlName="accountId">
|
||||
<option *ngFor="let account of selectAccounts" [value]="account.id">
|
||||
{{account.fullName}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="!debitVisible(i)" class="col-sm-4">
|
||||
<button *ngIf="!creditVisible(i) || !split.value.credit" type="button" class="btn btn-primary" (click)="showDebit(i)">Debit</button>
|
||||
</div>
|
||||
<div *ngIf="debitVisible(i)" class="col-sm-2">
|
||||
<input type="text" class="form-control" formControlName="debit" />
|
||||
</div>
|
||||
<div *ngIf="debitVisible(i)" class="col-sm-2">
|
||||
<input type="text" class="form-control" formControlName="debitNative" />
|
||||
</div>
|
||||
<div *ngIf="!creditVisible(i)" class="col-sm-4">
|
||||
<button *ngIf="!debitVisible(i) || !split.value.debit" type="button" class="btn btn-primary" (click)="showCredit(i)">Credit</button>
|
||||
</div>
|
||||
<div *ngIf="creditVisible(i)" class="col-sm-2">
|
||||
<input type="text" class="form-control" formControlName="creditNative" />
|
||||
</div>
|
||||
<div *ngIf="creditVisible(i)" class="col-sm-2">
|
||||
<input type="text" class="form-control" formControlName="credit" />
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<a (click)="deleteSplit(i)">X</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 p-3">
|
||||
<a (click)="addSplit()">Add Split</a>
|
||||
</div>
|
||||
</div>
|
||||
<p *ngIf="error" class="error">{{error.message}}</p>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="activeModal.dismiss()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="submit()">Save</button>
|
||||
</div>
|
||||
9
src/app/transaction/advancededit.scss
Normal file
9
src/app/transaction/advancededit.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
/*.account {
|
||||
overflow: hidden;
|
||||
direction: rtl;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}*/
|
||||
.splits .row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
245
src/app/transaction/advancededit.ts
Normal file
245
src/app/transaction/advancededit.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Logger } from '../core/logger';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TxItem } from './txitem';
|
||||
import { Transaction, Split } from '../shared/transaction';
|
||||
import { Account, AccountTree } from '../shared/account';
|
||||
import { Org } from '../shared/org';
|
||||
import { AppError } from '../shared/error';
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormArray,
|
||||
Validators,
|
||||
FormBuilder,
|
||||
AbstractControl
|
||||
} from '@angular/forms';
|
||||
import { Util } from '../shared/util';
|
||||
import { OrgService } from '../core/org.service';
|
||||
import { TransactionService } from '../core/transaction.service';
|
||||
|
||||
@Component({
|
||||
selector: 'advancededit',
|
||||
templateUrl: './advancededit.html',
|
||||
styleUrls: ['./advancededit.scss']
|
||||
})
|
||||
export class AdvancedEdit {
|
||||
public form: FormGroup;
|
||||
public error: AppError;
|
||||
private item: TxItem;
|
||||
private accountTree: AccountTree;
|
||||
private selectAccounts: Account[];
|
||||
private org: Org;
|
||||
private visibleDebits: any = {};
|
||||
private visibleCredits: any = {};
|
||||
|
||||
constructor(
|
||||
public activeModal: NgbActiveModal,
|
||||
private log: Logger,
|
||||
private fb: FormBuilder,
|
||||
private orgService: OrgService,
|
||||
private txService: TransactionService
|
||||
) {}
|
||||
|
||||
setData(item: TxItem, accountTree: AccountTree) {
|
||||
this.item = item;
|
||||
this.accountTree = accountTree;
|
||||
this.selectAccounts = accountTree.getFlattenedAccounts().filter(account => {
|
||||
return !account.children.length;
|
||||
});
|
||||
|
||||
this.org = this.orgService.getCurrentOrg();
|
||||
|
||||
let dateString = Util.getLocalDateString(item.tx.date);
|
||||
|
||||
this.form = new FormGroup({
|
||||
date: new FormControl(dateString),
|
||||
description: new FormControl(item.tx.description),
|
||||
splits: this.fb.array([])
|
||||
});
|
||||
|
||||
let orgPrecision = this.org.precision;
|
||||
|
||||
let splits = this.form.get('splits') as FormArray;
|
||||
for(let split of item.tx.splits) {
|
||||
let precision = orgPrecision;
|
||||
let account = this.accountTree.accountMap[split.accountId];
|
||||
|
||||
if(account) {
|
||||
precision = account.precision;
|
||||
}
|
||||
|
||||
let control = new FormGroup({
|
||||
accountId: new FormControl(split.accountId),
|
||||
debit: new FormControl(
|
||||
split.amount >= 0 ? split.amount / Math.pow(10, precision) : null
|
||||
),
|
||||
credit: new FormControl(
|
||||
split.amount < 0 ? -split.amount / Math.pow(10, precision) : null
|
||||
),
|
||||
debitNative: new FormControl(
|
||||
split.nativeAmount >= 0 ? split.nativeAmount / Math.pow(10, orgPrecision) : null
|
||||
),
|
||||
creditNative: new FormControl(
|
||||
split.nativeAmount < 0 ? -split.nativeAmount / Math.pow(10, orgPrecision) : null
|
||||
)
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
// control.valueChanges.subscribe(val => {
|
||||
// this.solveEquations(item);
|
||||
// this.fillEmptySplit(item);
|
||||
// });
|
||||
|
||||
splits.push(control);
|
||||
|
||||
console.log(splits);
|
||||
|
||||
//this.fillEmptySplit(item);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrency(accountId: string) {
|
||||
let account = this.accountTree.accountMap[accountId];
|
||||
return account ? account.currency : '';
|
||||
}
|
||||
|
||||
submit() {
|
||||
console.log('submit');
|
||||
console.log(this.form.value);
|
||||
|
||||
this.error = null;
|
||||
|
||||
let date = this.item.tx.id ? this.item.tx.date : new Date();
|
||||
let formDate = Util.getDateFromLocalDateString(this.form.value.date);
|
||||
|
||||
if(formDate.getTime()) {
|
||||
// make the time be at the very end of the day
|
||||
formDate.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
let sameDay = formDate.getFullYear() === date.getFullYear() &&
|
||||
formDate.getMonth() === date.getMonth() &&
|
||||
formDate.getDate() === date.getDate();
|
||||
|
||||
if(formDate.getTime() && !sameDay) {
|
||||
date = formDate;
|
||||
}
|
||||
|
||||
let tx = new Transaction({
|
||||
id: this.item.tx.id,
|
||||
date: date,
|
||||
description: this.form.value.description,
|
||||
splits: []
|
||||
});
|
||||
|
||||
for(let i = 0; i < this.form.value.splits.length; i++) {
|
||||
let split = this.form.value.splits[i];
|
||||
let account = this.accountTree.accountMap[split.accountId];
|
||||
|
||||
if(!account) {
|
||||
this.error = new AppError('Invalid account');
|
||||
return;
|
||||
}
|
||||
|
||||
let amount = split.debit ? parseFloat(split.debit) : -parseFloat(split.credit);
|
||||
amount = Math.round(amount * Math.pow(10, account.precision));
|
||||
|
||||
let nativeAmount = split.debitNative ? parseFloat(split.debitNative) : -parseFloat(split.creditNative);
|
||||
nativeAmount = Math.round(nativeAmount * Math.pow(10, this.org.precision))
|
||||
|
||||
tx.splits.push(new Split({
|
||||
accountId: split.accountId,
|
||||
amount: amount,
|
||||
nativeAmount: nativeAmount
|
||||
}));
|
||||
}
|
||||
|
||||
this.log.debug(tx);
|
||||
|
||||
if(tx.id) {
|
||||
// update tx
|
||||
let oldId = tx.id;
|
||||
tx.id = Util.newGuid();
|
||||
|
||||
this.txService.putTransaction(oldId, tx)
|
||||
.subscribe(tx => {
|
||||
this.activeModal.close();
|
||||
|
||||
}, error => {
|
||||
this.error = error;
|
||||
});
|
||||
} else {
|
||||
// new tx
|
||||
|
||||
tx.id = Util.newGuid();
|
||||
this.txService.newTransaction(tx)
|
||||
.subscribe(tx => {
|
||||
this.activeModal.close();
|
||||
|
||||
}, error => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
addSplit() {
|
||||
this.log.debug('add split');
|
||||
|
||||
let splits = this.form.get('splits') as FormArray;
|
||||
|
||||
let control = new FormGroup({
|
||||
accountId: new FormControl(),
|
||||
debit: new FormControl(),
|
||||
credit: new FormControl(),
|
||||
debitNative: new FormControl(),
|
||||
creditNative: new FormControl()
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
// control.valueChanges.subscribe(val => {
|
||||
// this.solveEquations(item);
|
||||
// this.fillEmptySplit(item);
|
||||
// });
|
||||
splits.push(control);
|
||||
|
||||
// this.fillEmptySplit(item);
|
||||
}
|
||||
|
||||
deleteSplit(index) {
|
||||
this.log.debug('delete split');
|
||||
|
||||
let splits = this.form.get('splits') as FormArray;
|
||||
|
||||
splits.removeAt(index);
|
||||
this.visibleDebits = {};
|
||||
this.visibleCredits = {};
|
||||
|
||||
}
|
||||
|
||||
getSplitControls(): AbstractControl[] {
|
||||
return (this.form.get('splits') as FormArray).controls;
|
||||
}
|
||||
|
||||
debitVisible(index: number) {
|
||||
let splits = this.getSplitControls();
|
||||
|
||||
return this.visibleDebits[index] || splits[index].value.debit;
|
||||
}
|
||||
|
||||
creditVisible(index: number) {
|
||||
let splits = this.getSplitControls();
|
||||
|
||||
return this.visibleCredits[index] || splits[index].value.credit;
|
||||
}
|
||||
|
||||
showDebit(index: number) {
|
||||
this.visibleDebits[index] = true;
|
||||
this.visibleCredits[index] = false;
|
||||
}
|
||||
|
||||
showCredit(index: number) {
|
||||
this.visibleCredits[index] = true;
|
||||
this.visibleDebits[index] = false;
|
||||
|
||||
}
|
||||
}
|
||||
7
src/app/transaction/autocomplete.html
Normal file
7
src/app/transaction/autocomplete.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="autocomplete" [ngClass]="{visible: visible}">
|
||||
<div class="inner">
|
||||
<div class="suggestion" *ngFor="let tx of txs$ | async">
|
||||
<a (click)="click(tx)">{{tx.description}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
22
src/app/transaction/autocomplete.scss
Normal file
22
src/app/transaction/autocomplete.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.autocomplete {
|
||||
display: none;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
.inner {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #d4d4d4;
|
||||
border-bottom: none;
|
||||
}
|
||||
.suggestion {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #d4d4d4;
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete.visible {
|
||||
display: block;
|
||||
}
|
||||
68
src/app/transaction/autocomplete.ts
Normal file
68
src/app/transaction/autocomplete.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Logger } from '../core/logger';
|
||||
import { TxItem } from './txitem';
|
||||
import { EmptyObservable } from 'rxjs/observable/EmptyObservable';
|
||||
import { TransactionService } from '../core/transaction.service';
|
||||
import { Transaction } from '../shared/transaction';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Component({
|
||||
selector: 'tx-autocomplete',
|
||||
templateUrl: 'autocomplete.html',
|
||||
styleUrls: ['./autocomplete.scss']
|
||||
})
|
||||
export class Autocomplete {
|
||||
@Input() item: TxItem;
|
||||
@Input() accountId: string;
|
||||
@Output() tx = new EventEmitter<Transaction>();
|
||||
public visible: boolean;
|
||||
public txs$: Observable<Transaction[]>;
|
||||
|
||||
constructor(
|
||||
private log: Logger,
|
||||
private txService: TransactionService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.txs$ = this.item.edit$
|
||||
.switchMap(() => {
|
||||
let control = this.item.form.get('description');
|
||||
return this.item.form.get('description').valueChanges;
|
||||
})
|
||||
.debounceTime(100)
|
||||
.filter(description => {
|
||||
if(!description || description.length < 3) {
|
||||
this.visible = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.switchMap(description => {
|
||||
this.log.debug('autocomplete', description);
|
||||
|
||||
let options = {limit: 5, descriptionStartsWith: description};
|
||||
return this.txService.getTransactionsByAccount(this.accountId, options);
|
||||
}).map(txs => {
|
||||
let txMap = {};
|
||||
return txs.filter(tx => {
|
||||
if(!txMap[tx.description]) {
|
||||
txMap[tx.description] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}).do((txs) => {
|
||||
if(txs.length) {
|
||||
this.visible = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
click(tx: Transaction) {
|
||||
this.tx.emit(tx);
|
||||
this.visible = false;
|
||||
}
|
||||
|
||||
}
|
||||
6
src/app/transaction/breadcrumbs.html
Normal file
6
src/app/transaction/breadcrumbs.html
Normal file
@@ -0,0 +1,6 @@
|
||||
<div class="breadcrumbs">
|
||||
<span *ngFor="let account of accountCrumbs; let i = index">
|
||||
<span><a routerLink="/accounts">{{account.name}}</a></span>
|
||||
<span *ngIf="i < accountCrumbs.length - 1"> > </span>
|
||||
</span>
|
||||
</div>
|
||||
0
src/app/transaction/breadcrumbs.scss
Normal file
0
src/app/transaction/breadcrumbs.scss
Normal file
25
src/app/transaction/breadcrumbs.ts
Normal file
25
src/app/transaction/breadcrumbs.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Account } from '../shared/account';
|
||||
|
||||
@Component({
|
||||
selector: 'breadcrumbs',
|
||||
templateUrl: 'breadcrumbs.html',
|
||||
styleUrls: ['./breadcrumbs.scss']
|
||||
})
|
||||
export class Breadcrumbs {
|
||||
@Input() account: Account;
|
||||
public accountCrumbs: Account[];
|
||||
|
||||
constructor() {
|
||||
this.accountCrumbs = [];
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
let currentAccount = this.account;
|
||||
while(currentAccount && currentAccount.depth > 0) {
|
||||
this.accountCrumbs.unshift(currentAccount);
|
||||
currentAccount = currentAccount.parent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
124
src/app/transaction/list.html
Normal file
124
src/app/transaction/list.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<h1 *ngIf="account">{{account.name | slice:0:30}}</h1>
|
||||
|
||||
<div class="section">
|
||||
<div *ngIf="account" class="mb-2">
|
||||
<breadcrumbs [account]="account"></breadcrumbs>
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col custom-3">
|
||||
<span>Date</span>
|
||||
</div>
|
||||
<div class="col custom-7">
|
||||
<span>Description</span>
|
||||
</div>
|
||||
<div class="col custom-5">
|
||||
<span>Transfer</span>
|
||||
</div>
|
||||
<div class="col custom-3">
|
||||
<span>Debit</span>
|
||||
</div>
|
||||
<div class="col custom-3">
|
||||
<span>Credit</span>
|
||||
</div>
|
||||
<div class="col custom-3">
|
||||
<span>Balance</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="body" #body id="mybody" (scroll)="onScroll()">
|
||||
<div class="container-fluid">
|
||||
<form [id]="'form' + item.tx.id + item.activeSplitIndex" [formGroup]="item.form" *ngFor="let item of items; let i = index">
|
||||
<div class="row" (click)="editTransaction(item, $event)" [ngClass]="{odd: !(i % 2), editing: item.editing}">
|
||||
<div class="col custom-3 date">
|
||||
<span *ngIf="!item.editing" class="date">{{item.tx.date | date:"M/d/y"}}</span>
|
||||
<input *ngIf="item.editing" type="date" formControlName="date" placeholder="Date" class="form-control" (keyup.enter)="onEnter(item, $event)" (blur)="onBlur(item)"/>
|
||||
</div>
|
||||
<div class="col custom-7 description">
|
||||
<div *ngIf="!item.editing">{{item.tx.description}}</div>
|
||||
<input *ngIf="item.editing" type="text" formControlName="description" placeholder="Description" class="form-control" (keyup.enter)="onEnter(item, $event)" (blur)="onBlur(item)"/>
|
||||
<tx-autocomplete [item]="item" [accountId]="accountId" (tx)="autocomplete(item, $event)"></tx-autocomplete>
|
||||
</div>
|
||||
<div class="col custom-5 transfer">
|
||||
<span *ngIf="!item.editing" class="transfer">{{getTransferString(item) | slice:0:50}}</span>
|
||||
<select *ngIf="item.editing" class="form-control" formControlName="accountId" [attr.disabled]="item.showSplits ? '' : null" (keyup.enter)="onEnter(item, $event)" (blur)="onBlur(item)">
|
||||
<option *ngFor="let account of selectAccounts" [value]="account.id">
|
||||
{{account.fullName | slice:0:50}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col custom-3 debit">
|
||||
<span *ngIf="!item.editing" class="debit">{{getDebit(item) | currencyFormat:account.precision:account.currency}}</span>
|
||||
<input *ngIf="item.editing" type="text" formControlName="debit" placeholder="Debit" class="form-control" (keyup.enter)="onEnter(item, $event)" (blur)="onBlur(item)"/>
|
||||
</div>
|
||||
<div class="col custom-3 credit">
|
||||
<span *ngIf="!item.editing" class="credit">{{getCredit(item) | currencyFormat:account.precision:account.currency}}</span>
|
||||
<input *ngIf="item.editing" type="text" formControlName="credit" placeholder="Credit" class="form-control" (keyup.enter)="onEnter(item, $event)" (blur)="onBlur(item)"/>
|
||||
</div>
|
||||
<div class="col custom-3 balance" [ngClass]="{'negative': item.balance < 0}">
|
||||
<span *ngIf="!item.editing" class="balance">{{item.balance | currencyFormat:account.precision:account.currency}}</span>
|
||||
|
||||
<div *ngIf="item.editing" ngbDropdown class="d-inline-block">
|
||||
<button class="btn btn-outline-primary" id="dropdownBasic1" ngbDropdownToggle>Action</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<button class="dropdown-item" (click)="addSplit(item)">Split</button>
|
||||
<button class="dropdown-item" (click)="advancedEdit(item)">Adv. Edit</button>
|
||||
<button *ngIf="item.tx.id" class="dropdown-item" (click)="deleteTransaction(item)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <a *ngIf="item.editing" [routerLink]="" (click)="addSplit(item)" (mousedown)="preventBlur(item)">Split</a><br/>
|
||||
<a *ngIf="item.editing" [routerLink]="" (click)="advancedEdit(item)" (mousedown)="preventBlur(item)">Advanced Edit</a><br/>
|
||||
<a *ngIf="item.editing && item.tx.id" [routerLink]="" (click)="deleteTransaction(item)" (mousedown)="preventBlur(item)">Delete</a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" *ngFor="let split of item.form.get('splits').controls; let i=index" [formGroup]="split">
|
||||
<div class="col custom-3">
|
||||
</div>
|
||||
<div class="col custom-7 add-split">
|
||||
<a [routerLink]="" (click)="deleteSplit(item, i)" (mousedown)="preventBlur(item)">Remove Split</a>
|
||||
</div>
|
||||
<div class="col custom-5">
|
||||
<select class="form-control" formControlName="accountId" (keyup.enter)="onEnter(item, $event)">
|
||||
<option *ngFor="let account of selectAccounts" [value]="account.id">
|
||||
{{account.fullName}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col custom-3">
|
||||
<input type="text" formControlName="debit" placeholder="Debit" class="form-control" (keyup.enter)="onEnter(item, $event)"/>
|
||||
</div>
|
||||
<div class="col custom-3">
|
||||
<input type="text" formControlName="credit" placeholder="Credit" class="form-control" (keyup.enter)="onEnter(item, $event)"/>
|
||||
</div>
|
||||
<div class="col custom-3 add-split">
|
||||
<a *ngIf="i === item.form.get('splits').controls.length - 1" [routerLink]="" (click)="addSplit(item)" (mousedown)="preventBlur(item)">Add Split</a>
|
||||
<!-- <button type="submit">hidden submit</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p *ngIf="error" class="error">{{error.message}}</p>
|
||||
</div>
|
||||
|
||||
<ng-template #confirmDeleteModal let-c="close" let-d="dismiss">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Confirm delete</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="d()">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this transaction?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="d()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" (click)="c()">Delete</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
76
src/app/transaction/list.scss
Normal file
76
src/app/transaction/list.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
@import '../../sass/variables';
|
||||
|
||||
.row > div {
|
||||
border-bottom: 1px solid #bdd7ef;
|
||||
}
|
||||
.header {
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.row.odd {
|
||||
background-color: #e4f6ff;
|
||||
}
|
||||
.negative {
|
||||
color: $negative;
|
||||
}
|
||||
|
||||
.description > div {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.transfer {
|
||||
overflow: hidden;
|
||||
direction: rtl;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editing .transfer {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.body {
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 170px);
|
||||
}
|
||||
|
||||
.custom-3 {
|
||||
flex: 0 0 12.5%;
|
||||
max-width: 12.5%;
|
||||
}
|
||||
|
||||
.custom-5 {
|
||||
flex: 0 0 20.8333%;
|
||||
max-width: 20.8333%;
|
||||
}
|
||||
|
||||
.custom-7 {
|
||||
flex: 0 0 29.1666%;
|
||||
max-width: 29.1666%;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: .2rem .2rem;
|
||||
}
|
||||
|
||||
.add-split {
|
||||
padding-top: .4rem;
|
||||
padding-bottom: .4rem;
|
||||
}
|
||||
|
||||
button[type="submit"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
padding: 5px 0px;
|
||||
font-size: 0.75rem
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-inner-spin-button,
|
||||
input[type="date"]::-webkit-clear-button {
|
||||
display: none
|
||||
}
|
||||
862
src/app/transaction/list.ts
Normal file
862
src/app/transaction/list.ts
Normal file
@@ -0,0 +1,862 @@
|
||||
import { Component, Input, OnInit, ViewChild, ElementRef, AfterViewChecked, Renderer } from '@angular/core';
|
||||
import { Logger } from '../core/logger';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TransactionService } from '../core/transaction.service';
|
||||
import { AccountService } from '../core/account.service';
|
||||
import { Account, AccountTree } from '../shared/account';
|
||||
import { Transaction, Split} from '../shared/transaction';
|
||||
import { AppError } from '../shared/error';
|
||||
//import { EditTxPage } from './edit';
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormArray,
|
||||
Validators,
|
||||
FormBuilder,
|
||||
AbstractControl
|
||||
} from '@angular/forms';
|
||||
import { NgbModal, ModalDismissReasons } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Util } from '../shared/util';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import 'rxjs/add/operator/mergeMap';
|
||||
import { AdvancedEdit } from './advancededit';
|
||||
import { TxItem } from './txitem';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-txlist',
|
||||
templateUrl: 'list.html',
|
||||
styleUrls: ['./list.scss']
|
||||
})
|
||||
export class TxListPage implements OnInit, AfterViewChecked {
|
||||
@ViewChild('body') body: ElementRef;
|
||||
@ViewChild('confirmDeleteModal') confirmDeleteModal: ElementRef;
|
||||
public account: Account;
|
||||
public items: TxItem[];
|
||||
public error: AppError;
|
||||
private accountId: string;
|
||||
private accountTree: AccountTree;
|
||||
private balance: number;
|
||||
private splits: any[];
|
||||
private selectAccounts: Account[];
|
||||
private needsScroll: boolean;
|
||||
private needsLittleScroll: boolean;
|
||||
private scrollLastHeight: number;
|
||||
private limit: number;
|
||||
private skip: number;
|
||||
private historyFinished: boolean;
|
||||
private fetching: boolean;
|
||||
private date: Date;
|
||||
private scrollSubject: Subject<any>;
|
||||
private hasScrolled: boolean;
|
||||
|
||||
constructor(
|
||||
private log: Logger,
|
||||
private route: ActivatedRoute,
|
||||
private txService: TransactionService,
|
||||
private accountService: AccountService,
|
||||
private fb: FormBuilder,
|
||||
private renderer: Renderer,
|
||||
private modalService: NgbModal
|
||||
) {
|
||||
this.items = [];
|
||||
this.limit = 50;
|
||||
this.historyFinished = false;
|
||||
this.fetching = false;
|
||||
this.scrollSubject = new Subject<any>();
|
||||
this.hasScrolled = false;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.accountId = this.route.snapshot.paramMap.get('id'); //+this.route.snapshot.paramMap.get('id');
|
||||
|
||||
this.accountService.getAccountTree().subscribe(tree => {
|
||||
this.account = tree.accountMap[this.accountId];
|
||||
this.selectAccounts = tree.getFlattenedAccounts().filter(account => {
|
||||
return !account.children.length;
|
||||
});
|
||||
|
||||
if(!this.accountTree) {
|
||||
this.accountTree = tree;
|
||||
|
||||
this.skip = 0;
|
||||
this.date = new Date();
|
||||
|
||||
let newTx = new Transaction({
|
||||
date: new Date(),
|
||||
splits: []
|
||||
});
|
||||
|
||||
newTx.date.setHours(23, 59, 59, 999);
|
||||
|
||||
newTx.splits.push(new Split({
|
||||
accountId: this.account.id
|
||||
}));
|
||||
newTx.splits.push(new Split());
|
||||
|
||||
this.appendTransaction(newTx);
|
||||
|
||||
let options = {limit: this.limit, beforeInserted: this.date.getTime()};
|
||||
let latestTxs$ = this.txService.getTransactionsByAccount(this.accountId, options).take(1);
|
||||
let newTxs$ = this.txService.getNewTransactionsByAccount(this.accountId);
|
||||
let deletedTxs$ = this.txService.getDeletedTransactionsByAccount(this.accountId);
|
||||
|
||||
latestTxs$.mergeMap(txs => txs).concat(newTxs$)
|
||||
.subscribe(tx => {
|
||||
// insert tx into list
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
deletedTxs$.subscribe(tx => {
|
||||
this.removeTransaction(tx);
|
||||
// remove tx from list
|
||||
});
|
||||
}
|
||||
|
||||
this.accountTree = tree;
|
||||
this.updateBalances();
|
||||
});
|
||||
|
||||
this.scrollSubject.debounceTime(100).subscribe(obj => {
|
||||
if(obj.percent < 0.2 && !this.fetching && !this.historyFinished) {
|
||||
this.fetchMoreTransactions();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchMoreTransactions() {
|
||||
this.fetching = true;
|
||||
this.log.debug('Fetching ' + this.limit + ' more transactions');
|
||||
this.skip += this.limit;
|
||||
let options = {limit: this.limit, skip: this.skip, beforeInserted: this.date.getTime()};
|
||||
this.txService.getTransactionsByAccount(this.accountId, options).subscribe(txs => {
|
||||
txs.forEach(tx => {
|
||||
this.addTransaction(tx);
|
||||
});
|
||||
|
||||
if(txs.length < this.limit) {
|
||||
this.historyFinished = true;
|
||||
}
|
||||
|
||||
this.fetching = false;
|
||||
this.needsScroll = false;
|
||||
this.needsLittleScroll = false;
|
||||
this.scrollLastHeight = this.body.nativeElement.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
addTransaction(tx: Transaction) {
|
||||
this.insertTransaction(tx);
|
||||
// it should only scroll to bottom if the user has not scrolled yet
|
||||
if(!this.hasScrolled) {
|
||||
this.needsScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
removeTransaction(tx: Transaction) {
|
||||
this.log.debug('remove tx');
|
||||
this.log.debug(tx);
|
||||
|
||||
for(let i = 0; i < this.items.length; i++) {
|
||||
if(this.items[i].tx.id === tx.id) {
|
||||
this.items.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
this.sortItems();
|
||||
this.updateBalances();
|
||||
}
|
||||
|
||||
ngAfterViewChecked() {
|
||||
if(this.needsLittleScroll) {
|
||||
this.scrollALittle();
|
||||
this.needsLittleScroll = false;
|
||||
}
|
||||
|
||||
let lastItemEditing = this.items.length && this.items[this.items.length - 1].editing;
|
||||
|
||||
if(this.needsScroll || lastItemEditing) {
|
||||
this.scrollToBottom();
|
||||
this.needsScroll = false;
|
||||
}
|
||||
|
||||
if(this.scrollLastHeight) {
|
||||
this.scrollDiffHeight();
|
||||
this.scrollLastHeight = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
this.hasScrolled = true;
|
||||
let element = this.body.nativeElement;
|
||||
this.scrollSubject.next({
|
||||
scrollTop: element.scrollTop,
|
||||
scrollHeight: element.scrollHeight,
|
||||
clientHeight: element.clientHeight,
|
||||
percent: element.scrollTop / (element.scrollHeight - element.clientHeight)
|
||||
});
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
let element = this.body.nativeElement;
|
||||
element.scrollTop = element.scrollHeight;
|
||||
}
|
||||
|
||||
scrollALittle() {
|
||||
let element = this.body.nativeElement;
|
||||
element.scrollTop += 50;
|
||||
}
|
||||
|
||||
scrollDiffHeight() {
|
||||
let element = this.body.nativeElement;
|
||||
let diff = element.scrollHeight - this.scrollLastHeight;
|
||||
element.scrollTop += diff;
|
||||
}
|
||||
|
||||
sortItems() {
|
||||
this.items.sort((a, b) => {
|
||||
// sort in ascending order
|
||||
if(!a.tx.date) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!b.tx.date) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
let dateDiff = a.tx.date.getTime() - b.tx.date.getTime();
|
||||
|
||||
if(dateDiff) {
|
||||
return dateDiff;
|
||||
}
|
||||
|
||||
let insertedDiff = a.tx.inserted.getTime() - b.tx.inserted.getTime();
|
||||
|
||||
return insertedDiff;
|
||||
});
|
||||
}
|
||||
|
||||
getTransferString(item: TxItem) {
|
||||
if(!item.tx.id) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let transferAccountId = this.getTransferAccountId(item);
|
||||
|
||||
if(!transferAccountId) {
|
||||
return 'Split Transaction';
|
||||
}
|
||||
|
||||
let transferAccount = this.accountTree.accountMap[transferAccountId];
|
||||
|
||||
if(!transferAccount) {
|
||||
return 'Unidentified';
|
||||
}
|
||||
|
||||
return transferAccount.fullName;
|
||||
}
|
||||
|
||||
getTransferAccountId(item: TxItem): string {
|
||||
let transferAccountId = null;
|
||||
|
||||
if(item.tx.splits.length === 2) {
|
||||
transferAccountId = item.tx.splits[0].accountId === this.account.id ?
|
||||
item.tx.splits[1].accountId :
|
||||
item.tx.splits[0].accountId;
|
||||
}
|
||||
|
||||
return transferAccountId;
|
||||
}
|
||||
|
||||
getDebit(item: TxItem) {
|
||||
return item.activeSplit.amount >= 0 ? item.activeSplit.amount : null;
|
||||
}
|
||||
|
||||
getCredit(item: TxItem) {
|
||||
return item.activeSplit.amount < 0 ? -item.activeSplit.amount : null;
|
||||
}
|
||||
|
||||
createTxItems(transaction: Transaction) {
|
||||
let items: TxItem[] = [];
|
||||
|
||||
for(let i = 0; i < transaction.splits.length; i++) {
|
||||
let split = transaction.splits[i];
|
||||
|
||||
if(split.accountId !== this.accountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let item = new TxItem();
|
||||
|
||||
item.tx = transaction;
|
||||
item.form = this.fb.group({
|
||||
splits: this.fb.array([])
|
||||
});
|
||||
item.activeSplit = split;
|
||||
item.activeSplitIndex = i;
|
||||
item.balance = 0;
|
||||
item.editing = false;
|
||||
item.edit$ = new Subject<any>();
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
appendTransaction(transaction: Transaction) {
|
||||
let items = this.createTxItems(transaction);
|
||||
this.items = this.items.concat(items);
|
||||
}
|
||||
|
||||
replaceTransaction(transaction: Transaction) {
|
||||
let items = this.createTxItems(transaction);
|
||||
|
||||
// remove tx from list
|
||||
for(let i = 0; i < this.items.length; i++) {
|
||||
if(this.items[i].tx.id === transaction.id) {
|
||||
this.items.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// add new items
|
||||
this.items = this.items.concat(items);
|
||||
|
||||
this.sortItems();
|
||||
this.updateBalances();
|
||||
}
|
||||
|
||||
insertTransaction(transaction: Transaction) {
|
||||
this.appendTransaction(transaction);
|
||||
this.sortItems();
|
||||
this.updateBalances();
|
||||
}
|
||||
|
||||
updateBalances() {
|
||||
let balance = this.account.debitBalance ? this.account.balance : -this.account.balance;
|
||||
|
||||
for(let i = this.items.length - 1; i >= 0; i--) {
|
||||
let item = this.items[i];
|
||||
item.balance = balance;
|
||||
|
||||
if(item.activeSplit.amount) {
|
||||
if(this.account.debitBalance) {
|
||||
balance -= item.activeSplit.amount;
|
||||
} else {
|
||||
balance += item.activeSplit.amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onTransaction(transaction: Transaction) {
|
||||
this.insertTransaction(transaction);
|
||||
}
|
||||
|
||||
editTransaction(item: TxItem, $event) {
|
||||
if(item.editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.debug($event);
|
||||
|
||||
this.log.debug('edit tx');
|
||||
this.log.debug(item);
|
||||
|
||||
item.editing = true;
|
||||
|
||||
let dateString = Util.getLocalDateString(item.tx.date);
|
||||
|
||||
this.log.debug(item);
|
||||
let debit = this.getDebit(item);
|
||||
let credit = this.getCredit(item);
|
||||
|
||||
let transferAccountId = this.getTransferAccountId(item);
|
||||
|
||||
if(item.tx.splits.length > 2) {
|
||||
transferAccountId = this.account.id;
|
||||
}
|
||||
|
||||
item.form = new FormGroup({
|
||||
date: new FormControl(dateString),
|
||||
description: new FormControl(item.tx.description, {updateOn: 'change'}),
|
||||
debit: new FormControl(debit ? debit / Math.pow(10, this.account.precision) : null),
|
||||
credit: new FormControl(credit ? credit / Math.pow(10, this.account.precision) : null),
|
||||
accountId: new FormControl(transferAccountId),
|
||||
splits: this.fb.array([])
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
let valueChanges = item.form.get('debit').valueChanges
|
||||
.merge(item.form.get('credit').valueChanges)
|
||||
.merge(item.form.get('splits').valueChanges);
|
||||
|
||||
valueChanges.subscribe(val => {
|
||||
if(!val) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log.debug('value changes', val);
|
||||
this.solveEquations(item);
|
||||
this.fillEmptySplit(item);
|
||||
});
|
||||
|
||||
if(item.tx.splits.length > 2) {
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
for(let split of item.tx.splits) {
|
||||
if(split.accountId === this.accountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let control = new FormGroup({
|
||||
accountId: new FormControl(split.accountId),
|
||||
debit: new FormControl(
|
||||
split.amount >= 0 ? split.amount / Math.pow(10, this.account.precision) : null
|
||||
),
|
||||
credit: new FormControl(
|
||||
split.amount < 0 ? -split.amount / Math.pow(10, this.account.precision) : null
|
||||
)
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
control.valueChanges.subscribe(val => {
|
||||
this.solveEquations(item);
|
||||
this.fillEmptySplit(item);
|
||||
});
|
||||
splits.push(control);
|
||||
|
||||
this.fillEmptySplit(item);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if($event && $event.target.className) {
|
||||
let cName = $event.target.classList[$event.target.classList.length - 1];
|
||||
try {
|
||||
this.renderer.selectRootElement('#form' + item.tx.id + item.activeSplitIndex + ' .' + cName + ' input').focus();
|
||||
} catch(e) {
|
||||
// don't do anything if the element doesn't exist
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
|
||||
// let modal = this.modalCtrl.create(EditTxPage, {transaction: transaction});
|
||||
// modal.present();
|
||||
// modal.onWillDismiss(() => {
|
||||
// this.loadData();
|
||||
// })
|
||||
item.edit$.next(null);
|
||||
}
|
||||
|
||||
preventBlur(item: TxItem) {
|
||||
this.log.debug('prevent blur');
|
||||
item.preventBlur = true;
|
||||
}
|
||||
|
||||
onBlur(item: TxItem) {
|
||||
this.log.debug('blur2');
|
||||
|
||||
setTimeout(() => {
|
||||
this.log.debug('blur', item.form.pristine);
|
||||
if(item.preventBlur) {
|
||||
item.preventBlur = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!item.form.pristine) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elem = document.activeElement as any;
|
||||
if(elem.form && elem.form.id === 'form' + item.tx.id + item.activeSplitIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.form = this.fb.group({
|
||||
splits: this.fb.array([])
|
||||
});
|
||||
|
||||
item.editing = false;
|
||||
}, 100); // timeout needs to be longer than in editTransaction
|
||||
}
|
||||
|
||||
deleteSplit(item: TxItem, index) {
|
||||
item.form.markAsDirty();
|
||||
this.log.debug('delete split');
|
||||
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
|
||||
if(splits.length === 1) {
|
||||
item.form.patchValue({
|
||||
accountId: splits.at(0).get('accountId').value
|
||||
});
|
||||
}
|
||||
|
||||
splits.removeAt(index);
|
||||
|
||||
}
|
||||
|
||||
addSplit(item: TxItem) {
|
||||
item.form.markAsDirty();
|
||||
//item.form.pristine = false;
|
||||
this.log.debug('add split');
|
||||
|
||||
// scroll down a little
|
||||
this.needsLittleScroll = true;
|
||||
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
|
||||
if(splits.length === 0) {
|
||||
this.addFirstSplit(item);
|
||||
return;
|
||||
}
|
||||
|
||||
let control = new FormGroup({
|
||||
accountId: new FormControl(),
|
||||
debit: new FormControl(),
|
||||
credit: new FormControl()
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
control.valueChanges.subscribe(val => {
|
||||
this.solveEquations(item);
|
||||
this.fillEmptySplit(item);
|
||||
});
|
||||
splits.push(control);
|
||||
|
||||
this.fillEmptySplit(item);
|
||||
}
|
||||
|
||||
addFirstSplit(item: TxItem) {
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
|
||||
let accountId = item.form.get('accountId').value || null;
|
||||
let debit = item.form.get('debit').value || null;
|
||||
let credit = item.form.get('credit').value || null;
|
||||
|
||||
item.form.patchValue({
|
||||
accountId: this.account.id
|
||||
});
|
||||
|
||||
let control = new FormGroup({
|
||||
accountId: new FormControl(accountId),
|
||||
debit: new FormControl(credit),
|
||||
credit: new FormControl(debit)
|
||||
}, {updateOn: 'blur'});
|
||||
|
||||
control.valueChanges.subscribe(val => {
|
||||
this.solveEquations(item);
|
||||
this.fillEmptySplit(item);
|
||||
});
|
||||
splits.push(control);
|
||||
|
||||
this.fillEmptySplit(item);
|
||||
}
|
||||
|
||||
fillEmptySplit(item: TxItem) {
|
||||
this.log.debug('fill empty split');
|
||||
|
||||
// Total up splits and fill in any empty split with the leftover value
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
|
||||
let emptySplit: AbstractControl;
|
||||
|
||||
let amount = item.form.get('debit').value - item.form.get('credit').value;
|
||||
|
||||
if(amount === 0) {
|
||||
emptySplit = item.form;
|
||||
this.log.debug('base split is empty');
|
||||
}
|
||||
|
||||
for(let i = 0; i < splits.length; i++) {
|
||||
let split = splits.at(i);
|
||||
amount += parseFloat(split.get('debit').value) || 0;
|
||||
amount -= parseFloat(split.get('credit').value) || 0;
|
||||
|
||||
if(!split.get('debit').value && !split.get('credit').value) {
|
||||
emptySplit = split;
|
||||
}
|
||||
}
|
||||
|
||||
if(emptySplit) {
|
||||
let precision = 2;
|
||||
|
||||
let account = this.accountTree.accountMap[emptySplit.get('accountId').value];
|
||||
if (account) {
|
||||
precision = account.precision;
|
||||
}
|
||||
|
||||
amount = this.round(-amount, precision);
|
||||
this.log.debug('amount', amount);
|
||||
|
||||
if(amount) {
|
||||
emptySplit.patchValue({
|
||||
debit: amount >= 0 ? amount : '',
|
||||
credit: amount < 0 ? -amount : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
round(amount, precision) {
|
||||
return Math.round(amount * Math.pow(10, precision)) / Math.pow(10, precision);
|
||||
}
|
||||
|
||||
submit(item: TxItem) {
|
||||
this.error = null;
|
||||
|
||||
this.log.debug('submit!');
|
||||
this.log.debug(item.form.value);
|
||||
|
||||
if(item.form.pristine) {
|
||||
return;
|
||||
}
|
||||
|
||||
let date = item.tx.id ? item.tx.date : new Date();
|
||||
let formDate = Util.getDateFromLocalDateString(item.form.value.date);
|
||||
|
||||
date = this.computeTransactionDate(formDate, date);
|
||||
|
||||
let tx = new Transaction({
|
||||
id: item.tx.id,
|
||||
date: date,
|
||||
description: item.form.value.description,
|
||||
splits: []
|
||||
});
|
||||
|
||||
if(!item.form.value.splits.length) {
|
||||
let amount = item.form.value.debit ? parseFloat(item.form.value.debit) : -parseFloat(item.form.value.credit);
|
||||
amount = Math.round(amount * Math.pow(10, this.account.precision));
|
||||
|
||||
tx.splits.push(new Split({
|
||||
accountId: this.account.id,
|
||||
amount: amount,
|
||||
nativeAmount: amount
|
||||
}));
|
||||
|
||||
tx.splits.push(new Split({
|
||||
accountId: item.form.value.accountId,
|
||||
amount: -amount,
|
||||
nativeAmount: -amount
|
||||
}));
|
||||
} else {
|
||||
let amount = item.form.value.debit ? parseFloat(item.form.value.debit) : -parseFloat(item.form.value.credit);
|
||||
amount = Math.round(amount * Math.pow(10, this.account.precision));
|
||||
|
||||
tx.splits.push(new Split({
|
||||
accountId: item.form.value.accountId,
|
||||
amount: amount,
|
||||
nativeAmount: amount
|
||||
}));
|
||||
}
|
||||
|
||||
for(let i = 0; i < item.form.value.splits.length; i++) {
|
||||
let split = item.form.value.splits[i];
|
||||
let account = this.accountTree.accountMap[split.accountId];
|
||||
|
||||
if(!account) {
|
||||
this.error = new AppError('Invalid account');
|
||||
return;
|
||||
}
|
||||
|
||||
let amount = split.debit ? parseFloat(split.debit) : -parseFloat(split.credit);
|
||||
amount = Math.round(amount * Math.pow(10, account.precision));
|
||||
|
||||
tx.splits.push(new Split({
|
||||
accountId: split.accountId,
|
||||
amount: amount,
|
||||
nativeAmount: amount
|
||||
}));
|
||||
}
|
||||
|
||||
this.log.debug(tx);
|
||||
|
||||
if(tx.id) {
|
||||
// update tx
|
||||
let oldId = tx.id;
|
||||
tx.id = Util.newGuid();
|
||||
|
||||
this.txService.putTransaction(oldId, tx)
|
||||
.subscribe(tx => {
|
||||
// do nothing
|
||||
|
||||
}, error => {
|
||||
this.error = error;
|
||||
});
|
||||
} else {
|
||||
// new tx
|
||||
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
while(splits.length) {
|
||||
splits.removeAt(0);
|
||||
}
|
||||
|
||||
item.form.reset();
|
||||
|
||||
let newTx = new Transaction({
|
||||
date: new Date(),
|
||||
splits: []
|
||||
});
|
||||
|
||||
newTx.date.setHours(23, 59, 59, 999);
|
||||
|
||||
newTx.splits.push(new Split({
|
||||
accountId: this.account.id
|
||||
}));
|
||||
newTx.splits.push(new Split());
|
||||
item.tx = newTx;
|
||||
|
||||
item.editing = false;
|
||||
item.activeSplit = newTx.splits[0];
|
||||
item.activeSplitIndex = 0;
|
||||
|
||||
tx.id = Util.newGuid();
|
||||
|
||||
this.txService.newTransaction(tx)
|
||||
.subscribe(tx => {
|
||||
// do nothing
|
||||
|
||||
}, error => {
|
||||
this.error = error;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
computeTransactionDate(formDate: Date, txDate: Date): Date {
|
||||
if(formDate.getTime()) {
|
||||
// make the time be at the very end of the day
|
||||
formDate.setHours(23, 59, 59, 999);
|
||||
}
|
||||
|
||||
let sameDay = formDate.getFullYear() === txDate.getFullYear() &&
|
||||
formDate.getMonth() === txDate.getMonth() &&
|
||||
formDate.getDate() === txDate.getDate();
|
||||
|
||||
if(formDate.getTime() && !sameDay) {
|
||||
txDate = formDate;
|
||||
}
|
||||
|
||||
return txDate;
|
||||
}
|
||||
|
||||
deleteTransaction(item) {
|
||||
this.modalService.open(this.confirmDeleteModal).result.then((result) => {
|
||||
this.log.debug('delete');
|
||||
this.txService.deleteTransaction(item.tx.id)
|
||||
.subscribe(() => {
|
||||
this.log.debug('successfully deleted transaction ' + item.tx.id);
|
||||
}, error => {
|
||||
this.error = error;
|
||||
})
|
||||
}, (reason) => {
|
||||
this.log.debug('cancel delete');
|
||||
});
|
||||
}
|
||||
|
||||
advancedEdit(item) {
|
||||
let modal = this.modalService.open(AdvancedEdit, {size: 'lg'});
|
||||
|
||||
modal.componentInstance.setData(item, this.accountTree);
|
||||
|
||||
modal.result.then((result) => {
|
||||
this.log.debug('advanced edit save');
|
||||
this.log.debug(item.form);
|
||||
}, (reason) => {
|
||||
this.log.debug('cancel advanced edit');
|
||||
});
|
||||
}
|
||||
|
||||
onEnter(item, $event) {
|
||||
$event.target.blur();
|
||||
this.submit(item);
|
||||
}
|
||||
|
||||
solveEquations(item: TxItem) {
|
||||
this.log.debug('solveEquations');
|
||||
let originalDebit = item.form.get('debit').value;
|
||||
let originalCredit = item.form.get('credit').value;
|
||||
let precision = this.account.precision;
|
||||
let debit = originalDebit ? this.round(this.solve('' + originalDebit), precision) : '';
|
||||
let credit = originalCredit ? this.round(this.solve('' + originalCredit), precision) : '';
|
||||
|
||||
if((originalDebit && debit !== originalDebit) || (originalCredit && credit !== originalCredit)) {
|
||||
this.log.debug('patch', debit, credit);
|
||||
this.log.debug('original', originalDebit, originalCredit);
|
||||
item.form.patchValue({
|
||||
debit: debit,
|
||||
credit: credit
|
||||
});
|
||||
}
|
||||
|
||||
let splits = item.form.get('splits') as FormArray;
|
||||
|
||||
for(let i = 0; i < splits.length; i++) {
|
||||
let split = splits.at(i);
|
||||
let originalDebit = split.get('debit').value;
|
||||
let originalCredit = split.get('credit').value;
|
||||
let debit = originalDebit ? this.round(this.solve('' + originalDebit), precision) : '';
|
||||
let credit = originalCredit ? this.round(this.solve('' + originalCredit), precision) : '';
|
||||
|
||||
if((originalDebit && debit !== originalDebit) || (originalCredit && credit !== originalCredit)) {
|
||||
this.log.debug('patch', debit, credit);
|
||||
this.log.debug('original', originalDebit, originalCredit);
|
||||
split.patchValue({
|
||||
debit: debit,
|
||||
credit: credit
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
solve(input: string) {
|
||||
// first pass: +-
|
||||
for(let i = input.length - 1; i >= 0; i--) {
|
||||
if(input.charAt(i) === '+') {
|
||||
return this.solve(input.slice(0, i)) + this.solve(input.slice(i + 1));
|
||||
} else if(input.charAt(i) === '-') {
|
||||
return this.solve(input.slice(0, i)) - this.solve(input.slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// second pass: */
|
||||
for(let i = input.length - 1; i >= 0; i--) {
|
||||
if(input.charAt(i) === '*') {
|
||||
return this.solve(input.slice(0, i)) * this.solve(input.slice(i + 1));
|
||||
} else if(input.charAt(i) === '/') {
|
||||
return this.solve(input.slice(0, i)) / this.solve(input.slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
return parseFloat(input.trim()) || 0;
|
||||
}
|
||||
|
||||
autocomplete(item: TxItem, tx: Transaction) {
|
||||
this.log.debug('chose tx', tx);
|
||||
|
||||
let formDate = Util.getDateFromLocalDateString(item.form.value.date);
|
||||
item.tx = new Transaction(
|
||||
{
|
||||
date: this.computeTransactionDate(formDate, new Date()),
|
||||
description: tx.description,
|
||||
splits: tx.splits
|
||||
}
|
||||
);
|
||||
|
||||
for(let i = 0; i < tx.splits.length; i++) {
|
||||
let split = tx.splits[i];
|
||||
|
||||
if(split.accountId !== this.accountId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
item.activeSplit = split;
|
||||
item.activeSplitIndex = i;
|
||||
}
|
||||
|
||||
this.log.debug(tx);
|
||||
|
||||
item.editing = false;
|
||||
item.preventBlur = true;
|
||||
this.editTransaction(item, {target: {className: 'description', classList: ['description']}});
|
||||
item.form.markAsDirty();
|
||||
}
|
||||
|
||||
}
|
||||
31
src/app/transaction/transaction.module.ts
Normal file
31
src/app/transaction/transaction.module.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { TxListPage } from './list';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AppRoutingModule } from '../app-routing.module';
|
||||
import { AdvancedEdit } from './advancededit';
|
||||
import { Autocomplete } from './autocomplete';
|
||||
import { Breadcrumbs } from './breadcrumbs';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TxListPage,
|
||||
AdvancedEdit,
|
||||
Autocomplete,
|
||||
Breadcrumbs
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
NgbModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule,
|
||||
AppRoutingModule
|
||||
],
|
||||
exports: [],
|
||||
providers: [],
|
||||
entryComponents: [AdvancedEdit]
|
||||
})
|
||||
export class TransactionModule { }
|
||||
14
src/app/transaction/txitem.ts
Normal file
14
src/app/transaction/txitem.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Transaction, Split} from '../shared/transaction';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export class TxItem {
|
||||
tx: Transaction;
|
||||
activeSplit: Split;
|
||||
activeSplitIndex: number;
|
||||
form: FormGroup;
|
||||
balance: number;
|
||||
editing: boolean;
|
||||
preventBlur: boolean;
|
||||
edit$: Subject<any>;
|
||||
}
|
||||
Reference in New Issue
Block a user