Add sleep target modal and improve sleep zone UI

Introduces a reusable ModalComponentHoursMinutesInput for setting sleep targets, integrates it into HealthSleepZone, and adds logic to update the user's sleep target. Enhances HealthSleepListTabsComponent with improved labels and i18n support, adds new utility functions for time conversion and health status mapping, and updates i18n files with relevant translations.
This commit is contained in:
João Vitória Silva
2025-11-27 10:27:54 +00:00
parent 836060d176
commit 399c954563
9 changed files with 256 additions and 10 deletions

View File

@@ -12,8 +12,33 @@
data-bs-target="#addSleepModal"
>{{ t('healthSleepZoneComponent.buttonAddSleep') }}</a
>
<a
class="w-100 btn btn-primary shadow-sm ms-1"
href="#"
role="button"
data-bs-toggle="modal"
data-bs-target="#addSleepTargetModal"
>{{ $t('healthSleepZoneComponent.buttonSleepTarget') }}</a
>
</div>
<!--<HealthStepsAddEditModalComponent
:action="'add'"
@isLoadingNewSteps="updateIsLoadingNewSteps"
@createdSteps="updateStepsListAdded"
/>-->
<ModalComponentHoursMinutesInput
modalId="addSleepTargetModal"
:title="t('healthSleepZoneComponent.buttonSleepTarget')"
:hoursFieldLabel="t('healthSleepZoneComponent.modalSleepTargetHoursLabel')"
:minutesFieldLabel="t('healthSleepZoneComponent.modalSleepTargetMinutesLabel')"
actionButtonType="success"
:actionButtonText="t('generalItems.buttonSubmit')"
:secondsDefaultValue="props.userHealthTargets?.sleep || 28800"
@fieldsToEmitAction="submitSetSleepTarget"
/>
<!-- Checking if userHealthSleepPagination is loaded and has length -->
<div
v-if="userHealthSleepPagination && userHealthSleepPagination.length"
@@ -73,6 +98,7 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ModalComponentHoursMinutesInput from '../Modals/ModalComponentHoursMinutesInput.vue'
import HealthSleepBarChartComponent from './HealthSleepZone/HealthSleepBarChartComponent.vue'
import HealthSleepListComponent from './HealthSleepZone/HealthSleepListComponent.vue'
import HealthSleepTimelineChartComponent from './HealthSleepZone/HealthSleepTimelineChartComponent.vue'
@@ -107,8 +133,6 @@ const props = defineProps({
}
})
console.log(props.userHealthSleep)
const emit = defineEmits(['pageNumberChanged'])
const { t } = useI18n()
@@ -117,4 +141,8 @@ const isLoadingNewSleep = ref(false)
function setPageNumber(page) {
emit('pageNumberChanged', page)
}
function submitSetSleepTarget(sleepTarget) {
emit('setSleepTarget', sleepTarget)
}
</script>

View File

@@ -42,10 +42,12 @@
>
<div class="row">
<div class="col">
<p>Score: {{ userHealthSleep.sleep_score_overall }}</p>
<p>Quality: {{ userHealthSleep.sleep_score_quality }}</p>
<p>Duration: {{ userHealthSleep.sleep_score_duration }}</p>
<p>Duration: {{ formatDuration(userHealthSleep.total_sleep_seconds) }}</p>
<p v-if="userHealthSleep.sleep_score_overall">{{ $t('healthSleepListTabsComponent.scoreLabel') }}: {{ userHealthSleep.sleep_score_overall }}</p>
<p v-if="userHealthSleep.sleep_score_quality">{{ $t('healthSleepListTabsComponent.qualityLabel') }}: {{ userHealthSleep.sleep_score_quality }}</p>
<p v-if="userHealthSleep.sleep_score_duration">{{ $t('healthSleepListTabsComponent.durationLabel') }}: {{ $t(getScoreStatusI18nKey(userHealthSleep.sleep_score_duration)) }}</p>
<p v-if="userHealthSleep.hrv_status">
HRV Status: {{ $t(getHrvStatusI18nKey(userHealthSleep.hrv_status)) }}
</p>
</div>
<div class="col">
<p>Deep: {{ formatDuration(userHealthSleep.deep_sleep_seconds) }}</p>
@@ -61,6 +63,7 @@
<script setup>
import { formatDuration, formatDateShort } from '@/utils/dateTimeUtils'
import { getHrvStatusI18nKey, getScoreStatusI18nKey } from '@/utils/healthUtils'
const props = defineProps({
userHealthSleep: {

View File

@@ -0,0 +1,144 @@
<template>
<div
ref="modalRef"
class="modal fade"
:id="modalId"
tabindex="-1"
:aria-labelledby="`${modalId}Title`"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" :id="`${modalId}Title`">{{ title }}</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="input-group mb-3">
<!-- Hours field -->
<input
:id="`${modalId}HoursInput`"
v-model="hoursToEmit"
class="form-control"
type="number"
:name="`${modalId}HoursInput`"
:placeholder="hoursFieldLabel"
:aria-label="hoursFieldLabel"
required
/>
<span class="input-group-text" id="basic-addon2">{{ hoursFieldLabel }}</span>
<!-- Minutes field -->
<input
:id="`${modalId}MinutesInput`"
v-model="minutesToEmit"
class="form-control"
type="number"
:name="`${modalId}MinutesInput`"
:placeholder="minutesFieldLabel"
:aria-label="minutesFieldLabel"
required
/>
<span class="input-group-text" id="basic-addon2">{{ minutesFieldLabel }}</span>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
aria-label="Close modal"
>
{{ $t('generalItems.buttonClose') }}
</button>
<button
type="button"
@click="submitAction"
class="btn"
:class="{
'btn-success': actionButtonType === 'success',
'btn-danger': actionButtonType === 'danger',
'btn-warning': actionButtonType === 'warning',
'btn-primary': actionButtonType === 'primary'
}"
data-bs-dismiss="modal"
:aria-label="actionButtonText"
>
{{ actionButtonText }}
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
// Vue composition API
import { ref, onMounted, onUnmounted, type PropType } from 'vue'
// Composables
import { useBootstrapModal } from '@/composables/useBootstrapModal'
import { returnHoursMinutesFromSeconds, returnSecondsFromHoursMinutes } from '@/utils/dateTimeUtils'
// Types
import type { ActionButtonType } from '@/types'
const props = defineProps({
modalId: {
type: String,
required: true
},
title: {
type: String,
required: true
},
hoursFieldLabel: {
type: String,
required: true
},
secondsDefaultValue: {
type: Number,
default: 28800
},
minutesFieldLabel: {
type: String,
required: true
},
actionButtonType: {
type: String as PropType<ActionButtonType>,
required: true,
validator: (value: string) => ['success', 'danger', 'warning', 'primary'].includes(value)
},
actionButtonText: {
type: String,
required: true
}
})
const emit = defineEmits<{
fieldsToEmitAction: [value: number]
}>()
const { initializeModal, disposeModal } = useBootstrapModal()
const modalRef = ref<HTMLDivElement | null>(null)
const { hours, minutes } = returnHoursMinutesFromSeconds(props.secondsDefaultValue)
const hoursToEmit = ref(hours)
const minutesToEmit = ref(minutes)
const submitAction = (): void => {
const secondsToEmit = returnSecondsFromHoursMinutes(hoursToEmit.value, minutesToEmit.value)
emit('fieldsToEmitAction', secondsToEmit)
}
onMounted(async () => {
await initializeModal(modalRef)
})
onUnmounted(() => {
disposeModal()
})
</script>

View File

@@ -1,4 +1,14 @@
{
"sleepScoreLabel": "Sleep score",
"sleepDetailsLabel": "Sleep details"
"sleepDetailsLabel": "Sleep details",
"scoreLabel": "Score",
"qualityLabel": "Quality",
"durationLabel": "Duration",
"balancedLabel": "Balanced",
"unbalancedLabel": "Unbalanced",
"lowLabel": "Low",
"poorLabel": "Poor",
"excellentLabel": "Excellent",
"goodLabel": "Good",
"fairLabel": "Fair"
}

View File

@@ -1,5 +1,8 @@
{
"buttonAddSleep": "Add sleep",
"buttonSleepTarget": "Sleep target",
"modalSleepTargetHoursLabel": "hours",
"modalSleepTargetMinutesLabel": "minutes",
"labelNumberOfHealthSleep1": "There is a total of ",
"labelNumberOfHealthSleep2": " sleep entries inserted (",
"labelNumberOfHealthSleep3": " loaded):"

View File

@@ -7,5 +7,7 @@
"successUpdatingWeightTarget": "Successfully updated weight target",
"errorUpdatingWeightTarget": "Error updating weight target",
"successUpdatingStepsTarget": "Successfully updated steps target",
"errorUpdatingStepsTarget": "Error updating steps target"
"errorUpdatingStepsTarget": "Error updating steps target",
"successUpdatingSleepTarget": "Successfully updated sleep target",
"errorUpdatingSleepTarget": "Error updating sleep target"
}

View File

@@ -67,6 +67,17 @@ export function formatSecondsToOnlyHours(totalSeconds) {
return 0
}
export function returnHoursMinutesFromSeconds(totalSeconds) {
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
return { hours, minutes }
}
export function returnSecondsFromHoursMinutes(hours, minutes) {
return hours * 3600 + minutes * 60
}
/**
* Formats seconds into a human-readable duration string without seconds.
*

View File

@@ -0,0 +1,31 @@
/**
* Maps HRV status enum values to i18n translation keys.
*
* @param {string} hrvStatus - The HRV status value (BALANCED, UNBALANCED, LOW, POOR)
* @returns {string} The i18n translation key for the HRV status
*/
export function getHrvStatusI18nKey(hrvStatus) {
if (!hrvStatus) return ''
const statusMap = {
'BALANCED': 'healthSleepListTabsComponent.balancedLabel',
'UNBALANCED': 'healthSleepListTabsComponent.unbalancedLabel',
'LOW': 'healthSleepListTabsComponent.lowLabel',
'POOR': 'healthSleepListTabsComponent.poorLabel'
}
return statusMap[hrvStatus] || ''
}
export function getScoreStatusI18nKey(scoreStatus) {
if (!scoreStatus) return ''
const statusMap = {
'EXCELLENT': 'healthSleepListTabsComponent.excellentLabel',
'GOOD': 'healthSleepListTabsComponent.goodLabel',
'FAIR': 'healthSleepListTabsComponent.fairLabel',
'POOR': 'healthSleepListTabsComponent.poorLabel'
}
return statusMap[scoreStatus] || ''
}

View File

@@ -58,6 +58,7 @@
:totalPages="totalPagesSleep"
:pageNumber="pageNumberSleep"
@pageNumberChanged="setPageNumberSleep"
@setSleepTarget="setSleepTarget"
v-if="activeSection === 'sleep' && !isLoading"
/>
</div>
@@ -322,7 +323,6 @@ function setWeightTarget(weightTarget) {
id: userHealthTargets.value.id,
user_id: userHealthTargets.value.user_id,
weight: weightTarget,
steps: userHealthTargets.value.steps,
}
try {
health_targets.setUserHealthTargets(data)
@@ -337,7 +337,6 @@ function setStepsTarget(stepsTarget) {
const data = {
id: userHealthTargets.value.id,
user_id: userHealthTargets.value.user_id,
weight: userHealthTargets.value.weight,
steps: stepsTarget,
}
try {
@@ -349,6 +348,21 @@ function setStepsTarget(stepsTarget) {
}
}
function setSleepTarget(sleepTarget) {
const data = {
id: userHealthTargets.value.id,
user_id: userHealthTargets.value.user_id,
sleep: sleepTarget,
}
try {
health_targets.setUserHealthTargets(data)
userHealthTargets.value.sleep = sleepTarget
push.success(t('healthView.successUpdatingSleepTarget'))
} catch (error) {
push.error(`${t('healthView.errorUpdatingSleepTarget')} - ${error}`)
}
}
// Watch functions
watch(pageNumberSleep, updateHealthSleep, { immediate: false })
watch(pageNumberSteps, updateHealthSteps, { immediate: false })