mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-08 23:38:01 -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,
|
"from": 1763251200000,
|
||||||
"until": 1763337599999,
|
"until": 1763337599999,
|
||||||
"weight": 68199.0,
|
"weight": 68199.0,
|
||||||
"bmi": None,
|
"bmi": 24.10,
|
||||||
"bodyFat": None,
|
"bodyFat": 21.20,
|
||||||
"bodyWater": None,
|
"bodyWater": 57.50,
|
||||||
"boneMass": None,
|
"boneMass": 4.27,
|
||||||
"muscleMass": None,
|
"muscleMass": 30.40,
|
||||||
"physiqueRating": None,
|
"physiqueRating": None,
|
||||||
"visceralFat": None,
|
"visceralFat": None,
|
||||||
"metabolicAge": None,
|
"metabolicAge": None,
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ def parse_frame_session(frame):
|
|||||||
get_value_from_frame(frame, "normalized_power"),
|
get_value_from_frame(frame, "normalized_power"),
|
||||||
get_value_from_frame(frame, "enhanced_avg_speed"),
|
get_value_from_frame(frame, "enhanced_avg_speed"),
|
||||||
get_value_from_frame(frame, "enhanced_max_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"),
|
get_value_from_frame(frame, "workout_rpe"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,59 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="row">
|
<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="col-lg-4 col-md-12">
|
||||||
<div class="card mb-3 text-center shadow-sm">
|
<div class="card mb-3 text-center shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -16,8 +69,9 @@
|
|||||||
<h1 v-else>{{ $t('generalItems.labelNotApplicable') }}</h1>
|
<h1 v-else>{{ $t('generalItems.labelNotApplicable') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-body-secondary">
|
<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-down']" class="me-1"
|
||||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else/>
|
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">
|
<span v-if="userHealthTargets && userHealthTargets['weight'] && Number(authStore?.user?.units) === 1">
|
||||||
{{ userHealthTargets.weight }} {{ $t('generalItems.unitsKg') }}
|
{{ userHealthTargets.weight }} {{ $t('generalItems.unitsKg') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -28,6 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- BMI -->
|
||||||
<div class="col-lg-4 col-md-12">
|
<div class="col-lg-4 col-md-12">
|
||||||
<div class="card mb-3 text-center shadow-sm">
|
<div class="card mb-3 text-center shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -41,11 +96,12 @@
|
|||||||
<span v-if="currentBMI">{{ bmiDescription }}</span>
|
<span v-if="currentBMI">{{ bmiDescription }}</span>
|
||||||
<span v-else-if="!currentBMI && currentWeight">{{
|
<span v-else-if="!currentBMI && currentWeight">{{
|
||||||
$t('healthDashboardZoneComponent.noHeightDefined')
|
$t('healthDashboardZoneComponent.noHeightDefined')
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-else>{{ $t('healthDashboardZoneComponent.noWeightData') }}</span>
|
<span v-else>{{ $t('healthDashboardZoneComponent.noWeightData') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Today's steps -->
|
||||||
<div class="col-lg-4 col-md-12">
|
<div class="col-lg-4 col-md-12">
|
||||||
<div class="card mb-3 text-center shadow-sm">
|
<div class="card mb-3 text-center shadow-sm">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -57,8 +113,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-body-secondary">
|
<div class="card-footer text-body-secondary">
|
||||||
<span v-if="userHealthTargets && userHealthTargets['steps']">
|
<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-down']" class="me-1"
|
||||||
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else/>
|
v-if="todaySteps < userHealthTargets.steps" />
|
||||||
|
<font-awesome-icon :icon="['fas', 'angle-up']" class="me-1" v-else />
|
||||||
{{ userHealthTargets.steps }} {{ $t('healthDashboardZoneComponent.stepsTargetLabel') }}
|
{{ userHealthTargets.steps }} {{ $t('healthDashboardZoneComponent.stepsTargetLabel') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>{{ $t('healthDashboardZoneComponent.noStepsTarget') }}</span>
|
<span v-else>{{ $t('healthDashboardZoneComponent.noStepsTarget') }}</span>
|
||||||
@@ -75,6 +132,8 @@ import { useI18n } from 'vue-i18n'
|
|||||||
// Importing the stores
|
// Importing the stores
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { kgToLbs } from '@/utils/unitsUtils'
|
import { kgToLbs } from '@/utils/unitsUtils'
|
||||||
|
import { formatDuration } from '@/utils/dateTimeUtils'
|
||||||
|
import { getHrvStatusI18nKey } from '@/utils/healthUtils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
userHealthWeight: {
|
userHealthWeight: {
|
||||||
@@ -85,6 +144,10 @@ const props = defineProps({
|
|||||||
type: [Object, null],
|
type: [Object, null],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
userHealthSleep: {
|
||||||
|
type: [Object, null],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
userHealthTargets: {
|
userHealthTargets: {
|
||||||
type: [Object, null],
|
type: [Object, null],
|
||||||
required: true
|
required: true
|
||||||
@@ -97,6 +160,10 @@ const currentWeight = ref(null)
|
|||||||
const currentBMI = ref(null)
|
const currentBMI = ref(null)
|
||||||
const bmiDescription = ref(null)
|
const bmiDescription = ref(null)
|
||||||
const todaySteps = ref(null)
|
const todaySteps = ref(null)
|
||||||
|
const todaySleep = ref(null)
|
||||||
|
const restingHeartRate = ref(null)
|
||||||
|
const hrvStatus = ref(null)
|
||||||
|
const avgSkinTempDeviation = ref(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.userHealthWeight) {
|
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>
|
</script>
|
||||||
@@ -1,54 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="col-lg-3 col-md-12">
|
<div class="col-lg-3 col-md-12">
|
||||||
<ul
|
<ul class="nav nav-pills flex-column mb-auto bg-body-tertiary rounded p-3 shadow-sm" id="sidebarNav">
|
||||||
class="nav nav-pills flex-column mb-auto bg-body-tertiary rounded p-3 shadow-sm"
|
|
||||||
id="sidebarNav"
|
|
||||||
>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'dashboard' }"
|
||||||
href="#"
|
@click.prevent="changeActive('dashboard')">
|
||||||
class="nav-link link-body-emphasis"
|
|
||||||
:class="{ active: activeSection === 'dashboard' }"
|
|
||||||
@click.prevent="changeActive('dashboard')"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="['fas', 'dashboard']" />
|
<font-awesome-icon :icon="['fas', 'dashboard']" />
|
||||||
<span class="ms-1">{{ $t('healthSideBarComponent.dashboardSection') }}</span>
|
<span class="ms-1">{{ $t('healthSideBarComponent.dashboardSection') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<hr />
|
<hr />
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'sleep' }"
|
||||||
href="#"
|
@click.prevent="changeActive('sleep')">
|
||||||
class="nav-link link-body-emphasis"
|
<font-awesome-icon :icon="['fas', 'bed']" />
|
||||||
:class="{ active: activeSection === 'weight' }"
|
<span class="ms-1">{{ $t('healthSideBarComponent.sleepSection') }}</span>
|
||||||
@click.prevent="changeActive('weight')"
|
</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']" />
|
<font-awesome-icon :icon="['fas', 'weight']" />
|
||||||
<span class="ms-2">{{ $t('healthSideBarComponent.weightSection') }}</span>
|
<span class="ms-2">{{ $t('healthSideBarComponent.weightSection') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a
|
<a href="#" class="nav-link link-body-emphasis" :class="{ active: activeSection === 'steps' }"
|
||||||
href="#"
|
@click.prevent="changeActive('steps')">
|
||||||
class="nav-link link-body-emphasis"
|
|
||||||
:class="{ active: activeSection === 'steps' }"
|
|
||||||
@click.prevent="changeActive('steps')"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="['fas', 'shoe-prints']" />
|
<font-awesome-icon :icon="['fas', 'shoe-prints']" />
|
||||||
<span class="ms-1">{{ $t('healthSideBarComponent.stepsSection') }}</span>
|
<span class="ms-1">{{ $t('healthSideBarComponent.stepsSection') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,16 +3,17 @@
|
|||||||
<ul class="nav nav-tabs mt-3" role="tablist">
|
<ul class="nav nav-tabs mt-3" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
<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"
|
:id="`sleep-score-tab-${userHealthSleep.id}`" data-bs-toggle="tab"
|
||||||
:data-bs-target="`#sessions-${userHealthSleep.id}`" type="button" role="tab"
|
:data-bs-target="`#sleep-score-${userHealthSleep.id}`" type="button" role="tab"
|
||||||
:aria-controls="`sessions-${userHealthSleep.id}`" aria-selected="true">
|
:aria-controls="`sleep-score-${userHealthSleep.id}`" aria-selected="true">
|
||||||
{{ $t('healthSleepListTabsComponent.sleepScoreLabel') }}
|
{{ $t('healthSleepListTabsComponent.sleepScoreLabel') }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
<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}`"
|
:id="`sleep-details-tab-${userHealthSleep.id}`" data-bs-toggle="tab"
|
||||||
type="button" role="tab" :aria-controls="`idps-${userHealthSleep.id}`" aria-selected="false">
|
:data-bs-target="`#sleep-details-${userHealthSleep.id}`" type="button" role="tab"
|
||||||
|
:aria-controls="`sleep-details-${userHealthSleep.id}`" aria-selected="false">
|
||||||
{{ $t('healthSleepListTabsComponent.sleepDetailsLabel') }}
|
{{ $t('healthSleepListTabsComponent.sleepDetailsLabel') }}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
<!-- Tab Content -->
|
<!-- Tab Content -->
|
||||||
<div class="tab-content mt-3">
|
<div class="tab-content mt-3">
|
||||||
<!-- Score tab -->
|
<!-- Score tab -->
|
||||||
<div class="tab-pane fade show active" :id="`sessions-${userHealthSleep.id}`" role="tabpanel"
|
<div class="tab-pane fade show active" :id="`sleep-score-${userHealthSleep.id}`" role="tabpanel"
|
||||||
:aria-labelledby="`sessions-tab-${userHealthSleep.id}`">
|
:aria-labelledby="`sleep-score-tab-${userHealthSleep.id}`">
|
||||||
<!-- Sleep summary -->
|
<!-- Sleep summary -->
|
||||||
<section class="pb-3 mb-3 border-bottom">
|
<section class="pb-3 mb-3 border-bottom">
|
||||||
<h6 class="fw-semibold mb-2">
|
<h6 class="fw-semibold mb-2">
|
||||||
@@ -30,32 +31,42 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6">
|
<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">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.scoreLabel') }}:
|
{{ $t('healthSleepListTabsComponent.scoreLabel') }}:
|
||||||
</span>
|
</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>
|
||||||
<p v-if="userHealthSleep.sleep_score_quality" class="mb-1">
|
<!-- sleep_score_quality -->
|
||||||
|
<p class="mb-1">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.qualityLabel') }}:
|
{{ $t('healthSleepListTabsComponent.qualityLabel') }}:
|
||||||
</span>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<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">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.durationLabel') }}:
|
{{ $t('healthSleepListTabsComponent.durationLabel') }}:
|
||||||
</span>
|
</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>
|
||||||
<p v-if="userHealthSleep.hrv_status" class="mb-0">
|
<!-- hrv_status -->
|
||||||
|
<p class="mb-0">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.HRVLabel') }}:
|
{{ $t('healthSleepListTabsComponent.HRVLabel') }}:
|
||||||
</span>
|
</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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,40 +79,228 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
|
<!-- deep_sleep_seconds -->
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.deepLabel') }}:
|
{{ $t('healthSleepListTabsComponent.deepLabel') }}:
|
||||||
</span>
|
</span>
|
||||||
{{ formatDuration(userHealthSleep.deep_sleep_seconds) }}
|
<span v-if="userHealthSleep.deep_sleep_seconds">
|
||||||
-
|
{{ formatDuration(userHealthSleep.deep_sleep_seconds) }}
|
||||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.deep_percentage_score)) }}
|
-
|
||||||
|
{{ $t(getScoreStatusI18nKey(userHealthSleep.deep_percentage_score)) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<!-- rem_sleep_seconds-->
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.REMLabel') }}:
|
{{ $t('healthSleepListTabsComponent.REMLabel') }}:
|
||||||
</span>
|
</span>
|
||||||
{{ formatDuration(userHealthSleep.rem_sleep_seconds) }}
|
<span v-if="userHealthSleep.rem_sleep_seconds">
|
||||||
-
|
{{ formatDuration(userHealthSleep.rem_sleep_seconds) }}
|
||||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.rem_percentage_score)) }}
|
-
|
||||||
|
{{ $t(getScoreStatusI18nKey(userHealthSleep.rem_percentage_score)) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
|
<!-- light_sleep_seconds -->
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.lightLabel') }}:
|
{{ $t('healthSleepListTabsComponent.lightLabel') }}:
|
||||||
</span>
|
</span>
|
||||||
{{ formatDuration(userHealthSleep.light_sleep_seconds) }}
|
<span v-if="userHealthSleep.light_sleep_seconds">
|
||||||
-
|
{{ formatDuration(userHealthSleep.light_sleep_seconds) }}
|
||||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.light_percentage_score)) }}
|
-
|
||||||
|
{{ $t(getScoreStatusI18nKey(userHealthSleep.light_percentage_score)) }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
<!-- awake_sleep_seconds -->
|
||||||
<p class="mb-1">
|
<p class="mb-1">
|
||||||
<span class="fw-semibold">
|
<span class="fw-semibold">
|
||||||
{{ $t('healthSleepListTabsComponent.awakeLabel') }}:
|
{{ $t('healthSleepListTabsComponent.awakeLabel') }}:
|
||||||
</span>
|
</span>
|
||||||
{{ formatDuration(userHealthSleep.awake_sleep_seconds) }}
|
<span v-if="userHealthSleep.awake_sleep_seconds">
|
||||||
-
|
{{ formatDuration(userHealthSleep.awake_sleep_seconds) }}
|
||||||
{{ $t(getScoreStatusI18nKey(userHealthSleep.awake_count_score)) }}
|
-
|
||||||
|
{{ $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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +310,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { formatDuration, formatDateShort } from '@/utils/dateTimeUtils'
|
import { formatDuration } from '@/utils/dateTimeUtils'
|
||||||
import { getHrvStatusI18nKey, getScoreStatusI18nKey } from '@/utils/healthUtils'
|
import { getHrvStatusI18nKey, getScoreStatusI18nKey } from '@/utils/healthUtils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"sleep": "Today's sleep",
|
||||||
|
"restingHeartRate": "Resting heart rate",
|
||||||
|
"avgSkinTemperatureDeviation": "Skin temp. dev.",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"noWeightData": "No weight data",
|
"noWeightData": "No weight data",
|
||||||
"noWeightTarget": "No weight target",
|
"noWeightTarget": "No weight target",
|
||||||
|
|||||||
@@ -17,5 +17,20 @@
|
|||||||
"poorLabel": "Poor",
|
"poorLabel": "Poor",
|
||||||
"excellentLabel": "Excellent",
|
"excellentLabel": "Excellent",
|
||||||
"goodLabel": "Good",
|
"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",
|
"genderFemale": "Female",
|
||||||
"genderUnspecified": "Unspecified",
|
"genderUnspecified": "Unspecified",
|
||||||
"labelAverage": "Average",
|
"labelAverage": "Average",
|
||||||
"labelMaximum": "Maximum"
|
"labelMaximum": "Maximum",
|
||||||
|
"unitsBrpm": "brpm",
|
||||||
|
"unitsCelsius": "°C",
|
||||||
|
"unitsFahrenheit": "°F"
|
||||||
}
|
}
|
||||||
@@ -2,65 +2,34 @@
|
|||||||
<h1>{{ $t('healthView.title') }}</h1>
|
<h1>{{ $t('healthView.title') }}</h1>
|
||||||
<div class="row row-gap-3">
|
<div class="row row-gap-3">
|
||||||
<!-- Include the HealthSideBarComponent -->
|
<!-- Include the HealthSideBarComponent -->
|
||||||
<HealthSideBarComponent
|
<HealthSideBarComponent :activeSection="activeSection" @update-active-section="updateActiveSection" />
|
||||||
:activeSection="activeSection"
|
|
||||||
@update-active-section="updateActiveSection"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadingComponent v-if="isLoading" />
|
<LoadingComponent v-if="isLoading" />
|
||||||
|
|
||||||
<!-- Include the HealthDashboardZone -->
|
<!-- Include the HealthDashboardZone -->
|
||||||
<HealthDashboardZone
|
<HealthDashboardZone :userHealthWeight="userHealthWeight" :userHealthSteps="userHealthSteps"
|
||||||
:userHealthWeight="userHealthWeight"
|
:userHealthSleep="userHealthSleep" :userHealthTargets="userHealthTargets"
|
||||||
:userHealthSteps="userHealthSteps"
|
v-if="activeSection === 'dashboard' && !isLoading" />
|
||||||
:userHealthTargets="userHealthTargets"
|
|
||||||
v-if="activeSection === 'dashboard' && !isLoading"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Include the HealthWeightZone -->
|
<!-- Include the HealthWeightZone -->
|
||||||
<HealthWeightZone
|
<HealthWeightZone :userHealthWeight="userHealthWeight" :userHealthWeightPagination="userHealthWeightPagination"
|
||||||
:userHealthWeight="userHealthWeight"
|
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesWeight"
|
||||||
:userHealthWeightPagination="userHealthWeightPagination"
|
:pageNumber="pageNumberWeight" @createdWeight="updateWeightListAdded" @deletedWeight="updateWeightListDeleted"
|
||||||
:userHealthTargets="userHealthTargets"
|
@editedWeight="updateWeightListEdited" @pageNumberChanged="setPageNumberWeight" @setWeightTarget="setWeightTarget"
|
||||||
:isLoading="isLoading"
|
v-if="activeSection === 'weight' && !isLoading" />
|
||||||
:totalPages="totalPagesWeight"
|
|
||||||
:pageNumber="pageNumberWeight"
|
|
||||||
@createdWeight="updateWeightListAdded"
|
|
||||||
@deletedWeight="updateWeightListDeleted"
|
|
||||||
@editedWeight="updateWeightListEdited"
|
|
||||||
@pageNumberChanged="setPageNumberWeight"
|
|
||||||
@setWeightTarget="setWeightTarget"
|
|
||||||
v-if="activeSection === 'weight' && !isLoading"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Include the HealthStepsZone -->
|
<!-- Include the HealthStepsZone -->
|
||||||
<HealthStepsZone
|
<HealthStepsZone :userHealthSteps="userHealthSteps" :userHealthStepsPagination="userHealthStepsPagination"
|
||||||
:userHealthSteps="userHealthSteps"
|
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSteps"
|
||||||
:userHealthStepsPagination="userHealthStepsPagination"
|
:pageNumber="pageNumberSteps" @createdSteps="updateStepsListAdded" @deletedSteps="updateStepsListDeleted"
|
||||||
:userHealthTargets="userHealthTargets"
|
@editedSteps="updateStepsListEdited" @pageNumberChanged="setPageNumberSteps" @setStepsTarget="setStepsTarget"
|
||||||
:isLoading="isLoading"
|
v-if="activeSection === 'steps' && !isLoading" />
|
||||||
:totalPages="totalPagesSteps"
|
|
||||||
:pageNumber="pageNumberSteps"
|
|
||||||
@createdSteps="updateStepsListAdded"
|
|
||||||
@deletedSteps="updateStepsListDeleted"
|
|
||||||
@editedSteps="updateStepsListEdited"
|
|
||||||
@pageNumberChanged="setPageNumberSteps"
|
|
||||||
@setStepsTarget="setStepsTarget"
|
|
||||||
v-if="activeSection === 'steps' && !isLoading"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Include the HealthSleepZone -->
|
<!-- Include the HealthSleepZone -->
|
||||||
<HealthSleepZone
|
<HealthSleepZone :userHealthSleep="userHealthSleep" :userHealthSleepPagination="userHealthSleepPagination"
|
||||||
:userHealthSleep="userHealthSleep"
|
:userHealthTargets="userHealthTargets" :isLoading="isLoading" :totalPages="totalPagesSleep"
|
||||||
:userHealthSleepPagination="userHealthSleepPagination"
|
:pageNumber="pageNumberSleep" @pageNumberChanged="setPageNumberSleep" @setSleepTarget="setSleepTarget"
|
||||||
:userHealthTargets="userHealthTargets"
|
v-if="activeSection === 'sleep' && !isLoading" />
|
||||||
:isLoading="isLoading"
|
|
||||||
:totalPages="totalPagesSleep"
|
|
||||||
:pageNumber="pageNumberSleep"
|
|
||||||
@pageNumberChanged="setPageNumberSleep"
|
|
||||||
@setSleepTarget="setSleepTarget"
|
|
||||||
v-if="activeSection === 'sleep' && !isLoading"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- back button -->
|
<!-- back button -->
|
||||||
<BackButtonComponent />
|
<BackButtonComponent />
|
||||||
|
|||||||
Reference in New Issue
Block a user