Add product admin

This commit is contained in:
2026-01-17 12:29:47 +01:00
parent a6e97c6170
commit a451d2e532
6 changed files with 242 additions and 10 deletions

View File

@@ -1,12 +1,14 @@
from decimal import Decimal
from typing import Annotated
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, File, Form, HTTPException, Request
from sqlalchemy import select
from starlette import status
from starlette.responses import RedirectResponse
from allmende_payment_system.api import types
from allmende_payment_system.api.dependencies import SessionDep, UserDep
from allmende_payment_system.models import Permission, User, UserGroup
from allmende_payment_system.models import Area, Permission, Product, User, UserGroup
from allmende_payment_system.tools import get_jinja_renderer
admin_router = APIRouter(prefix="/admin")
@@ -156,3 +158,112 @@ async def delete_group(
).scalar_one()
session.delete(group)
return RedirectResponse(url="/admin/groups", status_code=status.HTTP_303_SEE_OTHER)
# PRODUCTS
@admin_router.get("/products")
async def get_products(
request: Request,
session: SessionDep,
user: UserDep,
):
products = session.scalars(
select(Product).order_by(Product.area_id, Product.name)
).all()
templates = get_jinja_renderer()
return templates.TemplateResponse(
"products.html.jinja",
context={"request": request, "products": products},
)
@admin_router.get("/products/edit/{product_id}")
async def edit_product_get(
request: Request, session: SessionDep, user: UserDep, product_id: int
):
product = session.execute(select(Product).where(Product.id == product_id)).scalar()
areas = session.scalars(select(Area)).all()
templates = get_jinja_renderer()
return templates.TemplateResponse(
"product_edit.html.jinja",
context={
"request": request,
"product": product,
"edit_mode": True,
"areas": areas,
},
)
@admin_router.post("/products/edit/{product_id}")
async def edit_product_post(
request: Request,
session: SessionDep,
user: UserDep,
product_id: int,
product_data: Annotated[types.Product, Form()],
):
if not user.has_permission("product", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
product = session.execute(
select(Product).where(Product.id == product_id)
).scalar_one()
for field_name, data in product_data.model_dump().items():
setattr(product, field_name, data)
session.flush()
return RedirectResponse(
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
)
@admin_router.get("/products/new")
async def new_product_get(
request: Request,
session: SessionDep,
user: UserDep,
):
if not user.has_permission("product", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
areas = session.scalars(select(Area)).all()
templates = get_jinja_renderer()
return templates.TemplateResponse(
"product_edit.html.jinja",
context={"request": request, "edit_mode": False, "areas": areas},
)
@admin_router.post("/products/new")
async def new_product_post(
request: Request,
session: SessionDep,
user: UserDep,
product_data: Annotated[types.Product, Form()],
# product_image: Annotated[bytes, File()]
):
if not user.has_permission("product", "edit"):
raise HTTPException(status_code=403, detail="Insufficient permissions")
# print(len(product_image))
product = Product()
for field_name, data in product_data.model_dump().items():
setattr(product, field_name, data)
session.add(product)
return RedirectResponse(
url="/admin/products", status_code=status.HTTP_303_SEE_OTHER
)

View File

@@ -0,0 +1,20 @@
import typing
from decimal import Decimal
from pydantic import BaseModel
UnitsOfMeasure = typing.Literal[
"g",
"kg",
"l",
"piece",
]
class Product(BaseModel):
name: str
price: Decimal
area_id: int
vat_rate: Decimal
allow_fractional: bool = False
unit_of_measure: UnitsOfMeasure

View File

@@ -12,6 +12,8 @@ from sqlalchemy.orm import (
relationship,
)
from allmende_payment_system.api.types import UnitsOfMeasure
TABLE_PREFIX = "aps_"
@@ -131,14 +133,6 @@ class Area(Base):
products: Mapped[list["Product"]] = relationship("Product")
UnitsOfMeasure = typing.Literal[
"g",
"kg",
"l",
"piece",
]
class Product(Base):
__tablename__ = TABLE_PREFIX + "product"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)

View File

@@ -29,6 +29,13 @@
Einkaufen
</a>
</li>
{% if request.state.user.has_permission("product", "edit") %}
<li class="nav-item">
<a href="/admin/products" class="nav-link{% if request.url.path.startswith("/admin/products")%} active{% endif %}">
Produktverwaltung
</a>
</li>
{% endif %}
{% if request.state.user.has_permission("user", "edit") %}
<li class="nav-item">
<a href="/admin/users" class="nav-link{% if request.url.path.startswith("/admin/users")%} active{% endif %}">

View File

@@ -0,0 +1,57 @@
{% extends "base.html.jinja" %}
{% block content %}
<div class="mb-4">
<h2 class="h4 mb-3">Produkt {% if not edit_mode %}erstellen{% else %}bearbeiten{% endif %}</h2>
</div>
<form method="post">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" {% if edit_mode %}value="{{ product.name }}"{% endif %} required>
</div>
<div class="mb-3">
<label for="price" class="form-label">Preis</label>
<input type="number" step="0.01" class="form-control" id="price" name="price" {% if edit_mode %}value="{{ product.price }}"{% endif %} required>
</div>
<div class="mb-3">
<label for="unit_of_measure" class="form-label">Einheit</label>
<select class="form-select" id="unit_of_measure" name="unit_of_measure" required>
<option value="g" {% if edit_mode and product.unit_of_measure == 'g' %}selected{% endif %}>g</option>
<option value="kg" {% if edit_mode and product.unit_of_measure == 'kg' %}selected{% endif %}>kg</option>
<option value="l" {% if edit_mode and product.unit_of_measure == 'l' %}selected{% endif %}>l</option>
<option value="piece" {% if edit_mode and product.unit_of_measure == 'piece' %}selected{% endif %}>Stück</option>
</select>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="allow_fractional" name="allow_fractional" {% if edit_mode and product.allow_fractional %}checked{% endif %}>
<label class="form-check-label" for="allow_fractional">Bruchteile erlauben</label>
</div>
<div class="mb-3">
<label for="vat_rate" class="form-label">MwSt-Satz</label>
<select class="form-select" id="vat_rate" name="vat_rate" required>
<option value="7" {% if edit_mode and product.vat_rate == '7' %}selected{% endif %}>7 %</option>
<option value="19" {% if edit_mode and product.vat_rate == '19' %}selected{% endif %}>19 %</option>
</select>
</div>
<div class="mb-3">
<label for="area" class="form-label">Bereich</label>
<select class="form-select" id="area" name="area_id" required>
{% for area in areas %}
<option value="{{ area.id }}" {% if edit_mode and area.id == product.area_id %}selected{% endif %}>{{ area.name }}</option>
{% endfor %}
</select>
</div>
<!-- <div class="mb-3">
<label for="image_path" class="form-label">Bild-Pfad</label>
<input class="form-control" type="file" id="image_path" accept="image/*">
</div> -->
<button type="submit" class="btn btn-primary">Speichern</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "base.html.jinja" %}
{% block content %}
<div class="mb-4">
<h2 class="h4 mb-3">Produkte verwalten</h2>
<a class="btn btn-primary" href="/admin/products/new">Neues Produkt erstellen</a>
</div>
{% if products|length == 0 %}
<div class="alert alert-info">Keine Produkte vorhanden.</div>
{% else %}
<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Bereich</th>
<th>Preis</th>
<th>Einheit</th>
<th>Steuersatz</th>
<th></th>
</tr>
</thead>
<tbody>
{% for product in products %}
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>{{ product.area.name }}</td>
<td>{{ product.price | format_number}} €</td>
<td>{{ product.unit_of_measure | units_of_measure_de}}</td>
<td>{{ product.vat_rate | int }} %</td>
<td class="text-end">
<a class="btn btn-sm btn-primary" href="/admin/products/edit/{{ product.id }}">Bearbeiten</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}