Feature: Download nutritional information and store description and allergen information in products

This commit is contained in:
Amritanshu Agrawal 2023-12-28 13:52:35 +05:30
parent cd6a5e129f
commit 88f0c35b4d
11 changed files with 434 additions and 3 deletions

@ -0,0 +1,27 @@
"""allergen
Revision ID: 66abfc21db73
Revises: 2438cd581f00
Create Date: 2023-12-28 12:45:01.275322
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "66abfc21db73"
down_revision = "2438cd581f00"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("products", sa.Column("allergen", sa.Text(), server_default="", nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("products", "allergen")
# ### end Alembic commands ###

@ -39,6 +39,8 @@ class Product:
product_group: Mapped["ProductGroup"] = relationship("ProductGroup", back_populates="products") product_group: Mapped["ProductGroup"] = relationship("ProductGroup", back_populates="products")
account: Mapped["Account"] = relationship("Account", back_populates="products") account: Mapped["Account"] = relationship("Account", back_populates="products")
allergen: Mapped[str] = mapped_column(Text, nullable=False)
protein: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False) protein: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
carbohydrate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False) carbohydrate: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
total_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False) total_sugar: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=5), nullable=False)
@ -56,12 +58,14 @@ class Product:
def __init__( def __init__(
self, self,
name: str, name: str,
description: str | None,
fraction_units: str, fraction_units: str,
product_group_id: uuid.UUID, product_group_id: uuid.UUID,
account_id: uuid.UUID, account_id: uuid.UUID,
is_active: bool, is_active: bool,
is_purchased: bool, is_purchased: bool,
is_sold: bool, is_sold: bool,
allergen: str = "",
protein: Decimal = 0, protein: Decimal = 0,
carbohydrate: Decimal = 0, carbohydrate: Decimal = 0,
total_sugar: Decimal = 0, total_sugar: Decimal = 0,
@ -82,6 +86,7 @@ class Product:
if code is not None: if code is not None:
self.code = code self.code = code
self.name = name self.name = name
self.description = description
self.fraction_units = fraction_units self.fraction_units = fraction_units
self.product_group_id = product_group_id self.product_group_id = product_group_id
self.account_id = account_id self.account_id = account_id
@ -89,6 +94,7 @@ class Product:
self.is_purchased = is_purchased self.is_purchased = is_purchased
self.is_sold = is_sold self.is_sold = is_sold
self.allergen = allergen
self.protein = protein self.protein = protein
self.carbohydrate = carbohydrate self.carbohydrate = carbohydrate
self.total_sugar = total_sugar self.total_sugar = total_sugar

@ -0,0 +1,249 @@
import uuid
from ..schemas.nutritional_information import NutritionalInformation
from ..models.product_group import ProductGroup
from fastapi import HTTPException, status
from sqlalchemy import distinct, or_, select, update
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from ..models.product import Product
from ..models.recipe import Recipe
from ..models.recipe_item import RecipeItem
from ..models.stock_keeping_unit import StockKeepingUnit
def calculate_nutrition(db: Session):
try:
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(Product.product_group)
.where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
)
.scalars()
.all()
)
db.flush()
while len(products) > 0:
calculate(products, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
def calculate(products: set[uuid.UUID], db: Session) -> None:
sq = select(RecipeItem.recipe_id).where(RecipeItem.product_id.in_(products))
recipes = (
db.execute(
select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(products), Recipe.id.notin_(sq))
)
.scalars()
.all()
)
for recipe in recipes:
protein = sum(i.quantity * i.product.protein for i in recipe.items) / recipe.sku.fraction
carbohydrate = sum(i.quantity * i.product.carbohydrate for i in recipe.items) / recipe.sku.fraction
total_sugar = sum(i.quantity * i.product.total_sugar for i in recipe.items) / recipe.sku.fraction
added_sugar = sum(i.quantity * i.product.added_sugar for i in recipe.items) / recipe.sku.fraction
total_fat = sum(i.quantity * i.product.total_fat for i in recipe.items) / recipe.sku.fraction
saturated_fat = sum(i.quantity * i.product.saturated_fat for i in recipe.items) / recipe.sku.fraction
trans_fat = sum(i.quantity * i.product.trans_fat for i in recipe.items) / recipe.sku.fraction
cholestrol = sum(i.quantity * i.product.cholestrol for i in recipe.items) / recipe.sku.fraction
sodium = sum(i.quantity * i.product.sodium for i in recipe.items) / recipe.sku.fraction
msnf = sum(i.quantity * i.product.msnf for i in recipe.items) / recipe.sku.fraction
other_solids = sum(i.quantity * i.product.other_solids for i in recipe.items) / recipe.sku.fraction
total_solids = sum(i.quantity * i.product.total_solids for i in recipe.items) / recipe.sku.fraction
water = sum(i.quantity * i.product.water for i in recipe.items) / recipe.sku.fraction
db.execute(
update(Product)
.where(Product.id == recipe.sku.product_id)
.values(
protein=protein,
carbohydrate=carbohydrate,
total_sugar=total_sugar,
added_sugar=added_sugar,
total_fat=total_fat,
saturated_fat=saturated_fat,
trans_fat=trans_fat,
cholestrol=cholestrol,
sodium=sodium,
msnf=msnf,
other_solids=other_solids,
total_solids=total_solids,
water=water,
)
)
products.remove(recipe.sku.product_id)
db.flush()
def report_nutrition(db: Session) -> list[NutritionalInformation]:
try:
# Get all recipes that have nutritional values
products = set(
db.execute(
select(distinct(StockKeepingUnit.product_id))
.join(StockKeepingUnit.recipes)
.join(StockKeepingUnit.product)
.join(Product.product_group)
)
.scalars()
.all()
)
final, semi, ingredients = nut_final(db), nut_semi(db), nut_ingredients(db)
return report(products, final, semi, ingredients, db)
except SQLAlchemyError as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(e),
)
def report(
products: set[uuid.UUID], final: set[uuid.UUID], semi: set[uuid.UUID], ingredients: set[uuid.UUID], db: Session
) -> list[NutritionalInformation]:
_list: list[NutritionalInformation] = []
ingredient_dict: dict[str, set[str]] = {}
allergen_dict: dict[str, set[str]] = {}
ings = db.execute(select(Product).where(Product.id.in_(ingredients))).scalars().all()
for ingredient in ings:
if ingredient.id in products:
raise ValueError("Ingredient cannot be in a recipe")
ingredients.remove(ingredient.id)
ingredient_dict[ingredient.name] = set([ingredient.name])
allergen_dict[ingredient.name] = set(a.strip() for a in ingredient.allergen.split(",")) - set([""])
_list.append(
NutritionalInformation(
name=ingredient.name,
units=ingredient.fraction_units,
product_group="Ingredients",
description=ingredient.description,
ingredients=[ingredient.name],
allergen=list(allergen_dict[ingredient.name]),
protein=ingredient.protein,
carbohydrate=ingredient.carbohydrate,
total_sugar=ingredient.total_sugar,
added_sugar=ingredient.added_sugar,
total_fat=ingredient.total_fat,
saturated_fat=ingredient.saturated_fat,
trans_fat=ingredient.trans_fat,
cholestrol=ingredient.cholestrol,
sodium=ingredient.sodium,
msnf=ingredient.msnf,
other_solids=ingredient.other_solids,
total_solids=ingredient.total_solids,
water=ingredient.water,
)
)
while len(semi) > 0:
semi_products = (
db.execute(select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(semi))).scalars().all()
)
for semi_recipe in semi_products:
semi_product = semi_recipe.sku.product
products.remove(semi_product.id)
semi.remove(semi_product.id)
ingredient_dict[semi_product.name] = set.union(
*[ingredient_dict[i.product.name] for i in semi_recipe.items]
)
allergen_dict[semi_product.name] = set.union(
*[allergen_dict[i.product.name] for i in semi_recipe.items]
) - set([""])
_list.append(
NutritionalInformation(
name=semi_product.name,
units=semi_product.fraction_units,
product_group="Semi",
description=semi_product.description,
ingredients=list(ingredient_dict[semi_product.name]),
allergen=list(allergen_dict[semi_product.name]),
protein=semi_product.protein,
carbohydrate=semi_product.carbohydrate,
total_sugar=semi_product.total_sugar,
added_sugar=semi_product.added_sugar,
total_fat=semi_product.total_fat,
saturated_fat=semi_product.saturated_fat,
trans_fat=semi_product.trans_fat,
cholestrol=semi_product.cholestrol,
sodium=semi_product.sodium,
msnf=semi_product.msnf,
other_solids=semi_product.other_solids,
total_solids=semi_product.total_solids,
water=semi_product.water,
)
)
recipes = db.execute(select(Recipe).join(Recipe.sku).where(StockKeepingUnit.product_id.in_(final))).scalars().all()
for recipe in recipes:
recipe_product = recipe.sku.product
products.remove(recipe_product.id)
final.remove(recipe_product.id)
ingredient_dict[recipe_product.name] = set.union(*[ingredient_dict[i.product.name] for i in recipe.items])
allergen_dict[recipe_product.name] = set.union(*[allergen_dict[i.product.name] for i in recipe.items]) - set(
[""]
)
_list.append(
NutritionalInformation(
name=recipe_product.name,
units=recipe_product.fraction_units,
product_group=recipe_product.product_group.name,
description=recipe_product.description,
ingredients=list(ingredient_dict[recipe_product.name]),
allergen=list(allergen_dict[recipe_product.name]),
protein=recipe_product.protein,
carbohydrate=recipe_product.carbohydrate,
total_sugar=recipe_product.total_sugar,
added_sugar=recipe_product.added_sugar,
total_fat=recipe_product.total_fat,
saturated_fat=recipe_product.saturated_fat,
trans_fat=recipe_product.trans_fat,
cholestrol=recipe_product.cholestrol,
sodium=recipe_product.sodium,
msnf=recipe_product.msnf,
other_solids=recipe_product.other_solids,
total_solids=recipe_product.total_solids,
water=recipe_product.water,
)
)
if len(products) > 0 or len(final) > 0 or len(semi) > 0 or len(ingredients) > 0:
raise ValueError("They cannot be more than 0")
return _list
def nut_final(db: Session) -> set[uuid.UUID]:
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
ingredients = (
db.execute(select(Product.id).where(Product.id.notin_(select(RecipeItem.product_id)), Product.id.in_(sq)))
.scalars()
.all()
)
return set(ingredients)
def nut_ingredients(db: Session) -> set[uuid.UUID]:
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
ingredients = (
db.execute(select(Product.id).where(Product.id.in_(select(RecipeItem.product_id)), Product.id.notin_(sq)))
.scalars()
.all()
)
return set(ingredients)
def nut_semi(db: Session) -> set[uuid.UUID]:
sq = select(StockKeepingUnit.product_id).where(StockKeepingUnit.id.in_(select(Recipe.sku_id)))
ingredients = (
db.execute(select(Product.id).where(Product.id.in_(select(RecipeItem.product_id)), Product.id.in_(sq)))
.scalars()
.all()
)
return set(ingredients)

@ -34,12 +34,14 @@ def save(
with SessionFuture() as db: with SessionFuture() as db:
item = Product( item = Product(
name=data.name, name=data.name,
description=data.description,
fraction_units=data.fraction_units, fraction_units=data.fraction_units,
product_group_id=data.product_group.id_, product_group_id=data.product_group.id_,
account_id=Account.all_purchases(), account_id=Account.all_purchases(),
is_active=data.is_active, is_active=data.is_active,
is_purchased=data.is_purchased, is_purchased=data.is_purchased,
is_sold=data.is_sold, is_sold=data.is_sold,
allergen=data.allergen,
protein=data.protein, protein=data.protein,
carbohydrate=data.carbohydrate, carbohydrate=data.carbohydrate,
total_sugar=data.total_sugar, total_sugar=data.total_sugar,
@ -96,6 +98,7 @@ def update_route(
detail=f"{item.name} is a fixture and cannot be edited or deleted.", detail=f"{item.name} is a fixture and cannot be edited or deleted.",
) )
item.name = data.name item.name = data.name
item.description = data.description
item.fraction_units = data.fraction_units item.fraction_units = data.fraction_units
item.product_group_id = data.product_group.id_ item.product_group_id = data.product_group.id_
item.account_id = Account.all_purchases() item.account_id = Account.all_purchases()
@ -103,6 +106,7 @@ def update_route(
item.is_purchased = data.is_purchased item.is_purchased = data.is_purchased
item.is_sold = data.is_sold item.is_sold = data.is_sold
item.allergen = data.allergen
item.protein = data.protein item.protein = data.protein
item.carbohydrate = data.carbohydrate item.carbohydrate = data.carbohydrate
item.total_sugar = data.total_sugar item.total_sugar = data.total_sugar
@ -305,6 +309,7 @@ def product_info(product: Product) -> schemas.Product:
id_=product.id, id_=product.id,
code=product.code, code=product.code,
name=product.name, name=product.name,
description=product.description,
fraction_units=product.fraction_units, fraction_units=product.fraction_units,
skus=[ skus=[
schemas.StockKeepingUnit( schemas.StockKeepingUnit(
@ -322,6 +327,7 @@ def product_info(product: Product) -> schemas.Product:
is_purchased=product.is_purchased, is_purchased=product.is_purchased,
is_sold=product.is_sold, is_sold=product.is_sold,
product_group=schemas.ProductGroupLink(id_=product.product_group.id, name=product.product_group.name), product_group=schemas.ProductGroupLink(id_=product.product_group.id, name=product.product_group.name),
allergen=product.allergen,
protein=product.protein, protein=product.protein,
carbohydrate=product.carbohydrate, carbohydrate=product.carbohydrate,
total_sugar=product.total_sugar, total_sugar=product.total_sugar,
@ -341,12 +347,14 @@ def product_info(product: Product) -> schemas.Product:
def product_blank() -> schemas.ProductBlank: def product_blank() -> schemas.ProductBlank:
return schemas.ProductBlank( return schemas.ProductBlank(
name="", name="",
description="",
fraction_units="", fraction_units="",
skus=[], skus=[],
is_active=True, is_active=True,
is_purchased=True, is_purchased=True,
is_sold=False, is_sold=False,
is_fixture=False, is_fixture=False,
allergen="",
protein=0, protein=0,
carbohydrate=0, carbohydrate=0,
total_sugar=0, total_sugar=0,

@ -8,11 +8,14 @@ from decimal import Decimal
from io import BytesIO from io import BytesIO
from typing import Sequence from typing import Sequence
from ..schemas.nutritional_information import NutritionalInformation
from ..routers.calculate_nutrition import calculate_nutrition, report_nutrition
import brewman.schemas.recipe as schemas import brewman.schemas.recipe as schemas
import brewman.schemas.recipe_item as rischemas import brewman.schemas.recipe_item as rischemas
from brewman.models.price import Price from ..models.price import Price
from brewman.routers.calculate_prices import calculate_prices from ..routers.calculate_prices import calculate_prices
from fastapi import APIRouter, Depends, HTTPException, Request, Security, status from fastapi import APIRouter, Depends, HTTPException, Request, Security, status
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from openpyxl import Workbook from openpyxl import Workbook
@ -26,7 +29,6 @@ from ..db.session import SessionFuture
from ..models.product import Product from ..models.product import Product
from ..models.recipe import Recipe from ..models.recipe import Recipe
from ..models.recipe_item import RecipeItem from ..models.recipe_item import RecipeItem
from ..models.recipe_template import RecipeTemplate
from ..models.stock_keeping_unit import StockKeepingUnit from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas.user import UserToken from ..schemas.user import UserToken
@ -352,6 +354,89 @@ def excel(prices: list[tuple[str, str, Decimal, Decimal, Decimal]], recipes: lis
return virtual_workbook return virtual_workbook
@router.get("/nutrition", response_class=StreamingResponse)
def get_nutrition(
p: uuid.UUID | None = None,
) -> StreamingResponse:
with SessionFuture() as db:
calculate_nutrition(db)
db.commit()
list_: list[NutritionalInformation] = []
with SessionFuture() as db:
list_ = report_nutrition(db)
# q = (
# select(Recipe)
# .join(Recipe.sku)
# .join(StockKeepingUnit.product)
# .join(Product.product_group)
# .where(or_(ProductGroup.nutritional == True, ProductGroup.ice_cream == True)) # noqa: E712
# .options(
# contains_eager(Recipe.sku, StockKeepingUnit.product, Product.product_group),
# )
# )
# list_ = [
# NutritionalInformation(
# name=i.sku.product.name,
# units=i.sku.units,
# product_group=i.sku.product.product_group.name,
# **i.sku.product,
# )
# for i in db.execute(q).unique().scalars().all()
# ]
e = nut(sorted(list_, key=lambda r: r.name))
e.seek(0)
headers = {"Content-Disposition": "attachment; filename = nutritional.xlsx"}
return StreamingResponse(e, media_type="text/xlsx", headers=headers)
def nut(products: list[NutritionalInformation]) -> BytesIO:
wb = Workbook()
wb.active.title = "Ingredients"
pgs = set([x.product_group for x in products])
pgs.remove("Ingredients")
for pg in pgs:
wb.create_sheet(pg)
rows = defaultdict(lambda: 1)
register_styles(wb)
for item in products:
ws = wb[item.product_group]
row = rows[item.product_group]
if row == 1:
ws.cell(row=row, column=1, value="Product").style = "header"
ws.cell(row=row, column=2, value="Description").style = "header"
ws.cell(row=row, column=3, value="Ingredients").style = "header"
ws.cell(row=row, column=4, value="Allergen").style = "header"
ws.cell(row=row, column=5, value="Protein").style = "header"
ws.cell(row=row, column=6, value="Carbohydrate").style = "header"
ws.cell(row=row, column=7, value="Total Sugar").style = "header"
ws.cell(row=row, column=8, value="Added Sugar").style = "header"
ws.cell(row=row, column=9, value="Total Fat").style = "header"
ws.cell(row=row, column=10, value="Saturated Fat").style = "header"
ws.cell(row=row, column=11, value="Trans Fat").style = "header"
ws.cell(row=row, column=12, value="Cholestrol").style = "header"
ws.cell(row=row, column=13, value="Sodium").style = "header"
row += 1
ws.cell(row=row, column=1, value=f"{item.name} ({item.units})").style = "ing"
ws.cell(row=row, column=2, value=item.description).style = "ing"
ws.cell(row=row, column=3, value=", ".join(item.ingredients)).style = "ing"
ws.cell(row=row, column=4, value=", ".join(item.allergen)).style = "ing"
ws.cell(row=row, column=5, value=item.protein).style = "ing"
ws.cell(row=row, column=6, value=item.carbohydrate).style = "ing"
ws.cell(row=row, column=7, value=item.total_sugar).style = "ing"
ws.cell(row=row, column=8, value=item.added_sugar).style = "ing"
ws.cell(row=row, column=9, value=item.total_fat).style = "ing"
ws.cell(row=row, column=10, value=item.saturated_fat).style = "ing"
ws.cell(row=row, column=11, value=item.trans_fat).style = "ing"
ws.cell(row=row, column=12, value=item.cholestrol).style = "ing"
ws.cell(row=row, column=13, value=item.sodium).style = "ing"
row += 1
rows[item.product_group] = row + 1
virtual_workbook = BytesIO()
wb.save(virtual_workbook)
return virtual_workbook
def register_styles(wb: Workbook) -> tuple[NamedStyle, NamedStyle, NamedStyle, NamedStyle, NamedStyle]: def register_styles(wb: Workbook) -> tuple[NamedStyle, NamedStyle, NamedStyle, NamedStyle, NamedStyle]:
bd = Side(style="thin", color="000000") bd = Side(style="thin", color="000000")

@ -0,0 +1,28 @@
from decimal import Decimal
from pydantic import BaseModel
class NutritionalInformation(BaseModel):
name: str
units: str
product_group: str
description: str | None
ingredients: list[str]
allergen: list[str]
protein: Decimal
carbohydrate: Decimal
total_sugar: Decimal
added_sugar: Decimal
total_fat: Decimal
saturated_fat: Decimal
trans_fat: Decimal
cholestrol: Decimal
sodium: Decimal
msnf: Decimal
other_solids: Decimal
total_solids: Decimal
water: Decimal

@ -17,6 +17,7 @@ class ProductLink(BaseModel):
class ProductIn(BaseModel): class ProductIn(BaseModel):
name: str = Field(..., min_length=1) name: str = Field(..., min_length=1)
description: str | None
fraction_units: str = Field(..., min_length=1) fraction_units: str = Field(..., min_length=1)
skus: list[StockKeepingUnit] skus: list[StockKeepingUnit]
product_group: ProductGroupLink = Field(...) product_group: ProductGroupLink = Field(...)
@ -24,6 +25,7 @@ class ProductIn(BaseModel):
is_purchased: bool is_purchased: bool
is_sold: bool is_sold: bool
allergen: str
protein: Decimal protein: Decimal
carbohydrate: Decimal carbohydrate: Decimal
total_sugar: Decimal total_sugar: Decimal

@ -21,6 +21,7 @@ export class Product {
id: string | undefined; id: string | undefined;
code: number; code: number;
name: string; name: string;
description: string | undefined;
skus: StockKeepingUnit[]; skus: StockKeepingUnit[];
fractionUnits: string | undefined; fractionUnits: string | undefined;
@ -30,6 +31,7 @@ export class Product {
isSold: boolean; isSold: boolean;
productGroup?: ProductGroup; productGroup?: ProductGroup;
allergen: string;
protein: number; protein: number;
carbohydrate: number; carbohydrate: number;
totalSugar: number; totalSugar: number;
@ -55,6 +57,7 @@ export class Product {
this.isPurchased = true; this.isPurchased = true;
this.isSold = false; this.isSold = false;
this.allergen = '';
this.protein = 0; this.protein = 0;
this.carbohydrate = 0; this.carbohydrate = 0;
this.totalSugar = 0; this.totalSugar = 0;

@ -20,6 +20,12 @@
<input matInput formControlName="fractionUnits" /> <input matInput formControlName="fractionUnits" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-row justify-around content-start items-start">
<mat-form-field class="flex-auto">
<mat-label>Description</mat-label>
<input matInput #nameElement formControlName="description" />
</mat-form-field>
</div>
<div class="flex flex-row justify-around content-start items-start"> <div class="flex flex-row justify-around content-start items-start">
<mat-checkbox formControlName="isPurchased" class="flex-auto mr-5">Is Purchased?</mat-checkbox> <mat-checkbox formControlName="isPurchased" class="flex-auto mr-5">Is Purchased?</mat-checkbox>
<mat-checkbox formControlName="isSold" class="flex-auto mr-5">Is Sold?</mat-checkbox> <mat-checkbox formControlName="isSold" class="flex-auto mr-5">Is Sold?</mat-checkbox>
@ -35,6 +41,12 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="flex flex-row justify-around content-start items-start">
<mat-form-field class="flex-auto">
<mat-label>Allergen</mat-label>
<input matInput #nameElement formControlName="allergen" />
</mat-form-field>
</div>
<h2 *ngIf="item.productGroup?.nutritional ?? false">Nutritional Information</h2> <h2 *ngIf="item.productGroup?.nutritional ?? false">Nutritional Information</h2>
<div <div
class="flex flex-row justify-around content-start items-start" class="flex flex-row justify-around content-start items-start"

@ -23,6 +23,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
form: FormGroup<{ form: FormGroup<{
code: FormControl<string | number>; code: FormControl<string | number>;
name: FormControl<string | null>; name: FormControl<string | null>;
description: FormControl<string | null>;
fractionUnits: FormControl<string | null>; fractionUnits: FormControl<string | null>;
addRow: FormGroup<{ addRow: FormGroup<{
units: FormControl<string | null>; units: FormControl<string | null>;
@ -36,6 +37,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
isActive: FormControl<boolean>; isActive: FormControl<boolean>;
productGroup: FormControl<string | null>; productGroup: FormControl<string | null>;
allergen: FormControl<string | null>;
protein: FormControl<number>; protein: FormControl<number>;
carbohydrate: FormControl<number>; carbohydrate: FormControl<number>;
totalSugar: FormControl<number>; totalSugar: FormControl<number>;
@ -69,6 +71,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.form = new FormGroup({ this.form = new FormGroup({
code: new FormControl<string | number>({ value: 0, disabled: true }, { nonNullable: true }), code: new FormControl<string | number>({ value: 0, disabled: true }, { nonNullable: true }),
name: new FormControl<string | null>(null), name: new FormControl<string | null>(null),
description: new FormControl<string | null>(null),
fractionUnits: new FormControl<string | null>(null), fractionUnits: new FormControl<string | null>(null),
addRow: new FormGroup({ addRow: new FormGroup({
units: new FormControl<string | null>(null), units: new FormControl<string | null>(null),
@ -82,6 +85,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
isActive: new FormControl<boolean>(true, { nonNullable: true }), isActive: new FormControl<boolean>(true, { nonNullable: true }),
productGroup: new FormControl<string | null>(null), productGroup: new FormControl<string | null>(null),
allergen: new FormControl<string | null>(null),
protein: new FormControl<number>(0, { nonNullable: true }), protein: new FormControl<number>(0, { nonNullable: true }),
carbohydrate: new FormControl<number>(0, { nonNullable: true }), carbohydrate: new FormControl<number>(0, { nonNullable: true }),
totalSugar: new FormControl<number>(0, { nonNullable: true }), totalSugar: new FormControl<number>(0, { nonNullable: true }),
@ -116,6 +120,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
this.form.setValue({ this.form.setValue({
code: this.item.code || '(Auto)', code: this.item.code || '(Auto)',
name: this.item.name, name: this.item.name,
description: this.item.description || '',
fractionUnits: this.item.fractionUnits ?? '', fractionUnits: this.item.fractionUnits ?? '',
addRow: { addRow: {
units: '', units: '',
@ -129,6 +134,7 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
isActive: this.item.isActive, isActive: this.item.isActive,
productGroup: this.item.productGroup ? this.item.productGroup.id ?? '' : '', productGroup: this.item.productGroup ? this.item.productGroup.id ?? '' : '',
allergen: this.item.allergen ?? '',
protein: this.item.protein, protein: this.item.protein,
carbohydrate: this.item.carbohydrate, carbohydrate: this.item.carbohydrate,
totalSugar: this.item.totalSugar, totalSugar: this.item.totalSugar,
@ -266,11 +272,13 @@ export class ProductDetailComponent implements OnInit, AfterViewInit {
getItem(): Product { getItem(): Product {
const formModel = this.form.value; const formModel = this.form.value;
this.item.name = formModel.name ?? ''; this.item.name = formModel.name ?? '';
this.item.description = formModel.description ?? '';
this.item.fractionUnits = formModel.fractionUnits ?? ''; this.item.fractionUnits = formModel.fractionUnits ?? '';
this.item.isPurchased = formModel.isPurchased ?? true; this.item.isPurchased = formModel.isPurchased ?? true;
this.item.isSold = formModel.isSold ?? false; this.item.isSold = formModel.isSold ?? false;
this.item.isActive = formModel.isActive ?? true; this.item.isActive = formModel.isActive ?? true;
this.item.allergen = formModel.allergen ?? '';
this.item.protein = formModel.protein ?? 0; this.item.protein = formModel.protein ?? 0;
this.item.carbohydrate = formModel.carbohydrate ?? 0; this.item.carbohydrate = formModel.carbohydrate ?? 0;
this.item.totalSugar = formModel.totalSugar ?? 0; this.item.totalSugar = formModel.totalSugar ?? 0;

@ -5,6 +5,9 @@
<a mat-icon-button [href]="'/api/recipes/xlsx?t=' + period.id"> <a mat-icon-button [href]="'/api/recipes/xlsx?t=' + period.id">
<mat-icon>save_alt</mat-icon> <mat-icon>save_alt</mat-icon>
</a> </a>
<a mat-icon-button href="/api/recipes/nutrition">
<mat-icon>save_alt</mat-icon>
</a>
<a mat-button [routerLink]="['/recipes', 'new']"> <a mat-button [routerLink]="['/recipes', 'new']">
<mat-icon>add_box</mat-icon> <mat-icon>add_box</mat-icon>
Add Add