import { SelectionModel } from '@angular/cdk/collections'; import { Injectable, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { BehaviorSubject, throwError, Observable } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import { ModifierCategory } from '../core/modifier-category'; import { ProductQuery } from '../core/product-query'; import { ReceivePaymentItem } from '../core/receive-payment-item'; import { SaleCategory } from '../core/sale-category'; import { Table } from '../core/table'; import { ModifierCategoryService } from '../modifier-categories/modifier-category.service'; import { MathService } from '../shared/math.service'; import { Bill } from './bills/bill'; import { BillRow } from './bills/bill-row'; 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({ providedIn: 'root', }) export class BillService { private dialog = inject(MatDialog); private snackBar = inject(MatSnackBar); private math = inject(MathService); private ser = inject(VoucherService); private modifierCategoryService = inject(ModifierCategoryService); public dataObs: BehaviorSubject; public bill: Bill = new Bill(); private originalBill: Bill = new Bill(); public grossAmount: Observable; public discountAmount: Observable; public hhAmount: Observable; public taxAmount: Observable; public amount: BehaviorSubject; public selection = new SelectionModel(true, []); private updateTable: boolean; private allowDeactivate: boolean; // To disable Deactivate Guard on navigation after printing bill or kot. constructor() { this.dataObs = new BehaviorSubject([]); this.updateTable = true; this.allowDeactivate = false; this.grossAmount = this.dataObs.pipe( map((kots: Kot[]) => this.math.halfRoundEven( kots.reduce((t, k) => k.inventories.reduce((a, c) => a + c.price * c.quantity, 0) + t, 0), ), ), ); this.hhAmount = this.dataObs.pipe( map((kots: Kot[]) => { return this.math.halfRoundEven( kots.reduce( (t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? c.price : 0) * c.quantity, 0) + t, 0, ), ); }), ); this.discountAmount = this.dataObs.pipe( map((kots: Kot[]) => { return this.math.halfRoundEven( kots.reduce( (t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount, 0) + t, 0, ), ); }), ); this.taxAmount = this.dataObs.pipe( map((kots: Kot[]) => { return this.math.halfRoundEven( 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 = new BehaviorSubject(0); this.dataObs .pipe( map((kots: Kot[]) => { return this.math.halfRoundEven( 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, ), ); }), ) .subscribe((value) => this.amount.next(value)); } displayBill(): void { this.allowDeactivate = false; this.dataObs.next(this.bill.kots); } loadData(bill: Bill, updateTable: boolean): void { this.updateTable = updateTable; bill.kots.push(new Kot()); this.bill = bill; this.originalBill = JSON.parse(JSON.stringify(this.bill)); this.selection.clear(); this.displayBill(); } minimum(skuId: string, happyHour: boolean): number { return this.bill.kots.reduce( (t, k) => k.inventories .filter((i) => i.sku.id === skuId && i.isHappyHour === happyHour) .reduce((a, c) => a + c.quantity, 0) + t, 0, ); } addOneSku(sku: ProductQuery): void { const quantity = 1; const discount = 0; const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot; const old = newKot.inventories.find( (x) => x.sku.id === sku.id && x.isHappyHour === sku.hasHappyHour && x.type === (sku.isBundle ? 'bundle' : 'regular'), ); if (old !== undefined) { old.quantity += quantity; if (old.children) { old.children.forEach((child) => { // child. child.quantity += quantity * (old.sku.bundleItems.find((bi) => bi.id === child.sku.id)?.quantity || 0); }); } } else { const item = new Inventory({ sku, quantity, price: sku.price, isHappyHour: sku.hasHappyHour, taxRate: sku.tax?.rate, tax: sku.tax, discount, modifiers: [], type: sku.isBundle ? 'bundle' : 'regular', }); if (sku.isBundle) { for (const bi of sku.bundleItems) { const childItem = new Inventory({ sku: new ProductQuery(bi), quantity: quantity * bi.quantity, price: bi.price, isHappyHour: sku.hasHappyHour, taxRate: bi.tax?.rate, tax: bi.tax, discount: 0, modifiers: [], type: 'bundle_item', }); item.children.push(childItem); } } newKot.inventories.push(item); this.modifierCategoryService.listForSku(sku.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', }, width: '80vw', maxWidth: 'none', maxHeight: 'none', data: { list: this.modifierCategoryService.listForSku(item.sku.id as string), selected: Object.assign([], item.modifiers), }, }); dialogRef.afterClosed().subscribe((result) => { if (result !== undefined) { item.modifiers = result; } this.displayBill(); }); } addOne(item: BillRow): void { const old = item.inv as Inventory; old.quantity += 1; if (old.children) { old.children.forEach((child) => { child.quantity += old.sku.bundleItems.find((bi) => bi.id === child.sku.id)?.quantity || 0; }); } this.displayBill(); } quantity(item: BillRow, quantity: number): void { const old = item.inv as Inventory; old.quantity = quantity; if (old.children) { old.children.forEach((child) => { child.quantity = (old.sku.bundleItems.find((bi) => bi.id === child.sku.id)?.quantity || 0) * quantity; }); } this.displayBill(); } subtractOne(item: BillRow, canEdit: boolean): void { const newKot = item.kot as Kot; const old = item.inv as Inventory; if ( old.quantity >= 1 || (canEdit && this.minimum(item.inv?.sku.id as string, item.inv?.isHappyHour || false) >= 1) ) { old.quantity -= 1; if (old.children) { old.children.forEach((child) => { child.quantity -= old.sku.bundleItems.find((bi) => bi.id === child.sku.id)?.quantity || 0; }); } } else if (old.quantity === 0) { newKot.inventories.splice(newKot.inventories.indexOf(old), 1); } this.displayBill(); } removeItem(item: BillRow): void { const newKot = item.kot as Kot; const old = item.inv as Inventory; newKot.inventories.splice(newKot.inventories.indexOf(old), 1); this.displayBill(); } modifier(item: BillRow): void { const old = item.inv 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.sku.id as string); for (const inventory of kot.inventories) { if (noDiscount.indexOf(inventory.sku.id as string) !== -1) { continue; // No discount on happy hour items } const d = discounts.find((d) => d.id === (inventory.sku.saleCategory as SaleCategory).id); if (d) { inventory.discount = d.discount; } } } this.displayBill(); } isBillDiffent(bill: Bill, guestBookId: string | null): boolean { const newItems = bill.kots .filter((k: Kot) => k.id === undefined) .reduce((p: number, k: Kot) => p + k.inventories.filter((i) => i.quantity !== 0).length, 0); if (newItems > 0) { return true; } if (guestBookId !== null) { return true; } if (bill.pax != this.originalBill.pax) { return true; } if (bill.customer !== this.originalBill.customer) { return true; } return false; } printKot(guestBookId: string | null): Observable { const item = JSON.parse(JSON.stringify(this.bill)); if (!this.isBillDiffent(item, guestBookId)) { return throwError(() => Error('Cannot print a blank KOT\nPlease add some products!')); } if (!this.happyHourItemsBalanced() || !this.regularItemsMoreThanHappyHour()) { return throwError(() => Error('Happy hour products are not balanced.')); } return this.ser .saveOrUpdate(item, VoucherType.Kot, guestBookId, this.updateTable) .pipe(tap(() => (this.allowDeactivate = true))); } printBill(guestBookId: string | null, voucherType: VoucherType): Observable { const item = JSON.parse(JSON.stringify(this.bill)); const skus = item.kots.reduce((p: number, k: Kot) => p + k.inventories.filter((i) => i.quantity !== 0).length, 0); if (skus === 0) { return throwError(() => Error('Cannot print a blank Bill\nPlease add some products!')); } if (!this.happyHourItemsBalanced() || !this.regularItemsMoreThanHappyHour()) { return throwError(() => Error('Happy hour products are not balanced.')); } return this.ser .saveOrUpdate(item, voucherType, guestBookId, 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); } 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.sku.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 { return this.bill.kots.every((kot) => { const happyHourItems = kot.inventories .filter((x) => x.isHappyHour) .map((x) => ({ id: x.sku.id as string, quantity: x.quantity })) .sort((a, b) => (a.id < b.id ? -1 : 1)); const happyHourIds = happyHourItems.map((x) => x.id); const rest = kot.inventories .filter((x) => !x.isHappyHour && happyHourIds.indexOf(x.sku.id as string) !== -1) .map((x) => ({ id: x.sku.id as string, quantity: x.quantity })) .sort((a, b) => (a.id < b.id ? -1 : 1)); if (happyHourIds.length === 0) { return true; } return ( happyHourItems.length === rest.length && happyHourItems.every((v, i) => v.id === rest[i].id && v.quantity === rest[i].quantity) ); }); } private regularItemsMoreThanHappyHour(): boolean { // This is for the whole bill. eg. Kot 1 => Reg 2 + HH 2; Kot 2 => Reg 4; Kot 3 => Reg - 4 // This is pass okay in happy hours items balanced, but overall this is wrong. Hence this check const inventories = this.bill.kots .flatMap((kot) => kot.inventories) .reduce((acc: { id: string; quantity: number; isHappyHour: boolean }[], curr) => { const existing = acc.find((x) => x.id === curr.sku.id && x.isHappyHour === curr.isHappyHour); if (existing) { existing.quantity += curr.quantity; } else { acc.push({ id: curr.sku.id as string, quantity: curr.quantity, isHappyHour: curr.isHappyHour }); } return acc; }, []) .sort((a, b) => (a.id < b.id ? -1 : 1)); const happyHourItems = inventories.filter((x) => x.isHappyHour); const happyHourIds = happyHourItems.map((x) => x.id); const rest = inventories.filter((x) => !x.isHappyHour && happyHourIds.indexOf(x.id as string) !== -1); return ( happyHourItems.length === rest.length && happyHourItems.every((hh, index) => hh.id === rest[index].id && hh.quantity <= rest[index].quantity) ); } 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; } }