feat: Update database models, API documentation and frontend authentication system

This commit is contained in:
JoaoVitorMS0 2026-01-12 16:41:50 -03:00
parent 7010e8e7d9
commit 3430d6bab5
7 changed files with 328 additions and 37 deletions

View file

@ -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"

View file

@ -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"

View file

@ -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>
); );
}; };

View file

@ -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

View file

@ -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);

View file

@ -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();

View file

@ -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)}`);
} }