mitai-jinkendo/backend/routers/admin_food_mappings.py
Lars 919c77fcf8
Some checks failed
Deploy Development / deploy (push) Failing after 52s
Build Test / pytest-backend (push) Successful in 3s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Failing after 9s
feat: BLS-Stammdaten, FDDB-Mapping und Item-Tagebuch (#106)
Katalog, lernendes Mapping ohne KI, optionale Items und Import-Policy.
Playwright-Smoke und Issue-Audit um Ernährung/Zuordnen/API ergänzt.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 14:35:57 +02:00

104 lines
3.4 KiB
Python

"""Admin CRUD for food_name_mappings."""
from __future__ import annotations
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from auth import require_admin
from data_layer.food_mapping import upsert_food_mapping
from db import get_cursor, get_db, r2d
router = APIRouter(prefix="/api/admin/food-mappings", tags=["admin", "food-mappings"])
class FoodMappingCreate(BaseModel):
source_name: str
food_id: str
profile_id: Optional[str] = None
source_system: str = "fddb"
@router.get("")
def list_food_mappings(
profile_id: Optional[str] = None,
global_only: bool = False,
session: dict = Depends(require_admin),
):
with get_db() as conn:
cur = get_cursor(conn)
q = """
SELECT m.id, m.source_name_raw, m.source_name_normalized, m.food_id,
m.profile_id, m.source, m.created_at, m.updated_at,
f.name_de AS food_name_de, f.bls_code, f.catalog_kind
FROM food_name_mappings m
JOIN food_catalog f ON f.id = m.food_id
"""
conds, params = [], []
if global_only:
conds.append("m.profile_id IS NULL")
elif profile_id:
conds.append("m.profile_id = %s")
params.append(profile_id)
if conds:
q += " WHERE " + " AND ".join(conds)
q += " ORDER BY m.source_name_normalized"
cur.execute(q, params)
return [r2d(r) for r in cur.fetchall()]
@router.get("/stats/coverage")
def mapping_coverage(session: dict = Depends(require_admin)):
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""
SELECT
COUNT(*) AS total_items,
COUNT(food_id) AS mapped_items,
COUNT(*) - COUNT(food_id) AS unmapped_items,
COUNT(DISTINCT source_name_normalized) AS unique_names,
COUNT(DISTINCT CASE WHEN food_id IS NULL THEN source_name_normalized END) AS unmapped_names
FROM nutrition_items
"""
)
return r2d(cur.fetchone())
@router.post("")
def create_food_mapping(body: FoodMappingCreate, session: dict = Depends(require_admin)):
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT id FROM food_catalog WHERE id = %s", (body.food_id,))
if not cur.fetchone():
raise HTTPException(404, "Lebensmittel nicht gefunden")
mid = upsert_food_mapping(
cur,
source_name_raw=body.source_name,
food_id=body.food_id,
profile_id=body.profile_id or None,
source="admin",
source_system=body.source_system,
)
cur.execute(
"""
SELECT m.*, f.name_de AS food_name_de, f.bls_code
FROM food_name_mappings m
JOIN food_catalog f ON f.id = m.food_id
WHERE m.id = %s
""",
(mid,),
)
return r2d(cur.fetchone())
@router.delete("/{mapping_id}")
def delete_food_mapping(mapping_id: int, session: dict = Depends(require_admin)):
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("DELETE FROM food_name_mappings WHERE id = %s RETURNING id", (mapping_id,))
if not cur.fetchone():
raise HTTPException(404, "Mapping nicht gefunden")
return {"ok": True}