feat: Update database models, API documentation and frontend authentication system
This commit is contained in:
parent
7010e8e7d9
commit
3430d6bab5
7 changed files with 328 additions and 37 deletions
202
frontend/App.tsx
202
frontend/App.tsx
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
BrowserRouter,
|
BrowserRouter,
|
||||||
Routes,
|
Routes,
|
||||||
|
|
@ -28,6 +28,9 @@ import { LGPD } from "./pages/LGPD";
|
||||||
import { AuthProvider, useAuth } from "./contexts/AuthContext";
|
import { AuthProvider, useAuth } from "./contexts/AuthContext";
|
||||||
import { DataProvider } from "./contexts/DataContext";
|
import { DataProvider } from "./contexts/DataContext";
|
||||||
import { UserRole } from "./types";
|
import { UserRole } from "./types";
|
||||||
|
import { verifyAccessCode } from "./services/apiService";
|
||||||
|
import { Button } from "./components/Button";
|
||||||
|
import { X } from "lucide-react";
|
||||||
import { ShieldAlert } from "lucide-react";
|
import { ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
// Componente de acesso negado
|
// Componente de acesso negado
|
||||||
|
|
@ -55,6 +58,188 @@ const AccessDenied: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Componente de rota protegida por código de acesso
|
||||||
|
interface AccessCodeProtectedRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
type: 'client' | 'professional';
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccessCodeProtectedRoute: React.FC<AccessCodeProtectedRouteProps> = ({ children, type }) => {
|
||||||
|
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
||||||
|
const [accessCode, setAccessCode] = useState("");
|
||||||
|
const [codeError, setCodeError] = useState("");
|
||||||
|
const [isValidated, setIsValidated] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAccess = () => {
|
||||||
|
if (type === 'client') {
|
||||||
|
const isValidated = sessionStorage.getItem('accessCodeValidated');
|
||||||
|
const accessData = sessionStorage.getItem('accessCodeData');
|
||||||
|
|
||||||
|
if (!isValidated || !accessData) {
|
||||||
|
setShowAccessCodeModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValidated(true);
|
||||||
|
} else if (type === 'professional') {
|
||||||
|
const isValidated = sessionStorage.getItem('professionalAccessValidated');
|
||||||
|
const accessData = sessionStorage.getItem('professionalAccessData');
|
||||||
|
|
||||||
|
if (!isValidated || !accessData) {
|
||||||
|
setShowAccessCodeModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsValidated(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAccess();
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
const handleVerifyCode = async () => {
|
||||||
|
if (accessCode.trim() === "") {
|
||||||
|
setCodeError("Por favor, digite o código de acesso");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await verifyAccessCode(accessCode.toUpperCase());
|
||||||
|
if (res.data && res.data.valid) {
|
||||||
|
// Marcar na sessão que o código foi validado
|
||||||
|
if (type === 'client') {
|
||||||
|
sessionStorage.setItem('accessCodeValidated', 'true');
|
||||||
|
sessionStorage.setItem('accessCodeData', JSON.stringify({
|
||||||
|
code: accessCode.toUpperCase(),
|
||||||
|
empresa_id: res.data.empresa_id,
|
||||||
|
empresa_nome: res.data.empresa_nome
|
||||||
|
}));
|
||||||
|
} else if (type === 'professional') {
|
||||||
|
sessionStorage.setItem('professionalAccessValidated', 'true');
|
||||||
|
sessionStorage.setItem('professionalAccessData', JSON.stringify({
|
||||||
|
code: accessCode.toUpperCase()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowAccessCodeModal(false);
|
||||||
|
setIsValidated(true);
|
||||||
|
|
||||||
|
// Se tem empresa e é cliente, atualizar URL com parâmetros
|
||||||
|
if (type === 'client' && res.data.empresa_id) {
|
||||||
|
window.history.replaceState({}, '', `${window.location.pathname}?empresa_id=${res.data.empresa_id}&empresa_nome=${encodeURIComponent(res.data.empresa_nome || '')}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setCodeError("Erro ao verificar código");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCodeForm = () => {
|
||||||
|
setAccessCode("");
|
||||||
|
setCodeError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Se ainda não foi validado, mostrar modal de código
|
||||||
|
if (!isValidated) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||||
|
{/* Modal de código de acesso */}
|
||||||
|
{showAccessCodeModal && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4"
|
||||||
|
onClick={() => setShowAccessCodeModal(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6 sm:p-8 transform transition-all duration-300 ease-out scale-100"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
|
Código de Acesso
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => window.history.back()}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Digite o código de acesso fornecido pela empresa para continuar
|
||||||
|
com o cadastro.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Código de Acesso *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={accessCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAccessCode(e.target.value.toUpperCase());
|
||||||
|
resetCodeForm();
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => e.key === "Enter" && handleVerifyCode()}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#B9CF33] focus:border-transparent uppercase"
|
||||||
|
placeholder="Digite o código"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
{codeError && (
|
||||||
|
<p className="text-red-500 text-sm mt-2">{codeError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
|
<p className="text-sm text-yellow-800">
|
||||||
|
<strong>Atenção:</strong> O código de acesso é fornecido pela
|
||||||
|
empresa e tem validade definida pela empresa.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => window.history.back()}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleVerifyCode} className="flex-1">
|
||||||
|
Verificar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Conteúdo de fundo quando modal não está visível */}
|
||||||
|
{!showAccessCodeModal && (
|
||||||
|
<div className="max-w-md text-center">
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="w-16 h-16 bg-[#492E61] rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Acesso Restrito</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Esta página requer um código de acesso válido para continuar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
// Componente de rota protegida
|
// Componente de rota protegida
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -365,10 +550,21 @@ const AppContent: React.FC = () => {
|
||||||
{/* Rotas Públicas */}
|
{/* Rotas Públicas */}
|
||||||
<Route path="/" element={<HomeWithRouter />} />
|
<Route path="/" element={<HomeWithRouter />} />
|
||||||
<Route path="/entrar" element={<LoginWithRouter />} />
|
<Route path="/entrar" element={<LoginWithRouter />} />
|
||||||
<Route path="/cadastro" element={<RegisterWithRouter />} />
|
<Route
|
||||||
|
path="/cadastro"
|
||||||
|
element={
|
||||||
|
<AccessCodeProtectedRoute type="client">
|
||||||
|
<RegisterWithRouter />
|
||||||
|
</AccessCodeProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/cadastro-profissional"
|
path="/cadastro-profissional"
|
||||||
element={<ProfessionalRegisterWithRouter />}
|
element={
|
||||||
|
<AccessCodeProtectedRoute type="professional">
|
||||||
|
<ProfessionalRegisterWithRouter />
|
||||||
|
</AccessCodeProtectedRoute>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/privacidade"
|
path="/privacidade"
|
||||||
|
|
|
||||||
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
|
|
@ -1968,12 +1968,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^2.0.0",
|
"jwa": "^2.0.1",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -2306,9 +2306,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.9.6",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||||
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
|
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie": "^1.0.1",
|
"cookie": "^1.0.1",
|
||||||
|
|
@ -2328,12 +2328,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-router-dom": {
|
"node_modules/react-router-dom": {
|
||||||
"version": "7.9.6",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
|
||||||
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
|
"integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-router": "7.9.6"
|
"react-router": "7.12.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { createAccessCode, listAccessCodes, deleteAccessCode } from '../services/apiService';
|
import { createAccessCode, listAccessCodes, deleteAccessCode, getCompanies } from '../services/apiService';
|
||||||
import { Plus, Trash2, Copy, Check } from 'lucide-react';
|
import { Plus, Trash2, Copy, Check, ChevronDown } from 'lucide-react';
|
||||||
import { UserRole } from '../types';
|
import { UserRole } from '../types';
|
||||||
|
|
||||||
interface AccessCode {
|
interface AccessCode {
|
||||||
|
|
@ -13,6 +13,8 @@ interface AccessCode {
|
||||||
expira_em: string;
|
expira_em: string;
|
||||||
ativo: boolean;
|
ativo: boolean;
|
||||||
usos: number;
|
usos: number;
|
||||||
|
empresa_id?: string;
|
||||||
|
empresa_nome?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AccessCodes: React.FC = () => {
|
export const AccessCodes: React.FC = () => {
|
||||||
|
|
@ -20,17 +22,31 @@ export const AccessCodes: React.FC = () => {
|
||||||
const [codes, setCodes] = useState<AccessCode[]>([]);
|
const [codes, setCodes] = useState<AccessCode[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [companies, setCompanies] = useState<Array<{id: string; nome: string}>>([]);
|
||||||
|
|
||||||
// Form State
|
// Form State
|
||||||
const [newCode, setNewCode] = useState('');
|
const [newCode, setNewCode] = useState('');
|
||||||
const [days, setDays] = useState(30);
|
const [days, setDays] = useState(30);
|
||||||
const [customDays, setCustomDays] = useState('');
|
const [customDays, setCustomDays] = useState('');
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
|
const [selectedCompanyId, setSelectedCompanyId] = useState<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) fetchCodes();
|
if (token) fetchCodes();
|
||||||
}, [token]);
|
}, [token]);
|
||||||
|
|
||||||
|
const fetchCompanies = async () => {
|
||||||
|
const res = await getCompanies();
|
||||||
|
if (res.data) {
|
||||||
|
setCompanies(res.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateModal = () => {
|
||||||
|
setIsCreateModalOpen(true);
|
||||||
|
fetchCompanies();
|
||||||
|
};
|
||||||
|
|
||||||
const fetchCodes = async () => {
|
const fetchCodes = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await listAccessCodes(token!);
|
const res = await listAccessCodes(token!);
|
||||||
|
|
@ -44,17 +60,24 @@ export const AccessCodes: React.FC = () => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const codeToCreate = newCode.trim() || generateRandomCode();
|
const codeToCreate = newCode.trim() || generateRandomCode();
|
||||||
|
|
||||||
const res = await createAccessCode(token!, {
|
const payload: any = {
|
||||||
codigo: codeToCreate,
|
codigo: codeToCreate,
|
||||||
validade_dias: days,
|
validade_dias: days,
|
||||||
descricao: 'Gerado via Painel'
|
descricao: 'Gerado via Painel'
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (selectedCompanyId) {
|
||||||
|
payload.empresa_id = selectedCompanyId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await createAccessCode(token!, payload);
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
alert('Erro ao criar: ' + res.error);
|
alert('Erro ao criar: ' + res.error);
|
||||||
} else {
|
} else {
|
||||||
setIsCreateModalOpen(false);
|
setIsCreateModalOpen(false);
|
||||||
setNewCode('');
|
setNewCode('');
|
||||||
|
setSelectedCompanyId('');
|
||||||
fetchCodes();
|
fetchCodes();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -91,14 +114,15 @@ export const AccessCodes: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="min-h-screen bg-gray-50 pt-20">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-serif font-bold text-gray-900 mb-2">Gerenciar Códigos de Acesso</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Gerenciar Códigos de Acesso</h1>
|
||||||
<p className="text-gray-600">Crie e gerencie códigos de acesso temporários para novos cadastros</p>
|
<p className="mt-2 text-gray-600">Crie e gerencie códigos de acesso temporários para novos cadastros</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsCreateModalOpen(true)}
|
onClick={openCreateModal}
|
||||||
className="bg-[#492E61] text-white px-6 py-3 rounded-lg font-medium hover:bg-[#5a3a7a] transition-colors flex items-center gap-2 mb-8 shadow-sm"
|
className="bg-[#492E61] text-white px-6 py-3 rounded-lg font-medium hover:bg-[#5a3a7a] transition-colors flex items-center gap-2 mb-8 shadow-sm"
|
||||||
>
|
>
|
||||||
<Plus size={20} />
|
<Plus size={20} />
|
||||||
|
|
@ -112,6 +136,7 @@ export const AccessCodes: React.FC = () => {
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th>
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Status</th>
|
||||||
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Código</th>
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Código</th>
|
||||||
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Empresa</th>
|
||||||
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Validade (Dias)</th>
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Validade (Dias)</th>
|
||||||
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Data de Criação</th>
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Data de Criação</th>
|
||||||
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Data de Expiração</th>
|
<th className="px-6 py-4 text-xs font-semibold text-gray-500 uppercase tracking-wider">Data de Expiração</th>
|
||||||
|
|
@ -120,9 +145,9 @@ export const AccessCodes: React.FC = () => {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-100">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={6} className="px-6 py-8 text-center text-gray-500">Carregando...</td></tr>
|
<tr><td colSpan={7} className="px-6 py-8 text-center text-gray-500">Carregando...</td></tr>
|
||||||
) : codes.length === 0 ? (
|
) : codes.length === 0 ? (
|
||||||
<tr><td colSpan={6} className="px-6 py-8 text-center text-gray-500">Nenhum código gerado.</td></tr>
|
<tr><td colSpan={7} className="px-6 py-8 text-center text-gray-500">Nenhum código gerado.</td></tr>
|
||||||
) : (
|
) : (
|
||||||
codes.map(code => (
|
codes.map(code => (
|
||||||
<tr key={code.id} className="hover:bg-gray-50 transition-colors">
|
<tr key={code.id} className="hover:bg-gray-50 transition-colors">
|
||||||
|
|
@ -143,6 +168,9 @@ export const AccessCodes: React.FC = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-600">
|
||||||
|
{code.empresa_nome || <span className="text-gray-400 italic">Todas as empresas</span>}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 text-gray-600">{code.validade_dias === -1 ? 'Nunca expira' : `${code.validade_dias} dias`}</td>
|
<td className="px-6 py-4 text-gray-600">{code.validade_dias === -1 ? 'Nunca expira' : `${code.validade_dias} dias`}</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">{formatDate(code.criado_em)}</td>
|
<td className="px-6 py-4 text-sm text-gray-500">{formatDate(code.criado_em)}</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">{code.validade_dias === -1 ? '-' : formatDate(code.expira_em)}</td>
|
<td className="px-6 py-4 text-sm text-gray-500">{code.validade_dias === -1 ? '-' : formatDate(code.expira_em)}</td>
|
||||||
|
|
@ -180,6 +208,22 @@ export const AccessCodes: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 mt-1">Deixe em branco para gerar automaticamente.</p>
|
<p className="text-xs text-gray-500 mt-1">Deixe em branco para gerar automaticamente.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Empresa (Opcional)</label>
|
||||||
|
<select
|
||||||
|
value={selectedCompanyId}
|
||||||
|
onChange={e => setSelectedCompanyId(e.target.value)}
|
||||||
|
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-[#492E61] focus:border-transparent outline-none appearance-none bg-white"
|
||||||
|
>
|
||||||
|
<option value="">Todas as empresas</option>
|
||||||
|
{companies.map(company => (
|
||||||
|
<option key={company.id} value={company.id}>
|
||||||
|
{company.nome}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Deixe em branco para permitir uso por todas as empresas.</p>
|
||||||
|
</div>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Validade</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Validade</label>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|
@ -225,7 +269,10 @@ export const AccessCodes: React.FC = () => {
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsCreateModalOpen(false)}
|
onClick={() => {
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
setSelectedCompanyId('');
|
||||||
|
}}
|
||||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
|
|
@ -241,6 +288,7 @@ export const AccessCodes: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
||||||
const [accessCode, setAccessCode] = useState("");
|
const [accessCode, setAccessCode] = useState("");
|
||||||
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
|
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
|
||||||
|
const [selectedCadastroType, setSelectedCadastroType] = useState<'professional' | 'client' | null>(null);
|
||||||
const [codeError, setCodeError] = useState("");
|
const [codeError, setCodeError] = useState("");
|
||||||
// const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock
|
// const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock
|
||||||
|
|
||||||
|
|
@ -39,8 +40,27 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
try {
|
try {
|
||||||
const res = await verifyAccessCode(accessCode.toUpperCase());
|
const res = await verifyAccessCode(accessCode.toUpperCase());
|
||||||
if (res.data && res.data.valid) {
|
if (res.data && res.data.valid) {
|
||||||
|
// Marcar na sessão que o código foi validado
|
||||||
|
sessionStorage.setItem('accessCodeValidated', 'true');
|
||||||
|
sessionStorage.setItem('accessCodeData', JSON.stringify({
|
||||||
|
code: accessCode.toUpperCase(),
|
||||||
|
empresa_id: res.data.empresa_id,
|
||||||
|
empresa_nome: res.data.empresa_nome
|
||||||
|
}));
|
||||||
|
|
||||||
setShowAccessCodeModal(false);
|
setShowAccessCodeModal(false);
|
||||||
window.location.href = "/cadastro";
|
|
||||||
|
// Redirecionar baseado no tipo de cadastro selecionado
|
||||||
|
if (selectedCadastroType === 'professional') {
|
||||||
|
window.location.href = "/cadastro-profissional";
|
||||||
|
} else {
|
||||||
|
// Para cliente, se o código tem empresa associada, passa na URL
|
||||||
|
if (res.data.empresa_id) {
|
||||||
|
window.location.href = `/cadastro?empresa_id=${res.data.empresa_id}&empresa_nome=${encodeURIComponent(res.data.empresa_nome || '')}`;
|
||||||
|
} else {
|
||||||
|
window.location.href = "/cadastro";
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
|
setCodeError(res.data?.error || "Código de acesso inválido ou expirado");
|
||||||
}
|
}
|
||||||
|
|
@ -51,11 +71,8 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
|
|
||||||
const handleProfessionalChoice = (isProfessional: boolean) => {
|
const handleProfessionalChoice = (isProfessional: boolean) => {
|
||||||
setShowProfessionalPrompt(false);
|
setShowProfessionalPrompt(false);
|
||||||
if (isProfessional) {
|
setSelectedCadastroType(isProfessional ? 'professional' : 'client');
|
||||||
window.location.href = "/cadastro-profissional";
|
setShowAccessCodeModal(true);
|
||||||
} else {
|
|
||||||
setShowAccessCodeModal(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
|
|
@ -349,14 +366,17 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||||
<p className="text-sm text-yellow-800">
|
<p className="text-sm text-yellow-800">
|
||||||
<strong>Atenção:</strong> O código de acesso é fornecido pela
|
<strong>Atenção:</strong> O código de acesso é fornecido pela
|
||||||
empresa e tem validade temporária.
|
empresa e tem validade definida pela empresa.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setShowAccessCodeModal(false)}
|
onClick={() => {
|
||||||
|
setShowAccessCodeModal(false);
|
||||||
|
setSelectedCadastroType(null);
|
||||||
|
}}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,9 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Profissional cadastrado com sucesso!");
|
console.log("Profissional cadastrado com sucesso!");
|
||||||
|
// Limpar dados de sessão após cadastro bem-sucedido
|
||||||
|
sessionStorage.removeItem('professionalAccessValidated');
|
||||||
|
sessionStorage.removeItem('professionalAccessData');
|
||||||
setIsSuccess(true);
|
setIsSuccess(true);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao cadastrar profissional:", error);
|
console.error("Erro ao cadastrar profissional:", error);
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,19 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
loadCompanies();
|
loadCompanies();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Verifica se tem empresa pré-selecionada via URL (código de acesso)
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const empresaId = urlParams.get('empresa_id');
|
||||||
|
const empresaNome = urlParams.get('empresa_nome');
|
||||||
|
|
||||||
|
if (empresaId) {
|
||||||
|
setFormData(prev => ({ ...prev, empresaId }));
|
||||||
|
// Limpa os parâmetros da URL após usar
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
}, [companies]);
|
||||||
|
|
||||||
const handleChange = (field: string, value: string | boolean) => {
|
const handleChange = (field: string, value: string | boolean) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
setError("");
|
setError("");
|
||||||
|
|
@ -78,6 +91,9 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
role: "EVENT_OWNER", // Client Role
|
role: "EVENT_OWNER", // Client Role
|
||||||
empresaId: formData.empresaId,
|
empresaId: formData.empresaId,
|
||||||
});
|
});
|
||||||
|
// Limpar dados de sessão após cadastro bem-sucedido
|
||||||
|
sessionStorage.removeItem('accessCodeValidated');
|
||||||
|
sessionStorage.removeItem('accessCodeData');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPending(true);
|
setIsPending(true);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|
@ -230,6 +246,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
administração e selecione <strong>"Não Cadastrado"</strong>{" "}
|
administração e selecione <strong>"Não Cadastrado"</strong>{" "}
|
||||||
abaixo.
|
abaixo.
|
||||||
</p>
|
</p>
|
||||||
|
{formData.empresaId && companies.find(c => c.id === formData.empresaId) && (
|
||||||
|
<p className="text-[10px] text-green-600 mb-2 leading-tight">
|
||||||
|
✓ Empresa pré-selecionada baseada no seu código de acesso
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{isLoadingCompanies ? (
|
{isLoadingCompanies ? (
|
||||||
<p className="text-xs sm:text-sm text-gray-500">
|
<p className="text-xs sm:text-sm text-gray-500">
|
||||||
Carregando empresas...
|
Carregando empresas...
|
||||||
|
|
@ -241,8 +262,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
||||||
onChange={(e) => handleChange("empresaId", e.target.value)}
|
onChange={(e) => handleChange("empresaId", e.target.value)}
|
||||||
className="w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
|
className="w-full px-2.5 sm:px-3 md:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent"
|
||||||
style={{ focusRing: "2px solid #B9CF33" }}
|
style={{ focusRing: "2px solid #B9CF33" }}
|
||||||
|
disabled={!!formData.empresaId && !!companies.find(c => c.id === formData.empresaId)}
|
||||||
>
|
>
|
||||||
<option value="">Selecione uma empresa</option>
|
<option value="">
|
||||||
|
{formData.empresaId ? "Empresa selecionada" : "Selecione uma empresa"}
|
||||||
|
</option>
|
||||||
{companies
|
{companies
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const nameA = a.nome.toLowerCase();
|
const nameA = a.nome.toLowerCase();
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,7 @@ export async function getUploadURL(filename: string, contentType: string): Promi
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access Codes
|
// Access Codes
|
||||||
export async function createAccessCode(token: string, data: { codigo: string; descricao?: string; validade_dias: number }) {
|
export async function createAccessCode(token: string, data: { codigo: string; descricao?: string; validade_dias: number; empresa_id?: string }) {
|
||||||
return mutationFetch(`${API_BASE_URL}/api/codigos-acesso`, "POST", data, token);
|
return mutationFetch(`${API_BASE_URL}/api/codigos-acesso`, "POST", data, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1088,6 +1088,6 @@ export async function listPassengers(carroId: string, token: string) {
|
||||||
/**
|
/**
|
||||||
* Verifica se um código de acesso é válido
|
* Verifica se um código de acesso é válido
|
||||||
*/
|
*/
|
||||||
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string }>> {
|
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>> {
|
||||||
return fetchFromBackend<{ valid: boolean; error?: string }>(`/api/public/codigos-acesso/verificar?code=${encodeURIComponent(code)}`);
|
return fetchFromBackend<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>(`/api/public/codigos-acesso/verificar?code=${encodeURIComponent(code)}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue