Removed Recipe Template as it was not needed.

Moved host listeners to the component declaration.
Form submit on Return fixed.
This commit is contained in:
tanshu committed 2026-08-26 06:00:13 +00:00
1 parent fe5e4a2308
commit e3719faddd
79 files changed
+737 -990

No files matched your search

+321
View File
@@ -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.
+1 -1
View File
@@ -71,7 +71,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project uv sync --locked --no-install-project
COPY /brewman ./ COPY /brewman ./
COPY --from=builder /frontend/browser /app/static COPY --from=builder /frontend/browser /app/frontend
# Sync the project # Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers # Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
+1 -22
View File
@@ -1,24 +1,3 @@
{{ host }} { {{ host }} {
# Match and proxy API routes reverse_proxy {{ host_directory }}:80
@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
}
} }
@@ -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 ###
+2 -2
View File
@@ -17,6 +17,7 @@ from ..models.incentive import Incentive
from ..models.inventory import Inventory from ..models.inventory import Inventory
from ..models.journal import Journal from ..models.journal import Journal
from ..models.login_history import LoginHistory from ..models.login_history import LoginHistory
from ..models.mozimo_stock_register import MozimoStockRegister
from ..models.period import Period from ..models.period import Period
from ..models.permission import Permission from ..models.permission import Permission
from ..models.price import Price from ..models.price import Price
@@ -30,7 +31,6 @@ from ..models.recipe_item import RecipeItem
from ..models.recipe_photo import RecipePhoto from ..models.recipe_photo import RecipePhoto
from ..models.recipe_step import RecipeStep from ..models.recipe_step import RecipeStep
from ..models.recipe_tag import RecipeTag from ..models.recipe_tag import RecipeTag
from ..models.recipe_template import RecipeTemplate
from ..models.role import Role from ..models.role import Role
from ..models.role_include import RoleInclude from ..models.role_include import RoleInclude
from ..models.role_permission import RolePermission from ..models.role_permission import RolePermission
@@ -63,6 +63,7 @@ __all__ = [
"Inventory", "Inventory",
"Journal", "Journal",
"LoginHistory", "LoginHistory",
"MozimoStockRegister",
"Permission", "Permission",
"Period", "Period",
"Price", "Price",
@@ -76,7 +77,6 @@ __all__ = [
"RecipePhoto", "RecipePhoto",
"RecipeStep", "RecipeStep",
"RecipeTag", "RecipeTag",
"RecipeTemplate",
"reg", "reg",
"Role", "Role",
"RoleInclude", "RoleInclude",
-6
View File
@@ -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_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_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_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."), "uq_tags_name": FriendlyDBError(409, "TAG_NAME_EXISTS", "A tag with this name already exists."),
# --- role_permissions --- # --- role_permissions ---
@@ -114,9 +111,6 @@ _CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = {
"only_one_valid_attendance": FriendlyDBError( "only_one_valid_attendance": FriendlyDBError(
409, "ATTENDANCE_EXISTS", "A valid attendance already exists for this employee on this date." 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) # Optional: FK-specific friendly messages (use when you want better UX than generic FK failure)
+1 -4
View File
@@ -4,7 +4,6 @@ import uvicorn
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
@@ -45,7 +44,6 @@ from .routers import (
rate_contract, rate_contract,
rebase, rebase,
recipe, recipe,
recipe_template,
role, role,
tag, tag,
temporal_product, 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(temporal_product.router, prefix="/api/temporal-products", tags=["products"])
app.include_router(product_group.router, prefix="/api/product-groups", 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.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(period.router, prefix="/api/periods", tags=["periods"])
app.include_router(client.router, prefix="/api/clients", tags=["clients"]) 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(title.router, prefix="/api/title")
app.include_router(health.router, prefix="/health", tags=["health"]) app.include_router(health.router, prefix="/health", tags=["health"])
app.mount("/static", StaticFiles(directory="static"), name="static") app.frontend("/", directory="frontend", check_dir=False)
def init() -> None: def init() -> None:
+7 -3
View File
@@ -24,14 +24,18 @@ class RecipePhoto:
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") 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(
image_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("images.id"), nullable=False, index=True) 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) order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
caption: Mapped[str] = mapped_column(Text, nullable=False) caption: Mapped[str] = mapped_column(Text, nullable=False)
recipe: Mapped[Recipe] = relationship("Recipe", back_populates="photos") 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__( def __init__(
self, self,
+3 -1
View File
@@ -26,7 +26,9 @@ class RecipeStep:
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") 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) order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
phase: Mapped[str] = mapped_column(Text, nullable=False) # Prep/Cook/Finish/Serve phase: Mapped[str] = mapped_column(Text, nullable=False) # Prep/Cook/Finish/Serve
-40
View File
@@ -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_
-1
View File
@@ -4,4 +4,3 @@ import enum
class SettingType(enum.IntEnum): class SettingType(enum.IntEnum):
VOUCHER_LOCK = 0 VOUCHER_LOCK = 0
MAINTENANCE_MODE = 1 MAINTENANCE_MODE = 1
RECIPE_TEMPLATE = 2
@@ -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
)
+1 -1
View File
@@ -89,7 +89,7 @@ def build_report(
quantity=quantity, quantity=quantity,
rate=rate, rate=rate,
amount=amount, amount=amount,
url=["/", "product-ledger", str(product.id)], url=["/", "product-ledger", str(product.product_id)],
) )
body.append(row) body.append(row)
return ( return (
@@ -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)
+1 -1
View File
@@ -211,7 +211,7 @@ wheels = [
[[package]] [[package]]
name = "brewman" name = "brewman"
version = "14.2.0" version = "15.0.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
-4
View File
@@ -65,10 +65,6 @@ export const routes: Routes = [
path: 'recipes', path: 'recipes',
loadChildren: () => import('./recipe/recipe.routes').then((mod) => mod.routes), 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', path: 'roles',
loadChildren: () => import('./role/role.routes').then((mod) => mod.routes), loadChildren: () => import('./role/role.routes').then((mod) => mod.routes),
@@ -1,6 +1,6 @@
<h2>Attendance</h2> <h2>Attendance</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-4-5"> <mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@@ -15,7 +15,7 @@
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker> <mat-datepicker #date></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div> </div>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
@@ -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 { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
@@ -22,7 +22,7 @@ import { AttendanceTypeService } from './attendance-type.service';
import { AttendanceService } from './attendance.service'; import { AttendanceService } from './attendance.service';
export interface AttendanceFormData { export interface AttendanceFormData {
date: moment.Moment; date: Date;
attendances: { attendanceType: number | null }[]; attendances: { attendanceType: number | null }[];
} }
@@ -30,6 +30,9 @@ export interface AttendanceFormData {
selector: 'app-attendance', selector: 'app-attendance',
templateUrl: './attendance.component.html', templateUrl: './attendance.component.html',
styleUrls: ['./attendance.component.css'], styleUrls: ['./attendance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -64,7 +67,7 @@ export class AttendanceComponent {
model = linkedSignal({ model = linkedSignal({
source: this.info, source: this.info,
computation: (infoVal): AttendanceFormData => ({ computation: (infoVal): AttendanceFormData => ({
date: moment(infoVal.date), date: moment(infoVal.date, 'DD-MMM-YYYY').toDate(),
attendances: infoVal.body.map((x) => ({ attendances: infoVal.body.map((x) => ({
attendanceType: x.attendanceType.id, attendanceType: x.attendanceType.id,
})), })),
@@ -75,7 +78,6 @@ export class AttendanceComponent {
displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints']; displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Balance Sheet</h2> <h2>Balance Sheet</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@@ -8,7 +8,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker> <mat-datepicker #dateInput></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,8 +1,8 @@
import { CurrencyPipe } from '@angular/common'; 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'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface BalanceSheetFormData { export interface BalanceSheetFormData {
date: moment.Moment; date: Date;
} }
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -24,6 +24,9 @@ import { BalanceSheetService } from './balance-sheet.service';
selector: 'app-balance-sheet', selector: 'app-balance-sheet',
templateUrl: './balance-sheet.component.html', templateUrl: './balance-sheet.component.html',
styleUrls: ['./balance-sheet.component.css'], styleUrls: ['./balance-sheet.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -56,7 +59,7 @@ export class BalanceSheetComponent {
formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({ formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({
source: this.info, source: this.info,
computation: (info: BalanceSheet): BalanceSheetFormData => ({ 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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'subAmount', 'total']; displayedColumns = ['group', 'name', 'subAmount', 'total'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Cash Flow</h2> <h2>Cash Flow</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,9 +1,9 @@
import { CurrencyPipe } from '@angular/common'; 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'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface CashFlowFormData { export interface CashFlowFormData {
startDate: moment.Moment; startDate: Date;
finishDate: moment.Moment; finishDate: Date;
} }
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -24,6 +24,9 @@ import { CashFlowService } from './cash-flow.service';
selector: 'app-cash-flow', selector: 'app-cash-flow',
templateUrl: './cash-flow.component.html', templateUrl: './cash-flow.component.html',
styleUrls: ['./cash-flow.component.css'], styleUrls: ['./cash-flow.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -78,8 +81,8 @@ export class CashFlowComponent {
model = linkedSignal({ model = linkedSignal({
source: this.info, source: this.info,
computation: (info: CashFlow): CashFlowFormData => ({ computation: (info: CashFlow): CashFlowFormData => ({
startDate: info.startDate ? moment(info.startDate, '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') : moment(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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'amount']; displayedColumns = ['name', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
// TODO: also select the contents // TODO: also select the contents
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Department</mat-label> <mat-label>Department</mat-label>
@@ -25,7 +25,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker> <mat-datepicker #dateInput></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div> </div>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; 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 { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -29,7 +29,7 @@ import { ClosingStockItem } from './closing-stock-item';
import { ClosingStockService } from './closing-stock.service'; import { ClosingStockService } from './closing-stock.service';
export interface ClosingStockFormData { export interface ClosingStockFormData {
date: moment.Moment; date: Date;
costCentre: CostCentre | null; costCentre: CostCentre | null;
stocks: { physical: number; costCentre: string | undefined }[]; stocks: { physical: number; costCentre: string | undefined }[];
} }
@@ -38,6 +38,9 @@ export interface ClosingStockFormData {
selector: 'app-closing-stock', selector: 'app-closing-stock',
templateUrl: './closing-stock.component.html', templateUrl: './closing-stock.component.html',
styleUrls: ['./closing-stock.component.css'], styleUrls: ['./closing-stock.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
MatIconModule, MatIconModule,
FormField, FormField,
@@ -83,7 +86,7 @@ export class ClosingStockComponent {
model = linkedSignal({ model = linkedSignal({
source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })), source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })),
computation: ({ info, costCentres }): ClosingStockFormData => ({ 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, costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null,
stocks: info.items.map((x) => ({ stocks: info.items.map((x) => ({
physical: x.physical, physical: x.physical,
@@ -130,7 +133,6 @@ export class ClosingStockComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount']; displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -165,7 +167,7 @@ export class ClosingStockComponent {
getClosingStock(): ClosingStock { getClosingStock(): ClosingStock {
const formModel = this.model(); const formModel = this.model();
const currentInfo = this.info(); 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; const array = formModel.stocks;
currentInfo.items.forEach((item, index) => { currentInfo.items.forEach((item, index) => {
item.physical = array[index].physical ?? 0; item.physical = array[index].physical ?? 0;
@@ -178,7 +180,7 @@ export class ClosingStockComponent {
const formModel = this.model(); const formModel = this.model();
return new ClosingStock({ 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 ?? '' }), costCentre: new CostCentre({ id: formModel.costCentre?.id ?? '' }),
}); });
} }
@@ -58,7 +58,6 @@
<a mat-menu-item routerLink="/products">Products</a> <a mat-menu-item routerLink="/products">Products</a>
<a mat-menu-item routerLink="/product-groups">Product Groups</a> <a mat-menu-item routerLink="/product-groups">Product Groups</a>
<a mat-menu-item routerLink="/recipes">Recipes</a> <a mat-menu-item routerLink="/recipes">Recipes</a>
<a mat-menu-item routerLink="/recipe-templates">Recipe Templates</a>
<a mat-menu-item routerLink="/periods">Periods</a> <a mat-menu-item routerLink="/periods">Periods</a>
<a mat-menu-item routerLink="/tags">Tags</a> <a mat-menu-item routerLink="/tags">Tags</a>
</mat-menu> </mat-menu>
@@ -1,6 +1,6 @@
<h2>Daybook</h2> <h2>Daybook</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; 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'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface DaybookFormData { export interface DaybookFormData {
startDate: Date; startDate: Date;
@@ -24,6 +24,9 @@ import { DaybookService } from './daybook.service';
selector: 'app-daybook', selector: 'app-daybook',
templateUrl: './daybook.component.html', templateUrl: './daybook.component.html',
styleUrls: ['./daybook.component.css'], styleUrls: ['./daybook.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -107,7 +110,6 @@ export class DaybookComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['date', 'type', 'narration', 'debitText', 'debitAmount', 'creditText', 'creditAmount']; displayedColumns = ['date', 'type', 'narration', 'debitText', 'debitAmount', 'creditText', 'creditAmount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
// TODO: Also select the text // TODO: Also select the text
@@ -1,6 +1,6 @@
<h2>Employee Attendance</h2> <h2>Employee Attendance</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-1-4"> <mat-form-field class="flex-auto basis-1-4">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -48,7 +48,7 @@
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div> </div>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
@@ -1,13 +1,4 @@
import { import { computed, Component, inject, debounced, input, linkedSignal, afterNextRender } from '@angular/core';
computed,
Component,
HostListener,
inject,
debounced,
input,
linkedSignal,
afterNextRender,
} from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -42,6 +33,9 @@ export interface EmployeeAttendanceFormData {
selector: 'app-employee-attendance', selector: 'app-employee-attendance',
templateUrl: './employee-attendance.component.html', templateUrl: './employee-attendance.component.html',
styleUrls: ['./employee-attendance.component.css'], styleUrls: ['./employee-attendance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -101,7 +95,6 @@ export class EmployeeAttendanceComponent {
employeesResource = this.employeeSer.autocomplete(computed(() => this.debouncedEmployeeSearch.value() ?? '')); employeesResource = this.employeeSer.autocomplete(computed(() => this.debouncedEmployeeSearch.value() ?? ''));
employees = computed(() => this.employeesResource.value() ?? []); employees = computed(() => this.employeesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -48,6 +48,9 @@ export interface EmployeeBenefitsFormData {
selector: 'app-employee-benefits', selector: 'app-employee-benefits',
templateUrl: './employee-benefits.component.html', templateUrl: './employee-benefits.component.html',
styleUrls: ['./employee-benefits.component.css'], styleUrls: ['./employee-benefits.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -122,7 +125,6 @@ export class EmployeeBenefitsComponent {
employeesResource = this.employeeSer.autocomplete(computed(() => this.employeesSearch() ?? '')); employeesResource = this.employeeSer.autocomplete(computed(() => this.employeesSearch() ?? ''));
employees = computed(() => this.employeesResource.value() ?? []); employees = computed(() => this.employeesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.date().focusBoundControl(); this.form.date().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Entries</h2> <h2>Entries</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); refresh()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Create / Last Edit Date From</mat-label> <mat-label>Create / Last Edit Date From</mat-label>
@@ -20,9 +20,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button (click)="refresh()" class="flex-auto basis-1-5"> <button mat-raised-button type="submit" class="flex-auto basis-1-5"><mat-icon>refresh</mat-icon>Refresh</button>
<mat-icon>refresh</mat-icon>Refresh
</button>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-radio-group class="flex-auto basis-2-5" [formField]="form.posted" (change)="togglePosted($event)"> <mat-radio-group class="flex-auto basis-2-5" [formField]="form.posted" (change)="togglePosted($event)">
+4 -11
View File
@@ -1,15 +1,6 @@
/* eslint-disable @angular-eslint/no-input-rename */ /* eslint-disable @angular-eslint/no-input-rename */
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { import { Component, inject, input, computed, booleanAttribute, afterNextRender, linkedSignal } from '@angular/core';
Component,
HostListener,
inject,
input,
computed,
booleanAttribute,
afterNextRender,
linkedSignal,
} from '@angular/core';
export interface EntriesFormData { export interface EntriesFormData {
startDate: Date; startDate: Date;
finishDate: Date; finishDate: Date;
@@ -40,6 +31,9 @@ import { Report } from './report';
selector: 'app-entries', selector: 'app-entries',
templateUrl: './entries.component.html', templateUrl: './entries.component.html',
styleUrls: ['./entries.component.css'], styleUrls: ['./entries.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -120,7 +114,6 @@ export class EntriesComponent {
displayedColumns = ['date', 'voucherType', 'narration', 'debitNames', 'creditNames', 'amount', 'user']; displayedColumns = ['date', 'voucherType', 'narration', 'debitNames', 'creditNames', 'amount', 'user'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; 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 { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -23,7 +23,7 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.
import { LocalTimePipe } from '../shared/local-time.pipe'; import { LocalTimePipe } from '../shared/local-time.pipe';
export interface IncentiveFormData { export interface IncentiveFormData {
date: moment.Moment; date: Date;
incentives: { points: number }[]; incentives: { points: number }[];
} }
@@ -31,6 +31,9 @@ export interface IncentiveFormData {
selector: 'app-incentive', selector: 'app-incentive',
templateUrl: './incentive.component.html', templateUrl: './incentive.component.html',
styleUrls: ['./incentive.component.css'], styleUrls: ['./incentive.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -61,7 +64,7 @@ export class IncentiveComponent {
model = linkedSignal({ model = linkedSignal({
source: this.voucher, source: this.voucher,
computation: (v): IncentiveFormData => ({ 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 })), incentives: v.incentives.map((x) => ({ points: x.points })),
}), }),
}); });
@@ -75,7 +78,6 @@ export class IncentiveComponent {
displayedColumns = ['name', 'designation', 'department', 'daysWorked', 'points', 'amount']; displayedColumns = ['name', 'designation', 'department', 'daysWorked', 'points', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -159,7 +161,7 @@ export class IncentiveComponent {
getVoucher(): Voucher { getVoucher(): Voucher {
const formModel = this.model(); const formModel = this.model();
const v = this.voucher(); 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; const array = formModel.incentives;
v.incentives.forEach((item: Incentive, index: number) => { v.incentives.forEach((item: Incentive, index: number) => {
+7 -15
View File
@@ -1,15 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -76,6 +66,12 @@ export interface IssueFormModel {
selector: 'app-issue', selector: 'app-issue',
templateUrl: './issue.component.html', templateUrl: './issue.component.html',
styleUrls: ['./issue.component.css'], 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: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -174,19 +170,16 @@ export class IssueComponent {
issueGridResource = this.issueGridSer.issueGrid(this.balanceDate); issueGridResource = this.issueGridSer.issueGrid(this.balanceDate);
gridData = computed(() => this.issueGridResource.value() ?? []); gridData = computed(() => this.issueGridResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f3', ['$event'])
focusBatchHost(event: Event) { focusBatchHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.addRow.batch().focusBoundControl(); this.form.addRow.batch().focusBoundControl();
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -194,7 +187,6 @@ export class IssueComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
+12 -10
View File
@@ -1,6 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common'; 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 { FormField, FormRoot, form } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -72,6 +72,15 @@ export interface JournalFormModel {
selector: 'app-journal', selector: 'app-journal',
templateUrl: './journal.component.html', templateUrl: './journal.component.html',
styleUrls: ['./journal.component.css'], 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: [ imports: [
MatIconModule, MatIconModule,
MatFormFieldModule, MatFormFieldModule,
@@ -112,19 +121,16 @@ export class JournalComponent {
itemResource = this.ser.getVoucher(this.id, this.type, signal(null), this.d); itemResource = this.ser.getVoucher(this.id, this.type, signal(null), this.d);
item = linkedSignal(() => this.itemResource.value() ?? new Voucher()); item = linkedSignal(() => this.itemResource.value() ?? new Voucher());
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) { focusAccountHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.addRow.account().focusBoundControl(); this.form.addRow.account().focusBoundControl();
} }
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) { redirectToPayment(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['payment'], { this.router.navigate(['payment'], {
@@ -135,7 +141,6 @@ export class JournalComponent {
}); });
} }
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) { redirectToReciept(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['receipt'], { this.router.navigate(['receipt'], {
@@ -146,12 +151,10 @@ export class JournalComponent {
}); });
} }
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) { redirectToJournal(event: Event) {
event.preventDefault(); event.preventDefault();
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -159,7 +162,6 @@ export class JournalComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted() && this.auth.allowed('post-vouchers')) { 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([], { this.router.navigate([], {
queryParams: { d: newDate.format('DD-MMM-YYYY') }, queryParams: { d: moment(newDate).format('DD-MMM-YYYY') },
replaceUrl: true, replaceUrl: true,
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
}); });
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -33,18 +33,13 @@
[formField]="form.account" [formField]="form.account"
autocomplete="off" autocomplete="off"
/> />
<mat-autocomplete <mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn">
#auto="matAutocomplete"
autoActiveFirstOption
[displayWith]="displayFn"
(optionSelected)="selected($event)"
>
@for (account of accounts(); track account) { @for (account of accounts(); track account) {
<mat-option [value]="account">{{ account.name }}</mat-option> <mat-option [value]="account">{{ account.name }}</mat-option>
} }
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<button mat-raised-button type="button" class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-4-5"> <mat-form-field class="flex-auto basis-4-5">
@@ -55,7 +50,7 @@
} }
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button mat-raised-button tabindex="-1" extended class="flex-auto basis-1-5" (click)="labels()"> <button mat-raised-button tabindex="-1" type="button" extended class="flex-auto basis-1-5" (click)="labels()">
<mat-icon>label</mat-icon> <mat-icon>label</mat-icon>
Manage Labels Manage Labels
</button> </button>
+14 -14
View File
@@ -1,8 +1,8 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { CurrencyPipe } from '@angular/common'; 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 { 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 { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips'; 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 { TagDialogComponent } from '../tag-dialog/tag-dialog.component';
import { TagService } from '../tag/tag.service'; import { TagService } from '../tag/tag.service';
export interface LedgerFormData { export interface LedgerFormData {
startDate: moment.Moment; startDate: Date;
finishDate: moment.Moment; finishDate: Date;
account: string | Account | null; account: string | Account | null;
tags: string[]; tags: string[];
} }
@@ -42,6 +42,9 @@ import { LedgerService } from './ledger.service';
selector: 'app-ledger', selector: 'app-ledger',
templateUrl: './ledger.component.html', templateUrl: './ledger.component.html',
styleUrls: ['./ledger.component.css'], styleUrls: ['./ledger.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
MatCheckboxModule, MatCheckboxModule,
MatChipsModule, MatChipsModule,
@@ -144,9 +147,9 @@ export class LedgerComponent {
model = linkedSignal<import('./ledger').Ledger, LedgerFormData>({ model = linkedSignal<import('./ledger').Ledger, LedgerFormData>({
source: this.info, source: this.info,
computation: (info: Ledger): LedgerFormData => ({ computation: (info: Ledger): LedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, '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') : moment(new Date()), finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
account: info.account?.name ?? '', account: info.account ?? null,
tags: this.tags(), tags: this.tags(),
}), }),
}); });
@@ -165,7 +168,6 @@ export class LedgerComponent {
private accountsResource = this.accountSer.autocomplete(this.accountSearch); private accountsResource = this.accountSer.autocomplete(this.accountSearch);
accounts = computed(() => this.accountsResource.value() ?? []); accounts = computed(() => this.accountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); 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; return !account ? '' : typeof account === 'string' ? account : account.name;
} }
selected(event: MatAutocompleteSelectedEvent): void {
this.info().account = event.option.value as Account;
}
selectRow(id: string): void { selectRow(id: string): void {
this.selectedRowId = id; this.selectedRowId = id;
} }
@@ -247,9 +245,11 @@ export class LedgerComponent {
getInfo(): Ledger { getInfo(): Ledger {
const formModel = this.model(); const formModel = this.model();
const accVal = formModel.account;
const account = accVal && typeof accVal !== 'string' ? accVal : undefined;
return new Ledger({ return new Ledger({
account: this.info().account, account,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
}); });
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-4-5"> <mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@@ -22,7 +22,7 @@
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker> <mat-datepicker #date></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
@@ -25,7 +25,7 @@ import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service'; import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
export interface MozimoDailyRegisterFormData { export interface MozimoDailyRegisterFormData {
date: moment.Moment; date: Date;
items: { items: {
received: number; received: number;
sale: number; sale: number;
@@ -79,7 +79,7 @@ export class MozimoDailyRegisterComponent {
model = linkedSignal({ model = linkedSignal({
source: this.info, source: this.info,
computation: (v): MozimoDailyRegisterFormData => ({ computation: (v): MozimoDailyRegisterFormData => ({
date: moment(v.date, 'DD-MMM-YYYY'), date: moment(v.date, 'DD-MMM-YYYY').toDate(),
items: v.body.map((x) => ({ items: v.body.map((x) => ({
received: x.received, received: x.received,
sale: x.sale, sale: x.sale,
@@ -129,7 +129,7 @@ export class MozimoDailyRegisterComponent {
getMozimoDailyRegister(): MozimoDailyRegister { getMozimoDailyRegister(): MozimoDailyRegister {
const formModel = this.model(); const formModel = this.model();
const info = this.info(); 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; const array = formModel.items;
if (array) { if (array) {
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -44,7 +44,7 @@
} }
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
@@ -27,8 +27,8 @@ import { MozimoProductRegisterItem } from './mozimo-product-register-item';
import { MozimoProductRegisterService } from './mozimo-product-register.service'; import { MozimoProductRegisterService } from './mozimo-product-register.service';
export interface MozimoProductRegisterFormData { export interface MozimoProductRegisterFormData {
startDate: moment.Moment; startDate: Date;
finishDate: moment.Moment; finishDate: Date;
product: Product | string | null; product: Product | string | null;
items: { items: {
received: number; received: number;
@@ -84,11 +84,11 @@ export class MozimoProductRegisterComponent {
model = linkedSignal({ model = linkedSignal({
source: this.info, source: this.info,
computation: (v): MozimoProductRegisterFormData => ({ computation: (info): MozimoProductRegisterFormData => ({
startDate: moment(v.startDate, 'DD-MMM-YYYY'), startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: moment(v.finishDate, 'DD-MMM-YYYY'), finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
product: v.product ?? '', product: info.product ?? '',
items: v.body.map((x) => ({ items: info.body.map((x) => ({
received: x.received, received: x.received,
sale: x.sale, sale: x.sale,
nc: x.nc, nc: x.nc,
@@ -160,8 +160,8 @@ export class MozimoProductRegisterComponent {
getMozimoProductRegister(): MozimoProductRegister { getMozimoProductRegister(): MozimoProductRegister {
const formModel = this.model(); const formModel = this.model();
const info = this.info(); const info = this.info();
info.startDate = formModel.startDate.format('DD-MMM-YYYY'); info.startDate = moment(formModel.startDate).format('DD-MMM-YYYY');
info.finishDate = formModel.finishDate.format('DD-MMM-YYYY'); info.finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY');
const array = formModel.items; const array = formModel.items;
if (array) { if (array) {
@@ -183,8 +183,8 @@ export class MozimoProductRegisterComponent {
return new MozimoProductRegister({ return new MozimoProductRegister({
product: formModel.product as Product, product: formModel.product as Product,
startDate: formModel.startDate.format('DD-MMM-YYYY'), startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: formModel.finishDate.format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
}); });
} }
@@ -1,6 +1,6 @@
<h2>Net Transactions</h2> <h2>Net Transactions</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; 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 { export interface NetTransactionsFormData {
startDate: Date; startDate: Date;
finishDate: Date; finishDate: Date;
@@ -25,6 +25,9 @@ import { NetTransactionsService } from './net-transactions.service';
selector: 'app-net-transactions', selector: 'app-net-transactions',
templateUrl: './net-transactions.component.html', templateUrl: './net-transactions.component.html',
styleUrls: ['./net-transactions.component.css'], styleUrls: ['./net-transactions.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -103,7 +106,6 @@ export class NetTransactionsComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['type', 'name', 'debit', 'credit']; displayedColumns = ['type', 'name', 'debit', 'credit'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
// TODO: Also select the text // TODO: Also select the text
+9 -17
View File
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { import { Component, inject, computed, effect, signal, input, linkedSignal, afterNextRender } from '@angular/core';
Component,
HostListener,
inject,
computed,
effect,
signal,
input,
linkedSignal,
afterNextRender,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -83,6 +73,14 @@ export interface PaymentFormModel {
selector: 'app-payment', selector: 'app-payment',
templateUrl: './payment.component.html', templateUrl: './payment.component.html',
styleUrls: ['./payment.component.css'], 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: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -199,18 +197,15 @@ export class PaymentComponent {
paymentAccountsResource = this.accountSer.paymentAutocomplete(signal('')); paymentAccountsResource = this.accountSer.paymentAutocomplete(signal(''));
paymentAccounts = computed(() => this.paymentAccountsResource.value() ?? []); paymentAccounts = computed(() => this.paymentAccountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) { redirectToPayment(event: Event) {
event.preventDefault(); event.preventDefault();
} }
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) { redirectToReciept(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['receipt'], { this.router.navigate(['receipt'], {
@@ -222,7 +217,6 @@ export class PaymentComponent {
}); });
} }
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) { redirectToJournal(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['journal'], { this.router.navigate(['journal'], {
@@ -233,7 +227,6 @@ export class PaymentComponent {
}); });
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -241,7 +234,6 @@ export class PaymentComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -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 { export interface PeriodFormData {
validFrom: moment.Moment; validFrom: Date;
validTill: moment.Moment; validTill: Date;
} }
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -23,6 +23,9 @@ import { PeriodService } from '../period.service';
selector: 'app-period-detail', selector: 'app-period-detail',
templateUrl: './period-detail.component.html', templateUrl: './period-detail.component.html',
styleUrls: ['./period-detail.component.css'], styleUrls: ['./period-detail.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -49,14 +52,13 @@ export class PeriodDetailComponent {
model = linkedSignal({ model = linkedSignal({
source: this.item, source: this.item,
computation: (item): PeriodFormData => ({ computation: (item): PeriodFormData => ({
validFrom: item.validFrom ? moment(item.validFrom, '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') : moment(new Date()), validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY').toDate() : new Date(),
}), }),
}); });
form = createForm(this.model); form = createForm(this.model);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -44,7 +44,7 @@
} }
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,16 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { import { Component, inject, computed, signal, linkedSignal, input, afterNextRender } from '@angular/core';
Component,
HostListener,
inject,
computed,
signal,
linkedSignal,
ResourceRef,
Signal,
input,
afterNextRender,
} from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -30,9 +19,9 @@ import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service'; import { ProductService } from '../product/product.service';
import { ToCsvService } from '../shared/to-csv.service'; import { ToCsvService } from '../shared/to-csv.service';
export interface ProductLedgerFormData { export interface ProductLedgerFormData {
startDate: moment.Moment; startDate: Date;
finishDate: moment.Moment; finishDate: Date;
product: string; product: Product | string | null;
} }
import { ErrorStateComponent } from '../shared/error-state/error-state.component'; import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component'; import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -43,6 +32,9 @@ import { ProductLedgerService } from './product-ledger.service';
selector: 'app-product-ledger', selector: 'app-product-ledger',
templateUrl: './product-ledger.component.html', templateUrl: './product-ledger.component.html',
styleUrls: ['./product-ledger.component.css'], styleUrls: ['./product-ledger.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
MatIconModule, MatIconModule,
@@ -60,7 +52,6 @@ import { ProductLedgerService } from './product-ledger.service';
MatPaginatorModule, MatPaginatorModule,
DecimalPipe, DecimalPipe,
CurrencyPipe, CurrencyPipe,
SkeletonLoaderComponent, SkeletonLoaderComponent,
ErrorStateComponent, ErrorStateComponent,
], ],
@@ -161,9 +152,9 @@ export class ProductLedgerComponent {
model = linkedSignal({ model = linkedSignal({
source: this.info, source: this.info,
computation: (info: ProductLedger): ProductLedgerFormData => ({ computation: (info: ProductLedger): ProductLedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, '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') : moment(new Date()), finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
product: info.product?.name ?? '', product: info.product ?? null,
}), }),
}); });
@@ -185,11 +176,14 @@ export class ProductLedgerComponent {
'runningAmount', 'runningAmount',
]; ];
productSearch!: Signal<string | null>; productSearch = computed(() => {
productsResource!: ResourceRef<ProductSku[] | undefined>; const v = this.model().product;
products!: Signal<ProductSku[]>; 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) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -199,21 +193,15 @@ export class ProductLedgerComponent {
afterNextRender(() => { afterNextRender(() => {
this.form().focusBoundControl(); 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; return !product ? '' : typeof product === 'string' ? product : product.name;
} }
selected(event: MatAutocompleteSelectedEvent): void { selected(event: MatAutocompleteSelectedEvent): void {
const p = event.option.value; const p = event.option.value;
this.model.update((m) => ({ ...m, product: p.name })); this.model.update((m) => ({ ...m, product: p }));
} }
selectRow(id: string): void { selectRow(id: string): void {
@@ -234,9 +222,10 @@ export class ProductLedgerComponent {
getInfo(): ProductLedger { getInfo(): ProductLedger {
const formModel = this.model(); const formModel = this.model();
const prodVal = formModel.product;
const product = prodVal && typeof prodVal !== 'string' ? prodVal : undefined;
return new ProductLedger({ return new ProductLedger({
product: this.info().product, product,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
}); });
@@ -1,6 +1,6 @@
<h2>Profit & Loss</h2> <h2>Profit & Loss</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -17,14 +17,17 @@ import { ProfitLoss } from './profit-loss';
import { ProfitLossService } from './profit-loss.service'; import { ProfitLossService } from './profit-loss.service';
export interface ProfitLossFormData { export interface ProfitLossFormData {
startDate: moment.Moment; startDate: Date;
finishDate: moment.Moment; finishDate: Date;
} }
@Component({ @Component({
selector: 'app-profit-loss', selector: 'app-profit-loss',
templateUrl: './profit-loss.component.html', templateUrl: './profit-loss.component.html',
styleUrls: ['./profit-loss.component.css'], styleUrls: ['./profit-loss.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
@@ -57,8 +60,8 @@ export class ProfitLossComponent {
formModel = linkedSignal({ formModel = linkedSignal({
source: this.info, source: this.info,
computation: (info: ProfitLoss): ProfitLossFormData => ({ computation: (info: ProfitLoss): ProfitLossFormData => ({
startDate: info.startDate ? moment(info.startDate, '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') : moment(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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'amount', 'total']; displayedColumns = ['group', 'name', 'amount', 'total'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Purchase Entries</h2> <h2>Purchase Entries</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (resource.isLoading()) { @if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -17,14 +17,17 @@ import { PurchaseEntries } from './purchase-entries';
import { PurchaseEntriesService } from './purchase-entries.service'; import { PurchaseEntriesService } from './purchase-entries.service';
export interface PurchaseEntriesFormData { export interface PurchaseEntriesFormData {
startDate: moment.Moment | null | string; startDate: Date;
finishDate: moment.Moment | null | string; finishDate: Date;
} }
@Component({ @Component({
selector: 'app-purchase-entries', selector: 'app-purchase-entries',
templateUrl: './purchase-entries.component.html', templateUrl: './purchase-entries.component.html',
styleUrls: ['./purchase-entries.component.css'], styleUrls: ['./purchase-entries.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
@@ -101,10 +104,10 @@ export class PurchaseEntriesComponent {
}); });
formModel = linkedSignal({ formModel = linkedSignal({
source: () => null, source: () => this.info(),
computation: (): PurchaseEntriesFormData => ({ computation: (info: PurchaseEntries): PurchaseEntriesFormData => ({
startDate: moment(new Date()), startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: moment(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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['date', 'supplier', 'product', 'quantity', 'rate', 'tax', 'discount', 'amount']; displayedColumns = ['date', 'supplier', 'product', 'quantity', 'rate', 'tax', 'discount', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import { import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -86,6 +76,13 @@ export interface PurchaseReturnFormModel {
selector: 'app-purchase-return', selector: 'app-purchase-return',
templateUrl: './purchase-return.component.html', templateUrl: './purchase-return.component.html',
styleUrls: ['./purchase-return.component.css'], 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: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -208,25 +205,21 @@ export class PurchaseReturnComponent {
batchesResource = this.batchSer.autocomplete(this.balanceDate, this.batchSearch); batchesResource = this.batchSer.autocomplete(this.balanceDate, this.batchSearch);
batches = computed(() => this.batchesResource.value() ?? []); batches = computed(() => this.batchesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) { focusAccountHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.account().focusBoundControl(); this.form.account().focusBoundControl();
} }
@HostListener('window:keydown.f4', ['$event'])
focusBatchHost(event: Event) { focusBatchHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.addRow.batch().focusBoundControl(); this.form.addRow.batch().focusBoundControl();
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -234,7 +227,6 @@ export class PurchaseReturnComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import { import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -91,6 +81,13 @@ export interface PurchaseFormModel {
selector: 'app-purchase', selector: 'app-purchase',
templateUrl: './purchase.component.html', templateUrl: './purchase.component.html',
styleUrls: ['./purchase.component.css'], 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: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -222,25 +219,21 @@ export class PurchaseComponent {
products = computed(() => this.productsResource.value() ?? []); products = computed(() => this.productsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) { focusAccountHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.account().focusBoundControl(); this.form.account().focusBoundControl();
} }
@HostListener('window:keydown.f4', ['$event'])
focusProductHost(event: Event) { focusProductHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.addRow.product().focusBoundControl(); this.form.addRow.product().focusBoundControl();
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -248,7 +241,6 @@ export class PurchaseComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,6 +1,6 @@
<h2>Purchases</h2> <h2>Purchases</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (resource.isLoading()) { @if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -18,14 +18,17 @@ import { PurchasesItem } from './purchases-item';
import { PurchasesService } from './purchases.service'; import { PurchasesService } from './purchases.service';
export interface PurchasesFormData { export interface PurchasesFormData {
startDate: moment.Moment | null | string; startDate: Date;
finishDate: moment.Moment | null | string; finishDate: Date;
} }
@Component({ @Component({
selector: 'app-purchases', selector: 'app-purchases',
templateUrl: './purchases.component.html', templateUrl: './purchases.component.html',
styleUrls: ['./purchases.component.css'], styleUrls: ['./purchases.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
@@ -93,10 +96,10 @@ export class PurchasesComponent {
}); });
formModel = linkedSignal({ formModel = linkedSignal({
source: () => null, source: () => this.info(),
computation: (): PurchasesFormData => ({ computation: (info: Purchases): PurchasesFormData => ({
startDate: moment(new Date()), startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: moment(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. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'quantity', 'rate', 'amount']; displayedColumns = ['product', 'quantity', 'rate', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -7,7 +7,7 @@
} }
</h2> </h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -21,7 +21,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (infoResource.isLoading()) { @if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -19,14 +19,17 @@ import { RawMaterialCost } from './raw-material-cost';
import { RawMaterialCostService } from './raw-material-cost.service'; import { RawMaterialCostService } from './raw-material-cost.service';
export interface RawMaterialCostFormData { export interface RawMaterialCostFormData {
startDate: Date | null; startDate: Date;
finishDate: Date | null; finishDate: Date;
} }
@Component({ @Component({
selector: 'app-raw-material-cost', selector: 'app-raw-material-cost',
templateUrl: './raw-material-cost.component.html', templateUrl: './raw-material-cost.component.html',
styleUrls: ['./raw-material-cost.component.css'], styleUrls: ['./raw-material-cost.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
MatIconModule, MatIconModule,
@@ -104,10 +107,10 @@ export class RawMaterialCostComponent {
displayedColumns = computed(() => (this.info().id ? this.columnsId : this.columnsNoId)); displayedColumns = computed(() => (this.info().id ? this.columnsId : this.columnsNoId));
formModel = linkedSignal({ formModel = linkedSignal({
source: () => null, source: () => this.info(),
computation: (): RawMaterialCostFormData => ({ computation: (info): RawMaterialCostFormData => ({
startDate: new Date(), startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: 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']; columnsNoId = ['name', 'issue', 'sale', 'rmc'];
columnsId = ['name', 'group', 'quantity', 'net', 'gross']; columnsId = ['name', 'group', 'quantity', 'net', 'gross'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
+10 -18
View File
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@@ -83,6 +73,15 @@ export interface ReceiptFormModel {
selector: 'app-receipt', selector: 'app-receipt',
templateUrl: './receipt.component.html', templateUrl: './receipt.component.html',
styleUrls: ['./receipt.component.css'], 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: [ imports: [
FormField, FormField,
FormRoot, FormRoot,
@@ -199,19 +198,16 @@ export class ReceiptComponent {
receiptAccountsResource = this.accountSer.receiptAutocomplete(signal('')); receiptAccountsResource = this.accountSer.receiptAutocomplete(signal(''));
receiptAccounts = computed(() => this.receiptAccountsResource.value() ?? []); receiptAccounts = computed(() => this.receiptAccountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
} }
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) { focusAccountHost(event: Event) {
event.preventDefault(); event.preventDefault();
this.form.addRow.account().focusBoundControl(); this.form.addRow.account().focusBoundControl();
} }
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) { redirectToPayment(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['payment'], { this.router.navigate(['payment'], {
@@ -223,12 +219,10 @@ export class ReceiptComponent {
}); });
} }
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) { redirectToReciept(event: Event) {
event.preventDefault(); event.preventDefault();
} }
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) { redirectToJournal(event: Event) {
event.preventDefault(); event.preventDefault();
this.router.navigate(['journal'], { this.router.navigate(['journal'], {
@@ -239,7 +233,6 @@ export class ReceiptComponent {
}); });
} }
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) { saveListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.canSave()) { if (this.canSave()) {
@@ -247,7 +240,6 @@ export class ReceiptComponent {
} }
} }
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) { postListner(event: Event) {
event.preventDefault(); event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) { if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,35 +0,0 @@
<h2>RecipeTemplate</h2>
@if (itemResource.isLoading()) {
<app-skeleton-loader type="card" [count]="1" />
<app-skeleton-loader type="line" [count]="10" />
} @else if (itemResource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
} @else {
<form class="flex flex-col wrapped" [formRoot]="form">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Name</mat-label>
<input matInput [formField]="form.name" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Date</mat-label>
<input matInput [matDatepicker]="date" [formField]="form.date" autocomplete="off" #dateElement />
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<mat-checkbox [formField]="form.selected" class="flex-auto">Is Selected?</mat-checkbox>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Text</mat-label>
<textarea matInput matAutosizeMinRows="20" [formField]="form.text"></textarea>
</mat-form-field>
</div>
</form>
<div class="row-container">
<button mat-raised-button color="primary" (click)="save()">Save</button>
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
</div>
}
@@ -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<RecipeTemplateDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RecipeTemplateDetailComponent],
}).compileComponents();
fixture = TestBed.createComponent(RecipeTemplateDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -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;
}
}
@@ -1,47 +0,0 @@
<h2 class="row-container space-between">
<span>Recipe Templates</span>
<a mat-button [routerLink]="['/recipe-templates', 'new']">
<mat-icon>add_box</mat-icon>
Add
</a>
</h2>
@if (listResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
} @else if (listResource.error()) {
<app-error-state [message]="'Failed to load list.'" (retryAction)="listResource.reload()" />
} @else {
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let row"
><a [routerLink]="['/recipe-templates', row.id]">{{ row.name }}</a></mat-cell
>
</ng-container>
<!-- Selected Column -->
<ng-container matColumnDef="selected">
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Selected?</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.selected }}</mat-cell>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.date }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
(page)="handlePageEvent($event)"
[length]="list().length"
[pageIndex]="pageIndex()"
[pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250]"
>
</mat-paginator>
}
@@ -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<RecipeTemplateListComponent>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, RecipeTemplateListComponent],
}).compileComponents();
fixture = TestBed.createComponent(RecipeTemplateListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -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);
@@ -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',
},
},
];
@@ -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();
}));
});
@@ -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<string | null>) {
return httpResource<RecipeTemplate>(() => {
const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
return getUrl;
});
}
list() {
return httpResource<RecipeTemplate[]>(() => `${url}/list`);
}
save(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
return this.http
.post<RecipeTemplate>(`${url}`, recipeTemplate)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'save'))) as Observable<RecipeTemplate>;
}
update(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
return this.http
.put<RecipeTemplate>(`${url}/${recipeTemplate.id}`, recipeTemplate)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'update'))) as Observable<RecipeTemplate>;
}
saveOrUpdate(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
if (!recipeTemplate.id) {
return this.save(recipeTemplate);
}
return this.update(recipeTemplate);
}
delete(id: string): Observable<RecipeTemplate> {
return this.http
.delete<RecipeTemplate>(`${url}/${id}`)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'delete'))) as Observable<RecipeTemplate>;
}
}
@@ -1,16 +0,0 @@
export class RecipeTemplate {
id: string;
name: string;
date: string;
text: string;
selected: boolean;
public constructor(init?: Partial<RecipeTemplate>) {
this.id = '';
this.name = '';
this.date = '';
this.text = '';
this.selected = false;
Object.assign(this, init);
}
}
@@ -312,7 +312,7 @@
<div class="recipe-print__header"> <div class="recipe-print__header">
<h1 class="recipe-print__title">{{ dishName }}</h1> <h1 class="recipe-print__title">{{ dishName }}</h1>
<div class="recipe-print__meta"> <div class="recipe-print__meta">
<span><strong>Date:</strong> {{ formModel().basics.date.format('DD-MMM-YYYY') }}</span> <span><strong>Date:</strong> {{ formModel().basics.date }}</span>
<span><strong>Source:</strong> {{ formModel().basics.source }}</span> <span><strong>Source:</strong> {{ formModel().basics.source }}</span>
<span> <span>
<strong>Yield:</strong> <strong>Yield:</strong>
@@ -63,7 +63,7 @@ export interface IngredientFormData {
export interface RecipeDetailFormData { export interface RecipeDetailFormData {
basics: { basics: {
date: moment.Moment; date: string;
source: string; source: string;
recipeYield: string; recipeYield: string;
product: ProductSku | string | null; product: ProductSku | string | null;
@@ -136,7 +136,7 @@ export class RecipeDetailComponent {
source: this.item, source: this.item,
computation: (itemVal): RecipeDetailFormData => ({ computation: (itemVal): RecipeDetailFormData => ({
basics: { basics: {
date: itemVal.date ? moment(itemVal.date) : moment(new Date()), date: itemVal.date,
source: itemVal.source ?? '', source: itemVal.source ?? '',
recipeYield: `${itemVal.recipeYield ?? ''}`, recipeYield: `${itemVal.recipeYield ?? ''}`,
product: itemVal.sku ?? null, product: itemVal.sku ?? null,
+10 -10
View File
@@ -24,16 +24,16 @@ import { SettingsService } from './settings.service';
import { VoucherTypeService } from './voucher-type.service'; import { VoucherTypeService } from './voucher-type.service';
export interface LockInfoFormData { export interface LockInfoFormData {
validFrom: moment.Moment | null; validFrom: Date | null;
validTill: moment.Moment | null; validTill: Date | null;
index: number; index: number;
lockOlder: boolean; lockOlder: boolean;
olderRolling: boolean; olderRolling: boolean;
olderDate: moment.Moment; olderDate: Date;
olderDays: number; olderDays: number;
lockNewer: boolean; lockNewer: boolean;
newerRolling: boolean; newerRolling: boolean;
newerDate: moment.Moment; newerDate: Date;
newerDays: number; newerDays: number;
accountTypes: { accountType: boolean }[]; accountTypes: { accountType: boolean }[];
voucherTypes: { voucherType: boolean }[]; voucherTypes: { voucherType: boolean }[];
@@ -82,16 +82,16 @@ export class SettingsComponent {
lockInfoModel = linkedSignal({ lockInfoModel = linkedSignal({
source: this.lockInfoSource, source: this.lockInfoSource,
computation: (s): LockInfoFormData => ({ computation: (s): LockInfoFormData => ({
validFrom: null as moment.Moment | null, validFrom: null as Date | null,
validTill: null as moment.Moment | null, validTill: null as Date | null,
index: 0, index: 0,
lockOlder: false, lockOlder: false,
olderRolling: false, olderRolling: false,
olderDate: moment().date(moment().date(1).daysInMonth()), olderDate: moment().date(moment().date(1).daysInMonth()).toDate(),
olderDays: 0, olderDays: 0,
lockNewer: false, lockNewer: false,
newerRolling: false, newerRolling: false,
newerDate: moment().date(moment().date(1).daysInMonth()), newerDate: moment().date(moment().date(1).daysInMonth()).toDate(),
newerDays: 0, newerDays: 0,
accountTypes: s.accTypes.map(() => ({ accountType: false })), accountTypes: s.accTypes.map(() => ({ accountType: false })),
voucherTypes: s.vchTypes.map(() => ({ voucherType: false })), voucherTypes: s.vchTypes.map(() => ({ voucherType: false })),
@@ -133,13 +133,13 @@ export class SettingsComponent {
if (this.lockOlder() && this.olderRolling()) { if (this.lockOlder() && this.olderRolling()) {
item.start.days = model.olderDays ?? 0; item.start.days = model.olderDays ?? 0;
} else if (this.lockOlder() && !this.olderRolling()) { } 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()) { if (this.lockNewer() && this.newerRolling()) {
item.finish.days = model.newerDays ?? 0; item.finish.days = model.newerDays ?? 0;
} }
if (this.lockNewer() && !this.newerRolling()) { 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) => { this.accountTypes().forEach((at: AccountType, index: number) => {
@@ -1,6 +1,6 @@
<h2>Stock Movement</h2> <h2>Stock Movement</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button> <button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div> </div>
</form> </form>
@if (resource.isLoading()) { @if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { DecimalPipe } from '@angular/common'; 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 { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -25,6 +25,9 @@ export interface StockMovementFormData {
selector: 'app-stock-movement', selector: 'app-stock-movement',
templateUrl: './stock-movement.component.html', templateUrl: './stock-movement.component.html',
styleUrls: ['./stock-movement.component.css'], styleUrls: ['./stock-movement.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
@@ -104,7 +107,6 @@ export class StockMovementComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'opening', 'purchase', 'issue', 'closing']; displayedColumns = ['group', 'name', 'opening', 'purchase', 'issue', 'closing'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();
@@ -7,7 +7,7 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-30"> <mat-form-field class="flex-auto basis-30">
<mat-label>Product Group</mat-label> <mat-label>Product Group</mat-label>
<mat-select [formField]="form.productGroup" (selectionChange)="filterOn($event.value)"> <mat-select [formField]="form.productGroup">
<mat-option>-- All Products --</mat-option> <mat-option>-- All Products --</mat-option>
@for (pg of productGroups(); track pg) { @for (pg of productGroups(); track pg) {
<mat-option [value]="pg.id"> <mat-option [value]="pg.id">
@@ -18,10 +18,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
@if (resource.isLoading()) { @if (listResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" /> <app-skeleton-loader type="table" [count]="1" />
} @else if (resource.error()) { } @else if (listResource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="resource.reload()" /> <app-error-state [message]="'Failed to load data.'" (retryAction)="listResource.reload()" />
} @else { } @else {
<mat-table [dataSource]="dataSource()"> <mat-table [dataSource]="dataSource()">
<!-- Name Column --> <!-- Name Column -->
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common'; 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 { FormField, FormRoot, form as createForm } from '@angular/forms/signals';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@@ -7,12 +7,13 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table'; 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 { Product, StockKeepingUnit } from '../../core/product';
import { ProductGroupService } from '../../product-group/product-group.service'; import { ProductGroupService } from '../../product-group/product-group.service';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component'; import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component'; import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { TemporalProduct } from '../temporal-product';
import { TemporalProductService } from '../temporal-product.service'; import { TemporalProductService } from '../temporal-product.service';
export interface TemporalProductListFormData { export interface TemporalProductListFormData {
@@ -42,48 +43,84 @@ export interface TemporalProductListFormData {
], ],
}) })
export class TemporalProductListComponent { export class TemporalProductListComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
private ser = inject(TemporalProductService); private ser = inject(TemporalProductService);
private productGroupSer = inject(ProductGroupService); 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({ formModel = linkedSignal({
source: () => null, source: () => ({ q: this.q(), g: this.g() }),
computation: (): TemporalProductListFormData => ({ computation: (v: { q: string; g: string }): TemporalProductListFormData => ({
filter: '', filter: v.q,
productGroup: '', productGroup: v.g,
}), }),
}); });
form = createForm(this.formModel); 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(); const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
productGroups = computed(() => this.productGroupsResource.value() ?? []);
resource = this.ser.list(); return data.filter((tp: TemporalProduct) => {
const products = tp.products ?? [];
const skus = tp.skus ?? [];
dataSource = computed(() => { const matchesSearch =
let data = this.resource.value() ?? []; tokens.length === 0 ||
const filterValue = (this.formModel().filter ?? '').trim().toLowerCase(); tokens.every(
const groupValue = this.formModel().productGroup; (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) { const matchesProductGroup = !groupId || products.some((k) => (k.productGroup?.id ?? '') === groupId);
data = data.filter((d) => { return matchesSearch && matchesProductGroup;
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;
}); });
dataSource = computed(() => this.filteredList());
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns: string[] = ['product', 'sku', 'info']; displayedColumns: string[] = ['product', 'sku', 'info'];
filterOn(val: string) { constructor() {
this.formModel.update((m) => ({ ...m, productGroup: val })); 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,
});
});
} }
} }
@@ -1,6 +1,6 @@
<h2>Trial Balance</h2> <h2>Trial Balance</h2>
<form class="flex-col" [formRoot]="form"> <form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-4-5"> <mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@@ -8,7 +8,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker> <mat-datepicker #dateInput></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto">Show</button> <button mat-raised-button type="submit" color="primary" class="flex-auto">Show</button>
</div> </div>
</form> </form>
@if (resource.isLoading()) { @if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common'; 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 { FormField, FormRoot, form as createForm } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -25,6 +25,9 @@ export interface TrialBalanceFormData {
selector: 'app-trial-balance', selector: 'app-trial-balance',
templateUrl: './trial-balance.component.html', templateUrl: './trial-balance.component.html',
styleUrls: ['./trial-balance.component.css'], styleUrls: ['./trial-balance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [ imports: [
FormField, FormField,
@@ -100,7 +103,6 @@ export class TrialBalanceComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['type', 'name', 'debit', 'credit']; displayedColumns = ['type', 'name', 'debit', 'credit'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.form().focusBoundControl(); this.form().focusBoundControl();