mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-10 08:17:59 -05:00
Add HealthSleepAddEditModalComponent and refactor sleep zone
Introduces HealthSleepAddEditModalComponent for adding and editing sleep records. Refactors HealthSleepZone.vue to use the new modal, improves code formatting and readability across health and activity components, and updates event handling for sleep data management.
This commit is contained in:
@@ -298,7 +298,7 @@ import {
|
||||
activityTypeIsSailing,
|
||||
activityTypeNotSailing,
|
||||
activityTypeIsWindsurf,
|
||||
activityTypeNotWindsurf,
|
||||
activityTypeNotWindsurf
|
||||
} from '@/utils/activityUtils'
|
||||
import { formatSecondsToMinutes } from '@/utils/dateTimeUtils'
|
||||
import { metersToFeet } from '@/utils/unitsUtils'
|
||||
@@ -370,12 +370,20 @@ onMounted(async () => {
|
||||
elePresent.value = true
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 5) {
|
||||
if (activityTypeIsCycling(props.activity) || activityTypeIsSailing(props.activity) || activityTypeIsWindsurf(props.activity)) {
|
||||
if (
|
||||
activityTypeIsCycling(props.activity) ||
|
||||
activityTypeIsSailing(props.activity) ||
|
||||
activityTypeIsWindsurf(props.activity)
|
||||
) {
|
||||
velPresent.value = true
|
||||
}
|
||||
}
|
||||
if (props.activityActivityStreams[i].stream_type === 6) {
|
||||
if (activityTypeNotCycling(props.activity) && activityTypeNotSailing(props.activity) && activityTypeNotWindsurf(props.activity)) {
|
||||
if (
|
||||
activityTypeNotCycling(props.activity) &&
|
||||
activityTypeNotSailing(props.activity) &&
|
||||
activityTypeNotWindsurf(props.activity)
|
||||
) {
|
||||
pacePresent.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +181,7 @@ import {
|
||||
activityTypeIsSailing,
|
||||
activityTypeNotSailing,
|
||||
activityTypeIsWindsurf,
|
||||
activityTypeNotWindsurf,
|
||||
activityTypeNotWindsurf
|
||||
} from '@/utils/activityUtils'
|
||||
// Import Notivue push
|
||||
import { push } from 'notivue'
|
||||
@@ -299,7 +299,11 @@ onMounted(async () => {
|
||||
}
|
||||
if (element.stream_type === 5) {
|
||||
velPresent.value = true
|
||||
if (activityTypeIsCycling(props.activity) || activityTypeIsSailing(props.activity) || activityTypeIsWindsurf(props.activity)) {
|
||||
if (
|
||||
activityTypeIsCycling(props.activity) ||
|
||||
activityTypeIsSailing(props.activity) ||
|
||||
activityTypeIsWindsurf(props.activity)
|
||||
) {
|
||||
graphItems.value.push({
|
||||
type: 'vel',
|
||||
label: `${t('activityMandAbovePillsComponent.labelGraphVelocity')}`
|
||||
@@ -308,7 +312,11 @@ onMounted(async () => {
|
||||
}
|
||||
if (element.stream_type === 6) {
|
||||
pacePresent.value = true
|
||||
if (activityTypeNotCycling(props.activity) && activityTypeNotSailing(props.activity) && activityTypeNotWindsurf(props.activity)) {
|
||||
if (
|
||||
activityTypeNotCycling(props.activity) &&
|
||||
activityTypeNotSailing(props.activity) &&
|
||||
activityTypeNotWindsurf(props.activity)
|
||||
) {
|
||||
graphItems.value.push({
|
||||
type: 'pace',
|
||||
label: `${t('activityMandAbovePillsComponent.labelGraphPace')}`
|
||||
|
||||
@@ -232,7 +232,6 @@ const initMap = () => {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(leafletMap.value)
|
||||
|
||||
|
||||
const polyline = L.polyline(latlngs, {
|
||||
color: '#2563eb',
|
||||
weight: 4,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
activityTypeIsSwimming,
|
||||
activityTypeIsRunning,
|
||||
activityTypeIsRowing,
|
||||
activityTypeIsWalking,
|
||||
activityTypeIsWalking
|
||||
} from '@/utils/activityUtils'
|
||||
import { metersToFeet, kmToMiles } from '@/utils/unitsUtils'
|
||||
|
||||
@@ -197,7 +197,11 @@ export default {
|
||||
} else {
|
||||
// Compute converted pace (minutes per km or per mile for running, walking and rowing, minutes per 100m/100yd for swimming)
|
||||
let converted = null
|
||||
if (activityTypeIsRunning(props.activity) || activityTypeIsWalking(props.activity) || activityTypeIsRowing(props.activity)) {
|
||||
if (
|
||||
activityTypeIsRunning(props.activity) ||
|
||||
activityTypeIsWalking(props.activity) ||
|
||||
activityTypeIsRowing(props.activity)
|
||||
) {
|
||||
if (Number(units.value) === 1) {
|
||||
converted = (paceData.pace * 1000) / 60 // min/km
|
||||
} else {
|
||||
@@ -226,7 +230,11 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activityTypeIsRunning(props.activity) || activityTypeIsWalking(props.activity) || activityTypeIsRowing(props.activity)) {
|
||||
if (
|
||||
activityTypeIsRunning(props.activity) ||
|
||||
activityTypeIsWalking(props.activity) ||
|
||||
activityTypeIsRowing(props.activity)
|
||||
) {
|
||||
if (Number(units.value) === 1) {
|
||||
label = t('generalItems.labelPaceInMinKm')
|
||||
} else {
|
||||
|
||||
@@ -73,11 +73,7 @@
|
||||
role="button"
|
||||
v-if="activity.garminconnect_activity_id"
|
||||
>
|
||||
<img
|
||||
:src="INTEGRATION_LOGOS.garminConnectApp"
|
||||
alt="Garmin Connect logo"
|
||||
height="22"
|
||||
/>
|
||||
<img :src="INTEGRATION_LOGOS.garminConnectApp" alt="Garmin Connect logo" height="22" />
|
||||
</a>
|
||||
<div>
|
||||
<button
|
||||
@@ -251,12 +247,7 @@
|
||||
{{ formatPace(t, activity, authStore.user.units) }}
|
||||
</div>
|
||||
<!-- avg_speed sailing activities -->
|
||||
<div
|
||||
v-else-if="
|
||||
activityTypeIsWindsurf(activity) ||
|
||||
activityTypeIsSailing(activity)
|
||||
"
|
||||
>
|
||||
<div v-else-if="activityTypeIsWindsurf(activity) || activityTypeIsSailing(activity)">
|
||||
<span class="fw-lighter">
|
||||
{{ $t('activitySummaryComponent.activityAvgSpeed') }}
|
||||
</span>
|
||||
|
||||
@@ -303,7 +303,9 @@
|
||||
</select>
|
||||
<!-- hide map fields -->
|
||||
<label for="activityHideMapEdit"
|
||||
><b>* {{ $t('editActivityModalComponent.modalEditActivityHideMapLabel') }}</b></label
|
||||
><b
|
||||
>* {{ $t('editActivityModalComponent.modalEditActivityHideMapLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
@@ -416,7 +418,9 @@
|
||||
</div>
|
||||
<!-- hide laps fields -->
|
||||
<label for="activityHideLapsEdit"
|
||||
><b>* {{ $t('editActivityModalComponent.modalEditActivityHideLapsLabel') }}</b></label
|
||||
><b
|
||||
>* {{ $t('editActivityModalComponent.modalEditActivityHideLapsLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
@@ -431,7 +435,9 @@
|
||||
<label for="activityHideWorkoutSetsStepsEdit"
|
||||
><b
|
||||
>*
|
||||
{{ $t('editActivityModalComponent.modalEditActivityHideWorkoutSetsStepsLabel') }}</b
|
||||
{{
|
||||
$t('editActivityModalComponent.modalEditActivityHideWorkoutSetsStepsLabel')
|
||||
}}</b
|
||||
></label
|
||||
>
|
||||
<select
|
||||
@@ -445,7 +451,9 @@
|
||||
</select>
|
||||
<!-- hide gear fields -->
|
||||
<label for="activityHideGearEdit"
|
||||
><b>* {{ $t('editActivityModalComponent.modalEditActivityHideGearLabel') }}</b></label
|
||||
><b
|
||||
>* {{ $t('editActivityModalComponent.modalEditActivityHideGearLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
|
||||
@@ -26,11 +26,7 @@
|
||||
• v0.16.0
|
||||
</p>
|
||||
<p class="text-center text-muted">
|
||||
<img
|
||||
:src="INTEGRATION_LOGOS.strava"
|
||||
alt="Compatible with STRAVA image"
|
||||
height="25"
|
||||
/>
|
||||
<img :src="INTEGRATION_LOGOS.strava" alt="Compatible with STRAVA image" height="25" />
|
||||
•
|
||||
<img
|
||||
class="ms-2"
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
</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-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>
|
||||
@@ -29,7 +32,9 @@
|
||||
<h4>{{ $t('healthDashboardZoneComponent.restingHeartRate') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 v-if="restingHeartRate">{{ restingHeartRate }} {{ $t('generalItems.unitsBpm') }}</h1>
|
||||
<h1 v-if="restingHeartRate">
|
||||
{{ restingHeartRate }} {{ $t('generalItems.unitsBpm') }}
|
||||
</h1>
|
||||
<h1 v-else>{{ $t('generalItems.labelNoData') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
@@ -45,7 +50,9 @@
|
||||
<h4>{{ $t('healthDashboardZoneComponent.avgSkinTemperatureDeviation') }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 v-if="avgSkinTempDeviation">{{ avgSkinTempDeviation }} {{ $t('generalItems.unitsCelsius') }}</h1>
|
||||
<h1 v-if="avgSkinTempDeviation">
|
||||
{{ avgSkinTempDeviation }} {{ $t('generalItems.unitsCelsius') }}
|
||||
</h1>
|
||||
<h1 v-else>{{ $t('generalItems.labelNoData') }}</h1>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary">
|
||||
@@ -69,13 +76,28 @@
|
||||
<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-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">
|
||||
<span
|
||||
v-if="
|
||||
userHealthTargets &&
|
||||
userHealthTargets['weight'] &&
|
||||
Number(authStore?.user?.units) === 1
|
||||
"
|
||||
>
|
||||
{{ userHealthTargets.weight }} {{ $t('generalItems.unitsKg') }}
|
||||
</span>
|
||||
<span v-else-if="userHealthTargets && userHealthTargets['weight'] && Number(authStore?.user?.units) === 2">
|
||||
<span
|
||||
v-else-if="
|
||||
userHealthTargets &&
|
||||
userHealthTargets['weight'] &&
|
||||
Number(authStore?.user?.units) === 2
|
||||
"
|
||||
>
|
||||
{{ kgToLbs(userHealthTargets.weight) }} {{ $t('generalItems.unitsLbs') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('healthDashboardZoneComponent.noWeightTarget') }}</span>
|
||||
@@ -113,10 +135,14 @@
|
||||
</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-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') }}
|
||||
{{ userHealthTargets.steps }}
|
||||
{{ $t('healthDashboardZoneComponent.stepsTargetLabel') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('healthDashboardZoneComponent.noStepsTarget') }}</span>
|
||||
</div>
|
||||
@@ -211,4 +237,4 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -3,27 +3,40 @@
|
||||
<LoadingComponent v-if="isLoading" />
|
||||
<div v-else>
|
||||
<!-- Checking if userHealthSleep is loaded and has length -->
|
||||
<div v-if="userHealthSleep && userHealthSleep.length" class="p-3 bg-body-tertiary rounded shadow-sm">
|
||||
<div
|
||||
v-if="userHealthSleep && userHealthSleep.length"
|
||||
class="p-3 bg-body-tertiary rounded shadow-sm"
|
||||
>
|
||||
<!-- show graph -->
|
||||
<HealthRHRLineChartComponent :userHealthSleep="userHealthSleep" :isLoading="isLoading" />
|
||||
|
||||
<br />
|
||||
<p>
|
||||
{{ $t('healthRHRZoneComponent.labelNumberOfHealthRHR1')
|
||||
}}{{ userHealthSleep.length
|
||||
{{ $t('healthRHRZoneComponent.labelNumberOfHealthRHR1') }}{{ userHealthSleep.length
|
||||
}}{{ $t('healthRHRZoneComponent.labelNumberOfHealthRHR2')
|
||||
}}{{ userHealthSleepPagination.length
|
||||
}}{{ $t('healthRHRZoneComponent.labelNumberOfHealthRHR3') }}
|
||||
</p>
|
||||
|
||||
<!-- list zone -->
|
||||
<ul class="my-3 list-group list-group-flush" v-for="userHealthSleep in userHealthSleepPagination"
|
||||
:key="userHealthSleep.id" :userHealthSleep="userHealthSleep">
|
||||
<HealthRHRListComponent :userHealthSleep="userHealthSleep" v-if="userHealthSleep.resting_heart_rate" />
|
||||
<ul
|
||||
class="my-3 list-group list-group-flush"
|
||||
v-for="userHealthSleep in userHealthSleepPagination"
|
||||
:key="userHealthSleep.id"
|
||||
:userHealthSleep="userHealthSleep"
|
||||
>
|
||||
<HealthRHRListComponent
|
||||
:userHealthSleep="userHealthSleep"
|
||||
v-if="userHealthSleep.resting_heart_rate"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<!-- pagination area -->
|
||||
<PaginationComponent :totalPages="totalPages" :pageNumber="pageNumber" @pageNumberChanged="setPageNumber" />
|
||||
<PaginationComponent
|
||||
:totalPages="totalPages"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChanged="setPageNumber"
|
||||
/>
|
||||
</div>
|
||||
<!-- Displaying a message or component when there are no RHR measurements -->
|
||||
<div v-else>
|
||||
@@ -68,4 +81,4 @@ const emit = defineEmits(['pageNumberChanged'])
|
||||
function setPageNumber(page) {
|
||||
emit('pageNumberChanged', page)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -84,14 +84,11 @@ const chartData = computed(() => {
|
||||
for (const healthSleep of sortedSleep) {
|
||||
data.push(healthSleep.resting_heart_rate)
|
||||
|
||||
|
||||
const createdAt = new Date(healthSleep.date)
|
||||
labels.push(
|
||||
`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`
|
||||
)
|
||||
labels.push(`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`)
|
||||
}
|
||||
|
||||
const label = t('generalItems.RHRLabel') + " (" + t('generalItems.unitsBpm') + ")"
|
||||
const label = t('generalItems.RHRLabel') + ' (' + t('generalItems.unitsBpm') + ')'
|
||||
|
||||
const datasets = [
|
||||
{
|
||||
@@ -235,4 +232,4 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
/* Ensures the canvas stretches across the available width */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<span>{{ userHealthSleep.resting_heart_rate }} {{ $t('generalItems.unitsBpm') }}</span>
|
||||
</div>
|
||||
<span>
|
||||
{{ $t('healthStepsListComponent.labelDate') }}: {{ formatDateShort(userHealthSleep.date) }}
|
||||
{{ $t('healthStepsListComponent.labelDate') }}:
|
||||
{{ formatDateShort(userHealthSleep.date) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,61 @@
|
||||
<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 === 'sleep' }"
|
||||
@click.prevent="changeActive('sleep')">
|
||||
<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 === 'rhr' }"
|
||||
@click.prevent="changeActive('rhr')">
|
||||
<a
|
||||
href="#"
|
||||
class="nav-link link-body-emphasis"
|
||||
:class="{ active: activeSection === 'rhr' }"
|
||||
@click.prevent="changeActive('rhr')"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'heart-pulse']" />
|
||||
<span class="ms-1">{{ $t('healthSideBarComponent.RHRSection') }}</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 === 'weight' }"
|
||||
@click.prevent="changeActive('weight')">
|
||||
<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>
|
||||
|
||||
@@ -4,36 +4,56 @@
|
||||
<div v-else>
|
||||
<!-- add sleep button -->
|
||||
<div class="d-flex">
|
||||
<a class="w-100 btn btn-primary shadow-sm me-1 disabled" href="#" role="button" data-bs-toggle="modal"
|
||||
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>
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm me-1"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
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
|
||||
<HealthSleepAddEditModalComponent
|
||||
:action="'add'"
|
||||
@isLoadingNewSteps="updateIsLoadingNewSteps"
|
||||
@createdSteps="updateStepsListAdded"
|
||||
/>-->
|
||||
@isLoadingNewSleep="updateIsLoadingNewSleep"
|
||||
@createdSleep="updateSleepListAdded"
|
||||
/>
|
||||
|
||||
<ModalComponentHoursMinutesInput modalId="addSleepTargetModal"
|
||||
<ModalComponentHoursMinutesInput
|
||||
modalId="addSleepTargetModal"
|
||||
:title="t('healthSleepZoneComponent.buttonSleepTarget')"
|
||||
:hoursFieldLabel="t('healthSleepZoneComponent.modalSleepTargetHoursLabel')"
|
||||
:minutesFieldLabel="t('healthSleepZoneComponent.modalSleepTargetMinutesLabel')" actionButtonType="success"
|
||||
:minutesFieldLabel="t('healthSleepZoneComponent.modalSleepTargetMinutesLabel')"
|
||||
actionButtonType="success"
|
||||
:actionButtonText="t('generalItems.buttonSubmit')"
|
||||
:secondsDefaultValue="props.userHealthTargets?.sleep || 28800" @fieldsToEmitAction="submitSetSleepTarget" />
|
||||
:secondsDefaultValue="props.userHealthTargets?.sleep || 28800"
|
||||
@fieldsToEmitAction="submitSetSleepTarget"
|
||||
/>
|
||||
|
||||
<!-- Checking if userHealthSleepPagination is loaded and has length -->
|
||||
<div v-if="userHealthSleepPagination && userHealthSleepPagination.length"
|
||||
class="mt-3 p-3 bg-body-tertiary rounded shadow-sm">
|
||||
<div
|
||||
v-if="userHealthSleepPagination && userHealthSleepPagination.length"
|
||||
class="mt-3 p-3 bg-body-tertiary rounded shadow-sm"
|
||||
>
|
||||
<!-- show graph -->
|
||||
<HealthSleepBarChartComponent :userHealthTargets="userHealthTargets" :userHealthSleep="userHealthSleep"
|
||||
:isLoading="isLoading" />
|
||||
<HealthSleepBarChartComponent
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:userHealthSleep="userHealthSleep"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
{{ $t('healthSleepZoneComponent.labelNumberOfHealthSleep1')
|
||||
}}{{ userHealthSleep.length
|
||||
{{ $t('healthSleepZoneComponent.labelNumberOfHealthSleep1') }}{{ userHealthSleep.length
|
||||
}}{{ $t('healthSleepZoneComponent.labelNumberOfHealthSleep2')
|
||||
}}{{ userHealthSleepPagination.length
|
||||
}}{{ $t('healthSleepZoneComponent.labelNumberOfHealthSleep3') }}
|
||||
@@ -47,17 +67,28 @@
|
||||
</ul>
|
||||
|
||||
<!-- list zone -->
|
||||
<ul class="my-3 list-group list-group-flush" v-for="userHealthSleep in userHealthSleepPagination"
|
||||
:key="userHealthSleep.id" :data="userHealthSleep">
|
||||
<ul
|
||||
class="my-3 list-group list-group-flush"
|
||||
v-for="userHealthSleep in userHealthSleepPagination"
|
||||
:key="userHealthSleep.id"
|
||||
:data="userHealthSleep"
|
||||
>
|
||||
<!--<HealthSleepTimelineChartComponent
|
||||
:data="userHealthSleep.sleep_stages"
|
||||
/>-->
|
||||
<HealthSleepListComponent :userHealthSleep="userHealthSleep" @deletedSleep="updateSleepListDeleted"
|
||||
@editedSleep="updateSleepListEdited" />
|
||||
<HealthSleepListComponent
|
||||
:userHealthSleep="userHealthSleep"
|
||||
@deletedSleep="updateSleepListDeleted"
|
||||
@editedSleep="updateSleepListEdited"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<!-- pagination area -->
|
||||
<PaginationComponent :totalPages="totalPages" :pageNumber="pageNumber" @pageNumberChanged="setPageNumber" />
|
||||
<PaginationComponent
|
||||
:totalPages="totalPages"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChanged="setPageNumber"
|
||||
/>
|
||||
</div>
|
||||
<!-- Displaying a message or component when there are no weight measurements -->
|
||||
<div v-else class="mt-3">
|
||||
@@ -72,6 +103,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import HealthSleepAddEditModalComponent from './HealthSleepZone/HealthSleepAddEditModalComponent.vue'
|
||||
import ModalComponentHoursMinutesInput from '../Modals/ModalComponentHoursMinutesInput.vue'
|
||||
import HealthSleepBarChartComponent from './HealthSleepZone/HealthSleepBarChartComponent.vue'
|
||||
import HealthSleepListComponent from './HealthSleepZone/HealthSleepListComponent.vue'
|
||||
@@ -106,11 +138,25 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['deletedSleep', 'editedSleep', 'pageNumberChanged', 'setSleepTarget'])
|
||||
const emit = defineEmits([
|
||||
'createdSleep',
|
||||
'deletedSleep',
|
||||
'editedSleep',
|
||||
'pageNumberChanged',
|
||||
'setSleepTarget'
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoadingNewSleep = ref(false)
|
||||
|
||||
function updateIsLoadingNewSleep(isLoadingNewSleepNewValue) {
|
||||
isLoadingNewSleep.value = isLoadingNewSleepNewValue
|
||||
}
|
||||
|
||||
function updateSleepListAdded(createdSleep) {
|
||||
emit('createdSleep', createdSleep)
|
||||
}
|
||||
|
||||
function updateSleepListDeleted(deletedSleep) {
|
||||
emit('deletedSleep', deletedSleep)
|
||||
}
|
||||
@@ -126,4 +172,4 @@ function setPageNumber(page) {
|
||||
function submitSetSleepTarget(sleepTarget) {
|
||||
emit('setSleepTarget', sleepTarget)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,691 @@
|
||||
<template>
|
||||
<!-- Modal add/edit sleep -->
|
||||
<div class="modal fade" :id="action === 'add' ? 'addSleepModal' : action === 'edit' ? editSleepId : ''"
|
||||
tabindex="-1" :aria-labelledby="action === 'add' ? 'addSleepModal' : action === 'edit' ? editSleepId : ''"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" id="addSleepModal" v-if="action === 'add'">
|
||||
{{ $t('healthSleepAddEditModalComponent.addSleepModalTitle') }}
|
||||
</h1>
|
||||
<h1 class="modal-title fs-5" :id="editSleepId" v-else>
|
||||
{{ $t('healthSleepAddEditModalComponent.editSleepModalTitle') }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="modal-body">
|
||||
<!-- Date field -->
|
||||
<div class="mb-3">
|
||||
<label for="sleepDate" class="form-label">
|
||||
<b>* {{ $t('healthSleepAddEditModalComponent.dateLabel') }}</b>
|
||||
</label>
|
||||
<input id="sleepDate" class="form-control" type="date" v-model="formData.date" required />
|
||||
</div>
|
||||
|
||||
<!-- Sleep times section -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label for="sleepStartTime" class="form-label">
|
||||
<b>* {{ $t('healthSleepAddEditModalComponent.sleepStartTimeLabel') }}</b>
|
||||
</label>
|
||||
<input id="sleepStartTime" class="form-control" type="datetime-local"
|
||||
v-model="formData.sleepStartTime" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="sleepEndTime" class="form-label">
|
||||
<b>* {{ $t('healthSleepAddEditModalComponent.sleepEndTimeLabel') }}</b>
|
||||
</label>
|
||||
<input id="sleepEndTime" class="form-control" type="datetime-local"
|
||||
v-model="formData.sleepEndTime" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep durations section -->
|
||||
<!-- Total Sleep -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.totalSleepLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="totalSleepHours" v-model.number="formData.totalSleepHours"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="totalSleepMinutes" v-model.number="formData.totalSleepMinutes"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Deep Sleep -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.deepSleepLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="deepSleepHours" v-model.number="formData.deepSleepHours"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="deepSleepMinutes" v-model.number="formData.deepSleepMinutes"
|
||||
class="form-control" type="number"
|
||||
:placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.lightSleepLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="lightSleepHours" v-model.number="formData.lightSleepHours"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="lightSleepMinutes" v-model.number="formData.lightSleepMinutes"
|
||||
class="form-control" type="number"
|
||||
:placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- REM and Awake Sleep -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.remSleepLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="remSleepHours" v-model.number="formData.remSleepHours"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="remSleepMinutes" v-model.number="formData.remSleepMinutes"
|
||||
class="form-control" type="number"
|
||||
:placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.awakeSleepLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="awakeSleepHours" v-model.number="formData.awakeSleepHours"
|
||||
class="form-control" type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="awakeSleepMinutes" v-model.number="formData.awakeSleepMinutes"
|
||||
class="form-control" type="number"
|
||||
:placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nap Time -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.napTimeLabel') }}</b>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input id="napTimeHours" v-model.number="formData.napTimeHours" class="form-control"
|
||||
type="number" :placeholder="$t('generalItems.labelHours')"
|
||||
:aria-label="$t('generalItems.labelHours')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelHoursShort') }}</span>
|
||||
<input id="napTimeMinutes" v-model.number="formData.napTimeMinutes" class="form-control"
|
||||
type="number" :placeholder="$t('generalItems.labelMinutes')"
|
||||
:aria-label="$t('generalItems.labelMinutes')" />
|
||||
<span class="input-group-text">{{ $t('generalItems.labelMinutesShort') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heart rate section -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="avgHeartRate" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.avgHeartRateLabel') }}</b>
|
||||
</label>
|
||||
<input id="avgHeartRate"
|
||||
:placeholder="$t('healthSleepAddEditModalComponent.avgHeartRateLabel')"
|
||||
class="form-control" type="number" step="0.01"
|
||||
v-model.number="formData.avgHeartRate" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="minHeartRate" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.minHeartRateLabel') }}</b>
|
||||
</label>
|
||||
<input id="minHeartRate"
|
||||
:placeholder="$t('healthSleepAddEditModalComponent.minHeartRateLabel')"
|
||||
class="form-control" type="number" v-model.number="formData.minHeartRate" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="maxHeartRate" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.maxHeartRateLabel') }}</b>
|
||||
</label>
|
||||
<input id="maxHeartRate"
|
||||
:placeholder="$t('healthSleepAddEditModalComponent.maxHeartRateLabel')"
|
||||
class="form-control" type="number" v-model.number="formData.maxHeartRate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SpO2 section -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="avgSpo2" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.avgSpo2Label') }}</b>
|
||||
</label>
|
||||
<input id="avgSpo2" :placeholder="$t('healthSleepAddEditModalComponent.avgSpo2Label')"
|
||||
class="form-control" type="number" step="0.01" v-model.number="formData.avgSpo2" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="lowestSpo2" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.lowestSpo2Label') }}</b>
|
||||
</label>
|
||||
<input id="lowestSpo2"
|
||||
:placeholder="$t('healthSleepAddEditModalComponent.lowestSpo2Label')"
|
||||
class="form-control" type="number" v-model.number="formData.lowestSpo2" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="highestSpo2" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.highestSpo2Label') }}</b>
|
||||
</label>
|
||||
<input id="highestSpo2"
|
||||
:placeholder="$t('healthSleepAddEditModalComponent.highestSpo2Label')"
|
||||
class="form-control" type="number" v-model.number="formData.highestSpo2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep scores section -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label for="sleepScoreOverall" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.sleepScoreOverallLabel') }}</b>
|
||||
</label>
|
||||
<input id="sleepScoreOverall" class="form-control" type="number"
|
||||
v-model.number="formData.sleepScoreOverall" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="awakeCount" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.awakeCountLabel') }}</b>
|
||||
</label>
|
||||
<input id="awakeCount" class="form-control" type="number"
|
||||
v-model.number="formData.awakeCount" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="restlessMomentsCount" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.restlessMomentsCountLabel') }}</b>
|
||||
</label>
|
||||
<input id="restlessMomentsCount" class="form-control" type="number"
|
||||
v-model.number="formData.restlessMomentsCount" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sleep stages section (dynamic) -->
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.sleepStagesLabel') }}</b>
|
||||
</label>
|
||||
<button type="button" class="btn btn-sm btn-primary" @click="addSleepStage"
|
||||
aria-label="Add sleep stage">
|
||||
<i class="bi bi-plus-circle"></i>
|
||||
{{ $t('healthSleepAddEditModalComponent.addStageButton') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(stage, index) in formData.sleepStages" :key="index" class="card mb-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="card-title mb-0">
|
||||
{{ $t('healthSleepAddEditModalComponent.stageLabel') }} {{ index + 1 }}
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
@click="removeSleepStage(index)" :aria-label="`Remove stage ${index + 1}`">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label :for="`stageType${index}`" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.stageTypeLabel') }}</b>
|
||||
</label>
|
||||
<select :id="`stageType${index}`" class="form-select"
|
||||
v-model.number="stage.stageType">
|
||||
<option :value="0">
|
||||
{{ $t('healthSleepAddEditModalComponent.stageTypeDeep') }}
|
||||
</option>
|
||||
<option :value="1">
|
||||
{{ $t('healthSleepAddEditModalComponent.stageTypeLight') }}
|
||||
</option>
|
||||
<option :value="2">
|
||||
{{ $t('healthSleepAddEditModalComponent.stageTypeRem') }}
|
||||
</option>
|
||||
<option :value="3">
|
||||
{{ $t('healthSleepAddEditModalComponent.stageTypeAwake') }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label :for="`stageStartTime${index}`" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.stageStartTimeLabel') }}</b>
|
||||
</label>
|
||||
<input :id="`stageStartTime${index}`" class="form-control"
|
||||
type="datetime-local" v-model="stage.startTimeGmt" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label :for="`stageEndTime${index}`" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.stageEndTimeLabel') }}</b>
|
||||
</label>
|
||||
<input :id="`stageEndTime${index}`" class="form-control"
|
||||
type="datetime-local" v-model="stage.endTimeGmt" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label :for="`stageDuration${index}`" class="form-label">
|
||||
<b>{{ $t('healthSleepAddEditModalComponent.stageDurationLabel') }}</b>
|
||||
</label>
|
||||
<input :id="`stageDuration${index}`" class="form-control" type="number"
|
||||
v-model.number="stage.durationSeconds" placeholder="Seconds" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.sleepStages.length === 0" class="alert alert-info" role="alert">
|
||||
{{ $t('healthSleepAddEditModalComponent.noStagesMessage') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-muted">* {{ $t('generalItems.requiredField') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t('generalItems.buttonClose') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-if="action === 'add'">
|
||||
{{ $t('healthSleepAddEditModalComponent.addSleepModalTitle') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-else>
|
||||
{{ $t('healthSleepAddEditModalComponent.editSleepModalTitle') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { push } from 'notivue'
|
||||
import { health_sleep } from '@/services/health_sleepService'
|
||||
import { returnHoursMinutesFromSeconds, returnSecondsFromHoursMinutes } from '@/utils/dateTimeUtils'
|
||||
|
||||
interface SleepStage {
|
||||
stageType: number | null
|
||||
startTimeGmt: string | null
|
||||
endTimeGmt: string | null
|
||||
durationSeconds: number | null
|
||||
}
|
||||
|
||||
interface SleepFormData {
|
||||
date: string
|
||||
sleepStartTime: string
|
||||
sleepEndTime: string
|
||||
totalSleepHours: number | null
|
||||
totalSleepMinutes: number | null
|
||||
napTimeHours: number | null
|
||||
napTimeMinutes: number | null
|
||||
deepSleepHours: number | null
|
||||
deepSleepMinutes: number | null
|
||||
lightSleepHours: number | null
|
||||
lightSleepMinutes: number | null
|
||||
remSleepHours: number | null
|
||||
remSleepMinutes: number | null
|
||||
awakeSleepHours: number | null
|
||||
awakeSleepMinutes: number | null
|
||||
avgHeartRate: number | null
|
||||
minHeartRate: number | null
|
||||
maxHeartRate: number | null
|
||||
avgSpo2: number | null
|
||||
lowestSpo2: number | null
|
||||
highestSpo2: number | null
|
||||
sleepScoreOverall: number | null
|
||||
awakeCount: number | null
|
||||
restlessMomentsCount: number | null
|
||||
sleepStages: SleepStage[]
|
||||
}
|
||||
|
||||
interface UserHealthSleep {
|
||||
id: number
|
||||
user_id: number
|
||||
date: string
|
||||
sleep_start_time_gmt: string
|
||||
sleep_end_time_gmt: string
|
||||
sleep_start_time_local?: string
|
||||
sleep_end_time_local?: string
|
||||
total_sleep_seconds?: number
|
||||
nap_time_seconds?: number
|
||||
deep_sleep_seconds?: number
|
||||
light_sleep_seconds?: number
|
||||
rem_sleep_seconds?: number
|
||||
awake_sleep_seconds?: number
|
||||
avg_heart_rate?: number
|
||||
min_heart_rate?: number
|
||||
max_heart_rate?: number
|
||||
avg_spo2?: number
|
||||
lowest_spo2?: number
|
||||
highest_spo2?: number
|
||||
sleep_score_overall?: number
|
||||
awake_count?: number
|
||||
restless_moments_count?: number
|
||||
sleep_stages?: Array<{
|
||||
stage_type: number | null
|
||||
start_time_gmt: string | null
|
||||
end_time_gmt: string | null
|
||||
duration_seconds: number | null
|
||||
}>
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
action: 'add' | 'edit'
|
||||
userHealthSleep?: UserHealthSleep
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
isLoadingNewSleep: [value: boolean]
|
||||
createdSleep: [sleep: UserHealthSleep]
|
||||
editedSleep: [sleep: UserHealthSleep]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const formData = ref<SleepFormData>({
|
||||
date: new Date().toISOString().split('T')[0] as string,
|
||||
sleepStartTime: '',
|
||||
sleepEndTime: '',
|
||||
totalSleepHours: null,
|
||||
totalSleepMinutes: null,
|
||||
napTimeHours: null,
|
||||
napTimeMinutes: null,
|
||||
deepSleepHours: null,
|
||||
deepSleepMinutes: null,
|
||||
lightSleepHours: null,
|
||||
lightSleepMinutes: null,
|
||||
remSleepHours: null,
|
||||
remSleepMinutes: null,
|
||||
awakeSleepHours: null,
|
||||
awakeSleepMinutes: null,
|
||||
avgHeartRate: null,
|
||||
minHeartRate: null,
|
||||
maxHeartRate: null,
|
||||
avgSpo2: null,
|
||||
lowestSpo2: null,
|
||||
highestSpo2: null,
|
||||
sleepScoreOverall: null,
|
||||
awakeCount: null,
|
||||
restlessMomentsCount: null,
|
||||
sleepStages: []
|
||||
})
|
||||
|
||||
const editSleepId = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
if (props.userHealthSleep) {
|
||||
const totalSleep = returnHoursMinutesFromSeconds(props.userHealthSleep.total_sleep_seconds ?? 0)
|
||||
const napTime = returnHoursMinutesFromSeconds(props.userHealthSleep.nap_time_seconds ?? 0)
|
||||
const deepSleep = returnHoursMinutesFromSeconds(props.userHealthSleep.deep_sleep_seconds ?? 0)
|
||||
const lightSleep = returnHoursMinutesFromSeconds(props.userHealthSleep.light_sleep_seconds ?? 0)
|
||||
const remSleep = returnHoursMinutesFromSeconds(props.userHealthSleep.rem_sleep_seconds ?? 0)
|
||||
const awakeSleep = returnHoursMinutesFromSeconds(props.userHealthSleep.awake_sleep_seconds ?? 0)
|
||||
|
||||
formData.value = {
|
||||
date: props.userHealthSleep.date,
|
||||
sleepStartTime: formatDateTimeForInput(props.userHealthSleep.sleep_start_time_gmt),
|
||||
sleepEndTime: formatDateTimeForInput(props.userHealthSleep.sleep_end_time_gmt),
|
||||
totalSleepHours: totalSleep.hours,
|
||||
totalSleepMinutes: totalSleep.minutes,
|
||||
napTimeHours: napTime.hours,
|
||||
napTimeMinutes: napTime.minutes,
|
||||
deepSleepHours: deepSleep.hours,
|
||||
deepSleepMinutes: deepSleep.minutes,
|
||||
lightSleepHours: lightSleep.hours,
|
||||
lightSleepMinutes: lightSleep.minutes,
|
||||
remSleepHours: remSleep.hours,
|
||||
remSleepMinutes: remSleep.minutes,
|
||||
awakeSleepHours: awakeSleep.hours,
|
||||
awakeSleepMinutes: awakeSleep.minutes,
|
||||
avgHeartRate: props.userHealthSleep.avg_heart_rate ?? null,
|
||||
minHeartRate: props.userHealthSleep.min_heart_rate ?? null,
|
||||
maxHeartRate: props.userHealthSleep.max_heart_rate ?? null,
|
||||
avgSpo2: props.userHealthSleep.avg_spo2 ?? null,
|
||||
lowestSpo2: props.userHealthSleep.lowest_spo2 ?? null,
|
||||
highestSpo2: props.userHealthSleep.highest_spo2 ?? null,
|
||||
sleepScoreOverall: props.userHealthSleep.sleep_score_overall ?? null,
|
||||
awakeCount: props.userHealthSleep.awake_count ?? null,
|
||||
restlessMomentsCount: props.userHealthSleep.restless_moments_count ?? null,
|
||||
sleepStages:
|
||||
props.userHealthSleep.sleep_stages?.map((stage) => ({
|
||||
stageType: stage.stage_type,
|
||||
startTimeGmt: stage.start_time_gmt ? formatDateTimeForInput(stage.start_time_gmt) : null,
|
||||
endTimeGmt: stage.end_time_gmt ? formatDateTimeForInput(stage.end_time_gmt) : null,
|
||||
durationSeconds: stage.duration_seconds
|
||||
})) ?? []
|
||||
}
|
||||
editSleepId.value = `editSleepId${props.userHealthSleep.id}`
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Formats ISO datetime string for datetime-local input.
|
||||
*
|
||||
* @param isoString - ISO 8601 datetime string.
|
||||
* @returns Formatted string for datetime-local input (YYYY-MM-DDTHH:mm).
|
||||
*/
|
||||
function formatDateTimeForInput(isoString: string): string {
|
||||
return isoString.slice(0, 16)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty sleep stage to the form.
|
||||
*/
|
||||
function addSleepStage(): void {
|
||||
formData.value.sleepStages.push({
|
||||
stageType: 1,
|
||||
startTimeGmt: null,
|
||||
endTimeGmt: null,
|
||||
durationSeconds: null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a sleep stage at the specified index.
|
||||
*
|
||||
* @param index - Index of the sleep stage to remove.
|
||||
*/
|
||||
function removeSleepStage(index: number): void {
|
||||
formData.value.sleepStages.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form to add a new sleep entry.
|
||||
*
|
||||
* @throws Error if API request fails.
|
||||
*/
|
||||
async function submitAddSleep(): Promise<void> {
|
||||
emit('isLoadingNewSleep', true)
|
||||
try {
|
||||
const data = {
|
||||
date: formData.value.date,
|
||||
sleep_start_time_gmt: formData.value.sleepStartTime,
|
||||
sleep_end_time_gmt: formData.value.sleepEndTime,
|
||||
total_sleep_seconds:
|
||||
formData.value.totalSleepHours !== null && formData.value.totalSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.totalSleepHours,
|
||||
formData.value.totalSleepMinutes
|
||||
)
|
||||
: null,
|
||||
nap_time_seconds:
|
||||
formData.value.napTimeHours !== null && formData.value.napTimeMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.napTimeHours,
|
||||
formData.value.napTimeMinutes
|
||||
)
|
||||
: null,
|
||||
deep_sleep_seconds:
|
||||
formData.value.deepSleepHours !== null && formData.value.deepSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.deepSleepHours,
|
||||
formData.value.deepSleepMinutes
|
||||
)
|
||||
: null,
|
||||
light_sleep_seconds:
|
||||
formData.value.lightSleepHours !== null && formData.value.lightSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.lightSleepHours,
|
||||
formData.value.lightSleepMinutes
|
||||
)
|
||||
: null,
|
||||
rem_sleep_seconds:
|
||||
formData.value.remSleepHours !== null && formData.value.remSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.remSleepHours,
|
||||
formData.value.remSleepMinutes
|
||||
)
|
||||
: null,
|
||||
awake_sleep_seconds:
|
||||
formData.value.awakeSleepHours !== null && formData.value.awakeSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.awakeSleepHours,
|
||||
formData.value.awakeSleepMinutes
|
||||
)
|
||||
: null,
|
||||
avg_heart_rate: formData.value.avgHeartRate,
|
||||
min_heart_rate: formData.value.minHeartRate,
|
||||
max_heart_rate: formData.value.maxHeartRate,
|
||||
avg_spo2: formData.value.avgSpo2,
|
||||
lowest_spo2: formData.value.lowestSpo2,
|
||||
highest_spo2: formData.value.highestSpo2,
|
||||
sleep_score_overall: formData.value.sleepScoreOverall,
|
||||
awake_count: formData.value.awakeCount,
|
||||
restless_moments_count: formData.value.restlessMomentsCount,
|
||||
sleep_stages: formData.value.sleepStages.map((stage) => ({
|
||||
stage_type: stage.stageType,
|
||||
start_time_gmt: stage.startTimeGmt,
|
||||
end_time_gmt: stage.endTimeGmt,
|
||||
duration_seconds: stage.durationSeconds
|
||||
}))
|
||||
}
|
||||
|
||||
const createdSleep = await health_sleep.createHealthSleep(data)
|
||||
|
||||
emit('isLoadingNewSleep', false)
|
||||
emit('createdSleep', createdSleep)
|
||||
|
||||
push.success(t('healthSleepAddEditModalComponent.successAddSleep'))
|
||||
} catch (error) {
|
||||
emit('isLoadingNewSleep', false)
|
||||
push.error(
|
||||
`${t('healthSleepAddEditModalComponent.errorAddSleep')} - ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submits the form to edit an existing sleep entry.
|
||||
*/
|
||||
function submitEditSleep(): void {
|
||||
if (!props.userHealthSleep) return
|
||||
|
||||
const editedData = {
|
||||
id: props.userHealthSleep.id,
|
||||
user_id: props.userHealthSleep.user_id,
|
||||
date: formData.value.date,
|
||||
sleep_start_time_gmt: formData.value.sleepStartTime,
|
||||
sleep_end_time_gmt: formData.value.sleepEndTime,
|
||||
total_sleep_seconds:
|
||||
formData.value.totalSleepHours !== null && formData.value.totalSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.totalSleepHours,
|
||||
formData.value.totalSleepMinutes
|
||||
)
|
||||
: null,
|
||||
nap_time_seconds:
|
||||
formData.value.napTimeHours !== null && formData.value.napTimeMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(formData.value.napTimeHours, formData.value.napTimeMinutes)
|
||||
: null,
|
||||
deep_sleep_seconds:
|
||||
formData.value.deepSleepHours !== null && formData.value.deepSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.deepSleepHours,
|
||||
formData.value.deepSleepMinutes
|
||||
)
|
||||
: null,
|
||||
light_sleep_seconds:
|
||||
formData.value.lightSleepHours !== null && formData.value.lightSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.lightSleepHours,
|
||||
formData.value.lightSleepMinutes
|
||||
)
|
||||
: null,
|
||||
rem_sleep_seconds:
|
||||
formData.value.remSleepHours !== null && formData.value.remSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.remSleepHours,
|
||||
formData.value.remSleepMinutes
|
||||
)
|
||||
: null,
|
||||
awake_sleep_seconds:
|
||||
formData.value.awakeSleepHours !== null && formData.value.awakeSleepMinutes !== null
|
||||
? returnSecondsFromHoursMinutes(
|
||||
formData.value.awakeSleepHours,
|
||||
formData.value.awakeSleepMinutes
|
||||
)
|
||||
: null,
|
||||
avg_heart_rate: formData.value.avgHeartRate,
|
||||
min_heart_rate: formData.value.minHeartRate,
|
||||
max_heart_rate: formData.value.maxHeartRate,
|
||||
avg_spo2: formData.value.avgSpo2,
|
||||
lowest_spo2: formData.value.lowestSpo2,
|
||||
highest_spo2: formData.value.highestSpo2,
|
||||
sleep_score_overall: formData.value.sleepScoreOverall,
|
||||
awake_count: formData.value.awakeCount,
|
||||
restless_moments_count: formData.value.restlessMomentsCount,
|
||||
sleep_stages: formData.value.sleepStages.map((stage) => ({
|
||||
stage_type: stage.stageType,
|
||||
start_time_gmt: stage.startTimeGmt,
|
||||
end_time_gmt: stage.endTimeGmt,
|
||||
duration_seconds: stage.durationSeconds
|
||||
}))
|
||||
}
|
||||
|
||||
emit('editedSleep', editedData as UserHealthSleep)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form submission for both add and edit actions.
|
||||
*/
|
||||
function handleSubmit(): void {
|
||||
if (props.action === 'add') {
|
||||
submitAddSleep()
|
||||
} else {
|
||||
submitEditSleep()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -73,14 +73,12 @@ const chartData = computed(() => {
|
||||
|
||||
const data = []
|
||||
const labels = []
|
||||
|
||||
|
||||
for (const healthSleep of sortedSleep) {
|
||||
data.push(healthSleep.total_sleep_seconds)
|
||||
|
||||
const createdAt = new Date(healthSleep.date)
|
||||
labels.push(
|
||||
`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`
|
||||
)
|
||||
labels.push(`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`)
|
||||
}
|
||||
|
||||
const datasets = [
|
||||
@@ -143,7 +141,7 @@ onMounted(() => {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
stepSize: 3600, // 1 hour in seconds
|
||||
callback: function(value) {
|
||||
callback: function (value) {
|
||||
return formatDurationHHmm(value)
|
||||
}
|
||||
},
|
||||
@@ -181,7 +179,7 @@ onMounted(() => {
|
||||
if (value === null || value === undefined) {
|
||||
return `${label}: N/A`
|
||||
}
|
||||
|
||||
|
||||
return `${label}: ${formatDuration(value)}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
<span>{{ formatDuration(userHealthSleep.total_sleep_seconds) }}</span>
|
||||
</div>
|
||||
<span>
|
||||
{{ $t('healthSleepListComponent.labelDate') }}: {{ formatDateShort(userHealthSleep.date) }}
|
||||
{{ $t('healthSleepListComponent.labelDate') }}:
|
||||
{{ formatDateShort(userHealthSleep.date) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,6 +25,14 @@
|
||||
<img :src="INTEGRATION_LOGOS.garminConnectApp" alt="Garmin Connect logo" height="22" />
|
||||
</span>
|
||||
|
||||
<!-- edit weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editSleepId${userHealthSleep.id}`"><font-awesome-icon
|
||||
:icon="['fas', 'fa-pen-to-square']" /></a>
|
||||
|
||||
<HealthSleepAddEditModalComponent :action="'edit'" :userHealthSleep="userHealthSleep"
|
||||
@editedSleep="updateSleepListEdited" />
|
||||
|
||||
<!-- delete weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#deleteSleepModal${userHealthSleep.id}`"><font-awesome-icon
|
||||
@@ -51,6 +60,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import HealthSleepAddEditModalComponent from './HealthSleepAddEditModalComponent.vue'
|
||||
import HealthSleepListTabsComponent from './HealthSleepListTabsComponent.vue'
|
||||
import HealthSleepTimelineChartComponent from './HealthSleepTimelineChartComponent.vue'
|
||||
import ModalComponent from '@/components/Modals/ModalComponent.vue'
|
||||
@@ -110,4 +120,4 @@ onMounted(async () => {
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -2,18 +2,30 @@
|
||||
<!-- 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="`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">
|
||||
<button
|
||||
class="nav-link active link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
: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="`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">
|
||||
<button
|
||||
class="nav-link link-body-emphasis link-underline-opacity-0 link-underline-opacity-100-hover"
|
||||
: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>
|
||||
@@ -22,8 +34,12 @@
|
||||
<!-- Tab Content -->
|
||||
<div class="tab-content mt-3">
|
||||
<!-- Score tab -->
|
||||
<div class="tab-pane fade show active" :id="`sleep-score-${userHealthSleep.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sleep-score-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">
|
||||
@@ -36,7 +52,9 @@
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.scoreLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.sleep_score_overall">{{ userHealthSleep.sleep_score_overall }}</span>
|
||||
<span v-if="userHealthSleep.sleep_score_overall">{{
|
||||
userHealthSleep.sleep_score_overall
|
||||
}}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- sleep_score_quality -->
|
||||
@@ -45,7 +63,8 @@
|
||||
{{ $t('healthSleepListTabsComponent.qualityLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.sleep_score_quality">{{
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_quality)) }}</span>
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_quality))
|
||||
}}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -57,15 +76,16 @@
|
||||
{{ $t('healthSleepListTabsComponent.durationLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.sleep_score_duration">{{
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_duration)) }}</span>
|
||||
$t(getScoreStatusI18nKey(userHealthSleep.sleep_score_duration))
|
||||
}}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- hrv_status -->
|
||||
<p class="mb-0">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.HRVLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.hrv_status">{{ $t(getHrvStatusI18nKey(userHealthSleep.hrv_status)) }}</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.HRVLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.hrv_status">{{
|
||||
$t(getHrvStatusI18nKey(userHealthSleep.hrv_status))
|
||||
}}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -81,9 +101,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- deep_sleep_seconds -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.deepLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.deepLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.deep_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.deep_sleep_seconds) }}
|
||||
-
|
||||
@@ -93,9 +111,7 @@
|
||||
</p>
|
||||
<!-- rem_sleep_seconds-->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.REMLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.REMLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.rem_sleep_seconds">
|
||||
{{ formatDuration(userHealthSleep.rem_sleep_seconds) }}
|
||||
-
|
||||
@@ -136,8 +152,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Sleep Details Tab -->
|
||||
<div class="tab-pane fade" :id="`sleep-details-${userHealthSleep.id}`" role="tabpanel"
|
||||
:aria-labelledby="`sleep-details-tab-${userHealthSleep.id}`">
|
||||
<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">
|
||||
@@ -158,7 +178,8 @@
|
||||
{{ $t('healthSleepListTabsComponent.avgSkinTempDeviationLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_skin_temp_deviation">
|
||||
{{ parseFloat(userHealthSleep.avg_skin_temp_deviation) }} {{ $t('generalItems.unitsCelsius') }}
|
||||
{{ parseFloat(userHealthSleep.avg_skin_temp_deviation) }}
|
||||
{{ $t('generalItems.unitsCelsius') }}
|
||||
</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
@@ -186,9 +207,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.avgLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.avg_heart_rate">
|
||||
{{ Number(userHealthSleep.avg_heart_rate) }} {{ $t('generalItems.unitsBpm') }}
|
||||
</span>
|
||||
@@ -196,9 +215,7 @@
|
||||
</p>
|
||||
<!-- max_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.maxLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.maxLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.max_heart_rate">
|
||||
{{ Number(userHealthSleep.max_heart_rate) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
@@ -208,9 +225,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- min_heart_rate -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.minLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.min_heart_rate">
|
||||
{{ Number(userHealthSleep.min_heart_rate) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
@@ -229,19 +244,13 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_spo2 -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthSleep.avg_spo2">
|
||||
{{ Number(userHealthSleep.avg_spo2) }}%
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.avgLabel') }}: </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.maxLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.maxLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.highest_spo2">
|
||||
{{ Number(userHealthSleep.highest_spo2) }}%
|
||||
</span>
|
||||
@@ -251,9 +260,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- lowest_spo2 -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.minLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.lowest_spo2">
|
||||
{{ Number(userHealthSleep.lowest_spo2) }}%
|
||||
</span>
|
||||
@@ -272,9 +279,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- avg_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.avgLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.avgLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.avg_respiration">
|
||||
{{ Number(userHealthSleep.avg_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
@@ -282,9 +287,7 @@
|
||||
</p>
|
||||
<!-- highest_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.maxLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.maxLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.highest_respiration">
|
||||
{{ Number(userHealthSleep.highest_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
@@ -294,9 +297,7 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- lowest_respiration -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthSleepListTabsComponent.minLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthSleepListTabsComponent.minLabel') }}: </span>
|
||||
<span v-if="userHealthSleep.lowest_respiration">
|
||||
{{ Number(userHealthSleep.lowest_respiration) }} {{ $t('generalItems.unitsBrpm') }}
|
||||
</span>
|
||||
@@ -319,5 +320,4 @@ const props = defineProps({
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
||||
sleepStages: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -33,10 +33,10 @@ const yAxisLabels = computed(() => [
|
||||
|
||||
// Sleep stage mapping with vertical positioning
|
||||
const SLEEP_STAGES = {
|
||||
3: { name: 'Awake', color: 'rgba(156, 163, 175, 0.8)', yPos: 3, label: yAxisLabels.value[3] }, // Gray
|
||||
2: { name: 'REM', color: 'rgba(96, 165, 250, 0.8)', yPos: 2, label: yAxisLabels.value[2] }, // Light Blue
|
||||
1: { name: 'Light', color: 'rgba(37, 99, 235, 0.8)', yPos: 1, label: yAxisLabels.value[1] }, // Medium Blue
|
||||
0: { name: 'Deep', color: 'rgba(30, 64, 175, 0.8)', yPos: 0, label: yAxisLabels.value[0] } // Dark Blue
|
||||
3: { name: 'Awake', color: 'rgba(156, 163, 175, 0.8)', yPos: 3, label: yAxisLabels.value[3] }, // Gray
|
||||
2: { name: 'REM', color: 'rgba(96, 165, 250, 0.8)', yPos: 2, label: yAxisLabels.value[2] }, // Light Blue
|
||||
1: { name: 'Light', color: 'rgba(37, 99, 235, 0.8)', yPos: 1, label: yAxisLabels.value[1] }, // Medium Blue
|
||||
0: { name: 'Deep', color: 'rgba(30, 64, 175, 0.8)', yPos: 0, label: yAxisLabels.value[0] } // Dark Blue
|
||||
}
|
||||
|
||||
const chartData = computed(() => {
|
||||
@@ -45,12 +45,12 @@ const chartData = computed(() => {
|
||||
}
|
||||
|
||||
// Sort stages by time
|
||||
const sortedStages = [...props.sleepStages].sort((a, b) =>
|
||||
new Date(a.start_time_gmt) - new Date(b.start_time_gmt)
|
||||
const sortedStages = [...props.sleepStages].sort(
|
||||
(a, b) => new Date(a.start_time_gmt) - new Date(b.start_time_gmt)
|
||||
)
|
||||
|
||||
// Create segments with all necessary data
|
||||
const allSegments = sortedStages.map(stage => {
|
||||
const allSegments = sortedStages.map((stage) => {
|
||||
const startTime = new Date(stage.start_time_gmt)
|
||||
const endTime = new Date(stage.end_time_gmt)
|
||||
const stageInfo = SLEEP_STAGES[stage.stage_type]
|
||||
@@ -69,16 +69,18 @@ const chartData = computed(() => {
|
||||
|
||||
// Return single dataset with all segments
|
||||
return {
|
||||
datasets: [{
|
||||
label: t('generalItems.labelSleep'),
|
||||
data: allSegments,
|
||||
backgroundColor: function (context) {
|
||||
return context.raw?.backgroundColor || 'rgba(59, 130, 246, 0.8)'
|
||||
},
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
barThickness: 40
|
||||
}]
|
||||
datasets: [
|
||||
{
|
||||
label: t('generalItems.labelSleep'),
|
||||
data: allSegments,
|
||||
backgroundColor: function (context) {
|
||||
return context.raw?.backgroundColor || 'rgba(59, 130, 246, 0.8)'
|
||||
},
|
||||
borderWidth: 0,
|
||||
borderRadius: 0,
|
||||
barThickness: 40
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,14 +95,18 @@ watch(
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(() => props.sleepStages, () => {
|
||||
if (myChart) {
|
||||
myChart.options.scales.y.ticks.callback = function (value) {
|
||||
return yAxisLabels.value[value] || ''
|
||||
watch(
|
||||
() => props.sleepStages,
|
||||
() => {
|
||||
if (myChart) {
|
||||
myChart.options.scales.y.ticks.callback = function (value) {
|
||||
return yAxisLabels.value[value] || ''
|
||||
}
|
||||
myChart.update()
|
||||
}
|
||||
myChart.update()
|
||||
}
|
||||
}, { deep: true })
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// Watch for language changes to update y-axis labels and legend
|
||||
watch(yAxisLabels, () => {
|
||||
@@ -162,7 +168,7 @@ onMounted(() => {
|
||||
drawBorder: true,
|
||||
color: 'rgba(200, 200, 200, 0.3)'
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
|
||||
@@ -4,31 +4,55 @@
|
||||
<div v-else>
|
||||
<!-- add steps button -->
|
||||
<div class="d-flex">
|
||||
<a class="w-100 btn btn-primary shadow-sm me-1" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addStepsModal">{{ $t('healthStepsZoneComponent.buttonAddSteps') }}</a>
|
||||
<a class="w-100 btn btn-primary shadow-sm ms-1" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addStepsTargetModal">{{ $t('healthStepsZoneComponent.buttonStepsTarget') }}</a>
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm me-1"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addStepsModal"
|
||||
>{{ $t('healthStepsZoneComponent.buttonAddSteps') }}</a
|
||||
>
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm ms-1"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addStepsTargetModal"
|
||||
>{{ $t('healthStepsZoneComponent.buttonStepsTarget') }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<HealthStepsAddEditModalComponent :action="'add'" @isLoadingNewSteps="updateIsLoadingNewSteps"
|
||||
@createdSteps="updateStepsListAdded" />
|
||||
<HealthStepsAddEditModalComponent
|
||||
:action="'add'"
|
||||
@isLoadingNewSteps="updateIsLoadingNewSteps"
|
||||
@createdSteps="updateStepsListAdded"
|
||||
/>
|
||||
|
||||
<ModalComponentNumberInput modalId="addStepsTargetModal" :title="t('healthStepsZoneComponent.buttonStepsTarget')"
|
||||
:numberFieldLabel="t('healthStepsZoneComponent.modalStepsTargetLabel')" actionButtonType="success"
|
||||
<ModalComponentNumberInput
|
||||
modalId="addStepsTargetModal"
|
||||
:title="t('healthStepsZoneComponent.buttonStepsTarget')"
|
||||
:numberFieldLabel="t('healthStepsZoneComponent.modalStepsTargetLabel')"
|
||||
actionButtonType="success"
|
||||
:actionButtonText="t('generalItems.buttonSubmit')"
|
||||
:numberDefaultValue="props.userHealthTargets?.steps || parseInt(10000)"
|
||||
@numberToEmitAction="submitSetStepsTarget" />
|
||||
@numberToEmitAction="submitSetStepsTarget"
|
||||
/>
|
||||
|
||||
<!-- Checking if userHealthSteps is loaded and has length -->
|
||||
<div v-if="userHealthSteps && userHealthSteps.length" class="mt-3 p-3 bg-body-tertiary rounded shadow-sm">
|
||||
<div
|
||||
v-if="userHealthSteps && userHealthSteps.length"
|
||||
class="mt-3 p-3 bg-body-tertiary rounded shadow-sm"
|
||||
>
|
||||
<!-- show graph -->
|
||||
<HealthStepsBarChartComponent :userHealthTargets="userHealthTargets" :userHealthSteps="userHealthSteps"
|
||||
:isLoading="isLoading" />
|
||||
<HealthStepsBarChartComponent
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:userHealthSteps="userHealthSteps"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
{{ $t('healthStepsZoneComponent.labelNumberOfHealthSteps1')
|
||||
}}{{ userHealthSteps.length
|
||||
{{ $t('healthStepsZoneComponent.labelNumberOfHealthSteps1') }}{{ userHealthSteps.length
|
||||
}}{{ $t('healthStepsZoneComponent.labelNumberOfHealthSteps2')
|
||||
}}{{ userHealthStepsPagination.length
|
||||
}}{{ $t('healthStepsZoneComponent.labelNumberOfHealthSteps3') }}
|
||||
@@ -42,14 +66,25 @@
|
||||
</ul>
|
||||
|
||||
<!-- list zone -->
|
||||
<ul class="my-3 list-group list-group-flush" v-for="userHealthStep in userHealthStepsPagination"
|
||||
:key="userHealthStep.id" :userHealthStep="userHealthStep">
|
||||
<HealthStepsListComponent :userHealthStep="userHealthStep" @deletedSteps="updateStepsListDeleted"
|
||||
@editedSteps="updateStepsListEdited" />
|
||||
<ul
|
||||
class="my-3 list-group list-group-flush"
|
||||
v-for="userHealthStep in userHealthStepsPagination"
|
||||
:key="userHealthStep.id"
|
||||
:userHealthStep="userHealthStep"
|
||||
>
|
||||
<HealthStepsListComponent
|
||||
:userHealthStep="userHealthStep"
|
||||
@deletedSteps="updateStepsListDeleted"
|
||||
@editedSteps="updateStepsListEdited"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<!-- pagination area -->
|
||||
<PaginationComponent :totalPages="totalPages" :pageNumber="pageNumber" @pageNumberChanged="setPageNumber" />
|
||||
<PaginationComponent
|
||||
:totalPages="totalPages"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChanged="setPageNumber"
|
||||
/>
|
||||
</div>
|
||||
<!-- Displaying a message or component when there are no weight measurements -->
|
||||
<div v-else class="mt-3">
|
||||
@@ -98,7 +133,13 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['createdSteps', 'deletedSteps', 'editedSteps', 'pageNumberChanged', 'setStepsTarget'])
|
||||
const emit = defineEmits([
|
||||
'createdSteps',
|
||||
'deletedSteps',
|
||||
'editedSteps',
|
||||
'pageNumberChanged',
|
||||
'setStepsTarget'
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoadingNewSteps = ref(false)
|
||||
@@ -126,4 +167,4 @@ function setPageNumber(page) {
|
||||
function submitSetStepsTarget(stepsTarget) {
|
||||
emit('setStepsTarget', stepsTarget)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<!-- Modal add/edit steps -->
|
||||
<div class="modal fade" :id="action == 'add' ? 'addStepsModal' : action == 'edit' ? editStepsId : ''" tabindex="-1"
|
||||
:aria-labelledby="action == 'add' ? 'addStepsModal' : action == 'edit' ? editStepsId : ''" aria-hidden="true">
|
||||
<div
|
||||
class="modal fade"
|
||||
:id="action == 'add' ? 'addStepsModal' : action == 'edit' ? editStepsId : ''"
|
||||
tabindex="-1"
|
||||
:aria-labelledby="action == 'add' ? 'addStepsModal' : action == 'edit' ? editStepsId : ''"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -11,16 +16,38 @@
|
||||
<h1 class="modal-title fs-5" :id="editStepsId" v-else>
|
||||
{{ $t('healthStepsAddEditModalComponent.editStepsModalTitle') }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="modal-body">
|
||||
<!-- steps fields -->
|
||||
<label for="stepsAdd"><b>* {{ $t('healthStepsAddEditModalComponent.addStepsLabel') }}</b></label>
|
||||
<input class="form-control" type="number" step="0.1" name="stepsAdd" v-model="newEditSteps" required />
|
||||
<label for="stepsAdd"
|
||||
><b>* {{ $t('healthStepsAddEditModalComponent.addStepsLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.1"
|
||||
name="stepsAdd"
|
||||
v-model="newEditSteps"
|
||||
required
|
||||
/>
|
||||
<!-- date fields -->
|
||||
<label for="stepsDateAdd"><b>* {{ $t('healthStepsAddEditModalComponent.addStepsDateLabel') }}</b></label>
|
||||
<input class="form-control" type="date" name="stepsDateAdd" v-model="newEditStepsDate" required />
|
||||
<label for="stepsDateAdd"
|
||||
><b>* {{ $t('healthStepsAddEditModalComponent.addStepsDateLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
type="date"
|
||||
name="stepsDateAdd"
|
||||
v-model="newEditStepsDate"
|
||||
required
|
||||
/>
|
||||
|
||||
<p>* {{ $t('generalItems.requiredField') }}</p>
|
||||
</div>
|
||||
@@ -28,7 +55,12 @@
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t('generalItems.buttonClose') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-if="action == 'add'">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
data-bs-dismiss="modal"
|
||||
v-if="action == 'add'"
|
||||
>
|
||||
{{ $t('healthStepsAddEditModalComponent.addStepsModalTitle') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-else>
|
||||
|
||||
@@ -72,14 +72,12 @@ const chartData = computed(() => {
|
||||
|
||||
const data = []
|
||||
const labels = []
|
||||
|
||||
|
||||
for (const healthSteps of sortedSteps) {
|
||||
data.push(healthSteps.steps)
|
||||
|
||||
const createdAt = new Date(healthSteps.date)
|
||||
labels.push(
|
||||
`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`
|
||||
)
|
||||
labels.push(`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`)
|
||||
}
|
||||
|
||||
const datasets = [
|
||||
|
||||
@@ -16,23 +16,39 @@
|
||||
</span>
|
||||
|
||||
<!-- edit weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editStepsId${userHealthStep.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="`#editStepsId${userHealthStep.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-pen-to-square']"
|
||||
/></a>
|
||||
|
||||
<HealthStepsAddEditModalComponent :action="'edit'" :userHealthStep="userHealthStep"
|
||||
@editedSteps="updateStepsListEdited" />
|
||||
<HealthStepsAddEditModalComponent
|
||||
:action="'edit'"
|
||||
:userHealthStep="userHealthStep"
|
||||
@editedSteps="updateStepsListEdited"
|
||||
/>
|
||||
|
||||
<!-- delete weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#deleteStepsModal${userHealthStep.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="`#deleteStepsModal${userHealthStep.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-trash-can']"
|
||||
/></a>
|
||||
|
||||
<ModalComponent :modalId="`deleteStepsModal${userHealthStep.id}`"
|
||||
<ModalComponent
|
||||
:modalId="`deleteStepsModal${userHealthStep.id}`"
|
||||
:title="t('healthStepsListComponent.modalDeleteStepsTitle')"
|
||||
:body="`${t('healthStepsListComponent.modalDeleteStepsBody')}<b>${userHealthStep.date}</b>?`"
|
||||
:actionButtonType="`danger`" :actionButtonText="t('healthStepsListComponent.modalDeleteStepsTitle')"
|
||||
@submitAction="submitDeleteSteps" />
|
||||
:actionButtonType="`danger`"
|
||||
:actionButtonText="t('healthStepsListComponent.modalDeleteStepsTitle')"
|
||||
@submitAction="submitDeleteSteps"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
@@ -4,27 +4,51 @@
|
||||
<div v-else>
|
||||
<!-- add weight button -->
|
||||
<div class="d-flex">
|
||||
<a class="w-100 btn btn-primary shadow-sm me-1" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addWeightModal">{{ $t('healthWeightZoneComponent.buttonAddWeight') }}</a>
|
||||
<a class="w-100 btn btn-primary shadow-sm ms-1" href="#" role="button" data-bs-toggle="modal"
|
||||
data-bs-target="#addWeightTargetModal">{{ $t('healthWeightZoneComponent.buttonWeightTarget') }}</a>
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm me-1"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addWeightModal"
|
||||
>{{ $t('healthWeightZoneComponent.buttonAddWeight') }}</a
|
||||
>
|
||||
<a
|
||||
class="w-100 btn btn-primary shadow-sm ms-1"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#addWeightTargetModal"
|
||||
>{{ $t('healthWeightZoneComponent.buttonWeightTarget') }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<HealthWeightAddEditModalComponent :action="'add'" @isLoadingNewWeight="updateIsLoadingNewWeight"
|
||||
@createdWeight="updateWeightListAdded" />
|
||||
<HealthWeightAddEditModalComponent
|
||||
:action="'add'"
|
||||
@isLoadingNewWeight="updateIsLoadingNewWeight"
|
||||
@createdWeight="updateWeightListAdded"
|
||||
/>
|
||||
|
||||
<ModalComponentNumberInput modalId="addWeightTargetModal"
|
||||
<ModalComponentNumberInput
|
||||
modalId="addWeightTargetModal"
|
||||
:title="t('healthWeightZoneComponent.buttonWeightTarget')"
|
||||
:numberFieldLabel="t('healthWeightZoneComponent.modalWeightTargetLabel')" actionButtonType="success"
|
||||
:numberFieldLabel="t('healthWeightZoneComponent.modalWeightTargetLabel')"
|
||||
actionButtonType="success"
|
||||
:actionButtonText="t('generalItems.buttonSubmit')"
|
||||
:numberDefaultValue="props.userHealthTargets?.weight || parseInt(70)"
|
||||
@numberToEmitAction="submitSetWeightTarget" />
|
||||
@numberToEmitAction="submitSetWeightTarget"
|
||||
/>
|
||||
|
||||
<!-- Checking if userHealthWeight is loaded and has length -->
|
||||
<div v-if="userHealthWeight && userHealthWeight.length" class="mt-3 p-3 bg-body-tertiary rounded shadow-sm">
|
||||
<div
|
||||
v-if="userHealthWeight && userHealthWeight.length"
|
||||
class="mt-3 p-3 bg-body-tertiary rounded shadow-sm"
|
||||
>
|
||||
<!-- show graph -->
|
||||
<HealthWeightLineChartComponent :userHealthTargets="userHealthTargets" :userHealthWeight="userHealthWeight"
|
||||
:isLoading="isLoading" />
|
||||
<HealthWeightLineChartComponent
|
||||
:userHealthTargets="userHealthTargets"
|
||||
:userHealthWeight="userHealthWeight"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
@@ -43,14 +67,25 @@
|
||||
</ul>
|
||||
|
||||
<!-- list zone -->
|
||||
<ul class="my-3 list-group list-group-flush" v-for="userHealthWeight in userHealthWeightPagination"
|
||||
:key="userHealthWeight.id" :userHealthWeight="userHealthWeight">
|
||||
<HealthWeightListComponent :userHealthWeight="userHealthWeight" @deletedWeight="updateWeightListDeleted"
|
||||
@editedWeight="updateWeightListEdited" />
|
||||
<ul
|
||||
class="my-3 list-group list-group-flush"
|
||||
v-for="userHealthWeight in userHealthWeightPagination"
|
||||
:key="userHealthWeight.id"
|
||||
:userHealthWeight="userHealthWeight"
|
||||
>
|
||||
<HealthWeightListComponent
|
||||
:userHealthWeight="userHealthWeight"
|
||||
@deletedWeight="updateWeightListDeleted"
|
||||
@editedWeight="updateWeightListEdited"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<!-- pagination area -->
|
||||
<PaginationComponent :totalPages="totalPages" :pageNumber="pageNumber" @pageNumberChanged="setPageNumber" />
|
||||
<PaginationComponent
|
||||
:totalPages="totalPages"
|
||||
:pageNumber="pageNumber"
|
||||
@pageNumberChanged="setPageNumber"
|
||||
/>
|
||||
</div>
|
||||
<!-- Displaying a message or component when there are no weight measurements -->
|
||||
<div v-else class="mt-3">
|
||||
@@ -99,7 +134,13 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['createdWeight', 'deletedWeight', 'editedWeight', 'pageNumberChanged', 'setWeightTarget'])
|
||||
const emit = defineEmits([
|
||||
'createdWeight',
|
||||
'deletedWeight',
|
||||
'editedWeight',
|
||||
'pageNumberChanged',
|
||||
'setWeightTarget'
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
const isLoadingNewWeight = ref(false)
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<!-- Modal add/edit weight -->
|
||||
<div class="modal fade" :id="action == 'add' ? 'addWeightModal' : action == 'edit' ? editWeightId : ''" tabindex="-1"
|
||||
:aria-labelledby="action == 'add' ? 'addWeightModal' : action == 'edit' ? editWeightId : ''" aria-hidden="true">
|
||||
<div
|
||||
class="modal fade"
|
||||
:id="action == 'add' ? 'addWeightModal' : action == 'edit' ? editWeightId : ''"
|
||||
tabindex="-1"
|
||||
:aria-labelledby="action == 'add' ? 'addWeightModal' : action == 'edit' ? editWeightId : ''"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -11,78 +16,138 @@
|
||||
<h1 class="modal-title fs-5" :id="editWeightId" v-else>
|
||||
{{ $t('healthWeightAddEditModalComponent.editWeightModalTitle') }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="modal-body">
|
||||
<!-- weight fields -->
|
||||
<label for="weightWeightAddEdit"><b>* {{ $t('healthWeightAddEditModalComponent.addWeightWeightLabel')
|
||||
}}</b></label>
|
||||
<label for="weightWeightAddEdit"
|
||||
><b>* {{ $t('healthWeightAddEditModalComponent.addWeightWeightLabel') }}</b></label
|
||||
>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.01" name="weightWeightAddEdit"
|
||||
v-model="newEditWeightWeight" required />
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{ $t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightWeightAddEdit"
|
||||
v-model="newEditWeightWeight"
|
||||
required
|
||||
/>
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{
|
||||
$t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<span class="input-group-text" v-else>{{ $t('generalItems.unitsLbs') }}</span>
|
||||
</div>
|
||||
<!-- date fields -->
|
||||
<label for="weightDateAddEdit"><b>* {{ $t('healthWeightAddEditModalComponent.addWeightDateLabel')
|
||||
}}</b></label>
|
||||
<input class="form-control" type="date" name="weightDateAddEdit" v-model="newEditWeightDate" required />
|
||||
|
||||
<label for="weightDateAddEdit"
|
||||
><b>* {{ $t('healthWeightAddEditModalComponent.addWeightDateLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
type="date"
|
||||
name="weightDateAddEdit"
|
||||
v-model="newEditWeightDate"
|
||||
required
|
||||
/>
|
||||
|
||||
<div class="d-flex justify-content-start align-items-center">
|
||||
<span>{{ $t('healthWeightAddEditModalComponent.addWeightHiddenItemsLabel') }}</span>
|
||||
<!-- button toggle hidden fields -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" data-bs-toggle="collapse"
|
||||
:href="`#collapseAddEditWeightDetailsFields`" role="button" aria-expanded="false"
|
||||
:aria-controls="`collapseAddEditWeightDetailsFields`">
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
data-bs-toggle="collapse"
|
||||
:href="`#collapseAddEditWeightDetailsFields`"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
:aria-controls="`collapseAddEditWeightDetailsFields`"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'caret-down']" v-if="!detailFields" />
|
||||
<font-awesome-icon :icon="['fas', 'caret-up']" v-else />
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse" id="collapseAddEditWeightDetailsFields">
|
||||
<!-- bmi fields -->
|
||||
<label for="weightBMIAddEdit"><b>* {{ $t('healthWeightAddEditModalComponent.addWeightBMILabel')
|
||||
}}</b></label>
|
||||
<input class="form-control" type="number" step="0.01" name="weightBMIAddEdit"
|
||||
v-model="newEditWeightBMI" />
|
||||
<label for="weightBMIAddEdit"
|
||||
><b>* {{ $t('healthWeightAddEditModalComponent.addWeightBMILabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightBMIAddEdit"
|
||||
v-model="newEditWeightBMI"
|
||||
/>
|
||||
<!-- body fat fields -->
|
||||
<label for="weightBodyFatAddEdit"><b>* {{ $t('healthWeightAddEditModalComponent.addWeightBodyFatLabel')
|
||||
}}</b></label>
|
||||
<label for="weightBodyFatAddEdit"
|
||||
><b>* {{ $t('healthWeightAddEditModalComponent.addWeightBodyFatLabel') }}</b></label
|
||||
>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.01" name="weightBodyFatAddEdit"
|
||||
v-model="newEditWeightBodyFat" />
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightBodyFatAddEdit"
|
||||
v-model="newEditWeightBodyFat"
|
||||
/>
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<!-- body water fields -->
|
||||
<label for="weightBodyWaterAddEdit"><b>* {{
|
||||
$t('healthWeightAddEditModalComponent.addWeightBodyWaterLabel')
|
||||
}}</b></label>
|
||||
<label for="weightBodyWaterAddEdit"
|
||||
><b
|
||||
>* {{ $t('healthWeightAddEditModalComponent.addWeightBodyWaterLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.01" name="weightBodyWaterAddEdit"
|
||||
v-model="newEditWeightBodyWater" />
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightBodyWaterAddEdit"
|
||||
v-model="newEditWeightBodyWater"
|
||||
/>
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<!-- bone mass fields -->
|
||||
<label for="weightBoneMassAddEdit"><b>* {{ $t('healthWeightAddEditModalComponent.addWeightBoneMassLabel')
|
||||
}}</b></label>
|
||||
<label for="weightBoneMassAddEdit"
|
||||
><b
|
||||
>* {{ $t('healthWeightAddEditModalComponent.addWeightBoneMassLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.01" name="weightBoneMassAddEdit"
|
||||
v-model="newEditWeightBoneMass" />
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{ $t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightBoneMassAddEdit"
|
||||
v-model="newEditWeightBoneMass"
|
||||
/>
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{
|
||||
$t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<span class="input-group-text" v-else>{{ $t('generalItems.unitsLbs') }}</span>
|
||||
</div>
|
||||
<!-- muscle mass fields -->
|
||||
<label for="weightMuscleMassAddEdit"><b>* {{
|
||||
$t('healthWeightAddEditModalComponent.addWeightMuscleMassLabel')
|
||||
}}</b></label>
|
||||
<label for="weightMuscleMassAddEdit"
|
||||
><b
|
||||
>* {{ $t('healthWeightAddEditModalComponent.addWeightMuscleMassLabel') }}</b
|
||||
></label
|
||||
>
|
||||
<div class="input-group">
|
||||
<input class="form-control" type="number" step="0.01" name="weightMuscleMassAddEdit"
|
||||
v-model="newEditWeightMuscleMass" />
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{ $t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
name="weightMuscleMassAddEdit"
|
||||
v-model="newEditWeightMuscleMass"
|
||||
/>
|
||||
<span class="input-group-text" v-if="Number(authStore?.user?.units) === 1">{{
|
||||
$t('generalItems.unitsKg')
|
||||
}}</span>
|
||||
<span class="input-group-text" v-else>{{ $t('generalItems.unitsLbs') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,7 +158,12 @@
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
{{ $t('generalItems.buttonClose') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-if="action == 'add'">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
data-bs-dismiss="modal"
|
||||
v-if="action == 'add'"
|
||||
>
|
||||
{{ $t('healthWeightAddEditModalComponent.addWeightModalTitle') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" data-bs-dismiss="modal" v-else>
|
||||
@@ -145,7 +215,9 @@ const detailFields = ref(false)
|
||||
|
||||
if (props.userHealthWeight) {
|
||||
newEditWeightWeight.value =
|
||||
Number(authStore?.user?.units) === 1 ? props.userHealthWeight.weight : kgToLbs(props.userHealthWeight.weight)
|
||||
Number(authStore?.user?.units) === 1
|
||||
? props.userHealthWeight.weight
|
||||
: kgToLbs(props.userHealthWeight.weight)
|
||||
newEditWeightDate.value = props.userHealthWeight.date
|
||||
newEditWeightBMI.value = props.userHealthWeight.bmi
|
||||
newEditWeightBodyFat.value = props.userHealthWeight.body_fat
|
||||
@@ -167,13 +239,13 @@ async function submitAddWeight() {
|
||||
try {
|
||||
let bone_mass = null
|
||||
let muscle_mass = null
|
||||
if (newEditWeightBoneMass.value !== null && newEditWeightBoneMass.value !== "") {
|
||||
if (newEditWeightBoneMass.value !== null && newEditWeightBoneMass.value !== '') {
|
||||
bone_mass =
|
||||
Number(authStore?.user?.units) === 1
|
||||
? newEditWeightBoneMass.value
|
||||
: lbsToKg(newEditWeightBoneMass.value)
|
||||
}
|
||||
if (newEditWeightMuscleMass.value !== null && newEditWeightMuscleMass.value !== "") {
|
||||
if (newEditWeightMuscleMass.value !== null && newEditWeightMuscleMass.value !== '') {
|
||||
muscle_mass =
|
||||
Number(authStore?.user?.units) === 1
|
||||
? newEditWeightMuscleMass.value
|
||||
@@ -186,11 +258,20 @@ async function submitAddWeight() {
|
||||
? newEditWeightWeight.value
|
||||
: lbsToKg(newEditWeightWeight.value),
|
||||
date: newEditWeightDate.value,
|
||||
bmi: newEditWeightBMI.value !== null && newEditWeightBMI.value !== "" ? newEditWeightBMI.value : null,
|
||||
body_fat: newEditWeightBodyFat.value !== null && newEditWeightBodyFat.value !== "" ? newEditWeightBodyFat.value : null,
|
||||
body_water: newEditWeightBodyWater.value !== null && newEditWeightBodyWater.value !== "" ? newEditWeightBodyWater.value : null,
|
||||
bmi:
|
||||
newEditWeightBMI.value !== null && newEditWeightBMI.value !== ''
|
||||
? newEditWeightBMI.value
|
||||
: null,
|
||||
body_fat:
|
||||
newEditWeightBodyFat.value !== null && newEditWeightBodyFat.value !== ''
|
||||
? newEditWeightBodyFat.value
|
||||
: null,
|
||||
body_water:
|
||||
newEditWeightBodyWater.value !== null && newEditWeightBodyWater.value !== ''
|
||||
? newEditWeightBodyWater.value
|
||||
: null,
|
||||
bone_mass: bone_mass,
|
||||
muscle_mass: muscle_mass,
|
||||
muscle_mass: muscle_mass
|
||||
}
|
||||
|
||||
const createdWeight = await health_weight.createHealthWeight(data)
|
||||
@@ -214,13 +295,13 @@ function submitEditWeight() {
|
||||
console.log(newEditWeightBodyFat.value)
|
||||
let bone_mass = null
|
||||
let muscle_mass = null
|
||||
if (newEditWeightBoneMass.value !== null && newEditWeightBoneMass.value !== "") {
|
||||
if (newEditWeightBoneMass.value !== null && newEditWeightBoneMass.value !== '') {
|
||||
bone_mass =
|
||||
Number(authStore?.user?.units) === 1
|
||||
? newEditWeightBoneMass.value
|
||||
: lbsToKg(newEditWeightBoneMass.value)
|
||||
}
|
||||
if (newEditWeightMuscleMass.value !== null && newEditWeightMuscleMass.value !== "") {
|
||||
if (newEditWeightMuscleMass.value !== null && newEditWeightMuscleMass.value !== '') {
|
||||
muscle_mass =
|
||||
Number(authStore?.user?.units) === 1
|
||||
? newEditWeightMuscleMass.value
|
||||
@@ -234,11 +315,20 @@ function submitEditWeight() {
|
||||
? newEditWeightWeight.value
|
||||
: lbsToKg(newEditWeightWeight.value),
|
||||
date: newEditWeightDate.value,
|
||||
bmi: newEditWeightBMI.value !== null && newEditWeightBMI.value !== "" ? newEditWeightBMI.value : null,
|
||||
body_fat: newEditWeightBodyFat.value !== null && newEditWeightBodyFat.value !== "" ? newEditWeightBodyFat.value : null,
|
||||
body_water: newEditWeightBodyWater.value !== null && newEditWeightBodyWater.value !== "" ? newEditWeightBodyWater.value : null,
|
||||
bmi:
|
||||
newEditWeightBMI.value !== null && newEditWeightBMI.value !== ''
|
||||
? newEditWeightBMI.value
|
||||
: null,
|
||||
body_fat:
|
||||
newEditWeightBodyFat.value !== null && newEditWeightBodyFat.value !== ''
|
||||
? newEditWeightBodyFat.value
|
||||
: null,
|
||||
body_water:
|
||||
newEditWeightBodyWater.value !== null && newEditWeightBodyWater.value !== ''
|
||||
? newEditWeightBodyWater.value
|
||||
: null,
|
||||
bone_mass: bone_mass,
|
||||
muscle_mass: muscle_mass,
|
||||
muscle_mass: muscle_mass
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -97,9 +97,7 @@ const chartData = computed(() => {
|
||||
}
|
||||
|
||||
const createdAt = new Date(HealthWeight.date)
|
||||
labels.push(
|
||||
`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`
|
||||
)
|
||||
labels.push(`${createdAt.getDate()}/${createdAt.getMonth() + 1}/${createdAt.getFullYear()}`)
|
||||
}
|
||||
|
||||
let label = ''
|
||||
@@ -275,4 +273,4 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
/* Ensures the canvas stretches across the available width */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,51 +4,94 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
<div class="fw-bold">
|
||||
<span v-if="Number(authStore?.user?.units) === 1">{{ userHealthWeight.weight }} {{
|
||||
$t('generalItems.unitsKg') }}</span>
|
||||
<span v-else>{{ kgToLbs(userHealthWeight.weight) }} {{ $t('generalItems.unitsLbs') }}</span>
|
||||
<span v-if="Number(authStore?.user?.units) === 1"
|
||||
>{{ userHealthWeight.weight }} {{ $t('generalItems.unitsKg') }}</span
|
||||
>
|
||||
<span v-else
|
||||
>{{ kgToLbs(userHealthWeight.weight) }} {{ $t('generalItems.unitsLbs') }}</span
|
||||
>
|
||||
</div>
|
||||
<span>
|
||||
{{ $t('healthWeightListComponent.dateLabel') }}: {{ formatDateShort(userHealthWeight.date) }}
|
||||
{{ $t('healthWeightListComponent.dateLabel') }}:
|
||||
{{ formatDateShort(userHealthWeight.date) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- button toggle sleep details -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" data-bs-toggle="collapse"
|
||||
:href="`#collapseWeightDetails${userHealthWeight.id}`" role="button" aria-expanded="false"
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
data-bs-toggle="collapse"
|
||||
:href="`#collapseWeightDetails${userHealthWeight.id}`"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
:aria-controls="`collapseWeightDetails${userHealthWeight.id}`"
|
||||
v-if="userHealthWeight.bmi || userHealthWeight.body_fat || userHealthWeight.body_water || userHealthWeight.bone_mass || userHealthWeight.muscle_mass">
|
||||
v-if="
|
||||
userHealthWeight.bmi ||
|
||||
userHealthWeight.body_fat ||
|
||||
userHealthWeight.body_water ||
|
||||
userHealthWeight.bone_mass ||
|
||||
userHealthWeight.muscle_mass
|
||||
"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'caret-down']" v-if="!weightDetails" />
|
||||
<font-awesome-icon :icon="['fas', 'caret-up']" v-else />
|
||||
</a>
|
||||
<!-- source logo -->
|
||||
<span class="align-middle me-3 d-none d-sm-inline" v-if="userHealthWeight.source === 'garmin'">
|
||||
<span
|
||||
class="align-middle me-3 d-none d-sm-inline"
|
||||
v-if="userHealthWeight.source === 'garmin'"
|
||||
>
|
||||
<img :src="INTEGRATION_LOGOS.garminConnectApp" alt="Garmin Connect logo" height="22" />
|
||||
</span>
|
||||
|
||||
<!-- edit weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editWeightId${userHealthWeight.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="`#editWeightId${userHealthWeight.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-pen-to-square']"
|
||||
/></a>
|
||||
|
||||
<HealthWeightAddEditModalComponent :action="'edit'" :userHealthWeight="userHealthWeight"
|
||||
@editedWeight="updateWeightListEdited" />
|
||||
<HealthWeightAddEditModalComponent
|
||||
:action="'edit'"
|
||||
:userHealthWeight="userHealthWeight"
|
||||
@editedWeight="updateWeightListEdited"
|
||||
/>
|
||||
|
||||
<!-- delete weight button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#deleteWeightModal${userHealthWeight.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="`#deleteWeightModal${userHealthWeight.id}`"
|
||||
><font-awesome-icon :icon="['fas', 'fa-trash-can']"
|
||||
/></a>
|
||||
|
||||
<ModalComponent :modalId="`deleteWeightModal${userHealthWeight.id}`"
|
||||
<ModalComponent
|
||||
:modalId="`deleteWeightModal${userHealthWeight.id}`"
|
||||
:title="t('healthWeightListComponent.modalDeleteWeightTitle')"
|
||||
:body="`${t('healthWeightListComponent.modalDeleteWeightBody')}<b>${userHealthWeight.date}</b>?`"
|
||||
:actionButtonType="`danger`" :actionButtonText="t('healthWeightListComponent.modalDeleteWeightTitle')"
|
||||
@submitAction="submitDeleteWeight" />
|
||||
:actionButtonType="`danger`"
|
||||
:actionButtonText="t('healthWeightListComponent.modalDeleteWeightTitle')"
|
||||
@submitAction="submitDeleteWeight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse" :id="`collapseWeightDetails${userHealthWeight.id}`"
|
||||
v-if="userHealthWeight.bmi || userHealthWeight.body_fat || userHealthWeight.body_water || userHealthWeight.bone_mass || userHealthWeight.muscle_mass">
|
||||
<div
|
||||
class="collapse"
|
||||
:id="`collapseWeightDetails${userHealthWeight.id}`"
|
||||
v-if="
|
||||
userHealthWeight.bmi ||
|
||||
userHealthWeight.body_fat ||
|
||||
userHealthWeight.body_water ||
|
||||
userHealthWeight.bone_mass ||
|
||||
userHealthWeight.muscle_mass
|
||||
"
|
||||
>
|
||||
<!-- Details -->
|
||||
<section class="pb-3 mt-3 mb-3">
|
||||
<h6 class="fw-semibold mb-2">
|
||||
@@ -58,18 +101,16 @@
|
||||
<div class="col-12 col-md-6">
|
||||
<!-- bmi -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthWeightListComponent.bmiLabel') }}:
|
||||
</span>
|
||||
<span class="fw-semibold"> {{ $t('healthWeightListComponent.bmiLabel') }}: </span>
|
||||
<span v-if="userHealthWeight.bmi">{{ userHealthWeight.bmi.toFixed(2) }}</span>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- body_fat -->
|
||||
<p class="mb-1">
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthWeightListComponent.bodyFatLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthWeight.body_fat">{{ userHealthWeight.body_fat.toFixed(2) }}%</span>
|
||||
<span class="fw-semibold"> {{ $t('healthWeightListComponent.bodyFatLabel') }}: </span>
|
||||
<span v-if="userHealthWeight.body_fat"
|
||||
>{{ userHealthWeight.body_fat.toFixed(2) }}%</span
|
||||
>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- body_water -->
|
||||
@@ -77,7 +118,9 @@
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthWeightListComponent.bodyWaterLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthWeight.body_water">{{ userHealthWeight.body_water.toFixed(2) }}%</span>
|
||||
<span v-if="userHealthWeight.body_water"
|
||||
>{{ userHealthWeight.body_water.toFixed(2) }}%</span
|
||||
>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,11 +130,12 @@
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthWeightListComponent.boneMassLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthWeight.bone_mass && Number(authStore?.user?.units) === 1">{{
|
||||
userHealthWeight.bone_mass.toFixed(2) }} {{
|
||||
$t('generalItems.unitsKg') }}</span>
|
||||
<span v-else-if="userHealthWeight.bone_mass && Number(authStore?.user?.units) === 2">{{
|
||||
kgToLbs(userHealthWeight.bone_mass) }} {{ $t('generalItems.unitsLbs') }}</span>
|
||||
<span v-if="userHealthWeight.bone_mass && Number(authStore?.user?.units) === 1"
|
||||
>{{ userHealthWeight.bone_mass.toFixed(2) }} {{ $t('generalItems.unitsKg') }}</span
|
||||
>
|
||||
<span v-else-if="userHealthWeight.bone_mass && Number(authStore?.user?.units) === 2"
|
||||
>{{ kgToLbs(userHealthWeight.bone_mass) }} {{ $t('generalItems.unitsLbs') }}</span
|
||||
>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
<!-- muscle_mass -->
|
||||
@@ -99,11 +143,13 @@
|
||||
<span class="fw-semibold">
|
||||
{{ $t('healthWeightListComponent.muscleMassLabel') }}:
|
||||
</span>
|
||||
<span v-if="userHealthWeight.muscle_mass && Number(authStore?.user?.units) === 1">{{
|
||||
userHealthWeight.muscle_mass.toFixed(2) }} {{
|
||||
$t('generalItems.unitsKg') }}</span>
|
||||
<span v-else-if="userHealthWeight.muscle_mass && Number(authStore?.user?.units) === 2">{{
|
||||
kgToLbs(userHealthWeight.muscle_mass) }} {{ $t('generalItems.unitsLbs') }}</span>
|
||||
<span v-if="userHealthWeight.muscle_mass && Number(authStore?.user?.units) === 1"
|
||||
>{{ userHealthWeight.muscle_mass.toFixed(2) }}
|
||||
{{ $t('generalItems.unitsKg') }}</span
|
||||
>
|
||||
<span v-else-if="userHealthWeight.muscle_mass && Number(authStore?.user?.units) === 2"
|
||||
>{{ kgToLbs(userHealthWeight.muscle_mass) }} {{ $t('generalItems.unitsLbs') }}</span
|
||||
>
|
||||
<span v-else>{{ $t('generalItems.labelNoData') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -170,7 +216,9 @@ async function submitDeleteWeight() {
|
||||
|
||||
onMounted(async () => {
|
||||
// Attach Bootstrap collapse event listeners to sync icon state
|
||||
const collapseElement = document.getElementById(`collapseWeightDetails${props.userHealthWeight.id}`)
|
||||
const collapseElement = document.getElementById(
|
||||
`collapseWeightDetails${props.userHealthWeight.id}`
|
||||
)
|
||||
if (collapseElement) {
|
||||
collapseElement.addEventListener('show.bs.collapse', () => {
|
||||
weightDetails.value = true
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
<template>
|
||||
<li class="list-group-item bg-body-tertiary rounded px-0" :class="{ 'shadow rounded px-3 mb-3': providerDetails }">
|
||||
<li
|
||||
class="list-group-item bg-body-tertiary rounded px-0"
|
||||
:class="{ 'shadow rounded px-3 mb-3': providerDetails }"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- Provider Icon/Logo -->
|
||||
<div class="me-3">
|
||||
<img :src="getProviderCustomLogo(provider.icon)!" :alt="`${provider.name} logo`"
|
||||
<img
|
||||
:src="getProviderCustomLogo(provider.icon)!"
|
||||
:alt="`${provider.name} logo`"
|
||||
style="height: 55px; width: 55px; object-fit: contain"
|
||||
v-if="provider.icon === 'authelia' || provider.icon === 'authentik' || provider.icon === 'casdoor' || provider.icon === 'keycloak'" />
|
||||
<img :src="provider.icon" :alt="`${provider.name} logo`"
|
||||
style="height: 55px; width: 55px; object-fit: contain" v-else />
|
||||
v-if="
|
||||
provider.icon === 'authelia' ||
|
||||
provider.icon === 'authentik' ||
|
||||
provider.icon === 'casdoor' ||
|
||||
provider.icon === 'keycloak'
|
||||
"
|
||||
/>
|
||||
<img
|
||||
:src="provider.icon"
|
||||
:alt="`${provider.name} logo`"
|
||||
style="height: 55px; width: 55px; object-fit: contain"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Provider Info -->
|
||||
@@ -30,43 +45,71 @@
|
||||
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- Status Badge -->
|
||||
<span v-if="provider.enabled"
|
||||
<span
|
||||
v-if="provider.enabled"
|
||||
class="badge bg-success-subtle border border-success-subtle text-success-emphasis me-2 d-none d-sm-inline"
|
||||
:aria-label="$t('settingsIdentityProvidersZone.enabledBadge')">{{
|
||||
$t('settingsIdentityProvidersZone.enabledBadge') }}</span>
|
||||
<span v-else
|
||||
:aria-label="$t('settingsIdentityProvidersZone.enabledBadge')"
|
||||
>{{ $t('settingsIdentityProvidersZone.enabledBadge') }}</span
|
||||
>
|
||||
<span
|
||||
v-else
|
||||
class="badge bg-danger-subtle border border-danger-subtle text-danger-emphasis me-2 d-none d-sm-inline"
|
||||
:aria-label="$t('settingsIdentityProvidersZone.disabledBadge')">{{
|
||||
$t('settingsIdentityProvidersZone.disabledBadge') }}</span>
|
||||
:aria-label="$t('settingsIdentityProvidersZone.disabledBadge')"
|
||||
>{{ $t('settingsIdentityProvidersZone.disabledBadge') }}</span
|
||||
>
|
||||
|
||||
<!-- Toggle Collapse Details -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" data-bs-toggle="collapse"
|
||||
:href="`#collapseProviderDetails${provider.id}`" role="button" aria-expanded="false"
|
||||
:aria-controls="`collapseProviderDetails${provider.id}`">
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
data-bs-toggle="collapse"
|
||||
:href="`#collapseProviderDetails${provider.id}`"
|
||||
role="button"
|
||||
aria-expanded="false"
|
||||
:aria-controls="`collapseProviderDetails${provider.id}`"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'caret-down']" v-if="!providerDetails" />
|
||||
<font-awesome-icon :icon="['fas', 'caret-up']" v-else />
|
||||
</a>
|
||||
|
||||
<!-- Toggle Enable/Disable Button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" @click.prevent="handleToggleProvider"
|
||||
:aria-label="provider.enabled
|
||||
? $t('settingsIdentityProvidersZone.disableButton')
|
||||
: $t('settingsIdentityProvidersZone.enableButton')
|
||||
">
|
||||
<font-awesome-icon :icon="provider.enabled ? ['fas', 'toggle-on'] : ['fas', 'toggle-off']" />
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
@click.prevent="handleToggleProvider"
|
||||
:aria-label="
|
||||
provider.enabled
|
||||
? $t('settingsIdentityProvidersZone.disableButton')
|
||||
: $t('settingsIdentityProvidersZone.enableButton')
|
||||
"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="provider.enabled ? ['fas', 'toggle-on'] : ['fas', 'toggle-off']"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<a class="btn btn-link btn-lg link-body-emphasis" href="#" role="button" data-bs-toggle="modal"
|
||||
:data-bs-target="`#editIdentityProviderModal${provider.id}`" @click="handleEditProvider"
|
||||
:aria-label="$t('settingsIdentityProvidersZone.editButton')">
|
||||
<a
|
||||
class="btn btn-link btn-lg link-body-emphasis"
|
||||
href="#"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
:data-bs-target="`#editIdentityProviderModal${provider.id}`"
|
||||
@click="handleEditProvider"
|
||||
:aria-label="$t('settingsIdentityProvidersZone.editButton')"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'edit']" />
|
||||
</a>
|
||||
|
||||
<!-- Delete 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="`#deleteIdentityProviderModal${provider.id}`"
|
||||
:aria-label="$t('settingsIdentityProvidersZone.deleteButton')">
|
||||
:aria-label="$t('settingsIdentityProvidersZone.deleteButton')"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'trash-can']" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -115,14 +158,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Edit Identity Provider Modal -->
|
||||
<IdentityProvidersAddEditModalComponent :action="'edit'" :provider="provider" :templates="templates"
|
||||
@providerUpdated="handleProviderUpdated" />
|
||||
<IdentityProvidersAddEditModalComponent
|
||||
:action="'edit'"
|
||||
:provider="provider"
|
||||
:templates="templates"
|
||||
@providerUpdated="handleProviderUpdated"
|
||||
/>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<ModalComponent :modalId="`deleteIdentityProviderModal${provider.id}`"
|
||||
<ModalComponent
|
||||
:modalId="`deleteIdentityProviderModal${provider.id}`"
|
||||
:title="$t('settingsIdentityProvidersZone.deleteModalTitle')"
|
||||
:body="$t('settingsIdentityProvidersZone.deleteModalBody', { name: provider.name })" actionButtonType="danger"
|
||||
:actionButtonText="$t('settingsIdentityProvidersZone.deleteModalConfirm')" @submitAction="handleDeleteProvider" />
|
||||
:body="$t('settingsIdentityProvidersZone.deleteModalBody', { name: provider.name })"
|
||||
actionButtonType="danger"
|
||||
:actionButtonText="$t('settingsIdentityProvidersZone.deleteModalConfirm')"
|
||||
@submitAction="handleDeleteProvider"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<!-- Modal add/edit identity provider -->
|
||||
<div ref="modalRef" class="modal fade" :id="action === 'add' ? 'addIdentityProviderModal' : editModalId" tabindex="-1"
|
||||
:aria-labelledby="headingId" aria-hidden="true">
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="modal fade"
|
||||
:id="action === 'add' ? 'addIdentityProviderModal' : editModalId"
|
||||
tabindex="-1"
|
||||
:aria-labelledby="headingId"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@@ -11,16 +17,28 @@
|
||||
<h1 class="modal-title fs-5" :id="headingId" v-else>
|
||||
{{ $t('identityProvidersAddEditModal.editTitle') }}
|
||||
</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||||
@click="resetForm"></button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
@click="resetForm"
|
||||
></button>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="modal-body">
|
||||
<!-- Template Selection (Add Only) -->
|
||||
<div v-if="action === 'add'" class="mb-3">
|
||||
<label for="providerTemplate"><b>{{ $t('identityProvidersAddEditModal.templateLabel') }}</b></label>
|
||||
<select class="form-select" id="providerTemplate" v-model="selectedTemplate" @change="applyTemplate"
|
||||
aria-label="Provider template selection">
|
||||
<label for="providerTemplate"
|
||||
><b>{{ $t('identityProvidersAddEditModal.templateLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
id="providerTemplate"
|
||||
v-model="selectedTemplate"
|
||||
@change="applyTemplate"
|
||||
aria-label="Provider template selection"
|
||||
>
|
||||
<option value="">{{ $t('identityProvidersAddEditModal.templateCustom') }}</option>
|
||||
<option v-for="(template, index) in templates" :key="index" :value="index">
|
||||
{{ template.name }}
|
||||
@@ -33,82 +51,156 @@
|
||||
|
||||
<!-- Basic Information -->
|
||||
<!-- name field -->
|
||||
<label :for="`providerName_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.nameLabel') }}</b></label>
|
||||
<input type="text" class="form-control"
|
||||
:id="`providerName_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="providerName"
|
||||
v-model="formData.name" :placeholder="$t('identityProvidersAddEditModal.namePlaceholder')" maxlength="100"
|
||||
aria-label="Provider name" required />
|
||||
<label :for="`providerName_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.nameLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="`providerName_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="providerName"
|
||||
v-model="formData.name"
|
||||
:placeholder="$t('identityProvidersAddEditModal.namePlaceholder')"
|
||||
maxlength="100"
|
||||
aria-label="Provider name"
|
||||
required
|
||||
/>
|
||||
<!-- slug field -->
|
||||
<label :for="`providerSlug_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.slugLabel') }}</b></label>
|
||||
<input type="text" class="form-control" :class="{ 'is-invalid': !isSlugValid }"
|
||||
:id="`providerSlug_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="providerSlug"
|
||||
<label :for="`providerSlug_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.slugLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:class="{ 'is-invalid': !isSlugValid }"
|
||||
:id="`providerSlug_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="providerSlug"
|
||||
:aria-describedby="`validationSlugFeedback_${action === 'add' ? 'add' : `edit_${provider?.id}`} slugHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.slug" :placeholder="$t('identityProvidersAddEditModal.slugPlaceholder')" maxlength="50"
|
||||
pattern="[0-9a-z-]+" :disabled="action === 'edit'" :aria-invalid="!isSlugValid ? 'true' : 'false'"
|
||||
required />
|
||||
<div :id="`slugHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text"
|
||||
v-if="action === 'add'">
|
||||
v-model="formData.slug"
|
||||
:placeholder="$t('identityProvidersAddEditModal.slugPlaceholder')"
|
||||
maxlength="50"
|
||||
pattern="[0-9a-z-]+"
|
||||
:disabled="action === 'edit'"
|
||||
:aria-invalid="!isSlugValid ? 'true' : 'false'"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
:id="`slugHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
v-if="action === 'add'"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.slugHelp') }}
|
||||
</div>
|
||||
<div :id="`validationSlugFeedback_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="invalid-feedback" v-if="!isSlugValid">
|
||||
<div
|
||||
:id="`validationSlugFeedback_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="invalid-feedback"
|
||||
v-if="!isSlugValid"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.slugInvalid') }}
|
||||
</div>
|
||||
|
||||
<!-- Provider Type -->
|
||||
<label :for="`providerType_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.providerTypeLabel') }}</b></label>
|
||||
<select class="form-select" :id="`providerType_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="providerType" v-model="formData.provider_type" aria-label="Provider type selection" required>
|
||||
<label :for="`providerType_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.providerTypeLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
:id="`providerType_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="providerType"
|
||||
v-model="formData.provider_type"
|
||||
aria-label="Provider type selection"
|
||||
required
|
||||
>
|
||||
<option value="oidc">OpenID Connect (OIDC)</option>
|
||||
<option value="oauth2">OAuth 2.0</option>
|
||||
</select>
|
||||
|
||||
<!-- OAuth/OIDC Configuration -->
|
||||
<!-- issuer url field -->
|
||||
<label :for="`issuerUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.issuerUrlLabel') }}</b></label>
|
||||
<input type="url" class="form-control"
|
||||
:id="`issuerUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="issuerUrl"
|
||||
<label :for="`issuerUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.issuerUrlLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
:id="`issuerUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="issuerUrl"
|
||||
:aria-describedby="`issuerUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.issuer_url" :placeholder="$t('identityProvidersAddEditModal.issuerUrlPlaceholder')"
|
||||
maxlength="500" required />
|
||||
<div :id="`issuerUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
v-model="formData.issuer_url"
|
||||
:placeholder="$t('identityProvidersAddEditModal.issuerUrlPlaceholder')"
|
||||
maxlength="500"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
:id="`issuerUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.issuerUrlHelp') }}
|
||||
</div>
|
||||
|
||||
<!-- client id field -->
|
||||
<label :for="`clientId_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.clientIdLabel') }}</b></label>
|
||||
<input type="text" class="form-control"
|
||||
:id="`clientId_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="clientId"
|
||||
v-model="formData.client_id" :placeholder="$t('identityProvidersAddEditModal.clientIdPlaceholder')"
|
||||
maxlength="512" aria-label="Client ID" required />
|
||||
<label :for="`clientId_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.clientIdLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="`clientId_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="clientId"
|
||||
v-model="formData.client_id"
|
||||
:placeholder="$t('identityProvidersAddEditModal.clientIdPlaceholder')"
|
||||
maxlength="512"
|
||||
aria-label="Client ID"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- client secret field -->
|
||||
<label :for="`clientSecret_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>* {{
|
||||
$t('identityProvidersAddEditModal.clientSecretLabel') }}</b></label>
|
||||
<input type="password" class="form-control"
|
||||
:id="`clientSecret_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="clientSecret"
|
||||
:aria-describedby="action === 'edit' ? `clientSecretHelpText_edit_${provider?.id}` : undefined
|
||||
" v-model="formData.client_secret" :placeholder="action === 'edit'
|
||||
<label :for="`clientSecret_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>* {{ $t('identityProvidersAddEditModal.clientSecretLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
:id="`clientSecret_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="clientSecret"
|
||||
:aria-describedby="
|
||||
action === 'edit' ? `clientSecretHelpText_edit_${provider?.id}` : undefined
|
||||
"
|
||||
v-model="formData.client_secret"
|
||||
:placeholder="
|
||||
action === 'edit'
|
||||
? $t('identityProvidersAddEditModal.clientSecretPlaceholderEdit')
|
||||
: $t('identityProvidersAddEditModal.clientSecretPlaceholder')
|
||||
" maxlength="512" :required="action === 'add'" />
|
||||
<div :id="`clientSecretHelpText_edit_${provider?.id}`" class="form-text" v-if="action === 'edit'">
|
||||
"
|
||||
maxlength="512"
|
||||
:required="action === 'add'"
|
||||
/>
|
||||
<div
|
||||
:id="`clientSecretHelpText_edit_${provider?.id}`"
|
||||
class="form-text"
|
||||
v-if="action === 'edit'"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.clientSecretHelpEdit') }}
|
||||
</div>
|
||||
|
||||
<!-- scopes field -->
|
||||
<label :for="`scopes_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.scopesLabel') }}</b></label>
|
||||
<input type="text" class="form-control" :id="`scopes_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="scopes" :aria-describedby="`scopesHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.scopes" :placeholder="$t('identityProvidersAddEditModal.scopesPlaceholder')"
|
||||
maxlength="500" />
|
||||
<div :id="`scopesHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
<label :for="`scopes_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.scopesLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
:id="`scopes_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="scopes"
|
||||
:aria-describedby="`scopesHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.scopes"
|
||||
:placeholder="$t('identityProvidersAddEditModal.scopesPlaceholder')"
|
||||
maxlength="500"
|
||||
/>
|
||||
<div
|
||||
:id="`scopesHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.scopesHelp') }}
|
||||
</div>
|
||||
|
||||
@@ -117,30 +209,48 @@
|
||||
<h6>{{ $t('identityProvidersAddEditModal.appearanceSection') }}</h6>
|
||||
|
||||
<!-- icon field -->
|
||||
<label :for="`icon_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.iconLabel') }}</b></label>
|
||||
<select class="form-select" :id="`icon_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="icon"
|
||||
v-model="formData.icon" aria-label="Provider icon selection">
|
||||
<label :for="`icon_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.iconLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
:id="`icon_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="icon"
|
||||
v-model="formData.icon"
|
||||
aria-label="Provider icon selection"
|
||||
>
|
||||
<option value="authelia">Authelia</option>
|
||||
<option value="authentik">Authentik</option>
|
||||
<option value="casdoor">Casdoor</option>
|
||||
<option value="keycloak">Keycloak</option>
|
||||
<option value="custom">{{ $t('identityProvidersAddEditModal.iconCustom') }}</option>
|
||||
</select>
|
||||
<div :id="`iconHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
<div
|
||||
:id="`iconHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.iconHelp') }}
|
||||
</div>
|
||||
|
||||
<!-- custom icon url field (shown only when custom is selected) -->
|
||||
<div v-if="formData.icon === 'custom'" class="mt-2">
|
||||
<label :for="`customIconUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.customIconUrlLabel') }}</b></label>
|
||||
<input type="url" class="form-control"
|
||||
:id="`customIconUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" name="customIconUrl"
|
||||
<label :for="`customIconUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.customIconUrlLabel') }}</b></label
|
||||
>
|
||||
<input
|
||||
type="url"
|
||||
class="form-control"
|
||||
:id="`customIconUrl_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="customIconUrl"
|
||||
:aria-describedby="`customIconUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.custom_icon_url"
|
||||
:placeholder="$t('identityProvidersAddEditModal.customIconUrlPlaceholder')" maxlength="500" />
|
||||
<div :id="`customIconUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
:placeholder="$t('identityProvidersAddEditModal.customIconUrlPlaceholder')"
|
||||
maxlength="500"
|
||||
/>
|
||||
<div
|
||||
:id="`customIconUrlHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.customIconUrlHelp') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +260,16 @@
|
||||
<h6>{{ $t('identityProvidersAddEditModal.optionsSection') }}</h6>
|
||||
|
||||
<!-- enabled select -->
|
||||
<label :for="`enabled_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.enabledLabel') }}</b></label>
|
||||
<select class="form-select" :id="`enabled_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="enabled" v-model="formData.enabled" aria-label="Enable identity provider">
|
||||
<label :for="`enabled_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.enabledLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
:id="`enabled_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="enabled"
|
||||
v-model="formData.enabled"
|
||||
aria-label="Enable identity provider"
|
||||
>
|
||||
<option :value="true">
|
||||
{{ $t('generalItems.yes') }}
|
||||
</option>
|
||||
@@ -163,12 +279,16 @@
|
||||
</select>
|
||||
|
||||
<!-- auto create users select -->
|
||||
<label :for="`autoCreateUsers_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.autoCreateUsersLabel') }}</b></label>
|
||||
<select class="form-select" :id="`autoCreateUsers_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
<label :for="`autoCreateUsers_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.autoCreateUsersLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
:id="`autoCreateUsers_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="autoCreateUsers"
|
||||
:aria-describedby="`autoCreateUsersHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.auto_create_users">
|
||||
v-model="formData.auto_create_users"
|
||||
>
|
||||
<option :value="true">
|
||||
{{ $t('generalItems.yes') }}
|
||||
</option>
|
||||
@@ -176,17 +296,24 @@
|
||||
{{ $t('generalItems.no') }}
|
||||
</option>
|
||||
</select>
|
||||
<div :id="`autoCreateUsersHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
<div
|
||||
:id="`autoCreateUsersHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.autoCreateUsersHelp') }}
|
||||
</div>
|
||||
|
||||
<!-- sync user info select -->
|
||||
<label :for="`syncUserInfo_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"><b>{{
|
||||
$t('identityProvidersAddEditModal.syncUserInfoLabel') }}</b></label>
|
||||
<select class="form-select" :id="`syncUserInfo_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
<label :for="`syncUserInfo_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
><b>{{ $t('identityProvidersAddEditModal.syncUserInfoLabel') }}</b></label
|
||||
>
|
||||
<select
|
||||
class="form-select"
|
||||
:id="`syncUserInfo_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
name="syncUserInfo"
|
||||
:aria-describedby="`syncUserInfoHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
v-model="formData.sync_user_info">
|
||||
v-model="formData.sync_user_info"
|
||||
>
|
||||
<option :value="true">
|
||||
{{ $t('generalItems.yes') }}
|
||||
</option>
|
||||
@@ -194,7 +321,10 @@
|
||||
{{ $t('generalItems.no') }}
|
||||
</option>
|
||||
</select>
|
||||
<div :id="`syncUserInfoHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`" class="form-text">
|
||||
<div
|
||||
:id="`syncUserInfoHelpText_${action === 'add' ? 'add' : `edit_${provider?.id}`}`"
|
||||
class="form-text"
|
||||
>
|
||||
{{ $t('identityProvidersAddEditModal.syncUserInfoHelp') }}
|
||||
</div>
|
||||
|
||||
@@ -208,17 +338,32 @@
|
||||
<p>* {{ $t('generalItems.requiredField') }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="resetForm"
|
||||
aria-label="Close modal">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
data-bs-dismiss="modal"
|
||||
@click="resetForm"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
{{ $t('generalItems.buttonClose') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-success" name="providerSubmit" :disabled="isSubmitting || !isSlugValid"
|
||||
:aria-label="action === 'add'
|
||||
? $t('identityProvidersAddEditModal.addButton')
|
||||
: $t('identityProvidersAddEditModal.saveButton')
|
||||
">
|
||||
<span v-if="isSubmitting" class="spinner-border spinner-border-sm me-2" role="status"
|
||||
aria-hidden="true"></span>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-success"
|
||||
name="providerSubmit"
|
||||
:disabled="isSubmitting || !isSlugValid"
|
||||
:aria-label="
|
||||
action === 'add'
|
||||
? $t('identityProvidersAddEditModal.addButton')
|
||||
: $t('identityProvidersAddEditModal.saveButton')
|
||||
"
|
||||
>
|
||||
<span
|
||||
v-if="isSubmitting"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
{{
|
||||
action === 'add'
|
||||
? $t('identityProvidersAddEditModal.addButton')
|
||||
@@ -362,8 +507,13 @@ const loadProviderData = (): void => {
|
||||
if (props.action === 'edit' && props.provider) {
|
||||
let iconSelect = null
|
||||
let iconCustom = null
|
||||
if (props.provider.icon !== "authelia" && props.provider.icon !== "authentik" && props.provider.icon !== "casdoor" && props.provider.icon !== "keycloak") {
|
||||
iconSelect = "custom"
|
||||
if (
|
||||
props.provider.icon !== 'authelia' &&
|
||||
props.provider.icon !== 'authentik' &&
|
||||
props.provider.icon !== 'casdoor' &&
|
||||
props.provider.icon !== 'keycloak'
|
||||
) {
|
||||
iconSelect = 'custom'
|
||||
iconCustom = props.provider.icon
|
||||
} else {
|
||||
iconSelect = props.provider.icon
|
||||
@@ -410,7 +560,12 @@ const createProvider = async (): Promise<void> => {
|
||||
|
||||
try {
|
||||
let icon = null
|
||||
if (formData.value.icon !== "authelia" && formData.value.icon !== "authentik" && formData.value.icon !== "casdoor" && formData.value.icon !== "keycloak") {
|
||||
if (
|
||||
formData.value.icon !== 'authelia' &&
|
||||
formData.value.icon !== 'authentik' &&
|
||||
formData.value.icon !== 'casdoor' &&
|
||||
formData.value.icon !== 'keycloak'
|
||||
) {
|
||||
icon = formData.value.custom_icon_url
|
||||
} else {
|
||||
icon = formData.value.icon
|
||||
@@ -452,7 +607,12 @@ const updateProvider = async (): Promise<void> => {
|
||||
|
||||
try {
|
||||
let icon = null
|
||||
if (formData.value.icon !== "authelia" && formData.value.icon !== "authentik" && formData.value.icon !== "casdoor" && formData.value.icon !== "keycloak") {
|
||||
if (
|
||||
formData.value.icon !== 'authelia' &&
|
||||
formData.value.icon !== 'authentik' &&
|
||||
formData.value.icon !== 'casdoor' &&
|
||||
formData.value.icon !== 'keycloak'
|
||||
) {
|
||||
icon = formData.value.custom_icon_url
|
||||
} else {
|
||||
icon = formData.value.icon
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
<!-- Username field -->
|
||||
<div class="mb-3">
|
||||
<label for="garminConnectUsername" class="form-label">
|
||||
<b>* {{ $t('garminConnectLoginModalComponent.garminConnectAuthModalUsernameLabel') }}</b>
|
||||
<b
|
||||
>*
|
||||
{{
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalUsernameLabel')
|
||||
}}</b
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="garminConnectUsername"
|
||||
@@ -33,8 +38,12 @@
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="garminConnectUsername"
|
||||
:placeholder="$t('garminConnectLoginModalComponent.garminConnectAuthModalUsernamePlaceholder')"
|
||||
:aria-label="$t('garminConnectLoginModalComponent.garminConnectAuthModalUsernameLabel')"
|
||||
:placeholder="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalUsernamePlaceholder')
|
||||
"
|
||||
:aria-label="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalUsernameLabel')
|
||||
"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -42,7 +51,12 @@
|
||||
<!-- Password field -->
|
||||
<div class="mb-3">
|
||||
<label for="garminConnectPassword" class="form-label">
|
||||
<b>* {{ $t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordLabel') }}</b>
|
||||
<b
|
||||
>*
|
||||
{{
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordLabel')
|
||||
}}</b
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="garminConnectPassword"
|
||||
@@ -50,8 +64,12 @@
|
||||
class="form-control"
|
||||
type="password"
|
||||
name="garminConnectPassword"
|
||||
:placeholder="$t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordPlaceholder')"
|
||||
:aria-label="$t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordLabel')"
|
||||
:placeholder="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordPlaceholder')
|
||||
"
|
||||
:aria-label="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalPasswordLabel')
|
||||
"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -60,7 +78,12 @@
|
||||
<div v-if="mfaRequired" class="row g-3 align-items-end">
|
||||
<div class="col">
|
||||
<label for="garminConnectMfaCode" class="form-label">
|
||||
<b>* {{ $t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodeLabel') }}</b>
|
||||
<b
|
||||
>*
|
||||
{{
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodeLabel')
|
||||
}}</b
|
||||
>
|
||||
</label>
|
||||
<input
|
||||
id="garminConnectMfaCode"
|
||||
@@ -68,8 +91,12 @@
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="garminConnectMfaCode"
|
||||
:placeholder="$t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodePlaceholder')"
|
||||
:aria-label="$t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodeLabel')"
|
||||
:placeholder="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodePlaceholder')
|
||||
"
|
||||
:aria-label="
|
||||
$t('garminConnectLoginModalComponent.garminConnectAuthModalMfaCodeLabel')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -199,17 +226,17 @@ const handleWebSocketMessage = (event: MessageEvent): void => {
|
||||
*/
|
||||
const submitConnectGarminConnect = async (): Promise<void> => {
|
||||
loadingLogin.value = true
|
||||
|
||||
|
||||
const notification = push.promise(
|
||||
t('garminConnectLoginModalComponent.processingMessageLinkGarminConnect')
|
||||
)
|
||||
|
||||
|
||||
try {
|
||||
const data = {
|
||||
username: garminConnectUsername.value,
|
||||
password: garminConnectPassword.value
|
||||
}
|
||||
|
||||
|
||||
await garminConnect.linkGarminConnect(data)
|
||||
|
||||
// Update user object with linked status
|
||||
@@ -219,7 +246,7 @@ const submitConnectGarminConnect = async (): Promise<void> => {
|
||||
|
||||
// Show success message
|
||||
notification.resolve(t('garminConnectLoginModalComponent.successMessageLinkGarminConnect'))
|
||||
|
||||
|
||||
// Hide modal and reset form
|
||||
hideModal()
|
||||
resetForm()
|
||||
@@ -237,16 +264,18 @@ const submitConnectGarminConnect = async (): Promise<void> => {
|
||||
*/
|
||||
const submitMfaCode = async (): Promise<void> => {
|
||||
if (!mfaCode.value) return
|
||||
|
||||
|
||||
loadingLoginWithMfa.value = true
|
||||
|
||||
|
||||
try {
|
||||
const data = {
|
||||
mfa_code: mfaCode.value
|
||||
}
|
||||
await garminConnect.mfaGarminConnect(data)
|
||||
} catch (error) {
|
||||
push.error(`${t('garminConnectLoginModalComponent.errorMessageUnableToLinkGarminConnect')} - ${error}`)
|
||||
push.error(
|
||||
`${t('garminConnectLoginModalComponent.errorMessageUnableToLinkGarminConnect')} - ${error}`
|
||||
)
|
||||
loadingLoginWithMfa.value = false
|
||||
}
|
||||
}
|
||||
@@ -272,7 +301,7 @@ const resetForm = (): void => {
|
||||
*/
|
||||
onMounted(async () => {
|
||||
await initializeModal(modalRef)
|
||||
|
||||
|
||||
// Set up WebSocket message handler for MFA
|
||||
const websocket = authStore.user_websocket as WebSocket | null
|
||||
if (websocket) {
|
||||
@@ -289,7 +318,7 @@ onUnmounted(() => {
|
||||
if (websocket) {
|
||||
websocket.onmessage = null
|
||||
}
|
||||
|
||||
|
||||
disposeModal()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -101,11 +101,7 @@
|
||||
<li class="list-group-item d-flex justify-content-between bg-body-tertiary px-0 pb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<!--<font-awesome-icon :icon="['fas', 'file-import']" size="2x" />-->
|
||||
<img
|
||||
:src="INTEGRATION_LOGOS.garminConnectApp"
|
||||
alt="Garmin Connect logo"
|
||||
height="32"
|
||||
/>
|
||||
<img :src="INTEGRATION_LOGOS.garminConnectApp" alt="Garmin Connect logo" height="32" />
|
||||
<div class="ms-3">
|
||||
<div class="fw-bold">
|
||||
{{ $t('settingsIntegrationsZone.garminConnectIntegrationTitle') }}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<li class="list-group-item bg-body-tertiary rounded px-0" :class="{ 'shadow rounded px-3 mb-3': userDetails }">
|
||||
<li
|
||||
class="list-group-item bg-body-tertiary rounded px-0"
|
||||
:class="{ 'shadow rounded px-3 mb-3': userDetails }"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex align-items-center">
|
||||
<UserAvatarComponent :user="user" :width="55" :height="55" />
|
||||
@@ -9,97 +12,172 @@
|
||||
</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>
|
||||
@@ -108,26 +186,44 @@
|
||||
<!-- 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>
|
||||
|
||||
@@ -22,14 +22,14 @@ import waterSportsBoard1 from '@/assets/avatar/waterSportsBoard1.png'
|
||||
* - 8: Water sports board
|
||||
*/
|
||||
export const GEAR_AVATAR_MAP: Record<number, string> = {
|
||||
1: bicycle1,
|
||||
2: runningShoe1,
|
||||
3: wetsuit1,
|
||||
4: racquet1,
|
||||
5: skis1,
|
||||
6: snowboard1,
|
||||
7: windsurf1,
|
||||
8: waterSportsBoard1
|
||||
1: bicycle1,
|
||||
2: runningShoe1,
|
||||
3: wetsuit1,
|
||||
4: racquet1,
|
||||
5: skis1,
|
||||
6: snowboard1,
|
||||
7: windsurf1,
|
||||
8: waterSportsBoard1
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -39,5 +39,5 @@ export const GEAR_AVATAR_MAP: Record<number, string> = {
|
||||
* @returns The image path for the specified gear type, or the default bicycle avatar if not found.
|
||||
*/
|
||||
export function getGearAvatar(gearType: number): string {
|
||||
return GEAR_AVATAR_MAP[gearType] ?? bicycle1
|
||||
return GEAR_AVATAR_MAP[gearType] ?? bicycle1
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import garminConnectApp from '@/assets/garminconnect/Garmin_Connect_app_1024x102
|
||||
* - **garminConnectApp**: Garmin Connect app icon (1024x1024)
|
||||
*/
|
||||
export const INTEGRATION_LOGOS = {
|
||||
strava: stravaLogo,
|
||||
garminConnectBadge: garminConnectBadge,
|
||||
garminConnectApp: garminConnectApp
|
||||
strava: stravaLogo,
|
||||
garminConnectBadge: garminConnectBadge,
|
||||
garminConnectApp: garminConnectApp
|
||||
} as const
|
||||
|
||||
@@ -17,5 +17,5 @@ export const PROVIDER_CUSTOM_LOGO_MAP: Record<string, string> = {
|
||||
authelia: autheliaLogo,
|
||||
authentik: authentikLogo,
|
||||
casdoor: casdoorLogo,
|
||||
keycloak: keycloakLogo,
|
||||
keycloak: keycloakLogo
|
||||
} as const
|
||||
|
||||
@@ -12,9 +12,9 @@ import unspecifiedAvatar from '@/assets/avatar/unspecified1.png'
|
||||
* - **3**: Unspecified/neutral avatar
|
||||
*/
|
||||
export const USER_AVATAR_MAP: Record<number, string> = {
|
||||
1: maleAvatar,
|
||||
2: femaleAvatar,
|
||||
3: unspecifiedAvatar
|
||||
1: maleAvatar,
|
||||
2: femaleAvatar,
|
||||
3: unspecifiedAvatar
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -28,8 +28,8 @@ export const USER_AVATAR_MAP: Record<number, string> = {
|
||||
* Falls back to unspecified avatar if the gender value is not found in the map.
|
||||
*/
|
||||
export function getUserDefaultAvatar(gender?: number): string {
|
||||
if (!gender || gender < 1 || gender > 3) {
|
||||
return maleAvatar // Default to male avatar
|
||||
}
|
||||
return USER_AVATAR_MAP[gender] ?? unspecifiedAvatar
|
||||
if (!gender || gender < 1 || gender > 3) {
|
||||
return maleAvatar // Default to male avatar
|
||||
}
|
||||
return USER_AVATAR_MAP[gender] ?? unspecifiedAvatar
|
||||
}
|
||||
|
||||
12
frontend/app/src/env.d.ts
vendored
12
frontend/app/src/env.d.ts
vendored
@@ -1,16 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: string
|
||||
export default content
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const content: string
|
||||
export default content
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const content: string
|
||||
export default content
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Raquetes de neu",
|
||||
"inlineSkating": "Patinatge en línia",
|
||||
"labelWorkout": " entrenament"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Amaga equipament",
|
||||
"successActivityEdit": "Activitat editada correctament",
|
||||
"errorActivityEdit": "Error editant activitat"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Hi ha un total de ",
|
||||
"labelNumberOfHealthWeightWeight2": " mesure(s) de pes inserides (",
|
||||
"labelNumberOfHealthWeightWeight3": " carregat):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "No es poden obtenir dades de salut de GC",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "No es poden obtenir dades de salut de GC",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Recuperant dades de salut de Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Salut",
|
||||
"errorFetchingHealthWeight": "Error recollint dades de salut",
|
||||
"errorFetchingHealthTargets": "Error recollint objectius de salut"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Error obtenint usuari amb lògica de nom d'usuari",
|
||||
"errorFetchingActivityWithNameContains": "Error obtenint activitat amb lògica de nom d'usuari",
|
||||
"errorFetchingGearWithNicknameContains": "Error obtenint equipament amb lògica de nom d'usuari"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " 运动"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "隐藏装备",
|
||||
"successActivityEdit": "活动编辑成功",
|
||||
"errorActivityEdit": "活动编辑失败"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "共有",
|
||||
"labelNumberOfHealthWeightWeight2": "条体重记录(",
|
||||
"labelNumberOfHealthWeightWeight3": "条已加载):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "无法按天获取 Garmin Connect 健康数据",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "无法按日期范围获取 Garmin Connect 健康数据",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "正在获取 Garmin Connect 健康数据"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,4 @@
|
||||
"userIdpsLoadErrorMessage": "加载身份提供商时出错",
|
||||
"userIdpDeleteSuccessMessage": "身份提供商已成功解除链接",
|
||||
"userIdpDeleteErrorMessage": "解除身份提供商链接时出错"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "健康",
|
||||
"errorFetchingHealthWeight": "获取健康数据出错",
|
||||
"errorFetchingHealthTargets": "获取健康目标出错"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "获取用户名包含逻辑的用户时出错",
|
||||
"errorFetchingActivityWithNameContains": "获取名称中包含逻辑的活动时出错",
|
||||
"errorFetchingGearWithNicknameContains": "获取昵称中包含逻辑的装备时出错"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " Workout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Ausrüstung ausblenden",
|
||||
"successActivityEdit": "Aktivität erfolgreich bearbeitet",
|
||||
"errorActivityEdit": "Fehler beim Bearbeiten der Aktivität"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Insgesamt ",
|
||||
"labelNumberOfHealthWeightWeight2": " Gewichtsmessung(en) hinzugefügt (",
|
||||
"labelNumberOfHealthWeightWeight3": " geladen):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "Garmin Connect Gesundheitsdaten konnten nicht nach Tagen abgerufen werden",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "Garmin Connect Gesundheitsdaten konnten nicht nach Datumsbereich abgerufen werden",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Gesundheitsdaten von Garmin Connect abrufen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Gesundheit",
|
||||
"errorFetchingHealthWeight": "Fehler beim Laden der Gesundheitsdaten",
|
||||
"errorFetchingHealthTargets": "Fehler beim Laden der Gesundheitsziele"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Fehler beim Abrufen des Benutzers mit Benutzername enthält Logik",
|
||||
"errorFetchingActivityWithNameContains": "Fehler beim Abrufen der Aktivität mit dem Namen enthält Logik",
|
||||
"errorFetchingGearWithNicknameContains": "Fehler beim Abrufen der Ausrüstung mit Nickname enthält Logik"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " entrenamiento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Ocultar equipo",
|
||||
"successActivityEdit": "Actividad editada correctamente",
|
||||
"errorActivityEdit": "Error al editar actividad"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Hay un total de ",
|
||||
"labelNumberOfHealthWeightWeight2": " medición(es) de peso insertado(s) (",
|
||||
"labelNumberOfHealthWeightWeight3": " cargado):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "No se pueden obtener datos de salud de Garmin Connect por días",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "No se pueden obtener las actividades de Garmin Connect usando el rango de datos",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Obteniendo datos de salud de Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Salud",
|
||||
"errorFetchingHealthWeight": "Error obteniendo datos de salud",
|
||||
"errorFetchingHealthTargets": "Error obteniendo objetivos de salud"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Error obteniendo usuario con nombre de usuario contiene lógica",
|
||||
"errorFetchingActivityWithNameContains": "Error al obtener la actividad con el nombre contiene lógica",
|
||||
"errorFetchingGearWithNicknameContains": "Error al obtener el equipo con el nombre contiene lógica"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " workout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Masquer l'équipement",
|
||||
"successActivityEdit": "L'activité a bien été modifiée",
|
||||
"errorActivityEdit": "Erreur lors de la modification de l'activité"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Il y a un total de ",
|
||||
"labelNumberOfHealthWeightWeight2": " mesure(s) de poids insérée(s) (",
|
||||
"labelNumberOfHealthWeightWeight3": " chargé(e)(s) ):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "Impossible d'obtenir les données de santé de Garmin Connect par jours",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "Impossible d'obtenir les données de santé de Garmin Connect par période",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Récupération des données de santé de Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Santé",
|
||||
"errorFetchingHealthWeight": "Erreur lors de la récupération des données de santé",
|
||||
"errorFetchingHealthTargets": "Erreur lors de la récupération des objectifs de santé"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Erreur lors de la récupération de l'utilisateur avec le nom d'utilisateur",
|
||||
"errorFetchingActivityWithNameContains": "Erreur lors de la récupération de l'activité avec le nom",
|
||||
"errorFetchingGearWithNicknameContains": "Erreur lors de la récupération de l'équipement avec le surnom"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " adestramento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
"errorMessageActivityNotFound": "Non se atopa a actividade",
|
||||
"alertPrivacyMessage": "Tes información oculta nesta actividade. Podes vela, pero outras persoas non.",
|
||||
"isHiddenMessage": "A actividade está oculta. Probablemente porque é un duplicado ou a usuaria así o decideu."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
"activityNoData": "Sen datos",
|
||||
"errorFetchingUserById": "Erro ao obter a usuaria polo id",
|
||||
"errorDeletingActivity": "Erro ao eliminar a actividade"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Ocultar equipamento",
|
||||
"successActivityEdit": "Actividade editada correctamente",
|
||||
"errorActivityEdit": "Erro ao editar a actividade"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
"errorDeleteFollower": "Erro ao eliminar a seguidora",
|
||||
"errorUpdateFollower": "Erro ao actualizar a seguidora",
|
||||
"errorFetchingFollowersDetails": "Erro ao obter detalles das seguidoras"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@
|
||||
"errorGearAdd": "Erro ao engadir o equipamento",
|
||||
"successGearEdited": "Equipamento editado correctamente",
|
||||
"errorGearEdit": "Erro ao editar o equipamento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@
|
||||
"gearListModalDeleteGearBody": "Tes certeza de querer eliminar o equipamento ",
|
||||
"gearListGearDeleteSuccessMessage": "Equipamento eliminado correctamente",
|
||||
"gearListGearDeleteErrorMessage": "Erro ao eliminar o equipamento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Hai un total de ",
|
||||
"labelNumberOfHealthWeightWeight2": " engadido o peso (",
|
||||
"labelNumberOfHealthWeightWeight3": " cargado):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Oooi…",
|
||||
"subtitle": "Sen records"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "Non se puideron obter os datos diarios de saúde desde Garmin Connect",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "Non se puideron obter os datos de saúde do rango de datas desde Garmin Connect",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Obtendo datos de saúde desde Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
"successGearEdited": "Equipamento editado correctamente",
|
||||
"errorGearDelete": "Erro ao eliminar o equipamento",
|
||||
"errorFetchingGears": "Erro ao obter equipamentos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
"successGearDeleted": "Equipamento eliminado correctamente",
|
||||
"errorGearNotFound": "Non se atopa equipamento",
|
||||
"errorFetchingGears": "Erro ao obter equipamentos"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,4 +79,4 @@
|
||||
"genderUnspecified": "Sen especificar",
|
||||
"labelAverage": "Average",
|
||||
"labelMaximum": "Maximum"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Saúde",
|
||||
"errorFetchingHealthWeight": "Erro ao obter datos de saúde",
|
||||
"errorFetchingHealthTargets": "Erro ao obter obxectivos de saúde"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Ooooi! Non atopamos a páxina",
|
||||
"subTitle": "A páxina que solicitas non existe ou cambiou.",
|
||||
"backToHomeButton": "Volver ao inicio"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Error fetching user with username contains logic",
|
||||
"errorFetchingActivityWithNameContains": "Error fetching activity with name contains logic",
|
||||
"errorFetchingGearWithNicknameContains": "Error fetching gear with nickname contains logic"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ const componentPaths = {
|
||||
'components/health/healthStepsZone/healthStepsAddEditModalComponent.json',
|
||||
healthSleepZoneComponent: 'components/health/healthSleepZoneComponent.json',
|
||||
healthSleepListComponent: 'components/health/healthSleepZone/healthSleepListComponent.json',
|
||||
healthSleepListTabsComponent: 'components/health/healthSleepZone/healthSleepListTabsComponent.json',
|
||||
healthSleepListTabsComponent:
|
||||
'components/health/healthSleepZone/healthSleepListTabsComponent.json',
|
||||
healthSleepAddEditModalComponent:
|
||||
'components/health/healthSleepZone/healthSleepAddEditModalComponent.json',
|
||||
healthRHRZoneComponent: 'components/health/healthRHRZoneComponent.json',
|
||||
// Navbar components
|
||||
navbarBottomMobileComponent: 'components/navbar/navbarBottomMobileComponent.json',
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " allenamento"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Nascondi attrezzatura",
|
||||
"successActivityEdit": "Attività modificata con successo",
|
||||
"errorActivityEdit": "Errore nella modifica dell'attività"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "C'è un totale di ",
|
||||
"labelNumberOfHealthWeightWeight2": " misurazioni di peso inserite (",
|
||||
"labelNumberOfHealthWeightWeight3": " caricate):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "Impossibile ottenere i dati sulla salute Garmin Connect per giorni",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "Impossibile ottenere i dati sulla salute Garmin Connect usando l'intervallo di date",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Recupero dati sulla salute Garmin Connect"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Salute",
|
||||
"errorFetchingHealthWeight": "Errore nel recupero dei dati sulla salute",
|
||||
"errorFetchingHealthTargets": "Errore nel recupero degli obiettivi di salute"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Errore nel recupero dell'utente con nome utente contenente",
|
||||
"errorFetchingActivityWithNameContains": "Errore nel recupero dell'attività con nome contenente",
|
||||
"errorFetchingGearWithNicknameContains": "Errore nel recupero dell'attrezzatura con soprannome contenente"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " workout"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Verberg uitrusting",
|
||||
"successActivityEdit": "Activiteit succesvol bewerkt",
|
||||
"errorActivityEdit": "Fout bij bewerken activiteit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Er is een totaal van ",
|
||||
"labelNumberOfHealthWeightWeight2": " gewicht afmeting(en) ingevoegd (",
|
||||
"labelNumberOfHealthWeightWeight3": " geladen):"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,4 @@
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDays": "Unable to get Garmin Connect health data by days",
|
||||
"errorMessageUnableToGetGarminConnectHealthDataDateRange": "Garmin Connect gezondheidsgegevens ophalen voor datumbereik is niet gelukt",
|
||||
"loadingMessageRetrievingGarminConnectHealthData": "Ophalen van Garmin Connect gezondheidsgegevens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"title": "Gezondheid",
|
||||
"errorFetchingHealthWeight": "Fout bij het ophalen van gezondheidsgegevens",
|
||||
"errorFetchingHealthTargets": "Fout bij ophalen van gezondheidsdoelen"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@
|
||||
"errorFetchingUserWithUsernameContains": "Fout bij ophalen gebruiker met gebruikersnaam bevat logica",
|
||||
"errorFetchingActivityWithNameContains": "Fout bij ophalen van activiteit met naam bevat logica",
|
||||
"errorFetchingGearWithNicknameContains": "Fout bij ophalen van uitrusting met bijnaam bevat logica"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,4 @@
|
||||
"snowShoeing": "Snow shoeing",
|
||||
"inlineSkating": "Inline skating",
|
||||
"labelWorkout": " treino"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@
|
||||
"modalEditActivityHideGearLabel": "Ocultar equipamento",
|
||||
"successActivityEdit": "Atividade editada com sucesso",
|
||||
"errorActivityEdit": "Erro ao editar atividade"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"addSleepModalTitle": "Adicionar sono",
|
||||
"editSleepModalTitle": "Editar sono",
|
||||
"dateLabel": "Data",
|
||||
"sleepStartTimeLabel": "Hora de início do sono",
|
||||
"sleepEndTimeLabel": "Hora de fim do sono",
|
||||
"totalSleepLabel": "Sono Total",
|
||||
"deepSleepLabel": "Sono Profundo",
|
||||
"lightSleepLabel": "Sono Leve",
|
||||
"remSleepLabel": "Sono REM",
|
||||
"awakeSleepLabel": "Sono Acordado",
|
||||
"napTimeLabel": "Tempo de Sesta",
|
||||
"avgHeartRateLabel": "Frequência cardíaca média",
|
||||
"minHeartRateLabel": "Frequência cardíaca mínima",
|
||||
"maxHeartRateLabel": "Frequência cardíaca máxima",
|
||||
"avgSpo2Label": "SpO2 médio",
|
||||
"lowestSpo2Label": "SpO2 mínimo",
|
||||
"highestSpo2Label": "SpO2 máximo",
|
||||
"sleepScoreOverallLabel": "Pontuação geral do sono",
|
||||
"awakeCountLabel": "Número de despertares",
|
||||
"restlessMomentsCountLabel": "Momentos inquietos",
|
||||
"sleepStagesLabel": "Fases do sono",
|
||||
"addStageButton": "Adicionar fase",
|
||||
"stageLabel": "Fase",
|
||||
"stageTypeLabel": "Tipo de fase",
|
||||
"stageTypeDeep": "Profundo",
|
||||
"stageTypeLight": "Leve",
|
||||
"stageTypeRem": "REM",
|
||||
"stageTypeAwake": "Acordado",
|
||||
"stageStartTimeLabel": "Hora de início",
|
||||
"stageEndTimeLabel": "Hora de fim",
|
||||
"stageDurationLabel": "Duração (segundos)",
|
||||
"noStagesMessage": "Nenhuma fase de sono adicionada ainda. Clique em 'Adicionar fase' para adicionar uma fase de sono.",
|
||||
"successAddSleep": "Sono adicionado com sucesso",
|
||||
"errorAddSleep": "Erro ao adicionar sono"
|
||||
}
|
||||
@@ -3,4 +3,4 @@
|
||||
"labelNumberOfHealthWeightWeight1": "Existe um total de ",
|
||||
"labelNumberOfHealthWeightWeight2": " medida(s) de peso inserida(s) (",
|
||||
"labelNumberOfHealthWeightWeight3": " carregada(s)):"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user