feat: Enhance activity session handling and schema retrieval
- Updated `get_activity_session_logical_unit` to support optional parameters for form training context, allowing for more flexible schema resolution. - Introduced a new endpoint `/attribute-schema` to fetch activity attribute schemas without an existing session, improving manual data entry capabilities. - Enhanced the `getActivitySession` API method to accept query parameters for training category and type, facilitating dynamic schema retrieval. - Updated the frontend `ActivityPage` to utilize the new schema fetching logic, ensuring a smoother user experience when managing activity sessions and metrics.
This commit is contained in:
parent
92e334dcd2
commit
1220ee54fb
|
|
@ -621,16 +621,35 @@ def replace_activity_session_metrics(
|
|||
return fetch_activity_session_metrics(cur, activity_log_id)
|
||||
|
||||
|
||||
def get_activity_session_logical_unit(cur, profile_id: str, activity_log_id: str) -> Dict[str, Any]:
|
||||
def get_activity_session_logical_unit(
|
||||
cur,
|
||||
profile_id: str,
|
||||
activity_log_id: str,
|
||||
*,
|
||||
use_form_training_context: bool = False,
|
||||
form_training_category: Optional[str] = None,
|
||||
form_training_type_id: Optional[int] = None,
|
||||
) -> Dict[str, Any]:
|
||||
cur.execute("SELECT * FROM activity_log WHERE id = %s", (activity_log_id,))
|
||||
row = cur.fetchone()
|
||||
if not row or str(row["profile_id"]) != str(profile_id):
|
||||
raise ActivitySessionMetricsError(404, "Aktivität nicht gefunden")
|
||||
|
||||
header = dict(row)
|
||||
schema = resolve_activity_attribute_schema(
|
||||
cur, header.get("training_category"), header.get("training_type_id")
|
||||
)
|
||||
if use_form_training_context:
|
||||
cat = form_training_category
|
||||
if isinstance(cat, str):
|
||||
cat = cat.strip() or None
|
||||
tid = form_training_type_id
|
||||
else:
|
||||
cat = header.get("training_category")
|
||||
tid = header.get("training_type_id")
|
||||
if tid is not None:
|
||||
try:
|
||||
tid = int(tid)
|
||||
except (TypeError, ValueError):
|
||||
tid = None
|
||||
schema = resolve_activity_attribute_schema(cur, cat, tid)
|
||||
metrics = fetch_activity_session_metrics(cur, activity_log_id)
|
||||
merged_metrics = merge_column_backed_and_eav_metrics(header, schema, metrics)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -371,6 +371,25 @@ def get_activity_mappable_fields(session: dict = Depends(require_auth)):
|
|||
return get_mappable_activity_field_catalog(cur, pid)
|
||||
|
||||
|
||||
@router.get("/attribute-schema")
|
||||
def get_activity_attribute_schema(
|
||||
training_category: Optional[str] = Query(None),
|
||||
training_type_id: Optional[int] = Query(None),
|
||||
session: dict = Depends(require_auth),
|
||||
):
|
||||
"""
|
||||
Aufgelöstes Attributprofil (tcp/ttp) für Erfassung ohne bestehende Session —
|
||||
gleiche Logik wie resolve_activity_attribute_schema.
|
||||
"""
|
||||
from data_layer.activity_session_metrics import resolve_activity_attribute_schema
|
||||
|
||||
cat = (training_category or "").strip() or None
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
schema = resolve_activity_attribute_schema(cur, cat, training_type_id)
|
||||
return {"schema": schema}
|
||||
|
||||
|
||||
@router.put("/{eid}")
|
||||
def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
||||
"""Update existing activity entry."""
|
||||
|
|
@ -422,6 +441,12 @@ def replace_activity_metrics(
|
|||
@router.get("/{eid}")
|
||||
def get_activity_session(
|
||||
eid: str,
|
||||
use_form_schema: bool = Query(
|
||||
False,
|
||||
description="True: Schema aus Query training_category / training_type_id (Formular), nicht nur DB-Zeile",
|
||||
),
|
||||
training_category: Optional[str] = Query(None),
|
||||
training_type_id: Optional[int] = Query(None),
|
||||
session: dict = Depends(require_auth),
|
||||
):
|
||||
"""Session-Kopf + aufgelöstes Schema + EAV-Metriken (Layer 1)."""
|
||||
|
|
@ -435,7 +460,14 @@ def get_activity_session(
|
|||
try:
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
unit = get_activity_session_logical_unit(cur, pid, eid)
|
||||
unit = get_activity_session_logical_unit(
|
||||
cur,
|
||||
pid,
|
||||
eid,
|
||||
use_form_training_context=use_form_schema,
|
||||
form_training_category=training_category,
|
||||
form_training_type_id=training_type_id,
|
||||
)
|
||||
except ActivitySessionMetricsError as err:
|
||||
raise HTTPException(err.status_code, err.detail) from err
|
||||
unit["header"] = serialize_dates(unit["header"])
|
||||
|
|
|
|||
|
|
@ -554,6 +554,12 @@ export default function ActivityPage() {
|
|||
const [categories, setCategories] = useState({}) // v9d: Training categories
|
||||
const [sessionDetail, setSessionDetail] = useState(null)
|
||||
const [metricDraft, setMetricDraft] = useState({})
|
||||
/** Beim Wechsel Kategorie/Typ: Nutzerwerte für weiterhin vorhandene Schema-Keys nicht mit Server überschreiben */
|
||||
const editSchemaKeysPrevRef = useRef(new Set())
|
||||
const prevEditingIdRef = useRef(null)
|
||||
const [manualSchema, setManualSchema] = useState(null)
|
||||
const [manualMetricDraft, setManualMetricDraft] = useState({})
|
||||
const manualSchemaKeysPrevRef = useRef(new Set())
|
||||
const [sessionLoadError, setSessionLoadError] = useState(null)
|
||||
const [savingEdit, setSavingEdit] = useState(false)
|
||||
const [listLoadingMore, setListLoadingMore] = useState(false)
|
||||
|
|
@ -639,18 +645,31 @@ export default function ActivityPage() {
|
|||
api.getTrainingCategories().then(setCategories).catch(err => console.error('Failed to load categories:', err))
|
||||
}, [fetchMonthsChain])
|
||||
|
||||
useEffect(() => {
|
||||
editSchemaKeysPrevRef.current = new Set()
|
||||
}, [editing?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editing?.id) {
|
||||
setSessionDetail(null)
|
||||
setMetricDraft({})
|
||||
setSessionLoadError(null)
|
||||
prevEditingIdRef.current = null
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
setSessionLoadError(null)
|
||||
if (prevEditingIdRef.current !== editing.id) {
|
||||
setSessionDetail(null)
|
||||
prevEditingIdRef.current = editing.id
|
||||
}
|
||||
;(async () => {
|
||||
try {
|
||||
const d = await api.getActivitySession(editing.id)
|
||||
const d = await api.getActivitySession(editing.id, {
|
||||
useFormSchema: true,
|
||||
training_category: editing.training_category,
|
||||
training_type_id: editing.training_type_id,
|
||||
})
|
||||
if (!cancelled) setSessionDetail(d)
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
|
|
@ -660,25 +679,89 @@ export default function ActivityPage() {
|
|||
}
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [editing?.id])
|
||||
}, [editing?.id, editing?.training_category, editing?.training_type_id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionDetail) {
|
||||
setMetricDraft({})
|
||||
return
|
||||
}
|
||||
const m = {}
|
||||
for (const row of sessionDetail.metrics || []) {
|
||||
m[row.key] = row.value
|
||||
}
|
||||
for (const s of sessionDetail.schema || []) {
|
||||
if (!(s.key in m)) {
|
||||
m[s.key] = s.data_type === 'boolean' ? false : ''
|
||||
const newKeys = new Set((sessionDetail.schema || []).map((s) => s.key))
|
||||
const oldKeys = editSchemaKeysPrevRef.current
|
||||
|
||||
setMetricDraft((prev) => {
|
||||
const next = { ...prev }
|
||||
for (const row of sessionDetail.metrics || []) {
|
||||
const k = row.key
|
||||
if (oldKeys.size > 0 && oldKeys.has(k) && newKeys.has(k) && k in prev) {
|
||||
continue
|
||||
}
|
||||
next[k] = row.value
|
||||
}
|
||||
}
|
||||
setMetricDraft(m)
|
||||
for (const s of sessionDetail.schema || []) {
|
||||
if (!(s.key in next)) {
|
||||
next[s.key] = s.data_type === 'boolean' ? false : ''
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
editSchemaKeysPrevRef.current = newKeys
|
||||
}, [sessionDetail])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'add') {
|
||||
setManualSchema(null)
|
||||
setManualMetricDraft({})
|
||||
manualSchemaKeysPrevRef.current = new Set()
|
||||
return
|
||||
}
|
||||
const tid = form.training_type_id
|
||||
const cat = form.training_category
|
||||
if (tid == null && (cat == null || cat === '')) {
|
||||
setManualSchema(null)
|
||||
setManualMetricDraft({})
|
||||
manualSchemaKeysPrevRef.current = new Set()
|
||||
return
|
||||
}
|
||||
let cancelled = false
|
||||
;(async () => {
|
||||
try {
|
||||
const r = await api.getActivityAttributeSchema({
|
||||
training_category: cat || undefined,
|
||||
training_type_id: tid ?? undefined,
|
||||
})
|
||||
if (!cancelled) setManualSchema(Array.isArray(r.schema) ? r.schema : [])
|
||||
} catch (err) {
|
||||
console.error('attribute-schema:', err)
|
||||
if (!cancelled) setManualSchema([])
|
||||
}
|
||||
})()
|
||||
return () => { cancelled = true }
|
||||
}, [tab, form.training_category, form.training_type_id])
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'add' || !manualSchema) {
|
||||
return
|
||||
}
|
||||
const newKeys = new Set(manualSchema.map((s) => s.key))
|
||||
const oldKeys = manualSchemaKeysPrevRef.current
|
||||
|
||||
setManualMetricDraft((prev) => {
|
||||
const next = { ...prev }
|
||||
for (const s of manualSchema) {
|
||||
const k = s.key
|
||||
if (oldKeys.size > 0 && oldKeys.has(k) && newKeys.has(k) && k in prev) {
|
||||
continue
|
||||
}
|
||||
if (!(k in next)) {
|
||||
next[k] = s.data_type === 'boolean' ? false : ''
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
manualSchemaKeysPrevRef.current = newKeys
|
||||
}, [tab, manualSchema])
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
|
|
@ -698,7 +781,20 @@ export default function ActivityPage() {
|
|||
if(payload.hr_max) payload.hr_max = parseFloat(payload.hr_max)
|
||||
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
|
||||
payload.source = 'manual'
|
||||
await api.createActivity(payload)
|
||||
const created = await api.createActivity(payload)
|
||||
if (manualSchema && manualSchema.length > 0 && created?.id) {
|
||||
try {
|
||||
const metrics = buildMetricsPayload(manualSchema, manualMetricDraft)
|
||||
await api.putActivityMetrics(created.id, { metrics })
|
||||
} catch (metErr) {
|
||||
console.error(metErr)
|
||||
setError(
|
||||
metErr.message ||
|
||||
'Eintrag gespeichert, aber Zusatzfelder konnten nicht gespeichert werden.',
|
||||
)
|
||||
setTimeout(() => setError(null), 8000)
|
||||
}
|
||||
}
|
||||
setSaved(true)
|
||||
await load()
|
||||
await loadUsage() // Reload usage after save
|
||||
|
|
@ -864,9 +960,23 @@ export default function ActivityPage() {
|
|||
<span>Training eintragen</span>
|
||||
{activityUsage && <UsageBadge {...activityUsage} />}
|
||||
</div>
|
||||
<EntryForm form={form} setForm={setForm}
|
||||
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}
|
||||
saving={saving} error={error} usage={activityUsage}/>
|
||||
<EntryForm
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
onSave={handleSave}
|
||||
saveLabel={saved ? '✓ Gespeichert!' : 'Speichern'}
|
||||
saving={saving}
|
||||
error={error}
|
||||
usage={activityUsage}
|
||||
formExtras={
|
||||
<SessionMetricsFields
|
||||
schema={manualSchema}
|
||||
metrics={[]}
|
||||
values={manualMetricDraft}
|
||||
setValues={setManualMetricDraft}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -352,7 +352,39 @@ export const api = {
|
|||
adminDeleteTrainingTypeParameter: (id) =>
|
||||
req(`/admin/training-type-parameters/${id}`, { method: 'DELETE' }),
|
||||
|
||||
getActivitySession: (id) => req(`/activity/${encodeURIComponent(id)}`),
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {{ useFormSchema?: boolean, training_category?: string | null, training_type_id?: number | null }} [opts]
|
||||
*/
|
||||
getActivitySession: (id, opts = {}) => {
|
||||
const q = new URLSearchParams()
|
||||
if (opts.useFormSchema) {
|
||||
q.set('use_form_schema', 'true')
|
||||
if (opts.training_category != null && opts.training_category !== '') {
|
||||
q.set('training_category', String(opts.training_category))
|
||||
}
|
||||
if (opts.training_type_id != null && opts.training_type_id !== '') {
|
||||
q.set('training_type_id', String(opts.training_type_id))
|
||||
}
|
||||
}
|
||||
const qs = q.toString()
|
||||
return req(`/activity/${encodeURIComponent(id)}${qs ? `?${qs}` : ''}`)
|
||||
},
|
||||
/**
|
||||
* Attributprofil ohne Session (manuelle Erfassung / Vorschau).
|
||||
* @param {{ training_category?: string | null, training_type_id?: number | null }} [params]
|
||||
*/
|
||||
getActivityAttributeSchema: (params = {}) => {
|
||||
const q = new URLSearchParams()
|
||||
if (params.training_category != null && params.training_category !== '') {
|
||||
q.set('training_category', String(params.training_category))
|
||||
}
|
||||
if (params.training_type_id != null && params.training_type_id !== '') {
|
||||
q.set('training_type_id', String(params.training_type_id))
|
||||
}
|
||||
const qs = q.toString()
|
||||
return req(`/activity/attribute-schema${qs ? `?${qs}` : ''}`)
|
||||
},
|
||||
putActivityMetrics: (id, body) =>
|
||||
req(`/activity/${encodeURIComponent(id)}/metrics`, jput(body)),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user