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, {