new transaction wip

This commit is contained in:
Patrick Nagurny
2018-11-26 15:34:34 -05:00
parent 57394e292c
commit 72eac48f42
10 changed files with 508 additions and 6 deletions

9
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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({

View File

@@ -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');

View File

@@ -11,6 +11,7 @@
<p><a href="/docs/getting-started" target="_blank">Click here</a> for help getting started.</p>
</div>
</div> -->
<a class="btn btn-primary btn-large" routerLink="/transactions/new" role="button">New Transaction</a>
<div class="card budget">
<div class="card-body">
<div class="container-fluid" [ngClass]="{expanded: budgetExpanded}">

View File

@@ -0,0 +1,177 @@
<h1>New Transaction</h1>
<div class="section">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<ngb-accordion #acc="ngbAccordion" activeIds="toggle-1">
<ngb-panel id="toggle-1" [title]="getToggle1Title()">
<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">
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="expense"> Expense
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="income"> Income
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="openingBalance"> Opening Balance
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other"> Other
</label>
</div>
</div>
<div id="firstAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="radioBasic2" formControlName="firstAccountPrimary">
<label *ngFor="let account of expenseAccounts" ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" [value]="account.id">{{account.label | slice:0:30}}
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other">Other
</label>
</div>
<div id="firstAccountSelect" *ngIf="form.value?.firstAccountPrimary === 'other'" 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 expenseAccountsAll" [value]="account.id">
{{account.label | slice:0:30}}
</option>
</select>
</div>
</div>
</div>
<div id="firstAccountPrimary" *ngIf="form.value?.type === 'income'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="firstAccountPrimary" formControlName="firstAccountPrimary">
<label *ngFor="let account of incomeAccounts" ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" [value]="account.id">{{account.label | slice:0:30}}
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other">Other
</label>
</div>
<div id="firstAccountSelect" *ngIf="form.value?.firstAccountPrimary === 'other'" 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 incomeAccountsAll" [value]="account.id">
{{account.label | slice:0:30}}
</option>
</select>
</div>
</div>
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="toggle-2" [title]="getTitle()">
<ng-template ngbPanelContent>
<div id="secondAccountPrimary" *ngIf="form.value?.type === 'expense'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="secondAccountPrimary" formControlName="secondAccountPrimary">
<label *ngFor="let account of paymentAccounts" ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" [value]="account.id">{{account.label | slice:0:30}}
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other">Other
</label>
</div>
<div id="secondAccountSelect" *ngIf="form.value?.secondAccountPrimary === 'other'" 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>
</div>
<div id="secondAccountPrimary" *ngIf="form.value?.type === 'income'" class="mt-3">
<div class="btn-group-toggle" ngbRadioGroup name="secondAccountPrimary" formControlName="secondAccountPrimary">
<label *ngFor="let account of assetAccounts" ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" [value]="account.id">{{account.label | slice:0:30}}
</label>
<label ngbButtonLabel class="btn-primary mr-2">
<input ngbButton type="radio" value="other">Other
</label>
</div>
<div id="secondAccountSelect" *ngIf="form.value?.secondAccountPrimary === 'other'" 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 assetAccountsAll" [value]="account.id">
{{account.label | slice:0:30}}
</option>
</select>
</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>
</ngb-panel>
<ngb-panel id="toggle-3" title="Amount">
<ng-template ngbPanelContent>
<div class="form-group">
<input type="number" class="form-control" id="amount" formControlName="amount">
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="toggle-4" title="Description">
<ng-template ngbPanelContent>
<div class="form-group">
<input type="text" class="form-control" id="description" formControlName="description">
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="toggle-5" title="Date">
<ng-template ngbPanelContent>
<div class="form-group">
<input type="date" class="form-control" id="date" formControlName="date">
</div>
</ng-template>
</ngb-panel>
<ngb-panel id="toggle-6" title="Advanced">
<ng-template ngbPanelContent>
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.
</ng-template>
</ngb-panel>
</ngb-accordion>
<p *ngIf="error" class="error mt-3">{{error.message}}</p>
<button type="submit" class="btn btn-primary mt-3" [disabled]="!form.valid">Add Transaction</button>
</form>
</div>

View File

@@ -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;
}
}
}

292
src/app/transaction/new.ts Normal file
View File

@@ -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];
}
}

View File

@@ -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,

View File

@@ -1,4 +1,5 @@
/* Variables */
$black: #466e9a;
$blue: #08f;
$positive: #0a0;
$negative: #c00;