import { SelectionModel } from '@angular/cdk/collections'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { round } from 'mathjs'; import { BehaviorSubject, throwError } from 'rxjs'; import { Observable } from 'rxjs/internal/Observable'; import { tap } from 'rxjs/operators'; import { BillViewItem } from '../core/bill-view-item'; import { ModifierCategory } from '../core/modifier-category'; import { Product } from '../core/product'; import { ReceivePaymentItem } from '../core/receive-payment-item'; import { SaleCategory } from '../core/sale-category'; import { Table } from '../core/table'; import { ToasterService } from '../core/toaster.service'; import { ModifierCategoryService } from '../modifier-categories/modifier-category.service'; import { Bill } from './bills/bill'; import { BillSelectionItem } from './bills/bill-selection-item'; import { Inventory } from './bills/inventory'; import { Kot } from './bills/kot'; import { VoucherType } from './bills/voucher-type'; import { VoucherService } from './bills/voucher.service'; import { ModifiersComponent } from './modifiers/modifiers.component'; @Injectable() export class BillService { public dataObs: BehaviorSubject; public bill: Bill = new Bill(); public netAmount: BehaviorSubject; public discountAmount: BehaviorSubject; public taxAmount: BehaviorSubject; public amount: BehaviorSubject; public amountVal: number; public selection = new SelectionModel(true, []); constructor( private dialog: MatDialog, private toaster: ToasterService, private ser: VoucherService, private modifierCategoryService: ModifierCategoryService, ) { this.dataObs = new BehaviorSubject([]); this.netAmount = new BehaviorSubject(0); this.discountAmount = new BehaviorSubject(0); this.taxAmount = new BehaviorSubject(0); this.amount = new BehaviorSubject(0); this.amountVal = 0; this.amount.pipe(tap((x) => (this.amountVal = x))); } displayBill(): void { const data = this.transformBillToView(this.bill); this.dataObs.next(data); this.updateAmounts(); } transformBillToView(bill: Bill): BillViewItem[] { const view: BillViewItem[] = bill.kots .map((k: Kot) => [ new BillViewItem({ kotId: k.id, isKot: true, info: k.id ? `Kot: ${k.code} / ${k.date} (${k.user.name})` : '== New Kot ==', }), ...k.inventories.map( (i) => new BillViewItem({ id: i.id, kotId: k.id, isKot: false, productId: i.product.id, isHappyHour: i.isHappyHour, isPrinted: !!k.id, info: `${i.product.name} @ ${i.price} - ${round(i.discount * 100, 2)}%`, quantity: i.quantity, modifiers: i.modifiers, }), ), ]) .reduce((a, c) => a.concat(c), []); return view; } loadData(bill: Bill): void { bill.kots.push(new Kot()); this.bill = bill; this.displayBill(); } minimum(productId: string, happyHour: boolean): number { return this.bill.kots.reduce( (t, k) => k.inventories.reduce( (a, c) => c.product.id === productId && c.isHappyHour === happyHour ? a + c.quantity : a, 0, ) + t, 0, ); } addProduct(product: Product, quantity: number, discount: number): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === product.id && x.isHappyHour === product.hasHappyHour, ); if (quantity < 0) { const minimum = this.minimum(product.id as string, product.hasHappyHour) + quantity; if (minimum + quantity < 0) { this.toaster.show('Error', 'Total quantity cannot be negative!'); return; } } if (old !== undefined) { old.quantity += quantity; } else { const item = new Inventory({ product, quantity, price: product.price, isHappyHour: product.hasHappyHour, taxRate: product.tax.rate, tax: product.tax, discount, modifiers: [], }); newKot.inventories.push(item); this.modifierCategoryService.listForProduct(product.id as string).subscribe((result) => { if (result.reduce((a: number, c: ModifierCategory) => a + c.minimum, 0)) { this.showModifier(item); } }); } this.displayBill(); } showModifier(item: Inventory): void { // [routerLink]="['/sales', 'modifiers', item.id]" const dialogRef = this.dialog.open(ModifiersComponent, { position: { top: '10vh', }, data: { list: this.modifierCategoryService.listForProduct(item.product.id as string), selected: item.modifiers, }, }); dialogRef.afterClosed().subscribe((result) => { if (result !== undefined) { item.modifiers = result; } }); this.displayBill(); } addOne(item: BillViewItem): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour, ) as Inventory; old.quantity += 1; this.displayBill(); } quantity(item: BillViewItem, quantity: number): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour, ) as Inventory; old.quantity = quantity; this.displayBill(); } subtractOne(item: BillViewItem, canEdit: boolean): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour, ) as Inventory; if ( item.quantity > 1 || (canEdit && this.minimum(item.productId as string, item.isHappyHour) >= 1) ) { old.quantity -= 1; } else if (item.quantity === 0) { newKot.inventories.splice(newKot.inventories.indexOf(old), 1); } this.displayBill(); } removeItem(item: BillViewItem): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour, ) as Inventory; newKot.inventories.splice(newKot.inventories.indexOf(old), 1); this.displayBill(); } modifier(item: BillViewItem): void { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.product.id === item.productId && x.isHappyHour === item.isHappyHour, ) as Inventory; this.showModifier(old); } discount(discounts: { id: string; name: string; discount: number }[]): void { for (const kot of this.bill.kots) { const noDiscount = kot.inventories .filter((x) => x.isHappyHour) .map((x) => x.product.id as string); for (const inventory of kot.inventories) { const e = discounts.find( (d) => d.id === (inventory.product.saleCategory as SaleCategory).id, ); if (e === undefined || noDiscount.indexOf(inventory.product.id as string) !== -1) { continue; } inventory.discount = e.discount; } } this.displayBill(); } printKot(guestBookId: string | null): Observable { const item = JSON.parse(JSON.stringify(this.bill)); const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; if (newKot.inventories.length === 0) { return throwError('Cannot print a blank KOT\nPlease add some products!'); } if (!this.happyHourItemsBalanced()) { return throwError('Happy hour products are not balanced.'); } return this.ser.saveOrUpdate(item, VoucherType.Kot, guestBookId, true); } printBill(guest_book_id: string | null, voucherType: VoucherType): Observable { const item = JSON.parse(JSON.stringify(this.bill)); const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; if (item.kots.length === 1 && newKot.inventories.length === 0) { return throwError('Cannot print a blank Bill\nPlease add some products!'); } if (!this.happyHourItemsBalanced()) { return throwError('Happy hour products are not balanced.'); } return this.ser.saveOrUpdate(item, voucherType, guest_book_id, true); } type() { return this.bill.voucherType; } receivePayment(value: { choices: ReceivePaymentItem[]; reason: string }): Observable { return this.ser.receivePayment(this.bill.id as string, value.choices, value.reason, true); } moveTable(table: Table): Observable { return this.ser.moveTable(this.bill.id as string, table); } mergeTable(table: Table): Observable { return this.ser.mergeTable(this.bill.id as string, table); } moveKot(kotId: string, table: Table): Observable { return this.ser.moveKotToNewTable(this.bill.id as string, kotId, table); } mergeKot(kotId: string, table: Table): Observable { return this.ser.mergeKotWithOldBill(this.bill.id as string, kotId, table); } voidBill(reason: string): Observable { return this.ser.voidBill(this.bill.id as string, reason, true); } updateAmounts() { this.netAmount.next( round( this.bill.kots.reduce( (t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity, 0) + t, 0, ), ), ); this.discountAmount.next( round( this.bill.kots.reduce( (t, k) => k.inventories.reduce( (a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount, 0, ) + t, 0, ), ), ); this.taxAmount.next( round( this.bill.kots.reduce( (t, k) => k.inventories.reduce( (a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * c.taxRate, 0, ) + t, 0, ), ), ); this.amount.next( round( this.bill.kots.reduce( (t, k) => k.inventories.reduce( (a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate), 0, ) + t, 0, ), ), ); } splitBill(table: Table): Observable { const inventoriesToMove: string[] = this.selection.selected.map( (x: string) => (JSON.parse(x) as BillSelectionItem).inventoryId as string, ); return this.ser.splitBill(this.bill.id as string, inventoriesToMove, table); } private happyHourItemsBalanced(): boolean { const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const happyHourItems = newKot.inventories .filter((x) => x.isHappyHour) .map((x) => ({ id: x.product.id as string, quantity: x.quantity })); for (const item of happyHourItems) { const q = newKot.inventories.find( (x) => !x.isHappyHour && x.product.id === item.id && x.quantity === item.quantity, ); if (q === undefined) { return false; } } return true; } }