feat(alta): borrador, pre-llenado desde cotización y liga remota de firma
- 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
This commit is contained in:
@@ -97,3 +97,143 @@ export async function subirDocumentos(clienteId: string, payload: AltaCliente):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Borradores del alta (auth) ----------
|
||||
|
||||
export interface BorradorResumen {
|
||||
id: string
|
||||
nombre: string
|
||||
paso: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface Borrador extends BorradorResumen {
|
||||
datosJson: string
|
||||
}
|
||||
|
||||
/** GET /borradores — los míos, sin datosJson */
|
||||
export async function listarBorradores(): Promise<BorradorResumen[]> {
|
||||
const res = await fetch(`${API_URL}/borradores`, conAuth())
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as BorradorResumen[]
|
||||
}
|
||||
|
||||
/** POST /borradores — upsert: con `id` actualiza, sin `id` crea */
|
||||
export async function guardarBorrador(payload: {
|
||||
id?: string
|
||||
paso: number
|
||||
datos: unknown
|
||||
}): Promise<Borrador> {
|
||||
const res = await fetch(
|
||||
`${API_URL}/borradores`,
|
||||
conAuth({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
)
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as Borrador
|
||||
}
|
||||
|
||||
/** GET /borradores/:id — borrador completo */
|
||||
export async function obtenerBorrador(id: string): Promise<Borrador> {
|
||||
const res = await fetch(`${API_URL}/borradores/${id}`, conAuth())
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as Borrador
|
||||
}
|
||||
|
||||
/** DELETE /borradores/:id */
|
||||
export async function eliminarBorrador(id: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/borradores/${id}`, conAuth({ method: 'DELETE' }))
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
// ---------- Pre-llenado desde cotización (auth) ----------
|
||||
|
||||
/** GET /cotizaciones/:id — datos para prellenar el alta (?cotizacion=<id>) */
|
||||
export async function obtenerCotizacion(id: string): Promise<import('../lib/cotizacionAlta').CotizacionParaAlta> {
|
||||
const res = await fetch(`${API_URL}/cotizaciones/${id}`, conAuth())
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as import('../lib/cotizacionAlta').CotizacionParaAlta
|
||||
}
|
||||
|
||||
// ---------- Liga remota de firma (auth) ----------
|
||||
|
||||
export interface LigaRemota {
|
||||
token: string
|
||||
url: string // ruta pública relativa: /client/firma/<token>
|
||||
expiraEn: string
|
||||
}
|
||||
|
||||
/** POST /clientes/:id/liga — genera la liga pública de firma/documentos */
|
||||
export async function generarLigaRemota(clienteId: string): Promise<LigaRemota> {
|
||||
const res = await fetch(`${API_URL}/clientes/${clienteId}/liga`, conAuth({ method: 'POST' }))
|
||||
manejar401(res)
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return (await res.json()) as LigaRemota
|
||||
}
|
||||
|
||||
// ---------- Endpoints públicos de la liga (SIN auth; el token va en la URL) ----------
|
||||
|
||||
export interface DocumentoPublico {
|
||||
tipo: string
|
||||
nombreArchivo: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ClientePublico {
|
||||
nombre: string
|
||||
proyecto: string
|
||||
unidad: string | null
|
||||
firma: boolean
|
||||
documentos: DocumentoPublico[]
|
||||
}
|
||||
|
||||
/** Error de liga inválida/expirada con mensaje para el cliente final */
|
||||
export class ErrorLiga extends Error {
|
||||
constructor(message = 'La liga es inválida o ya expiró. Pide una nueva a tu asesor.') {
|
||||
super(message)
|
||||
this.name = 'ErrorLiga'
|
||||
}
|
||||
}
|
||||
|
||||
const manejarErrorLiga = (res: Response): void => {
|
||||
if (res.status === 401 || res.status === 404) throw new ErrorLiga()
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
/** GET /clientes/publico/:token — solo nombre, proyecto, unidad y estado */
|
||||
export async function obtenerClientePublico(token: string): Promise<ClientePublico> {
|
||||
const res = await fetch(`${API_URL}/clientes/publico/${token}`)
|
||||
manejarErrorLiga(res)
|
||||
return (await res.json()) as ClientePublico
|
||||
}
|
||||
|
||||
/** POST /clientes/publico/:token/firma — guarda la firma (PNG data URL) */
|
||||
export async function enviarFirmaRemota(token: string, firmaDataUrl: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/clientes/publico/${token}/firma`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ firmaDataUrl }),
|
||||
})
|
||||
manejarErrorLiga(res)
|
||||
}
|
||||
|
||||
/** POST /clientes/publico/:token/documentos — adjunta UN documento por slot */
|
||||
export async function subirDocumentoRemoto(token: string, tipo: string, archivo: File): Promise<void> {
|
||||
const form = new FormData()
|
||||
form.append('tipo', tipo)
|
||||
form.append('archivos', archivo, archivo.name)
|
||||
const res = await fetch(`${API_URL}/clientes/publico/${token}/documentos`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
manejarErrorLiga(res)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user