import React, { useEffect, useMemo, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { AlertTriangle, Calendar, CheckCircle2, Eye, FileSpreadsheet, Filter, Loader2, Menu, RefreshCw, Search, X, } from 'lucide-react'; import { CountryConfig, HistoricalReport, PeriodRange, ReconciliationRow } from '../types'; import BambooCorrectionModal from './BambooCorrectionModal'; import ReportIssueButton from './ReportIssueButton'; import { supabase } from '../lib/supabase'; interface HistoryProps { onShowToast: (msg: string, type: 'success' | 'error' | 'info') => void; onOpenSidebar?: () => void; operatorName: string; operatorEmail: string; country: CountryConfig; } const REPORTS_PER_PAGE = 10; function getHistoryUrl(countryCode: string) { if (countryCode === 'TT') { return (import.meta.env.VITE_HISTORICOS_TT_URL as string | undefined) || ''; } if (countryCode === 'PA') { return ( (import.meta.env.VITE_HISTORICOS_PA_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pa-historicos' ); } if (countryCode === 'SV') { return ( (import.meta.env.VITE_HISTORICOS_SV_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-historicos' ); } if (countryCode === 'HN') { return ( (import.meta.env.VITE_HISTORICOS_HN_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-historicos' ); } return ( (import.meta.env.VITE_HISTORICOS_GT_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-historicos' ); } function getMarkResolvedUrl(countryCode: string) { if (countryCode === 'TT') { return (import.meta.env.VITE_MARCAR_RESUELTO_TT_URL as string | undefined) || ''; } if (countryCode === 'PA') { return ( (import.meta.env.VITE_MARCAR_RESUELTO_PA_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-pa-marcar-resuelto' ); } if (countryCode === 'SV') { return ( (import.meta.env.VITE_MARCAR_RESUELTO_SV_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-sv-marcar-resuelto' ); } if (countryCode === 'HN') { return ( (import.meta.env.VITE_MARCAR_RESUELTO_HN_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-hn-marcar-resuelto' ); } return ( (import.meta.env.VITE_MARCAR_RESUELTO_GT_URL as string | undefined) || 'https://agenteit.digitalcompass.agency/webhook/cruce-cuentas-gt-marcar-resuelto' ); } function toNumber(value: unknown) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : 0; } function formatDate(value: unknown, locale: string) { if (!value) return ''; const date = new Date(String(value)); if (Number.isNaN(date.getTime())) return String(value).slice(0, 10); return date.toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', }); } function formatDateTime(value: unknown, locale: string) { if (!value) return ''; const date = new Date(String(value)); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString(locale, { dateStyle: 'short', timeStyle: 'short', }); } function normalizeStatus(value: unknown): HistoricalReport['status'] { return String(value ?? '').toLowerCase().includes('resuelto') ? 'Resuelto' : 'Pendiente revisión'; } function normalizeReports(input: unknown, country: CountryConfig): HistoricalReport[] { const payload = input as Record | unknown[] | null | undefined; const data = (!Array.isArray(payload) && payload ? payload : {}) as Record; const rawReports = Array.isArray(payload) ? payload : Array.isArray(data.reports) ? data.reports : []; return rawReports .map((raw, index) => { const report = raw as Record; const discrepancies = toNumber(report.discrepancias ?? report.discrepancies); const bankWithoutPayroll = toNumber( report.bancoSinNomina ?? report.banco_sin_nomina ?? report.bankWithoutPayroll ); const bankWithoutBamboo = toNumber( report.bancoSinBamboo ?? report.banco_sin_bamboo ?? report.bankWithoutBamboo ); const payrollWithoutAccount = toNumber( report.nominaSinCuenta ?? report.nomina_sin_cuenta ?? report.payrollWithoutAccount ); const nameDifferences = toNumber( report.diferenciasNombreBanco ?? report.diferencias_nombre_banco ?? report.nameDifferences ); const totalPending = discrepancies + bankWithoutPayroll + bankWithoutBamboo + payrollWithoutAccount + nameDifferences; const rawCountry = String(report.country ?? report.pais ?? report.country_code ?? country.code).toUpperCase(); const bankFiles = Array.isArray(report.archivosBanco) ? report.archivosBanco.map(String) : Array.isArray(report.bank_file_names) ? report.bank_file_names.map(String) : []; const rawExecutionDate = report.updated_at ?? report.fechaEjecucion ?? report.ejecutado_en ?? report.created_at; return { id: String(report.id ?? `reporte-${index + 1}`), country: (rawCountry === 'TT' ? 'TT' : rawCountry === 'PA' ? 'PA' : rawCountry === 'SV' ? 'SV' : rawCountry === 'HN' ? 'HN' : 'GT') as HistoricalReport['country'], date: formatDate(report.fecha ?? report.period_end ?? rawExecutionDate, country.locale), executionDate: formatDateTime(rawExecutionDate, country.locale), period: String(report.periodo ?? report.period_label ?? report.period ?? 'Período no especificado'), periodStart: report.period_start ? String(report.period_start) : undefined, periodEnd: report.period_end ? String(report.period_end) : undefined, payrollFile: String(report.archivoNomina ?? report.payroll_file_name ?? report.payrollFile ?? ''), bankFiles, matches: toNumber(report.coincidencias ?? report.matches), discrepancies, bankWithoutPayroll, bankWithoutBamboo, payrollWithoutAccount, nameDifferences, totalPending, totalPayroll: toNumber(report.totalNomina ?? report.total_nomina), totalBank: toNumber(report.totalBanco ?? report.total_banco), totalDifference: toNumber(report.diferenciaTotal ?? report.diferencia_total), status: normalizeStatus(report.estado ?? report.status ?? report.estadoLabel), reportUrl: report.reportUrl || report.report_url ? String(report.reportUrl ?? report.report_url) : undefined, spreadsheetId: report.spreadsheetId || report.spreadsheet_id ? String(report.spreadsheetId ?? report.spreadsheet_id) : undefined, executedBy: report.ejecutadoPorNombre || report.ejecutado_por_nombre ? String(report.ejecutadoPorNombre ?? report.ejecutado_por_nombre) : undefined, executedByEmail: report.ejecutadoPorEmail || report.ejecutado_por_email ? String(report.ejecutadoPorEmail ?? report.ejecutado_por_email) : undefined, resolvedBy: report.resueltoPorNombre || report.resuelto_por_nombre ? String(report.resueltoPorNombre ?? report.resuelto_por_nombre) : undefined, resolvedByEmail: report.resueltoPorEmail || report.resuelto_por_email ? String(report.resueltoPorEmail ?? report.resuelto_por_email) : undefined, resolvedAt: formatDateTime(report.resueltoEn ?? report.resuelto_en, country.locale), resolutionComment: report.comentarioResolucion || report.comentario_resolucion ? String(report.comentarioResolucion ?? report.comentario_resolucion) : undefined, _sortTime: new Date(String(rawExecutionDate ?? report.period_end ?? '')).getTime() || 0, } as HistoricalReport & { _sortTime: number }; }) .filter((report) => report.country === country.code) .sort((a, b) => b._sortTime - a._sortTime) .map(({ _sortTime: _ignored, ...report }) => report); } function normalizeIdentityText(value: unknown) { return String(value ?? '') .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9 ]/g, ' ') .replace(/\s+/g, ' ') .trim(); } function normalizeAccount(value: unknown) { return String(value ?? '') .replace(/\.0$/g, '') .replace(/\D/g, '') .trim(); } function extractCaseNumber(item: Record, keys: string[]) { for (const key of keys) { const value = item[key]; if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string' && value.trim()) { const parsed = Number( value .replace(/[A-Za-z$Q,\s]/g, '') .trim(), ); if (Number.isFinite(parsed)) return parsed; } } return null; } function normalizeHistoricalBambooCases(input: unknown): ReconciliationRow[] { if (!Array.isArray(input)) return []; return input.map((raw, index) => { const item = (raw ?? {}) as Record; const sourceName = String( item.employee ?? item.employee_name ?? item.bank_name_file ?? item.bank_account_holder ?? 'Sin nombre', ); const amountBank = extractCaseNumber(item, [ 'bankAmount', 'bank_amount', 'amount', 'monto_banco', ]); const bestCandidate = item.best_bamboo_candidate && typeof item.best_bamboo_candidate === 'object' ? item.best_bamboo_candidate as Record : null; return { id: String( item.id ?? `historical-bamboo-${normalizeIdentityText(sourceName)}-${normalizeAccount(item.account ?? item.bank_account)}-${index + 1}`, ), employee: sourceName, employeeNumber: '', account: String(item.account ?? item.bank_account ?? item.bankAccount ?? ''), amountPayroll: 0, amountBank, difference: amountBank === null ? null : 0 - amountBank, status: 'Pendiente revisión', category: 'banco_sin_bamboo', source: 'banco_sin_bamboo', observation: String( item.observation ?? item.observacion ?? 'Pago bancario sin empleado identificado de forma confiable en BambooHR.', ), bankNameFile: String(item.bank_name_file ?? item.bankNameFile ?? ''), bankAccountHolder: String(item.bank_account_holder ?? item.bankAccountHolder ?? ''), bestBambooCandidate: bestCandidate ? { employeeNumber: String(bestCandidate.employee_number ?? bestCandidate.employeeNumber ?? ''), employeeName: String(bestCandidate.employee_name ?? bestCandidate.employeeName ?? ''), score: extractCaseNumber(bestCandidate, ['score']), } : null, ambiguousBambooMatch: Boolean(item.ambiguous_bamboo_match), }; }); } interface HistoricalBambooCorrection { id: string; sourceName: string; sourceNames: string[]; bankAccount: string; status: string; emailSent: boolean; } function normalizeHistoricalBambooCorrections(input: unknown): HistoricalBambooCorrection[] { if (!Array.isArray(input)) return []; return input.map((raw) => { const item = (raw ?? {}) as Record; return { id: String(item.id ?? ''), sourceName: String(item.nombre_origen_principal ?? item.source_name ?? ''), sourceNames: Array.isArray(item.nombres_origen) ? item.nombres_origen.map(String) : [], bankAccount: String(item.cuenta_bancaria ?? item.bank_account ?? ''), status: String(item.estado ?? ''), emailSent: item.correo_enviado === true, }; }); } function correctionMatchesRow( correction: HistoricalBambooCorrection, row: ReconciliationRow, ) { const normalizedStatus = correction.status.toLowerCase(); const wasActuallyNotified = correction.emailSent || normalizedStatus === 'notificado_rrhh' || normalizedStatus === 'corregido_bamboo'; // Si n8n logró registrar el intento pero el correo falló, se permite volver // a enviarlo desde Históricos en vez de bloquear el caso por error. if (!wasActuallyNotified) return false; const correctionAccount = normalizeAccount(correction.bankAccount); const rowAccount = normalizeAccount(row.account); if ( correctionAccount && rowAccount && correctionAccount !== rowAccount ) { return false; } const correctionNames = new Set( [correction.sourceName, ...correction.sourceNames] .map(normalizeIdentityText) .filter(Boolean), ); const rowNames = [ row.employee, row.bankNameFile, row.bankAccountHolder, ] .map(normalizeIdentityText) .filter(Boolean); return rowNames.some((name) => correctionNames.has(name)); } function statusClass(status: HistoricalReport['status']) { if (status === 'Resuelto') return 'bg-blue-50 text-blue-800 border-blue-200'; return 'bg-amber-50 text-amber-800 border-amber-200'; } function statusText(status: HistoricalReport['status']) { return status === 'Resuelto' ? 'RESUELTO' : 'PENDIENTE REVISIÓN'; } export default function History({ onShowToast, onOpenSidebar, operatorName, operatorEmail, country, }: HistoryProps) { const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState<'todos' | 'pendiente' | 'resuelto'>('todos'); const [reports, setReports] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const [isSavingResolution, setIsSavingResolution] = useState(false); const [loadError, setLoadError] = useState(''); const [selectedReport, setSelectedReport] = useState(null); const [resolutionComment, setResolutionComment] = useState(''); const [bambooReviewReport, setBambooReviewReport] = useState(null); const [bambooCases, setBambooCases] = useState([]); const [reportedBambooRows, setReportedBambooRows] = useState>(new Set()); const [isLoadingBambooCases, setIsLoadingBambooCases] = useState(false); const [bambooCasesError, setBambooCasesError] = useState(''); const [bambooCorrectionRow, setBambooCorrectionRow] = useState(null); const [totalReports, setTotalReports] = useState(0); const [totalFilteredReports, setTotalFilteredReports] = useState(0); const requestSequenceRef = useRef(0); const activeRequestControllerRef = useRef(null); const totalPages = Math.max(1, Math.ceil(totalFilteredReports / REPORTS_PER_PAGE)); const paginatedReports = reports; useEffect(() => { setCurrentPage(1); }, [searchTerm, statusFilter, country.code]); useEffect(() => { if (currentPage > totalPages) setCurrentPage(totalPages); }, [currentPage, totalPages]); const loadReports = async () => { const requestId = ++requestSequenceRef.current; activeRequestControllerRef.current?.abort(); const controller = new AbortController(); activeRequestControllerRef.current = controller; const isCurrentRequest = () => requestSequenceRef.current === requestId && !controller.signal.aborted; setIsLoading(true); setLoadError(''); try { // Camino principal: RPC paginada en Supabase. Cada respuesta se valida // contra un identificador monotónico para que una solicitud vieja nunca // pueda sobrescribir el filtro seleccionado más recientemente. if (supabase) { const rpcStatus = statusFilter === 'pendiente' ? 'pendiente_revision' : statusFilter === 'resuelto' ? 'resuelto' : null; const { data: rpcData, error: rpcError } = await supabase.rpc( 'cruce_cuentas_get_historicos', { p_country: country.code, p_page: currentPage, p_page_size: REPORTS_PER_PAGE, p_search: searchTerm.trim() || null, p_status: rpcStatus, } ); if (!isCurrentRequest()) return; if (!rpcError && rpcData) { const payload = Array.isArray(rpcData) ? rpcData[0] : rpcData; const normalized = normalizeReports(payload, country); const total = toNumber((payload as any)?.total); const filteredTotal = toNumber((payload as any)?.filtered_total ?? total); setReports(normalized); setTotalReports(total); setTotalFilteredReports(filteredTotal); return; } } if (!isCurrentRequest()) return; // Respaldo temporal: webhook histórico existente. También se envía el país // para impedir que se mezclen reportes de Guatemala, Trinidad y Tobago, Panamá, El Salvador y Honduras. const historyUrl = getHistoryUrl(country.code); if (!historyUrl) { throw new Error(`No está configurado el origen histórico de ${country.name}.`); } const url = new URL(historyUrl); url.searchParams.set('country', country.code); const response = await fetch(url.toString(), { method: 'GET', signal: controller.signal, }); const payload = await response.json().catch(() => null); if (!isCurrentRequest()) return; if (!response.ok || payload?.ok === false) { throw new Error('No se pudieron cargar los reportes históricos.'); } const allCountryReports = normalizeReports(payload, country); const normalizedSearch = searchTerm.trim().toLowerCase(); const filtered = allCountryReports.filter((report) => { const matchesStatus = statusFilter === 'todos' || (statusFilter === 'pendiente' && report.status === 'Pendiente revisión') || (statusFilter === 'resuelto' && report.status === 'Resuelto'); const matchesSearch = !normalizedSearch || report.period.toLowerCase().includes(normalizedSearch) || report.payrollFile.toLowerCase().includes(normalizedSearch) || report.id.toLowerCase().includes(normalizedSearch) || (report.resolvedBy ?? '').toLowerCase().includes(normalizedSearch) || (report.executedBy ?? '').toLowerCase().includes(normalizedSearch); return matchesStatus && matchesSearch; }); if (!isCurrentRequest()) return; const offset = (currentPage - 1) * REPORTS_PER_PAGE; setReports(filtered.slice(offset, offset + REPORTS_PER_PAGE)); setTotalReports(allCountryReports.length); setTotalFilteredReports(filtered.length); } catch (error) { if ( !isCurrentRequest() || (error instanceof DOMException && error.name === 'AbortError') ) { return; } setReports([]); setTotalReports(0); setTotalFilteredReports(0); setLoadError( error instanceof Error ? error.message : 'No se pudieron cargar los reportes históricos. Intenta nuevamente.' ); onShowToast('No se pudieron cargar los reportes históricos.', 'error'); } finally { if (isCurrentRequest()) { setIsLoading(false); if (activeRequestControllerRef.current === controller) { activeRequestControllerRef.current = null; } } } }; useEffect(() => { const timer = window.setTimeout(() => { void loadReports(); }, searchTerm.trim() ? 300 : 0); return () => { window.clearTimeout(timer); requestSequenceRef.current += 1; activeRequestControllerRef.current?.abort(); activeRequestControllerRef.current = null; }; }, [country.code, currentPage, searchTerm, statusFilter]); const closeBambooReview = () => { setBambooCorrectionRow(null); setBambooReviewReport(null); setBambooCases([]); setReportedBambooRows(new Set()); setBambooCasesError(''); }; const handleOpenBambooCases = async (report: HistoricalReport) => { if (report.bankWithoutBamboo <= 0) { onShowToast('Este reporte no tiene casos de Banco sin Bamboo.', 'info'); return; } if (!supabase) { onShowToast('Supabase no está configurado en esta aplicación.', 'error'); return; } setBambooReviewReport(report); setBambooCorrectionRow(null); setBambooCases([]); setReportedBambooRows(new Set()); setBambooCasesError(''); setIsLoadingBambooCases(true); try { const { data, error } = await supabase.rpc( 'cruce_cuentas_get_banco_sin_bamboo_reporte', { p_report_id: report.id, p_country: country.code, }, ); if (error) throw error; const payload = Array.isArray(data) ? data[0] : data; if (!payload || payload.ok === false) { throw new Error( String(payload?.message || 'No se pudieron recuperar los casos de Banco sin Bamboo.'), ); } const cases = normalizeHistoricalBambooCases(payload.cases); const corrections = normalizeHistoricalBambooCorrections(payload.corrections); const alreadyReported = new Set( cases .filter((row) => corrections.some((correction) => correctionMatchesRow(correction, row))) .map((row) => row.id), ); setBambooCases(cases); setReportedBambooRows(alreadyReported); if (cases.length === 0) { setBambooCasesError( 'Este reporte fue creado antes de que se guardara el detalle de Banco sin Bamboo. Los reportes nuevos conservarán los casos para revisarlos desde Históricos.', ); } } catch (error) { const message = error instanceof Error ? error.message : 'No se pudieron recuperar los casos de Banco sin Bamboo.'; setBambooCasesError(message); onShowToast(message, 'error'); } finally { setIsLoadingBambooCases(false); } }; const bambooPeriodRange: PeriodRange = { label: bambooReviewReport?.period || 'Período no especificado', start: bambooReviewReport?.periodStart || '', end: bambooReviewReport?.periodEnd || '', }; const handleViewReport = (report: HistoricalReport) => { if (report.reportUrl) { window.open(report.reportUrl, '_blank', 'noopener,noreferrer'); return; } onShowToast('Este reporte aún no tiene un Google Sheet disponible.', 'info'); }; const handleResolveReport = (report: HistoricalReport) => { if (report.status === 'Resuelto') { onShowToast('Este reporte ya fue marcado como resuelto.', 'info'); return; } setSelectedReport(report); setResolutionComment(''); }; const handleConfirmResolve = async () => { if (!selectedReport) return; const trimmedComment = resolutionComment.trim(); if (trimmedComment.length < 10) { onShowToast('Debes escribir un comentario de resolución más descriptivo.', 'error'); return; } const markResolvedUrl = getMarkResolvedUrl(country.code); if (!markResolvedUrl) { onShowToast(`La resolución de reportes para ${country.name} estará disponible próximamente.`, 'info'); return; } setIsSavingResolution(true); try { const response = await fetch(markResolvedUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reportId: selectedReport.id, comentarioResolucion: trimmedComment, resueltoPorNombre: operatorName, resueltoPorEmail: operatorEmail, country: country.code, countryName: country.name, }), }); const payload = await response.json().catch(() => null); if (!response.ok || payload?.ok === false) { const message = Array.isArray(payload?.errors) ? payload.errors.join('\n') : payload?.message || 'No se pudo marcar el reporte como resuelto.'; throw new Error(message); } const updated = payload.report ?? {}; const resolvedAt = formatDateTime(updated.resueltoEn ?? new Date().toISOString(), country.locale); setReports((currentReports) => currentReports.map((item) => item.id === selectedReport.id ? { ...item, status: 'Resuelto', resolvedBy: String(updated.resueltoPorNombre ?? operatorName), resolvedByEmail: String(updated.resueltoPorEmail ?? operatorEmail), resolvedAt, resolutionComment: String(updated.comentarioResolucion ?? trimmedComment), } : item ) ); setSelectedReport(null); setResolutionComment(''); onShowToast('Reporte marcado como resuelto correctamente.', 'success'); } catch (error) { const message = error instanceof Error ? error.message : 'No se pudo marcar el reporte como resuelto.'; onShowToast(message, 'error'); } finally { setIsSavingResolution(false); } }; return (

Reportes Históricos - {country.name}

{`Bandera
setSearchTerm(event.target.value)} className="w-full pl-10 pr-4 py-2 border border-[#D0D0D0] text-xs font-medium placeholder-neutral-400 focus:outline-none focus:border-[#4F758B] transition-colors" />
{[ ['todos', 'Todos'], ['pendiente', 'Pendiente revisión'], ['resuelto', 'Resuelto'], ].map(([value, label]) => ( ))}
Mostrando: {totalFilteredReports} de {totalReports} reportes
{loadError && (
{loadError}
)}
{isLoading ? ( ) : paginatedReports.length > 0 ? ( paginatedReports.map((report) => ( )) ) : ( )}
Fecha Período Archivo de Nómina / Bono 14 Coincidencias Banco sin Bamboo Pendientes Estado Resuelto por Acciones
Cargando reportes históricos...
{report.date} {report.executionDate &&

{report.executionDate}

}
{report.period}
{report.payrollFile || 'No especificado'}
{report.matches} {report.bankWithoutBamboo > 0 ? ( ) : ( 0 )} 0 ? 'bg-red-50 text-[#ba1a1a] border-red-200' : 'bg-emerald-50 text-emerald-800 border-emerald-200' }`} > {report.totalPending === 0 ? 'Sin pendientes' : `${report.totalPending} pendiente${report.totalPending > 1 ? 's' : ''}`} {statusText(report.status)} {report.resolvedBy ? (

{report.resolvedBy}

{report.resolvedAt}

) : ( Pendiente )}
No se encontraron reportes históricos.
Mostrando {paginatedReports.length} de {totalFilteredReports} reporte(s).
Página {currentPage} de {totalPages}
{bambooReviewReport && (

Histórico · Banco sin BambooHR

{bambooReviewReport.period}

{country.name} · {bambooReviewReport.bankWithoutBamboo} caso{bambooReviewReport.bankWithoutBamboo === 1 ? '' : 's'} guardado{bambooReviewReport.bankWithoutBamboo === 1 ? '' : 's'} en este reporte

Aquí puedes volver días después al reporte y enviar a RRHH únicamente los falsos positivos que ya verificaste manualmente. Los casos previamente enviados se muestran como Reportado a RRHH.
{isLoadingBambooCases ? (
Recuperando casos del reporte...
) : bambooCasesError ? (
{bambooCasesError}
) : bambooCases.length > 0 ? (
{bambooCases.map((row) => { const isReported = reportedBambooRows.has(row.id); const amount = row.amountBank; return ( ); })}
Nombre detectado Cuenta Monto banco Candidato BambooHR Acción

{row.employee}

{(row.bankNameFile || row.bankAccountHolder) && (

{[row.bankNameFile, row.bankAccountHolder].filter(Boolean).join(' · ')}

)}
{row.account || '—'} {amount === null ? '—' : new Intl.NumberFormat(country.locale, { style: 'currency', currency: country.currency, maximumFractionDigits: 2, }).format(amount)} {row.bestBambooCandidate?.employeeName ? (

{row.bestBambooCandidate.employeeName}

{row.bestBambooCandidate.employeeNumber && (

Employee Number {row.bestBambooCandidate.employeeNumber}

)}
) : ( Sin candidato confiable )}
{isReported ? ( Reportado a RRHH ) : ( )}
) : (
No hay casos guardados en este reporte.
)}
ID del reporte: {bambooReviewReport.id}
)} {bambooCorrectionRow && bambooReviewReport && ( setBambooCorrectionRow(null)} onSuccess={(rowId) => { setReportedBambooRows((current) => { const next = new Set(current); next.add(rowId); return next; }); }} onShowToast={onShowToast} /> )} {selectedReport && (

Marcar reporte como resuelto

{selectedReport.period}

Antes de resolver, confirma que las diferencias fueron revisadas y explica cómo se cerró el caso.