"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { useParams } from "next/navigation"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 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", date_in_given: "date in given name", no_surname: "no surname", packed_given: "long given name", }; export default function CleanupPage() { const params = useParams<{ id: string }>(); const treeId = params.id; // 1) Deceased by birth year const [year, setYear] = useState(1930); const [deceased, setDeceased] = useState(null); const [decSel, setDecSel] = useState>(new Set()); const [decMsg, setDecMsg] = useState(null); // 2) Gender from source GEDCOM const [gender, setGender] = useState(null); const [genSel, setGenSel] = useState>(new Set()); 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>({}); const [nameMsg, setNameMsg] = useState(null); async function previewDeceased() { setDecMsg(null); const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/deceased", { params: { path: { tree_id: treeId }, query: { born_on_or_before: year } }, }); setDeceased(data ?? []); setDecSel(new Set((data ?? []).map((d) => d.person_id))); } async function applyDeceased() { const ids = [...decSel]; const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/deceased", { params: { path: { tree_id: treeId } }, body: { person_ids: ids }, }); setDecMsg(`Marked ${data?.updated ?? 0} people deceased.`); setDeceased(null); } async function previewGender(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (genFile.current) genFile.current.value = ""; if (!file) return; setGenMsg(null); const fd = new FormData(); fd.append("file", file); const resp = await fetch(`/api/v1/trees/${treeId}/cleanup/gender/preview`, { method: "POST", body: fd, credentials: "include", }); if (resp.ok) { const data: GenderProp[] = await resp.json(); setGender(data); 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", { params: { path: { tree_id: treeId } }, }); setGender(data ?? []); setGenSel(new Set((data ?? []).map((g) => g.person_id))); } async function guessGenderFromSpouse() { setGenMsg(null); const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/gender/from-spouse", { params: { path: { tree_id: treeId } }, }); setGender(data ?? []); setGenSel(new Set((data ?? []).map((g) => g.person_id))); } async function applyGender() { const updates = (gender ?? []) .filter((g) => genSel.has(g.person_id)) .map((g) => ({ person_id: g.person_id, gender: g.proposed_gender })); const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/gender", { params: { path: { tree_id: treeId } }, body: { updates }, }); setGenMsg(`Set gender on ${data?.updated ?? 0} people.`); setGender(null); loadUnset(); } const loadNames = useCallback(async () => { setNameMsg(null); const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/names", { params: { path: { tree_id: treeId } }, }); setIssues(data ?? []); const init: Record = {}; for (const i of data ?? []) { init[i.name_id] = { given: i.given ?? "", surname: i.surname ?? "", on: false }; } setEdits(init); }, [treeId]); useEffect(() => { loadNames(); loadUnset(); }, [loadNames, loadUnset]); async function applyNames() { const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on); const body = { edits: chosen.map((i) => ({ name_id: i.name_id, given: edits[i.name_id].given, surname: edits[i.name_id].surname, })), }; if (!body.edits.length) return; const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/names", { params: { path: { tree_id: treeId } }, body, }); setNameMsg(`Fixed ${data?.updated ?? 0} names.`); loadNames(); } const toggle = (set: Set, id: string, setter: (s: Set) => void) => { const n = new Set(set); if (n.has(id)) n.delete(id); else n.add(id); setter(n); }; return (

Cleanup

Fix common import messes in bulk. Each tool previews its changes — nothing is saved until you apply.

{/* 1) Deceased by year */} Mark deceased by birth year
{decMsg &&

{decMsg}

} {deceased && (

{deceased.length} people born ≤ {year} (not already marked deceased).

    {deceased.map((d) => (
  • toggle(decSel, d.person_id, setDecSel)} /> {d.name} b. {d.birth_year}
  • ))}
{deceased.length > 0 && ( )}
)}
{/* 2) Gender from source */} Set sex from a source GEDCOM

Upload your source .ged (it carries each person’s sex). We match by name and propose sex only for people who don’t have it set.

“Guess from first name” uses a built-in name dictionary for people with no sex set. “Infer from spouse” sets the opposite sex for an unset partner of someone whose sex is known (e.g. a confirmed-male husband ⇒ a female wife) — review before applying, since it assumes opposite-sex couples.

{genMsg &&

{genMsg}

} {gender && (

{gender.length} matches with a sex to set.

    {gender.map((g) => (
  • toggle(genSel, g.person_id, setGenSel)} /> {g.name} {g.proposed_gender}
  • ))}
{gender.length > 0 && ( )}
)}
{/* 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 */} Names that look broken {nameMsg &&

{nameMsg}

} {issues === null ? (

Scanning…

) : issues.length === 0 ? (

No obvious name problems found.

) : (

{issues.length} flagged. Edit given/surname, tick the ones to fix, then apply.

    {issues.map((i) => { const e = edits[i.name_id] ?? { given: "", surname: "", on: false }; return (
  • setEdits((p) => ({ ...p, [i.name_id]: { ...e, on: !e.on } })) } /> setEdits((p) => ({ ...p, [i.name_id]: { ...e, given: ev.target.value } })) } /> setEdits((p) => ({ ...p, [i.name_id]: { ...e, surname: ev.target.value }, })) } /> {ISSUE_LABEL[i.issue] ?? i.issue}
  • ); })}
)}
); }