diff --git a/barker/barker/main.py b/barker/barker/main.py
index a4e1b90..1619084 100644
--- a/barker/barker/main.py
+++ b/barker/barker/main.py
@@ -35,6 +35,7 @@ from .routers.reports import (
bill_settlement_report,
cashier_report,
discount_report,
+ menu_engineering_report,
product_sale_report,
product_updates_report,
sale_report,
@@ -101,6 +102,7 @@ app.include_router(discount_report.router, prefix="/api/discount-report", tags=[
app.include_router(product_sale_report.router, prefix="/api/product-sale-report", tags=["reports"])
app.include_router(sale_report.router, prefix="/api/sale-report", tags=["reports"])
app.include_router(tax_report.router, prefix="/api/tax-report", tags=["reports"])
+app.include_router(menu_engineering_report.router, prefix="/api/menu-engineering-report", tags=["reports"])
app.include_router(guest_book.router, prefix="/api/guest-book", tags=["guest-book"])
app.include_router(customer.router, prefix="/api/customers", tags=["guest-book"])
diff --git a/barker/barker/models/inventory.py b/barker/barker/models/inventory.py
index 84f239c..ae88faa 100644
--- a/barker/barker/models/inventory.py
+++ b/barker/barker/models/inventory.py
@@ -24,7 +24,6 @@ from ..db.base_class import reg
if TYPE_CHECKING:
from .inventory_modifier import InventoryModifier
from .kot import Kot
- from .product_version import ProductVersion
from .tax import Tax
@@ -51,10 +50,8 @@ class Inventory:
sort_order: Mapped[int] = mapped_column("sort_order", Integer, nullable=False)
kot: Mapped["Kot"] = relationship(back_populates="inventories")
- tax: Mapped["Tax"] = relationship("Tax", foreign_keys=tax_id)
- product: Mapped["ProductVersion"] = relationship(
- secondary=Product.__table__, back_populates="inventories", viewonly=True # type: ignore[attr-defined]
- )
+ tax: Mapped["Tax"] = relationship(back_populates="inventories")
+ product: Mapped["Product"] = relationship(back_populates="inventories")
modifiers: Mapped[List["InventoryModifier"]] = relationship(back_populates="inventory")
diff --git a/barker/barker/models/product.py b/barker/barker/models/product.py
index 44b415b..d86b08a 100644
--- a/barker/barker/models/product.py
+++ b/barker/barker/models/product.py
@@ -11,6 +11,7 @@ from ..db.base_class import reg
if TYPE_CHECKING:
+ from .inventory import Inventory
from .modifier_category import ModifierCategory
from .product_version import ProductVersion
@@ -21,7 +22,8 @@ class Product:
id: Mapped[uuid.UUID] = mapped_column(
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), insert_default=uuid.uuid4
)
- versions: Mapped[List["ProductVersion"]] = relationship("ProductVersion", back_populates="product")
+ versions: Mapped[List["ProductVersion"]] = relationship(back_populates="product")
+ inventories: Mapped[List["Inventory"]] = relationship(back_populates="product")
modifier_categories: Mapped[List["ModifierCategory"]] = relationship(
"ModifierCategory",
secondary=ModifierCategoryProduct.__table__, # type: ignore[attr-defined]
diff --git a/barker/barker/models/product_version.py b/barker/barker/models/product_version.py
index 1689989..2b47a12 100644
--- a/barker/barker/models/product_version.py
+++ b/barker/barker/models/product_version.py
@@ -2,7 +2,7 @@ import uuid
from datetime import date
from decimal import Decimal
-from typing import TYPE_CHECKING, List, Optional
+from typing import TYPE_CHECKING, Optional
from barker.models.product import Product
from sqlalchemy import (
@@ -25,7 +25,6 @@ from ..db.base_class import reg
if TYPE_CHECKING:
- from .inventory import Inventory
from .menu_category import MenuCategory
from .sale_category import SaleCategory
@@ -66,9 +65,6 @@ class ProductVersion:
sale_category: Mapped["SaleCategory"] = relationship(back_populates="products")
product: Mapped["Product"] = relationship(back_populates="versions")
- inventories: Mapped[List["Inventory"]] = relationship(
- secondary=Product.__table__, back_populates="product", viewonly=True # type: ignore[attr-defined]
- )
__table_args__ = (
postgresql.ExcludeConstraint(
diff --git a/barker/barker/models/tax.py b/barker/barker/models/tax.py
index 8713757..d6b3aaf 100644
--- a/barker/barker/models/tax.py
+++ b/barker/barker/models/tax.py
@@ -11,6 +11,7 @@ from ..db.base_class import reg
if TYPE_CHECKING:
+ from .inventory import Inventory
from .regime import Regime
from .sale_category import SaleCategory
@@ -28,6 +29,7 @@ class Tax:
is_fixture: Mapped[bool] = mapped_column("is_fixture", Boolean, nullable=False)
sale_categories: Mapped[List["SaleCategory"]] = relationship(back_populates="tax")
+ inventories: Mapped[List["Inventory"]] = relationship(back_populates="tax")
regime: Mapped["Regime"] = relationship(back_populates="taxes")
def __init__(self, name=None, rate=None, regime_id=None, is_fixture=False, id_=None):
diff --git a/barker/barker/routers/reports/beer_sale_report.py b/barker/barker/routers/reports/beer_sale_report.py
index 0a719e4..487af54 100644
--- a/barker/barker/routers/reports/beer_sale_report.py
+++ b/barker/barker/routers/reports/beer_sale_report.py
@@ -10,6 +10,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.kot import Kot
+from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType
@@ -40,6 +41,7 @@ def beer_consumption(
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.product)
+ .join(Product.versions)
.where(
day >= start_date,
day <= finish_date,
diff --git a/barker/barker/routers/reports/discount_report.py b/barker/barker/routers/reports/discount_report.py
index 8106e46..a273260 100644
--- a/barker/barker/routers/reports/discount_report.py
+++ b/barker/barker/routers/reports/discount_report.py
@@ -11,6 +11,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.kot import Kot
+from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.voucher import Voucher
@@ -54,6 +55,7 @@ def get_discount_report(s: date, f: date, db: Session) -> list[DiscountReportIte
.join(Voucher.kots)
.join(Kot.inventories)
.join(Inventory.product)
+ .join(Product.versions)
.join(ProductVersion.sale_category)
.where(
Inventory.discount != 0,
diff --git a/barker/barker/routers/reports/menu_engineering_report.py b/barker/barker/routers/reports/menu_engineering_report.py
new file mode 100644
index 0000000..25a434e
--- /dev/null
+++ b/barker/barker/routers/reports/menu_engineering_report.py
@@ -0,0 +1,136 @@
+import uuid
+
+from datetime import date, datetime, time, timedelta
+from typing import Any
+
+from fastapi import APIRouter, Cookie, Depends, Security
+from sqlalchemy import func, nulls_last, or_, select
+from sqlalchemy.orm import Session
+
+from ...core.config import settings
+from ...core.security import get_current_active_user as get_user
+from ...db.session import SessionFuture
+from ...models.inventory import Inventory
+from ...models.kot import Kot
+from ...models.menu_category import MenuCategory
+from ...models.product import Product
+from ...models.product_version import ProductVersion
+from ...models.sale_category import SaleCategory
+from ...models.voucher import Voucher
+from ...models.voucher_type import VoucherType
+from ...printing.product_sale_report import print_product_sale_report
+from ...schemas.user_token import UserToken
+from . import check_audit_permission, report_finish_date, report_start_date
+
+
+router = APIRouter()
+
+
+@router.get("")
+def menu_engineering_report_view(
+ start_date: date = Depends(report_start_date),
+ finish_date: date = Depends(report_finish_date),
+ user: UserToken = Security(get_user, scopes=["product-sale-report"]),
+):
+ check_audit_permission(start_date, user.permissions)
+ with SessionFuture() as db:
+ return {
+ "startDate": start_date.strftime("%d-%b-%Y"),
+ "finishDate": finish_date.strftime("%d-%b-%Y"),
+ "amounts": menu_engineering_report(start_date, finish_date, db),
+ }
+
+
+def menu_engineering_report(s: date, f: date, db: Session):
+ start_date = datetime.combine(s, time()) + timedelta(
+ minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES
+ )
+ finish_date = datetime.combine(f, time()) + timedelta(
+ days=1, minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES
+ )
+
+ day = func.date_trunc("day", Voucher.date - timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES)).label("day")
+ q = (
+ select(
+ SaleCategory.name,
+ MenuCategory.name,
+ ProductVersion.id,
+ ProductVersion.full_name,
+ ProductVersion.price,
+ func.sum(Inventory.quantity),
+ func.sum(Inventory.net),
+ )
+ .join(ProductVersion.sale_category)
+ .join(ProductVersion.menu_category)
+ .join(ProductVersion.product)
+ .join(Product.inventories, isouter=True)
+ .join(Inventory.kot, isouter=True)
+ .join(Kot.voucher, isouter=True)
+ .where(
+ or_(
+ Voucher.date == None, # noqa: E711
+ Voucher.date >= start_date,
+ ),
+ or_(
+ Voucher.date == None, # noqa: E711
+ Voucher.date <= finish_date,
+ ),
+ or_(
+ Voucher.voucher_type == None, # noqa: E711
+ Voucher.voucher_type == VoucherType.REGULAR_BILL,
+ ),
+ or_(
+ ProductVersion.valid_from == None, # noqa: E711
+ ProductVersion.valid_from <= day,
+ Voucher.date == None, # noqa: E711
+ ),
+ or_(
+ ProductVersion.valid_till == None, # noqa: E711
+ ProductVersion.valid_till >= day,
+ Voucher.date == None, # noqa: E711
+ ),
+ )
+ .group_by(
+ SaleCategory.name,
+ MenuCategory.name,
+ ProductVersion.id,
+ ProductVersion.full_name,
+ )
+ .order_by(SaleCategory.name, nulls_last(func.sum(Inventory.net).desc()))
+ )
+ print(q)
+ list_ = db.execute(q).all()
+ info: list[Any] = []
+ for sc, mc, id_, name, price, quantity, amount in list_:
+ info.append(
+ {
+ "id": id_,
+ "name": name,
+ "price": price,
+ "average": round(amount / quantity) if amount and quantity else price,
+ "saleCategory": sc,
+ "menuCategory": mc,
+ "quantity": quantity,
+ "amount": amount,
+ }
+ )
+ return info
+
+
+@router.get("/print", response_model=bool)
+def print_report(
+ start_date: date = Depends(report_start_date),
+ finish_date: date = Depends(report_finish_date),
+ device_id: uuid.UUID = Cookie(None),
+ user: UserToken = Security(get_user, scopes=["product-sale-report"]),
+) -> bool:
+ check_audit_permission(start_date, user.permissions)
+ with SessionFuture() as db:
+ report = {
+ "userName": user.name,
+ "startDate": start_date.strftime("%d-%b-%Y"),
+ "finishDate": finish_date.strftime("%d-%b-%Y"),
+ "amounts": menu_engineering_report(start_date, finish_date, db),
+ }
+ print_product_sale_report(report, device_id, db)
+ return True
diff --git a/barker/barker/routers/reports/product_sale_report.py b/barker/barker/routers/reports/product_sale_report.py
index f4e1741..dd6c1ae 100644
--- a/barker/barker/routers/reports/product_sale_report.py
+++ b/barker/barker/routers/reports/product_sale_report.py
@@ -13,6 +13,7 @@ from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.kot import Kot
from ...models.menu_category import MenuCategory
+from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.voucher import Voucher
@@ -61,6 +62,7 @@ def product_sale_report(s: date, f: date, db: Session):
.join(Inventory.kot)
.join(Kot.voucher)
.join(Inventory.product)
+ .join(Product.versions)
.join(ProductVersion.sale_category)
.join(ProductVersion.menu_category)
.where(
diff --git a/barker/barker/routers/reports/sale_report.py b/barker/barker/routers/reports/sale_report.py
index 5cec538..647c5ee 100644
--- a/barker/barker/routers/reports/sale_report.py
+++ b/barker/barker/routers/reports/sale_report.py
@@ -11,6 +11,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture
from ...models.inventory import Inventory
from ...models.kot import Kot
+from ...models.product import Product
from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory
from ...models.settle_option import SettleOption
@@ -64,6 +65,7 @@ def get_sale(s: date, f: date, db: Session) -> list[SaleReportItem]:
.join(Inventory.kot)
.join(Kot.voucher)
.join(Inventory.product)
+ .join(Product.versions)
.join(ProductVersion.sale_category)
.where(
Voucher.date >= start_date,
diff --git a/bookie/src/app/app-routing.module.ts b/bookie/src/app/app-routing.module.ts
index 0498681..52c2f92 100644
--- a/bookie/src/app/app-routing.module.ts
+++ b/bookie/src/app/app-routing.module.ts
@@ -39,6 +39,11 @@ const routes: Routes = [
path: 'header-footer',
loadChildren: () => import('./header-footer/header-footer.module').then((mod) => mod.HeaderFooterModule),
},
+ {
+ path: 'menu-engineering-report',
+ loadChildren: () =>
+ import('./menu-engineering-report/menu-engineering-report.module').then((mod) => mod.MenuEngineeringReportModule),
+ },
{
path: 'modifiers',
loadChildren: () => import('./modifiers/modifiers.module').then((mod) => mod.ModifiersModule),
diff --git a/bookie/src/app/home/home.component.html b/bookie/src/app/home/home.component.html
index e7e009e..329b6e5 100644
--- a/bookie/src/app/home/home.component.html
+++ b/bookie/src/app/home/home.component.html
@@ -81,6 +81,14 @@
>
Discount Report
+
+ Menu Engineering Report
+
+ (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+export class MenuEngineeringReportDataSource extends DataSource {
+ private filterValue = '';
+ constructor(
+ public data: MenuEngineeringReportItem[],
+ private filter: Observable,
+ private paginator?: MatPaginator,
+ private sort?: MatSort,
+ ) {
+ super();
+ this.filter = filter.pipe(
+ tap((x) => {
+ this.filterValue = x;
+ }),
+ );
+ }
+
+ connect(): Observable {
+ const dataMutations: (EventEmitter | EventEmitter)[] = [];
+ if (this.paginator) {
+ dataMutations.push((this.paginator as MatPaginator).page);
+ }
+ if (this.sort) {
+ dataMutations.push((this.sort as MatSort).sortChange);
+ }
+ return merge(observableOf(this.data), this.filter, ...dataMutations)
+ .pipe(
+ map(() => this.getFilteredData([...this.data])),
+ tap((x: MenuEngineeringReportItem[]) => {
+ if (this.paginator) {
+ this.paginator.length = x.length;
+ }
+ }),
+ )
+ .pipe(map((x: MenuEngineeringReportItem[]) => this.getPagedData(this.getSortedData(x))));
+ }
+
+ disconnect() {}
+
+ private getFilteredData(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
+ return this.filterValue.split(' ').reduce(
+ (p: MenuEngineeringReportItem[], c: string) =>
+ p.filter((x) => {
+ if (c.startsWith('n:')) {
+ return x.name.toLowerCase().indexOf(c.substring(2)) !== -1;
+ }
+ if (c.startsWith('sc:')) {
+ return x.saleCategory.toLowerCase().indexOf(c.substring(3)) !== -1;
+ }
+ if (c.startsWith('mc:')) {
+ return x.menuCategory.toLowerCase().indexOf(c.substring(3)) !== -1;
+ }
+ if (c.startsWith('q:')) {
+ const result = c.match(/^q:(?=|<=|>=|<|>)(?\d*)$/);
+ if (result && result.groups) {
+ return math_it_up(result.groups['sign'])(x.quantity, +result.groups['amount'] ?? '');
+ }
+ }
+ if (c.startsWith('a:')) {
+ const result = c.match(/^a:(?=|<=|>=|<|>)(?\d*)$/);
+ if (result && result.groups) {
+ return math_it_up(result.groups['sign'])(x.amount, +result.groups['amount'] ?? '');
+ }
+ }
+ const itemString = `${x.name} ${x.saleCategory} ${x.menuCategory}`.toLowerCase();
+ return itemString.indexOf(c) !== -1;
+ }),
+ Object.assign([], data),
+ );
+ }
+
+ private getPagedData(data: MenuEngineeringReportItem[]) {
+ if (this.paginator === undefined) {
+ return data;
+ }
+ const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
+ return data.splice(startIndex, this.paginator.pageSize);
+ }
+
+ private getSortedData(data: MenuEngineeringReportItem[]): MenuEngineeringReportItem[] {
+ if (this.sort === undefined) {
+ return data;
+ }
+ if (!this.sort.active || this.sort.direction === '') {
+ return data;
+ }
+
+ const sort = this.sort as MatSort;
+ return data.sort((a, b) => {
+ const isAsc = sort.direction === 'asc';
+ switch (sort.active) {
+ case 'name':
+ return compare(a.name, b.name, isAsc);
+ case 'price':
+ return compare(a.price, b.price, isAsc);
+ case 'saleCategory':
+ return compare(a.saleCategory, b.saleCategory, isAsc);
+ case 'menuCategory':
+ return compare(a.menuCategory, b.menuCategory, isAsc);
+ case 'quantity':
+ return compare(a.quantity, b.quantity, isAsc);
+ case 'amount':
+ return compare(a.amount, b.amount, isAsc);
+ default:
+ return 0;
+ }
+ });
+ }
+}
+
+function eq(x: number, y: number) {
+ return x > y;
+}
+function gt(x: number, y: number) {
+ return x > y;
+}
+function lt(x: number, y: number) {
+ return x < y;
+}
+function gte(x: number, y: number) {
+ return x >= y;
+}
+function lte(x: number, y: number) {
+ return x <= y;
+}
+
+function math_it_up(sign: string) {
+ if (sign == '=') {
+ return eq;
+ }
+ if (sign == '>') {
+ return gt;
+ }
+ if (sign == '<') {
+ return lt;
+ }
+ if (sign == '>=') {
+ return gte;
+ }
+ if (sign == '<=') {
+ return lte;
+ }
+ return eq;
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report-item.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report-item.ts
new file mode 100644
index 0000000..998df09
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report-item.ts
@@ -0,0 +1,22 @@
+export class MenuEngineeringReportItem {
+ id: string;
+ name: string;
+ price: number;
+ average: number;
+ saleCategory: string;
+ menuCategory: string;
+ quantity: number;
+ amount: number;
+
+ public constructor(init?: Partial) {
+ this.id = '';
+ this.name = '';
+ this.price = 0;
+ this.average = 0;
+ this.saleCategory = '';
+ this.menuCategory = '';
+ this.quantity = 0;
+ this.amount = 0;
+ Object.assign(this, init);
+ }
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.spec.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.spec.ts
new file mode 100644
index 0000000..66d2c2d
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.spec.ts
@@ -0,0 +1,15 @@
+import { inject, TestBed } from '@angular/core/testing';
+
+import { MenuEngineeringReportResolver } from './menu-engineering-report-resolver.service';
+
+describe('MenuEngineeringReportResolver', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [MenuEngineeringReportResolver],
+ });
+ });
+
+ it('should be created', inject([MenuEngineeringReportResolver], (service: MenuEngineeringReportResolver) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.ts
new file mode 100644
index 0000000..8c790cb
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report-resolver.service.ts
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
+import { Observable } from 'rxjs';
+
+import { MenuEngineeringReport } from './menu-engineering-report';
+import { MenuEngineeringReportService } from './menu-engineering-report.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class MenuEngineeringReportResolver implements Resolve {
+ constructor(private ser: MenuEngineeringReportService) {}
+
+ resolve(route: ActivatedRouteSnapshot): Observable {
+ const startDate = route.queryParamMap.get('startDate') ?? null;
+ const finishDate = route.queryParamMap.get('finishDate') ?? null;
+ return this.ser.get(startDate, finishDate);
+ }
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.spec.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.spec.ts
new file mode 100644
index 0000000..93b8e28
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.spec.ts
@@ -0,0 +1,13 @@
+import { MenuEngineeringReportRoutingModule } from './menu-engineering-report-routing.module';
+
+describe('MenuEngineeringReportRoutingModule', () => {
+ let menuEngineeringReportRoutingModule: MenuEngineeringReportRoutingModule;
+
+ beforeEach(() => {
+ menuEngineeringReportRoutingModule = new MenuEngineeringReportRoutingModule();
+ });
+
+ it('should create an instance', () => {
+ expect(menuEngineeringReportRoutingModule).toBeTruthy();
+ });
+});
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.ts
new file mode 100644
index 0000000..1a72a81
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report-routing.module.ts
@@ -0,0 +1,30 @@
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+
+import { AuthGuard } from '../auth/auth-guard.service';
+
+import { MenuEngineeringReportResolver } from './menu-engineering-report-resolver.service';
+import { MenuEngineeringReportComponent } from './menu-engineering-report.component';
+
+const menuEngineeringReportRoutes: Routes = [
+ {
+ path: '',
+ component: MenuEngineeringReportComponent,
+ canActivate: [AuthGuard],
+ data: {
+ permission: 'Product Sale Report',
+ },
+ resolve: {
+ info: MenuEngineeringReportResolver,
+ },
+ runGuardsAndResolvers: 'always',
+ },
+];
+
+@NgModule({
+ imports: [CommonModule, RouterModule.forChild(menuEngineeringReportRoutes)],
+ exports: [RouterModule],
+ providers: [MenuEngineeringReportResolver],
+})
+export class MenuEngineeringReportRoutingModule {}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.css b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.css
new file mode 100644
index 0000000..747e946
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.css
@@ -0,0 +1,8 @@
+.right {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.spacer {
+ flex: 1 1 auto;
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html
new file mode 100644
index 0000000..51ee0e0
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html
@@ -0,0 +1,104 @@
+
+
+
+ Menu Engineering Report
+
+
+
+
+
+
+
+
+
+
+ Name
+ {{ row.name }}
+
+
+
+
+ Price
+ {{ row.average | number : '1.2-2' }} / {{ row.price | number : '1.2-2' }}
+
+
+
+
+ Sale Category
+ {{ row.saleCategory }}
+
+
+
+
+ Menu Category
+ {{ row.menuCategory }}
+
+
+
+
+ Quantity
+ {{ row.quantity | number : '1.2-2' }}
+
+
+
+
+ Amount
+ {{ row.amount | number : '1.2-2' }}
+
+
+
+
+
+
+
+
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.spec.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.spec.ts
new file mode 100644
index 0000000..709d5b3
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.spec.ts
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
+
+import { MenuEngineeringReportComponent } from './menu-engineering-report.component';
+
+describe('MenuEngineeringReportComponent', () => {
+ let component: MenuEngineeringReportComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [MenuEngineeringReportComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MenuEngineeringReportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.ts
new file mode 100644
index 0000000..df5e8f7
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.ts
@@ -0,0 +1,119 @@
+import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
+import { FormControl, FormGroup } from '@angular/forms';
+import { MatPaginator } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { ActivatedRoute, Router } from '@angular/router';
+import * as moment from 'moment';
+import { Observable } from 'rxjs';
+import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
+
+import { ToasterService } from '../core/toaster.service';
+import { ToCsvService } from '../shared/to-csv.service';
+
+import { MenuEngineeringReport } from './menu-engineering-report';
+import { MenuEngineeringReportDataSource } from './menu-engineering-report-datasource';
+import { MenuEngineeringReportService } from './menu-engineering-report.service';
+
+@Component({
+ selector: 'app-menu-engineering-report',
+ templateUrl: './menu-engineering-report.component.html',
+ styleUrls: ['./menu-engineering-report.component.css'],
+})
+export class MenuEngineeringReportComponent implements OnInit {
+ @ViewChild('filterElement', { static: true }) filterElement?: ElementRef;
+ @ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator;
+ @ViewChild(MatSort, { static: true }) sort?: MatSort;
+ info: MenuEngineeringReport = new MenuEngineeringReport();
+ filter: Observable;
+ dataSource: MenuEngineeringReportDataSource;
+ form: FormGroup<{
+ startDate: FormControl;
+ finishDate: FormControl;
+ filter: FormControl;
+ }>;
+
+ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
+ displayedColumns = ['name', 'price', 'saleCategory', 'menuCategory', 'quantity', 'amount'];
+
+ constructor(
+ private route: ActivatedRoute,
+ private router: Router,
+ private toCsv: ToCsvService,
+ private toaster: ToasterService,
+ private ser: MenuEngineeringReportService,
+ ) {
+ // Create form
+ this.form = new FormGroup({
+ startDate: new FormControl(new Date(), { nonNullable: true }),
+ finishDate: new FormControl(new Date(), { nonNullable: true }),
+ filter: new FormControl('', { nonNullable: true }),
+ });
+ // Listen to Filter Change
+ this.filter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged());
+ this.dataSource = new MenuEngineeringReportDataSource(this.info.amounts, this.filter);
+ }
+
+ ngOnInit() {
+ this.route.data.subscribe((value) => {
+ const data = value as { info: MenuEngineeringReport };
+ this.info = data.info;
+ this.form.setValue({
+ startDate: moment(this.info.startDate, 'DD-MMM-YYYY').toDate(),
+ finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY').toDate(),
+ filter: '',
+ });
+ this.dataSource = new MenuEngineeringReportDataSource(this.info.amounts, this.filter, this.paginator, this.sort);
+ });
+ }
+
+ show() {
+ const info = this.getInfo();
+ this.router.navigate(['menu-engineering-report'], {
+ queryParams: {
+ startDate: info.startDate,
+ finishDate: info.finishDate,
+ },
+ });
+ }
+
+ getInfo(): MenuEngineeringReport {
+ const formModel = this.form.value;
+
+ return new MenuEngineeringReport({
+ startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
+ finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
+ });
+ }
+
+ print() {
+ this.ser.print(this.info.startDate, this.info.finishDate).subscribe(
+ () => {
+ this.toaster.show('', 'Successfully Printed');
+ },
+ (error) => {
+ this.toaster.show('Error', error);
+ },
+ );
+ }
+
+ exportCsv() {
+ const headers = {
+ Name: 'name',
+ Price: 'price',
+ Average: 'average',
+ 'Sale Category': 'saleCategory',
+ 'Menu Category': 'menuCategory',
+ Quantity: 'quantity',
+ Amount: 'amount',
+ };
+ const csvData = new Blob([this.toCsv.toCsv(headers, this.dataSource.data)], {
+ type: 'text/csv;charset=utf-8;',
+ });
+ const link = document.createElement('a');
+ link.href = window.URL.createObjectURL(csvData);
+ link.setAttribute('download', 'menu-engineering-report.csv');
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.module.spec.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.module.spec.ts
new file mode 100644
index 0000000..9efc1d7
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.module.spec.ts
@@ -0,0 +1,13 @@
+import { MenuEngineeringReportModule } from './menu-engineering-report.module';
+
+describe('MenuEngineeringReportModule', () => {
+ let menuEngineeringReportModule: MenuEngineeringReportModule;
+
+ beforeEach(() => {
+ menuEngineeringReportModule = new MenuEngineeringReportModule();
+ });
+
+ it('should create an instance', () => {
+ expect(menuEngineeringReportModule).toBeTruthy();
+ });
+});
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.module.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.module.ts
new file mode 100644
index 0000000..84896ff
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.module.ts
@@ -0,0 +1,62 @@
+import { A11yModule } from '@angular/cdk/a11y';
+import { CdkTableModule } from '@angular/cdk/table';
+import { CommonModule } from '@angular/common';
+import { NgModule } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatButtonModule } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatNativeDateModule } from '@angular/material/core';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatIconModule } from '@angular/material/icon';
+import { MatInputModule } from '@angular/material/input';
+import { MatPaginatorModule } from '@angular/material/paginator';
+import { MatSortModule } from '@angular/material/sort';
+import { MatTableModule } from '@angular/material/table';
+import { MomentDateAdapter } from '@angular/material-moment-adapter';
+
+import { SharedModule } from '../shared/shared.module';
+
+import { MenuEngineeringReportRoutingModule } from './menu-engineering-report-routing.module';
+import { MenuEngineeringReportComponent } from './menu-engineering-report.component';
+
+export const MY_FORMATS = {
+ parse: {
+ dateInput: 'DD-MMM-YYYY',
+ },
+ display: {
+ dateInput: 'DD-MMM-YYYY',
+ monthYearLabel: 'MMM YYYY',
+ dateA11yLabel: 'DD-MMM-YYYY',
+ monthYearA11yLabel: 'MMM YYYY',
+ },
+};
+
+@NgModule({
+ imports: [
+ A11yModule,
+ CommonModule,
+ CdkTableModule,
+ MatAutocompleteModule,
+ MatButtonModule,
+ MatCardModule,
+ MatDatepickerModule,
+ MatFormFieldModule,
+ MatIconModule,
+ MatInputModule,
+ MatNativeDateModule,
+ MatPaginatorModule,
+ MatSortModule,
+ MatTableModule,
+ ReactiveFormsModule,
+ SharedModule,
+ MenuEngineeringReportRoutingModule,
+ ],
+ declarations: [MenuEngineeringReportComponent],
+ providers: [
+ { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
+ { provide: MAT_DATE_FORMATS, useValue: MY_FORMATS },
+ ],
+})
+export class MenuEngineeringReportModule {}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.service.spec.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.service.spec.ts
new file mode 100644
index 0000000..0478864
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.service.spec.ts
@@ -0,0 +1,15 @@
+import { inject, TestBed } from '@angular/core/testing';
+
+import { MenuEngineeringReportService } from './menu-engineering-report.service';
+
+describe('MenuEngineeringReportService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [MenuEngineeringReportService],
+ });
+ });
+
+ it('should be created', inject([MenuEngineeringReportService], (service: MenuEngineeringReportService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.service.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.service.ts
new file mode 100644
index 0000000..048dffc
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.service.ts
@@ -0,0 +1,45 @@
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+
+import { ErrorLoggerService } from '../core/error-logger.service';
+
+import { MenuEngineeringReport } from './menu-engineering-report';
+
+const url = '/api/menu-engineering-report';
+const serviceName = 'MenuEngineeringReportService';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class MenuEngineeringReportService {
+ constructor(private http: HttpClient, private log: ErrorLoggerService) {}
+
+ get(startDate: string | null, finishDate: string | null): Observable {
+ const options = { params: new HttpParams() };
+ if (startDate !== null) {
+ options.params = options.params.set('s', startDate);
+ }
+ if (finishDate !== null) {
+ options.params = options.params.set('f', finishDate);
+ }
+ return this.http
+ .get(url, options)
+ .pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable;
+ }
+
+ print(startDate: string | null, finishDate: string | null): Observable {
+ const printUrl = `${url}/print`;
+ const options = { params: new HttpParams() };
+ if (startDate !== null) {
+ options.params = options.params.set('s', startDate);
+ }
+ if (finishDate !== null) {
+ options.params = options.params.set('f', finishDate);
+ }
+ return this.http
+ .get(printUrl, options)
+ .pipe(catchError(this.log.handleError(serviceName, 'print'))) as Observable;
+ }
+}
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.ts b/bookie/src/app/menu-engineering-report/menu-engineering-report.ts
new file mode 100644
index 0000000..aa47375
--- /dev/null
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.ts
@@ -0,0 +1,14 @@
+import { MenuEngineeringReportItem } from './menu-engineering-report-item';
+
+export class MenuEngineeringReport {
+ startDate: string;
+ finishDate: string;
+ amounts: MenuEngineeringReportItem[];
+
+ public constructor(init?: Partial) {
+ this.startDate = '';
+ this.finishDate = '';
+ this.amounts = [];
+ Object.assign(this, init);
+ }
+}