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 + + + + + + +
+
+ + Start Date + + + + + + Finish Date + + + + + +
+
+ + Filter + + n: Name, sc: Sale Category, mc: Menu Category, q:(=/<=/>=/</>)Quantity, + a:(=/<=/>=/</>)Amount + +
+
+ + + + 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); + } +}