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