Modifier Category Working

This commit is contained in:
tanshu committed 2026-01-24 03:26:03 +00:00
1 parent e4072d0872
commit 4f0346551a
24 files changed
+231 -215

No files matched your search

+107 -24
View File
@@ -14,6 +14,8 @@ from ..models.menu_category import MenuCategory
from ..models.modifier_category import ModifierCategory from ..models.modifier_category import ModifierCategory
from ..models.product import Product from ..models.product import Product
from ..models.product_version import ProductVersion from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas import modifier_category as schemas from ..schemas import modifier_category as schemas
from ..schemas.menu_category import MenuCategoryLink from ..schemas.menu_category import MenuCategoryLink
from ..schemas.modifier_category_for_product import ModifierCategoryForProduct, ModifierLink from ..schemas.modifier_category_for_product import ModifierCategoryForProduct, ModifierLink
@@ -41,7 +43,7 @@ def save(
is_active=data.is_active, is_active=data.is_active,
) )
db.add(item) db.add(item)
add_products(item, data.menu_categories, db) add_skus(item, data.menu_categories, db)
db.commit() db.commit()
return modifier_category_info(item, date_, db=db) return modifier_category_info(item, date_, db=db)
except SQLAlchemyError as e: except SQLAlchemyError as e:
@@ -65,7 +67,7 @@ def update_route(
item.minimum = data.minimum item.minimum = data.minimum
item.maximum = None if data.maximum == 0 else data.maximum item.maximum = None if data.maximum == 0 else data.maximum
item.is_active = data.is_active item.is_active = data.is_active
add_products(item, data.menu_categories, db) add_skus(item, data.menu_categories, db)
db.commit() db.commit()
return modifier_category_info(item, date_, db=db) return modifier_category_info(item, date_, db=db)
except SQLAlchemyError as e: except SQLAlchemyError as e:
@@ -109,7 +111,7 @@ def show_list(
) -> list[schemas.ModifierCategory]: ) -> list[schemas.ModifierCategory]:
with SessionFuture() as db: with SessionFuture() as db:
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.menu_category_id == MenuCategory.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_, ProductVersion.valid_from <= date_,
@@ -119,6 +121,22 @@ def show_list(
ProductVersion.valid_till >= date_, ProductVersion.valid_till >= date_,
), ),
) )
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
sku_version_menu_category_onclause = and_(
SkuVersion.menu_category_id == MenuCategory.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
list_ = ( list_ = (
db.execute(select(ModifierCategory).order_by(ModifierCategory.sort_order).order_by(ModifierCategory.name)) db.execute(select(ModifierCategory).order_by(ModifierCategory.sort_order).order_by(ModifierCategory.name))
.scalars() .scalars()
@@ -127,9 +145,17 @@ def show_list(
menu_categories = ( menu_categories = (
db.execute( db.execute(
select(MenuCategory) select(MenuCategory)
.join(SkuVersion, onclause=sku_version_menu_category_onclause)
.join(StockKeepingUnit, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=product_version_onclause)
.order_by(MenuCategory.sort_order, ProductVersion.sort_order, ProductVersion.name) .order_by(MenuCategory.sort_order, StockKeepingUnit.sort_order, ProductVersion.name)
.options(contains_eager(MenuCategory.products)) .options(
contains_eager(MenuCategory.skus)
.contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
)
) )
.unique() .unique()
.scalars() .scalars()
@@ -149,13 +175,17 @@ def show_list(
schemas.MenuCategoryLink( schemas.MenuCategoryLink(
id_=mc.id, id_=mc.id,
name=mc.name, name=mc.name,
enabled=reduce(lambda x, y: x and (y.product_id in products), mc.products, True), enabled=reduce(lambda x, y: x and (y.sku.product_id in products), mc.skus, True),
products=[ProductLink(id_=p.id, name=p.name) for p in mc.products if p.product_id in products], skus=[
ProductLink(id_=p.sku.product_id, name=f"{p.sku.product.versions[0].name} ({p.units})")
for p in mc.skus
if p.sku.product_id in products
],
) )
for mc in menu_categories for mc in menu_categories
], ],
) )
modifier_category.menu_categories = [i for i in modifier_category.menu_categories if len(i.products) > 0] modifier_category.menu_categories = [i for i in modifier_category.menu_categories if len(i.skus) > 0]
modifier_categories.append(modifier_category) modifier_categories.append(modifier_category)
return modifier_categories return modifier_categories
@@ -198,7 +228,7 @@ def show_id(
def modifier_category_info(item: ModifierCategory, date_: date, db: Session) -> schemas.ModifierCategory: def modifier_category_info(item: ModifierCategory, date_: date, db: Session) -> schemas.ModifierCategory:
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.menu_category_id == MenuCategory.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_, ProductVersion.valid_from <= date_,
@@ -208,18 +238,42 @@ def modifier_category_info(item: ModifierCategory, date_: date, db: Session) ->
ProductVersion.valid_till >= date_, ProductVersion.valid_till >= date_,
), ),
) )
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
sku_version_menu_category_onclause = and_(
SkuVersion.menu_category_id == MenuCategory.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
menu_categories = ( menu_categories = (
db.execute( db.execute(
select(MenuCategory) select(MenuCategory)
.join(SkuVersion, onclause=sku_version_menu_category_onclause)
.join(StockKeepingUnit, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=product_version_onclause)
.order_by(MenuCategory.sort_order, ProductVersion.sort_order, ProductVersion.name) .order_by(MenuCategory.sort_order, StockKeepingUnit.sort_order, ProductVersion.name)
.options(contains_eager(MenuCategory.products)) .options(
contains_eager(MenuCategory.skus)
.contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
)
) )
.unique() .unique()
.scalars() .scalars()
.all() .all()
) )
products = [p.id for p in item.products] products = [x.id for x in item.products]
return schemas.ModifierCategory( return schemas.ModifierCategory(
id_=item.id, id_=item.id,
name=item.name, name=item.name,
@@ -230,13 +284,13 @@ def modifier_category_info(item: ModifierCategory, date_: date, db: Session) ->
schemas.MenuCategoryLink( schemas.MenuCategoryLink(
id_=mc.id, id_=mc.id,
name=mc.name, name=mc.name,
products=[ skus=[
ProductLinkSchema( ProductLink(
id_=p.product_id, id_=p.sku.product_id,
name=p.name, name=f"{p.sku.product.versions[0].name} ({p.units})",
enabled=p.product_id in products, enabled=p.sku.product_id in products,
) )
for p in mc.products for p in mc.skus
], ],
) )
for mc in menu_categories for mc in menu_categories
@@ -247,7 +301,7 @@ def modifier_category_info(item: ModifierCategory, date_: date, db: Session) ->
def modifier_category_blank(date_: date, db: Session) -> schemas.ModifierCategoryBlank: def modifier_category_blank(date_: date, db: Session) -> schemas.ModifierCategoryBlank:
product_version_onclause = and_( product_version_onclause = and_(
ProductVersion.menu_category_id == MenuCategory.id, ProductVersion.product_id == Product.id,
or_( or_(
ProductVersion.valid_from == None, # noqa: E711 ProductVersion.valid_from == None, # noqa: E711
ProductVersion.valid_from <= date_, ProductVersion.valid_from <= date_,
@@ -257,12 +311,36 @@ def modifier_category_blank(date_: date, db: Session) -> schemas.ModifierCategor
ProductVersion.valid_till >= date_, ProductVersion.valid_till >= date_,
), ),
) )
sku_version_onclause = and_(
SkuVersion.sku_id == StockKeepingUnit.id,
or_(SkuVersion.valid_from == None, SkuVersion.valid_from <= date_), # noqa: E711
or_(SkuVersion.valid_till == None, SkuVersion.valid_till >= date_), # noqa: E711
)
sku_version_menu_category_onclause = and_(
SkuVersion.menu_category_id == MenuCategory.id,
or_(
SkuVersion.valid_from == None, # noqa: E711
SkuVersion.valid_from <= date_,
),
or_(
SkuVersion.valid_till == None, # noqa: E711
SkuVersion.valid_till >= date_,
),
)
menu_categories = ( menu_categories = (
db.execute( db.execute(
select(MenuCategory) select(MenuCategory)
.join(SkuVersion, onclause=sku_version_menu_category_onclause)
.join(StockKeepingUnit, onclause=sku_version_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=product_version_onclause) .join(ProductVersion, onclause=product_version_onclause)
.order_by(MenuCategory.sort_order, ProductVersion.sort_order, ProductVersion.name) .order_by(MenuCategory.sort_order, StockKeepingUnit.sort_order, ProductVersion.name)
.options(contains_eager(MenuCategory.products)) .options(
contains_eager(MenuCategory.skus)
.contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
)
) )
.unique() .unique()
.scalars() .scalars()
@@ -277,16 +355,21 @@ def modifier_category_blank(date_: date, db: Session) -> schemas.ModifierCategor
schemas.MenuCategoryLink( schemas.MenuCategoryLink(
id_=mc.id, id_=mc.id,
name=mc.name, name=mc.name,
products=[ProductLinkSchema(id_=p.product_id, name=p.name, enabled=False) for p in mc.products], skus=[
ProductLinkSchema(
id_=p.sku.product_id, name=f"{p.sku.product.versions[0].name} ({p.units})", enabled=False
)
for p in mc.skus
],
) )
for mc in menu_categories for mc in menu_categories
], ],
) )
def add_products(modifier_category: ModifierCategory, menu_categories: list[MenuCategoryLink], db: Session) -> None: def add_skus(modifier_category: ModifierCategory, menu_categories: list[MenuCategoryLink], db: Session) -> None:
for mc in menu_categories: for mc in menu_categories:
for p in mc.products: for p in mc.skus:
old = next((x for x in modifier_category.products if x.id == p.id_), None) old = next((x for x in modifier_category.products if x.id == p.id_), None)
if p.enabled and old is None: if p.enabled and old is None:
product_object = db.execute(select(Product).where(Product.id == p.id_)).scalar_one() product_object = db.execute(select(Product).where(Product.id == p.id_)).scalar_one()
+1 -1
View File
@@ -28,5 +28,5 @@ class MenuCategoryLink(BaseModel):
id_: uuid.UUID = Field(...) id_: uuid.UUID = Field(...)
name: str | None = None name: str | None = None
enabled: bool | None = None enabled: bool | None = None
products: list[ProductLink] skus: list[ProductLink]
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
+3 -3
View File
@@ -1,4 +1,4 @@
import { StockKeepingUnit as Product } from './stock-keeping-unit'; import { StockKeepingUnit } from './product';
export class MenuCategory { export class MenuCategory {
id: string | undefined; id: string | undefined;
@@ -6,7 +6,7 @@ export class MenuCategory {
isActive: boolean; isActive: boolean;
isFixture: boolean; isFixture: boolean;
sortOrder: number; sortOrder: number;
products: Product[]; skus: StockKeepingUnit[];
enabled?: boolean; enabled?: boolean;
public constructor(init?: Partial<MenuCategory>) { public constructor(init?: Partial<MenuCategory>) {
@@ -15,7 +15,7 @@ export class MenuCategory {
this.isActive = true; this.isActive = true;
this.isFixture = false; this.isFixture = false;
this.sortOrder = 0; this.sortOrder = 0;
this.products = []; this.skus = [];
Object.assign(this, init); Object.assign(this, init);
} }
} }
+27 -1
View File
@@ -1,6 +1,32 @@
import { MenuCategory } from './menu-category';
import { Modifier } from './modifier'; import { Modifier } from './modifier';
export class StockKeepingUnitLink {
id: string | undefined;
name: string;
enabled: boolean;
public constructor(init?: Partial<StockKeepingUnitLink>) {
this.name = '';
this.enabled = true;
Object.assign(this, init);
}
}
export class MenuCategory {
id: string | undefined;
name: string;
skus: StockKeepingUnitLink[];
enabled?: boolean;
public constructor(init?: Partial<MenuCategory>) {
this.id = undefined;
this.name = '';
this.enabled = true;
this.skus = [];
Object.assign(this, init);
}
}
export class ModifierCategory { export class ModifierCategory {
id: string | undefined; id: string | undefined;
name: string; name: string;
@@ -12,9 +12,7 @@ import { MatTreeModule, MatTreeNestedDataSource } from '@angular/material/tree';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { MenuCategory } from '../../core/menu-category'; import { ModifierCategory, MenuCategory, StockKeepingUnitLink } from '../../core/modifier-category';
import { ModifierCategory } from '../../core/modifier-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ModifierCategoryService } from '../modifier-category.service'; import { ModifierCategoryService } from '../modifier-category.service';
import { NodeItem } from '../node-item'; import { NodeItem } from '../node-item';
@@ -35,7 +33,6 @@ interface ModifierCategoryNode {
styleUrls: ['./modifier-category-detail.component.css'], styleUrls: ['./modifier-category-detail.component.css'],
imports: [ imports: [
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,
MatDividerModule, MatDividerModule,
MatFormFieldModule, MatFormFieldModule,
@@ -65,8 +62,8 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
item: ModifierCategory = new ModifierCategory(); item: ModifierCategory = new ModifierCategory();
dataSource = new MatTreeNestedDataSource<NodeItem>(); dataSource = new MatTreeNestedDataSource<NodeItem>();
products: Map<string, Product> = new Map<string, Product>(); products: Map<string, StockKeepingUnitLink> = new Map<string, StockKeepingUnitLink>();
productsOfMenuCategory: Map<string, Product[]> = new Map<string, Product[]>(); productsOfMenuCategory: Map<string, StockKeepingUnitLink[]> = new Map<string, StockKeepingUnitLink[]>();
constructor() { constructor() {
// Create form // Create form
@@ -87,7 +84,7 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
const tree: NodeItem[] = data.item.menuCategories.map((x: MenuCategory) => ({ const tree: NodeItem[] = data.item.menuCategories.map((x: MenuCategory) => ({
id: x.id as string, id: x.id as string,
name: x.name, name: x.name,
children: x.products.map((y: Product) => ({ id: y.id as string, name: y.name })), children: x.skus.map((y: StockKeepingUnitLink) => ({ id: y.id as string, name: y.name })),
})); }));
return { item: data.item, tree }; return { item: data.item, tree };
}), }),
@@ -109,12 +106,12 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
this.products = new Map(); this.products = new Map();
this.productsOfMenuCategory = new Map(); this.productsOfMenuCategory = new Map();
this.item.menuCategories.forEach((mc: MenuCategory) => { this.item.menuCategories.forEach((mc: MenuCategory) => {
mc.products.forEach((p: Product) => { mc.skus.forEach((p: StockKeepingUnitLink) => {
this.products.set(p.id as string, p); this.products.set(p.id as string, p);
if (!this.productsOfMenuCategory.has(mc.id as string)) { if (!this.productsOfMenuCategory.has(mc.id as string)) {
this.productsOfMenuCategory.set(mc.id as string, []); this.productsOfMenuCategory.set(mc.id as string, []);
} }
(this.productsOfMenuCategory.get(mc.id as string) as Product[]).push(p); (this.productsOfMenuCategory.get(mc.id as string) as StockKeepingUnitLink[]).push(p);
}); });
}); });
// this.setMenuCategories(); // this.setMenuCategories();
@@ -180,19 +177,19 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
} }
isProductSelected(node: NodeItem) { isProductSelected(node: NodeItem) {
return (this.products.get(node.id) as Product).enabled; return (this.products.get(node.id) as StockKeepingUnitLink).enabled;
} }
isMenuCategorySelected(node: NodeItem) { isMenuCategorySelected(node: NodeItem) {
return (this.productsOfMenuCategory.get(node.id) as Product[]).reduce( return (this.productsOfMenuCategory.get(node.id) as StockKeepingUnitLink[]).reduce(
(acc: boolean, current: Product) => acc && current.enabled, (acc: boolean, current: StockKeepingUnitLink) => acc && current.enabled,
true, true,
); );
} }
isMenuCategoryPartiallySelected(node: NodeItem) { isMenuCategoryPartiallySelected(node: NodeItem) {
const total = (this.productsOfMenuCategory.get(node.id) as Product[]).length; const total = (this.productsOfMenuCategory.get(node.id) as StockKeepingUnitLink[]).length;
const ticked = (this.productsOfMenuCategory.get(node.id) as Product[]).reduce((acc, current) => { const ticked = (this.productsOfMenuCategory.get(node.id) as StockKeepingUnitLink[]).reduce((acc, current) => {
if (current.enabled) { if (current.enabled) {
acc += 1; acc += 1;
} }
@@ -202,12 +199,13 @@ export class ModifierCategoryDetailComponent implements OnInit, AfterViewInit {
} }
toggleProductSelection(node: NodeItem) { toggleProductSelection(node: NodeItem) {
(this.products.get(node.id) as Product).enabled = !(this.products.get(node.id) as Product).enabled; (this.products.get(node.id) as StockKeepingUnitLink).enabled = !(this.products.get(node.id) as StockKeepingUnitLink)
.enabled;
} }
toggleMenuCategorySelection(node: NodeItem) { toggleMenuCategorySelection(node: NodeItem) {
const sel = !this.isMenuCategorySelected(node); const sel = !this.isMenuCategorySelected(node);
(this.productsOfMenuCategory.get(node.id as string) as Product[]).forEach((p) => { (this.productsOfMenuCategory.get(node.id as string) as StockKeepingUnitLink[]).forEach((p) => {
p.enabled = sel; p.enabled = sel;
}); });
return sel; return sel;
@@ -47,7 +47,7 @@
{{ mc.name }} {{ mc.name }}
@if (!mc.enabled) { @if (!mc.enabled) {
<ul> <ul>
@for (p of mc.products; track p) { @for (p of mc.skus; track p) {
<li>{{ p.name }}</li> <li>{{ p.name }}</li>
} }
</ul> </ul>
@@ -63,13 +63,13 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-checkbox formControlName="hasHappyHour" class="flex-auto">Has Happy Hour?</mat-checkbox> <mat-checkbox formControlName="hasHappyHour" class="flex-auto">Has Happy Hour?</mat-checkbox>
<mat-checkbox formControlName="isNotAvailable" class="flex-auto">Not Available?</mat-checkbox> <mat-checkbox formControlName="isNotAvailable" class="flex-auto">Not Available?</mat-checkbox>
<button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button> <button mat-raised-button color="primary" (click)="addRow()" class="flex-auto">Add</button>
</div>
</div></form> </form>
<!-- SKU table --> <!-- SKU table -->
<div class="row-container wrapped"> <div class="row-container wrapped">
<mat-table [dataSource]="dataSource" aria-label="Stock Keeping Units" class="flex-auto"> <mat-table [dataSource]="dataSource" aria-label="Stock Keeping Units" class="flex-auto">
<!-- Units Column --> <!-- Units Column -->
@@ -1,3 +1,4 @@
import { CurrencyPipe } from '@angular/common';
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnInit, ViewChild, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -5,21 +6,20 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
import { Product, StockKeepingUnit } from '../../core/product'; import { Product, StockKeepingUnit } from '../../core/product';
import { SaleCategory } from '../../core/sale-category'; import { SaleCategory } from '../../core/sale-category';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ProductService } from '../product.service'; import { ProductService } from '../product.service';
import { BehaviorSubject } from 'rxjs';
import { ProductDetailDatasource } from './product-detail-datasource'; import { ProductDetailDatasource } from './product-detail-datasource';
import { MatTableModule } from '@angular/material/table';
import { CurrencyPipe } from '@angular/common';
import { MatIconModule } from '@angular/material/icon';
@Component({ @Component({
selector: 'app-product-detail', selector: 'app-product-detail',
@@ -46,7 +46,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
private ser = inject(ProductService); private ser = inject(ProductService);
@ViewChild('name', { static: true }) nameElement?: ElementRef; @ViewChild('name', { static: true }) nameElement?: ElementRef;
form: FormGroup<{ form: FormGroup<{
// Product fields // Product fields
name: FormControl<string>; name: FormControl<string>;
fractionUnits: FormControl<string>; fractionUnits: FormControl<string>;
@@ -66,18 +66,27 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
}>; }>;
}>; }>;
menuCategories: MenuCategory[] = []; menuCategories: MenuCategory[] = [];
saleCategories: SaleCategory[] = []; saleCategories: SaleCategory[] = [];
public skus = new BehaviorSubject<StockKeepingUnit[]>([]); public skus = new BehaviorSubject<StockKeepingUnit[]>([]);
dataSource: ProductDetailDatasource = new ProductDetailDatasource(this.skus); dataSource: ProductDetailDatasource = new ProductDetailDatasource(this.skus);
item: Product = new Product(); item: Product = new Product();
displayedColumns = ['units', 'fraction', 'productYield', 'costPrice', 'salePrice', 'menuCategory', 'hasHappyHour', 'isNotAvailable', 'action']; displayedColumns = [
'units',
'fraction',
'productYield',
'costPrice',
'salePrice',
'menuCategory',
'hasHappyHour',
'isNotAvailable',
'action',
];
constructor() { constructor() {
// Create form // Create form
this.form = new FormGroup({ this.form = new FormGroup({
name: new FormControl<string>('', { nonNullable: true }), name: new FormControl<string>('', { nonNullable: true }),
fractionUnits: new FormControl<string>('', { nonNullable: true }), fractionUnits: new FormControl<string>('', { nonNullable: true }),
saleCategory: new FormControl<string>('', { nonNullable: true }), saleCategory: new FormControl<string>('', { nonNullable: true }),
@@ -174,13 +183,16 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.snackBar.open('Menu Category is required', 'Error'); this.snackBar.open('Menu Category is required', 'Error');
return; return;
} }
const sku = new StockKeepingUnit({ const sku = new StockKeepingUnit({
units, units,
fraction, fraction,
productYield, productYield,
costPrice, costPrice,
salePrice, salePrice,
menuCategory: new MenuCategory({ id: menuCategoryId, name: this.menuCategories.find((x) => x.id === menuCategoryId)?.name ?? '' }), menuCategory: new MenuCategory({
id: menuCategoryId,
name: this.menuCategories.find((x) => x.id === menuCategoryId)?.name ?? '',
}),
hasHappyHour: formValue.hasHappyHour ?? false, hasHappyHour: formValue.hasHappyHour ?? false,
isNotAvailable: formValue.isNotAvailable ?? false, isNotAvailable: formValue.isNotAvailable ?? false,
}); });
@@ -203,7 +215,6 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
// isPurchased: this.item.isPurchased, // isPurchased: this.item.isPurchased,
// }, // },
// }); // });
// dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => { // dialogRef.afterClosed().subscribe((result: boolean | StockKeepingUnit) => {
// if (!result) { // if (!result) {
// return; // return;
@@ -280,14 +291,12 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
} }
} }
// if (this.item.menuCategory === null || this.item.menuCategory === undefined) {
// this.item.menuCategory = new MenuCategory();
// }
// this.item.menuCategory.id = formModel.menuCategory;
// this.item.price = formModel.price ?? 0;
// if (this.item.menuCategory === null || this.item.menuCategory === undefined) { // this.item.hasHappyHour = formModel.hasHappyHour ?? false;
// this.item.menuCategory = new MenuCategory(); // this.item.isNotAvailable = formModel.isNotAvailable ?? false;
// } // this.item.quantity = formModel.quantity ?? 0;
// this.item.menuCategory.id = formModel.menuCategory;
// this.item.price = formModel.price ?? 0;
// this.item.hasHappyHour = formModel.hasHappyHour ?? false;
// this.item.isNotAvailable = formModel.isNotAvailable ?? false;
// this.item.quantity = formModel.quantity ?? 0;
@@ -1,7 +1,7 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router'; import { ResolveFn } from '@angular/router';
import { StockKeepingUnit as Product } from '../core/stock-keeping-unit'; import { Product } from '../core/product';
import { ProductService } from './product.service'; import { ProductService } from './product.service';
export const productListResolver: ResolveFn<Product[]> = () => { export const productListResolver: ResolveFn<Product[]> = () => {
@@ -2,7 +2,6 @@ import { DataSource } from '@angular/cdk/collections';
import { merge, Observable } from 'rxjs'; import { merge, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { MenuCategory } from '../../core/menu-category';
import { Product } from '../../core/product'; import { Product } from '../../core/product';
export class ProductListDataSource extends DataSource<Product> { export class ProductListDataSource extends DataSource<Product> {
@@ -50,8 +50,7 @@
<ul> <ul>
@for (sku of row.skus; track sku) { @for (sku of row.skus; track sku) {
<li> <li>
<a [routerLink]="['/products', row.id]" <a [routerLink]="['/products', row.id]">{{ row.name }} ({{ sku.units }})</a>
>{{ row.name }} ({{ sku.units }})</a>
</li> </li>
} }
</ul> </ul>
@@ -99,19 +98,19 @@
<ul> <ul>
@for (sku of row.skus; track sku) { @for (sku of row.skus; track sku) {
<li layout="row"> <li layout="row">
<div> <div>
<mat-icon> <mat-icon>
{{ sku.hasHappyHour ? 'sentiment_satisfied_alt' : 'sentiment_dissatisfied' }} {{ sku.hasHappyHour ? 'sentiment_satisfied_alt' : 'sentiment_dissatisfied' }}
</mat-icon> </mat-icon>
<b> {{ sku.hasHappyHour ? 'Happy Hour' : 'Regular' }}</b> <b> {{ sku.hasHappyHour ? 'Happy Hour' : 'Regular' }}</b>
</div> </div>
<div> <div>
<mat-icon> <mat-icon>
{{ row.isNotAvailable ? 'pause' : 'play_arrow' }} {{ row.isNotAvailable ? 'pause' : 'play_arrow' }}
</mat-icon> </mat-icon>
<b> {{ row.isNotAvailable ? 'Not Available' : 'Available' }}</b> <b> {{ row.isNotAvailable ? 'Not Available' : 'Available' }}</b>
</div> </div>
</li> </li>
} }
</ul> </ul>
</mat-cell> </mat-cell>
@@ -121,15 +120,11 @@
<ng-container matColumnDef="quantity"> <ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell> <mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right"> <mat-cell *matCellDef="let row" class="right">
<ul> <ul>
@for (sku of row.skus; track sku) { @for (sku of row.skus; track sku) {
<li> <li>{{ sku.fraction | number: '1.2-2' }}{{ row.fractionUnits }}</li>
{{ sku.fraction | number: '1.2-2' }}{{ row.fractionUnits }}
</li>
} }
</ul> </ul>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@@ -1,4 +1,4 @@
import { CdkDragDrop, moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop'; import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop';
import { AsyncPipe, DecimalPipe, CurrencyPipe } from '@angular/common'; import { AsyncPipe, DecimalPipe, CurrencyPipe } from '@angular/common';
import { Component, OnInit, inject } from '@angular/core'; import { Component, OnInit, inject } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
@@ -1,85 +0,0 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { StockKeepingUnit as Product } from '../core/stock-keeping-unit';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
};
const url = '/api/products';
const serviceName = 'ProductService';
@Injectable({ providedIn: 'root' })
export class ProductService {
private http = inject(HttpClient);
private log = inject(ErrorLoggerService);
get(id: string | null): Observable<Product> {
const getUrl: string = id === null ? `${url}` : `${url}/${id}`;
return this.http
.get<Product>(getUrl)
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Product>;
}
list(): Observable<Product[]> {
return this.http
.get<Product[]>(`${url}/list`)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Product[]>;
}
listOfSaleCategory(id: string): Observable<Product[]> {
const options = { params: new HttpParams().set('sc', id) };
return this.http
.get<Product[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'listOfSaleCategory'))) as Observable<Product[]>;
}
listIsActiveOfCategory(id: string): Observable<Product[]> {
const options = { params: new HttpParams().set('mc', id) };
return this.http
.get<Product[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'listIsActiveOfCategory'))) as Observable<Product[]>;
}
save(product: Product): Observable<void> {
return this.http
.post<Product>(`${url}`, product, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<void>;
}
update(product: Product): Observable<void> {
return this.http
.put<Product>(`${url}/${product.id}`, product, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<void>;
}
updateSortOrder(list: Product[]): Observable<Product[]> {
return this.http
.post<Product[]>(`${url}/list`, list, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'updateSortOrder'))) as Observable<Product[]>;
}
saveOrUpdate(product: Product): Observable<void> {
if (!product.versionId) {
return this.save(product);
}
return this.update(product);
}
delete(id: string): Observable<void> {
return this.http
.delete<Product>(`${url}/${id}`, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<void>;
}
balance(id: string, date: string): Observable<number> {
const options = { params: new HttpParams().set('d', date) };
return this.http
.get<number>(`${url}/balance`, options)
.pipe(catchError(this.log.handleError(serviceName, 'balance'))) as Observable<number>;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router'; import { ResolveFn } from '@angular/router';
import { StockKeepingUnit as Product } from '../core/stock-keeping-unit'; import { Product } from '../core/product';
import { ProductService } from './product.service'; import { ProductService } from './product.service';
export const productResolver: ResolveFn<Product> = (route) => { export const productResolver: ResolveFn<Product> = (route) => {
+9 -10
View File
@@ -4,8 +4,7 @@ import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service'; import { ErrorLoggerService } from '../core/error-logger.service';
import { StockKeepingUnit as Product } from '../core/stock-keeping-unit'; import { Product } from '../core/product';
import { Product as ProductNew } from '../core/product';
const httpOptions = { const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }), headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
@@ -46,25 +45,25 @@ export class ProductService {
.pipe(catchError(this.log.handleError(serviceName, 'listIsActiveOfCategory'))) as Observable<Product[]>; .pipe(catchError(this.log.handleError(serviceName, 'listIsActiveOfCategory'))) as Observable<Product[]>;
} }
save(product: ProductNew): Observable<void> { save(product: Product): Observable<void> {
return this.http return this.http
.post<ProductNew>(`${url}`, product, httpOptions) .post<Product>(`${url}`, product, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<void>; .pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<void>;
} }
update(product: ProductNew): Observable<void> { update(product: Product): Observable<void> {
return this.http return this.http
.put<ProductNew>(`${url}/${product.id}`, product, httpOptions) .put<Product>(`${url}/${product.id}`, product, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<void>; .pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<void>;
} }
updateSortOrder(list: ProductNew[]): Observable<ProductNew[]> { updateSortOrder(list: Product[]): Observable<Product[]> {
return this.http return this.http
.post<ProductNew[]>(`${url}/list`, list, httpOptions) .post<Product[]>(`${url}/list`, list, httpOptions)
.pipe(catchError(this.log.handleError(serviceName, 'updateSortOrder'))) as Observable<ProductNew[]>; .pipe(catchError(this.log.handleError(serviceName, 'updateSortOrder'))) as Observable<Product[]>;
} }
saveOrUpdate(product: ProductNew): Observable<void> { saveOrUpdate(product: Product): Observable<void> {
if (!product.versionId) { if (!product.versionId) {
return this.save(product); return this.save(product);
} }
@@ -2,7 +2,7 @@ import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router'; import { ResolveFn } from '@angular/router';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { StockKeepingUnit as Product } from '../core/stock-keeping-unit'; import { Product } from '../core/product';
import { ProductService } from '../product/product.service'; import { ProductService } from '../product/product.service';
export const productListResolver: ResolveFn<Product[]> = (route) => { export const productListResolver: ResolveFn<Product[]> = (route) => {
@@ -14,8 +14,8 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { round } from 'mathjs'; import { round } from 'mathjs';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { SaleCategory } from '../../core/sale-category'; import { SaleCategory } from '../../core/sale-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { Tax } from '../../core/tax'; import { Tax } from '../../core/tax';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { SaleCategoryService } from '../sale-category.service'; import { SaleCategoryService } from '../sale-category.service';
+4 -12
View File
@@ -7,9 +7,9 @@ import { tap } from 'rxjs/operators';
import { BillViewItem } from '../core/bill-view-item'; import { BillViewItem } from '../core/bill-view-item';
import { ModifierCategory } from '../core/modifier-category'; import { ModifierCategory } from '../core/modifier-category';
import { StockKeepingUnit } from '../core/stock-keeping-unit';
import { ReceivePaymentItem } from '../core/receive-payment-item'; import { ReceivePaymentItem } from '../core/receive-payment-item';
import { SaleCategory } from '../core/sale-category'; import { SaleCategory } from '../core/sale-category';
import { StockKeepingUnit } from '../core/stock-keeping-unit';
import { Table } from '../core/table'; import { Table } from '../core/table';
import { ModifierCategoryService } from '../modifier-categories/modifier-category.service'; import { ModifierCategoryService } from '../modifier-categories/modifier-category.service';
import { MathService } from '../shared/math.service'; import { MathService } from '../shared/math.service';
@@ -104,10 +104,7 @@ export class BillService {
minimum(skuId: string, happyHour: boolean): number { minimum(skuId: string, happyHour: boolean): number {
return this.bill.kots.reduce( return this.bill.kots.reduce(
(t, k) => (t, k) =>
k.inventories.reduce( k.inventories.reduce((a, c) => (c.sku.id === skuId && c.isHappyHour === happyHour ? a + c.quantity : a), 0) + t,
(a, c) => (c.sku.id === skuId && c.isHappyHour === happyHour ? a + c.quantity : a),
0,
) + t,
0, 0,
); );
} }
@@ -264,10 +261,7 @@ export class BillService {
printBill(guestBookId: string | null, voucherType: VoucherType): Observable<boolean> { printBill(guestBookId: string | null, voucherType: VoucherType): Observable<boolean> {
const item = JSON.parse(JSON.stringify(this.bill)); const item = JSON.parse(JSON.stringify(this.bill));
const skus = item.kots.reduce( const skus = item.kots.reduce((p: number, k: Kot) => p + k.inventories.filter((i) => i.quantity !== 0).length, 0);
(p: number, k: Kot) => p + k.inventories.filter((i) => i.quantity !== 0).length,
0,
);
if (skus === 0) { if (skus === 0) {
return throwError(() => Error('Cannot print a blank Bill\nPlease add some products!')); return throwError(() => Error('Cannot print a blank Bill\nPlease add some products!'));
} }
@@ -380,9 +374,7 @@ export class BillService {
.filter((x) => x.isHappyHour) .filter((x) => x.isHappyHour)
.map((x) => ({ id: x.sku.id as string, quantity: x.quantity })); .map((x) => ({ id: x.sku.id as string, quantity: x.quantity }));
for (const item of happyHourItems) { for (const item of happyHourItems) {
const q = kot.inventories.find( const q = kot.inventories.find((x) => !x.isHappyHour && x.sku.id === item.id && x.quantity === item.quantity);
(x) => !x.isHappyHour && x.sku.id === item.id && x.quantity === item.quantity,
);
if (q === undefined) { if (q === undefined) {
return false; return false;
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import { MenuCategory } from './menu-category'; import { MenuCategory } from '../core/menu-category';
import { SaleCategory } from './sale-category'; import { SaleCategory } from '../core/sale-category';
import { Tax } from './tax'; import { Tax } from '../core/tax';
export class Product { export class Product {
id: string | undefined; id: string | undefined;
@@ -4,8 +4,8 @@ import { MatCardModule } from '@angular/material/card';
import { MatRippleModule } from '@angular/material/core'; import { MatRippleModule } from '@angular/material/core';
import { ActivatedRoute, RouterLink } from '@angular/router'; import { ActivatedRoute, RouterLink } from '@angular/router';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { BillService } from '../bill.service'; import { BillService } from '../bill.service';
import { Product } from '../product';
@Component({ @Component({
selector: 'app-products', selector: 'app-products',
@@ -1,7 +1,7 @@
import { inject } from '@angular/core'; import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router'; import { ResolveFn } from '@angular/router';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit'; import { Product } from '../../core/product';
import { ProductService } from '../../product/product.service'; import { ProductService } from '../../product/product.service';
export const productsResolver: ResolveFn<Product[]> = (route) => { export const productsResolver: ResolveFn<Product[]> = (route) => {
@@ -13,8 +13,8 @@ import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { SaleCategory } from '../../core/sale-category'; import { SaleCategory } from '../../core/sale-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { TemporalProductService } from '../temporal-product.service'; import { TemporalProductService } from '../temporal-product.service';
@@ -3,8 +3,8 @@ import { merge, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators'; import { map, tap } from 'rxjs/operators';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { SaleCategory } from '../../core/sale-category'; import { SaleCategory } from '../../core/sale-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
export class TemporalProductListDatasource extends DataSource<Product> { export class TemporalProductListDatasource extends DataSource<Product> {
public data: Product[][]; public data: Product[][];
@@ -12,8 +12,8 @@ import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { SaleCategory } from '../../core/sale-category'; import { SaleCategory } from '../../core/sale-category';
import { StockKeepingUnit as Product } from '../../core/stock-keeping-unit';
import { TemporalProductListDatasource } from './temporal-product-list-datasource'; import { TemporalProductListDatasource } from './temporal-product-list-datasource';
@Component({ @Component({