feat(contacts): enhance import modal with company column support and improved CSV parsing

- Updated `parseContactCsv` to include a flag for the presence of a company column in the CSV.
- Modified the import modal state management to handle the new company column, improving user experience during contact import.
- Enhanced the `ImportPreviewTags` component to conditionally render based on the presence of company data.
- Adjusted the UI to better display tag and company information in the import preview.
This commit is contained in:
Abhishek Sonwal 2026-06-15 13:27:56 +05:30
parent 73062cf06d
commit b780fddb3b
3 changed files with 251 additions and 102 deletions

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useRef } from 'react'; import { useMemo, useRef, useState } from 'react';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { useAuth } from '@/hooks/use-auth'; import { useAuth } from '@/hooks/use-auth';
import { import {
@ -14,6 +14,7 @@ import {
resolveImportTagIds, resolveImportTagIds,
type ContactTagAssignment, type ContactTagAssignment,
} from '@/lib/contacts/resolve-import-tags'; } from '@/lib/contacts/resolve-import-tags';
import { cn } from '@/lib/utils';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
Dialog, Dialog,
@ -24,9 +25,45 @@ import {
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Upload, FileText, Loader2, CheckCircle, XCircle, AlertTriangle } from 'lucide-react'; import {
Upload,
FileText,
Loader2,
CheckCircle,
XCircle,
AlertTriangle,
Tag,
} from 'lucide-react';
const DEFAULT_TAG_COLOR = '#3b82f6'; const DEFAULT_TAG_COLOR = '#3b82f6';
const PREVIEW_LIMIT = 5;
function truncateFilename(name: string, max = 48): string {
if (name.length <= max) return name;
const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')) : '';
const base = name.slice(0, name.length - ext.length);
const keep = max - ext.length - 1;
return `${base.slice(0, Math.max(keep, 12))}…${ext}`;
}
function PreviewCell({
value,
mono,
maxWidth = 'max-w-[9rem]',
}: {
value: string;
mono?: boolean;
maxWidth?: string;
}) {
return (
<span
className={cn('block truncate', maxWidth, mono && 'font-mono text-[11px]')}
title={value}
>
{value}
</span>
);
}
function ImportPreviewTags({ function ImportPreviewTags({
tagNames, tagNames,
@ -36,24 +73,30 @@ function ImportPreviewTags({
tagColorByKey: Map<string, string>; tagColorByKey: Map<string, string>;
}) { }) {
if (tagNames.length === 0) { if (tagNames.length === 0) {
return <span className="text-slate-500">-</span>; return <span className="text-slate-600">—</span>;
} }
return ( return (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 min-w-[4.5rem]">
{tagNames.map((name) => { {tagNames.map((name) => {
const color = tagColorByKey.get(name.trim().toLowerCase()) ?? DEFAULT_TAG_COLOR; const color = tagColorByKey.get(name.trim().toLowerCase()) ?? DEFAULT_TAG_COLOR;
const isKnown = tagColorByKey.has(name.trim().toLowerCase());
return ( return (
<span <span
key={name} key={name}
className="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium" className="inline-flex max-w-full items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-medium leading-none"
style={{ style={{
backgroundColor: `${color}20`, backgroundColor: `${color}18`,
color, color,
border: `1px solid ${color}40`, border: `1px solid ${color}${isKnown ? '55' : '30'}`,
}} }}
title={isKnown ? name : `${name} (will be created on import)`}
> >
{name} <span
className="size-1.5 shrink-0 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="truncate">{name}</span>
</span> </span>
); );
})} })}
@ -75,6 +118,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const [parsedRows, setParsedRows] = useState<ParsedContactRow[]>([]); const [parsedRows, setParsedRows] = useState<ParsedContactRow[]>([]);
const [hasTagsColumn, setHasTagsColumn] = useState(false); const [hasTagsColumn, setHasTagsColumn] = useState(false);
const [hasCompanyColumn, setHasCompanyColumn] = useState(false);
const [tagColorByKey, setTagColorByKey] = useState<Map<string, string>>(new Map()); const [tagColorByKey, setTagColorByKey] = useState<Map<string, string>>(new Map());
const [importing, setImporting] = useState(false); const [importing, setImporting] = useState(false);
const [result, setResult] = useState<{ const [result, setResult] = useState<{
@ -88,14 +132,15 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
setFile(null); setFile(null);
setParsedRows([]); setParsedRows([]);
setHasTagsColumn(false); setHasTagsColumn(false);
setHasCompanyColumn(false);
setTagColorByKey(new Map()); setTagColorByKey(new Map());
setResult(null); setResult(null);
if (fileInputRef.current) fileInputRef.current.value = ''; if (fileInputRef.current) fileInputRef.current.value = '';
} }
function handleOpenChange(open: boolean) { function handleOpenChange(next: boolean) {
if (!open) reset(); if (!next) reset();
onOpenChange(open); onOpenChange(next);
} }
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) { async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
@ -106,18 +151,21 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
setResult(null); setResult(null);
const text = await selected.text(); const text = await selected.text();
const { rows, hasTagsColumn: csvHasTags } = parseContactCsv(text); const { rows, hasTagsColumn: csvHasTags, hasCompanyColumn: csvHasCompany } =
parseContactCsv(text);
if (rows.length === 0) { if (rows.length === 0) {
toast.error('No valid rows found. Ensure CSV has a "phone" column header.'); toast.error('No valid rows found. Ensure CSV has a "phone" column header.');
setParsedRows([]); setParsedRows([]);
setHasTagsColumn(false); setHasTagsColumn(false);
setHasCompanyColumn(false);
setTagColorByKey(new Map()); setTagColorByKey(new Map());
return; return;
} }
setParsedRows(rows); setParsedRows(rows);
setHasTagsColumn(csvHasTags); setHasTagsColumn(csvHasTags);
setHasCompanyColumn(csvHasCompany);
if (csvHasTags && accountId) { if (csvHasTags && accountId) {
const { data: tags } = await supabase const { data: tags } = await supabase
@ -152,12 +200,9 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
let skipped = 0; let skipped = 0;
let failed = 0; let failed = 0;
// 1) De-dupe within the file by normalized phone (keep first).
const { unique, duplicates: inFileDupes } = dedupeByPhone(parsedRows); const { unique, duplicates: inFileDupes } = dedupeByPhone(parsedRows);
skipped += inFileDupes; skipped += inFileDupes;
// 2) Skip numbers already in this account. One read of the
// generated `phone_normalized` column (migration 022) → Set.
const { data: existingRows } = await supabase const { data: existingRows } = await supabase
.from('contacts') .from('contacts')
.select('phone_normalized') .select('phone_normalized')
@ -176,7 +221,6 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
return true; return true;
}); });
// 3) Resolve tag names → ids (create missing tags for admin+).
const allTagNames = toInsert.flatMap((row) => row.tagNames); const allTagNames = toInsert.flatMap((row) => row.tagNames);
const { tagIdByKey, skippedNames } = await resolveImportTagIds(supabase, { const { tagIdByKey, skippedNames } = await resolveImportTagIds(supabase, {
accountId, accountId,
@ -186,10 +230,8 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
}); });
const tagAssignments: ContactTagAssignment[] = []; const tagAssignments: ContactTagAssignment[] = [];
// 4) Batch insert contacts in chunks of 50. The DB unique index is
// the backstop: a 23505 counts as skipped, not failed.
const chunkSize = 50; const chunkSize = 50;
for (let i = 0; i < toInsert.length; i += chunkSize) { for (let i = 0; i < toInsert.length; i += chunkSize) {
const chunk = toInsert.slice(i, i + chunkSize); const chunk = toInsert.slice(i, i + chunkSize);
const rows = chunk.map((row) => ({ const rows = chunk.map((row) => ({
@ -207,8 +249,6 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
.select('id'); .select('id');
if (error) { if (error) {
// Retry individually so one bad/duplicate row doesn't sink
// the whole chunk.
for (let j = 0; j < rows.length; j++) { for (let j = 0; j < rows.length; j++) {
const row = rows[j]; const row = rows[j];
const source = chunk[j]; const source = chunk[j];
@ -246,7 +286,6 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
} }
} }
// 5) Wire tags onto the contacts we just created.
const tagsAssigned = await assignImportedContactTags( const tagsAssigned = await assignImportedContactTags(
supabase, supabase,
tagAssignments, tagAssignments,
@ -285,44 +324,90 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
} }
} }
const preview = parsedRows.slice(0, 5); const preview = parsedRows.slice(0, PREVIEW_LIMIT);
const previewHasTags = hasTagsColumn || preview.some((row) => row.tagNames.length > 0); const previewHasTags =
hasTagsColumn || preview.some((row) => row.tagNames.length > 0);
const previewHasCompany =
hasCompanyColumn && preview.some((row) => row.company?.trim());
const tagStats = useMemo(() => {
const names = new Set<string>();
let rowsWithTags = 0;
for (const row of parsedRows) {
if (row.tagNames.length === 0) continue;
rowsWithTags++;
for (const name of row.tagNames) names.add(name.trim().toLowerCase());
}
return { unique: names.size, rowsWithTags };
}, [parsedRows]);
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="bg-slate-900 border-slate-700 text-slate-200 sm:max-w-lg"> <DialogContent className="flex max-h-[min(90vh,720px)] flex-col gap-0 overflow-hidden border-slate-700/80 bg-slate-900 p-0 text-slate-200 sm:max-w-2xl">
<DialogHeader> <div className="shrink-0 space-y-4 border-b border-slate-800/80 px-6 pt-6 pb-5">
<DialogTitle className="text-white">Import Contacts</DialogTitle> <DialogHeader className="gap-1.5">
<DialogDescription className="text-slate-400"> <DialogTitle className="text-lg text-white">Import Contacts</DialogTitle>
Upload a CSV file with a &quot;phone&quot; column (required). Optional columns: <DialogDescription className="text-slate-400 leading-relaxed">
name, email, company, tags (comma-separated tag names — quote the Upload a CSV with a required{' '}
cell if a tag list contains commas, e.g. &quot;VIP, Lead&quot;). <code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
phone
</code>{' '}
column. Optional:{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
name
</code>
,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
email
</code>
,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
company
</code>
,{' '}
<code className="rounded bg-slate-800 px-1 py-0.5 text-[11px] text-slate-300">
tags
</code>{' '}
(comma-separated; quote multi-tag cells).
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4">
{/* Upload area */}
<div <div
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
className="flex flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-slate-700 p-6 cursor-pointer hover:border-primary/50 transition-colors" onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') fileInputRef.current?.click();
}}
className={cn(
'group flex cursor-pointer flex-col items-center justify-center gap-2 rounded-xl border border-dashed p-5 transition-all',
file
? 'border-primary/35 bg-primary/[0.04]'
: 'border-slate-700/80 bg-slate-950/40 hover:border-primary/40 hover:bg-slate-950/70',
)}
> >
{file ? ( {file ? (
<> <>
<FileText className="size-8 text-primary" /> <div className="flex size-10 items-center justify-center rounded-lg bg-primary/15 ring-1 ring-primary/25">
<p className="text-sm text-slate-300">{file.name}</p> <FileText className="size-5 text-primary" />
<p className="text-xs text-slate-500"> </div>
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''} detected <p
className="max-w-full truncate px-2 text-sm font-medium text-slate-200"
title={file.name}
>
{truncateFilename(file.name)}
</p> </p>
<span className="rounded-full bg-slate-800 px-2.5 py-0.5 text-[11px] font-medium text-slate-400">
{parsedRows.length} row{parsedRows.length !== 1 ? 's' : ''} ready
</span>
</> </>
) : ( ) : (
<> <>
<Upload className="size-8 text-slate-500" /> <div className="flex size-10 items-center justify-center rounded-lg bg-slate-800/80 ring-1 ring-slate-700/80 transition-colors group-hover:bg-slate-800">
<p className="text-sm text-slate-400"> <Upload className="size-5 text-slate-500 group-hover:text-slate-400" />
Click to upload CSV file </div>
</p> <p className="text-sm text-slate-400">Click to choose a CSV file</p>
<p className="text-xs text-slate-500"> <p className="text-[11px] text-slate-600">.csv up to your browser limit</p>
CSV with &quot;phone&quot; column required
</p>
</> </>
)} )}
</div> </div>
@ -334,35 +419,84 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
onChange={handleFileChange} onChange={handleFileChange}
className="hidden" className="hidden"
/> />
</div>
{/* Preview table */} <div className="min-h-0 flex-1 overflow-y-auto px-6 py-4">
{preview.length > 0 && !result && ( {preview.length > 0 && !result && (
<div className="space-y-2"> <div className="space-y-3">
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider"> <div className="flex flex-wrap items-center justify-between gap-2">
Preview (first {preview.length} rows) <p className="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
Preview · first {preview.length}
</p> </p>
<div className="rounded-lg border border-slate-700 overflow-hidden"> <div className="flex flex-wrap items-center gap-1.5">
<table className="w-full text-xs"> {tagStats.rowsWithTags > 0 && (
<span className="inline-flex items-center gap-1 rounded-md bg-slate-800/90 px-2 py-0.5 text-[11px] text-slate-400">
<Tag className="size-3 text-primary/80" />
{tagStats.unique} tag{tagStats.unique !== 1 ? 's' : ''} ·{' '}
{tagStats.rowsWithTags} contact
{tagStats.rowsWithTags !== 1 ? 's' : ''}
</span>
)}
</div>
</div>
<div className="overflow-hidden rounded-xl border border-slate-800 ring-1 ring-slate-800/50">
<div className="overflow-x-auto">
<table className="w-full min-w-[32rem] text-xs">
<thead> <thead>
<tr className="bg-slate-800"> <tr className="border-b border-slate-800 bg-slate-950/60">
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Phone</th> <th className="whitespace-nowrap px-3 py-2 text-left font-medium text-slate-500">
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Name</th> Phone
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Email</th> </th>
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Company</th> <th className="whitespace-nowrap px-3 py-2 text-left font-medium text-slate-500">
Name
</th>
<th className="whitespace-nowrap px-3 py-2 text-left font-medium text-slate-500">
Email
</th>
{previewHasCompany && (
<th className="whitespace-nowrap px-3 py-2 text-left font-medium text-slate-500">
Company
</th>
)}
{previewHasTags && ( {previewHasTags && (
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Tags</th> <th className="whitespace-nowrap px-3 py-2 text-left font-medium text-slate-500">
Tags
</th>
)} )}
</tr> </tr>
</thead> </thead>
<tbody> <tbody className="divide-y divide-slate-800/70">
{preview.map((row, i) => ( {preview.map((row, i) => (
<tr key={i} className="border-t border-slate-700/50"> <tr
<td className="px-3 py-1.5 text-slate-300">{row.phone}</td> key={i}
<td className="px-3 py-1.5 text-slate-300">{row.name || '-'}</td> className="bg-slate-900/40 transition-colors hover:bg-slate-800/30"
<td className="px-3 py-1.5 text-slate-300">{row.email || '-'}</td> >
<td className="px-3 py-1.5 text-slate-300">{row.company || '-'}</td> <td className="whitespace-nowrap px-3 py-2 text-slate-300">
<PreviewCell value={row.phone} mono maxWidth="max-w-[7.5rem]" />
</td>
<td className="px-3 py-2 text-slate-200">
<PreviewCell
value={row.name || '—'}
maxWidth="max-w-[8.5rem]"
/>
</td>
<td className="px-3 py-2 text-slate-400">
<PreviewCell
value={row.email || '—'}
maxWidth="max-w-[10rem]"
/>
</td>
{previewHasCompany && (
<td className="px-3 py-2 text-slate-400">
<PreviewCell
value={row.company || '—'}
maxWidth="max-w-[7rem]"
/>
</td>
)}
{previewHasTags && ( {previewHasTags && (
<td className="px-3 py-1.5 text-slate-300"> <td className="px-3 py-2 align-top">
<ImportPreviewTags <ImportPreviewTags
tagNames={row.tagNames} tagNames={row.tagNames}
tagColorByKey={tagColorByKey} tagColorByKey={tagColorByKey}
@ -374,40 +508,42 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
</tbody> </tbody>
</table> </table>
</div> </div>
{parsedRows.length > 5 && ( </div>
<p className="text-xs text-slate-500">
...and {parsedRows.length - 5} more rows {parsedRows.length > PREVIEW_LIMIT && (
<p className="text-center text-[11px] text-slate-600">
+ {parsedRows.length - PREVIEW_LIMIT} more row
{parsedRows.length - PREVIEW_LIMIT !== 1 ? 's' : ''} not shown
</p> </p>
)} )}
</div> </div>
)} )}
{/* Results */}
{result && ( {result && (
<div className="rounded-lg border border-slate-700 p-4 space-y-2"> <div className="rounded-xl border border-slate-800 bg-slate-950/50 p-4">
<p className="text-sm font-medium text-white">Import Complete</p> <p className="text-sm font-medium text-white">Import complete</p>
<div className="flex flex-wrap items-center gap-4"> <div className="mt-3 flex flex-wrap gap-3">
{result.imported > 0 && ( {result.imported > 0 && (
<div className="flex items-center gap-1.5 text-primary text-sm"> <div className="flex items-center gap-1.5 text-sm text-primary">
<CheckCircle className="size-4" /> <CheckCircle className="size-4 shrink-0" />
{result.imported} imported {result.imported} imported
</div> </div>
)} )}
{result.tagsAssigned > 0 && ( {result.tagsAssigned > 0 && (
<div className="flex items-center gap-1.5 text-cyan-400 text-sm"> <div className="flex items-center gap-1.5 text-sm text-cyan-400">
<CheckCircle className="size-4" /> <CheckCircle className="size-4 shrink-0" />
{result.tagsAssigned} tag{result.tagsAssigned !== 1 ? 's' : ''} assigned {result.tagsAssigned} tag{result.tagsAssigned !== 1 ? 's' : ''} assigned
</div> </div>
)} )}
{result.skipped > 0 && ( {result.skipped > 0 && (
<div className="flex items-center gap-1.5 text-amber-400 text-sm"> <div className="flex items-center gap-1.5 text-sm text-amber-400">
<AlertTriangle className="size-4" /> <AlertTriangle className="size-4 shrink-0" />
{result.skipped} duplicate{result.skipped !== 1 ? 's' : ''} skipped {result.skipped} skipped
</div> </div>
)} )}
{result.failed > 0 && ( {result.failed > 0 && (
<div className="flex items-center gap-1.5 text-red-400 text-sm"> <div className="flex items-center gap-1.5 text-sm text-red-400">
<XCircle className="size-4" /> <XCircle className="size-4 shrink-0" />
{result.failed} failed {result.failed} failed
</div> </div>
)} )}
@ -416,7 +552,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
)} )}
</div> </div>
<DialogFooter className="bg-slate-900 border-slate-700"> <DialogFooter className="mt-0 shrink-0 gap-2 border-t border-slate-800/80 bg-slate-950/50 px-6 py-4 sm:justify-end">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@ -433,7 +569,8 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
className="bg-primary hover:bg-primary/90 text-primary-foreground" className="bg-primary hover:bg-primary/90 text-primary-foreground"
> >
{importing && <Loader2 className="size-4 animate-spin" />} {importing && <Loader2 className="size-4 animate-spin" />}
Import {parsedRows.length > 0 ? `${parsedRows.length} Contacts` : ''} Import {parsedRows.length > 0 ? parsedRows.length : ''} contact
{parsedRows.length !== 1 ? 's' : ''}
</Button> </Button>
)} )}
</DialogFooter> </DialogFooter>

View File

@ -32,6 +32,7 @@ describe("parseContactCsv", () => {
expect(parseContactCsv(csv)).toEqual({ expect(parseContactCsv(csv)).toEqual({
hasTagsColumn: true, hasTagsColumn: true,
hasCompanyColumn: false,
rows: [ rows: [
{ {
phone: "+15551234567", phone: "+15551234567",
@ -57,6 +58,7 @@ describe("parseContactCsv", () => {
expect(parseContactCsv(csv)).toEqual({ expect(parseContactCsv(csv)).toEqual({
hasTagsColumn: false, hasTagsColumn: false,
hasCompanyColumn: false,
rows: [ rows: [
{ {
phone: "+15551234567", phone: "+15551234567",

View File

@ -35,18 +35,24 @@ export interface ParseContactCsvResult {
rows: ParsedContactRow[]; rows: ParsedContactRow[];
/** True when the CSV header includes a `tags` column. */ /** True when the CSV header includes a `tags` column. */
hasTagsColumn: boolean; hasTagsColumn: boolean;
/** True when the CSV header includes a `company` column. */
hasCompanyColumn: boolean;
} }
export function parseContactCsv(text: string): ParseContactCsvResult { export function parseContactCsv(text: string): ParseContactCsvResult {
const lines = text.trim().split(/\r?\n/); const lines = text.trim().split(/\r?\n/);
if (lines.length < 2) return { rows: [], hasTagsColumn: false }; if (lines.length < 2) {
return { rows: [], hasTagsColumn: false, hasCompanyColumn: false };
}
const headers = lines[0] const headers = lines[0]
.split(",") .split(",")
.map((h) => h.trim().toLowerCase().replace(/["']/g, "")); .map((h) => h.trim().toLowerCase().replace(/["']/g, ""));
const phoneIdx = headers.indexOf("phone"); const phoneIdx = headers.indexOf("phone");
if (phoneIdx === -1) return { rows: [], hasTagsColumn: false }; if (phoneIdx === -1) {
return { rows: [], hasTagsColumn: false, hasCompanyColumn: false };
}
const nameIdx = headers.indexOf("name"); const nameIdx = headers.indexOf("name");
const emailIdx = headers.indexOf("email"); const emailIdx = headers.indexOf("email");
@ -82,7 +88,11 @@ export function parseContactCsv(text: string): ParseContactCsvResult {
}); });
} }
return { rows, hasTagsColumn: tagsIdx >= 0 }; return {
rows,
hasTagsColumn: tagsIdx >= 0,
hasCompanyColumn: companyIdx >= 0,
};
} }
/** Simple CSV line parse (handles quoted fields). */ /** Simple CSV line parse (handles quoted fields). */