"use client"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; 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 Report = { counts: Record; unmapped_tags: string[] }; type Preview = components["schemas"]["ImportPreview"]; type Dup = components["schemas"]["DuplicateMatch"]; type Tree = components["schemas"]["TreeRead"]; type Action = "new" | "skip" | "merge" | "overwrite"; const ACTIONS: { value: Action; label: string }[] = [ { value: "new", label: "Import as new" }, { value: "merge", label: "Merge into existing" }, { value: "skip", label: "Skip (use existing)" }, { value: "overwrite", label: "Overwrite existing" }, ]; const fieldCls = "h-9 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm"; /** * GEDCOM import with a selectable destination (a new tree, or an existing one). * Importing into an existing tree previews likely duplicates so each can be * resolved (new / skip / merge / overwrite) before anything is written. * * @param fixedTreeId When set, the destination defaults to that tree (the * tree-scoped Import page). When omitted (the global Import page), the user * picks a destination. */ export function GedcomImport({ fixedTreeId }: { fixedTreeId?: string }) { const [trees, setTrees] = useState([]); const [mode, setMode] = useState<"new" | "existing">(fixedTreeId ? "existing" : "new"); const [chosenTreeId, setChosenTreeId] = useState(fixedTreeId ?? ""); const [newName, setNewName] = useState(""); const [busy, setBusy] = useState(false); const [report, setReport] = useState(null); const [importedTreeId, setImportedTreeId] = useState(null); const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [resolutions, setResolutions] = useState>({}); const fileRef = useRef(null); useEffect(() => { api.GET("/api/v1/trees").then((r) => setTrees(r.data ?? [])); }, []); function resetRun() { setReport(null); setImportedTreeId(null); setPreview(null); setFile(null); setResolutions({}); } async function postImport(tid: string, f: File, resolutionsJson?: string) { const fd = new FormData(); fd.append("file", f); if (resolutionsJson) fd.append("resolutions", resolutionsJson); const resp = await fetch(`/api/v1/trees/${tid}/gedcom/import`, { method: "POST", body: fd, credentials: "include", }); if (resp.ok) { setReport(await resp.json()); setImportedTreeId(tid); } } async function onFile(e: React.ChangeEvent) { const f = e.target.files?.[0]; if (fileRef.current) fileRef.current.value = ""; if (!f) return; setBusy(true); resetRun(); if (mode === "new") { const { data } = await api.POST("/api/v1/trees", { body: { name: newName.trim() || "Imported tree" }, }); if (data) await postImport(data.id, f); setBusy(false); return; } if (!chosenTreeId) { setBusy(false); return; } // Existing tree — preview duplicates first. setFile(f); const fd = new FormData(); fd.append("file", f); const resp = await fetch(`/api/v1/trees/${chosenTreeId}/gedcom/preview`, { method: "POST", body: fd, credentials: "include", }); if (resp.ok) { const pv: Preview = await resp.json(); setPreview(pv); const init: Record = {}; for (const d of pv.potential_duplicates) init[d.xref] = d.score === "high" ? "merge" : "new"; setResolutions(init); } setBusy(false); } async function runImport() { if (!file || !chosenTreeId) return; setBusy(true); const map: Record = {}; for (const d of preview?.potential_duplicates ?? []) { const action = resolutions[d.xref] ?? "new"; if (action !== "new") map[d.xref] = { action, target_id: d.existing_person_id }; } await postImport(chosenTreeId, file, JSON.stringify(map)); setPreview(null); setFile(null); setBusy(false); } const dups = preview?.potential_duplicates ?? []; return ( Import a GEDCOM file {!preview && ( <> {mode === "new" && ( setNewName(e.target.value)} /> )} {mode === "existing" && ( )} )} {/* Duplicate-resolution step */} {preview && (
{Object.entries(preview.counts).map(([k, v]) => ( {v} {k} ))}
{dups.length === 0 ? (

No likely duplicates found — everyone will be imported as new.

) : (

{dups.length} possible duplicate{dups.length === 1 ? "" : "s"}

    {dups.map((d: Dup) => (
  • {d.incoming_name} {d.incoming_birth_year && ( b. {d.incoming_birth_year} )} {d.existing_name} {d.existing_birth_year && ( b. {d.existing_birth_year} )} {d.score}
  • ))}
)}
)} {report && (
Import complete
{Object.entries(report.counts).map(([k, v]) => ( {v} {k} ))}
{report.unmapped_tags.length > 0 && (
Unmapped tags (skipped): {report.unmapped_tags.join(", ")}
)} {importedTreeId && ( Open the tree → )}
)}
); }