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, Observable, of as observableOf} from 'rxjs'; import {debounceTime, distinctUntilChanged, map, startWith, switchMap} from 'rxjs/operators'; import {IssueDataSource} from './issue-datasource'; import {VoucherService} from '../core/voucher.service'; import {Batch, Inventory, Journal, Voucher} from '../core/voucher'; import * as moment from 'moment'; import {AuthService} from '../auth/auth.service'; import {ConfirmDialogComponent} from '../shared/confirm-dialog/confirm-dialog.component'; import {ToasterService} from '../core/toaster.service'; import {IssueDialogComponent} from './issue-dialog.component'; import {BatchService} from '../core/batch.service'; import {IssueGridService} from './issue-grid.service'; import {CostCentre} from '../core/cost-centre'; import {IssueGridDataSource} from './issue-grid-datasource'; import {Hotkey, HotkeysService} from "angular2-hotkeys"; @Component({ selector: 'app-issue', templateUrl: './issue.component.html', styleUrls: ['./issue.component.css'] }) export class IssueComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('batchElement', { static: true }) batchElement: ElementRef; @ViewChild('dateElement', { static: true }) dateElement: ElementRef; public inventoryObservable = new BehaviorSubject([]); public gridObservable = new BehaviorSubject([]); dataSource: IssueDataSource; gridDataSource: IssueGridDataSource; form: FormGroup; sourceJournal: Journal; destinationJournal: Journal; voucher: Voucher; costCentres: CostCentre[]; batch: Batch; displayedColumns = ['product', 'batch', 'quantity', 'rate', 'amount', 'action']; gridColumns = ['source', 'destination', 'gridAmount', 'load']; batches: 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 batchSer: BatchService, private issueGridSer: IssueGridService ) { this.createForm(); this.listenToBatchAutocompleteChange(); this.listenToDateChange(); } ngOnInit() { this.gridDataSource = new IssueGridDataSource(this.gridObservable); this.route.data .subscribe((data: { voucher: Voucher, costCentres: CostCentre[] }) => { this.costCentres = data.costCentres; 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'])); } ngAfterViewInit() { this.focusBatch(); } ngOnDestroy() { this.hotkeys.reset(); } loadVoucher(voucher: Voucher) { this.voucher = voucher; this.sourceJournal = this.voucher.journals.filter(x => x.debit === -1)[0]; this.destinationJournal = this.voucher.journals.filter(x => x.debit === 1)[0]; this.form.setValue({ date: moment(this.voucher.date, 'DD-MMM-YYYY').toDate(), sourceCostCentre: this.sourceJournal.costCentre.id, destinationCostCentre: this.destinationJournal.costCentre.id, amount: this.sourceJournal.amount, addRow: { batch: '', quantity: '' }, narration: this.voucher.narration }); this.dataSource = new IssueDataSource(this.inventoryObservable); this.updateView(); } focusBatch() { setTimeout(() => { this.batchElement.nativeElement.focus(); }, 0); } addRow() { const formValue = this.form.get('addRow').value; const quantity = +(formValue.quantity); const isConsumption = this.voucher.journals .filter( x => x.debit === -1 )[0].costCentre.id === '7b845f95-dfef-fa4a-897c-f0baf15284a3'; if (this.batch === null || quantity <= 0) { return; } const oldFiltered = this.voucher.inventories.filter((x) => x.product.id === this.batch.product.id); const old = oldFiltered.length ? oldFiltered[0] : null; if (oldFiltered.length) { if (old.batch.id !== this.batch.id) { this.toaster.show('Danger', 'Product with a different batch already added'); return; } if (isConsumption && old.quantity + quantity > this.batch.quantityRemaining) { this.toaster.show('Danger', 'Quantity issued cannot be more than quantity available'); return; } old.quantity += quantity; } else { if (isConsumption && quantity > this.batch.quantityRemaining) { this.toaster.show('Danger', 'Quantity issued cannot be more than quantity available'); return; } this.voucher.inventories.push({ id: null, quantity: quantity, rate: this.batch.rate, tax: this.batch.tax, discount: this.batch.discount, amount: quantity * this.batch.rate * (1 + this.batch.tax) * (1 - this.batch.discount), product: this.batch.product, batch: this.batch }); } this.resetAddRow(); this.updateView(); } resetAddRow() { this.form.get('addRow').reset({ batch: null, quantity: '' }); this.batch = null; setTimeout(() => { this.batchElement.nativeElement.focus(); }, 0); } updateView() { this.inventoryObservable.next(this.voucher.inventories); const amount = Math.abs(this.voucher.inventories.map((x) => x.amount).reduce((p, c) => p + c, 0)); this.sourceJournal.amount = amount; this.destinationJournal.amount = amount; this.form.get('amount').setValue(amount); } editRow(row: Inventory) { const dialogRef = this.dialog.open(IssueDialogComponent, { width: '750px', data: {inventory: Object.assign({}, row), date: moment(this.form.value.date).format('DD-MMM-YYYY')} }); dialogRef.afterClosed().subscribe((result: boolean | Inventory) => { if (!result) { return; } const j = result as Inventory; if (j.product.id !== row.product.id && this.voucher.inventories.filter((x) => x.product.id === j.product.id).length) { return; } Object.assign(row, j); this.updateView(); }); } deleteRow(row: Inventory) { this.voucher.inventories.splice(this.voucher.inventories.indexOf(row), 1); this.updateView(); } createForm() { this.form = this.fb.group({ date: '', sourceCostCentre: '', destinationCostCentre: '', amount: {value: '', disabled: true}, addRow: this.fb.group({ batch: '', quantity: '' }), narration: '' }); } 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; } } save() { this.ser.save(this.getVoucher()) .subscribe( (result) => { this.loadVoucher(result); this.toaster.show('Success', ''); this.router.navigate(['/issue', result.id]); }, (error) => { this.toaster.show('Danger', error.error); } ); } newVoucher() { this.router.navigate(['/issue']); } getVoucher(): Voucher { const formModel = this.form.value; this.voucher.date = moment(formModel.date).format('DD-MMM-YYYY'); this.sourceJournal.costCentre.id = formModel.sourceCostCentre; this.destinationJournal.costCentre.id = formModel.destinationCostCentre; this.voucher.narration = formModel.narration; return this.voucher; } delete() { this.ser.delete(this.voucher.id) .subscribe( (result) => { this.toaster.show('Success', ''); this.router.navigate(['/issue'], {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(); } }); } listenToBatchAutocompleteChange(): void { const control = this.form.get('addRow').get('batch'); this.batches = control.valueChanges .pipe( startWith(null), map(x => (x !== null && x.length >= 1) ? x : null), debounceTime(150), distinctUntilChanged(), switchMap( x => (x === null) ? observableOf([]) : this.batchSer.autocomplete( moment(this.form.value.date).format('DD-MMM-YYYY'), x ) ) ); } listenToDateChange(): void { this.form.get('date').valueChanges .pipe( map(x => moment(x).format('DD-MMM-YYYY')) ) .subscribe(x => this.showGrid(x)); } showGrid(date: string) { this.issueGridSer.issueGrid(date) .subscribe( x => this.gridObservable.next(x) ); } displayBatchName(batch?: Batch): string | undefined { return batch ? batch.name : undefined; } batchSelected(event: MatAutocompleteSelectedEvent): void { this.batch = event.option.value; } goToVoucher(id: string) { this.router.navigate(['/issue', id]); } }