Dark mode fixes: - Cards secundarias: Mis capturas, Mis clientes, Actividades, Apartados por vencer, Equipo - Botones: Exportar, Descartar, Afluencia, Agendo, Forzar reconexión, Subir archivo - Config: ModoTrabajo, Usuarios, Sistema, RolesPermisos, ConfigPage - Cotizador: inputs, tabs, placeholder, botón compartir - Alta clientes: BotonOcrIne, FileUploader - Cobranza: toggle dashboard, modal abono - Dashboards: tabs Gráficas Recharts: - CSS variables para colores chart (grid, text, tooltip) - Helper isDark() + axisTextColor() + gridStroke() + tooltipProps() - DashboardVendedor: grid, axes, tooltips adaptados - TableroGeneral: grid, axes, tooltips (BarChart, Funnel, PieChart) - MiTablero: tooltip adaptado - Perfilamiento: grid, axes, tooltip 7 apps construidas limpias.
146 lines
5.9 KiB
TypeScript
146 lines
5.9 KiB
TypeScript
import { useState } from 'react'
|
|
import type { DatosGenerales } from '../types'
|
|
import { parsearTextoIne } from '../lib/ocrIne'
|
|
import type { SugerenciasIne } from '../lib/ocrIne'
|
|
|
|
type CamposAplicables = Partial<Pick<DatosGenerales, 'curp' | 'nombreCompleto' | 'fechaNacimiento'>>
|
|
|
|
interface Props {
|
|
/** Imagen (JPG/PNG) de la credencial INE ya cargada */
|
|
archivo: File
|
|
/** Aplica un campo sugerido al formulario de datos generales */
|
|
onAplicar: (campos: CamposAplicables) => void
|
|
}
|
|
|
|
type Estado =
|
|
| { fase: 'inactivo' }
|
|
| { fase: 'procesando'; progreso: number }
|
|
| { fase: 'listo'; sugerencias: SugerenciasIne }
|
|
| { fase: 'error'; mensaje: string }
|
|
|
|
interface SugerenciaVisible {
|
|
clave: keyof CamposAplicables | 'sexo'
|
|
etiqueta: string
|
|
valor: string
|
|
/** Solo estos campos tienen destino en el formulario */
|
|
aplicable: boolean
|
|
}
|
|
|
|
function aVisibles(s: SugerenciasIne): SugerenciaVisible[] {
|
|
const v: SugerenciaVisible[] = []
|
|
if (s.curp) v.push({ clave: 'curp', etiqueta: 'CURP', valor: s.curp, aplicable: true })
|
|
if (s.nombre) v.push({ clave: 'nombreCompleto', etiqueta: 'Nombre', valor: s.nombre, aplicable: true })
|
|
if (s.fechaNacimiento)
|
|
v.push({ clave: 'fechaNacimiento', etiqueta: 'Fecha de nacimiento', valor: s.fechaNacimiento, aplicable: true })
|
|
if (s.sexo) v.push({ clave: 'sexo', etiqueta: 'Sexo', valor: s.sexo === 'H' ? 'Hombre' : 'Mujer', aplicable: false })
|
|
return v
|
|
}
|
|
|
|
/**
|
|
* OCR del INE con tesseract.js (import dinámico para no inflar el bundle).
|
|
* El idioma español se sirve local desde /client/tessdata (base path /client/).
|
|
*/
|
|
export default function BotonOcrIne({ archivo, onAplicar }: Props) {
|
|
const [estado, setEstado] = useState<Estado>({ fase: 'inactivo' })
|
|
const [aplicados, setAplicados] = useState<string[]>([])
|
|
|
|
const leer = async () => {
|
|
setEstado({ fase: 'procesando', progreso: 0 })
|
|
setAplicados([])
|
|
try {
|
|
const { createWorker } = await import('tesseract.js')
|
|
const worker = await createWorker('spa', undefined, {
|
|
langPath: `${import.meta.env.BASE_URL}tessdata`,
|
|
gzip: false,
|
|
logger: (m: { status: string; progress: number }) => {
|
|
if (m.status === 'recognizing text') {
|
|
setEstado({ fase: 'procesando', progreso: Math.round(m.progress * 100) })
|
|
}
|
|
},
|
|
})
|
|
try {
|
|
const { data } = await worker.recognize(archivo)
|
|
const sugerencias = parsearTextoIne(data.text)
|
|
if (aVisibles(sugerencias).length === 0) {
|
|
setEstado({
|
|
fase: 'error',
|
|
mensaje: 'No se detectaron datos legibles en la imagen. Intenta con una foto más nítida.',
|
|
})
|
|
} else {
|
|
setEstado({ fase: 'listo', sugerencias })
|
|
}
|
|
} finally {
|
|
await worker.terminate()
|
|
}
|
|
} catch {
|
|
setEstado({ fase: 'error', mensaje: 'No fue posible ejecutar el OCR. Verifica tu conexión e inténtalo de nuevo.' })
|
|
}
|
|
}
|
|
|
|
const aplicar = (s: SugerenciaVisible) => {
|
|
if (!s.aplicable) return
|
|
onAplicar({ [s.clave]: s.valor } as CamposAplicables)
|
|
setAplicados((prev) => [...prev, s.clave])
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">Lectura automática (OCR)</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => void leer()}
|
|
disabled={estado.fase === 'procesando'}
|
|
className="shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB] disabled:opacity-40"
|
|
>
|
|
{estado.fase === 'procesando' ? `Leyendo… ${estado.progreso}%` : 'Leer INE con OCR'}
|
|
</button>
|
|
</div>
|
|
|
|
{estado.fase === 'procesando' && (
|
|
<p className="mt-3 text-xs text-[#64748B] dark:text-[#94A3B8]" role="status">
|
|
Reconociendo texto de la credencial… {estado.progreso}%
|
|
</p>
|
|
)}
|
|
|
|
{estado.fase === 'error' && (
|
|
<p className="mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700" role="alert">
|
|
{estado.mensaje}
|
|
</p>
|
|
)}
|
|
|
|
{estado.fase === 'listo' && (
|
|
<div className="mt-3">
|
|
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">
|
|
Sugerencias detectadas — nada se sobreescribe sin tu confirmación:
|
|
</p>
|
|
<ul className="mt-2 space-y-2">
|
|
{aVisibles(estado.sugerencias).map((s) => (
|
|
<li key={s.clave} className="flex items-baseline justify-between gap-4 text-sm">
|
|
<span className="text-[#111827] dark:text-[#E2E8F0]">
|
|
<span className="mr-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
|
{s.etiqueta}
|
|
</span>
|
|
{s.valor}
|
|
</span>
|
|
{s.aplicable &&
|
|
(aplicados.includes(s.clave) ? (
|
|
<span className="rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700">✓</span>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
onClick={() => aplicar(s)}
|
|
className="shrink-0 rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-1.5 text-xs font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
|
>
|
|
Aplicar
|
|
</button>
|
|
))}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|