Compare commits
2 Commits
aa59e642af
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39ac833495 | ||
|
|
c512809a38 |
@@ -26,7 +26,9 @@ RUN npm ci --omit=dev
|
||||
# Copy backend source directly into root
|
||||
COPY backend/index.js ./index.js
|
||||
COPY backend/db.js ./db.js
|
||||
COPY backend/config ./config
|
||||
COPY backend/policies ./policies
|
||||
COPY backend/services ./services
|
||||
COPY backend/utils ./utils
|
||||
|
||||
# Copy built frontend
|
||||
|
||||
88
GEMINI.md
88
GEMINI.md
@@ -1,88 +0,0 @@
|
||||
# Fasto Project Documentation
|
||||
|
||||
## Overview
|
||||
Fasto is a commercial team management system built with React (Vite) on the frontend and Node.js (Express) on the backend. It uses a MySQL database. It features a complete multi-tenant architecture designed to securely host multiple client organizations within a single deployment.
|
||||
|
||||
## 🚀 Recent Major Changes (March 2026)
|
||||
We have transitioned from a mock-based prototype to a **secure, multi-tenant production architecture**:
|
||||
|
||||
- **Multi-Tenancy & Data Isolation:** All backend routes (Users, Teams, Attendances) now strictly enforce `tenant_id` checks. It is technically impossible for one organization to query data from another.
|
||||
- **Advanced 2-Token Authentication (Rolling Sessions):**
|
||||
- Replaced the vulnerable 1-year static JWT with a highly secure dual-token system.
|
||||
- Generates a short-lived `AccessToken` (15 min) and a stateful `RefreshToken` (30 days) stored in the DB (`refresh_tokens` table).
|
||||
- Built an Axios-like `apiFetch` interceptor on the frontend that automatically catches 401 Unauthorized errors, fetches a new Access Token in the background, extends the Refresh Token by another 30 days (Sliding Expiration), and retries the original request without logging the user out.
|
||||
- Full remote revocation capability (Logout drops the token from the DB immediately).
|
||||
- **God Mode (Tenant Impersonation):** Super Admins can securely impersonate Tenant Admins via a specialized, temporary JWT (`/api/impersonate/:tenantId`). This allows seamless cross-domain support without storing passwords.
|
||||
- **Role-Based Access Control (RBAC) Simplification:**
|
||||
- Removed the redundant 'owner' role. The system now strictly relies on 4 tiers:
|
||||
- **Super Admin:** Global management of all tenants and API keys (via the hidden `system` tenant).
|
||||
- **Admin:** Full control over members, teams, funnels, and origins within their specific organization.
|
||||
- **Manager:** Mid-level control. Can edit basic info of users in their specific team, but cannot change user roles or re-assign users to different teams (only Admins can).
|
||||
- **Agent:** Restricted access. Can only view their own performance metrics and historical attendances.
|
||||
- **Dynamic Funnel & Origin Managers:**
|
||||
- Funnel stages and Lead Origins are no longer hardcoded ENUMs. Each tenant can create multiple dynamic funnel/origin groups via relational tables (`funnels`, `funnel_stages`, `origin_groups`, `origin_items`).
|
||||
- Admins can customize the exact Tailwind color class (e.g., "bg-green-100") for each stage and origin via visual UI pickers.
|
||||
- Admins assign specific Teams to specific Funnels/Origin Groups.
|
||||
- The Dashboard pie charts and data tables strictly filter and color-code data based on the active team's configuration. Deleted data falls back to an "Outros" category to prevent chart breakage.
|
||||
- **n8n / External API Webhooks (Completed):**
|
||||
- Super Admins can generate persistent `api_keys` for specific tenants.
|
||||
- `GET /api/integration/users`, `/funnels`, and `/origins` allow the n8n AI to dynamically map the tenant's actual agents and workflow stages before processing a chat.
|
||||
- `POST /api/integration/attendances` accepts the AI's final JSON payload (including the `full_summary` text) and injects it directly into the dashboard.
|
||||
- **Real-Time Notification System:**
|
||||
- Built a persistent notification tray (`/api/notifications`) with real-time polling (10s intervals) and a hidden HTML5 `<audio>` player for cross-browser sound playback (custom `.mp3` loaded via Vite).
|
||||
- Automated Triggers: Super Admins are notified of new organizations; Admins/Super Admins are notified of new user setups; Agents are notified of team assignment changes; Managers get "Venda Fechada" alerts when n8n posts a converted lead.
|
||||
- **Enhanced UI/UX:**
|
||||
- Premium "Onyx & Gold" True Black dark mode (Zinc scale).
|
||||
- Fully collapsible interactive sidebar with memory (`localStorage`).
|
||||
- All Date/Time displays localized to strict Brazilian formatting (`pt-BR`, 24h, `DD/MM/YY`).
|
||||
|
||||
## 📌 Roadmap / To-Do
|
||||
- [ ] **Advanced AI Notification Triggers:** Implement backend logic to automatically notify Managers when an attendance payload from n8n receives a critically low quality score (`score < 50`), or breaches a specific Response Time SLA (e.g., `first_response_time_min > 60`).
|
||||
- [ ] **Data Export/Reporting:** Allow Admins to export attendance and KPI data to CSV/Excel.
|
||||
- [ ] **Billing/Subscription Management:** Integrate a payment gateway (e.g., Stripe/Asaas) to manage tenant trial periods and active statuses dynamically.
|
||||
|
||||
## 🛠 Architecture
|
||||
- **Frontend**: React 19, TypeScript, Vite, TailwindCSS (CDN), Recharts, Lucide React.
|
||||
- **Backend**: Node.js, Express, MySQL2 (Pool-based), Nodemailer.
|
||||
- **Database**: MySQL 8.0 (Schema: `fasto_db` or `agenciac_comia` depending on `.env`).
|
||||
- **Deployment**: Docker Compose for local development; Gitea Actions for CI/CD pushing to a Gitea Registry and deploying via Portainer webhook.
|
||||
|
||||
## 📋 Prerequisites
|
||||
- Docker & Docker Compose
|
||||
- Node.js (for local development outside Docker)
|
||||
|
||||
## ⚙️ Setup & Running
|
||||
|
||||
### 1. Environment Variables
|
||||
Copy `.env.example` to `.env` and adjust values:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
*Note:* The backend automatically strips literal quotes from Docker `.env` string values (like `SMTP_PASS`) to prevent authentication crashes.
|
||||
|
||||
### 2. Database
|
||||
The project expects a MySQL database. The Node.js backend automatically runs non-destructive schema migrations on startup (adding tables like `refresh_tokens`, `api_keys`, `origin_groups`, etc.).
|
||||
|
||||
### 3. Running Locally (Docker Compose)
|
||||
To start the application and database locally:
|
||||
```bash
|
||||
docker-compose -f docker-compose.local.yml up -d --build
|
||||
```
|
||||
- **App**: http://localhost:3001
|
||||
- **Database**: Port 3306
|
||||
|
||||
### 4. Gitea Runner
|
||||
The `docker-compose.yml` includes a service for a Gitea Runner (`fasto-runner`).
|
||||
- Persistent data is in `./fasto_runner/data`.
|
||||
|
||||
## 🔄 CI/CD Pipeline
|
||||
The project uses Gitea Actions defined in `.gitea/workflows/build-deploy.yaml`.
|
||||
- **Triggers**: Push to `main` or `master`.
|
||||
- **Steps**:
|
||||
1. Checkout code.
|
||||
2. Build Docker image.
|
||||
3. Push to `gitea.blyzer.com.br`.
|
||||
4. Trigger Portainer webhook.
|
||||
|
||||
## 💻 Development
|
||||
The Dockerfile uses a unified root structure. Both the frontend build and the backend Node.js server are hosted from the same container image.
|
||||
13
backend/config/cors.js
Normal file
13
backend/config/cors.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const cors = require('cors');
|
||||
|
||||
const createCorsMiddleware = ({ allowedOrigins, isProduction }) => cors({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || !isProduction || allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
return callback(new Error('Origem não permitida pelo CORS.'));
|
||||
},
|
||||
credentials: true
|
||||
});
|
||||
|
||||
module.exports = { createCorsMiddleware };
|
||||
50
backend/config/runtime.js
Normal file
50
backend/config/runtime.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const { stripEnvQuotes } = require('../utils/security');
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const port = process.env.PORT || 3001;
|
||||
const jwtSecret = process.env.JWT_SECRET || (isProduction ? null : 'fasto_dev_secret_change_me');
|
||||
|
||||
if (!jwtSecret) {
|
||||
throw new Error('JWT_SECRET is required in production.');
|
||||
}
|
||||
|
||||
const allowedOrigins = (process.env.CORS_ORIGIN || '')
|
||||
.split(',')
|
||||
.map(origin => origin.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const smtp = {
|
||||
host: process.env.SMTP_HOST || 'mail.blyzer.com.br',
|
||||
port: parseInt(process.env.SMTP_PORT, 10) || 587,
|
||||
user: stripEnvQuotes(process.env.SMTP_USER || 'nao-responda@blyzer.com.br'),
|
||||
pass: stripEnvQuotes(process.env.SMTP_PASS || ''),
|
||||
debug: process.env.SMTP_DEBUG === 'true',
|
||||
};
|
||||
|
||||
const getBaseUrl = (req) => {
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
|
||||
const host = req ? (req.get('host') || 'localhost:3001') : 'localhost:3001';
|
||||
const protocol = (req && (req.protocol === 'https' || req.get('x-forwarded-proto') === 'https')) ? 'https' : 'http';
|
||||
|
||||
if (isProduction && !host.includes('localhost')) {
|
||||
return `https://${host}`;
|
||||
}
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
const getStartupBaseUrl = () => {
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
if (isProduction) return 'https://fasto.blyzer.com.br';
|
||||
return 'http://localhost:3001';
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
allowedOrigins,
|
||||
getBaseUrl,
|
||||
getStartupBaseUrl,
|
||||
isProduction,
|
||||
jwtSecret,
|
||||
port,
|
||||
smtp,
|
||||
};
|
||||
@@ -1,16 +1,17 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const nodemailer = require('nodemailer');
|
||||
const multer = require('multer');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const fs = require('fs');
|
||||
const pool = require('./db');
|
||||
const { stripEnvQuotes, hashSecret, maskSecret } = require('./utils/security');
|
||||
const { hashSecret, maskSecret } = require('./utils/security');
|
||||
const transporter = require('./services/mailer');
|
||||
const { createCorsMiddleware } = require('./config/cors');
|
||||
const { allowedOrigins, getBaseUrl, getStartupBaseUrl, isProduction, jwtSecret: JWT_SECRET, port: PORT } = require('./config/runtime');
|
||||
const {
|
||||
canReadUser,
|
||||
canUpdateUser,
|
||||
@@ -21,69 +22,9 @@ const {
|
||||
} = require('./policies/accessPolicy');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const JWT_SECRET = process.env.JWT_SECRET || (isProduction ? null : 'fasto_dev_secret_change_me');
|
||||
|
||||
if (!JWT_SECRET) {
|
||||
throw new Error('JWT_SECRET is required in production.');
|
||||
}
|
||||
|
||||
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, sound_enabled, created_at';
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST || 'mail.blyzer.com.br',
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: false, // false para 587 (STARTTLS)
|
||||
auth: {
|
||||
user: stripEnvQuotes(process.env.SMTP_USER || 'nao-responda@blyzer.com.br'),
|
||||
pass: stripEnvQuotes(process.env.SMTP_PASS || ''),
|
||||
},
|
||||
tls: {
|
||||
ciphers: 'SSLv3',
|
||||
rejectUnauthorized: false
|
||||
},
|
||||
debug: process.env.SMTP_DEBUG === 'true',
|
||||
logger: process.env.SMTP_DEBUG === 'true'
|
||||
});
|
||||
|
||||
// Helper para obter a URL base
|
||||
const getBaseUrl = (req) => {
|
||||
// Use explicit environment variable if set
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
|
||||
// Otherwise, attempt to construct it from the request object dynamically
|
||||
const host = req ? (req.get('host') || 'localhost:3001') : 'localhost:3001';
|
||||
const protocol = (req && (req.protocol === 'https' || req.get('x-forwarded-proto') === 'https')) ? 'https' : 'http';
|
||||
|
||||
// Se estivermos em produção e o host não for localhost, force HTTPS
|
||||
if (process.env.NODE_ENV === 'production' && !host.includes('localhost')) {
|
||||
return `https://${host}`;
|
||||
}
|
||||
return `${protocol}://${host}`;
|
||||
};
|
||||
|
||||
// Quando não temos a request (ex: startup), usamos uma versão simplificada
|
||||
const getStartupBaseUrl = () => {
|
||||
if (process.env.APP_URL) return process.env.APP_URL;
|
||||
if (process.env.NODE_ENV === 'production') return 'https://fasto.blyzer.com.br';
|
||||
return 'http://localhost:3001';
|
||||
};
|
||||
|
||||
const allowedOrigins = (process.env.CORS_ORIGIN || '')
|
||||
.split(',')
|
||||
.map(origin => origin.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
app.use(cors({
|
||||
origin: (origin, callback) => {
|
||||
if (!origin || !isProduction || allowedOrigins.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
return callback(new Error('Origem não permitida pelo CORS.'));
|
||||
},
|
||||
credentials: true
|
||||
}));
|
||||
app.use(createCorsMiddleware({ allowedOrigins, isProduction }));
|
||||
app.use(express.json());
|
||||
|
||||
// Logger de Requisições
|
||||
|
||||
20
backend/services/mailer.js
Normal file
20
backend/services/mailer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const { smtp } = require('../config/runtime');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtp.host,
|
||||
port: smtp.port,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: smtp.user,
|
||||
pass: smtp.pass,
|
||||
},
|
||||
tls: {
|
||||
ciphers: 'SSLv3',
|
||||
rejectUnauthorized: false
|
||||
},
|
||||
debug: smtp.debug,
|
||||
logger: smtp.debug
|
||||
});
|
||||
|
||||
module.exports = transporter;
|
||||
@@ -235,7 +235,11 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
|
||||
{!isSuperAdmin && (
|
||||
<>
|
||||
<SidebarItem to="/" icon={LayoutDashboard} label="Dashboard" collapsed={isSidebarCollapsed} />
|
||||
|
||||
|
||||
{currentUser.role === 'agent' && (
|
||||
<SidebarItem to={`/users/${currentUser.slug || currentUser.id}`} icon={MessageSquare} label="Seus Atendimentos" collapsed={isSidebarCollapsed} />
|
||||
)}
|
||||
|
||||
{currentUser.role !== 'agent' && (
|
||||
<>
|
||||
<SidebarItem to="/ranking" icon={Trophy} label="Ranking de Vendedores" collapsed={isSidebarCollapsed} />
|
||||
|
||||
48
src/services/attendancesService.ts
Normal file
48
src/services/attendancesService.ts
Normal file
@@ -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<Attendance[]> => {
|
||||
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<Attendance | undefined> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
180
src/services/authService.ts
Normal file
180
src/services/authService.ts
Normal file
@@ -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<any> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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;
|
||||
};
|
||||
@@ -1,771 +1,10 @@
|
||||
|
||||
import { Attendance, DashboardFilter, User } from '../types';
|
||||
import { API_URL, apiFetch, getHeaders, setSessionExpiredHandler } from './apiClient';
|
||||
|
||||
export const getNotifications = async (): Promise<any[]> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<Attendance[]> => {
|
||||
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<User[]> => {
|
||||
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<User | undefined> => {
|
||||
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<boolean> => {
|
||||
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<string | null> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<Attendance | undefined> => {
|
||||
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<any[]> => {
|
||||
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<any[]> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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';
|
||||
|
||||
94
src/services/funnelsService.ts
Normal file
94
src/services/funnelsService.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getFunnels = async (tenantId: string): Promise<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
54
src/services/integrationsService.ts
Normal file
54
src/services/integrationsService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { User } from '../types';
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getApiKeys = async (tenantId: string): Promise<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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: [] };
|
||||
}
|
||||
};
|
||||
66
src/services/notificationsService.ts
Normal file
66
src/services/notificationsService.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getNotifications = async (): Promise<any[]> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
94
src/services/originsService.ts
Normal file
94
src/services/originsService.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getOrigins = async (tenantId: string): Promise<any[]> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<any> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
55
src/services/teamsService.ts
Normal file
55
src/services/teamsService.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getTeams = async (tenantId: string): Promise<any[]> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
62
src/services/tenantsService.ts
Normal file
62
src/services/tenantsService.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getTenants = async (): Promise<any[]> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
117
src/services/usersService.ts
Normal file
117
src/services/usersService.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { User } from '../types';
|
||||
import { API_URL, apiFetch, getHeaders } from './apiClient';
|
||||
|
||||
export const getUsers = async (tenantId: string): Promise<User[]> => {
|
||||
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<User | undefined> => {
|
||||
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<boolean> => {
|
||||
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<string | null> => {
|
||||
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<boolean> => {
|
||||
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<boolean> => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user