/** * api.ts * * Cliente HTTP tipado para a API Helpdesk. * Centraliza toda comunicação com o backend — nenhum componente * deve chamar fetch() diretamente. */ // ─── Configuração ───────────────────────────────────────────────────────────── const BASE_URL = import.meta.env['VITE_API_URL'] ?? '/api/v1'; const TOKEN_KEY = 'helpdesk:token'; function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } // ─── Tipos (espelham o schema do backend) ───────────────────────────────────── export type TicketStatus = 'open' | 'in_progress' | 'waiting' | 'resolved' | 'closed'; export type TicketPriority = 'low' | 'medium' | 'high' | 'critical'; export interface Category { id: number; name: string; slug: string; description: string | null; createdAt: string; updatedAt: string; } export interface Ticket { id: number; title: string; description: string; status: TicketStatus; priority: TicketPriority; creatorId: number; assigneeId: number | null; categoryId: number | null; dueAt: string | null; resolvedAt: string | null; createdAt: string; updatedAt: string; creatorName?: string; assigneeName?: string | null; categoryName?: string | null; } export interface Comment { id: number; content: string; isInternal: boolean; ticketId: number; authorId: number; createdAt: string; updatedAt: string; } export interface PaginatedResponse { data: T[]; meta: { page: number; limit: number; total: number; totalPages: number; }; } export interface SystemSettings { allowRequesterChat: boolean; } // ─── Filtros para listagem ──────────────────────────────────────────────────── export interface ListTicketsParams { status?: TicketStatus; priority?: TicketPriority; assigneeId?: number; creatorId?: number; categoryId?: number; page?: number; limit?: number; } // ─── Erro padronizado da API ────────────────────────────────────────────────── export class ApiError extends Error { constructor( public readonly status: number, message: string, ) { super(message); this.name = 'ApiError'; } } // ─── Função base de fetch ───────────────────────────────────────────────────── async function request( path: string, options?: RequestInit, ): Promise { const token = getToken(); const headers: Record = { ...options?.headers as Record }; if (options?.body && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } if (token && !headers['Authorization']) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`${BASE_URL}${path}`, { cache: 'no-store', ...options, headers, }); if (!response.ok) { const body = await response.json().catch(() => ({ message: response.statusText })); throw new ApiError( response.status, (body as { message?: string }).message ?? 'Erro desconhecido', ); } if (response.status === 204) return null as T; return response.json() as Promise; } // ─── Tipos adicionais para Auth ─────────────────────────────────────────────── export interface UserResponse { id: number; name: string; email: string; role: 'requester' | 'agent' | 'admin'; isActive: boolean; createdAt: string; } export interface AuthResponse { token: string; user: UserResponse; } export interface StatsResponse { totalOpen: number; totalInProgress: number; totalWaiting: number; totalResolved: number; totalClosed: number; totalPending: number; } // ─── Endpoints: Tickets ─────────────────────────────────────────────────────── export const api = { settings: { get(): Promise { return request('/settings'); }, update(data: { allowRequesterChat: boolean }): Promise { return request('/settings', { method: 'PATCH', body: JSON.stringify(data) }); } }, stats: { get(): Promise { return request('/stats'); }, }, auth: { login(data: { email: string; password: string }): Promise { return request('/auth/login', { method: 'POST', body: JSON.stringify(data) }); }, register(data: { name: string; email: string; password: string; role?: string }): Promise { return request('/auth/register', { method: 'POST', body: JSON.stringify(data) }); }, me(): Promise { return request('/auth/me'); }, }, categories: { list(): Promise { return request('/categories'); }, get(id: number): Promise { return request(`/categories/${id}`); }, create(data: { name: string; slug?: string; description?: string }): Promise { return request('/categories', { method: 'POST', body: JSON.stringify(data) }); }, update(id: number, data: { name?: string; slug?: string; description?: string }): Promise { return request(`/categories/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); }, delete(id: number): Promise { return request(`/categories/${id}`, { method: 'DELETE' }); } }, tickets: { list(params: ListTicketsParams = {}): Promise> { const query = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined) query.set(key, String(value)); }); const qs = query.toString(); return request(`/tickets${qs ? `?${qs}` : ''}`); }, get(id: number): Promise { return request(`/tickets/${id}`); }, create(data: { title: string; description: string; priority?: TicketPriority; categoryId?: number; creatorId: number; }): Promise { return request('/tickets', { method: 'POST', body: JSON.stringify(data), }); }, updateStatus(ticketId: number, status: TicketStatus, reason?: string): Promise { return request(`/tickets/${ticketId}/status`, { method: 'PATCH', body: JSON.stringify({ status, reason }), }); }, assign(ticketId: number, assigneeId: number): Promise { return request(`/tickets/${ticketId}/assign`, { method: 'PATCH', body: JSON.stringify({ assigneeId }), }); }, updatePriority(ticketId: number, priority: TicketPriority): Promise { return request(`/tickets/${ticketId}/priority`, { method: 'PATCH', body: JSON.stringify({ priority }), }); }, delete(id: number): Promise { return request(`/tickets/${id}`, { method: 'DELETE' }); }, }, comments: { list(ticketId: number): Promise { return request(`/tickets/${ticketId}/comments`); }, add(ticketId: number, data: { content: string; isInternal: boolean; authorId: number; }): Promise { return request(`/tickets/${ticketId}/comments`, { method: 'POST', body: JSON.stringify(data), }); }, }, users: { list(): Promise { return request('/users'); }, create(data: { name: string; email: string; password: string; role: 'admin' | 'agent' | 'requester' }): Promise { return request('/users', { method: 'POST', body: JSON.stringify(data), }); }, update(id: number, data: { name?: string; email?: string; password?: string; role?: 'admin' | 'agent' | 'requester'; isActive?: boolean }): Promise { return request(`/users/${id}`, { method: 'PATCH', body: JSON.stringify(data), }); }, delete(id: number): Promise { return request(`/users/${id}`, { method: 'DELETE' }); } }, };