mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
feat(contacts): enhance import modal with tag preview and CSV parsing improvements
- Added `ImportPreviewTags` component to display tags with color coding in the import modal. - Updated `parseContactCsv` to return an object containing both parsed rows and a flag indicating the presence of a tags column. - Modified state management in the import modal to handle tags and their colors, improving user experience during contact import. - Adjusted the import modal to conditionally render the tags column based on the presence of tags in the preview.
This commit is contained in:
parent
3a416832a7
commit
73062cf06d
@ -8,7 +8,7 @@ import {
|
||||
isUniqueViolation,
|
||||
normalizeKey,
|
||||
} from '@/lib/contacts/dedupe';
|
||||
import { parseContactCsv } from '@/lib/contacts/parse-contact-csv';
|
||||
import { parseContactCsv, type ParsedContactRow } from '@/lib/contacts/parse-contact-csv';
|
||||
import {
|
||||
assignImportedContactTags,
|
||||
resolveImportTagIds,
|
||||
@ -26,6 +26,41 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Upload, FileText, Loader2, CheckCircle, XCircle, AlertTriangle } from 'lucide-react';
|
||||
|
||||
const DEFAULT_TAG_COLOR = '#3b82f6';
|
||||
|
||||
function ImportPreviewTags({
|
||||
tagNames,
|
||||
tagColorByKey,
|
||||
}: {
|
||||
tagNames: string[];
|
||||
tagColorByKey: Map<string, string>;
|
||||
}) {
|
||||
if (tagNames.length === 0) {
|
||||
return <span className="text-slate-500">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tagNames.map((name) => {
|
||||
const color = tagColorByKey.get(name.trim().toLowerCase()) ?? DEFAULT_TAG_COLOR;
|
||||
return (
|
||||
<span
|
||||
key={name}
|
||||
className="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color,
|
||||
border: `1px solid ${color}40`,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportModalProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
@ -38,7 +73,9 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [parsedRows, setParsedRows] = useState<ReturnType<typeof parseContactCsv>>([]);
|
||||
const [parsedRows, setParsedRows] = useState<ParsedContactRow[]>([]);
|
||||
const [hasTagsColumn, setHasTagsColumn] = useState(false);
|
||||
const [tagColorByKey, setTagColorByKey] = useState<Map<string, string>>(new Map());
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [result, setResult] = useState<{
|
||||
imported: number;
|
||||
@ -50,6 +87,8 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
function reset() {
|
||||
setFile(null);
|
||||
setParsedRows([]);
|
||||
setHasTagsColumn(false);
|
||||
setTagColorByKey(new Map());
|
||||
setResult(null);
|
||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||
}
|
||||
@ -67,15 +106,34 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
setResult(null);
|
||||
|
||||
const text = await selected.text();
|
||||
const rows = parseContactCsv(text);
|
||||
const { rows, hasTagsColumn: csvHasTags } = parseContactCsv(text);
|
||||
|
||||
if (rows.length === 0) {
|
||||
toast.error('No valid rows found. Ensure CSV has a "phone" column header.');
|
||||
setParsedRows([]);
|
||||
setHasTagsColumn(false);
|
||||
setTagColorByKey(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
setParsedRows(rows);
|
||||
setHasTagsColumn(csvHasTags);
|
||||
|
||||
if (csvHasTags && accountId) {
|
||||
const { data: tags } = await supabase
|
||||
.from('tags')
|
||||
.select('name, color')
|
||||
.eq('account_id', accountId);
|
||||
|
||||
const colors = new Map<string, string>();
|
||||
for (const tag of tags ?? []) {
|
||||
const key = tag.name.trim().toLowerCase();
|
||||
if (!colors.has(key)) colors.set(key, tag.color);
|
||||
}
|
||||
setTagColorByKey(colors);
|
||||
} else {
|
||||
setTagColorByKey(new Map());
|
||||
}
|
||||
}
|
||||
|
||||
async function handleImport() {
|
||||
@ -228,7 +286,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
}
|
||||
|
||||
const preview = parsedRows.slice(0, 5);
|
||||
const hasTagsColumn = parsedRows.some((row) => row.tagNames.length > 0);
|
||||
const previewHasTags = hasTagsColumn || preview.some((row) => row.tagNames.length > 0);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
@ -291,7 +349,7 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Name</th>
|
||||
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Email</th>
|
||||
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Company</th>
|
||||
{hasTagsColumn && (
|
||||
{previewHasTags && (
|
||||
<th className="px-3 py-1.5 text-left text-slate-400 font-medium">Tags</th>
|
||||
)}
|
||||
</tr>
|
||||
@ -303,9 +361,12 @@ export function ImportModal({ open, onOpenChange, onImported }: ImportModalProps
|
||||
<td className="px-3 py-1.5 text-slate-300">{row.name || '-'}</td>
|
||||
<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>
|
||||
{hasTagsColumn && (
|
||||
{previewHasTags && (
|
||||
<td className="px-3 py-1.5 text-slate-300">
|
||||
{row.tagNames.length > 0 ? row.tagNames.join(', ') : '-'}
|
||||
<ImportPreviewTags
|
||||
tagNames={row.tagNames}
|
||||
tagColorByKey={tagColorByKey}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
|
||||
@ -30,7 +30,9 @@ describe("parseContactCsv", () => {
|
||||
+15551234567,Alice,"VIP, Lead"
|
||||
+15559876543,Bob,Customer`;
|
||||
|
||||
expect(parseContactCsv(csv)).toEqual([
|
||||
expect(parseContactCsv(csv)).toEqual({
|
||||
hasTagsColumn: true,
|
||||
rows: [
|
||||
{
|
||||
phone: "+15551234567",
|
||||
name: "Alice",
|
||||
@ -45,14 +47,17 @@ describe("parseContactCsv", () => {
|
||||
company: undefined,
|
||||
tagNames: ["Customer"],
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty tagNames when tags column is absent", () => {
|
||||
const csv = `phone,name
|
||||
+15551234567,Alice`;
|
||||
|
||||
expect(parseContactCsv(csv)).toEqual([
|
||||
expect(parseContactCsv(csv)).toEqual({
|
||||
hasTagsColumn: false,
|
||||
rows: [
|
||||
{
|
||||
phone: "+15551234567",
|
||||
name: "Alice",
|
||||
@ -60,6 +65,7 @@ describe("parseContactCsv", () => {
|
||||
company: undefined,
|
||||
tagNames: [],
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -31,16 +31,22 @@ export function parseTagCell(value: string | undefined): string[] {
|
||||
return names;
|
||||
}
|
||||
|
||||
export function parseContactCsv(text: string): ParsedContactRow[] {
|
||||
export interface ParseContactCsvResult {
|
||||
rows: ParsedContactRow[];
|
||||
/** True when the CSV header includes a `tags` column. */
|
||||
hasTagsColumn: boolean;
|
||||
}
|
||||
|
||||
export function parseContactCsv(text: string): ParseContactCsvResult {
|
||||
const lines = text.trim().split(/\r?\n/);
|
||||
if (lines.length < 2) return [];
|
||||
if (lines.length < 2) return { rows: [], hasTagsColumn: false };
|
||||
|
||||
const headers = lines[0]
|
||||
.split(",")
|
||||
.map((h) => h.trim().toLowerCase().replace(/["']/g, ""));
|
||||
|
||||
const phoneIdx = headers.indexOf("phone");
|
||||
if (phoneIdx === -1) return [];
|
||||
if (phoneIdx === -1) return { rows: [], hasTagsColumn: false };
|
||||
|
||||
const nameIdx = headers.indexOf("name");
|
||||
const emailIdx = headers.indexOf("email");
|
||||
@ -76,7 +82,7 @@ export function parseContactCsv(text: string): ParsedContactRow[] {
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
return { rows, hasTagsColumn: tagsIdx >= 0 };
|
||||
}
|
||||
|
||||
/** Simple CSV line parse (handles quoted fields). */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user