Feature: Made a Menu Engineering Report

This commit is contained in:
2023-03-18 23:20:11 +05:30
parent 40a357edc8
commit d39712a347
27 changed files with 825 additions and 11 deletions

View File

@ -35,6 +35,7 @@ from .routers.reports import (
bill_settlement_report, bill_settlement_report,
cashier_report, cashier_report,
discount_report, discount_report,
menu_engineering_report,
product_sale_report, product_sale_report,
product_updates_report, product_updates_report,
sale_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(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(sale_report.router, prefix="/api/sale-report", tags=["reports"])
app.include_router(tax_report.router, prefix="/api/tax-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(guest_book.router, prefix="/api/guest-book", tags=["guest-book"])
app.include_router(customer.router, prefix="/api/customers", tags=["guest-book"]) app.include_router(customer.router, prefix="/api/customers", tags=["guest-book"])

View File

@ -24,7 +24,6 @@ from ..db.base_class import reg
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory_modifier import InventoryModifier from .inventory_modifier import InventoryModifier
from .kot import Kot from .kot import Kot
from .product_version import ProductVersion
from .tax import Tax from .tax import Tax
@ -51,10 +50,8 @@ class Inventory:
sort_order: Mapped[int] = mapped_column("sort_order", Integer, nullable=False) sort_order: Mapped[int] = mapped_column("sort_order", Integer, nullable=False)
kot: Mapped["Kot"] = relationship(back_populates="inventories") kot: Mapped["Kot"] = relationship(back_populates="inventories")
tax: Mapped["Tax"] = relationship("Tax", foreign_keys=tax_id) tax: Mapped["Tax"] = relationship(back_populates="inventories")
product: Mapped["ProductVersion"] = relationship( product: Mapped["Product"] = relationship(back_populates="inventories")
secondary=Product.__table__, back_populates="inventories", viewonly=True # type: ignore[attr-defined]
)
modifiers: Mapped[List["InventoryModifier"]] = relationship(back_populates="inventory") modifiers: Mapped[List["InventoryModifier"]] = relationship(back_populates="inventory")

View File

@ -11,6 +11,7 @@ from ..db.base_class import reg
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory
from .modifier_category import ModifierCategory from .modifier_category import ModifierCategory
from .product_version import ProductVersion from .product_version import ProductVersion
@ -21,7 +22,8 @@ class Product:
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
"id", UUID(as_uuid=True), primary_key=True, server_default=text("gen_random_uuid()"), insert_default=uuid.uuid4 "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( modifier_categories: Mapped[List["ModifierCategory"]] = relationship(
"ModifierCategory", "ModifierCategory",
secondary=ModifierCategoryProduct.__table__, # type: ignore[attr-defined] secondary=ModifierCategoryProduct.__table__, # type: ignore[attr-defined]

View File

@ -2,7 +2,7 @@ import uuid
from datetime import date from datetime import date
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, Optional
from barker.models.product import Product from barker.models.product import Product
from sqlalchemy import ( from sqlalchemy import (
@ -25,7 +25,6 @@ from ..db.base_class import reg
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory
from .menu_category import MenuCategory from .menu_category import MenuCategory
from .sale_category import SaleCategory from .sale_category import SaleCategory
@ -66,9 +65,6 @@ class ProductVersion:
sale_category: Mapped["SaleCategory"] = relationship(back_populates="products") sale_category: Mapped["SaleCategory"] = relationship(back_populates="products")
product: Mapped["Product"] = relationship(back_populates="versions") 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__ = ( __table_args__ = (
postgresql.ExcludeConstraint( postgresql.ExcludeConstraint(

View File

@ -11,6 +11,7 @@ from ..db.base_class import reg
if TYPE_CHECKING: if TYPE_CHECKING:
from .inventory import Inventory
from .regime import Regime from .regime import Regime
from .sale_category import SaleCategory from .sale_category import SaleCategory
@ -28,6 +29,7 @@ class Tax:
is_fixture: Mapped[bool] = mapped_column("is_fixture", Boolean, nullable=False) is_fixture: Mapped[bool] = mapped_column("is_fixture", Boolean, nullable=False)
sale_categories: Mapped[List["SaleCategory"]] = relationship(back_populates="tax") sale_categories: Mapped[List["SaleCategory"]] = relationship(back_populates="tax")
inventories: Mapped[List["Inventory"]] = relationship(back_populates="tax")
regime: Mapped["Regime"] = relationship(back_populates="taxes") regime: Mapped["Regime"] = relationship(back_populates="taxes")
def __init__(self, name=None, rate=None, regime_id=None, is_fixture=False, id_=None): def __init__(self, name=None, rate=None, regime_id=None, is_fixture=False, id_=None):

View File

@ -10,6 +10,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture from ...db.session import SessionFuture
from ...models.inventory import Inventory from ...models.inventory import Inventory
from ...models.kot import Kot from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion from ...models.product_version import ProductVersion
from ...models.voucher import Voucher from ...models.voucher import Voucher
from ...models.voucher_type import VoucherType from ...models.voucher_type import VoucherType
@ -40,6 +41,7 @@ def beer_consumption(
.join(Voucher.kots) .join(Voucher.kots)
.join(Kot.inventories) .join(Kot.inventories)
.join(Inventory.product) .join(Inventory.product)
.join(Product.versions)
.where( .where(
day >= start_date, day >= start_date,
day <= finish_date, day <= finish_date,

View File

@ -11,6 +11,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture from ...db.session import SessionFuture
from ...models.inventory import Inventory from ...models.inventory import Inventory
from ...models.kot import Kot from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory from ...models.sale_category import SaleCategory
from ...models.voucher import Voucher 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(Voucher.kots)
.join(Kot.inventories) .join(Kot.inventories)
.join(Inventory.product) .join(Inventory.product)
.join(Product.versions)
.join(ProductVersion.sale_category) .join(ProductVersion.sale_category)
.where( .where(
Inventory.discount != 0, Inventory.discount != 0,

View File

@ -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

View File

@ -13,6 +13,7 @@ from ...db.session import SessionFuture
from ...models.inventory import Inventory from ...models.inventory import Inventory
from ...models.kot import Kot from ...models.kot import Kot
from ...models.menu_category import MenuCategory from ...models.menu_category import MenuCategory
from ...models.product import Product
from ...models.product_version import ProductVersion from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory from ...models.sale_category import SaleCategory
from ...models.voucher import Voucher from ...models.voucher import Voucher
@ -61,6 +62,7 @@ def product_sale_report(s: date, f: date, db: Session):
.join(Inventory.kot) .join(Inventory.kot)
.join(Kot.voucher) .join(Kot.voucher)
.join(Inventory.product) .join(Inventory.product)
.join(Product.versions)
.join(ProductVersion.sale_category) .join(ProductVersion.sale_category)
.join(ProductVersion.menu_category) .join(ProductVersion.menu_category)
.where( .where(

View File

@ -11,6 +11,7 @@ from ...core.security import get_current_active_user as get_user
from ...db.session import SessionFuture from ...db.session import SessionFuture
from ...models.inventory import Inventory from ...models.inventory import Inventory
from ...models.kot import Kot from ...models.kot import Kot
from ...models.product import Product
from ...models.product_version import ProductVersion from ...models.product_version import ProductVersion
from ...models.sale_category import SaleCategory from ...models.sale_category import SaleCategory
from ...models.settle_option import SettleOption 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(Inventory.kot)
.join(Kot.voucher) .join(Kot.voucher)
.join(Inventory.product) .join(Inventory.product)
.join(Product.versions)
.join(ProductVersion.sale_category) .join(ProductVersion.sale_category)
.where( .where(
Voucher.date >= start_date, Voucher.date >= start_date,

View File

@ -39,6 +39,11 @@ const routes: Routes = [
path: 'header-footer', path: 'header-footer',
loadChildren: () => import('./header-footer/header-footer.module').then((mod) => mod.HeaderFooterModule), 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', path: 'modifiers',
loadChildren: () => import('./modifiers/modifiers.module').then((mod) => mod.ModifiersModule), loadChildren: () => import('./modifiers/modifiers.module').then((mod) => mod.ModifiersModule),

View File

@ -81,6 +81,14 @@
> >
<h3 class="item-name">Discount Report</h3> <h3 class="item-name">Discount Report</h3>
</mat-card> </mat-card>
<mat-card
class="flex flex-col square-button mr-5, mb-5"
matRipple
*ngIf="auth.allowed('product-sale-report')"
[routerLink]="['/', 'menu-engineering-report']"
>
<h3 class="item-name">Menu Engineering Report</h3>
</mat-card>
</div> </div>
<div class="flex flex-row flex-wrap -mr-5 -mb-5"> <div class="flex flex-row flex-wrap -mr-5 -mb-5">
<mat-card <mat-card

View File

@ -0,0 +1,155 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { MenuEngineeringReportItem } from './menu-engineering-report-item';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class MenuEngineeringReportDataSource extends DataSource<MenuEngineeringReportItem> {
private filterValue = '';
constructor(
public data: MenuEngineeringReportItem[],
private filter: Observable<string>,
private paginator?: MatPaginator,
private sort?: MatSort,
) {
super();
this.filter = filter.pipe(
tap((x) => {
this.filterValue = x;
}),
);
}
connect(): Observable<MenuEngineeringReportItem[]> {
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
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:(?<sign>=|<=|>=|<|>)(?<amount>\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:(?<sign>=|<=|>=|<|>)(?<amount>\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;
}

View File

@ -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<MenuEngineeringReportItem>) {
this.id = '';
this.name = '';
this.price = 0;
this.average = 0;
this.saleCategory = '';
this.menuCategory = '';
this.quantity = 0;
this.amount = 0;
Object.assign(this, init);
}
}

View File

@ -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();
}));
});

View File

@ -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<MenuEngineeringReport> {
constructor(private ser: MenuEngineeringReportService) {}
resolve(route: ActivatedRouteSnapshot): Observable<MenuEngineeringReport> {
const startDate = route.queryParamMap.get('startDate') ?? null;
const finishDate = route.queryParamMap.get('finishDate') ?? null;
return this.ser.get(startDate, finishDate);
}
}

View File

@ -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();
});
});

View File

@ -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 {}

View File

@ -0,0 +1,8 @@
.right {
display: flex;
justify-content: flex-end;
}
.spacer {
flex: 1 1 auto;
}

View File

@ -0,0 +1,104 @@
<mat-card>
<mat-card-header>
<mat-card-title-group>
<mat-card-title>Menu Engineering Report</mat-card-title>
<span class="spacer"></span>
<button mat-icon-button (click)="exportCsv()">
<mat-icon>save_alt</mat-icon>
</button>
<button mat-icon-button (click)="print()">
<mat-icon>print</mat-icon>
</button>
</mat-card-title-group>
</mat-card-header>
<mat-card-content>
<form [formGroup]="form" class="flex flex-col">
<div class="flex flex-row justify-around content-start items-start sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-2/5 mr-5">
<mat-label>Start Date</mat-label>
<input
matInput
[matDatepicker]="startDate"
(focus)="startDate.open()"
formControlName="startDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-auto basis-2/5 mr-5">
<mat-label>Finish Date</mat-label>
<input
matInput
[matDatepicker]="finishDate"
(focus)="finishDate.open()"
formControlName="finishDate"
autocomplete="off"
/>
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1/5" color="primary" (click)="show()">Show</button>
</div>
<div class="flex flex-row justify-around content-start items-start">
<mat-form-field class="flex-auto">
<mat-label>Filter</mat-label>
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" />
<mat-hint
>n: Name, sc: Sale Category, mc: Menu Category, q:(=/&lt;=/&gt;=/&lt;/&gt;)Quantity,
a:(=/&lt;=/&gt;=/&lt;/&gt;)Amount</mat-hint
>
</mat-form-field>
</div>
</form>
<mat-table #table [dataSource]="dataSource" matSort>
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Price</mat-header-cell>
<mat-cell *matCellDef="let row" class="right"
>{{ row.average | number : '1.2-2' }} / {{ row.price | number : '1.2-2' }}</mat-cell
>
</ng-container>
<!-- Sale Category Column -->
<ng-container matColumnDef="saleCategory">
<mat-header-cell *matHeaderCellDef mat-sort-header>Sale Category</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.saleCategory }}</mat-cell>
</ng-container>
<!-- Menu Category Column -->
<ng-container matColumnDef="menuCategory">
<mat-header-cell *matHeaderCellDef mat-sort-header>Menu Category</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.menuCategory }}</mat-cell>
</ng-container>
<!-- Quantity Column -->
<ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Quantity</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.quantity | number : '1.2-2' }}</mat-cell>
</ng-container>
<!-- Amount Column -->
<ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | number : '1.2-2' }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
#paginator
[length]="dataSource.data.length"
[pageIndex]="0"
[pageSize]="500"
[pageSizeOptions]="[25, 50, 100, 250]"
>
</mat-paginator>
</mat-card-content>
</mat-card>

View File

@ -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<MenuEngineeringReportComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [MenuEngineeringReportComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MenuEngineeringReportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<string>;
dataSource: MenuEngineeringReportDataSource;
form: FormGroup<{
startDate: FormControl<Date>;
finishDate: FormControl<Date>;
filter: FormControl<string>;
}>;
/** 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<string>('', { 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);
}
}

View File

@ -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();
});
});

View File

@ -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 {}

View File

@ -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();
}));
});

View File

@ -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<MenuEngineeringReport> {
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<MenuEngineeringReport>(url, options)
.pipe(catchError(this.log.handleError(serviceName, 'get'))) as Observable<MenuEngineeringReport>;
}
print(startDate: string | null, finishDate: string | null): Observable<boolean> {
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<boolean>(printUrl, options)
.pipe(catchError(this.log.handleError(serviceName, 'print'))) as Observable<boolean>;
}
}

View File

@ -0,0 +1,14 @@
import { MenuEngineeringReportItem } from './menu-engineering-report-item';
export class MenuEngineeringReport {
startDate: string;
finishDate: string;
amounts: MenuEngineeringReportItem[];
public constructor(init?: Partial<MenuEngineeringReport>) {
this.startDate = '';
this.finishDate = '';
this.amounts = [];
Object.assign(this, init);
}
}