From cc0f57758a8a330465e25343d004bcfdb4fe9f0d Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 16 Apr 2026 13:46:29 +0200 Subject: [PATCH] feat: Implement sorting and categorization for activity profile schema rows - Introduced a new sorting mechanism for activity profile schema rows based on defined categories and UI groups, enhancing the organization of displayed metrics. - Added constants for training parameter categories and their German labels to improve clarity in the UI. - Refactored the `SessionMetricsFields` component to utilize the new sorting logic, replacing the previous mapping approach for better maintainability and user experience. - Ensured that orphan metrics are sorted correctly for consistent display alongside the main metrics. --- frontend/src/pages/ActivityPage.jsx | 164 ++++++++++++++++++++++------ 1 file changed, 130 insertions(+), 34 deletions(-) diff --git a/frontend/src/pages/ActivityPage.jsx b/frontend/src/pages/ActivityPage.jsx index ea8284e..163857c 100644 --- a/frontend/src/pages/ActivityPage.jsx +++ b/frontend/src/pages/ActivityPage.jsx @@ -123,6 +123,50 @@ function activitySchemaHeadlineBinding(s) { return null } +/** training_parameters.category (siehe Migration 013); feste Reihenfolge der Wertegruppen */ +const TRAINING_PARAM_CATEGORY_ORDER = [ + 'physical', + 'physiological', + 'performance', + 'subjective', + 'environmental', +] + +const TRAINING_PARAM_CATEGORY_LABEL_DE = { + physical: 'Physisch / Bewegung', + physiological: 'Physiologie', + performance: 'Leistung', + subjective: 'Subjektiv und Wahrnehmung', + environmental: 'Umwelt', +} + +function compareActivityProfileSchemaRows(a, b) { + const ca = (a.param_category && String(a.param_category).trim().toLowerCase()) || '' + const cb = (b.param_category && String(b.param_category).trim().toLowerCase()) || '' + const ia = TRAINING_PARAM_CATEGORY_ORDER.indexOf(ca) + const ib = TRAINING_PARAM_CATEGORY_ORDER.indexOf(cb) + const ra = ia === -1 ? 1000 : ia + const rb = ib === -1 ? 1000 : ib + if (ra !== rb) return ra - rb + if (ca !== cb) return ca.localeCompare(cb, 'de') + + const ga = (a.ui_group && String(a.ui_group).trim()) || '' + const gb = (b.ui_group && String(b.ui_group).trim()) || '' + if (ga !== gb) { + if (!ga) return -1 + if (!gb) return 1 + return ga.localeCompare(gb, 'de') + } + const sa = Number(a.sort_order) || 0 + const sb = Number(b.sort_order) || 0 + if (sa !== sb) return sa - sb + return String(a.key).localeCompare(String(b.key), 'de') +} + +function sortActivityProfileSchemaRows(rows) { + return [...rows].sort(compareActivityProfileSchemaRows) +} + function empty() { return { date: dayjs().format('YYYY-MM-DD'), @@ -189,42 +233,94 @@ function SessionMetricsFields({ schema, values, setValues, metrics }) { if (schemaForDisplay.length === 0 && orphanMetrics.length === 0) return null const set = (k, v) => setValues((prev) => ({ ...prev, [k]: v })) + + const sortedForDisplay = sortActivityProfileSchemaRows(schemaForDisplay) + const profileFieldNodes = [] + let lastCategoryKey = null + let lastUiGroup = null + for (const s of sortedForDisplay) { + const catRaw = (s.param_category && String(s.param_category).trim().toLowerCase()) || '' + const catKey = catRaw || '_other' + if (catKey !== lastCategoryKey) { + lastCategoryKey = catKey + lastUiGroup = null + const catTitle = + (catRaw && TRAINING_PARAM_CATEGORY_LABEL_DE[catRaw]) || s.param_category || 'Sonstige' + profileFieldNodes.push( +
+ {catTitle} +
, + ) + } + const ug = (s.ui_group && String(s.ui_group).trim()) || '' + if (ug) { + if (ug !== lastUiGroup) { + lastUiGroup = ug + profileFieldNodes.push( +
+ {ug} +
, + ) + } + } else { + lastUiGroup = null + } + profileFieldNodes.push( +
+ + {s.data_type === 'boolean' ? ( + set(s.key, e.target.checked)} + /> + ) : s.data_type === 'integer' || s.data_type === 'float' ? ( + set(s.key, e.target.value)} + /> + ) : ( + set(s.key, e.target.value)} + /> + )} + +
, + ) + } + + const orphansSorted = [...orphanMetrics].sort((a, b) => + String(a.key).localeCompare(String(b.key), 'de'), + ) + return (
Weitere Kennwerte (Profil)
- {schemaForDisplay.map((s) => ( -
- - {s.data_type === 'boolean' ? ( - set(s.key, e.target.checked)} - /> - ) : s.data_type === 'integer' || s.data_type === 'float' ? ( - set(s.key, e.target.value)} - /> - ) : ( - set(s.key, e.target.value)} - /> - )} - -
- ))} + {profileFieldNodes} {orphanMetrics.length > 0 && (
@@ -232,7 +328,7 @@ function SessionMetricsFields({ schema, values, setValues, metrics }) { in activity_log) nicht ins Schema passen — nur Anzeige. Sichtbar nach erneutem Laden, wenn die Daten in der Datenbank stehen.
- {orphanMetrics.map((row) => { + {orphansSorted.map((row) => { const disp = values[row.key] === null || values[row.key] === undefined || values[row.key] === '' ? '—'