Download apps/admin/src/components/crm/FileImporter.tsx from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 4.2 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/crm/FileImporter.tsx
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/admin/src/components/crm/FileImporter.tsx
-
curl -L -o FileImporter.tsx https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/crm/FileImporter.tsx
4.2 kB
| 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); | |
| }} | |
| /> | |
| ); | |
| }; | |