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}
|
_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:
|
def visibility_of(food: dict[str, Any] | None) -> str:
|
||||||
if not food:
|
if not food:
|
||||||
return VIS_BLS
|
return VIS_BLS
|
||||||
|
|
|
||||||
|
|
@ -188,7 +188,7 @@ def admin_list_foods(
|
||||||
|
|
||||||
|
|
||||||
@router.get("/foods/{food_id}")
|
@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
|
from data_layer.nutrition_items import resolve_food_attributes
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
|
@ -197,6 +197,13 @@ def admin_food_detail(food_id: str, session: dict = Depends(require_admin)):
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(404, "Lebensmittel nicht gefunden")
|
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)
|
attrs = resolve_food_attributes(cur, food_id)
|
||||||
return {**r2d(row), "attributes": attrs}
|
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
|
SELECT id, name_de, name_en, catalog_kind, owner_profile_id
|
||||||
FROM food_catalog
|
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_id,),
|
||||||
)
|
)
|
||||||
food = cur.fetchone()
|
food = cur.fetchone()
|
||||||
if not food:
|
if not food:
|
||||||
raise HTTPException(404, "Zentrales Lebensmittel nicht gefunden")
|
raise HTTPException(404, "Manuelles Lebensmittel nicht gefunden")
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE food_catalog
|
UPDATE food_catalog
|
||||||
|
|
|
||||||
|
|
@ -238,8 +238,8 @@ def get_food_detail(food_id: str, session: dict = Depends(require_auth)):
|
||||||
if not food:
|
if not food:
|
||||||
raise HTTPException(404, "Lebensmittel nicht gefunden")
|
raise HTTPException(404, "Lebensmittel nicht gefunden")
|
||||||
food = dict(food)
|
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, same_id
|
||||||
from data_layer.food_visibility import attach_visibility
|
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))
|
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_global_mapping_target,
|
||||||
is_visible_to,
|
is_visible_to,
|
||||||
require_mapping_target,
|
require_mapping_target,
|
||||||
|
same_id,
|
||||||
visibility_of,
|
visibility_of,
|
||||||
visibility_rank,
|
visibility_rank,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_id_ignores_dashes_and_case():
|
||||||
|
assert same_id("AaBbCcDd-0000-0000-0000-000000000001", "aabbccdd000000000000000000000001")
|
||||||
|
|
||||||
|
|
||||||
def test_visibility_scopes():
|
def test_visibility_scopes():
|
||||||
assert visibility_of({"catalog_kind": "official_bls"}) == "bls"
|
assert visibility_of({"catalog_kind": "official_bls"}) == "bls"
|
||||||
assert visibility_of({"catalog_kind": "manual_admin", "owner_profile_id": None}) == "global"
|
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
|
"activity": "1.2.1", # Legacy CSV import: activity_entries feature enforcement
|
||||||
"nutrition": "1.3.7", # Sichtbarkeit persönlich vs. zentral im Mapping
|
"nutrition": "1.3.7", # Sichtbarkeit persönlich vs. zentral im Mapping
|
||||||
"tandoor": "1.0.1", # Router-Import ohne future-annotations (Backend startet)
|
"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",
|
"photos": "1.0.0",
|
||||||
"insights": "1.3.0",
|
"insights": "1.3.0",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ export default function FoodEditDialog({ foodId, onClose, onSaved, zIndex, mode
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let alive = true
|
let alive = true
|
||||||
api.getBlsFood(foodId).then((d) => {
|
const load = isAdmin ? api.adminBlsFoodDetail(foodId, true) : api.getBlsFood(foodId)
|
||||||
|
load.then((d) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
setName(d.name_de || '')
|
setName(d.name_de || '')
|
||||||
const m = d.macros_per_100g || {}
|
const m = d.macros_per_100g || {}
|
||||||
|
|
@ -43,7 +44,8 @@ export default function FoodEditDialog({ foodId, onClose, onSaved, zIndex, mode
|
||||||
inputUnit: row.attr?.unit || 'g',
|
inputUnit: row.attr?.unit || 'g',
|
||||||
})))
|
})))
|
||||||
setServingG(numStr(d.serving_g || 100) === '0' ? '100' : numStr(d.serving_g || 100))
|
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)
|
setLoading(false)
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
if (!alive) return
|
if (!alive) return
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
|
import { useAuth } from '../context/AuthContext'
|
||||||
import FoodEditDialog from './FoodEditDialog'
|
import FoodEditDialog from './FoodEditDialog'
|
||||||
import { foodVisibilitySuffix } from '../utils/foodVisibility'
|
import { foodVisibilitySuffix } from '../utils/foodVisibility'
|
||||||
|
|
||||||
|
|
@ -9,6 +10,7 @@ function filterByName(list, term) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MyFoodsPanel() {
|
export default function MyFoodsPanel() {
|
||||||
|
const { isAdmin } = useAuth()
|
||||||
const [foods, setFoods] = useState([])
|
const [foods, setFoods] = useState([])
|
||||||
const [shared, setShared] = useState([])
|
const [shared, setShared] = useState([])
|
||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
|
|
@ -17,6 +19,7 @@ export default function MyFoodsPanel() {
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [notice, setNotice] = useState(null)
|
const [notice, setNotice] = useState(null)
|
||||||
const [editId, setEditId] = useState(null)
|
const [editId, setEditId] = useState(null)
|
||||||
|
const [editMode, setEditMode] = useState('user')
|
||||||
const [creating, setCreating] = useState(false)
|
const [creating, setCreating] = useState(false)
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
|
|
@ -80,11 +83,15 @@ export default function MyFoodsPanel() {
|
||||||
)}
|
)}
|
||||||
{shownMine.map((f) => (
|
{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 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={{ fontWeight: 600 }}>{f.name_de}</div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>nur du</div>
|
<div style={{ fontSize: 12, color: 'var(--text3)' }}>nur du</div>
|
||||||
</div>
|
</button>
|
||||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12 }} onClick={() => setEditId(f.id)}>Bearbeiten</button>
|
<button type="button" className="btn btn-primary" style={{ fontSize: 12 }} onClick={() => { setEditMode('user'); setEditId(f.id) }}>Bearbeiten</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{visible < filteredMine.length && (
|
{visible < filteredMine.length && (
|
||||||
|
|
@ -101,14 +108,20 @@ export default function MyFoodsPanel() {
|
||||||
<p className="muted">Kein zentraler Treffer für „{q.trim()}“.</p>
|
<p className="muted">Kein zentraler Treffer für „{q.trim()}“.</p>
|
||||||
)}
|
)}
|
||||||
{filteredShared.map((f) => (
|
{filteredShared.map((f) => (
|
||||||
<div key={f.id} style={{ padding: '10px 0', borderTop: '1px solid var(--border)' }}>
|
<div key={f.id} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, padding: '10px 0', borderTop: '1px solid var(--border)', alignItems: 'center' }}>
|
||||||
<div style={{ fontWeight: 600 }}>{f.name_de}</div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>für alle Nutzer{foodVisibilitySuffix(f.catalog_kind, f.visibility)}</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>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{(creating || editId) && (
|
{(creating || editId) && (
|
||||||
<FoodEditDialog
|
<FoodEditDialog
|
||||||
|
mode={creating ? 'user' : editMode}
|
||||||
foodId={creating ? null : editId}
|
foodId={creating ? null : editId}
|
||||||
onClose={() => { setCreating(false); setEditId(null) }}
|
onClose={() => { setCreating(false); setEditId(null) }}
|
||||||
onSaved={(res) => {
|
onSaved={(res) => {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,15 @@ export default function AdminBlsFoodsPage() {
|
||||||
api.adminBlsFoodDetail(id).then(setDetail).catch((e) => setError(e.message))
|
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
|
const list = tab === 'manual' ? shownManual : rows
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -91,8 +100,7 @@ export default function AdminBlsFoodsPage() {
|
||||||
{tab === 'manual' && (
|
{tab === 'manual' && (
|
||||||
<>
|
<>
|
||||||
<p style={{ fontSize: 13, color: 'var(--text2)', margin: '0 0 10px' }}>
|
<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;
|
Zentral (für alle Nutzer) und persönlich (Nutzer-eigene). Zeile oder Bearbeiten öffnet den Editor.
|
||||||
persönliche bleiben beim Owner.
|
|
||||||
</p>
|
</p>
|
||||||
<div style={{ marginBottom: 10 }}>
|
<div style={{ marginBottom: 10 }}>
|
||||||
<button type="button" className="btn btn-primary" onClick={() => setCreating(true)}>Zentral anlegen</button>
|
<button type="button" className="btn btn-primary" onClick={() => setCreating(true)}>Zentral anlegen</button>
|
||||||
|
|
@ -124,13 +132,13 @@ export default function AdminBlsFoodsPage() {
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
style={{ flex: 1, textAlign: 'left' }}
|
style={{ flex: 1, textAlign: 'left' }}
|
||||||
onClick={() => openDetail(r.id)}
|
onClick={() => openEdit(r)}
|
||||||
>
|
>
|
||||||
{r.bls_code ? `${r.bls_code} · ` : ''}{r.name_de}
|
{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' ? ' · zentral (alle)' : r.catalog_kind === 'manual_user' ? ' · persönlich' : ''}
|
||||||
</button>
|
</button>
|
||||||
{r.catalog_kind === 'manual_admin' && (
|
{r.catalog_kind && r.catalog_kind !== 'official_bls' && (
|
||||||
<button type="button" className="btn btn-secondary" onClick={() => setEditId(r.id)}>Bearbeiten</button>
|
<button type="button" className="btn btn-primary" onClick={() => openEdit(r)}>Bearbeiten</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ export const api = {
|
||||||
adminBlsImportJob: (id) => req(`/admin/bls/import/jobs/${id}`),
|
adminBlsImportJob: (id) => req(`/admin/bls/import/jobs/${id}`),
|
||||||
adminBlsImportApply: (id) => req(`/admin/bls/import/jobs/${id}/apply`, {method:'POST'}),
|
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):''}`),
|
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)),
|
adminCreateManualFood: (d) => req('/admin/bls/foods/manual', json(d)),
|
||||||
adminUpdateManualFood: (id, d) => req(`/admin/bls/foods/manual/${encodeURIComponent(id)}`, jput(d)),
|
adminUpdateManualFood: (id, d) => req(`/admin/bls/foods/manual/${encodeURIComponent(id)}`, jput(d)),
|
||||||
adminBlsAttributes: () => req('/admin/bls/attributes'),
|
adminBlsAttributes: () => req('/admin/bls/attributes'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user