diff --git a/backend/data_layer/food_visibility.py b/backend/data_layer/food_visibility.py index 93dcc23..2993449 100644 --- a/backend/data_layer/food_visibility.py +++ b/backend/data_layer/food_visibility.py @@ -29,6 +29,10 @@ _KIND_VIS = { _VIS_RANK = {VIS_PERSONAL: 0, VIS_GLOBAL: 1, VIS_BLS: 2} +def same_id(a, b) -> bool: + return str(a or "").replace("-", "").lower() == str(b or "").replace("-", "").lower() + + def visibility_of(food: dict[str, Any] | None) -> str: if not food: return VIS_BLS diff --git a/backend/routers/admin_bls.py b/backend/routers/admin_bls.py index d078521..16e3457 100644 --- a/backend/routers/admin_bls.py +++ b/backend/routers/admin_bls.py @@ -188,7 +188,7 @@ def admin_list_foods( @router.get("/foods/{food_id}") -def admin_food_detail(food_id: str, session: dict = Depends(require_admin)): +def admin_food_detail(food_id: str, editor: bool = False, session: dict = Depends(require_admin)): from data_layer.nutrition_items import resolve_food_attributes with get_db() as conn: @@ -197,6 +197,13 @@ def admin_food_detail(food_id: str, session: dict = Depends(require_admin)): row = cur.fetchone() if not row: raise HTTPException(404, "Lebensmittel nicht gefunden") + food = dict(row) + if editor: + from data_layer.food_attributes import editor_payload_for_food + from data_layer.food_visibility import attach_visibility + + editable = food.get("catalog_kind") in ("manual_admin", "manual_user") + return attach_visibility(editor_payload_for_food(cur, food, editable=editable)) attrs = resolve_food_attributes(cur, food_id) return {**r2d(row), "attributes": attrs} @@ -252,13 +259,13 @@ def admin_update_manual_food(food_id: str, body: ManualFoodCreate, session: dict """ SELECT id, name_de, name_en, catalog_kind, owner_profile_id FROM food_catalog - WHERE id = %s AND catalog_kind = 'manual_admin' AND is_active = true + WHERE id = %s AND catalog_kind IN ('manual_admin', 'manual_user') AND is_active = true """, (food_id,), ) food = cur.fetchone() if not food: - raise HTTPException(404, "Zentrales Lebensmittel nicht gefunden") + raise HTTPException(404, "Manuelles Lebensmittel nicht gefunden") cur.execute( """ UPDATE food_catalog diff --git a/backend/routers/bls.py b/backend/routers/bls.py index 366934d..f45723d 100644 --- a/backend/routers/bls.py +++ b/backend/routers/bls.py @@ -238,8 +238,8 @@ def get_food_detail(food_id: str, session: dict = Depends(require_auth)): if not food: raise HTTPException(404, "Lebensmittel nicht gefunden") food = dict(food) - editable = food.get("catalog_kind") == "manual_user" and str(food.get("owner_profile_id") or "") == str(pid) - from data_layer.food_visibility import attach_visibility + from data_layer.food_visibility import attach_visibility, same_id + editable = food.get("catalog_kind") == "manual_user" and same_id(food.get("owner_profile_id"), pid) return attach_visibility(editor_payload_for_food(cur, food, editable=editable)) diff --git a/backend/tests/test_food_visibility.py b/backend/tests/test_food_visibility.py index 42c9b0d..56b77ae 100644 --- a/backend/tests/test_food_visibility.py +++ b/backend/tests/test_food_visibility.py @@ -3,11 +3,16 @@ from data_layer.food_visibility import ( is_global_mapping_target, is_visible_to, require_mapping_target, + same_id, visibility_of, visibility_rank, ) +def test_same_id_ignores_dashes_and_case(): + assert same_id("AaBbCcDd-0000-0000-0000-000000000001", "aabbccdd000000000000000000000001") + + def test_visibility_scopes(): assert visibility_of({"catalog_kind": "official_bls"}) == "bls" assert visibility_of({"catalog_kind": "manual_admin", "owner_profile_id": None}) == "global" diff --git a/backend/version.py b/backend/version.py index 99f48b7..4ce2790 100644 --- a/backend/version.py +++ b/backend/version.py @@ -22,7 +22,7 @@ MODULE_VERSIONS = { "activity": "1.2.1", # Legacy CSV import: activity_entries feature enforcement "nutrition": "1.3.7", # Sichtbarkeit persönlich vs. zentral im Mapping "tandoor": "1.0.1", # Router-Import ohne future-annotations (Backend startet) - "bls": "1.0.9", # Nutzer-Foods privat, Admin-Foods zentral; Mapping achtet Sichtbarkeit + "bls": "1.0.10", # Manuelle Liste: Zeile öffnet Editor; Admin darf alle Manuals ändern "photos": "1.0.0", "insights": "1.3.0", diff --git a/frontend/src/components/FoodEditDialog.jsx b/frontend/src/components/FoodEditDialog.jsx index a69376c..7c7a77b 100644 --- a/frontend/src/components/FoodEditDialog.jsx +++ b/frontend/src/components/FoodEditDialog.jsx @@ -27,7 +27,8 @@ export default function FoodEditDialog({ foodId, onClose, onSaved, zIndex, mode return } let alive = true - api.getBlsFood(foodId).then((d) => { + const load = isAdmin ? api.adminBlsFoodDetail(foodId, true) : api.getBlsFood(foodId) + load.then((d) => { if (!alive) return setName(d.name_de || '') const m = d.macros_per_100g || {} @@ -43,7 +44,8 @@ export default function FoodEditDialog({ foodId, onClose, onSaved, zIndex, mode inputUnit: row.attr?.unit || 'g', }))) setServingG(numStr(d.serving_g || 100) === '0' ? '100' : numStr(d.serving_g || 100)) - setEditable(isAdmin ? d.catalog_kind === 'manual_admin' : Boolean(d.editable)) + const canEdit = Boolean(d.editable) || (isAdmin && d.catalog_kind && d.catalog_kind !== 'official_bls') + setEditable(canEdit) setLoading(false) }).catch((e) => { if (!alive) return diff --git a/frontend/src/components/MyFoodsPanel.jsx b/frontend/src/components/MyFoodsPanel.jsx index 0de7f68..250dfc5 100644 --- a/frontend/src/components/MyFoodsPanel.jsx +++ b/frontend/src/components/MyFoodsPanel.jsx @@ -1,5 +1,6 @@ import { useEffect, useMemo, useState } from 'react' import { api } from '../utils/api' +import { useAuth } from '../context/AuthContext' import FoodEditDialog from './FoodEditDialog' import { foodVisibilitySuffix } from '../utils/foodVisibility' @@ -9,6 +10,7 @@ function filterByName(list, term) { } export default function MyFoodsPanel() { + const { isAdmin } = useAuth() const [foods, setFoods] = useState([]) const [shared, setShared] = useState([]) const [q, setQ] = useState('') @@ -17,6 +19,7 @@ export default function MyFoodsPanel() { const [error, setError] = useState(null) const [notice, setNotice] = useState(null) const [editId, setEditId] = useState(null) + const [editMode, setEditMode] = useState('user') const [creating, setCreating] = useState(false) const load = async () => { @@ -80,11 +83,15 @@ export default function MyFoodsPanel() { )} {shownMine.map((f) => (
-
+
- + +
))} {visible < filteredMine.length && ( @@ -101,14 +108,20 @@ export default function MyFoodsPanel() {

Kein zentraler Treffer für „{q.trim()}“.

)} {filteredShared.map((f) => ( -
-
{f.name_de}
-
für alle Nutzer{foodVisibilitySuffix(f.catalog_kind, f.visibility)}
+
+
+
{f.name_de}
+
für alle Nutzer{foodVisibilitySuffix(f.catalog_kind, f.visibility)}
+
+ {isAdmin && ( + + )}
))} {(creating || editId) && ( { setCreating(false); setEditId(null) }} onSaved={(res) => { diff --git a/frontend/src/pages/AdminBlsFoodsPage.jsx b/frontend/src/pages/AdminBlsFoodsPage.jsx index 59dab86..994b2f7 100644 --- a/frontend/src/pages/AdminBlsFoodsPage.jsx +++ b/frontend/src/pages/AdminBlsFoodsPage.jsx @@ -54,6 +54,15 @@ export default function AdminBlsFoodsPage() { api.adminBlsFoodDetail(id).then(setDetail).catch((e) => setError(e.message)) } + const openEdit = (row) => { + if (row.catalog_kind === 'official_bls') { + openDetail(row.id) + return + } + setDetail(null) + setEditId(row.id) + } + const list = tab === 'manual' ? shownManual : rows return ( @@ -91,8 +100,7 @@ export default function AdminBlsFoodsPage() { {tab === 'manual' && ( <>

- Zentral (für alle Nutzer) und persönlich (nur der jeweilige Nutzer). Zentrale Einträge hier anlegen; - persönliche bleiben beim Owner. + Zentral (für alle Nutzer) und persönlich (Nutzer-eigene). Zeile oder Bearbeiten öffnet den Editor.

@@ -124,13 +132,13 @@ export default function AdminBlsFoodsPage() { type="button" className="btn btn-secondary" style={{ flex: 1, textAlign: 'left' }} - onClick={() => openDetail(r.id)} + onClick={() => openEdit(r)} > {r.bls_code ? `${r.bls_code} · ` : ''}{r.name_de} {r.catalog_kind === 'manual_admin' ? ' · zentral (alle)' : r.catalog_kind === 'manual_user' ? ' · persönlich' : ''} - {r.catalog_kind === 'manual_admin' && ( - + {r.catalog_kind && r.catalog_kind !== 'official_bls' && ( + )}
))} diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index f1cd94a..8517058 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -306,7 +306,7 @@ export const api = { adminBlsImportJob: (id) => req(`/admin/bls/import/jobs/${id}`), adminBlsImportApply: (id) => req(`/admin/bls/import/jobs/${id}/apply`, {method:'POST'}), adminBlsFoods: (q, kind, limit) => req(`/admin/bls/foods?${q?('q='+encodeURIComponent(q)+'&'):''}${kind?('kind='+encodeURIComponent(kind)+'&'):''}${limit?('limit='+limit):''}`), - adminBlsFoodDetail: (id) => req(`/admin/bls/foods/${id}`), + adminBlsFoodDetail: (id, editor=false) => req(`/admin/bls/foods/${encodeURIComponent(id)}${editor?'?editor=true':''}`), adminCreateManualFood: (d) => req('/admin/bls/foods/manual', json(d)), adminUpdateManualFood: (id, d) => req(`/admin/bls/foods/manual/${encodeURIComponent(id)}`, jput(d)), adminBlsAttributes: () => req('/admin/bls/attributes'),