mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
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:
@@ -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>
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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):"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
31
frontend/app/src/utils/healthUtils.js
Normal file
31
frontend/app/src/utils/healthUtils.js
Normal 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] || ''
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user