435 lines
14 KiB
TypeScript
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;
|
|
}
|
|
}
|