import {AfterViewInit, Component, ElementRef, OnInit, OnDestroy, ViewChild} from '@angular/core'; import {FormBuilder, FormGroup} from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatDialog } from '@angular/material/dialog'; import {ActivatedRoute, Router} from '@angular/router'; import {BehaviorSubject, fromEvent, Observable, of, of as observableOf, zip} from 'rxjs'; import {ReceiptDataSource} from './receipt-datasource'; import {Account} from '../core/account'; import {VoucherService} from '../core/voucher.service'; import {AccountService} from '../core/account.service'; import {DbFile, Journal, Voucher} from '../core/voucher'; import * as moment from 'moment'; import * as math from 'mathjs'; import {AuthService} from '../auth/auth.service'; import {ConfirmDialogComponent} from '../shared/confirm-dialog/confirm-dialog.component'; import {ToasterService} from '../core/toaster.service'; import {debounceTime, distinctUntilChanged, map, startWith, switchMap} from 'rxjs/operators'; import {ReceiptDialogComponent} from './receipt-dialog.component'; import {ImageDialogComponent} from '../shared/image-dialog/image-dialog.component'; import {Hotkey, HotkeysService} from "angular2-hotkeys"; @Component({ selector: 'app-receipt', templateUrl: './receipt.component.html', styleUrls: ['./receipt.component.css'] }) export class ReceiptComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('accountElement', { static: true }) accountElement: ElementRef; @ViewChild('dateElement', { static: true }) dateElement: ElementRef; public journalObservable = new BehaviorSubject([]); dataSource: ReceiptDataSource; form: FormGroup; receiptAccounts: Account[]; receiptJournal: Journal; voucher: Voucher; account: Account; accBal: any; displayedColumns = ['account', 'amount', 'action']; accounts: Observable; constructor( private route: ActivatedRoute, private router: Router, private fb: FormBuilder, private dialog: MatDialog, private hotkeys: HotkeysService, private toaster: ToasterService, private auth: AuthService, private ser: VoucherService, private accountSer: AccountService ) { this.account = null; this.createForm(); this.listenToAccountAutocompleteChange(); this.listenToReceiptAccountChange(); } ngOnInit() { this.route.data .subscribe((data: { voucher: Voucher, receiptAccounts: Account[] }) => { this.receiptAccounts = data.receiptAccounts; this.loadVoucher(data.voucher); }); this.hotkeys.add(new Hotkey('f2', (event: KeyboardEvent): boolean => { setTimeout(() => { this.dateElement.nativeElement.focus(); }, 0); return false; // Prevent bubbling }, ['INPUT', 'SELECT', 'TEXTAREA'])); this.hotkeys.add(new Hotkey('ctrl+s', (event: KeyboardEvent): boolean => { if (this.canSave()) this.save(); return false; // Prevent bubbling }, ['INPUT', 'SELECT', 'TEXTAREA'])); this.hotkeys.add(new Hotkey('ctrl+p', (event: KeyboardEvent): boolean => { if (this.voucher.id && !this.voucher.posted && this.auth.user.perms.indexOf('Post Vouchers') !== -1) this.post(); return false; // Prevent bubbling }, ['INPUT', 'SELECT', 'TEXTAREA'])); } ngAfterViewInit() { this.focusAccount(); } ngOnDestroy() { this.hotkeys.reset(); } loadVoucher(voucher: Voucher) { this.voucher = voucher; this.receiptJournal = this.voucher.journals.filter(x => x.debit === 1)[0]; this.form.setValue({ date: moment(this.voucher.date, 'DD-MMM-YYYY').toDate(), receiptAccount: this.receiptJournal.account.id, receiptAmount: this.receiptJournal.amount, addRow: { account: '', amount: 0 }, narration: this.voucher.narration }); this.dataSource = new ReceiptDataSource(this.journalObservable); this.updateView(); } focusAccount() { setTimeout(() => { this.accountElement.nativeElement.focus(); }, 0); } addRow() { const amount = this.rowAmount(this.form.get('addRow').value.amount); const debit = -1; if (this.account === null || amount <= 0) { return; } const oldFiltered = this.voucher.journals.filter((x) => x.account.id === this.account.id); const old = oldFiltered.length ? oldFiltered[0] : null; if (old && (old.debit === 1 || old.id === this.receiptJournal.id)) { return; } if (old) { old.amount += amount; } else { this.voucher.journals.push({ id: null, debit: debit, amount: amount, account: this.account, costCentre: null }); } this.resetAddRow(); this.updateView(); } rowAmount(amount: string = ''): number { try { return math.eval(amount.trim().replace(',', '')); } catch { return 0; } } resetAddRow() { this.form.get('addRow').reset({ account: null, amount: null }); this.account = null; this.accBal = null; setTimeout(() => { this.accountElement.nativeElement.focus(); }, 0); } updateView() { const journals = this.voucher.journals.filter(x => x.debit === -1); this.journalObservable.next(journals); this.receiptJournal.amount = Math.abs(journals.map((x) => x.amount).reduce((p, c) => p + c, 0)); this.form.get('receiptAmount').setValue(this.receiptJournal.amount); } editRow(row: Journal) { const dialogRef = this.dialog.open(ReceiptDialogComponent, { width: '750px', data: {journal: Object.assign({}, row), date: moment(this.form.get('date').value).format('DD-MMM-YYYY')} }); dialogRef.afterClosed().subscribe((result: boolean | Journal) => { if (!result) { return; } const j = result as Journal; if (j.account.id !== row.account.id && this.voucher.journals.filter((x) => x.account.id === j.account.id).length) { return; } Object.assign(row, j); this.updateView(); }); } deleteRow(row: Journal) { this.voucher.journals.splice(this.voucher.journals.indexOf(row), 1); this.updateView(); } createForm() { this.form = this.fb.group({ date: '', receiptAccount: '', receiptAmount: {value: '', disabled: true}, addRow: this.fb.group({ account: '', amount: '' }), narration: '' }); this.accBal = null; } canSave() { if (!this.voucher.id) { return true; } else if (this.voucher.posted && this.auth.user.perms.indexOf('Edit Posted Vouchers') !== -1) { return true; } else { return this.voucher.user.id === this.auth.user.id || this.auth.user.perms.indexOf('Edit Other User\'s Vouchers') !== -1; } } post() { this.ser.post(this.voucher.id) .subscribe( (result) => { this.loadVoucher(result); this.toaster.show('Success', 'Voucher Posted'); }, (error) => { this.toaster.show('Danger', error.error); } ); } save() { this.ser.saveOrUpdate(this.getVoucher()) .subscribe( (result) => { this.loadVoucher(result); this.toaster.show('Success', ''); this.router.navigate(['/receipt', result.id]); }, (error) => { this.toaster.show('Danger', error.error); } ); } getVoucher(): Voucher { const formModel = this.form.value; this.voucher.date = moment(formModel.date).format('DD-MMM-YYYY'); this.receiptJournal.account.id = formModel.receiptAccount; this.voucher.narration = formModel.narration; return this.voucher; } delete() { this.ser.delete(this.voucher.id) .subscribe( (result) => { this.toaster.show('Success', ''); this.router.navigate(['/receipt'], {replaceUrl: true}); }, (error) => { this.toaster.show('Danger', error.error); } ); } confirmDelete(): void { const dialogRef = this.dialog.open(ConfirmDialogComponent, { width: '250px', data: {title: 'Delete Voucher?', content: 'Are you sure? This cannot be undone.'} }); dialogRef.afterClosed().subscribe((result: boolean) => { if (result) { this.delete(); } }); } listenToAccountAutocompleteChange(): void { const control = this.form.get('addRow').get('account'); this.accounts = control.valueChanges .pipe( startWith(null), map(x => (x !== null && x.length >= 1) ? x : null), debounceTime(150), distinctUntilChanged(), switchMap(x => (x === null) ? observableOf([]) : this.accountSer.autocomplete(x)) ); } listenToReceiptAccountChange(): void { this.form.get('receiptAccount').valueChanges .subscribe(x => this.router.navigate([], { relativeTo: this.route, queryParams: {a: x}, replaceUrl: true }) ); } displayAccount(account?: Account): string | undefined { return account ? account.name : undefined; } accountSelected(event: MatAutocompleteSelectedEvent): void { this.account = event.option.value; const date = moment(this.form.get('date').value).format('DD-MMM-YYYY'); this.accountSer.balance(this.account.id, date).subscribe((v) => { this.accBal = v; }); } zoomImage(file: DbFile) { this.dialog.open(ImageDialogComponent, { width: '750px', data: file.resized }); } deleteImage(file: DbFile) { const index = this.voucher.files.indexOf(file); this.voucher.files.splice(index, 1); } detectFiles(event) { const files = event.target.files; if (files) { for (const file of files) { const reader = new FileReader(); reader.onload = (e: any) => { zip(of(e.target.result), this.resizeImage(e.target.result, 100, 150), this.resizeImage(e.target.result, 825, 1170) ).subscribe( val => this.voucher.files.push({id: null, thumbnail: val[1], resized: val[2]}) ); }; reader.readAsDataURL(file); } } } resizeImage(image, MAX_WIDTH, MAX_HEIGHT) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); const ex = fromEvent(img, 'load').pipe( map((e) => { let width = img.naturalWidth, height = img.naturalHeight; const ratio = Math.min(1, MAX_WIDTH / width, MAX_HEIGHT / height); if (ratio === 1) { return image; } width *= ratio; height *= ratio; canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); return canvas.toDataURL('image/jpeg', 0.95); }) ); img.src = image; return ex; } }