diff --git a/ansible/bake-pies.yml b/ansible/bake-pies.yml
index 3002600..291851d 100755
--- a/ansible/bake-pies.yml
+++ b/ansible/bake-pies.yml
@@ -59,15 +59,15 @@
line: "{{ terminus_key }}"
when: terminuskey_test.stdout == "0"
- - name: Check rohan public key
- shell: "grep -c \"{{ rohan_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
- register: rohankey_test
+ - name: Check clair public key
+ shell: "grep -c \"{{ clair_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
+ register: clairkey_test
- - name: Add Rohan public key
+ - name: Add clair public key
lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys"
- line: "{{ rohan_key }}"
- when: rohankey_test.stdout == "0"
+ line: "{{ clair_key }}"
+ when: clairkey_test.stdout == "0"
- name: Check Anjin public key
shell: "grep -c \"{{ anjin_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
@@ -146,21 +146,21 @@
groups: lp
append: yes
- - name: install rohan-redis-tunnel systemd unit file
+ - name: install nats-tunnel systemd unit file
template:
- src: "files/rohan-redis-tunnel.service"
- dest: "/etc/systemd/system/rohan-redis-tunnel.service"
+ src: "files/nats-tunnel.service"
+ dest: "/etc/systemd/system/nats-tunnel.service"
- - name: enable service rohan-redis-tunnel and ensure it is not masked
+ - name: enable service nats-tunnel and ensure it is not masked
systemd:
- name: rohan-redis-tunnel
+ name: nats-tunnel
enabled: yes
masked: no
- - name: Make sure rohan-redis-tunnel service is running
+ - name: Make sure nats-tunnel service is running
systemd:
state: started
- name: rohan-redis-tunnel
+ name: nats-tunnel
- name: install leardal systemd unit file
template:
diff --git a/ansible/files/config b/ansible/files/config
index cee95cd..9e56a2a 100644
--- a/ansible/files/config
+++ b/ansible/files/config
@@ -1,4 +1,4 @@
-Host rohan rohan.tanshu.com
- HostName rohan.tanshu.com
+Host {{ server }} {{ server }}.tanshu.com
+ HostName {{ server }}.tanshu.com
IdentityFile ~/.ssh/id_ed25519
User tanshu
diff --git a/ansible/files/frank.service b/ansible/files/frank.service
index 613a245..242d3a3 100644
--- a/ansible/files/frank.service
+++ b/ansible/files/frank.service
@@ -1,9 +1,9 @@
[Unit]
Description=Frank container service
-After=docker.service rohan-redis-tunnel.service
-Wants=network-online.target docker.socket rohan-redis-tunnel.service
-Requires=docker.socket rohan-redis-tunnel.service
-PartOf=rohan-redis-tunnel.service
+After=docker.service {{ server }}-redis-tunnel.service
+Wants=network-online.target docker.socket {{ server }}-redis-tunnel.service
+Requires=docker.socket {{ server }}-redis-tunnel.service
+PartOf={{ server }}-redis-tunnel.service
[Service]
Restart=always
diff --git a/ansible/files/leardal.service b/ansible/files/leardal.service
index 4795a3c..604e5bf 100644
--- a/ansible/files/leardal.service
+++ b/ansible/files/leardal.service
@@ -6,7 +6,7 @@ After=network.target
Type=simple
User={{ user }}
-ExecStart=/usr/bin/ssh -NTg -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no -i /home/{{ user }}/.ssh/id_ed25519 -R {{ ssh_port }}:localhost:22 tanshu@rohan.tanshu.com
+ExecStart=/usr/bin/ssh -NTg -o ServerAliveInterval=60 -o ExitOnForwardFailure=yes -o ServerAliveCountMax=3 -o StrictHostKeyChecking=no -i /home/{{ user }}/.ssh/id_ed25519 -R {{ ssh_port }}:localhost:22 tanshu@{{ server }}.tanshu.com
# Restart every >2 seconds to avoid StartLimitInterval failure
Restart=always
diff --git a/ansible/files/rohan-redis-tunnel.service b/ansible/files/nats-tunnel.service
similarity index 79%
rename from ansible/files/rohan-redis-tunnel.service
rename to ansible/files/nats-tunnel.service
index 2d67793..5cc3b89 100644
--- a/ansible/files/rohan-redis-tunnel.service
+++ b/ansible/files/nats-tunnel.service
@@ -5,7 +5,7 @@ After=network.target
[Service]
User={{ user }}
-ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes -o ServerAliveCountMax=2 -o StrictHostKeyChecking=no -i /home/{{ user }}/.ssh/id_ed25519 -L 6379:localhost:6379 tanshu@rohan.tanshu.com
+ExecStart=/usr/bin/ssh -NT -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes -o ServerAliveCountMax=2 -o StrictHostKeyChecking=no -i /home/{{ user }}/.ssh/id_ed25519 -L 4222:localhost:4222 tanshu@{{ server }}.tanshu.com
# Restart every >2 seconds to avoid StartLimitInterval failure
RestartSec=5
diff --git a/ansible/files/nginx.conf.j2 b/ansible/files/nginx.conf.j2
deleted file mode 100644
index 9bd5782..0000000
--- a/ansible/files/nginx.conf.j2
+++ /dev/null
@@ -1,39 +0,0 @@
-server {
-
- listen 80;
- server_name {{ http_host }};
-
- # Allow large attachments
- client_max_body_size 128M;
-
- location /api {
- proxy_set_header Host $host:$server_port;
- proxy_set_header X-Scheme $scheme;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_pass http://localhost:{{ host_port }};
- }
- location /token {
-
- proxy_set_header Host $host:$server_port;
- proxy_set_header X-Scheme $scheme;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_pass http://localhost:{{ host_port }};
- }
- location /refresh {
- proxy_set_header Host $host:$server_port;
- proxy_set_header X-Scheme $scheme;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_pass http://localhost:{{ host_port }};
- }
- location /db-image {
- proxy_set_header Host $host:$server_port;
- proxy_set_header X-Scheme $scheme;
- proxy_set_header X-Forwarded-For $remote_addr;
- proxy_pass http://localhost:{{ host_port }};
- }
- location / {
- root /var/lib/{{ host_directory }}/frontend;
- index index.html index.htm;
- try_files $uri $uri/ /index.html =404;
- }
-}
diff --git a/ansible/vars/pies.yml b/ansible/vars/pies.yml
index bcf7af6..2924a22 100644
--- a/ansible/vars/pies.yml
+++ b/ansible/vars/pies.yml
@@ -3,5 +3,6 @@ hostname: "moz2"
ssh_port: "22542"
user: "vari"
terminus_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8WQHla0uCcNwmN0DUE49lbjNWa6+7A6OxrX3WEPQH0 tanshu@terminus 2023.01.08"
-rohan_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKk6HHpwGQTfciXFaKOBWg+zh09XtTYvYxFZaaW3yMln tanshu@rohan 2023.08.06"
+clair_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBeTz3weHvjJRTlBg+ovP3KfQNDvyn9mVsekAGwX1HMn tanshu@clair 2025-06-30"
anjin_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMlvalE0JqKyjpEsGTgtf/N1d9QK2MgNFJib+e8O2h9M tanshu@anjin 2024.09.28"
+server: "clair"
diff --git a/barker/barker/__version__.py b/barker/barker/__version__.py
index 4bdf804..ba852a5 100644
--- a/barker/barker/__version__.py
+++ b/barker/barker/__version__.py
@@ -1 +1 @@
-__version__ = "14.5.0"
+__version__ = "14.5.1"
diff --git a/barker/barker/core/arq.py b/barker/barker/core/arq.py
deleted file mode 100644
index 1e405b6..0000000
--- a/barker/barker/core/arq.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from arq.connections import RedisSettings
-
-from .config import settings as sett
-
-
-settings = RedisSettings(host=sett.REDIS_HOST, port=sett.REDIS_PORT)
diff --git a/barker/barker/core/config.py b/barker/barker/core/config.py
index 743ae1b..35f3a70 100644
--- a/barker/barker/core/config.py
+++ b/barker/barker/core/config.py
@@ -18,14 +18,10 @@ class Settings(BaseSettings):
LOG_LEVEL: str = "NOTSET"
SQLALCHEMY_DATABASE_URI: str = ""
- REDIS_HOST: str = "127.0.0.1"
- REDIS_PORT: int = 6379
-
ALEMBIC_LOG_LEVEL: str = "INFO"
ALEMBIC_SQLALCHEMY_LOG_LEVEL: str = "WARN"
- WORKER_REDIS_HOST: str = "127.0.0.1"
- WORKER_REDIS_PORT: int = 6379
+ NATS_URL: str = "nats://127.0.0.1:4222"
WORKER_QUEUE_NAME: str = "arq:queue"
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env")
diff --git a/barker/barker/core/taskiq.py b/barker/barker/core/taskiq.py
new file mode 100644
index 0000000..6d6cf38
--- /dev/null
+++ b/barker/barker/core/taskiq.py
@@ -0,0 +1,28 @@
+import asyncio
+
+from taskiq_nats import PullBasedJetStreamBroker
+
+from .config import settings
+
+
+broker = PullBasedJetStreamBroker(settings.NATS_URL)
+
+
+@broker.task(task_name="sent_to_printer")
+async def sent_to_printer(data: str, address: str, cut_code: str) -> None:
+ pass
+
+
+def enqueue_print_job_sync(printer_name: str, data: str, address: str, cut_code: str) -> None:
+ async def _enqueue() -> None:
+ await broker.startup()
+ try:
+ await (
+ sent_to_printer.kicker().with_labels(queue=f"barker.print.{printer_name}").kiq(data, address, cut_code)
+ )
+ finally:
+ await broker.shutdown()
+
+ loop = asyncio.new_event_loop()
+ loop.run_until_complete(_enqueue())
+ loop.close()
diff --git a/barker/barker/main.py b/barker/barker/main.py
index be10f3e..2849890 100644
--- a/barker/barker/main.py
+++ b/barker/barker/main.py
@@ -4,7 +4,6 @@ import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
-from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.middleware.sessions import SessionMiddleware
@@ -148,7 +147,7 @@ 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.mount("/static", StaticFiles(directory="static"), name="static")
+app.frontend("/", directory="frontend", check_dir="auto")
def init() -> None:
diff --git a/barker/barker/printing/bill.py b/barker/barker/printing/bill.py
index e056ce4..f5a9572 100644
--- a/barker/barker/printing/bill.py
+++ b/barker/barker/printing/bill.py
@@ -1,16 +1,14 @@
-import asyncio
import re
import uuid
from datetime import timedelta
from decimal import Decimal
-from arq import ArqRedis, create_pool
from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
from ..core.config import settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.bill import Bill
from ..models.db_setting import DbSetting
from ..models.inventory import Inventory
@@ -49,22 +47,11 @@ def print_bill(voucher_id: uuid.UUID, db: Session) -> None:
bill_text = design_bill(voucher, regime_id, inventories, taxes_list, db)
bills.append(bill_text)
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
if len(bills) > 1:
total = design_total(voucher, regimes, db)
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", total, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
+ enqueue_print_job_sync(printer.name, total, printer.address, printer.cut_code)
for bill in bills:
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", bill, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, bill, printer.address, printer.cut_code)
def get_inventories(voucher: Voucher) -> list[Inventory]:
diff --git a/barker/barker/printing/cashier_report.py b/barker/barker/printing/cashier_report.py
index 27a259a..27f6a6f 100644
--- a/barker/barker/printing/cashier_report.py
+++ b/barker/barker/printing/cashier_report.py
@@ -1,14 +1,12 @@
-import asyncio
import uuid
from datetime import datetime, timedelta
-from arq import ArqRedis, create_pool
from sqlalchemy import select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
from ..core.config import settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
@@ -27,19 +25,8 @@ def print_cashier_report(report: CashierReport, device_id: uuid.UUID, db: Sessio
.where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one()
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", summary, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", details, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, summary, printer.address, printer.cut_code)
+ enqueue_print_job_sync(printer.name, details, printer.address, printer.cut_code)
def design_cashier_report_summary(report: CashierReport) -> str:
diff --git a/barker/barker/printing/discount_report.py b/barker/barker/printing/discount_report.py
index 24bf4ea..2130299 100644
--- a/barker/barker/printing/discount_report.py
+++ b/barker/barker/printing/discount_report.py
@@ -1,11 +1,9 @@
-import asyncio
import uuid
-from arq import ArqRedis, create_pool
from sqlalchemy import select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
@@ -23,14 +21,7 @@ def print_discount_report(report: DiscountReport, device_id: uuid.UUID, db: Sess
.where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one()
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", data, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
def design_discount_report(report: DiscountReport) -> str:
diff --git a/barker/barker/printing/kot.py b/barker/barker/printing/kot.py
index bc4eec6..daf71c5 100644
--- a/barker/barker/printing/kot.py
+++ b/barker/barker/printing/kot.py
@@ -1,15 +1,13 @@
-import asyncio
import uuid
from datetime import timedelta
from typing import Any
-from arq import ArqRedis, create_pool
from sqlalchemy import Date, func, or_, select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
from ..core.config import settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.inventory import Inventory
from ..models.kot import Kot
from ..models.printer import Printer
@@ -80,20 +78,9 @@ def print_kot(voucher_id: uuid.UUID, db: Session) -> None:
if key not in my_hash:
my_hash[key] = (printer, [])
my_hash[key][1].append(item)
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
for key, value in my_hash.items():
printer_id, copies = key
printer, items = value
for c in range(int(copies)):
data = design_kot(voucher, kot, items, c)
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer",
- data,
- printer.address,
- printer.cut_code,
- _queue_name=f"barker:print:{printer.name}",
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
diff --git a/barker/barker/printing/product_sale_report.py b/barker/barker/printing/product_sale_report.py
index 0a43bde..40b4a2f 100644
--- a/barker/barker/printing/product_sale_report.py
+++ b/barker/barker/printing/product_sale_report.py
@@ -1,14 +1,12 @@
-import asyncio
import uuid
from datetime import datetime, timedelta
-from arq import ArqRedis, create_pool
from sqlalchemy import select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
from ..core.config import settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
@@ -25,14 +23,7 @@ def print_product_sale_report(report: ProductSaleReport, device_id: uuid.UUID, d
.where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one()
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", data, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
def design_product_sale_report(report: ProductSaleReport) -> str:
diff --git a/barker/barker/printing/sale_report.py b/barker/barker/printing/sale_report.py
index 91a5b42..b829d9e 100644
--- a/barker/barker/printing/sale_report.py
+++ b/barker/barker/printing/sale_report.py
@@ -1,14 +1,12 @@
-import asyncio
import uuid
from datetime import datetime, timedelta
-from arq import ArqRedis, create_pool
from sqlalchemy import select
from sqlalchemy.orm import Session
-from ..core.arq import settings as redis_settings
from ..core.config import settings
+from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device
from ..models.printer import Printer
from ..models.section_printer import SectionPrinter
@@ -26,14 +24,7 @@ def print_sale_report(report: SaleReport, device_id: uuid.UUID, db: Session) ->
.where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one()
- loop = asyncio.new_event_loop()
- redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
- loop.run_until_complete(
- redis.enqueue_job(
- "sent_to_printer", data, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
- )
- )
- loop.close()
+ enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
def design_sale_report(report: SaleReport) -> str:
diff --git a/barker/barker/routers/customer.py b/barker/barker/routers/customer.py
index a890fc2..c002280 100644
--- a/barker/barker/routers/customer.py
+++ b/barker/barker/routers/customer.py
@@ -122,8 +122,8 @@ def show_term(
q: str | None,
p: int = 0, # Page number
s: int = 25, # Page size
- f: str | None = None, # Sort field
- d: str | None = None, # Sort direction
+ f: str = "name", # Sort field
+ d: str = "asc", # Sort direction
) -> list[schemas.Customer]:
# Handle dynamic sorting safely
sort_field = ALLOWED_SORT_FIELDS.get(f or "name", Customer.name) # Default to name
@@ -139,8 +139,8 @@ def customer_list(
filter: list[str],
page: int | None,
page_size: int | None,
- sort_field: InstrumentedAttribute[Any] | None,
- sort_direction: Any | None,
+ sort_field: InstrumentedAttribute[Any],
+ sort_direction: Any,
db: Session,
) -> tuple[Sequence[Customer], int]:
# Handle dynamic sorting safely
diff --git a/barker/pyproject.toml b/barker/pyproject.toml
index e2d70aa..7492d51 100644
--- a/barker/pyproject.toml
+++ b/barker/pyproject.toml
@@ -1,13 +1,12 @@
[project]
name = "barker"
-version = "14.5.0"
+version = "14.5.1"
description = "Point of Sale for a restaurant"
requires-python = ">=3.14"
authors = [{ name = "tanshu", email = "git@tanshu.com" }]
dependencies = [
"aiohttp>=3.14.3",
"alembic>=1.19.1",
- "arq>=0.28.0",
"cryptography>=50.0.0",
"fastapi[all]>=0.141.1",
"gunicorn>=26.0.0",
@@ -20,6 +19,8 @@ dependencies = [
"python-dotenv>=1.2.2",
"python-multipart>=0.0.32",
"sqlalchemy>=2.0.51",
+ "taskiq>=0.12.4",
+ "taskiq-nats>=0.6.0",
"uvicorn[standard]>=0.52.1",
]
@@ -105,9 +106,7 @@ ignore = [
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
-fixable = [
- "ALL"
-] # Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"] # Allow fix for all enabled rules (when `--fix`) is provided.
dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.isort]
diff --git a/barker/uv.lock b/barker/uv.lock
index 4e10573..6dafc25 100644
--- a/barker/uv.lock
+++ b/barker/uv.lock
@@ -129,19 +129,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
-[[package]]
-name = "arq"
-version = "0.28.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "redis", extra = ["hiredis"] },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a4/81/7f9db65a89c29ba374000309b9dd95509500045df5c7e22f26c3731b7380/arq-0.28.0.tar.gz", hash = "sha256:a458188aefc2d7ee17d136f80d8fa8df1d6eba4ceebdead87e9f172d027dc311", size = 416141, upload-time = "2026-04-16T10:50:23.893Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/32/66b616976c5058d434ca2017979bfffd784888177b16b5038abcec93954a/arq-0.28.0-py3-none-any.whl", hash = "sha256:b1696bf5614d60f4172a2c0cbdc177e23ba03a5eb9acc29bd8181f4ea71fff94", size = 26061, upload-time = "2026-04-16T10:50:22.321Z" },
-]
-
[[package]]
name = "ast-serialize"
version = "0.8.0"
@@ -244,12 +231,11 @@ wheels = [
[[package]]
name = "barker"
-version = "14.5.0"
+version = "14.5.1"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
{ name = "alembic" },
- { name = "arq" },
{ name = "cryptography" },
{ name = "fastapi", extra = ["all"] },
{ name = "gunicorn" },
@@ -262,6 +248,8 @@ dependencies = [
{ name = "python-dotenv" },
{ name = "python-multipart" },
{ name = "sqlalchemy" },
+ { name = "taskiq" },
+ { name = "taskiq-nats" },
{ name = "uvicorn", extra = ["standard"] },
]
@@ -278,7 +266,6 @@ dev = [
requires-dist = [
{ name = "aiohttp", specifier = ">=3.14.3" },
{ name = "alembic", specifier = ">=1.19.1" },
- { name = "arq", specifier = ">=0.28.0" },
{ name = "cryptography", specifier = ">=50.0.0" },
{ name = "fastapi", extras = ["all"], specifier = ">=0.141.1" },
{ name = "gunicorn", specifier = ">=26.0.0" },
@@ -291,6 +278,8 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "python-multipart", specifier = ">=0.0.32" },
{ name = "sqlalchemy", specifier = ">=2.0.51" },
+ { name = "taskiq", specifier = ">=0.12.4" },
+ { name = "taskiq-nats", specifier = ">=0.6.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.52.1" },
]
@@ -720,40 +709,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
-[[package]]
-name = "hiredis"
-version = "3.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/82/d2817ce0653628e0a0cb128533f6af0dd6318a49f3f3a6a7bd1f2f2154af/hiredis-3.3.0.tar.gz", hash = "sha256:105596aad9249634361815c574351f1bd50455dc23b537c2940066c4a9dea685", size = 89048, upload-time = "2025-10-14T16:33:34.263Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/b3/b948ee76a6b2bc7e45249861646f91f29704f743b52565cf64cee9c4658b/hiredis-3.3.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c135bda87211f7af9e2fd4e046ab433c576cd17b69e639a0f5bb2eed5e0e71a9", size = 82105, upload-time = "2025-10-14T16:32:37.204Z" },
- { url = "https://files.pythonhosted.org/packages/a2/9b/4210f4ebfb3ab4ada964b8de08190f54cbac147198fb463cd3c111cc13e0/hiredis-3.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f855c678230aed6fc29b962ce1cc67e5858a785ef3a3fd6b15dece0487a2e60", size = 46237, upload-time = "2025-10-14T16:32:38.07Z" },
- { url = "https://files.pythonhosted.org/packages/b3/7a/e38bfd7d04c05036b4ccc6f42b86b1032185cf6ae426e112a97551fece14/hiredis-3.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4059c78a930cbb33c391452ccce75b137d6f89e2eebf6273d75dafc5c2143c03", size = 41894, upload-time = "2025-10-14T16:32:38.929Z" },
- { url = "https://files.pythonhosted.org/packages/28/d3/eae43d9609c5d9a6effef0586ee47e13a0d84b44264b688d97a75cd17ee5/hiredis-3.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:334a3f1d14c253bb092e187736c3384203bd486b244e726319bbb3f7dffa4a20", size = 170486, upload-time = "2025-10-14T16:32:40.147Z" },
- { url = "https://files.pythonhosted.org/packages/c3/fd/34d664554880b27741ab2916d66207357563b1639e2648685f4c84cfb755/hiredis-3.3.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd137b147235447b3d067ec952c5b9b95ca54b71837e1b38dbb2ec03b89f24fc", size = 182031, upload-time = "2025-10-14T16:32:41.06Z" },
- { url = "https://files.pythonhosted.org/packages/08/a3/0c69fdde3f4155b9f7acc64ccffde46f312781469260061b3bbaa487fd34/hiredis-3.3.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f88f4f2aceb73329ece86a1cb0794fdbc8e6d614cb5ca2d1023c9b7eb432db8", size = 180542, upload-time = "2025-10-14T16:32:42.993Z" },
- { url = "https://files.pythonhosted.org/packages/68/7a/ad5da4d7bc241e57c5b0c4fe95aa75d1f2116e6e6c51577394d773216e01/hiredis-3.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:550f4d1538822fc75ebf8cf63adc396b23d4958bdbbad424521f2c0e3dfcb169", size = 172353, upload-time = "2025-10-14T16:32:43.965Z" },
- { url = "https://files.pythonhosted.org/packages/4b/dc/c46eace64eb047a5b31acd5e4b0dc6d2f0390a4a3f6d507442d9efa570ad/hiredis-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54b14211fbd5930fc696f6fcd1f1f364c660970d61af065a80e48a1fa5464dd6", size = 166435, upload-time = "2025-10-14T16:32:44.97Z" },
- { url = "https://files.pythonhosted.org/packages/4a/ac/ad13a714e27883a2e4113c980c94caf46b801b810de5622c40f8d3e8335f/hiredis-3.3.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9e96f63dbc489fc86f69951e9f83dadb9582271f64f6822c47dcffa6fac7e4a", size = 177218, upload-time = "2025-10-14T16:32:45.936Z" },
- { url = "https://files.pythonhosted.org/packages/c2/38/268fabd85b225271fe1ba82cb4a484fcc1bf922493ff2c74b400f1a6f339/hiredis-3.3.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:106e99885d46684d62ab3ec1d6b01573cc0e0083ac295b11aaa56870b536c7ec", size = 170477, upload-time = "2025-10-14T16:32:46.898Z" },
- { url = "https://files.pythonhosted.org/packages/20/6b/02bb8af810ea04247334ab7148acff7a61c08a8832830c6703f464be83a9/hiredis-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:087e2ef3206361281b1a658b5b4263572b6ba99465253e827796964208680459", size = 167915, upload-time = "2025-10-14T16:32:47.847Z" },
- { url = "https://files.pythonhosted.org/packages/83/94/901fa817e667b2e69957626395e6dee416e31609dca738f28e6b545ca6c2/hiredis-3.3.0-cp314-cp314-win32.whl", hash = "sha256:80638ebeab1cefda9420e9fedc7920e1ec7b4f0513a6b23d58c9d13c882f8065", size = 21165, upload-time = "2025-10-14T16:32:50.753Z" },
- { url = "https://files.pythonhosted.org/packages/b1/7e/4881b9c1d0b4cdaba11bd10e600e97863f977ea9d67c5988f7ec8cd363e5/hiredis-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a68aaf9ba024f4e28cf23df9196ff4e897bd7085872f3a30644dca07fa787816", size = 22996, upload-time = "2025-10-14T16:32:51.543Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b6/d7e6c17da032665a954a89c1e6ee3bd12cb51cd78c37527842b03519981d/hiredis-3.3.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f7f80442a32ce51ee5d89aeb5a84ee56189a0e0e875f1a57bbf8d462555ae48f", size = 83034, upload-time = "2025-10-14T16:32:52.395Z" },
- { url = "https://files.pythonhosted.org/packages/27/6c/6751b698060cdd1b2d8427702cff367c9ed7a1705bcf3792eb5b896f149b/hiredis-3.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a1a67530da714954ed50579f4fe1ab0ddbac9c43643b1721c2cb226a50dde263", size = 46701, upload-time = "2025-10-14T16:32:53.572Z" },
- { url = "https://files.pythonhosted.org/packages/ce/8e/20a5cf2c83c7a7e08c76b9abab113f99f71cd57468a9c7909737ce6e9bf8/hiredis-3.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:616868352e47ab355559adca30f4f3859f9db895b4e7bc71e2323409a2add751", size = 42381, upload-time = "2025-10-14T16:32:54.762Z" },
- { url = "https://files.pythonhosted.org/packages/be/0a/547c29c06e8c9c337d0df3eec39da0cf1aad701daf8a9658dd37f25aca66/hiredis-3.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e799b79f3150083e9702fc37e6243c0bd47a443d6eae3f3077b0b3f510d6a145", size = 180313, upload-time = "2025-10-14T16:32:55.644Z" },
- { url = "https://files.pythonhosted.org/packages/89/8a/488de5469e3d0921a1c425045bf00e983d48b2111a90e47cf5769eaa536c/hiredis-3.3.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ef1dfb0d2c92c3701655e2927e6bbe10c499aba632c7ea57b6392516df3864b", size = 190488, upload-time = "2025-10-14T16:32:56.649Z" },
- { url = "https://files.pythonhosted.org/packages/b5/59/8493edc3eb9ae0dbea2b2230c2041a52bc03e390b02ffa3ac0bca2af9aea/hiredis-3.3.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c290da6bc2a57e854c7da9956cd65013483ede935677e84560da3b848f253596", size = 189210, upload-time = "2025-10-14T16:32:57.759Z" },
- { url = "https://files.pythonhosted.org/packages/f0/de/8c9a653922057b32fb1e2546ecd43ef44c9aa1a7cf460c87cae507eb2bc7/hiredis-3.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd8c438d9e1728f0085bf9b3c9484d19ec31f41002311464e75b69550c32ffa8", size = 180972, upload-time = "2025-10-14T16:32:58.737Z" },
- { url = "https://files.pythonhosted.org/packages/e4/a3/51e6e6afaef2990986d685ca6e254ffbd191f1635a59b2d06c9e5d10c8a2/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bbc6b8a88bbe331e3ebf6685452cebca6dfe6d38a6d4efc5651d7e363ba28bd", size = 175315, upload-time = "2025-10-14T16:32:59.774Z" },
- { url = "https://files.pythonhosted.org/packages/96/54/e436312feb97601f70f8b39263b8da5ac4a5d18305ebdfb08ad7621f6119/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:55d8c18fe9a05496c5c04e6eccc695169d89bf358dff964bcad95696958ec05f", size = 185653, upload-time = "2025-10-14T16:33:00.749Z" },
- { url = "https://files.pythonhosted.org/packages/ed/a3/88e66030d066337c6c0f883a912c6d4b2d6d7173490fbbc113a6cbe414ff/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4ddc79afa76b805d364e202a754666cb3c4d9c85153cbfed522871ff55827838", size = 179032, upload-time = "2025-10-14T16:33:01.711Z" },
- { url = "https://files.pythonhosted.org/packages/bc/1f/fb7375467e9adaa371cd617c2984fefe44bdce73add4c70b8dd8cab1b33a/hiredis-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e8a4b8540581dcd1b2b25827a54cfd538e0afeaa1a0e3ca87ad7126965981cc", size = 176127, upload-time = "2025-10-14T16:33:02.793Z" },
- { url = "https://files.pythonhosted.org/packages/66/14/0dc2b99209c400f3b8f24067273e9c3cb383d894e155830879108fb19e98/hiredis-3.3.0-cp314-cp314t-win32.whl", hash = "sha256:298593bb08487753b3afe6dc38bac2532e9bac8dcee8d992ef9977d539cc6776", size = 22024, upload-time = "2025-10-14T16:33:03.812Z" },
- { url = "https://files.pythonhosted.org/packages/b2/2f/8a0befeed8bbe142d5a6cf3b51e8cbe019c32a64a596b0ebcbc007a8f8f1/hiredis-3.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b442b6ab038a6f3b5109874d2514c4edf389d8d8b553f10f12654548808683bc", size = 23808, upload-time = "2025-10-14T16:33:04.965Z" },
-]
-
[[package]]
name = "httpcore"
version = "1.0.9"
@@ -1079,6 +1034,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "nats-py"
+version = "2.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/f0/fc5e93f2b0dd14a202590ad9d30eda1955ea872039b5204357348d0f4b1e/nats_py-2.15.0.tar.gz", hash = "sha256:6622c547d9a7d2313d9c147d46c386188f4ec2c7b5c9f9a0438a4d1b55f54a93", size = 75995, upload-time = "2026-06-05T07:34:03.904Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/a8/b55606c7c621fb813c8ec78baf201d2c78bf6051091ec0c7ada572999e95/nats_py-2.15.0-py3-none-any.whl", hash = "sha256:9f8d36aa52a9926a88b8f1d70cf1fdce0ad387941479b500ee9ab3e51073cefd", size = 90334, upload-time = "2026-06-05T07:34:02.81Z" },
+]
+
[[package]]
name = "nltk"
version = "3.9.2"
@@ -1258,6 +1222,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
+[[package]]
+name = "pycron"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/5d/340be12ae4a69c33102dfb6ddc1dc6e53e69b2d504fa26b5d34a472c3057/pycron-3.2.0.tar.gz", hash = "sha256:e125a28aca0295769541a40633f70b602579df48c9cb357c36c28d2628ba2b13", size = 4248, upload-time = "2025-06-05T13:24:12.636Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/76/caf316909f4545e7158e0e1defd8956a1da49f4af04f5d16b18c358dfeac/pycron-3.2.0-py3-none-any.whl", hash = "sha256:6d2349746270bd642b71b9f7187cf13f4d9ee2412b4710396a507b5fe4f60dac", size = 4904, upload-time = "2025-06-05T13:24:11.477Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.13.4"
@@ -1408,23 +1381,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
-[[package]]
-name = "redis"
-version = "5.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyjwt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6a/cf/128b1b6d7086200c9f387bd4be9b2572a30b90745ef078bd8b235042dc9f/redis-5.3.1.tar.gz", hash = "sha256:ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c", size = 4626200, upload-time = "2025-07-25T08:06:27.778Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/26/5c5fa0e83c3621db835cfc1f1d789b37e7fa99ed54423b5f519beb931aa7/redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97", size = 272833, upload-time = "2025-07-25T08:06:26.317Z" },
-]
-
-[package.optional-dependencies]
-hiredis = [
- { name = "hiredis" },
-]
-
[[package]]
name = "regex"
version = "2026.2.19"
@@ -1679,6 +1635,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" },
]
+[[package]]
+name = "taskiq"
+version = "0.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "anyio" },
+ { name = "packaging" },
+ { name = "pycron" },
+ { name = "pydantic" },
+ { name = "taskiq-dependencies" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/68/7b3c557782af291e22b26479127f287520fdf066d4684838e1bdd146cdc0/taskiq-0.12.5.tar.gz", hash = "sha256:fbdfc949fdefe746f771195faa34769fa6dbbb109cbd41cb1d056b794f933f43", size = 407451, upload-time = "2026-08-21T14:25:50.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/4b/c37a3cdfaff2ff53c3ca5d5289636b42141c9964f67d16df3017a97be9c6/taskiq-0.12.5-py3-none-any.whl", hash = "sha256:3937969fcb5290d1a007e068e7af45d0a44f885e3bfe4bf1860f7e81794930f8", size = 94204, upload-time = "2026-08-21T14:25:48.948Z" },
+]
+
+[[package]]
+name = "taskiq-dependencies"
+version = "1.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/90/47a627696e53bfdcacabc3e8c05b73bf1424685bcb5f17209cb8b12da1bf/taskiq_dependencies-1.5.7.tar.gz", hash = "sha256:0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4", size = 14875, upload-time = "2025-02-26T22:07:39.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", size = 13801, upload-time = "2025-02-26T22:07:38.622Z" },
+]
+
+[[package]]
+name = "taskiq-nats"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nats-py" },
+ { name = "taskiq" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/db/4a/35a179f3bb4e41048fe68401cd3fae91c371e9b877f9266b3d8e80fb13e6/taskiq_nats-0.6.0.tar.gz", hash = "sha256:3335fc416e9872a6ed3dbb0af3b51d1d61f23aaa9983c91337c16534abe71c72", size = 7611, upload-time = "2025-11-25T11:06:03.02Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/b0/6b564579476b26c1e881e051e256c62ba3eb4a0f522fb536531e35f150d2/taskiq_nats-0.6.0-py3-none-any.whl", hash = "sha256:3d08288cbcca1da500663a45a3c33f03160721380e61cb2351e962f4805cadba", size = 8930, upload-time = "2025-11-25T11:06:02.048Z" },
+]
+
[[package]]
name = "tenacity"
version = "9.1.4"
diff --git a/bookie/package.json b/bookie/package.json
index 2c1ede2..6b082fb 100644
--- a/bookie/package.json
+++ b/bookie/package.json
@@ -1,6 +1,6 @@
{
"name": "bookie",
- "version": "14.5.0",
+ "version": "14.5.1",
"scripts": {
"ng": "ng",
"start": "ng serve",
diff --git a/bookie/src/app/app.environment.ts b/bookie/src/app/app.environment.ts
index 96913ab..7ce7e1e 100644
--- a/bookie/src/app/app.environment.ts
+++ b/bookie/src/app/app.environment.ts
@@ -1,7 +1,7 @@
export const environment = {
production: true,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
- version: '14.5.0',
+ version: '14.5.1',
};
export const dateFormat = {
diff --git a/bookie/src/app/auth/login/login.component.html b/bookie/src/app/auth/login/login.component.html
index 57d3aa7..39050e3 100644
--- a/bookie/src/app/auth/login/login.component.html
+++ b/bookie/src/app/auth/login/login.component.html
@@ -3,14 +3,14 @@
Username
-
+
Password
-
- {{ hide ? 'visibility' : 'visibility_off' }}
+
+ {{ hide() ? 'visibility' : 'visibility_off' }}
@if (unregisteredDevice()) {
diff --git a/bookie/src/app/auth/login/login.component.ts b/bookie/src/app/auth/login/login.component.ts
index 31c9f66..739c917 100644
--- a/bookie/src/app/auth/login/login.component.ts
+++ b/bookie/src/app/auth/login/login.component.ts
@@ -1,13 +1,4 @@
-import {
- AfterViewInit,
- ChangeDetectorRef,
- Component,
- ElementRef,
- OnInit,
- ViewChild,
- inject,
- signal,
-} from '@angular/core';
+import { Component, afterNextRender, inject, input, signal } from '@angular/core';
import { form, FormField, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
@@ -15,7 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
-import { ActivatedRoute, Router } from '@angular/router';
+import { Router } from '@angular/router';
import { CookieService } from '../../shared/cookie.service';
import { AuthService } from '../auth.service';
@@ -31,14 +22,15 @@ export interface LoginFormData {
styleUrls: ['./login.component.css'],
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, FormField],
})
-export class LoginComponent implements OnInit, AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
- private route = inject(ActivatedRoute);
+export class LoginComponent {
private auth = inject(AuthService);
private router = inject(Router);
private snackBar = inject(MatSnackBar);
private cs = inject(CookieService);
- private cd = inject(ChangeDetectorRef);
+
+ returnUrl = input('/', {
+ transform: (v: string | null | undefined): string => v ?? '/',
+ });
formModel = signal({
username: '',
@@ -50,28 +42,14 @@ export class LoginComponent implements OnInit, AfterViewInit {
required(schemaPath.password);
});
- hide: boolean;
- unregisteredDevice = signal(undefined);
- deviceName = signal(undefined);
- private returnUrl: string;
+ hide = signal(true);
+ unregisteredDevice = signal(false);
+ deviceName = signal('');
constructor() {
- this.hide = true;
- this.unregisteredDevice.set(false);
- this.deviceName.set('');
- this.returnUrl = '';
- }
-
- ngOnInit() {
- this.returnUrl = this.route.snapshot.queryParams['returnUrl'] ?? '/';
- }
-
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
login(): void {
@@ -80,13 +58,12 @@ export class LoginComponent implements OnInit, AfterViewInit {
const password = formModel.password ?? '';
this.auth.login(username.trim(), password.trim()).subscribe({
next: () => {
- this.router.navigateByUrl(this.returnUrl);
+ this.router.navigateByUrl(this.returnUrl() ?? '/');
},
error: (error) => {
if (error.status === 401 && error.error.detail === 'Device is not registered') {
this.unregisteredDevice.set(true);
this.deviceName.set(this.cs.getCookie('device'));
- this.cd.detectChanges();
}
this.snackBar.open(error.error.detail, 'Error');
},
diff --git a/bookie/src/app/auth/logout/logout.component.ts b/bookie/src/app/auth/logout/logout.component.ts
index 5b9fab2..8425559 100644
--- a/bookie/src/app/auth/logout/logout.component.ts
+++ b/bookie/src/app/auth/logout/logout.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, inject } from '@angular/core';
+import { Component, inject } from '@angular/core';
import { AuthService } from '../auth.service';
@@ -7,10 +7,10 @@ import { AuthService } from '../auth.service';
template: '',
standalone: true,
})
-export class LogoutComponent implements OnInit {
+export class LogoutComponent {
private auth = inject(AuthService);
- ngOnInit() {
+ constructor() {
this.auth.logout();
}
}
diff --git a/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.html b/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.html
index d8337ca..8ee1cdc 100644
--- a/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.html
+++ b/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.html
@@ -5,7 +5,7 @@
Item
-
+
@for (p of products(); track p) {
{{ p.name }}
}
diff --git a/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.ts b/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.ts
index aaf7198..dd10415 100644
--- a/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.ts
+++ b/bookie/src/app/bundles/bundle-detail/bundle-detail-dialog.component.ts
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common';
-import { Component, OnInit, inject, signal, computed, effect, debounced } from '@angular/core';
+import { Component, inject, computed, debounced, linkedSignal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
-import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@@ -13,7 +13,7 @@ import { ProductService } from '../../product/product.service';
import { BundleItem } from '../bundle';
export interface BundleDetailDialogFormData {
- product: ProductQuery | string;
+ product: BundleItem | ProductQuery | string;
quantity: number;
salePrice: number;
printInBill: boolean;
@@ -35,19 +35,20 @@ export interface BundleDetailDialogFormData {
FormField,
],
})
-export class BundleDetailDialogComponent implements OnInit {
+export class BundleDetailDialogComponent {
private readonly dialogRef = inject>(MatDialogRef);
private readonly productSer = inject(ProductService);
data = inject<{ item: BundleItem }>(MAT_DIALOG_DATA);
- selectedProduct = signal(null);
-
- formModel = signal({
- product: '',
- quantity: 1,
- salePrice: 0,
- printInBill: true,
+ formModel = linkedSignal({
+ source: () => this.data.item,
+ computation: (item) => ({
+ product: item,
+ quantity: Number(item.quantity ?? 1),
+ salePrice: Number(item.salePrice ?? 0),
+ printInBill: item.printInBill ?? false,
+ }),
});
form = form(this.formModel);
@@ -59,62 +60,35 @@ export class BundleDetailDialogComponent implements OnInit {
debouncedProduct = debounced(this.productSignal, 150);
- productsResource = this.productSer.query(computed(() => this.debouncedProduct.value()));
+ productsResource = this.productSer.query(this.debouncedProduct.value);
products = computed(() => {
if (!this.debouncedProduct.value()) return [];
return this.productsResource.value() ?? [];
});
- constructor() {
- // typing after selecting invalidates prior selection
- effect(() => {
- const x = this.formModel().product;
- if (typeof x === 'string') this.selectedProduct.set(null);
- });
- }
-
- ngOnInit(): void {
- // preload display text using existing name
- this.formModel.set({
- product: this.data.item.name ?? '',
- quantity: Number(this.data.item.quantity ?? 1),
- salePrice: Number(this.data.item.salePrice ?? 0),
- printInBill: this.data.item.printInBill,
- });
-
- this.selectedProduct.set(null);
- }
-
displayFn(product?: ProductQuery | string): string {
return !product ? '' : typeof product === 'string' ? product : product.name;
}
- productSelected(event: MatAutocompleteSelectedEvent): void {
- const p = event.option.value as ProductQuery;
- this.selectedProduct.set(p);
- }
-
accept(): void {
- const v = this.formModel();
+ const formValue = this.form().value();
- const quantity = Number(v.quantity ?? 0);
+ const quantity = Number(formValue.quantity ?? 0);
if (Number.isNaN(quantity) || quantity <= 0) return;
- const salePrice = Number(v.salePrice ?? 0);
+ const salePrice = Number(formValue.salePrice ?? 0);
if (Number.isNaN(salePrice) || salePrice < 0) return;
- // If a new product was selected, update itemId + name
- if (this.selectedProduct()?.id) {
- this.data.item.itemId = this.selectedProduct()?.id ?? '';
- this.data.item.name = this.selectedProduct()?.name ?? '';
- } else {
- // If no selection, don't allow changing SKU silently
- // but still allow quantity/price edit
+ const product = typeof formValue.product === 'string' ? null : formValue.product;
+ if (!product || !product.id) {
+ return;
}
+ this.data.item.itemId = product.id ?? '';
+ this.data.item.name = product.name ?? '';
this.data.item.quantity = quantity;
this.data.item.salePrice = salePrice;
- this.data.item.printInBill = v.printInBill || false;
+ this.data.item.printInBill = formValue.printInBill ?? false;
this.dialogRef.close(this.data.item);
}
diff --git a/bookie/src/app/bundles/bundle-detail/bundle-detail.component.html b/bookie/src/app/bundles/bundle-detail/bundle-detail.component.html
index 7d47054..70d60ef 100644
--- a/bookie/src/app/bundles/bundle-detail/bundle-detail.component.html
+++ b/bookie/src/app/bundles/bundle-detail/bundle-detail.component.html
@@ -4,7 +4,7 @@
Name
-
+
@@ -65,7 +65,7 @@
[matAutocomplete]="autoItem"
[value]="displayFn(formModel().addRow.itemId)"
/>
-
+
@for (p of itemProducts(); track p) {
{{ p.name }}
}
@@ -88,7 +88,7 @@
-
+
Item
@@ -143,7 +143,7 @@
- @if (!!item.id) {
+ @if (!!item().id) {
}
diff --git a/bookie/src/app/bundles/bundle-detail/bundle-detail.component.ts b/bookie/src/app/bundles/bundle-detail/bundle-detail.component.ts
index 05834c2..9d6a518 100644
--- a/bookie/src/app/bundles/bundle-detail/bundle-detail.component.ts
+++ b/bookie/src/app/bundles/bundle-detail/bundle-detail.component.ts
@@ -1,19 +1,7 @@
import { CurrencyPipe } from '@angular/common';
-import {
- AfterViewInit,
- Component,
- ElementRef,
- ViewChild,
- inject,
- computed,
- signal,
- effect,
- debounced,
- linkedSignal,
- input,
-} from '@angular/core';
+import { Component, inject, computed, effect, debounced, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
-import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core';
@@ -24,7 +12,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
-import { ActivatedRoute, Router } from '@angular/router';
+import { Router } from '@angular/router';
import { MenuCategory } from '../../core/menu-category';
import { ProductQuery } from '../../core/product-query';
@@ -41,8 +29,8 @@ export interface BundleDetailFormData {
name: string;
units: string;
salePrice: number;
- menuCategory: string;
- saleCategory: string;
+ menuCategory: MenuCategory | null;
+ saleCategory: SaleCategory | null;
hasHappyHour: boolean;
isNotAvailable: boolean;
addRow: {
@@ -51,6 +39,7 @@ export interface BundleDetailFormData {
salePrice: number;
printInBill: boolean;
};
+ items: BundleItem[];
}
@Component({
@@ -71,53 +60,34 @@ export interface BundleDetailFormData {
FormField,
],
})
-export class BundleDetailComponent implements AfterViewInit {
- @ViewChild('name', { static: true }) nameElement?: ElementRef;
+export class BundleDetailComponent {
private menuCategoryService = inject(MenuCategoryService);
private saleCategoryService = inject(SaleCategoryService);
- private route = inject(ActivatedRoute);
private router = inject(Router);
private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar);
private ser = inject(BundleService);
private productSer = inject(ProductService);
- id = input(null, { transform: (v: string | null | undefined) => v ?? null });
-
- saleCategoriesResource = this.saleCategoryService.list();
-
menuCategoriesResource = this.menuCategoryService.list(undefined);
-
+ menuCategories = computed(() => this.menuCategoriesResource.value() ?? []);
+ saleCategoriesResource = this.saleCategoryService.list();
+ saleCategories = computed(() => this.saleCategoriesResource.value() ?? []);
+ id = input(null, { transform: (v: string | null | undefined) => v ?? null });
itemResource = this.ser.get(this.id);
-
+ item = computed(() => this.itemResource.value() ?? new Bundle());
formModel = linkedSignal({
- source: () => this.menuCategoriesResource.value(),
- computation: (menuCategoriesVal): BundleDetailFormData => {
- if (menuCategoriesVal) {
- return {
- name: this.item.name ?? '',
- units: this.item.units ?? '',
- salePrice: Number(this.item.salePrice ?? 0),
- menuCategory: this.item.menuCategory?.id ?? '',
- saleCategory: this.item.saleCategory?.id ?? '',
- hasHappyHour: this.item.hasHappyHour ?? false,
- isNotAvailable: this.item.isNotAvailable ?? false,
-
- addRow: {
- itemId: '',
- quantity: 1,
- salePrice: 0,
- printInBill: true,
- },
- };
- }
+ source: this.item,
+ computation: (itemVal): BundleDetailFormData => {
+ const items = [...(itemVal.items ?? [])];
return {
- name: '',
- units: '',
- salePrice: 0,
- menuCategory: '',
- saleCategory: '',
- hasHappyHour: false,
- isNotAvailable: false,
+ name: itemVal.name ?? '',
+ units: itemVal.units ?? '',
+ salePrice: Number(itemVal.salePrice ?? 0),
+ menuCategory: itemVal.menuCategory ?? null,
+ saleCategory: itemVal.saleCategory ?? null,
+ hasHappyHour: itemVal.hasHappyHour ?? false,
+ isNotAvailable: itemVal.isNotAvailable ?? false,
+ items,
addRow: {
itemId: '',
quantity: 1,
@@ -130,25 +100,15 @@ export class BundleDetailComponent implements AfterViewInit {
form = form(this.formModel);
- menuCategories = signal([]);
- saleCategories = signal([]);
- items = signal([]);
- dataSource = computed(() => this.items());
-
- item: Bundle = new Bundle();
-
- itemProduct = signal(null);
-
- itemIdSignal = computed(() => {
- const x = this.formModel().addRow?.itemId;
- return typeof x === 'string' ? x.trim() : '';
+ private itemProductSearch = computed(() => {
+ const v = this.form.addRow.itemId().value();
+ return typeof v === 'string' ? v : null;
});
- debouncedItemId = debounced(this.itemIdSignal, 150);
+ debouncedItemProduct = debounced(this.itemProductSearch, 150);
- itemProductsResource = this.productSer.query(computed(() => this.debouncedItemId.value()));
+ itemProductsResource = this.productSer.query(this.debouncedItemProduct.value);
itemProducts = computed(() => {
- if (!this.debouncedItemId.value()) return [];
return this.itemProductsResource.value() ?? [];
});
@@ -156,25 +116,26 @@ export class BundleDetailComponent implements AfterViewInit {
constructor() {
effect(() => {
- const x = this.formModel().addRow?.itemId;
- if (typeof x === 'string') {
- this.itemProduct.set(null);
+ const v = this.form.addRow.itemId().value();
+ if (typeof v === 'object' && v !== null) {
+ this.formModel.update((f) => ({
+ ...f,
+ addRow: {
+ ...f.addRow,
+ salePrice: v.price ?? 0,
+ },
+ }));
}
});
effect(() => {
- const menuCategoriesVal = this.menuCategoriesResource.value();
- if (menuCategoriesVal) this.menuCategories.set(menuCategoriesVal);
- const saleCategoriesVal = this.saleCategoriesResource.value();
- if (saleCategoriesVal) this.saleCategories.set(saleCategoriesVal);
- const itemVal = this.itemResource.value();
- if (itemVal) {
- this.item = itemVal;
-
- this.itemProduct.set(null);
-
- this.items.set(this.item.items ?? []);
- this.recalcBundleSalePrice();
- }
+ const items = this.form.items().value();
+ this.formModel.update((f) => ({
+ ...f,
+ salePrice: Number(items.reduce((sum, x) => sum + x.quantity * x.salePrice, 0).toFixed(2)),
+ }));
+ });
+ afterNextRender(() => {
+ this.form().focusBoundControl();
});
}
@@ -182,40 +143,6 @@ export class BundleDetailComponent implements AfterViewInit {
return !product ? '' : typeof product === 'string' ? product : product.name;
}
- bundleSelected(event: MatAutocompleteSelectedEvent): void {
- const product = event.option.value as ProductQuery;
- this.itemProduct.set(product);
- this.formModel.update((f) => ({
- ...f,
- addRow: {
- ...f.addRow,
- salePrice: product.price ?? 0,
- },
- }));
- }
-
- private recalcBundleSalePrice(): void {
- const total = (this.item.items ?? []).reduce((sum, x) => {
- const qty = Number(x.quantity ?? 0);
- const price = Number(x.salePrice ?? 0);
- if (Number.isNaN(qty) || Number.isNaN(price)) return sum;
- return sum + qty * price;
- }, 0);
-
- // keep model + form in sync
- this.item.salePrice = Number(total.toFixed(2));
-
- this.formModel.update((f) => ({ ...f, salePrice: this.item.salePrice }));
- }
-
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
- }
-
resetAddRow() {
this.formModel.update((f) => ({
...f,
@@ -226,47 +153,43 @@ export class BundleDetailComponent implements AfterViewInit {
printInBill: true,
},
}));
- this.itemProduct.set(null);
}
addRow() {
- const v = this.formModel().addRow;
- if (!v) {
- return;
- }
+ const formValue = this.form().value().addRow;
+ if (!formValue) return;
+ const itemProduct = formValue.itemId as ProductQuery;
- if (!this.itemProduct()?.id) {
+ if (!itemProduct || typeof itemProduct === 'string') {
this.snackBar.open('Please select a product', 'Error');
return;
}
- const quantity = Number(v.quantity ?? 0);
+ const quantity = Number(formValue.quantity ?? 0);
if (Number.isNaN(quantity) || quantity <= 0) {
this.snackBar.open('Quantity has to be > 0', 'Error');
return;
}
- const salePrice = Number(v.salePrice ?? 0);
+ const salePrice = Number(formValue.salePrice ?? 0);
if (Number.isNaN(salePrice) || salePrice < 0) {
this.snackBar.open('Sale Price has to be >= 0', 'Error');
return;
}
- // name is expected from backend as "name (units)" on GET;
- // for new rows we keep it blank (or you can set placeholder)
const bi = new BundleItem({
- itemId: this.itemProduct()?.id,
- name: this.itemProduct()?.name ?? '',
+ itemId: itemProduct.id,
+ name: itemProduct.name ?? '',
quantity,
salePrice,
- printInBill: v.printInBill || false,
+ printInBill: formValue.printInBill || false,
});
- this.item.items.push(bi);
- this.items.set(this.item.items);
+ this.formModel.update((m) => ({
+ ...m,
+ items: [...m.items, bi],
+ }));
this.resetAddRow();
-
- this.recalcBundleSalePrice();
}
editRow(row: BundleItem) {
@@ -281,25 +204,28 @@ export class BundleDetailComponent implements AfterViewInit {
if (!result) {
return;
}
- Object.assign(row, result as BundleItem);
- this.items.set(this.item.items);
+ const updated = result as BundleItem;
+ this.formModel.update((m) => ({
+ ...m,
+ items: m.items.map((item) => (item === row ? Object.assign(new BundleItem(item), updated) : item)),
+ }));
this.resetAddRow();
- this.recalcBundleSalePrice();
});
}
deleteRow(row: BundleItem) {
- this.item.items.splice(this.item.items.indexOf(row), 1);
- this.items.set(this.item.items);
- this.recalcBundleSalePrice();
+ this.formModel.update((m) => ({
+ ...m,
+ items: m.items.filter((item) => item !== row),
+ }));
}
save() {
- if (!this.item.items || this.item.items.length === 0) {
- this.snackBar.open('Bundle must contain at least one item', 'Error');
+ const item = this.getItem();
+ if (!item) {
return;
}
- this.ser.saveOrUpdate(this.getItem()).subscribe({
+ this.ser.saveOrUpdate(item).subscribe({
next: () => {
this.snackBar.open('', 'Success');
this.router.navigateByUrl('/bundles');
@@ -311,7 +237,7 @@ export class BundleDetailComponent implements AfterViewInit {
}
delete() {
- this.ser.delete(this.item.id as string).subscribe({
+ this.ser.delete(this.item().id as string).subscribe({
next: () => {
this.snackBar.open('', 'Success');
this.router.navigateByUrl('/bundles');
@@ -335,47 +261,33 @@ export class BundleDetailComponent implements AfterViewInit {
});
}
- getItem(): Bundle {
+ getItem(): Bundle | null {
const v = this.formModel();
-
- this.item.name = v.name ?? '';
- this.item.units = v.units ?? '';
- this.item.salePrice = Number(v.salePrice ?? 0);
- this.item.hasHappyHour = v.hasHappyHour ?? false;
- this.item.isNotAvailable = v.isNotAvailable ?? false;
- this.item.sortOrder = this.item.sortOrder ?? 0;
-
// menu category
- const menuCategoryId = v.menuCategory ?? '';
- if (!menuCategoryId) {
- // keep it as-is; backend will 422 anyway, but we can show UI error too
+ if (!v.menuCategory) {
this.snackBar.open('Menu Category is required', 'Error');
- return this.item;
+ return null;
}
-
- if (this.item.menuCategory === null || this.item.menuCategory === undefined) {
- this.item.menuCategory = new MenuCategory();
- }
- this.item.menuCategory.id = menuCategoryId;
-
// sale category
- const saleCategoryId = v.saleCategory ?? '';
- if (!saleCategoryId) {
- // keep it as-is; backend will 422 anyway, but we can show UI error too
+ if (!v.saleCategory) {
this.snackBar.open('Menu Category is required', 'Error');
- return this.item;
+ return null;
}
-
- if (this.item.saleCategory === null || this.item.saleCategory === undefined) {
- this.item.saleCategory = new SaleCategory();
+ if (!v.items.length) {
+ this.snackBar.open('Bundle must contain at least one item', 'Error');
+ return null;
}
- this.item.saleCategory.id = saleCategoryId;
-
- // ensure items array exists
- if (!this.item.items) {
- this.item.items = [];
- }
-
- return this.item;
+ return new Bundle({
+ ...this.item(),
+ name: v.name ?? '',
+ units: v.units ?? '',
+ salePrice: Number(v.salePrice ?? 0),
+ hasHappyHour: v.hasHappyHour ?? false,
+ isNotAvailable: v.isNotAvailable ?? false,
+ sortOrder: this.item().sortOrder ?? 0,
+ menuCategory: v.menuCategory,
+ saleCategory: v.saleCategory,
+ items: v.items ?? [],
+ });
}
}
diff --git a/bookie/src/app/customers/customer-detail/customer-detail.component.html b/bookie/src/app/customers/customer-detail/customer-detail.component.html
index 32c34d8..6c48a03 100644
--- a/bookie/src/app/customers/customer-detail/customer-detail.component.html
+++ b/bookie/src/app/customers/customer-detail/customer-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/customers/customer-detail/customer-detail.component.ts b/bookie/src/app/customers/customer-detail/customer-detail.component.ts
index 6626737..1224106 100644
--- a/bookie/src/app/customers/customer-detail/customer-detail.component.ts
+++ b/bookie/src/app/customers/customer-detail/customer-detail.component.ts
@@ -1,5 +1,5 @@
import { PercentPipe } from '@angular/common';
-import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -39,8 +39,7 @@ export interface CustomerDetailFormData {
FormField,
],
})
-export class CustomerDetailComponent implements AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
+export class CustomerDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private snackBar = inject(MatSnackBar);
@@ -68,12 +67,10 @@ export class CustomerDetailComponent implements AfterViewInit {
hide = true;
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/customers/customer-list/customer-list.component.html b/bookie/src/app/customers/customer-list/customer-list.component.html
index b45111d..051fff4 100644
--- a/bookie/src/app/customers/customer-list/customer-list.component.html
+++ b/bookie/src/app/customers/customer-list/customer-list.component.html
@@ -9,11 +9,11 @@
Filter
-
+
-
+
Name
diff --git a/bookie/src/app/customers/customer-list/customer-list.component.ts b/bookie/src/app/customers/customer-list/customer-list.component.ts
index 130485e..d05841e 100644
--- a/bookie/src/app/customers/customer-list/customer-list.component.ts
+++ b/bookie/src/app/customers/customer-list/customer-list.component.ts
@@ -1,21 +1,12 @@
+/* eslint-disable @angular-eslint/no-input-rename */
import { PercentPipe } from '@angular/common';
-import {
- AfterViewInit,
- Component,
- ElementRef,
- ViewChild,
- inject,
- signal,
- effect,
- computed,
- debounced,
-} from '@angular/core';
+import { Component, inject, signal, effect, computed, debounced, afterNextRender, input } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
-import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
-import { MatSort, MatSortModule } from '@angular/material/sort';
+import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
+import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -42,13 +33,23 @@ export interface CustomerListFormData {
FormField,
],
})
-export class CustomerListComponent implements AfterViewInit {
- @ViewChild('filterElement', { static: true }) filterElement!: ElementRef;
- @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
- @ViewChild(MatSort, { static: true }) sort!: MatSort;
+export class CustomerListComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private customerService = inject(CustomerService);
+ q = input('', { transform: (v: string | null | undefined) => v ?? '' });
+ pageSize = input(50, {
+ transform: (v: string | number | null | undefined): number => (v ? Number(v) : 50),
+ alias: 's',
+ });
+
+ pageIndex = input(0, {
+ transform: (v: string | number | null | undefined): number => (v ? Number(v) : 0),
+ alias: 'i',
+ });
+
+ sortInput = input('name', { alias: 'f' });
+ direction = input('asc', { alias: 'd' });
formModel = signal({
filter: this.route.snapshot.queryParamMap.get('q') ?? '',
@@ -56,23 +57,10 @@ export class CustomerListComponent implements AfterViewInit {
form = form(this.formModel);
- filterSignal = computed(() => this.formModel().filter);
+ filterSignal = computed(() => this.formModel().filter ?? null);
debouncedFilter = debounced(this.filterSignal, 150);
- pageIndex = signal(+(this.route.snapshot.queryParamMap.get('p') ?? '0'));
- pageSize = signal(+(this.route.snapshot.queryParamMap.get('s') ?? '50'));
- sortField = signal(this.route.snapshot.queryParamMap.get('f') ?? 'name');
- sortDirection = signal(this.route.snapshot.queryParamMap.get('d') ?? 'asc');
-
- queryParams = computed(() => ({
- q: this.debouncedFilter.value() ?? '',
- p: this.pageIndex(),
- s: this.pageSize(),
- f: this.sortField(),
- d: this.sortDirection(),
- }));
-
- listResource = this.customerService.query(this.queryParams);
+ listResource = this.customerService.query(this.q, this.pageIndex, this.pageSize, this.sortInput, this.direction);
dataSource = computed(() => {
const data = this.listResource.value();
@@ -84,32 +72,37 @@ export class CustomerListComponent implements AfterViewInit {
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
constructor() {
- // Sync query string whenever params change
effect(() => {
- const p = this.queryParams();
+ const q = this.debouncedFilter.value();
this.router.navigate([], {
relativeTo: this.route,
- queryParams: { q: p.q, p: p.p, s: p.s, f: p.f, d: p.d },
+ queryParams: { q: q },
queryParamsHandling: 'merge',
replaceUrl: true,
});
});
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
- ngAfterViewInit() {
- this.sort.sortChange.subscribe(() => {
- this.sortField.set(this.sort.active);
- this.sortDirection.set(this.sort.direction);
- this.pageIndex.set(0);
- });
+ handlePageEvent(e: PageEvent) {
+ if (e.pageSize !== this.pageSize() || e.pageIndex !== this.pageIndex()) {
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: { s: e.pageSize, p: e.pageIndex },
+ queryParamsHandling: 'merge',
+ replaceUrl: true,
+ });
+ }
+ }
- this.paginator.page.subscribe(() => {
- this.pageSize.set(this.paginator.pageSize);
- this.pageIndex.set(this.paginator.pageIndex);
+ sortData(sort: Sort) {
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParams: { f: sort.active || null, d: sort.direction || null },
+ queryParamsHandling: 'merge',
+ replaceUrl: true,
});
-
- setTimeout(() => {
- this.filterElement.nativeElement.focus();
- }, 0);
}
}
diff --git a/bookie/src/app/customers/customer.service.ts b/bookie/src/app/customers/customer.service.ts
index 36e7138..eece7ed 100644
--- a/bookie/src/app/customers/customer.service.ts
+++ b/bookie/src/app/customers/customer.service.ts
@@ -41,18 +41,21 @@ export class CustomerService {
}
query(
- params: Signal<{ q: string; p: number; s: number; f: string; d: string }>,
+ q: Signal,
+ p: Signal,
+ s: Signal,
+ f: Signal,
+ d: Signal,
): HttpResourceRef | undefined> {
return httpResource>(() => {
- const p = params();
return {
url: `${url}/list`,
params: {
- ...(p.q ? { q: p.q } : {}),
- p: p.p,
- s: p.s,
- f: p.f,
- d: p.d,
+ q: q(),
+ p: p(),
+ s: s(),
+ f: f(),
+ d: d(),
},
};
});
diff --git a/bookie/src/app/devices/device-detail/device-detail.component.html b/bookie/src/app/devices/device-detail/device-detail.component.html
index 75512c7..a1de46d 100644
--- a/bookie/src/app/devices/device-detail/device-detail.component.html
+++ b/bookie/src/app/devices/device-detail/device-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/devices/device-detail/device-detail.component.ts b/bookie/src/app/devices/device-detail/device-detail.component.ts
index e2caf7b..562eec1 100644
--- a/bookie/src/app/devices/device-detail/device-detail.component.ts
+++ b/bookie/src/app/devices/device-detail/device-detail.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,8 +38,7 @@ export interface DeviceDetailFormData {
FormField,
],
})
-export class DeviceDetailComponent implements AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
+export class DeviceDetailComponent {
private sectionService = inject(SectionService);
private route = inject(ActivatedRoute);
private router = inject(Router);
@@ -68,12 +67,10 @@ export class DeviceDetailComponent implements AfterViewInit {
sections = computed(() => this.sectionsResource.value() ?? []);
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.html b/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.html
index 54798e6..b9ea107 100644
--- a/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.html
+++ b/bookie/src/app/guest-book/guest-book-detail/guest-book-detail.component.html
@@ -3,13 +3,13 @@
Name
-
+
Phone
-
+
{
- if (this.phoneElement !== undefined) {
- this.phoneElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/header-footer/header-footer.component.ts b/bookie/src/app/header-footer/header-footer.component.ts
index a096df4..1bf1b31 100644
--- a/bookie/src/app/header-footer/header-footer.component.ts
+++ b/bookie/src/app/header-footer/header-footer.component.ts
@@ -1,4 +1,4 @@
-import { Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
@@ -33,7 +33,6 @@ export interface HeaderFooterFormData {
],
})
export class HeaderFooterComponent {
- @ViewChild('section', { static: true }) sectionElement?: ElementRef;
private route = inject(ActivatedRoute);
private router = inject(Router);
private snackBar = inject(MatSnackBar);
diff --git a/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.html b/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.html
index 34a668f..bb73daf 100644
--- a/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.html
+++ b/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.ts b/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.ts
index 59d754a..7520b49 100644
--- a/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.ts
+++ b/bookie/src/app/menu-category/menu-category-detail/menu-category-detail.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -23,8 +23,7 @@ export interface MenuCategoryDetailFormData {
styleUrls: ['./menu-category-detail.component.css'],
imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField],
})
-export class MenuCategoryDetailComponent implements AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
+export class MenuCategoryDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private dialog = inject(MatDialog);
@@ -47,12 +46,10 @@ export class MenuCategoryDetailComponent implements AfterViewInit {
form = form(this.formModel);
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html
index 62e25fb..28e5931 100644
--- a/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html
+++ b/bookie/src/app/menu-engineering-report/menu-engineering-report.component.html
@@ -40,7 +40,7 @@
Filter
-
+
n: Name, sc: Sale Category, mc: Menu Category, q:(=/<=/>=/</>)Quantity,
s:(=/<=/>=/</>)Sales this.formModel().filter);
this.filter = toObservable(filterSignal).pipe(debounceTime(150), distinctUntilChanged());
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
show() {
diff --git a/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.html b/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.html
index dfd9a69..5922f28 100644
--- a/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.html
+++ b/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.ts b/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.ts
index 2ab79e9..8e02dd9 100644
--- a/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.ts
+++ b/bookie/src/app/modifier-categories/modifier-category-detail/modifier-category-detail.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, effect, inject, input, signal } from '@angular/core';
+import { Component, effect, inject, input, signal, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -48,8 +48,7 @@ export interface ModifierCategoryDetailFormData {
FormField,
],
})
-export class ModifierCategoryDetailComponent implements AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
+export class ModifierCategoryDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private snackBar = inject(MatSnackBar);
@@ -108,14 +107,9 @@ export class ModifierCategoryDetailComponent implements AfterViewInit {
});
}
});
- }
-
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.html b/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.html
index 5916bfc..0f837ad 100644
--- a/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.html
+++ b/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.ts b/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.ts
index 9487636..7441cec 100644
--- a/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.ts
+++ b/bookie/src/app/modifiers/modifier-detail/modifier-detail.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,8 +38,7 @@ export interface ModifierDetailFormData {
FormField,
],
})
-export class ModifierDetailComponent implements AfterViewInit {
- @ViewChild('name', { static: true }) nameElement?: ElementRef;
+export class ModifierDetailComponent {
private modifierCategoryService = inject(ModifierCategoryService);
private route = inject(ActivatedRoute);
private router = inject(Router);
@@ -71,12 +70,10 @@ export class ModifierDetailComponent implements AfterViewInit {
required(schema.modifierCategory, { message: 'Modifier Category is required' });
});
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/printers/printer-detail/printer-detail.component.html b/bookie/src/app/printers/printer-detail/printer-detail.component.html
index c77beb3..dcee6c1 100644
--- a/bookie/src/app/printers/printer-detail/printer-detail.component.html
+++ b/bookie/src/app/printers/printer-detail/printer-detail.component.html
@@ -3,7 +3,7 @@
Name
-
+
diff --git a/bookie/src/app/printers/printer-detail/printer-detail.component.ts b/bookie/src/app/printers/printer-detail/printer-detail.component.ts
index d236ce1..4d55d94 100644
--- a/bookie/src/app/printers/printer-detail/printer-detail.component.ts
+++ b/bookie/src/app/printers/printer-detail/printer-detail.component.ts
@@ -1,4 +1,4 @@
-import { AfterViewInit, Component, ElementRef, ViewChild, computed, inject, linkedSignal, input } from '@angular/core';
+import { Component, computed, inject, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
@@ -23,8 +23,7 @@ export interface PrinterDetailFormData {
styleUrls: ['./printer-detail.component.css'],
imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField],
})
-export class PrinterDetailComponent implements AfterViewInit {
- @ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
+export class PrinterDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router);
private dialog = inject(MatDialog);
@@ -48,12 +47,10 @@ export class PrinterDetailComponent implements AfterViewInit {
form = form(this.formModel);
- ngAfterViewInit() {
- setTimeout(() => {
- if (this.nameElement !== undefined) {
- this.nameElement.nativeElement.focus();
- }
- }, 0);
+ constructor() {
+ afterNextRender(() => {
+ this.form().focusBoundControl();
+ });
}
save() {
diff --git a/bookie/src/app/product-sale-report/product-sale-report.component.html b/bookie/src/app/product-sale-report/product-sale-report.component.html
index c6b1721..de47a00 100644
--- a/bookie/src/app/product-sale-report/product-sale-report.component.html
+++ b/bookie/src/app/product-sale-report/product-sale-report.component.html
@@ -9,7 +9,7 @@
-