Files
Jyotisha/frontend/src/components/birth-time-intake.tsx
T
Jesse_Chen 995da2d4b4
Independent Staging Quality Gate / validate (push) Successful in 20m10s
Independent Staging Quality Gate / publish (push) Successful in 8m46s
fix(onboarding): restore uncertain birth-time intake
2026-08-15 19:29:33 +08:00

233 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useId, useState } from "react";
import { BirthDatePicker } from "@/components/birth-date-picker";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { birthTimeConsultationOptionsCopy } from "@/lib/birth-time-consultation-consent";
import {
birthTimeDisplayState,
birthTimePeriodOptions,
birthTimeSourceDefaults,
birthTimeSourceOptions,
type BirthTimeDraft,
type BirthTimeDraftPatch,
} from "@/lib/birth-time-intake-model";
type BirthTimeIntakeProps = {
readonly value: BirthTimeDraft;
readonly onPatch: (patch: BirthTimeDraftPatch) => void;
};
const hours = Array.from({ length: 24 }, (_, index) => String(index).padStart(2, "0"));
const minutes = Array.from({ length: 60 }, (_, index) => String(index).padStart(2, "0"));
function splitBirthTime(value: string) {
const [hour = "", minute = ""] = value.split(":");
return { hour, minute };
}
type BirthClockSelectProps = {
readonly value: string;
readonly onChange: (value: string) => void;
};
function BirthClockSelect({ value, onChange }: BirthClockSelectProps) {
const currentTime = value || "";
const [timeParts, setTimeParts] = useState(() => splitBirthTime(currentTime));
const patchTime = (part: "hour" | "minute", nextValue: string | null) => {
if (nextValue === null) return;
const nextParts = { ...timeParts, [part]: nextValue };
setTimeParts(nextParts);
if (nextParts.hour && nextParts.minute) onChange(`${nextParts.hour}:${nextParts.minute}`);
};
return (
<div className="birth-time-clock-selects" aria-label="选择出生时间">
<Select required value={timeParts.hour || null} onValueChange={(nextValue) => patchTime("hour", nextValue)}>
<SelectTrigger aria-label="出生时间小时">
<SelectValue placeholder="时" />
</SelectTrigger>
<SelectContent className="birth-time-clock-menu" style={{ width: 108, minWidth: 108 }}>
{hours.map((hour) => <SelectItem key={hour} value={hour}>{hour}</SelectItem>)}
</SelectContent>
</Select>
<span className="birth-time-clock-separator" aria-hidden="true">:</span>
<Select required value={timeParts.minute || null} onValueChange={(nextValue) => patchTime("minute", nextValue)}>
<SelectTrigger aria-label="出生时间分钟">
<SelectValue placeholder="分" />
</SelectTrigger>
<SelectContent className="birth-time-clock-menu" style={{ width: 108, minWidth: 108 }}>
{minutes.map((minute) => <SelectItem key={minute} value={minute}>{minute}</SelectItem>)}
</SelectContent>
</Select>
</div>
);
}
export function BirthTimeIntakeFields({ value, onPatch }: BirthTimeIntakeProps) {
const groupId = useId();
const source = value.birthTimeSource;
const isConfirmed = value.birthTimeStatus === "confirmed";
const displayState = birthTimeDisplayState(value);
const knowledgeMode = source === "period_only" || source === "unknown"
? "uncertain"
: source ? "exact" : "";
const usesClockTime = source === "hospital_record"
|| source === "family_exact"
|| source === "approximate"
|| source === "legacy_import";
return (
<div className="birth-time-intake">
{displayState && (
<section className="birth-time-profile-result" aria-label="生时校正结果">
<div className="birth-time-profile-result-heading">
<span></span>
<strong>{displayState.kind === "candidate" ? "候选时间" : displayState.kind === "accepted" ? "校正采用" : "引擎确认"}</strong>
</div>
<dl>
<div>
<dt>{displayState.kind === "candidate" ? "待验证候选" : "当前排盘时间"}</dt>
<dd>{displayState.activeTime}</dd>
</div>
<div>
<dt></dt>
<dd>{displayState.reportedLabel}</dd>
</div>
</dl>
{displayState.kind === "candidate" && (
<p>{birthTimeConsultationOptionsCopy(value)}</p>
)}
{displayState.kind === "accepted" && (
<p>使</p>
)}
</section>
)}
<BirthDatePicker
value={value.date}
disabled={isConfirmed}
onChange={(date) => onPatch({ date })}
/>
{source === "legacy_import" && (
<p className="birth-time-legacy-note">
</p>
)}
{!isConfirmed && <fieldset className="birth-time-source-fieldset">
<legend></legend>
<p className="birth-time-source-intro"></p>
<div className="birth-time-source-list">
{birthTimeSourceOptions.map((option) => (
<label
className={`birth-time-source-option ${option.value === "family_exact"
? knowledgeMode === "exact" ? "is-selected" : ""
: knowledgeMode === "uncertain" ? "is-selected" : ""}`}
key={option.value}
>
<input
checked={option.value === "family_exact"
? knowledgeMode === "exact"
: knowledgeMode === "uncertain"}
name={`birth-time-source-${groupId}`}
type="radio"
value={option.value}
onChange={() => onPatch({
birthTimeSource: option.value,
birthTimeStatus: "reported",
time: "",
...birthTimeSourceDefaults[option.value],
})}
/>
<span>
<b>{option.label}</b>
<small>{option.hint}</small>
</span>
</label>
))}
</div>
</fieldset>}
{usesClockTime && (
<div className="birth-time-detail-grid onboarding-card-reveal">
<label>
<span>{isConfirmed ? "当前排盘时间" : source === "approximate" ? "大概时间" : "记录时间"}</span>
<BirthClockSelect
key={`${source}-${value.reportedTime || value.time}`}
value={value.reportedTime || value.time}
onChange={(reportedTime) => onPatch({ reportedTime })}
/>
</label>
{source === "hospital_record" && (
<p className="birth-time-detail-note"> 2 D9 / D10 </p>
)}
</div>
)}
{source === "period_only" && (
<div className="birth-time-detail-grid birth-time-period-details onboarding-card-reveal">
<label>
<span></span>
<Select
required
value={value.birthTimePeriod || null}
onValueChange={(nextValue) => {
if (typeof nextValue === "string") onPatch({ birthTimePeriod: nextValue });
}}
>
<SelectTrigger aria-label="最接近的时间范围">
<SelectValue placeholder="请选择大致时段">
{(selectedValue) => birthTimePeriodOptions.find((option) => option.value === selectedValue)?.label ?? "请选择大致时段"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{birthTimePeriodOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
))}
</SelectContent>
</Select>
</label>
<label>
<span></span>
<textarea
maxLength={240}
placeholder="例如:天刚亮、午饭前后、家人记得大约 6—8 点"
rows={2}
value={value.birthTimeClue}
onChange={(event) => onPatch({ birthTimeClue: event.target.value })}
/>
</label>
<button
className="button-secondary birth-time-skip-button"
type="button"
onClick={() => onPatch({
birthTimeSource: "unknown",
reportedTime: "",
birthTimePeriod: "",
birthTimeClue: "",
uncertaintyBeforeMinutes: null,
uncertaintyAfterMinutes: null,
birthTimeStatus: "reported",
time: "",
})}
></button>
</div>
)}
{source === "unknown" && (
<div className="birth-time-detail-note onboarding-card-reveal" role="status">
<p>使</p>
<button
className="button-secondary"
type="button"
onClick={() => onPatch({ birthTimeSource: "period_only", birthTimeStatus: "reported" })}
></button>
</div>
)}
</div>
);
}