import { useRef, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import { client, errorMessage } from '../../api/client' import type { ContactGroup, ImportMapping, ImportPreview, ImportResult } from '../../api/types' import { Icon } from '../../components/Icon' import { useToasts } from '../../components/Toasts' import { Alert, Button, Select, Spinner } from '../../components/ui' import { Modal } from '../../components/Modal' /** * Import kontaktow z pliku CSV. * * TRZY KROKI, NIE JEDEN * ------------------------------------------------------------------------ * Wybor pliku, potem mapowanie kolumn z podgladem, dopiero na koncu import. * * Podglad jest tu najwazniejszy: pomylka w kolejnosci kolumn oznaczalaby setki * kontaktow z nazwiskiem w polu firmy, a odkrecanie tego recznie kosztuje * wielokrotnie wiecej niz jedno spojrzenie na piec przykladowych wierszy. */ /** Pola, ktore da sie zmapowac. Kolejnosc jak w formularzu kontaktu. */ const FIELDS: { key: keyof ImportMapping; label: string; required: boolean }[] = [ { key: 'email', label: 'Adres e-mail', required: true }, { key: 'firstName', label: 'Imię', required: false }, { key: 'lastName', label: 'Nazwisko', required: false }, { key: 'phone', label: 'Telefon', required: false }, { key: 'company', label: 'Firma', required: false }, { key: 'position', label: 'Stanowisko', required: false }, ] export function ContactImportDialog({ groups, onClose, }: { groups: ContactGroup[] onClose: () => void }) { const queryClient = useQueryClient() const toasts = useToasts() const fileInput = useRef(null) const [file, setFile] = useState(null) const [preview, setPreview] = useState(null) const [mapping, setMapping] = useState(null) const [groupId, setGroupId] = useState('') const [grantConsent, setGrantConsent] = useState(false) const [busy, setBusy] = useState(false) const [result, setResult] = useState(null) async function handleFile(selected: File) { setFile(selected) setBusy(true) try { const form = new FormData() form.append('file', selected) const response = await client.post('/admin/contacts/import/preview', form) setPreview(response.data) setMapping(response.data.suggested) } catch (error) { toasts.error(errorMessage(error, 'Nie udało się odczytać pliku')) setFile(null) } finally { setBusy(false) } } async function handleImport() { if (!file || !mapping) return setBusy(true) try { const form = new FormData() form.append('file', file) const response = await client.post('/admin/contacts/import', form, { params: { emailColumn: mapping.email, firstNameColumn: mapping.firstName, lastNameColumn: mapping.lastName, phoneColumn: mapping.phone, companyColumn: mapping.company, positionColumn: mapping.position, groupId: groupId || undefined, grantConsent, }, timeout: 0, }) setResult(response.data) queryClient.invalidateQueries({ queryKey: ['contacts'] }) queryClient.invalidateQueries({ queryKey: ['contact-groups'] }) } catch (error) { toasts.error(errorMessage(error, 'Nie udało się zaimportować pliku')) } finally { setBusy(false) } } // --- raport po imporcie --- if (result) { return (
{result.skipped.length > 0 && (

Wiersze pominięte wraz z powodem

{result.skipped.map((row) => ( ))}
Wiersz Adres Powód
{row.line} {row.email || '-'} {row.reason}
)}
) } // --- wybor pliku i mapowanie --- return (
{ const selected = e.target.files?.[0] if (selected) handleFile(selected) }} /> {!preview && (

Wybierz plik CSV z listą kontaktów.

Separatorem może być średnik albo przecinek. Pierwszy wiersz to nazwy kolumn.

)} {busy && preview && } {preview && mapping && ( <>

Plik zawiera {preview.totalRows}{' '} wierszy z danymi. Sprawdź, czy kolumny zostały rozpoznane poprawnie.

{FIELDS.map((field) => (
))}
{mapping.email < 0 && ( Wskaż kolumnę z adresem e-mail. Bez niej nie da się rozpoznać kontaktów. )} {/* podglad: tak zostana zrozumiane pierwsze wiersze */}

Podgląd pierwszych wierszy

{FIELDS.filter((f) => mapping[f.key] >= 0).map((f) => ( ))} {preview.sample.map((row, index) => ( {FIELDS.filter((f) => mapping[f.key] >= 0).map((f) => ( ))} ))}
{f.label}
{row[mapping[f.key]] ?? ''}
)}
) } function Summary({ label, value, tone, }: { label: string value: number tone: 'ok' | 'info' | 'warn' }) { const toneClass = tone === 'ok' ? 'text-ok-600' : tone === 'warn' ? 'text-warn-600' : 'text-blue-700' return (

{value}

{label}

) }