import { useDataProvider, useGetIdentity, type DataProvider } from "ra-core"; import { useCallback, useMemo } from "react"; import type { Company, Tag } from "../types"; export type ContactImportSchema = { first_name: string; last_name: string; gender: string; title: string; company: string; email_work: string; email_home: string; email_other: string; phone_work: string; phone_home: string; phone_other: string; background: string; avatar: string; first_seen: string; last_seen: string; has_newsletter: string; status: string; tags: string; linkedin_url: string; }; export function useContactImport() { const today = new Date().toISOString(); const user = useGetIdentity(); const dataProvider = useDataProvider(); // company cache to avoid creating the same company multiple times and costly roundtrips // Cache is dependent of dataProvider, so it's safe to use it as a dependency const companiesCache = useMemo( () => new Map(), // eslint-disable-next-line react-hooks/exhaustive-deps [dataProvider], ); const getCompanies = useCallback( async (names: string[]) => fetchRecordsWithCache( "companies", companiesCache, names, (name) => ({ name, created_at: new Date().toISOString(), sales_id: user?.identity?.id, }), dataProvider, ), [companiesCache, user?.identity?.id, dataProvider], ); // Tags cache to avoid creating the same tag multiple times and costly roundtrips // Cache is dependent of dataProvider, so it's safe to use it as a dependency // eslint-disable-next-line react-hooks/exhaustive-deps const tagsCache = useMemo(() => new Map(), [dataProvider]); const getTags = useCallback( async (names: string[]) => fetchRecordsWithCache( "tags", tagsCache, names, (name) => ({ name, color: "#f9f9f9", }), dataProvider, ), [tagsCache, dataProvider], ); const processBatch = useCallback( async (batch: ContactImportSchema[]) => { const [companies, tags] = await Promise.all([ getCompanies( batch .map((contact) => contact.company?.trim()) .filter((name) => name), ), getTags(batch.flatMap((batch) => parseTags(batch.tags))), ]); await Promise.all( batch.map( async ({ first_name, last_name, gender, title, email_work, email_home, email_other, phone_work, phone_home, phone_other, background, first_seen, last_seen, has_newsletter, status, company: companyName, tags: tagNames, linkedin_url, }) => { const email_jsonb = [ { email: email_work, type: "Work" }, { email: email_home, type: "Home" }, { email: email_other, type: "Other" }, ].filter(({ email }) => email); const phone_jsonb = [ { number: phone_work, type: "Work" }, { number: phone_home, type: "Home" }, { number: phone_other, type: "Other" }, ].filter(({ number }) => number); const company = companyName?.trim() ? companies.get(companyName.trim()) : undefined; const tagList = parseTags(tagNames) .map((name) => tags.get(name)) .filter((tag): tag is Tag => !!tag); return dataProvider.create("contacts", { data: { first_name, last_name, gender, title, email_jsonb, phone_jsonb, background, first_seen: first_seen ? new Date(first_seen).toISOString() : today, last_seen: last_seen ? new Date(last_seen).toISOString() : today, has_newsletter, status, company_id: company?.id, tags: tagList.map((tag) => tag.id), sales_id: user?.identity?.id, linkedin_url, }, }); }, ), ); }, [dataProvider, getCompanies, getTags, user?.identity?.id, today], ); return processBatch; } const fetchRecordsWithCache = async function ( resource: string, cache: Map, names: string[], getCreateData: (name: string) => Partial, dataProvider: DataProvider, ) { const trimmedNames = [...new Set(names.map((name) => name.trim()))]; const uncachedRecordNames = trimmedNames.filter((name) => !cache.has(name)); // check the backend for existing records if (uncachedRecordNames.length > 0) { const response = await dataProvider.getList(resource, { filter: { "name@in": `(${uncachedRecordNames .map((name) => `"${name}"`) .join(",")})`, }, pagination: { page: 1, perPage: trimmedNames.length }, sort: { field: "id", order: "ASC" }, }); for (const record of response.data) { cache.set(record.name.trim(), record); } } // create missing records in parallel await Promise.all( uncachedRecordNames.map(async (name) => { if (cache.has(name)) return; const response = await dataProvider.create(resource, { data: getCreateData(name), }); cache.set(name, response.data); }), ); // now all records are in cache, return a map of all records return trimmedNames.reduce((acc, name) => { acc.set(name, cache.get(name) as T); return acc; }, new Map()); }; const parseTags = (tags: string) => tags ?.split(",") ?.map((tag: string) => tag.trim()) ?.filter((tag: string) => tag) ?? [];