import { Plural, Trans, useLingui } from "@lingui/react/macro"; import type { MetricCsvMapping } from "@rakazo/contracts"; import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Field, FieldLabel, Input, NativeSelect, NativeSelectOption, } from "@rakazo/ui-web"; import { useEffect, useId, useMemo, useState } from "react"; import type { CsvDelimiter, MappingColumn } from "../../../lib/media-library"; import { csvHeaders, firstCsvLine, guessColumns, guessDelimiter, guessPublicationKey, MAPPING_COLUMNS, } from "../../../lib/media-library"; import { rpcErrorMessage } from "../../../lib/media-studio"; import { rpc } from "../../../lib/rpc"; const MAX_CSV_CHARS = 5_000_000; const NEW = ""; type Draft = Omit & { columns: Partial>; }; function draftFrom(mapping: MetricCsvMapping): Draft { return { ...mapping, columns: { ...mapping.columns } }; } function toMapping(draft: Draft): MetricCsvMapping | null { const { publication, observedAt } = draft.columns; if (!publication || !observedAt || !draft.name.trim()) return null; if (!/^[A-Za-z]{3}$/.test(draft.defaultCurrency)) return null; const columns: MetricCsvMapping["columns"] = { publication, observedAt }; for (const key of MAPPING_COLUMNS) { const value = draft.columns[key]; if (value) columns[key] = value; } return { ...draft, name: draft.name.trim(), defaultCurrency: draft.defaultCurrency.toUpperCase(), columns, }; } export function CsvImportDialog({ open, onOpenChange, onImported, }: { open: boolean; onOpenChange: (open: boolean) => void; onImported: () => void; }) { const { t } = useLingui(); const fileId = useId(); const mappingId = useId(); const nameId = useId(); const [mappings, setMappings] = useState([]); const [csv, setCsv] = useState(null); const [fileName, setFileName] = useState(""); const [draft, setDraft] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState<{ imported: number; skipped: Array<{ row: number; reason: string }>; } | null>(null); useEffect(() => { if (!open) return; setCsv(null); setFileName(""); setDraft(null); setError(null); setResult(null); rpc.media.metrics .mappings({}) .then(setMappings) .catch(() => setMappings([])); }, [open]); const headers = useMemo( () => (csv && draft ? csvHeaders(csv, draft.delimiter) : []), [csv, draft?.delimiter, draft], ); const labels: Record = { publication: t`Publication`, observedAt: t`Date`, views: t`Views`, clicks: t`Clicks`, conversions: t`Purchases`, revenue: t`Revenue`, currency: t`Currency`, }; const keyLabels: Record = { utm_content: t`UTM content`, tracking_url: t`Tracking link`, external_url: t`Post link`, id: t`Publication ID`, }; const readFile = async (file: File) => { setError(null); setResult(null); if (file.size > MAX_CSV_CHARS) { setError(t`This file is too large.`); return; } const text = await file.text(); setCsv(text); setFileName(file.name); const delimiter: CsvDelimiter = guessDelimiter(firstCsvLine(text)); const found = csvHeaders(text, delimiter); const matching = mappings.find((mapping) => Object.values(mapping.columns).every((column) => !column || found.includes(column)), ); if (matching) { setDraft(draftFrom(matching)); return; } const columns = guessColumns(found); setDraft({ name: file.name.replace(/\.[^.]+$/, "").slice(0, 60) || t`Import`, delimiter, columns, publicationKey: guessPublicationKey(columns.publication), revenueUnit: "major", defaultCurrency: "EUR", }); }; const mapping = draft ? toMapping(draft) : null; const runImport = async () => { if (!csv || !mapping) return; setBusy(true); setError(null); try { const saved = await rpc.media.metrics.saveMapping(mapping); if (!saved.id) throw new Error(t`Could not save the mapping.`); const outcome = await rpc.media.metrics.importCsv({ mappingId: saved.id, csv }); setResult(outcome); onImported(); } catch (cause) { setError(rpcErrorMessage(cause, t`Import failed.`)); } finally { setBusy(false); } }; return ( Import results {result ? (

{result.skipped.length > 0 ? (
    {result.skipped.slice(0, 20).map((skip) => (
  • Row {skip.row}: {skip.reason}
  • ))}
) : null}
) : (
{ event.preventDefault(); void runImport(); }} > CSV file { const file = event.target.files?.[0]; if (file) void readFile(file); }} /> {draft && csv ? ( <> {mappings.length > 0 ? ( Mapping { const chosen = mappings.find((row) => row.id === event.target.value); setDraft(chosen ? draftFrom(chosen) : { ...draft, id: undefined }); }} > {t`New mapping`} {mappings.map((row) => ( {row.name} ))} ) : null} Platform setDraft({ ...draft, name: event.target.value })} />
{MAPPING_COLUMNS.map((column) => ( {labels[column]} setDraft({ ...draft, columns: { ...draft.columns, [column]: event.target.value || undefined, }, }) } > – {headers.map((header) => ( {header} ))} ))} Publication is setDraft({ ...draft, publicationKey: event.target.value as MetricCsvMapping["publicationKey"], }) } > {(Object.keys(keyLabels) as MetricCsvMapping["publicationKey"][]).map( (key) => ( {keyLabels[key]} ), )} Revenue in
setDraft({ ...draft, revenueUnit: event.target.value as MetricCsvMapping["revenueUnit"], }) } > 1.00 100 setDraft({ ...draft, defaultCurrency: event.target.value.toUpperCase() }) } />
) : null} {error ? (

{error}

) : null}
)}
); }