feat(alta-clientes): telefono obligatorio en alta y afluencia, importacion de informacion Enkontrol por ID en pantalla final y campos de obra movidos a la seccion Obra en captura

This commit is contained in:
2026-09-25 21:41:59 +00:00
parent 2400ecf6ff
commit 6435086181
11 changed files with 269 additions and 24 deletions

View File

@@ -426,6 +426,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente, valor
placeholder="664 123 4567"
value={form.telefono}
onChange={cambiar('telefono')}
required
className={`${inputClase} ${errores.telefono ? 'border-red-400' : ''}`}
/>
</div>

View File

@@ -19,6 +19,7 @@ import {
eliminarBorrador,
generarLigaRemota,
guardarBorrador,
importarInformacionEnkontrol,
iniciarProgreso,
listarBorradores,
obtenerBorrador,
@@ -27,7 +28,9 @@ import {
subirDocumentos,
type BorradorResumen,
type LeadResumen,
type ResultadoImportacionEnkontrol,
} from '../services/api'
import { getSesion } from '../services/auth'
import { cotizacionAPrellenado } from '../lib/cotizacionAlta'
import { apartadoAPrellenado } from '../lib/apartadoAlta'
import { leadAPrellenado } from '../lib/leadAlta'
@@ -137,6 +140,29 @@ export default function AltaClientePage() {
const [ligaCopiada, setLigaCopiada] = useState(false)
const [errorLiga, setErrorLiga] = useState('')
// ---------- Importación del documento de Enkontrol (solo administración) ----------
const rolSesion = getSesion()?.usuario.rol ?? ''
const puedeImportarEnkontrol = rolSesion === 'admin' || rolSesion === 'manager'
const [importandoEnkontrol, setImportandoEnkontrol] = useState(false)
const [resultadoImportacion, setResultadoImportacion] = useState<ResultadoImportacionEnkontrol | null>(null)
const [errorImportacion, setErrorImportacion] = useState('')
const inputImportacionRef = useRef<HTMLInputElement>(null)
const importarEnkontrol = async (archivo: File | undefined) => {
if (!archivo) return
setImportandoEnkontrol(true)
setErrorImportacion('')
setResultadoImportacion(null)
try {
setResultadoImportacion(await importarInformacionEnkontrol(archivo))
} catch (err) {
setErrorImportacion(err instanceof Error ? err.message : 'No se pudo importar el documento.')
} finally {
setImportandoEnkontrol(false)
if (inputImportacionRef.current) inputImportacionRef.current.value = ''
}
}
// Afluencia prellenada desde Mis oportunidades (Mi día): actualiza la
// oportunidad existente en vez de crear una nueva
const [afluenciaPrevia, setAfluenciaPrevia] = useState<{
@@ -610,6 +636,51 @@ export default function AltaClientePage() {
</button>
</div>
{puedeImportarEnkontrol && (
<div className="mt-6 rounded-lg border border-[#E5E7EB] bg-white p-4 dark:border-[#334155] dark:bg-[#0F172A]">
<p className="text-sm font-medium text-[#111827] dark:text-[#E2E8F0]">
Importar información de Enkontrol
</p>
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
Sube el documento de Enkontrol (Excel o CSV). Cada fila se empareja con la ficha
del cliente usando el ID de Enkontrol como campo determinante y su información se
guarda en la ficha.
</p>
<div className="mt-3">
<button
type="button"
onClick={() => inputImportacionRef.current?.click()}
disabled={importandoEnkontrol}
className="rounded-lg border border-accent bg-white px-4 py-2 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:opacity-50 dark:bg-[#0F172A] dark:border-accent dark:text-accent"
>
{importandoEnkontrol ? 'Importando…' : 'Seleccionar documento…'}
</button>
<input
ref={inputImportacionRef}
type="file"
accept=".xlsx,.xls,.csv"
className="sr-only"
onChange={(e) => void importarEnkontrol(e.target.files?.[0])}
/>
</div>
{resultadoImportacion && (
<p role="status" className="mt-3 rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-xs text-green-700 dark:border-green-800 dark:bg-green-900/30 dark:text-green-300">
Importación lista: {resultadoImportacion.actualizados} de {resultadoImportacion.totalFilas}{' '}
clientes actualizados.
{resultadoImportacion.noEncontrados.length > 0 && (
<> Sin ficha local: {resultadoImportacion.noEncontrados.slice(0, 10).join(', ')}
{resultadoImportacion.noEncontrados.length > 10 ? '…' : ''}</>
)}
</p>
)}
{errorImportacion && (
<p role="alert" className="mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700">
{errorImportacion}
</p>
)}
</div>
)}
<p className="mt-6 text-xs text-green-800/80 dark:text-green-400/80">
Con la liga de firma, el cliente firma su expediente y sube documentos desde su
celular (vigencia 72 h, sin usuario ni contraseña).

View File

@@ -73,8 +73,7 @@ async function enviarAlta(ruta: string, payload: AltaCliente): Promise<Resultado
/**
* Registra el alta en el API Gateway (POST /clientes). El backend persiste en
* SQLite.
*/
export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoAlta> {
*/export async function crearAltaCliente(payload: AltaCliente): Promise<ResultadoAlta> {
return enviarAlta('/clientes', payload)
}
@@ -414,3 +413,30 @@ export async function subirDocumentoRemoto(token: string, tipo: string, archivo:
})
manejarErrorLiga(res)
}
/** Resultado de la importación del documento de Enkontrol */
export interface ResultadoImportacionEnkontrol {
totalFilas: number
actualizados: number
noEncontrados: string[]
}
/**
* POST /clientes/importar-enkontrol — sube el documento de Enkontrol (Excel o
* CSV). El ID de Enkontrol es el campo determinante: cada fila actualiza la
* ficha del cliente con ese usuarioEnkontrol.
*/
export async function importarInformacionEnkontrol(
archivo: File,
): Promise<ResultadoImportacionEnkontrol> {
const form = new FormData()
form.append('archivo', archivo, archivo.name)
const res = await fetch(`${API_URL}/clientes/importar-enkontrol`, conAuth({ method: 'POST', body: form }))
manejar401(res)
if (!res.ok) {
const cuerpo = (await res.json().catch(() => null)) as { error?: string } | null
throw new Error(cuerpo?.error ?? `No se pudo importar (HTTP ${res.status})`)
}
return (await res.json()) as ResultadoImportacionEnkontrol
}

View File

@@ -40,6 +40,15 @@ export const CAMPOS_POR_DEPARTAMENTO: Record<DepartamentoId, CampoDef[]> = {
{ campo: 'listas_avaluo', tipo: 'boolean', label: 'Listas para Avaluo', conFecha: 'listas_avaluo_fecha' },
{ campo: 'listas_avaluo_fecha', tipo: 'date', label: 'Fecha Listas para Avaluo' },
{ campo: 'habitabilidad', tipo: 'date', label: 'Habitabilidad a partir de' },
// Fechas de obra (las llena el área de obra; rigen el arranque y la entrega)
{ campo: 'inicio_obra', tipo: 'boolean', label: 'Inicio Obra', conFecha: 'inicio_obra_fecha' },
{ campo: 'inicio_obra_fecha', tipo: 'date', label: 'Fecha Inicio Obra' },
{ campo: 'entrega_programada', tipo: 'boolean', label: 'Entrega Programada', conFecha: 'entrega_programada_fecha' },
{ campo: 'entrega_programada_fecha', tipo: 'date', label: 'Fecha Programada Entrega Vivienda' },
{ campo: 'entrega_reprogramada', tipo: 'boolean', label: 'Entrega Reprogramada', conFecha: 'entrega_reprogramada_fecha' },
{ campo: 'entrega_reprogramada_fecha', tipo: 'date', label: 'Fecha Reprogramada Entrega Vivienda' },
{ campo: 'desviacion', tipo: 'number', label: 'Desviación (días)' },
{ campo: 'fecha_autorizacion', tipo: 'date', label: 'Fecha Autorización' },
{ campo: 'comentarios_obra', tipo: 'text', label: 'Comentarios' },
],
VENTAS: [
@@ -55,13 +64,6 @@ export const CAMPOS_POR_DEPARTAMENTO: Record<DepartamentoId, CampoDef[]> = {
{ campo: 'precio_venta', tipo: 'number', label: 'Precio Venta' },
],
ESCRITURACION: [
{ campo: 'inicio_obra', tipo: 'boolean', label: 'Inicio Obra', conFecha: 'inicio_obra_fecha' },
{ campo: 'inicio_obra_fecha', tipo: 'date', label: 'Fecha Inicio Obra' },
{ campo: 'entrega_programada', tipo: 'boolean', label: 'Entrega Programada', conFecha: 'entrega_programada_fecha' },
{ campo: 'entrega_programada_fecha', tipo: 'date', label: 'Fecha Programada Entrega Vivienda' },
{ campo: 'entrega_reprogramada', tipo: 'boolean', label: 'Entrega Reprogramada', conFecha: 'entrega_reprogramada_fecha' },
{ campo: 'entrega_reprogramada_fecha', tipo: 'date', label: 'Fecha Reprogramada Entrega Vivienda' },
{ campo: 'fecha_autorizacion', tipo: 'date', label: 'Fecha Autorización' },
{ campo: 'fecha_firma', tipo: 'date', label: 'Fecha Firma' },
{ campo: 'notaria', tipo: 'text', label: 'Notaría' },
{ campo: 'fecha_pago_banco', tipo: 'date', label: 'Fecha Pago Banco' },