"use client"; import { useState, useRef } from "react"; import { trpc } from "~/lib/trpc/client.js"; import { parseSpreadsheet, isSpreadsheetFile } from "~/lib/excel.js"; type ImportStage = "idle" | "preview" | "importing" | "done"; interface ImportResult { total: number; created: number; updated: number; errors: { row: number; message: string }[]; dryRun: boolean; message?: string; } interface Props { onClose: () => void; } export function ImportModal({ onClose }: Props) { const [stage, setStage] = useState("idle"); const [rows, setRows] = useState[]>([]); const [fileName, setFileName] = useState(""); const [fileError, setFileError] = useState(""); const [result, setResult] = useState(null); const [dryRun, setDryRun] = useState(true); const fileInputRef = useRef(null); const importMutation = trpc.importExport.importCSV.useMutation({ onSuccess: (data) => { setResult(data); setStage("done"); }, onError: (err) => { setFileError(err.message); setStage("preview"); }, }); async function handleFileChange(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setFileError(""); setRows([]); setResult(null); if (!isSpreadsheetFile(file)) { setFileError("Unsupported file type. Please upload an Excel (.xlsx, .xls) or CSV file."); return; } setFileName(file.name); try { const parsed = await parseSpreadsheet(file); setRows(parsed); setStage("preview"); } catch (err) { setFileError(err instanceof Error ? err.message : "Failed to parse file."); } } function handleImport() { if (rows.length === 0) return; setStage("importing"); importMutation.mutate({ entityType: "resources", rows, dryRun, }); } function handleReset() { setStage("idle"); setRows([]); setFileName(""); setFileError(""); setResult(null); if (fileInputRef.current) { fileInputRef.current.value = ""; } } const previewHeaders = rows.length > 0 ? Object.keys(rows[0]!) : []; const previewRows = rows.slice(0, 5); return (
{/* Header */}

Import Resources

{/* Body */}
{/* File picker */} {stage === "idle" && (

Upload an Excel or CSV file to import resources. The first row must contain column headers matching the resource fields (e.g.{" "} eid, displayName, email, chapter, lcrCents ).

{fileError && (

{fileError}

)}
)} {/* Preview */} {(stage === "preview" || stage === "importing") && rows.length > 0 && (

{fileName}

{rows.length} row{rows.length !== 1 ? "s" : ""} parsed

{previewHeaders.length > 0 && (

Preview (first {previewRows.length} of {rows.length} rows)

{previewHeaders.map((h) => ( ))} {previewRows.map((row, i) => ( {previewHeaders.map((h) => ( ))} ))}
{h}
{row[h] ?? ""}
{rows.length > 5 && (

…and {rows.length - 5} more rows

)}
)} {fileError && (

{fileError}

)} {/* Import options */}
setDryRun(e.target.checked)} className="rounded border-gray-300" />
)} {/* Done */} {stage === "done" && result && (
0 ? "bg-yellow-50 border border-yellow-200" : "bg-green-50 border border-green-200"}`}>

{result.dryRun ? "Dry run complete" : "Import complete"}

  • Total rows: {result.total}
  • {!result.dryRun && ( <>
  • Created: {result.created}
  • Updated: {result.updated}
  • )} {result.message &&
  • {result.message}
  • } {result.errors.length > 0 && (
  • Errors: {result.errors.length}
  • )}
{result.errors.length > 0 && (

Errors

    {result.errors.map((e, i) => (
  • Row {e.row}: {e.message}
  • ))}
)} {result.dryRun && result.errors.length === 0 && (
{ setDryRun(false); setStage("preview"); setResult(null); }} className="rounded border-gray-300" />
)}
)}
{/* Footer */}
{(stage === "preview") && ( )} {stage === "importing" && ( Importing… )} {stage === "done" && ( )}
); }