diff --git a/package-lock.json b/package-lock.json index fdd94c3..3f8ff3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -536,9 +536,12 @@ } }, "@ng-bootstrap/ng-bootstrap": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-2.0.0.tgz", - "integrity": "sha512-t4QZ3es/u/yB6QchmyJemJbdtrVH4FtenlKgHJZ8095IOeKy8YVXgUwBqyLLZdU2JAwkOESmEns5ESciJHR18Q==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-3.3.1.tgz", + "integrity": "sha512-awty+5Kil0i/xIV7SSmKa5YozU83EdIx2EenF2AUDTczSKhHNhRByo82rjtwIhshN25/ZEss4aSDhgILLI88fw==", + "requires": { + "tslib": "^1.9.0" + } }, "@ngtools/webpack": { "version": "6.2.5", diff --git a/package.json b/package.json index c503b56..d55366f 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@angular/platform-browser": "^6.1.0", "@angular/platform-browser-dynamic": "^6.1.0", "@angular/router": "^6.1.0", - "@ng-bootstrap/ng-bootstrap": "2.0.0", + "@ng-bootstrap/ng-bootstrap": "^3.3.1", "bootstrap": "^4.1.3", "core-js": "^2.5.4", "rxjs": "~6.2.0", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index f51ef5d..9d27015 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -15,6 +15,7 @@ import { NewOrgPage } from './org/neworg'; import { OrgPage } from './org/org'; import { SettingsPage } from './settings/settings'; import { PriceDbPage } from './price/pricedb'; +import { NewTransactionPage } from './transaction/new'; import { ReportsPage } from './reports/reports'; import { IncomeReport } from './reports/income'; @@ -41,7 +42,8 @@ const routes: Routes = [ { path: 'orgs', component: OrgPage }, { path: 'settings', component: SettingsPage }, { path: 'tools/reconcile', component: ReconcilePage }, - { path: 'prices', component: PriceDbPage } + { path: 'prices', component: PriceDbPage }, + { path: 'transactions/new', component: NewTransactionPage } ]; @NgModule({ diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 296a232..1e1fe33 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -24,6 +24,10 @@ export class AppComponent implements OnInit { link: '/dashboard', name: 'Dashboard' }, + '/transactions/new': { + link: '/transactions/new', + name: 'New Transaction' + }, '/accounts': { link: '/accounts', name: 'Accounts' @@ -61,6 +65,7 @@ export class AppComponent implements OnInit { public leftNav: any[] = [ this.navItems['/dashboard'], + this.navItems['/transactions/new'], this.navItems['/accounts'], this.navItems['/reports'], this.navItems['/prices'], @@ -178,6 +183,7 @@ export class AppComponent implements OnInit { showLoggedInMenu() { this.showNavItem('/dashboard'); + this.showNavItem('/transactions/new'); this.showNavItem('/accounts'); this.showNavItem('/reports'); this.showNavItem('/prices'); @@ -189,6 +195,7 @@ export class AppComponent implements OnInit { showCreateOrgMenu() { this.hideNavItem('/dashboard'); + this.hideNavItem('/transactions/new'); this.hideNavItem('/accounts'); this.hideNavItem('/reports'); this.hideNavItem('/prices'); @@ -200,6 +207,7 @@ export class AppComponent implements OnInit { showLoggedOutMenu() { this.hideNavItem('/dashboard'); + this.hideNavItem('/transactions/new'); this.hideNavItem('/accounts'); this.hideNavItem('/reports'); this.hideNavItem('/prices'); diff --git a/src/app/dashboard/dashboard.html b/src/app/dashboard/dashboard.html index 1c69bee..aa10f7b 100644 --- a/src/app/dashboard/dashboard.html +++ b/src/app/dashboard/dashboard.html @@ -11,6 +11,7 @@

Click here for help getting started.

--> +New Transaction
diff --git a/src/app/transaction/new.html b/src/app/transaction/new.html new file mode 100644 index 0000000..ba1a78e --- /dev/null +++ b/src/app/transaction/new.html @@ -0,0 +1,177 @@ +

New Transaction

+ +
+
+ + + + +
+ + +
+ + + + +
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ + + 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. + + +
+

{{error.message}}

+ +
+
\ No newline at end of file diff --git a/src/app/transaction/new.scss b/src/app/transaction/new.scss new file mode 100644 index 0000000..eeaa310 --- /dev/null +++ b/src/app/transaction/new.scss @@ -0,0 +1,16 @@ +@import '../../sass/variables'; + +.accordion { + .btn-link { + color: $black; + } + .card { + margin: 0; + } + .btn-group-toggle { + .btn-primary { + background-color: transparent; + color: $blue; + } + } +} \ No newline at end of file diff --git a/src/app/transaction/new.ts b/src/app/transaction/new.ts new file mode 100644 index 0000000..263496c --- /dev/null +++ b/src/app/transaction/new.ts @@ -0,0 +1,292 @@ +import { Component, ViewEncapsulation, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { + FormGroup, + FormControl, + Validators, + FormBuilder, + AbstractControl, + ValidationErrors +} from '@angular/forms'; +import { AccountService } from '../core/account.service'; +import { OrgService } from '../core/org.service'; +import { Account, AccountApi, AccountTree } from '../shared/account'; +import { Util } from '../shared/util'; +import { AppError } from '../shared/error'; + +@Component({ + selector: 'app-txnew', + templateUrl: 'new.html', + styleUrls: ['./new.scss'], + encapsulation: ViewEncapsulation.None +}) +export class NewTransactionPage { + + public form: FormGroup; + public type: string; + public typeDescription: string; + public secondDescription: string; + public firstAccountPrimary: string; + public selectAccounts: any[]; + public error: AppError; + public numAccountsShown: number; + public expenseAccounts: any[] = []; + public incomeAccounts: any[] = []; + public paymentAccounts: any[] = []; + public assetAccounts: any[] = []; + public expenseAccountsAll: any[] = []; + public incomeAccountsAll: any[] = []; + public paymentAccountsAll: any[] = []; + public assetAccountsAll: any[] = []; + private accountTree: AccountTree; + @ViewChild('acc') acc: any; + + constructor( + private router: Router, + private accountService: AccountService, + private orgService: OrgService, + private fb: FormBuilder) { + + 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.accountService.getAccountTree().subscribe(tree => { + this.accountTree = tree; + this.selectAccounts = tree.getFlattenedAccounts().filter(account => { + let isAsset = account.fullName.match(/^Assets/); + let isLiability = account.fullName.match(/^Liabilities/); + return !account.children.length && (isAsset || isLiability); + }); + + this.getExpenseAccounts(); + this.getIncomeAccounts(); + this.getPaymentAccounts(); + this.getAssetAccounts(); + }); + + 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') { + return; + } + + this.acc.collapse('toggle-1'); + this.acc.expand('toggle-2'); + }); + + this.form.get('firstAccountSecondary').valueChanges.subscribe(val => { + this.acc.collapse('toggle-1'); + this.acc.expand('toggle-2'); + }); + + this.form.get('secondAccountPrimary').valueChanges.subscribe(val => { + if(val === 'other') { + return; + } + + this.acc.collapse('toggle-2'); + this.acc.expand('toggle-3'); + this.acc.expand('toggle-4'); + }); + + this.form.get('secondAccountSecondary').valueChanges.subscribe(val => { + this.acc.collapse('toggle-2'); + this.acc.expand('toggle-3'); + this.acc.expand('toggle-4'); + }); + } + + onSubmit() { + let account = new AccountApi(this.form.value); + account.id = Util.newGuid(); + let parentAccount = this.accountTree.accountMap[account.parent]; + + if(!parentAccount) { + this.error = new AppError('Invalid parent account'); + return; + } + + 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; + + this.accountService.newAccount(account) + .subscribe( + account => { + this.router.navigate(['/accounts']); + }, + err => { + this.error = err; + } + ); + } + + getExpenseAccounts() { + // Get most used expense accounts + + let expenseAccounts = this.accountTree.getAccountAtoms( + this.accountTree.getAccountByName('Expenses', 1) + ); + + this.processAccounts('expenseAccounts', expenseAccounts, 2); + } + + getIncomeAccounts() { + // Get most used income accounts + + let incomeAccounts = this.accountTree.getAccountAtoms( + this.accountTree.getAccountByName('Income', 1) + ); + + this.processAccounts('incomeAccounts', incomeAccounts, 2); + } + + getPaymentAccounts() { + // Get most used asset / liability accounts + + let assetAccounts = this.accountTree.getAccountAtoms( + this.accountTree.getAccountByName('Assets', 1) + ); + + let liabilityAccounts = this.accountTree.getAccountAtoms( + this.accountTree.getAccountByName('Liabilities', 1) + ); + + let paymentAccounts = assetAccounts.concat(liabilityAccounts); + + this.processAccounts('paymentAccounts', paymentAccounts, 3); + } + + getAssetAccounts() { + // Get most used asset accounts + + let assetAccounts = this.accountTree.getAccountAtoms( + this.accountTree.getAccountByName('Assets', 1) + ); + + this.processAccounts('assetAccounts', assetAccounts, 3); + } + + processAccounts(variable, data, depth) { + data.sort((a, b) => { + return b.recentTxCount - a.recentTxCount; + }); + + let dataWithLabels = data.map((account, i) => { + return { + id: account.id, + name: account.name, + label: this.accountTree.getAccountLabel(account, depth), + hidden: i < this.numAccountsShown ? false: true + } + }); + + let firstAccounts = dataWithLabels.slice(0, this.numAccountsShown); + let nextAccounts = dataWithLabels.slice(this.numAccountsShown); + + nextAccounts.sort((a, b) => { + 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) { + return 1; + } + + if(aAlpha && !bAlpha) { + return -1; + } + + if(a.label > b.label) { + return 1; + } + + if(a.label < b.label) { + return -1; + } + + return 0; + }); + + this[variable] = firstAccounts; + this[variable + 'All'] = nextAccounts; + } + + getToggle1Title() { + let account = this.getFirstAccount(); + + let str = 'What type of transaction? '; + + let type = this.form.value.type; + + if(type) { + str += type.charAt(0).toUpperCase() + type.substr(1); + if(account) { + str += ' (' + account.name + ')'; + } + } + + return str; + } + + getTitle() { + let account = this.getSecondAccount(); + + if(this.form.value.type === 'income') { + return 'Where was the money sent? ' + (account ? account.name : ''); + } else if(this.form.value.type === 'openingBalance') { + return 'What account? ' + (account ? account.name : ''); + } + + return 'How did you pay? ' + (account ? account.name : ''); + } + + getFirstAccount() { + if(!this.accountTree) { + 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.firstAccountSecondary]; + } + + getSecondAccount() { + if(!this.accountTree) { + return null; + } + + 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]; + } + + + + +} \ No newline at end of file diff --git a/src/app/transaction/transaction.module.ts b/src/app/transaction/transaction.module.ts index 8acc922..42e0262 100644 --- a/src/app/transaction/transaction.module.ts +++ b/src/app/transaction/transaction.module.ts @@ -8,6 +8,7 @@ import { AppRoutingModule } from '../app-routing.module'; import { AdvancedEdit } from './advancededit'; import { Autocomplete } from './autocomplete'; import { Breadcrumbs } from './breadcrumbs'; +import { NewTransactionPage } from './new'; @NgModule({ @@ -15,7 +16,8 @@ import { Breadcrumbs } from './breadcrumbs'; TxListPage, AdvancedEdit, Autocomplete, - Breadcrumbs + Breadcrumbs, + NewTransactionPage ], imports: [ BrowserModule, diff --git a/src/sass/variables.scss b/src/sass/variables.scss index 9a9cf97..50a71f2 100644 --- a/src/sass/variables.scss +++ b/src/sass/variables.scss @@ -1,4 +1,5 @@ /* Variables */ +$black: #466e9a; $blue: #08f; $positive: #0a0; $negative: #c00; \ No newline at end of file