Files
barker/bookie/src/app/sales/bill.service.ts

435 lines
14 KiB
TypeScript

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<Kot[]>;
public bill: Bill = new Bill();
private originalBill: Bill = new Bill();
public grossAmount: Observable<number>;
public discountAmount: Observable<number>;
public hhAmount: Observable<number>;
public taxAmount: Observable<number>;
public amount: BehaviorSubject<number>;
public selection = new SelectionModel<string>(true, []);
private updateTable: boolean;
private allowDeactivate: boolean;
// To disable Deactivate Guard on navigation after printing bill or kot.
constructor() {
this.dataObs = new BehaviorSubject<Kot[]>([]);
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<number>(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<boolean> {
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<boolean> {
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<boolean> {
return this.ser.receivePayment(this.bill.id as string, value.choices, value.reason, this.updateTable);
}
moveTable(table: Table): Observable<boolean> {
return this.ser.moveTable(this.bill.id as string, table);
}
mergeTable(table: Table): Observable<boolean> {
return this.ser.mergeTable(this.bill.id as string, table);
}
moveKot(kotId: string, table: Table): Observable<boolean> {
return this.ser.moveKotToNewTable(this.bill.id as string, kotId, table);
}
mergeKot(kotId: string, table: Table): Observable<boolean> {
return this.ser.mergeKotWithOldBill(this.bill.id as string, kotId, table);
}
cancelBill(reason: string): Observable<boolean> {
return this.ser.cancelBill(this.bill.id as string, reason, this.updateTable);
}
splitBill(inventories: string[], table: Table): Observable<boolean> {
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;
}
}