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

397 lines
13 KiB
TypeScript

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<BillViewItem[]>;
public bill: Bill = new Bill();
public grossAmount: BehaviorSubject<number>;
public discountAmount: BehaviorSubject<number>;
public hhAmount: BehaviorSubject<number>;
public taxAmount: BehaviorSubject<number>;
public amount: Observable<number>;
public amountVal: number;
public selection = new SelectionModel<string>(true, []);
private amountBs: BehaviorSubject<number>;
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<BillViewItem[]>([]);
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<boolean> {
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<boolean> {
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<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);
}
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<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.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;
}
}