- Borradores por vendedor (guardar/continuar/eliminar), se limpia al registrar - Pre-llenado con ?cotizacion=<id> (cliente, contacto, proyecto, montos); botón 'Iniciar alta' en Mis cotizaciones del cotizador - Liga remota pública /client/firma/<token> (JWT scope firma-remota, 72h, rate limit, solo datos mínimos sin PLD): firma digital + subida de documentos que prende checkboxes en Monday Apartado - Tokens de liga no sirven como sesión y viceversa - Tests: api 228, alta 82, cotizador 82
82 lines
3.2 KiB
TypeScript
82 lines
3.2 KiB
TypeScript
import type { Request, Response } from 'express'
|
|
import { z } from 'zod'
|
|
import { prisma } from '../services/prisma.js'
|
|
import { AppError, NotFoundError } from '../utils/errors.js'
|
|
|
|
// ---------- Borradores del alta de clientes ----------
|
|
|
|
const guardarBorradorSchema = z.object({
|
|
id: z.string().min(1).optional(),
|
|
nombre: z.string().max(200).optional(),
|
|
paso: z.number().int().min(0).max(50).default(0),
|
|
// Estado del wizard serializado (objeto JSON cualquiera; sin Files)
|
|
datos: z.unknown(),
|
|
})
|
|
|
|
/** sub del JWT; con AUTH_ENABLED=false (tests) cae a un id fijo */
|
|
const usuarioDe = (req: Request): string => req.usuarioToken?.sub ?? 'anon'
|
|
const esAdmin = (req: Request): boolean => req.usuarioToken?.rol === 'admin'
|
|
|
|
/** 404 si no existe, 403 si es de otro usuario (salvo admin) */
|
|
async function borradorPropio(req: Request, id: string) {
|
|
const borrador = await prisma.borradorAlta.findUnique({ where: { id } })
|
|
if (!borrador) throw new NotFoundError('Borrador no encontrado')
|
|
if (borrador.usuarioId !== usuarioDe(req) && !esAdmin(req)) {
|
|
throw new AppError(403, 'El borrador pertenece a otro usuario')
|
|
}
|
|
return borrador
|
|
}
|
|
|
|
// Etiqueta visible: nombre del cliente dentro del estado, o "Sin nombre"
|
|
function etiquetaDe(datos: unknown, nombreExplicito: string | undefined): string {
|
|
if (nombreExplicito?.trim()) return nombreExplicito.trim()
|
|
const dg = (datos as { datosGenerales?: { nombreCompleto?: unknown } } | null)?.datosGenerales
|
|
const nombre = typeof dg?.nombreCompleto === 'string' ? dg.nombreCompleto.trim() : ''
|
|
return nombre || 'Sin nombre'
|
|
}
|
|
|
|
/** GET /api/borradores — lista los míos, SIN datosJson (metadatos) */
|
|
export async function listarBorradores(req: Request, res: Response): Promise<void> {
|
|
const borradores = await prisma.borradorAlta.findMany({
|
|
where: { usuarioId: usuarioDe(req) },
|
|
select: { id: true, nombre: true, paso: true, createdAt: true, updatedAt: true },
|
|
orderBy: { updatedAt: 'desc' },
|
|
})
|
|
res.json(borradores)
|
|
}
|
|
|
|
/** POST /api/borradores — upsert: con `id` actualiza (si es mío), sin `id` crea */
|
|
export async function guardarBorrador(req: Request, res: Response): Promise<void> {
|
|
const body = guardarBorradorSchema.parse(req.body)
|
|
const datosJson = JSON.stringify(body.datos ?? null)
|
|
const nombre = etiquetaDe(body.datos, body.nombre)
|
|
|
|
if (body.id) {
|
|
await borradorPropio(req, body.id)
|
|
const actualizado = await prisma.borradorAlta.update({
|
|
where: { id: body.id },
|
|
data: { nombre, datosJson, paso: body.paso },
|
|
})
|
|
res.json(actualizado)
|
|
return
|
|
}
|
|
|
|
const creado = await prisma.borradorAlta.create({
|
|
data: { usuarioId: usuarioDe(req), nombre, datosJson, paso: body.paso },
|
|
})
|
|
res.status(201).json(creado)
|
|
}
|
|
|
|
/** GET /api/borradores/:id — borrador completo (solo el dueño o admin) */
|
|
export async function obtenerBorrador(req: Request, res: Response): Promise<void> {
|
|
const borrador = await borradorPropio(req, String(req.params['id']))
|
|
res.json(borrador)
|
|
}
|
|
|
|
/** DELETE /api/borradores/:id — solo el dueño o admin */
|
|
export async function eliminarBorrador(req: Request, res: Response): Promise<void> {
|
|
const borrador = await borradorPropio(req, String(req.params['id']))
|
|
await prisma.borradorAlta.delete({ where: { id: borrador.id } })
|
|
res.status(204).end()
|
|
}
|