From 70a20b66119965707218a46acecae0c501708952 Mon Sep 17 00:00:00 2001 From: Patrick Nagurny Date: Tue, 27 Nov 2018 16:46:00 -0500 Subject: [PATCH] got new transaction working --- src/app/transaction/new.html | 101 +++++----- src/app/transaction/new.scss | 3 + src/app/transaction/new.ts | 358 ++++++++++++++++++++++++++++------- src/sass/styles.scss | 8 + 4 files changed, 359 insertions(+), 111 deletions(-) diff --git a/src/app/transaction/new.html b/src/app/transaction/new.html index ba1a78e..648daae 100644 --- a/src/app/transaction/new.html +++ b/src/app/transaction/new.html @@ -1,28 +1,10 @@

New Transaction

-
+ - -
- -
-
+
+
+
+ + +
+
+
- +
@@ -122,22 +115,12 @@
-
-
- - -
-
- +
@@ -157,17 +140,47 @@
- Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon - officia - aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon - tempor, - sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh - helvetica, - craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. - Leggings - occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them - accusamus - labore sustainable VHS. +
+
+
+

Account

+
+
+

Debit

+
+
+

Credit

+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

+ Add Split +

+
+
+
diff --git a/src/app/transaction/new.scss b/src/app/transaction/new.scss index eeaa310..d2dadba 100644 --- a/src/app/transaction/new.scss +++ b/src/app/transaction/new.scss @@ -13,4 +13,7 @@ color: $blue; } } + h3 { + font-size: 1rem; + } } \ No newline at end of file diff --git a/src/app/transaction/new.ts b/src/app/transaction/new.ts index 263496c..1a3186d 100644 --- a/src/app/transaction/new.ts +++ b/src/app/transaction/new.ts @@ -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 { +import { FormGroup, FormControl, Validators, FormBuilder, + FormArray, AbstractControl, ValidationErrors } from '@angular/forms'; import { AccountService } from '../core/account.service'; +import { TransactionService } from '../core/transaction.service'; import { OrgService } from '../core/org.service'; import { Account, AccountApi, AccountTree } from '../shared/account'; import { Util } from '../shared/util'; import { AppError } from '../shared/error'; +import { Transaction, Split } from '../shared/transaction'; +import { Logger } from '../core/logger'; @Component({ selector: 'app-txnew', @@ -38,109 +42,240 @@ export class NewTransactionPage { public incomeAccountsAll: any[] = []; public paymentAccountsAll: any[] = []; public assetAccountsAll: any[] = []; + public openingBalances: Account; private accountTree: AccountTree; + public accountMap: any; @ViewChild('acc') acc: any; + @ViewChild('amount') amount: ElementRef + + // TODO This code needs to be cleaned up constructor( private router: Router, private accountService: AccountService, + private txService: TransactionService, private orgService: OrgService, - private fb: FormBuilder) { + private fb: FormBuilder, + private log: Logger) { this.numAccountsShown = 3; let org = this.orgService.getCurrentOrg(); let dateString = Util.getLocalDateString(new Date()); - this.form = fb.group({ - 'type': ['', Validators.required], - 'firstAccountPrimary': [null, Validators.required], - 'firstAccountSecondary': [null], - 'secondAccountPrimary': [null, Validators.required], - 'secondAccountSecondary': [null], - 'amount': [null, Validators.required], - 'description': [''], - 'date': [dateString, Validators.required] + this.form = this.fb.group({ + type: ['', Validators.required], + firstAccountPrimary: [null, Validators.required], + firstAccountSecondary: [null], + secondAccountPrimary: [null, Validators.required], + secondAccountSecondary: [null], + amount: [null, Validators.required], + description: [''], + 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.accountMap = tree.accountMap; this.selectAccounts = tree.getFlattenedAccounts().filter(account => { - let isAsset = account.fullName.match(/^Assets/); - let isLiability = account.fullName.match(/^Liabilities/); - return !account.children.length && (isAsset || isLiability); + return !account.children.length; }); this.getExpenseAccounts(); this.getIncomeAccounts(); this.getPaymentAccounts(); 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 => { - if(val === 'other') { + if (val === 'other') { return; } this.acc.collapse('toggle-1'); 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 => { + 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.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 => { - if(val === 'other') { + if (val === 'other') { return; } this.acc.collapse('toggle-2'); this.acc.expand('toggle-3'); 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.acc.collapse('toggle-2'); this.acc.expand('toggle-3'); 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() { - let account = new AccountApi(this.form.value); - account.id = Util.newGuid(); - let parentAccount = this.accountTree.accountMap[account.parent]; + this.error = null; - if(!parentAccount) { - this.error = new AppError('Invalid parent account'); - return; + let 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 org = this.orgService.getCurrentOrg(); - account.orgId = org.id; - account.debitBalance = parentAccount.debitBalance; - account.currency = account.currency || parentAccount.currency; - account.precision = account.precision !== null ? account.precision : parentAccount.precision; + let sameDay = formDate.getFullYear() === date.getFullYear() && + formDate.getMonth() === date.getMonth() && + formDate.getDate() === date.getDate(); - this.accountService.newAccount(account) - .subscribe( - account => { - this.router.navigate(['/accounts']); - }, - err => { - this.error = err; - } - ); + if (formDate.getTime() && !sameDay) { + date = formDate; + } + + let tx = new Transaction({ + id: Util.newGuid(), + 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() { @@ -199,7 +334,7 @@ export class NewTransactionPage { id: account.id, name: account.name, 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 bAlpha = b.label.charCodeAt(0) >= 65 && b.label.charCodeAt(0) <= 122; - if(!aAlpha && bAlpha) { + if (!aAlpha && bAlpha) { return 1; } - if(aAlpha && !bAlpha) { + if (aAlpha && !bAlpha) { return -1; } - if(a.label > b.label) { + if (a.label > b.label) { return 1; } - if(a.label < b.label) { + if (a.label < b.label) { return -1; } @@ -240,9 +375,9 @@ export class NewTransactionPage { let type = this.form.value.type; - if(type) { + if (type) { str += type.charAt(0).toUpperCase() + type.substr(1); - if(account) { + if (account) { str += ' (' + account.name + ')'; } } @@ -250,12 +385,12 @@ export class NewTransactionPage { return str; } - getTitle() { + getToggle2Title() { 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 : ''); - } else if(this.form.value.type === 'openingBalance') { + } else if (this.form.value.type === 'openingBalance') { return 'What account? ' + (account ? account.name : ''); } @@ -263,30 +398,119 @@ export class NewTransactionPage { } getFirstAccount() { - if(!this.accountTree) { - return null; - } - - if(this.form.value.firstAccountPrimary && this.form.value.firstAccountPrimary !== 'other') { + 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.firstAccountSecondary]; } - getSecondAccount() { - if(!this.accountTree) { - return null; - } + getFirstAccountAmount() { + let account = this.getFirstAccount(); - 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.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); + } } \ No newline at end of file diff --git a/src/sass/styles.scss b/src/sass/styles.scss index 3727141..a281b4a 100644 --- a/src/sass/styles.scss +++ b/src/sass/styles.scss @@ -86,4 +86,12 @@ a:hover { .content > * > .description { padding: 0 0.5rem; } +} + +.positive { + color: $positive; +} + +.negative { + color: $negative; } \ No newline at end of file