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:
@@ -154,3 +154,39 @@ export async function listarDocumentosApartado(req: Request, res: Response): Pro
|
||||
})
|
||||
res.json(documentos)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/apartados/:itemId/documentos/:docId/archivo — sirve el archivo del
|
||||
* documento (PDF/imagen) para previsualizarlo o descargarlo desde "Mis
|
||||
* apartados". El documento debe pertenecer al cliente ligado al apartado.
|
||||
*/
|
||||
export async function descargarDocumentoApartado(req: Request, res: Response): Promise<void> {
|
||||
const itemId = String(req.params['itemId'])
|
||||
const docId = String(req.params['docId'])
|
||||
const cliente = await prisma.cliente.findFirst({ where: { mondayApartadoId: itemId } })
|
||||
if (!cliente) {
|
||||
res.status(404).json({ error: 'Apartado sin cliente ligado' })
|
||||
return
|
||||
}
|
||||
const documento = await prisma.documento.findFirst({ where: { id: docId, clienteId: cliente.id } })
|
||||
if (!documento) {
|
||||
res.status(404).json({ error: 'Documento no encontrado' })
|
||||
return
|
||||
}
|
||||
// Guarda anti path-traversal: la ruta guardada vive dentro de uploadsDir
|
||||
const rutaAbsoluta = path.resolve(config.uploadsDir, documento.ruta)
|
||||
if (!rutaAbsoluta.startsWith(config.uploadsDir + path.sep)) {
|
||||
res.status(400).json({ error: 'Ruta de archivo inválida' })
|
||||
return
|
||||
}
|
||||
if (!fs.existsSync(rutaAbsoluta)) {
|
||||
res.status(404).json({ error: 'Archivo no encontrado en disco' })
|
||||
return
|
||||
}
|
||||
res.setHeader('Content-Type', documento.mime || 'application/octet-stream')
|
||||
res.setHeader(
|
||||
'Content-Disposition',
|
||||
`inline; filename="${documento.nombreArchivo.replace(/[^\w.\- ]+/g, '_')}"`,
|
||||
)
|
||||
res.sendFile(rutaAbsoluta)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import { folioCotizacion, vigenciaCotizacion } from '../utils/folio.js'
|
||||
const crearCotizacionSchema = z.object({
|
||||
unidadCodigo: z.string().min(1),
|
||||
acabados: z.boolean(),
|
||||
apartado: z.number().positive().optional(),
|
||||
// Sin mínimo: el vendedor puede apartar con cualquier monto, incluso $0
|
||||
apartado: z.number().min(0, 'El apartado no puede ser negativo').optional(),
|
||||
clienteNombre: z.string().min(1),
|
||||
clienteEmail: z.string().email().optional(),
|
||||
clienteTelefono: z.string().min(7).optional(),
|
||||
|
||||
@@ -48,7 +48,7 @@ import {
|
||||
cambiarEtapaOportunidadEndpoint,
|
||||
perderOportunidadEndpoint,
|
||||
} from '../controllers/oportunidades.controller.js'
|
||||
import { cambiarEstadoApartadoEndpoint, listarDocumentosApartado, subirDocumentosApartado, uploadDocumentosApartado } from '../controllers/apartados.controller.js'
|
||||
import { cambiarEstadoApartadoEndpoint, listarDocumentosApartado, subirDocumentosApartado, descargarDocumentoApartado, uploadDocumentosApartado } from '../controllers/apartados.controller.js'
|
||||
import { correrRecordatorios, listarRecordatorios } from '../controllers/recordatorios.controller.js'
|
||||
import {
|
||||
cambiarEstadoLeadEndpoint,
|
||||
@@ -214,6 +214,8 @@ apiRouter.post('/apartados/:itemId/estado', cambiarEstadoApartadoEndpoint)
|
||||
// el cliente ligado al apartado (se crea la ficha mínima si aún no existe)
|
||||
apiRouter.get('/apartados/:itemId/documentos', listarDocumentosApartado)
|
||||
apiRouter.post('/apartados/:itemId/documentos', uploadDocumentosApartado.array('archivos', 10), subirDocumentosApartado)
|
||||
// Archivo de un documento (previsualizar / descargar desde "Mis apartados")
|
||||
apiRouter.get('/apartados/:itemId/documentos/:docId/archivo', descargarDocumentoApartado)
|
||||
// Notas del apartado
|
||||
apiRouter.post('/apartados/:id/notas', crearNotaEndpoint)
|
||||
apiRouter.get('/apartados/:id/notas', listarNotasEndpoint)
|
||||
|
||||
Reference in New Issue
Block a user