fix(auditoria): afluencia (validacion, update sin perder datos ni retroceder etapa, dedup cruzado, notas), alta cliente (registro tras firma, documentos por cliente, aviso de fallos) y reporte de clientes aplanado

This commit is contained in:
2026-09-18 16:05:18 +00:00
parent 4965a4488a
commit 656116c4e5
13 changed files with 295 additions and 59 deletions

View File

@@ -54,7 +54,7 @@ describe('services/api', () => {
expect(resultado).toEqual({ ok: true, id: 'srv-123' })
expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe('http://192.168.10.123:3000/api/clientes')
expect(url).toBe('/api/clientes')
expect(init.method).toBe('POST')
expect(init.headers).toEqual({ 'Content-Type': 'application/json' })
@@ -119,7 +119,7 @@ describe('services/api', () => {
expect(fetchMock).toHaveBeenCalledTimes(2)
const [url1, init1] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url1).toBe('http://192.168.10.123:3000/api/clientes/srv-123/documentos')
expect(url1).toBe('/api/clientes/srv-123/documentos')
expect(init1.method).toBe('POST')
// Sin Content-Type manual: el navegador pone el boundary del multipart
expect(init1.headers).toBeUndefined()
@@ -143,11 +143,13 @@ describe('services/api', () => {
expect(fetchMock).not.toHaveBeenCalled()
})
it('ante un fallo de red avisa y continúa sin lanzar', async () => {
it('ante un fallo de red avisa y devuelve los documentos que no subieron', async () => {
fetchMock.mockRejectedValue(new TypeError('fetch failed'))
await expect(subirDocumentos('srv-123', altaConDocumentos())).resolves.toBeUndefined()
expect(warnSpy).toHaveBeenCalledTimes(2)
// No lanza: el alta ya quedó registrada; se reportan los fallos al usuario
const { fallidos } = await subirDocumentos('srv-123', altaConDocumentos())
expect(fallidos.length).toBeGreaterThanOrEqual(1)
expect(warnSpy).toHaveBeenCalled()
})
})
})

View File

@@ -126,10 +126,17 @@ export async function completarProgreso(id: string, payload: AltaCliente): Promi
* Sube los documentos reales del expediente (POST multipart
* /clientes/:id/documentos, campo 'archivos'). El backend acepta un solo
* `tipo` por petición, así que se envía una petición por documento con su
* slot como tipo. Los fallos se reportan con console.warn sin interrumpir
* el flujo local.
* slot como tipo.
*
* Devuelve los documentos que NO se pudieron subir: el alta ya quedó
* registrada, así que el usuario debe enterarse para reintentar (antes se
* perdían en silencio).
*/
export async function subirDocumentos(clienteId: string, payload: AltaCliente): Promise<void> {
export async function subirDocumentos(
clienteId: string,
payload: AltaCliente,
): Promise<{ fallidos: { slot: string; nombre: string }[] }> {
const fallidos: { slot: string; nombre: string }[] = []
for (const doc of payload.documentos) {
try {
const form = new FormData()
@@ -146,8 +153,10 @@ export async function subirDocumentos(clienteId: string, payload: AltaCliente):
if (!res.ok) throw new Error(`HTTP ${res.status}`)
} catch (err) {
console.warn(`[api] subirDocumentos: no se pudo subir ${doc.nombre} (${doc.slot})`, err)
fallidos.push({ slot: doc.slot, nombre: doc.nombre })
}
}
return { fallidos }
}
// ---------- Borradores del alta (auth) ----------
@@ -264,6 +273,7 @@ export interface LeadCreado {
export type ResultadoCrearLead =
| { ok: true; lead: LeadCreado }
| { ok: false; motivo: 'duplicado'; leadId: string; nombre: string }
| { ok: false; motivo: 'validacion'; detalle: string }
| { ok: false; motivo: 'conexion' }
/**
@@ -308,13 +318,25 @@ export async function crearAfluencia(payload: {
return { ok: false, motivo: 'conexion' }
}
manejar401(res)
if (res.status === 201) {
// 201 = afluencia nueva; 200 = actualización de una afluencia existente
if (res.status === 201 || res.status === 200) {
return { ok: true, lead: (await res.json()) as LeadCreado }
}
if (res.status === 409) {
const cuerpo = (await res.json()) as { leadId: string; nombre: string }
return { ok: false, motivo: 'duplicado', leadId: cuerpo.leadId, nombre: cuerpo.nombre }
}
if (res.status === 400) {
// Validación del servidor: se muestra el motivo real, no "sin conexión"
const cuerpo = (await res.json().catch(() => null)) as
| { error?: string; issues?: { message?: string; path?: (string | number)[] }[] }
| null
const detalle =
cuerpo?.issues?.map((i) => `${i.path?.join('.') ?? ''}: ${i.message ?? ''}`).filter(Boolean).join(' · ') ||
cuerpo?.error ||
'Datos incompletos o inválidos'
return { ok: false, motivo: 'validacion', detalle }
}
console.warn(`[api] crearAfluencia: HTTP ${res.status}`)
return { ok: false, motivo: 'conexion' }
}