Cleanup: list people with no sex set + inline set
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) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
type Deceased = components["schemas"]["DeceasedCandidate"];
|
type Deceased = components["schemas"]["DeceasedCandidate"];
|
||||||
type GenderProp = components["schemas"]["GenderProposal"];
|
type GenderProp = components["schemas"]["GenderProposal"];
|
||||||
type NameIssue = components["schemas"]["NameIssue"];
|
type NameIssue = components["schemas"]["NameIssue"];
|
||||||
|
type Person = components["schemas"]["PersonRead"];
|
||||||
|
|
||||||
const ISSUE_LABEL: Record<string, string> = {
|
const ISSUE_LABEL: Record<string, string> = {
|
||||||
date_in_surname: "date in surname",
|
date_in_surname: "date in surname",
|
||||||
@@ -36,6 +37,9 @@ export default function CleanupPage() {
|
|||||||
const [genMsg, setGenMsg] = useState<string | null>(null);
|
const [genMsg, setGenMsg] = useState<string | null>(null);
|
||||||
const genFile = useRef<HTMLInputElement>(null);
|
const genFile = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// People still missing a sex (manual mop-up)
|
||||||
|
const [unset, setUnset] = useState<Person[] | null>(null);
|
||||||
|
|
||||||
// 3) Name issues
|
// 3) Name issues
|
||||||
const [issues, setIssues] = useState<NameIssue[] | null>(null);
|
const [issues, setIssues] = useState<NameIssue[] | null>(null);
|
||||||
const [edits, setEdits] = useState<Record<string, { given: string; surname: string; on: boolean }>>({});
|
const [edits, setEdits] = useState<Record<string, { given: string; surname: string; on: boolean }>>({});
|
||||||
@@ -77,6 +81,25 @@ export default function CleanupPage() {
|
|||||||
setGenSel(new Set(data.map((g) => g.person_id)));
|
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() {
|
async function guessGender() {
|
||||||
setGenMsg(null);
|
setGenMsg(null);
|
||||||
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/gender/guess", {
|
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.`);
|
setGenMsg(`Set gender on ${data?.updated ?? 0} people.`);
|
||||||
setGender(null);
|
setGender(null);
|
||||||
|
loadUnset();
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadNames = useCallback(async () => {
|
const loadNames = useCallback(async () => {
|
||||||
@@ -113,7 +137,8 @@ export default function CleanupPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadNames();
|
loadNames();
|
||||||
}, [loadNames]);
|
loadUnset();
|
||||||
|
}, [loadNames, loadUnset]);
|
||||||
|
|
||||||
async function applyNames() {
|
async function applyNames() {
|
||||||
const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on);
|
const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on);
|
||||||
@@ -261,6 +286,49 @@ export default function CleanupPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* People still missing a sex */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">
|
||||||
|
People with no sex set{unset ? ` (${unset.length})` : ""}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{unset === null ? (
|
||||||
|
<p className="text-sm text-[var(--muted)]">Loading…</p>
|
||||||
|
) : unset.length === 0 ? (
|
||||||
|
<p className="text-sm text-[var(--muted)]">Everyone has a sex set. 🎉</p>
|
||||||
|
) : (
|
||||||
|
<ul className="max-h-80 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||||
|
{unset.map((p) => (
|
||||||
|
<li key={p.id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||||
|
<a
|
||||||
|
href={`/trees/${treeId}/persons/${p.id}`}
|
||||||
|
className="flex-1 truncate hover:underline"
|
||||||
|
>
|
||||||
|
{p.primary_name ?? "Unnamed"}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
onClick={() => setSex(p.id, "male")}
|
||||||
|
className="rounded px-2 py-0.5 text-xs"
|
||||||
|
style={{ color: "rgb(120,159,172)", border: "1px solid rgb(120,159,172)" }}
|
||||||
|
>
|
||||||
|
♂ Male
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSex(p.id, "female")}
|
||||||
|
className="rounded px-2 py-0.5 text-xs"
|
||||||
|
style={{ color: "rgb(196,138,146)", border: "1px solid rgb(196,138,146)" }}
|
||||||
|
>
|
||||||
|
♀ Female
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 3) Name issues */}
|
{/* 3) Name issues */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user