178 lines
4.6 KiB
TypeScript
178 lines
4.6 KiB
TypeScript
import type {
|
|
ChronicDisease,
|
|
ExerciseFrequency,
|
|
PainLevel,
|
|
RiskLevel,
|
|
SelfAssessmentRequest,
|
|
WantedImprovement,
|
|
} from "@/api/types";
|
|
|
|
export interface GuestSelfAssessmentDraft {
|
|
assessment: SelfAssessmentRequest;
|
|
provisionalRiskLevel: RiskLevel;
|
|
/** 同一份草稿重送正式自評時沿用同一個 key。 */
|
|
idempotencyKey?: string;
|
|
}
|
|
|
|
export const GUEST_SELF_ASSESSMENT_DRAFT_KEY = "guest-self-assessment-draft";
|
|
|
|
const EXERCISE_FREQUENCY_VALUES: ExerciseFrequency[] = [
|
|
"none",
|
|
"oneToTwo",
|
|
"threeOrMore",
|
|
];
|
|
const PAIN_LEVEL_VALUES: PainLevel[] = ["none", "mild", "moderate", "severe"];
|
|
const RISK_LEVEL_VALUES: RiskLevel[] = ["low", "followUp", "consultation"];
|
|
const WANTED_IMPROVEMENT_VALUES: WantedImprovement[] = [
|
|
"strength",
|
|
"balance",
|
|
"cardio",
|
|
"flexibility",
|
|
"agility",
|
|
"bodyComposition",
|
|
];
|
|
const CHRONIC_DISEASE_VALUES: ChronicDisease[] = [
|
|
"hypertension",
|
|
"diabetes",
|
|
"hyperlipidemia",
|
|
"heartDisease",
|
|
"osteoarthritis",
|
|
"osteoporosis",
|
|
];
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
function isOneOf<T extends string>(
|
|
values: readonly T[],
|
|
value: unknown,
|
|
): value is T {
|
|
return typeof value === "string" && values.includes(value as T);
|
|
}
|
|
|
|
function isStringArray<T extends string>(
|
|
values: readonly T[],
|
|
value: unknown,
|
|
): value is T[] {
|
|
return (
|
|
Array.isArray(value) &&
|
|
value.every((item) => isOneOf(values, item)) &&
|
|
new Set(value).size === value.length
|
|
);
|
|
}
|
|
|
|
function isValidAssessment(value: unknown): value is SelfAssessmentRequest {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
isOneOf(EXERCISE_FREQUENCY_VALUES, value.exerciseFrequency) &&
|
|
isStringArray(WANTED_IMPROVEMENT_VALUES, value.wantedImprovements) &&
|
|
typeof value.hasFalls === "boolean" &&
|
|
isOneOf(PAIN_LEVEL_VALUES, value.painLevel) &&
|
|
isStringArray(CHRONIC_DISEASE_VALUES, value.chronicDiseases)
|
|
);
|
|
}
|
|
|
|
function isValidDraft(value: unknown): value is GuestSelfAssessmentDraft {
|
|
if (!isRecord(value)) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
isValidAssessment(value.assessment) &&
|
|
isOneOf(RISK_LEVEL_VALUES, value.provisionalRiskLevel) &&
|
|
(value.idempotencyKey === undefined ||
|
|
typeof value.idempotencyKey === "string")
|
|
);
|
|
}
|
|
|
|
/** 依後端規則計算訪客階段的暫存結果;正式結果仍以後端回應為準。 */
|
|
export function calculateProvisionalRiskLevel(
|
|
assessment: SelfAssessmentRequest,
|
|
): RiskLevel {
|
|
if (
|
|
assessment.painLevel === "moderate" ||
|
|
assessment.painLevel === "severe" ||
|
|
assessment.chronicDiseases.length >= 2
|
|
) {
|
|
return "consultation";
|
|
}
|
|
|
|
if (assessment.exerciseFrequency === "none" || assessment.hasFalls === true) {
|
|
return "followUp";
|
|
}
|
|
|
|
return "low";
|
|
}
|
|
|
|
/** 僅在自評完成並確認送出後保存不含身分資料的訪客草稿。 */
|
|
export function saveGuestSelfAssessmentDraft(
|
|
draft: GuestSelfAssessmentDraft,
|
|
): void {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
window.sessionStorage.setItem(
|
|
GUEST_SELF_ASSESSMENT_DRAFT_KEY,
|
|
JSON.stringify(draft),
|
|
);
|
|
} catch {
|
|
// 儲存空間不可用時不讓表單提交流程因瀏覽器安全設定而崩潰。
|
|
}
|
|
}
|
|
|
|
/** 讀取並驗證訪客草稿;格式不正確的資料視為不存在。 */
|
|
export function readGuestSelfAssessmentDraft(): GuestSelfAssessmentDraft | null {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const rawDraft = window.sessionStorage.getItem(
|
|
GUEST_SELF_ASSESSMENT_DRAFT_KEY,
|
|
);
|
|
if (!rawDraft) {
|
|
return null;
|
|
}
|
|
|
|
const parsedDraft: unknown = JSON.parse(rawDraft);
|
|
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 {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** 正式保存成功後清除訪客草稿;登入取消或 API 失敗時不應呼叫。 */
|
|
export function clearGuestSelfAssessmentDraft(): void {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
window.sessionStorage.removeItem(GUEST_SELF_ASSESSMENT_DRAFT_KEY);
|
|
} catch {
|
|
// sessionStorage 不可用時沒有可清除的資料,保持流程可繼續。
|
|
}
|
|
}
|