got new transaction working

This commit is contained in:
Patrick Nagurny
2018-11-27 16:46:00 -05:00
parent 72eac48f42
commit 70a20b6611
4 changed files with 359 additions and 111 deletions

View File

@@ -1,28 +1,10 @@
<h1>New Transaction</h1> <h1>New Transaction</h1>
<div class="section"> <div class="section">
<form [formGroup]="form" (ngSubmit)="onSubmit()"> <form [formGroup]="form" (ngSubmit)="onSubmit()" *ngIf="accountTree">
<ngb-accordion #acc="ngbAccordion" activeIds="toggle-1"> <ngb-accordion #acc="ngbAccordion" activeIds="toggle-1">
<ngb-panel id="toggle-1" [title]="getToggle1Title()"> <ngb-panel id="toggle-1" [title]="getToggle1Title()">
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<!-- <input type="radio" class="btn btn-outline-primary mr-2" id="type-expense" value="Expense">
<input type="radio" class="btn btn-outline-primary mr-2" id="type-income" value="Income">
<input type="radio" class="btn btn-outline-primary mr-2" id="type-ob" value="Opening Balance">
<input type="radio" class="btn btn-outline-primary mr-2" id="type-other" value="Other"> -->
<div>
<!-- <button type="button" class="btn mr-2" (click)="setType('expense')" [ngClass]="{'btn-outline-primary': type !== 'expense', 'btn-primary': type === 'expense'}">
Expense
</button>
<button type="button" class="btn mr-2" (click)="setType('income')" [ngClass]="{'btn-outline-primary': type !== 'income', 'btn-primary': type === 'income'}">
Income
</button>
<button type="button" class="btn mr-2" (click)="setType('openingBalance')" [ngClass]="{'btn-outline-primary': type !== 'openingBalance', 'btn-primary': type === 'openingBalance'}">
Opening Balance
</button>
<button type="button" class="btn mr-2" (click)="setType('other')" [ngClass]="{'btn-outline-primary': type !== 'other', 'btn-primary': type === 'other'}">
Other
</button> -->
<div class="btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="type"> <div class="btn-group-toggle" ngbRadioGroup name="radioBasic" formControlName="type">
<label ngbButtonLabel class="btn-primary mr-2"> <label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="expense"> Expense <input ngbButton type="radio" value="expense"> Expense
@@ -30,14 +12,13 @@
<label ngbButtonLabel class="btn-primary mr-2"> <label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="income"> Income <input ngbButton type="radio" value="income"> Income
</label> </label>
<label ngbButtonLabel class="btn-primary mr-2"> <label ngbButtonLabel class="btn-primary mr-2" *ngIf="openingBalances">
<input ngbButton type="radio" value="openingBalance"> Opening Balance <input ngbButton type="radio" value="openingBalance"> Opening Balance
</label> </label>
<label ngbButtonLabel class="btn-primary mr-2"> <label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other"> Other <input ngbButton type="radio" value="other"> Other
</label> </label>
</div> </div>
</div>
<div id="firstAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3"> <div id="firstAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="radioBasic2" formControlName="firstAccountPrimary"> <div class="btn-group-toggle" ngbRadioGroup name="radioBasic2" formControlName="firstAccountPrimary">
<label *ngFor="let account of expenseAccounts" ngbButtonLabel class="btn-primary mr-2"> <label *ngFor="let account of expenseAccounts" ngbButtonLabel class="btn-primary mr-2">
@@ -78,9 +59,21 @@
</div> </div>
</div> </div>
</div> </div>
<div id="firstAccountPrimary" *ngIf="form.value?.type === 'openingBalance'" class="mt-3">
<div id="firstAccountSelect" class="mt-3">
<div class="form-group">
<label for="firstAccountSecondary" class="col-sm-3 col-form-label">Choose Account</label>
<select class="form-control" id="account" formControlName="firstAccountSecondary">
<option *ngFor="let account of paymentAccountsAll" [value]="account.id">
{{account.label | slice:0:30}}
</option>
</select>
</div>
</div>
</div>
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
<ngb-panel id="toggle-2" [title]="getTitle()"> <ngb-panel id="toggle-2" [title]="getToggle2Title()" *ngIf="form.value?.type !== 'openingBalance'">
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<div id="secondAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3"> <div id="secondAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="secondAccountPrimary" formControlName="secondAccountPrimary"> <div class="btn-group-toggle" ngbRadioGroup name="secondAccountPrimary" formControlName="secondAccountPrimary">
@@ -122,22 +115,12 @@
</div> </div>
</div> </div>
</div> </div>
<div id="secondAccountSelect" *ngIf="form.value?.type === 'openingBalance'" class="mt-3">
<div class="form-group">
<label for="secondAccountSecondary" class="col-sm-3 col-form-label">Choose Account</label>
<select class="form-control" id="account" formControlName="secondAccountSecondary">
<option *ngFor="let account of paymentAccountsAll" [value]="account.id">
{{account.label | slice:0:30}}
</option>
</select>
</div>
</div>
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
<ngb-panel id="toggle-3" title="Amount"> <ngb-panel id="toggle-3" title="Amount">
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
<div class="form-group"> <div class="form-group">
<input type="number" class="form-control" id="amount" formControlName="amount"> <input #amount type="number" class="form-control" id="amount" formControlName="amount">
</div> </div>
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
@@ -157,17 +140,47 @@
</ngb-panel> </ngb-panel>
<ngb-panel id="toggle-6" title="Advanced"> <ngb-panel id="toggle-6" title="Advanced">
<ng-template ngbPanelContent> <ng-template ngbPanelContent>
Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon <div class="container-fluid" formArrayName="splits">
officia <div class="row">
aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon <div class="col-8">
tempor, <h3>Account</h3>
sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh </div>
helvetica, <div class="col-2">
craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. <h3>Debit</h3>
Leggings </div>
occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them <div class="col-2">
accusamus <h3>Credit</h3>
labore sustainable VHS. </div>
</div>
<div class="row" *ngFor="let split of form.get('splits').controls; let i=index" [formGroup]="split">
<div class="col-8">
<div class="form-group">
<select class="form-control" id="account" formControlName="accountId">
<option *ngFor="let account of selectAccounts" [value]="account.id">
{{account.fullName | slice:0:50}}
</option>
</select>
</div>
</div>
<div class="col-2">
<div class="form-group">
<input type="number" class="form-control" formControlName="debit" [ngClass]="{'positive': accountMap[split.value.accountId]?.debitBalance, 'negative': !accountMap[split.value.accountId]?.debitBalance}"/>
</div>
</div>
<div class="col-2">
<div class="form-group">
<input type="number" class="form-control" formControlName="credit" [ngClass]="{'positive': !accountMap[split.value.accountId]?.debitBalance, 'negative': accountMap[split.value.accountId]?.debitBalance}"/>
</div>
</div>
</div>
<div class="row">
<div class="col-8">
<p>
<a (click)="addSplit()">Add Split</a>
</p>
</div>
</div>
</div>
</ng-template> </ng-template>
</ngb-panel> </ngb-panel>
</ngb-accordion> </ngb-accordion>

View File

@@ -13,4 +13,7 @@
color: $blue; color: $blue;
} }
} }
h3 {
font-size: 1rem;
}
} }

View File

@@ -1,18 +1,22 @@
import { Component, ViewEncapsulation, ViewChild } from '@angular/core'; import { Component, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { import {
FormGroup, FormGroup,
FormControl, FormControl,
Validators, Validators,
FormBuilder, FormBuilder,
FormArray,
AbstractControl, AbstractControl,
ValidationErrors ValidationErrors
} from '@angular/forms'; } from '@angular/forms';
import { AccountService } from '../core/account.service'; import { AccountService } from '../core/account.service';
import { TransactionService } from '../core/transaction.service';
import { OrgService } from '../core/org.service'; import { OrgService } from '../core/org.service';
import { Account, AccountApi, AccountTree } from '../shared/account'; import { Account, AccountApi, AccountTree } from '../shared/account';
import { Util } from '../shared/util'; import { Util } from '../shared/util';
import { AppError } from '../shared/error'; import { AppError } from '../shared/error';
import { Transaction, Split } from '../shared/transaction';
import { Logger } from '../core/logger';
@Component({ @Component({
selector: 'app-txnew', selector: 'app-txnew',
@@ -38,109 +42,240 @@ export class NewTransactionPage {
public incomeAccountsAll: any[] = []; public incomeAccountsAll: any[] = [];
public paymentAccountsAll: any[] = []; public paymentAccountsAll: any[] = [];
public assetAccountsAll: any[] = []; public assetAccountsAll: any[] = [];
public openingBalances: Account;
private accountTree: AccountTree; private accountTree: AccountTree;
public accountMap: any;
@ViewChild('acc') acc: any; @ViewChild('acc') acc: any;
@ViewChild('amount') amount: ElementRef
// TODO This code needs to be cleaned up
constructor( constructor(
private router: Router, private router: Router,
private accountService: AccountService, private accountService: AccountService,
private txService: TransactionService,
private orgService: OrgService, private orgService: OrgService,
private fb: FormBuilder) { private fb: FormBuilder,
private log: Logger) {
this.numAccountsShown = 3; this.numAccountsShown = 3;
let org = this.orgService.getCurrentOrg(); let org = this.orgService.getCurrentOrg();
let dateString = Util.getLocalDateString(new Date()); let dateString = Util.getLocalDateString(new Date());
this.form = fb.group({ this.form = this.fb.group({
'type': ['', Validators.required], type: ['', Validators.required],
'firstAccountPrimary': [null, Validators.required], firstAccountPrimary: [null, Validators.required],
'firstAccountSecondary': [null], firstAccountSecondary: [null],
'secondAccountPrimary': [null, Validators.required], secondAccountPrimary: [null, Validators.required],
'secondAccountSecondary': [null], secondAccountSecondary: [null],
'amount': [null, Validators.required], amount: [null, Validators.required],
'description': [''], description: [''],
'date': [dateString, Validators.required] date: [dateString, Validators.required],
splits: fb.array([]),
}); });
this.accountService.getAccountTree().subscribe(tree => { this.addSplit();
this.addSplit();
this.accountService.getAccountTree().take(1).subscribe(tree => {
this.accountTree = tree; this.accountTree = tree;
this.accountMap = tree.accountMap;
this.selectAccounts = tree.getFlattenedAccounts().filter(account => { this.selectAccounts = tree.getFlattenedAccounts().filter(account => {
let isAsset = account.fullName.match(/^Assets/); return !account.children.length;
let isLiability = account.fullName.match(/^Liabilities/);
return !account.children.length && (isAsset || isLiability);
}); });
this.getExpenseAccounts(); this.getExpenseAccounts();
this.getIncomeAccounts(); this.getIncomeAccounts();
this.getPaymentAccounts(); this.getPaymentAccounts();
this.getAssetAccounts(); this.getAssetAccounts();
this.openingBalances = tree.getAccountByName('Opening Balances', 2);
}); });
this.form.get('type').valueChanges.subscribe(val => {
if(val === 'openingBalance') {
this.acc.collapse('toggle-1');
this.acc.expand('toggle-2');
this.form.patchValue({description: 'Opening Balance'});
}
})
this.form.get('firstAccountPrimary').valueChanges.subscribe(val => { this.form.get('firstAccountPrimary').valueChanges.subscribe(val => {
if(val === 'other') { if (val === 'other') {
return; return;
} }
this.acc.collapse('toggle-1'); this.acc.collapse('toggle-1');
this.acc.expand('toggle-2'); this.acc.expand('toggle-2');
let splits = this.form.get('splits') as FormArray;
if (splits.length > 0) {
splits.at(0).patchValue({
accountId: val
});
}
}); });
this.form.get('firstAccountSecondary').valueChanges.subscribe(val => { this.form.get('firstAccountSecondary').valueChanges.subscribe(val => {
if (this.form.value.type === 'openingBalance') {
this.acc.collapse('toggle-1');
this.acc.expand('toggle-3');
this.acc.expand('toggle-4');
this.form.patchValue({
description: 'Opening Balance',
firstAccountPrimary: 'other',
secondAccountPrimary: this.openingBalances.id
});
this.focusAmount();
let splits = this.form.get('splits') as FormArray;
if (splits.length > 1) {
let firstAccount = this.getFirstAccount();
let secondAccount = this.openingBalances;
splits.at(0).patchValue({
accountId: firstAccount.id
});
splits.at(1).patchValue({
accountId: secondAccount.id
});
}
return;
}
this.acc.collapse('toggle-1'); this.acc.collapse('toggle-1');
this.acc.expand('toggle-2'); this.acc.expand('toggle-2');
let splits = this.form.get('splits') as FormArray;
if (splits.length > 0) {
let account = this.getFirstAccount();
splits.at(0).patchValue({
accountId: account.id
});
}
}); });
this.form.get('secondAccountPrimary').valueChanges.subscribe(val => { this.form.get('secondAccountPrimary').valueChanges.subscribe(val => {
if(val === 'other') { if (val === 'other') {
return; return;
} }
this.acc.collapse('toggle-2'); this.acc.collapse('toggle-2');
this.acc.expand('toggle-3'); this.acc.expand('toggle-3');
this.acc.expand('toggle-4'); this.acc.expand('toggle-4');
this.focusAmount();
let splits = this.form.get('splits') as FormArray;
if (splits.length > 1) {
splits.at(1).patchValue({
accountId: val
});
}
}); });
this.form.get('secondAccountSecondary').valueChanges.subscribe(val => { this.form.get('secondAccountSecondary').valueChanges.subscribe(val => {
this.acc.collapse('toggle-2'); this.acc.collapse('toggle-2');
this.acc.expand('toggle-3'); this.acc.expand('toggle-3');
this.acc.expand('toggle-4'); this.acc.expand('toggle-4');
this.focusAmount();
let splits = this.form.get('splits') as FormArray;
if (splits.length > 1) {
splits.at(1).patchValue({
accountId: val
});
}
});
this.form.get('amount').valueChanges.subscribe(amount => {
let type = this.form.get('type').value;
let splits = this.form.get('splits') as FormArray;
if (type === 'expense') {
splits.at(0).patchValue({
debit: amount
});
splits.at(1).patchValue({
credit: amount
});
} else if (type === 'income') {
splits.at(0).patchValue({
credit: amount
});
splits.at(1).patchValue({
debit: amount
});
} else if (type === 'openingBalance') {
let firstAccount = this.getFirstAccount();
if (firstAccount.debitBalance) {
splits.at(0).patchValue({
debit: amount
});
splits.at(1).patchValue({
credit: amount
});
} else {
splits.at(0).patchValue({
credit: amount
});
splits.at(1).patchValue({
debit: amount
});
}
}
}); });
} }
onSubmit() { onSubmit() {
let account = new AccountApi(this.form.value); this.error = null;
account.id = Util.newGuid();
let parentAccount = this.accountTree.accountMap[account.parent];
if(!parentAccount) { let date = new Date();
this.error = new AppError('Invalid parent account'); let formDate = Util.getDateFromLocalDateString(this.form.value.date);
return;
if (formDate.getTime()) {
// make the time be at the very end of the day
formDate.setHours(23, 59, 59, 999);
} }
let org = this.orgService.getCurrentOrg(); let sameDay = formDate.getFullYear() === date.getFullYear() &&
account.orgId = org.id; formDate.getMonth() === date.getMonth() &&
account.debitBalance = parentAccount.debitBalance; formDate.getDate() === date.getDate();
account.currency = account.currency || parentAccount.currency;
account.precision = account.precision !== null ? account.precision : parentAccount.precision;
this.accountService.newAccount(account) if (formDate.getTime() && !sameDay) {
.subscribe( date = formDate;
account => { }
this.router.navigate(['/accounts']);
}, let tx = new Transaction({
err => { id: Util.newGuid(),
this.error = err; description: this.form.value.description,
} date: date,
); 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));
tx.splits.push(new Split({
accountId: split.accountId,
amount: amount,
nativeAmount: amount
}));
}
this.log.debug(tx);
this.txService.newTransaction(tx)
.subscribe(tx => {
this.router.navigate(['/dashboard']);
}, error => {
this.error = error;
});
} }
getExpenseAccounts() { getExpenseAccounts() {
@@ -199,7 +334,7 @@ export class NewTransactionPage {
id: account.id, id: account.id,
name: account.name, name: account.name,
label: this.accountTree.getAccountLabel(account, depth), label: this.accountTree.getAccountLabel(account, depth),
hidden: i < this.numAccountsShown ? false: true hidden: i < this.numAccountsShown ? false : true
} }
}); });
@@ -210,19 +345,19 @@ export class NewTransactionPage {
let aAlpha = a.label.charCodeAt(0) >= 65 && a.label.charCodeAt(0) <= 122; let aAlpha = a.label.charCodeAt(0) >= 65 && a.label.charCodeAt(0) <= 122;
let bAlpha = b.label.charCodeAt(0) >= 65 && b.label.charCodeAt(0) <= 122; let bAlpha = b.label.charCodeAt(0) >= 65 && b.label.charCodeAt(0) <= 122;
if(!aAlpha && bAlpha) { if (!aAlpha && bAlpha) {
return 1; return 1;
} }
if(aAlpha && !bAlpha) { if (aAlpha && !bAlpha) {
return -1; return -1;
} }
if(a.label > b.label) { if (a.label > b.label) {
return 1; return 1;
} }
if(a.label < b.label) { if (a.label < b.label) {
return -1; return -1;
} }
@@ -240,9 +375,9 @@ export class NewTransactionPage {
let type = this.form.value.type; let type = this.form.value.type;
if(type) { if (type) {
str += type.charAt(0).toUpperCase() + type.substr(1); str += type.charAt(0).toUpperCase() + type.substr(1);
if(account) { if (account) {
str += ' (' + account.name + ')'; str += ' (' + account.name + ')';
} }
} }
@@ -250,12 +385,12 @@ export class NewTransactionPage {
return str; return str;
} }
getTitle() { getToggle2Title() {
let account = this.getSecondAccount(); let account = this.getSecondAccount();
if(this.form.value.type === 'income') { if (this.form.value.type === 'income') {
return 'Where was the money sent? ' + (account ? account.name : ''); return 'Where was the money sent? ' + (account ? account.name : '');
} else if(this.form.value.type === 'openingBalance') { } else if (this.form.value.type === 'openingBalance') {
return 'What account? ' + (account ? account.name : ''); return 'What account? ' + (account ? account.name : '');
} }
@@ -263,30 +398,119 @@ export class NewTransactionPage {
} }
getFirstAccount() { getFirstAccount() {
if(!this.accountTree) { if (this.form.value.firstAccountPrimary && this.form.value.firstAccountPrimary !== 'other') {
return null;
}
if(this.form.value.firstAccountPrimary && this.form.value.firstAccountPrimary !== 'other') {
return this.accountTree.accountMap[this.form.value.firstAccountPrimary]; return this.accountTree.accountMap[this.form.value.firstAccountPrimary];
} }
return this.accountTree.accountMap[this.form.value.firstAccountSecondary]; return this.accountTree.accountMap[this.form.value.firstAccountSecondary];
} }
getSecondAccount() { getFirstAccountAmount() {
if(!this.accountTree) { let account = this.getFirstAccount();
return null;
}
if(this.form.value.secondAccountPrimary && this.form.value.secondAccountPrimary !== 'other') { switch (this.form.value.type) {
case 'expense':
return this.form.value.amount * Math.pow(10, account.precision);
case 'income':
return -this.form.value.amount * Math.pow(10, account.precision);
case 'openingBalance':
return this.form.value.amount * Math.pow(10, account.precision)
* (account.debitBalance ? 1 : -1);
}
}
getSecondAccount() {
if (this.form.value.secondAccountPrimary && this.form.value.secondAccountPrimary !== 'other') {
return this.accountTree.accountMap[this.form.value.secondAccountPrimary]; return this.accountTree.accountMap[this.form.value.secondAccountPrimary];
} }
return this.accountTree.accountMap[this.form.value.secondAccountSecondary]; return this.accountTree.accountMap[this.form.value.secondAccountSecondary];
} }
getSecondAccountAmount() {
let firstAccount = this.getFirstAccount();
let secondAccount = this.getSecondAccount();
switch (this.form.value.type) {
case 'expense':
return -this.form.value.amount * Math.pow(10, secondAccount.precision);
case 'income':
return this.form.value.amount * Math.pow(10, secondAccount.precision);
case 'openingBalance':
return this.form.value.amount * Math.pow(10, secondAccount.precision)
* (firstAccount.debitBalance ? -1 : 1);
}
}
addSplit() {
let splits = this.form.get('splits') as FormArray;
let control = new FormGroup({
accountId: new FormControl(),
debit: new FormControl(),
credit: new FormControl()
}, { updateOn: 'blur' });
control.valueChanges.subscribe(val => {
this.fillEmptySplit();
});
splits.push(control);
this.fillEmptySplit();
}
fillEmptySplit() {
// Total up splits and fill in any empty split with the leftover value
let splits = this.form.get('splits') as FormArray;
let amount = 0;
let emptySplit: AbstractControl;
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 accountId = emptySplit.get('accountId').value;
let account = null;
if (this.accountTree && accountId) {
account = this.accountTree.accountMap[emptySplit.get('accountId').value];
}
if (account) {
precision = account.precision;
}
amount = this.round(-amount, precision);
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);
}
focusAmount() {
// TODO Not sure how to get rid of this hack
setTimeout(() => {
if (this.amount) {
this.amount.nativeElement.focus();
}
}, 1);
}
} }

View File

@@ -86,4 +86,12 @@ a:hover {
.content > * > .description { .content > * > .description {
padding: 0 0.5rem; padding: 0 0.5rem;
} }
}
.positive {
color: $positive;
}
.negative {
color: $negative;
} }