fds
This commit is contained in:
1 parent
311464e593
commit
ff537edb06
19 files changed
+456
-393
No files matched your search
Vendored
+12
-1
@@ -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
@@ -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,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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Vendored
+22
@@ -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
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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(' | '),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in new issue
Block a user