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 {
|
||||
BrowserRouter,
|
||||
Routes,
|
||||
|
|
@ -28,6 +28,9 @@ import { LGPD } from "./pages/LGPD";
|
|||
import { AuthProvider, useAuth } from "./contexts/AuthContext";
|
||||
import { DataProvider } from "./contexts/DataContext";
|
||||
import { UserRole } from "./types";
|
||||
import { verifyAccessCode } from "./services/apiService";
|
||||
import { Button } from "./components/Button";
|
||||
import { X } from "lucide-react";
|
||||
import { ShieldAlert } from "lucide-react";
|
||||
|
||||
// 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
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -365,10 +550,21 @@ const AppContent: React.FC = () => {
|
|||
{/* Rotas Públicas */}
|
||||
<Route path="/" element={<HomeWithRouter />} />
|
||||
<Route path="/entrar" element={<LoginWithRouter />} />
|
||||
<Route path="/cadastro" element={<RegisterWithRouter />} />
|
||||
<Route
|
||||
path="/cadastro"
|
||||
element={
|
||||
<AccessCodeProtectedRoute type="client">
|
||||
<RegisterWithRouter />
|
||||
</AccessCodeProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/cadastro-profissional"
|
||||
element={<ProfessionalRegisterWithRouter />}
|
||||
element={
|
||||
<AccessCodeProtectedRoute type="professional">
|
||||
<ProfessionalRegisterWithRouter />
|
||||
</AccessCodeProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/privacidade"
|
||||
|
|
|
|||
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
|
|
@ -1968,12 +1968,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
|
|
@ -2306,9 +2306,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
|
||||
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||
"integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
|
|
@ -2328,12 +2328,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
|
||||
"integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
|
||||
"version": "7.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
|
||||
"integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.6"
|
||||
"react-router": "7.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { createAccessCode, listAccessCodes, deleteAccessCode } from '../services/apiService';
|
||||
import { Plus, Trash2, Copy, Check } from 'lucide-react';
|
||||
import { createAccessCode, listAccessCodes, deleteAccessCode, getCompanies } from '../services/apiService';
|
||||
import { Plus, Trash2, Copy, Check, ChevronDown } from 'lucide-react';
|
||||
import { UserRole } from '../types';
|
||||
|
||||
interface AccessCode {
|
||||
|
|
@ -13,6 +13,8 @@ interface AccessCode {
|
|||
expira_em: string;
|
||||
ativo: boolean;
|
||||
usos: number;
|
||||
empresa_id?: string;
|
||||
empresa_nome?: string;
|
||||
}
|
||||
|
||||
export const AccessCodes: React.FC = () => {
|
||||
|
|
@ -20,17 +22,31 @@ export const AccessCodes: React.FC = () => {
|
|||
const [codes, setCodes] = useState<AccessCode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [companies, setCompanies] = useState<Array<{id: string; nome: string}>>([]);
|
||||
|
||||
// Form State
|
||||
const [newCode, setNewCode] = useState('');
|
||||
const [days, setDays] = useState(30);
|
||||
const [customDays, setCustomDays] = useState('');
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [selectedCompanyId, setSelectedCompanyId] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (token) fetchCodes();
|
||||
}, [token]);
|
||||
|
||||
const fetchCompanies = async () => {
|
||||
const res = await getCompanies();
|
||||
if (res.data) {
|
||||
setCompanies(res.data);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setIsCreateModalOpen(true);
|
||||
fetchCompanies();
|
||||
};
|
||||
|
||||
const fetchCodes = async () => {
|
||||
setLoading(true);
|
||||
const res = await listAccessCodes(token!);
|
||||
|
|
@ -44,17 +60,24 @@ export const AccessCodes: React.FC = () => {
|
|||
e.preventDefault();
|
||||
const codeToCreate = newCode.trim() || generateRandomCode();
|
||||
|
||||
const res = await createAccessCode(token!, {
|
||||
const payload: any = {
|
||||
codigo: codeToCreate,
|
||||
validade_dias: days,
|
||||
descricao: 'Gerado via Painel'
|
||||
});
|
||||
};
|
||||
|
||||
if (selectedCompanyId) {
|
||||
payload.empresa_id = selectedCompanyId;
|
||||
}
|
||||
|
||||
const res = await createAccessCode(token!, payload);
|
||||
|
||||
if (res.error) {
|
||||
alert('Erro ao criar: ' + res.error);
|
||||
} else {
|
||||
setIsCreateModalOpen(false);
|
||||
setNewCode('');
|
||||
setSelectedCompanyId('');
|
||||
fetchCodes();
|
||||
}
|
||||
};
|
||||
|
|
@ -91,14 +114,15 @@ export const AccessCodes: React.FC = () => {
|
|||
}
|
||||
|
||||
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">
|
||||
<h1 className="text-3xl font-serif font-bold text-gray-900 mb-2">Gerenciar Códigos de Acesso</h1>
|
||||
<p className="text-gray-600">Crie e gerencie códigos de acesso temporários para novos cadastros</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Gerenciar Códigos de Acesso</h1>
|
||||
<p className="mt-2 text-gray-600">Crie e gerencie códigos de acesso temporários para novos cadastros</p>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
<Plus size={20} />
|
||||
|
|
@ -112,6 +136,7 @@ export const AccessCodes: React.FC = () => {
|
|||
<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">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">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>
|
||||
|
|
@ -120,9 +145,9 @@ export const AccessCodes: React.FC = () => {
|
|||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{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 ? (
|
||||
<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 => (
|
||||
<tr key={code.id} className="hover:bg-gray-50 transition-colors">
|
||||
|
|
@ -143,6 +168,9 @@ export const AccessCodes: React.FC = () => {
|
|||
</button>
|
||||
</div>
|
||||
</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-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>
|
||||
|
|
@ -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>
|
||||
</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">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Validade</label>
|
||||
<div className="space-y-3">
|
||||
|
|
@ -225,7 +269,10 @@ export const AccessCodes: React.FC = () => {
|
|||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsCreateModalOpen(false)}
|
||||
onClick={() => {
|
||||
setIsCreateModalOpen(false);
|
||||
setSelectedCompanyId('');
|
||||
}}
|
||||
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg"
|
||||
>
|
||||
Cancelar
|
||||
|
|
@ -241,6 +288,7 @@ export const AccessCodes: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
const [showAccessCodeModal, setShowAccessCodeModal] = useState(false);
|
||||
const [accessCode, setAccessCode] = useState("");
|
||||
const [showProfessionalPrompt, setShowProfessionalPrompt] = useState(false);
|
||||
const [selectedCadastroType, setSelectedCadastroType] = useState<'professional' | 'client' | null>(null);
|
||||
const [codeError, setCodeError] = useState("");
|
||||
// const MOCK_ACCESS_CODE = "PHOTUM2025"; // Removed mock
|
||||
|
||||
|
|
@ -39,8 +40,27 @@ export const Login: React.FC<LoginProps> = ({ onNavigate }) => {
|
|||
try {
|
||||
const res = await verifyAccessCode(accessCode.toUpperCase());
|
||||
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);
|
||||
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 {
|
||||
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) => {
|
||||
setShowProfessionalPrompt(false);
|
||||
if (isProfessional) {
|
||||
window.location.href = "/cadastro-profissional";
|
||||
} else {
|
||||
setShowAccessCodeModal(true);
|
||||
}
|
||||
setSelectedCadastroType(isProfessional ? 'professional' : 'client');
|
||||
setShowAccessCodeModal(true);
|
||||
};
|
||||
|
||||
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">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowAccessCodeModal(false)}
|
||||
onClick={() => {
|
||||
setShowAccessCodeModal(false);
|
||||
setSelectedCadastroType(null);
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancelar
|
||||
|
|
|
|||
|
|
@ -98,6 +98,9 @@ export const ProfessionalRegister: React.FC<ProfessionalRegisterProps> = ({
|
|||
}
|
||||
|
||||
console.log("Profissional cadastrado com sucesso!");
|
||||
// Limpar dados de sessão após cadastro bem-sucedido
|
||||
sessionStorage.removeItem('professionalAccessValidated');
|
||||
sessionStorage.removeItem('professionalAccessData');
|
||||
setIsSuccess(true);
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao cadastrar profissional:", error);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,19 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
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) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
setError("");
|
||||
|
|
@ -78,6 +91,9 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
role: "EVENT_OWNER", // Client Role
|
||||
empresaId: formData.empresaId,
|
||||
});
|
||||
// Limpar dados de sessão após cadastro bem-sucedido
|
||||
sessionStorage.removeItem('accessCodeValidated');
|
||||
sessionStorage.removeItem('accessCodeData');
|
||||
setIsLoading(false);
|
||||
setIsPending(true);
|
||||
} catch (err: any) {
|
||||
|
|
@ -230,6 +246,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
administração e selecione <strong>"Não Cadastrado"</strong>{" "}
|
||||
abaixo.
|
||||
</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 ? (
|
||||
<p className="text-xs sm:text-sm text-gray-500">
|
||||
Carregando empresas...
|
||||
|
|
@ -241,8 +262,11 @@ export const Register: React.FC<RegisterProps> = ({ onNavigate }) => {
|
|||
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"
|
||||
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
|
||||
.sort((a, b) => {
|
||||
const nameA = a.nome.toLowerCase();
|
||||
|
|
|
|||
|
|
@ -847,7 +847,7 @@ export async function getUploadURL(filename: string, contentType: string): Promi
|
|||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -1088,6 +1088,6 @@ export async function listPassengers(carroId: string, token: string) {
|
|||
/**
|
||||
* Verifica se um código de acesso é válido
|
||||
*/
|
||||
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string }>> {
|
||||
return fetchFromBackend<{ valid: boolean; error?: string }>(`/api/public/codigos-acesso/verificar?code=${encodeURIComponent(code)}`);
|
||||
export async function verifyAccessCode(code: string): Promise<ApiResponse<{ valid: boolean; error?: string; empresa_id?: string; empresa_nome?: string }>> {
|
||||
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