edtech / apps /admin /src /components /crm /FileImporter.tsx
CognxSafeTrack
fix(crm): normalize Excel column names before bulk contact upload
3d1299e
import React from 'react';
import { read, utils } from 'xlsx';
import { api } from '../../lib/api';
import { logError } from '../../lib/logger';
interface FileImporterProps {
token: string;
selectedOrgId: string;
onImportSuccess: (data: { listId: string; listName: string; count: number }) => void;
onImportError: (error: string) => void;
setIsUploading: (loading: boolean) => void;
}
export const FileImporter: React.FC<FileImporterProps> = ({
token,
selectedOrgId,
onImportSuccess,
onImportError,
setIsUploading
}) => {
const normalizeContacts = (rows: any[]): { phoneNumber: string; name?: string; attributes: Record<string, unknown> }[] => {
const PHONE_KEYS = ['phonenumber', 'phone', 'telephone', 'téléphone', 'mobile', 'numero', 'numéro', 'number', 'whatsapp', 'tel'];
const NAME_KEYS = ['name', 'nom', 'prenom', 'prénom', 'firstname', 'lastname', 'fullname', 'contact'];
return rows.reduce<{ phoneNumber: string; name?: string; attributes: Record<string, unknown> }[]>((acc, row) => {
const keys = Object.keys(row);
const phoneKey = keys.find(k => PHONE_KEYS.includes(k.toLowerCase().replace(/[\s_-]/g, '')));
const nameKey = keys.find(k => NAME_KEYS.includes(k.toLowerCase().replace(/[\s_-]/g, '')));
const raw = phoneKey ? String(row[phoneKey]).replace(/\s+/g, '') : null;
if (!raw) return acc; // skip rows with no phone
// Keep remaining columns as attributes
const attributes: Record<string, unknown> = {};
keys.forEach(k => {
if (k !== phoneKey && k !== nameKey) attributes[k] = row[k];
});
acc.push({
phoneNumber: raw,
...(nameKey ? { name: String(row[nameKey]) } : {}),
attributes
});
return acc;
}, []);
};
const handleFile = async (file: File) => {
if (!token || !selectedOrgId) return;
setIsUploading(true);
const reader = new FileReader();
reader.onload = async (event) => {
try {
// 1. Parse Excel/CSV on the client side
const data = new Uint8Array(event.target?.result as ArrayBuffer);
const workbook = read(data, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawRows = utils.sheet_to_json(worksheet);
// 2. Normalize column names → { phoneNumber, name?, attributes }
const contacts = normalizeContacts(rawRows);
if (contacts.length === 0) {
onImportError("Aucun contact trouvé. Vérifiez que votre fichier contient une colonne \"Téléphone\" ou \"Mobile\".");
setIsUploading(false);
return;
}
// 3. Send JSON to the bulk endpoint
const responseData = await api.post(`/v1/organizations/${selectedOrgId}/contacts/bulk`, {
contacts,
listName: file.name.split('.')[0]
}, token, selectedOrgId);
onImportSuccess({
listId: responseData.listId,
listName: responseData.listName,
count: responseData.results.created
});
} catch (err: any) {
logError("Parsing/Upload failed:", err);
const msg = typeof err?.message === 'string' && !err.message.includes('[object') ? err.message : "Une erreur technique est survenue.";
onImportError(msg);
} finally {
setIsUploading(false);
}
};
reader.readAsArrayBuffer(file);
};
return (
<input
type="file"
id="crm-file-upload"
className="hidden"
accept=".xlsx,.csv"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFile(file);
}}
/>
);
};