import { SelectionModel } from '@angular/cdk/collections'; import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject, throwError } from 'rxjs'; import { Observable } from 'rxjs'; 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 { MathService } from '../shared/math.service'; import { Bill } from './bills/bill'; 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 grossAmount: BehaviorSubject; public discountAmount: BehaviorSubject; public hhAmount: BehaviorSubject; public taxAmount: BehaviorSubject; public amount: Observable; public amountVal: number; public selection = new SelectionModel(true, []); private amountBs: BehaviorSubject; private updateTable: boolean; private allowDeactivate: boolean; // To disable Deactivate Guard on navigation after printing bill or kot. constructor( private dialog: MatDialog, private toaster: ToasterService, private math: MathService, private ser: VoucherService, private modifierCategoryService: ModifierCategoryService, ) { this.dataObs = new BehaviorSubject([]); this.grossAmount = new BehaviorSubject(0); this.discountAmount = new BehaviorSubject(0); this.hhAmount = new BehaviorSubject(0); this.taxAmount = new BehaviorSubject(0); this.amountBs = new BehaviorSubject(0); this.amountVal = 0; this.updateTable = true; this.allowDeactivate = false; this.amount = this.amountBs.pipe(tap((x) => (this.amountVal = x))); } displayBill(): void { this.allowDeactivate = false; const data = this.transformBillToView(this.bill); this.dataObs.next(data); this.updateAmounts(); } transformBillToView(bill: Bill): BillViewItem[] { return 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} - ${this.math.halfRoundEven(i.discount * 100, 2)}%`, quantity: i.quantity, modifiers: i.modifiers, }), ), ]) .reduce((a, c) => a.concat(c), []); } loadData(bill: Bill, updateTable: boolean): void { this.updateTable = updateTable; bill.kots.push(new Kot()); this.bill = bill; this.selection.clear(); 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.filter((x) => x.quantity !== 0).length === 0) { return throwError('Cannot print a blank KOT\nPlease add some products!'); } if (!this.happyHourItemsBalanced() || this.happyHourItemsMoreThanRegular()) { return throwError('Happy hour products are not balanced.'); } return this.ser .saveOrUpdate(item, VoucherType.Kot, guestBookId, this.updateTable) .pipe(tap(() => (this.allowDeactivate = 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() || this.happyHourItemsMoreThanRegular()) { return throwError('Happy hour products are not balanced.'); } return this.ser .saveOrUpdate(item, voucherType, guest_book_id, this.updateTable) .pipe(tap(() => (this.allowDeactivate = true))); } receivePayment(value: { choices: ReceivePaymentItem[]; reason: string }): Observable { return this.ser.receivePayment(this.bill.id as string, value.choices, value.reason, this.updateTable); } 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); } cancelBill(reason: string): Observable { return this.ser.cancelBill(this.bill.id as string, reason, this.updateTable); } updateAmounts() { this.grossAmount.next( this.math.halfRoundEven( this.bill.kots.reduce((t, k) => k.inventories.reduce((a, c) => a + c.price * c.quantity, 0) + t, 0), ), ); this.hhAmount.next( this.math.halfRoundEven( this.bill.kots.reduce( (t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? c.price : 0) * c.quantity, 0) + t, 0, ), ), ); this.discountAmount.next( this.math.halfRoundEven( 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( this.math.halfRoundEven( 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.amountBs.next( this.math.halfRoundEven( this.bill.kots.reduce( (t, k) => k.inventories.reduce( (a, c) => a + this.math.halfRoundEven( (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate), 2, ), 0, ) + t, 0, ), ), ); } splitBill(inventories: string[], table: Table): Observable { return this.ser.splitBill(this.bill.id as string, inventories, table, this.updateTable); } public getInventories(saleCategories: string[]): { move: string[]; keep: string[] } { const data: { move: string[]; keep: string[] } = { move: [], keep: [] }; for (const kot of this.bill.kots) { for (const inv of kot.inventories) { if (saleCategories.indexOf(inv.product.saleCategory?.id as string) === -1) { data.keep.push(inv.id as string); } else { data.move.push(inv.id as string); } } } return data; } private happyHourItemsBalanced(): boolean { for (const kot of this.bill.kots) { const happyHourItems = kot.inventories .filter((x) => x.isHappyHour) .map((x) => ({ id: x.product.id as string, quantity: x.quantity })); for (const item of happyHourItems) { const q = kot.inventories.find( (x) => !x.isHappyHour && x.product.id === item.id && x.quantity === item.quantity, ); if (q === undefined) { return false; } } } return true; } private happyHourItemsMoreThanRegular(): boolean { const invs: { [id: string]: { normal: number; happy: number } } = {}; for (const kot of this.bill.kots) { for (const inventory of kot.inventories) { const pid = inventory.product.id as string; if (invs[pid] === undefined) { invs[pid] = { normal: 0, happy: 0 }; } if (inventory.isHappyHour) { invs[pid].happy += inventory.quantity; } else { invs[pid].normal += inventory.quantity; } } } for (const [, value] of Object.entries(invs)) { if (value.happy > value.normal) { return true; } } return false; } public canDeactivate(): boolean { if (this.allowDeactivate) { return true; } const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; return newKot.inventories.length === 0; } }