Removed Recipe Template as it was not needed.

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

No files matched your search

+321
View File
@@ -0,0 +1,321 @@
---
name: fastapi
description: FastAPI best practices and conventions. Use when working with FastAPI APIs, Pydantic models, dependencies, streaming responses including Server-Sent Events (SSE), and serving frontend apps. Keeps FastAPI code clean and up to date with the latest features and patterns.
---
# FastAPI
Official FastAPI skill to write code with best practices, keeping up to date with new versions and features.
## Quick Reference
* Serve frontend apps: use `app.frontend()` or `router.frontend()` for built frontend assets; see [Serve Frontend Apps](#serve-frontend-apps).
* Server-Sent Events (SSE): use `response_class=EventSourceResponse` and `yield`; see [Streaming](#streaming-json-lines-sse-bytes) and [the streaming reference](references/streaming.md).
* JSON Lines and byte streaming: see [the streaming reference](references/streaming.md).
* Dependencies: use `Annotated[..., Depends(...)]`; see [Dependency Injection](#dependency-injection) and [the dependency injection reference](references/dependencies.md) for `yield`, scopes, and class dependencies.
* Response models: prefer return types; use `response_model` when the public response schema differs from the internal return value; see [the response reference](references/responses.md).
* Pydantic models: do not use ellipsis or `RootModel`; see [the Pydantic reference](references/pydantic.md).
* Routing: declare router-level prefix, tags, and shared dependencies on the `APIRouter`; see [the path operation reference](references/path-operations.md).
* Tooling and related libraries: use uv, Ruff, ty, Asyncer, SQLModel, and HTTPX when applicable; see [the other tools reference](references/other-tools.md).
## Use the `fastapi` CLI
Run the development server on localhost with reload:
```bash
fastapi dev
```
Run the production server:
```bash
fastapi run
```
Prefer declaring the entrypoint in `pyproject.toml`:
```toml
[tool.fastapi]
entrypoint = "my_app.main:app"
```
When adding the entrypoint is not possible, or the user explicitly asks not to, pass the app file path:
```bash
fastapi dev my_app/main.py
```
## Use `Annotated`
Always prefer the `Annotated` style for parameter and dependency declarations. It keeps function signatures working in other contexts, respects the types, and allows reusability.
Use `Annotated` for parameter declarations, including `Path`, `Query`, `Header`, etc.:
```python
from typing import Annotated
from fastapi import FastAPI, Path, Query
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(
item_id: Annotated[int, Path(ge=1, description="The item ID")],
q: Annotated[str | None, Query(max_length=50)] = None,
):
return {"message": "Hello World"}
```
Use `Annotated` for dependencies with `Depends()`. Unless asked not to, create a new type alias for the dependency to allow reusing it:
```python
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_current_user():
return {"username": "johndoe"}
CurrentUserDep = Annotated[dict, Depends(get_current_user)]
@app.get("/items/")
async def read_item(current_user: CurrentUserDep):
return {"message": "Hello World"}
```
## Do not use Ellipsis for *path operations* or Pydantic models
Do not use `...` as a default value for required parameters or model fields. It's not needed and not recommended.
```python
from typing import Annotated
from fastapi import FastAPI, Query
from pydantic import BaseModel, Field
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float = Field(gt=0)
@app.post("/items/")
async def create_item(item: Item, project_id: Annotated[int, Query()]):
return item
```
See [the Pydantic reference](references/pydantic.md) for more details.
## Return Type or Response Model
When possible, include a return type. It will be used to validate, filter, document, and serialize the response.
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
@app.get("/items/me")
async def get_item() -> Item:
return Item(name="Plumbus", description="All-purpose home device")
```
Return types or response models filter data to avoid exposing sensitive information, and they let Pydantic serialize the data on the Rust side for performance.
Use `response_model` when the type you return is not the same as the public schema you want to validate, filter, document, and serialize. See [the response reference](references/responses.md).
## Performance
Do not use `ORJSONResponse` or `UJSONResponse`, they are deprecated.
Instead, declare a return type or response model. Pydantic will handle the data serialization on the Rust side.
## Including Routers
When declaring routers, prefer to add router-level parameters like prefix, tags, and shared dependencies to the router itself instead of in `include_router()`.
```python
from fastapi import APIRouter, Depends, FastAPI
app = FastAPI()
def get_current_user():
return {"username": "johndoe"}
router = APIRouter(
prefix="/items",
tags=["items"],
dependencies=[Depends(get_current_user)],
)
@router.get("/")
async def list_items():
return []
app.include_router(router)
```
See [the path operation reference](references/path-operations.md) for more routing patterns.
## Serve Frontend Apps
Use `app.frontend()` to serve a built static frontend app, for example a directory generated by Vite, Astro, Angular, Svelte, Vue, or a similar tool.
```python
from fastapi import FastAPI
app = FastAPI()
app.frontend("/", directory="dist")
```
Use `router.frontend()` when the frontend belongs to an `APIRouter`; normal router prefix behavior applies when the router is included.
```python
from fastapi import APIRouter, FastAPI
app = FastAPI()
router = APIRouter(prefix="/admin")
router.frontend("/", directory="admin-dist")
app.include_router(router)
```
`app.frontend()` and `router.frontend()` are low-priority routes: regular API routes are matched first, then frontend files and client-side routing fallbacks. Use this for single-page apps and built frontend assets instead of mounting `StaticFiles` manually.
## Dependency Injection
Use dependencies when the logic can't be declared in Pydantic validation, depends on external resources, needs cleanup with `yield`, or is shared across endpoints.
Apply shared dependencies at the router level via `dependencies=[Depends(...)]`.
See [the dependency injection reference](references/dependencies.md) for detailed patterns including `yield` with `scope`, and class dependencies.
## Async vs Sync *path operations*
Use `async` *path operations* only when fully certain that the logic called inside is compatible with async and await, and that it doesn't block.
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/async-items/")
async def read_async_items():
data = await some_async_library.fetch_items()
return data
@app.get("/items/")
def read_items():
data = some_blocking_library.fetch_items()
return data
```
In case of doubt, or by default, use regular `def` functions. They will be run in a threadpool so they don't block the event loop. The same rules apply to dependencies.
Make sure blocking code is not run inside of `async` functions. The logic will work, but will damage performance heavily.
When needing to mix blocking and async code, see Asyncer in [the other tools reference](references/other-tools.md).
## Streaming (JSON Lines, SSE, bytes)
To stream Server-Sent Events, use `response_class=EventSourceResponse` and `yield` items from the endpoint.
```python
from collections.abc import AsyncIterable
from fastapi import FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent
app = FastAPI()
@app.get("/events", response_class=EventSourceResponse)
async def stream_events() -> AsyncIterable[ServerSentEvent]:
yield ServerSentEvent(data={"status": "started"}, event="status", id="1")
```
Plain objects are automatically JSON-serialized as `data:` fields. Use `ServerSentEvent` for full control over SSE fields (`event`, `id`, `retry`, `comment`) and `raw_data` for pre-formatted strings.
See [the streaming reference](references/streaming.md) for JSON Lines, Server-Sent Events (`EventSourceResponse`, `ServerSentEvent`), and byte streaming (`StreamingResponse`) patterns.
## Tooling
See [the other tools reference](references/other-tools.md) for details on uv, Ruff, ty for package management, linting, type checking, formatting, etc.
## Other Libraries
See [the other tools reference](references/other-tools.md) for details on other libraries:
* Asyncer for handling async and await, concurrency, mixing async and blocking code, prefer it over AnyIO or asyncio.
* SQLModel for working with SQL databases, prefer it over SQLAlchemy.
* HTTPX for interacting with HTTP (other APIs), prefer it over Requests.
## Do not use Pydantic RootModels
Do not use Pydantic `RootModel`; instead use regular type annotations with `Annotated` and Pydantic validation utilities.
```python
from typing import Annotated
from fastapi import Body, FastAPI
from pydantic import Field
app = FastAPI()
@app.post("/items/")
async def create_items(items: Annotated[list[int], Field(min_length=1), Body()]):
return items
```
FastAPI supports these type annotations and will create a Pydantic `TypeAdapter` for them, so types work normally without custom wrapper models. See [the Pydantic reference](references/pydantic.md).
## Use one HTTP operation per function
Don't mix HTTP operations in a single function. Having one function per HTTP operation helps separate concerns and organize the code.
```python
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
@app.get("/items/")
async def list_items():
return []
@app.post("/items/")
async def create_item(item: Item):
return item
```
See [the path operation reference](references/path-operations.md) for more examples.
+1 -1
View File
@@ -71,7 +71,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-install-project
COPY /brewman ./
COPY --from=builder /frontend/browser /app/static
COPY --from=builder /frontend/browser /app/frontend
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
+1 -22
View File
@@ -1,24 +1,3 @@
{{ host }} {
# Match and proxy API routes
@apiRoutes {
path_regexp ^/(api|token|refresh|attendance-report|fingerprint-report|db-image)
}
handle @apiRoutes {
reverse_proxy @apiRoutes {{ host_directory }}:80
}
# Match requests that end with .js, .css, .ico, or .html
@staticFiles {
path_regexp \.(js|css|ico|html)$
}
handle @staticFiles {
rewrite * /static{uri}
reverse_proxy {{ host_directory }}:80
}
# All other frontend routes → /static/index.html
handle {
rewrite * /static/index.html
reverse_proxy {{ host_directory }}:80
}
reverse_proxy {{ host_directory }}:80
}
@@ -0,0 +1,66 @@
"""recipe template removed
Revision ID: ee77296e69b7
Revises: 09a4f0ca450f
Create Date: 2026-08-26 05:54:36.396717
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ee77296e69b7"
down_revision = "09a4f0ca450f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("recipe_templates")
op.alter_column(
"recipes",
"recipe_yield",
existing_type=sa.NUMERIC(precision=15, scale=2),
type_=sa.Numeric(precision=12, scale=4),
existing_nullable=False,
)
op.drop_index(op.f("ix_recipes_date"), table_name="recipes")
op.drop_constraint(op.f("uq_recipes_sku_id"), "recipes", type_="unique")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(
op.f("uq_recipes_sku_id"), "recipes", ["sku_id", "date"], postgresql_nulls_not_distinct=False
)
op.create_index(op.f("ix_recipes_date"), "recipes", ["date"], unique=False)
op.alter_column(
"recipes",
"recipe_yield",
existing_type=sa.Numeric(precision=12, scale=4),
type_=sa.NUMERIC(precision=15, scale=2),
existing_nullable=False,
)
op.create_table(
"recipe_templates",
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), autoincrement=False, nullable=False),
sa.Column("name", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("date", sa.DATE(), autoincrement=False, nullable=False),
sa.Column("text", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("selected", sa.BOOLEAN(), autoincrement=False, nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_recipe_templates")),
sa.UniqueConstraint(
"name", name=op.f("uq_recipe_templates_name"), postgresql_include=[], postgresql_nulls_not_distinct=False
),
)
op.create_index(
op.f("only_one_selected_template"),
"recipe_templates",
["selected"],
unique=True,
postgresql_where="(selected = true)",
)
# ### end Alembic commands ###
+2 -2
View File
@@ -17,6 +17,7 @@ from ..models.incentive import Incentive
from ..models.inventory import Inventory
from ..models.journal import Journal
from ..models.login_history import LoginHistory
from ..models.mozimo_stock_register import MozimoStockRegister
from ..models.period import Period
from ..models.permission import Permission
from ..models.price import Price
@@ -30,7 +31,6 @@ from ..models.recipe_item import RecipeItem
from ..models.recipe_photo import RecipePhoto
from ..models.recipe_step import RecipeStep
from ..models.recipe_tag import RecipeTag
from ..models.recipe_template import RecipeTemplate
from ..models.role import Role
from ..models.role_include import RoleInclude
from ..models.role_permission import RolePermission
@@ -63,6 +63,7 @@ __all__ = [
"Inventory",
"Journal",
"LoginHistory",
"MozimoStockRegister",
"Permission",
"Period",
"Price",
@@ -76,7 +77,6 @@ __all__ = [
"RecipePhoto",
"RecipeStep",
"RecipeTag",
"RecipeTemplate",
"reg",
"Role",
"RoleInclude",
-6
View File
@@ -89,9 +89,6 @@ _CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = {
),
"uq_recipe_items_recipe_id": FriendlyDBError(409, "RECIPE_ITEMS_EXISTS", "Items already exist for this recipe."),
"uq_recipe_tags_recipe_id": FriendlyDBError(409, "RECIPE_TAGS_EXISTS", "Tags already exist for this recipe."),
"uq_recipe_templates_name": FriendlyDBError(
409, "RECIPE_TEMPLATE_NAME_EXISTS", "A recipe template with this name already exists."
),
"uq_recipes_sku_id": FriendlyDBError(409, "RECIPE_FOR_SKU_EXISTS", "A recipe already exists for this SKU."),
"uq_tags_name": FriendlyDBError(409, "TAG_NAME_EXISTS", "A tag with this name already exists."),
# --- role_permissions ---
@@ -114,9 +111,6 @@ _CONSTRAINT_FRIENDLY: dict[str, FriendlyDBError] = {
"only_one_valid_attendance": FriendlyDBError(
409, "ATTENDANCE_EXISTS", "A valid attendance already exists for this employee on this date."
),
"only_one_selected_template": FriendlyDBError(
409, "TEMPLATE_ALREADY_SELECTED", "Only one template can be selected at a time."
),
}
# Optional: FK-specific friendly messages (use when you want better UX than generic FK failure)
+1 -4
View File
@@ -4,7 +4,6 @@ import uvicorn
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
from starlette.middleware.sessions import SessionMiddleware
@@ -45,7 +44,6 @@ from .routers import (
rate_contract,
rebase,
recipe,
recipe_template,
role,
tag,
temporal_product,
@@ -124,7 +122,6 @@ app.include_router(product.router, prefix="/api/products", tags=["products"])
app.include_router(temporal_product.router, prefix="/api/temporal-products", tags=["products"])
app.include_router(product_group.router, prefix="/api/product-groups", tags=["products"])
app.include_router(recipe.router, prefix="/api/recipes", tags=["products"])
app.include_router(recipe_template.router, prefix="/api/recipe-templates", tags=["products"])
app.include_router(period.router, prefix="/api/periods", tags=["periods"])
app.include_router(client.router, prefix="/api/clients", tags=["clients"])
@@ -174,7 +171,7 @@ app.include_router(rebase.router, prefix="/api/rebase", tags=["management"])
app.include_router(title.router, prefix="/api/title")
app.include_router(health.router, prefix="/health", tags=["health"])
app.mount("/static", StaticFiles(directory="static"), name="static")
app.frontend("/", directory="frontend", check_dir=False)
def init() -> None:
+7 -3
View File
@@ -24,14 +24,18 @@ class RecipePhoto:
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True)
image_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("images.id"), nullable=False, index=True)
recipe_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("recipes.id", ondelete="CASCADE"), nullable=False, index=True
)
image_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("images.id", ondelete="CASCADE"), nullable=False, index=True
)
order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
caption: Mapped[str] = mapped_column(Text, nullable=False)
recipe: Mapped[Recipe] = relationship("Recipe", back_populates="photos")
image: Mapped[DbImage] = relationship("DbImage")
image: Mapped[DbImage] = relationship("DbImage", cascade="all, delete-orphan", single_parent=True)
def __init__(
self,
+3 -1
View File
@@ -26,7 +26,9 @@ class RecipeStep:
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True)
recipe_id: Mapped[uuid.UUID] = mapped_column(
Uuid, ForeignKey("recipes.id", ondelete="CASCADE"), nullable=False, index=True
)
order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
phase: Mapped[str] = mapped_column(Text, nullable=False) # Prep/Cook/Finish/Serve
-40
View File
@@ -1,40 +0,0 @@
from __future__ import annotations
import uuid
from datetime import date
from sqlalchemy import Boolean, Date, Index, Unicode, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column
from ..db.base_class import reg
@reg.mapped_as_dataclass(unsafe_hash=True)
class RecipeTemplate:
__tablename__ = "recipe_templates"
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
name: Mapped[str] = mapped_column(Unicode, unique=True, nullable=False)
date_: Mapped[date] = mapped_column("date", Date, nullable=False)
text_: Mapped[str] = mapped_column("text", Unicode, nullable=False)
selected: Mapped[bool] = mapped_column(Boolean, nullable=False)
__table_args__ = (
Index(
"only_one_selected_template",
"selected",
unique=True,
postgresql_where=(selected == True), # noqa: E712
),
)
def __init__(self, name: str, date_: date, text: str, selected: bool, id_: uuid.UUID | None = None) -> None:
self.name = name
self.date_ = date_
self.text_ = text
self.selected = selected
if id_ is not None:
self.id = id_
-1
View File
@@ -4,4 +4,3 @@ import enum
class SettingType(enum.IntEnum):
VOUCHER_LOCK = 0
MAINTENANCE_MODE = 1
RECIPE_TEMPLATE = 2
@@ -1,84 +0,0 @@
import datetime
import uuid
from typing import Annotated
from fastapi import APIRouter, Security
from sqlalchemy import delete, select, update
from ..core.security import get_current_active_user as get_user
from ..db.session import SessionDep
from ..models.recipe_template import RecipeTemplate
from ..schemas import recipe_template as schemas
from ..schemas.user import UserToken
router = APIRouter()
@router.post("", response_model=None)
def save(
data: schemas.RecipeTemplateIn, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
date_ = data.date_ if data.date_ is not None else datetime.date.today()
if data.selected:
db.execute(update(RecipeTemplate).values(selected=False))
item = RecipeTemplate(name=data.name, date_=date_, text=data.text, selected=data.selected)
db.add(item)
db.commit()
return None
@router.put("/{id_}", response_model=None)
def update_route(
id_: uuid.UUID,
data: schemas.RecipeTemplateIn,
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
db: SessionDep,
) -> None:
date_ = data.date_ if data.date_ is not None else datetime.date.today()
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
if data.selected and not item.selected:
db.execute(update(RecipeTemplate).where(RecipeTemplate.id != id_).values(selected=False))
item.name = data.name
item.text_ = data.text
item.selected = data.selected
item.date_ = date_
db.commit()
return None
@router.delete("/{id_}", response_model=None)
def delete_route(
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> None:
db.execute(delete(RecipeTemplate).where(RecipeTemplate.id == id_))
db.commit()
return None
@router.get("", response_model=schemas.RecipeTemplateIn)
def show_blank(
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])],
) -> schemas.RecipeTemplateIn:
return schemas.RecipeTemplateIn(name="", date_=datetime.date.today(), text="", selected=False)
@router.get("/list", response_model=list[schemas.RecipeTemplate])
async def show_list(
user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> list[schemas.RecipeTemplate]:
list_ = db.execute(select(RecipeTemplate).order_by(RecipeTemplate.name)).scalars().all()
return [
schemas.RecipeTemplate(id_=i.id, name=i.name, date_=i.date_, text=i.text_, selected=i.selected) for i in list_
]
@router.get("/{id_}", response_model=schemas.RecipeTemplate)
def show_id(
id_: uuid.UUID, user: Annotated[UserToken, Security(get_user, scopes=["recipes"])], db: SessionDep
) -> schemas.RecipeTemplate:
item = db.execute(select(RecipeTemplate).where(RecipeTemplate.id == id_)).scalar_one()
return schemas.RecipeTemplate(
id_=item.id, name=item.name, date_=item.date_, text=item.text_, selected=item.selected
)
+1 -1
View File
@@ -89,7 +89,7 @@ def build_report(
quantity=quantity,
rate=rate,
amount=amount,
url=["/", "product-ledger", str(product.id)],
url=["/", "product-ledger", str(product.product_id)],
)
body.append(row)
return (
@@ -1,40 +0,0 @@
import uuid
from datetime import date, datetime
from pydantic import (
BaseModel,
ConfigDict,
FieldSerializationInfo,
field_serializer,
field_validator,
)
from . import to_camel
class RecipeTemplateIn(BaseModel):
name: str
date_: date | None = None
text: str
selected: bool
@field_validator("date_", mode="before")
@classmethod
def parse_valid_from(cls, value: date | str | None) -> date | None:
if value is None:
return None
if isinstance(value, date):
return value
return datetime.strptime(value, "%d-%b-%Y").date()
@field_serializer("date_")
def serialize_date(self, value: date | None, info: FieldSerializationInfo) -> str | None:
return None if value is None else value.strftime("%d-%b-%Y")
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class RecipeTemplate(RecipeTemplateIn):
id_: uuid.UUID
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+1 -1
View File
@@ -211,7 +211,7 @@ wheels = [
[[package]]
name = "brewman"
version = "14.2.0"
version = "15.0.0"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
-4
View File
@@ -65,10 +65,6 @@ export const routes: Routes = [
path: 'recipes',
loadChildren: () => import('./recipe/recipe.routes').then((mod) => mod.routes),
},
{
path: 'recipe-templates',
loadChildren: () => import('./recipe-template/recipe-template.routes').then((mod) => mod.routes),
},
{
path: 'roles',
loadChildren: () => import('./role/role.routes').then((mod) => mod.routes),
@@ -1,6 +1,6 @@
<h2>Attendance</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label>
@@ -15,7 +15,7 @@
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
<button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div>
@if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
@@ -1,4 +1,4 @@
import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core';
import { Component, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
@@ -22,7 +22,7 @@ import { AttendanceTypeService } from './attendance-type.service';
import { AttendanceService } from './attendance.service';
export interface AttendanceFormData {
date: moment.Moment;
date: Date;
attendances: { attendanceType: number | null }[];
}
@@ -30,6 +30,9 @@ export interface AttendanceFormData {
selector: 'app-attendance',
templateUrl: './attendance.component.html',
styleUrls: ['./attendance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -64,7 +67,7 @@ export class AttendanceComponent {
model = linkedSignal({
source: this.info,
computation: (infoVal): AttendanceFormData => ({
date: moment(infoVal.date),
date: moment(infoVal.date, 'DD-MMM-YYYY').toDate(),
attendances: infoVal.body.map((x) => ({
attendanceType: x.attendanceType.id,
})),
@@ -75,7 +78,6 @@ export class AttendanceComponent {
displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Balance Sheet</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Date</mat-label>
@@ -8,7 +8,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,8 +1,8 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core';
import { Component, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface BalanceSheetFormData {
date: moment.Moment;
date: Date;
}
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -24,6 +24,9 @@ import { BalanceSheetService } from './balance-sheet.service';
selector: 'app-balance-sheet',
templateUrl: './balance-sheet.component.html',
styleUrls: ['./balance-sheet.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -56,7 +59,7 @@ export class BalanceSheetComponent {
formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({
source: this.info,
computation: (info: BalanceSheet): BalanceSheetFormData => ({
date: info.date ? moment(info.date, 'DD-MMM-YYYY') : moment(new Date()),
date: info.date ? moment(info.date, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -103,7 +106,6 @@ export class BalanceSheetComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'subAmount', 'total'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Cash Flow</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
<button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,9 +1,9 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core';
import { Component, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface CashFlowFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
startDate: Date;
finishDate: Date;
}
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -24,6 +24,9 @@ import { CashFlowService } from './cash-flow.service';
selector: 'app-cash-flow',
templateUrl: './cash-flow.component.html',
styleUrls: ['./cash-flow.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -78,8 +81,8 @@ export class CashFlowComponent {
model = linkedSignal({
source: this.info,
computation: (info: CashFlow): CashFlowFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -88,7 +91,6 @@ export class CashFlowComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
// TODO: also select the contents
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Department</mat-label>
@@ -25,7 +25,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
<button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div>
@if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { Component, HostListener, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core';
import { Component, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
@@ -29,7 +29,7 @@ import { ClosingStockItem } from './closing-stock-item';
import { ClosingStockService } from './closing-stock.service';
export interface ClosingStockFormData {
date: moment.Moment;
date: Date;
costCentre: CostCentre | null;
stocks: { physical: number; costCentre: string | undefined }[];
}
@@ -38,6 +38,9 @@ export interface ClosingStockFormData {
selector: 'app-closing-stock',
templateUrl: './closing-stock.component.html',
styleUrls: ['./closing-stock.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
MatIconModule,
FormField,
@@ -83,7 +86,7 @@ export class ClosingStockComponent {
model = linkedSignal({
source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })),
computation: ({ info, costCentres }): ClosingStockFormData => ({
date: moment(info.date, 'DD-MMM-YYYY'),
date: moment(info.date, 'DD-MMM-YYYY').toDate(),
costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null,
stocks: info.items.map((x) => ({
physical: x.physical,
@@ -130,7 +133,6 @@ export class ClosingStockComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -165,7 +167,7 @@ export class ClosingStockComponent {
getClosingStock(): ClosingStock {
const formModel = this.model();
const currentInfo = this.info();
currentInfo.date = formModel.date.format('DD-MMM-YYYY');
currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = formModel.stocks;
currentInfo.items.forEach((item, index) => {
item.physical = array[index].physical ?? 0;
@@ -178,7 +180,7 @@ export class ClosingStockComponent {
const formModel = this.model();
return new ClosingStock({
date: formModel.date.format('DD-MMM-YYYY'),
date: moment(formModel.date).format('DD-MMM-YYYY'),
costCentre: new CostCentre({ id: formModel.costCentre?.id ?? '' }),
});
}
@@ -58,7 +58,6 @@
<a mat-menu-item routerLink="/products">Products</a>
<a mat-menu-item routerLink="/product-groups">Product Groups</a>
<a mat-menu-item routerLink="/recipes">Recipes</a>
<a mat-menu-item routerLink="/recipe-templates">Recipe Templates</a>
<a mat-menu-item routerLink="/periods">Periods</a>
<a mat-menu-item routerLink="/tags">Tags</a>
</mat-menu>
@@ -1,6 +1,6 @@
<h2>Daybook</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, linkedSignal, input, computed, afterNextRender, signal } from '@angular/core';
import { Component, inject, linkedSignal, input, computed, afterNextRender, signal } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface DaybookFormData {
startDate: Date;
@@ -24,6 +24,9 @@ import { DaybookService } from './daybook.service';
selector: 'app-daybook',
templateUrl: './daybook.component.html',
styleUrls: ['./daybook.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -107,7 +110,6 @@ export class DaybookComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['date', 'type', 'narration', 'debitText', 'debitAmount', 'creditText', 'creditAmount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
// TODO: Also select the text
@@ -1,6 +1,6 @@
<h2>Employee Attendance</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-1-4">
<mat-label>Start Date</mat-label>
@@ -48,7 +48,7 @@
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
<button mat-raised-button type="submit" color="primary" class="flex-auto basis-1-5">Show</button>
</div>
@if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
@@ -1,13 +1,4 @@
import {
computed,
Component,
HostListener,
inject,
debounced,
input,
linkedSignal,
afterNextRender,
} from '@angular/core';
import { computed, Component, inject, debounced, input, linkedSignal, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -42,6 +33,9 @@ export interface EmployeeAttendanceFormData {
selector: 'app-employee-attendance',
templateUrl: './employee-attendance.component.html',
styleUrls: ['./employee-attendance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -101,7 +95,6 @@ export class EmployeeAttendanceComponent {
employeesResource = this.employeeSer.autocomplete(computed(() => this.debouncedEmployeeSearch.value() ?? ''));
employees = computed(() => this.employeesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { afterNextRender, Component, computed, HostListener, inject, input, linkedSignal, signal } from '@angular/core';
import { afterNextRender, Component, computed, inject, input, linkedSignal, signal } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -48,6 +48,9 @@ export interface EmployeeBenefitsFormData {
selector: 'app-employee-benefits',
templateUrl: './employee-benefits.component.html',
styleUrls: ['./employee-benefits.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -122,7 +125,6 @@ export class EmployeeBenefitsComponent {
employeesResource = this.employeeSer.autocomplete(computed(() => this.employeesSearch() ?? ''));
employees = computed(() => this.employeesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form.date().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Entries</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); refresh()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Create / Last Edit Date From</mat-label>
@@ -20,9 +20,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button (click)="refresh()" class="flex-auto basis-1-5">
<mat-icon>refresh</mat-icon>Refresh
</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5"><mat-icon>refresh</mat-icon>Refresh</button>
</div>
<div class="row-container">
<mat-radio-group class="flex-auto basis-2-5" [formField]="form.posted" (change)="togglePosted($event)">
+4 -11
View File
@@ -1,15 +1,6 @@
/* eslint-disable @angular-eslint/no-input-rename */
import { CurrencyPipe } from '@angular/common';
import {
Component,
HostListener,
inject,
input,
computed,
booleanAttribute,
afterNextRender,
linkedSignal,
} from '@angular/core';
import { Component, inject, input, computed, booleanAttribute, afterNextRender, linkedSignal } from '@angular/core';
export interface EntriesFormData {
startDate: Date;
finishDate: Date;
@@ -40,6 +31,9 @@ import { Report } from './report';
selector: 'app-entries',
templateUrl: './entries.component.html',
styleUrls: ['./entries.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -120,7 +114,6 @@ export class EntriesComponent {
displayedColumns = ['date', 'voucherType', 'narration', 'debitNames', 'creditNames', 'amount', 'user'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { signal, Component, HostListener, inject, input, computed, linkedSignal } from '@angular/core';
import { signal, Component, inject, input, computed, linkedSignal } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -23,7 +23,7 @@ import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.
import { LocalTimePipe } from '../shared/local-time.pipe';
export interface IncentiveFormData {
date: moment.Moment;
date: Date;
incentives: { points: number }[];
}
@@ -31,6 +31,9 @@ export interface IncentiveFormData {
selector: 'app-incentive',
templateUrl: './incentive.component.html',
styleUrls: ['./incentive.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -61,7 +64,7 @@ export class IncentiveComponent {
model = linkedSignal({
source: this.voucher,
computation: (v): IncentiveFormData => ({
date: moment(v.date, 'DD-MMM-YYYY'),
date: moment(v.date, 'DD-MMM-YYYY').toDate(),
incentives: v.incentives.map((x) => ({ points: x.points })),
}),
});
@@ -75,7 +78,6 @@ export class IncentiveComponent {
displayedColumns = ['name', 'designation', 'department', 'daysWorked', 'points', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -159,7 +161,7 @@ export class IncentiveComponent {
getVoucher(): Voucher {
const formModel = this.model();
const v = this.voucher();
v.date = formModel.date.format('DD-MMM-YYYY');
v.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = formModel.incentives;
v.incentives.forEach((item: Incentive, index: number) => {
+7 -15
View File
@@ -1,15 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import {
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -76,6 +66,12 @@ export interface IssueFormModel {
selector: 'app-issue',
templateUrl: './issue.component.html',
styleUrls: ['./issue.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f3)': 'focusBatchHost($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
FormField,
FormRoot,
@@ -174,19 +170,16 @@ export class IssueComponent {
issueGridResource = this.issueGridSer.issueGrid(this.balanceDate);
gridData = computed(() => this.issueGridResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f3', ['$event'])
focusBatchHost(event: Event) {
event.preventDefault();
this.form.addRow.batch().focusBoundControl();
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -194,7 +187,6 @@ export class IssueComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
+12 -10
View File
@@ -1,6 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, input, computed, signal, linkedSignal, afterNextRender } from '@angular/core';
import { Component, inject, input, computed, signal, linkedSignal, afterNextRender } from '@angular/core';
import { FormField, FormRoot, form } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -72,6 +72,15 @@ export interface JournalFormModel {
selector: 'app-journal',
templateUrl: './journal.component.html',
styleUrls: ['./journal.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f3)': 'focusAccountHost($event)',
'(window:keydown.f5)': 'redirectToPayment($event)',
'(window:keydown.f6)': 'redirectToReciept($event)',
'(window:keydown.f7)': 'redirectToJournal($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
MatIconModule,
MatFormFieldModule,
@@ -112,19 +121,16 @@ export class JournalComponent {
itemResource = this.ser.getVoucher(this.id, this.type, signal(null), this.d);
item = linkedSignal(() => this.itemResource.value() ?? new Voucher());
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) {
event.preventDefault();
this.form.addRow.account().focusBoundControl();
}
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) {
event.preventDefault();
this.router.navigate(['payment'], {
@@ -135,7 +141,6 @@ export class JournalComponent {
});
}
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) {
event.preventDefault();
this.router.navigate(['receipt'], {
@@ -146,12 +151,10 @@ export class JournalComponent {
});
}
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) {
event.preventDefault();
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -159,7 +162,6 @@ export class JournalComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted() && this.auth.allowed('post-vouchers')) {
@@ -233,9 +235,9 @@ export class JournalComponent {
});
}
dateChanged(newDate: moment.Moment) {
dateChanged(newDate: Date) {
this.router.navigate([], {
queryParams: { d: newDate.format('DD-MMM-YYYY') },
queryParams: { d: moment(newDate).format('DD-MMM-YYYY') },
replaceUrl: true,
queryParamsHandling: 'merge',
});
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label>
@@ -33,18 +33,13 @@
[formField]="form.account"
autocomplete="off"
/>
<mat-autocomplete
#auto="matAutocomplete"
autoActiveFirstOption
[displayWith]="displayFn"
(optionSelected)="selected($event)"
>
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption [displayWith]="displayFn">
@for (account of accounts(); track account) {
<mat-option [value]="account">{{ account.name }}</mat-option>
}
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button type="button" class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
<div class="row-container">
<mat-form-field class="flex-auto basis-4-5">
@@ -55,7 +50,7 @@
}
</mat-select>
</mat-form-field>
<button mat-raised-button tabindex="-1" extended class="flex-auto basis-1-5" (click)="labels()">
<button mat-raised-button tabindex="-1" type="button" extended class="flex-auto basis-1-5" (click)="labels()">
<mat-icon>label</mat-icon>
Manage Labels
</button>
+14 -14
View File
@@ -1,8 +1,8 @@
import { SelectionModel } from '@angular/cdk/collections';
import { CurrencyPipe } from '@angular/common';
import { Component, inject, input, linkedSignal, computed, signal, afterNextRender, HostListener } from '@angular/core';
import { Component, inject, input, linkedSignal, computed, signal, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
@@ -27,8 +27,8 @@ import { ToCsvService } from '../shared/to-csv.service';
import { TagDialogComponent } from '../tag-dialog/tag-dialog.component';
import { TagService } from '../tag/tag.service';
export interface LedgerFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
startDate: Date;
finishDate: Date;
account: string | Account | null;
tags: string[];
}
@@ -42,6 +42,9 @@ import { LedgerService } from './ledger.service';
selector: 'app-ledger',
templateUrl: './ledger.component.html',
styleUrls: ['./ledger.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
MatCheckboxModule,
MatChipsModule,
@@ -144,9 +147,9 @@ export class LedgerComponent {
model = linkedSignal<import('./ledger').Ledger, LedgerFormData>({
source: this.info,
computation: (info: Ledger): LedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
account: info.account?.name ?? '',
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
account: info.account ?? null,
tags: this.tags(),
}),
});
@@ -165,7 +168,6 @@ export class LedgerComponent {
private accountsResource = this.accountSer.autocomplete(this.accountSearch);
accounts = computed(() => this.accountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -177,14 +179,10 @@ export class LedgerComponent {
});
}
displayFn(account?: Account | string): string {
displayFn(account: Account | string | null): string {
return !account ? '' : typeof account === 'string' ? account : account.name;
}
selected(event: MatAutocompleteSelectedEvent): void {
this.info().account = event.option.value as Account;
}
selectRow(id: string): void {
this.selectedRowId = id;
}
@@ -247,9 +245,11 @@ export class LedgerComponent {
getInfo(): Ledger {
const formModel = this.model();
const accVal = formModel.account;
const account = accVal && typeof accVal !== 'string' ? accVal : undefined;
return new Ledger({
account: this.info().account,
account,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
});
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label>
@@ -22,7 +22,7 @@
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
@if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
@@ -25,7 +25,7 @@ import { MozimoDailyRegisterItem } from './mozimo-daily-register-item';
import { MozimoDailyRegisterService } from './mozimo-daily-register.service';
export interface MozimoDailyRegisterFormData {
date: moment.Moment;
date: Date;
items: {
received: number;
sale: number;
@@ -79,7 +79,7 @@ export class MozimoDailyRegisterComponent {
model = linkedSignal({
source: this.info,
computation: (v): MozimoDailyRegisterFormData => ({
date: moment(v.date, 'DD-MMM-YYYY'),
date: moment(v.date, 'DD-MMM-YYYY').toDate(),
items: v.body.map((x) => ({
received: x.received,
sale: x.sale,
@@ -129,7 +129,7 @@ export class MozimoDailyRegisterComponent {
getMozimoDailyRegister(): MozimoDailyRegister {
const formModel = this.model();
const info = this.info();
info.date = formModel.date.format('DD-MMM-YYYY');
info.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = formModel.items;
if (array) {
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label>
@@ -44,7 +44,7 @@
}
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
@if (infoResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
@@ -27,8 +27,8 @@ import { MozimoProductRegisterItem } from './mozimo-product-register-item';
import { MozimoProductRegisterService } from './mozimo-product-register.service';
export interface MozimoProductRegisterFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
startDate: Date;
finishDate: Date;
product: Product | string | null;
items: {
received: number;
@@ -84,11 +84,11 @@ export class MozimoProductRegisterComponent {
model = linkedSignal({
source: this.info,
computation: (v): MozimoProductRegisterFormData => ({
startDate: moment(v.startDate, 'DD-MMM-YYYY'),
finishDate: moment(v.finishDate, 'DD-MMM-YYYY'),
product: v.product ?? '',
items: v.body.map((x) => ({
computation: (info): MozimoProductRegisterFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
product: info.product ?? '',
items: info.body.map((x) => ({
received: x.received,
sale: x.sale,
nc: x.nc,
@@ -160,8 +160,8 @@ export class MozimoProductRegisterComponent {
getMozimoProductRegister(): MozimoProductRegister {
const formModel = this.model();
const info = this.info();
info.startDate = formModel.startDate.format('DD-MMM-YYYY');
info.finishDate = formModel.finishDate.format('DD-MMM-YYYY');
info.startDate = moment(formModel.startDate).format('DD-MMM-YYYY');
info.finishDate = moment(formModel.finishDate).format('DD-MMM-YYYY');
const array = formModel.items;
if (array) {
@@ -183,8 +183,8 @@ export class MozimoProductRegisterComponent {
return new MozimoProductRegister({
product: formModel.product as Product,
startDate: formModel.startDate.format('DD-MMM-YYYY'),
finishDate: formModel.finishDate.format('DD-MMM-YYYY'),
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
});
}
@@ -1,6 +1,6 @@
<h2>Net Transactions</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, input, linkedSignal, computed, afterNextRender, signal } from '@angular/core';
import { Component, inject, input, linkedSignal, computed, afterNextRender, signal } from '@angular/core';
export interface NetTransactionsFormData {
startDate: Date;
finishDate: Date;
@@ -25,6 +25,9 @@ import { NetTransactionsService } from './net-transactions.service';
selector: 'app-net-transactions',
templateUrl: './net-transactions.component.html',
styleUrls: ['./net-transactions.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -103,7 +106,6 @@ export class NetTransactionsComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['type', 'name', 'debit', 'credit'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
// TODO: Also select the text
+9 -17
View File
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common';
import {
Component,
HostListener,
inject,
computed,
effect,
signal,
input,
linkedSignal,
afterNextRender,
} from '@angular/core';
import { Component, inject, computed, effect, signal, input, linkedSignal, afterNextRender } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -83,6 +73,14 @@ export interface PaymentFormModel {
selector: 'app-payment',
templateUrl: './payment.component.html',
styleUrls: ['./payment.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f5)': 'redirectToPayment($event)',
'(window:keydown.f6)': 'redirectToReciept($event)',
'(window:keydown.f7)': 'redirectToJournal($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
FormField,
FormRoot,
@@ -199,18 +197,15 @@ export class PaymentComponent {
paymentAccountsResource = this.accountSer.paymentAutocomplete(signal(''));
paymentAccounts = computed(() => this.paymentAccountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) {
event.preventDefault();
}
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) {
event.preventDefault();
this.router.navigate(['receipt'], {
@@ -222,7 +217,6 @@ export class PaymentComponent {
});
}
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) {
event.preventDefault();
this.router.navigate(['journal'], {
@@ -233,7 +227,6 @@ export class PaymentComponent {
});
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -241,7 +234,6 @@ export class PaymentComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,7 +1,7 @@
import { Component, HostListener, inject, linkedSignal, input, computed } from '@angular/core';
import { Component, inject, linkedSignal, input, computed } from '@angular/core';
export interface PeriodFormData {
validFrom: moment.Moment;
validTill: moment.Moment;
validFrom: Date;
validTill: Date;
}
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
@@ -23,6 +23,9 @@ import { PeriodService } from '../period.service';
selector: 'app-period-detail',
templateUrl: './period-detail.component.html',
styleUrls: ['./period-detail.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
FormRoot,
@@ -49,14 +52,13 @@ export class PeriodDetailComponent {
model = linkedSignal({
source: this.item,
computation: (item): PeriodFormData => ({
validFrom: item.validFrom ? moment(item.validFrom, 'DD-MMM-YYYY') : moment(new Date()),
validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY') : moment(new Date()),
validFrom: item.validFrom ? moment(item.validFrom, 'DD-MMM-YYYY').toDate() : new Date(),
validTill: item.validTill ? moment(item.validTill, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
form = createForm(this.model);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Start Date</mat-label>
@@ -44,7 +44,7 @@
}
</mat-autocomplete>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,16 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import {
Component,
HostListener,
inject,
computed,
signal,
linkedSignal,
ResourceRef,
Signal,
input,
afterNextRender,
} from '@angular/core';
import { Component, inject, computed, signal, linkedSignal, input, afterNextRender } from '@angular/core';
import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -30,9 +19,9 @@ import { ProductSku } from '../core/product-sku';
import { ProductService } from '../product/product.service';
import { ToCsvService } from '../shared/to-csv.service';
export interface ProductLedgerFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
product: string;
startDate: Date;
finishDate: Date;
product: Product | string | null;
}
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
@@ -43,6 +32,9 @@ import { ProductLedgerService } from './product-ledger.service';
selector: 'app-product-ledger',
templateUrl: './product-ledger.component.html',
styleUrls: ['./product-ledger.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
MatIconModule,
@@ -60,7 +52,6 @@ import { ProductLedgerService } from './product-ledger.service';
MatPaginatorModule,
DecimalPipe,
CurrencyPipe,
SkeletonLoaderComponent,
ErrorStateComponent,
],
@@ -161,9 +152,9 @@ export class ProductLedgerComponent {
model = linkedSignal({
source: this.info,
computation: (info: ProductLedger): ProductLedgerFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
product: info.product?.name ?? '',
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
product: info.product ?? null,
}),
});
@@ -185,11 +176,14 @@ export class ProductLedgerComponent {
'runningAmount',
];
productSearch!: Signal<string | null>;
productsResource!: ResourceRef<ProductSku[] | undefined>;
products!: Signal<ProductSku[]>;
productSearch = computed(() => {
const v = this.model().product;
return typeof v === 'string' ? v : null;
});
productsResource = this.productSer.autocompleteProduct(this.productSearch, signal(null));
products = computed(() => this.productsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -199,21 +193,15 @@ export class ProductLedgerComponent {
afterNextRender(() => {
this.form().focusBoundControl();
});
this.productSearch = computed(() => {
const v = this.model().product;
return typeof v === 'string' ? v : null;
});
this.productsResource = this.productSer.autocompleteProduct(this.productSearch, signal(null));
this.products = computed(() => this.productsResource.value() ?? []);
}
displayFn(product?: Product | string): string {
displayFn(product?: Product | ProductSku | string | null): string {
return !product ? '' : typeof product === 'string' ? product : product.name;
}
selected(event: MatAutocompleteSelectedEvent): void {
const p = event.option.value;
this.model.update((m) => ({ ...m, product: p.name }));
this.model.update((m) => ({ ...m, product: p }));
}
selectRow(id: string): void {
@@ -234,9 +222,10 @@ export class ProductLedgerComponent {
getInfo(): ProductLedger {
const formModel = this.model();
const prodVal = formModel.product;
const product = prodVal && typeof prodVal !== 'string' ? prodVal : undefined;
return new ProductLedger({
product: this.info().product,
product,
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
});
@@ -1,6 +1,6 @@
<h2>Profit & Loss</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, linkedSignal, computed, signal, input, afterNextRender } from '@angular/core';
import { Component, inject, linkedSignal, computed, signal, input, afterNextRender } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -17,14 +17,17 @@ import { ProfitLoss } from './profit-loss';
import { ProfitLossService } from './profit-loss.service';
export interface ProfitLossFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
startDate: Date;
finishDate: Date;
}
@Component({
selector: 'app-profit-loss',
templateUrl: './profit-loss.component.html',
styleUrls: ['./profit-loss.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
@@ -57,8 +60,8 @@ export class ProfitLossComponent {
formModel = linkedSignal({
source: this.info,
computation: (info: ProfitLoss): ProfitLossFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -103,7 +106,6 @@ export class ProfitLossComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'amount', 'total'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,6 +1,6 @@
<h2>Purchase Entries</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -17,14 +17,17 @@ import { PurchaseEntries } from './purchase-entries';
import { PurchaseEntriesService } from './purchase-entries.service';
export interface PurchaseEntriesFormData {
startDate: moment.Moment | null | string;
finishDate: moment.Moment | null | string;
startDate: Date;
finishDate: Date;
}
@Component({
selector: 'app-purchase-entries',
templateUrl: './purchase-entries.component.html',
styleUrls: ['./purchase-entries.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
@@ -101,10 +104,10 @@ export class PurchaseEntriesComponent {
});
formModel = linkedSignal({
source: () => null,
computation: (): PurchaseEntriesFormData => ({
startDate: moment(new Date()),
finishDate: moment(new Date()),
source: () => this.info(),
computation: (info: PurchaseEntries): PurchaseEntriesFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -114,7 +117,6 @@ export class PurchaseEntriesComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['date', 'supplier', 'product', 'quantity', 'rate', 'tax', 'discount', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import {
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -86,6 +76,13 @@ export interface PurchaseReturnFormModel {
selector: 'app-purchase-return',
templateUrl: './purchase-return.component.html',
styleUrls: ['./purchase-return.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f3)': 'focusAccountHost($event)',
'(window:keydown.f4)': 'focusBatchHost($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
FormField,
FormRoot,
@@ -208,25 +205,21 @@ export class PurchaseReturnComponent {
batchesResource = this.batchSer.autocomplete(this.balanceDate, this.batchSearch);
batches = computed(() => this.batchesResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) {
event.preventDefault();
this.form.account().focusBoundControl();
}
@HostListener('window:keydown.f4', ['$event'])
focusBatchHost(event: Event) {
event.preventDefault();
this.form.addRow.batch().focusBoundControl();
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -234,7 +227,6 @@ export class PurchaseReturnComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import {
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -91,6 +81,13 @@ export interface PurchaseFormModel {
selector: 'app-purchase',
templateUrl: './purchase.component.html',
styleUrls: ['./purchase.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f3)': 'focusAccountHost($event)',
'(window:keydown.f4)': 'focusProductHost($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
FormField,
FormRoot,
@@ -222,25 +219,21 @@ export class PurchaseComponent {
products = computed(() => this.productsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) {
event.preventDefault();
this.form.account().focusBoundControl();
}
@HostListener('window:keydown.f4', ['$event'])
focusProductHost(event: Event) {
event.preventDefault();
this.form.addRow.product().focusBoundControl();
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -248,7 +241,6 @@ export class PurchaseComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,6 +1,6 @@
<h2>Purchases</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -18,14 +18,17 @@ import { PurchasesItem } from './purchases-item';
import { PurchasesService } from './purchases.service';
export interface PurchasesFormData {
startDate: moment.Moment | null | string;
finishDate: moment.Moment | null | string;
startDate: Date;
finishDate: Date;
}
@Component({
selector: 'app-purchases',
templateUrl: './purchases.component.html',
styleUrls: ['./purchases.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
@@ -93,10 +96,10 @@ export class PurchasesComponent {
});
formModel = linkedSignal({
source: () => null,
computation: (): PurchasesFormData => ({
startDate: moment(new Date()),
finishDate: moment(new Date()),
source: () => this.info(),
computation: (info: Purchases): PurchasesFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -106,7 +109,6 @@ export class PurchasesComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'quantity', 'rate', 'amount'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -7,7 +7,7 @@
}
</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -21,7 +21,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (infoResource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe, DecimalPipe, PercentPipe } from '@angular/common';
import { Component, HostListener, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { Component, inject, linkedSignal, computed, signal, input } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -19,14 +19,17 @@ import { RawMaterialCost } from './raw-material-cost';
import { RawMaterialCostService } from './raw-material-cost.service';
export interface RawMaterialCostFormData {
startDate: Date | null;
finishDate: Date | null;
startDate: Date;
finishDate: Date;
}
@Component({
selector: 'app-raw-material-cost',
templateUrl: './raw-material-cost.component.html',
styleUrls: ['./raw-material-cost.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
MatIconModule,
@@ -104,10 +107,10 @@ export class RawMaterialCostComponent {
displayedColumns = computed(() => (this.info().id ? this.columnsId : this.columnsNoId));
formModel = linkedSignal({
source: () => null,
computation: (): RawMaterialCostFormData => ({
startDate: new Date(),
finishDate: new Date(),
source: () => this.info(),
computation: (info): RawMaterialCostFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY').toDate() : new Date(),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY').toDate() : new Date(),
}),
});
@@ -122,7 +125,6 @@ export class RawMaterialCostComponent {
columnsNoId = ['name', 'issue', 'sale', 'rmc'];
columnsId = ['name', 'group', 'quantity', 'net', 'gross'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
+10 -18
View File
@@ -1,16 +1,6 @@
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { CurrencyPipe } from '@angular/common';
import {
afterNextRender,
Component,
computed,
effect,
HostListener,
inject,
input,
linkedSignal,
signal,
} from '@angular/core';
import { afterNextRender, Component, computed, effect, inject, input, linkedSignal, signal } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
@@ -83,6 +73,15 @@ export interface ReceiptFormModel {
selector: 'app-receipt',
templateUrl: './receipt.component.html',
styleUrls: ['./receipt.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
'(window:keydown.f3)': 'focusAccountHost($event)',
'(window:keydown.f5)': 'redirectToPayment($event)',
'(window:keydown.f6)': 'redirectToReciept($event)',
'(window:keydown.f7)': 'redirectToJournal($event)',
'(window:keydown.control.s)': 'saveListner($event)',
'(window:keydown.control.p)': 'postListner($event)',
},
imports: [
FormField,
FormRoot,
@@ -199,19 +198,16 @@ export class ReceiptComponent {
receiptAccountsResource = this.accountSer.receiptAutocomplete(signal(''));
receiptAccounts = computed(() => this.receiptAccountsResource.value() ?? []);
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
}
@HostListener('window:keydown.f3', ['$event'])
focusAccountHost(event: Event) {
event.preventDefault();
this.form.addRow.account().focusBoundControl();
}
@HostListener('window:keydown.f5', ['$event'])
redirectToPayment(event: Event) {
event.preventDefault();
this.router.navigate(['payment'], {
@@ -223,12 +219,10 @@ export class ReceiptComponent {
});
}
@HostListener('window:keydown.f6', ['$event'])
redirectToReciept(event: Event) {
event.preventDefault();
}
@HostListener('window:keydown.f7', ['$event'])
redirectToJournal(event: Event) {
event.preventDefault();
this.router.navigate(['journal'], {
@@ -239,7 +233,6 @@ export class ReceiptComponent {
});
}
@HostListener('window:keydown.control.s', ['$event'])
saveListner(event: Event) {
event.preventDefault();
if (this.canSave()) {
@@ -247,7 +240,6 @@ export class ReceiptComponent {
}
}
@HostListener('window:keydown.control.p', ['$event'])
postListner(event: Event) {
event.preventDefault();
if (this.id() && !this.form.posted().controlValue() && this.auth.allowed('post-vouchers')) {
@@ -1,35 +0,0 @@
<h2>RecipeTemplate</h2>
@if (itemResource.isLoading()) {
<app-skeleton-loader type="card" [count]="1" />
<app-skeleton-loader type="line" [count]="10" />
} @else if (itemResource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
} @else {
<form class="flex flex-col wrapped" [formRoot]="form">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Name</mat-label>
<input matInput [formField]="form.name" />
</mat-form-field>
<mat-form-field class="flex-auto">
<mat-label>Date</mat-label>
<input matInput [matDatepicker]="date" [formField]="form.date" autocomplete="off" #dateElement />
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date></mat-datepicker>
</mat-form-field>
<mat-checkbox [formField]="form.selected" class="flex-auto">Is Selected?</mat-checkbox>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Text</mat-label>
<textarea matInput matAutosizeMinRows="20" [formField]="form.text"></textarea>
</mat-form-field>
</div>
</form>
<div class="row-container">
<button mat-raised-button color="primary" (click)="save()">Save</button>
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
</div>
}
@@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RecipeTemplateDetailComponent } from './recipe-template-detail.component';
describe('RecipeTemplateDetailComponent', () => {
let component: RecipeTemplateDetailComponent;
let fixture: ComponentFixture<RecipeTemplateDetailComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RecipeTemplateDetailComponent],
}).compileComponents();
fixture = TestBed.createComponent(RecipeTemplateDetailComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,119 +0,0 @@
import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core';
import { FormField, FormRoot, form as createForm } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import moment from 'moment';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { RecipeTemplate } from '../recipe-template';
import { RecipeTemplateService } from '../recipe-template.service';
export interface RecipeTemplateDetailFormData {
name: string;
date: moment.Moment | null | string;
text: string;
selected: boolean;
}
@Component({
selector: 'app-recipe-template-detail',
templateUrl: './recipe-template-detail.component.html',
styleUrls: ['./recipe-template-detail.component.css'],
imports: [
FormField,
FormRoot,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule,
MatCheckboxModule,
MatButtonModule,
SkeletonLoaderComponent,
ErrorStateComponent,
],
})
export class RecipeTemplateDetailComponent {
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
private router = inject(Router);
private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar);
private ser = inject(RecipeTemplateService);
itemResource = this.ser.get(this.id);
item = computed(() => this.itemResource.value() ?? new RecipeTemplate());
formModel = linkedSignal({
source: this.item,
computation: (itemVal): RecipeTemplateDetailFormData => ({
name: itemVal.name || '',
date: itemVal.date ? moment(itemVal.date, 'DD-MMM-YYYY') : moment(new Date()),
text: itemVal.text || '',
selected: itemVal.selected || false,
}),
});
form = createForm(this.formModel, () => {});
constructor() {
afterNextRender(() => {
this.form().focusBoundControl();
});
}
save() {
this.ser.saveOrUpdate(this.getItem()).subscribe({
next: () => {
this.snackBar.open('', 'Success');
this.router.navigateByUrl('/recipe-templates');
},
error: (error) => {
this.snackBar.open(error as string, 'Danger');
},
});
}
delete() {
this.ser.delete(this.item().id).subscribe({
next: () => {
this.snackBar.open('', 'Success');
this.router.navigateByUrl('/recipe-templates');
},
error: (error) => {
this.snackBar.open(error as string, 'Danger');
},
});
}
confirmDelete(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: { title: 'Delete Recipe Template?', content: 'Are you sure? This cannot be undone.' },
});
dialogRef.afterClosed().subscribe((result: boolean) => {
if (result) {
this.delete();
}
});
}
getItem(): RecipeTemplate {
const formModel = this.formModel();
const item = this.item();
item.name = formModel.name ?? '';
item.date = moment(formModel.date).format('DD-MMM-YYYY');
item.text = formModel.text ?? '';
item.selected = formModel.selected ?? false;
return item;
}
}
@@ -1,47 +0,0 @@
<h2 class="row-container space-between">
<span>Recipe Templates</span>
<a mat-button [routerLink]="['/recipe-templates', 'new']">
<mat-icon>add_box</mat-icon>
Add
</a>
</h2>
@if (listResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
} @else if (listResource.error()) {
<app-error-state [message]="'Failed to load list.'" (retryAction)="listResource.reload()" />
} @else {
<mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let row"
><a [routerLink]="['/recipe-templates', row.id]">{{ row.name }}</a></mat-cell
>
</ng-container>
<!-- Selected Column -->
<ng-container matColumnDef="selected">
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Selected?</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.selected }}</mat-cell>
</ng-container>
<!-- Date Column -->
<ng-container matColumnDef="date">
<mat-header-cell *matHeaderCellDef mat-sort-header>Date</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.date }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
(page)="handlePageEvent($event)"
[length]="list().length"
[pageIndex]="pageIndex()"
[pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250]"
>
</mat-paginator>
}
@@ -1,22 +0,0 @@
import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { RecipeTemplateListComponent } from './recipe-template-list.component';
describe('RecipeTemplateListComponent', () => {
let component: RecipeTemplateListComponent;
let fixture: ComponentFixture<RecipeTemplateListComponent>;
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, RecipeTemplateListComponent],
}).compileComponents();
fixture = TestBed.createComponent(RecipeTemplateListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('should compile', () => {
expect(component).toBeTruthy();
});
});
@@ -1,81 +0,0 @@
import { Component, computed, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { RecipeTemplateService } from '../recipe-template.service';
@Component({
selector: 'app-recipe-template-list',
templateUrl: './recipe-template-list.component.html',
styleUrls: ['./recipe-template-list.component.css'],
imports: [
RouterModule,
MatIconModule,
MatTableModule,
MatSortModule,
MatPaginatorModule,
MatButtonModule,
SkeletonLoaderComponent,
ErrorStateComponent,
],
})
export class RecipeTemplateListComponent {
private ser = inject(RecipeTemplateService);
pageSize = signal(50);
pageIndex = signal(0);
sortActive = signal('');
sortDirection = signal('');
listResource = this.ser.list();
list = computed(() => this.listResource.value() ?? []);
sortedList = computed(() => {
const data = this.list();
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'name':
return compare(a.name, b.name, isAsc);
case 'selected':
return compare(a.selected, b.selected, isAsc);
case 'date':
return compare(a.date, b.date, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => {
const data = this.sortedList();
const pageIndex = this.pageIndex();
const pageSize = this.pageSize();
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
});
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'selected', 'date'];
handlePageEvent(e: PageEvent) {
this.pageSize.set(e.pageSize);
this.pageIndex.set(e.pageIndex);
}
sortData(sort: Sort) {
this.sortActive.set(sort.active);
this.sortDirection.set(sort.direction);
}
}
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,32 +0,0 @@
import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service';
import { RecipeTemplateDetailComponent } from './recipe-template-detail/recipe-template-detail.component';
import { RecipeTemplateListComponent } from './recipe-template-list/recipe-template-list.component';
export const routes: Routes = [
{
path: '',
component: RecipeTemplateListComponent,
canActivate: [authGuard],
data: {
permission: 'Recipes',
},
},
{
path: 'new',
component: RecipeTemplateDetailComponent,
canActivate: [authGuard],
data: {
permission: 'Recipes',
},
},
{
path: ':id',
component: RecipeTemplateDetailComponent,
canActivate: [authGuard],
data: {
permission: 'Recipes',
},
},
];
@@ -1,17 +0,0 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { RecipeTemplateService } from './recipe-template.service';
describe('RecipeTemplateService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [RecipeTemplateService, provideHttpClient(withInterceptorsFromDi())],
});
});
it('should be created', inject([RecipeTemplateService], (service: RecipeTemplateService) => {
expect(service).toBeTruthy();
}));
});
@@ -1,52 +0,0 @@
import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { RecipeTemplate } from './recipe-template';
const url = '/api/recipe-templates';
@Injectable({
providedIn: 'root',
})
export class RecipeTemplateService {
private http = inject(HttpClient);
private log = inject(ErrorLoggerService);
get(id: Signal<string | null>) {
return httpResource<RecipeTemplate>(() => {
const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
return getUrl;
});
}
list() {
return httpResource<RecipeTemplate[]>(() => `${url}/list`);
}
save(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
return this.http
.post<RecipeTemplate>(`${url}`, recipeTemplate)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'save'))) as Observable<RecipeTemplate>;
}
update(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
return this.http
.put<RecipeTemplate>(`${url}/${recipeTemplate.id}`, recipeTemplate)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'update'))) as Observable<RecipeTemplate>;
}
saveOrUpdate(recipeTemplate: RecipeTemplate): Observable<RecipeTemplate> {
if (!recipeTemplate.id) {
return this.save(recipeTemplate);
}
return this.update(recipeTemplate);
}
delete(id: string): Observable<RecipeTemplate> {
return this.http
.delete<RecipeTemplate>(`${url}/${id}`)
.pipe(catchError(this.log.handleError('RecipeTemplateService', 'delete'))) as Observable<RecipeTemplate>;
}
}
@@ -1,16 +0,0 @@
export class RecipeTemplate {
id: string;
name: string;
date: string;
text: string;
selected: boolean;
public constructor(init?: Partial<RecipeTemplate>) {
this.id = '';
this.name = '';
this.date = '';
this.text = '';
this.selected = false;
Object.assign(this, init);
}
}
@@ -312,7 +312,7 @@
<div class="recipe-print__header">
<h1 class="recipe-print__title">{{ dishName }}</h1>
<div class="recipe-print__meta">
<span><strong>Date:</strong> {{ formModel().basics.date.format('DD-MMM-YYYY') }}</span>
<span><strong>Date:</strong> {{ formModel().basics.date }}</span>
<span><strong>Source:</strong> {{ formModel().basics.source }}</span>
<span>
<strong>Yield:</strong>
@@ -63,7 +63,7 @@ export interface IngredientFormData {
export interface RecipeDetailFormData {
basics: {
date: moment.Moment;
date: string;
source: string;
recipeYield: string;
product: ProductSku | string | null;
@@ -136,7 +136,7 @@ export class RecipeDetailComponent {
source: this.item,
computation: (itemVal): RecipeDetailFormData => ({
basics: {
date: itemVal.date ? moment(itemVal.date) : moment(new Date()),
date: itemVal.date,
source: itemVal.source ?? '',
recipeYield: `${itemVal.recipeYield ?? ''}`,
product: itemVal.sku ?? null,
+10 -10
View File
@@ -24,16 +24,16 @@ import { SettingsService } from './settings.service';
import { VoucherTypeService } from './voucher-type.service';
export interface LockInfoFormData {
validFrom: moment.Moment | null;
validTill: moment.Moment | null;
validFrom: Date | null;
validTill: Date | null;
index: number;
lockOlder: boolean;
olderRolling: boolean;
olderDate: moment.Moment;
olderDate: Date;
olderDays: number;
lockNewer: boolean;
newerRolling: boolean;
newerDate: moment.Moment;
newerDate: Date;
newerDays: number;
accountTypes: { accountType: boolean }[];
voucherTypes: { voucherType: boolean }[];
@@ -82,16 +82,16 @@ export class SettingsComponent {
lockInfoModel = linkedSignal({
source: this.lockInfoSource,
computation: (s): LockInfoFormData => ({
validFrom: null as moment.Moment | null,
validTill: null as moment.Moment | null,
validFrom: null as Date | null,
validTill: null as Date | null,
index: 0,
lockOlder: false,
olderRolling: false,
olderDate: moment().date(moment().date(1).daysInMonth()),
olderDate: moment().date(moment().date(1).daysInMonth()).toDate(),
olderDays: 0,
lockNewer: false,
newerRolling: false,
newerDate: moment().date(moment().date(1).daysInMonth()),
newerDate: moment().date(moment().date(1).daysInMonth()).toDate(),
newerDays: 0,
accountTypes: s.accTypes.map(() => ({ accountType: false })),
voucherTypes: s.vchTypes.map(() => ({ voucherType: false })),
@@ -133,13 +133,13 @@ export class SettingsComponent {
if (this.lockOlder() && this.olderRolling()) {
item.start.days = model.olderDays ?? 0;
} else if (this.lockOlder() && !this.olderRolling()) {
item.start.date = (model.olderDate ?? moment()).format('DD-MMM-YYYY');
item.start.date = moment(model.olderDate ?? new Date()).format('DD-MMM-YYYY');
}
if (this.lockNewer() && this.newerRolling()) {
item.finish.days = model.newerDays ?? 0;
}
if (this.lockNewer() && !this.newerRolling()) {
item.finish.date = (model.newerDate ?? moment()).format('DD-MMM-YYYY');
item.finish.date = moment(model.newerDate ?? new Date()).format('DD-MMM-YYYY');
}
this.accountTypes().forEach((at: AccountType, index: number) => {
@@ -1,6 +1,6 @@
<h2>Stock Movement</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label>
@@ -14,7 +14,7 @@
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker>
</mat-form-field>
<button mat-raised-button class="flex-auto basis-1-5" color="primary" (click)="show()">Show</button>
<button mat-raised-button type="submit" class="flex-auto basis-1-5" color="primary">Show</button>
</div>
</form>
@if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { DecimalPipe } from '@angular/common';
import { Component, HostListener, inject, computed, linkedSignal, signal, input } from '@angular/core';
import { Component, inject, computed, linkedSignal, signal, input } from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -25,6 +25,9 @@ export interface StockMovementFormData {
selector: 'app-stock-movement',
templateUrl: './stock-movement.component.html',
styleUrls: ['./stock-movement.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
@@ -104,7 +107,6 @@ export class StockMovementComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'opening', 'purchase', 'issue', 'closing'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();
@@ -7,7 +7,7 @@
</mat-form-field>
<mat-form-field class="flex-auto basis-30">
<mat-label>Product Group</mat-label>
<mat-select [formField]="form.productGroup" (selectionChange)="filterOn($event.value)">
<mat-select [formField]="form.productGroup">
<mat-option>-- All Products --</mat-option>
@for (pg of productGroups(); track pg) {
<mat-option [value]="pg.id">
@@ -18,10 +18,10 @@
</mat-form-field>
</div>
</form>
@if (resource.isLoading()) {
@if (listResource.isLoading()) {
<app-skeleton-loader type="table" [count]="1" />
} @else if (resource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="resource.reload()" />
} @else if (listResource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="listResource.reload()" />
} @else {
<mat-table [dataSource]="dataSource()">
<!-- Name Column -->
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject, computed, linkedSignal } from '@angular/core';
import { Component, inject, computed, linkedSignal, input, debounced, effect } from '@angular/core';
import { FormField, FormRoot, form as createForm } from '@angular/forms/signals';
import { MatOptionModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -7,12 +7,13 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { RouterLink } from '@angular/router';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Product, StockKeepingUnit } from '../../core/product';
import { ProductGroupService } from '../../product-group/product-group.service';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { TemporalProduct } from '../temporal-product';
import { TemporalProductService } from '../temporal-product.service';
export interface TemporalProductListFormData {
@@ -42,48 +43,84 @@ export interface TemporalProductListFormData {
],
})
export class TemporalProductListComponent {
private router = inject(Router);
private route = inject(ActivatedRoute);
private ser = inject(TemporalProductService);
private productGroupSer = inject(ProductGroupService);
q = input('', { transform: (v: string | null | undefined) => v ?? '' });
g = input('', { transform: (v: string | null | undefined) => v ?? '' });
productGroupsResource = this.productGroupSer.list();
productGroups = computed(() => this.productGroupsResource.value() ?? []);
listResource = this.ser.list();
list = computed(() => this.listResource.value() ?? []);
formModel = linkedSignal({
source: () => null,
computation: (): TemporalProductListFormData => ({
filter: '',
productGroup: '',
source: () => ({ q: this.q(), g: this.g() }),
computation: (v: { q: string; g: string }): TemporalProductListFormData => ({
filter: v.q,
productGroup: v.g,
}),
});
form = createForm(this.formModel);
filterSignal = computed(() => this.formModel().filter);
debouncedFilter = debounced(this.filterSignal, 150);
filteredList = computed(() => {
const data = this.list();
const search = this.debouncedFilter.value() ?? '';
const productGroup = this.formModel().productGroup;
const groupId =
typeof productGroup === 'string'
? productGroup
: productGroup && 'id' in productGroup
? (productGroup.id ?? '')
: '';
productGroupsResource = this.productGroupSer.list();
productGroups = computed(() => this.productGroupsResource.value() ?? []);
const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
resource = this.ser.list();
return data.filter((tp: TemporalProduct) => {
const products = tp.products ?? [];
const skus = tp.skus ?? [];
dataSource = computed(() => {
let data = this.resource.value() ?? [];
const filterValue = (this.formModel().filter ?? '').trim().toLowerCase();
const groupValue = this.formModel().productGroup;
const matchesSearch =
tokens.length === 0 ||
tokens.every(
(token) =>
products.some((p) => {
const hay = `${p.name ?? ''} ${p.fractionUnits ?? ''} ${p.productGroup?.name ?? ''}`.toLowerCase();
return hay.includes(token);
}) ||
skus.some((k) => {
const hay = `${k.units ?? ''}`.toLowerCase();
return hay.includes(token);
}),
);
if (filterValue || groupValue) {
data = data.filter((d) => {
let match = true;
if (filterValue) {
match = match && d.products[0].name.toLowerCase().includes(filterValue);
}
if (groupValue) {
match = match && d.products[0].productGroup?.id === groupValue;
}
return match;
});
}
return data;
const matchesProductGroup = !groupId || products.some((k) => (k.productGroup?.id ?? '') === groupId);
return matchesSearch && matchesProductGroup;
});
});
dataSource = computed(() => this.filteredList());
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns: string[] = ['product', 'sku', 'info'];
filterOn(val: string) {
this.formModel.update((m) => ({ ...m, productGroup: val }));
constructor() {
effect(() => {
const q = this.debouncedFilter.value() ?? '';
const g = this.formModel().productGroup;
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
q: q || null,
g: g || null,
},
queryParamsHandling: 'merge',
replaceUrl: true,
});
});
}
}
@@ -1,6 +1,6 @@
<h2>Trial Balance</h2>
<form class="flex-col" [formRoot]="form">
<form class="flex-col" [formRoot]="form" (submit)="$event.preventDefault(); show()">
<div class="row-container">
<mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label>
@@ -8,7 +8,7 @@
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker>
</mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto">Show</button>
<button mat-raised-button type="submit" color="primary" class="flex-auto">Show</button>
</div>
</form>
@if (resource.isLoading()) {
@@ -1,5 +1,5 @@
import { CurrencyPipe } from '@angular/common';
import { Component, HostListener, inject, computed, linkedSignal, signal, input } from '@angular/core';
import { Component, inject, computed, linkedSignal, signal, input } from '@angular/core';
import { FormField, FormRoot, form as createForm } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -25,6 +25,9 @@ export interface TrialBalanceFormData {
selector: 'app-trial-balance',
templateUrl: './trial-balance.component.html',
styleUrls: ['./trial-balance.component.css'],
host: {
'(window:keydown.f2)': 'focusDate($event)',
},
imports: [
FormField,
@@ -100,7 +103,6 @@ export class TrialBalanceComponent {
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['type', 'name', 'debit', 'credit'];
@HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) {
event.preventDefault();
this.form().focusBoundControl();