fix(6-mejoras): subcategoría+apartado libre+PDF+coacreditado+docs+dedup

1. Subcategoría en modal descarte (catálogo completo + select)
2. Apartado sin monto mínimo (eliminada validación de 0,000)
3. PDF solo con propuesta elegida (no las 4)
4. Co-acreditado: checkbox 'no tiene' valida sin campos obligatorios
5. Documentos apartado: previsualizar + descargar (endpoint + frontend)
6. Oportunidades: sin duplicados, orden Confirmar→Afluencia→Cotizar→Aparto(verde)→Descartar
This commit is contained in:
2026-09-12 07:09:15 +00:00
parent c8357fe582
commit 852182fabc
12 changed files with 261 additions and 112 deletions

View File

@@ -35,6 +35,7 @@ const generalesValidos: DatosGenerales = {
tipoVivienda: 'PROPIA',
tipoCompra: 'VIVIR_AHI',
actividadOcupacion: 'AGENTE DEL MINISTERIO PUBLICO FEDERAL',
usuarioEnkontrol: '',
}
describe('regex de identificadores', () => {
@@ -128,6 +129,16 @@ describe('coacreditadoSchema', () => {
expect(r.success).toBe(true)
})
it('acepta datos ocultos a medias cuando el toggle está apagado', () => {
const conBasura = {
...base,
tieneCoacreditado: false as const,
generales: { ...base.generales, fecha: '', email: 'no-es-correo', codigoPostal: 'abc' },
}
const r = coacreditadoSchema.safeParse(conBasura)
expect(r.success).toBe(true)
})
it('exige el nombre del co-acreditado cuando el toggle está activo', () => {
const r = coacreditadoSchema.safeParse({ ...base, tieneCoacreditado: true })
expect(r.success).toBe(false)
@@ -141,6 +152,19 @@ describe('coacreditadoSchema', () => {
})
expect(r.success).toBe(true)
})
it('no exige usuarioEnkontrol del co-acreditado (no se captura en el bloque compacto)', () => {
const sinUsuarioEnkontrol = JSON.parse(
JSON.stringify({
...base,
tieneCoacreditado: true,
generales: { ...base.generales, nombreCompleto: 'MARIA LOPEZ PEREZ' },
}),
) as typeof base & { tieneCoacreditado: true }
delete (sinUsuarioEnkontrol.generales as Record<string, unknown>).usuarioEnkontrol
const r = coacreditadoSchema.safeParse(sinUsuarioEnkontrol)
expect(r.success).toBe(true)
})
})
describe('referenciasSchema', () => {

View File

@@ -112,33 +112,47 @@ export const economicosSchema = z.object({
})
// Datos generales del co-acreditado: el nombre se exige vía superRefine solo
// cuando el toggle está activo; el resto se valida solo si se captura
// cuando el toggle está activo; el resto se valida solo si se captura. La
// fecha y usuarioEnkontrol no se capturan en el bloque compacto y no bloquean.
const coacreditadoGeneralesSchema = datosGeneralesSchema.innerType().extend({
nombreCompleto: z.string().trim().max(200),
curp: curpOpcional,
rfc: rfcOpcional,
email: emailOpcional,
fecha: textoOpcional,
fechaNacimiento: textoOpcional,
celular: textoOpcional,
usuarioEnkontrol: textoOpcional.optional(),
})
export const coacreditadoSchema = z
.object({
tieneCoacreditado: z.boolean(),
generales: coacreditadoGeneralesSchema,
empleo: empleoSchema,
economicos: economicosSchema,
})
.superRefine((datos, ctx) => {
// Los datos del co-acreditado solo se exigen si el toggle está activo
if (datos.tieneCoacreditado && datos.generales.nombreCompleto.trim().length < 3) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['generales', 'nombreCompleto'],
message: 'El nombre del co-acreditado es requerido',
})
}
})
export const coacreditadoSchema = z.union([
// Checkbox "El cliente no tiene co-acreditado" MARCADO: los bloques están
// ocultos y pueden quedar a medias (o faltar); NO se valida nada.
z.object({
tieneCoacreditado: z.literal(false),
generales: z.any(),
empleo: z.any(),
economicos: z.any(),
}),
// Checkbox SIN marcar (hay co-acreditado): se validan los bloques; el único
// campo obligatorio es el nombre (el resto es opcional).
z
.object({
tieneCoacreditado: z.literal(true),
generales: coacreditadoGeneralesSchema,
empleo: empleoSchema,
economicos: economicosSchema,
})
.superRefine((datos, ctx) => {
if (datos.generales.nombreCompleto.trim().length < 3) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['generales', 'nombreCompleto'],
message: 'El nombre del co-acreditado es requerido',
})
}
}),
])
const referenciaSchema = z.object({
nombre: textoOpcional,