"""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 apply_mapping_to_items, upsert_food_mapping from data_layer.nutrition_items import dates_for_normalized_name, rebuild_daily_nutrients 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" class FoodMappingUpdate(BaseModel): food_id: str grams_per_unit: Optional[float] = None source_unit: Optional[str] = None @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.put("/{mapping_id}") def update_food_mapping(mapping_id: int, body: FoodMappingUpdate, session: dict = Depends(require_admin)): with get_db() as conn: cur = get_cursor(conn) cur.execute( """ SELECT id, source_name_raw, source_name_normalized, profile_id FROM food_name_mappings WHERE id = %s """, (mapping_id,), ) row = cur.fetchone() if not row: raise HTTPException(404, "Mapping nicht gefunden") cur.execute("SELECT id FROM food_catalog WHERE id = %s AND is_active = true", (body.food_id,)) if not cur.fetchone(): raise HTTPException(404, "Lebensmittel nicht gefunden") cur.execute( """ UPDATE food_name_mappings SET food_id = %s, grams_per_unit = COALESCE(%s, grams_per_unit), source_unit = COALESCE(%s, source_unit), source = 'admin', updated_at = NOW() WHERE id = %s """, (body.food_id, body.grams_per_unit, body.source_unit, mapping_id), ) if row.get("profile_id"): apply_mapping_to_items(cur, str(row["profile_id"]), row["source_name_normalized"], body.food_id, mapping_id) dates = dates_for_normalized_name(cur, str(row["profile_id"]), row["source_name_normalized"]) for d in dates: rebuild_daily_nutrients(cur, str(row["profile_id"]), d) 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 """, (mapping_id,), ) 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}