All voucher routes now work with bundles.
Need to check / update all reports and prints
This commit is contained in:
@ -1,34 +0,0 @@
|
||||
import { Modifier } from './modifier';
|
||||
|
||||
export class BillViewItem {
|
||||
id: string | undefined;
|
||||
kotId: string | undefined;
|
||||
isKot: boolean;
|
||||
info: string;
|
||||
|
||||
skuId: string;
|
||||
isHappyHour: boolean;
|
||||
isPrinted: boolean;
|
||||
quantity: number;
|
||||
modifiers: Modifier[];
|
||||
|
||||
public get isOldKot(): boolean {
|
||||
return this.isKot && this.kotId !== undefined;
|
||||
}
|
||||
|
||||
public get isNewKot(): boolean {
|
||||
return this.isKot && this.kotId === undefined;
|
||||
}
|
||||
|
||||
public constructor(init?: Partial<BillViewItem>) {
|
||||
this.isKot = true;
|
||||
this.info = '';
|
||||
this.kotId = '';
|
||||
this.skuId = '';
|
||||
this.isHappyHour = false;
|
||||
this.isPrinted = false;
|
||||
this.quantity = 0;
|
||||
this.modifiers = [];
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@ -2,17 +2,35 @@ import { MenuCategory } from './menu-category';
|
||||
import { SaleCategory } from './sale-category';
|
||||
import { Tax } from './tax';
|
||||
|
||||
export class BundleItemQuery {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number;
|
||||
quantity: number;
|
||||
tax: Tax;
|
||||
|
||||
public constructor(init?: Partial<BundleItemQuery>) {
|
||||
this.id = '';
|
||||
this.name = '';
|
||||
this.price = 0;
|
||||
this.quantity = 1;
|
||||
this.tax = new Tax();
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
export class ProductQuery {
|
||||
id: string | undefined;
|
||||
name: string;
|
||||
price: number;
|
||||
hasHappyHour: boolean;
|
||||
isNotAvailable: boolean;
|
||||
isBundle: boolean;
|
||||
sortOrder: number;
|
||||
menuCategory?: MenuCategory;
|
||||
saleCategory?: SaleCategory;
|
||||
|
||||
tax: Tax;
|
||||
bundleItems: BundleItemQuery[];
|
||||
|
||||
public constructor(init?: Partial<ProductQuery>) {
|
||||
this.id = undefined;
|
||||
@ -20,8 +38,10 @@ export class ProductQuery {
|
||||
this.price = 0;
|
||||
this.hasHappyHour = false;
|
||||
this.isNotAvailable = false;
|
||||
this.isBundle = false;
|
||||
this.sortOrder = 0;
|
||||
this.tax = new Tax();
|
||||
this.bundleItems = [];
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,19 +3,19 @@ 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 { tap } from 'rxjs/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
import { BillViewItem } from '../core/bill-view-item';
|
||||
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 { ProductLink } from './bills/product-link';
|
||||
import { VoucherType } from './bills/voucher-type';
|
||||
import { VoucherService } from './bills/voucher.service';
|
||||
import { ModifiersComponent } from './modifiers/modifiers.component';
|
||||
@ -30,66 +30,97 @@ export class BillService {
|
||||
private ser = inject(VoucherService);
|
||||
private modifierCategoryService = inject(ModifierCategoryService);
|
||||
|
||||
public dataObs: BehaviorSubject<BillViewItem[]>;
|
||||
public dataObs: BehaviorSubject<Kot[]>;
|
||||
public bill: Bill = new Bill();
|
||||
private originalBill: Bill = new Bill();
|
||||
public grossAmount: BehaviorSubject<number>;
|
||||
public discountAmount: BehaviorSubject<number>;
|
||||
public hhAmount: BehaviorSubject<number>;
|
||||
public taxAmount: BehaviorSubject<number>;
|
||||
public grossAmount: Observable<number>;
|
||||
public discountAmount: Observable<number>;
|
||||
public hhAmount: Observable<number>;
|
||||
public taxAmount: Observable<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() {
|
||||
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.dataObs = new BehaviorSubject<Kot[]>([]);
|
||||
this.amountVal = 0;
|
||||
this.updateTable = true;
|
||||
this.allowDeactivate = false;
|
||||
this.amount = this.amountBs.pipe(tap((x) => (this.amountVal = x)));
|
||||
|
||||
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 = 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,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
skuId: i.sku.id,
|
||||
isHappyHour: i.isHappyHour,
|
||||
isPrinted: !!k.id,
|
||||
info: `${i.sku.name} @ ${i.price} - ${this.math.halfRoundEven(i.discount * 100, 2)}%`,
|
||||
quantity: i.quantity,
|
||||
modifiers: i.modifiers,
|
||||
}),
|
||||
),
|
||||
])
|
||||
.reduce((a, c) => a.concat(c), []);
|
||||
this.dataObs.next(this.bill.kots);
|
||||
}
|
||||
|
||||
loadData(bill: Bill, updateTable: boolean): void {
|
||||
@ -104,23 +135,29 @@ export class BillService {
|
||||
minimum(skuId: string, happyHour: boolean): number {
|
||||
return this.bill.kots.reduce(
|
||||
(t, k) =>
|
||||
k.inventories.reduce((a, c) => (c.sku.id === skuId && c.isHappyHour === happyHour ? a + c.quantity : a), 0) + t,
|
||||
k.inventories
|
||||
.filter((i) => i.sku.id === skuId && i.isHappyHour === happyHour)
|
||||
.reduce((a, c) => a + c.quantity, 0) + t,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
addSku(sku: ProductLink, quantity: number, discount: number): void {
|
||||
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);
|
||||
if (quantity < 0) {
|
||||
const minimum = this.minimum(sku.id as string, sku.hasHappyHour) + quantity;
|
||||
if (minimum + quantity < 0) {
|
||||
this.snackBar.open('Total quantity cannot be negative!', 'Error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
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,
|
||||
@ -131,7 +168,24 @@ export class BillService {
|
||||
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)) {
|
||||
@ -165,51 +219,56 @@ export class BillService {
|
||||
});
|
||||
}
|
||||
|
||||
addOne(item: BillViewItem): void {
|
||||
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
|
||||
const old = newKot.inventories.find(
|
||||
(x) => x.sku.id === item.skuId && x.isHappyHour === item.isHappyHour,
|
||||
) as Inventory;
|
||||
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: BillViewItem, quantity: number): void {
|
||||
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
|
||||
const old = newKot.inventories.find(
|
||||
(x) => x.sku.id === item.skuId && x.isHappyHour === item.isHappyHour,
|
||||
) as Inventory;
|
||||
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: BillViewItem, canEdit: boolean): void {
|
||||
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
|
||||
const old = newKot.inventories.find(
|
||||
(x) => x.sku.id === item.skuId && x.isHappyHour === item.isHappyHour,
|
||||
) as Inventory;
|
||||
if (item.quantity >= 1 || (canEdit && this.minimum(item.skuId as string, item.isHappyHour) >= 1)) {
|
||||
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;
|
||||
} else if (item.quantity === 0) {
|
||||
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: BillViewItem): void {
|
||||
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
|
||||
const old = newKot.inventories.find(
|
||||
(x) => x.sku.id === item.skuId && x.isHappyHour === item.isHappyHour,
|
||||
) as Inventory;
|
||||
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: BillViewItem): void {
|
||||
const newKot = this.bill.kots.find((k) => k.id === undefined) as Kot;
|
||||
const old = newKot.inventories.find(
|
||||
(x) => x.sku.id === item.skuId && x.isHappyHour === item.isHappyHour,
|
||||
) as Inventory;
|
||||
modifier(item: BillRow): void {
|
||||
const old = item.inv as Inventory;
|
||||
this.showModifier(old);
|
||||
}
|
||||
|
||||
@ -217,11 +276,14 @@ export class BillService {
|
||||
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) {
|
||||
const e = discounts.find((d) => d.id === (inventory.sku.saleCategory as SaleCategory).id);
|
||||
if (e === undefined || noDiscount.indexOf(inventory.sku.id as string) !== -1) {
|
||||
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;
|
||||
}
|
||||
inventory.discount = e.discount;
|
||||
}
|
||||
}
|
||||
this.displayBill();
|
||||
@ -251,7 +313,7 @@ export class BillService {
|
||||
if (!this.isBillDiffent(item, guestBookId)) {
|
||||
return throwError(() => Error('Cannot print a blank KOT\nPlease add some products!'));
|
||||
}
|
||||
if (!this.happyHourItemsBalanced() || this.happyHourItemsMoreThanRegular()) {
|
||||
if (!this.happyHourItemsBalanced() || !this.regularItemsMoreThanHappyHour()) {
|
||||
return throwError(() => Error('Happy hour products are not balanced.'));
|
||||
}
|
||||
return this.ser
|
||||
@ -265,7 +327,7 @@ export class BillService {
|
||||
if (skus === 0) {
|
||||
return throwError(() => Error('Cannot print a blank Bill\nPlease add some products!'));
|
||||
}
|
||||
if (!this.happyHourItemsBalanced() || this.happyHourItemsMoreThanRegular()) {
|
||||
if (!this.happyHourItemsBalanced() || !this.regularItemsMoreThanHappyHour()) {
|
||||
return throwError(() => Error('Happy hour products are not balanced.'));
|
||||
}
|
||||
return this.ser
|
||||
@ -297,59 +359,6 @@ export class BillService {
|
||||
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);
|
||||
}
|
||||
@ -369,43 +378,49 @@ export class BillService {
|
||||
}
|
||||
|
||||
private happyHourItemsBalanced(): boolean {
|
||||
for (const kot of this.bill.kots) {
|
||||
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 }));
|
||||
for (const item of happyHourItems) {
|
||||
const q = kot.inventories.find((x) => !x.isHappyHour && x.sku.id === item.id && x.quantity === item.quantity);
|
||||
if (q === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private happyHourItemsMoreThanRegular(): 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 invs: Record<string, { normal: number; happy: number }> = {};
|
||||
for (const kot of this.bill.kots) {
|
||||
for (const inventory of kot.inventories) {
|
||||
const pid = inventory.sku.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) {
|
||||
.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 false;
|
||||
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 {
|
||||
|
||||
18
bookie/src/app/sales/bills/bill-row.ts
Normal file
18
bookie/src/app/sales/bills/bill-row.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Inventory } from './inventory';
|
||||
import { Kot } from './kot';
|
||||
|
||||
export class BillRow {
|
||||
id: string;
|
||||
kind: 'kot' | 'inv' | 'child';
|
||||
kot: Kot;
|
||||
inv: Inventory | undefined;
|
||||
parent: Inventory | undefined;
|
||||
public constructor(init?: Partial<BillRow>) {
|
||||
this.id = '';
|
||||
this.kind = 'kot';
|
||||
this.kot = new Kot();
|
||||
this.inv = undefined;
|
||||
this.parent = undefined;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
export class BillSelectionItem {
|
||||
kotId?: string;
|
||||
inventoryId?: string;
|
||||
skuId: string;
|
||||
isHappyHour: boolean;
|
||||
|
||||
public constructor(init?: Partial<BillSelectionItem>) {
|
||||
this.kotId = undefined;
|
||||
this.inventoryId = undefined;
|
||||
this.skuId = '';
|
||||
this.isHappyHour = false;
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,51 @@
|
||||
import { DataSource } from '@angular/cdk/collections';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, Observable, tap } from 'rxjs';
|
||||
|
||||
import { BillViewItem } from '../../core/bill-view-item';
|
||||
import { BillRow } from './bill-row';
|
||||
import { Kot } from './kot';
|
||||
|
||||
export class BillsDataSource extends DataSource<BillViewItem> {
|
||||
constructor(private data: Observable<BillViewItem[]>) {
|
||||
export class BillsDataSource extends DataSource<BillRow> {
|
||||
private data: Kot[] = [];
|
||||
constructor(private readonly dataObs: Observable<Kot[]>) {
|
||||
super();
|
||||
}
|
||||
|
||||
connect(): Observable<BillViewItem[]> {
|
||||
return this.data;
|
||||
connect(): Observable<BillRow[]> {
|
||||
return this.dataObs.pipe(
|
||||
tap((x) => {
|
||||
this.data = x;
|
||||
}),
|
||||
map((d) =>
|
||||
d.flatMap((k) => {
|
||||
const kotRow: BillRow = new BillRow({ kind: 'kot', kot: k, id: k.id! });
|
||||
const rows: BillRow[] = [];
|
||||
for (const inv of k.inventories) {
|
||||
if (!inv.id) {
|
||||
inv.clientId = inv.clientId || crypto.randomUUID();
|
||||
}
|
||||
rows.push(new BillRow({ kind: 'inv', kot: k, inv, id: `${k.id}-${inv.id || inv.clientId}` }));
|
||||
if (inv.type === 'bundle') {
|
||||
for (const child of inv.children) {
|
||||
if (!child.id) {
|
||||
child.clientId = child.clientId || crypto.randomUUID();
|
||||
}
|
||||
child.parentId = inv.id || inv.clientId;
|
||||
rows.push(
|
||||
new BillRow({
|
||||
kind: 'child',
|
||||
kot: k,
|
||||
parent: inv,
|
||||
inv: child,
|
||||
id: `${k.id}-${child.id || child.clientId}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [kotRow, ...rows];
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
<h2>Bill</h2>
|
||||
|
||||
<table mat-table #table [dataSource]="dataSource" class="mat-elevation-z8" [trackBy]="trackByKotskuId">
|
||||
<table mat-table #table [dataSource]="dataSource" class="mat-elevation-z8" [trackBy]="trackByRow">
|
||||
<ng-container matColumnDef="bill-no-title">
|
||||
<mat-header-cell *matHeaderCellDef class="deep-purple-200 bold">Bill / KOT number</mat-header-cell>
|
||||
</ng-container>
|
||||
@ -35,9 +34,9 @@
|
||||
</ng-container>
|
||||
|
||||
<!-- Checkbox Column -->
|
||||
<ng-container matColumnDef="select">
|
||||
<ng-container matColumnDef="selectKot">
|
||||
<mat-cell *matCellDef="let row">
|
||||
@if (row.isOldKot) {
|
||||
@if (row.id) {
|
||||
<mat-checkbox
|
||||
(change)="$event ? masterToggle(row) : null"
|
||||
[checked]="bs.selection.hasValue() && isAllSelected(row)"
|
||||
@ -45,71 +44,108 @@
|
||||
>
|
||||
</mat-checkbox>
|
||||
}
|
||||
@if (!row.isKot) {
|
||||
<mat-checkbox
|
||||
[disabled]="!row.id"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? toggle(row) : null"
|
||||
[checked]="isSelected(row)"
|
||||
>
|
||||
</mat-checkbox>
|
||||
}
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Info Column -->
|
||||
<ng-container matColumnDef="info">
|
||||
<ng-container matColumnDef="selectInv">
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span>
|
||||
{{ row.info }}
|
||||
</span>
|
||||
<mat-checkbox
|
||||
[disabled]="!row.id"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? toggle(row) : null"
|
||||
[checked]="isSelected(row)"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="selectChild">
|
||||
<mat-cell *matCellDef="let row"> </mat-cell>
|
||||
</ng-container>
|
||||
<!-- Info Column -->
|
||||
<ng-container matColumnDef="infoKot">
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span>{{ row.id ? `Kot: ${row.kot.code} / ${row.kot.date} (${row.kot.user.name})` : '== New Kot ==' }}</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="infoInv">
|
||||
<mat-cell *matCellDef="let row">
|
||||
<span>{{ `${row.inv.sku.name} @ ${row.inv.price} - ${discountPct(row.inv)}%` }}</span>
|
||||
<ul>
|
||||
@for (m of row.modifiers; track m.id) {
|
||||
@for (m of row.inv.modifiers; track m.id) {
|
||||
<li>{{ m.name }}</li>
|
||||
}
|
||||
</ul>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="infoChild">
|
||||
<mat-cell *matCellDef="let row" class="child-product">
|
||||
<span>{{ `${row.inv.sku.name} @ ${row.inv.price} - ${discountPct(row.inv)}%` }}</span>
|
||||
<ul>
|
||||
@for (m of row.inv.modifiers; track m.id) {
|
||||
<li>{{ m.name }}</li>
|
||||
}
|
||||
</ul>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<!-- Quantity Column -->
|
||||
<ng-container matColumnDef="kotActions">
|
||||
<mat-header-cell *matHeaderCellDef>Quantity</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right-align">
|
||||
<button mat-icon-button (click)="moveKot(row)" [disabled]="!row.kotId">
|
||||
<mat-icon class="del">open_in_new</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="quantity">
|
||||
<mat-header-cell *matHeaderCellDef>Quantity</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right-align">
|
||||
@if (!row.isKot) {
|
||||
<button mat-icon-button class="small-icon-button" (click)="subtractOne(row)" [disabled]="row.isPrinted">
|
||||
<mat-icon>indeterminate_check_box</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!row.isKot) {
|
||||
<button mat-icon-button class="small-icon-button" (click)="quantity(row)" [disabled]="rowQuantityDisabled(row)">
|
||||
{{ row.quantity }}
|
||||
</button>
|
||||
}
|
||||
@if (!row.isKot) {
|
||||
<button mat-icon-button class="small-icon-button" (click)="addOne(row)" [disabled]="row.isPrinted">
|
||||
<mat-icon>control_point</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!row.isKot) {
|
||||
<button mat-icon-button class="small-icon-button" (click)="modifier(row)" [disabled]="row.isPrinted">
|
||||
<mat-icon>assignment</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (row.isKot) {
|
||||
<button mat-icon-button (click)="moveKot(row)" [disabled]="row.isKot && !row.kotId">
|
||||
<mat-icon class="del">open_in_new</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<button mat-icon-button class="small-icon-button" (click)="subtractOne(row)" [disabled]="!!row.kot.id">
|
||||
<mat-icon>indeterminate_check_box</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="small-icon-button" (click)="quantity(row)" [disabled]="rowQuantityDisabled(row)">
|
||||
{{ row.inv.quantity }}
|
||||
</button>
|
||||
<button mat-icon-button class="small-icon-button" (click)="addOne(row)" [disabled]="!!row.kot.id">
|
||||
<mat-icon>control_point</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button class="small-icon-button" (click)="modifier(row)" [disabled]="!!row.kot.id">
|
||||
<mat-icon>assignment</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="childQuantity">
|
||||
<mat-header-cell *matHeaderCellDef>Quantity</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right-align">
|
||||
<span mat-icon-button class="small-icon-button"> </span>
|
||||
<button mat-icon-button class="small-icon-button" (click)="quantity(row)" [disabled]="true">
|
||||
{{ row.inv.quantity }}
|
||||
</button>
|
||||
<span mat-icon-button class="small-icon-button"> </span>
|
||||
<button mat-icon-button class="small-icon-button" (click)="modifier(row)" [disabled]="!!row.kot.id">
|
||||
<mat-icon>assignment</mat-icon>
|
||||
</button>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="['bill-no-title', 'bill-no-details']"></mat-header-row>
|
||||
<mat-header-row *matHeaderRowDef="['time-title', 'time-details']"></mat-header-row>
|
||||
<mat-header-row *matHeaderRowDef="['table-title', 'table-details']"></mat-header-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[class.old-kot]="row.isOldKot"
|
||||
[class.new-kot]="row.isNewKot"
|
||||
[class.is-printed]="row.isPrinted && !row.isHappyHour"
|
||||
[class.hh-new]="row.isHappyHour && !row.isPrinted"
|
||||
[class.hh-printed]="row.isPrinted && row.isHappyHour"
|
||||
*matRowDef="let row; columns: kotColumns; when: isKot"
|
||||
[class.old-kot]="!!row.id"
|
||||
[class.new-kot]="!row.id"
|
||||
></mat-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: displayedColumns; when: !isKot"
|
||||
[class.is-printed]="!!row.kot.id && !row.inv.isHappyHour"
|
||||
[class.hh-new]="!row.kot.id && row.inv.isHappyHour"
|
||||
[class.hh-printed]="!!row.kot.id && row.inv.isHappyHour"
|
||||
></mat-row>
|
||||
<mat-row
|
||||
*matRowDef="let row; columns: childColumns; when: isChild"
|
||||
class="child-row"
|
||||
[class.is-printed]="!!row.kot.id && !row.inv.isHappyHour"
|
||||
[class.hh-new]="!row.kot.id && row.inv.isHappyHour"
|
||||
[class.hh-printed]="!!row.kot.id && row.inv.isHappyHour"
|
||||
></mat-row>
|
||||
<mat-footer-row *matFooterRowDef="['gross-title', 'gross-amount']"></mat-footer-row>
|
||||
<ng-container matColumnDef="gross-title">
|
||||
|
||||
@ -1,88 +1,62 @@
|
||||
@use '@angular/material' as mat
|
||||
|
||||
$my-grey: mat.m2-define-palette(mat.$m2-grey-palette)
|
||||
$my-green: mat.m2-define-palette(mat.$m2-green-palette)
|
||||
|
||||
.right-align
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
|
||||
table
|
||||
width: 100%
|
||||
|
||||
.mat-column-select
|
||||
flex: 0 0 60px
|
||||
.mat-column-selectKot, .mat-column-selectInv, .mat-column-selectChild
|
||||
flex: 0 0 48px
|
||||
width: 48px
|
||||
max-width: 48px
|
||||
padding-left: 4px
|
||||
padding-right: 4px
|
||||
|
||||
.grey900, .mat-column-amount-title, .mat-column-amount-amount
|
||||
.mat-column-kotActions, .mat-column-quantity, .mat-column-childQuantity
|
||||
flex: 0 0 min-content
|
||||
|
||||
.mat-column-amount-title, .mat-column-amount-amount
|
||||
background-color: #1b5e20
|
||||
color: #ffffff
|
||||
font-size: 1.2em
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '900-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 900)
|
||||
|
||||
.grey700, .mat-column-tax-title, .mat-column-tax-amount
|
||||
.mat-column-tax-title, .mat-column-tax-amount
|
||||
background-color: #388e3c
|
||||
color: #ffffff
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '900-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 900)
|
||||
|
||||
.grey500, .mat-column-discount-title, .mat-column-discount-amount
|
||||
.mat-column-discount-title, .mat-column-discount-amount, .mat-column-hh-title, .mat-column-hh-amount
|
||||
background-color: #4caf50
|
||||
color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.grey300, .mat-column-hh-title, .mat-column-hh-amount, .mat-column-gross-title, .mat-column-gross-amount
|
||||
.mat-column-gross-title, .mat-column-gross-amount
|
||||
background-color: #81c784
|
||||
color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.blue400, .old-kot
|
||||
.old-kot
|
||||
background-color: #42a5f5
|
||||
color: #ffffff
|
||||
// color: mat.m2-get-color-from-palette($my-green, 700)
|
||||
// background-color: mat.m2-get-color-from-palette($my-green, 500)
|
||||
|
||||
.blue800, .new-kot
|
||||
.new-kot
|
||||
background-color: #1565c0
|
||||
color: #ffffff
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.red100, .is-printed
|
||||
// background-color: #ffcdd2
|
||||
// color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
.is-printed
|
||||
background-color: #ffcdd2
|
||||
color: #000000
|
||||
|
||||
.deep-purple-50
|
||||
background-color: #ede7f6
|
||||
color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.deep-purple-100
|
||||
background-color: #d1c4e9
|
||||
color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.deep-purple-200
|
||||
background-color: #b39ddb
|
||||
color: #000000
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.yellow300, .hh-new
|
||||
background-color: #fff176
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
.hh-printed
|
||||
background-color: #f7ca18
|
||||
// color: mat.m2-get-color-from-palette($my-grey, '300-contrast')
|
||||
// background: mat.m2-get-color-from-palette($my-grey, 300)
|
||||
|
||||
// https://github.com/btxtiger/mat-icon-button-sizes
|
||||
$button-size: 32px
|
||||
@ -109,3 +83,8 @@ $icon-size: 19px
|
||||
.mat-mdc-button-touch-target
|
||||
width: $button-size !important
|
||||
height: $button-size !important
|
||||
|
||||
.child-row
|
||||
font: var(--mat-sys-label-small)
|
||||
letter-spacing: var(--mat-sys-label-small-tracking)
|
||||
color: color-mix(in srgb, var(--mat-sys-on-surface) 38%, transparent)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AsyncPipe, CurrencyPipe } from '@angular/common';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
@ -12,10 +12,10 @@ import { Observable } from 'rxjs';
|
||||
import { map, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from '../../auth/auth.service';
|
||||
import { BillViewItem } from '../../core/bill-view-item';
|
||||
import { Customer } from '../../core/customer';
|
||||
import { Table } from '../../core/table';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { MathService } from '../../shared/math.service';
|
||||
import { TableService } from '../../tables/table.service';
|
||||
import { BillService } from '../bill.service';
|
||||
import { ChooseCustomerComponent } from '../choose-customer/choose-customer.component';
|
||||
@ -23,7 +23,7 @@ import { PaxComponent } from '../pax/pax.component';
|
||||
import { QuantityComponent } from '../quantity/quantity.component';
|
||||
import { TablesDialogComponent } from '../tables-dialog/tables-dialog.component';
|
||||
import { Bill } from './bill';
|
||||
import { BillSelectionItem } from './bill-selection-item';
|
||||
import { BillRow } from './bill-row';
|
||||
import { BillsDataSource } from './bills-datasource';
|
||||
import { Inventory } from './inventory';
|
||||
import { Kot } from './kot';
|
||||
@ -34,7 +34,7 @@ import { VoucherType } from './voucher-type';
|
||||
templateUrl: './bills.component.html',
|
||||
styleUrls: ['./bills.component.sass'],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CommonModule,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
@ -49,13 +49,16 @@ export class BillsComponent implements OnInit {
|
||||
private router = inject(Router);
|
||||
private dialog = inject(MatDialog);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private math = inject(MathService);
|
||||
private auth = inject(AuthService);
|
||||
bs = inject(BillService);
|
||||
private tSer = inject(TableService);
|
||||
|
||||
dataSource: BillsDataSource = new BillsDataSource(this.bs.dataObs);
|
||||
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
|
||||
displayedColumns: string[] = ['select', 'info', 'quantity'];
|
||||
kotColumns: string[] = ['selectKot', 'infoKot', 'kotActions'];
|
||||
displayedColumns: string[] = ['selectInv', 'infoInv', 'quantity'];
|
||||
childColumns: string[] = ['selectChild', 'infoChild', 'childQuantity'];
|
||||
|
||||
ngOnInit() {
|
||||
this.route.data.subscribe((value) => {
|
||||
@ -102,62 +105,28 @@ export class BillsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
isAllSelected(kotView: BillViewItem): boolean {
|
||||
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
|
||||
isAllSelected(row: BillRow): boolean {
|
||||
const kot = row.kot as Kot;
|
||||
return kot.inventories.reduce(
|
||||
(p: boolean, c: Inventory) =>
|
||||
p &&
|
||||
this.bs.selection.isSelected(
|
||||
JSON.stringify(
|
||||
new BillSelectionItem({
|
||||
kotId: kot.id,
|
||||
inventoryId: c.id,
|
||||
skuId: c.sku.id,
|
||||
isHappyHour: c.isHappyHour,
|
||||
}),
|
||||
),
|
||||
),
|
||||
(p: boolean, c: Inventory) => p && this.bs.selection.isSelected(`${kot.id}-${c.id || c.clientId}`),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
toggle(invView: BillViewItem) {
|
||||
const key = JSON.stringify(
|
||||
new BillSelectionItem({
|
||||
kotId: invView.kotId,
|
||||
inventoryId: invView.id,
|
||||
skuId: invView.skuId,
|
||||
isHappyHour: invView.isHappyHour,
|
||||
}),
|
||||
);
|
||||
this.bs.selection.toggle(key);
|
||||
toggle(row: BillRow) {
|
||||
this.bs.selection.toggle(row.id);
|
||||
}
|
||||
|
||||
isSelected(invView: BillViewItem): boolean {
|
||||
const key = JSON.stringify(
|
||||
new BillSelectionItem({
|
||||
kotId: invView.kotId,
|
||||
inventoryId: invView.id,
|
||||
skuId: invView.skuId,
|
||||
isHappyHour: invView.isHappyHour,
|
||||
}),
|
||||
);
|
||||
return this.bs.selection.isSelected(key);
|
||||
isSelected(row: BillRow): boolean {
|
||||
return this.bs.selection.isSelected(row.id);
|
||||
}
|
||||
|
||||
isAnySelected(kotView: BillViewItem) {
|
||||
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
|
||||
isAnySelected(row: BillRow) {
|
||||
const kot = row.kot as Kot;
|
||||
let total = 0;
|
||||
let found = 0;
|
||||
for (const item of kot.inventories) {
|
||||
const key = JSON.stringify(
|
||||
new BillSelectionItem({
|
||||
kotId: kot.id,
|
||||
inventoryId: item.id,
|
||||
skuId: item.sku.id,
|
||||
isHappyHour: item.isHappyHour,
|
||||
}),
|
||||
);
|
||||
const key = `${kot.id}-${item.id || item.clientId}`;
|
||||
total += 1;
|
||||
if (this.bs.selection.isSelected(key)) {
|
||||
found += 1;
|
||||
@ -166,18 +135,11 @@ export class BillsComponent implements OnInit {
|
||||
return found > 0 && found < total;
|
||||
}
|
||||
|
||||
masterToggle(kotView: BillViewItem) {
|
||||
const isAllSelected = this.isAllSelected(kotView);
|
||||
const kot = this.bs.bill.kots.find((k) => k.id === kotView.kotId) as Kot;
|
||||
masterToggle(row: BillRow) {
|
||||
const isAllSelected = this.isAllSelected(row);
|
||||
const kot = row.kot as Kot;
|
||||
for (const item of kot.inventories) {
|
||||
const key = JSON.stringify(
|
||||
new BillSelectionItem({
|
||||
kotId: kot.id,
|
||||
inventoryId: item.id,
|
||||
skuId: item.sku.id,
|
||||
isHappyHour: item.isHappyHour,
|
||||
}),
|
||||
);
|
||||
const key = `${kot.id}-${item.id || item.clientId}`;
|
||||
if (isAllSelected) {
|
||||
this.bs.selection.deselect(key);
|
||||
} else {
|
||||
@ -186,21 +148,26 @@ export class BillsComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
trackByKotskuId(index: number, row: BillViewItem): string {
|
||||
// Fallbacks in case fields are undefined/null
|
||||
const kotId = row.kotId ?? '';
|
||||
const skuId = row.skuId ?? '';
|
||||
return `${kotId}_${skuId}`;
|
||||
trackByRow = (_: number, row: BillRow) => {
|
||||
return row.id;
|
||||
};
|
||||
|
||||
isChild = (_: number, row: BillRow) => row.kind === 'child';
|
||||
|
||||
isKot = (_: number, row: BillRow) => row.kind === 'kot';
|
||||
|
||||
discountPct(row: Inventory): number {
|
||||
return this.math.halfRoundEven(row.discount * 100, 2);
|
||||
}
|
||||
|
||||
addOne(item: BillViewItem): void {
|
||||
addOne(item: BillRow): void {
|
||||
this.bs.addOne(item);
|
||||
}
|
||||
|
||||
quantity(item: BillViewItem): void {
|
||||
quantity(item: BillRow): void {
|
||||
const dialogRef = this.dialog.open(QuantityComponent, {
|
||||
// width: '750px',
|
||||
data: item.quantity,
|
||||
data: item.inv?.quantity,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: boolean | number) => {
|
||||
@ -211,12 +178,12 @@ export class BillsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
subtractOne(item: BillViewItem): void {
|
||||
subtractOne(item: BillRow): void {
|
||||
const canEdit = this.auth.allowed('edit-printed-product');
|
||||
this.bs.subtractOne(item, canEdit);
|
||||
}
|
||||
|
||||
modifier(item: BillViewItem): void {
|
||||
modifier(item: BillRow): void {
|
||||
this.bs.modifier(item);
|
||||
}
|
||||
|
||||
@ -257,7 +224,7 @@ export class BillsComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
moveKot(kot: BillViewItem) {
|
||||
moveKot(kot: BillRow): void {
|
||||
const canMergeTables = this.auth.allowed('merge-tables');
|
||||
this.chooseTable(canMergeTables)
|
||||
.pipe(
|
||||
@ -270,9 +237,9 @@ export class BillsComponent implements OnInit {
|
||||
return this.bs.moveTable(table);
|
||||
}
|
||||
if (table.status) {
|
||||
return this.bs.mergeKot(kot.kotId as string, table);
|
||||
return this.bs.mergeKot(kot.kot.id as string, table);
|
||||
}
|
||||
return this.bs.moveKot(kot.kotId as string, table);
|
||||
return this.bs.moveKot(kot.kot.id as string, table);
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
@ -286,8 +253,9 @@ export class BillsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
rowQuantityDisabled(row: BillViewItem) {
|
||||
if (!row.isPrinted) {
|
||||
rowQuantityDisabled(row: BillRow): boolean {
|
||||
const isPrinted = !!(row.inv as Inventory).id;
|
||||
if (!isPrinted) {
|
||||
return false;
|
||||
}
|
||||
if (this.bs.bill.voucherType === VoucherType.Void) {
|
||||
|
||||
@ -1,30 +1,43 @@
|
||||
import { Modifier } from '../../core/modifier';
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { Tax } from '../../core/tax';
|
||||
import { ProductLink } from './product-link';
|
||||
|
||||
export class Inventory {
|
||||
id: string | undefined;
|
||||
sku: ProductLink;
|
||||
clientId: string | undefined;
|
||||
sku: ProductQuery;
|
||||
quantity: number;
|
||||
price: number;
|
||||
isHappyHour: boolean;
|
||||
taxRate: number;
|
||||
type: 'regular' | 'bundle' | 'bundle_item';
|
||||
parentId?: string;
|
||||
tax: Tax;
|
||||
taxRate: number;
|
||||
discount: number;
|
||||
modifiers: Modifier[];
|
||||
sortOrder: number;
|
||||
children: Inventory[];
|
||||
|
||||
public get stableId(): string | undefined {
|
||||
return this.id ?? this.clientId;
|
||||
}
|
||||
|
||||
public constructor(init?: Partial<Inventory>) {
|
||||
this.id = undefined;
|
||||
this.sku = new ProductLink();
|
||||
if (!this.id) {
|
||||
this.clientId = crypto.randomUUID();
|
||||
}
|
||||
this.sku = new ProductQuery();
|
||||
this.quantity = 0;
|
||||
this.price = 0;
|
||||
this.isHappyHour = false;
|
||||
this.type = 'regular';
|
||||
this.taxRate = 0;
|
||||
this.tax = new Tax();
|
||||
this.discount = 0;
|
||||
this.modifiers = [];
|
||||
this.sortOrder = 0;
|
||||
this.children = [];
|
||||
Object.assign(this, init);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dial
|
||||
import { TableService } from '../../tables/table.service';
|
||||
import { BillTypeComponent } from '../bill-type/bill-type.component';
|
||||
import { BillService } from '../bill.service';
|
||||
import { BillSelectionItem } from '../bills/bill-selection-item';
|
||||
import { VoucherType } from '../bills/voucher-type';
|
||||
import { CustomerDiscountsService } from '../discount/customer-discounts.service';
|
||||
import { DiscountComponent } from '../discount/discount.component';
|
||||
@ -412,9 +411,8 @@ export class SalesHomeComponent {
|
||||
}
|
||||
|
||||
splitBillWithSelection() {
|
||||
const inventories: string[] = this.bs.selection.selected.map(
|
||||
(x: string) => (JSON.parse(x) as BillSelectionItem).inventoryId as string,
|
||||
);
|
||||
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const inventories: string[] = this.bs.selection.selected.map((v) => v.match(uuidRegex)?.[0] as string);
|
||||
return observableOf(inventories);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,6 @@ import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { BillService } from '../bill.service';
|
||||
import { ProductLink } from '../bills/product-link';
|
||||
|
||||
@Component({
|
||||
selector: 'app-products',
|
||||
@ -31,6 +30,6 @@ export class ProductsComponent implements OnInit {
|
||||
if (product.isNotAvailable) {
|
||||
return;
|
||||
}
|
||||
this.bs.addSku(new ProductLink(product), 1, 0);
|
||||
this.bs.addOneSku(product);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<h2 mat-dialog-title>Tables</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="tables-grid">
|
||||
@for (table of list; track table) {
|
||||
@for (table of list | async; track table) {
|
||||
<mat-card
|
||||
class="flex-col square-button"
|
||||
matRipple
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CdkScrollableModule } from '@angular/cdk/scrolling';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { CommonModule, CurrencyPipe } from '@angular/common';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
@ -13,7 +13,15 @@ import { Table } from '../../core/table';
|
||||
selector: 'app-tables-dialog',
|
||||
templateUrl: './tables-dialog.component.html',
|
||||
styleUrls: ['./tables-dialog.component.sass'],
|
||||
imports: [CdkScrollableModule, CurrencyPipe, MatButtonModule, MatDialogModule, MatRippleModule, MatCardModule],
|
||||
imports: [
|
||||
CdkScrollableModule,
|
||||
CommonModule,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatRippleModule,
|
||||
MatCardModule,
|
||||
],
|
||||
})
|
||||
export class TablesDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<TablesDialogComponent>>(MatDialogRef);
|
||||
@ -22,16 +30,13 @@ export class TablesDialogComponent {
|
||||
canChooseRunning: boolean;
|
||||
}>(MAT_DIALOG_DATA);
|
||||
|
||||
list: Table[] = [];
|
||||
list: Observable<Table[]>;
|
||||
canChooseRunning: boolean;
|
||||
selected: Table | null;
|
||||
|
||||
constructor() {
|
||||
const data = this.data;
|
||||
|
||||
this.data.list.subscribe((list: Table[]) => {
|
||||
this.list = list;
|
||||
});
|
||||
this.list = data.list;
|
||||
this.canChooseRunning = data.canChooseRunning;
|
||||
this.selected = null;
|
||||
}
|
||||
|
||||
@ -46,5 +46,9 @@
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
.right-align
|
||||
display: flex
|
||||
justify-content: flex-end
|
||||
|
||||
.warn
|
||||
background-color: red
|
||||
|
||||
@ -38,12 +38,12 @@
|
||||
color: get-on-color($sbsp) !important
|
||||
background: $sbsp !important
|
||||
|
||||
.accent
|
||||
.square-button.accent
|
||||
$a: map.get(mat.$orange-palette, 70)
|
||||
color: get-on-color($a) !important
|
||||
background: $a !important
|
||||
|
||||
.strong-accent
|
||||
.square-button.strong-accent
|
||||
$sa: map.get(mat.$orange-palette, 20)
|
||||
color: get-on-color($sa) !important
|
||||
background: $sa !important
|
||||
|
||||
@ -4,16 +4,11 @@
|
||||
@use 'square-buttons'
|
||||
@use 'layout'
|
||||
|
||||
// @tailwind base
|
||||
// @tailwind components
|
||||
// @tailwind utilities
|
||||
|
||||
|
||||
@include mat.core()
|
||||
|
||||
|
||||
|
||||
html
|
||||
:root
|
||||
// color-scheme: light dark
|
||||
@include mat.theme((
|
||||
color: (
|
||||
@ -23,9 +18,9 @@ html
|
||||
),
|
||||
typography: (
|
||||
plain-family: Montserrat,
|
||||
brand-family: Montserrat
|
||||
brand-family: Montserrat,
|
||||
),
|
||||
density: 0
|
||||
density: 0,
|
||||
))
|
||||
font-family: "Helvetica Neue", Montserrat, sans-serif
|
||||
|
||||
@ -37,16 +32,15 @@ a
|
||||
color: rgb(0, 0, 238)
|
||||
text-decoration: underline
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
.warn
|
||||
background-color: red
|
||||
|
||||
button.mat-primary
|
||||
.mat-mdc-button.mat-primary,
|
||||
.mat-mdc-raised-button.mat-primary,
|
||||
.mat-mdc-unelevated-button.mat-primary
|
||||
background: var(--mat-sys-primary) !important
|
||||
color: var(--mat-sys-on-primary) !important
|
||||
|
||||
button.mat-warn
|
||||
.mat-mdc-button.mat-warn,
|
||||
.mat-mdc-raised-button.mat-warn,
|
||||
.mat-mdc-unelevated-button.mat-warn
|
||||
background: var(--mat-sys-error) !important
|
||||
color: var(--mat-sys-on-error) !important
|
||||
|
||||
Reference in New Issue
Block a user