diff --git a/src/services/attendancesService.ts b/src/services/attendancesService.ts new file mode 100644 index 0000000..d64c2ba --- /dev/null +++ b/src/services/attendancesService.ts @@ -0,0 +1,48 @@ +import { Attendance, DashboardFilter } from '../types'; +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getAttendances = async (tenantId: string, filter: DashboardFilter): Promise => { + try { + const params = new URLSearchParams(); + params.append('tenantId', tenantId); + + if (filter.dateRange.start) params.append('startDate', filter.dateRange.start.toISOString()); + if (filter.dateRange.end) params.append('endDate', filter.dateRange.end.toISOString()); + + if (filter.userId && filter.userId !== 'all') params.append('userId', filter.userId); + if (filter.teamId && filter.teamId !== 'all') params.append('teamId', filter.teamId); + if (filter.funnelStage && filter.funnelStage !== 'all') params.append('funnelStage', filter.funnelStage); + if (filter.origin && filter.origin !== 'all') params.append('origin', filter.origin); + + const response = await apiFetch(`${API_URL}/attendances?${params.toString()}`, { + headers: getHeaders() + }); + + if (!response.ok) { + throw new Error('Falha ao buscar atendimentos do servidor'); + } + + return await response.json(); + } catch (error) { + console.error("API Error (getAttendances):", error); + return []; + } +}; + +export const getAttendanceById = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/attendances/${id}`, { + headers: getHeaders() + }); + if (!response.ok) return undefined; + + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return await response.json(); + } + return undefined; + } catch (error) { + console.error("API Error (getAttendanceById):", error); + return undefined; + } +}; diff --git a/src/services/authService.ts b/src/services/authService.ts new file mode 100644 index 0000000..8063f8e --- /dev/null +++ b/src/services/authService.ts @@ -0,0 +1,180 @@ +import { API_URL, apiFetch, getHeaders, setSessionExpiredHandler } from './apiClient'; + +export let isReloadingForImpersonation = false; + +export const logout = () => { + if (isReloadingForImpersonation) return; + + const refreshToken = localStorage.getItem('ctms_refresh_token'); + + localStorage.removeItem('ctms_token'); + localStorage.removeItem('ctms_refresh_token'); + localStorage.removeItem('ctms_user_id'); + localStorage.removeItem('ctms_tenant_id'); + + if (refreshToken) { + fetch(`${API_URL}/auth/logout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }) + }).catch(e => console.error("Failed to revoke refresh token", e)); + } +}; + +setSessionExpiredHandler(logout); + +export const login = async (credentials: any): Promise => { + const response = await fetch(`${API_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(credentials) + }); + + const contentType = response.headers.get("content-type"); + const isJson = contentType && contentType.indexOf("application/json") !== -1; + + if (!response.ok) { + const error = isJson ? await response.json() : { error: 'Erro no servidor' }; + throw new Error(error.error || 'Erro no login'); + } + + const data = isJson ? await response.json() : null; + if (data && data.token) { + localStorage.setItem('ctms_token', data.token); + if (data.refreshToken) localStorage.setItem('ctms_refresh_token', data.refreshToken); + localStorage.setItem('ctms_user_id', data.user.id); + localStorage.setItem('ctms_tenant_id', data.user.tenant_id || ''); + } + return data; +}; + +export const impersonateTenant = async (tenantId: string): Promise => { + const response = await apiFetch(`${API_URL}/impersonate/${tenantId}`, { + method: 'POST', + headers: getHeaders() + }); + + const contentType = response.headers.get("content-type"); + const isJson = contentType && contentType.indexOf("application/json") !== -1; + + if (!response.ok) { + const errorData = isJson ? await response.json() : { error: 'Erro no servidor' }; + throw new Error(errorData.error || 'Erro ao assumir identidade'); + } + + isReloadingForImpersonation = true; + + const data = await response.json(); + const oldToken = localStorage.getItem('ctms_token'); + if (oldToken) { + localStorage.setItem('ctms_super_admin_token', oldToken); + } + + localStorage.setItem('ctms_token', data.token); + localStorage.setItem('ctms_user_id', data.user.id); + localStorage.setItem('ctms_tenant_id', data.user.tenant_id || ''); + + window.location.hash = '#/'; + window.location.reload(); + + return data; +}; + +export const returnToSuperAdmin = (): boolean => { + const superAdminToken = localStorage.getItem('ctms_super_admin_token'); + if (superAdminToken) { + try { + isReloadingForImpersonation = true; + + const base64Url = superAdminToken.split('.')[1]; + let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const pad = base64.length % 4; + if (pad) { + base64 += '='.repeat(4 - pad); + } + + const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { + return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); + }).join('')); + const payload = JSON.parse(jsonPayload); + + localStorage.setItem('ctms_token', superAdminToken); + localStorage.setItem('ctms_user_id', payload.id); + localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system'); + localStorage.removeItem('ctms_super_admin_token'); + + window.location.hash = '#/super-admin'; + window.location.reload(); + return true; + } catch (e) { + isReloadingForImpersonation = false; + console.error("Failed to restore super admin token", e); + return false; + } + } + return false; +}; + +export const register = async (userData: any): Promise => { + const response = await apiFetch(`${API_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(userData) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro no registro'); + } + return true; +}; + +export const verifyCode = async (data: any): Promise => { + const response = await apiFetch(`${API_URL}/auth/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Código inválido ou expirado'); + } + return true; +}; + +export const forgotPassword = async (email: string): Promise => { + const response = await apiFetch(`${API_URL}/auth/forgot-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }) + }); + + const contentType = response.headers.get("content-type"); + const isJson = contentType && contentType.indexOf("application/json") !== -1; + + if (!response.ok) { + const error = isJson ? await response.json() : { error: 'Erro no servidor' }; + throw new Error(error.error || 'Erro ao processar solicitação'); + } + + const data = isJson ? await response.json() : { message: 'Solicitação processada' }; + return data.message; +}; + +export const resetPassword = async (password: string, token: string, name?: string): Promise => { + const response = await apiFetch(`${API_URL}/auth/reset-password`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password, token, name }) + }); + + const contentType = response.headers.get("content-type"); + const isJson = contentType && contentType.indexOf("application/json") !== -1; + + if (!response.ok) { + const error = isJson ? await response.json() : { error: 'Erro no servidor' }; + throw new Error(error.error || 'Erro ao resetar senha'); + } + + const data = isJson ? await response.json() : { message: 'Senha redefinida' }; + return data.message; +}; diff --git a/src/services/dataService.ts b/src/services/dataService.ts index 82e5d47..bd56e27 100644 --- a/src/services/dataService.ts +++ b/src/services/dataService.ts @@ -1,771 +1,10 @@ - -import { Attendance, DashboardFilter, User } from '../types'; -import { API_URL, apiFetch, getHeaders, setSessionExpiredHandler } from './apiClient'; - -export const getNotifications = async (): Promise => { - try { - const response = await apiFetch(`${API_URL}/notifications`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Failed to fetch notifications'); - return await response.json(); - } catch (error) { - console.error("API Error (getNotifications):", error); - return []; - } -}; - -export const markNotificationAsRead = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/notifications/${id}`, { - method: 'PUT', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (markNotificationAsRead):", error); - return false; - } -}; - -export const markAllNotificationsAsRead = async (): Promise => { - try { - const response = await apiFetch(`${API_URL}/notifications/read-all`, { - method: 'PUT', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (markAllNotificationsAsRead):", error); - return false; - } -}; - -export const deleteNotification = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/notifications/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteNotification):", error); - return false; - } -}; - -export const clearAllNotifications = async (): Promise => { - try { - const response = await apiFetch(`${API_URL}/notifications/clear-all`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (clearAllNotifications):", error); - return false; - } -}; - -// --- Funnels Functions --- -export const getFunnels = async (tenantId: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/funnels?tenantId=${tenantId}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar funis'); - return await response.json(); - } catch (error) { - console.error("API Error (getFunnels):", error); - return []; - } -}; - -export const createFunnel = async (data: { name: string, tenantId: string }): Promise => { - const response = await apiFetch(`${API_URL}/funnels`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro ao criar funil'); - } - return await response.json(); -}; - -export const updateFunnel = async (id: string, data: { name?: string, teamIds?: string[] }): Promise => { - try { - const response = await apiFetch(`${API_URL}/funnels/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateFunnel):", error); - return false; - } -}; - -export const deleteFunnel = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/funnels/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteFunnel):", error); - return false; - } -}; - -export const createFunnelStage = async (funnelId: string, data: any): Promise => { - const response = await apiFetch(`${API_URL}/funnels/${funnelId}/stages`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro ao criar etapa'); - } - return await response.json(); -}; - -export const updateFunnelStage = async (id: string, data: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateFunnelStage):", error); - return false; - } -}; - -export const deleteFunnelStage = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteFunnelStage):", error); - return false; - } -}; - -// --- Origins Functions --- -export const getOrigins = async (tenantId: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/origins?tenantId=${tenantId}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar origens'); - return await response.json(); - } catch (error) { - console.error("API Error (getOrigins):", error); - return []; - } -}; - -export const createOriginGroup = async (data: { name: string, tenantId: string }): Promise => { - const response = await apiFetch(`${API_URL}/origins`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro ao criar grupo de origens'); - } - return await response.json(); -}; - -export const updateOriginGroup = async (id: string, data: { name?: string, teamIds?: string[] }): Promise => { - try { - const response = await apiFetch(`${API_URL}/origins/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateOriginGroup):", error); - return false; - } -}; - -export const deleteOriginGroup = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/origins/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteOriginGroup):", error); - return false; - } -}; - -export const createOriginItem = async (groupId: string, data: { name: string, color_class?: string }): Promise => { - const response = await apiFetch(`${API_URL}/origins/${groupId}/items`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro ao criar item de origem'); - } - return await response.json(); -}; - -export const updateOriginItem = async (id: string, data: { name: string, color_class?: string }): Promise => { - try { - const response = await apiFetch(`${API_URL}/origin_items/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(data) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateOriginItem):", error); - return false; - } -}; - -export const deleteOriginItem = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/origin_items/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteOriginItem):", error); - return false; - } -}; - -// --- API Keys Functions --- -export const getApiKeys = async (tenantId: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/api-keys?tenantId=${tenantId}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar chaves'); - return await response.json(); - } catch (error) { - console.error("API Error (getApiKeys):", error); - return []; - } -}; - -export const createApiKey = async (data: { name: string, tenantId: string }): Promise => { - const response = await apiFetch(`${API_URL}/api-keys`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro ao criar chave de API'); - } - return await response.json(); -}; - -export const deleteApiKey = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/api-keys/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteApiKey):", error); - return false; - } -}; - -export const searchGlobal = async (query: string): Promise<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }> => { - try { - const response = await apiFetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Search failed'); - return await response.json(); - } catch (error) { - console.error("API Error (searchGlobal):", error); - return { members: [], teams: [], attendances: [], organizations: [] }; - } -}; - -export const getAttendances = async (tenantId: string, filter: DashboardFilter): Promise => { - try { - const params = new URLSearchParams(); - params.append('tenantId', tenantId); - - if (filter.dateRange.start) params.append('startDate', filter.dateRange.start.toISOString()); - if (filter.dateRange.end) params.append('endDate', filter.dateRange.end.toISOString()); - - if (filter.userId && filter.userId !== 'all') params.append('userId', filter.userId); - if (filter.teamId && filter.teamId !== 'all') params.append('teamId', filter.teamId); - if (filter.funnelStage && filter.funnelStage !== 'all') params.append('funnelStage', filter.funnelStage); - if (filter.origin && filter.origin !== 'all') params.append('origin', filter.origin); - - const response = await apiFetch(`${API_URL}/attendances?${params.toString()}`, { - headers: getHeaders() - }); - - if (!response.ok) { - throw new Error('Falha ao buscar atendimentos do servidor'); - } - - return await response.json(); - } catch (error) { - console.error("API Error (getAttendances):", error); - // Fallback vazio ou lançar erro para a UI tratar - return []; - } -}; - -export const getUsers = async (tenantId: string): Promise => { - try { - const params = new URLSearchParams(); - if (tenantId !== 'all') params.append('tenantId', tenantId); - - const response = await apiFetch(`${API_URL}/users?${params.toString()}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar usuários'); - - return await response.json(); - } catch (error) { - console.error("API Error (getUsers):", error); - return []; - } -}; - -export const getUserById = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/users/${id}`, { - headers: getHeaders() - }); - if (!response.ok) { - if (response.status === 401 || response.status === 403 || response.status === 404) { - return undefined; // Invalid user or token - } - throw new Error(`Server error: ${response.status}`); - } - - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return await response.json(); - } - } catch (error) { - console.error("API Error (getUserById):", error); - throw error; // Rethrow so AuthGuard catches it and doesn't wipe tokens - } -}; - -export const updateUser = async (id: string, userData: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/users/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(userData) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(errorData?.error || 'Erro ao atualizar usuário no servidor'); - } - - return true; - } catch (error) { - console.error("API Error (updateUser):", error); - throw error; - } -}; - -export const uploadAvatar = async (id: string, file: File): Promise => { - try { - const formData = new FormData(); - formData.append('avatar', file); - - const token = localStorage.getItem('ctms_token'); - const response = await apiFetch(`${API_URL}/users/${id}/avatar`, { - method: 'POST', - headers: { - ...(token ? { 'Authorization': `Bearer ${token}` } : {}) - }, - body: formData - }); - - if (!response.ok) throw new Error('Falha no upload'); - const data = await response.json(); - return data.avatarUrl; - } catch (error) { - console.error("API Error (uploadAvatar):", error); - return null; - } -}; - -export const createMember = async (userData: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/users`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(userData) - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error(errorData?.error || 'Erro ao criar membro no servidor'); - } - - return true; - } catch (error) { - console.error("API Error (createMember):", error); - throw error; - } -}; - -export const deleteUser = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/users/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteUser):", error); - return false; - } -}; - -export const getAttendanceById = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/attendances/${id}`, { - headers: getHeaders() - }); - if (!response.ok) return undefined; - - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return await response.json(); - } - return undefined; - } catch (error) { - console.error("API Error (getAttendanceById):", error); - return undefined; - } -}; - -export const getTenants = async (): Promise => { - try { - const response = await apiFetch(`${API_URL}/tenants`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar tenants'); - - const contentType = response.headers.get("content-type"); - if (contentType && contentType.indexOf("application/json") !== -1) { - return await response.json(); - } - return []; - } catch (error) { - console.error("API Error (getTenants):", error); - return []; - } -}; - -export const getTeams = async (tenantId: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/teams?tenantId=${tenantId}`, { - headers: getHeaders() - }); - if (!response.ok) throw new Error('Falha ao buscar equipes'); - return await response.json(); - } catch (error) { - console.error("API Error (getTeams):", error); - return []; - } -}; - -export const createTeam = async (teamData: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/teams`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(teamData) - }); - return response.ok; - } catch (error) { - console.error("API Error (createTeam):", error); - return false; - } -}; - -export const updateTeam = async (id: string, teamData: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/teams/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(teamData) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateTeam):", error); - return false; - } -}; - -export const deleteTeam = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/teams/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteTeam):", error); - return false; - } -}; - -export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => { - try { - const response = await apiFetch(`${API_URL}/tenants`, { - method: 'POST', - headers: getHeaders(), - body: JSON.stringify(tenantData) - }); - const data = await response.json().catch(() => null); - if (!response.ok) throw new Error(data?.error || 'Erro ao criar organização'); - return { success: true, message: data?.message || 'Organização criada!' }; - } catch (error: any) { - console.error("API Error (createTenant):", error); - return { success: false, message: error.message }; - } -}; - -export const updateTenant = async (id: string, tenantData: any): Promise => { - try { - const response = await apiFetch(`${API_URL}/tenants/${id}`, { - method: 'PUT', - headers: getHeaders(), - body: JSON.stringify(tenantData) - }); - return response.ok; - } catch (error) { - console.error("API Error (updateTenant):", error); - return false; - } -}; - -export const deleteTenant = async (id: string): Promise => { - try { - const response = await apiFetch(`${API_URL}/tenants/${id}`, { - method: 'DELETE', - headers: getHeaders() - }); - return response.ok; - } catch (error) { - console.error("API Error (deleteTenant):", error); - return false; - } -}; - -// --- Auth Functions --- - -// Flag to prevent background fetches from throwing 401 and logging out during impersonation handoffs -export let isReloadingForImpersonation = false; - -export const logout = () => { - if (isReloadingForImpersonation) return; // Prevent logout if we are just switching tokens - - const refreshToken = localStorage.getItem('ctms_refresh_token'); - - // Clear local storage synchronously for instant UI update - localStorage.removeItem('ctms_token'); - localStorage.removeItem('ctms_refresh_token'); - localStorage.removeItem('ctms_user_id'); - localStorage.removeItem('ctms_tenant_id'); - - // Attempt to revoke in background - if (refreshToken) { - fetch(`${API_URL}/auth/logout`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken }) - }).catch(e => console.error("Failed to revoke refresh token", e)); - } -}; - -setSessionExpiredHandler(logout); - -export const login = async (credentials: any): Promise => { - const response = await fetch(`${API_URL}/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(credentials) - }); - - const contentType = response.headers.get("content-type"); - const isJson = contentType && contentType.indexOf("application/json") !== -1; - - if (!response.ok) { - const error = isJson ? await response.json() : { error: 'Erro no servidor' }; - throw new Error(error.error || 'Erro no login'); - } - - const data = isJson ? await response.json() : null; - if (data && data.token) { - localStorage.setItem('ctms_token', data.token); - if (data.refreshToken) localStorage.setItem('ctms_refresh_token', data.refreshToken); - localStorage.setItem('ctms_user_id', data.user.id); - localStorage.setItem('ctms_tenant_id', data.user.tenant_id || ''); - } - return data; -}; - -export const impersonateTenant = async (tenantId: string): Promise => { - const response = await apiFetch(`${API_URL}/impersonate/${tenantId}`, { - method: 'POST', - headers: getHeaders() - }); - - const contentType = response.headers.get("content-type"); - const isJson = contentType && contentType.indexOf("application/json") !== -1; - - if (!response.ok) { - const errorData = isJson ? await response.json() : { error: 'Erro no servidor' }; - throw new Error(errorData.error || 'Erro ao assumir identidade'); - } - - isReloadingForImpersonation = true; // Block logouts - - const data = await response.json(); - const oldToken = localStorage.getItem('ctms_token'); - if (oldToken) { - localStorage.setItem('ctms_super_admin_token', oldToken); - } - - localStorage.setItem('ctms_token', data.token); - localStorage.setItem('ctms_user_id', data.user.id); - localStorage.setItem('ctms_tenant_id', data.user.tenant_id || ''); - - window.location.hash = '#/'; - window.location.reload(); - - return data; -}; - -export const returnToSuperAdmin = (): boolean => { - const superAdminToken = localStorage.getItem('ctms_super_admin_token'); - if (superAdminToken) { - try { - isReloadingForImpersonation = true; // Block logouts - - // Correctly decode Base64Url JWT payload with proper padding - const base64Url = superAdminToken.split('.')[1]; - let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const pad = base64.length % 4; - if (pad) { - base64 += '='.repeat(4 - pad); - } - - const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); - const payload = JSON.parse(jsonPayload); - - localStorage.setItem('ctms_token', superAdminToken); - localStorage.setItem('ctms_user_id', payload.id); - localStorage.setItem('ctms_tenant_id', payload.tenant_id || 'system'); - localStorage.removeItem('ctms_super_admin_token'); - - window.location.hash = '#/super-admin'; - window.location.reload(); - return true; - } catch (e) { - isReloadingForImpersonation = false; - console.error("Failed to restore super admin token", e); - return false; - } - } - return false; -}; - -export const register = async (userData: any): Promise => { - const response = await apiFetch(`${API_URL}/auth/register`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(userData) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Erro no registro'); - } - return true; -}; - -export const verifyCode = async (data: any): Promise => { - const response = await apiFetch(`${API_URL}/auth/verify`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }); - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || 'Código inválido ou expirado'); - } - return true; -}; - -export const forgotPassword = async (email: string): Promise => { - const response = await apiFetch(`${API_URL}/auth/forgot-password`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email }) - }); - - const contentType = response.headers.get("content-type"); - const isJson = contentType && contentType.indexOf("application/json") !== -1; - - if (!response.ok) { - const error = isJson ? await response.json() : { error: 'Erro no servidor' }; - throw new Error(error.error || 'Erro ao processar solicitação'); - } - - const data = isJson ? await response.json() : { message: 'Solicitação processada' }; - return data.message; -}; - -export const resetPassword = async (password: string, token: string, name?: string): Promise => { - const response = await apiFetch(`${API_URL}/auth/reset-password`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password, token, name }) - }); - - const contentType = response.headers.get("content-type"); - const isJson = contentType && contentType.indexOf("application/json") !== -1; - - if (!response.ok) { - const error = isJson ? await response.json() : { error: 'Erro no servidor' }; - throw new Error(error.error || 'Erro ao resetar senha'); - } - - const data = isJson ? await response.json() : { message: 'Senha redefinida' }; - return data.message; -}; +export * from './apiClient'; +export * from './attendancesService'; +export * from './authService'; +export * from './funnelsService'; +export * from './integrationsService'; +export * from './notificationsService'; +export * from './originsService'; +export * from './teamsService'; +export * from './tenantsService'; +export * from './usersService'; diff --git a/src/services/funnelsService.ts b/src/services/funnelsService.ts new file mode 100644 index 0000000..3ec019b --- /dev/null +++ b/src/services/funnelsService.ts @@ -0,0 +1,94 @@ +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getFunnels = async (tenantId: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/funnels?tenantId=${tenantId}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar funis'); + return await response.json(); + } catch (error) { + console.error("API Error (getFunnels):", error); + return []; + } +}; + +export const createFunnel = async (data: { name: string, tenantId: string }): Promise => { + const response = await apiFetch(`${API_URL}/funnels`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar funil'); + } + return await response.json(); +}; + +export const updateFunnel = async (id: string, data: { name?: string, teamIds?: string[] }): Promise => { + try { + const response = await apiFetch(`${API_URL}/funnels/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(data) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateFunnel):", error); + return false; + } +}; + +export const deleteFunnel = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/funnels/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteFunnel):", error); + return false; + } +}; + +export const createFunnelStage = async (funnelId: string, data: any): Promise => { + const response = await apiFetch(`${API_URL}/funnels/${funnelId}/stages`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar etapa'); + } + return await response.json(); +}; + +export const updateFunnelStage = async (id: string, data: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(data) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateFunnelStage):", error); + return false; + } +}; + +export const deleteFunnelStage = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/funnel_stages/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteFunnelStage):", error); + return false; + } +}; diff --git a/src/services/integrationsService.ts b/src/services/integrationsService.ts new file mode 100644 index 0000000..8dc54c8 --- /dev/null +++ b/src/services/integrationsService.ts @@ -0,0 +1,54 @@ +import { User } from '../types'; +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getApiKeys = async (tenantId: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/api-keys?tenantId=${tenantId}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar chaves'); + return await response.json(); + } catch (error) { + console.error("API Error (getApiKeys):", error); + return []; + } +}; + +export const createApiKey = async (data: { name: string, tenantId: string }): Promise => { + const response = await apiFetch(`${API_URL}/api-keys`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar chave de API'); + } + return await response.json(); +}; + +export const deleteApiKey = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/api-keys/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteApiKey):", error); + return false; + } +}; + +export const searchGlobal = async (query: string): Promise<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }> => { + try { + const response = await apiFetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Search failed'); + return await response.json(); + } catch (error) { + console.error("API Error (searchGlobal):", error); + return { members: [], teams: [], attendances: [], organizations: [] }; + } +}; diff --git a/src/services/notificationsService.ts b/src/services/notificationsService.ts new file mode 100644 index 0000000..63bcec3 --- /dev/null +++ b/src/services/notificationsService.ts @@ -0,0 +1,66 @@ +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getNotifications = async (): Promise => { + try { + const response = await apiFetch(`${API_URL}/notifications`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Failed to fetch notifications'); + return await response.json(); + } catch (error) { + console.error("API Error (getNotifications):", error); + return []; + } +}; + +export const markNotificationAsRead = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/notifications/${id}`, { + method: 'PUT', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (markNotificationAsRead):", error); + return false; + } +}; + +export const markAllNotificationsAsRead = async (): Promise => { + try { + const response = await apiFetch(`${API_URL}/notifications/read-all`, { + method: 'PUT', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (markAllNotificationsAsRead):", error); + return false; + } +}; + +export const deleteNotification = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/notifications/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteNotification):", error); + return false; + } +}; + +export const clearAllNotifications = async (): Promise => { + try { + const response = await apiFetch(`${API_URL}/notifications/clear-all`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (clearAllNotifications):", error); + return false; + } +}; diff --git a/src/services/originsService.ts b/src/services/originsService.ts new file mode 100644 index 0000000..aebd5dd --- /dev/null +++ b/src/services/originsService.ts @@ -0,0 +1,94 @@ +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getOrigins = async (tenantId: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/origins?tenantId=${tenantId}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar origens'); + return await response.json(); + } catch (error) { + console.error("API Error (getOrigins):", error); + return []; + } +}; + +export const createOriginGroup = async (data: { name: string, tenantId: string }): Promise => { + const response = await apiFetch(`${API_URL}/origins`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar grupo de origens'); + } + return await response.json(); +}; + +export const updateOriginGroup = async (id: string, data: { name?: string, teamIds?: string[] }): Promise => { + try { + const response = await apiFetch(`${API_URL}/origins/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(data) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateOriginGroup):", error); + return false; + } +}; + +export const deleteOriginGroup = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/origins/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteOriginGroup):", error); + return false; + } +}; + +export const createOriginItem = async (groupId: string, data: { name: string, color_class?: string }): Promise => { + const response = await apiFetch(`${API_URL}/origins/${groupId}/items`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(data) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Erro ao criar item de origem'); + } + return await response.json(); +}; + +export const updateOriginItem = async (id: string, data: { name: string, color_class?: string }): Promise => { + try { + const response = await apiFetch(`${API_URL}/origin_items/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(data) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateOriginItem):", error); + return false; + } +}; + +export const deleteOriginItem = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/origin_items/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteOriginItem):", error); + return false; + } +}; diff --git a/src/services/teamsService.ts b/src/services/teamsService.ts new file mode 100644 index 0000000..4da05f4 --- /dev/null +++ b/src/services/teamsService.ts @@ -0,0 +1,55 @@ +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getTeams = async (tenantId: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/teams?tenantId=${tenantId}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar equipes'); + return await response.json(); + } catch (error) { + console.error("API Error (getTeams):", error); + return []; + } +}; + +export const createTeam = async (teamData: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/teams`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(teamData) + }); + return response.ok; + } catch (error) { + console.error("API Error (createTeam):", error); + return false; + } +}; + +export const updateTeam = async (id: string, teamData: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/teams/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(teamData) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateTeam):", error); + return false; + } +}; + +export const deleteTeam = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/teams/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteTeam):", error); + return false; + } +}; diff --git a/src/services/tenantsService.ts b/src/services/tenantsService.ts new file mode 100644 index 0000000..35015d2 --- /dev/null +++ b/src/services/tenantsService.ts @@ -0,0 +1,62 @@ +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getTenants = async (): Promise => { + try { + const response = await apiFetch(`${API_URL}/tenants`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar tenants'); + + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return await response.json(); + } + return []; + } catch (error) { + console.error("API Error (getTenants):", error); + return []; + } +}; + +export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => { + try { + const response = await apiFetch(`${API_URL}/tenants`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(tenantData) + }); + const data = await response.json().catch(() => null); + if (!response.ok) throw new Error(data?.error || 'Erro ao criar organização'); + return { success: true, message: data?.message || 'Organização criada!' }; + } catch (error: any) { + console.error("API Error (createTenant):", error); + return { success: false, message: error.message }; + } +}; + +export const updateTenant = async (id: string, tenantData: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/tenants/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(tenantData) + }); + return response.ok; + } catch (error) { + console.error("API Error (updateTenant):", error); + return false; + } +}; + +export const deleteTenant = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/tenants/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteTenant):", error); + return false; + } +}; diff --git a/src/services/usersService.ts b/src/services/usersService.ts new file mode 100644 index 0000000..dee06c3 --- /dev/null +++ b/src/services/usersService.ts @@ -0,0 +1,117 @@ +import { User } from '../types'; +import { API_URL, apiFetch, getHeaders } from './apiClient'; + +export const getUsers = async (tenantId: string): Promise => { + try { + const params = new URLSearchParams(); + if (tenantId !== 'all') params.append('tenantId', tenantId); + + const response = await apiFetch(`${API_URL}/users?${params.toString()}`, { + headers: getHeaders() + }); + if (!response.ok) throw new Error('Falha ao buscar usuários'); + + return await response.json(); + } catch (error) { + console.error("API Error (getUsers):", error); + return []; + } +}; + +export const getUserById = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/users/${id}`, { + headers: getHeaders() + }); + if (!response.ok) { + if (response.status === 401 || response.status === 403 || response.status === 404) { + return undefined; + } + throw new Error(`Server error: ${response.status}`); + } + + const contentType = response.headers.get("content-type"); + if (contentType && contentType.indexOf("application/json") !== -1) { + return await response.json(); + } + } catch (error) { + console.error("API Error (getUserById):", error); + throw error; + } +}; + +export const updateUser = async (id: string, userData: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/users/${id}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify(userData) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.error || 'Erro ao atualizar usuário no servidor'); + } + + return true; + } catch (error) { + console.error("API Error (updateUser):", error); + throw error; + } +}; + +export const uploadAvatar = async (id: string, file: File): Promise => { + try { + const formData = new FormData(); + formData.append('avatar', file); + + const token = localStorage.getItem('ctms_token'); + const response = await apiFetch(`${API_URL}/users/${id}/avatar`, { + method: 'POST', + headers: { + ...(token ? { 'Authorization': `Bearer ${token}` } : {}) + }, + body: formData + }); + + if (!response.ok) throw new Error('Falha no upload'); + const data = await response.json(); + return data.avatarUrl; + } catch (error) { + console.error("API Error (uploadAvatar):", error); + return null; + } +}; + +export const createMember = async (userData: any): Promise => { + try { + const response = await apiFetch(`${API_URL}/users`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify(userData) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error(errorData?.error || 'Erro ao criar membro no servidor'); + } + + return true; + } catch (error) { + console.error("API Error (createMember):", error); + throw error; + } +}; + +export const deleteUser = async (id: string): Promise => { + try { + const response = await apiFetch(`${API_URL}/users/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteUser):", error); + return false; + } +};