diff --git a/app/routers/ordered_products.py b/app/routers/ordered_products.py
index e33e00c..f0e6813 100644
--- a/app/routers/ordered_products.py
+++ b/app/routers/ordered_products.py
@@ -1,7 +1,8 @@
from fastapi import APIRouter, Request
from ..store import SortOrderedProductsBy as SortBy
-from ..store import select_ordered_products
+from ..store import load_ordered_products
+
# from .. import templates
router = APIRouter()
@@ -9,7 +10,7 @@
@router.get("/ordered-products")
async def get_ordered_products(_: Request, sort_by: SortBy = SortBy.PRODUCT_ID):
- ordered_products = await select_ordered_products(sort_by, False, False)
+ ordered_products = await load_ordered_products(sort_by)
# NOTE: Actually, we might not need this template and instead modify and use templates.placements.
# templates.ordered_products(request, ordered_products)
return ordered_products
diff --git a/app/routers/placements.py b/app/routers/placements.py
index 9f42b5d..ade9987 100644
--- a/app/routers/placements.py
+++ b/app/routers/placements.py
@@ -1,42 +1,48 @@
-from typing import Annotated
+# from typing import Annotated
-from fastapi import APIRouter, Header, Request
+# from fastapi import APIRouter, Header, Request
+from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from .. import templates
-from ..store import PlacementTable, select_placements
+from ..store import (
+ PlacementTable,
+ load_canceled_placements,
+ load_completed_placements,
+ load_incoming_placements,
+)
router = APIRouter()
-@router.get("/placements", response_class=HTMLResponse)
-async def get_placements(
- request: Request,
- canceled: bool = False,
- completed: bool = False,
- hx_request: Annotated[str | None, Header()] = None,
-):
- placements = await select_placements(canceled, completed)
- macro = (
- templates.components.placements
- if hx_request == "true"
- else templates.placements
- )
- return HTMLResponse(macro(request, placements, canceled, completed))
+@router.get("/incoming-placements", response_class=HTMLResponse)
+async def get_incoming_placements(request: Request):
+ placements = await load_incoming_placements()
+ return HTMLResponse(templates.incoming_placements(request, placements))
-# @router.get("/placements/{placement_id}")
-# async def get_placement(request: Request, placement_id: int):
-# if (placement := await PlacementTable.by_placement_id(placement_id)) is None:
-# raise HTTPException(404, f"Placement {placement_id} not found")
-# return templates.components.placement(request, placement)
+@router.get("/canceled-placements", response_class=HTMLResponse)
+async def get_canceled_placements(request: Request):
+ placements = await load_canceled_placements()
+ return HTMLResponse(templates.canceled_placements(request, placements))
-@router.post("/placements/{placement_id}")
+@router.get("/completed-placements", response_class=HTMLResponse)
+async def get_completed_placements(request: Request):
+ placements = await load_completed_placements()
+ return HTMLResponse(templates.completed_placements(request, placements))
+
+
+@router.post("/incoming-placements/{placement_id}")
+async def reset_placement(placement_id: int):
+ await PlacementTable.reset(placement_id)
+
+
+@router.post("/completed-placements/{placement_id}")
async def complete_placement(placement_id: int):
await PlacementTable.complete(placement_id)
-@router.delete("/placements/{placement_id}")
+@router.post("/canceled-placements/{placement_id}")
async def cancel_placement(placement_id: int):
await PlacementTable.cancel(placement_id)
diff --git a/app/store/__init__.py b/app/store/__init__.py
index af8a198..f355725 100644
--- a/app/store/__init__.py
+++ b/app/store/__init__.py
@@ -1,12 +1,15 @@
from datetime import datetime
-from enum import Enum
-from typing import assert_never
+from enum import Enum, auto
+from typing import Any, Awaitable, Callable, Mapping, assert_never
import sqlalchemy
+import sqlmodel
from databases import Database
-from sqlmodel import SQLModel
+from sqlalchemy import orm as sa_orm
+from sqlmodel import col
from . import placed_item, placement, product
+from ._helper import _colname
from .placed_item import PlacedItem
from .placement import Placement
from .product import Product
@@ -24,70 +27,235 @@ def _to_time(unix_epoch: int) -> str:
return datetime.fromtimestamp(unix_epoch).strftime("%H:%M:%S")
-async def select_placements(
- canceled: bool,
- completed: bool,
-) -> list[dict[str, int | list[dict[str, int | str]] | str | datetime | None]]:
- # list of products with each row alongside the number of ordered items
- query = f"""
- SELECT
- {Placement.placement_id},
- unixepoch({Placement.placed_at}) as placed_at,
- unixepoch({Placement.completed_at}) as completed_at,
- {PlacedItem.product_id},
- COUNT({PlacedItem.product_id}) AS count,
- {Product.name},
- {Product.filename},
- {Product.price}
- FROM {Placement.__tablename__} as {Placement.__name__}
- JOIN {PlacedItem.__tablename__} as {PlacedItem.__name__} ON {Placement.placement_id} = {PlacedItem.placement_id}
- JOIN {Product.__tablename__} as {Product.__name__} ON {PlacedItem.product_id} = {Product.product_id}
- WHERE {Placement.canceled} = {int(canceled)} AND
- {Placement.completed} = {int(completed)}
- GROUP BY {Placement.placement_id}, {PlacedItem.product_id}
- ORDER BY {Placement.placement_id} ASC, {PlacedItem.product_id} ASC
- """
- placements: list[
- dict[str, int | list[dict[str, int | str]] | str | datetime | None]
- ] = []
- prev_placement_id = -1
- products = []
- total_price = 0
- async for map in database.iterate(query):
- if (placement_id := map["placement_id"]) != prev_placement_id:
- prev_placement_id = placement_id
- if len(placements) > 0:
- placements[-1]["total_price"] = Product.to_price_str(total_price)
- products = []
- completed_at = _to_time(field) if (field := map["completed_at"]) else None
- placements.append(
- {
- "placement_id": placement_id,
- "products": products,
- "placed_at": _to_time(map["placed_at"]),
- "completed_at": completed_at,
- }
+type products_t = list[dict[str, int | str]]
+type placements_t = list[dict[str, int | products_t | str | datetime | None]]
+
+
+# TODO: there should be a way to use the unixepoch function without this boiler plate
+def unixepoch(attr: sa_orm.Mapped) -> sqlalchemy.Label:
+ colname = _colname(col(attr))
+ alias = getattr(attr, "name")
+ return sqlalchemy.literal_column(f"unixepoch({colname})").label(alias)
+
+
+class SortOrderedProductsBy(Enum):
+ PRODUCT_ID = "product_id"
+ TIME = "time"
+ NO_ITEMS = "no_items"
+
+ def order_by(self) -> sqlalchemy.ColumnElement:
+ match self:
+ case self.PRODUCT_ID:
+ return sqlmodel.asc(col(PlacedItem.product_id))
+ case self.TIME:
+ return sqlmodel.desc(col(Placement.placed_at))
+ case self.NO_ITEMS:
+ return sqlmodel.desc(sqlmodel.literal_column("count"))
+
+
+class PlacementsQuery(Enum):
+ incoming = auto()
+ canceled = auto()
+ completed = auto()
+
+ def by_placement_id(self) -> sqlalchemy.Select:
+ # Query from the placements table
+ query: sqlalchemy.Select[Any] = (
+ sqlmodel.select(Placement.placement_id)
+ .group_by(col(Placement.placement_id))
+ .order_by(col(Placement.placement_id).asc())
+ .add_columns(unixepoch(col(Placement.placed_at)))
+ )
+
+ query = self._extra_timestamps(query)
+
+ query = (
+ # Query the list of placed items
+ query.select_from(sqlmodel.join(Placement, PlacedItem))
+ .add_columns(col(PlacedItem.product_id))
+ .group_by(col(PlacedItem.product_id))
+ .order_by(col(PlacedItem.product_id).asc())
+ .add_columns(sqlmodel.func.count(col(PlacedItem.product_id)).label("count"))
+ # Query product information
+ .join(Product)
+ .add_columns(col(Product.name), col(Product.filename))
+ )
+
+ # Include prices for canceled/completed placements
+ if self != self.incoming:
+ query = query.add_columns(col(Product.price))
+
+ return query
+
+ def by_ordered_products(self, sort_by: SortOrderedProductsBy) -> sqlalchemy.Select:
+ query = (
+ sqlmodel.select(
+ PlacedItem.placement_id,
+ sqlmodel.func.count(col(PlacedItem.product_id)).label("count"),
+ PlacedItem.product_id,
)
- total_price = 0
- count, price = map["count"], map["price"]
- products.append(
- {
- "product_id": map["product_id"],
- "count": count,
- "name": map["name"],
- "filename": map["filename"],
- "price": Product.to_price_str(price),
- }
+ .group_by(col(PlacedItem.placement_id), col(PlacedItem.product_id))
+ .select_from(sqlmodel.join(PlacedItem, Product))
+ .add_columns(col(Product.name), col(Product.filename))
+ .join(Placement)
)
- total_price += count * price
- if len(placements) > 0:
- placements[-1]["total_price"] = Product.to_price_str(total_price)
- return placements
+ query = self._extra_timestamps(query)
+
+ return query.order_by(sort_by.order_by())
+
+ def _extra_timestamps(self, query: sqlalchemy.Select) -> sqlalchemy.Select:
+ """Conditionally include/exclude extra timestamps."""
+ match self:
+ case self.incoming:
+ return query.where(
+ col(Placement.canceled_at).is_(None)
+ & col(Placement.completed_at).is_(None)
+ )
+ case self.canceled:
+ return query.where(col(Placement.canceled_at).isnot(None)).add_columns(
+ unixepoch(col(Placement.canceled_at))
+ )
+ case self.completed:
+ return query.where(col(Placement.completed_at).isnot(None)).add_columns(
+ unixepoch(col(Placement.completed_at))
+ )
+
+
+class _PlacementsLoader:
+ def __new__(cls, status: PlacementsQuery) -> Callable[[], Awaitable[placements_t]]:
+ placements: placements_t = []
+
+ match status:
+ case status.incoming:
+
+ def init_cb(placement_id: int, map: Mapping) -> products_t:
+ products = []
+ placements.append(
+ {
+ "placement_id": placement_id,
+ "products": products,
+ "placed_at": _to_time(map["placed_at"]),
+ }
+ )
+ return products
+
+ def update_product_cb(map: Mapping) -> dict[str, int | str]:
+ return {
+ "product_id": map["product_id"],
+ "count": map["count"],
+ "name": map["name"],
+ "filename": map["filename"],
+ }
+
+ def last_cb(): ...
+
+ case status.canceled:
+ total_price = 0
+
+ def init_cb(placement_id: int, map: Mapping) -> products_t:
+ products = []
+ placements.append(
+ {
+ "placement_id": placement_id,
+ "products": products,
+ "placed_at": _to_time(map["placed_at"]),
+ "canceled_at": _to_time(map["canceled_at"]),
+ }
+ )
+ nonlocal total_price
+ total_price = 0
+ return products
+
+ def update_product_cb(map: Mapping) -> dict[str, int | str]:
+ count, price = map["count"], map["price"]
+ nonlocal total_price
+ total_price += count * price
+ return {
+ "product_id": map["product_id"],
+ "count": count,
+ "name": map["name"],
+ "filename": map["filename"],
+ "price": Product.to_price_str(price),
+ }
+
+ def last_cb() -> None:
+ if len(placements) > 0:
+ placements[-1]["total_price"] = Product.to_price_str(
+ total_price
+ )
+
+ case status.completed:
+ total_price = 0
+
+ def init_cb(placement_id: int, map: Mapping) -> products_t:
+ products = []
+ placements.append(
+ {
+ "placement_id": placement_id,
+ "products": products,
+ "placed_at": _to_time(map["placed_at"]),
+ "completed_at": _to_time(map["completed_at"]),
+ }
+ )
+ nonlocal total_price
+ total_price = 0
+ return products
+
+ def update_product_cb(map: Mapping) -> dict[str, int | str]:
+ count, price = map["count"], map["price"]
+ nonlocal total_price
+ total_price += count * price
+ return {
+ "product_id": map["product_id"],
+ "count": count,
+ "name": map["name"],
+ "filename": map["filename"],
+ "price": Product.to_price_str(price),
+ }
+
+ def last_cb() -> None:
+ if len(placements) > 0:
+ placements[-1]["total_price"] = Product.to_price_str(
+ total_price
+ )
+
+ case _:
+ assert_never()
+
+ query = str(status.by_placement_id().compile())
+
+ async def loader():
+ placements.clear()
+ await cls._execute(query, init_cb, update_product_cb, last_cb)
+ return placements
+
+ return loader
+
+ @staticmethod
+ async def _execute(
+ query: str,
+ init_cb: Callable[[int, Mapping], products_t],
+ update_product_cb: Callable[[Mapping], dict[str, int | str]],
+ last_cb: Callable[[], None],
+ ):
+ products = []
+ prev_placement_id = -1
+ async for map in database.iterate(query):
+ if (placement_id := map["placement_id"]) != prev_placement_id:
+ prev_placement_id = placement_id
+ last_cb()
+ products = init_cb(placement_id, map)
+ products.append(update_product_cb(map))
+ last_cb()
+
+
+load_incoming_placements = _PlacementsLoader(PlacementsQuery.incoming)
+load_canceled_placements = _PlacementsLoader(PlacementsQuery.canceled)
+load_completed_placements = _PlacementsLoader(PlacementsQuery.completed)
# NOTE:get placements by incoming order in datetime
-# TODO: add a datetime field to PlacedItem
#
# async def select_placements_by_incoming_order() -> dict[int, list[dict]]:
# query = f"""
@@ -106,41 +274,10 @@ async def select_placements(
# return placements
-class SortOrderedProductsBy(Enum):
- PRODUCT_ID = "product_id"
- TIME = "time"
- NO_ITEMS = "no_items"
-
-
-async def select_ordered_products(
+async def load_ordered_products(
sort_by: SortOrderedProductsBy,
- canceled: bool,
- completed: bool,
) -> list[dict[str, int | str | list[dict[str, int]]]]:
- match sort_by:
- case SortOrderedProductsBy.PRODUCT_ID:
- order_by = f"{PlacedItem.product_id} ASC"
- case SortOrderedProductsBy.TIME:
- order_by = f"{Placement.placed_at} DESC"
- case SortOrderedProductsBy.NO_ITEMS:
- order_by = "count DESC"
- case _:
- assert_never(SortOrderedProductsBy)
- query = f"""
- SELECT
- {PlacedItem.placement_id},
- COUNT({PlacedItem.product_id}) AS count,
- {PlacedItem.product_id},
- {Product.name},
- {Product.filename}
- FROM {PlacedItem.__tablename__} as {PlacedItem.__name__}
- JOIN {Product.__tablename__} as {Product.__name__} ON {PlacedItem.product_id} = {Product.product_id}
- JOIN {Placement.__tablename__} as {Placement.__name__} ON {PlacedItem.placement_id} = {Placement.placement_id}
- WHERE {Placement.canceled} = {int(canceled)} AND
- {Placement.completed} = {int(completed)}
- GROUP BY {PlacedItem.placement_id}, {PlacedItem.product_id}
- ORDER BY {order_by}
- """
+ query = PlacementsQuery.incoming.by_ordered_products(sort_by)
ret: dict[int, dict[str, int | str | list[dict[str, int]]]] = {}
async for map in database.iterate(query):
product_id = map["product_id"]
@@ -167,7 +304,7 @@ async def _startup_db() -> None:
# TODO: we should use a database schema migration tool like Alembic as explained in:
# https://www.encode.io/databases/database_queries/#creating-tables
- for table in SQLModel.metadata.tables.values():
+ for table in sqlmodel.SQLModel.metadata.tables.values():
schema = sqlalchemy.schema.CreateTable(table, if_not_exists=True)
query = str(schema.compile())
await database.execute(query)
diff --git a/app/store/placement.py b/app/store/placement.py
index cfde40e..44d9107 100644
--- a/app/store/placement.py
+++ b/app/store/placement.py
@@ -1,8 +1,10 @@
from datetime import datetime, timezone
from typing import Annotated
+import sqlalchemy
import sqlmodel
from databases import Database
+from sqlmodel import col
class Placement(sqlmodel.SQLModel, table=True):
@@ -11,20 +13,15 @@ class Placement(sqlmodel.SQLModel, table=True):
id: int | None = sqlmodel.Field(default=None, primary_key=True)
placement_id: int
- # Column(..., server_default=sqlalchemy.text("0"))
- canceled: Annotated[
- bool, sqlmodel.Field(sa_column_kwargs={"server_default": sqlmodel.text("0")})
- ]
- # Column(..., server_default=sqlalchemy.text("0"))
- completed: Annotated[
- bool, sqlmodel.Field(sa_column_kwargs={"server_default": sqlmodel.text("0")})
- ]
placed_at: Annotated[
datetime,
sqlmodel.Field(
sa_column_kwargs={"server_default": sqlmodel.text("CURRENT_TIMESTAMP")}
),
]
+ canceled_at: datetime | None = sqlmodel.Field(
+ default=None, sa_column=sqlmodel.Column(sqlmodel.DateTime(timezone=True))
+ )
completed_at: datetime | None = sqlmodel.Field(
default=None, sa_column=sqlmodel.Column(sqlmodel.DateTime(timezone=True))
)
@@ -38,25 +35,22 @@ async def insert(self, placement_id: int) -> None:
query = sqlmodel.insert(Placement)
await self._db.execute(query, {"placement_id": placement_id})
- async def update(self, placement_id: int, canceled: bool, completed: bool) -> None:
- clause = Placement.placement_id == placement_id
- # NOTE: I don't why, but this where clause argument does not typecheck
- query = sqlmodel.update(Placement).where(clause) # pyright: ignore[reportArgumentType]
- values = {
- "canceled": canceled,
- "completed": completed,
- "completed_at": datetime.now(timezone.utc) if completed else None,
- }
- await self._db.execute(query, values)
+ @staticmethod
+ def _update(placement_id: int) -> sqlalchemy.Update:
+ clause = col(Placement.placement_id) == placement_id
+ return sqlmodel.update(Placement).where(clause)
async def cancel(self, placement_id: int) -> None:
- await self.update(placement_id, canceled=True, completed=False)
+ values = {"canceled_at": datetime.now(timezone.utc), "completed_at": None}
+ await self._db.execute(self._update(placement_id), values)
async def complete(self, placement_id: int) -> None:
- await self.update(placement_id, canceled=False, completed=True)
+ values = {"canceled_at": None, "completed_at": datetime.now(timezone.utc)}
+ await self._db.execute(self._update(placement_id), values)
async def reset(self, placement_id: int) -> None:
- await self.update(placement_id, canceled=False, completed=False)
+ values = {"canceled_at": None, "completed_at": None}
+ await self._db.execute(self._update(placement_id), values)
async def by_placement_id(self, placement_id: int) -> Placement | None:
query = sqlmodel.select(Placement).where(Placement.placement_id == placement_id)
diff --git a/app/store/test__init__.py b/app/store/test__init__.py
new file mode 100644
index 0000000..3405e5f
--- /dev/null
+++ b/app/store/test__init__.py
@@ -0,0 +1,39 @@
+import sqlalchemy
+from inline_snapshot import snapshot
+
+from . import PlacementsQuery
+
+
+def strip_lines(query: sqlalchemy.Select) -> str:
+ stripped_lines = [line.strip() for line in str(query).split("\n")]
+ return "\n".join(stripped_lines)
+
+
+def test_incoming_query():
+ assert strip_lines(PlacementsQuery.incoming.by_placement_id()) == snapshot(
+ """\
+SELECT placements.placement_id, unixepoch(placements.placed_at) AS placed_at, placed_items.product_id, count(placed_items.product_id) AS count, products.name, products.filename
+FROM placements JOIN placed_items ON placements.placement_id = placed_items.placement_id JOIN products ON products.product_id = placed_items.product_id
+WHERE placements.canceled_at IS NULL AND placements.completed_at IS NULL GROUP BY placements.placement_id, placed_items.product_id ORDER BY placements.placement_id ASC, placed_items.product_id ASC\
+"""
+ )
+
+
+def test_canceled_query():
+ assert strip_lines(PlacementsQuery.canceled.by_placement_id()) == snapshot(
+ """\
+SELECT placements.placement_id, unixepoch(placements.placed_at) AS placed_at, unixepoch(placements.canceled_at) AS canceled_at, placed_items.product_id, count(placed_items.product_id) AS count, products.name, products.filename, products.price
+FROM placements JOIN placed_items ON placements.placement_id = placed_items.placement_id JOIN products ON products.product_id = placed_items.product_id
+WHERE placements.canceled_at IS NOT NULL GROUP BY placements.placement_id, placed_items.product_id ORDER BY placements.placement_id ASC, placed_items.product_id ASC\
+"""
+ )
+
+
+def test_completed_query():
+ assert strip_lines(PlacementsQuery.completed.by_placement_id()) == snapshot(
+ """\
+SELECT placements.placement_id, unixepoch(placements.placed_at) AS placed_at, unixepoch(placements.completed_at) AS completed_at, placed_items.product_id, count(placed_items.product_id) AS count, products.name, products.filename, products.price
+FROM placements JOIN placed_items ON placements.placement_id = placed_items.placement_id JOIN products ON products.product_id = placed_items.product_id
+WHERE placements.completed_at IS NOT NULL GROUP BY placements.placement_id, placed_items.product_id ORDER BY placements.placement_id ASC, placed_items.product_id ASC\
+"""
+ )
diff --git a/app/templates.py b/app/templates.py
index 4141291..1a19edf 100644
--- a/app/templates.py
+++ b/app/templates.py
@@ -1,5 +1,4 @@
import os
-from datetime import datetime
from functools import wraps
from pathlib import Path
from typing import Any, Callable, Protocol
@@ -10,7 +9,7 @@
from jinja2.ext import debug as debug_ext
from .env import DEBUG
-from .store import Product
+from .store import Product, placements_t
TEMPLATES_DIR = Path("app/templates")
@@ -69,6 +68,12 @@ def with_request(request, *args: P.args, **kwargs: P.kwargs) -> str:
return type_signature
+@macro_template("layout.html")
+def layout(
+ title: str = "murchace", head: str = "", caller: Callable[[], str] = lambda: ""
+): ...
+
+
@macro_template("index.html")
def index(): ...
@@ -88,14 +93,16 @@ def orders(
): ...
-@macro_template("placements.html")
-def placements(
- placements: list[
- dict[str, int | list[dict[str, int | str]] | str | datetime | None]
- ],
- canceled: bool = False,
- completed: bool = False,
-): ...
+@macro_template("incoming-placements.html")
+def incoming_placements(placements: placements_t): ...
+
+
+@macro_template("canceled-placements.html")
+def canceled_placements(placements: placements_t): ...
+
+
+@macro_template("completed-placements.html")
+def completed_placements(placements: placements_t): ...
# namespace
@@ -114,12 +121,6 @@ def order_session(
order_frozen: bool = False,
): ...
- @macro_template("components/placements.html")
- @staticmethod
- def placements(
- placements: list[
- dict[str, int | list[dict[str, int | str]] | str | datetime | None]
- ],
- canceled: bool = False,
- completed: bool = False,
- ): ...
+ # @macro_template("components/incoming-placements.html")
+ # @staticmethod
+ # def incoming_placements(placements: placements_t): ...
diff --git a/app/templates/canceled-placements.html b/app/templates/canceled-placements.html
new file mode 100644
index 0000000..5be6371
--- /dev/null
+++ b/app/templates/canceled-placements.html
@@ -0,0 +1,64 @@
+{% from "layout.html" import layout %}
+{% from "components/clock.html" import clock %}
+
+{% macro canceled_placements(placements) %}
+ {% call layout("取消した確定注文 - murchace") %}
+
+
+
+
+ {{ clock() }}
+
+
+ {% for placement in placements | reverse %}
+
+
+
+
#{{ placement.placement_id }}
+ @{{ placement.placed_at }}
+
+
+
+
+ {% for product in placement.products %}
+ -
+ {{ product.name }}
+ {{ product.price }} x {{ product.count }}
+
+ {% endfor %}
+
+
+ 合計金額
+ {{ placement.total_price }}
+
+
+
+ {% endfor %}
+
+
+ {% endcall %}
+{% endmacro %}
diff --git a/app/templates/completed-placements.html b/app/templates/completed-placements.html
new file mode 100644
index 0000000..bc02864
--- /dev/null
+++ b/app/templates/completed-placements.html
@@ -0,0 +1,64 @@
+{% from "layout.html" import layout %}
+{% from "components/clock.html" import clock %}
+
+{% macro completed_placements(placements) %}
+ {% call layout("完了した確定注文 - murchace") %}
+
+
+
+
+ {{ clock() }}
+
+
+ {% for placement in placements | reverse %}
+
+
+
+
#{{ placement.placement_id }}
+ @{{ placement.placed_at }}
+
+
+
+
+ {% for product in placement.products %}
+ -
+ {{ product.name }}
+ {{ product.price }} x {{ product.count }}
+
+ {% endfor %}
+
+
+ 合計金額
+ {{ placement.total_price }}
+
+
+
+ {% endfor %}
+
+
+ {% endcall %}
+{% endmacro %}
diff --git a/app/templates/components/placements.html b/app/templates/components/placements.html
deleted file mode 100644
index 36611e5..0000000
--- a/app/templates/components/placements.html
+++ /dev/null
@@ -1,50 +0,0 @@
-{% macro placements(placements, canceled = false, completed = false) %}
- {% for placement in placements | reverse %}
-
-
-
-
#{{ placement.placement_id }}
-
@{{ placement.placed_at }}
- {% if placement.completed_at %}
-
/{{ placement.completed_at }}
- {% endif %}
-
- {% if canceled == false %}
-
- {% endif %}
-
-
- {% for product in placement.products %}
- -
- {{ product.name }}
- {{ product.price }} x {{ product.count }}
-
- {% endfor %}
-
-
- 合計金額
- {{ placement.total_price }}
-
- {% if completed == false %}
-
- {% endif %}
-
- {% endfor %}
-{% endmacro %}
diff --git a/app/templates/incoming-placements.html b/app/templates/incoming-placements.html
new file mode 100644
index 0000000..8258f7e
--- /dev/null
+++ b/app/templates/incoming-placements.html
@@ -0,0 +1,65 @@
+{% from "layout.html" import layout %}
+{% from "components/clock.html" import clock %}
+
+{% macro _head() %}
+
+{% endmacro %}
+
+{% macro incoming_placements(placements) %}
+ {% call layout("確定注文 - murchace", _head()) %}
+
+
+
+
+ {{ clock() }}
+
+
+ {% for placement in placements | reverse %}
+
+
+
+
#{{ placement.placement_id }}
+ @{{ placement.placed_at }}
+
+
+
+
+ {% for product in placement.products %}
+ -
+ {{ product.name }}
+ x {{ product.count }}
+
+ {% endfor %}
+
+
+
+ {% endfor %}
+
+
+ {% endcall %}
+{% endmacro %}
diff --git a/app/templates/index.html b/app/templates/index.html
index 3cb6205..7115574 100644
--- a/app/templates/index.html
+++ b/app/templates/index.html
@@ -6,7 +6,7 @@
新しい注文
-
+
確定注文一覧
diff --git a/app/templates/placements.html b/app/templates/placements.html
deleted file mode 100644
index 6403d92..0000000
--- a/app/templates/placements.html
+++ /dev/null
@@ -1,49 +0,0 @@
-{% from "layout.html" import layout %}
-{% from "components/clock.html" import clock %}
-{% from "components/placements.html" import placements as placements_component %}
-
-{% macro _head() %}
-
-{% endmacro %}
-
-{% macro placements(placements, canceled = false, completed = false) %}
- {% call layout("確定注文 - murchace", _head()) %}
-
-
-
-
- {{ clock() }}
-
-
- {{ placements_component(placements, canceled, completed) }}
-
-
- {% endcall %}
-{% endmacro %}
diff --git a/static/styles.css b/static/styles.css
index c059b29..cab5211 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -571,6 +571,11 @@ video {
margin-right: 0.25rem;
}
+.mx-8 {
+ margin-left: 2rem;
+ margin-right: 2rem;
+}
+
.mx-auto {
margin-left: auto;
margin-right: auto;
@@ -733,28 +738,15 @@ video {
gap: 0.75rem;
}
-.gap-6 {
- gap: 1.5rem;
-}
-
.gap-x-2 {
-moz-column-gap: 0.5rem;
column-gap: 0.5rem;
}
-.gap-x-6 {
- -moz-column-gap: 1.5rem;
- column-gap: 1.5rem;
-}
-
.gap-y-2 {
row-gap: 0.5rem;
}
-.gap-y-3 {
- row-gap: 0.75rem;
-}
-
.divide-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-divide-y-reverse: 0;
border-top-width: calc(4px * calc(1 - var(--tw-divide-y-reverse)));
@@ -829,11 +821,6 @@ video {
border-color: rgb(96 165 250 / var(--tw-border-opacity));
}
-.border-blue-500 {
- --tw-border-opacity: 1;
- border-color: rgb(59 130 246 / var(--tw-border-opacity));
-}
-
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
@@ -849,6 +836,11 @@ video {
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
+.bg-blue-600 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(37 99 235 / var(--tw-bg-opacity));
+}
+
.bg-gray-200 {
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
@@ -859,11 +851,21 @@ video {
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
}
+.bg-gray-900 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(17 24 39 / var(--tw-bg-opacity));
+}
+
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
+.bg-red-600 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(220 38 38 / var(--tw-bg-opacity));
+}
+
.p-2 {
padding: 0.5rem;
}
@@ -882,11 +884,6 @@ video {
padding-right: 0.5rem;
}
-.px-8 {
- padding-left: 2rem;
- padding-right: 2rem;
-}
-
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
@@ -1009,9 +1006,17 @@ video {
display: inline-block;
}
+ .md\:flex {
+ display: flex;
+ }
+
.md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+
+ .md\:flex-row {
+ flex-direction: row;
+ }
}
@media (min-width: 1024px) {
@@ -1037,10 +1042,6 @@ video {
}
@media (min-width: 1280px) {
- .xl\:inline-block {
- display: inline-block;
- }
-
.xl\:grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
diff --git a/static/styles.min.css b/static/styles.min.css
index e7a0414..61afc60 100644
--- a/static/styles.min.css
+++ b/static/styles.min.css
@@ -1 +1 @@
-/*! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.static{position:static}.float-left{float:left}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-0{margin-bottom:0}.ml-1{margin-left:.25rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.size-full{height:100%;width:100%}.h-60{height:15rem}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.min-h-0{min-height:0}.w-1\/2{width:50%}.w-2\/6{width:33.333333%}.w-4\/6{width:66.666667%}.w-full{width:100%}.flex-grow,.grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.basis-2\/4{flex-basis:50%}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.auto-cols-max{grid-auto-columns:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-6{gap:1.5rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-6{-moz-column-gap:1.5rem;column-gap:1.5rem}.gap-y-2{row-gap:.5rem}.gap-y-3{row-gap:.75rem}.divide-y-4>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(4px*var(--tw-divide-y-reverse));border-top-width:calc(4px*(1 - var(--tw-divide-y-reverse)))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity))}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-16{padding-left:4rem;padding-right:4rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pl-10{padding-left:2.5rem}.pr-6{padding-right:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.active\:bg-blue-300:active{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity))}.active\:bg-gray-100:active{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}@media (min-width:640px){.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:flex-1{flex:1 1 0%}.sm\:justify-between{justify-content:space-between}}@media (min-width:768px){.md\:inline-block{display:inline-block}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:inline-block{display:inline-block}.lg\:w-2\/6{width:33.333333%}.lg\:w-4\/6{width:66.666667%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1280px){.xl\:inline-block{display:inline-block}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1536px){.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
\ No newline at end of file
+/*! tailwindcss v3.4.11 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-tap-highlight-color:transparent}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-feature-settings:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.static{position:static}.float-left{float:left}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-8{margin-left:2rem;margin-right:2rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-0{margin-bottom:0}.ml-1{margin-left:.25rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.size-full{height:100%;width:100%}.h-60{height:15rem}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.min-h-0{min-height:0}.w-1\/2{width:50%}.w-2\/6{width:33.333333%}.w-4\/6{width:66.666667%}.w-full{width:100%}.flex-grow,.grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.basis-2\/4{flex-basis:50%}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.auto-cols-max{grid-auto-columns:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-2{row-gap:.5rem}.divide-y-4>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(4px*var(--tw-divide-y-reverse));border-top-width:calc(4px*(1 - var(--tw-divide-y-reverse)))}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-4{border-width:4px}.border-b{border-bottom-width:1px}.border-black{--tw-border-opacity:1;border-color:rgb(0 0 0/var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity))}.p-2{padding:.5rem}.p-4{padding:1rem}.px-16{padding-left:4rem;padding-right:4rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pl-10{padding-left:2.5rem}.pr-6{padding-right:1.5rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.active\:bg-blue-300:active{--tw-bg-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity))}.active\:bg-gray-100:active{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}@media (min-width:640px){.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:flex-1{flex:1 1 0%}.sm\:justify-between{justify-content:space-between}}@media (min-width:768px){.md\:inline-block{display:inline-block}.md\:flex{display:flex}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:flex-row{flex-direction:row}}@media (min-width:1024px){.lg\:inline-block{display:inline-block}.lg\:w-2\/6{width:33.333333%}.lg\:w-4\/6{width:66.666667%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:1536px){.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}
\ No newline at end of file