fix: manuelle Lebensmittel in der Liste wieder bearbeiten
Admin-Katalog und Nutzer-Liste oeffnen den Editor statt nur die Lesansicht; PUT gilt fuer zentrale und persoenliche Manuals. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8b1e1da5d8
commit
9a0767c8ac
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<div key={f.id} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '10px 0', borderTop: '1px solid var(--border)', alignItems: 'center' }}>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setEditMode('user'); setEditId(f.id) }}
|
||||
style={{ background: 'none', border: 0, padding: 0, textAlign: 'left', color: 'inherit', cursor: 'pointer' }}
|
||||
>
|
||||
<div style={{ fontWeight: 600 }}>{f.name_de}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>nur du</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => setEditId(f.id)}>Bearbeiten</button>
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary" style={{ fontSize: 12 }} onClick={() => { setEditMode('user'); setEditId(f.id) }}>Bearbeiten</button>
|
||||
</div>
|
||||
))}
|
||||
{visible < filteredMine.length && (
|
||||
|
|
@ -101,14 +108,20 @@ export default function MyFoodsPanel() {
|
|||
<p className="muted">Kein zentraler Treffer für „{q.trim()}“.</p>
|
||||
)}
|
||||
{filteredShared.map((f) => (
|
||||
<div key={f.id} style={{ padding: '10px 0', borderTop: '1px solid var(--border)' }}>
|
||||
<div style={{ fontWeight: 600 }}>{f.name_de}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>für alle Nutzer{foodVisibilitySuffix(f.catalog_kind, f.visibility)}</div>
|
||||
<div key={f.id} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '10px 0', borderTop: '1px solid var(--border)', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600 }}>{f.name_de}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>für alle Nutzer{foodVisibilitySuffix(f.catalog_kind, f.visibility)}</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<button type="button" className="btn btn-primary" style={{ fontSize: 12 }} onClick={() => { setEditMode('admin'); setEditId(f.id) }}>Bearbeiten</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(creating || editId) && (
|
||||
<FoodEditDialog
|
||||
mode={creating ? 'user' : editMode}
|
||||
foodId={creating ? null : editId}
|
||||
onClose={() => { setCreating(false); setEditId(null) }}
|
||||
onSaved={(res) => {
|
||||
|
|
|
|||
|
|
@ -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' && (
|
||||
<>
|
||||
<p style={{ fontSize: 13, color: 'var(--text2)', margin: '0 0 10px' }}>
|
||||
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.
|
||||
</p>
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<button type="button" className="btn btn-primary" onClick={() => setCreating(true)}>Zentral anlegen</button>
|
||||
|
|
@ -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' : ''}
|
||||
</button>
|
||||
{r.catalog_kind === 'manual_admin' && (
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setEditId(r.id)}>Bearbeiten</button>
|
||||
{r.catalog_kind && r.catalog_kind !== 'official_bls' && (
|
||||
<button type="button" className="btn btn-primary" onClick={() => openEdit(r)}>Bearbeiten</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user