mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-07 23:13:57 -05:00
Enhance health dashboard and sleep details UI
Added new cards for sleep, resting heart rate, and skin temperature deviation to the health dashboard. Expanded HealthSleepListTabsComponent with detailed tabs for heart rate, SpO2, respiratory rate, and additional sleep metrics, including improved handling of missing data. Updated i18n files for new labels and units, and adjusted HealthSideBarComponent navigation order. Minor backend and script fixes for field naming and test data.
This commit is contained in:
@@ -228,11 +228,11 @@ if api:
|
||||
"from": 1763251200000,
|
||||
"until": 1763337599999,
|
||||
"weight": 68199.0,
|
||||
"bmi": None,
|
||||
"bodyFat": None,
|
||||
"bodyWater": None,
|
||||
"boneMass": None,
|
||||
"muscleMass": None,
|
||||
"bmi": 24.10,
|
||||
"bodyFat": 21.20,
|
||||
"bodyWater": 57.50,
|
||||
"boneMass": 4.27,
|
||||
"muscleMass": 30.40,
|
||||
"physiqueRating": None,
|
||||
"visceralFat": None,
|
||||
"metabolicAge": None,
|
||||
|
||||
@@ -805,7 +805,7 @@ def parse_frame_session(frame):
|
||||
get_value_from_frame(frame, "normalized_power"),
|
||||
get_value_from_frame(frame, "enhanced_avg_speed"),
|
||||
get_value_from_frame(frame, "enhanced_max_speed"),
|
||||
get_value_from_frame(frame, "workout_feeling"),
|
||||
get_value_from_frame(frame, "workout_feel"),
|
||||
get_value_from_frame(frame, "workout_rpe"),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,59 @@
|
||||
<template>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<!-- Today's sleep -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
<h4>{{ $t('healthDashboardZoneComponent.sleep') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 v-if="todaySleep">{{ formatDuration(todaySleep) }}</h1>
|
||||
<h1 v-else>{{ $t('generalItems.labelNoData') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
<span v-if="userHealthTargets && userHealthTargets['sleep']">
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="me-1"
|
||||
v-if="todaySleep < userHealthTargets.sleep" />
|
||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else />
|
||||
{{ formatDuration(userHealthTargets.sleep) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('healthDashboardZoneComponent.noSleepTarget') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- resting heart rate -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
<h4>{{ $t('healthDashboardZoneComponent.restingHeartRate') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 v-if="restingHeartRate">{{ restingHeartRate }} {{ $t('generalItems.unitsBpm') }}</h1>
|
||||
<h1 v-else>{{ $t('generalItems.labelNoData') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
<span v-if="hrvStatus">{{ $t(getHrvStatusI18nKey(hrvStatus)) }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- avg skin temperature deviation -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
<h4>{{ $t('healthDashboardZoneComponent.avgSkinTemperatureDeviation') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 v-if="avgSkinTempDeviation">{{ avgSkinTempDeviation }} {{ $t('generalItems.unitsCelsius') }}</h1>
|
||||
<h1 v-else>{{ $t('generalItems.labelNoData') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
<span>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- weight -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
@@ -16,8 +69,9 @@
|
||||
<h1 v-else>{{ $t('generalItems.labelNotApplicable') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="me-1" v-if="currentWeight > userHealthTargets.weight" />
|
||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else/>
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="me-1"
|
||||
v-if="currentWeight > userHealthTargets.weight" />
|
||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else />
|
||||
<span v-if="userHealthTargets && userHealthTargets['weight'] && Number(authStore?.user?.units) === 1">
|
||||
{{ userHealthTargets.weight }} {{ $t('generalItems.unitsKg') }}
|
||||
</span>
|
||||
@@ -28,6 +82,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- BMI -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
@@ -41,11 +96,12 @@
|
||||
<span v-if="currentBMI">{{ bmiDescription }}</span>
|
||||
<span v-else-if="!currentBMI && currentWeight">{{
|
||||
$t('healthDashboardZoneComponent.noHeightDefined')
|
||||
}}</span>
|
||||
}}</span>
|
||||
<span v-else>{{ $t('healthDashboardZoneComponent.noWeightData') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Today's steps -->
|
||||
<div class="col-lg-4 col-md-12">
|
||||
<div class="card mb-3 text-center shadow-sm">
|
||||
<div class="card-header">
|
||||
@@ -57,8 +113,9 @@
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
<span v-if="userHealthTargets && userHealthTargets['steps']">
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="me-1" v-if="todaySteps < userHealthTargets.steps" />
|
||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else/>
|
||||
<font-awesome-icon :icon="['fas', 'angle-down']" class="me-1"
|
||||
v-if="todaySteps < userHealthTargets.steps" />
|
||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else />
|
||||
{{ userHealthTargets.steps }} {{ $t('healthDashboardZoneComponent.stepsTargetLabel') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('healthDashboardZoneComponent.noStepsTarget') }}</span>
|
||||
@@ -75,6 +132,8 @@ import { useI18n } from 'vue-i18n'
|
||||
// Importing the stores
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { kgToLbs } from '@/utils/unitsUtils'
|
||||
import { formatDuration } from '@/utils/dateTimeUtils'
|
||||
import { getHrvStatusI18nKey } from '@/utils/healthUtils'
|
||||
|
||||
const props = defineProps({
|
||||
userHealthWeight: {
|
||||
@@ -85,6 +144,10 @@ const props = defineProps({
|
||||
type: [Object, null],
|
||||
required: true
|
||||
},
|
||||
userHealthSleep: {
|
||||
type: [Object, null],
|
||||
required: true
|
||||
},
|
||||
userHealthTargets: {
|
||||
type: [Object, null],
|
||||
required: true
|
||||
@@ -97,6 +160,10 @@ const currentWeight = ref(null)
|
||||
const currentBMI = ref(null)
|
||||
const bmiDescription = ref(null)
|
||||
const todaySteps = ref(null)
|
||||
const todaySleep = ref(null)
|
||||
const restingHeartRate = ref(null)
|
||||
const hrvStatus = ref(null)
|
||||
const avgSkinTempDeviation = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.userHealthWeight) {
|
||||
@@ -132,5 +199,16 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (props.userHealthSleep) {
|
||||
for (const data of props.userHealthSleep) {
|
||||
if (data.total_sleep_seconds) {
|
||||
todaySleep.value = data.total_sleep_seconds
|
||||
restingHeartRate.value = data.resting_heart_rate
|
||||
hrvStatus.value = data.hrv_status
|
||||
avgSkinTempDeviation.value = data.avg_skin_temp_deviation
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,54 +1,35 @@
|
||||
<template>
|
||||
<div class="col-lg-3 col-md-12">
|
||||
<ul
|
||||
class="nav nav-pills flex-column mb-auto bg-body-tertiary rounded p-3 shadow-sm"
|
||||
id="sidebarNav"
|
||||
>
|
||||
<ul class="nav nav-pills flex-column mb-auto bg-body-tertiary rounded p-3 shadow-sm" id="sidebarNav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'dashboard' }"
|
||||
@click.prevent="changeActive('dashboard')"
|
||||
>
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'dashboard' }"
|
||||
@click.prevent="changeActive('dashboard')">
|
||||
<font-awesome-icon :icon="['fas', 'dashboard']" />
|
||||
<span class="ms-1">{{ $t('healthSideBarComponent.dashboardSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<hr />
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'weight' }"
|
||||
@click.prevent="changeActive('weight')"
|
||||
>
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'sleep' }"
|
||||
@click.prevent="changeActive('sleep')">
|
||||
<font-awesome-icon :icon="['fas', 'bed']" />
|
||||
<span class="ms-1">{{ $t('healthSideBarComponent.sleepSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'weight' }"
|
||||
@click.prevent="changeActive('weight')">
|
||||
<font-awesome-icon :icon="['fas', 'weight']" />
|
||||
<span class="ms-2">{{ $t('healthSideBarComponent.weightSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'steps' }"
|
||||
@click.prevent="changeActive('steps')"
|
||||
>
|
||||
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'steps' }"
|
||||
@click.prevent="changeActive('steps')">
|
||||
<font-awesome-icon :icon="['fas', 'shoe-prints']" />
|
||||
<span class="ms-1">{{ $t('healthSideBarComponent.stepsSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'sleep' }"
|
||||
@click.prevent="changeActive('sleep')"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'bed']" />
|
||||
<span class="ms-1">{{ $t('healthSideBarComponent.sleepSection') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
<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-${userHealthSleep.id}`" data-bs-toggle="tab"
|
||||
:data-bs-target="`#sessions-${userHealthSleep.id}`" type="button" role="tab"
|
||||
:aria-controls="`sessions-${userHealthSleep.id}`" aria-selected="true">
|
||||
:id="`sleep-score-tab-${userHealthSleep.id}`" data-bs-toggle="tab"
|
||||
:data-bs-target="`#sleep-score-${userHealthSleep.id}`" type="button" role="tab"
|
||||
:aria-controls="`sleep-score-${userHealthSleep.id}`" aria-selected="true">
|
||||
{{ $t('healthSleepListTabsComponent.sleepScoreLabel') }}
|
||||
</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-${userHealthSleep.id}`" data-bs-toggle="tab" :data-bs-target="`#idps-${userHealthSleep.id}`"
|
||||
type="button" role="tab" :aria-controls="`idps-${userHealthSleep.id}`" aria-selected="false">
|
||||
:id="`sleep-details-tab-${userHealthSleep.id}`" data-bs-toggle="tab"
|
||||
:data-bs-target="`#sleep-details-${userHealthSleep.id}`" type="button" role="tab"
|
||||
:aria-controls="`sleep-details-${userHealthSleep.id}`" aria-selected="false">
|
||||
{{ $t('healthSleepListTabsComponent.sleepDetailsLabel') }}
|
||||
</button>
|
||||
</li>
|
||||
@@ -21,8 +22,8 @@
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<!-- Score tab -->
|
||||
<div class="tab-pane fade show active" :id="`sessions-${userHealthSleep.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sessions-tab-${userHealthSleep.id}`">
|
||||
<div class="tab-pane fade show active" :id="`sleep-score-${userHealthSleep.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sleep-score-tab-${userHealthSleep.id}`">
|
||||
<!-- Sleep summary -->
|
||||
<section class="pb-3 mb-3 border-bottom">
|
||||
<h6 class="fw-semibold mb-2">
|
||||
@@ -30,32 +31,42 @@
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<p v-if="userHealthSleep.sleep_score_overall" class="mb-1">
|
||||
<!-- sleep_score_overall -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.scoreLabel') }}:
|
||||
</span>
|
||||
{{ userHealthSleep.sleep_score_overall }}
|
||||
<span v-if="userHealthSleep.sleep_score_overall">{{ userHealthSleep.sleep_score_overall }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<p v-if="userHealthSleep.sleep_score_quality" class="mb-1">
|
||||
<!-- sleep_score_quality -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.qualityLabel') }}:
|
||||
</span>
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.sleep_score_quality)) }}
|
||||
<span v-if="userHealthSleep.sleep_score_quality">{{
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_quality)) }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<p v-if="userHealthSleep.sleep_score_duration" class="mb-1">
|
||||
<!-- sleep_score_duration-->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.durationLabel') }}:
|
||||
</span>
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.sleep_score_duration)) }}
|
||||
<span v-if="userHealthSleep.sleep_score_duration">{{
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_duration)) }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<p v-if="userHealthSleep.hrv_status" class="mb-0">
|
||||
<!-- hrv_status -->
|
||||
<p class="mb-0">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.HRVLabel') }}:
|
||||
</span>
|
||||
{{ $t(getHrvStatusI18nKey(userHealthSleep.hrv_status)) }}
|
||||
<span v-if="userHealthSleep.hrv_status">{{ $t(getHrvStatusI18nKey(userHealthSleep.hrv_status)) }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,40 +79,228 @@
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- deep_sleep_seconds -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.deepLabel') }}:
|
||||
</span>
|
||||
{{ formatDuration(userHealthSleep.deep_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.deep_percentage_score)) }}
|
||||
<span v-if="userHealthSleep.deep_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.deep_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.deep_percentage_score)) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- rem_sleep_seconds-->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.REMLabel') }}:
|
||||
</span>
|
||||
{{ formatDuration(userHealthSleep.rem_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.rem_percentage_score)) }}
|
||||
<span v-if="userHealthSleep.rem_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.rem_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.rem_percentage_score)) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- light_sleep_seconds -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.lightLabel') }}:
|
||||
</span>
|
||||
{{ formatDuration(userHealthSleep.light_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.light_percentage_score)) }}
|
||||
<span v-if="userHealthSleep.light_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.light_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.light_percentage_score)) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- awake_sleep_seconds -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.awakeLabel') }}:
|
||||
</span>
|
||||
{{ formatDuration(userHealthSleep.awake_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.awake_count_score)) }}
|
||||
<span v-if="userHealthSleep.awake_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.awake_sleep_seconds) }}
|
||||
-
|
||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.awake_count_score)) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Sleep Details Tab -->
|
||||
<div class="tab-pane fade" :id="`sleep-details-${userHealthSleep.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sleep-details-tab-${userHealthSleep.id}`">
|
||||
<!-- Heart Rate -->
|
||||
<section class="pb-3 mb-3 border-bottom">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- resting_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.restingHeartRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.resting_heart_rate">
|
||||
{{ Number(userHealthSleep.resting_heart_rate) }} {{ $t('generalItems.unitsBpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- avg_skin_temp_deviation -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgSkinTempDeviationLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_skin_temp_deviation">
|
||||
{{ parseFloat(userHealthSleep.avg_skin_temp_deviation) }} {{ $t('generalItems.unitsCelsius') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_sleep_stress -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgSleepStressLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_sleep_stress">
|
||||
{{ Number(userHealthSleep.avg_sleep_stress) }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Heart Rate -->
|
||||
<section class="pb-3 mb-3 border-bottom">
|
||||
<h6 class="fw-semibold mb-2">
|
||||
{{ $t('healthSleepListTabsComponent.heartRateTitle') }}
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgHeartRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_heart_rate">
|
||||
{{ Number(userHealthSleep.avg_heart_rate) }} {{ $t('generalItems.unitsBpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- max_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.maxHeartRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.max_heart_rate">
|
||||
{{ Number(userHealthSleep.max_heart_rate) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- min_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minHeartRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.min_heart_rate">
|
||||
{{ Number(userHealthSleep.min_heart_rate) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SpO2 -->
|
||||
<section class="pb-3 mb-3 border-bottom">
|
||||
<h6 class="fw-semibold mb-2">
|
||||
{{ $t('healthSleepListTabsComponent.spo2Title') }}
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_spo2 -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgSpo2Label') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_spo2">
|
||||
{{ Number(userHealthSleep.avg_spo2) }}%
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- highest_spo2 -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.maxSpo2Label') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.highest_spo2">
|
||||
{{ Number(userHealthSleep.highest_spo2) }}%
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- lowest_spo2 -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minSpo2Label') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.lowest_spo2">
|
||||
{{ Number(userHealthSleep.lowest_spo2) }}%
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Respiratory Rate -->
|
||||
<section class="pb-3 mb-3 border-bottom">
|
||||
<h6 class="fw-semibold mb-2">
|
||||
{{ $t('healthSleepListTabsComponent.respiratoryTitle') }}
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgRespiratoryRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_respiration">
|
||||
{{ Number(userHealthSleep.avg_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- highest_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.maxRespiratoryRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.highest_respiration">
|
||||
{{ Number(userHealthSleep.highest_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- lowest_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minRespiratoryRateLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.lowest_respiration">
|
||||
{{ Number(userHealthSleep.lowest_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +310,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { formatDuration, formatDateShort } from '@/utils/dateTimeUtils'
|
||||
import { formatDuration } from '@/utils/dateTimeUtils'
|
||||
import { getHrvStatusI18nKey, getScoreStatusI18nKey } from '@/utils/healthUtils'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"sleep": "Today's sleep",
|
||||
"restingHeartRate": "Resting heart rate",
|
||||
"avgSkinTemperatureDeviation": "Skin temp. dev.",
|
||||
"weight": "Weight",
|
||||
"noWeightData": "No weight data",
|
||||
"noWeightTarget": "No weight target",
|
||||
|
||||
@@ -17,5 +17,20 @@
|
||||
"poorLabel": "Poor",
|
||||
"excellentLabel": "Excellent",
|
||||
"goodLabel": "Good",
|
||||
"fairLabel": "Fair"
|
||||
"fairLabel": "Fair",
|
||||
"heartRateTitle": "Heart Rate",
|
||||
"restingHeartRateLabel": "Resting",
|
||||
"avgHeartRateLabel": "Avg.",
|
||||
"maxHeartRateLabel": "Max.",
|
||||
"minHeartRateLabel": "Min.",
|
||||
"respiratoryTitle": "Respiratory Rate",
|
||||
"avgRespiratoryRateLabel": "Avg.",
|
||||
"maxRespiratoryRateLabel": "Max.",
|
||||
"minRespiratoryRateLabel": "Min.",
|
||||
"spo2Title": "Blood Oxygen Saturation (SpO2)",
|
||||
"avgSpo2Label": "Avg.",
|
||||
"maxSpo2Label": "Max.",
|
||||
"minSpo2Label": "Min.",
|
||||
"avgSkinTempDeviationLabel": "Avg. Skin Temp. Deviation",
|
||||
"avgSleepStressLabel": "Avg. Sleep Stress"
|
||||
}
|
||||
@@ -89,5 +89,8 @@
|
||||
"genderFemale": "Female",
|
||||
"genderUnspecified": "Unspecified",
|
||||
"labelAverage": "Average",
|
||||
"labelMaximum": "Maximum"
|
||||
"labelMaximum": "Maximum",
|
||||
"unitsBrpm": "brpm",
|
||||
"unitsCelsius": "°C",
|
||||
"unitsFahrenheit": "°F"
|
||||
}
|
||||
@@ -2,65 +2,34 @@
|
||||
<h1>{{ $t('healthView.title') }}</h1>
|
||||
<div class="row row-gap-3">
|
||||
<!-- Include the HealthSideBarComponent -->
|
||||
<HealthSideBarComponent
|
||||
:activeSection="activeSection"
|
||||
@update-active-section="updateActiveSection"
|
||||
/>
|
||||
<HealthSideBarComponent :activeSection="activeSection" @update-active-section="updateActiveSection" />
|
||||
|
||||
<LoadingComponent v-if="isLoading" />
|
||||
|
||||
<!-- Include the HealthDashboardZone -->
|
||||
<HealthDashboardZone
|
||||
:userHealthWeight="userHealthWeight"
|
||||
:userHealthSteps="userHealthSteps"
|
||||
:userHealthTargets="userHealthTargets"
|
||||
v-if="activeSection === 'dashboard' && !isLoading"
|
||||
/>
|
||||
<HealthDashboardZone :userHealthWeight="userHealthWeight" :userHealthSteps="userHealthSteps"
|
||||
:userHealthSleep="userHealthSleep" :userHealthTargets="userHealthTargets"
|
||||
v-if="activeSection === 'dashboard' && !isLoading" />
|
||||
|
||||
<!-- Include the HealthWeightZone -->
|
||||
<HealthWeightZone
|
||||
:userHealthWeight="userHealthWeight"
|
||||
:userHealthWeightPagination="userHealthWeightPagination"
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:isLoading="isLoading"
|
||||
:totalPages="totalPagesWeight"
|
||||
:pageNumber="pageNumberWeight"
|
||||
@createdWeight="updateWeightListAdded"
|
||||
@deletedWeight="updateWeightListDeleted"
|
||||
@editedWeight="updateWeightListEdited"
|
||||
@pageNumberChanged="setPageNumberWeight"
|
||||
@setWeightTarget="setWeightTarget"
|
||||
v-if="activeSection === 'weight' && !isLoading"
|
||||
/>
|
||||
<HealthWeightZone :userHealthWeight="userHealthWeight" :userHealthWeightPagination="userHealthWeightPagination"
|
||||
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesWeight"
|
||||
:pageNumber="pageNumberWeight" @createdWeight="updateWeightListAdded" @deletedWeight="updateWeightListDeleted"
|
||||
@editedWeight="updateWeightListEdited" @pageNumberChanged="setPageNumberWeight" @setWeightTarget="setWeightTarget"
|
||||
v-if="activeSection === 'weight' && !isLoading" />
|
||||
|
||||
<!-- Include the HealthStepsZone -->
|
||||
<HealthStepsZone
|
||||
:userHealthSteps="userHealthSteps"
|
||||
:userHealthStepsPagination="userHealthStepsPagination"
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:isLoading="isLoading"
|
||||
:totalPages="totalPagesSteps"
|
||||
:pageNumber="pageNumberSteps"
|
||||
@createdSteps="updateStepsListAdded"
|
||||
@deletedSteps="updateStepsListDeleted"
|
||||
@editedSteps="updateStepsListEdited"
|
||||
@pageNumberChanged="setPageNumberSteps"
|
||||
@setStepsTarget="setStepsTarget"
|
||||
v-if="activeSection === 'steps' && !isLoading"
|
||||
/>
|
||||
<HealthStepsZone :userHealthSteps="userHealthSteps" :userHealthStepsPagination="userHealthStepsPagination"
|
||||
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSteps"
|
||||
:pageNumber="pageNumberSteps" @createdSteps="updateStepsListAdded" @deletedSteps="updateStepsListDeleted"
|
||||
@editedSteps="updateStepsListEdited" @pageNumberChanged="setPageNumberSteps" @setStepsTarget="setStepsTarget"
|
||||
v-if="activeSection === 'steps' && !isLoading" />
|
||||
|
||||
<!-- Include the HealthSleepZone -->
|
||||
<HealthSleepZone
|
||||
:userHealthSleep="userHealthSleep"
|
||||
:userHealthSleepPagination="userHealthSleepPagination"
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:isLoading="isLoading"
|
||||
:totalPages="totalPagesSleep"
|
||||
:pageNumber="pageNumberSleep"
|
||||
@pageNumberChanged="setPageNumberSleep"
|
||||
@setSleepTarget="setSleepTarget"
|
||||
v-if="activeSection === 'sleep' && !isLoading"
|
||||
/>
|
||||
<HealthSleepZone :userHealthSleep="userHealthSleep" :userHealthSleepPagination="userHealthSleepPagination"
|
||||
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSleep"
|
||||
:pageNumber="pageNumberSleep" @pageNumberChanged="setPageNumberSleep" @setSleepTarget="setSleepTarget"
|
||||
v-if="activeSection === 'sleep' && !isLoading" />
|
||||
</div>
|
||||
<!-- back button -->
|
||||
<BackButtonComponent />
|
||||
|
||||
Reference in New Issue
Block a user