This commit is contained in:
tanshu committed 2026-08-27 11:38:38 +00:00
1 parent 311464e593
commit ff537edb06
19 files changed
+456 -393

No files matched your search

+12 -1
View File
@@ -1,5 +1,11 @@
{ {
"python.defaultInterpreterPath": "${workspaceFolder}/barker/.venv/bin/python", "python.defaultInterpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
"python.interpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
"python.venvFolders": [
"barker/.venv",
".venv"
],
"python.venvPath": "${workspaceFolder}/barker",
"python.terminal.activateEnvironment": true, "python.terminal.activateEnvironment": true,
// Quality-of-life // Quality-of-life
/* --- Pylance: keep IntelliSense, disable type checking --- */ /* --- Pylance: keep IntelliSense, disable type checking --- */
@@ -7,6 +13,11 @@
// "python.analysis.diagnosticMode": "openFilesOnly", // "python.analysis.diagnosticMode": "openFilesOnly",
"python.analysis.typeCheckingMode": "basic", "python.analysis.typeCheckingMode": "basic",
"python.analysis.extraPaths": [ "python.analysis.extraPaths": [
"${workspaceFolder}/barker",
"./barker"
],
"python.autoComplete.extraPaths": [
"${workspaceFolder}/barker",
"./barker" "./barker"
], ],
// Ruff (you have ruff installed via uv) // Ruff (you have ruff installed via uv)
+1 -1
View File
@@ -65,7 +65,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project uv sync --locked --no-install-project
COPY /barker ./ COPY /barker ./
COPY --from=builder /frontend/browser /app/static COPY --from=builder /frontend/browser /app/frontend
# Sync the project # Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers # Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
+1 -22
View File
@@ -1,24 +1,3 @@
{{ host }} { {{ host }} {
# Match and proxy API routes reverse_proxy {{ host_directory }}:80
@apiRoutes {
path_regexp ^/(api|token|refresh|db-image)
}
handle @apiRoutes {
reverse_proxy @apiRoutes {{ host_directory }}:80
}
# Match requests that end with .js, .css, .ico, or .html
@staticFiles {
path_regexp \.(js|css|ico|html)$
}
handle @staticFiles {
rewrite * /static{uri}
reverse_proxy {{ host_directory }}:80
}
# All other frontend routes → /static/index.html
handle {
rewrite * /static/index.html
reverse_proxy {{ host_directory }}:80
}
} }
+22
View File
@@ -0,0 +1,22 @@
{
"python.defaultInterpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
"python.interpreterPath": "/home/tanshu/programming/barker/barker/.venv/bin/python",
"python.venvFolders": [
".venv"
],
"python.terminal.activateEnvironment": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.extraPaths": [
"${workspaceFolder}"
],
"python.autoComplete.extraPaths": [
"${workspaceFolder}"
],
"editor.formatOnSave": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "always"
}
}
+344 -289
View File
@@ -1,13 +1,14 @@
import uuid import uuid
from collections import defaultdict
from collections.abc import Sequence from collections.abc import Sequence
from datetime import date, timedelta from datetime import date, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Security, status from fastapi import APIRouter, Depends, HTTPException, Security, status
from sqlalchemy import Date, func, or_, select from sqlalchemy import Date, Select, func, select
from sqlalchemy.orm import contains_eager from sqlalchemy.orm import Session, contains_eager
from ..core.config import settings from ..core.config import settings
from ..core.security import get_current_active_user as get_user from ..core.security import get_current_active_user as get_user
@@ -17,6 +18,7 @@ from ..models.inventory import Inventory
from ..models.kot import Kot from ..models.kot import Kot
from ..models.product import Product 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.sku_version import SkuVersion from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit from ..models.stock_keeping_unit import StockKeepingUnit
from ..models.voucher import Voucher from ..models.voucher import Voucher
@@ -24,20 +26,95 @@ from ..schemas import bundle as schemas
from ..schemas.menu_category import MenuCategoryLink from ..schemas.menu_category import MenuCategoryLink
from ..schemas.sale_category import SaleCategoryLink from ..schemas.sale_category import SaleCategoryLink
from ..schemas.user_token import UserToken from ..schemas.user_token import UserToken
from . import _pv_onclause, _sv_onclause, effective_date from . import _bundle_active, _pv_onclause, _sv_onclause, effective_date
from .product import query_product_info
router = APIRouter() router = APIRouter()
# Bundle-specific constants: bundles always have fraction=1, yield=1, cost=0.
FRACTION_1 = Decimal("1") FRACTION_1 = Decimal("1")
YIELD_1 = Decimal("1") YIELD_1 = Decimal("1")
COST_0 = Decimal("0") COST_0 = Decimal("0")
# Rounding precision for monetary amounts and quantities.
# ---------- helpers ---------- PRICE_PLACES = Decimal("0.01")
QTY_PLACES = Decimal("0.00001")
def bundle_blank() -> schemas.BundleBlank: def _round_price(value: Decimal) -> Decimal:
return value.quantize(PRICE_PLACES)
def _round_qty(value: Decimal) -> Decimal:
return value.quantize(QTY_PLACES)
# ---------- query builders ----------
def _select_bundle_header(date_: date, sku_id: uuid.UUID | None = None) -> Select[tuple[SkuVersion]]:
"""Build the common query to load a bundle's SkuVersion with eagerly-loaded
SKU → Product → ProductVersion and MenuCategory chains.
When *sku_id* is ``None`` the query returns **all** active bundles (for the
list endpoint); otherwise it filters to a single bundle.
"""
stmt = (
select(SkuVersion)
.join(StockKeepingUnit, onclause=_sv_onclause(date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(SkuVersion.menu_category)
.where(StockKeepingUnit.is_bundle == True) # noqa: E712
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product).contains_eager(Product.versions),
contains_eager(SkuVersion.menu_category),
)
)
if sku_id is not None:
stmt = stmt.where(StockKeepingUnit.id == sku_id)
return stmt
def _select_bundle_items(
date_: date,
bundle_sku_ids: Sequence[uuid.UUID],
) -> Select[tuple[BundleItemModel]]:
"""Build the common query to load active BundleItem rows for one or more
bundles, eagerly loading the child SKU's version chain (menu category,
product version, sale category, tax).
"""
return (
select(BundleItemModel)
.join(BundleItemModel.item)
.join(SkuVersion, onclause=_sv_onclause(date_))
.join(SkuVersion.menu_category)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(ProductVersion.sale_category)
.join(SaleCategory.tax)
.where(
BundleItemModel.bundle_id.in_(bundle_sku_ids),
_bundle_active(date_),
)
.options(
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.versions)
.contains_eager(SkuVersion.menu_category),
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category)
.contains_eager(SaleCategory.tax),
)
)
# ---------- response helpers ----------
def _bundle_blank() -> schemas.BundleBlank:
return schemas.BundleBlank( return schemas.BundleBlank(
name="", name="",
units="", units="",
@@ -72,8 +149,7 @@ def _bundle_info(pv: ProductVersion, sv: SkuVersion, items: Sequence[BundleItemM
items=[ items=[
schemas.BundleItem( schemas.BundleItem(
id_=bi.id, id_=bi.id,
name=f"{bi.item.product.versions[0].name} ({bi.item.versions[0].units})", sku=query_product_info(bi.item.versions[0], False, []),
item_id=bi.item_id,
sale_price=bi.sale_price, sale_price=bi.sale_price,
quantity=bi.quantity, quantity=bi.quantity,
print_in_bill=bi.print_in_bill, print_in_bill=bi.print_in_bill,
@@ -83,6 +159,198 @@ def _bundle_info(pv: ProductVersion, sv: SkuVersion, items: Sequence[BundleItemM
) )
# ---------- temporal update helpers ----------
def _compute_sale_price(items: list[schemas.BundleItem]) -> Decimal:
"""Derive the bundle sale price from its items: Σ(item_price × qty)."""
return _round_price(
sum(
(_round_price(it.sale_price) * _round_qty(it.quantity) for it in items),
start=Decimal(0),
)
)
def _update_product_version(
db: Session,
pv: ProductVersion,
date_: date,
data: schemas.BundleIn,
) -> None:
"""Temporally update the ProductVersion if name, sale_category, or
fraction_units changed. Edits in-place when *pv* was created today,
otherwise closes it and inserts a new version.
"""
pv_changed = (
pv.name != data.name or pv.sale_category_id != data.sale_category.id_ or pv.fraction_units != data.units
)
if not pv_changed:
return
if pv.valid_from == date_:
pv.name = data.name
pv.sale_category_id = data.sale_category.id_
pv.fraction_units = data.units
else:
pv.valid_till = date_ - timedelta(days=1)
db.add(
ProductVersion(
product_id=pv.product_id,
name=data.name,
fraction_units=data.units,
sale_category_id=data.sale_category.id_,
valid_from=date_,
valid_till=None,
)
)
def _update_sku_version(
db: Session,
sv: SkuVersion,
date_: date,
data: schemas.BundleIn,
sale_price: Decimal,
) -> None:
"""Temporally update the SkuVersion if units, sale_price, happy-hour, or
menu_category changed. Edits in-place when *sv* was created today,
otherwise closes it and inserts a new version.
"""
sv_changed = (
sv.units != data.units
or _round_price(Decimal(sv.sale_price)) != sale_price
or sv.has_happy_hour != data.has_happy_hour
or sv.menu_category_id != data.menu_category.id_
)
if not sv_changed:
return
if sv.valid_from == date_:
sv.units = data.units
sv.sale_price = sale_price
sv.has_happy_hour = data.has_happy_hour
sv.menu_category_id = data.menu_category.id_
else:
sv.valid_till = date_ - timedelta(days=1)
db.add(
SkuVersion(
sku_id=sv.sku_id,
units=data.units,
fraction=FRACTION_1,
product_yield=YIELD_1,
cost_price=COST_0,
sale_price=sale_price,
has_happy_hour=data.has_happy_hour,
menu_category_id=data.menu_category.id_,
valid_from=date_,
valid_till=None,
)
)
def _sync_bundle_items(
db: Session,
bundle_sku_id: uuid.UUID,
date_: date,
items: list[schemas.BundleItem],
) -> None:
"""Reconcile the incoming item list against the existing active rows.
For each existing row that is no longer in the incoming list, it is either
hard-deleted (if created today) or temporally closed. New items are
inserted and changed items are updated with the same temporal logic.
"""
existing = (
db.execute(
select(BundleItemModel).where(
BundleItemModel.bundle_id == bundle_sku_id,
_bundle_active(date_),
)
)
.scalars()
.all()
)
existing_by_id: dict[uuid.UUID, BundleItemModel] = {x.id: x for x in existing}
existing_by_item: dict[uuid.UUID, BundleItemModel] = {x.item_id: x for x in existing}
def _find_existing(it: schemas.BundleItem) -> BundleItemModel | None:
if it.id_ is not None:
return existing_by_id.get(it.id_)
return existing_by_item.get(it.sku.id_)
# Identify which existing rows are still matched by incoming items.
matched_ids: set[uuid.UUID] = set()
for it in items:
ex = _find_existing(it)
if ex is not None:
matched_ids.add(ex.id)
# Remove rows that are no longer in the incoming list.
for ex in existing:
if ex.id not in matched_ids:
if ex.valid_from == date_:
db.delete(ex)
else:
ex.valid_till = date_ - timedelta(days=1)
# Add new rows or update changed ones.
for it in items:
price = _round_price(it.sale_price)
qty = _round_qty(it.quantity)
ex = _find_existing(it)
if ex is None:
db.add(
BundleItemModel(
bundle_id=bundle_sku_id,
item_id=it.sku.id_,
sale_price=price,
quantity=qty,
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
continue
item_changed = (
ex.item_id != it.sku.id_
or _round_price(Decimal(ex.sale_price)) != price
or _round_qty(Decimal(ex.quantity)) != qty
or ex.print_in_bill != it.print_in_bill
)
if not item_changed:
continue
if ex.valid_from == date_:
ex.item_id = it.sku.id_
ex.sale_price = price
ex.quantity = qty
ex.print_in_bill = it.print_in_bill
else:
ex.valid_till = date_ - timedelta(days=1)
db.add(
BundleItemModel(
bundle_id=bundle_sku_id,
item_id=it.sku.id_,
sale_price=price,
quantity=qty,
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
def _close_or_delete(db: Session, entity: ProductVersion | SkuVersion | BundleItemModel, date_: date) -> None:
"""Hard-delete if created today, otherwise temporally close."""
if entity.valid_from == date_:
db.delete(entity)
else:
entity.valid_till = date_ - timedelta(days=1)
# ---------- routes ---------- # ---------- routes ----------
@@ -98,23 +366,22 @@ def save(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough bundle items.", detail="Not enough bundle items.",
) )
sale_price = round(sum((round(it.sale_price, 2) * round(it.quantity, 5) for it in data.items), start=Decimal(0)), 2) sale_price = _compute_sale_price(data.items)
product = Product(sort_order=data.sort_order) product = Product(sort_order=data.sort_order)
db.add(product) db.add(product)
db.flush() db.flush()
pv = ProductVersion( db.add(
product_id=None, ProductVersion(
product=product, product_id=product.id,
name=data.name, name=data.name,
fraction_units=data.units, # per your rule fraction_units=data.units,
sale_category_id=data.sale_category.id_, sale_category_id=data.sale_category.id_,
sale_category=None, valid_from=date_,
valid_from=date_, valid_till=None,
valid_till=None, )
) )
db.add(pv)
sku = StockKeepingUnit( sku = StockKeepingUnit(
product=product, product=product,
@@ -125,28 +392,28 @@ def save(
db.add(sku) db.add(sku)
db.flush() db.flush()
sv = SkuVersion( db.add(
sku_id=sku.id, SkuVersion(
units=data.units, sku_id=sku.id,
fraction=FRACTION_1, units=data.units,
product_yield=YIELD_1, fraction=FRACTION_1,
cost_price=COST_0, product_yield=YIELD_1,
sale_price=sale_price, cost_price=COST_0,
has_happy_hour=data.has_happy_hour, sale_price=sale_price,
menu_category_id=data.menu_category.id_, has_happy_hour=data.has_happy_hour,
valid_from=date_, menu_category_id=data.menu_category.id_,
valid_till=None, valid_from=date_,
valid_till=None,
)
) )
db.add(sv)
# bundle items
for it in data.items: for it in data.items:
db.add( db.add(
BundleItemModel( BundleItemModel(
bundle_id=sku.id, bundle_id=sku.id,
item_id=it.item_id, item_id=it.sku.id_,
quantity=round(it.quantity, 5), quantity=_round_qty(it.quantity),
sale_price=round(it.sale_price, 2), sale_price=_round_price(it.sale_price),
print_in_bill=it.print_in_bill, print_in_bill=it.print_in_bill,
valid_from=date_, valid_from=date_,
valid_till=None, valid_till=None,
@@ -158,7 +425,7 @@ def save(
@router.put("/{id_}", response_model=None) @router.put("/{id_}", response_model=None)
def update_route( def update_route(
id_: uuid.UUID, # bundle header SKU id id_: uuid.UUID,
data: schemas.BundleIn, data: schemas.BundleIn,
date_: Annotated[date, Depends(effective_date)], date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])], user: Annotated[UserToken, Security(get_user, scopes=["products"])],
@@ -169,142 +436,24 @@ def update_route(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Not enough bundle items.", detail="Not enough bundle items.",
) )
sale_price = round(sum((round(it.sale_price, 2) * round(it.quantity, 5) for it in data.items), start=Decimal(0)), 2) sale_price = _compute_sale_price(data.items)
# Load header SKU + active header SkuVersion + menu category + product + active ProductVersion
sv: SkuVersion | None = (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=_sv_onclause(date_))
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=_pv_onclause(date_))
.join(SkuVersion.menu_category)
.where(
StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712
)
.options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
contains_eager(SkuVersion.menu_category),
)
)
.unique()
.scalars()
.one_or_none()
)
sv: SkuVersion | None = db.execute(_select_bundle_header(date_, sku_id=id_)).unique().scalars().one_or_none()
if sv is None: if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
sku = sv.sku sku = sv.sku
product = sku.product product = sku.product
pv = product.versions[0] pv = product.versions[0]
pv_changed = pv.name != data.name or pv.sale_category_id != data.sale_category.id_
# Always-mutable fields (no versioning needed).
sku.is_not_available = data.is_not_available sku.is_not_available = data.is_not_available
sku.sort_order = data.sort_order sku.sort_order = data.sort_order
product.sort_order = data.sort_order product.sort_order = data.sort_order
if pv_changed: _update_product_version(db, pv, date_, data)
if pv.valid_from == date_: _update_sku_version(db, sv, date_, data, sale_price)
pv.name = data.name _sync_bundle_items(db, sku.id, date_, data.items)
pv.sale_category_id = data.sale_category.id_
else:
pv.valid_till = date_ - timedelta(days=1)
db.add(
ProductVersion(
product_id=product.id,
product=product,
name=data.name,
fraction_units=pv.fraction_units,
sale_category_id=data.sale_category.id_,
valid_from=date_,
valid_till=None,
)
)
sv_changed = (
sv.units != data.units
or Decimal(sv.sale_price).quantize(Decimal("0.01")) != sale_price
or sv.has_happy_hour != data.has_happy_hour
or sv.menu_category_id != data.menu_category.id_
)
if sv_changed:
if sv.valid_from == date_:
sv.units = data.units
sv.sale_price = sale_price
sv.has_happy_hour = data.has_happy_hour
sv.menu_category_id = data.menu_category.id_
# # enforce bundle constants
# sv.fraction = FRACTION_1
# sv.product_yield = YIELD_1
# sv.cost_price = COST_0
else:
sv.valid_till = date_ - timedelta(days=1)
db.add(
SkuVersion(
sku_id=sku.id,
units=data.units,
fraction=FRACTION_1,
product_yield=YIELD_1,
cost_price=COST_0,
sale_price=sale_price,
has_happy_hour=data.has_happy_hour,
menu_category_id=data.menu_category.id_,
valid_from=date_,
valid_till=None,
)
)
# ---- Bundle items replace/update
existing = db.execute(select(BundleItemModel).where(BundleItemModel.bundle_id == sku.id)).scalars().all()
existing_by_item = {x.item_id: x for x in existing}
incoming_ids = {x.item_id for x in data.items}
existing_by_item = {x.item_id: x for x in existing}
incoming_ids = {x.item_id for x in data.items}
# delete removed
for ex_d in existing:
if ex_d.item_id not in incoming_ids:
if ex_d.valid_from == date_:
db.delete(ex_d)
else:
ex_d.valid_till = date_ - timedelta(days=1)
# add/update
for it in data.items:
ex = existing_by_item.get(it.item_id)
if ex is None:
db.add(
BundleItemModel(
bundle_id=sku.id,
item_id=it.item_id,
sale_price=round(it.sale_price, 2),
quantity=round(it.quantity, 5),
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
elif ex.valid_from == date_:
ex.sale_price = round(it.sale_price, 2)
ex.quantity = round(it.quantity, 5)
else:
ex.valid_till = date_ - timedelta(days=1)
db.add(
BundleItemModel(
bundle_id=sku.id,
item_id=it.item_id,
sale_price=round(it.sale_price, 2),
quantity=round(it.quantity, 5),
print_in_bill=it.print_in_bill,
valid_from=date_,
valid_till=None,
)
)
db.commit() db.commit()
@@ -313,7 +462,7 @@ def update_route(
def show_blank( def show_blank(
user: Annotated[UserToken, Security(get_user, scopes=["products"])], user: Annotated[UserToken, Security(get_user, scopes=["products"])],
) -> schemas.BundleBlank: ) -> schemas.BundleBlank:
return bundle_blank() return _bundle_blank()
@router.get("/list", response_model=list[schemas.Bundle]) @router.get("/list", response_model=list[schemas.Bundle])
@@ -322,133 +471,47 @@ def show_list(
user: Annotated[UserToken, Security(get_user, scopes=["products"])], user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep, db: SessionDep,
) -> list[schemas.Bundle]: ) -> list[schemas.Bundle]:
sv_onclause = _sv_onclause(date_) rows: Sequence[SkuVersion] = (
pv_onclause = _pv_onclause(date_) db.execute(_select_bundle_header(date_).order_by(StockKeepingUnit.sort_order, SkuVersion.units))
rows = (
db.execute(
select(SkuVersion)
# .select_from(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(SkuVersion.menu_category)
.where(StockKeepingUnit.is_bundle == True) # noqa: E712
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product),
contains_eager(SkuVersion.menu_category),
)
.order_by(StockKeepingUnit.sort_order, SkuVersion.units)
)
.unique() .unique()
.scalars() .scalars()
.all() .all()
) )
if not rows:
return []
out: list[schemas.Bundle] = [] # Batch-load all bundle items in a single query to avoid N+1.
for sv in rows: all_sku_ids = [sv.sku_id for sv in rows]
pv = sv.sku.product.versions[0] all_items: Sequence[BundleItemModel] = db.execute(_select_bundle_items(date_, all_sku_ids)).unique().scalars().all()
items = ( items_by_bundle: dict[uuid.UUID, list[BundleItemModel]] = defaultdict(list)
db.execute( for bi in all_items:
select(BundleItemModel) items_by_bundle[bi.bundle_id].append(bi)
.join(BundleItemModel.item)
.join(SkuVersion, onclause=sv_onclause) return [_bundle_info(pv=sv.sku.product.versions[0], sv=sv, items=items_by_bundle.get(sv.sku_id, [])) for sv in rows]
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(ProductVersion.sale_category)
.where(
BundleItemModel.bundle_id == sv.sku_id,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
)
.options(
contains_eager(BundleItemModel.item).contains_eager(StockKeepingUnit.versions),
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalars()
.all()
)
out.append(_bundle_info(pv=pv, sv=sv, items=items))
return out
@router.get("/{id_}", response_model=schemas.Bundle) @router.get("/{id_}", response_model=schemas.Bundle)
def show_id( def show_id(
id_: uuid.UUID, # bundle header SKU id id_: uuid.UUID,
date_: Annotated[date, Depends(effective_date)], date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])], user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep, db: SessionDep,
) -> schemas.Bundle: ) -> schemas.Bundle:
sv_onclause = _sv_onclause(date_) sv: SkuVersion | None = db.execute(_select_bundle_header(date_, sku_id=id_)).unique().scalar_one_or_none()
pv_onclause = _pv_onclause(date_)
sv = (
db.execute(
select(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(SkuVersion.menu_category)
.where(
StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712
)
.options(
contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product),
contains_eager(SkuVersion.menu_category),
)
)
.unique()
.scalar_one_or_none()
)
if sv is None: if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
pv = sv.sku.product.versions[0] items: Sequence[BundleItemModel] = db.execute(_select_bundle_items(date_, [sv.sku_id])).unique().scalars().all()
return _bundle_info(pv=sv.sku.product.versions[0], sv=sv, items=items)
items = (
db.execute(
select(BundleItemModel)
.join(BundleItemModel.item)
.join(SkuVersion, onclause=sv_onclause)
.join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause)
.join(ProductVersion.sale_category)
.where(
BundleItemModel.bundle_id == sv.sku_id,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
)
.options(
contains_eager(BundleItemModel.item).contains_eager(StockKeepingUnit.versions),
contains_eager(BundleItemModel.item)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions)
.contains_eager(ProductVersion.sale_category),
)
)
.unique()
.scalars()
.all()
)
return _bundle_info(pv=pv, sv=sv, items=items)
@router.delete("/{id_}", response_model=None) @router.delete("/{id_}", response_model=None)
def delete_route( def delete_route(
id_: uuid.UUID, # bundle header SKU id id_: uuid.UUID,
date_: Annotated[date, Depends(effective_date)], date_: Annotated[date, Depends(effective_date)],
user: Annotated[UserToken, Security(get_user, scopes=["products"])], user: Annotated[UserToken, Security(get_user, scopes=["products"])],
db: SessionDep, db: SessionDep,
) -> None: ) -> None:
sv_onclause = _sv_onclause(date_)
pv_onclause = _pv_onclause(date_)
day = func.cast( day = func.cast(
Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date Voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES), Date
).label("day") ).label("day")
@@ -461,57 +524,49 @@ def delete_route(
if billed > 0: if billed > 0:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="The cannot delete this product as it was billed", detail="Cannot delete this product as it was billed.",
) )
sv = ( # The delete query intentionally omits the menu_category join: it doesn't
# need menu category data and the bundle may not have one after versioning.
sv: SkuVersion | None = (
db.execute( db.execute(
select(SkuVersion) select(SkuVersion)
.join(StockKeepingUnit, onclause=sv_onclause) .join(StockKeepingUnit, onclause=_sv_onclause(date_))
.join(StockKeepingUnit.product) .join(StockKeepingUnit.product)
.join(ProductVersion, onclause=pv_onclause) .join(ProductVersion, onclause=_pv_onclause(date_))
.where( .where(
StockKeepingUnit.id == id_, StockKeepingUnit.id == id_,
StockKeepingUnit.is_bundle == True, # noqa: E712 StockKeepingUnit.is_bundle == True, # noqa: E712
) )
.options(contains_eager(SkuVersion.sku).contains_eager(StockKeepingUnit.product)) .options(
contains_eager(SkuVersion.sku)
.contains_eager(StockKeepingUnit.product)
.contains_eager(Product.versions),
)
) )
.unique() .unique()
.scalar_one_or_none() .scalar_one_or_none()
) )
if sv is None: if sv is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Bundle not found.")
pv = sv.sku.product.versions[0] pv = sv.sku.product.versions[0]
# close/delete sku version _close_or_delete(db, sv, date_)
if sv.valid_from == date_: _close_or_delete(db, pv, date_)
db.delete(sv)
else:
sv.valid_till = date_ - timedelta(days=1)
# close/delete product version active_items: Sequence[BundleItemModel] = (
if pv.valid_from == date_:
db.delete(pv)
else:
pv.valid_till = date_ - timedelta(days=1)
items = (
db.execute( db.execute(
select(BundleItemModel).where( select(BundleItemModel).where(
BundleItemModel.bundle_id == id_, BundleItemModel.bundle_id == id_,
or_(BundleItemModel.valid_from == None, BundleItemModel.valid_from <= date_), # noqa: E711 _bundle_active(date_),
or_(BundleItemModel.valid_till == None, BundleItemModel.valid_till >= date_), # noqa: E711
) )
) )
.scalars() .scalars()
.all() .all()
) )
for bi in items: for bi in active_items:
if bi.valid_from == date_: _close_or_delete(db, bi, date_)
db.delete(bi)
else:
bi.valid_till = date_ - timedelta(days=1)
db.commit() db.commit()
+2 -2
View File
@@ -7,13 +7,13 @@ from pydantic import BaseModel, ConfigDict, Field
from . import Daf, to_camel from . import Daf, to_camel
from .menu_category import MenuCategoryLink from .menu_category import MenuCategoryLink
from .product_query import ProductQuery
from .sale_category import SaleCategoryLink from .sale_category import SaleCategoryLink
class BundleItem(BaseModel): class BundleItem(BaseModel):
id_: uuid.UUID | None = None id_: uuid.UUID | None = None
name: Annotated[str, Field(min_length=1)] sku: ProductQuery
item_id: Annotated[uuid.UUID, Field(description="StockKeepingUnit ID of the item")]
sale_price: Annotated[Daf, Field(ge=Decimal(0))] sale_price: Annotated[Daf, Field(ge=Decimal(0))]
quantity: Annotated[Daf, Field(gt=Decimal(0))] quantity: Annotated[Daf, Field(gt=Decimal(0))]
print_in_bill: bool print_in_bill: bool
+1 -1
View File
@@ -30,5 +30,5 @@ class MenuCategoryLink(BaseModel):
id_: uuid.UUID id_: uuid.UUID
name: str | None = None name: str | None = None
enabled: bool | None = None enabled: bool | None = None
skus: list[ProductLink] skus: list[ProductLink] | None = None
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@@ -13,7 +13,7 @@ import { ProductService } from '../../product/product.service';
import { BundleItem } from '../bundle'; import { BundleItem } from '../bundle';
export interface BundleDetailDialogFormData { export interface BundleDetailDialogFormData {
product: BundleItem | ProductQuery | string; product: ProductQuery | string;
quantity: number; quantity: number;
salePrice: number; salePrice: number;
printInBill: boolean; printInBill: boolean;
@@ -44,7 +44,7 @@ export class BundleDetailDialogComponent {
formModel = linkedSignal<BundleItem, BundleDetailDialogFormData>({ formModel = linkedSignal<BundleItem, BundleDetailDialogFormData>({
source: () => this.data.item, source: () => this.data.item,
computation: (item) => ({ computation: (item) => ({
product: item, product: item.sku ?? '',
quantity: Number(item.quantity ?? 1), quantity: Number(item.quantity ?? 1),
salePrice: Number(item.salePrice ?? 0), salePrice: Number(item.salePrice ?? 0),
printInBill: item.printInBill ?? false, printInBill: item.printInBill ?? false,
@@ -84,8 +84,7 @@ export class BundleDetailDialogComponent {
return; return;
} }
this.data.item.itemId = product.id ?? ''; this.data.item.sku = product;
this.data.item.name = product.name ?? '';
this.data.item.quantity = quantity; this.data.item.quantity = quantity;
this.data.item.salePrice = salePrice; this.data.item.salePrice = salePrice;
this.data.item.printInBill = formValue.printInBill ?? false; this.data.item.printInBill = formValue.printInBill ?? false;
@@ -24,7 +24,7 @@
<mat-label>Menu Category</mat-label> <mat-label>Menu Category</mat-label>
<mat-select [formField]="form.menuCategory"> <mat-select [formField]="form.menuCategory">
@for (mc of menuCategories(); track mc) { @for (mc of menuCategories(); track mc) {
<mat-option [value]="mc.id"> <mat-option [value]="mc">
{{ mc.name }} {{ mc.name }}
</mat-option> </mat-option>
} }
@@ -35,7 +35,7 @@
<mat-label>Sale Category</mat-label> <mat-label>Sale Category</mat-label>
<mat-select [formField]="form.saleCategory"> <mat-select [formField]="form.saleCategory">
@for (sc of saleCategories(); track sc) { @for (sc of saleCategories(); track sc) {
<mat-option [value]="sc.id"> <mat-option [value]="sc">
{{ sc.name }} {{ sc.name }}
</mat-option> </mat-option>
} }
@@ -61,9 +61,9 @@
<input <input
type="text" type="text"
matInput matInput
[formField]="form.addRow.itemId" [formField]="form.addRow.sku"
[matAutocomplete]="autoItem" [matAutocomplete]="autoItem"
[value]="displayFn(formModel().addRow.itemId)" [value]="displayFn(formModel().addRow.sku)"
/> />
<mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn"> <mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn">
@for (p of itemProducts(); track p) { @for (p of itemProducts(); track p) {
@@ -89,11 +89,11 @@
<div class="row-container wrapped"> <div class="row-container wrapped">
<mat-table [dataSource]="formModel().items" aria-label="Bundle Items" class="flex-auto"> <mat-table [dataSource]="formModel().items" aria-label="Bundle Items" class="flex-auto">
<!-- Name Column (name is "name (units)") --> <!-- Name Column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>Item</mat-header-cell> <mat-header-cell *matHeaderCellDef>Item</mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
{{ row.name }} {{ row.sku?.name }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
@@ -34,7 +34,7 @@ export interface BundleDetailFormData {
hasHappyHour: boolean; hasHappyHour: boolean;
isNotAvailable: boolean; isNotAvailable: boolean;
addRow: { addRow: {
itemId: ProductQuery | string; sku: ProductQuery | string;
quantity: number; quantity: number;
salePrice: number; salePrice: number;
printInBill: boolean; printInBill: boolean;
@@ -89,7 +89,7 @@ export class BundleDetailComponent {
isNotAvailable: itemVal.isNotAvailable ?? false, isNotAvailable: itemVal.isNotAvailable ?? false,
items, items,
addRow: { addRow: {
itemId: '', sku: '',
quantity: 1, quantity: 1,
salePrice: 0, salePrice: 0,
printInBill: true, printInBill: true,
@@ -101,7 +101,7 @@ export class BundleDetailComponent {
form = form(this.formModel); form = form(this.formModel);
private itemProductSearch = computed(() => { private itemProductSearch = computed(() => {
const v = this.form.addRow.itemId().value(); const v = this.form.addRow.sku().value();
return typeof v === 'string' ? v : null; return typeof v === 'string' ? v : null;
}); });
@@ -116,7 +116,7 @@ export class BundleDetailComponent {
constructor() { constructor() {
effect(() => { effect(() => {
const v = this.form.addRow.itemId().value(); const v = this.form.addRow.sku().value();
if (typeof v === 'object' && v !== null) { if (typeof v === 'object' && v !== null) {
this.formModel.update((f) => ({ this.formModel.update((f) => ({
...f, ...f,
@@ -147,7 +147,7 @@ export class BundleDetailComponent {
this.formModel.update((f) => ({ this.formModel.update((f) => ({
...f, ...f,
addRow: { addRow: {
itemId: '', sku: '',
quantity: 1, quantity: 1,
salePrice: 0, salePrice: 0,
printInBill: true, printInBill: true,
@@ -158,7 +158,7 @@ export class BundleDetailComponent {
addRow() { addRow() {
const formValue = this.form().value().addRow; const formValue = this.form().value().addRow;
if (!formValue) return; if (!formValue) return;
const itemProduct = formValue.itemId as ProductQuery; const itemProduct = formValue.sku as ProductQuery;
if (!itemProduct || typeof itemProduct === 'string') { if (!itemProduct || typeof itemProduct === 'string') {
this.snackBar.open('Please select a product', 'Error'); this.snackBar.open('Please select a product', 'Error');
@@ -178,8 +178,7 @@ export class BundleDetailComponent {
} }
const bi = new BundleItem({ const bi = new BundleItem({
itemId: itemProduct.id, sku: itemProduct,
name: itemProduct.name ?? '',
quantity, quantity,
salePrice, salePrice,
printInBill: formValue.printInBill || false, printInBill: formValue.printInBill || false,
@@ -72,7 +72,9 @@
<mat-icon> <mat-icon>
{{ item.printInBill ? 'visibility' : 'visibility_off' }} {{ item.printInBill ? 'visibility' : 'visibility_off' }}
</mat-icon> </mat-icon>
<div class="item-name">{{ item.name }} x {{ item.quantity }} @ {{ item.salePrice | currency: 'INR' }}</div> <div class="item-name">
{{ item.sku?.name }} x {{ item.quantity }} @ {{ item.salePrice | currency: 'INR' }}
</div>
} }
</div> </div>
</mat-cell> </mat-cell>
@@ -88,7 +88,7 @@ export class BundleListComponent {
...b, ...b,
menuCategory: b.menuCategory?.name ?? '', menuCategory: b.menuCategory?.name ?? '',
items: (b.items ?? []) items: (b.items ?? [])
.map((i) => `${i.name}${i.quantity && i.quantity !== 1 ? ` x${i.quantity}` : ''}`) .map((i) => `${i.sku?.name ?? ''}${i.quantity && i.quantity !== 1 ? ` x${i.quantity}` : ''}`)
.join(' | '), .join(' | '),
})); }));
+2 -3
View File
@@ -3,14 +3,13 @@ import { ProductQuery } from '../core/product-query';
import { SaleCategory } from '../core/sale-category'; import { SaleCategory } from '../core/sale-category';
export class BundleItem { export class BundleItem {
sku: ProductQuery | undefined; id?: string;
name: string; sku?: ProductQuery;
salePrice: number; salePrice: number;
quantity: number; quantity: number;
printInBill: boolean; printInBill: boolean;
public constructor(init?: Partial<BundleItem>) { public constructor(init?: Partial<BundleItem>) {
this.name = '';
this.salePrice = 0; this.salePrice = 0;
this.quantity = 0; this.quantity = 0;
this.printInBill = true; this.printInBill = true;
@@ -1,6 +1,6 @@
/* eslint-disable @angular-eslint/no-input-rename */ /* eslint-disable @angular-eslint/no-input-rename */
import { PercentPipe } from '@angular/common'; import { PercentPipe } from '@angular/common';
import { Component, inject, signal, effect, computed, debounced, afterNextRender, input } from '@angular/core'; import { Component, inject, linkedSignal, effect, computed, debounced, afterNextRender, input } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
@@ -51,8 +51,11 @@ export class CustomerListComponent {
sortInput = input<string>('name', { alias: 'f' }); sortInput = input<string>('name', { alias: 'f' });
direction = input<string>('asc', { alias: 'd' }); direction = input<string>('asc', { alias: 'd' });
formModel = signal<CustomerListFormData>({ formModel = linkedSignal({
filter: this.route.snapshot.queryParamMap.get('q') ?? '', source: this.q,
computation: (q): CustomerListFormData => ({
filter: q,
}),
}); });
form = form(this.formModel); form = form(this.formModel);
@@ -1,4 +1,4 @@
import { Component, inject, computed, effect, signal } from '@angular/core'; import { Component, inject, computed, effect, input, linkedSignal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -40,8 +40,21 @@ export class GuestBookListComponent {
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private ser = inject(GuestBookService); private ser = inject(GuestBookService);
formModel = signal<GuestBookListFormData>({ date = input(new Date(), {
date: this.initialDate(), transform: (v: string | null | undefined): Date => {
if (v) {
const parsed = moment(v, 'DD-MMM-YYYY', true);
if (parsed.isValid()) return parsed.toDate();
}
return new Date();
},
});
formModel = linkedSignal({
source: this.date,
computation: (d): GuestBookListFormData => ({
date: d,
}),
}); });
form = form(this.formModel); form = form(this.formModel);
@@ -65,13 +78,4 @@ export class GuestBookListComponent {
}); });
}); });
} }
private initialDate(): Date {
const qp = this.route.snapshot.queryParamMap.get('date');
if (qp) {
const parsed = moment(qp, 'DD-MMM-YYYY', true);
if (parsed.isValid()) return parsed.toDate();
}
return new Date();
}
} }
@@ -19,10 +19,7 @@ export class GuestBookService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
get( get(id: Signal<string | null>, type?: Signal<GuestBookType | string>): HttpResourceRef<GuestBook | undefined> {
id: Signal<string | null>,
type?: Signal<GuestBookType | string>,
): HttpResourceRef<GuestBook | undefined> {
return httpResource<GuestBook>(() => { return httpResource<GuestBook>(() => {
const idVal = id(); const idVal = id();
const typeVal = type ? type() : ''; const typeVal = type ? type() : '';
@@ -27,29 +27,27 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput [formField]="form.name" /> <input matInput [value]="formModel().name" readonly />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Address</mat-label> <mat-label>Address</mat-label>
<textarea matInput [formField]="form.address"> </textarea> <textarea matInput [value]="formModel().address" readonly> </textarea>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-checkbox [formField]="form.printInBill">Print in Bill?</mat-checkbox> <mat-checkbox [checked]="formModel().printInBill" [disabled]="true">Print in Bill?</mat-checkbox>
</div> </div>
<p></p> <p></p>
<div class="discounts"> <div class="discounts">
@for (r of item.discounts; track r; let i = $index) { <ul>
<div class="row-container"> @for (r of formModel().discounts; track r.id) {
<mat-form-field class="flex-auto"> @if (r.discount > 0) {
<mat-label>Discount on {{ r.name }}</mat-label> <li>{{ r.name }} - {{ r.discount | percent: '1.2-2' }}</li>
<input matInput [formField]="form.discounts[i].discount" /> }
<span matTextSuffix>%</span> }
</mat-form-field> </ul>
</div>
}
</div> </div>
</form> </form>
</mat-dialog-content> </mat-dialog-content>
@@ -1,4 +1,5 @@
import { CdkScrollableModule } from '@angular/cdk/scrolling'; import { CdkScrollableModule } from '@angular/cdk/scrolling';
import { PercentPipe } from '@angular/common';
import { Component, inject, computed, debounced, linkedSignal, signal } from '@angular/core'; import { Component, inject, computed, debounced, linkedSignal, signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatAutocompleteSelectedEvent, MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent, MatAutocompleteModule } from '@angular/material/autocomplete';
@@ -8,9 +9,9 @@ import { MatOptionModule } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { round } from 'mathjs';
import { Customer } from '../../core/customer'; import { Customer } from '../../core/customer';
import { CustomerDiscount } from '../../core/customer-discount';
import { CustomerService } from '../../customers/customer.service'; import { CustomerService } from '../../customers/customer.service';
export interface ChooseCustomerFormData { export interface ChooseCustomerFormData {
@@ -19,9 +20,7 @@ export interface ChooseCustomerFormData {
phone: string; phone: string;
address: string; address: string;
printInBill: boolean; printInBill: boolean;
discounts: { discounts: CustomerDiscount[];
discount: number | null;
}[];
} }
@Component({ @Component({
@@ -37,6 +36,7 @@ export interface ChooseCustomerFormData {
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatOptionModule, MatOptionModule,
PercentPipe,
FormField, FormField,
], ],
}) })
@@ -55,9 +55,7 @@ export class ChooseCustomerComponent {
phone: data.phone, phone: data.phone,
address: data.address ?? '', address: data.address ?? '',
printInBill: data.printInBill, printInBill: data.printInBill,
discounts: data.discounts.map((x) => ({ discounts: data.discounts ?? [],
discount: x.discount ? x.discount * 100 : null,
})),
}), }),
}); });
@@ -70,7 +68,9 @@ export class ChooseCustomerComponent {
debouncedPhone = debounced(this.phoneSignal, 150); debouncedPhone = debounced(this.phoneSignal, 150);
customersResource = this.ser.autocomplete(this.debouncedPhone.value); customersResource = this.ser.autocomplete(this.debouncedPhone.value);
customers = computed(() => { return this.customersResource.value() ?? [] }); customers = computed(() => {
return this.customersResource.value() ?? [];
});
save() { save() {
const customer = this.getItem(); const customer = this.getItem();
@@ -92,7 +92,6 @@ export class ChooseCustomerComponent {
getItem(): Customer { getItem(): Customer {
const formModel = this.formModel(); const formModel = this.formModel();
const array = formModel.discounts;
return new Customer({ return new Customer({
id: formModel.id, id: formModel.id,
@@ -100,14 +99,7 @@ export class ChooseCustomerComponent {
phone: formModel.phone ?? '', phone: formModel.phone ?? '',
address: formModel.address ?? '', address: formModel.address ?? '',
printInBill: formModel.printInBill ?? false, printInBill: formModel.printInBill ?? false,
discounts: formModel.discounts.map((item, index) => { discounts: formModel.discounts.map((item) => ({ ...item })),
const array_item = array?.[index];
return {
...this.item().discounts?.[index],
discount:
array_item && array_item?.discount ? Math.max(Math.min(round(array_item.discount / 100, 5), 100), 0) : 0,
};
}),
}); });
} }
} }
@@ -12,12 +12,15 @@ export class CustomerDiscountsService {
private injector = inject(Injector); private injector = inject(Injector);
list(id: Signal<string | undefined>): HttpResourceRef<DiscountItem[] | undefined> { list(id: Signal<string | undefined>): HttpResourceRef<DiscountItem[] | undefined> {
return httpResource<DiscountItem[]>(() => { return httpResource<DiscountItem[]>(
const idVal = id(); () => {
return idVal === undefined ? url : `${url}/${idVal}`; const idVal = id();
}, { return idVal === undefined ? url : `${url}/${idVal}`;
injector: this.injector, },
}); {
injector: this.injector,
},
);
} }
listForDiscount(): HttpResourceRef<{ name: string; discount: number; discountLimit: number }[] | undefined> { listForDiscount(): HttpResourceRef<{ name: string; discount: number; discountLimit: number }[] | undefined> {