diff --git a/.agents/skills/fastapi/SKILL.md b/.agents/skills/fastapi/SKILL.md new file mode 100644 index 00000000..e4268975 --- /dev/null +++ b/.agents/skills/fastapi/SKILL.md @@ -0,0 +1,321 @@ +--- +name: fastapi +description: FastAPI best practices and conventions. Use when working with FastAPI APIs, Pydantic models, dependencies, streaming responses including Server-Sent Events (SSE), and serving frontend apps. Keeps FastAPI code clean and up to date with the latest features and patterns. +--- + +# FastAPI + +Official FastAPI skill to write code with best practices, keeping up to date with new versions and features. + +## Quick Reference + +* Serve frontend apps: use `app.frontend()` or `router.frontend()` for built frontend assets; see [Serve Frontend Apps](#serve-frontend-apps). +* Server-Sent Events (SSE): use `response_class=EventSourceResponse` and `yield`; see [Streaming](#streaming-json-lines-sse-bytes) and [the streaming reference](references/streaming.md). +* JSON Lines and byte streaming: see [the streaming reference](references/streaming.md). +* Dependencies: use `Annotated[..., Depends(...)]`; see [Dependency Injection](#dependency-injection) and [the dependency injection reference](references/dependencies.md) for `yield`, scopes, and class dependencies. +* Response models: prefer return types; use `response_model` when the public response schema differs from the internal return value; see [the response reference](references/responses.md). +* Pydantic models: do not use ellipsis or `RootModel`; see [the Pydantic reference](references/pydantic.md). +* Routing: declare router-level prefix, tags, and shared dependencies on the `APIRouter`; see [the path operation reference](references/path-operations.md). +* Tooling and related libraries: use uv, Ruff, ty, Asyncer, SQLModel, and HTTPX when applicable; see [the other tools reference](references/other-tools.md). + +## Use the `fastapi` CLI + +Run the development server on localhost with reload: + +```bash +fastapi dev +``` + +Run the production server: + +```bash +fastapi run +``` + +Prefer declaring the entrypoint in `pyproject.toml`: + +```toml +[tool.fastapi] +entrypoint = "my_app.main:app" +``` + +When adding the entrypoint is not possible, or the user explicitly asks not to, pass the app file path: + +```bash +fastapi dev my_app/main.py +``` + +## Use `Annotated` + +Always prefer the `Annotated` style for parameter and dependency declarations. It keeps function signatures working in other contexts, respects the types, and allows reusability. + +Use `Annotated` for parameter declarations, including `Path`, `Query`, `Header`, etc.: + +```python +from typing import Annotated + +from fastapi import FastAPI, Path, Query + +app = FastAPI() + + +@app.get("/items/{item_id}") +async def read_item( + item_id: Annotated[int, Path(ge=1, description="The item ID")], + q: Annotated[str | None, Query(max_length=50)] = None, +): + return {"message": "Hello World"} +``` + +Use `Annotated` for dependencies with `Depends()`. Unless asked not to, create a new type alias for the dependency to allow reusing it: + +```python +from typing import Annotated + +from fastapi import Depends, FastAPI + +app = FastAPI() + + +def get_current_user(): + return {"username": "johndoe"} + + +CurrentUserDep = Annotated[dict, Depends(get_current_user)] + + +@app.get("/items/") +async def read_item(current_user: CurrentUserDep): + return {"message": "Hello World"} +``` + +## Do not use Ellipsis for *path operations* or Pydantic models + +Do not use `...` as a default value for required parameters or model fields. It's not needed and not recommended. + +```python +from typing import Annotated + +from fastapi import FastAPI, Query +from pydantic import BaseModel, Field + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + price: float = Field(gt=0) + + +@app.post("/items/") +async def create_item(item: Item, project_id: Annotated[int, Query()]): + return item +``` + +See [the Pydantic reference](references/pydantic.md) for more details. + +## Return Type or Response Model + +When possible, include a return type. It will be used to validate, filter, document, and serialize the response. + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + description: str | None = None + + +@app.get("/items/me") +async def get_item() -> Item: + return Item(name="Plumbus", description="All-purpose home device") +``` + +Return types or response models filter data to avoid exposing sensitive information, and they let Pydantic serialize the data on the Rust side for performance. + +Use `response_model` when the type you return is not the same as the public schema you want to validate, filter, document, and serialize. See [the response reference](references/responses.md). + +## Performance + +Do not use `ORJSONResponse` or `UJSONResponse`, they are deprecated. + +Instead, declare a return type or response model. Pydantic will handle the data serialization on the Rust side. + +## Including Routers + +When declaring routers, prefer to add router-level parameters like prefix, tags, and shared dependencies to the router itself instead of in `include_router()`. + +```python +from fastapi import APIRouter, Depends, FastAPI + +app = FastAPI() + + +def get_current_user(): + return {"username": "johndoe"} + + +router = APIRouter( + prefix="/items", + tags=["items"], + dependencies=[Depends(get_current_user)], +) + + +@router.get("/") +async def list_items(): + return [] + + +app.include_router(router) +``` + +See [the path operation reference](references/path-operations.md) for more routing patterns. + +## Serve Frontend Apps + +Use `app.frontend()` to serve a built static frontend app, for example a directory generated by Vite, Astro, Angular, Svelte, Vue, or a similar tool. + +```python +from fastapi import FastAPI + +app = FastAPI() + +app.frontend("/", directory="dist") +``` + +Use `router.frontend()` when the frontend belongs to an `APIRouter`; normal router prefix behavior applies when the router is included. + +```python +from fastapi import APIRouter, FastAPI + +app = FastAPI() +router = APIRouter(prefix="/admin") + +router.frontend("/", directory="admin-dist") +app.include_router(router) +``` + +`app.frontend()` and `router.frontend()` are low-priority routes: regular API routes are matched first, then frontend files and client-side routing fallbacks. Use this for single-page apps and built frontend assets instead of mounting `StaticFiles` manually. + +## Dependency Injection + +Use dependencies when the logic can't be declared in Pydantic validation, depends on external resources, needs cleanup with `yield`, or is shared across endpoints. + +Apply shared dependencies at the router level via `dependencies=[Depends(...)]`. + +See [the dependency injection reference](references/dependencies.md) for detailed patterns including `yield` with `scope`, and class dependencies. + +## Async vs Sync *path operations* + +Use `async` *path operations* only when fully certain that the logic called inside is compatible with async and await, and that it doesn't block. + +```python +from fastapi import FastAPI + +app = FastAPI() + + +@app.get("/async-items/") +async def read_async_items(): + data = await some_async_library.fetch_items() + return data + + +@app.get("/items/") +def read_items(): + data = some_blocking_library.fetch_items() + return data +``` + +In case of doubt, or by default, use regular `def` functions. They will be run in a threadpool so they don't block the event loop. The same rules apply to dependencies. + +Make sure blocking code is not run inside of `async` functions. The logic will work, but will damage performance heavily. + +When needing to mix blocking and async code, see Asyncer in [the other tools reference](references/other-tools.md). + +## Streaming (JSON Lines, SSE, bytes) + +To stream Server-Sent Events, use `response_class=EventSourceResponse` and `yield` items from the endpoint. + +```python +from collections.abc import AsyncIterable + +from fastapi import FastAPI +from fastapi.sse import EventSourceResponse, ServerSentEvent + +app = FastAPI() + + +@app.get("/events", response_class=EventSourceResponse) +async def stream_events() -> AsyncIterable[ServerSentEvent]: + yield ServerSentEvent(data={"status": "started"}, event="status", id="1") +``` + +Plain objects are automatically JSON-serialized as `data:` fields. Use `ServerSentEvent` for full control over SSE fields (`event`, `id`, `retry`, `comment`) and `raw_data` for pre-formatted strings. + +See [the streaming reference](references/streaming.md) for JSON Lines, Server-Sent Events (`EventSourceResponse`, `ServerSentEvent`), and byte streaming (`StreamingResponse`) patterns. + +## Tooling + +See [the other tools reference](references/other-tools.md) for details on uv, Ruff, ty for package management, linting, type checking, formatting, etc. + +## Other Libraries + +See [the other tools reference](references/other-tools.md) for details on other libraries: + +* Asyncer for handling async and await, concurrency, mixing async and blocking code, prefer it over AnyIO or asyncio. +* SQLModel for working with SQL databases, prefer it over SQLAlchemy. +* HTTPX for interacting with HTTP (other APIs), prefer it over Requests. + +## Do not use Pydantic RootModels + +Do not use Pydantic `RootModel`; instead use regular type annotations with `Annotated` and Pydantic validation utilities. + +```python +from typing import Annotated + +from fastapi import Body, FastAPI +from pydantic import Field + +app = FastAPI() + + +@app.post("/items/") +async def create_items(items: Annotated[list[int], Field(min_length=1), Body()]): + return items +``` + +FastAPI supports these type annotations and will create a Pydantic `TypeAdapter` for them, so types work normally without custom wrapper models. See [the Pydantic reference](references/pydantic.md). + +## Use one HTTP operation per function + +Don't mix HTTP operations in a single function. Having one function per HTTP operation helps separate concerns and organize the code. + +```python +from fastapi import FastAPI +from pydantic import BaseModel + +app = FastAPI() + + +class Item(BaseModel): + name: str + + +@app.get("/items/") +async def list_items(): + return [] + + +@app.post("/items/") +async def create_item(item: Item): + return item +``` + +See [the path operation reference](references/path-operations.md) for more examples. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 307817be..cacb5c99 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,7 +71,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-install-project COPY /brewman ./ -COPY --from=builder /frontend/browser /app/static +COPY --from=builder /frontend/browser /app/frontend # Sync the project # Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers diff --git a/ansible/files/Caddyfile.j2 b/ansible/files/Caddyfile.j2 index 72722b95..770221dc 100644 --- a/ansible/files/Caddyfile.j2 +++ b/ansible/files/Caddyfile.j2 @@ -1,24 +1,3 @@ {{ host }} { - # Match and proxy API routes - @apiRoutes { - path_regexp ^/(api|token|refresh|attendance-report|fingerprint-report|db-image) - } - handle @apiRoutes { - reverse_proxy @apiRoutes {{ host_directory }}:80 - } - - # Match requests that end with .js, .css, .ico, or .html - @staticFiles { - path_regexp \.(js|css|ico|html)$ - } - handle @staticFiles { - rewrite * /static{uri} - reverse_proxy {{ host_directory }}:80 - } - - # All other frontend routes → /static/index.html - handle { - rewrite * /static/index.html - reverse_proxy {{ host_directory }}:80 - } + reverse_proxy {{ host_directory }}:80 } diff --git a/brewman/alembic/versions/ee77296e69b7_recipe_template_removed.py b/brewman/alembic/versions/ee77296e69b7_recipe_template_removed.py new file mode 100644 index 00000000..148d7d88 --- /dev/null +++ b/brewman/alembic/versions/ee77296e69b7_recipe_template_removed.py @@ -0,0 +1,66 @@ +"""recipe template removed + +Revision ID: ee77296e69b7 +Revises: 09a4f0ca450f +Create Date: 2026-08-26 05:54:36.396717 + +""" + +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "ee77296e69b7" +down_revision = "09a4f0ca450f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("recipe_templates") + op.alter_column( + "recipes", + "recipe_yield", + existing_type=sa.NUMERIC(precision=15, scale=2), + type_=sa.Numeric(precision=12, scale=4), + existing_nullable=False, + ) + op.drop_index(op.f("ix_recipes_date"), table_name="recipes") + op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + op.f("uq_recipes_sku_id"), "recipes", ["sku_id", "date"], postgresql_nulls_not_distinct=False + ) + op.create_index(op.f("ix_recipes_date"), "recipes", ["date"], unique=False) + op.alter_column( + "recipes", + "recipe_yield", + existing_type=sa.Numeric(precision=12, scale=4), + type_=sa.NUMERIC(precision=15, scale=2), + existing_nullable=False, + ) + op.create_table( + "recipe_templates", + sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), autoincrement=False, nullable=False), + sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("date", sa.DATE(), autoincrement=False, nullable=False), + sa.Column("text", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("selected", sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_recipe_templates")), + sa.UniqueConstraint( + "name", name=op.f("uq_recipe_templates_name"), postgresql_include=[], postgresql_nulls_not_distinct=False + ), + ) + op.create_index( + op.f("only_one_selected_template"), + "recipe_templates", + ["selected"], + unique=True, + postgresql_where="(selected = true)", + ) + # ### end Alembic commands ### diff --git a/brewman/brewman/db/base.py b/brewman/brewman/db/base.py index 77ac8f9a..0891b877 100644 --- a/brewman/brewman/db/base.py +++ b/brewman/brewman/db/base.py @@ -17,6 +17,7 @@ from ..models.incentive import Incentive from ..models.inventory import Inventory from ..models.journal import Journal from ..models.login_history import LoginHistory +from ..models.mozimo_stock_register import MozimoStockRegister from ..models.period import Period from ..models.permission import Permission from ..models.price import Price @@ -30,7 +31,6 @@ from ..models.recipe_item import RecipeItem from ..models.recipe_photo import RecipePhoto from ..models.recipe_step import RecipeStep from ..models.recipe_tag import RecipeTag -from ..models.recipe_template import RecipeTemplate from ..models.role import Role from ..models.role_include import RoleInclude from ..models.role_permission import RolePermission @@ -63,6 +63,7 @@ __all__ = [ "Inventory", "Journal", "LoginHistory", + "MozimoStockRegister", "Permission", "Period", "Price", @@ -76,7 +77,6 @@ __all__ = [ "RecipePhoto", "RecipeStep", "RecipeTag", - "RecipeTemplate", "reg", "Role", "RoleInclude", diff --git a/brewman/brewman/db/friendly_db_error.py b/brewman/brewman/db/friendly_db_error.py index 8bddf0c2..96a2551d 100644 --- a/brewman/brewman/db/friendly_db_error.py +++ b/brewman/brewman/db/friendly_db_error.py @@ -89,9 +89,6 @@ _CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = { ), "uq_recipe_items_recipe_id": FriendlyDBError(409, "RECIPE_ITEMS_EXISTS", "Items already exist for this recipe."), "uq_recipe_tags_recipe_id": FriendlyDBError(409, "RECIPE_TAGS_EXISTS", "Tags already exist for this recipe."), - "uq_recipe_templates_name": FriendlyDBError( - 409, "RECIPE_TEMPLATE_NAME_EXISTS", "A recipe template with this name already exists." - ), "uq_recipes_sku_id": FriendlyDBError(409, "RECIPE_FOR_SKU_EXISTS", "A recipe already exists for this SKU."), "uq_tags_name": FriendlyDBError(409, "TAG_NAME_EXISTS", "A tag with this name already exists."), # --- role_permissions --- @@ -114,9 +111,6 @@ _CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = { "only_one_valid_attendance": FriendlyDBError( 409, "ATTENDANCE_EXISTS", "A valid attendance already exists for this employee on this date." ), - "only_one_selected_template": FriendlyDBError( - 409, "TEMPLATE_ALREADY_SELECTED", "Only one template can be selected at a time." - ), } # Optional: FK-specific friendly messages (use when you want better UX than generic FK failure) diff --git a/brewman/brewman/main.py b/brewman/brewman/main.py index 1e5d1ab3..e45c62b3 100644 --- a/brewman/brewman/main.py +++ b/brewman/brewman/main.py @@ -4,7 +4,6 @@ import uvicorn from fastapi import FastAPI, Request from fastapi.responses import JSONResponse -from fastapi.staticfiles import StaticFiles from sqlalchemy.exc import IntegrityError, SQLAlchemyError from starlette.middleware.sessions import SessionMiddleware @@ -45,7 +44,6 @@ from .routers import ( rate_contract, rebase, recipe, - recipe_template, role, tag, temporal_product, @@ -124,7 +122,6 @@ app.include_router(product.router, prefix="/api/products", tags=["products"]) app.include_router(temporal_product.router, prefix="/api/temporal-products", tags=["products"]) app.include_router(product_group.router, prefix="/api/product-groups", tags=["products"]) app.include_router(recipe.router, prefix="/api/recipes", tags=["products"]) -app.include_router(recipe_template.router, prefix="/api/recipe-templates", tags=["products"]) app.include_router(period.router, prefix="/api/periods", tags=["periods"]) app.include_router(client.router, prefix="/api/clients", tags=["clients"]) @@ -174,7 +171,7 @@ app.include_router(rebase.router, prefix="/api/rebase", tags=["management"]) app.include_router(title.router, prefix="/api/title") app.include_router(health.router, prefix="/health", tags=["health"]) -app.mount("/static", StaticFiles(directory="static"), name="static") +app.frontend("/", directory="frontend", check_dir=False) def init() -> None: diff --git a/brewman/brewman/models/recipe_photo.py b/brewman/brewman/models/recipe_photo.py index d54dd8c5..968366cd 100644 --- a/brewman/brewman/models/recipe_photo.py +++ b/brewman/brewman/models/recipe_photo.py @@ -24,14 +24,18 @@ class RecipePhoto: id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") ) - recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True) - image_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("images.id"), nullable=False, index=True) + recipe_id: Mapped[uuid.UUID] = mapped_column( + Uuid, ForeignKey("recipes.id", ondelete="CASCADE"), nullable=False, index=True + ) + image_id: Mapped[uuid.UUID] = mapped_column( + Uuid, ForeignKey("images.id", ondelete="CASCADE"), nullable=False, index=True + ) order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True) caption: Mapped[str] = mapped_column(Text, nullable=False) recipe: Mapped[Recipe] = relationship("Recipe", back_populates="photos") - image: Mapped[DbImage] = relationship("DbImage") + image: Mapped[DbImage] = relationship("DbImage", cascade="all, delete-orphan", single_parent=True) def __init__( self, diff --git a/brewman/brewman/models/recipe_step.py b/brewman/brewman/models/recipe_step.py index 6f2eb302..c69c5191 100644 --- a/brewman/brewman/models/recipe_step.py +++ b/brewman/brewman/models/recipe_step.py @@ -26,7 +26,9 @@ class RecipeStep: id: Mapped[uuid.UUID] = mapped_column( Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") ) - recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True) + recipe_id: Mapped[uuid.UUID] = mapped_column( + Uuid, ForeignKey("recipes.id", ondelete="CASCADE"), nullable=False, index=True + ) order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True) phase: Mapped[str] = mapped_column(Text, nullable=False) # Prep/Cook/Finish/Serve diff --git a/brewman/brewman/models/recipe_template.py b/brewman/brewman/models/recipe_template.py deleted file mode 100644 index 8b171b2d..00000000 --- a/brewman/brewman/models/recipe_template.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -import uuid - -from datetime import date - -from sqlalchemy import Boolean, Date, Index, Unicode, Uuid, text -from sqlalchemy.orm import Mapped, mapped_column - -from ..db.base_class import reg - - -@reg.mapped_as_dataclass(unsafe_hash=True) -class RecipeTemplate: - __tablename__ = "recipe_templates" - - id: Mapped[uuid.UUID] = mapped_column( - Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") - ) - name: Mapped[str] = mapped_column(Unicode, unique=True, nullable=False) - date_: Mapped[date] = mapped_column("date", Date, nullable=False) - text_: Mapped[str] = mapped_column("text", Unicode, nullable=False) - selected: Mapped[bool] = mapped_column(Boolean, nullable=False) - - __table_args__ = ( - Index( - "only_one_selected_template", - "selected", - unique=True, - postgresql_where=(selected == True), # noqa: E712 - ), - ) - - def __init__(self, name: str, date_: date, text: str, selected: bool, id_: uuid.UUID | None = None) -> None: - self.name = name - self.date_ = date_ - self.text_ = text - self.selected = selected - if id_ is not None: - self.id = id_ diff --git a/brewman/brewman/models/setting_type.py b/brewman/brewman/models/setting_type.py index a0e76e75..fa51f656 100644 --- a/brewman/brewman/models/setting_type.py +++ b/brewman/brewman/models/setting_type.py @@ -4,4 +4,3 @@ import enum class SettingType(enum.IntEnum): VOUCHER_LOCK = 0 MAINTENANCE_MODE = 1 - RECIPE_TEMPLATE = 2 diff --git a/brewman/brewman/routers/recipe_template.py b/brewman/brewman/routers/recipe_template.py deleted file mode 100644 index 9f10d679..00000000 --- a/brewman/brewman/routers/recipe_template.py +++ /dev/null @@ -1,84 +0,0 @@ -import datetime -import uuid - -from typing import Annotated - -from fastapi import APIRouter, Security -from sqlalchemy import delete, select, update - -from ..core.security import get_current_active_user as get_user -from ..db.session import SessionDep -from ..models.recipe_template import RecipeTemplate -from ..schemas import recipe_template as schemas -from ..schemas.user import UserToken - - -router = APIRouter() - - -@router.post("", response_model=None) -def save( - data: schemas.RecipeTemplateIn, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep -) -> None: - date_ = data.date_ if data.date_ is not None else datetime.date.today() - if data.selected: - db.execute(update(RecipeTemplate).values(selected=False)) - item = RecipeTemplate(name=data.name, date_=date_, text=data.text, selected=data.selected) - db.add(item) - db.commit() - return None - - -@router.put("/{id_}", response_model=None) -def update_route( - id_: uuid.UUID, - data: schemas.RecipeTemplateIn, - user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], - db: SessionDep, -) -> None: - date_ = data.date_ if data.date_ is not None else datetime.date.today() - item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one() - if data.selected and not item.selected: - db.execute(update(RecipeTemplate).where(RecipeTemplate.id != id_).values(selected=False)) - item.name = data.name - item.text_ = data.text - item.selected = data.selected - item.date_ = date_ - db.commit() - return None - - -@router.delete("/{id_}", response_model=None) -def delete_route( - id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep -) -> None: - db.execute(delete(RecipeTemplate).where(RecipeTemplate.id == id_)) - db.commit() - return None - - -@router.get("", response_model=schemas.RecipeTemplateIn) -def show_blank( - user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], -) -> schemas.RecipeTemplateIn: - return schemas.RecipeTemplateIn(name="", date_=datetime.date.today(), text="", selected=False) - - -@router.get("/list", response_model=list[schemas.RecipeTemplate]) -async def show_list( - user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep -) -> list[schemas.RecipeTemplate]: - list_ = db.execute(select(RecipeTemplate).order_by(RecipeTemplate.name)).scalars().all() - return [ - schemas.RecipeTemplate(id_=i.id, name=i.name, date_=i.date_, text=i.text_, selected=i.selected) for i in list_ - ] - - -@router.get("/{id_}", response_model=schemas.RecipeTemplate) -def show_id( - id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep -) -> schemas.RecipeTemplate: - item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one() - return schemas.RecipeTemplate( - id_=item.id, name=item.name, date_=item.date_, text=item.text_, selected=item.selected - ) diff --git a/brewman/brewman/routers/reports/purchases.py b/brewman/brewman/routers/reports/purchases.py index c6ccdf0d..0678df6e 100644 --- a/brewman/brewman/routers/reports/purchases.py +++ b/brewman/brewman/routers/reports/purchases.py @@ -89,7 +89,7 @@ def build_report( quantity=quantity, rate=rate, amount=amount, - url=["/", "product-ledger", str(product.id)], + url=["/", "product-ledger", str(product.product_id)], ) body.append(row) return ( diff --git a/brewman/brewman/schemas/recipe_template.py b/brewman/brewman/schemas/recipe_template.py deleted file mode 100644 index 1c23f31d..00000000 --- a/brewman/brewman/schemas/recipe_template.py +++ /dev/null @@ -1,40 +0,0 @@ -import uuid - -from datetime import date, datetime - -from pydantic import ( - BaseModel, - ConfigDict, - FieldSerializationInfo, - field_serializer, - field_validator, -) - -from . import to_camel - - -class RecipeTemplateIn(BaseModel): - name: str - date_: date | None = None - text: str - selected: bool - - @field_validator("date_", mode="before") - @classmethod - def parse_valid_from(cls, value: date | str | None) -> date | None: - if value is None: - return None - if isinstance(value, date): - return value - return datetime.strptime(value, "%d-%b-%Y").date() - - @field_serializer("date_") - def serialize_date(self, value: date | None, info: FieldSerializationInfo) -> str | None: - return None if value is None else value.strftime("%d-%b-%Y") - - model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True) - - -class RecipeTemplate(RecipeTemplateIn): - id_: uuid.UUID - model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True) diff --git a/brewman/uv.lock b/brewman/uv.lock index 3575378e..7249dd4f 100644 --- a/brewman/uv.lock +++ b/brewman/uv.lock @@ -211,7 +211,7 @@ wheels = [ [[package]] name = "brewman" -version = "14.2.0" +version = "15.0.0" source = { editable = "." } dependencies = [ { name = "alembic" }, diff --git a/overlord/src/app/app.routes.ts b/overlord/src/app/app.routes.ts index 9a5beb12..8e1bf134 100644 --- a/overlord/src/app/app.routes.ts +++ b/overlord/src/app/app.routes.ts @@ -65,10 +65,6 @@ export const routes: Routes = [ path: 'recipes', loadChildren: () => import('./recipe/recipe.routes').then((mod) => mod.routes), }, - { - path: 'recipe-templates', - loadChildren: () => import('./recipe-template/recipe-template.routes').then((mod) => mod.routes), - }, { path: 'roles', loadChildren: () => import('./role/role.routes').then((mod) => mod.routes), diff --git a/overlord/src/app/attendance/attendance.component.html b/overlord/src/app/attendance/attendance.component.html index 00ca0d5f..172c1e06 100644 --- a/overlord/src/app/attendance/attendance.component.html +++ b/overlord/src/app/attendance/attendance.component.html @@ -1,6 +1,6 @@

Attendance

-
+
Date @@ -15,7 +15,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/attendance/attendance.component.ts b/overlord/src/app/attendance/attendance.component.ts index 7a9a252a..7f779eaa 100644 --- a/overlord/src/app/attendance/attendance.component.ts +++ b/overlord/src/app/attendance/attendance.component.ts @@ -1,4 +1,4 @@ -import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core'; +import { Component, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; @@ -22,7 +22,7 @@ import { AttendanceTypeService } from './attendance-type.service'; import { AttendanceService } from './attendance.service'; export interface AttendanceFormData { - date: moment.Moment; + date: Date; attendances: { attendanceType: number | null }[]; } @@ -30,6 +30,9 @@ export interface AttendanceFormData { selector: 'app-attendance', templateUrl: './attendance.component.html', styleUrls: ['./attendance.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -64,7 +67,7 @@ export class AttendanceComponent { model = linkedSignal({ source: this.info, computation: (infoVal): AttendanceFormData => ({ - date: moment(infoVal.date), + date: moment(infoVal.date, 'DD-MMM-YYYY').toDate(), attendances: infoVal.body.map((x) => ({ attendanceType: x.attendanceType.id, })), @@ -75,7 +78,6 @@ export class AttendanceComponent { displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/balance-sheet/balance-sheet.component.html b/overlord/src/app/balance-sheet/balance-sheet.component.html index ee4c0993..3b627614 100644 --- a/overlord/src/app/balance-sheet/balance-sheet.component.html +++ b/overlord/src/app/balance-sheet/balance-sheet.component.html @@ -1,6 +1,6 @@

Balance Sheet

- +
Date @@ -8,7 +8,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/balance-sheet/balance-sheet.component.ts b/overlord/src/app/balance-sheet/balance-sheet.component.ts index 35801712..e4da1fb2 100644 --- a/overlord/src/app/balance-sheet/balance-sheet.component.ts +++ b/overlord/src/app/balance-sheet/balance-sheet.component.ts @@ -1,8 +1,8 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core'; +import { Component, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; export interface BalanceSheetFormData { - date: moment.Moment; + date: Date; } import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -24,6 +24,9 @@ import { BalanceSheetService } from './balance-sheet.service'; selector: 'app-balance-sheet', templateUrl: './balance-sheet.component.html', styleUrls: ['./balance-sheet.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -56,7 +59,7 @@ export class BalanceSheetComponent { formModel = linkedSignal({ source: this.info, computation: (info: BalanceSheet): BalanceSheetFormData => ({ - date: info.date ? moment(info.date, 'DD-MMM-YYYY') : moment(new Date()), + date: info.date ? moment(info.date, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -103,7 +106,6 @@ export class BalanceSheetComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['group', 'name', 'subAmount', 'total']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/cash-flow/cash-flow.component.html b/overlord/src/app/cash-flow/cash-flow.component.html index 9278bbb0..41332dfe 100644 --- a/overlord/src/app/cash-flow/cash-flow.component.html +++ b/overlord/src/app/cash-flow/cash-flow.component.html @@ -1,6 +1,6 @@

Cash Flow

-
+
Start Date @@ -14,7 +14,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/cash-flow/cash-flow.component.ts b/overlord/src/app/cash-flow/cash-flow.component.ts index d0e21734..7f3bf53e 100644 --- a/overlord/src/app/cash-flow/cash-flow.component.ts +++ b/overlord/src/app/cash-flow/cash-flow.component.ts @@ -1,9 +1,9 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core'; +import { Component, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; export interface CashFlowFormData { - startDate: moment.Moment; - finishDate: moment.Moment; + startDate: Date; + finishDate: Date; } import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -24,6 +24,9 @@ import { CashFlowService } from './cash-flow.service'; selector: 'app-cash-flow', templateUrl: './cash-flow.component.html', styleUrls: ['./cash-flow.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -78,8 +81,8 @@ export class CashFlowComponent { model = linkedSignal({ source: this.info, computation: (info: CashFlow): CashFlowFormData => ({ - startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()), - finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()), + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -88,7 +91,6 @@ export class CashFlowComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['name', 'amount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); // TODO: also select the contents diff --git a/overlord/src/app/closing-stock/closing-stock.component.html b/overlord/src/app/closing-stock/closing-stock.component.html index b85d759a..c32055a5 100644 --- a/overlord/src/app/closing-stock/closing-stock.component.html +++ b/overlord/src/app/closing-stock/closing-stock.component.html @@ -7,7 +7,7 @@ } -
+
Department @@ -25,7 +25,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/closing-stock/closing-stock.component.ts b/overlord/src/app/closing-stock/closing-stock.component.ts index 20b8db59..c0ea39d9 100644 --- a/overlord/src/app/closing-stock/closing-stock.component.ts +++ b/overlord/src/app/closing-stock/closing-stock.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { Component, HostListener, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core'; +import { Component, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatOptionModule } from '@angular/material/core'; @@ -29,7 +29,7 @@ import { ClosingStockItem } from './closing-stock-item'; import { ClosingStockService } from './closing-stock.service'; export interface ClosingStockFormData { - date: moment.Moment; + date: Date; costCentre: CostCentre | null; stocks: { physical: number; costCentre: string | undefined }[]; } @@ -38,6 +38,9 @@ export interface ClosingStockFormData { selector: 'app-closing-stock', templateUrl: './closing-stock.component.html', styleUrls: ['./closing-stock.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ MatIconModule, FormField, @@ -83,7 +86,7 @@ export class ClosingStockComponent { model = linkedSignal({ source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })), computation: ({ info, costCentres }): ClosingStockFormData => ({ - date: moment(info.date, 'DD-MMM-YYYY'), + date: moment(info.date, 'DD-MMM-YYYY').toDate(), costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null, stocks: info.items.map((x) => ({ physical: x.physical, @@ -130,7 +133,6 @@ export class ClosingStockComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); @@ -165,7 +167,7 @@ export class ClosingStockComponent { getClosingStock(): ClosingStock { const formModel = this.model(); const currentInfo = this.info(); - currentInfo.date = formModel.date.format('DD-MMM-YYYY'); + currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY'); const array = formModel.stocks; currentInfo.items.forEach((item, index) => { item.physical = array[index].physical ?? 0; @@ -178,7 +180,7 @@ export class ClosingStockComponent { const formModel = this.model(); return new ClosingStock({ - date: formModel.date.format('DD-MMM-YYYY'), + date: moment(formModel.date).format('DD-MMM-YYYY'), costCentre: new CostCentre({ id: formModel.costCentre?.id ?? '' }), }); } diff --git a/overlord/src/app/core/nav-bar/nav-bar.component.html b/overlord/src/app/core/nav-bar/nav-bar.component.html index a3644529..7a4413e8 100644 --- a/overlord/src/app/core/nav-bar/nav-bar.component.html +++ b/overlord/src/app/core/nav-bar/nav-bar.component.html @@ -58,7 +58,6 @@ Products Product Groups Recipes - Recipe Templates Periods Tags diff --git a/overlord/src/app/daybook/daybook.component.html b/overlord/src/app/daybook/daybook.component.html index 124f25e2..06ebeb66 100644 --- a/overlord/src/app/daybook/daybook.component.html +++ b/overlord/src/app/daybook/daybook.component.html @@ -1,6 +1,6 @@

Daybook

- +
Start Date @@ -14,7 +14,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/daybook/daybook.component.ts b/overlord/src/app/daybook/daybook.component.ts index 60b22b87..d9529e97 100644 --- a/overlord/src/app/daybook/daybook.component.ts +++ b/overlord/src/app/daybook/daybook.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, linkedSignal, input, computed, afterNextRender, signal } from '@angular/core'; +import { Component, inject, linkedSignal, input, computed, afterNextRender, signal } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; export interface DaybookFormData { startDate: Date; @@ -24,6 +24,9 @@ import { DaybookService } from './daybook.service'; selector: 'app-daybook', templateUrl: './daybook.component.html', styleUrls: ['./daybook.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -107,7 +110,6 @@ export class DaybookComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['date', 'type', 'narration', 'debitText', 'debitAmount', 'creditText', 'creditAmount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); // TODO: Also select the text diff --git a/overlord/src/app/employee-attendance/employee-attendance.component.html b/overlord/src/app/employee-attendance/employee-attendance.component.html index 8c8aa33b..7bca6850 100644 --- a/overlord/src/app/employee-attendance/employee-attendance.component.html +++ b/overlord/src/app/employee-attendance/employee-attendance.component.html @@ -1,6 +1,6 @@

Employee Attendance

-
+
Start Date @@ -48,7 +48,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/employee-attendance/employee-attendance.component.ts b/overlord/src/app/employee-attendance/employee-attendance.component.ts index 0c3703bb..ea62ec82 100644 --- a/overlord/src/app/employee-attendance/employee-attendance.component.ts +++ b/overlord/src/app/employee-attendance/employee-attendance.component.ts @@ -1,13 +1,4 @@ -import { - computed, - Component, - HostListener, - inject, - debounced, - input, - linkedSignal, - afterNextRender, -} from '@angular/core'; +import { computed, Component, inject, debounced, input, linkedSignal, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -42,6 +33,9 @@ export interface EmployeeAttendanceFormData { selector: 'app-employee-attendance', templateUrl: './employee-attendance.component.html', styleUrls: ['./employee-attendance.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -101,7 +95,6 @@ export class EmployeeAttendanceComponent { employeesResource = this.employeeSer.autocomplete(computed(() => this.debouncedEmployeeSearch.value() ?? '')); employees = computed(() => this.employeesResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/employee-benefits/employee-benefits.component.ts b/overlord/src/app/employee-benefits/employee-benefits.component.ts index 8cc9c8c2..33c5d868 100644 --- a/overlord/src/app/employee-benefits/employee-benefits.component.ts +++ b/overlord/src/app/employee-benefits/employee-benefits.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { afterNextRender, Component, computed, HostListener, inject, input, linkedSignal, signal } from '@angular/core'; +import { afterNextRender, Component, computed, inject, input, linkedSignal, signal } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -48,6 +48,9 @@ export interface EmployeeBenefitsFormData { selector: 'app-employee-benefits', templateUrl: './employee-benefits.component.html', styleUrls: ['./employee-benefits.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -122,7 +125,6 @@ export class EmployeeBenefitsComponent { employeesResource = this.employeeSer.autocomplete(computed(() => this.employeesSearch() ?? '')); employees = computed(() => this.employeesResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form.date().focusBoundControl(); diff --git a/overlord/src/app/entries/entries.component.html b/overlord/src/app/entries/entries.component.html index a23bf759..80534ef9 100644 --- a/overlord/src/app/entries/entries.component.html +++ b/overlord/src/app/entries/entries.component.html @@ -1,6 +1,6 @@

Entries

- +
Create / Last Edit Date From @@ -20,9 +20,7 @@ - +
diff --git a/overlord/src/app/entries/entries.component.ts b/overlord/src/app/entries/entries.component.ts index e71f46eb..379b3124 100644 --- a/overlord/src/app/entries/entries.component.ts +++ b/overlord/src/app/entries/entries.component.ts @@ -1,15 +1,6 @@ /* eslint-disable @angular-eslint/no-input-rename */ import { CurrencyPipe } from '@angular/common'; -import { - Component, - HostListener, - inject, - input, - computed, - booleanAttribute, - afterNextRender, - linkedSignal, -} from '@angular/core'; +import { Component, inject, input, computed, booleanAttribute, afterNextRender, linkedSignal } from '@angular/core'; export interface EntriesFormData { startDate: Date; finishDate: Date; @@ -40,6 +31,9 @@ import { Report } from './report'; selector: 'app-entries', templateUrl: './entries.component.html', styleUrls: ['./entries.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -120,7 +114,6 @@ export class EntriesComponent { displayedColumns = ['date', 'voucherType', 'narration', 'debitNames', 'creditNames', 'amount', 'user']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/incentive/incentive.component.ts b/overlord/src/app/incentive/incentive.component.ts index 392b41e0..7acdf2f9 100644 --- a/overlord/src/app/incentive/incentive.component.ts +++ b/overlord/src/app/incentive/incentive.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { signal, Component, HostListener, inject, input, computed, linkedSignal } from '@angular/core'; +import { signal, Component, inject, input, computed, linkedSignal } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -23,7 +23,7 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog. import { LocalTimePipe } from '../shared/local-time.pipe'; export interface IncentiveFormData { - date: moment.Moment; + date: Date; incentives: { points: number }[]; } @@ -31,6 +31,9 @@ export interface IncentiveFormData { selector: 'app-incentive', templateUrl: './incentive.component.html', styleUrls: ['./incentive.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -61,7 +64,7 @@ export class IncentiveComponent { model = linkedSignal({ source: this.voucher, computation: (v): IncentiveFormData => ({ - date: moment(v.date, 'DD-MMM-YYYY'), + date: moment(v.date, 'DD-MMM-YYYY').toDate(), incentives: v.incentives.map((x) => ({ points: x.points })), }), }); @@ -75,7 +78,6 @@ export class IncentiveComponent { displayedColumns = ['name', 'designation', 'department', 'daysWorked', 'points', 'amount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); @@ -159,7 +161,7 @@ export class IncentiveComponent { getVoucher(): Voucher { const formModel = this.model(); const v = this.voucher(); - v.date = formModel.date.format('DD-MMM-YYYY'); + v.date = moment(formModel.date).format('DD-MMM-YYYY'); const array = formModel.incentives; v.incentives.forEach((item: Incentive, index: number) => { diff --git a/overlord/src/app/issue/issue.component.ts b/overlord/src/app/issue/issue.component.ts index 80477595..f472eebc 100644 --- a/overlord/src/app/issue/issue.component.ts +++ b/overlord/src/app/issue/issue.component.ts @@ -1,15 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { - afterNextRender, - Component, - computed, - effect, - HostListener, - inject, - input, - linkedSignal, - signal, -} from '@angular/core'; +import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -76,6 +66,12 @@ export interface IssueFormModel { selector: 'app-issue', templateUrl: './issue.component.html', styleUrls: ['./issue.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f3)': 'focusBatchHost($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ FormField, FormRoot, @@ -174,19 +170,16 @@ export class IssueComponent { issueGridResource = this.issueGridSer.issueGrid(this.balanceDate); gridData = computed(() => this.issueGridResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f3', ['$event']) focusBatchHost(event: Event) { event.preventDefault(); this.form.addRow.batch().focusBoundControl(); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -194,7 +187,6 @@ export class IssueComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { diff --git a/overlord/src/app/journal/journal.component.ts b/overlord/src/app/journal/journal.component.ts index fe557354..7ea73286 100644 --- a/overlord/src/app/journal/journal.component.ts +++ b/overlord/src/app/journal/journal.component.ts @@ -1,6 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, input, computed, signal, linkedSignal, afterNextRender } from '@angular/core'; +import { Component, inject, input, computed, signal, linkedSignal, afterNextRender } from '@angular/core'; import { FormField, FormRoot, form } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -72,6 +72,15 @@ export interface JournalFormModel { selector: 'app-journal', templateUrl: './journal.component.html', styleUrls: ['./journal.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f3)': 'focusAccountHost($event)', + '(window:keydown.f5)': 'redirectToPayment($event)', + '(window:keydown.f6)': 'redirectToReciept($event)', + '(window:keydown.f7)': 'redirectToJournal($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ MatIconModule, MatFormFieldModule, @@ -112,19 +121,16 @@ export class JournalComponent { itemResource = this.ser.getVoucher(this.id, this.type, signal(null), this.d); item = linkedSignal(() => this.itemResource.value() ?? new Voucher()); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f3', ['$event']) focusAccountHost(event: Event) { event.preventDefault(); this.form.addRow.account().focusBoundControl(); } - @HostListener('window:keydown.f5', ['$event']) redirectToPayment(event: Event) { event.preventDefault(); this.router.navigate(['payment'], { @@ -135,7 +141,6 @@ export class JournalComponent { }); } - @HostListener('window:keydown.f6', ['$event']) redirectToReciept(event: Event) { event.preventDefault(); this.router.navigate(['receipt'], { @@ -146,12 +151,10 @@ export class JournalComponent { }); } - @HostListener('window:keydown.f7', ['$event']) redirectToJournal(event: Event) { event.preventDefault(); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -159,7 +162,6 @@ export class JournalComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted() && this.auth.allowed('post-vouchers')) { @@ -233,9 +235,9 @@ export class JournalComponent { }); } - dateChanged(newDate: moment.Moment) { + dateChanged(newDate: Date) { this.router.navigate([], { - queryParams: { d: newDate.format('DD-MMM-YYYY') }, + queryParams: { d: moment(newDate).format('DD-MMM-YYYY') }, replaceUrl: true, queryParamsHandling: 'merge', }); diff --git a/overlord/src/app/ledger/ledger.component.html b/overlord/src/app/ledger/ledger.component.html index a8276a5d..f124df60 100644 --- a/overlord/src/app/ledger/ledger.component.html +++ b/overlord/src/app/ledger/ledger.component.html @@ -7,7 +7,7 @@ } - +
Start Date @@ -33,18 +33,13 @@ [formField]="form.account" autocomplete="off" /> - + @for (account of accounts(); track account) { {{ account.name }} } - +
@@ -55,7 +50,7 @@ } - diff --git a/overlord/src/app/ledger/ledger.component.ts b/overlord/src/app/ledger/ledger.component.ts index ebcc95eb..982f0707 100644 --- a/overlord/src/app/ledger/ledger.component.ts +++ b/overlord/src/app/ledger/ledger.component.ts @@ -1,8 +1,8 @@ import { SelectionModel } from '@angular/cdk/collections'; import { CurrencyPipe } from '@angular/common'; -import { Component, inject, input, linkedSignal, computed, signal, afterNextRender, HostListener } from '@angular/core'; +import { Component, inject, input, linkedSignal, computed, signal, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; -import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipsModule } from '@angular/material/chips'; @@ -27,8 +27,8 @@ import { ToCsvService } from '../shared/to-csv.service'; import { TagDialogComponent } from '../tag-dialog/tag-dialog.component'; import { TagService } from '../tag/tag.service'; export interface LedgerFormData { - startDate: moment.Moment; - finishDate: moment.Moment; + startDate: Date; + finishDate: Date; account: string | Account | null; tags: string[]; } @@ -42,6 +42,9 @@ import { LedgerService } from './ledger.service'; selector: 'app-ledger', templateUrl: './ledger.component.html', styleUrls: ['./ledger.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ MatCheckboxModule, MatChipsModule, @@ -144,9 +147,9 @@ export class LedgerComponent { model = linkedSignal({ source: this.info, computation: (info: Ledger): LedgerFormData => ({ - startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()), - finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()), - account: info.account?.name ?? '', + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), + account: info.account ?? null, tags: this.tags(), }), }); @@ -165,7 +168,6 @@ export class LedgerComponent { private accountsResource = this.accountSer.autocomplete(this.accountSearch); accounts = computed(() => this.accountsResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); @@ -177,14 +179,10 @@ export class LedgerComponent { }); } - displayFn(account?: Account | string): string { + displayFn(account: Account | string | null): string { return !account ? '' : typeof account === 'string' ? account : account.name; } - selected(event: MatAutocompleteSelectedEvent): void { - this.info().account = event.option.value as Account; - } - selectRow(id: string): void { this.selectedRowId = id; } @@ -247,9 +245,11 @@ export class LedgerComponent { getInfo(): Ledger { const formModel = this.model(); + const accVal = formModel.account; + const account = accVal && typeof accVal !== 'string' ? accVal : undefined; return new Ledger({ - account: this.info().account, + account, startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), }); diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html index 080886cb..4c33411a 100644 --- a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.html @@ -7,7 +7,7 @@ } - +
Date @@ -22,7 +22,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts index a756c6cc..04cba1e7 100644 --- a/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts +++ b/overlord/src/app/mozimo-daily-register/mozimo-daily-register.component.ts @@ -25,7 +25,7 @@ import { MozimoDailyRegisterItem } from './mozimo-daily-register-item'; import { MozimoDailyRegisterService } from './mozimo-daily-register.service'; export interface MozimoDailyRegisterFormData { - date: moment.Moment; + date: Date; items: { received: number; sale: number; @@ -79,7 +79,7 @@ export class MozimoDailyRegisterComponent { model = linkedSignal({ source: this.info, computation: (v): MozimoDailyRegisterFormData => ({ - date: moment(v.date, 'DD-MMM-YYYY'), + date: moment(v.date, 'DD-MMM-YYYY').toDate(), items: v.body.map((x) => ({ received: x.received, sale: x.sale, @@ -129,7 +129,7 @@ export class MozimoDailyRegisterComponent { getMozimoDailyRegister(): MozimoDailyRegister { const formModel = this.model(); const info = this.info(); - info.date = formModel.date.format('DD-MMM-YYYY'); + info.date = moment(formModel.date).format('DD-MMM-YYYY'); const array = formModel.items; if (array) { diff --git a/overlord/src/app/mozimo-product-register/mozimo-product-register.component.html b/overlord/src/app/mozimo-product-register/mozimo-product-register.component.html index 2e49a27f..1e4c23e4 100644 --- a/overlord/src/app/mozimo-product-register/mozimo-product-register.component.html +++ b/overlord/src/app/mozimo-product-register/mozimo-product-register.component.html @@ -7,7 +7,7 @@ } - +
Start Date @@ -44,7 +44,7 @@ } - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/mozimo-product-register/mozimo-product-register.component.ts b/overlord/src/app/mozimo-product-register/mozimo-product-register.component.ts index 20aa3a70..307ef341 100644 --- a/overlord/src/app/mozimo-product-register/mozimo-product-register.component.ts +++ b/overlord/src/app/mozimo-product-register/mozimo-product-register.component.ts @@ -27,8 +27,8 @@ import { MozimoProductRegisterItem } from './mozimo-product-register-item'; import { MozimoProductRegisterService } from './mozimo-product-register.service'; export interface MozimoProductRegisterFormData { - startDate: moment.Moment; - finishDate: moment.Moment; + startDate: Date; + finishDate: Date; product: Product | string | null; items: { received: number; @@ -84,11 +84,11 @@ export class MozimoProductRegisterComponent { model = linkedSignal({ source: this.info, - computation: (v): MozimoProductRegisterFormData => ({ - startDate: moment(v.startDate, 'DD-MMM-YYYY'), - finishDate: moment(v.finishDate, 'DD-MMM-YYYY'), - product: v.product ?? '', - items: v.body.map((x) => ({ + computation: (info): MozimoProductRegisterFormData => ({ + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), + product: info.product ?? '', + items: info.body.map((x) => ({ received: x.received, sale: x.sale, nc: x.nc, @@ -160,8 +160,8 @@ export class MozimoProductRegisterComponent { getMozimoProductRegister(): MozimoProductRegister { const formModel = this.model(); const info = this.info(); - info.startDate = formModel.startDate.format('DD-MMM-YYYY'); - info.finishDate = formModel.finishDate.format('DD-MMM-YYYY'); + info.startDate = moment(formModel.startDate).format('DD-MMM-YYYY'); + info.finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY'); const array = formModel.items; if (array) { @@ -183,8 +183,8 @@ export class MozimoProductRegisterComponent { return new MozimoProductRegister({ product: formModel.product as Product, - startDate: formModel.startDate.format('DD-MMM-YYYY'), - finishDate: formModel.finishDate.format('DD-MMM-YYYY'), + startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), + finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), }); } diff --git a/overlord/src/app/net-transactions/net-transactions.component.html b/overlord/src/app/net-transactions/net-transactions.component.html index 6431d7b5..c9793f1a 100644 --- a/overlord/src/app/net-transactions/net-transactions.component.html +++ b/overlord/src/app/net-transactions/net-transactions.component.html @@ -1,6 +1,6 @@

Net Transactions

- +
Start Date @@ -14,7 +14,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/net-transactions/net-transactions.component.ts b/overlord/src/app/net-transactions/net-transactions.component.ts index c0e314d0..b2430777 100644 --- a/overlord/src/app/net-transactions/net-transactions.component.ts +++ b/overlord/src/app/net-transactions/net-transactions.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, input, linkedSignal, computed, afterNextRender, signal } from '@angular/core'; +import { Component, inject, input, linkedSignal, computed, afterNextRender, signal } from '@angular/core'; export interface NetTransactionsFormData { startDate: Date; finishDate: Date; @@ -25,6 +25,9 @@ import { NetTransactionsService } from './net-transactions.service'; selector: 'app-net-transactions', templateUrl: './net-transactions.component.html', styleUrls: ['./net-transactions.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -103,7 +106,6 @@ export class NetTransactionsComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['type', 'name', 'debit', 'credit']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); // TODO: Also select the text diff --git a/overlord/src/app/payment/payment.component.ts b/overlord/src/app/payment/payment.component.ts index 8792bf47..bdb53a6e 100644 --- a/overlord/src/app/payment/payment.component.ts +++ b/overlord/src/app/payment/payment.component.ts @@ -1,16 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CurrencyPipe } from '@angular/common'; -import { - Component, - HostListener, - inject, - computed, - effect, - signal, - input, - linkedSignal, - afterNextRender, -} from '@angular/core'; +import { Component, inject, computed, effect, signal, input, linkedSignal, afterNextRender } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -83,6 +73,14 @@ export interface PaymentFormModel { selector: 'app-payment', templateUrl: './payment.component.html', styleUrls: ['./payment.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f5)': 'redirectToPayment($event)', + '(window:keydown.f6)': 'redirectToReciept($event)', + '(window:keydown.f7)': 'redirectToJournal($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ FormField, FormRoot, @@ -199,18 +197,15 @@ export class PaymentComponent { paymentAccountsResource = this.accountSer.paymentAutocomplete(signal('')); paymentAccounts = computed(() => this.paymentAccountsResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f5', ['$event']) redirectToPayment(event: Event) { event.preventDefault(); } - @HostListener('window:keydown.f6', ['$event']) redirectToReciept(event: Event) { event.preventDefault(); this.router.navigate(['receipt'], { @@ -222,7 +217,6 @@ export class PaymentComponent { }); } - @HostListener('window:keydown.f7', ['$event']) redirectToJournal(event: Event) { event.preventDefault(); this.router.navigate(['journal'], { @@ -233,7 +227,6 @@ export class PaymentComponent { }); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -241,7 +234,6 @@ export class PaymentComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { diff --git a/overlord/src/app/period/period-detail/period-detail.component.ts b/overlord/src/app/period/period-detail/period-detail.component.ts index f0ce7485..8c62eebd 100644 --- a/overlord/src/app/period/period-detail/period-detail.component.ts +++ b/overlord/src/app/period/period-detail/period-detail.component.ts @@ -1,7 +1,7 @@ -import { Component, HostListener, inject, linkedSignal, input, computed } from '@angular/core'; +import { Component, inject, linkedSignal, input, computed } from '@angular/core'; export interface PeriodFormData { - validFrom: moment.Moment; - validTill: moment.Moment; + validFrom: Date; + validTill: Date; } import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; @@ -23,6 +23,9 @@ import { PeriodService } from '../period.service'; selector: 'app-period-detail', templateUrl: './period-detail.component.html', styleUrls: ['./period-detail.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, FormRoot, @@ -49,14 +52,13 @@ export class PeriodDetailComponent { model = linkedSignal({ source: this.item, computation: (item): PeriodFormData => ({ - validFrom: item.validFrom ? moment(item.validFrom, 'DD-MMM-YYYY') : moment(new Date()), - validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY') : moment(new Date()), + validFrom: item.validFrom ? moment(item.validFrom, 'DD-MMM-YYYY').toDate() : new Date(), + validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY').toDate() : new Date(), }), }); form = createForm(this.model); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/product-ledger/product-ledger.component.html b/overlord/src/app/product-ledger/product-ledger.component.html index b09b149f..514d6f57 100644 --- a/overlord/src/app/product-ledger/product-ledger.component.html +++ b/overlord/src/app/product-ledger/product-ledger.component.html @@ -7,7 +7,7 @@ } -
+
Start Date @@ -44,7 +44,7 @@ } - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/product-ledger/product-ledger.component.ts b/overlord/src/app/product-ledger/product-ledger.component.ts index 37191a6b..e7207b57 100644 --- a/overlord/src/app/product-ledger/product-ledger.component.ts +++ b/overlord/src/app/product-ledger/product-ledger.component.ts @@ -1,16 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { - Component, - HostListener, - inject, - computed, - signal, - linkedSignal, - ResourceRef, - Signal, - input, - afterNextRender, -} from '@angular/core'; +import { Component, inject, computed, signal, linkedSignal, input, afterNextRender } from '@angular/core'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -30,9 +19,9 @@ import { ProductSku } from '../core/product-sku'; import { ProductService } from '../product/product.service'; import { ToCsvService } from '../shared/to-csv.service'; export interface ProductLedgerFormData { - startDate: moment.Moment; - finishDate: moment.Moment; - product: string; + startDate: Date; + finishDate: Date; + product: Product | string | null; } import { ErrorStateComponent } from '../shared/error-state/error-state.component'; import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component'; @@ -43,6 +32,9 @@ import { ProductLedgerService } from './product-ledger.service'; selector: 'app-product-ledger', templateUrl: './product-ledger.component.html', styleUrls: ['./product-ledger.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ MatIconModule, @@ -60,7 +52,6 @@ import { ProductLedgerService } from './product-ledger.service'; MatPaginatorModule, DecimalPipe, CurrencyPipe, - SkeletonLoaderComponent, ErrorStateComponent, ], @@ -161,9 +152,9 @@ export class ProductLedgerComponent { model = linkedSignal({ source: this.info, computation: (info: ProductLedger): ProductLedgerFormData => ({ - startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()), - finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()), - product: info.product?.name ?? '', + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), + product: info.product ?? null, }), }); @@ -185,11 +176,14 @@ export class ProductLedgerComponent { 'runningAmount', ]; - productSearch!: Signal; - productsResource!: ResourceRef; - products!: Signal; + productSearch = computed(() => { + const v = this.model().product; + return typeof v === 'string' ? v : null; + }); + + productsResource = this.productSer.autocompleteProduct(this.productSearch, signal(null)); + products = computed(() => this.productsResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); @@ -199,21 +193,15 @@ export class ProductLedgerComponent { afterNextRender(() => { this.form().focusBoundControl(); }); - this.productSearch = computed(() => { - const v = this.model().product; - return typeof v === 'string' ? v : null; - }); - this.productsResource = this.productSer.autocompleteProduct(this.productSearch, signal(null)); - this.products = computed(() => this.productsResource.value() ?? []); } - displayFn(product?: Product | string): string { + displayFn(product?: Product | ProductSku | string | null): string { return !product ? '' : typeof product === 'string' ? product : product.name; } selected(event: MatAutocompleteSelectedEvent): void { const p = event.option.value; - this.model.update((m) => ({ ...m, product: p.name })); + this.model.update((m) => ({ ...m, product: p })); } selectRow(id: string): void { @@ -234,9 +222,10 @@ export class ProductLedgerComponent { getInfo(): ProductLedger { const formModel = this.model(); - + const prodVal = formModel.product; + const product = prodVal && typeof prodVal !== 'string' ? prodVal : undefined; return new ProductLedger({ - product: this.info().product, + product, startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), }); diff --git a/overlord/src/app/profit-loss/profit-loss.component.html b/overlord/src/app/profit-loss/profit-loss.component.html index b520bb78..d80bf143 100644 --- a/overlord/src/app/profit-loss/profit-loss.component.html +++ b/overlord/src/app/profit-loss/profit-loss.component.html @@ -1,6 +1,6 @@

Profit & Loss

-
+
Start Date @@ -14,7 +14,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/profit-loss/profit-loss.component.ts b/overlord/src/app/profit-loss/profit-loss.component.ts index a61f4027..bffac886 100644 --- a/overlord/src/app/profit-loss/profit-loss.component.ts +++ b/overlord/src/app/profit-loss/profit-loss.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, linkedSignal, computed, signal, input, afterNextRender } from '@angular/core'; +import { Component, inject, linkedSignal, computed, signal, input, afterNextRender } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -17,14 +17,17 @@ import { ProfitLoss } from './profit-loss'; import { ProfitLossService } from './profit-loss.service'; export interface ProfitLossFormData { - startDate: moment.Moment; - finishDate: moment.Moment; + startDate: Date; + finishDate: Date; } @Component({ selector: 'app-profit-loss', templateUrl: './profit-loss.component.html', styleUrls: ['./profit-loss.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, @@ -57,8 +60,8 @@ export class ProfitLossComponent { formModel = linkedSignal({ source: this.info, computation: (info: ProfitLoss): ProfitLossFormData => ({ - startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()), - finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()), + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -103,7 +106,6 @@ export class ProfitLossComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['group', 'name', 'amount', 'total']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/purchase-entries/purchase-entries.component.html b/overlord/src/app/purchase-entries/purchase-entries.component.html index 8d87b03b..b4cc7364 100644 --- a/overlord/src/app/purchase-entries/purchase-entries.component.html +++ b/overlord/src/app/purchase-entries/purchase-entries.component.html @@ -1,6 +1,6 @@

Purchase Entries

-
+
Start Date @@ -14,7 +14,7 @@ - +
@if (resource.isLoading()) { diff --git a/overlord/src/app/purchase-entries/purchase-entries.component.ts b/overlord/src/app/purchase-entries/purchase-entries.component.ts index a120f8b2..cb3f2de2 100644 --- a/overlord/src/app/purchase-entries/purchase-entries.component.ts +++ b/overlord/src/app/purchase-entries/purchase-entries.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; -import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core'; +import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -17,14 +17,17 @@ import { PurchaseEntries } from './purchase-entries'; import { PurchaseEntriesService } from './purchase-entries.service'; export interface PurchaseEntriesFormData { - startDate: moment.Moment | null | string; - finishDate: moment.Moment | null | string; + startDate: Date; + finishDate: Date; } @Component({ selector: 'app-purchase-entries', templateUrl: './purchase-entries.component.html', styleUrls: ['./purchase-entries.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, @@ -101,10 +104,10 @@ export class PurchaseEntriesComponent { }); formModel = linkedSignal({ - source: () => null, - computation: (): PurchaseEntriesFormData => ({ - startDate: moment(new Date()), - finishDate: moment(new Date()), + source: () => this.info(), + computation: (info: PurchaseEntries): PurchaseEntriesFormData => ({ + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -114,7 +117,6 @@ export class PurchaseEntriesComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['date', 'supplier', 'product', 'quantity', 'rate', 'tax', 'discount', 'amount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/purchase-return/purchase-return.component.ts b/overlord/src/app/purchase-return/purchase-return.component.ts index 6f89ec34..eecbe7dd 100644 --- a/overlord/src/app/purchase-return/purchase-return.component.ts +++ b/overlord/src/app/purchase-return/purchase-return.component.ts @@ -1,16 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; -import { - afterNextRender, - Component, - computed, - effect, - HostListener, - inject, - input, - linkedSignal, - signal, -} from '@angular/core'; +import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -86,6 +76,13 @@ export interface PurchaseReturnFormModel { selector: 'app-purchase-return', templateUrl: './purchase-return.component.html', styleUrls: ['./purchase-return.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f3)': 'focusAccountHost($event)', + '(window:keydown.f4)': 'focusBatchHost($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ FormField, FormRoot, @@ -208,25 +205,21 @@ export class PurchaseReturnComponent { batchesResource = this.batchSer.autocomplete(this.balanceDate, this.batchSearch); batches = computed(() => this.batchesResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f3', ['$event']) focusAccountHost(event: Event) { event.preventDefault(); this.form.account().focusBoundControl(); } - @HostListener('window:keydown.f4', ['$event']) focusBatchHost(event: Event) { event.preventDefault(); this.form.addRow.batch().focusBoundControl(); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -234,7 +227,6 @@ export class PurchaseReturnComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { diff --git a/overlord/src/app/purchase/purchase.component.ts b/overlord/src/app/purchase/purchase.component.ts index 031fed16..a9f1173e 100644 --- a/overlord/src/app/purchase/purchase.component.ts +++ b/overlord/src/app/purchase/purchase.component.ts @@ -1,16 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; -import { - afterNextRender, - Component, - computed, - effect, - HostListener, - inject, - input, - linkedSignal, - signal, -} from '@angular/core'; +import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -91,6 +81,13 @@ export interface PurchaseFormModel { selector: 'app-purchase', templateUrl: './purchase.component.html', styleUrls: ['./purchase.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f3)': 'focusAccountHost($event)', + '(window:keydown.f4)': 'focusProductHost($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ FormField, FormRoot, @@ -222,25 +219,21 @@ export class PurchaseComponent { products = computed(() => this.productsResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f3', ['$event']) focusAccountHost(event: Event) { event.preventDefault(); this.form.account().focusBoundControl(); } - @HostListener('window:keydown.f4', ['$event']) focusProductHost(event: Event) { event.preventDefault(); this.form.addRow.product().focusBoundControl(); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -248,7 +241,6 @@ export class PurchaseComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { diff --git a/overlord/src/app/purchases/purchases.component.html b/overlord/src/app/purchases/purchases.component.html index fa0e29d3..31d7b0fc 100644 --- a/overlord/src/app/purchases/purchases.component.html +++ b/overlord/src/app/purchases/purchases.component.html @@ -1,6 +1,6 @@

Purchases

-
+
Start Date @@ -14,7 +14,7 @@ - +
@if (resource.isLoading()) { diff --git a/overlord/src/app/purchases/purchases.component.ts b/overlord/src/app/purchases/purchases.component.ts index ae341fae..a4a4f6aa 100644 --- a/overlord/src/app/purchases/purchases.component.ts +++ b/overlord/src/app/purchases/purchases.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe } from '@angular/common'; -import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core'; +import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -18,14 +18,17 @@ import { PurchasesItem } from './purchases-item'; import { PurchasesService } from './purchases.service'; export interface PurchasesFormData { - startDate: moment.Moment | null | string; - finishDate: moment.Moment | null | string; + startDate: Date; + finishDate: Date; } @Component({ selector: 'app-purchases', templateUrl: './purchases.component.html', styleUrls: ['./purchases.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, @@ -93,10 +96,10 @@ export class PurchasesComponent { }); formModel = linkedSignal({ - source: () => null, - computation: (): PurchasesFormData => ({ - startDate: moment(new Date()), - finishDate: moment(new Date()), + source: () => this.info(), + computation: (info: Purchases): PurchasesFormData => ({ + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -106,7 +109,6 @@ export class PurchasesComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['product', 'quantity', 'rate', 'amount']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/raw-material-cost/raw-material-cost.component.html b/overlord/src/app/raw-material-cost/raw-material-cost.component.html index deb33183..8b52416d 100644 --- a/overlord/src/app/raw-material-cost/raw-material-cost.component.html +++ b/overlord/src/app/raw-material-cost/raw-material-cost.component.html @@ -7,7 +7,7 @@ } -
+
Start Date @@ -21,7 +21,7 @@ - +
@if (infoResource.isLoading()) { diff --git a/overlord/src/app/raw-material-cost/raw-material-cost.component.ts b/overlord/src/app/raw-material-cost/raw-material-cost.component.ts index 86445956..84abac0a 100644 --- a/overlord/src/app/raw-material-cost/raw-material-cost.component.ts +++ b/overlord/src/app/raw-material-cost/raw-material-cost.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; -import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core'; +import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -19,14 +19,17 @@ import { RawMaterialCost } from './raw-material-cost'; import { RawMaterialCostService } from './raw-material-cost.service'; export interface RawMaterialCostFormData { - startDate: Date | null; - finishDate: Date | null; + startDate: Date; + finishDate: Date; } @Component({ selector: 'app-raw-material-cost', templateUrl: './raw-material-cost.component.html', styleUrls: ['./raw-material-cost.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ MatIconModule, @@ -104,10 +107,10 @@ export class RawMaterialCostComponent { displayedColumns = computed(() => (this.info().id ? this.columnsId : this.columnsNoId)); formModel = linkedSignal({ - source: () => null, - computation: (): RawMaterialCostFormData => ({ - startDate: new Date(), - finishDate: new Date(), + source: () => this.info(), + computation: (info): RawMaterialCostFormData => ({ + startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(), + finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(), }), }); @@ -122,7 +125,6 @@ export class RawMaterialCostComponent { columnsNoId = ['name', 'issue', 'sale', 'rmc']; columnsId = ['name', 'group', 'quantity', 'net', 'gross']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/receipt/receipt.component.ts b/overlord/src/app/receipt/receipt.component.ts index 3159a14e..3848e072 100644 --- a/overlord/src/app/receipt/receipt.component.ts +++ b/overlord/src/app/receipt/receipt.component.ts @@ -1,16 +1,6 @@ import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { CurrencyPipe } from '@angular/common'; -import { - afterNextRender, - Component, - computed, - effect, - HostListener, - inject, - input, - linkedSignal, - signal, -} from '@angular/core'; +import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -83,6 +73,15 @@ export interface ReceiptFormModel { selector: 'app-receipt', templateUrl: './receipt.component.html', styleUrls: ['./receipt.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + '(window:keydown.f3)': 'focusAccountHost($event)', + '(window:keydown.f5)': 'redirectToPayment($event)', + '(window:keydown.f6)': 'redirectToReciept($event)', + '(window:keydown.f7)': 'redirectToJournal($event)', + '(window:keydown.control.s)': 'saveListner($event)', + '(window:keydown.control.p)': 'postListner($event)', + }, imports: [ FormField, FormRoot, @@ -199,19 +198,16 @@ export class ReceiptComponent { receiptAccountsResource = this.accountSer.receiptAutocomplete(signal('')); receiptAccounts = computed(() => this.receiptAccountsResource.value() ?? []); - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); } - @HostListener('window:keydown.f3', ['$event']) focusAccountHost(event: Event) { event.preventDefault(); this.form.addRow.account().focusBoundControl(); } - @HostListener('window:keydown.f5', ['$event']) redirectToPayment(event: Event) { event.preventDefault(); this.router.navigate(['payment'], { @@ -223,12 +219,10 @@ export class ReceiptComponent { }); } - @HostListener('window:keydown.f6', ['$event']) redirectToReciept(event: Event) { event.preventDefault(); } - @HostListener('window:keydown.f7', ['$event']) redirectToJournal(event: Event) { event.preventDefault(); this.router.navigate(['journal'], { @@ -239,7 +233,6 @@ export class ReceiptComponent { }); } - @HostListener('window:keydown.control.s', ['$event']) saveListner(event: Event) { event.preventDefault(); if (this.canSave()) { @@ -247,7 +240,6 @@ export class ReceiptComponent { } } - @HostListener('window:keydown.control.p', ['$event']) postListner(event: Event) { event.preventDefault(); if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { diff --git a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.css b/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.css deleted file mode 100644 index e69de29b..00000000 diff --git a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.html b/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.html deleted file mode 100644 index 0daae4a4..00000000 --- a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.html +++ /dev/null @@ -1,35 +0,0 @@ -

RecipeTemplate

- -@if (itemResource.isLoading()) { - - -} @else if (itemResource.error()) { - -} @else { -
-
- - Name - - - - Date - - - - - Is Selected? -
-
- - Text - - -
-
- -
- - -
-} diff --git a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.spec.ts b/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.spec.ts deleted file mode 100644 index d34d5a60..00000000 --- a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { RecipeTemplateDetailComponent } from './recipe-template-detail.component'; - -describe('RecipeTemplateDetailComponent', () => { - let component: RecipeTemplateDetailComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [RecipeTemplateDetailComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(RecipeTemplateDetailComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.ts b/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.ts deleted file mode 100644 index 011b35e7..00000000 --- a/overlord/src/app/recipe-template/recipe-template-detail/recipe-template-detail.component.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core'; -import { FormField, FormRoot, form as createForm } from '@angular/forms/signals'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDatepickerModule } from '@angular/material/datepicker'; -import { MatDialog } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { Router } from '@angular/router'; -import moment from 'moment'; - -import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; -import { ErrorStateComponent } from '../../shared/error-state/error-state.component'; -import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component'; -import { RecipeTemplate } from '../recipe-template'; -import { RecipeTemplateService } from '../recipe-template.service'; - -export interface RecipeTemplateDetailFormData { - name: string; - date: moment.Moment | null | string; - text: string; - selected: boolean; -} - -@Component({ - selector: 'app-recipe-template-detail', - templateUrl: './recipe-template-detail.component.html', - styleUrls: ['./recipe-template-detail.component.css'], - imports: [ - FormField, - FormRoot, - MatFormFieldModule, - MatInputModule, - MatDatepickerModule, - MatCheckboxModule, - MatButtonModule, - - SkeletonLoaderComponent, - ErrorStateComponent, - ], -}) -export class RecipeTemplateDetailComponent { - id = input(null, { transform: (v: string | null | undefined) => v ?? null }); - - private router = inject(Router); - private dialog = inject(MatDialog); - private snackBar = inject(MatSnackBar); - private ser = inject(RecipeTemplateService); - - itemResource = this.ser.get(this.id); - - item = computed(() => this.itemResource.value() ?? new RecipeTemplate()); - - formModel = linkedSignal({ - source: this.item, - computation: (itemVal): RecipeTemplateDetailFormData => ({ - name: itemVal.name || '', - date: itemVal.date ? moment(itemVal.date, 'DD-MMM-YYYY') : moment(new Date()), - text: itemVal.text || '', - selected: itemVal.selected || false, - }), - }); - - form = createForm(this.formModel, () => {}); - - constructor() { - afterNextRender(() => { - this.form().focusBoundControl(); - }); - } - - save() { - this.ser.saveOrUpdate(this.getItem()).subscribe({ - next: () => { - this.snackBar.open('', 'Success'); - this.router.navigateByUrl('/recipe-templates'); - }, - error: (error) => { - this.snackBar.open(error as string, 'Danger'); - }, - }); - } - - delete() { - this.ser.delete(this.item().id).subscribe({ - next: () => { - this.snackBar.open('', 'Success'); - this.router.navigateByUrl('/recipe-templates'); - }, - error: (error) => { - this.snackBar.open(error as string, 'Danger'); - }, - }); - } - - confirmDelete(): void { - const dialogRef = this.dialog.open(ConfirmDialogComponent, { - width: '250px', - data: { title: 'Delete Recipe Template?', content: 'Are you sure? This cannot be undone.' }, - }); - - dialogRef.afterClosed().subscribe((result: boolean) => { - if (result) { - this.delete(); - } - }); - } - - getItem(): RecipeTemplate { - const formModel = this.formModel(); - const item = this.item(); - item.name = formModel.name ?? ''; - item.date = moment(formModel.date).format('DD-MMM-YYYY'); - item.text = formModel.text ?? ''; - item.selected = formModel.selected ?? false; - return item; - } -} diff --git a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.css b/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.css deleted file mode 100644 index e69de29b..00000000 diff --git a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.html b/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.html deleted file mode 100644 index d4fc484b..00000000 --- a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.html +++ /dev/null @@ -1,47 +0,0 @@ -

- Recipe Templates - - add_box - Add - -

- -@if (listResource.isLoading()) { - -} @else if (listResource.error()) { - -} @else { - - - - Name - {{ row.name }} - - - - - Is Selected? - {{ row.selected }} - - - - - Date - {{ row.date }} - - - - - - - - -} diff --git a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.spec.ts b/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.spec.ts deleted file mode 100644 index 5bf65e7d..00000000 --- a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; - -import { RecipeTemplateListComponent } from './recipe-template-list.component'; - -describe('RecipeTemplateListComponent', () => { - let component: RecipeTemplateListComponent; - let fixture: ComponentFixture; - - beforeEach(fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [RouterTestingModule, RecipeTemplateListComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(RecipeTemplateListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - })); - - it('should compile', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.ts b/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.ts deleted file mode 100644 index 20975c53..00000000 --- a/overlord/src/app/recipe-template/recipe-template-list/recipe-template-list.component.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Component, computed, inject, signal } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; -import { MatSortModule, Sort } from '@angular/material/sort'; -import { MatTableModule } from '@angular/material/table'; -import { RouterModule } from '@angular/router'; - -import { ErrorStateComponent } from '../../shared/error-state/error-state.component'; -import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component'; -import { RecipeTemplateService } from '../recipe-template.service'; - -@Component({ - selector: 'app-recipe-template-list', - templateUrl: './recipe-template-list.component.html', - styleUrls: ['./recipe-template-list.component.css'], - imports: [ - RouterModule, - MatIconModule, - MatTableModule, - MatSortModule, - MatPaginatorModule, - MatButtonModule, - SkeletonLoaderComponent, - ErrorStateComponent, - ], -}) -export class RecipeTemplateListComponent { - private ser = inject(RecipeTemplateService); - - pageSize = signal(50); - pageIndex = signal(0); - sortActive = signal(''); - sortDirection = signal(''); - listResource = this.ser.list(); - list = computed(() => this.listResource.value() ?? []); - sortedList = computed(() => { - const data = this.list(); - const active = this.sortActive(); - const direction = this.sortDirection(); - - if (!active || direction === '') { - return data; - } - - return [...data].sort((a, b) => { - const isAsc = direction === 'asc'; - switch (active) { - case 'name': - return compare(a.name, b.name, isAsc); - case 'selected': - return compare(a.selected, b.selected, isAsc); - case 'date': - return compare(a.date, b.date, isAsc); - default: - return 0; - } - }); - }); - - dataSource = computed(() => { - const data = this.sortedList(); - const pageIndex = this.pageIndex(); - const pageSize = this.pageSize(); - return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); - }); - - /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ - displayedColumns = ['name', 'selected', 'date']; - handlePageEvent(e: PageEvent) { - this.pageSize.set(e.pageSize); - this.pageIndex.set(e.pageIndex); - } - - sortData(sort: Sort) { - this.sortActive.set(sort.active); - this.sortDirection.set(sort.direction); - } -} -const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) => - (a < b ? -1 : 1) * (isAsc ? 1 : -1); diff --git a/overlord/src/app/recipe-template/recipe-template.routes.ts b/overlord/src/app/recipe-template/recipe-template.routes.ts deleted file mode 100644 index aa5f3445..00000000 --- a/overlord/src/app/recipe-template/recipe-template.routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Routes } from '@angular/router'; - -import { authGuard } from '../auth/auth-guard.service'; -import { RecipeTemplateDetailComponent } from './recipe-template-detail/recipe-template-detail.component'; -import { RecipeTemplateListComponent } from './recipe-template-list/recipe-template-list.component'; - -export const routes: Routes = [ - { - path: '', - component: RecipeTemplateListComponent, - canActivate: [authGuard], - data: { - permission: 'Recipes', - }, - }, - { - path: 'new', - component: RecipeTemplateDetailComponent, - canActivate: [authGuard], - data: { - permission: 'Recipes', - }, - }, - { - path: ':id', - component: RecipeTemplateDetailComponent, - canActivate: [authGuard], - data: { - permission: 'Recipes', - }, - }, -]; diff --git a/overlord/src/app/recipe-template/recipe-template.service.spec.ts b/overlord/src/app/recipe-template/recipe-template.service.spec.ts deleted file mode 100644 index a0f31fac..00000000 --- a/overlord/src/app/recipe-template/recipe-template.service.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { inject, TestBed } from '@angular/core/testing'; - -import { RecipeTemplateService } from './recipe-template.service'; - -describe('RecipeTemplateService', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - providers: [RecipeTemplateService, provideHttpClient(withInterceptorsFromDi())], - }); - }); - - it('should be created', inject([RecipeTemplateService], (service: RecipeTemplateService) => { - expect(service).toBeTruthy(); - })); -}); diff --git a/overlord/src/app/recipe-template/recipe-template.service.ts b/overlord/src/app/recipe-template/recipe-template.service.ts deleted file mode 100644 index ec52967a..00000000 --- a/overlord/src/app/recipe-template/recipe-template.service.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { HttpClient, httpResource } from '@angular/common/http'; -import { inject, Injectable, Signal } from '@angular/core'; -import { Observable } from 'rxjs/internal/Observable'; -import { catchError } from 'rxjs/operators'; - -import { ErrorLoggerService } from '../core/error-logger.service'; -import { RecipeTemplate } from './recipe-template'; - -const url = '/api/recipe-templates'; -@Injectable({ - providedIn: 'root', -}) -export class RecipeTemplateService { - private http = inject(HttpClient); - private log = inject(ErrorLoggerService); - - get(id: Signal) { - return httpResource(() => { - const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`; - return getUrl; - }); - } - - list() { - return httpResource(() => `${url}/list`); - } - - save(recipeTemplate: RecipeTemplate): Observable { - return this.http - .post(`${url}`, recipeTemplate) - .pipe(catchError(this.log.handleError('RecipeTemplateService', 'save'))) as Observable; - } - - update(recipeTemplate: RecipeTemplate): Observable { - return this.http - .put(`${url}/${recipeTemplate.id}`, recipeTemplate) - .pipe(catchError(this.log.handleError('RecipeTemplateService', 'update'))) as Observable; - } - - saveOrUpdate(recipeTemplate: RecipeTemplate): Observable { - if (!recipeTemplate.id) { - return this.save(recipeTemplate); - } - return this.update(recipeTemplate); - } - - delete(id: string): Observable { - return this.http - .delete(`${url}/${id}`) - .pipe(catchError(this.log.handleError('RecipeTemplateService', 'delete'))) as Observable; - } -} diff --git a/overlord/src/app/recipe-template/recipe-template.ts b/overlord/src/app/recipe-template/recipe-template.ts deleted file mode 100644 index 61598f25..00000000 --- a/overlord/src/app/recipe-template/recipe-template.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class RecipeTemplate { - id: string; - name: string; - date: string; - text: string; - selected: boolean; - - public constructor(init?: Partial) { - this.id = ''; - this.name = ''; - this.date = ''; - this.text = ''; - this.selected = false; - Object.assign(this, init); - } -} diff --git a/overlord/src/app/recipe/recipe-detail/recipe-detail.component.html b/overlord/src/app/recipe/recipe-detail/recipe-detail.component.html index d36dacb9..671a7188 100644 --- a/overlord/src/app/recipe/recipe-detail/recipe-detail.component.html +++ b/overlord/src/app/recipe/recipe-detail/recipe-detail.component.html @@ -312,7 +312,7 @@

{{ dishName }}

- Date: {{ formModel().basics.date.format('DD-MMM-YYYY') }} + Date: {{ formModel().basics.date }} Source: {{ formModel().basics.source }} Yield: diff --git a/overlord/src/app/recipe/recipe-detail/recipe-detail.component.ts b/overlord/src/app/recipe/recipe-detail/recipe-detail.component.ts index 9d188f10..846cc653 100644 --- a/overlord/src/app/recipe/recipe-detail/recipe-detail.component.ts +++ b/overlord/src/app/recipe/recipe-detail/recipe-detail.component.ts @@ -63,7 +63,7 @@ export interface IngredientFormData { export interface RecipeDetailFormData { basics: { - date: moment.Moment; + date: string; source: string; recipeYield: string; product: ProductSku | string | null; @@ -136,7 +136,7 @@ export class RecipeDetailComponent { source: this.item, computation: (itemVal): RecipeDetailFormData => ({ basics: { - date: itemVal.date ? moment(itemVal.date) : moment(new Date()), + date: itemVal.date, source: itemVal.source ?? '', recipeYield: `${itemVal.recipeYield ?? ''}`, product: itemVal.sku ?? null, diff --git a/overlord/src/app/settings/settings.component.ts b/overlord/src/app/settings/settings.component.ts index 2068faae..ef6a84ad 100644 --- a/overlord/src/app/settings/settings.component.ts +++ b/overlord/src/app/settings/settings.component.ts @@ -24,16 +24,16 @@ import { SettingsService } from './settings.service'; import { VoucherTypeService } from './voucher-type.service'; export interface LockInfoFormData { - validFrom: moment.Moment | null; - validTill: moment.Moment | null; + validFrom: Date | null; + validTill: Date | null; index: number; lockOlder: boolean; olderRolling: boolean; - olderDate: moment.Moment; + olderDate: Date; olderDays: number; lockNewer: boolean; newerRolling: boolean; - newerDate: moment.Moment; + newerDate: Date; newerDays: number; accountTypes: { accountType: boolean }[]; voucherTypes: { voucherType: boolean }[]; @@ -82,16 +82,16 @@ export class SettingsComponent { lockInfoModel = linkedSignal({ source: this.lockInfoSource, computation: (s): LockInfoFormData => ({ - validFrom: null as moment.Moment | null, - validTill: null as moment.Moment | null, + validFrom: null as Date | null, + validTill: null as Date | null, index: 0, lockOlder: false, olderRolling: false, - olderDate: moment().date(moment().date(1).daysInMonth()), + olderDate: moment().date(moment().date(1).daysInMonth()).toDate(), olderDays: 0, lockNewer: false, newerRolling: false, - newerDate: moment().date(moment().date(1).daysInMonth()), + newerDate: moment().date(moment().date(1).daysInMonth()).toDate(), newerDays: 0, accountTypes: s.accTypes.map(() => ({ accountType: false })), voucherTypes: s.vchTypes.map(() => ({ voucherType: false })), @@ -133,13 +133,13 @@ export class SettingsComponent { if (this.lockOlder() && this.olderRolling()) { item.start.days = model.olderDays ?? 0; } else if (this.lockOlder() && !this.olderRolling()) { - item.start.date = (model.olderDate ?? moment()).format('DD-MMM-YYYY'); + item.start.date = moment(model.olderDate ?? new Date()).format('DD-MMM-YYYY'); } if (this.lockNewer() && this.newerRolling()) { item.finish.days = model.newerDays ?? 0; } if (this.lockNewer() && !this.newerRolling()) { - item.finish.date = (model.newerDate ?? moment()).format('DD-MMM-YYYY'); + item.finish.date = moment(model.newerDate ?? new Date()).format('DD-MMM-YYYY'); } this.accountTypes().forEach((at: AccountType, index: number) => { diff --git a/overlord/src/app/stock-movement/stock-movement.component.html b/overlord/src/app/stock-movement/stock-movement.component.html index c8117f0d..076619ff 100644 --- a/overlord/src/app/stock-movement/stock-movement.component.html +++ b/overlord/src/app/stock-movement/stock-movement.component.html @@ -1,6 +1,6 @@

Stock Movement

-
+
Start Date @@ -14,7 +14,7 @@ - +
@if (resource.isLoading()) { diff --git a/overlord/src/app/stock-movement/stock-movement.component.ts b/overlord/src/app/stock-movement/stock-movement.component.ts index 05cdee32..d8cc3570 100644 --- a/overlord/src/app/stock-movement/stock-movement.component.ts +++ b/overlord/src/app/stock-movement/stock-movement.component.ts @@ -1,5 +1,5 @@ import { DecimalPipe } from '@angular/common'; -import { Component, HostListener, inject, computed, linkedSignal, signal, input } from '@angular/core'; +import { Component, inject, computed, linkedSignal, signal, input } from '@angular/core'; import { form, FormField, FormRoot } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -25,6 +25,9 @@ export interface StockMovementFormData { selector: 'app-stock-movement', templateUrl: './stock-movement.component.html', styleUrls: ['./stock-movement.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, @@ -104,7 +107,6 @@ export class StockMovementComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['group', 'name', 'opening', 'purchase', 'issue', 'closing']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl(); diff --git a/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.html b/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.html index 42de0e2c..803aa134 100644 --- a/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.html +++ b/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.html @@ -7,7 +7,7 @@ Product Group - + -- All Products -- @for (pg of productGroups(); track pg) { @@ -18,10 +18,10 @@
-@if (resource.isLoading()) { +@if (listResource.isLoading()) { -} @else if (resource.error()) { - +} @else if (listResource.error()) { + } @else { diff --git a/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.ts b/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.ts index 0d4d9225..870959f3 100644 --- a/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.ts +++ b/overlord/src/app/temporal-product/temporal-product-list/temporal-product-list.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, inject, computed, linkedSignal } from '@angular/core'; +import { Component, inject, computed, linkedSignal, input, debounced, effect } from '@angular/core'; import { FormField, FormRoot, form as createForm } from '@angular/forms/signals'; import { MatOptionModule } from '@angular/material/core'; import { MatFormFieldModule } from '@angular/material/form-field'; @@ -7,12 +7,13 @@ import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; -import { RouterLink } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { Product, StockKeepingUnit } from '../../core/product'; import { ProductGroupService } from '../../product-group/product-group.service'; import { ErrorStateComponent } from '../../shared/error-state/error-state.component'; import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component'; +import { TemporalProduct } from '../temporal-product'; import { TemporalProductService } from '../temporal-product.service'; export interface TemporalProductListFormData { @@ -42,48 +43,84 @@ export interface TemporalProductListFormData { ], }) export class TemporalProductListComponent { + private router = inject(Router); + private route = inject(ActivatedRoute); private ser = inject(TemporalProductService); private productGroupSer = inject(ProductGroupService); + q = input('', { transform: (v: string | null | undefined) => v ?? '' }); + g = input('', { transform: (v: string | null | undefined) => v ?? '' }); + productGroupsResource = this.productGroupSer.list(); + productGroups = computed(() => this.productGroupsResource.value() ?? []); + + listResource = this.ser.list(); + list = computed(() => this.listResource.value() ?? []); + formModel = linkedSignal({ - source: () => null, - computation: (): TemporalProductListFormData => ({ - filter: '', - productGroup: '', + source: () => ({ q: this.q(), g: this.g() }), + computation: (v: { q: string; g: string }): TemporalProductListFormData => ({ + filter: v.q, + productGroup: v.g, }), }); form = createForm(this.formModel); + filterSignal = computed(() => this.formModel().filter); + debouncedFilter = debounced(this.filterSignal, 150); + filteredList = computed(() => { + const data = this.list(); + const search = this.debouncedFilter.value() ?? ''; + const productGroup = this.formModel().productGroup; + const groupId = + typeof productGroup === 'string' + ? productGroup + : productGroup && 'id' in productGroup + ? (productGroup.id ?? '') + : ''; - productGroupsResource = this.productGroupSer.list(); - productGroups = computed(() => this.productGroupsResource.value() ?? []); + const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean); - resource = this.ser.list(); + return data.filter((tp: TemporalProduct) => { + const products = tp.products ?? []; + const skus = tp.skus ?? []; - dataSource = computed(() => { - let data = this.resource.value() ?? []; - const filterValue = (this.formModel().filter ?? '').trim().toLowerCase(); - const groupValue = this.formModel().productGroup; + const matchesSearch = + tokens.length === 0 || + tokens.every( + (token) => + products.some((p) => { + const hay = `${p.name ?? ''} ${p.fractionUnits ?? ''} ${p.productGroup?.name ?? ''}`.toLowerCase(); + return hay.includes(token); + }) || + skus.some((k) => { + const hay = `${k.units ?? ''}`.toLowerCase(); + return hay.includes(token); + }), + ); - if (filterValue || groupValue) { - data = data.filter((d) => { - let match = true; - if (filterValue) { - match = match && d.products[0].name.toLowerCase().includes(filterValue); - } - if (groupValue) { - match = match && d.products[0].productGroup?.id === groupValue; - } - return match; - }); - } - return data; + const matchesProductGroup = !groupId || products.some((k) => (k.productGroup?.id ?? '') === groupId); + return matchesSearch && matchesProductGroup; + }); }); + dataSource = computed(() => this.filteredList()); + /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns: string[] = ['product', 'sku', 'info']; - filterOn(val: string) { - this.formModel.update((m) => ({ ...m, productGroup: val })); + constructor() { + effect(() => { + const q = this.debouncedFilter.value() ?? ''; + const g = this.formModel().productGroup; + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + q: q || null, + g: g || null, + }, + queryParamsHandling: 'merge', + replaceUrl: true, + }); + }); } } diff --git a/overlord/src/app/trial-balance/trial-balance.component.html b/overlord/src/app/trial-balance/trial-balance.component.html index c576adc9..2b525334 100644 --- a/overlord/src/app/trial-balance/trial-balance.component.html +++ b/overlord/src/app/trial-balance/trial-balance.component.html @@ -1,6 +1,6 @@

Trial Balance

-
+
Date @@ -8,7 +8,7 @@ - +
@if (resource.isLoading()) { diff --git a/overlord/src/app/trial-balance/trial-balance.component.ts b/overlord/src/app/trial-balance/trial-balance.component.ts index 298cc9c1..4c87a1f7 100644 --- a/overlord/src/app/trial-balance/trial-balance.component.ts +++ b/overlord/src/app/trial-balance/trial-balance.component.ts @@ -1,5 +1,5 @@ import { CurrencyPipe } from '@angular/common'; -import { Component, HostListener, inject, computed, linkedSignal, signal, input } from '@angular/core'; +import { Component, inject, computed, linkedSignal, signal, input } from '@angular/core'; import { FormField, FormRoot, form as createForm } from '@angular/forms/signals'; import { MatButtonModule } from '@angular/material/button'; import { MatDatepickerModule } from '@angular/material/datepicker'; @@ -25,6 +25,9 @@ export interface TrialBalanceFormData { selector: 'app-trial-balance', templateUrl: './trial-balance.component.html', styleUrls: ['./trial-balance.component.css'], + host: { + '(window:keydown.f2)': 'focusDate($event)', + }, imports: [ FormField, @@ -100,7 +103,6 @@ export class TrialBalanceComponent { /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ displayedColumns = ['type', 'name', 'debit', 'credit']; - @HostListener('window:keydown.f2', ['$event']) focusDate(event: Event) { event.preventDefault(); this.form().focusBoundControl();