import React, { useState, useEffect } from 'react'; import * as XLSX from 'xlsx'; import { useAuth } from '../contexts/AuthContext'; import { useRegion } from '../contexts/RegionContext'; import { Button } from '../components/Button'; import { Upload, FileText, CheckCircle, AlertTriangle, Calendar, Database, UserPlus } from 'lucide-react'; const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; type ImportType = 'fot' | 'agenda' | 'profissionais' | 'financeiro'; interface ImportFotInput { fot: string; empresa_nome: string; curso_nome: string; ano_formatura_label: string; instituicao: string; cidade: string; estado: string; observacoes: string; gastos_captacao: number; pre_venda: boolean; } interface ImportAgendaInput { fot: string; data: string; tipo_evento: string; observacoes: string; local: string; endereco: string; horario: string; qtd_formandos: number; qtd_fotografos: number; qtd_cinegrafistas: number; qtd_recepcionistas: number; qtd_estudios: number; qtd_ponto_foto: number; qtd_ponto_id: number; qtd_ponto_decorado: number; qtd_pontos_led: number; qtd_plataforma_360: number; foto_faltante: number; recep_faltante: number; cine_faltante: number; logistica_observacoes: string; pre_venda: boolean; } interface ImportProfissionalInput { id?: string; // Optional for validation display nome: string; funcao_profissional_id: string; // Must resolve to ID endereco: string; cidade: string; uf: string; whatsapp: string; cpf_cnpj_titular: string; banco: string; agencia: string; conta_pix: string; observacao: string; email: string; // Sometimes in PIX column // Add other fields as needed } interface ImportFinanceInput { fot: string; data: string; tipo_evento: string; tipo_servico: string; nome: string; whatsapp: string; cpf: string; tabela_free: string; valor_free: number; valor_extra: number; descricao_extra: string; total_pagar: number; data_pgto: string; pgto_ok: boolean; } export const ImportData: React.FC = () => { const { token } = useAuth(); const { currentRegion } = useRegion(); // Read initial tab from URL const query = new URLSearchParams(window.location.search); const initialTab = (query.get('tab') as ImportType) || 'fot'; const [activeTab, setActiveTab] = useState(initialTab); // Generic data state (can be Fot, Agenda, Profissionais) const [data, setData] = useState([]); const [filename, setFilename] = useState(""); const [isLoading, setIsLoading] = useState(false); const [result, setResult] = useState<{ success: number; errors: string[] } | null>(null); // Cache functions for mapping const [functionsMap, setFunctionsMap] = useState>({}); useEffect(() => { // Fetch functions to map Name -> ID const fetchFunctions = async () => { if (!token) return; try { const res = await fetch(`${API_BASE_URL}/api/funcoes`, { headers: { "Authorization": `Bearer ${token}` } }); if (res.ok) { const list = await res.json(); const map: Record = {}; list.forEach((f: any) => { map[f.nome.toLowerCase()] = f.id; map[f.nome] = f.id; // Case sensitive fallback }); // Add mappings for variations if common map['fotografo'] = map['fotógrafo']; map['fotógrafa'] = map['fotógrafo']; map['recepcionista'] = map['recepcionista']; // ... setFunctionsMap(map); } } catch (e) { console.error("Failed to fetch functions", e); } }; fetchFunctions(); }, [token]); const [skippedCount, setSkippedCount] = useState(0); const [skippedReasons, setSkippedReasons] = useState>({}); const handleTabChange = (tab: ImportType) => { if (tab !== activeTab) { setActiveTab(tab); setData([]); setSkippedCount(0); setFilename(""); setResult(null); } }; const handleFileUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setFilename(file.name); setSkippedCount(0); const reader = new FileReader(); reader.onload = (evt) => { const bstr = evt.target?.result; const wb = XLSX.read(bstr, { type: 'binary', cellStyles: true, cellNF: true, sheetStubs: true }); const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; const jsonData = XLSX.utils.sheet_to_json(ws, { header: 1 }) as any[][]; const rows = jsonData as any[]; // Access row metadata for hidden status (if available) const rowMeta = ws['!rows'] || []; let mappedData: any[] = []; let skipped = 0; let reasons: Record = {}; const processedFots = new Set(); const addReason = (msg: string) => { skipped++; reasons[msg] = (reasons[msg] || 0) + 1; }; // HELPER: Check if row is hidden or empty const isRowHidden = (rowIndex: number) => { // Check if row exists in metadata and is hidden if (rowMeta && rowMeta[rowIndex] && rowMeta[rowIndex].hidden) return true; return false; }; if (activeTab === 'financeiro') { // Dynamic Header Mapping // 1. Find header row (look for "FOT" and "Nome") let headerIdx = -1; const colMap: {[key: string]: number} = {}; for (let i = 0; i < 20 && i < rows.length; i++) { const rowStrings = (rows[i] as any[]).map(c => String(c).toLowerCase().trim()); if (rowStrings.some(s => s === 'fot' || s === 'nome' || s === 'cpf')) { headerIdx = i; rowStrings.forEach((h, idx) => { if (h === 'fot' || h.includes('contrato')) colMap['fot'] = idx; else if (h === 'data' || h.includes('dt evento') || h.includes('data evento')) colMap['data'] = idx; else if (h.includes('evento')) colMap['evento'] = idx; // Match 'Tipo Evento', 'Evento', etc. else if (h === 'serviço' || h === 'servico' || h.includes('função') || h.includes('tp serv') || h.includes('tipo de serv') || h.includes('tipo serv')) colMap['servico'] = idx; else if (h === 'nome' || h === 'profissional') colMap['nome'] = idx; else if (h === 'whatsapp' || h === 'whats' || h === 'tel' || h === 'cel') colMap['whatsapp'] = idx; else if (h === 'cpf') colMap['cpf'] = idx; else if (h.includes('tab') || h.includes('tabela')) colMap['tabela'] = idx; else if (h.includes('v. free') || h.includes('valor free') || h === 'cache') colMap['vfree'] = idx; else if (h.includes('v. extra') || h.includes('valor extra')) colMap['vextra'] = idx; else if (h.includes('desc') || h.includes('obs extra')) colMap['descextra'] = idx; else if (h === 'total' || h.includes('total')) colMap['total'] = idx; else if (h.includes('dt pgto') || h.includes('data pag') || h.includes('data pgto')) colMap['datapgto'] = idx; else if (h === 'ok' || h.includes('pago') || h === 'status') colMap['pgtook'] = idx; }); break; } } if (headerIdx === -1) { // Fallback to index based if header not found headerIdx = 0; colMap['fot'] = 0; colMap['data'] = 1; colMap['evento'] = 2; colMap['servico'] = 3; colMap['nome'] = 4; colMap['whatsapp'] = 5; colMap['cpf'] = 6; colMap['tabela'] = 7; colMap['vfree'] = 8; colMap['vextra'] = 9; colMap['descextra'] = 10; colMap['total'] = 11; colMap['datapgto'] = 12; colMap['pgtook'] = 13; } // Iterate data rows for (let i = headerIdx + 1; i < rows.length; i++) { if (isRowHidden(i)) continue; // SKIP HIDDEN ROWS const row = rows[i] as any[]; if (!row || row.length === 0) { addReason("Linha Vazia/Separador"); continue; } const getCol = (key: string) => { const idx = colMap[key]; if (idx === undefined || idx < 0) return ""; return row[idx] !== undefined ? String(row[idx]).trim() : ""; }; const getRawCol = (key: string) => { const idx = colMap[key]; if (idx === undefined || idx < 0) return undefined; return row[idx]; }; const fot = getCol('fot'); const nome = getCol('nome'); const totalPagar = getCol('total'); const valorFree = getCol('vfree'); // Skip if Nome is empty AND (Total is 0 or empty AND Free is 0 or empty) // AND FOT checks. const isTotalZero = !totalPagar || parseFloat(totalPagar.replace(/[R$\s.]/g, '').replace(',', '.')) === 0; const isFreeZero = !valorFree || parseFloat(valorFree.replace(/[R$\s.]/g, '').replace(',', '.')) === 0; if (!fot && !nome) { addReason("Linha Vazia/Separador"); continue; } if (!nome && isTotalZero && isFreeZero) { addReason("Linha sem Dados Financeiros"); continue; } // VALIDATION STRICT: Skip if FOT and Nome are empty or just "-" if ((!fot || fot.length < 1 || fot === '-') && (!nome || nome.length < 2)) { addReason("Dados Incompletos (Sem FOT/Nome)"); continue; } const parseVal = (str: string) => { if (!str) return 0; // Handle 1.234,56 or 1234.56 let s = str.replace(/[R$\s]/g, ''); if (s.includes(',') && s.includes('.')) { // 1.234,56 -> remove dots, replace comma with dot s = s.replace(/\./g, '').replace(',', '.'); } else if (s.includes(',')) { s = s.replace(',', '.'); } return parseFloat(s) || 0; }; const parseDateCol = (key: string) => { const raw = getRawCol(key); if (typeof raw === 'number') { const dateObj = new Date(Math.round((raw - 25569)*86400*1000)); // Return YYYY-MM-DD for backend consistency return dateObj.toISOString().split('T')[0]; } const str = getCol(key); // Try to fix DD/MM/YYYY to YYYY-MM-DD if (str.includes('/')) { const parts = str.split('/'); if (parts.length === 3) return `${parts[2]}-${parts[1]}-${parts[0]}`; } return str; }; const item: ImportFinanceInput = { fot: fot, data: parseDateCol('data'), tipo_evento: getCol('evento'), tipo_servico: getCol('servico'), nome: getCol('nome'), whatsapp: getCol('whatsapp'), cpf: getCol('cpf'), tabela_free: getCol('tabela'), valor_free: parseVal(getCol('vfree')), valor_extra: parseVal(getCol('vextra')), descricao_extra: getCol('descextra'), total_pagar: parseVal(getCol('total')), data_pgto: parseDateCol('datapgto'), pgto_ok: getCol('pgtook').toLowerCase().includes('sim') || getCol('pgtook').toLowerCase() === 'ok', }; mappedData.push(item); } setData(mappedData); setSkippedCount(0); setResult(null); return; // EXIT early for Finance } // Start from row 1 (skip header) for OTHER inputs for (let i = 1; i < jsonData.length; i++) { if (isRowHidden(i)) continue; // SKIP HIDDEN ROWS const row = jsonData[i]; if (!row || row.length === 0) continue; // Helper to get string const getStr = (idx: number) => row[idx] ? String(row[idx]).trim() : ""; // Helper to extract/clean CPF/CNPJ const cleanCpf = (raw: string): string => { if (!raw) return ""; // 1. If explicit "CPF" or "CNPJ" label exists, prioritize digits after it const labelMatch = raw.match(/(?:cpf|cnpj)[\s:./-]*([\d.-]+)/i); if (labelMatch && labelMatch[1]) { const digits = labelMatch[1].replace(/\D/g, ''); if (digits.length === 11 || digits.length === 14) return digits; // Return digits only? Or formatted? // Let's return formatted if possible or just digits. // Storing digits-only is safer for matching. return digits; } // 2. Try to find standard CPF pattern (11 digits with separators) // 111.222.333-44 const cpfPattern = /\b\d{3}\.\d{3}\.\d{3}-\d{2}\b/; const cpfMatch = raw.match(cpfPattern); if (cpfMatch) return cpfMatch[0].replace(/\D/g, ''); // 3. Try standard CNPJ pattern const cnpjPattern = /\b\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}\b/; const cnpjMatch = raw.match(cnpjPattern); if (cnpjMatch) return cnpjMatch[0].replace(/\D/g, ''); // 4. Fallback: Cleanup all non-digits const onlyDigits = raw.replace(/\D/g, ''); // If length is 11 (CPF) or 14 (CNPJ), assume valid if (onlyDigits.length === 11 || onlyDigits.length === 14) return onlyDigits; // If length is ambiguous (e.g. RG+CPF concatenated without separators and no labels), // It's hard. But users usually put separators or labels. // If the user pasted "328 065 058 52" (Spaces), onlyDigits handles it (11 chars). // Should we return strict or loose? // If we can't detect, return empty to force 'Skipped'? // Or return original raw and let user verify? // Returning original raw breaks Claim match. // Better to return empty if invalid format, to alert user they need to fix Excel. if (onlyDigits.length > 0) return onlyDigits; // Return whatever we found? return ""; }; // Helper to clean UF (State code) const cleanUf = (raw: string): string => { if (!raw) return ""; return raw.trim().slice(0, 2).toUpperCase(); }; // Helper to clean generic string with max len const cleanStr = (raw: string, maxLen: number): string => { if (!raw) return ""; let s = raw.trim(); if (s.length > maxLen) { s = s.substring(0, maxLen); } return s; }; // Helper to get int const getInt = (idx: number) => { const val = row[idx]; if (!val) return 0; if (typeof val === 'number') return Math.floor(val); const parsed = parseInt(String(val).replace(/\D/g, ''), 10); return isNaN(parsed) ? 0 : parsed; }; if (activeTab === 'fot') { // Dynamic Mapping for FOT let headerIdx = -1; const colMap: {[key: string]: number} = {}; // 1. Find header row for (let i = 0; i < 20 && i < rows.length; i++) { const rowStrings = (rows[i] as any[]).map(c => String(c).toLowerCase().trim()); if (rowStrings.some(s => s === 'fot' || s === 'empresa' || s.includes('contrato'))) { headerIdx = i; rowStrings.forEach((h, idx) => { if (h === 'fot' || h.includes('contrato')) colMap['fot'] = idx; else if (h === 'empresa' || h.includes('cliente')) colMap['empresa'] = idx; else if (h.includes('curso')) colMap['curso'] = idx; else if (h.includes('obs') || h.includes('observa')) colMap['obs'] = idx; else if (h.includes('institui')) colMap['instituicao'] = idx; else if (h.includes('ano') || h.includes('formatur')) colMap['ano'] = idx; else if (h === 'cidade') colMap['cidade'] = idx; else if (h === 'estado' || h === 'uf') colMap['estado'] = idx; else if (h.includes('gasto') || h.includes('capta')) colMap['gastos'] = idx; else if (h.includes('pré') || h.includes('pre') || h.includes('venda')) colMap['prevenda'] = idx; }); break; } } if (headerIdx === -1) { // Fallback to indices if not found (Legacy/Default) headerIdx = 0; // Default: FOT, Empresa, Curso, Obs, Inst, Ano, Cid, UF, Gastos, Prevenda colMap['fot'] = 0; colMap['empresa'] = 1; colMap['curso'] = 2; colMap['obs'] = 3; colMap['instituicao'] = 4; colMap['ano'] = 5; colMap['cidade'] = 6; colMap['estado'] = 7; colMap['gastos'] = 8; colMap['prevenda'] = 9; } // Iterate for (let i = headerIdx + 1; i < rows.length; i++) { if (isRowHidden(i)) continue; // SKIP HIDDEN ROWS const row = rows[i] as any[]; if (!row || row.length === 0) continue; const getCol = (key: string) => { const idx = colMap[key]; if (idx === undefined || idx < 0) return ""; const val = row[idx]; return (val !== undefined && val !== null) ? String(val).trim() : ""; }; // REGION FILTER const rowEstado = getCol('estado'); if (rowEstado) { const r = rowEstado.toUpperCase().trim(); const c = currentRegion.toUpperCase().trim(); const normalize = (s: string) => { if (s === 'MINAS GERAIS') return 'MG'; if (s === 'SÃO PAULO' || s === 'SAO PAULO') return 'SP'; return s; }; if (normalize(r) !== normalize(c)) { addReason(`Região Incompatível (Esperado ${c}, Encontrado ${r})`); continue; } } const fot = getCol('fot'); // Strict validation if (!fot || fot.length < 3 || fot.toLowerCase() === 'null' || fot.toLowerCase() === 'undefined') { addReason("FOT Inválido/Vazio"); continue; } // DEDUPLICATION if (processedFots.has(fot)) { addReason("FOT Duplicado no Arquivo"); continue; } processedFots.add(fot); // Parse Gastos let gastos = 0; const rawGastos = getCol('gastos'); // Note: getCol returns string/empty. We need raw access. const valIdx = colMap['gastos']; const valRaw = (valIdx !== undefined && valIdx >= 0) ? row[valIdx] : null; if (typeof valRaw === 'number') { gastos = valRaw; } else { let gastosStr = String(valRaw || ""); if (gastosStr) { const cleanStr = gastosStr.replace(/[^\d,-]/g, '').replace(',', '.'); gastos = parseFloat(cleanStr) || 0; } } const item: ImportFotInput = { fot: fot, empresa_nome: getCol('empresa'), curso_nome: getCol('curso'), observacoes: getCol('obs'), instituicao: getCol('instituicao'), ano_formatura_label: getCol('ano'), cidade: getCol('cidade'), estado: getCol('estado'), gastos_captacao: gastos, pre_venda: getCol('prevenda').toLowerCase().includes('sim'), }; mappedData.push(item); } } else if (activeTab === 'agenda') { const fot = getStr(0); if (!fot) continue; // Agenda Parsing let dateStr = getStr(1); if (typeof row[1] === 'number') { const dateObj = new Date(Math.round((row[1] - 25569)*86400*1000)); dateStr = dateObj.toLocaleDateString('pt-BR'); } const item: ImportAgendaInput = { fot: fot, data: dateStr, tipo_evento: getStr(7), // H observacoes: getStr(8), // I local: getStr(9), // J endereco: getStr(10), // K horario: getStr(11), // L qtd_formandos: getInt(12), // M qtd_fotografos: getInt(13), // N qtd_cinegrafistas: getInt(14), // O qtd_estudios: getInt(15), qtd_recepcionistas: getInt(22) > 0 ? getInt(22) : 0, qtd_ponto_foto: getInt(16), // Q qtd_ponto_id: getInt(17), // R qtd_ponto_decorado: getInt(18), // S qtd_pontos_led: getInt(19), // T qtd_plataforma_360: getInt(20), // U foto_faltante: parseInt(row[23]) || 0, // X recep_faltante: parseInt(row[24]) || 0, // Y cine_faltante: parseInt(row[25]) || 0, // Z logistica_observacoes: getStr(26), // AA pre_venda: getStr(27).toLowerCase().includes('sim'), // AB }; mappedData.push(item); } else if (activeTab === 'profissionais') { const nome = getStr(0); if (!nome) continue; // Skip empty names too // Mapping based on screenshot // A=Nome, B=Funcao, C=End, D=Cid, E=UF, F=Whats, G=CPF, H=Banco, I=Agencia, J=Conta const funcaoNome = getStr(1).toLowerCase(); let funcaoId = ""; for (const key in functionsMap) { if (funcaoNome.includes(key)) { funcaoId = functionsMap[key]; break; } } const emailInPix = getStr(9).includes('@') ? getStr(9) : ""; // Extracts clean CPF (using helper defined above in file, assumed lines 150-190) // But cleanCpf matches lines 150-190? Yes. I need to make sure I invoke it. // Wait, where is cleanCpf? // In snippet 15598 lines 150-190 is anonymous arrow function? // NO. Snippet 15598 line 152: `const labelMatch = raw.match...` // It seems cleanCpf was defined as: // Lines 150... // It was NOT named `cleanCpf` in snippet 15598! // Lint error `cleanCpf` not found. // I must define `cleanCpf` too! // Snippet 15598 shows just logic inside `handleFileUpload` loop? // Or was it inside `cleanCpf`? // Let's assume lines 150-190 ARE inside `cleanCpf`. // But where is the declaration? // I'll assume I need to ADD declaration `const cleanCpf = ...`. // I'll check lines 140-150 in next step. For now I must create it if missing. // Or I can inline it. // Better to assume `cleanCpf` is missing and I should add it or use `cleanCpf` logic inline. // I will use `cleanStr` for now and risk it? No, CPF cleaning is complex. // I will invoke `cleanCpf(getStr(6))` but I need to ensure `cleanCpf` exists. // I will add `cleanCpf` definition in this block for safety. const cleanCpf = (raw: string) => { if (!raw) return ""; const onlyDigits = raw.replace(/\D/g, ''); if (onlyDigits.length === 11 || onlyDigits.length === 14) return onlyDigits; return ""; }; const cpfVal = cleanCpf(getStr(6)); const item: ImportProfissionalInput = { nome: cleanStr(nome, 255), funcao_profissional_id: funcaoId, endereco: cleanStr(getStr(2), 255), cidade: cleanStr(getStr(3), 100), uf: cleanUf(getStr(4)), whatsapp: cleanStr(getStr(5), 20), cpf_cnpj_titular: cpfVal, banco: cleanStr(getStr(7), 100), agencia: cleanStr(getStr(8), 20), conta_pix: cleanStr(emailInPix ? emailInPix : getStr(9), 120), observacao: cleanStr(getStr(14), 65535), email: emailInPix, }; if (item.cpf_cnpj_titular) { mappedData.push(item); } else { addReason("CPF/CNPJ Inválido ou Ausente"); } } } setData(mappedData); setSkippedCount(skipped); setSkippedReasons(reasons); // Update state setResult(null); }; reader.readAsBinaryString(file); }; const handleImport = async () => { if (!token) return; setIsLoading(true); // Reset result before start setResult(null); try { let endpoint = ""; if (activeTab === 'fot') endpoint = '/api/import/fot'; else if (activeTab === 'agenda') endpoint = '/api/import/agenda'; else if (activeTab === 'profissionais') endpoint = '/api/profissionais/import'; else if (activeTab === 'financeiro') endpoint = '/api/finance/import'; const CHUNK_SIZE = 1000; let successes = 0; let errorsList: string[] = []; // Batch loop for (let i = 0; i < data.length; i += CHUNK_SIZE) { const chunk = data.slice(i, i + CHUNK_SIZE); console.log(`Importing chunk ${(i/CHUNK_SIZE)+1}/${Math.ceil(data.length/CHUNK_SIZE)} (${chunk.length} records)`); const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" }, body: JSON.stringify(chunk) }); if (!response.ok) { const errBody = await response.text(); // Try parse JSON let errMsg = response.statusText; try { const jsonErr = JSON.parse(errBody); errMsg = jsonErr.error || errMsg; } catch (e) {} errorsList.push(`Chunk ${i}: ${errMsg}`); console.error(`Chunk failed: ${errMsg}`); // We continue processing other chunks? // Or stop? // Usually valid records in other chunks should be saved. // But if user expects all or nothing, this is tricky. // For now, continue best effort. } else { const resData = await response.json(); if (activeTab === 'profissionais') { successes += (resData.created || 0) + (resData.updated || 0); if (resData.errors && resData.errors.length > 0) { errorsList.push(...resData.errors); } } else if (resData.message) { // Some endpoints return just { message: "xxx" } implying all success? // Check handler. // If success, assume chunk length inserted if no detailed count? // Or parse message "Imported X records"? // Assuming chunk success: successes += chunk.length; } else { // Standard { SuccessCount: X, Errors: [] } successes += (resData.SuccessCount || 0) + (resData.imported || 0); // Handle variations if (resData.Errors && resData.Errors.length > 0) errorsList.push(...resData.Errors); } } } // Final Result if (errorsList.length > 0) { console.warn("Import finished with errors", errorsList); // Show summary setResult({ success: successes, errors: errorsList }); alert(`Importação concluída com parcialidade.\nSucesso: ${successes}\nErros: ${errorsList.length}. Verifique o resultado.`); } else { setResult({ success: successes, errors: [] }); alert(`Importação concluída com sucesso! ${successes} registros.`); } } catch (error: any) { console.error("Import error description:", error); alert("Erro crítico ao importar: " + (error.message || "Desconhecido")); } finally { setIsLoading(false); } }; // Drag scroll logic (omitted rewrite, standard) const tableContainerRef = React.useRef(null); const [isDragging, setIsDragging] = useState(false); const [startX, setStartX] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); const handleMouseDown = (e: React.MouseEvent) => { if (!tableContainerRef.current) return; setIsDragging(true); setStartX(e.pageX - tableContainerRef.current.offsetLeft); setScrollLeft(tableContainerRef.current.scrollLeft); }; const handleMouseLeave = () => { setIsDragging(false); }; const handleMouseUp = () => { setIsDragging(false); }; const handleMouseMove = (e: React.MouseEvent) => { if (!isDragging || !tableContainerRef.current) return; e.preventDefault(); const x = e.pageX - tableContainerRef.current.offsetLeft; const walk = (x - startX) * 2; tableContainerRef.current.scrollLeft = scrollLeft - walk; }; return (

Importação de Dados

Carregue planilhas do Excel para alimentar o sistema.

{/* Tabs */}
{/* Info Box */}
{activeTab === 'fot' && (

Colunas Esperadas (A-J): FOT, Empresa, Curso, Observações, Instituição, Ano Formatura, Cidade, Estado, Gastos Captação, Pré Venda.

)} {activeTab === 'agenda' && (

Colunas Esperadas (A-AB): FOT, Data, ..., Tipo Evento, Obs, Local, Endereço, Horário, Qtds (Formandos, Foto, Cine...), Faltantes, Logística.

)} {activeTab === 'profissionais' && (

Colunas Esperadas (A-J): Nome, Função, Endereço, Cidade, UF, Whatsapp, CPF/CNPJ (Obrigatório), Banco, Agencia, Conta PIX.

)} {activeTab === 'financeiro' && (

Colunas Esperadas (A-N): FOT, Data, Tipo Evento, Tipo Serviço, Nome, Whatsapp, CPF, Tabela Free, Valor Free, Valor Extra, Desc. Extra, Total, Data Pgto, Pgto OK.

)}
{filename && (
Arquivo selecionado: {filename} ({data.length} registros)
)}
{data.length > 0 && !result && (

Pré-visualização ({activeTab.toUpperCase()})

Total: {data.length}
{activeTab === 'fot' && ( <> )} {activeTab === 'agenda' && ( <> )} {activeTab === 'profissionais' && ( <> )} {activeTab === 'financeiro' && ( <> )} {data.map((row, idx) => ( {activeTab === 'fot' && ( <> )} {activeTab === 'agenda' && ( <> )} {activeTab === 'profissionais' && ( <> )} {activeTab === 'financeiro' && ( <> )} ))}
FOT Empresa Curso Instituição Ano GastosFOT Data Evento Local Horário Formandos LogísticaNome CPF/CNPJ Função (ID) Whatsapp Cidade/UFFOT Data Profissional Serviço Total Status
{row.fot} {row.empresa_nome} {row.curso_nome} {row.instituicao} {row.ano_formatura_label} R$ {row.gastos_captacao?.toLocaleString('pt-BR', { minimumFractionDigits: 2 })}{row.fot} {row.data} {row.tipo_evento} {row.local} {row.horario} {row.qtd_formandos} {row.logistica_observacoes}{row.nome} {row.cpf_cnpj_titular} {row.funcao_profissional_id ? ( MATCH ) : ( NO ID )} {row.whatsapp} {row.cidade}/{row.uf}{row.fot} {row.data} {row.nome}
{row.cpf}
{row.tipo_servico} R$ {row.total_pagar.toFixed(2)} {row.pgto_ok ? ( Pago ) : ( Pendente )}
{skippedCount > 0 && (
{skippedCount} registros ignorados
{Object.entries(skippedReasons).map(([reason, count]) => (
• {count}x {reason}
))}
)} Total de Registros: {data.length}
)} {result && (
0 ? 'bg-yellow-50' : 'bg-green-50'}`}>
{result.errors.length === 0 ? ( ) : ( )}

Resultado da Importação

Sucesso: {result.success} registros importados/atualizados.

{result.errors.length > 0 && (

Erros ({result.errors.length}):

    {result.errors.map((err, idx) => (
  • {err}
  • ))}
)}
)}
); };