157 lines
5.7 KiB
TypeScript
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()
|
|
}
|