- Sidebar 「生时校正」 under 星盘, behind a new account `rectification` flag. - /rectification page: status card from entry-summary (not started / in progress with window and confirmed event count / adopted time), a short data check, then the home rectification surface (?rectify=1, or ?rectify=new to start over; ?c=<id> to continue or view). No price on the page. - entry-summary gains session id, candidate range, confirmed evidence count and adopted times (add-only migration 20261028170000). - The offer card under a consultation answer becomes one line and a link to /rectification; an explicit request in chat also goes there. No case is opened from a conversation any more. - Starter home shows a rectification prompt for self charts with an estimated time; onboarding checklist gains 做一次生时校正 for them. - Chart-page link points to /rectification. Shell registers its router so home hooks can navigate client-side without useRouter in Home(). - Docs: VOICE, DESIGN, CHANGELOG, device checklist. Self profile only; other people are issue #160. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
137 lines
5.3 KiB
TypeScript
137 lines
5.3 KiB
TypeScript
/**
|
|
* The standalone /rectification page (issue #145). Which card it shows comes
|
|
* from the server's entry summary and the account; the page never guesses
|
|
* from session lists. Self profile only (other people: issue #160).
|
|
*/
|
|
|
|
import { rectificationEntryHref, sessionHref } from "./chat-session-url.ts";
|
|
import { blocksNewRectification } from "./rectification-in-chat.ts";
|
|
import type { RectificationEntrySummary } from "./rectification-entry.ts";
|
|
|
|
export type RectificationPageState =
|
|
| { readonly kind: "signed-out" }
|
|
| { readonly kind: "unavailable" }
|
|
| { readonly kind: "self-missing" }
|
|
| { readonly kind: "none" }
|
|
| {
|
|
readonly kind: "active";
|
|
readonly sessionId: string | null;
|
|
readonly range: { readonly start: string; readonly end: string } | null;
|
|
readonly evidenceCount: number | null;
|
|
}
|
|
| { readonly kind: "done"; readonly sessionId: string | null; readonly time: string };
|
|
|
|
/**
|
|
* The birth time is only an estimate and no time has been adopted yet. Drives
|
|
* the onboarding prompt and the checklist item (issue #145). A certificate or
|
|
* a self-declared exact minute does not count; nor does an adopted time.
|
|
*/
|
|
export function birthTimeNeedsRectification(profile: {
|
|
readonly birthTimeSource: string;
|
|
readonly birthTimeStatus: string;
|
|
}): boolean {
|
|
if (profile.birthTimeStatus === "accepted" || profile.birthTimeStatus === "confirmed") return false;
|
|
return birthTimeWasEstimated(profile);
|
|
}
|
|
|
|
/** The self birth time came from an estimate, adopted since or not (checklist item, issue #145). */
|
|
export function birthTimeWasEstimated(profile: { readonly birthTimeSource: string }): boolean {
|
|
return profile.birthTimeSource === "approximate"
|
|
|| profile.birthTimeSource === "period_only"
|
|
|| profile.birthTimeSource === "unknown";
|
|
}
|
|
|
|
export const RECTIFICATION_HOME_HINT = {
|
|
text: "出生时间还不确定,可以先做生时校正。",
|
|
link: "去校正",
|
|
} as const;
|
|
|
|
export const RECTIFICATION_PAGE_COPY = {
|
|
title: "生时校正",
|
|
intro: "出生时间不确定时,用几件记得日期的大事,把钟点一步步收窄。",
|
|
waiting: "校正记录还在读",
|
|
loadFailed: "校正记录这会儿读不到。",
|
|
retry: "再试一次",
|
|
signedOut: "登录后可以做生时校正。",
|
|
signIn: "去登录",
|
|
unavailable: "生时校正这会儿还做不了。",
|
|
selfMissing: "先把你自己的出生日期、时间和地点填好,才能做生时校正。",
|
|
fillSelf: "去填资料",
|
|
currentTime: "你现在用的时间",
|
|
start: "开始",
|
|
activeLabel: "进行中",
|
|
windowLabel: "时间窗",
|
|
evidenceLabel: "已收大事",
|
|
resume: "继续",
|
|
doneLabel: "已采用",
|
|
doneNote: "之后排盘、咨询和报告都用这个时间。",
|
|
viewResult: "查看结果",
|
|
again: "再校正一次",
|
|
back: "返回",
|
|
checkTitle: "先核对一下资料",
|
|
birthDate: "出生日期",
|
|
birthTime: "出生时间",
|
|
birthPlace: "出生城市",
|
|
confirmStart: "开始校正",
|
|
editProfile: "改这份",
|
|
} as const;
|
|
|
|
/** `07:42:00` → `07:42`. Anything else is returned trimmed. */
|
|
export function clockLabel(value: string): string {
|
|
const match = /^(\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?$/.exec(value.trim());
|
|
return match ? `${match[1]}:${match[2]}` : value.trim();
|
|
}
|
|
|
|
export function evidenceCountLabel(count: number): string {
|
|
return `${count} 件`;
|
|
}
|
|
|
|
export function rectificationPageState(input: {
|
|
readonly signedOut: boolean;
|
|
/** `account.rectification`; missing means the flag did not arrive, so the page stays closed. */
|
|
readonly enabled: boolean | undefined;
|
|
readonly selfComplete: boolean;
|
|
readonly summary: RectificationEntrySummary;
|
|
}): RectificationPageState {
|
|
if (input.signedOut) return { kind: "signed-out" };
|
|
if (input.enabled !== true) return { kind: "unavailable" };
|
|
if (!input.selfComplete) return { kind: "self-missing" };
|
|
|
|
const open = input.summary.latestResumable;
|
|
if (open && blocksNewRectification(open.status)) {
|
|
const range = open.candidateRange
|
|
? { start: clockLabel(open.candidateRange.startTime), end: clockLabel(open.candidateRange.endTime) }
|
|
: null;
|
|
return {
|
|
kind: "active",
|
|
sessionId: open.sessionId ?? null,
|
|
range,
|
|
evidenceCount: open.evidenceCount ?? null,
|
|
};
|
|
}
|
|
// An accepted case is a finished stop (BUG-1423), even though the shared
|
|
// resumable set still lists its status.
|
|
if (open && open.status === "candidate_accepted" && open.acceptedTime) {
|
|
return { kind: "done", sessionId: open.sessionId ?? null, time: clockLabel(open.acceptedTime) };
|
|
}
|
|
const finished = input.summary.latestTerminal;
|
|
const finishedTime = finished?.confirmedTime ?? finished?.acceptedTime ?? null;
|
|
if (finished && finished.hasUsableTime && finishedTime) {
|
|
return { kind: "done", sessionId: finished.sessionId ?? null, time: clockLabel(finishedTime) };
|
|
}
|
|
return { kind: "none" };
|
|
}
|
|
|
|
/** Where 开始校正 goes: a fresh case after a finished one, otherwise the homepage opener. */
|
|
export function rectificationStartHref(state: RectificationPageState): string {
|
|
return rectificationEntryHref(state.kind === "done" ? "new" : "homepage");
|
|
}
|
|
|
|
/**
|
|
* 继续 / 查看结果: the case's own conversation. Without a session id (an older
|
|
* server) the homepage opener still resumes the open case.
|
|
*/
|
|
export function rectificationSessionHref(sessionId: string | null): string {
|
|
return sessionId ? sessionHref("", sessionId) : rectificationEntryHref("homepage");
|
|
}
|