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