Started with area page
This commit is contained in:
@@ -18,3 +18,13 @@ async def get_shop(request: Request, session: SessionDep):
|
|||||||
"shop.html.jinja",
|
"shop.html.jinja",
|
||||||
context={"request": request, "areas": areas},
|
context={"request": request, "areas": areas},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@shop_router.get("/shop/area/{area_id}")
|
||||||
|
async def get_shop(request: Request, session: SessionDep, area_id: int):
|
||||||
|
query = select(Area).where(Area.id == area_id)
|
||||||
|
area = session.scalars(query).one()
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"area.html.jinja",
|
||||||
|
context={"request": request, "area": area},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import locale
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI
|
from fastapi import Depends, FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
@@ -5,6 +7,7 @@ from allmende_payment_system.api import root_router
|
|||||||
from allmende_payment_system.api.dependencies import get_user
|
from allmende_payment_system.api.dependencies import get_user
|
||||||
from allmende_payment_system.api.shop import shop_router
|
from allmende_payment_system.api.shop import shop_router
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, "de_DE.UTF-8")
|
||||||
app = FastAPI(dependencies=[Depends(get_user)])
|
app = FastAPI(dependencies=[Depends(get_user)])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ class Area(Base):
|
|||||||
description: Mapped[str] = mapped_column(nullable=True)
|
description: Mapped[str] = mapped_column(nullable=True)
|
||||||
image_path: Mapped[str] = mapped_column(nullable=True)
|
image_path: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
products: Mapped[list["Product"]] = relationship("Product")
|
||||||
|
|
||||||
|
|
||||||
|
UnitsOfMeasure = typing.Literal[
|
||||||
|
"g",
|
||||||
|
"kg",
|
||||||
|
"piece",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class Product(Base):
|
class Product(Base):
|
||||||
__tablename__ = TABLE_PREFIX + "product"
|
__tablename__ = TABLE_PREFIX + "product"
|
||||||
@@ -62,11 +71,14 @@ class Product(Base):
|
|||||||
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
name: Mapped[str] = mapped_column(nullable=False, unique=True)
|
||||||
|
|
||||||
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
|
unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False)
|
||||||
# TODO: limit this to actually used vat rates?
|
# TODO: limit this to actually used vat rates?
|
||||||
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
|
||||||
|
|
||||||
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
area_id: Mapped[int] = mapped_column(ForeignKey(TABLE_PREFIX + "area.id"))
|
||||||
area: Mapped["Area"] = relationship("Area")
|
area: Mapped["Area"] = relationship("Area", back_populates="products")
|
||||||
|
|
||||||
|
image_path: Mapped[str] = mapped_column(nullable=True)
|
||||||
|
|
||||||
|
|
||||||
TransactionTypes = typing.Literal[
|
TransactionTypes = typing.Literal[
|
||||||
|
|||||||
BIN
src/allmende_payment_system/static/img/placeholder.jpg
Normal file
BIN
src/allmende_payment_system/static/img/placeholder.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
36
src/allmende_payment_system/templates/area.html.jinja
Normal file
36
src/allmende_payment_system/templates/area.html.jinja
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html.jinja" %}
|
||||||
|
{% block content %}
|
||||||
|
<!-- Area Header -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="h4 mb-3">{{ area.name }}</h2>
|
||||||
|
<p class="text-muted">{{ area.description or ''}} </p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Products Grid -->
|
||||||
|
<div class="row g-3">
|
||||||
|
{% for product in area.products %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
|
<a href="#" class="text-decoration-none text-dark">
|
||||||
|
<!-- Product Image -->
|
||||||
|
<img
|
||||||
|
src="/static/img/{{ product.image_path if product.image_path else 'placeholder.jpg' }}"
|
||||||
|
alt="{{ product.name }}"
|
||||||
|
class="card-img-top img-fluid rounded-top"
|
||||||
|
style="height: 100px; object-fit: cover;"
|
||||||
|
>
|
||||||
|
<!-- Product Details -->
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title mb-2">{{ product.name }}</h5>
|
||||||
|
<p class="card-text text-muted small mb-3">{{ product.description }}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<span class="fw-bold">{{ product.price|format_number }} € pro {{ product.unit_of_measure|units_of_measure_de }}</span>
|
||||||
|
<button class="btn btn-sm btn-outline-primary">Add to Cart</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -9,13 +9,13 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
{% for area in areas %}
|
{% for area in areas %}
|
||||||
<div class="col-md-6 col-lg-4">
|
<div class="col-md-6 col-lg-4">
|
||||||
<a href="#" class="text-decoration-none">
|
<a href="/shop/area/{{ area.id }}" class="text-decoration-none">
|
||||||
<div class="card h-100 border-0 shadow-sm">
|
<div class="card h-100 border-0 shadow-sm">
|
||||||
<div class="card-body d-flex align-items-center p-3">
|
<div class="card-body d-flex align-items-center p-3">
|
||||||
<!-- Image on the left -->
|
<!-- Image on the left -->
|
||||||
<div class="me-3" style="width: 120px; flex-shrink: 0;">
|
<div class="me-3" style="width: 120px; flex-shrink: 0;">
|
||||||
<img
|
<img
|
||||||
src="/static/img/{{ area.image_path }}"
|
src="/static/img/{{ area.image_path if area.image_path !='' else 'placeholder.png'}}" }}"
|
||||||
alt="{{ area.name }}"
|
alt="{{ area.name }}"
|
||||||
class="img-fluid rounded"
|
class="img-fluid rounded"
|
||||||
style="max-height: 100px; width: 100%; object-fit: contain;"
|
style="max-height: 100px; width: 100%; object-fit: contain;"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import locale
|
||||||
|
import numbers
|
||||||
|
|
||||||
from starlette.templating import Jinja2Templates
|
from starlette.templating import Jinja2Templates
|
||||||
|
|
||||||
TRANSACTION_TYPE_DE = {
|
TRANSACTION_TYPE_DE = {
|
||||||
@@ -7,8 +10,19 @@ TRANSACTION_TYPE_DE = {
|
|||||||
"product": "Einkauf",
|
"product": "Einkauf",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UNITS_OF_MEASURE = {"piece": "Stück"}
|
||||||
|
|
||||||
|
|
||||||
|
def format_number(value: float):
|
||||||
|
try:
|
||||||
|
return f"{value:n}"
|
||||||
|
except TypeError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def get_jinja_renderer() -> Jinja2Templates:
|
def get_jinja_renderer() -> Jinja2Templates:
|
||||||
renderer = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
renderer = Jinja2Templates(directory="src/allmende_payment_system/templates")
|
||||||
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
|
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
|
||||||
|
renderer.env.filters["units_of_measure_de"] = lambda x: UNITS_OF_MEASURE.get(x, x)
|
||||||
|
renderer.env.filters["format_number"] = format_number
|
||||||
return renderer
|
return renderer
|
||||||
|
|||||||
Reference in New Issue
Block a user