diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-07-25 21:23:29 +0000 |
| commit | ef55d9bb24430610b60c1b77821307cd72c08c55 (patch) | |
| tree | e6935a810b34370b0a4c821e55f6397aac50c832 /web/src/lib | |
| download | helpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.tar.gz helpdesk-ef55d9bb24430610b60c1b77821307cd72c08c55.zip | |
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'web/src/lib')
| -rw-r--r-- | web/src/lib/api.ts | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..1d1e63d --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,297 @@ +/** + * 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<T> { + 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<T>( + path: string, + options?: RequestInit, +): Promise<T> { + const token = getToken(); + const headers: Record<string, string> = { ...options?.headers as Record<string, string> }; + + 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<T>; +} + +// ─── 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<SystemSettings> { + return request('/settings'); + }, + update(data: { allowRequesterChat: boolean }): Promise<SystemSettings> { + return request('/settings', { method: 'PATCH', body: JSON.stringify(data) }); + } + }, + + stats: { + get(): Promise<StatsResponse> { + return request('/stats'); + }, + }, + + auth: { + login(data: { email: string; password: string }): Promise<AuthResponse> { + return request('/auth/login', { method: 'POST', body: JSON.stringify(data) }); + }, + register(data: { name: string; email: string; password: string; role?: string }): Promise<AuthResponse> { + return request('/auth/register', { method: 'POST', body: JSON.stringify(data) }); + }, + me(): Promise<UserResponse> { + return request('/auth/me'); + }, + }, + + categories: { + list(): Promise<Category[]> { + return request('/categories'); + }, + get(id: number): Promise<Category> { + return request(`/categories/${id}`); + }, + create(data: { name: string; slug?: string; description?: string }): Promise<Category> { + return request('/categories', { method: 'POST', body: JSON.stringify(data) }); + }, + update(id: number, data: { name?: string; slug?: string; description?: string }): Promise<Category> { + return request(`/categories/${id}`, { method: 'PATCH', body: JSON.stringify(data) }); + }, + delete(id: number): Promise<void> { + return request(`/categories/${id}`, { method: 'DELETE' }); + } + }, + + tickets: { + list(params: ListTicketsParams = {}): Promise<PaginatedResponse<Ticket>> { + 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<Ticket> { + return request(`/tickets/${id}`); + }, + + create(data: { + title: string; + description: string; + priority?: TicketPriority; + categoryId?: number; + creatorId: number; + }): Promise<Ticket> { + return request('/tickets', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + updateStatus(ticketId: number, status: TicketStatus, reason?: string): Promise<Ticket> { + return request(`/tickets/${ticketId}/status`, { + method: 'PATCH', + body: JSON.stringify({ status, reason }), + }); + }, + + assign(ticketId: number, assigneeId: number): Promise<Ticket> { + return request(`/tickets/${ticketId}/assign`, { + method: 'PATCH', + body: JSON.stringify({ assigneeId }), + }); + }, + + updatePriority(ticketId: number, priority: TicketPriority): Promise<Ticket> { + return request(`/tickets/${ticketId}/priority`, { + method: 'PATCH', + body: JSON.stringify({ priority }), + }); + }, + + delete(id: number): Promise<void> { + return request(`/tickets/${id}`, { method: 'DELETE' }); + }, + }, + + comments: { + list(ticketId: number): Promise<Comment[]> { + return request(`/tickets/${ticketId}/comments`); + }, + + add(ticketId: number, data: { + content: string; + isInternal: boolean; + authorId: number; + }): Promise<Comment> { + return request(`/tickets/${ticketId}/comments`, { + method: 'POST', + body: JSON.stringify(data), + }); + }, + }, + + users: { + list(): Promise<UserResponse[]> { + return request('/users'); + }, + create(data: { name: string; email: string; password: string; role: 'admin' | 'agent' | 'requester' }): Promise<UserResponse> { + 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<UserResponse> { + return request(`/users/${id}`, { + method: 'PATCH', + body: JSON.stringify(data), + }); + }, + delete(id: number): Promise<void> { + return request(`/users/${id}`, { method: 'DELETE' }); + } + }, +}; |