Files
FRASA-CAS/apps/landing/src/services/soporte.ts

157 lines
5.7 KiB
TypeScript

import { API_URL, conAuth, manejar401 } from './auth'
/** Departamentos a los que se puede dirigir una solicitud de soporte */
export const DEPARTAMENTOS_SOPORTE = [
'Sistemas',
'Administración',
'Ventas',
'Cobranza',
'Escrituración',
'Dirección',
] as const
/** 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
titulo: string
descripcion: string
departamento: string
prioridad: string
responsable: string | null
fechaSolicitud: string
fechaLimite: string | null
notas: string | null
/** Rutas relativas de los adjuntos dentro de uploads/ */
archivos: string[]
solicitante: string
solicitanteEmail: string | null
estado: string
createdAt: string
updatedAt: string
}
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
departamento: DepartamentoSoporte
prioridad: PrioridadSoporte
responsable?: string
fechaSolicitud: string
fechaLimite?: string
notas?: string
solicitante?: string
email?: string
/** Adjuntos reales: viajan en el multipart, el API los guarda en disco */
archivos?: File[]
}): Promise<SolicitudSoporte> {
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) throw new Error(await mensajeDeError(resp))
return (await resp.json()) as SolicitudSoporte
}
/** 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) 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()
}