fix: 自評去識別化
This commit is contained in:
@@ -209,10 +209,10 @@ const existingDraft = readGuestSelfAssessmentDraft();
|
|||||||
const routePhone = normalizePhone(String(route.query.phone ?? ""));
|
const routePhone = normalizePhone(String(route.query.phone ?? ""));
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
phone: routePhone || existingDraft?.profile.phone || "",
|
phone: routePhone,
|
||||||
password: "",
|
password: "",
|
||||||
passwordConfirmation: "",
|
passwordConfirmation: "",
|
||||||
consent: existingDraft?.profile.consent ?? false,
|
consent: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
v-for="(stepName, idx) in stepNames"
|
v-for="(stepName, idx) in stepNames"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
class="text-center"
|
class="text-center"
|
||||||
cols="3"
|
cols="4"
|
||||||
>
|
>
|
||||||
<v-avatar
|
<v-avatar
|
||||||
class="text-white font-weight-bold mb-1 elevation-1"
|
class="text-white font-weight-bold mb-1 elevation-1"
|
||||||
@@ -54,149 +54,8 @@
|
|||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<!-- 步驟 1:基本資料 -->
|
<!-- 步驟 1:活動量與自評 -->
|
||||||
<div v-if="currentStep === 0">
|
<div v-if="currentStep === 0">
|
||||||
<h2
|
|
||||||
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
|
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
class="mr-2"
|
|
||||||
color="secondary"
|
|
||||||
icon="mdi-account-circle"
|
|
||||||
/>
|
|
||||||
基本資料確認
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="profileForm.name"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="姓名"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="profileForm.phone"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="手機號碼"
|
|
||||||
type="tel"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-select
|
|
||||||
v-model="profileForm.gender"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
:items="genderOptions"
|
|
||||||
label="性別"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-date-input
|
|
||||||
v-model="birthdayDate"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="生日"
|
|
||||||
prepend-icon=""
|
|
||||||
prepend-inner-icon="mdi-calendar"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model.number="profileForm.height"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="身高 (公分)"
|
|
||||||
type="number"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model.number="profileForm.weight"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="體重 (公斤)"
|
|
||||||
type="number"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="profileForm.email"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="Email(選填)"
|
|
||||||
type="email"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="profileForm.emergencyContact"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="緊急聯絡人(選填)"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<v-col cols="12" sm="6">
|
|
||||||
<v-text-field
|
|
||||||
v-model="profileForm.emergencyPhone"
|
|
||||||
class="mb-3"
|
|
||||||
color="primary"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
label="緊急聯絡電話(選填)"
|
|
||||||
type="tel"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<v-checkbox
|
|
||||||
v-model="profileForm.consent"
|
|
||||||
class="mt-2 text-caption"
|
|
||||||
color="primary"
|
|
||||||
density="compact"
|
|
||||||
hide-details
|
|
||||||
label="我已閱讀並同意個資授權條款"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 步驟 2:活動量與自評 -->
|
|
||||||
<div v-if="currentStep === 1">
|
|
||||||
<h2
|
<h2
|
||||||
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
|
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
|
||||||
>
|
>
|
||||||
@@ -305,8 +164,8 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步驟 3:身體狀況自評 -->
|
<!-- 步驟 2:身體狀況自評 -->
|
||||||
<div v-if="currentStep === 2">
|
<div v-if="currentStep === 1">
|
||||||
<h2
|
<h2
|
||||||
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
|
class="text-title-medium font-weight-bold text-primary mb-4 d-flex align-center"
|
||||||
>
|
>
|
||||||
@@ -373,8 +232,8 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步驟 4:自評結果 -->
|
<!-- 步驟 3:自評結果 -->
|
||||||
<div v-if="currentStep === 3" class="text-center py-4">
|
<div v-if="currentStep === 2" class="text-center py-4">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
class="elevation-2 mb-4"
|
class="elevation-2 mb-4"
|
||||||
:color="getResultColor(resultLevel)"
|
:color="getResultColor(resultLevel)"
|
||||||
@@ -444,7 +303,7 @@
|
|||||||
|
|
||||||
<v-card-actions class="px-5 py-4 d-flex justify-between">
|
<v-card-actions class="px-5 py-4 d-flex justify-between">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="currentStep > 0 && currentStep < 3"
|
v-if="currentStep > 0 && currentStep < 2"
|
||||||
class="rounded-lg px-4"
|
class="rounded-lg px-4"
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@@ -456,10 +315,9 @@
|
|||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="currentStep < 2"
|
v-if="currentStep < 1"
|
||||||
class="rounded-lg px-6"
|
class="rounded-lg px-6"
|
||||||
color="primary"
|
color="primary"
|
||||||
:disabled="currentStep === 0 && !profileForm.consent"
|
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@click="goToNextStep"
|
@click="goToNextStep"
|
||||||
>
|
>
|
||||||
@@ -467,7 +325,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-else-if="currentStep === 2"
|
v-else-if="currentStep === 1"
|
||||||
class="rounded-lg px-6 text-white"
|
class="rounded-lg px-6 text-white"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -493,7 +351,7 @@
|
|||||||
|
|
||||||
<!-- 返回按鈕 (結果頁專用) -->
|
<!-- 返回按鈕 (結果頁專用) -->
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="currentStep === 3"
|
v-if="currentStep === 2"
|
||||||
block
|
block
|
||||||
class="rounded-lg py-2"
|
class="rounded-lg py-2"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -507,18 +365,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import type {
|
import type {
|
||||||
ChronicDisease,
|
ChronicDisease,
|
||||||
ExerciseFrequency,
|
ExerciseFrequency,
|
||||||
Gender,
|
|
||||||
PainLevel,
|
PainLevel,
|
||||||
RiskLevel,
|
RiskLevel,
|
||||||
SelfAssessmentRequest,
|
SelfAssessmentRequest,
|
||||||
WantedImprovement,
|
WantedImprovement,
|
||||||
} from "@/api/types";
|
} from "@/api/types";
|
||||||
import { CONSENT_VERSION } from "@/api/types";
|
|
||||||
import Layout from "@/components/layout/DefaultLayout.vue";
|
import Layout from "@/components/layout/DefaultLayout.vue";
|
||||||
import PageHeading from "@/components/layout/PageHeading.vue";
|
import PageHeading from "@/components/layout/PageHeading.vue";
|
||||||
import { APPOINTMENT_ENABLED } from "@/config/features";
|
import { APPOINTMENT_ENABLED } from "@/config/features";
|
||||||
@@ -535,36 +391,7 @@ const router = useRouter();
|
|||||||
const currentStep = ref(0);
|
const currentStep = ref(0);
|
||||||
const appointmentLoading = ref(false);
|
const appointmentLoading = ref(false);
|
||||||
const errorMessage = ref("");
|
const errorMessage = ref("");
|
||||||
const stepNames = ["基本資料", "功能狀態", "健康限制", "自評結果"];
|
const stepNames = ["功能狀態", "健康限制", "自評結果"];
|
||||||
|
|
||||||
const profileForm = reactive<{
|
|
||||||
name: string;
|
|
||||||
phone: string;
|
|
||||||
birthday: string;
|
|
||||||
gender: Gender | "";
|
|
||||||
height: number;
|
|
||||||
weight: number;
|
|
||||||
email: string;
|
|
||||||
emergencyContact: string;
|
|
||||||
emergencyPhone: string;
|
|
||||||
consent: boolean;
|
|
||||||
}>({
|
|
||||||
name: store.userProfile.name,
|
|
||||||
phone: store.userProfile.phone,
|
|
||||||
birthday: store.userProfile.birthday,
|
|
||||||
gender:
|
|
||||||
store.userProfile.gender === "男"
|
|
||||||
? "male"
|
|
||||||
: store.userProfile.gender === "女"
|
|
||||||
? "female"
|
|
||||||
: "",
|
|
||||||
height: store.userProfile.height,
|
|
||||||
weight: store.userProfile.weight,
|
|
||||||
email: store.userProfile.email,
|
|
||||||
emergencyContact: store.userProfile.emergencyContact,
|
|
||||||
emergencyPhone: store.userProfile.emergencyPhone,
|
|
||||||
consent: store.userProfile.consent,
|
|
||||||
});
|
|
||||||
|
|
||||||
const assessmentForm = reactive<{
|
const assessmentForm = reactive<{
|
||||||
exerciseFrequency: ExerciseFrequency | "";
|
exerciseFrequency: ExerciseFrequency | "";
|
||||||
@@ -582,10 +409,6 @@ const assessmentForm = reactive<{
|
|||||||
|
|
||||||
const resultLevel = ref<RiskLevel | "">(store.selfAssessment.scoreLevel);
|
const resultLevel = ref<RiskLevel | "">(store.selfAssessment.scoreLevel);
|
||||||
|
|
||||||
const genderOptions = [
|
|
||||||
{ title: "男", value: "male" as const },
|
|
||||||
{ title: "女", value: "female" as const },
|
|
||||||
];
|
|
||||||
const painOptions = [
|
const painOptions = [
|
||||||
{ title: "無", value: "none" as const },
|
{ title: "無", value: "none" as const },
|
||||||
{ title: "輕微", value: "mild" as const },
|
{ title: "輕微", value: "mild" as const },
|
||||||
@@ -617,59 +440,21 @@ const diseaseOptions = [
|
|||||||
{ title: "骨質疏鬆", value: "osteoporosis" as const },
|
{ title: "骨質疏鬆", value: "osteoporosis" as const },
|
||||||
];
|
];
|
||||||
|
|
||||||
const birthdayDate = computed<Date | null>({
|
|
||||||
get: () => parseDateString(profileForm.birthday),
|
|
||||||
set: (value) => {
|
|
||||||
profileForm.birthday = formatDateString(value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const draft = readGuestSelfAssessmentDraft();
|
const draft = readGuestSelfAssessmentDraft();
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
profileForm.name = draft.profile.name;
|
|
||||||
profileForm.phone = draft.profile.phone;
|
|
||||||
profileForm.birthday = draft.profile.birthday;
|
|
||||||
profileForm.gender = draft.profile.gender;
|
|
||||||
profileForm.height = draft.profile.heightCm;
|
|
||||||
profileForm.weight = draft.profile.weightKg;
|
|
||||||
profileForm.email = draft.profile.email ?? "";
|
|
||||||
profileForm.emergencyContact = draft.profile.emergencyContact ?? "";
|
|
||||||
profileForm.emergencyPhone = draft.profile.emergencyPhone ?? "";
|
|
||||||
profileForm.consent = draft.profile.consent;
|
|
||||||
assessmentForm.exerciseFrequency = draft.assessment.exerciseFrequency;
|
assessmentForm.exerciseFrequency = draft.assessment.exerciseFrequency;
|
||||||
assessmentForm.wantedImprovements = [...draft.assessment.wantedImprovements];
|
assessmentForm.wantedImprovements = [...draft.assessment.wantedImprovements];
|
||||||
assessmentForm.hasFalls = draft.assessment.hasFalls;
|
assessmentForm.hasFalls = draft.assessment.hasFalls;
|
||||||
assessmentForm.painLevel = draft.assessment.painLevel;
|
assessmentForm.painLevel = draft.assessment.painLevel;
|
||||||
assessmentForm.chronicDiseases = [...draft.assessment.chronicDiseases];
|
assessmentForm.chronicDiseases = [...draft.assessment.chronicDiseases];
|
||||||
resultLevel.value = draft.provisionalRiskLevel;
|
resultLevel.value = draft.provisionalRiskLevel;
|
||||||
currentStep.value = 3;
|
currentStep.value = 2;
|
||||||
});
|
});
|
||||||
|
|
||||||
function parseDateString(value: string) {
|
|
||||||
const [year, month, day] = value.split("-").map(Number);
|
|
||||||
if (!year || !month || !day) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Date(year, month - 1, day);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateString(value: Date | null) {
|
|
||||||
if (!value) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = value.getFullYear();
|
|
||||||
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(value.getDate()).padStart(2, "0");
|
|
||||||
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleImprovement(improvement: WantedImprovement) {
|
function toggleImprovement(improvement: WantedImprovement) {
|
||||||
const index = assessmentForm.wantedImprovements.indexOf(improvement);
|
const index = assessmentForm.wantedImprovements.indexOf(improvement);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -699,26 +484,9 @@ function goToNextStep() {
|
|||||||
scrollToPageTop();
|
scrollToPageTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(profileForm, clearErrorMessage);
|
|
||||||
watch(assessmentForm, clearErrorMessage);
|
watch(assessmentForm, clearErrorMessage);
|
||||||
|
|
||||||
function buildAssessmentRequest(): SelfAssessmentRequest | null {
|
function buildAssessmentRequest(): SelfAssessmentRequest | null {
|
||||||
if (
|
|
||||||
!profileForm.name.trim() ||
|
|
||||||
!/^09\d{8}$/.test(profileForm.phone.replace(/\D/g, "")) ||
|
|
||||||
!profileForm.birthday ||
|
|
||||||
!profileForm.gender ||
|
|
||||||
profileForm.height < 50 ||
|
|
||||||
profileForm.height > 250 ||
|
|
||||||
profileForm.weight < 20 ||
|
|
||||||
profileForm.weight > 300 ||
|
|
||||||
!profileForm.consent
|
|
||||||
) {
|
|
||||||
errorMessage.value = "請完整填寫基本資料,並同意個資授權條款。";
|
|
||||||
currentStep.value = 0;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!assessmentForm.exerciseFrequency || !assessmentForm.painLevel) {
|
if (!assessmentForm.exerciseFrequency || !assessmentForm.painLevel) {
|
||||||
errorMessage.value = "請完成所有自評題目後再送出。";
|
errorMessage.value = "請完成所有自評題目後再送出。";
|
||||||
return null;
|
return null;
|
||||||
@@ -736,26 +504,12 @@ function buildAssessmentRequest(): SelfAssessmentRequest | null {
|
|||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
errorMessage.value = "";
|
errorMessage.value = "";
|
||||||
const assessment = buildAssessmentRequest();
|
const assessment = buildAssessmentRequest();
|
||||||
if (!assessment || !profileForm.gender) {
|
if (!assessment) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedPhone = profileForm.phone.replace(/\D/g, "");
|
|
||||||
const provisionalRiskLevel = calculateProvisionalRiskLevel(assessment);
|
const provisionalRiskLevel = calculateProvisionalRiskLevel(assessment);
|
||||||
const draft: GuestSelfAssessmentDraft = {
|
const draft: GuestSelfAssessmentDraft = {
|
||||||
profile: {
|
|
||||||
name: profileForm.name.trim(),
|
|
||||||
phone: normalizedPhone,
|
|
||||||
birthday: profileForm.birthday,
|
|
||||||
gender: profileForm.gender,
|
|
||||||
heightCm: profileForm.height,
|
|
||||||
weightKg: profileForm.weight,
|
|
||||||
email: profileForm.email.trim() || null,
|
|
||||||
emergencyContact: profileForm.emergencyContact.trim() || null,
|
|
||||||
emergencyPhone: profileForm.emergencyPhone.replace(/\D/g, "") || null,
|
|
||||||
consent: profileForm.consent,
|
|
||||||
consentVersion: CONSENT_VERSION,
|
|
||||||
},
|
|
||||||
assessment,
|
assessment,
|
||||||
provisionalRiskLevel,
|
provisionalRiskLevel,
|
||||||
};
|
};
|
||||||
@@ -763,7 +517,7 @@ function handleSubmit() {
|
|||||||
saveGuestSelfAssessmentDraft(draft);
|
saveGuestSelfAssessmentDraft(draft);
|
||||||
store.setProvisionalSelfAssessment(assessment);
|
store.setProvisionalSelfAssessment(assessment);
|
||||||
resultLevel.value = provisionalRiskLevel;
|
resultLevel.value = provisionalRiskLevel;
|
||||||
currentStep.value = 3;
|
currentStep.value = 2;
|
||||||
scrollToPageTop();
|
scrollToPageTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
clearGuestSelfAssessmentDraft,
|
clearGuestSelfAssessmentDraft,
|
||||||
readGuestSelfAssessmentDraft,
|
readGuestSelfAssessmentDraft,
|
||||||
saveGuestSelfAssessmentDraft,
|
saveGuestSelfAssessmentDraft,
|
||||||
type GuestSelfAssessmentDraft,
|
|
||||||
} from "@/utils/guest-self-assessment";
|
} from "@/utils/guest-self-assessment";
|
||||||
import { getApiErrorMessage, getApiErrorStatus } from "@/utils/api-error";
|
import { getApiErrorMessage, getApiErrorStatus } from "@/utils/api-error";
|
||||||
import { calculateProvisionalRiskLevel } from "@/utils/self-assessment";
|
import { calculateProvisionalRiskLevel } from "@/utils/self-assessment";
|
||||||
@@ -154,23 +153,6 @@ function applySelfAssessmentToStore(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProfileRequest(
|
|
||||||
profile: GuestSelfAssessmentDraft["profile"],
|
|
||||||
): ProfileRequest {
|
|
||||||
return {
|
|
||||||
name: profile.name,
|
|
||||||
birthday: profile.birthday,
|
|
||||||
gender: profile.gender,
|
|
||||||
heightCm: profile.heightCm,
|
|
||||||
weightKg: profile.weightKg,
|
|
||||||
email: profile.email,
|
|
||||||
emergencyContact: profile.emergencyContact,
|
|
||||||
emergencyPhone: profile.emergencyPhone,
|
|
||||||
consent: profile.consent,
|
|
||||||
consentVersion: profile.consentVersion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function toProfileRequestFromStore(profile: UserProfile): ProfileRequest {
|
function toProfileRequestFromStore(profile: UserProfile): ProfileRequest {
|
||||||
if (profile.gender !== "男" && profile.gender !== "女") {
|
if (profile.gender !== "男" && profile.gender !== "女") {
|
||||||
throw new Error("請先完成性別資料");
|
throw new Error("請先完成性別資料");
|
||||||
@@ -543,10 +525,7 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 將登入/註冊後的匿名訪客自評正式保存;失敗時保留草稿與冪等鍵。 */
|
||||||
* 將登入/註冊後的訪客草稿依序正式保存。
|
|
||||||
* Profile 失敗時不會呼叫自評;任一步驟失敗都保留草稿與冪等鍵。
|
|
||||||
*/
|
|
||||||
async function persistGuestSelfAssessment() {
|
async function persistGuestSelfAssessment() {
|
||||||
const draft = readGuestSelfAssessmentDraft();
|
const draft = readGuestSelfAssessmentDraft();
|
||||||
if (!draft) {
|
if (!draft) {
|
||||||
@@ -563,33 +542,25 @@ export const useAppStore = defineStore("app", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (USE_MOCK) {
|
if (USE_MOCK) {
|
||||||
const profile: Profile = {
|
|
||||||
...toProfileRequest(draft.profile),
|
|
||||||
phone: draft.profile.phone,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
const assessment: SelfAssessmentResponse = {
|
const assessment: SelfAssessmentResponse = {
|
||||||
...draft.assessment,
|
...draft.assessment,
|
||||||
assessmentId: `mock-${idempotencyKey}`,
|
assessmentId: `mock-${idempotencyKey}`,
|
||||||
riskLevel: draft.provisionalRiskLevel,
|
riskLevel: draft.provisionalRiskLevel,
|
||||||
completedAt: new Date().toISOString(),
|
completedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
userProfile.value = applyProfileToStore(profile);
|
|
||||||
setSelfAssessment(assessment);
|
setSelfAssessment(assessment);
|
||||||
clearGuestSelfAssessmentDraft();
|
clearGuestSelfAssessmentDraft();
|
||||||
return { profile, assessment };
|
return assessment;
|
||||||
}
|
}
|
||||||
|
|
||||||
const profile = await apiUpdateProfile(toProfileRequest(draft.profile));
|
|
||||||
const assessment = await createSelfAssessment(
|
const assessment = await createSelfAssessment(
|
||||||
draft.assessment,
|
draft.assessment,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
userProfile.value = applyProfileToStore(profile);
|
|
||||||
setSelfAssessment(assessment);
|
setSelfAssessment(assessment);
|
||||||
clearGuestSelfAssessmentDraft();
|
clearGuestSelfAssessmentDraft();
|
||||||
return { profile, assessment };
|
return assessment;
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerAppointment(appointmentId: string) {
|
function registerAppointment(appointmentId: string) {
|
||||||
|
|||||||
@@ -5,7 +5,4 @@ export {
|
|||||||
readGuestSelfAssessmentDraft,
|
readGuestSelfAssessmentDraft,
|
||||||
saveGuestSelfAssessmentDraft,
|
saveGuestSelfAssessmentDraft,
|
||||||
} from "./self-assessment";
|
} from "./self-assessment";
|
||||||
export type {
|
export type { GuestSelfAssessmentDraft } from "./self-assessment";
|
||||||
GuestSelfAssessmentDraft,
|
|
||||||
GuestSelfAssessmentProfile,
|
|
||||||
} from "./self-assessment";
|
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
import type {
|
import type {
|
||||||
ChronicDisease,
|
ChronicDisease,
|
||||||
ExerciseFrequency,
|
ExerciseFrequency,
|
||||||
Gender,
|
|
||||||
PainLevel,
|
PainLevel,
|
||||||
RiskLevel,
|
RiskLevel,
|
||||||
SelfAssessmentRequest,
|
SelfAssessmentRequest,
|
||||||
WantedImprovement,
|
WantedImprovement,
|
||||||
} from "@/api/types";
|
} from "@/api/types";
|
||||||
|
|
||||||
export interface GuestSelfAssessmentProfile {
|
|
||||||
name: string;
|
|
||||||
phone: string;
|
|
||||||
birthday: string;
|
|
||||||
gender: Gender;
|
|
||||||
heightCm: number;
|
|
||||||
weightKg: number;
|
|
||||||
email: string | null;
|
|
||||||
emergencyContact: string | null;
|
|
||||||
emergencyPhone: string | null;
|
|
||||||
consent: boolean;
|
|
||||||
consentVersion: "v1";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuestSelfAssessmentDraft {
|
export interface GuestSelfAssessmentDraft {
|
||||||
profile: GuestSelfAssessmentProfile;
|
|
||||||
assessment: SelfAssessmentRequest;
|
assessment: SelfAssessmentRequest;
|
||||||
provisionalRiskLevel: RiskLevel;
|
provisionalRiskLevel: RiskLevel;
|
||||||
/** 同一份草稿重送正式自評時沿用同一個 key。 */
|
/** 同一份草稿重送正式自評時沿用同一個 key。 */
|
||||||
@@ -32,7 +16,6 @@ export interface GuestSelfAssessmentDraft {
|
|||||||
|
|
||||||
export const GUEST_SELF_ASSESSMENT_DRAFT_KEY = "guest-self-assessment-draft";
|
export const GUEST_SELF_ASSESSMENT_DRAFT_KEY = "guest-self-assessment-draft";
|
||||||
|
|
||||||
const GENDER_VALUES: Gender[] = ["male", "female"];
|
|
||||||
const EXERCISE_FREQUENCY_VALUES: ExerciseFrequency[] = [
|
const EXERCISE_FREQUENCY_VALUES: ExerciseFrequency[] = [
|
||||||
"none",
|
"none",
|
||||||
"oneToTwo",
|
"oneToTwo",
|
||||||
@@ -79,30 +62,6 @@ function isStringArray<T extends string>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNullableString(value: unknown): value is string | null {
|
|
||||||
return value === null || typeof value === "string";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidProfile(value: unknown): value is GuestSelfAssessmentProfile {
|
|
||||||
if (!isRecord(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
typeof value.name === "string" &&
|
|
||||||
typeof value.phone === "string" &&
|
|
||||||
typeof value.birthday === "string" &&
|
|
||||||
isOneOf(GENDER_VALUES, value.gender) &&
|
|
||||||
typeof value.heightCm === "number" &&
|
|
||||||
typeof value.weightKg === "number" &&
|
|
||||||
isNullableString(value.email) &&
|
|
||||||
isNullableString(value.emergencyContact) &&
|
|
||||||
isNullableString(value.emergencyPhone) &&
|
|
||||||
typeof value.consent === "boolean" &&
|
|
||||||
value.consentVersion === "v1"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidAssessment(value: unknown): value is SelfAssessmentRequest {
|
function isValidAssessment(value: unknown): value is SelfAssessmentRequest {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -123,7 +82,6 @@ function isValidDraft(value: unknown): value is GuestSelfAssessmentDraft {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isValidProfile(value.profile) &&
|
|
||||||
isValidAssessment(value.assessment) &&
|
isValidAssessment(value.assessment) &&
|
||||||
isOneOf(RISK_LEVEL_VALUES, value.provisionalRiskLevel) &&
|
isOneOf(RISK_LEVEL_VALUES, value.provisionalRiskLevel) &&
|
||||||
(value.idempotencyKey === undefined ||
|
(value.idempotencyKey === undefined ||
|
||||||
@@ -150,7 +108,7 @@ export function calculateProvisionalRiskLevel(
|
|||||||
return "low";
|
return "low";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 僅在自評完成並確認送出後保存一份完整訪客草稿。 */
|
/** 僅在自評完成並確認送出後保存不含身分資料的訪客草稿。 */
|
||||||
export function saveGuestSelfAssessmentDraft(
|
export function saveGuestSelfAssessmentDraft(
|
||||||
draft: GuestSelfAssessmentDraft,
|
draft: GuestSelfAssessmentDraft,
|
||||||
): void {
|
): void {
|
||||||
@@ -183,7 +141,23 @@ export function readGuestSelfAssessmentDraft(): GuestSelfAssessmentDraft | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parsedDraft: unknown = JSON.parse(rawDraft);
|
const parsedDraft: unknown = JSON.parse(rawDraft);
|
||||||
return isValidDraft(parsedDraft) ? parsedDraft : null;
|
if (!isValidDraft(parsedDraft)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedDraft: GuestSelfAssessmentDraft = {
|
||||||
|
assessment: {
|
||||||
|
...parsedDraft.assessment,
|
||||||
|
wantedImprovements: [...parsedDraft.assessment.wantedImprovements],
|
||||||
|
chronicDiseases: [...parsedDraft.assessment.chronicDiseases],
|
||||||
|
},
|
||||||
|
provisionalRiskLevel: parsedDraft.provisionalRiskLevel,
|
||||||
|
...(parsedDraft.idempotencyKey
|
||||||
|
? { idempotencyKey: parsedDraft.idempotencyKey }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
saveGuestSelfAssessmentDraft(sanitizedDraft);
|
||||||
|
return sanitizedDraft;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user