diff --git a/src/modules/attendance/pages/NewAttendancePage.jsx b/src/modules/attendance/pages/NewAttendancePage.jsx index 8605c11..3cedf81 100644 --- a/src/modules/attendance/pages/NewAttendancePage.jsx +++ b/src/modules/attendance/pages/NewAttendancePage.jsx @@ -2,7 +2,7 @@ import { Link, useNavigate } from 'react-router-dom'; import { BrandMark } from '../../../shared/components/BrandMark'; import { useViewport } from '../../../shared/hooks/useViewport'; -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; import { getCurrentUser } from '../../auth/services/sessionService'; import { listContactProfiles, saveContactProfile } from '../../chat/services/contactProfileService'; import { getAccessOptions } from '../../management/services/adminAccessService'; @@ -177,19 +177,17 @@ function getUserAreas(user) { } async function listWhatsappTemplates() { - const response = await fetch(`${API_BASE_URL}/whatsapp/templates`); - if (!response.ok) throw new Error('Falha ao carregar templates do WhatsApp.'); - return response.json(); + return apiRequest('/whatsapp/templates', { + fallbackMessage: 'Falha ao carregar templates do WhatsApp.', + }); } async function startWhatsappAttendance(payload) { - const response = await fetch(`${API_BASE_URL}/whatsapp/start-attendance`, { + return apiRequest('/whatsapp/start-attendance', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), + fallbackMessage: 'Falha ao iniciar atendimento pelo WhatsApp.', }); - if (!response.ok) throw new Error('Falha ao iniciar atendimento pelo WhatsApp.'); - return response.json(); } export function NewAttendancePage({ embedded = false }) { diff --git a/src/modules/auth/hooks/useLogin.js b/src/modules/auth/hooks/useLogin.js index 1cec748..d77e00d 100644 --- a/src/modules/auth/hooks/useLogin.js +++ b/src/modules/auth/hooks/useLogin.js @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { getAuthConfig, loginWithAd, @@ -17,20 +17,22 @@ function decodeAuthPayload(payload) { export function useLogin() { const navigate = useNavigate(); + const location = useLocation(); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(''); const [providers, setProviders] = useState({ ldap: true, microsoft: false }); + const targetPath = typeof location.state?.from === 'string' ? location.state.from : '/home'; useEffect(() => { if (!window.location.hash.includes('auth=') && isAuthenticated()) { - navigate('/home', { replace: true }); + navigate(targetPath, { replace: true }); return; } getAuthConfig() .then((config) => setProviders(config.providers || { ldap: true, microsoft: false })) .catch(() => setProviders({ ldap: true, microsoft: false })); - }, [navigate]); + }, [navigate, targetPath]); useEffect(() => { const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, '')); @@ -40,7 +42,7 @@ export function useLogin() { try { storeAuthSession(decodeAuthPayload(authPayload)); window.history.replaceState({}, document.title, window.location.pathname); - navigate('/home', { replace: true }); + navigate(targetPath, { replace: true }); return; } catch { window.history.replaceState({}, document.title, window.location.pathname); @@ -52,7 +54,7 @@ export function useLogin() { window.history.replaceState({}, document.title, window.location.pathname); setError('Fluxo de login expirado. Tente novamente.'); } - }, [navigate]); + }, [navigate, targetPath]); async function login(credentials) { setIsSubmitting(true); @@ -61,7 +63,7 @@ export function useLogin() { try { const authResult = await loginWithAd(credentials); storeAuthSession(authResult); - navigate('/home'); + navigate(targetPath); } catch (loginError) { setError(loginError.message || 'Falha ao autenticar.'); } finally { diff --git a/src/modules/auth/services/authService.js b/src/modules/auth/services/authService.js index b539d3f..dbc4801 100644 --- a/src/modules/auth/services/authService.js +++ b/src/modules/auth/services/authService.js @@ -1,28 +1,18 @@ import { API_BASE_URL } from '../../../shared/services/apiConfig'; - -async function parseJsonResponse(response) { - const data = await response.json().catch(() => null); - - if (!response.ok) { - throw new Error(data?.message || 'Não foi possível autenticar.'); - } - - return data; -} +import { apiRequest } from '../../../shared/services/apiClient'; export async function getAuthConfig() { - const response = await fetch(`${API_BASE_URL}/auth/config`); - return parseJsonResponse(response); + return apiRequest('/auth/config', { + fallbackMessage: 'Nao foi possivel carregar configuracao de login.', + }); } export async function loginWithAd(credentials) { - const response = await fetch(`${API_BASE_URL}/auth/login`, { + return apiRequest('/auth/login', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials), + fallbackMessage: 'Nao foi possivel autenticar.', }); - - return parseJsonResponse(response); } export function startMicrosoftLogin() { diff --git a/src/modules/chat/services/agentPresenceService.js b/src/modules/chat/services/agentPresenceService.js index c96d8c3..2ff9bed 100644 --- a/src/modules/chat/services/agentPresenceService.js +++ b/src/modules/chat/services/agentPresenceService.js @@ -1,19 +1,10 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; -async function request(path, options = {}) { - const response = await fetch(`${API_BASE_URL}${path}`, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, +function request(path, options = {}) { + return apiRequest(path, { + fallbackMessage: 'Falha ao atualizar presenca do agente', ...options, }); - - if (!response.ok) { - throw new Error('Falha ao atualizar presenca do agente'); - } - - return response.json(); } export async function getAgentPresence(userId) { diff --git a/src/modules/chat/services/contactProfileService.js b/src/modules/chat/services/contactProfileService.js index e8c4f96..15bd34a 100644 --- a/src/modules/chat/services/contactProfileService.js +++ b/src/modules/chat/services/contactProfileService.js @@ -1,23 +1,21 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; export async function listContactProfiles() { - const response = await fetch(`${API_BASE_URL}/contacts`); - if (!response.ok) throw new Error('Falha ao carregar agenda.'); - return response.json(); + return apiRequest('/contacts', { + fallbackMessage: 'Falha ao carregar agenda.', + }); } export async function getContactProfile(chatId) { - const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`); - if (!response.ok) throw new Error('Falha ao carregar contato.'); - return response.json(); + return apiRequest(`/contacts/${encodeURIComponent(chatId)}`, { + fallbackMessage: 'Falha ao carregar contato.', + }); } export async function saveContactProfile(chatId, payload) { - const response = await fetch(`${API_BASE_URL}/contacts/${encodeURIComponent(chatId)}`, { + return apiRequest(`/contacts/${encodeURIComponent(chatId)}`, { method: 'PUT', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), + fallbackMessage: 'Falha ao salvar contato.', }); - if (!response.ok) throw new Error('Falha ao salvar contato.'); - return response.json(); } diff --git a/src/modules/home/services/agentNotesService.js b/src/modules/home/services/agentNotesService.js index 5613a87..bc47106 100644 --- a/src/modules/home/services/agentNotesService.js +++ b/src/modules/home/services/agentNotesService.js @@ -1,27 +1,24 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; export async function listAgentNotes(userId) { if (!userId) return []; - const response = await fetch(`${API_BASE_URL}/agent/notes?userId=${encodeURIComponent(userId)}`); - if (!response.ok) throw new Error('Falha ao carregar anotações.'); - return response.json(); + + return apiRequest(`/agent/notes?userId=${encodeURIComponent(userId)}`, { + fallbackMessage: 'Falha ao carregar anotacoes.', + }); } export async function createAgentNote(userId, text) { - const response = await fetch(`${API_BASE_URL}/agent/notes`, { + return apiRequest('/agent/notes', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, text }), + fallbackMessage: 'Falha ao salvar anotacao.', }); - if (!response.ok) throw new Error('Falha ao salvar anotação.'); - return response.json(); } export async function deleteAgentNote(userId, noteId) { - const response = await fetch( - `${API_BASE_URL}/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`, - { method: 'DELETE' }, - ); - if (!response.ok) throw new Error('Falha ao excluir anotação.'); - return response.json(); + return apiRequest(`/agent/notes/${encodeURIComponent(noteId)}?userId=${encodeURIComponent(userId)}`, { + method: 'DELETE', + fallbackMessage: 'Falha ao excluir anotacao.', + }); } diff --git a/src/modules/management/components/MassMessagePanel.jsx b/src/modules/management/components/MassMessagePanel.jsx index ac81e1a..4691d99 100644 --- a/src/modules/management/components/MassMessagePanel.jsx +++ b/src/modules/management/components/MassMessagePanel.jsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react'; -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; import { getCurrentUser } from '../../auth/services/sessionService'; import { listContactProfiles } from '../../chat/services/contactProfileService'; import { DataPanel } from './DataPanel'; @@ -54,17 +54,11 @@ function renderPreview(content, variables) { } async function startAttendance(payload) { - const response = await fetch(`${API_BASE_URL}/whatsapp/start-attendance`, { + return apiRequest('/whatsapp/start-attendance', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), + fallbackMessage: 'Falha ao enviar disparo.', }); - - if (!response.ok) { - throw new Error('Falha ao enviar disparo.'); - } - - return response.json(); } export function MassMessagePanel({ diff --git a/src/modules/management/pages/WhatsappAdminPage.jsx b/src/modules/management/pages/WhatsappAdminPage.jsx index 3ca98cb..80d3cb0 100644 --- a/src/modules/management/pages/WhatsappAdminPage.jsx +++ b/src/modules/management/pages/WhatsappAdminPage.jsx @@ -1,6 +1,7 @@ import React, { useEffect, useState } from 'react'; import { io } from 'socket.io-client'; -import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig'; +import { WHATSAPP_SOCKET_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; import { getAuthToken } from '../../auth/services/sessionService'; export const WhatsappAdminPage = () => { @@ -16,8 +17,9 @@ export const WhatsappAdminPage = () => { socket.on('connect', () => { console.log('Connected to WhatsApp WebSocket'); - fetch(`${API_BASE_URL}/whatsapp/status`) - .then((response) => response.json()) + apiRequest('/whatsapp/status', { + fallbackMessage: 'Falha ao consultar status do WhatsApp.', + }) .then((data) => setStatus(data.status)) .catch(console.error); }); diff --git a/src/modules/management/services/adminAccessService.js b/src/modules/management/services/adminAccessService.js index 3dd3c00..0604899 100644 --- a/src/modules/management/services/adminAccessService.js +++ b/src/modules/management/services/adminAccessService.js @@ -1,26 +1,10 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; -async function request(path, options = {}) { - const response = await fetch(`${API_BASE_URL}${path}`, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, +function request(path, options = {}) { + return apiRequest(path, { + fallbackMessage: 'Falha ao consultar acessos', ...options, }); - - if (!response.ok) { - let message = 'Falha ao consultar acessos'; - try { - const payload = await response.json(); - message = payload?.message || payload?.error || message; - } catch { - // Mantem mensagem padrao. - } - throw new Error(message); - } - - return response.json(); } export async function getAccessOptions() { diff --git a/src/modules/management/services/knowledgeService.js b/src/modules/management/services/knowledgeService.js index 7c220e5..d3088aa 100644 --- a/src/modules/management/services/knowledgeService.js +++ b/src/modules/management/services/knowledgeService.js @@ -1,28 +1,10 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; -async function request(path, options = {}) { - const response = await fetch(`${API_BASE_URL}${path}`, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, +function request(path, options = {}) { + return apiRequest(path, { + fallbackMessage: 'Falha ao consultar base de conhecimento.', ...options, }); - - if (!response.ok) { - let message = 'Falha ao consultar base de conhecimento.'; - try { - const payload = await response.json(); - message = Array.isArray(payload?.message) - ? payload.message.join(' ') - : payload?.message || payload?.error || message; - } catch { - // Mantem a mensagem padrao quando a API nao devolve JSON. - } - throw new Error(message); - } - - return response.json(); } export function listRoutingKeywords(areaId) { diff --git a/src/modules/management/services/templateService.js b/src/modules/management/services/templateService.js index c72290b..3b14f0b 100644 --- a/src/modules/management/services/templateService.js +++ b/src/modules/management/services/templateService.js @@ -1,19 +1,10 @@ -import { API_BASE_URL } from '../../../shared/services/apiConfig'; +import { apiRequest } from '../../../shared/services/apiClient'; -async function request(path, options = {}) { - const response = await fetch(`${API_BASE_URL}${path}`, { - headers: { - 'Content-Type': 'application/json', - ...options.headers, - }, +function request(path, options = {}) { + return apiRequest(path, { + fallbackMessage: 'Falha ao consultar templates.', ...options, }); - - if (!response.ok) { - throw new Error('Falha ao consultar templates.'); - } - - return response.json(); } export function listTemplates() { diff --git a/src/routes/ProtectedRoute.jsx b/src/routes/ProtectedRoute.jsx index cd0a586..8474199 100644 --- a/src/routes/ProtectedRoute.jsx +++ b/src/routes/ProtectedRoute.jsx @@ -1,9 +1,11 @@ -import { Navigate } from 'react-router-dom'; +import { Navigate, useLocation } from 'react-router-dom'; import { getCurrentUserProfile, isAuthenticated } from '../modules/auth/services/sessionService'; export function ProtectedRoute({ children, allowedProfiles = [] }) { + const location = useLocation(); + if (!isAuthenticated()) { - return ; + return ; } if (allowedProfiles.length && !allowedProfiles.includes(getCurrentUserProfile())) { diff --git a/src/shared/hooks/useWhatsappSocket.js b/src/shared/hooks/useWhatsappSocket.js index e483a7c..1c7c8a2 100644 --- a/src/shared/hooks/useWhatsappSocket.js +++ b/src/shared/hooks/useWhatsappSocket.js @@ -1,6 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import io from 'socket.io-client'; -import { API_BASE_URL, WHATSAPP_SOCKET_URL } from '../services/apiConfig'; +import { WHATSAPP_SOCKET_URL } from '../services/apiConfig'; +import { apiRequest } from '../services/apiClient'; import { getAuthToken } from '../../modules/auth/services/sessionService'; export function useWhatsappSocket() { @@ -27,8 +28,9 @@ export function useWhatsappSocket() { newSocket.on('connect', () => { console.log('Conectado ao WebSocket do WhatsApp'); // Fetch status atual - fetch(`${API_BASE_URL}/whatsapp/status`) - .then(res => res.json()) + apiRequest('/whatsapp/status', { + fallbackMessage: 'Falha ao consultar status do WhatsApp.', + }) .then(data => setStatus(data.status)) .catch(console.error); }); diff --git a/src/shared/services/apiClient.js b/src/shared/services/apiClient.js new file mode 100644 index 0000000..7201385 --- /dev/null +++ b/src/shared/services/apiClient.js @@ -0,0 +1,62 @@ +import { API_BASE_URL } from './apiConfig'; + +function buildApiUrl(path) { + if (/^https?:\/\//i.test(path)) { + return path; + } + + return `${API_BASE_URL}${path}`; +} + +async function parseResponse(response) { + const text = await response.text(); + + if (!text) { + return null; + } + + try { + return JSON.parse(text); + } catch { + return text; + } +} + +function resolveErrorMessage(payload, fallbackMessage) { + if (Array.isArray(payload?.message)) { + return payload.message.join(' '); + } + + return payload?.message || payload?.error || fallbackMessage; +} + +export async function apiRequest(path, options = {}) { + const { + headers, + fallbackMessage = 'Falha ao consultar API.', + ...requestOptions + } = options; + + const requestHeaders = new Headers(headers || {}); + const hasBody = requestOptions.body !== undefined && requestOptions.body !== null; + const isFormData = typeof FormData !== 'undefined' && requestOptions.body instanceof FormData; + + if (hasBody && !isFormData && !requestHeaders.has('Content-Type')) { + requestHeaders.set('Content-Type', 'application/json'); + } + + const response = await fetch(buildApiUrl(path), { + ...requestOptions, + headers: requestHeaders, + }); + const payload = await parseResponse(response); + + if (!response.ok) { + const error = new Error(resolveErrorMessage(payload, fallbackMessage)); + error.status = response.status; + error.payload = payload; + throw error; + } + + return payload; +} diff --git a/src/shared/services/authFetch.js b/src/shared/services/authFetch.js index 512709d..f4eb3e5 100644 --- a/src/shared/services/authFetch.js +++ b/src/shared/services/authFetch.js @@ -1,14 +1,10 @@ import { API_BASE_URL } from './apiConfig'; -import { clearSession } from '../../modules/auth/services/sessionService'; - -function getStoredToken() { - return window.sessionStorage.getItem('authToken'); -} +import { clearSession, getAuthToken } from '../../modules/auth/services/sessionService'; function shouldAttachToken(resource) { const url = typeof resource === 'string' ? resource : resource?.url; - return Boolean(url && url.startsWith(API_BASE_URL) && getStoredToken()); + return Boolean(url && url.startsWith(API_BASE_URL) && getAuthToken()); } export function installAuthFetchInterceptor() { @@ -25,7 +21,7 @@ export function installAuthFetchInterceptor() { const headers = new Headers(options.headers || {}); if (!headers.has('Authorization')) { - headers.set('Authorization', `Bearer ${getStoredToken()}`); + headers.set('Authorization', `Bearer ${getAuthToken()}`); } const response = await nativeFetch(resource, {