"use client"; import { useState, useRef, useCallback } from "react"; import { useTranslations } from "next-intl"; import { Upload, FileText, AlertTriangle, X, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { parseVCard, detectDuplicates } from "@/lib/vcard"; import type { ContactCard, AddressBook } from "@/lib/jmap/types"; import { getContactDisplayName, getContactPrimaryEmail } from "@/stores/contact-store"; import { parseCSV, autoMapColumns, mapRowToContact, detectDuplicatesByEmail, type CsvColumnMapping, type CsvParseResult, } from "@/lib/contact-csv-import"; type FileType = "vcf" | "csv" | null; interface ContactImportDialogProps { existingContacts: ContactCard[]; addressBooks?: AddressBook[]; onImport: (contacts: ContactCard[]) => Promise; onClose: () => void; } export function ContactImportDialog({ existingContacts, addressBooks, onImport, onClose, }: ContactImportDialogProps) { const t = useTranslations("contacts"); const fileRef = useRef(null); const [fileType, setFileType] = useState(null); const [parsed, setParsed] = useState([]); const [selected, setSelected] = useState>(new Set()); const [duplicates, setDuplicates] = useState>(new Map()); const [isImporting, setIsImporting] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [csvData, setCsvData] = useState(null); const [mapping, setMapping] = useState(null); const [targetBookId, setTargetBookId] = useState(""); const [showPreview, setShowPreview] = useState(false); const ALLOWED_ACCEPT = ".vcf,.vcard,.csv,text/csv,text/vcard"; const books = addressBooks || []; const defaultBookId = books.find((b) => b.isDefault)?.id || books[0]?.id || ""; const effectiveBookId = targetBookId || defaultBookId; const bookOptions = books.map((b) => ({ value: b.id, label: b.name, })); const handleFileChange = useCallback(async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setError(null); setResult(null); setFileType(null); setParsed([]); setSelected(new Set()); setDuplicates(new Map()); setCsvData(null); setMapping(null); setShowPreview(false); setTargetBookId(""); if (file.size > 10 * 1024 * 1024) { setError(t("import.file_too_large")); return; } const name = file.name.toLowerCase(); try { if (name.endsWith(".csv") || file.type === "text/csv") { setFileType("csv"); const text = await file.text(); const result = parseCSV(text); if (result.rows.length === 0) { setError(t("import.no_contacts")); return; } setCsvData(result); setMapping(autoMapColumns(result.headers)); setTargetBookId(defaultBookId); } else { setFileType("vcf"); const text = await file.text(); const contacts = parseVCard(text); if (contacts.length === 0) { setError(t("import.no_contacts")); return; } const dupes = detectDuplicates(existingContacts, contacts); setParsed(contacts); setDuplicates(dupes); const initialSelected = new Set(); contacts.forEach((_, idx) => { if (!dupes.has(idx)) initialSelected.add(idx); }); setSelected(initialSelected); } } catch (err) { console.error("Failed to parse file:", err); setError(t("import.parse_error")); } }, [existingContacts, t, defaultBookId]); const applyCsvMapping = useCallback(() => { if (!csvData || !mapping) return; const bookIds = effectiveBookId ? { [effectiveBookId]: true } : {}; const contacts: ContactCard[] = []; for (const row of csvData.rows) { const contact = mapRowToContact(row, mapping, bookIds); if (contact) contacts.push(contact); } if (contacts.length === 0) { setError(t("import.no_contacts")); return; } const dupes = detectDuplicatesByEmail(existingContacts, contacts); setParsed(contacts); setDuplicates(dupes); const initialSelected = new Set(); contacts.forEach((_, idx) => { if (!dupes.has(idx)) initialSelected.add(idx); }); setSelected(initialSelected); setShowPreview(true); }, [csvData, mapping, effectiveBookId, existingContacts, t]); const toggleSelect = (idx: number) => { const next = new Set(selected); if (next.has(idx)) { next.delete(idx); } else { next.add(idx); } setSelected(next); }; const selectAll = () => { setSelected(new Set(parsed.map((_, i) => i))); }; const deselectAll = () => { setSelected(new Set()); }; const handleImport = async () => { const toImport = parsed.filter((_, i) => selected.has(i)); if (toImport.length === 0) return; setIsImporting(true); try { const count = await onImport(toImport); setResult(count); } catch (err) { console.error("Failed to import contacts:", err); setError(t("import.failed")); } finally { setIsImporting(false); } }; const renderCsvMapping = () => { if (!csvData || !mapping) return null; const fields: Array<{ key: keyof CsvColumnMapping; label: string }> = [ { key: "firstName", label: t("import.csv_first_name") }, { key: "lastName", label: t("import.csv_last_name") }, { key: "email", label: t("import.csv_email") }, { key: "phone", label: t("import.csv_phone") }, { key: "company", label: t("import.csv_company") }, { key: "jobTitle", label: t("import.csv_job_title") }, { key: "address", label: t("import.csv_address") }, { key: "city", label: t("import.csv_city") }, { key: "region", label: t("import.csv_region") }, { key: "postcode", label: t("import.csv_postcode") }, { key: "country", label: t("import.csv_country") }, { key: "website", label: t("import.csv_website") }, { key: "note", label: t("import.csv_note") }, { key: "nickname", label: t("import.csv_nickname") }, ]; const headerOptions = csvData.headers.map((h, i) => ({ value: String(i), label: h, })); return (

{t("import.csv_map_columns")}

{fields.map(({ key, label }) => (
))}
{books.length > 0 && (
)}
); }; const renderCsvPreview = () => { if (!csvData || !mapping || !showPreview) return null; const previewRows = csvData.rows.slice(0, 5); return (

{t("import.csv_preview_title", { count: parsed.length })}

{csvData.headers.map((h, i) => ( ))} {previewRows.map((row, ri) => ( {row.map((cell, ci) => ( ))} ))}
{h}
{cell}
); }; return (

{t("import.title")}

{result !== null ? (

{t("import.success", { count: result })}

) : fileType === null ? ( <> {error && (
{error}
)} ) : fileType === "csv" && csvData && !showPreview ? ( renderCsvMapping() ) : fileType === "csv" && csvData && showPreview ? ( renderCsvPreview() ) : ( <> {error && (
{error}
)}

{t("import.found", { count: parsed.length })}

{parsed.map((contact, idx) => { const cName = getContactDisplayName(contact); const cEmail = getContactPrimaryEmail(contact); const isDupe = duplicates.has(idx); const isSelected = selected.has(idx); return ( ); })}
)}
{parsed.length > 0 && result === null && fileType !== "csv" && (

{t("import.selected", { count: selected.size })}

)} {fileType === "csv" && showPreview && parsed.length > 0 && result === null && (

{t("import.selected", { count: selected.size })}

)}
); }