This commit is contained in:
tanshu committed 2026-08-27 05:38:32 +00:00
1 parent 2a29dc9195
commit 43204e8441
78 files changed
+903 -1234

No files matched your search

+13 -13
View File
@@ -59,15 +59,15 @@
line: "{{ terminus_key }}" line: "{{ terminus_key }}"
when: terminuskey_test.stdout == "0" when: terminuskey_test.stdout == "0"
- name: Check rohan public key - name: Check clair public key
shell: "grep -c \"{{ rohan_key }}\" /home/{{ user }}/.ssh/authorized_keys || true" shell: "grep -c \"{{ clair_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
register: rohankey_test register: clairkey_test
- name: Add Rohan public key - name: Add clair public key
lineinfile: lineinfile:
dest: "/home/{{ user }}/.ssh/authorized_keys" dest: "/home/{{ user }}/.ssh/authorized_keys"
line: "{{ rohan_key }}" line: "{{ clair_key }}"
when: rohankey_test.stdout == "0" when: clairkey_test.stdout == "0"
- name: Check Anjin public key - name: Check Anjin public key
shell: "grep -c \"{{ anjin_key }}\" /home/{{ user }}/.ssh/authorized_keys || true" shell: "grep -c \"{{ anjin_key }}\" /home/{{ user }}/.ssh/authorized_keys || true"
@@ -146,21 +146,21 @@
groups: lp groups: lp
append: yes append: yes
- name: install rohan-redis-tunnel systemd unit file - name: install nats-tunnel systemd unit file
template: template:
src: "files/rohan-redis-tunnel.service" src: "files/nats-tunnel.service"
dest: "/etc/systemd/system/rohan-redis-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: systemd:
name: rohan-redis-tunnel name: nats-tunnel
enabled: yes enabled: yes
masked: no masked: no
- name: Make sure rohan-redis-tunnel service is running - name: Make sure nats-tunnel service is running
systemd: systemd:
state: started state: started
name: rohan-redis-tunnel name: nats-tunnel
- name: install leardal systemd unit file - name: install leardal systemd unit file
template: template:
+2 -2
View File
@@ -1,4 +1,4 @@
Host rohan rohan.tanshu.com Host {{ server }} {{ server }}.tanshu.com
HostName rohan.tanshu.com HostName {{ server }}.tanshu.com
IdentityFile ~/.ssh/id_ed25519 IdentityFile ~/.ssh/id_ed25519
User tanshu User tanshu
+4 -4
View File
@@ -1,9 +1,9 @@
[Unit] [Unit]
Description=Frank container service Description=Frank container service
After=docker.service rohan-redis-tunnel.service After=docker.service {{ server }}-redis-tunnel.service
Wants=network-online.target docker.socket rohan-redis-tunnel.service Wants=network-online.target docker.socket {{ server }}-redis-tunnel.service
Requires=docker.socket rohan-redis-tunnel.service Requires=docker.socket {{ server }}-redis-tunnel.service
PartOf=rohan-redis-tunnel.service PartOf={{ server }}-redis-tunnel.service
[Service] [Service]
Restart=always Restart=always
+1 -1
View File
@@ -6,7 +6,7 @@ After=network.target
Type=simple Type=simple
User={{ user }} 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 every >2 seconds to avoid StartLimitInterval failure
Restart=always Restart=always
@@ -5,7 +5,7 @@ After=network.target
[Service] [Service]
User={{ user }} 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 # Restart every >2 seconds to avoid StartLimitInterval failure
RestartSec=5 RestartSec=5
-39
View File
@@ -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;
}
}
+2 -1
View File
@@ -3,5 +3,6 @@ hostname: "moz2"
ssh_port: "22542" ssh_port: "22542"
user: "vari" user: "vari"
terminus_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK8WQHla0uCcNwmN0DUE49lbjNWa6+7A6OxrX3WEPQH0 tanshu@terminus 2023.01.08" 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" anjin_key: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMlvalE0JqKyjpEsGTgtf/N1d9QK2MgNFJib+e8O2h9M tanshu@anjin 2024.09.28"
server: "clair"
+1 -1
View File
@@ -1 +1 @@
__version__ = "14.5.0" __version__ = "14.5.1"
-6
View File
@@ -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)
+1 -5
View File
@@ -18,14 +18,10 @@ class Settings(BaseSettings):
LOG_LEVEL: str = "NOTSET" LOG_LEVEL: str = "NOTSET"
SQLALCHEMY_DATABASE_URI: str = "" SQLALCHEMY_DATABASE_URI: str = ""
REDIS_HOST: str = "127.0.0.1"
REDIS_PORT: int = 6379
ALEMBIC_LOG_LEVEL: str = "INFO" ALEMBIC_LOG_LEVEL: str = "INFO"
ALEMBIC_SQLALCHEMY_LOG_LEVEL: str = "WARN" ALEMBIC_SQLALCHEMY_LOG_LEVEL: str = "WARN"
WORKER_REDIS_HOST: str = "127.0.0.1" NATS_URL: str = "nats://127.0.0.1:4222"
WORKER_REDIS_PORT: int = 6379
WORKER_QUEUE_NAME: str = "arq:queue" WORKER_QUEUE_NAME: str = "arq:queue"
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env") model_config = SettingsConfigDict(case_sensitive=True, env_file=".env")
+28
View File
@@ -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()
+1 -2
View File
@@ -4,7 +4,6 @@ import uvicorn
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.middleware.sessions import SessionMiddleware 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(change.router, prefix="/api/voucher", tags=["voucher"])
app.include_router(health.router, prefix="/health", tags=["health"]) 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: def init() -> None:
+3 -16
View File
@@ -1,16 +1,14 @@
import asyncio
import re import re
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from decimal import Decimal from decimal import Decimal
from arq import ArqRedis, create_pool
from sqlalchemy import Date, func, select from sqlalchemy import Date, func, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..core.config import settings from ..core.config import settings
from ..core.taskiq import enqueue_print_job_sync
from ..models.bill import Bill from ..models.bill import Bill
from ..models.db_setting import DbSetting from ..models.db_setting import DbSetting
from ..models.inventory import Inventory 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) bill_text = design_bill(voucher, regime_id, inventories, taxes_list, db)
bills.append(bill_text) bills.append(bill_text)
loop = asyncio.new_event_loop()
redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings))
if len(bills) > 1: if len(bills) > 1:
total = design_total(voucher, regimes, db) total = design_total(voucher, regimes, db)
loop.run_until_complete( enqueue_print_job_sync(printer.name, total, printer.address, printer.cut_code)
redis.enqueue_job(
"sent_to_printer", total, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
)
)
for bill in bills: for bill in bills:
loop.run_until_complete( enqueue_print_job_sync(printer.name, bill, printer.address, printer.cut_code)
redis.enqueue_job(
"sent_to_printer", bill, printer.address, printer.cut_code, _queue_name=f"barker:print:{printer.name}"
)
)
loop.close()
def get_inventories(voucher: Voucher) -> list[Inventory]: def get_inventories(voucher: Voucher) -> list[Inventory]:
+3 -16
View File
@@ -1,14 +1,12 @@
import asyncio
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from arq import ArqRedis, create_pool
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..core.config import settings from ..core.config import settings
from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device from ..models.device import Device
from ..models.printer import Printer from ..models.printer import Printer
from ..models.section_printer import SectionPrinter 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 .where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one() ).scalar_one()
loop = asyncio.new_event_loop() enqueue_print_job_sync(printer.name, summary, printer.address, printer.cut_code)
redis: ArqRedis = loop.run_until_complete(create_pool(redis_settings)) enqueue_print_job_sync(printer.name, details, printer.address, printer.cut_code)
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()
def design_cashier_report_summary(report: CashierReport) -> str: def design_cashier_report_summary(report: CashierReport) -> str:
+2 -11
View File
@@ -1,11 +1,9 @@
import asyncio
import uuid import uuid
from arq import ArqRedis, create_pool
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session 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.device import Device
from ..models.printer import Printer from ..models.printer import Printer
from ..models.section_printer import SectionPrinter 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 .where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one() ).scalar_one()
loop = asyncio.new_event_loop() enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
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()
def design_discount_report(report: DiscountReport) -> str: def design_discount_report(report: DiscountReport) -> str:
+2 -15
View File
@@ -1,15 +1,13 @@
import asyncio
import uuid import uuid
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from arq import ArqRedis, create_pool
from sqlalchemy import Date, func, or_, select from sqlalchemy import Date, func, or_, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..core.config import settings from ..core.config import settings
from ..core.taskiq import enqueue_print_job_sync
from ..models.inventory import Inventory from ..models.inventory import Inventory
from ..models.kot import Kot from ..models.kot import Kot
from ..models.printer import Printer 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: if key not in my_hash:
my_hash[key] = (printer, []) my_hash[key] = (printer, [])
my_hash[key][1].append(item) 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(): for key, value in my_hash.items():
printer_id, copies = key printer_id, copies = key
printer, items = value printer, items = value
for c in range(int(copies)): for c in range(int(copies)):
data = design_kot(voucher, kot, items, c) data = design_kot(voucher, kot, items, c)
loop.run_until_complete( enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
redis.enqueue_job(
"sent_to_printer",
data,
printer.address,
printer.cut_code,
_queue_name=f"barker:print:{printer.name}",
)
)
loop.close()
+2 -11
View File
@@ -1,14 +1,12 @@
import asyncio
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from arq import ArqRedis, create_pool
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..core.config import settings from ..core.config import settings
from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device from ..models.device import Device
from ..models.printer import Printer from ..models.printer import Printer
from ..models.section_printer import SectionPrinter 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 .where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one() ).scalar_one()
loop = asyncio.new_event_loop() enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
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()
def design_product_sale_report(report: ProductSaleReport) -> str: def design_product_sale_report(report: ProductSaleReport) -> str:
+2 -11
View File
@@ -1,14 +1,12 @@
import asyncio
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from arq import ArqRedis, create_pool
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..core.arq import settings as redis_settings
from ..core.config import settings from ..core.config import settings
from ..core.taskiq import enqueue_print_job_sync
from ..models.device import Device from ..models.device import Device
from ..models.printer import Printer from ..models.printer import Printer
from ..models.section_printer import SectionPrinter 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 .where(SectionPrinter.sale_category_id == None) # noqa: E711
).scalar_one() ).scalar_one()
loop = asyncio.new_event_loop() enqueue_print_job_sync(printer.name, data, printer.address, printer.cut_code)
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()
def design_sale_report(report: SaleReport) -> str: def design_sale_report(report: SaleReport) -> str:
+4 -4
View File
@@ -122,8 +122,8 @@ def show_term(
q: str | None, q: str | None,
p: int = 0, # Page number p: int = 0, # Page number
s: int = 25, # Page size s: int = 25, # Page size
f: str | None = None, # Sort field f: str = "name", # Sort field
d: str | None = None, # Sort direction d: str = "asc", # Sort direction
) -> list[schemas.Customer]: ) -> list[schemas.Customer]:
# Handle dynamic sorting safely # Handle dynamic sorting safely
sort_field = ALLOWED_SORT_FIELDS.get(f or "name", Customer.name) # Default to name sort_field = ALLOWED_SORT_FIELDS.get(f or "name", Customer.name) # Default to name
@@ -139,8 +139,8 @@ def customer_list(
filter: list[str], filter: list[str],
page: int | None, page: int | None,
page_size: int | None, page_size: int | None,
sort_field: InstrumentedAttribute[Any] | None, sort_field: InstrumentedAttribute[Any],
sort_direction: Any | None, sort_direction: Any,
db: Session, db: Session,
) -> tuple[Sequence[Customer], int]: ) -> tuple[Sequence[Customer], int]:
# Handle dynamic sorting safely # Handle dynamic sorting safely
+4 -5
View File
@@ -1,13 +1,12 @@
[project] [project]
name = "barker" name = "barker"
version = "14.5.0" version = "14.5.1"
description = "Point of Sale for a restaurant" description = "Point of Sale for a restaurant"
requires-python = ">=3.14" requires-python = ">=3.14"
authors = [{ name = "tanshu", email = "git@tanshu.com" }] authors = [{ name = "tanshu", email = "git@tanshu.com" }]
dependencies = [ dependencies = [
"aiohttp>=3.14.3", "aiohttp>=3.14.3",
"alembic>=1.19.1", "alembic>=1.19.1",
"arq>=0.28.0",
"cryptography>=50.0.0", "cryptography>=50.0.0",
"fastapi[all]>=0.141.1", "fastapi[all]>=0.141.1",
"gunicorn>=26.0.0", "gunicorn>=26.0.0",
@@ -20,6 +19,8 @@ dependencies = [
"python-dotenv>=1.2.2", "python-dotenv>=1.2.2",
"python-multipart>=0.0.32", "python-multipart>=0.0.32",
"sqlalchemy>=2.0.51", "sqlalchemy>=2.0.51",
"taskiq>=0.12.4",
"taskiq-nats>=0.6.0",
"uvicorn[standard]>=0.52.1", "uvicorn[standard]>=0.52.1",
] ]
@@ -105,9 +106,7 @@ ignore = [
"W191", # indentation contains tabs "W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException "B904", # Allow raising exceptions without from e, for HTTPException
] ]
fixable = [ fixable = ["ALL"] # Allow fix for all enabled rules (when `--fix`) is provided.
"ALL"
] # Allow fix for all enabled rules (when `--fix`) is provided.
dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
+62 -67
View File
@@ -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" }, { 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]] [[package]]
name = "ast-serialize" name = "ast-serialize"
version = "0.8.0" version = "0.8.0"
@@ -244,12 +231,11 @@ wheels = [
[[package]] [[package]]
name = "barker" name = "barker"
version = "14.5.0" version = "14.5.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
{ name = "alembic" }, { name = "alembic" },
{ name = "arq" },
{ name = "cryptography" }, { name = "cryptography" },
{ name = "fastapi", extra = ["all"] }, { name = "fastapi", extra = ["all"] },
{ name = "gunicorn" }, { name = "gunicorn" },
@@ -262,6 +248,8 @@ dependencies = [
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "taskiq" },
{ name = "taskiq-nats" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
@@ -278,7 +266,6 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "aiohttp", specifier = ">=3.14.3" }, { name = "aiohttp", specifier = ">=3.14.3" },
{ name = "alembic", specifier = ">=1.19.1" }, { name = "alembic", specifier = ">=1.19.1" },
{ name = "arq", specifier = ">=0.28.0" },
{ name = "cryptography", specifier = ">=50.0.0" }, { name = "cryptography", specifier = ">=50.0.0" },
{ name = "fastapi", extras = ["all"], specifier = ">=0.141.1" }, { name = "fastapi", extras = ["all"], specifier = ">=0.141.1" },
{ name = "gunicorn", specifier = ">=26.0.0" }, { name = "gunicorn", specifier = ">=26.0.0" },
@@ -291,6 +278,8 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-dotenv", specifier = ">=1.2.2" },
{ name = "python-multipart", specifier = ">=0.0.32" }, { name = "python-multipart", specifier = ">=0.0.32" },
{ name = "sqlalchemy", specifier = ">=2.0.51" }, { 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" }, { 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" }, { 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]] [[package]]
name = "httpcore" name = "httpcore"
version = "1.0.9" 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" }, { 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]] [[package]]
name = "nltk" name = "nltk"
version = "3.9.2" 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" }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.4" 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" }, { 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]] [[package]]
name = "regex" name = "regex"
version = "2026.2.19" 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" }, { 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]] [[package]]
name = "tenacity" name = "tenacity"
version = "9.1.4" version = "9.1.4"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "bookie", "name": "bookie",
"version": "14.5.0", "version": "14.5.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
+1 -1
View File
@@ -1,7 +1,7 @@
export const environment = { export const environment = {
production: true, production: true,
ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry ACCESS_TOKEN_REFRESH_MINUTES: 10, // refresh token 10 minutes before expiry
version: '14.5.0', version: '14.5.1',
}; };
export const dateFormat = { export const dateFormat = {
@@ -3,14 +3,14 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Username</mat-label> <mat-label>Username</mat-label>
<input matInput #nameElement [formField]="form.username" /> <input matInput [formField]="form.username" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Password</mat-label> <mat-label>Password</mat-label>
<input matInput [formField]="form.password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" /> <input matInput [formField]="form.password" [type]="hide() ? 'password' : 'text'" (keyup.enter)="login()" />
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon> <mat-icon matSuffix (click)="hide.set(!hide())">{{ hide() ? 'visibility' : 'visibility_off' }}</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
@if (unregisteredDevice()) { @if (unregisteredDevice()) {
+14 -37
View File
@@ -1,13 +1,4 @@
import { import { Component, afterNextRender, inject, input, signal } from '@angular/core';
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
OnInit,
ViewChild,
inject,
signal,
} from '@angular/core';
import { form, FormField, required } from '@angular/forms/signals'; import { form, FormField, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
@@ -15,7 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar'; 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 { CookieService } from '../../shared/cookie.service';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
@@ -31,14 +22,15 @@ export interface LoginFormData {
styleUrls: ['./login.component.css'], styleUrls: ['./login.component.css'],
imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, FormField], imports: [MatButtonModule, MatDividerModule, MatFormFieldModule, MatIconModule, MatInputModule, FormField],
}) })
export class LoginComponent implements OnInit, AfterViewInit { export class LoginComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute);
private auth = inject(AuthService); private auth = inject(AuthService);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private cs = inject(CookieService); private cs = inject(CookieService);
private cd = inject(ChangeDetectorRef);
returnUrl = input<string | null | undefined, string>('/', {
transform: (v: string | null | undefined): string => v ?? '/',
});
formModel = signal<LoginFormData>({ formModel = signal<LoginFormData>({
username: '', username: '',
@@ -50,28 +42,14 @@ export class LoginComponent implements OnInit, AfterViewInit {
required(schemaPath.password); required(schemaPath.password);
}); });
hide: boolean; hide = signal<boolean>(true);
unregisteredDevice = signal<boolean | undefined>(undefined); unregisteredDevice = signal<boolean>(false);
deviceName = signal<string | undefined>(undefined); deviceName = signal<string>('');
private returnUrl: string;
constructor() { constructor() {
this.hide = true; afterNextRender(() => {
this.unregisteredDevice.set(false); this.form().focusBoundControl();
this.deviceName.set(''); });
this.returnUrl = '';
}
ngOnInit() {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] ?? '/';
}
ngAfterViewInit() {
setTimeout(() => {
if (this.nameElement !== undefined) {
this.nameElement.nativeElement.focus();
}
}, 0);
} }
login(): void { login(): void {
@@ -80,13 +58,12 @@ export class LoginComponent implements OnInit, AfterViewInit {
const password = formModel.password ?? ''; const password = formModel.password ?? '';
this.auth.login(username.trim(), password.trim()).subscribe({ this.auth.login(username.trim(), password.trim()).subscribe({
next: () => { next: () => {
this.router.navigateByUrl(this.returnUrl); this.router.navigateByUrl(this.returnUrl() ?? '/');
}, },
error: (error) => { error: (error) => {
if (error.status === 401 && error.error.detail === 'Device is not registered') { if (error.status === 401 && error.error.detail === 'Device is not registered') {
this.unregisteredDevice.set(true); this.unregisteredDevice.set(true);
this.deviceName.set(this.cs.getCookie('device')); this.deviceName.set(this.cs.getCookie('device'));
this.cd.detectChanges();
} }
this.snackBar.open(error.error.detail, 'Error'); this.snackBar.open(error.error.detail, 'Error');
}, },
@@ -1,4 +1,4 @@
import { Component, OnInit, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
@@ -7,10 +7,10 @@ import { AuthService } from '../auth.service';
template: '', template: '',
standalone: true, standalone: true,
}) })
export class LogoutComponent implements OnInit { export class LogoutComponent {
private auth = inject(AuthService); private auth = inject(AuthService);
ngOnInit() { constructor() {
this.auth.logout(); this.auth.logout();
} }
} }
@@ -5,7 +5,7 @@
<mat-form-field class="w-full"> <mat-form-field class="w-full">
<mat-label>Item</mat-label> <mat-label>Item</mat-label>
<input matInput type="text" [formField]="form.product" [matAutocomplete]="autoP" /> <input matInput type="text" [formField]="form.product" [matAutocomplete]="autoP" />
<mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn" (optionSelected)="productSelected($event)"> <mat-autocomplete #autoP="matAutocomplete" [displayWith]="displayFn">
@for (p of products(); track p) { @for (p of products(); track p) {
<mat-option [value]="p">{{ p.name }}</mat-option> <mat-option [value]="p">{{ p.name }}</mat-option>
} }
@@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; 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 { 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 { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
@@ -13,7 +13,7 @@ import { ProductService } from '../../product/product.service';
import { BundleItem } from '../bundle'; import { BundleItem } from '../bundle';
export interface BundleDetailDialogFormData { export interface BundleDetailDialogFormData {
product: ProductQuery | string; product: BundleItem | ProductQuery | string;
quantity: number; quantity: number;
salePrice: number; salePrice: number;
printInBill: boolean; printInBill: boolean;
@@ -35,19 +35,20 @@ export interface BundleDetailDialogFormData {
FormField, FormField,
], ],
}) })
export class BundleDetailDialogComponent implements OnInit { export class BundleDetailDialogComponent {
private readonly dialogRef = inject<MatDialogRef<BundleDetailDialogComponent>>(MatDialogRef); private readonly dialogRef = inject<MatDialogRef<BundleDetailDialogComponent>>(MatDialogRef);
private readonly productSer = inject(ProductService); private readonly productSer = inject(ProductService);
data = inject<{ item: BundleItem }>(MAT_DIALOG_DATA); data = inject<{ item: BundleItem }>(MAT_DIALOG_DATA);
selectedProduct = signal<ProductQuery | null>(null); formModel = linkedSignal<BundleItem, BundleDetailDialogFormData>({
source: () => this.data.item,
formModel = signal<BundleDetailDialogFormData>({ computation: (item) => ({
product: '', product: item,
quantity: 1, quantity: Number(item.quantity ?? 1),
salePrice: 0, salePrice: Number(item.salePrice ?? 0),
printInBill: true, printInBill: item.printInBill ?? false,
}),
}); });
form = form(this.formModel); form = form(this.formModel);
@@ -59,62 +60,35 @@ export class BundleDetailDialogComponent implements OnInit {
debouncedProduct = debounced(this.productSignal, 150); debouncedProduct = debounced(this.productSignal, 150);
productsResource = this.productSer.query(computed(() => this.debouncedProduct.value())); productsResource = this.productSer.query(this.debouncedProduct.value);
products = computed(() => { products = computed(() => {
if (!this.debouncedProduct.value()) return []; if (!this.debouncedProduct.value()) return [];
return this.productsResource.value() ?? []; 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 { displayFn(product?: ProductQuery | string): string {
return !product ? '' : typeof product === 'string' ? product : product.name; 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 { 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; 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 (Number.isNaN(salePrice) || salePrice < 0) return;
// If a new product was selected, update itemId + name const product = typeof formValue.product === 'string' ? null : formValue.product;
if (this.selectedProduct()?.id) { if (!product || !product.id) {
this.data.item.itemId = this.selectedProduct()?.id ?? ''; return;
this.data.item.name = this.selectedProduct()?.name ?? '';
} else {
// If no selection, don't allow changing SKU silently
// but still allow quantity/price edit
} }
this.data.item.itemId = product.id ?? '';
this.data.item.name = product.name ?? '';
this.data.item.quantity = quantity; this.data.item.quantity = quantity;
this.data.item.salePrice = salePrice; this.data.item.salePrice = salePrice;
this.data.item.printInBill = v.printInBill || false; this.data.item.printInBill = formValue.printInBill ?? false;
this.dialogRef.close(this.data.item); this.dialogRef.close(this.data.item);
} }
@@ -4,7 +4,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="basis-3-4"> <mat-form-field class="basis-3-4">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #name [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
<mat-form-field class="basis-1-4"> <mat-form-field class="basis-1-4">
@@ -65,7 +65,7 @@
[matAutocomplete]="autoItem" [matAutocomplete]="autoItem"
[value]="displayFn(formModel().addRow.itemId)" [value]="displayFn(formModel().addRow.itemId)"
/> />
<mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn" (optionSelected)="bundleSelected($event)"> <mat-autocomplete #autoItem="matAutocomplete" [displayWith]="displayFn">
@for (p of itemProducts(); track p) { @for (p of itemProducts(); track p) {
<mat-option [value]="p">{{ p.name }}</mat-option> <mat-option [value]="p">{{ p.name }}</mat-option>
} }
@@ -88,7 +88,7 @@
</form> </form>
<div class="row-container wrapped"> <div class="row-container wrapped">
<mat-table [dataSource]="dataSource()" aria-label="Bundle Items" class="flex-auto"> <mat-table [dataSource]="formModel().items" aria-label="Bundle Items" class="flex-auto">
<!-- Name Column (name is "name (units)") --> <!-- Name Column (name is "name (units)") -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>Item</mat-header-cell> <mat-header-cell *matHeaderCellDef>Item</mat-header-cell>
@@ -143,7 +143,7 @@
<div class="row-container"> <div class="row-container">
<button type="button" mat-raised-button color="primary" (click)="save()">Save</button> <button type="button" mat-raised-button color="primary" (click)="save()">Save</button>
@if (!!item.id) { @if (!!item().id) {
<button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button> <button type="button" mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
} }
</div> </div>
@@ -1,19 +1,7 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { import { Component, inject, computed, effect, debounced, linkedSignal, input, afterNextRender } from '@angular/core';
AfterViewInit,
Component,
ElementRef,
ViewChild,
inject,
computed,
signal,
effect,
debounced,
linkedSignal,
input,
} from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; 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 { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -24,7 +12,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import { MenuCategory } from '../../core/menu-category'; import { MenuCategory } from '../../core/menu-category';
import { ProductQuery } from '../../core/product-query'; import { ProductQuery } from '../../core/product-query';
@@ -41,8 +29,8 @@ export interface BundleDetailFormData {
name: string; name: string;
units: string; units: string;
salePrice: number; salePrice: number;
menuCategory: string; menuCategory: MenuCategory | null;
saleCategory: string; saleCategory: SaleCategory | null;
hasHappyHour: boolean; hasHappyHour: boolean;
isNotAvailable: boolean; isNotAvailable: boolean;
addRow: { addRow: {
@@ -51,6 +39,7 @@ export interface BundleDetailFormData {
salePrice: number; salePrice: number;
printInBill: boolean; printInBill: boolean;
}; };
items: BundleItem[];
} }
@Component({ @Component({
@@ -71,53 +60,34 @@ export interface BundleDetailFormData {
FormField, FormField,
], ],
}) })
export class BundleDetailComponent implements AfterViewInit { export class BundleDetailComponent {
@ViewChild('name', { static: true }) nameElement?: ElementRef;
private menuCategoryService = inject(MenuCategoryService); private menuCategoryService = inject(MenuCategoryService);
private saleCategoryService = inject(SaleCategoryService); private saleCategoryService = inject(SaleCategoryService);
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private ser = inject(BundleService); private ser = inject(BundleService);
private productSer = inject(ProductService); private productSer = inject(ProductService);
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
saleCategoriesResource = this.saleCategoryService.list();
menuCategoriesResource = this.menuCategoryService.list(undefined); 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); itemResource = this.ser.get(this.id);
item = computed(() => this.itemResource.value() ?? new Bundle());
formModel = linkedSignal({ formModel = linkedSignal({
source: () => this.menuCategoriesResource.value(), source: this.item,
computation: (menuCategoriesVal): BundleDetailFormData => { computation: (itemVal): BundleDetailFormData => {
if (menuCategoriesVal) { const items = [...(itemVal.items ?? [])];
return { return {
name: this.item.name ?? '', name: itemVal.name ?? '',
units: this.item.units ?? '', units: itemVal.units ?? '',
salePrice: Number(this.item.salePrice ?? 0), salePrice: Number(itemVal.salePrice ?? 0),
menuCategory: this.item.menuCategory?.id ?? '', menuCategory: itemVal.menuCategory ?? null,
saleCategory: this.item.saleCategory?.id ?? '', saleCategory: itemVal.saleCategory ?? null,
hasHappyHour: this.item.hasHappyHour ?? false, hasHappyHour: itemVal.hasHappyHour ?? false,
isNotAvailable: this.item.isNotAvailable ?? false, isNotAvailable: itemVal.isNotAvailable ?? false,
items,
addRow: {
itemId: '',
quantity: 1,
salePrice: 0,
printInBill: true,
},
};
}
return {
name: '',
units: '',
salePrice: 0,
menuCategory: '',
saleCategory: '',
hasHappyHour: false,
isNotAvailable: false,
addRow: { addRow: {
itemId: '', itemId: '',
quantity: 1, quantity: 1,
@@ -130,25 +100,15 @@ export class BundleDetailComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
menuCategories = signal<MenuCategory[]>([]); private itemProductSearch = computed(() => {
saleCategories = signal<SaleCategory[]>([]); const v = this.form.addRow.itemId().value();
items = signal<BundleItem[]>([]); return typeof v === 'string' ? v : null;
dataSource = computed(() => this.items());
item: Bundle = new Bundle();
itemProduct = signal<ProductQuery | null>(null);
itemIdSignal = computed(() => {
const x = this.formModel().addRow?.itemId;
return typeof x === 'string' ? x.trim() : '';
}); });
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(() => { itemProducts = computed(() => {
if (!this.debouncedItemId.value()) return [];
return this.itemProductsResource.value() ?? []; return this.itemProductsResource.value() ?? [];
}); });
@@ -156,25 +116,26 @@ export class BundleDetailComponent implements AfterViewInit {
constructor() { constructor() {
effect(() => { effect(() => {
const x = this.formModel().addRow?.itemId; const v = this.form.addRow.itemId().value();
if (typeof x === 'string') { if (typeof v === 'object' && v !== null) {
this.itemProduct.set(null); this.formModel.update((f) => ({
...f,
addRow: {
...f.addRow,
salePrice: v.price ?? 0,
},
}));
} }
}); });
effect(() => { effect(() => {
const menuCategoriesVal = this.menuCategoriesResource.value(); const items = this.form.items().value();
if (menuCategoriesVal) this.menuCategories.set(menuCategoriesVal); this.formModel.update((f) => ({
const saleCategoriesVal = this.saleCategoriesResource.value(); ...f,
if (saleCategoriesVal) this.saleCategories.set(saleCategoriesVal); salePrice: Number(items.reduce((sum, x) => sum + x.quantity * x.salePrice, 0).toFixed(2)),
const itemVal = this.itemResource.value(); }));
if (itemVal) { });
this.item = itemVal; afterNextRender(() => {
this.form().focusBoundControl();
this.itemProduct.set(null);
this.items.set(this.item.items ?? []);
this.recalcBundleSalePrice();
}
}); });
} }
@@ -182,40 +143,6 @@ export class BundleDetailComponent implements AfterViewInit {
return !product ? '' : typeof product === 'string' ? product : product.name; 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() { resetAddRow() {
this.formModel.update((f) => ({ this.formModel.update((f) => ({
...f, ...f,
@@ -226,47 +153,43 @@ export class BundleDetailComponent implements AfterViewInit {
printInBill: true, printInBill: true,
}, },
})); }));
this.itemProduct.set(null);
} }
addRow() { addRow() {
const v = this.formModel().addRow; const formValue = this.form().value().addRow;
if (!v) { if (!formValue) return;
return; const itemProduct = formValue.itemId as ProductQuery;
}
if (!this.itemProduct()?.id) { if (!itemProduct || typeof itemProduct === 'string') {
this.snackBar.open('Please select a product', 'Error'); this.snackBar.open('Please select a product', 'Error');
return; return;
} }
const quantity = Number(v.quantity ?? 0); const quantity = Number(formValue.quantity ?? 0);
if (Number.isNaN(quantity) || quantity <= 0) { if (Number.isNaN(quantity) || quantity <= 0) {
this.snackBar.open('Quantity has to be > 0', 'Error'); this.snackBar.open('Quantity has to be > 0', 'Error');
return; return;
} }
const salePrice = Number(v.salePrice ?? 0); const salePrice = Number(formValue.salePrice ?? 0);
if (Number.isNaN(salePrice) || salePrice < 0) { if (Number.isNaN(salePrice) || salePrice < 0) {
this.snackBar.open('Sale Price has to be >= 0', 'Error'); this.snackBar.open('Sale Price has to be >= 0', 'Error');
return; 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({ const bi = new BundleItem({
itemId: this.itemProduct()?.id, itemId: itemProduct.id,
name: this.itemProduct()?.name ?? '', name: itemProduct.name ?? '',
quantity, quantity,
salePrice, salePrice,
printInBill: v.printInBill || false, printInBill: formValue.printInBill || false,
}); });
this.item.items.push(bi); this.formModel.update((m) => ({
this.items.set(this.item.items); ...m,
items: [...m.items, bi],
}));
this.resetAddRow(); this.resetAddRow();
this.recalcBundleSalePrice();
} }
editRow(row: BundleItem) { editRow(row: BundleItem) {
@@ -281,25 +204,28 @@ export class BundleDetailComponent implements AfterViewInit {
if (!result) { if (!result) {
return; return;
} }
Object.assign(row, result as BundleItem); const updated = result as BundleItem;
this.items.set(this.item.items); this.formModel.update((m) => ({
...m,
items: m.items.map((item) => (item === row ? Object.assign(new BundleItem(item), updated) : item)),
}));
this.resetAddRow(); this.resetAddRow();
this.recalcBundleSalePrice();
}); });
} }
deleteRow(row: BundleItem) { deleteRow(row: BundleItem) {
this.item.items.splice(this.item.items.indexOf(row), 1); this.formModel.update((m) => ({
this.items.set(this.item.items); ...m,
this.recalcBundleSalePrice(); items: m.items.filter((item) => item !== row),
}));
} }
save() { save() {
if (!this.item.items || this.item.items.length === 0) { const item = this.getItem();
this.snackBar.open('Bundle must contain at least one item', 'Error'); if (!item) {
return; return;
} }
this.ser.saveOrUpdate(this.getItem()).subscribe({ this.ser.saveOrUpdate(item).subscribe({
next: () => { next: () => {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
this.router.navigateByUrl('/bundles'); this.router.navigateByUrl('/bundles');
@@ -311,7 +237,7 @@ export class BundleDetailComponent implements AfterViewInit {
} }
delete() { delete() {
this.ser.delete(this.item.id as string).subscribe({ this.ser.delete(this.item().id as string).subscribe({
next: () => { next: () => {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
this.router.navigateByUrl('/bundles'); this.router.navigateByUrl('/bundles');
@@ -335,47 +261,33 @@ export class BundleDetailComponent implements AfterViewInit {
}); });
} }
getItem(): Bundle { getItem(): Bundle | null {
const v = this.formModel(); 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 // menu category
const menuCategoryId = v.menuCategory ?? ''; if (!v.menuCategory) {
if (!menuCategoryId) {
// keep it as-is; backend will 422 anyway, but we can show UI error too
this.snackBar.open('Menu Category is required', 'Error'); 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 // sale category
const saleCategoryId = v.saleCategory ?? ''; if (!v.saleCategory) {
if (!saleCategoryId) {
// keep it as-is; backend will 422 anyway, but we can show UI error too
this.snackBar.open('Menu Category is required', 'Error'); this.snackBar.open('Menu Category is required', 'Error');
return this.item; return null;
} }
if (!v.items.length) {
if (this.item.saleCategory === null || this.item.saleCategory === undefined) { this.snackBar.open('Bundle must contain at least one item', 'Error');
this.item.saleCategory = new SaleCategory(); return null;
} }
this.item.saleCategory.id = saleCategoryId; return new Bundle({
...this.item(),
// ensure items array exists name: v.name ?? '',
if (!this.item.items) { units: v.units ?? '',
this.item.items = []; salePrice: Number(v.salePrice ?? 0),
} hasHappyHour: v.hasHappyHour ?? false,
isNotAvailable: v.isNotAvailable ?? false,
return this.item; sortOrder: this.item().sortOrder ?? 0,
menuCategory: v.menuCategory,
saleCategory: v.saleCategory,
items: v.items ?? [],
});
} }
} }
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -1,5 +1,5 @@
import { PercentPipe } from '@angular/common'; 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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -39,8 +39,7 @@ export interface CustomerDetailFormData {
FormField, FormField,
], ],
}) })
export class CustomerDetailComponent implements AfterViewInit { export class CustomerDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@@ -68,12 +67,10 @@ export class CustomerDetailComponent implements AfterViewInit {
hide = true; hide = true;
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -9,11 +9,11 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Filter</mat-label> <mat-label>Filter</mat-label>
<input type="text" matInput #filterElement [formField]="form.filter" autocomplete="off" /> <input type="text" matInput [formField]="form.filter" autocomplete="off" />
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
<mat-table #table [dataSource]="dataSource().items" matSort aria-label="Elements"> <mat-table #table [dataSource]="dataSource().items" matSort (matSortChange)="sortData($event)" aria-label="Elements">
<!-- Name Column --> <!-- Name Column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
@@ -1,21 +1,12 @@
/* eslint-disable @angular-eslint/no-input-rename */
import { PercentPipe } from '@angular/common'; import { PercentPipe } from '@angular/common';
import { import { Component, inject, signal, effect, computed, debounced, afterNextRender, input } from '@angular/core';
AfterViewInit,
Component,
ElementRef,
ViewChild,
inject,
signal,
effect,
computed,
debounced,
} from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
@@ -42,13 +33,23 @@ export interface CustomerListFormData {
FormField, FormField,
], ],
}) })
export class CustomerListComponent implements AfterViewInit { export class CustomerListComponent {
@ViewChild('filterElement', { static: true }) filterElement!: ElementRef<HTMLInputElement>;
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
@ViewChild(MatSort, { static: true }) sort!: MatSort;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private customerService = inject(CustomerService); 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<string>('name', { alias: 'f' });
direction = input<string>('asc', { alias: 'd' });
formModel = signal<CustomerListFormData>({ formModel = signal<CustomerListFormData>({
filter: this.route.snapshot.queryParamMap.get('q') ?? '', filter: this.route.snapshot.queryParamMap.get('q') ?? '',
@@ -56,23 +57,10 @@ export class CustomerListComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
filterSignal = computed(() => this.formModel().filter); filterSignal = computed(() => this.formModel().filter ?? null);
debouncedFilter = debounced(this.filterSignal, 150); debouncedFilter = debounced(this.filterSignal, 150);
pageIndex = signal(+(this.route.snapshot.queryParamMap.get('p') ?? '0')); listResource = this.customerService.query(this.q, this.pageIndex, this.pageSize, this.sortInput, this.direction);
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);
dataSource = computed(() => { dataSource = computed(() => {
const data = this.listResource.value(); const data = this.listResource.value();
@@ -84,32 +72,37 @@ export class CustomerListComponent implements AfterViewInit {
displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts']; displayedColumns = ['name', 'phone', 'address', 'printInBill', 'discounts'];
constructor() { constructor() {
// Sync query string whenever params change
effect(() => { effect(() => {
const p = this.queryParams(); const q = this.debouncedFilter.value();
this.router.navigate([], { this.router.navigate([], {
relativeTo: this.route, relativeTo: this.route,
queryParams: { q: p.q, p: p.p, s: p.s, f: p.f, d: p.d }, queryParams: { q: q },
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
replaceUrl: true, replaceUrl: true,
}); });
}); });
afterNextRender(() => {
this.form().focusBoundControl();
});
} }
ngAfterViewInit() { handlePageEvent(e: PageEvent) {
this.sort.sortChange.subscribe(() => { if (e.pageSize !== this.pageSize() || e.pageIndex !== this.pageIndex()) {
this.sortField.set(this.sort.active); this.router.navigate([], {
this.sortDirection.set(this.sort.direction); relativeTo: this.route,
this.pageIndex.set(0); queryParams: { s: e.pageSize, p: e.pageIndex },
queryParamsHandling: 'merge',
replaceUrl: true,
}); });
}
}
this.paginator.page.subscribe(() => { sortData(sort: Sort) {
this.pageSize.set(this.paginator.pageSize); this.router.navigate([], {
this.pageIndex.set(this.paginator.pageIndex); relativeTo: this.route,
queryParams: { f: sort.active || null, d: sort.direction || null },
queryParamsHandling: 'merge',
replaceUrl: true,
}); });
setTimeout(() => {
this.filterElement.nativeElement.focus();
}, 0);
} }
} }
+10 -7
View File
@@ -41,18 +41,21 @@ export class CustomerService {
} }
query( query(
params: Signal<{ q: string; p: number; s: number; f: string; d: string }>, q: Signal<string>,
p: Signal<number>,
s: Signal<number>,
f: Signal<string>,
d: Signal<string>,
): HttpResourceRef<PagedResult<Customer> | undefined> { ): HttpResourceRef<PagedResult<Customer> | undefined> {
return httpResource<PagedResult<Customer>>(() => { return httpResource<PagedResult<Customer>>(() => {
const p = params();
return { return {
url: `${url}/list`, url: `${url}/list`,
params: { params: {
...(p.q ? { q: p.q } : {}), q: q(),
p: p.p, p: p(),
s: p.s, s: s(),
f: p.f, f: f(),
d: p.d, d: d(),
}, },
}; };
}); });
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" /> <input matInput [formField]="form.name" (keyup.enter)="save()" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,8 +38,7 @@ export interface DeviceDetailFormData {
FormField, FormField,
], ],
}) })
export class DeviceDetailComponent implements AfterViewInit { export class DeviceDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private sectionService = inject(SectionService); private sectionService = inject(SectionService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
@@ -68,12 +67,10 @@ export class DeviceDetailComponent implements AfterViewInit {
sections = computed(() => this.sectionsResource.value() ?? []); sections = computed(() => this.sectionsResource.value() ?? []);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,13 +3,13 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #name [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Phone</mat-label> <mat-label>Phone</mat-label>
<input matInput #phone type="text" [formField]="form.phone" [matAutocomplete]="auto" autocomplete="off" /> <input matInput type="text" [formField]="form.phone" [matAutocomplete]="auto" autocomplete="off" />
</mat-form-field> </mat-form-field>
<mat-autocomplete <mat-autocomplete
#auto="matAutocomplete" #auto="matAutocomplete"
@@ -1,14 +1,4 @@
import { import { Component, computed, inject, debounced, linkedSignal, input, afterNextRender } from '@angular/core';
AfterViewInit,
Component,
ElementRef,
ViewChild,
computed,
inject,
debounced,
linkedSignal,
input,
} from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatAutocompleteSelectedEvent, MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -53,8 +43,7 @@ export interface GuestBookDetailFormData {
FormField, FormField,
], ],
}) })
export class GuestBookDetailComponent implements AfterViewInit { export class GuestBookDetailComponent {
@ViewChild('phone', { static: true }) phoneElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -111,12 +100,10 @@ export class GuestBookDetailComponent implements AfterViewInit {
return this.customersResource.value() ?? []; return this.customersResource.value() ?? [];
}); });
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.phoneElement !== undefined) { this.form().focusBoundControl();
this.phoneElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -33,7 +33,6 @@ export interface HeaderFooterFormData {
], ],
}) })
export class HeaderFooterComponent { export class HeaderFooterComponent {
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -23,8 +23,7 @@ export interface MenuCategoryDetailFormData {
styleUrls: ['./menu-category-detail.component.css'], styleUrls: ['./menu-category-detail.component.css'],
imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField], imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField],
}) })
export class MenuCategoryDetailComponent implements AfterViewInit { export class MenuCategoryDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -47,12 +46,10 @@ export class MenuCategoryDetailComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -40,7 +40,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Filter</mat-label> <mat-label>Filter</mat-label>
<input type="text" matInput #filterElement [formField]="form.filter" autocomplete="off" /> <input type="text" matInput [formField]="form.filter" autocomplete="off" />
<mat-hint <mat-hint
>n: Name, sc: Sale Category, mc: Menu Category, q:(=/&lt;=/&gt;=/&lt;/&gt;)Quantity, >n: Name, sc: Sale Category, mc: Menu Category, q:(=/&lt;=/&gt;=/&lt;/&gt;)Quantity,
s:(=/&lt;=/&gt;=/&lt;/&gt;)Sales</mat-hint s:(=/&lt;=/&gt;=/&lt;/&gt;)Sales</mat-hint
@@ -1,5 +1,5 @@
import { DecimalPipe, PercentPipe } from '@angular/common'; import { DecimalPipe, PercentPipe } from '@angular/common';
import { Component, ViewChild, ElementRef, inject, computed, linkedSignal, input } from '@angular/core'; import { Component, inject, computed, linkedSignal, input, afterNextRender } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop'; import { toObservable } from '@angular/core/rxjs-interop';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -7,9 +7,9 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
@@ -45,9 +45,6 @@ export interface MenuEngineeringReportFormData {
], ],
}) })
export class MenuEngineeringReportComponent { export class MenuEngineeringReportComponent {
@ViewChild('filterElement', { static: true }) filterElement?: ElementRef;
@ViewChild(MatPaginator, { static: true }) paginator?: MatPaginator;
@ViewChild(MatSort, { static: true }) sort?: MatSort;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private toCsv = inject(ToCsvService); private toCsv = inject(ToCsvService);
@@ -89,6 +86,9 @@ export class MenuEngineeringReportComponent {
// Listen to Filter Change // Listen to Filter Change
const filterSignal = computed(() => this.formModel().filter); const filterSignal = computed(() => this.formModel().filter);
this.filter = toObservable(filterSignal).pipe(debounceTime(150), distinctUntilChanged()); this.filter = toObservable(filterSignal).pipe(debounceTime(150), distinctUntilChanged());
afterNextRender(() => {
this.form().focusBoundControl();
});
} }
show() { show() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -48,8 +48,7 @@ export interface ModifierCategoryDetailFormData {
FormField, FormField,
], ],
}) })
export class ModifierCategoryDetailComponent implements AfterViewInit { export class ModifierCategoryDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@@ -108,14 +107,9 @@ export class ModifierCategoryDetailComponent implements AfterViewInit {
}); });
} }
}); });
} afterNextRender(() => {
this.form().focusBoundControl();
ngAfterViewInit() { });
setTimeout(() => {
if (this.nameElement !== undefined) {
this.nameElement.nativeElement.focus();
}
}, 0);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #name [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField, required } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,8 +38,7 @@ export interface ModifierDetailFormData {
FormField, FormField,
], ],
}) })
export class ModifierDetailComponent implements AfterViewInit { export class ModifierDetailComponent {
@ViewChild('name', { static: true }) nameElement?: ElementRef;
private modifierCategoryService = inject(ModifierCategoryService); private modifierCategoryService = inject(ModifierCategoryService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
@@ -71,12 +70,10 @@ export class ModifierDetailComponent implements AfterViewInit {
required(schema.modifierCategory, { message: 'Modifier Category is required' }); required(schema.modifierCategory, { message: 'Modifier Category is required' });
}); });
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@@ -23,8 +23,7 @@ export interface PrinterDetailFormData {
styleUrls: ['./printer-detail.component.css'], styleUrls: ['./printer-detail.component.css'],
imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField], imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField],
}) })
export class PrinterDetailComponent implements AfterViewInit { export class PrinterDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -48,12 +47,10 @@ export class PrinterDetailComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -9,7 +9,7 @@
</button> </button>
</h2> </h2>
<form class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container sm:max-lg:flex-col"> <div class="row-container sm:max-lg:flex-col">
<mat-form-field class="flex-auto basis-1-3"> <mat-form-field class="flex-auto basis-1-3">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -1,6 +1,6 @@
import { DecimalPipe } from '@angular/common'; import { DecimalPipe } from '@angular/common';
import { Component, inject, computed, linkedSignal, input } from '@angular/core'; import { Component, inject, computed, linkedSignal, input } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@@ -37,6 +37,7 @@ export interface ProductSaleReportFormData {
MatSelectModule, MatSelectModule,
MatTableModule, MatTableModule,
FormField, FormField,
FormRoot,
], ],
}) })
export class ProductSaleReportComponent { export class ProductSaleReportComponent {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="basis-3-4"> <mat-form-field class="basis-3-4">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #name [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
<mat-form-field class="basis-1-4"> <mat-form-field class="basis-1-4">
<mat-label>Fraction Units</mat-label> <mat-label>Fraction Units</mat-label>
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { AfterViewInit, Component, ElementRef, ViewChild, inject, computed, linkedSignal, input } from '@angular/core'; import { Component, inject, computed, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField, required, min, max } from '@angular/forms/signals'; import { form, FormField, required, min, max } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -57,8 +57,7 @@ export interface ProductDetailFormData {
FormField, FormField,
], ],
}) })
export class ProductDetailComponent implements AfterViewInit { export class ProductDetailComponent {
@ViewChild('name', { static: true }) nameElement?: ElementRef;
private menuCategoryService = inject(MenuCategoryService); private menuCategoryService = inject(MenuCategoryService);
private saleCategoryService = inject(SaleCategoryService); private saleCategoryService = inject(SaleCategoryService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
@@ -123,12 +122,10 @@ export class ProductDetailComponent implements AfterViewInit {
'action', 'action',
]; ];
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
addRow() { addRow() {
+2 -2
View File
@@ -44,9 +44,9 @@ export class ProductService {
}); });
} }
query(query: Signal<string | null> | string | null): HttpResourceRef<ProductQuery[] | undefined> { query(query: Signal<string | null>): HttpResourceRef<ProductQuery[] | undefined> {
return httpResource<ProductQuery[]>(() => { return httpResource<ProductQuery[]>(() => {
const qVal = typeof query === 'function' ? query() : query; const qVal = query();
return `${url}/query?q=${qVal ?? ''}`; return `${url}/query?q=${qVal ?? ''}`;
}); });
} }
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" /> <input matInput [formField]="form.name" (keyup.enter)="save()" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@@ -23,8 +23,7 @@ export interface RegimeDetailFormData {
styleUrls: ['./regime-detail.component.css'], styleUrls: ['./regime-detail.component.css'],
imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField], imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField],
}) })
export class RegimeDetailComponent implements AfterViewInit { export class RegimeDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -48,12 +47,10 @@ export class RegimeDetailComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -24,8 +24,7 @@ export interface RoleDetailFormData {
styleUrls: ['./role-detail.component.css'], styleUrls: ['./role-detail.component.css'],
imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField], imports: [MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, FormField],
}) })
export class RoleDetailComponent implements AfterViewInit { export class RoleDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@@ -60,12 +59,10 @@ export class RoleDetailComponent implements AfterViewInit {
return implied; return implied;
}); });
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { AfterViewInit, Component, ElementRef, ViewChild, inject, computed, linkedSignal, input } from '@angular/core'; import { Component, inject, computed, linkedSignal, input, afterNextRender } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -43,8 +43,7 @@ export interface SaleCategoryDetailFormData {
RouterLink, RouterLink,
], ],
}) })
export class SaleCategoryDetailComponent implements AfterViewInit { export class SaleCategoryDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private taxService = inject(TaxService); private taxService = inject(TaxService);
private productService = inject(ProductService); private productService = inject(ProductService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
@@ -80,12 +79,10 @@ export class SaleCategoryDetailComponent implements AfterViewInit {
dataSource = computed(() => this.products()); dataSource = computed(() => this.products());
displayedColumns = ['name', 'price', 'menuCategory']; displayedColumns = ['name', 'price', 'menuCategory'];
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -20,7 +20,6 @@ export class SaleCategoryListComponent {
private ser = inject(SaleCategoryService); private ser = inject(SaleCategoryService);
listResource = this.ser.list(); listResource = this.ser.list();
// @ViewChild('table', { static: true }) table?: MatTable<SaleCategory>;
dataSource = computed(() => this.listResource.value() ?? []); dataSource = computed(() => this.listResource.value() ?? []);
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
@@ -1,5 +1,5 @@
import { CdkScrollableModule } from '@angular/cdk/scrolling'; import { CdkScrollableModule } from '@angular/cdk/scrolling';
import { Component, ElementRef, ViewChild, inject, computed, signal } from '@angular/core'; import { Component, inject, computed, signal } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
@@ -26,7 +26,6 @@ export interface ReasonFormData {
], ],
}) })
export class ReasonComponent { export class ReasonComponent {
@ViewChild('son', { static: true }) son?: ElementRef;
dialogRef = inject<MatDialogRef<ReasonComponent>>(MatDialogRef); dialogRef = inject<MatDialogRef<ReasonComponent>>(MatDialogRef);
private data = inject<{ private data = inject<{
title: string; title: string;
@@ -1,6 +1,6 @@
import { CdkScrollableModule } from '@angular/cdk/scrolling'; import { CdkScrollableModule } from '@angular/cdk/scrolling';
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { Component, ElementRef, ViewChild, inject, signal, computed } from '@angular/core'; import { Component, inject, signal, computed } from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
@@ -38,7 +38,6 @@ export interface ReceivePaymentFormData {
], ],
}) })
export class ReceivePaymentComponent { export class ReceivePaymentComponent {
@ViewChild('son', { static: true }) son?: ElementRef;
private ser = inject(SettleOptionService); private ser = inject(SettleOptionService);
dialogRef = inject<MatDialogRef<ReceivePaymentComponent>>(MatDialogRef); dialogRef = inject<MatDialogRef<ReceivePaymentComponent>>(MatDialogRef);
data = inject<{ data = inject<{
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -43,7 +43,6 @@ export interface SectionPrinterFormData {
], ],
}) })
export class SectionPrinterComponent { export class SectionPrinterComponent {
@ViewChild('section', { static: true }) sectionElement?: ElementRef;
private printerService = inject(PrinterService); private printerService = inject(PrinterService);
private sectionService = inject(SectionService); private sectionService = inject(SectionService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" /> <input matInput [formField]="form.name" (keyup.enter)="save()" />
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
@@ -21,8 +21,7 @@ export interface SectionDetailFormData {
styleUrls: ['./section-detail.component.css'], styleUrls: ['./section-detail.component.css'],
imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField], imports: [MatButtonModule, MatFormFieldModule, MatInputModule, FormField],
}) })
export class SectionDetailComponent implements AfterViewInit { export class SectionDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -44,12 +43,10 @@ export class SectionDetailComponent implements AfterViewInit {
form = form(this.formModel); form = form(this.formModel);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -37,8 +37,7 @@ export interface SettleOptionDetailFormData {
FormField, FormField,
], ],
}) })
export class SettleOptionDetailComponent implements AfterViewInit { export class SettleOptionDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
@@ -73,14 +72,9 @@ export class SettleOptionDetailComponent implements AfterViewInit {
this.reportingLevel = Object.keys(ReportingLevel) this.reportingLevel = Object.keys(ReportingLevel)
.filter((e) => !isNaN(+e)) .filter((e) => !isNaN(+e))
.map((o) => ({ id: +o, name: ReportingLevel[+o] as string })); .map((o) => ({ id: +o, name: ReportingLevel[+o] as string }));
} afterNextRender(() => {
this.form().focusBoundControl();
ngAfterViewInit() { });
setTimeout(() => {
if (this.nameElement !== undefined) {
this.nameElement.nativeElement.focus();
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" /> <input matInput [formField]="form.name" (keyup.enter)="save()" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -37,8 +37,7 @@ export interface TableDetailFormData {
FormField, FormField,
], ],
}) })
export class TableDetailComponent implements AfterViewInit { export class TableDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private sectionService = inject(SectionService); private sectionService = inject(SectionService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
@@ -68,12 +67,10 @@ export class TableDetailComponent implements AfterViewInit {
sections = computed(() => this.sectionsResource.value() ?? []); sections = computed(() => this.sectionsResource.value() ?? []);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" (keyup.enter)="save()" /> <input matInput [formField]="form.name" (keyup.enter)="save()" />
<mat-hint> Format for GST: ST GST &#64; x% (1/2) ; CGST &#64; x% (1/2) </mat-hint> <mat-hint> Format for GST: ST GST &#64; x% (1/2) ; CGST &#64; x% (1/2) </mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -28,8 +28,7 @@ export interface TaxDetailFormData {
styleUrls: ['./tax-detail.component.css'], styleUrls: ['./tax-detail.component.css'],
imports: [MatButtonModule, MatFormFieldModule, MatInputModule, MatOptionModule, MatSelectModule, FormField], imports: [MatButtonModule, MatFormFieldModule, MatInputModule, MatOptionModule, MatSelectModule, FormField],
}) })
export class TaxDetailComponent implements AfterViewInit { export class TaxDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private regimeService = inject(RegimeService); private regimeService = inject(RegimeService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
@@ -58,12 +57,10 @@ export class TaxDetailComponent implements AfterViewInit {
regimes = computed(() => this.regimesResource.value() ?? []); regimes = computed(() => this.regimesResource.value() ?? []);
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.form().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
save() { save() {
@@ -6,7 +6,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-3-4"> <mat-form-field class="flex-auto basis-3-4">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #name [formField]="productForm.name" /> <input matInput [formField]="productForm.name" />
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-1-4"> <mat-form-field class="flex-auto basis-1-4">
<mat-label>Fraction Units</mat-label> <mat-label>Fraction Units</mat-label>
@@ -1,14 +1,4 @@
import { import { Component, computed, inject, signal, linkedSignal, input, afterNextRender } from '@angular/core';
AfterViewInit,
Component,
ElementRef,
ViewChild,
computed,
inject,
signal,
linkedSignal,
input,
} from '@angular/core';
import { form, FormField } from '@angular/forms/signals'; import { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@@ -74,8 +64,7 @@ export interface TemporalSkuFormData {
FormField, FormField,
], ],
}) })
export class TemporalProductDetailComponent implements AfterViewInit { export class TemporalProductDetailComponent {
@ViewChild('name', { static: true }) nameElement?: ElementRef;
private menuCategoryService = inject(MenuCategoryService); private menuCategoryService = inject(MenuCategoryService);
private saleCategoryService = inject(SaleCategoryService); private saleCategoryService = inject(SaleCategoryService);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
@@ -175,12 +164,10 @@ export class TemporalProductDetailComponent implements AfterViewInit {
// //
// // } // // }
ngAfterViewInit() { constructor() {
setTimeout(() => { afterNextRender(() => {
if (this.nameElement !== undefined) { this.productForm().focusBoundControl();
this.nameElement.nativeElement.focus(); });
}
}, 0);
} }
editProduct(p: Product) { editProduct(p: Product) {
@@ -188,7 +175,7 @@ export class TemporalProductDetailComponent implements AfterViewInit {
console.log(p); console.log(p);
setTimeout(() => this.nameElement?.nativeElement?.focus?.(), 0); this.productForm().focusBoundControl();
} }
updateProduct() { updateProduct() {
@@ -3,7 +3,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput #nameElement [formField]="form.name" /> <input matInput [formField]="form.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
@@ -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 { form, FormField } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -35,8 +35,7 @@ export interface UserDetailFormData {
FormField, FormField,
], ],
}) })
export class UserDetailComponent implements AfterViewInit { export class UserDetailComponent {
@ViewChild('nameElement', { static: true }) nameElement?: ElementRef;
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
@@ -64,15 +63,10 @@ export class UserDetailComponent implements AfterViewInit {
hide = true; hide = true;
constructor() { constructor() {
this.hide = true; afterNextRender(() => {
} this.form().focusBoundControl();
// this.formModel().root.focus("name");
ngAfterViewInit() { });
setTimeout(() => {
if (this.nameElement !== undefined) {
this.nameElement.nativeElement.focus();
}
}, 0);
} }
save() { save() {
+438 -451
View File
File diff suppressed because it is too large. Load diff