fix(people): 本人 can edit their own birth data in 星盘档案 (S1, BUG-1081)

The owner had no edit entry anywhere once /people replaced the settings
chart pane. 本人 now gets 编辑 with the same form; the save goes through
PATCH /api/account via a shared lib/self-profile-save.ts that the home
onboarding also uses. The dead saveProfile is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
This commit is contained in:
Jesse_Chen
2026-09-29 00:43:43 +08:00
co-authored by Claude Opus 5.5
parent adbb205440
commit 8415fdca72
18 changed files with 337 additions and 106 deletions
+54 -12
View File
@@ -9,6 +9,10 @@ import { ChartProfileForm } from "@/components/chart-profile-form";
import { SecondaryPageShell } from "@/components/secondary-page-shell";
import { bindCurrentSubjectAccount, invalidateSubjectCatalog, setCurrentSubject, useCurrentSubject } from "@/lib/current-subject";
import { fetchAccount } from "@/lib/home-cloud-sync";
import { clearHomeWarmSnapshot } from "@/lib/home-warm-snapshot";
import { invalidateEphemerisPage, pinChartSnapshotIdentity } from "@/lib/secondary-page-data";
import { saveSelfGender, saveSelfProfile } from "@/lib/self-profile-save";
import { useSessionList } from "@/hooks/use-session-list";
import {
missingOtherProfileStep,
profileBirthTimeLabel,
@@ -41,8 +45,6 @@ const DELETE_FAILED = "删除失败,请重试";
const NEW_PERSON_PARAM = "new";
const LIMIT_COPY = "最多保存 5 个人的星盘";
/** 本人 has no edit view here; the owner's gender is set in 设置 → 个人资料. */
const SELF_GENDER_NOTE = "在「设置 → 个人资料」里改";
/** D1: below this width the page is list → detail; at and above it, two columns. */
const PEOPLE_TWO_COLUMN_QUERY = "(min-width: 1024px)";
@@ -82,6 +84,8 @@ export function PeoplePage() {
const [pendingDelete, setPendingDelete] = useState<Usage | null>(null);
const [saving, setSaving] = useState(false);
const pushedDetail = useRef(false);
const accountIdRef = useRef("");
const { setAccount } = useSessionList();
async function load() {
setStatus("loading");
@@ -98,6 +102,7 @@ export function PeoplePage() {
const profile = readProfile(row.profile);
return [{ id: row.id, role: "other" as const, profile, incomplete: incompleteProfile(profile) }];
});
accountIdRef.current = account.user.id;
bindCurrentSubjectAccount(account.user.id, others.map((person) => person.id));
setPeople([
{ id: "self", role: "self", profile: self, incomplete: incompleteProfile(self) },
@@ -220,6 +225,42 @@ export function PeoplePage() {
}
}
/**
* 本人 (BUG-1081). The owner is never a `chart_profiles` row: birth data goes
* through the one account write (`PATCH /api/account`, `birth_time_source`
* checked there), gender through its own one-field PATCH. Then everything
* that painted the old birth data forgets it: the layout's account, Home's
* warm snapshot and today's card, the chart and ephemeris caches.
*/
async function saveSelf(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const current = people.find((person) => person.role === "self");
if (!current || missingOtherProfileStep(draft) || saving) return;
setSaving(true);
setNotice("");
try {
const saved = await saveSelfProfile(draft);
const gender = draft.gender ?? null;
if (gender !== (current.profile.gender ?? null)) await saveSelfGender(gender);
const profile: Profile = { ...saved.profile, gender };
if (saved.fingerprint) pinChartSnapshotIdentity({ accountId: accountIdRef.current, fingerprint: saved.fingerprint });
clearHomeWarmSnapshot();
invalidateEphemerisPage();
setAccount((account) => account ? {
...account,
profile,
...(saved.fingerprint ? { chartBirth: { fingerprint: saved.fingerprint } } : {}),
} : account);
setEditing(false);
invalidateSubjectCatalog();
await load();
} catch (caught) {
setNotice(caught instanceof Error && caught.message ? caught.message : "保存失败,请重试");
} finally {
setSaving(false);
}
}
async function askDelete() {
if (!selected || selected.role !== "other") return;
setPendingDelete(null);
@@ -285,6 +326,7 @@ export function PeoplePage() {
onCancelEdit={() => setEditing(false)}
onDraftChange={setDraft}
saveOther={saveOther}
saveSelf={saveSelf}
askDelete={askDelete}
onCancelDelete={() => setPendingDelete(null)}
confirmDelete={confirmDelete}
@@ -323,6 +365,7 @@ export type PeopleArchiveViewProps = {
readonly onCancelEdit: () => void;
readonly onDraftChange: Dispatch<SetStateAction<Profile>>;
readonly saveOther: (event: FormEvent<HTMLFormElement>) => Promise<void>;
readonly saveSelf: (event: FormEvent<HTMLFormElement>) => Promise<void>;
readonly askDelete: () => Promise<void>;
readonly onCancelDelete: () => void;
readonly confirmDelete: () => Promise<void>;
@@ -352,6 +395,7 @@ export function PeopleArchiveView({
onCancelEdit,
onDraftChange,
saveOther,
saveSelf,
askDelete,
onCancelDelete,
confirmDelete,
@@ -359,6 +403,7 @@ export function PeopleArchiveView({
}: PeopleArchiveViewProps) {
const remaining = MAX_SAVED_SUBJECTS - people.length;
const deleteNotice = notice === DELETE_USAGE_UNAVAILABLE || notice === DELETE_FAILED;
const editingSelf = editing && selectedId !== "" && selected?.role === "self";
return (
<div className="people-archive" data-view={view}>
<section className="people-archive-list" aria-label="星盘档案">
@@ -399,14 +444,14 @@ export function PeopleArchiveView({
</button>
{editing ? (
<ChartProfileForm
title={selectedId ? "编辑人物" : "添加一个人"}
description="称呼和出生资料。填报的出生时间保存后不能再改。"
title={editingSelf ? "编辑我的资料" : selectedId ? "编辑人物" : "添加一个人"}
description={editingSelf ? "称呼和出生资料。" : "称呼和出生资料。填报的出生时间保存后不能再改。"}
value={draft}
onChange={onDraftChange}
nameInputId="people-archive-name"
showAyanamsa
showGender
onSubmit={(event) => void saveOther(event)}
onSubmit={(event) => void (editingSelf ? saveSelf(event) : saveOther(event))}
onCancel={onCancelEdit}
submitLabel={saving ? "保存中" : "保存"}
submitDisabled={saving || Boolean(missingOtherProfileStep(draft))}
@@ -438,10 +483,7 @@ export function PeopleArchiveView({
<dt>岁差</dt>
<dd>{peopleAyanamsaLabel(selected.profile)}</dd>
<dt>性别</dt>
<dd>
{profileGenderLabel(selected.profile.gender)}
{selected.role === "self" ? <small className="people-archive-fact-note">{SELF_GENDER_NOTE}</small> : null}
</dd>
<dd>{profileGenderLabel(selected.profile.gender)}</dd>
</dl>
<div className="people-archive-actions">
<button type="button" onClick={() => openPerson(selected.id, newChatHref())}>
@@ -457,10 +499,10 @@ export function PeopleArchiveView({
{selected.role === "other" ? (
<button type="button" className="people-archive-secondary" onClick={() => window.location.assign(`/?synastry=${selected.id}`)}>和我合盘</button>
) : null}
{/* BUG-1081: 本人 is edited here too; only deleting stays off. */}
<div className="people-archive-foot">
{selected.role === "self" ? <p>本人不能删除</p> : (
<button type="button" className="people-archive-secondary" onClick={() => onEdit(selected.profile)}>编辑</button>
)}
{selected.role === "self" ? <p>本人不能删除</p> : null}
<button type="button" className="people-archive-secondary" onClick={() => onEdit(selected.profile)}>编辑</button>
</div>
{notice ? <p className="form-error" role="alert">{notice}</p> : null}
</div>