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)
|
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,))
|
cur.execute("SELECT * FROM activity_log WHERE id = %s", (activity_log_id,))
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row or str(row["profile_id"]) != str(profile_id):
|
if not row or str(row["profile_id"]) != str(profile_id):
|
||||||
raise ActivitySessionMetricsError(404, "Aktivität nicht gefunden")
|
raise ActivitySessionMetricsError(404, "Aktivität nicht gefunden")
|
||||||
|
|
||||||
header = dict(row)
|
header = dict(row)
|
||||||
schema = resolve_activity_attribute_schema(
|
if use_form_training_context:
|
||||||
cur, header.get("training_category"), header.get("training_type_id")
|
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)
|
metrics = fetch_activity_session_metrics(cur, activity_log_id)
|
||||||
merged_metrics = merge_column_backed_and_eav_metrics(header, schema, metrics)
|
merged_metrics = merge_column_backed_and_eav_metrics(header, schema, metrics)
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,25 @@ def get_activity_mappable_fields(session: dict = Depends(require_auth)):
|
||||||
return get_mappable_activity_field_catalog(cur, pid)
|
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}")
|
@router.put("/{eid}")
|
||||||
def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
||||||
"""Update existing activity entry."""
|
"""Update existing activity entry."""
|
||||||
|
|
@ -422,6 +441,12 @@ def replace_activity_metrics(
|
||||||
@router.get("/{eid}")
|
@router.get("/{eid}")
|
||||||
def get_activity_session(
|
def get_activity_session(
|
||||||
eid: str,
|
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: dict = Depends(require_auth),
|
||||||
):
|
):
|
||||||
"""Session-Kopf + aufgelöstes Schema + EAV-Metriken (Layer 1)."""
|
"""Session-Kopf + aufgelöstes Schema + EAV-Metriken (Layer 1)."""
|
||||||
|
|
@ -435,7 +460,14 @@ def get_activity_session(
|
||||||
try:
|
try:
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(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:
|
except ActivitySessionMetricsError as err:
|
||||||
raise HTTPException(err.status_code, err.detail) from err
|
raise HTTPException(err.status_code, err.detail) from err
|
||||||
unit["header"] = serialize_dates(unit["header"])
|
unit["header"] = serialize_dates(unit["header"])
|
||||||
|
|
|
||||||
|
|
@ -554,6 +554,12 @@ export default function ActivityPage() {
|
||||||
const [categories, setCategories] = useState({}) // v9d: Training categories
|
const [categories, setCategories] = useState({}) // v9d: Training categories
|
||||||
const [sessionDetail, setSessionDetail] = useState(null)
|
const [sessionDetail, setSessionDetail] = useState(null)
|
||||||
const [metricDraft, setMetricDraft] = useState({})
|
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 [sessionLoadError, setSessionLoadError] = useState(null)
|
||||||
const [savingEdit, setSavingEdit] = useState(false)
|
const [savingEdit, setSavingEdit] = useState(false)
|
||||||
const [listLoadingMore, setListLoadingMore] = 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))
|
api.getTrainingCategories().then(setCategories).catch(err => console.error('Failed to load categories:', err))
|
||||||
}, [fetchMonthsChain])
|
}, [fetchMonthsChain])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editSchemaKeysPrevRef.current = new Set()
|
||||||
|
}, [editing?.id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editing?.id) {
|
if (!editing?.id) {
|
||||||
setSessionDetail(null)
|
setSessionDetail(null)
|
||||||
setMetricDraft({})
|
setMetricDraft({})
|
||||||
setSessionLoadError(null)
|
setSessionLoadError(null)
|
||||||
|
prevEditingIdRef.current = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
setSessionLoadError(null)
|
setSessionLoadError(null)
|
||||||
|
if (prevEditingIdRef.current !== editing.id) {
|
||||||
|
setSessionDetail(null)
|
||||||
|
prevEditingIdRef.current = editing.id
|
||||||
|
}
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
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)
|
if (!cancelled) setSessionDetail(d)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|
@ -660,25 +679,89 @@ export default function ActivityPage() {
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [editing?.id])
|
}, [editing?.id, editing?.training_category, editing?.training_type_id])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sessionDetail) {
|
if (!sessionDetail) {
|
||||||
setMetricDraft({})
|
setMetricDraft({})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const m = {}
|
const newKeys = new Set((sessionDetail.schema || []).map((s) => s.key))
|
||||||
for (const row of sessionDetail.metrics || []) {
|
const oldKeys = editSchemaKeysPrevRef.current
|
||||||
m[row.key] = row.value
|
|
||||||
}
|
setMetricDraft((prev) => {
|
||||||
for (const s of sessionDetail.schema || []) {
|
const next = { ...prev }
|
||||||
if (!(s.key in m)) {
|
for (const row of sessionDetail.metrics || []) {
|
||||||
m[s.key] = s.data_type === 'boolean' ? false : ''
|
const k = row.key
|
||||||
|
if (oldKeys.size > 0 && oldKeys.has(k) && newKeys.has(k) && k in prev) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next[k] = row.value
|
||||||
}
|
}
|
||||||
}
|
for (const s of sessionDetail.schema || []) {
|
||||||
setMetricDraft(m)
|
if (!(s.key in next)) {
|
||||||
|
next[s.key] = s.data_type === 'boolean' ? false : ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
editSchemaKeysPrevRef.current = newKeys
|
||||||
}, [sessionDetail])
|
}, [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 () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
@ -698,7 +781,20 @@ export default function ActivityPage() {
|
||||||
if(payload.hr_max) payload.hr_max = parseFloat(payload.hr_max)
|
if(payload.hr_max) payload.hr_max = parseFloat(payload.hr_max)
|
||||||
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
|
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
|
||||||
payload.source = 'manual'
|
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)
|
setSaved(true)
|
||||||
await load()
|
await load()
|
||||||
await loadUsage() // Reload usage after save
|
await loadUsage() // Reload usage after save
|
||||||
|
|
@ -864,9 +960,23 @@ export default function ActivityPage() {
|
||||||
<span>Training eintragen</span>
|
<span>Training eintragen</span>
|
||||||
{activityUsage && <UsageBadge {...activityUsage} />}
|
{activityUsage && <UsageBadge {...activityUsage} />}
|
||||||
</div>
|
</div>
|
||||||
<EntryForm form={form} setForm={setForm}
|
<EntryForm
|
||||||
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}
|
form={form}
|
||||||
saving={saving} error={error} usage={activityUsage}/>
|
setForm={setForm}
|
||||||
|
onSave={handleSave}
|
||||||
|
saveLabel={saved ? '✓ Gespeichert!' : 'Speichern'}
|
||||||
|
saving={saving}
|
||||||
|
error={error}
|
||||||
|
usage={activityUsage}
|
||||||
|
formExtras={
|
||||||
|
<SessionMetricsFields
|
||||||
|
schema={manualSchema}
|
||||||
|
metrics={[]}
|
||||||
|
values={manualMetricDraft}
|
||||||
|
setValues={setManualMetricDraft}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -352,7 +352,39 @@ export const api = {
|
||||||
adminDeleteTrainingTypeParameter: (id) =>
|
adminDeleteTrainingTypeParameter: (id) =>
|
||||||
req(`/admin/training-type-parameters/${id}`, { method: 'DELETE' }),
|
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) =>
|
putActivityMetrics: (id, body) =>
|
||||||
req(`/activity/${encodeURIComponent(id)}/metrics`, jput(body)),
|
req(`/activity/${encodeURIComponent(id)}/metrics`, jput(body)),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user