From a53858f9205cd27e3650b8552d2ce398bf6f4e56 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Mon, 8 Jun 2026 10:43:08 -0400 Subject: [PATCH] Cleanup: list people with no sex set + inline set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "People with no sex set" section to the Cleanup page — lists everyone whose gender is still null with inline ♂ Male / ♀ Female buttons (and a link to their page). Refreshes after the source-match and first-name guess passes, so it's the manual mop-up for whatever those leave behind. Frontend only (reuses person list + PATCH) — no migration. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/app/trees/[id]/cleanup/page.tsx | 70 +++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/frontend/app/trees/[id]/cleanup/page.tsx b/frontend/app/trees/[id]/cleanup/page.tsx index d159d62..a2dd59d 100644 --- a/frontend/app/trees/[id]/cleanup/page.tsx +++ b/frontend/app/trees/[id]/cleanup/page.tsx @@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input"; type Deceased = components["schemas"]["DeceasedCandidate"]; type GenderProp = components["schemas"]["GenderProposal"]; type NameIssue = components["schemas"]["NameIssue"]; +type Person = components["schemas"]["PersonRead"]; const ISSUE_LABEL: Record = { date_in_surname: "date in surname", @@ -36,6 +37,9 @@ export default function CleanupPage() { const [genMsg, setGenMsg] = useState(null); const genFile = useRef(null); + // People still missing a sex (manual mop-up) + const [unset, setUnset] = useState(null); + // 3) Name issues const [issues, setIssues] = useState(null); const [edits, setEdits] = useState>({}); @@ -77,6 +81,25 @@ export default function CleanupPage() { setGenSel(new Set(data.map((g) => g.person_id))); } } + const loadUnset = useCallback(async () => { + const { data } = await api.GET("/api/v1/trees/{tree_id}/persons", { + params: { path: { tree_id: treeId } }, + }); + setUnset( + (data ?? []) + .filter((p) => !p.gender) + .sort((a, b) => (a.primary_name ?? "").localeCompare(b.primary_name ?? "")), + ); + }, [treeId]); + + async function setSex(personId: string, gender: "male" | "female") { + await api.PATCH("/api/v1/trees/{tree_id}/persons/{person_id}", { + params: { path: { tree_id: treeId, person_id: personId } }, + body: { gender }, + }); + setUnset((prev) => (prev ? prev.filter((p) => p.id !== personId) : prev)); + } + async function guessGender() { setGenMsg(null); const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/gender/guess", { @@ -96,6 +119,7 @@ export default function CleanupPage() { }); setGenMsg(`Set gender on ${data?.updated ?? 0} people.`); setGender(null); + loadUnset(); } const loadNames = useCallback(async () => { @@ -113,7 +137,8 @@ export default function CleanupPage() { useEffect(() => { loadNames(); - }, [loadNames]); + loadUnset(); + }, [loadNames, loadUnset]); async function applyNames() { const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on); @@ -261,6 +286,49 @@ export default function CleanupPage() { + {/* People still missing a sex */} + + + + People with no sex set{unset ? ` (${unset.length})` : ""} + + + + {unset === null ? ( +

Loading…

+ ) : unset.length === 0 ? ( +

Everyone has a sex set. 🎉

+ ) : ( +
    + {unset.map((p) => ( +
  • + + {p.primary_name ?? "Unnamed"} + + + +
  • + ))} +
+ )} +
+
+ {/* 3) Name issues */} -- 2.52.0