Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dfbfd8fcb | ||
|
|
221fadfdfc | ||
|
|
11b93cb11c | ||
|
|
302a4eb311 | ||
|
|
217f636573 | ||
|
|
95ac73bbc4 | ||
|
|
3d86f8706f | ||
|
|
30cbb60efe | ||
|
|
227f5b5f74 | ||
|
|
50974c7574 | ||
|
|
c8e1ab74b6 | ||
|
|
85ba7b326a | ||
|
|
458dfaad50 | ||
|
|
0a9675f270 | ||
|
|
6cf42d3641 | ||
|
|
cf680cd254 | ||
|
|
80d21ec31e | ||
|
|
4377016cda | ||
|
|
67209886b9 | ||
|
|
4e1c0d1ec7 | ||
|
|
c7412f080d | ||
|
|
d023089720 | ||
|
|
dd02a97ba2 |
No files matched your search
Vendored
+20
-1
@@ -18,7 +18,26 @@
|
||||
"module": "barker",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/barker",
|
||||
"justMyCode": true
|
||||
"justMyCode": true,
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
},
|
||||
{
|
||||
"name": "Python: FastAPI (Live Reload)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"barker.main:app",
|
||||
"--host",
|
||||
"0.0.0.0",
|
||||
"--port",
|
||||
"9995",
|
||||
"--reload"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/barker",
|
||||
"subProcess": true,
|
||||
"justMyCode": true,
|
||||
"envFile": "${workspaceFolder}/.env"
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:current-trixie-slim AS base
|
||||
FROM node:lts-trixie-slim AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
|
||||
+3
-3
@@ -6,9 +6,9 @@ SQLALCHEMY_DATABASE_URI=postgresql+psycopg://postgres:123456@postgres:5432/petty
|
||||
MODULE_NAME=barker.main
|
||||
PROJECT_NAME=barker
|
||||
MAX_WORKERS=4
|
||||
NATS_URL=nats://{{ nats_host }}:{{ nats_port }}
|
||||
SUBJECT="barker.print"
|
||||
STREAM="barker_print_stream"
|
||||
NATS_URL={{ nats_url }}
|
||||
SUBJECT=barker.print
|
||||
STREAM=barker_print_stream
|
||||
SECRET_KEY={{ secret_key }}
|
||||
MIDDLEWARE_SECRET_KEY={{ middleware_key }}
|
||||
ALGORITHM=HS256
|
||||
|
||||
@@ -24,4 +24,4 @@
|
||||
create: true
|
||||
mode: "0644"
|
||||
when: not caddy_snippet_present
|
||||
notify: Restart caddy container
|
||||
notify: Reload Caddy configuration
|
||||
@@ -6,3 +6,4 @@ middleware_key: c4b0b98ff9
|
||||
host: knox.mozimo.in
|
||||
host_directory: "barker-{{ app_name }}"
|
||||
docker_network: "{{ host_directory }}_net"
|
||||
nats_url: "nats://clair.lan.tanshu.com:4222"
|
||||
@@ -1 +1 @@
|
||||
__version__ = "15.0.0"
|
||||
__version__ = "15.1.6"
|
||||
@@ -4,31 +4,31 @@ from typing import Any
|
||||
|
||||
def get_date(session: dict[str, Any]) -> str:
|
||||
if "date" not in session:
|
||||
session["date"] = date.today().strftime("%d-%b-%Y")
|
||||
session["date"] = date.today().isoformat()
|
||||
return session["date"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def set_date(date_: str | date, session: dict[str, Any]) -> None:
|
||||
session["date"] = date_ if isinstance(date_, str) else date_.strftime("%d-%b-%Y")
|
||||
session["date"] = date_ if isinstance(date_, str) else date_.isoformat()
|
||||
|
||||
|
||||
def get_start_date(session: dict[str, Any]) -> str:
|
||||
if "start" not in session:
|
||||
session["start"] = get_first_day(date.today()).strftime("%d-%b-%Y")
|
||||
session["start"] = get_first_day(date.today()).isoformat()
|
||||
return session["start"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def get_finish_date(session: dict[str, Any]) -> str:
|
||||
if "finish" not in session:
|
||||
session["finish"] = get_last_day(date.today()).strftime("%d-%b-%Y")
|
||||
session["finish"] = get_last_day(date.today()).isoformat()
|
||||
return session["finish"] # type: ignore[no-any-return]
|
||||
|
||||
|
||||
def set_period(start: str | date | None, finish: str | date | None, session: dict[str, Any]) -> None:
|
||||
if start is not None:
|
||||
session["start"] = start if isinstance(start, str) else start.strftime("%d-%b-%Y")
|
||||
session["start"] = start if isinstance(start, str) else start.isoformat()
|
||||
if finish is not None:
|
||||
session["finish"] = finish if isinstance(finish, str) else finish.strftime("%d-%b-%Y")
|
||||
session["finish"] = finish if isinstance(finish, str) else finish.isoformat()
|
||||
|
||||
|
||||
def get_first_day(dt: date, d_years: int = 0, d_months: int = 0) -> date:
|
||||
|
||||
@@ -17,6 +17,7 @@ from .db.base import reg # noqa: F401
|
||||
from .db.friendly_db_error import constraint_to_friendly
|
||||
from .routers import (
|
||||
bundle,
|
||||
client_log,
|
||||
customer,
|
||||
customer_discount,
|
||||
db_settings,
|
||||
@@ -162,8 +163,13 @@ app.include_router(split.router, prefix="/api", tags=["voucher"])
|
||||
app.include_router(change.router, prefix="/api/voucher", tags=["voucher"])
|
||||
|
||||
app.include_router(health.router, prefix="/health", tags=["health"])
|
||||
app.include_router(client_log.router, prefix="/api/client-logs", tags=["logs"])
|
||||
app.frontend("/", directory="frontend", check_dir="auto")
|
||||
|
||||
|
||||
def init() -> None:
|
||||
uvicorn.run(app, host=settings.HOST, port=9995)
|
||||
uvicorn.run("barker.main:app", host=settings.HOST, port=settings.PORT, reload=settings.DEBUG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
init()
|
||||
@@ -12,11 +12,11 @@ from ..models.stock_keeping_unit import StockKeepingUnit
|
||||
|
||||
|
||||
def query_date(d: str | None = None) -> date:
|
||||
return date.today() if d is None else datetime.strptime(d, "%d-%b-%Y").date()
|
||||
return date.today() if d is None else date.fromisoformat(d)
|
||||
|
||||
|
||||
def optional_query_date(d: str | None = None) -> date | None:
|
||||
return None if d is None else datetime.strptime(d, "%d-%b-%Y").date()
|
||||
return None if d is None else date.fromisoformat(d)
|
||||
|
||||
|
||||
def effective_date(d: str | None = None) -> date:
|
||||
@@ -26,7 +26,7 @@ def effective_date(d: str | None = None) -> date:
|
||||
+ timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES - settings.NEW_DAY_OFFSET_MINUTES)
|
||||
).date()
|
||||
if d is None
|
||||
else datetime.strptime(d, "%d-%b-%Y").date()
|
||||
else date.fromisoformat(d)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import logging
|
||||
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
|
||||
from fastapi import APIRouter, Request, status
|
||||
from jwt import PyJWTError
|
||||
|
||||
from ..core.config import settings
|
||||
from ..schemas.client_log import ClientErrorPayload
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger("frontend.client")
|
||||
|
||||
|
||||
def _extract_user_from_request(request: Request) -> str | None:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
return None
|
||||
token = auth_header.removeprefix("Bearer ").strip()
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
user: Any = payload.get("sub")
|
||||
return str(user) if user else None
|
||||
except PyJWTError, Exception:
|
||||
return None
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def log_client_error(payload: ClientErrorPayload, request: Request) -> None:
|
||||
user = _extract_user_from_request(request)
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
user_agent = request.headers.get("user-agent", "unknown")
|
||||
|
||||
log_context = {
|
||||
"url": payload.url,
|
||||
"source": payload.source,
|
||||
"status_code": payload.status_code,
|
||||
"endpoint": payload.endpoint,
|
||||
"component": payload.component,
|
||||
"user": user or "anonymous",
|
||||
"client_ip": client_ip,
|
||||
"user_agent": user_agent,
|
||||
"extra": payload.extra,
|
||||
}
|
||||
|
||||
log_msg = (
|
||||
f"Frontend [{payload.source}] {payload.message} | "
|
||||
f"URL: {payload.url} | "
|
||||
f"User: {user or 'anonymous'} | "
|
||||
f"IP: {client_ip}"
|
||||
)
|
||||
if payload.endpoint:
|
||||
log_msg += f" | Endpoint: {payload.endpoint} (Status: {payload.status_code})"
|
||||
|
||||
if payload.level == "warning":
|
||||
logger.warning(log_msg, extra={"client_context": log_context})
|
||||
elif payload.level == "info":
|
||||
logger.info(log_msg, extra={"client_context": log_context})
|
||||
else:
|
||||
logger.error(
|
||||
log_msg + (f"\nStack:\n{payload.stack}" if payload.stack else ""),
|
||||
extra={"client_context": log_context},
|
||||
)
|
||||
@@ -44,9 +44,8 @@ def save(
|
||||
else:
|
||||
customer.name = data.name or customer.name
|
||||
customer.address = data.address or customer.address
|
||||
td = timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
|
||||
booking_date = None if data.booking_date is None else data.booking_date - td
|
||||
arrival_date = None if data.arrival_date is None else data.arrival_date - td
|
||||
booking_date = data.booking_date.astimezone(UTC).replace(tzinfo=None) if data.booking_date else None
|
||||
arrival_date = data.arrival_date.astimezone(UTC).replace(tzinfo=None) if data.arrival_date else None
|
||||
item = GuestBook(
|
||||
pax=data.pax,
|
||||
customer=customer,
|
||||
@@ -72,11 +71,8 @@ def update_route(
|
||||
item.customer.phone = data.phone
|
||||
item.customer.address = data.address
|
||||
item.pax = data.pax
|
||||
td = timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
|
||||
booking_date = None if data.booking_date is None else data.booking_date - td
|
||||
arrival_date = None if data.arrival_date is None else data.arrival_date - td
|
||||
item.booking_date = booking_date
|
||||
item.arrival_date = arrival_date
|
||||
item.booking_date = data.booking_date.astimezone(UTC).replace(tzinfo=None) if data.booking_date else None
|
||||
item.arrival_date = data.arrival_date.astimezone(UTC).replace(tzinfo=None) if data.arrival_date else None
|
||||
item.last_edit_date = datetime.now()
|
||||
item.notes = data.notes
|
||||
item.type_ = data.type_
|
||||
@@ -105,11 +101,12 @@ def show_blank(
|
||||
def show_list(
|
||||
user: Annotated[UserToken, Security(get_user)],
|
||||
db: SessionDep,
|
||||
q: str | None = None,
|
||||
q: date | None = None,
|
||||
) -> schemas.GuestBookList:
|
||||
d = date.today() if q is None or q == "" else datetime.strptime(q, "%d-%b-%Y")
|
||||
s = d + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES)
|
||||
f = d + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES, days=1)
|
||||
d = date.today() if q is None else q
|
||||
dt = datetime.combine(d, datetime.min.time())
|
||||
s = dt + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES)
|
||||
f = dt + timedelta(minutes=settings.NEW_DAY_OFFSET_MINUTES - settings.TIMEZONE_OFFSET_MINUTES, days=1)
|
||||
list_ = (
|
||||
select(GuestBook)
|
||||
.where(
|
||||
@@ -124,7 +121,7 @@ def show_list(
|
||||
guest_book: list[schemas.GuestBookListItem] = []
|
||||
count = 0
|
||||
for i, item in enumerate(db.execute(list_).scalars().all()):
|
||||
status = GuestBookType(item.type_.name) if item.status is None else GuestBookType(item.status.status.name)
|
||||
item_status = GuestBookType(item.type_.name) if item.status is None else GuestBookType(item.status.status.name)
|
||||
if item.type_ != GuestBookType.booking:
|
||||
count += item.pax
|
||||
gbli = schemas.GuestBookListItem(
|
||||
@@ -137,7 +134,7 @@ def show_list(
|
||||
booking_date=item.booking_date,
|
||||
arrival_date=item.arrival_date,
|
||||
last_edit_date=item.last_edit_date,
|
||||
status=status,
|
||||
status=item_status,
|
||||
table_id=None if item.status is None else item.status.food_table.id,
|
||||
voucher_id=None if item.status is None else item.status.voucher_id,
|
||||
table_name=None if item.status is None else item.status.food_table.name,
|
||||
@@ -170,25 +167,22 @@ def show_id(
|
||||
|
||||
|
||||
def guest_book_info(item: GuestBook) -> schemas.GuestBook:
|
||||
td = timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
|
||||
booking_date = None if item.booking_date is None else item.booking_date + td
|
||||
arrival_date = None if item.arrival_date is None else item.arrival_date + td
|
||||
return schemas.GuestBook(
|
||||
id_=item.id,
|
||||
name=item.customer.name,
|
||||
phone=item.customer.phone,
|
||||
pax=item.pax,
|
||||
address=item.customer.address,
|
||||
booking_date=booking_date,
|
||||
arrival_date=arrival_date,
|
||||
last_edit_date=item.last_edit_date + td,
|
||||
booking_date=item.booking_date,
|
||||
arrival_date=item.arrival_date,
|
||||
last_edit_date=item.last_edit_date,
|
||||
notes=item.notes,
|
||||
type_=GuestBookType(item.type_.name),
|
||||
)
|
||||
|
||||
|
||||
def blank_guest_book_info(type_: GuestBookType) -> schemas.GuestBookIn:
|
||||
now = datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
|
||||
now = datetime.now()
|
||||
booking_date = None if type_ != GuestBookType.booking else now
|
||||
arrival_date = None if type_ != GuestBookType.walk_in else now
|
||||
return schemas.GuestBookIn(
|
||||
|
||||
@@ -9,7 +9,7 @@ def report_start_date(s: str | None = None) -> date:
|
||||
return (
|
||||
(datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).date()
|
||||
if s is None
|
||||
else datetime.strptime(s, "%d-%b-%Y").date()
|
||||
else datetime.fromisoformat(s).date()
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def report_finish_date(f: str | None = None) -> date:
|
||||
return (
|
||||
(datetime.now(UTC).replace(tzinfo=None) + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).date()
|
||||
if f is None
|
||||
else datetime.strptime(f, "%d-%b-%Y").date()
|
||||
else datetime.fromisoformat(f).date()
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Security
|
||||
from sqlalchemy import select, update
|
||||
|
||||
from ..core.config import settings
|
||||
from ..core.security import get_current_active_user as get_user
|
||||
from ..db.session import SessionDep
|
||||
from ..models.food_table import FoodTable
|
||||
@@ -119,7 +117,7 @@ def show_running(user: Annotated[UserToken, Security(get_user)], db: SessionDep)
|
||||
ft.status = item.status.status
|
||||
ft.voucher_id = item.status.voucher_id
|
||||
ft.pax = item.status.voucher.pax
|
||||
ft.date_ = item.status.voucher.date + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)
|
||||
ft.date_ = item.status.voucher.date
|
||||
ft.amount = int(round(item.status.voucher.amount, 0))
|
||||
if item.status.guest is not None:
|
||||
ft.guest = item.status.guest.customer.name
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
@@ -6,9 +6,6 @@ from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SerializationInfo,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
@@ -19,17 +16,6 @@ from .sale_category import SaleCategoryLink
|
||||
class BeerConsumptionReportItem(BaseModel):
|
||||
date_: date
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
# dynamic fields (e.g., regular, nc, kot)
|
||||
dynamic_amounts: Annotated[dict[str, Daf], Field(default_factory=dict)]
|
||||
|
||||
@@ -65,25 +51,3 @@ class BeerConsumptionReport(BaseModel):
|
||||
headers: list[str]
|
||||
data: list[BeerConsumptionReportItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
|
||||
@@ -12,42 +12,9 @@ class BillSettlementItem(BaseModel):
|
||||
amount: Daf
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | datetime) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
|
||||
class BillSettlement(BaseModel):
|
||||
start_date: date
|
||||
finish_date: date
|
||||
amounts: list[BillSettlementItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
from .user import UserLink
|
||||
@@ -33,13 +33,6 @@ class InfoItem(BaseModel):
|
||||
amount: Daf
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | datetime) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
|
||||
class CashierReport(BaseModel):
|
||||
start_date: date
|
||||
@@ -50,25 +43,3 @@ class CashierReport(BaseModel):
|
||||
amounts: list[NameAmount]
|
||||
info: dict[str, list[InfoItem]]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -0,0 +1,15 @@
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ClientErrorPayload(BaseModel):
|
||||
message: str
|
||||
stack: str | None = None
|
||||
url: str
|
||||
source: Literal["unhandled_error", "http_resource", "service_error", "global_error"] = "unhandled_error"
|
||||
status_code: int | None = None
|
||||
endpoint: str | None = None
|
||||
level: Literal["error", "warning", "info"] = "error"
|
||||
component: str | None = None
|
||||
extra: dict[str, Any] | None = None
|
||||
@@ -3,7 +3,7 @@ import uuid
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from . import to_camel
|
||||
from .section import SectionLink
|
||||
@@ -23,14 +23,6 @@ class Device(DeviceIn):
|
||||
last_date: datetime | None = None
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_serializer("creation_date")
|
||||
def serialize_creation_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("last_date")
|
||||
def serialize_last_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
|
||||
class DeviceLink(BaseModel):
|
||||
id_: uuid.UUID
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
|
||||
@@ -16,25 +16,3 @@ class DiscountReport(BaseModel):
|
||||
finish_date: date
|
||||
amounts: list[DiscountReportItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: date | str) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: date | str) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -9,7 +9,6 @@ from pydantic import (
|
||||
Field,
|
||||
SerializationInfo,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
@@ -29,32 +28,6 @@ class GuestBookIn(BaseModel):
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("booking_date", mode="before")
|
||||
@classmethod
|
||||
def parse_booking_date(cls, value: str | datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("booking_date")
|
||||
def serialize_booking_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
@field_validator("arrival_date", mode="before")
|
||||
@classmethod
|
||||
def parse_arrival_date(cls, value: str | datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("arrival_date")
|
||||
def serialize_arrival_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
@model_validator(mode="after")
|
||||
def nulls(self) -> GuestBookIn:
|
||||
if self.arrival_date is None and self.booking_date is None:
|
||||
@@ -68,17 +41,6 @@ class GuestBook(GuestBookIn):
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("last_edit_date", mode="before")
|
||||
@classmethod
|
||||
def parse_last_edit_date(cls, value: str | datetime) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("last_edit_date")
|
||||
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
|
||||
class GuestBookListItem(BaseModel):
|
||||
id_: uuid.UUID
|
||||
@@ -98,39 +60,6 @@ class GuestBookListItem(BaseModel):
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("booking_date", mode="before")
|
||||
@classmethod
|
||||
def parse_booking_date(cls, value: str | datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("booking_date")
|
||||
def serialize_booking_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
@field_validator("arrival_date", mode="before")
|
||||
@classmethod
|
||||
def parse_arrival_date(cls, value: str | datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("arrival_date")
|
||||
def serialize_arrival_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
|
||||
@field_validator("last_edit_date", mode="before")
|
||||
@classmethod
|
||||
def parse_last_edit_date(cls, value: str | datetime) -> datetime:
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("last_edit_date")
|
||||
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y %H:%M")
|
||||
@@ -140,14 +69,3 @@ class GuestBookList(BaseModel):
|
||||
date_: date
|
||||
list: list[GuestBookListItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,8 +1,8 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
|
||||
@@ -27,25 +27,3 @@ class MeReport(BaseModel):
|
||||
finish_date: date
|
||||
amounts: list[MeItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,9 +1,9 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from . import to_camel
|
||||
from .sale_category import SaleCategoryLink
|
||||
@@ -27,32 +27,6 @@ class Product(ProductIn):
|
||||
valid_till: date | None = None
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("valid_from", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_from(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_from")
|
||||
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("valid_till", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_till(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_till")
|
||||
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
|
||||
class ProductBlank(BaseModel):
|
||||
name: str
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
@@ -8,9 +8,6 @@ from pydantic import (
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
SerializationInfo,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_serializer,
|
||||
)
|
||||
|
||||
@@ -56,25 +53,3 @@ class ProductSaleReport(BaseModel):
|
||||
amounts: list[ProductSaleReportItem]
|
||||
user: UserLink
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import to_camel
|
||||
|
||||
@@ -11,25 +11,3 @@ class ProductUpdatesReport(BaseModel):
|
||||
report: list[str]
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import to_camel
|
||||
from .section import SectionLink
|
||||
@@ -26,18 +26,3 @@ class RunningTable(BaseModel):
|
||||
bold: bool | None = None
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | datetime | None) -> datetime | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y %H:%M")
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
return value.strftime("%d-%b-%Y %H:%M")
|
||||
@@ -1,8 +1,8 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
from .user import UserLink
|
||||
@@ -21,25 +21,3 @@ class SaleReport(BaseModel):
|
||||
amounts: list[SaleReportItem]
|
||||
user: UserLink
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,10 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from . import Daf, to_camel
|
||||
from .menu_category import MenuCategoryLink
|
||||
@@ -27,29 +27,3 @@ class StockKeepingUnit(BaseModel):
|
||||
valid_from: date | None = None
|
||||
valid_till: date | None = None
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("valid_from", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_from(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_from")
|
||||
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("valid_till", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_till(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_till")
|
||||
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
|
||||
@@ -18,25 +18,3 @@ class TaxReport(BaseModel):
|
||||
finish_date: date
|
||||
amounts: list[TaxReportItem]
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("start_date", mode="before")
|
||||
@classmethod
|
||||
def parse_start_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("start_date")
|
||||
def serialize_start_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("finish_date", mode="before")
|
||||
@classmethod
|
||||
def parse_finish_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("finish_date")
|
||||
def serialize_finish_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -1,10 +1,10 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from . import Daf, to_camel
|
||||
from .menu_category import MenuCategoryLink
|
||||
@@ -23,32 +23,6 @@ class Product(BaseModel):
|
||||
valid_till: date | None = None
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("valid_from", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_from(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_from")
|
||||
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("valid_till", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_till(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_till")
|
||||
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
|
||||
class StockKeepingUnit(BaseModel):
|
||||
id_: uuid.UUID
|
||||
@@ -68,32 +42,6 @@ class StockKeepingUnit(BaseModel):
|
||||
valid_till: date | None = None
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("valid_from", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_from(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_from")
|
||||
def serialize_valid_from(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
@field_validator("valid_till", mode="before")
|
||||
@classmethod
|
||||
def parse_valid_till(cls, value: str | date | None) -> date | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("valid_till")
|
||||
def serialize_valid_till(self, value: date | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y")
|
||||
|
||||
|
||||
class TemporalProduct(BaseModel):
|
||||
products: list[Product]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import uuid
|
||||
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer, field_validator
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import Daf, to_camel
|
||||
|
||||
@@ -20,14 +20,3 @@ class UpdateProductPrices(BaseModel):
|
||||
menu_category_id: uuid.UUID | None = None
|
||||
items: list[UpdateProductPricesItem]
|
||||
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_validator("date_", mode="before")
|
||||
@classmethod
|
||||
def parse_date(cls, value: str | date) -> date:
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
return datetime.strptime(value, "%d-%b-%Y").date()
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: date, _info: SerializationInfo) -> str:
|
||||
return value.strftime("%d-%b-%Y")
|
||||
@@ -2,7 +2,7 @@ import uuid
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, SerializationInfo, field_serializer
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from . import to_camel
|
||||
from .role import RoleItem
|
||||
@@ -41,7 +41,3 @@ class UserList(BaseModel):
|
||||
last_date: datetime | None = None
|
||||
|
||||
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
|
||||
|
||||
@field_serializer("last_date")
|
||||
def serialize_last_date(self, value: datetime | None, _info: SerializationInfo) -> str | None:
|
||||
return None if value is None else value.strftime("%d-%b-%Y %H:%M")
|
||||
@@ -126,13 +126,10 @@ class VoucherBlank(BaseModel):
|
||||
class VoucherOut(BaseModel):
|
||||
id_: uuid.UUID
|
||||
date_: datetime
|
||||
date_tip: datetime
|
||||
pax: int
|
||||
user: UserLink
|
||||
creation_date: datetime
|
||||
creation_date_tip: datetime
|
||||
last_edit_date: datetime
|
||||
last_edit_date_tip: datetime
|
||||
kot_id: int
|
||||
bill_id: str
|
||||
table: TableLink
|
||||
@@ -147,27 +144,3 @@ class VoucherOut(BaseModel):
|
||||
alias_generator=to_camel,
|
||||
populate_by_name=True,
|
||||
)
|
||||
|
||||
@field_serializer("date_")
|
||||
def serialize_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
|
||||
|
||||
@field_serializer("date_tip")
|
||||
def serialize_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
|
||||
|
||||
@field_serializer("creation_date")
|
||||
def serialize_creation_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
|
||||
|
||||
@field_serializer("creation_date_tip")
|
||||
def serialize_creation_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
|
||||
|
||||
@field_serializer("last_edit_date")
|
||||
def serialize_last_edit_date(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%H:%M")
|
||||
|
||||
@field_serializer("last_edit_date_tip")
|
||||
def serialize_last_edit_date_tip(self, value: datetime, _info: SerializationInfo) -> str:
|
||||
return (value + timedelta(minutes=settings.TIMEZONE_OFFSET_MINUTES)).strftime("%d-%b-%Y %H:%M:%S")
|
||||
@@ -30,6 +30,12 @@ port = os.getenv("PORT", "9995")
|
||||
bind_env = os.getenv("BIND", None)
|
||||
use_bind = bind_env if bind_env else f"{host}:{port}"
|
||||
|
||||
# Tell Gunicorn to trust X-Forwarded-For headers from the reverse proxy
|
||||
forwarded_allow_ips = "*"
|
||||
|
||||
# Optional: Ensure proxy protocol is recognized
|
||||
proxy_allow_ips = "*"
|
||||
|
||||
loglevel = os.getenv("LOG_LEVEL", "info")
|
||||
accesslog_var = os.getenv("ACCESS_LOG", "-")
|
||||
accesslog = accesslog_var or None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "barker"
|
||||
version = "15.0.0"
|
||||
version = "15.1.6"
|
||||
description = "Point of Sale for a restaurant"
|
||||
requires-python = ">=3.14"
|
||||
authors = [{ name = "tanshu", email = "git@tanshu.com" }]
|
||||
|
||||
Generated
+1
-1
@@ -231,7 +231,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "barker"
|
||||
version = "15.0.0"
|
||||
version = "15.1.6"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
+12
-5
@@ -32,7 +32,9 @@
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": ["src/styles.sass"],
|
||||
"styles": [
|
||||
"src/styles.sass"
|
||||
],
|
||||
"allowedCommonJsDependencies": [
|
||||
"typed-function",
|
||||
"javascript-natural-sort",
|
||||
@@ -46,8 +48,8 @@
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
"maximumWarning": "1000kb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
@@ -86,14 +88,19 @@
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"schematicCollections": ["@angular-eslint/schematics"],
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
],
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bookie",
|
||||
"version": "15.0.0",
|
||||
"version": "15.1.6",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LayoutModule } from '@angular/cdk/layout';
|
||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
ErrorHandler,
|
||||
LOCALE_ID,
|
||||
importProvidersFrom,
|
||||
isDevMode,
|
||||
@@ -25,15 +26,20 @@ import { MatTableModule } from '@angular/material/table';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
|
||||
import { Settings } from 'luxon';
|
||||
|
||||
import { dateFormat } from './app.environment';
|
||||
import { routes } from './app.routes';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { authInterceptor } from './core/auth.interceptor';
|
||||
import { delayInterceptor } from './core/delay-interceptor';
|
||||
import { GlobalErrorHandler } from './core/global-error-handler';
|
||||
import { httpErrorInterceptor } from './core/http-error.interceptor';
|
||||
import { jwtInterceptor } from './core/jwt.interceptor';
|
||||
import { refreshInterceptor } from './core/refresh.interceptor';
|
||||
|
||||
Settings.defaultLocale = 'en-US';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
importProvidersFrom(
|
||||
@@ -61,6 +67,7 @@ export const appConfig: ApplicationConfig = {
|
||||
authInterceptor,
|
||||
jwtInterceptor,
|
||||
refreshInterceptor,
|
||||
httpErrorInterceptor,
|
||||
...(isDevMode() ? [delayInterceptor] : []),
|
||||
]),
|
||||
),
|
||||
@@ -71,6 +78,8 @@ export const appConfig: ApplicationConfig = {
|
||||
}),
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
|
||||
{ provide: MAT_DATE_LOCALE, useValue: 'en-US' },
|
||||
{ provide: DateAdapter, useClass: LuxonDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: dateFormat },
|
||||
{ provide: MAT_SNACK_BAR_DEFAULT_OPTIONS, useValue: { duration: 3000 } },
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
export const environment = {
|
||||
production: true,
|
||||
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
|
||||
version: '15.0.0',
|
||||
version: '15.1.6',
|
||||
};
|
||||
|
||||
export const dateFormat = {
|
||||
parse: {
|
||||
dateInput: 'dd-MMM-yyyy',
|
||||
dateTimeInput: 'dd-MMM-yyyy HH:mm',
|
||||
timeInput: 'hh:mm a',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'dd-MMM-yyyy',
|
||||
dateTimeInput: 'dd-MMM-yyyy HH:mm',
|
||||
timeInput: 'hh:mm a',
|
||||
timeOptionLabel: 'hh:mm a',
|
||||
monthYearLabel: 'MMM yyyy',
|
||||
|
||||
@@ -42,13 +42,13 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select [formField]="form.saleCategory">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
@for (sc of saleCategories(); track sc.id) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@for (error of form.saleCategory().errors(); track error) {
|
||||
@for (error of form.saleCategory().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@@ -81,8 +81,8 @@ export class BeerSaleReportComponent {
|
||||
computation: (info): BeerSaleReportFormData => {
|
||||
if (info) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(info.startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(info.finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(info.startDate),
|
||||
finishDate: DateTime.fromISO(info.finishDate),
|
||||
saleCategory: info.saleCategory?.id ?? '',
|
||||
regular: info.regular,
|
||||
happy: info.happy,
|
||||
@@ -131,8 +131,8 @@ export class BeerSaleReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new BeerSaleReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
saleCategory: this.saleCategories().find((sc) => sc.id === formModel.saleCategory),
|
||||
regular: formModel.regular,
|
||||
happy: formModel.happy,
|
||||
|
||||
@@ -56,8 +56,8 @@ export class BillSettlementReportComponent {
|
||||
computation: (infoVal): BillSettlementReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -86,8 +86,8 @@ export class BillSettlementReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new BillSettlementReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
@if (productsResource.isLoading()) {
|
||||
<app-skeleton-loader type="card" [count]="1" />
|
||||
} @else if (productsResource.error()) {
|
||||
<app-error-state (retryAction)="productsResource.reload()" />
|
||||
} @else {
|
||||
<h1 mat-dialog-title>Edit Bundle Item</h1>
|
||||
<h1 mat-dialog-title>Edit Bundle Item</h1>
|
||||
|
||||
<div mat-dialog-content>
|
||||
<form>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Item</mat-label>
|
||||
<input matInput type="text" [formField]="form.product" [matAutocomplete]="autoP" />
|
||||
<mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn">
|
||||
@for (p of products(); track p) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
<div mat-dialog-content>
|
||||
<form>
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Item</mat-label>
|
||||
<input matInput type="text" [formField]="form.product" [matAutocomplete]="autoP" />
|
||||
<mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn">
|
||||
@for (p of products(); track p.id) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-3">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" [formField]="form.quantity" />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-3">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Quantity</mat-label>
|
||||
<input matInput type="number" [formField]="form.quantity" />
|
||||
</mat-form-field>
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.salePrice" />
|
||||
</mat-form-field>
|
||||
<mat-checkbox [formField]="form.printInBill" class="flex-auto">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.salePrice" />
|
||||
</mat-form-field>
|
||||
<mat-checkbox [formField]="form.printInBill" class="flex-auto">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button color="warn" [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
||||
}
|
||||
<div mat-dialog-actions>
|
||||
<button mat-raised-button color="warn" [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||
</div>
|
||||
@@ -10,8 +10,6 @@ import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { ProductQuery } from '../../core/product-query';
|
||||
import { ProductService } from '../../product/product.service';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { BundleItem } from '../bundle';
|
||||
|
||||
export interface BundleDetailDialogFormData {
|
||||
@@ -35,8 +33,6 @@ export interface BundleDetailDialogFormData {
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
FormField,
|
||||
ErrorStateComponent,
|
||||
SkeletonLoaderComponent,
|
||||
],
|
||||
})
|
||||
export class BundleDetailDialogComponent {
|
||||
@@ -62,12 +58,15 @@ export class BundleDetailDialogComponent {
|
||||
return typeof p === 'string' ? p.trim() : '';
|
||||
});
|
||||
|
||||
debouncedProduct = debounced(this.productSignal, 150);
|
||||
debouncedProduct = debounced(this.productSignal, 300);
|
||||
|
||||
productsResource = this.productSer.query(this.debouncedProduct.value);
|
||||
products = computed(() => {
|
||||
if (!this.debouncedProduct.value()) return [];
|
||||
return this.productsResource.value() ?? [];
|
||||
products = linkedSignal<ProductQuery[] | undefined, ProductQuery[]>({
|
||||
source: () => this.productsResource.value(),
|
||||
computation: (newVal, previous) => {
|
||||
if (!this.debouncedProduct.value()) return [];
|
||||
return newVal ?? previous?.value ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
displayFn(product?: ProductQuery | string): string {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="form.menuCategory">
|
||||
@for (mc of menuCategories(); track mc) {
|
||||
@for (mc of menuCategories(); track mc.id) {
|
||||
<mat-option [value]="mc">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -39,7 +39,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select [formField]="form.saleCategory">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
@for (sc of saleCategories(); track sc.id) {
|
||||
<mat-option [value]="sc">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
@@ -71,7 +71,7 @@
|
||||
[value]="displayFn(formModel().addRow.sku)"
|
||||
/>
|
||||
<mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn">
|
||||
@for (p of itemProducts(); track p) {
|
||||
@for (p of itemProducts(); track p.id) {
|
||||
<mat-option [value]="p">{{ p.name }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
|
||||
@@ -109,11 +109,15 @@ export class BundleDetailComponent {
|
||||
return typeof v === 'string' ? v : null;
|
||||
});
|
||||
|
||||
debouncedItemProduct = debounced(this.itemProductSearch, 150);
|
||||
debouncedItemProduct = debounced(this.itemProductSearch, 300);
|
||||
|
||||
itemProductsResource = this.productSer.query(this.debouncedItemProduct.value);
|
||||
itemProducts = computed(() => {
|
||||
return this.itemProductsResource.value() ?? [];
|
||||
itemProducts = linkedSignal<ProductQuery[] | undefined, ProductQuery[]>({
|
||||
source: () => this.itemProductsResource.value(),
|
||||
computation: (newVal, previous) => {
|
||||
if (!this.debouncedItemProduct.value()) return [];
|
||||
return newVal ?? previous?.value ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
displayedColumns = ['name', 'quantity', 'salePrice', 'printInBill', 'action'];
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="form.menuCategory">
|
||||
<mat-option [value]="null">-- All Bundles --</mat-option>
|
||||
@for (mc of menuCategoriesResource.value(); track mc) {
|
||||
@for (mc of menuCategoriesResource.value(); track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -73,7 +73,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>Items</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<div class="items-list">
|
||||
@for (item of row.items; track item) {
|
||||
@for (item of row.items; track item.id) {
|
||||
<mat-icon>
|
||||
{{ item.printInBill ? 'visibility' : 'visibility_off' }}
|
||||
</mat-icon>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<mat-label>Cashier</mat-label>
|
||||
<mat-select [formField]="form.cashier">
|
||||
<mat-option [value]="null">-- Cashier --</mat-option>
|
||||
@for (ac of cashiers(); track ac) {
|
||||
@for (ac of cashiers(); track ac.id) {
|
||||
<mat-option [value]="ac.id">
|
||||
{{ ac.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -67,8 +67,8 @@ export class CashierReportComponent {
|
||||
if (infoVal) {
|
||||
return {
|
||||
cashier: this.info().cashier.id ?? '',
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -115,8 +115,8 @@ export class CashierReportComponent {
|
||||
return new CashierReport({
|
||||
cashier: new User({ id: formModel.cashier ?? undefined }),
|
||||
cashiers: this.info().cashiers,
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,11 @@ import { AuthService } from '../auth/auth.service';
|
||||
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
|
||||
|
||||
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const authService = inject(AuthService);
|
||||
const snackBar = inject(MatSnackBar);
|
||||
const dialog = inject(MatDialog);
|
||||
const router = inject(Router);
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((err) => {
|
||||
// We don't want to refresh token for some requests like login or refresh token itself
|
||||
@@ -18,34 +23,36 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
// We do another check to see if refresh token failed
|
||||
// In this case we want to logout user and to redirect it to login page
|
||||
if (req.url.includes('/refresh')) {
|
||||
inject(AuthService).logout();
|
||||
authService.logout();
|
||||
}
|
||||
return throwError(() => err);
|
||||
}
|
||||
// If error status is different than 401 we want to skip refresh token
|
||||
// So we check that and throw the error if it's the case
|
||||
if (err.status !== 401) {
|
||||
const error = err.error.message || err.error.detail || err.statusText;
|
||||
const error = err.error?.message || err.error?.detail || err.statusText || err;
|
||||
return throwError(() => error);
|
||||
}
|
||||
// auto logout if 401 response returned from api
|
||||
inject(AuthService).logout();
|
||||
inject(MatSnackBar).open('User has been logged out', 'Danger');
|
||||
const dialogRef = inject(MatDialog).open(ConfirmDialogComponent, {
|
||||
width: '250px',
|
||||
data: {
|
||||
title: 'Logged out!',
|
||||
content:
|
||||
'You have been logged out.\n' +
|
||||
'You can press Cancel to stay on page and login in another tab to resume here, ' +
|
||||
'or you can press Ok to navigate to the login page.',
|
||||
},
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
inject(Router).navigate(['login']);
|
||||
}
|
||||
});
|
||||
authService.logout();
|
||||
if (dialog.openDialogs.length === 0) {
|
||||
snackBar.open('User has been logged out', 'Danger');
|
||||
const dialogRef = dialog.open(ConfirmDialogComponent, {
|
||||
width: '350px',
|
||||
data: {
|
||||
title: 'Logged out!',
|
||||
content:
|
||||
'You have been logged out.\n' +
|
||||
'You can press Cancel to stay on page and login in another tab to resume here, ' +
|
||||
'or you can press Ok to navigate to the login page.',
|
||||
},
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: boolean) => {
|
||||
if (result) {
|
||||
router.navigate(['/login']);
|
||||
}
|
||||
});
|
||||
}
|
||||
return throwError(() => err);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export interface ClientErrorPayload {
|
||||
message: string;
|
||||
stack?: string | null;
|
||||
url: string;
|
||||
source?: 'unhandled_error' | 'http_resource' | 'service_error' | 'global_error';
|
||||
status_code?: number | null;
|
||||
endpoint?: string | null;
|
||||
level?: 'error' | 'warning' | 'info';
|
||||
component?: string | null;
|
||||
extra?: Record<string, unknown> | null;
|
||||
}
|
||||
@@ -1,10 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { ClientErrorPayload } from './client-log';
|
||||
|
||||
const LOG_ENDPOINT = '/api/client-logs';
|
||||
const DEDUPE_WINDOW_MS = 5000;
|
||||
const RATE_LIMIT_WINDOW_MS = 60000;
|
||||
const MAX_LOGS_PER_WINDOW = 10;
|
||||
const SENSITIVE_KEYS = new Set(['password', 'token', 'authorization', 'secret', 'credit_card']);
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ErrorLoggerService {
|
||||
private static dedupeCache = new Map<string, number>();
|
||||
private static logTimestamps: number[] = [];
|
||||
private authService = inject(AuthService, { optional: true });
|
||||
|
||||
public static log(serviceName = 'error-logger', message: string) {
|
||||
console.log(`${serviceName}Service: ${message}`);
|
||||
}
|
||||
@@ -14,12 +27,117 @@ export class ErrorLoggerService {
|
||||
* Let the app continue.
|
||||
*
|
||||
* @param operation - name of the operation that failed
|
||||
* @param result - optional value to return as the observable result
|
||||
*/
|
||||
public handleError<T>(serviceName = 'error-logger', operation = 'operation') {
|
||||
return (error: unknown): Observable<T> => {
|
||||
ErrorLoggerService.log(serviceName, `${operation} failed: ${error}`);
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
ErrorLoggerService.log(serviceName, `${operation} failed: ${errorMsg}`);
|
||||
|
||||
this.sendToBackend({
|
||||
message: `${serviceName}: ${operation} failed: ${errorMsg}`,
|
||||
stack: stack ?? null,
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
component: serviceName,
|
||||
source: 'service_error',
|
||||
level: 'error',
|
||||
});
|
||||
|
||||
return throwError(() => error);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a structured error payload to the backend with rate-limiting,
|
||||
* deduplication, recursion protection, and sensitive data scrubbing.
|
||||
*/
|
||||
public sendToBackend(payload: ClientErrorPayload): void {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Recursion Guard: Ignore logs about the logging endpoint itself
|
||||
if (payload.endpoint?.includes(LOG_ENDPOINT) || payload.url.includes(LOG_ENDPOINT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// 2. Deduplication Guard: Check if identical error was dispatched recently
|
||||
const fingerprint = `${payload.source || ''}:${payload.status_code || ''}:${payload.endpoint || ''}:${payload.message}:${payload.url}`;
|
||||
const lastSeen = ErrorLoggerService.dedupeCache.get(fingerprint);
|
||||
if (lastSeen && now - lastSeen < DEDUPE_WINDOW_MS) {
|
||||
return;
|
||||
}
|
||||
ErrorLoggerService.dedupeCache.set(fingerprint, now);
|
||||
|
||||
// Prune stale deduplication cache entries
|
||||
for (const [key, timestamp] of ErrorLoggerService.dedupeCache.entries()) {
|
||||
if (now - timestamp > DEDUPE_WINDOW_MS * 2) {
|
||||
ErrorLoggerService.dedupeCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Rate-Limiting Guard: Max X logs per window
|
||||
ErrorLoggerService.logTimestamps = ErrorLoggerService.logTimestamps.filter((ts) => now - ts < RATE_LIMIT_WINDOW_MS);
|
||||
if (ErrorLoggerService.logTimestamps.length >= MAX_LOGS_PER_WINDOW) {
|
||||
console.warn('Frontend error logging throttled: rate limit exceeded.');
|
||||
return;
|
||||
}
|
||||
ErrorLoggerService.logTimestamps.push(now);
|
||||
|
||||
// 4. Sanitize sensitive fields
|
||||
const sanitizedPayload: ClientErrorPayload = {
|
||||
...payload,
|
||||
extra: this.sanitizeData(payload.extra),
|
||||
};
|
||||
|
||||
// 5. Send via fetch (with keepalive: true) or sendBeacon
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const token = this.authService?.user()?.access_token;
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const bodyString = JSON.stringify(sanitizedPayload);
|
||||
|
||||
try {
|
||||
if (typeof fetch !== 'undefined') {
|
||||
fetch(LOG_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: bodyString,
|
||||
keepalive: true,
|
||||
}).catch((err) => {
|
||||
// Never re-log transport errors into the error service
|
||||
console.warn('Failed to send error log to backend:', err);
|
||||
});
|
||||
} else if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
|
||||
const blob = new Blob([bodyString], { type: 'application/json' });
|
||||
navigator.sendBeacon(LOG_ENDPOINT, blob);
|
||||
}
|
||||
} catch (transportErr) {
|
||||
console.warn('Error during error dispatch transport:', transportErr);
|
||||
}
|
||||
}
|
||||
|
||||
private sanitizeData(data?: Record<string, unknown> | null): Record<string, unknown> | null {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return data ?? null;
|
||||
}
|
||||
const clean: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (SENSITIVE_KEYS.has(key.toLowerCase())) {
|
||||
clean[key] = '[REDACTED]';
|
||||
} else if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
clean[key] = this.sanitizeData(value as Record<string, unknown>);
|
||||
} else {
|
||||
clean[key] = value;
|
||||
}
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ErrorHandler, inject, Injectable } from '@angular/core';
|
||||
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
private errorLogger = inject(ErrorLoggerService);
|
||||
|
||||
public handleError(error: unknown): void {
|
||||
// Keep standard console.error for local debugging
|
||||
console.error('Unhandled application error:', error);
|
||||
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const stack = error instanceof Error ? error.stack : undefined;
|
||||
|
||||
this.errorLogger.sendToBackend({
|
||||
message: `Uncaught Error: ${message}`,
|
||||
stack: stack ?? null,
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
source: 'unhandled_error',
|
||||
level: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import { catchError, throwError } from 'rxjs';
|
||||
|
||||
import { ErrorLoggerService } from './error-logger.service';
|
||||
|
||||
const IGNORED_URLS = ['/api/client-logs'];
|
||||
|
||||
export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
const errorLogger = inject(ErrorLoggerService);
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((error: unknown) => {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
const isIgnored = IGNORED_URLS.some((url) => req.url.includes(url));
|
||||
if (!isIgnored) {
|
||||
const detail =
|
||||
typeof error.error === 'object' && error.error !== null
|
||||
? JSON.stringify(error.error)
|
||||
: String(error.error || error.message);
|
||||
|
||||
const isServerError = error.status >= 500 || error.status === 0;
|
||||
|
||||
errorLogger.sendToBackend({
|
||||
message: `HTTP ${error.status}: ${detail}`,
|
||||
url: typeof window !== 'undefined' ? window.location.href : '',
|
||||
source: 'http_resource',
|
||||
status_code: error.status,
|
||||
endpoint: req.url,
|
||||
level: isServerError ? 'error' : 'warning',
|
||||
extra: {
|
||||
method: req.method,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="discounts">
|
||||
@for (r of item().discounts; track r; let i = $index) {
|
||||
@for (r of item().discounts; track r.id; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Discount on {{ r.name }}</mat-label>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>Discounts</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (discount of row.discounts; track discount) {
|
||||
@for (discount of row.discounts; track discount.id) {
|
||||
<li>{{ discount.name }} - {{ discount.discount | percent: '1.2-2' }}</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Section</mat-label>
|
||||
<mat-select [formField]="form.section">
|
||||
@for (s of sections(); track s) {
|
||||
@for (s of sections(); track s.id) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -56,8 +56,8 @@ export class DiscountReportComponent {
|
||||
computation: (infoVal): DiscountReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -86,8 +86,8 @@ export class DiscountReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new DiscountReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="selected($event)"
|
||||
>
|
||||
@for (customer of customers(); track customer) {
|
||||
@for (customer of customers(); track customer.id) {
|
||||
<mat-option [value]="customer">{{ customer.name }} - {{ customer.phone }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
|
||||
@@ -73,8 +73,8 @@ export class GuestBookDetailComponent {
|
||||
pax: itemVal.pax,
|
||||
address: itemVal.address ?? '',
|
||||
notes: itemVal.notes ?? '',
|
||||
date: dt ? DateTime.fromJSDate(new Date(dt)) : DateTime.now(),
|
||||
time: dt ? DateTime.fromJSDate(new Date(dt)) : DateTime.now(),
|
||||
date: dt ? DateTime.fromISO(dt).minus({ minutes: new Date().getTimezoneOffset() }) : DateTime.now(),
|
||||
time: dt ? DateTime.fromISO(dt).minus({ minutes: new Date().getTimezoneOffset() }) : DateTime.now(),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -95,12 +95,15 @@ export class GuestBookDetailComponent {
|
||||
return typeof p === 'string' ? p.trim() : (p as Customer).phone;
|
||||
});
|
||||
|
||||
debouncedPhone = debounced(this.phoneSignal, 150);
|
||||
debouncedPhone = debounced(this.phoneSignal, 300);
|
||||
|
||||
customersResource = this.customerService.autocomplete(computed(() => this.debouncedPhone.value()));
|
||||
customers = computed(() => {
|
||||
if (!this.debouncedPhone.value()) return [];
|
||||
return this.customersResource.value() ?? [];
|
||||
customers = linkedSignal<Customer[] | undefined, Customer[]>({
|
||||
source: () => this.customersResource.value(),
|
||||
computation: (newVal, previous) => {
|
||||
if (!this.debouncedPhone.value()) return [];
|
||||
return newVal ?? previous?.value ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
constructor() {
|
||||
@@ -172,16 +175,25 @@ export class GuestBookDetailComponent {
|
||||
item.pax = formModel.pax ?? 0;
|
||||
item.address = formModel.address;
|
||||
item.notes = formModel.notes;
|
||||
const timeStr = formModel.time ? formModel.time.toFormat('HH:mm') : '00:00';
|
||||
const time = formModel.time ?? DateTime.now();
|
||||
const dt = formModel.date.set({ hour: time.hour, minute: time.minute, second: 0, millisecond: 0 });
|
||||
item.type = item.type || this.t() || 'walk_in';
|
||||
|
||||
if (item.type === 'booking') {
|
||||
item.bookingDate = formModel.date.toFormat('dd-MMM-yyyy') + ' ' + timeStr;
|
||||
item.bookingDate = dt.toISO() ?? '';
|
||||
} else {
|
||||
const existingDate = item.arrivalDate
|
||||
? (item.arrivalDate as string).substring(0, 12)
|
||||
: formModel.date.toFormat('dd-MMM-yyyy ');
|
||||
item.arrivalDate = existingDate + timeStr;
|
||||
if (item.arrivalDate) {
|
||||
// preserve existing arrival date but update time
|
||||
const arrival = DateTime.fromISO(item.arrivalDate as string).set({
|
||||
hour: time.hour,
|
||||
minute: time.minute,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
item.arrivalDate = arrival.toISO() ?? '';
|
||||
} else {
|
||||
item.arrivalDate = dt.toISO() ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
|
||||
@@ -45,7 +45,7 @@ export class GuestBookListComponent {
|
||||
date = input(DateTime.now(), {
|
||||
transform: (v: string | null | undefined): DateTime => {
|
||||
if (v) {
|
||||
const parsed = DateTime.fromFormat(v, 'dd-MMM-yyyy');
|
||||
const parsed = DateTime.fromISO(v);
|
||||
if (parsed.isValid) return parsed;
|
||||
}
|
||||
return DateTime.now();
|
||||
@@ -63,7 +63,7 @@ export class GuestBookListComponent {
|
||||
|
||||
displayedColumns = ['sno', 'name', 'phone', 'pax', 'date', 'action'];
|
||||
|
||||
dateSignal = computed(() => this.formModel().date.toFormat('dd-MMM-yyyy'));
|
||||
dateSignal = computed(() => this.formModel().date.toISODate() ?? '');
|
||||
listResource = this.ser.list(this.dateSignal);
|
||||
|
||||
dataSource = computed(() => this.listResource.value()?.list ?? []);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Header / Footer</mat-label>
|
||||
<mat-select [formField]="form.id" (selectionChange)="show($event)">
|
||||
@for (s of list(); track s) {
|
||||
@for (s of list(); track s.id) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -64,8 +64,8 @@ export class MenuEngineeringReportComponent {
|
||||
computation: (infoVal): MenuEngineeringReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
filter: '',
|
||||
};
|
||||
}
|
||||
@@ -143,8 +143,8 @@ export class MenuEngineeringReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new MenuEngineeringReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -47,12 +47,12 @@
|
||||
<mat-header-cell *matHeaderCellDef>Products</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (mc of row.menuCategories; track mc) {
|
||||
@for (mc of row.menuCategories; track mc.id) {
|
||||
<li>
|
||||
{{ mc.name }}
|
||||
@if (!mc.enabled) {
|
||||
<ul>
|
||||
@for (p of mc.skus; track p) {
|
||||
@for (p of mc.skus; track p.id) {
|
||||
<li>{{ p.name }}</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Category</mat-label>
|
||||
<mat-select [formField]="form.modifierCategory">
|
||||
@for (mc of modifierCategories(); track mc) {
|
||||
@for (mc of modifierCategories(); track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@for (error of form.modifierCategory().errors(); track error) {
|
||||
@for (error of form.modifierCategory().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@@ -66,8 +66,8 @@ export class ProductSaleReportComponent {
|
||||
computation: (sectionsVal): ProductSaleReportFormData => {
|
||||
if (sectionsVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
section: this.info().sectionId || null,
|
||||
};
|
||||
}
|
||||
@@ -102,8 +102,8 @@ export class ProductSaleReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
const data = new ProductSaleReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
if (formModel.section) {
|
||||
data.sectionId = formModel.section;
|
||||
|
||||
@@ -52,8 +52,8 @@ export class ProductUpdatesReportComponent {
|
||||
computation: (infoVal): ProductUpdatesReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -82,8 +82,8 @@ export class ProductUpdatesReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new ProductUpdatesReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Units</mat-label>
|
||||
<input matInput [formField]="form.units" />
|
||||
@for (error of form.units().errors(); track error) {
|
||||
@for (error of form.units().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -13,7 +13,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Fraction</mat-label>
|
||||
<input matInput type="number" [formField]="form.fraction" />
|
||||
@for (error of form.fraction().errors(); track error) {
|
||||
@for (error of form.fraction().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -21,7 +21,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Yield</mat-label>
|
||||
<input matInput type="number" [formField]="form.productYield" />
|
||||
@for (error of form.productYield().errors(); track error) {
|
||||
@for (error of form.productYield().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -29,7 +29,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Cost Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.costPrice" />
|
||||
@for (error of form.costPrice().errors(); track error) {
|
||||
@for (error of form.costPrice().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -37,7 +37,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.salePrice" />
|
||||
@for (error of form.salePrice().errors(); track error) {
|
||||
@for (error of form.salePrice().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -45,7 +45,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="form.menuCategory">
|
||||
@for (mc of menuCategories; track mc) {
|
||||
@for (mc of menuCategories; track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<mat-form-field class="basis-3-4">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select [formField]="form.saleCategory">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
@for (sc of saleCategories(); track sc.id) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
@@ -35,7 +35,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Units</mat-label>
|
||||
<input matInput [formField]="form.addRow.units" />
|
||||
@for (error of form.addRow.units().errors(); track error) {
|
||||
@for (error of form.addRow.units().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -43,7 +43,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Fraction</mat-label>
|
||||
<input matInput type="number" [formField]="form.addRow.fraction" />
|
||||
@for (error of form.addRow.fraction().errors(); track error) {
|
||||
@for (error of form.addRow.fraction().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -51,7 +51,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Yield</mat-label>
|
||||
<input matInput type="number" [formField]="form.addRow.productYield" />
|
||||
@for (error of form.addRow.productYield().errors(); track error) {
|
||||
@for (error of form.addRow.productYield().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -59,7 +59,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Cost Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.addRow.costPrice" />
|
||||
@for (error of form.addRow.costPrice().errors(); track error) {
|
||||
@for (error of form.addRow.costPrice().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -67,7 +67,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Price</mat-label>
|
||||
<input matInput type="number" [formField]="form.addRow.salePrice" />
|
||||
@for (error of form.addRow.salePrice().errors(); track error) {
|
||||
@for (error of form.addRow.salePrice().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@@ -75,13 +75,13 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="form.addRow.menuCategory">
|
||||
@for (mc of menuCategories(); track mc) {
|
||||
@for (mc of menuCategories(); track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@for (error of form.addRow.menuCategory().errors(); track error) {
|
||||
@for (error of form.addRow.menuCategory().errors(); track $index) {
|
||||
<mat-error>{{ error.message }}</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="form.menuCategory">
|
||||
<mat-option [value]="null">-- All Products --</mat-option>
|
||||
@for (mc of menuCategoriesResource.value(); track mc) {
|
||||
@for (mc of menuCategoriesResource.value(); track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
@@ -54,7 +54,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (sku of row.skus; track sku) {
|
||||
@for (sku of row.skus; track sku.id) {
|
||||
<li>
|
||||
<a [routerLink]="['/products', row.id]">{{ row.name }} ({{ sku.units }})</a>
|
||||
</li>
|
||||
@@ -68,7 +68,7 @@
|
||||
<mat-header-cell *matHeaderCellDef class="right">Price</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">
|
||||
<ul>
|
||||
@for (sku of row.skus; track sku) {
|
||||
@for (sku of row.skus; track sku.id) {
|
||||
<li>
|
||||
{{ sku.salePrice | currency: 'INR' }}
|
||||
</li>
|
||||
@@ -82,7 +82,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>Menu Category</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (sku of row.skus; track sku) {
|
||||
@for (sku of row.skus; track sku.id) {
|
||||
<li>
|
||||
{{ sku.menuCategory.name }}
|
||||
</li>
|
||||
@@ -102,7 +102,7 @@
|
||||
<mat-header-cell *matHeaderCellDef>Details</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row">
|
||||
<ul>
|
||||
@for (sku of row.skus; track sku) {
|
||||
@for (sku of row.skus; track sku.id) {
|
||||
<li layout="row">
|
||||
<div>
|
||||
<mat-icon>
|
||||
@@ -127,7 +127,7 @@
|
||||
<mat-header-cell *matHeaderCellDef class="right">Quantity</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row" class="right">
|
||||
<ul>
|
||||
@for (sku of row.skus; track sku) {
|
||||
@for (sku of row.skus; track sku.id) {
|
||||
<li>{{ sku.fraction | number: '1.2-2' }}{{ row.fractionUnits }}</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<div class="two-col">
|
||||
<div class="col">
|
||||
<h3>Permissions</h3>
|
||||
@for (p of form.permissions; track p; let i = $index) {
|
||||
@for (p of form.permissions; track p.id().value() || i; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-checkbox
|
||||
[formField]="p.enabled"
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div class="col">
|
||||
<h3>Includes Roles</h3>
|
||||
@for (r of form.includedRoles; track r; let i = $index) {
|
||||
@for (r of form.includedRoles; track r.id().value() || i; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="r.enabled" class="flex-auto">{{ r.name().value() }}</mat-checkbox>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Tax</mat-label>
|
||||
<mat-select [formField]="form.tax">
|
||||
@for (t of taxes(); track t) {
|
||||
@for (t of taxes(); track t.id) {
|
||||
<mat-option [value]="t.id">
|
||||
{{ t.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -67,8 +67,8 @@ export class SaleReportComponent {
|
||||
computation: (sectionsVal): SaleReportFormData => {
|
||||
if (sectionsVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
section: this.info().sectionId || null,
|
||||
};
|
||||
}
|
||||
@@ -102,8 +102,8 @@ export class SaleReportComponent {
|
||||
getInfo(): SaleReport {
|
||||
const formModel = this.formModel();
|
||||
const data = new SaleReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
if (formModel.section) {
|
||||
data.sectionId = formModel.section;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<mat-form-field class="flex-auto basis-15">
|
||||
<mat-label>Regime</mat-label>
|
||||
<mat-select [formField]="form.regime">
|
||||
@for (r of regimes; track r) {
|
||||
@for (r of regimes; track r.id) {
|
||||
<mat-option [value]="r">
|
||||
{{ r.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
</mat-card>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button mat-button [mat-dialog-close]="selectedVal">Done</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -37,25 +37,36 @@ export class BillService {
|
||||
|
||||
public grossAmount = computed(() =>
|
||||
this.math.halfRoundEven(
|
||||
this.data().reduce((t, k) => k.inventories.reduce((a, c) => a + c.price * c.quantity, 0) + t, 0),
|
||||
this.data().reduce(
|
||||
(t, k) => k.inventories.filter((c) => !c.parentId).reduce((a, c) => a + c.price * c.quantity, 0) + t,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
public hhAmount = computed(() =>
|
||||
this.math.halfRoundEven(
|
||||
this.data().reduce(
|
||||
(t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? c.price : 0) * c.quantity, 0) + t,
|
||||
(t, k) =>
|
||||
k.inventories.filter((c) => !c.parentId).reduce((a, c) => a + (c.isHappyHour ? c.price : 0) * c.quantity, 0) +
|
||||
t,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
public discountAmount = computed(() =>
|
||||
this.math.halfRoundEven(
|
||||
this.data().reduce(
|
||||
(t, k) => k.inventories.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount, 0) + t,
|
||||
(t, k) =>
|
||||
k.inventories
|
||||
.filter((c) => !c.parentId)
|
||||
.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * c.discount, 0) + t,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,16 +74,36 @@ export class BillService {
|
||||
this.math.halfRoundEven(
|
||||
this.data().reduce(
|
||||
(t, k) =>
|
||||
k.inventories.reduce(
|
||||
(a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * c.taxRate,
|
||||
0,
|
||||
) + t,
|
||||
k.inventories
|
||||
.filter((c) => !c.parentId)
|
||||
.reduce((a, c) => a + (c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * c.taxRate, 0) + t,
|
||||
0,
|
||||
),
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
public amount = computed(() =>
|
||||
this.math.halfRoundEven(
|
||||
this.data().reduce(
|
||||
(t, k) =>
|
||||
k.inventories
|
||||
.filter((c) => !c.parentId)
|
||||
.reduce(
|
||||
(a, c) =>
|
||||
a +
|
||||
this.math.halfRoundEven(
|
||||
(c.isHappyHour ? 0 : c.price) * c.quantity * (1 - c.discount) * (1 + c.taxRate),
|
||||
2,
|
||||
),
|
||||
0,
|
||||
) + t,
|
||||
0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
public amount = computed(() => this.grossAmount() - this.discountAmount() + this.taxAmount());
|
||||
public selection = new SelectionModel<string>(true, []);
|
||||
private updateTable = signal<boolean>(true);
|
||||
private allowDeactivate = signal<boolean>(false);
|
||||
|
||||
@@ -8,13 +8,10 @@ import { VoucherType } from './voucher-type';
|
||||
export class Bill {
|
||||
id: string | undefined;
|
||||
date: string;
|
||||
dateTip: string;
|
||||
pax: number;
|
||||
user: User;
|
||||
creationDate: string;
|
||||
creationDateTip: string;
|
||||
lastEditDate: string;
|
||||
lastEditDateTip: string;
|
||||
kotId: string;
|
||||
billId: string;
|
||||
table: Table;
|
||||
@@ -27,13 +24,10 @@ export class Bill {
|
||||
public constructor(init?: Partial<Bill>) {
|
||||
this.id = undefined;
|
||||
this.date = '';
|
||||
this.dateTip = '';
|
||||
this.pax = 0;
|
||||
this.user = new User();
|
||||
this.creationDate = '';
|
||||
this.creationDateTip = '';
|
||||
this.lastEditDate = '';
|
||||
this.lastEditDateTip = '';
|
||||
this.billId = '';
|
||||
this.kotId = '';
|
||||
this.table = new Table();
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="time-details">
|
||||
<mat-header-cell *matHeaderCellDef class="deep-purple-100 bold right-align"
|
||||
><span [matTooltip]="bs.bill().dateTip">{{ bs.bill().date }}</span
|
||||
> / <span [matTooltip]="bs.bill().creationDateTip">{{ bs.bill().creationDate }}</span
|
||||
> / <span [matTooltip]="bs.bill().lastEditDateTip">{{
|
||||
bs.bill().lastEditDate
|
||||
><span [matTooltip]="bs.bill().date | localTime: 'datetime'">{{ bs.bill().date | localTime: 'time' }}</span
|
||||
> / <span [matTooltip]="bs.bill().creationDate | localTime: 'datetime'">{{
|
||||
bs.bill().creationDate | localTime: 'time'
|
||||
}}</span
|
||||
> / <span [matTooltip]="bs.bill().lastEditDate | localTime: 'datetime'">{{
|
||||
bs.bill().lastEditDate | localTime: 'time'
|
||||
}}</span></mat-header-cell
|
||||
>
|
||||
</ng-container>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Customer } from '../../core/customer';
|
||||
import { Table } from '../../core/table';
|
||||
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { MathService } from '../../shared/math.service';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { TableService } from '../../tables/table.service';
|
||||
@@ -36,6 +37,7 @@ import { VoucherService } from './voucher.service';
|
||||
templateUrl: './bills.component.html',
|
||||
styleUrls: ['./bills.component.sass'],
|
||||
imports: [
|
||||
LocalTimePipe,
|
||||
CommonModule,
|
||||
CurrencyPipe,
|
||||
MatButtonModule,
|
||||
|
||||
@@ -1,63 +1,57 @@
|
||||
@if (customersResource.isLoading()) {
|
||||
<app-skeleton-loader type="card" [count]="1" />
|
||||
} @else if (customersResource.error()) {
|
||||
<app-error-state (retryAction)="customersResource.reload()" />
|
||||
} @else {
|
||||
<h2 mat-dialog-title>Customer</h2>
|
||||
<mat-dialog-content>
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formField]="form.phone"
|
||||
[matAutocomplete]="auto"
|
||||
autocomplete="off"
|
||||
cdkFocusInitial
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="selected($event)"
|
||||
>
|
||||
@for (customer of customers(); track customer) {
|
||||
<mat-option [value]="customer">{{ customer.name }} - {{ customer.phone }}</mat-option>
|
||||
<h2 mat-dialog-title>Customer</h2>
|
||||
<mat-dialog-content>
|
||||
<form class="flex-col">
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Phone</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
[formField]="form.phone"
|
||||
[matAutocomplete]="auto"
|
||||
autocomplete="off"
|
||||
cdkFocusInitial
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-autocomplete
|
||||
#auto="matAutocomplete"
|
||||
autoActiveFirstOption
|
||||
[displayWith]="displayFn"
|
||||
(optionSelected)="selected($event)"
|
||||
>
|
||||
@for (customer of customers(); track customer.id) {
|
||||
<mat-option [value]="customer">{{ customer.name }} - {{ customer.phone }}</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [formField]="form.name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Address</mat-label>
|
||||
<textarea matInput [formField]="form.address"> </textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="form.printInBill">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="discounts">
|
||||
<ul>
|
||||
@for (r of form().value().discounts; track r.id) {
|
||||
@if (r.discount > 0) {
|
||||
<li>{{ r.name }} - {{ r.discount | percent: '1.2-2' }}</li>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput [value]="formModel().name" readonly />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Address</mat-label>
|
||||
<textarea matInput [value]="formModel().address" readonly> </textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="row-container">
|
||||
<mat-checkbox [checked]="formModel().printInBill" [disabled]="true">Print in Bill?</mat-checkbox>
|
||||
</div>
|
||||
<p></p>
|
||||
<div class="discounts">
|
||||
<ul>
|
||||
@for (r of formModel().discounts; track r.id) {
|
||||
@if (r.discount > 0) {
|
||||
<li>{{ r.name }} - {{ r.discount | percent: '1.2-2' }}</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="save()">Select</button>
|
||||
</mat-dialog-actions>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-raised-button color="primary" (click)="save()">Select</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -13,8 +13,6 @@ import { MatInputModule } from '@angular/material/input';
|
||||
import { Customer } from '../../core/customer';
|
||||
import { CustomerDiscount } from '../../core/customer-discount';
|
||||
import { CustomerService } from '../../customers/customer.service';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
|
||||
export interface ChooseCustomerFormData {
|
||||
id: string;
|
||||
@@ -40,8 +38,6 @@ export interface ChooseCustomerFormData {
|
||||
MatOptionModule,
|
||||
PercentPipe,
|
||||
FormField,
|
||||
ErrorStateComponent,
|
||||
SkeletonLoaderComponent,
|
||||
],
|
||||
})
|
||||
export class ChooseCustomerComponent {
|
||||
@@ -66,18 +62,24 @@ export class ChooseCustomerComponent {
|
||||
form = form(this.formModel);
|
||||
|
||||
phoneSignal = computed(() => {
|
||||
return this.formModel().phone;
|
||||
const p = this.formModel().phone;
|
||||
return typeof p === 'string' ? p.trim() : null;
|
||||
});
|
||||
|
||||
debouncedPhone = debounced(this.phoneSignal, 150);
|
||||
debouncedPhone = debounced(this.phoneSignal, 300);
|
||||
|
||||
customersResource = this.ser.autocomplete(this.debouncedPhone.value);
|
||||
customers = computed(() => {
|
||||
return this.customersResource.value() ?? [];
|
||||
customers = linkedSignal<Customer[] | undefined, Customer[]>({
|
||||
source: () => this.customersResource.value(),
|
||||
computation: (newVal, previous) => {
|
||||
if (!this.debouncedPhone.value()) return [];
|
||||
return newVal ?? previous?.value ?? [];
|
||||
},
|
||||
});
|
||||
|
||||
save() {
|
||||
const customer = this.getItem();
|
||||
console.log(customer);
|
||||
const promise = this.ser.saveOrUpdate(customer);
|
||||
promise.subscribe((x) => this.dialogRef.close(x));
|
||||
}
|
||||
@@ -95,15 +97,15 @@ export class ChooseCustomerComponent {
|
||||
}
|
||||
|
||||
getItem(): Customer {
|
||||
const formModel = this.formModel();
|
||||
const formValue = this.form().value();
|
||||
|
||||
return new Customer({
|
||||
id: formModel.id,
|
||||
name: formModel.name ?? '',
|
||||
phone: formModel.phone ?? '',
|
||||
address: formModel.address ?? '',
|
||||
printInBill: formModel.printInBill ?? false,
|
||||
discounts: formModel.discounts.map((item) => ({ ...item })),
|
||||
id: formValue.id,
|
||||
name: formValue.name ?? '',
|
||||
phone: formValue.phone ?? '',
|
||||
address: formValue.address ?? '',
|
||||
printInBill: formValue.printInBill ?? false,
|
||||
discounts: formValue.discounts.map((item) => ({ ...item })),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
<input matInput type="number" [formField]="form.discounts[i].discount" autocomplete="off" />
|
||||
<span matTextSuffix>%</span>
|
||||
<mat-hint>Cust: {{ row.customer | percent: '1.2-2' }}</mat-hint>
|
||||
<mat-hint align="end">Max: {{ row.limit | percent: '1.2-2' }}</mat-hint>
|
||||
<mat-hint>Max: {{ row.limit | percent: '1.2-2' }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
@@ -26,7 +26,7 @@
|
||||
</mat-table>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-button (click)="accept()" color="primary">Ok</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -7,7 +7,7 @@
|
||||
<mat-card class="flex-col square-button warn" matRipple [routerLink]="['../']" queryParamsHandling="preserve">
|
||||
<h3>Back</h3>
|
||||
</mat-card>
|
||||
@for (item of list(); track item) {
|
||||
@for (item of list(); track item.id) {
|
||||
<mat-card
|
||||
class="primary flex-col square-button"
|
||||
matRipple
|
||||
|
||||
@@ -30,6 +30,6 @@
|
||||
}
|
||||
</mat-tab-group>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="selected()">Done</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -12,7 +12,7 @@
|
||||
>
|
||||
<h3>Back</h3>
|
||||
</mat-card>
|
||||
@for (item of list(); track item) {
|
||||
@for (item of list(); track item.id) {
|
||||
<mat-card
|
||||
class="flex-col square-button"
|
||||
matRipple
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</mat-table>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-button (click)="accept()" color="primary" [disabled]="!selected">Ok</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -6,7 +6,12 @@
|
||||
<h2 mat-dialog-title>Receive Payment</h2>
|
||||
<mat-dialog-content>
|
||||
<form class="flex-col">
|
||||
<mat-table #table [dataSource]="dataSource()" [style.visibility]="this.displayTable() ? 'visible' : 'hidden'">
|
||||
<mat-table
|
||||
#table
|
||||
[dataSource]="formModel().amounts"
|
||||
[trackBy]="trackByRow"
|
||||
[style.visibility]="displayTable() ? 'visible' : 'hidden'"
|
||||
>
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef class="bold">Amount</mat-header-cell>
|
||||
@@ -16,7 +21,7 @@
|
||||
<!-- Amount Column -->
|
||||
<ng-container matColumnDef="amount">
|
||||
<mat-header-cell *matHeaderCellDef class="center bold" class="flex-auto">{{
|
||||
amount | currency: 'INR'
|
||||
data.amount | currency: 'INR'
|
||||
}}</mat-header-cell>
|
||||
<mat-cell *matCellDef="let row; let i = index" class="center flex-auto">
|
||||
<mat-form-field>
|
||||
@@ -29,7 +34,7 @@
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
|
||||
<mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row>
|
||||
</mat-table>
|
||||
<mat-form-field class="flex-auto" [style.visibility]="this.displayReason() ? 'visible' : 'hidden'">
|
||||
<mat-form-field class="flex-auto" [style.visibility]="displayReason() ? 'visible' : 'hidden'">
|
||||
<mat-label>Reason</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -43,7 +48,7 @@
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]>Cancel</button>
|
||||
<button type="button" mat-button (click)="accept()" color="primary" [disabled]="!!balance()">Ok</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CdkScrollableModule } from '@angular/cdk/scrolling';
|
||||
import { CurrencyPipe } from '@angular/common';
|
||||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
import { FormField, form } from '@angular/forms/signals';
|
||||
import { Component, computed, inject, linkedSignal, signal } from '@angular/core';
|
||||
import { form, FormField } from '@angular/forms/signals';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
@@ -15,10 +15,7 @@ import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-l
|
||||
import { VoucherType } from '../bills/voucher-type';
|
||||
|
||||
export interface ReceivePaymentFormData {
|
||||
amounts: {
|
||||
name: string;
|
||||
amount: number;
|
||||
}[];
|
||||
amounts: ReceivePaymentItem[];
|
||||
son: string;
|
||||
}
|
||||
|
||||
@@ -47,59 +44,45 @@ export class ReceivePaymentComponent {
|
||||
amount: number;
|
||||
}>(MAT_DIALOG_DATA);
|
||||
|
||||
typeSignal = signal<VoucherType | undefined>(this.data.type);
|
||||
type = this.data.type;
|
||||
|
||||
typeSignal = signal<VoucherType | undefined>(this.type);
|
||||
settleOptionsResource = this.ser.listForType(this.typeSignal);
|
||||
|
||||
choices = signal<ReceivePaymentItem[]>([]);
|
||||
choicesWithAmount = computed(() => {
|
||||
const amounts = this.formModel().amounts;
|
||||
return this.choices().map((c, i) => ({ ...c, amount: amounts[i]?.amount ?? 0 }));
|
||||
});
|
||||
displayReason = computed(() => this.settleOptionsResource.value()?.some((n) => n.hasReason) ?? false);
|
||||
displayTable = computed(() => (this.settleOptionsResource.value()?.length ?? 0) > 1);
|
||||
|
||||
type: VoucherType;
|
||||
amount: number;
|
||||
balance = computed(() => {
|
||||
return this.amount - this.formModel().amounts.reduce((a, c) => a + (c.amount ?? 0), 0);
|
||||
});
|
||||
|
||||
reason = '';
|
||||
displayReason = signal<boolean | undefined>(undefined);
|
||||
displayTable = signal<boolean | undefined>(undefined);
|
||||
formModel = signal<ReceivePaymentFormData>({
|
||||
amounts: [],
|
||||
son: '',
|
||||
formModel = linkedSignal({
|
||||
source: () => this.settleOptionsResource.value(),
|
||||
computation: (options): ReceivePaymentFormData => {
|
||||
if (!options) {
|
||||
return { amounts: [], son: '' };
|
||||
}
|
||||
const isTable = options.length > 1;
|
||||
return {
|
||||
amounts: options.map(
|
||||
(y) =>
|
||||
({
|
||||
...y,
|
||||
amount: !isTable ? this.data.amount : 0,
|
||||
}) as ReceivePaymentItem,
|
||||
),
|
||||
son: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
form = form(this.formModel);
|
||||
|
||||
dataSource = this.choicesWithAmount;
|
||||
balance = computed(() => {
|
||||
return this.data.amount - this.formModel().amounts.reduce((a, c) => a + (c.amount ?? 0), 0);
|
||||
});
|
||||
|
||||
displayedColumns = ['name', 'amount'];
|
||||
|
||||
constructor() {
|
||||
const data = this.data;
|
||||
this.type = data.type;
|
||||
this.amount = data.amount;
|
||||
this.displayReason.set(false);
|
||||
this.displayTable.set(false);
|
||||
reason = '';
|
||||
|
||||
effect(() => {
|
||||
const x = this.settleOptionsResource.value();
|
||||
if (!x) return;
|
||||
this.displayReason.set(x.reduce((o, n) => o || n.hasReason, false));
|
||||
this.displayTable.set(x.length > 1);
|
||||
const isTable = x.length > 1;
|
||||
const choices = x.map((y) => ({ ...y, amount: !isTable ? this.amount : 0 }) as ReceivePaymentItem);
|
||||
this.choices.set(choices);
|
||||
this.formModel.update((f) => ({
|
||||
...f,
|
||||
amounts: x.map((y) => ({
|
||||
name: y.name,
|
||||
amount: !isTable ? this.amount : 0,
|
||||
})),
|
||||
}));
|
||||
});
|
||||
}
|
||||
trackByRow = (_: number, row: ReceivePaymentItem) => row.id ?? row.name ?? _;
|
||||
|
||||
select(reason: string) {
|
||||
this.reason = reason.trim();
|
||||
@@ -107,7 +90,7 @@ export class ReceivePaymentComponent {
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
this.dialogRef.close({ choices: this.choicesWithAmount(), reason: this.reason });
|
||||
this.dialogRef.close({ choices: this.formModel().amounts, reason: this.formModel().son.trim() || this.reason });
|
||||
}
|
||||
|
||||
maxAmount(row: ReceivePaymentItem, index: number) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<h2>Running Tables</h2>
|
||||
|
||||
<div class="tables-grid">
|
||||
@for (table of list(); track table) {
|
||||
@for (table of list(); track table.id) {
|
||||
<mat-card
|
||||
class="flex-col square-button"
|
||||
matRipple
|
||||
@@ -19,7 +19,7 @@
|
||||
<span>{{ table.guest }}</span>
|
||||
<span>{{ table.pax ?? '-' }} / {{ table.seats }} / {{ table.section?.name }}</span>
|
||||
@if (table.date) {
|
||||
<span>{{ table.date }}</span>
|
||||
<span>{{ table.date | localTime: 'time' }}</span>
|
||||
}
|
||||
@if (table.amount) {
|
||||
<span>{{ table.amount | currency: 'INR' }}</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { map } from 'rxjs/operators';
|
||||
import { Table } from '../../core/table';
|
||||
import { RegimeService } from '../../regimes/regime.service';
|
||||
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
|
||||
import { LocalTimePipe } from '../../shared/local-time.pipe';
|
||||
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
|
||||
import { TableService } from '../../tables/table.service';
|
||||
import { BillNumberComponent } from '../bill-number/bill-number.component';
|
||||
@@ -18,7 +19,7 @@ import { BillNumberComponent } from '../bill-number/bill-number.component';
|
||||
selector: 'app-running-tables',
|
||||
templateUrl: './running-tables.component.html',
|
||||
styleUrls: ['./running-tables.component.sass'],
|
||||
imports: [CurrencyPipe, MatRippleModule, MatCardModule, ErrorStateComponent, SkeletonLoaderComponent],
|
||||
imports: [LocalTimePipe, CurrencyPipe, MatRippleModule, MatCardModule, ErrorStateComponent, SkeletonLoaderComponent],
|
||||
})
|
||||
export class RunningTablesComponent {
|
||||
private tableService = inject(TableService);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<mat-dialog-content>
|
||||
<form class="flex-col">
|
||||
<div>
|
||||
@for (sc of list(); track sc; let i = $index) {
|
||||
@for (sc of list(); track sc.id; let i = $index) {
|
||||
<div class="row-container">
|
||||
<mat-checkbox [formField]="form.saleCategories[i].saleCategory" class="flex-auto">{{ sc.name }}</mat-checkbox>
|
||||
</div>
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-button (click)="accept()" color="primary">Ok</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -1,7 +1,7 @@
|
||||
<h2 mat-dialog-title>Tables</h2>
|
||||
<mat-dialog-content>
|
||||
<div class="tables-grid">
|
||||
@for (table of list.value(); track table) {
|
||||
@for (table of list.value(); track table.id) {
|
||||
<mat-card
|
||||
class="flex-col square-button"
|
||||
matRipple
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions align="end">
|
||||
<mat-dialog-actions>
|
||||
<button mat-button [mat-dialog-close]="false">Cancel</button>
|
||||
<button type="button" mat-button (click)="accept()" color="primary">Ok</button>
|
||||
</mat-dialog-actions>
|
||||
@@ -9,7 +9,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Section</mat-label>
|
||||
<mat-select [formField]="form.section" (selectionChange)="show($event)">
|
||||
@for (s of sections(); track s) {
|
||||
@for (s of sections(); track s.id) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
@@ -33,7 +33,7 @@
|
||||
<mat-label>Printer</mat-label>
|
||||
<mat-select [formField]="form.saleCategories[i].printer">
|
||||
<mat-option [value]="null">-- Default --</mat-option>
|
||||
@for (p of printers(); track p) {
|
||||
@for (p of printers(); track p.id) {
|
||||
<mat-option [value]="p.id">
|
||||
{{ p.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -68,8 +68,8 @@ export class SettingsComponent {
|
||||
|
||||
runMaintenance() {
|
||||
const formModel = this.formModel();
|
||||
const startDate = DateTime.fromJSDate(formModel.startDate).toFormat('dd-MMM-yyyy');
|
||||
const finishDate = DateTime.fromJSDate(formModel.finishDate).toFormat('dd-MMM-yyyy');
|
||||
const startDate = DateTime.fromJSDate(formModel.startDate).toISODate() ?? '';
|
||||
const finishDate = DateTime.fromJSDate(formModel.finishDate).toISODate() ?? '';
|
||||
|
||||
if (!this.beerFile || !this.saleFile) {
|
||||
this.snackBar.open('Please choose both files first!', 'Danger');
|
||||
|
||||
+2
-2
@@ -15,7 +15,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Voucher Type</mat-label>
|
||||
<mat-select [formField]="form.voucherType">
|
||||
@for (vt of voucherTypes; track vt) {
|
||||
@for (vt of voucherTypes; track vt.id) {
|
||||
<mat-option [value]="vt.id">
|
||||
{{ vt.name }}
|
||||
</mat-option>
|
||||
@@ -25,7 +25,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Reporting Level</mat-label>
|
||||
<mat-select [formField]="form.reportingLevel">
|
||||
@for (rl of reportingLevel; track rl) {
|
||||
@for (rl of reportingLevel; track rl.id) {
|
||||
<mat-option [value]="rl.id">
|
||||
{{ rl.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -6,15 +6,21 @@ import { DateTime } from 'luxon';
|
||||
standalone: true,
|
||||
})
|
||||
export class LocalTimePipe implements PipeTransform {
|
||||
transform(value: string): string {
|
||||
if (value === undefined || value === null) {
|
||||
transform(value: string | undefined | null, format: 'datetime' | 'time' | 'date' = 'datetime'): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (value.length === 5) {
|
||||
return DateTime.fromFormat(value, 'HH:mm').minus({ minutes: new Date().getTimezoneOffset() }).toFormat('HH:mm');
|
||||
const dt =
|
||||
value.length === 5
|
||||
? DateTime.fromFormat(value, 'HH:mm').minus({ minutes: new Date().getTimezoneOffset() })
|
||||
: DateTime.fromISO(value).minus({ minutes: new Date().getTimezoneOffset() });
|
||||
|
||||
if (format === 'time') {
|
||||
return dt.toFormat('HH:mm');
|
||||
}
|
||||
return DateTime.fromFormat(value, 'dd-MMM-yyyy HH:mm')
|
||||
.minus({ minutes: new Date().getTimezoneOffset() })
|
||||
.toFormat('dd-MMM-yyyy HH:mm');
|
||||
if (format === 'date') {
|
||||
return dt.toFormat('dd-MMM-yyyy', { locale: 'en-US' });
|
||||
}
|
||||
return dt.toFormat('dd-MMM-yyyy HH:mm', { locale: 'en-US' });
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Section</mat-label>
|
||||
<mat-select [formField]="form.section">
|
||||
@for (s of sections(); track s) {
|
||||
@for (s of sections(); track s.id) {
|
||||
<mat-option [value]="s.id">
|
||||
{{ s.name }}
|
||||
</mat-option>
|
||||
|
||||
@@ -55,8 +55,8 @@ export class TaxReportComponent {
|
||||
computation: (infoVal): TaxReportFormData => {
|
||||
if (infoVal) {
|
||||
return {
|
||||
startDate: DateTime.fromFormat(this.info().startDate, 'dd-MMM-yyyy'),
|
||||
finishDate: DateTime.fromFormat(this.info().finishDate, 'dd-MMM-yyyy'),
|
||||
startDate: DateTime.fromISO(this.info().startDate),
|
||||
finishDate: DateTime.fromISO(this.info().finishDate),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -85,8 +85,8 @@ export class TaxReportComponent {
|
||||
const formModel = this.formModel();
|
||||
|
||||
return new TaxReport({
|
||||
startDate: formModel.startDate.toFormat('dd-MMM-yyyy'),
|
||||
finishDate: formModel.finishDate.toFormat('dd-MMM-yyyy'),
|
||||
startDate: formModel.startDate.toISODate() ?? '',
|
||||
finishDate: formModel.finishDate.toISODate() ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Regime</mat-label>
|
||||
<mat-select [formField]="form.regime">
|
||||
@for (reg of regimes(); track reg) {
|
||||
@for (reg of regimes(); track reg.id) {
|
||||
<mat-option [value]="reg.id">
|
||||
{{ reg.name }}
|
||||
</mat-option>
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Sale Category</mat-label>
|
||||
<mat-select [formField]="productForm.saleCategory">
|
||||
@for (sc of saleCategories(); track sc) {
|
||||
@for (sc of saleCategories(); track sc.id) {
|
||||
<mat-option [value]="sc.id">
|
||||
{{ sc.name }}
|
||||
</mat-option>
|
||||
@@ -131,7 +131,7 @@
|
||||
<mat-form-field class="flex-auto">
|
||||
<mat-label>Menu Category</mat-label>
|
||||
<mat-select [formField]="skuForm.menuCategory">
|
||||
@for (mc of menuCategories(); track mc) {
|
||||
@for (mc of menuCategories(); track mc.id) {
|
||||
<mat-option [value]="mc.id">
|
||||
{{ mc.name }}
|
||||
</mat-option>
|
||||
|
||||
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user