feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol
This commit is contained in:
@@ -13,8 +13,17 @@ export const DEPARTAMENTOS_SOPORTE = [
|
||||
/** Prioridades de una solicitud */
|
||||
export const PRIORIDADES_SOPORTE = ['Baja', 'Media', 'Alta', 'Urgente'] as const
|
||||
|
||||
/** Estados por los que pasa una solicitud (los mueve admin/manager) */
|
||||
export const ESTADOS_SOPORTE = ['ABIERTA', 'EN_PROCESO', 'CERRADA'] as const
|
||||
|
||||
export type DepartamentoSoporte = (typeof DEPARTAMENTOS_SOPORTE)[number]
|
||||
export type PrioridadSoporte = (typeof PRIORIDADES_SOPORTE)[number]
|
||||
export type EstadoSoporte = (typeof ESTADOS_SOPORTE)[number]
|
||||
|
||||
/** Tipos y tamaños de adjunto aceptados (igual que el API) */
|
||||
export const MIMES_ADJUNTO_SOPORTE = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']
|
||||
export const MAX_BYTES_ADJUNTO_SOPORTE = 10 * 1024 * 1024
|
||||
export const MAX_ADJUNTOS_SOPORTE = 10
|
||||
|
||||
export interface SolicitudSoporte {
|
||||
id: string
|
||||
@@ -26,6 +35,7 @@ export interface SolicitudSoporte {
|
||||
fechaSolicitud: string
|
||||
fechaLimite: string | null
|
||||
notas: string | null
|
||||
/** Rutas relativas de los adjuntos dentro de uploads/ */
|
||||
archivos: string[]
|
||||
solicitante: string
|
||||
solicitanteEmail: string | null
|
||||
@@ -34,7 +44,44 @@ export interface SolicitudSoporte {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
/** POST /api/soporte — crear solicitud de soporte */
|
||||
export interface FiltrosSoporte {
|
||||
estado?: string
|
||||
departamento?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida un adjunto antes de subirlo: devuelve el motivo del rechazo o null si
|
||||
* es válido (el API valida lo mismo, aquí es para no esperar la subida).
|
||||
*/
|
||||
export function validarAdjuntoSoporte(archivo: File): string | null {
|
||||
if (!MIMES_ADJUNTO_SOPORTE.includes(archivo.type)) {
|
||||
return `${archivo.name}: solo se aceptan PDF, JPG o PNG.`
|
||||
}
|
||||
if (archivo.size > MAX_BYTES_ADJUNTO_SOPORTE) {
|
||||
return `${archivo.name}: excede 10 MB.`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Nombre legible de un adjunto: sin carpeta ni prefijo uuid */
|
||||
export function nombreArchivoSoporte(ruta: string): string {
|
||||
const base = ruta.split(/[\\/]/).pop() ?? ruta
|
||||
return base.replace(/^[0-9a-f]{8}-/i, '')
|
||||
}
|
||||
|
||||
/** Mensaje real del servidor: el error handler responde { error, issues? } */
|
||||
async function mensajeDeError(resp: Response): Promise<string> {
|
||||
const body = (await resp.json().catch(() => null)) as
|
||||
| { error?: string; issues?: Array<{ path?: Array<string | number>; message?: string }> }
|
||||
| null
|
||||
const detalles = (body?.issues ?? [])
|
||||
.map((i) => [i.path?.join('.'), i.message].filter(Boolean).join(': '))
|
||||
.filter((d) => d.length > 0)
|
||||
if (detalles.length > 0) return `${body?.error ?? 'Validación fallida'}: ${detalles.join('; ')}`
|
||||
return body?.error ?? `HTTP ${resp.status}`
|
||||
}
|
||||
|
||||
/** POST /api/soporte — crear solicitud de soporte (multipart si hay adjuntos) */
|
||||
export async function crearSolicitudSoporte(payload: {
|
||||
titulo: string
|
||||
descripcion: string
|
||||
@@ -46,28 +93,64 @@ export async function crearSolicitudSoporte(payload: {
|
||||
notas?: string
|
||||
solicitante?: string
|
||||
email?: string
|
||||
archivos?: string[]
|
||||
/** Adjuntos reales: viajan en el multipart, el API los guarda en disco */
|
||||
archivos?: File[]
|
||||
}): Promise<SolicitudSoporte> {
|
||||
const resp = await fetch(`${API_URL}/soporte`, conAuth({
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}))
|
||||
const form = new FormData()
|
||||
form.set('titulo', payload.titulo)
|
||||
form.set('descripcion', payload.descripcion)
|
||||
form.set('departamento', payload.departamento)
|
||||
form.set('prioridad', payload.prioridad)
|
||||
form.set('fechaSolicitud', payload.fechaSolicitud)
|
||||
if (payload.responsable) form.set('responsable', payload.responsable)
|
||||
if (payload.fechaLimite) form.set('fechaLimite', payload.fechaLimite)
|
||||
if (payload.notas) form.set('notas', payload.notas)
|
||||
if (payload.solicitante) form.set('solicitante', payload.solicitante)
|
||||
if (payload.email) form.set('email', payload.email)
|
||||
for (const archivo of payload.archivos ?? []) form.append('archivos', archivo)
|
||||
|
||||
// Sin Content-Type explícito: el navegador pone el boundary del multipart
|
||||
const resp = await fetch(`${API_URL}/soporte`, conAuth({ method: 'POST', body: form }))
|
||||
manejar401(resp)
|
||||
if (!resp.ok) {
|
||||
const body = (await resp.json().catch(() => null)) as { error?: string } | null
|
||||
throw new Error(body?.error ?? `HTTP ${resp.status}`)
|
||||
}
|
||||
if (!resp.ok) throw new Error(await mensajeDeError(resp))
|
||||
return (await resp.json()) as SolicitudSoporte
|
||||
}
|
||||
|
||||
/** GET /api/soporte — solicitudes propias (admin: todas) */
|
||||
export async function listarSolicitudesSoporte(): Promise<SolicitudSoporte[]> {
|
||||
const resp = await fetch(`${API_URL}/soporte`, conAuth())
|
||||
/** GET /api/soporte — solicitudes propias (admin/manager: todas) con filtros */
|
||||
export async function listarSolicitudesSoporte(filtros: FiltrosSoporte = {}): Promise<SolicitudSoporte[]> {
|
||||
const params = new URLSearchParams()
|
||||
if (filtros.estado) params.set('estado', filtros.estado)
|
||||
if (filtros.departamento) params.set('departamento', filtros.departamento)
|
||||
const qs = params.toString()
|
||||
|
||||
const resp = await fetch(`${API_URL}/soporte${qs ? `?${qs}` : ''}`, conAuth())
|
||||
manejar401(resp)
|
||||
if (!resp.ok) {
|
||||
const body = (await resp.json().catch(() => null)) as { error?: string } | null
|
||||
throw new Error(body?.error ?? `HTTP ${resp.status}`)
|
||||
}
|
||||
if (!resp.ok) throw new Error(await mensajeDeError(resp))
|
||||
return (await resp.json()) as SolicitudSoporte[]
|
||||
}
|
||||
|
||||
/** PATCH /api/soporte/:id — cambiar el estado (admin/manager) */
|
||||
export async function cambiarEstadoSolicitud(id: string, estado: EstadoSoporte): Promise<SolicitudSoporte> {
|
||||
const resp = await fetch(
|
||||
`${API_URL}/soporte/${encodeURIComponent(id)}`,
|
||||
conAuth({
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ estado }),
|
||||
}),
|
||||
)
|
||||
manejar401(resp)
|
||||
if (!resp.ok) throw new Error(await mensajeDeError(resp))
|
||||
return (await resp.json()) as SolicitudSoporte
|
||||
}
|
||||
|
||||
/** GET /api/soporte/:id/archivos/:indice — descarga un adjunto (blob) */
|
||||
export async function descargarArchivoSolicitud(id: string, indice: number): Promise<Blob> {
|
||||
const resp = await fetch(
|
||||
`${API_URL}/soporte/${encodeURIComponent(id)}/archivos/${indice}`,
|
||||
conAuth(),
|
||||
)
|
||||
manejar401(resp)
|
||||
if (!resp.ok) throw new Error(await mensajeDeError(resp))
|
||||
return await resp.blob()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user