"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"]; 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); // 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))); } } 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); } 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(); }, [loadNames]); 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.

{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 && ( )}
)}
{/* 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}
  • ); })}
)}
); }