mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 08:17:59 -05:00
Enhance health weight chart with more metrics
Expanded the HealthWeightLineChartComponent to display additional metrics (BMI, body fat, body water, bone mass, muscle mass) alongside weight, each with its own dataset and axis. Improved tooltip formatting and axis labeling. Also refactored and cleaned up UsersListComponent markup for readability, and improved type annotations in UsersListComponent. Backend health_utils.py now includes stricter validation for Garmin data fields to prevent errors from invalid or missing values.
This commit is contained in:
@@ -137,6 +137,9 @@ def fetch_and_process_ds_by_dates(
|
||||
count_processed = 0
|
||||
# Process steps
|
||||
for ds in garmin_ds:
|
||||
if ds["totalSteps"] is None:
|
||||
continue
|
||||
|
||||
health_steps = health_steps_schema.HealthSteps(
|
||||
user_id=user_id,
|
||||
date=ds["calendarDate"],
|
||||
@@ -255,13 +258,20 @@ def fetch_and_process_sleep_by_dates(
|
||||
|
||||
# Process sleep stages from sleepLevels array
|
||||
sleep_stages = []
|
||||
if "sleepLevels" in garmin_sleep:
|
||||
if "sleepLevels" in garmin_sleep and garmin_sleep["sleepLevels"]:
|
||||
for level in garmin_sleep["sleepLevels"]:
|
||||
activity_level = level.get("activityLevel")
|
||||
if activity_level is not None:
|
||||
if (
|
||||
activity_level is not None
|
||||
and activity_level in health_sleep_schema.SleepStageType
|
||||
):
|
||||
# Map Garmin activity levels to sleep stage types
|
||||
# 0=deep, 1=light, 2=REM, 3=awake
|
||||
stage_type = health_sleep_schema.SleepStageType(activity_level)
|
||||
stage_type = (
|
||||
health_sleep_schema.SleepStageType(activity_level)
|
||||
if activity_level
|
||||
else None
|
||||
)
|
||||
|
||||
start_gmt_str = level.get("startGMT")
|
||||
end_gmt_str = level.get("endGMT")
|
||||
@@ -352,6 +362,7 @@ def fetch_and_process_sleep_by_dates(
|
||||
hrv_status=(
|
||||
health_sleep_schema.HRVStatus(garmin_sleep.get("hrvStatus"))
|
||||
if garmin_sleep.get("hrvStatus")
|
||||
and garmin_sleep.get("hrvStatus") in health_sleep_schema.HRVStatus
|
||||
else None
|
||||
),
|
||||
resting_heart_rate=garmin_sleep.get("restingHeartRate"),
|
||||
@@ -359,11 +370,15 @@ def fetch_and_process_sleep_by_dates(
|
||||
awake_count_score=(
|
||||
health_sleep_schema.SleepScore(awake_count_score.get("qualifierKey"))
|
||||
if awake_count_score
|
||||
and awake_count_score.get("qualifierKey")
|
||||
in health_sleep_schema.SleepScore
|
||||
else None
|
||||
),
|
||||
rem_percentage_score=(
|
||||
health_sleep_schema.SleepScore(rem_percentage_score.get("qualifierKey"))
|
||||
if rem_percentage_score
|
||||
and rem_percentage_score.get("qualifierKey")
|
||||
in health_sleep_schema.SleepScore
|
||||
else None
|
||||
),
|
||||
deep_percentage_score=(
|
||||
@@ -371,6 +386,8 @@ def fetch_and_process_sleep_by_dates(
|
||||
deep_percentage_score.get("qualifierKey")
|
||||
)
|
||||
if deep_percentage_score
|
||||
and deep_percentage_score.get("qualifierKey")
|
||||
in health_sleep_schema.SleepScore
|
||||
else None
|
||||
),
|
||||
light_percentage_score=(
|
||||
@@ -378,12 +395,16 @@ def fetch_and_process_sleep_by_dates(
|
||||
light_percentage_score.get("qualifierKey")
|
||||
)
|
||||
if light_percentage_score
|
||||
and light_percentage_score.get("qualifierKey")
|
||||
in health_sleep_schema.SleepScore
|
||||
else None
|
||||
),
|
||||
avg_sleep_stress=sleep_dto.get("avgSleepStress"),
|
||||
sleep_stress_score=(
|
||||
health_sleep_schema.SleepScore(sleep_stress_score.get("qualifierKey"))
|
||||
if sleep_stress_score
|
||||
and sleep_stress_score.get("qualifierKey")
|
||||
in health_sleep_schema.SleepScore
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
12
backend/poetry.lock
generated
12
backend/poetry.lock
generated
@@ -1159,14 +1159,14 @@ test = ["pytest", "pytest-cov", "pytest-mpl", "pytest-subtests"]
|
||||
|
||||
[[package]]
|
||||
name = "garminconnect"
|
||||
version = "0.2.34"
|
||||
version = "0.2.35"
|
||||
description = "Python 3 API wrapper for Garmin Connect"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "garminconnect-0.2.34-py3-none-any.whl", hash = "sha256:347fdca2ddcba50c62583b5d64fd1696cf8956e246b4a860641520726eb00874"},
|
||||
{file = "garminconnect-0.2.34.tar.gz", hash = "sha256:cc58b28b4dd65142f325f061e31afc8ec6b3b1414ad8fbeb47c025608aa16f5f"},
|
||||
{file = "garminconnect-0.2.35-py3-none-any.whl", hash = "sha256:8ad92b21d0e769cd6d31c190659188e816c1f8cec8117c6c1aaffd8c48898555"},
|
||||
{file = "garminconnect-0.2.35.tar.gz", hash = "sha256:5623d7d4d0ee68b30db92aa2fd6ed070ecef9ccfbdea663bc0e61f2918b96985"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2757,14 +2757,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.4"
|
||||
version = "2.12.5"
|
||||
description = "Data validation using Python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e"},
|
||||
{file = "pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac"},
|
||||
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
|
||||
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
||||
84
frontend/app/package-lock.json
generated
84
frontend/app/package-lock.json
generated
@@ -1770,9 +1770,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.17.tgz",
|
||||
"integrity": "sha512-LCC++2h8pLUSPY+EsZmrrJ1EOUu+5iClpEiDhhdw3zRJpPbABML/N5lmRuBHjxtKm9VnRcsUzioyD0sekFMF0A==",
|
||||
"version": "1.0.19",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.19.tgz",
|
||||
"integrity": "sha512-QW5/SM2ARltEhoKcmRI1LoLf3/C7dHGswwCnfLcoMgqurBT4f8GvwXMgAbK/FwcxthmJRK5MGTtddj0yQn0J9g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2560,13 +2560,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.1.tgz",
|
||||
"integrity": "sha512-2V1A4yaN9ElAnQ6ih3HHEc+jZ+sHV6BlQHjCsnIVlOotL5NCUgJElIxgUFiJs6zV4puoAq3hHuQIfWNp+J+8yQ==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.2.tgz",
|
||||
"integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.1",
|
||||
"@intlify/shared": "11.2.1"
|
||||
"@intlify/message-compiler": "11.2.2",
|
||||
"@intlify/shared": "11.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -2576,12 +2576,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.1.tgz",
|
||||
"integrity": "sha512-J2454D3Agg3Kvgaj14gxTleJU8/H06Sisz7C2BwiHF0/i5Soyfb5ySpwn8GCL6yscDbOGj6xM+lUe6gO6BFQyg==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.2.tgz",
|
||||
"integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.1",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2592,9 +2592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.1.tgz",
|
||||
"integrity": "sha512-O67LZM4dbfr70WCsZLW+g+pIXdgQ66laLVd/FicW7iYgP/RuH0X1FDGSh+Hr9Gou/8TeldUE6KmTGdLwX2ufIA==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.2.tgz",
|
||||
"integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -4919,9 +4919,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.260",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz",
|
||||
"integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==",
|
||||
"version": "1.5.262",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz",
|
||||
"integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -5261,9 +5261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.0.tgz",
|
||||
"integrity": "sha512-TsoFluWxOpsJlE/l2jJygLQLWBPJ3Qdkesv7tBIunICbTcG0dS1/NBw/Ol4tJw5kHWlAVds4lUmC29/vlPUcEQ==",
|
||||
"version": "10.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.6.2.tgz",
|
||||
"integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7563,9 +7563,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7587,9 +7587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.1.tgz",
|
||||
"integrity": "sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -8572,9 +8572,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
||||
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
@@ -9201,17 +9201,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.1.0.tgz",
|
||||
"integrity": "sha512-VsSpdubPzXhHWVINcSx6uHRMpOHVHQcHsef1QgkOlEoaIDAlssFEW88LBq1a59BuokAhsh2kUDJbaX1bZv4Bjw==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz",
|
||||
"integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.6",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"tinyglobby": "^0.2.10",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
@@ -9222,8 +9222,8 @@
|
||||
"peerDependencies": {
|
||||
"@vite-pwa/assets-generator": "^1.0.0",
|
||||
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0"
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vite-pwa/assets-generator": {
|
||||
@@ -9426,13 +9426,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.1.tgz",
|
||||
"integrity": "sha512-cc3Wx4eJZac9WMS8mxhfYiCipm9PBQ2Dz15piWYm7DwNcCehaKRgpolEdiqrjjT27T3Wijz3xJ7NeIc8ofIWAA==",
|
||||
"version": "11.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.2.tgz",
|
||||
"integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.1",
|
||||
"@intlify/shared": "11.2.1",
|
||||
"@intlify/core-base": "11.2.2",
|
||||
"@intlify/shared": "11.2.2",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -86,33 +86,53 @@ const chartData = computed(() => {
|
||||
return new Date(a.date) - new Date(b.date)
|
||||
})
|
||||
|
||||
const data = []
|
||||
const weightData = []
|
||||
const bmiData = []
|
||||
const bodyFatData = []
|
||||
const bodyWaterData = []
|
||||
const boneMassData = []
|
||||
const muscleMassData = []
|
||||
const labels = []
|
||||
|
||||
|
||||
for (const HealthWeight of sortedWeight) {
|
||||
if (Number(authStore?.user?.units) === 1) {
|
||||
data.push(HealthWeight.weight)
|
||||
weightData.push(HealthWeight.weight)
|
||||
} else {
|
||||
data.push(kgToLbs(HealthWeight.weight))
|
||||
weightData.push(kgToLbs(HealthWeight.weight))
|
||||
}
|
||||
|
||||
// Add other metrics
|
||||
bmiData.push(HealthWeight.bmi || null)
|
||||
bodyFatData.push(HealthWeight.body_fat || null)
|
||||
bodyWaterData.push(HealthWeight.body_water || null)
|
||||
boneMassData.push(HealthWeight.bone_mass || null)
|
||||
muscleMassData.push(HealthWeight.muscle_mass || null)
|
||||
|
||||
const createdAt = new Date(HealthWeight.date)
|
||||
labels.push(
|
||||
`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`
|
||||
)
|
||||
}
|
||||
|
||||
let label = ''
|
||||
|
||||
let weightLabel = ''
|
||||
let unitsLabel = ''
|
||||
if (Number(authStore?.user?.units) === 1) {
|
||||
label = t('generalItems.labelWeightInKg')
|
||||
weightLabel = t('generalItems.labelWeightInKg')
|
||||
unitsLabel = t('generalItems.unitsKg')
|
||||
} else {
|
||||
label = t('generalItems.labelWeightInLbs')
|
||||
weightLabel = t('generalItems.labelWeightInLbs')
|
||||
unitsLabel = t('generalItems.unitsLbs')
|
||||
}
|
||||
const BMILabel = t('healthWeightAddEditModalComponent.addWeightBMILabel')
|
||||
const bodyFatLabel = t('healthWeightAddEditModalComponent.addWeightBodyFatLabel')
|
||||
const bodyWaterLabel = t('healthWeightAddEditModalComponent.addWeightBodyWaterLabel')
|
||||
const boneMassLabel = t('healthWeightAddEditModalComponent.addWeightBoneMassLabel')
|
||||
const muscleMassLabel = t('healthWeightAddEditModalComponent.addWeightMuscleMassLabel')
|
||||
|
||||
const datasets = [
|
||||
{
|
||||
label: label,
|
||||
data: data,
|
||||
label: weightLabel,
|
||||
data: weightData,
|
||||
backgroundColor: function (context) {
|
||||
const chart = context.chart
|
||||
const { ctx, chartArea } = chart
|
||||
@@ -121,10 +141,61 @@ const chartData = computed(() => {
|
||||
}
|
||||
return createGradient(ctx, chartArea)
|
||||
},
|
||||
borderColor: 'rgba(59, 130, 246, 0.8)', // Blue border
|
||||
borderColor: 'rgba(59, 130, 246, 0.8)', // Blue
|
||||
fill: true,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(59, 130, 246, 0.8)'
|
||||
pointHoverBackgroundColor: 'rgba(59, 130, 246, 0.8)',
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: BMILabel,
|
||||
data: bmiData,
|
||||
borderColor: 'rgba(34, 197, 94, 0.8)', // Green
|
||||
backgroundColor: 'rgba(34, 197, 94, 0.1)',
|
||||
fill: false,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(34, 197, 94, 0.8)',
|
||||
yAxisID: 'y1'
|
||||
},
|
||||
{
|
||||
label: bodyFatLabel + ' (%)',
|
||||
data: bodyFatData,
|
||||
borderColor: 'rgba(251, 146, 60, 0.8)', // Orange
|
||||
backgroundColor: 'rgba(251, 146, 60, 0.1)',
|
||||
fill: false,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(251, 146, 60, 0.8)',
|
||||
yAxisID: 'y1'
|
||||
},
|
||||
{
|
||||
label: bodyWaterLabel + ' (%)',
|
||||
data: bodyWaterData,
|
||||
borderColor: 'rgba(14, 165, 233, 0.8)', // Cyan
|
||||
backgroundColor: 'rgba(14, 165, 233, 0.1)',
|
||||
fill: false,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(14, 165, 233, 0.8)',
|
||||
yAxisID: 'y1'
|
||||
},
|
||||
{
|
||||
label: boneMassLabel + ' (' + unitsLabel + ')',
|
||||
data: boneMassData,
|
||||
borderColor: 'rgba(168, 85, 247, 0.8)', // Purple
|
||||
backgroundColor: 'rgba(168, 85, 247, 0.1)',
|
||||
fill: false,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(168, 85, 247, 0.8)',
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: muscleMassLabel + ' (' + unitsLabel + ')',
|
||||
data: muscleMassData,
|
||||
borderColor: 'rgba(236, 72, 153, 0.8)', // Pink
|
||||
backgroundColor: 'rgba(236, 72, 153, 0.1)',
|
||||
fill: false,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBackgroundColor: 'rgba(236, 72, 153, 0.8)',
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -193,11 +264,31 @@ onMounted(() => {
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
beginAtZero: false,
|
||||
grid: {
|
||||
lineWidth: 1,
|
||||
drawBorder: true,
|
||||
borderWidth: 1
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Weight / Mass'
|
||||
}
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
beginAtZero: false,
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Percentage (%)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
@@ -229,8 +320,11 @@ onMounted(() => {
|
||||
return `${label}: N/A`
|
||||
}
|
||||
|
||||
// Format weight with 1 decimal place
|
||||
return `${label}: ${value.toFixed(1)}`
|
||||
// Format values with appropriate decimal places
|
||||
if (label.includes('%')) {
|
||||
return `${label}: ${value.toFixed(1)}%`
|
||||
}
|
||||
return `${label}: ${value.toFixed(2)}`
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -272,6 +366,7 @@ onUnmounted(() => {
|
||||
<style scoped>
|
||||
.chart-canvas {
|
||||
max-height: 300px;
|
||||
width: 100%; /* Ensures the canvas stretches across the available width */
|
||||
width: 100%;
|
||||
/* Ensures the canvas stretches across the available width */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,172 +9,97 @@
|
||||
</div>
|
||||
<span v-if="user.access_type == 1">{{
|
||||
$t('usersListComponent.userListAccessTypeOption1')
|
||||
}}</span>
|
||||
}}</span>
|
||||
<span v-else>{{ $t('usersListComponent.userListAccessTypeOption2') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="badge bg-secondary-subtle border border-secondary-subtle text-secondary-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.id == authStore.user.id"
|
||||
>{{ $t('usersListComponent.userListUserIsMeBadge') }}</span
|
||||
>
|
||||
<span
|
||||
class="badge bg-warning-subtle border border-warning-subtle text-warning-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.access_type == 2"
|
||||
>{{ $t('usersListComponent.userListUserIsAdminBadge') }}</span
|
||||
>
|
||||
<span
|
||||
class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.active == false"
|
||||
>{{ $t('usersListComponent.userListUserIsInactiveBadge') }}</span
|
||||
>
|
||||
<span
|
||||
class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.email_verified == false"
|
||||
>{{ $t('usersListComponent.userListUserHasUnverifiedEmailBadge') }}</span
|
||||
>
|
||||
<span
|
||||
class="badge bg-info-subtle border border-info-subtle text-info-emphasis d-none d-sm-inline"
|
||||
v-if="user.id == authStore.user.id">{{ $t('usersListComponent.userListUserIsMeBadge') }}</span>
|
||||
<span class="badge bg-warning-subtle border border-warning-subtle text-warning-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.access_type == 2">{{ $t('usersListComponent.userListUserIsAdminBadge') }}</span>
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.active == false">{{ $t('usersListComponent.userListUserIsInactiveBadge') }}</span>
|
||||
<span class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis me-2 d-none d-sm-inline"
|
||||
v-if="user.email_verified == false">{{ $t('usersListComponent.userListUserHasUnverifiedEmailBadge') }}</span>
|
||||
<span class="badge bg-info-subtle border border-info-subtle text-info-emphasis d-none d-sm-inline"
|
||||
v-if="user.external_auth_count && user.external_auth_count > 0"
|
||||
:aria-label="$t('usersListComponent.userListUserHasExternalAuthBadge')"
|
||||
>{{ $t('usersListComponent.userListUserHasExternalAuthBadge') }}</span
|
||||
>
|
||||
:aria-label="$t('usersListComponent.userListUserHasExternalAuthBadge')">{{
|
||||
$t('usersListComponent.userListUserHasExternalAuthBadge') }}</span>
|
||||
|
||||
<!-- button toggle user details -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
data-bs-toggle="collapse"
|
||||
:href="`#collapseUserDetails${user.id}`"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
:aria-controls="`collapseUserDetails${user.id}`"
|
||||
>
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" data-bs-toggle="collapse"
|
||||
:href="`#collapseUserDetails${user.id}`" role="button" aria-expanded="false"
|
||||
:aria-controls="`collapseUserDetails${user.id}`">
|
||||
<font-awesome-icon :icon="['fas', 'caret-down']" v-if="!userDetails" />
|
||||
<font-awesome-icon :icon="['fas', 'caret-up']" v-else />
|
||||
</a>
|
||||
|
||||
<!-- approve sign-up button -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#approveSignUpModal${user.id}`"
|
||||
v-if="user.pending_admin_approval && user.email_verified"
|
||||
><font-awesome-icon :icon="['fas', 'fa-check']"
|
||||
/></a>
|
||||
v-if="user.pending_admin_approval && user.email_verified"><font-awesome-icon
|
||||
:icon="['fas', 'fa-check']" /></a>
|
||||
|
||||
<!-- approve sign up modal -->
|
||||
<ModalComponent
|
||||
:modalId="`approveSignUpModal${user.id}`"
|
||||
<ModalComponent :modalId="`approveSignUpModal${user.id}`"
|
||||
:title="t('usersListComponent.modalApproveSignUpTitle')"
|
||||
:body="`${t('usersListComponent.modalApproveSignUpBody')}<b>${user.username}</b>?`"
|
||||
:actionButtonType="`success`"
|
||||
:actionButtonText="t('usersListComponent.modalApproveSignUpTitle')"
|
||||
@submitAction="submitApproveSignUp"
|
||||
v-if="user.pending_admin_approval && user.email_verified"
|
||||
/>
|
||||
:actionButtonType="`success`" :actionButtonText="t('usersListComponent.modalApproveSignUpTitle')"
|
||||
@submitAction="submitApproveSignUp" v-if="user.pending_admin_approval && user.email_verified" />
|
||||
|
||||
<!-- reject sign-up button -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#rejectSignUpModal${user.id}`"
|
||||
v-if="user.pending_admin_approval && user.email_verified"
|
||||
><font-awesome-icon :icon="['fas', 'fa-xmark']"
|
||||
/></a>
|
||||
v-if="user.pending_admin_approval && user.email_verified"><font-awesome-icon
|
||||
:icon="['fas', 'fa-xmark']" /></a>
|
||||
|
||||
<!-- reject sign up modal -->
|
||||
<ModalComponent
|
||||
:modalId="`rejectSignUpModal${user.id}`"
|
||||
:title="t('usersListComponent.modalRejectSignUpTitle')"
|
||||
<ModalComponent :modalId="`rejectSignUpModal${user.id}`" :title="t('usersListComponent.modalRejectSignUpTitle')"
|
||||
:body="`${t('usersListComponent.modalRejectSignUpBody1')}<b>${user.username}</b>? ${t('usersListComponent.modalRejectSignUpBody2')}`"
|
||||
:actionButtonType="`danger`"
|
||||
:actionButtonText="t('usersListComponent.modalRejectSignUpTitle')"
|
||||
@submitAction="submitDeleteUser"
|
||||
v-if="user.pending_admin_approval && user.email_verified"
|
||||
/>
|
||||
:actionButtonType="`danger`" :actionButtonText="t('usersListComponent.modalRejectSignUpTitle')"
|
||||
@submitAction="submitDeleteUser" v-if="user.pending_admin_approval && user.email_verified" />
|
||||
|
||||
<!-- change user password button -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#editUserPasswordModal${user.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-key']"
|
||||
/></a>
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editUserPasswordModal${user.id}`"><font-awesome-icon :icon="['fas', 'fa-key']" /></a>
|
||||
|
||||
<!-- change user password Modal -->
|
||||
<UsersChangeUserPasswordModalComponent :user="user" />
|
||||
|
||||
<!-- edit user button -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#editUserModal${user.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-pen-to-square']"
|
||||
/></a>
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editUserModal${user.id}`"><font-awesome-icon :icon="['fas', 'fa-pen-to-square']" /></a>
|
||||
|
||||
<!-- edit user modal -->
|
||||
<UsersAddEditUserModalComponent :action="'edit'" :user="user" @editedUser="editUserList" />
|
||||
|
||||
<!-- delete user button -->
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#deleteUserModal${user.id}`"
|
||||
v-if="authStore.user.id != user.id"
|
||||
><font-awesome-icon :icon="['fas', 'fa-trash-can']"
|
||||
/></a>
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#deleteUserModal${user.id}`" v-if="authStore.user.id != user.id"><font-awesome-icon
|
||||
:icon="['fas', 'fa-trash-can']" /></a>
|
||||
|
||||
<!-- delete user modal -->
|
||||
<ModalComponent
|
||||
:modalId="`deleteUserModal${user.id}`"
|
||||
:title="t('usersListComponent.modalDeleteUserTitle')"
|
||||
:body="`${t('usersListComponent.modalDeleteUserBody')}<b>${user.username}</b>?`"
|
||||
:actionButtonType="`danger`"
|
||||
:actionButtonText="t('usersListComponent.modalDeleteUserTitle')"
|
||||
@submitAction="submitDeleteUser"
|
||||
/>
|
||||
<ModalComponent :modalId="`deleteUserModal${user.id}`" :title="t('usersListComponent.modalDeleteUserTitle')"
|
||||
:body="`${t('usersListComponent.modalDeleteUserBody')}<b>${user.username}</b>?`" :actionButtonType="`danger`"
|
||||
:actionButtonText="t('usersListComponent.modalDeleteUserTitle')" @submitAction="submitDeleteUser" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" :id="`collapseUserDetails${user.id}`">
|
||||
<!-- Bootstrap Tabs Navigation -->
|
||||
<ul class="nav nav-tabs mt-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link active link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
:id="`sessions-tab-${user.id}`"
|
||||
data-bs-toggle="tab"
|
||||
:data-bs-target="`#sessions-${user.id}`"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-controls="`sessions-${user.id}`"
|
||||
aria-selected="true"
|
||||
>
|
||||
<button class="nav-link active link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
:id="`sessions-tab-${user.id}`" data-bs-toggle="tab" :data-bs-target="`#sessions-${user.id}`" type="button"
|
||||
role="tab" :aria-controls="`sessions-${user.id}`" aria-selected="true">
|
||||
{{ $t('usersListComponent.tabSessions') }}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
:id="`idps-tab-${user.id}`"
|
||||
data-bs-toggle="tab"
|
||||
:data-bs-target="`#idps-${user.id}`"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-controls="`idps-${user.id}`"
|
||||
aria-selected="false"
|
||||
@click="loadUserIdpsIfNeeded"
|
||||
>
|
||||
<button class="nav-link link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
:id="`idps-tab-${user.id}`" data-bs-toggle="tab" :data-bs-target="`#idps-${user.id}`" type="button"
|
||||
role="tab" :aria-controls="`idps-${user.id}`" aria-selected="false" @click="loadUserIdpsIfNeeded">
|
||||
{{ $t('usersListComponent.tabIdentityProviders') }}
|
||||
</button>
|
||||
</li>
|
||||
@@ -183,44 +108,26 @@
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<!-- Sessions Tab -->
|
||||
<div
|
||||
class="tab-pane fade show active"
|
||||
:id="`sessions-${user.id}`"
|
||||
role="tabpanel"
|
||||
:aria-labelledby="`sessions-tab-${user.id}`"
|
||||
>
|
||||
<div class="tab-pane fade show active" :id="`sessions-${user.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sessions-tab-${user.id}`">
|
||||
<div v-if="isLoadingSessions">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div v-else-if="userSessions && userSessions.length > 0">
|
||||
<UserSessionsListComponent
|
||||
v-for="session in userSessions"
|
||||
:key="session.id"
|
||||
:session="session"
|
||||
@sessionDeleted="updateSessionListDeleted"
|
||||
/>
|
||||
<UserSessionsListComponent v-for="session in userSessions" :key="session.id" :session="session"
|
||||
@sessionDeleted="updateSessionListDeleted" />
|
||||
</div>
|
||||
<NoItemsFoundComponents :show-shadow="false" v-else />
|
||||
</div>
|
||||
|
||||
<!-- Identity Providers Tab -->
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
:id="`idps-${user.id}`"
|
||||
role="tabpanel"
|
||||
:aria-labelledby="`idps-tab-${user.id}`"
|
||||
>
|
||||
<div class="tab-pane fade" :id="`idps-${user.id}`" role="tabpanel" :aria-labelledby="`idps-tab-${user.id}`">
|
||||
<div v-if="isLoadingIdps">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div v-else-if="userIdps && userIdps.length > 0">
|
||||
<UserIdentityProviderListComponent
|
||||
v-for="idp in userIdps"
|
||||
:key="idp.id"
|
||||
:idp="idp"
|
||||
:userId="user.id"
|
||||
@idpDeleted="updateIdpListDeleted"
|
||||
/>
|
||||
<UserIdentityProviderListComponent v-for="idp in userIdps" :key="idp.id" :idp="idp" :userId="user.id"
|
||||
@idpDeleted="updateIdpListDeleted" />
|
||||
</div>
|
||||
<NoItemsFoundComponents :show-shadow="false" v-else />
|
||||
</div>
|
||||
@@ -230,7 +137,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, type Ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { users } from '@/services/usersService'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
@@ -268,8 +175,8 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
const userDetails = ref(false)
|
||||
const userSessions = ref([])
|
||||
const userIdps = ref([])
|
||||
const userSessions: Ref<any[]> = ref([])
|
||||
const userIdps: Ref<UserIdentityProviderEnriched[]> = ref([])
|
||||
const isLoadingSessions = ref(true)
|
||||
const isLoadingIdps = ref(false)
|
||||
const idpsLoaded = ref(false)
|
||||
|
||||
Reference in New Issue
Block a user