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 { evaluate } 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 { amount = amount.replace(new RegExp('(₹[s]*)|(,)|(s)', 'g'), ''); return evaluate(amount); } 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); }, ); } save() { const voucher: Voucher = this.getVoucher(); this.ser.saveOrUpdate(voucher).subscribe( (result) => { this.toaster.show('Success', ''); if (voucher.id === result.id) { this.loadVoucher(result); } else { this.router.navigate(['/receipt', result.id]); } }, (error) => { this.toaster.show('Danger', 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); }, ); } 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; } }