Compare commits
4 Commits
107bba89d8
...
b0ab1e86ca
| Author | SHA1 | Date | |
|---|---|---|---|
| b0ab1e86ca | |||
| 498d6c80c1 | |||
| b50050a545 | |||
| 6aef2b4910 |
29
.gitignore
vendored
@ -1,2 +1,29 @@
|
|||||||
node_modules/
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.tsriseup-squad20/
|
||||||
|
susconecta/riseup-squad20/
|
||||||
|
riseup-squad20/
|
||||||
|
|||||||
370
IMPLEMENTACAO_OPCAO2_VINCULO_EMAIL.md
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
# ✅ Implementação: Opção 2 - Vínculo por Email
|
||||||
|
|
||||||
|
## 🎯 Solução Implementada
|
||||||
|
|
||||||
|
**Vínculo entre Supabase Auth e API Mock através do EMAIL**
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐
|
||||||
|
│ Supabase Auth │
|
||||||
|
│ (Login/Autenticação) │
|
||||||
|
│ │
|
||||||
|
│ email: "user@email.com"│ ◄─┐
|
||||||
|
│ password: "senha123!" │ │
|
||||||
|
│ userType: "paciente" │ │
|
||||||
|
└──────────────────────────┘ │
|
||||||
|
│ VÍNCULO
|
||||||
|
┌──────────────────────────┐ │ POR EMAIL
|
||||||
|
│ API Mock (Apidog) │ │
|
||||||
|
│ (Dados do Sistema) │ │
|
||||||
|
│ │ │
|
||||||
|
│ email: "user@email.com"│ ◄─┘
|
||||||
|
│ full_name: "João Silva"│
|
||||||
|
│ cpf: "123.456.789-00" │
|
||||||
|
└──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Código Implementado
|
||||||
|
|
||||||
|
### `lib/api.ts` - Funções de Criação de Usuários
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ENV_CONFIG } from '@/lib/env-config';
|
||||||
|
import { API_KEY } from '@/lib/config';
|
||||||
|
|
||||||
|
// Gera senha aleatória (formato: senhaXXX!)
|
||||||
|
export function gerarSenhaAleatoria(): string {
|
||||||
|
const num1 = Math.floor(Math.random() * 10);
|
||||||
|
const num2 = Math.floor(Math.random() * 10);
|
||||||
|
const num3 = Math.floor(Math.random() * 10);
|
||||||
|
return `senha${num1}${num2}${num3}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cria usuário MÉDICO no Supabase Auth
|
||||||
|
export async function criarUsuarioMedico(medico: {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_mobile: string;
|
||||||
|
}): Promise<CreateUserWithPasswordResponse> {
|
||||||
|
const senha = gerarSenhaAleatoria();
|
||||||
|
|
||||||
|
// Endpoint do Supabase Auth (mesmo que auth.ts usa)
|
||||||
|
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email: medico.email, // ◄── VÍNCULO!
|
||||||
|
password: senha,
|
||||||
|
data: {
|
||||||
|
userType: 'profissional',
|
||||||
|
full_name: medico.full_name,
|
||||||
|
phone: medico.phone_mobile,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(signupUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
apikey: API_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Erro ao criar usuário: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: responseData.user,
|
||||||
|
email: medico.email,
|
||||||
|
password: senha,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cria usuário PACIENTE no Supabase Auth
|
||||||
|
export async function criarUsuarioPaciente(paciente: {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_mobile: string;
|
||||||
|
}): Promise<CreateUserWithPasswordResponse> {
|
||||||
|
const senha = gerarSenhaAleatoria();
|
||||||
|
|
||||||
|
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email: paciente.email, // ◄── VÍNCULO!
|
||||||
|
password: senha,
|
||||||
|
data: {
|
||||||
|
userType: 'paciente',
|
||||||
|
full_name: paciente.full_name,
|
||||||
|
phone: paciente.phone_mobile,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch(signupUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
apikey: API_KEY,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
throw new Error(`Erro ao criar usuário: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
user: responseData.user,
|
||||||
|
email: paciente.email,
|
||||||
|
password: senha,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Fluxo Completo
|
||||||
|
|
||||||
|
### 1️⃣ **Admin Cadastra Paciente**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// components/forms/patient-registration-form.tsx
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
// 1. Salva paciente na API Mock
|
||||||
|
const saved = await salvarPaciente({
|
||||||
|
full_name: form.nome,
|
||||||
|
email: form.email, // ◄── EMAIL usado como vínculo
|
||||||
|
cpf: form.cpf,
|
||||||
|
telefone: form.telefone,
|
||||||
|
// ...outros dados
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Cria usuário no Supabase Auth com MESMO EMAIL
|
||||||
|
if (mode === 'create' && form.email) {
|
||||||
|
try {
|
||||||
|
const credentials = await criarUsuarioPaciente({
|
||||||
|
email: form.email, // ◄── MESMO EMAIL!
|
||||||
|
full_name: form.nome,
|
||||||
|
phone_mobile: form.telefone,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Mostra popup com credenciais
|
||||||
|
setCredentials(credentials);
|
||||||
|
setShowCredentials(true);
|
||||||
|
} catch (error) {
|
||||||
|
alert('Paciente cadastrado, mas houve erro ao criar usuário de acesso');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ **Paciente Faz Login**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Paciente vai em /login-paciente
|
||||||
|
// Digita: email = "jonas@email.com", password = "senha481!"
|
||||||
|
|
||||||
|
// hooks/useAuth.tsx
|
||||||
|
const login = async (email, password, userType) => {
|
||||||
|
// Autentica no Supabase Auth
|
||||||
|
const response = await loginUser(email, password, userType);
|
||||||
|
|
||||||
|
// Token JWT contém o email
|
||||||
|
const token = response.access_token;
|
||||||
|
const decoded = decodeJWT(token);
|
||||||
|
console.log(decoded.email); // "jonas@email.com"
|
||||||
|
|
||||||
|
// Salva sessão
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
|
||||||
|
// Redireciona para /paciente
|
||||||
|
router.push('/paciente');
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ **Buscar Dados do Paciente na Área Logada**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/paciente/page.tsx
|
||||||
|
|
||||||
|
export default function PacientePage() {
|
||||||
|
const { user } = useAuth(); // user.email = "jonas@email.com"
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function carregarDados() {
|
||||||
|
// Busca paciente pelo EMAIL (vínculo)
|
||||||
|
const response = await fetch(
|
||||||
|
`https://mock.apidog.com/pacientes?email=${user.email}`
|
||||||
|
);
|
||||||
|
const paciente = await response.json();
|
||||||
|
|
||||||
|
// Agora tem os dados completos do paciente
|
||||||
|
console.log(paciente);
|
||||||
|
// {
|
||||||
|
// id: "123",
|
||||||
|
// full_name: "Jonas Francisco",
|
||||||
|
// email: "jonas@email.com", ◄── VÍNCULO!
|
||||||
|
// cpf: "123.456.789-00",
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
carregarDados();
|
||||||
|
}, [user.email]);
|
||||||
|
|
||||||
|
return <div>Bem-vindo, {user.email}!</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Estrutura dos Dados
|
||||||
|
|
||||||
|
### **Supabase Auth (`auth.users`)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"email": "jonas@email.com",
|
||||||
|
"encrypted_password": "$2a$10$...",
|
||||||
|
"created_at": "2025-10-03T00:00:00",
|
||||||
|
"user_metadata": {
|
||||||
|
"userType": "paciente",
|
||||||
|
"full_name": "Jonas Francisco",
|
||||||
|
"phone": "(79) 99649-8907"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **API Mock - Tabela `pacientes`**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "123",
|
||||||
|
"full_name": "Jonas Francisco Nascimento Bonfim",
|
||||||
|
"email": "jonas@email.com",
|
||||||
|
"cpf": "123.456.789-00",
|
||||||
|
"telefone": "(79) 99649-8907",
|
||||||
|
"data_nascimento": "1990-01-15",
|
||||||
|
"endereco": {
|
||||||
|
"cep": "49000-000",
|
||||||
|
"logradouro": "Rua Principal",
|
||||||
|
"cidade": "Aracaju"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Vínculo:** Campo `email` presente em ambos os sistemas!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Vantagens da Opção 2
|
||||||
|
|
||||||
|
✅ **Simples:** Não precisa modificar estrutura da API Mock
|
||||||
|
✅ **Natural:** Email já é único e obrigatório
|
||||||
|
✅ **Sem duplicação:** Usa campo existente
|
||||||
|
✅ **Funcional:** Supabase Auth retorna email no token JWT
|
||||||
|
✅ **Escalável:** Fácil de manter e debugar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Considerações Importantes
|
||||||
|
|
||||||
|
### **Email DEVE ser único**
|
||||||
|
|
||||||
|
- Cada email só pode ter um usuário no Supabase Auth
|
||||||
|
- Cada email só pode ter um paciente/médico na API Mock
|
||||||
|
- Se tentar cadastrar email duplicado, Supabase retorna erro
|
||||||
|
|
||||||
|
### **Email DEVE ser válido**
|
||||||
|
|
||||||
|
- Supabase valida formato (nome@dominio.com)
|
||||||
|
- Use validação no formulário antes de enviar
|
||||||
|
|
||||||
|
### **Formato da senha**
|
||||||
|
|
||||||
|
- Supabase exige mínimo 6 caracteres
|
||||||
|
- Geramos: `senhaXXX!` (10 caracteres) ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Logs Esperados (Sucesso)
|
||||||
|
|
||||||
|
```
|
||||||
|
🏥 [CRIAR PACIENTE] Iniciando criação no Supabase Auth...
|
||||||
|
📧 Email: jonas@email.com
|
||||||
|
👤 Nome: Jonas Francisco Nascimento Bonfim
|
||||||
|
📱 Telefone: (79) 99649-8907
|
||||||
|
🔑 Senha gerada: senha481!
|
||||||
|
📤 [CRIAR PACIENTE] Enviando para: https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/signup
|
||||||
|
📋 [CRIAR PACIENTE] Status da resposta: 200 OK
|
||||||
|
✅ [CRIAR PACIENTE] Usuário criado com sucesso no Supabase Auth!
|
||||||
|
🆔 User ID: 550e8400-e29b-41d4-a716-446655440000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❌ Possíveis Erros
|
||||||
|
|
||||||
|
### **Erro: "Este email já está cadastrado no sistema"**
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ [CRIAR PACIENTE] Erro: User already registered
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solução:** Email já existe no Supabase. Use outro email ou delete o usuário existente.
|
||||||
|
|
||||||
|
### **Erro: "Formato de email inválido"**
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ [CRIAR PACIENTE] Erro: Invalid email format
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solução:** Valide o formato do email antes de enviar.
|
||||||
|
|
||||||
|
### **Erro: 429 - Too Many Requests**
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ [CRIAR PACIENTE] Erro: 429
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solução:** Aguarde 1 minuto. Supabase limita taxa de requisições.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Resultado Final
|
||||||
|
|
||||||
|
**Agora o sistema funciona assim:**
|
||||||
|
|
||||||
|
1. ✅ Admin cadastra paciente → Salva na API Mock
|
||||||
|
2. ✅ Sistema cria usuário no Supabase Auth (mesmo email)
|
||||||
|
3. ✅ Popup mostra credenciais (email + senha)
|
||||||
|
4. ✅ Paciente faz login em `/login-paciente`
|
||||||
|
5. ✅ Login funciona! Token JWT é gerado
|
||||||
|
6. ✅ Sistema busca dados do paciente pelo email
|
||||||
|
7. ✅ Paciente acessa área `/paciente` com todos os dados
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Data da Implementação
|
||||||
|
|
||||||
|
3 de outubro de 2025
|
||||||
|
|
||||||
|
## 🔗 Arquivos Modificados
|
||||||
|
|
||||||
|
- ✅ `susconecta/lib/api.ts` - Funções de criação (Supabase Auth)
|
||||||
|
- ✅ `susconecta/components/forms/patient-registration-form.tsx` - Já integrado
|
||||||
|
- ✅ `susconecta/components/forms/doctor-registration-form.tsx` - Já integrado
|
||||||
|
- ✅ `susconecta/components/credentials-dialog.tsx` - Já implementado
|
||||||
74
eslint.config.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// eslint.config.js
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import nextPlugin from '@next/eslint-plugin-next';
|
||||||
|
import unicornPlugin from 'eslint-plugin-unicorn';
|
||||||
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
'@next/next': nextPlugin,
|
||||||
|
unicorn: unicornPlugin,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
parser: tseslint.parser,
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...nextPlugin.configs.recommended.rules,
|
||||||
|
...nextPlugin.configs['core-web-vitals'].rules,
|
||||||
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/prefer-string-replace-all': 'off',
|
||||||
|
'unicorn/prefer-string-slice': 'off',
|
||||||
|
'unicorn/prefer-number-properties': 'off',
|
||||||
|
'unicorn/no-array-reduce': 'off',
|
||||||
|
'unicorn/no-array-for-each': 'off',
|
||||||
|
'unicorn/prefer-global-this': 'off',
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
|
'unicorn/explicit-length-check': 'off',
|
||||||
|
'unicorn/consistent-existence-index-check': 'off',
|
||||||
|
'unicorn/prefer-ternary': 'off',
|
||||||
|
'unicorn/numeric-separators-style': 'off',
|
||||||
|
'unicorn/filename-case': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
camelCase: true,
|
||||||
|
pascalCase: true,
|
||||||
|
kebabCase: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'unicorn/prefer-add-event-listener': 'off',
|
||||||
|
'unicorn/prefer-spread': 'off',
|
||||||
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
|
'unicorn/no-document-cookie': 'off',
|
||||||
|
'unicorn/no-negated-condition': 'off',
|
||||||
|
'unicorn/prefer-code-point': 'off',
|
||||||
|
'unicorn/prefer-single-call': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'prefer-const': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettierConfig,
|
||||||
|
];
|
||||||
9470
package-lock.json
generated
94
package.json
@ -1,9 +1,93 @@
|
|||||||
{
|
{
|
||||||
|
"name": "my-v0-project",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev": "next dev",
|
||||||
|
"lint": "next lint",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.7",
|
"@fullcalendar/core": "^6.1.19",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@fullcalendar/daygrid": "^6.1.19",
|
||||||
"date-fns": "^4.1.0",
|
"@fullcalendar/interaction": "^6.1.19",
|
||||||
"react-big-calendar": "^1.19.4",
|
"@fullcalendar/react": "^6.1.19",
|
||||||
"react-signature-canvas": "^1.1.0-alpha.2"
|
"@fullcalendar/timegrid": "^6.1.19",
|
||||||
|
"@hookform/resolvers": "^3.10.0",
|
||||||
|
"@radix-ui/react-accordion": "latest",
|
||||||
|
"@radix-ui/react-alert-dialog": "latest",
|
||||||
|
"@radix-ui/react-aspect-ratio": "latest",
|
||||||
|
"@radix-ui/react-avatar": "latest",
|
||||||
|
"@radix-ui/react-checkbox": "latest",
|
||||||
|
"@radix-ui/react-collapsible": "latest",
|
||||||
|
"@radix-ui/react-context-menu": "latest",
|
||||||
|
"@radix-ui/react-dialog": "latest",
|
||||||
|
"@radix-ui/react-dropdown-menu": "latest",
|
||||||
|
"@radix-ui/react-hover-card": "latest",
|
||||||
|
"@radix-ui/react-label": "latest",
|
||||||
|
"@radix-ui/react-menubar": "latest",
|
||||||
|
"@radix-ui/react-navigation-menu": "latest",
|
||||||
|
"@radix-ui/react-popover": "latest",
|
||||||
|
"@radix-ui/react-progress": "latest",
|
||||||
|
"@radix-ui/react-radio-group": "latest",
|
||||||
|
"@radix-ui/react-scroll-area": "latest",
|
||||||
|
"@radix-ui/react-select": "latest",
|
||||||
|
"@radix-ui/react-separator": "latest",
|
||||||
|
"@radix-ui/react-slider": "latest",
|
||||||
|
"@radix-ui/react-slot": "latest",
|
||||||
|
"@radix-ui/react-switch": "latest",
|
||||||
|
"@radix-ui/react-tabs": "latest",
|
||||||
|
"@radix-ui/react-toast": "latest",
|
||||||
|
"@radix-ui/react-toggle": "latest",
|
||||||
|
"@radix-ui/react-toggle-group": "latest",
|
||||||
|
"@radix-ui/react-tooltip": "latest",
|
||||||
|
"@vercel/analytics": "1.3.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "latest",
|
||||||
|
"date-fns": "4.1.0",
|
||||||
|
"embla-carousel-react": "latest",
|
||||||
|
"geist": "^1.3.1",
|
||||||
|
"input-otp": "latest",
|
||||||
|
"jspdf": "^3.0.3",
|
||||||
|
"lucide-react": "^0.454.0",
|
||||||
|
"next-themes": "latest",
|
||||||
|
"react": "^18",
|
||||||
|
"react-day-picker": "latest",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-hook-form": "latest",
|
||||||
|
"react-quill": "^2.0.0",
|
||||||
|
"react-resizable-panels": "latest",
|
||||||
|
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||||
|
"recharts": "latest",
|
||||||
|
"sonner": "latest",
|
||||||
|
"tailwind-merge": "^2.5.5",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"vaul": "latest",
|
||||||
|
"zod": "3.25.67"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@next/eslint-plugin-next": "^15.5.4",
|
||||||
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.45.0",
|
||||||
|
"@typescript-eslint/parser": "^8.45.0",
|
||||||
|
"eslint": "^9.36.0",
|
||||||
|
"eslint-config-next": "^15.5.4",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-unicorn": "^61.0.2",
|
||||||
|
"globals": "^16.4.0",
|
||||||
|
"next": "^15.5.4",
|
||||||
|
"postcss": "^8.5",
|
||||||
|
"tailwindcss": "^4.1.9",
|
||||||
|
"tw-animate-css": "1.3.3",
|
||||||
|
"typescript": "^5",
|
||||||
|
"typescript-eslint": "^8.45.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
susconecta/pnpm-lock.yaml → pnpm-lock.yaml
generated
@ -186,6 +186,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.36.0
|
specifier: ^9.36.0
|
||||||
version: 9.36.0
|
version: 9.36.0
|
||||||
|
'@next/eslint-plugin-next':
|
||||||
|
specifier: ^15.5.4
|
||||||
|
version: 15.5.4
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.9
|
specifier: ^4.1.9
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
@ -216,6 +219,9 @@ importers:
|
|||||||
eslint-plugin-unicorn:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^61.0.2
|
specifier: ^61.0.2
|
||||||
version: 61.0.2(eslint@9.36.0(jiti@2.5.1))
|
version: 61.0.2(eslint@9.36.0(jiti@2.5.1))
|
||||||
|
globals:
|
||||||
|
specifier: ^16.4.0
|
||||||
|
version: 16.4.0
|
||||||
next:
|
next:
|
||||||
specifier: ^15.5.4
|
specifier: ^15.5.4
|
||||||
version: 15.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
Before Width: | Height: | Size: 801 KiB After Width: | Height: | Size: 801 KiB |
|
Before Width: | Height: | Size: 8.3 MiB After Width: | Height: | Size: 8.3 MiB |
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
@ -8,8 +8,6 @@ import dayGridPlugin from "@fullcalendar/daygrid";
|
|||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import { EventInput } from "@fullcalendar/core/index.js";
|
import { EventInput } from "@fullcalendar/core/index.js";
|
||||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
|
||||||
import { PagesHeader } from "@/components/dashboard/header";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
mockAppointments,
|
mockAppointments,
|
||||||
@ -54,7 +54,7 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks";
|
import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks";
|
||||||
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
|
import { CalendarRegistrationForm } from "@/features/agendamento/components/forms/calendar-registration-form";
|
||||||
|
|
||||||
|
|
||||||
const formatDate = (date: string | Date) => {
|
const formatDate = (date: string | Date) => {
|
||||||
@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
|
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
|
import { DoctorRegistrationForm } from "@/features/profissionais/components/forms/doctor-registration-form";
|
||||||
|
|
||||||
|
|
||||||
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
|
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/layout/ProtectedRoute";
|
||||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
import { Sidebar } from "@/components/layout/app/Sidebar";
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { PagesHeader } from "@/components/dashboard/header";
|
import { PagesHeader } from "@/components/layout/app/Header";
|
||||||
|
|
||||||
export default function MainRoutesLayout({
|
export default function MainRoutesLayout({
|
||||||
children,
|
children,
|
||||||
@ -11,7 +11,7 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
|
import { MoreHorizontal, Plus, Search, Eye, Edit, Trash2, ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
import { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
|
||||||
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
|
import { PatientRegistrationForm } from "@/features/pacientes/components/forms/patient-registration-form";
|
||||||
|
|
||||||
|
|
||||||
function normalizePaciente(p: any): Paciente {
|
function normalizePaciente(p: any): Paciente {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/layout/marketing/Header"
|
||||||
import { AboutSection } from "@/components/about-section"
|
import { AboutSection } from "@/features/marketing/components/about-section"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/layout/marketing/Footer"
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return (
|
return (
|
||||||
11
src/app/(paciente)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import ProtectedRoute from '@/components/layout/ProtectedRoute'
|
||||||
|
|
||||||
|
export default function PacienteLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requiredUserType={['paciente']}>
|
||||||
|
{children}
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
92
src/app/(paciente)/paciente/page.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
'use client'
|
||||||
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { User, LogOut, Home } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function PacientePage() {
|
||||||
|
const { logout, user } = useAuth()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
console.log('[PACIENTE] Iniciando logout...')
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||||||
|
<Card className="w-full max-w-md shadow-lg">
|
||||||
|
<CardHeader className="text-center">
|
||||||
|
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<User className="h-8 w-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<CardTitle className="text-2xl font-bold text-gray-900">
|
||||||
|
Portal do Paciente
|
||||||
|
</CardTitle>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Bem-vindo ao seu espaço pessoal
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Informações do Paciente */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-2">
|
||||||
|
Maria Silva Santos
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
CPF: 123.456.789-00
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Idade: 35 anos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Informações do Login */}
|
||||||
|
<div className="bg-gray-100 rounded-lg p-4">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
|
Conectado como:
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-gray-800">
|
||||||
|
{user?.email || 'paciente@example.com'}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
Tipo de usuário: Paciente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botão Voltar ao Início */}
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="outline"
|
||||||
|
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<Link href="/">
|
||||||
|
<Home className="h-4 w-4" />
|
||||||
|
Voltar ao Início
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Botão de Logout */}
|
||||||
|
<Button
|
||||||
|
onClick={handleLogout}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full flex items-center justify-center gap-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
<LogOut className="h-4 w-4" />
|
||||||
|
Sair
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Informação adicional */}
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Em breve, mais funcionalidades estarão disponíveis
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/app/(profissional)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import ProtectedRoute from '@/components/layout/ProtectedRoute'
|
||||||
|
|
||||||
|
export default function ProfissionalLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requiredUserType={['profissional']}>
|
||||||
|
{children}
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import SignatureCanvas from "react-signature-canvas";
|
import SignatureCanvas from "react-signature-canvas";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { buscarPacientes } from "@/lib/api";
|
import { buscarPacientes } from "@/lib/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -3251,8 +3250,7 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredUserType={["profissional"]}>
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<header className="bg-card shadow-md rounded-lg border border-border p-4 mb-6 flex items-center justify-between">
|
<header className="bg-card shadow-md rounded-lg border border-border p-4 mb-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-12 w-12">
|
<Avatar className="h-12 w-12">
|
||||||
@ -3510,7 +3508,6 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
3514
src/app/(profissional)/profissional/page.tsx
Normal file
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
|
import { CalendarRegistrationForm } from "@/features/agendamento/components/forms/calendar-registration-form";
|
||||||
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
|
||||||
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
import FooterAgenda from "@/components/agenda/FooterAgenda";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@ -56,6 +56,7 @@ export default function NovoAgendamentoPage() {
|
|||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { AuthProvider } from "@/hooks/useAuth"
|
import { AuthProvider } from "@/features/autenticacao/hooks/useAuth"
|
||||||
import { ThemeProvider } from "@/components/theme-provider"
|
import { ThemeProvider } from "@/components/layout/ThemeProvider"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
206
src/app/login/page.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState, type ChangeEvent, type FormEvent } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { useAuth } from '@/features/autenticacao/hooks/useAuth'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
|
import { AuthenticationError } from '@/lib/auth'
|
||||||
|
import { AUTH_STORAGE_KEYS } from '@/features/autenticacao/types'
|
||||||
|
|
||||||
|
type UserRole = 'profissional' | 'paciente' | 'administrador'
|
||||||
|
|
||||||
|
const USER_ROLES: readonly UserRole[] = ['profissional', 'paciente', 'administrador'] as const
|
||||||
|
|
||||||
|
const roleConfig: Record<UserRole, {
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
redirect: string
|
||||||
|
cta: string
|
||||||
|
}> = {
|
||||||
|
profissional: {
|
||||||
|
title: 'Login Profissional de Saúde',
|
||||||
|
subtitle: 'Entre com suas credenciais para acessar o sistema clínico',
|
||||||
|
redirect: '/profissional',
|
||||||
|
cta: 'Entrar como Profissional'
|
||||||
|
},
|
||||||
|
paciente: {
|
||||||
|
title: 'Sou Paciente',
|
||||||
|
subtitle: 'Acesse sua área pessoal e gerencie suas consultas',
|
||||||
|
redirect: '/paciente',
|
||||||
|
cta: 'Entrar na Minha Área'
|
||||||
|
},
|
||||||
|
administrador: {
|
||||||
|
title: 'Login Administrativo',
|
||||||
|
subtitle: 'Gerencie agendas, pacientes e finanças da clínica',
|
||||||
|
redirect: '/dashboard',
|
||||||
|
cta: 'Entrar no Painel Administrativo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserRole = (value: string | null): value is UserRole => {
|
||||||
|
if (!value) return false
|
||||||
|
return USER_ROLES.includes(value as UserRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [selectedRole, setSelectedRole] = useState<UserRole>('profissional')
|
||||||
|
const { login } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const roleParam = searchParams.get('role')
|
||||||
|
if (isUserRole(roleParam)) {
|
||||||
|
setSelectedRole(roleParam)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, roleParam)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const storedRole = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||||
|
if (isUserRole(storedRole)) {
|
||||||
|
setSelectedRole(storedRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const { title, subtitle, redirect, cta } = useMemo(() => roleConfig[selectedRole], [selectedRole])
|
||||||
|
|
||||||
|
const handleSelectRole = (role: UserRole) => {
|
||||||
|
setSelectedRole(role)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, role)
|
||||||
|
}
|
||||||
|
router.replace(`/login?role=${role}`, { scroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await login(credentials.email, credentials.password, selectedRole)
|
||||||
|
if (success) {
|
||||||
|
router.push(redirect)
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof AuthenticationError) {
|
||||||
|
setError(err.message)
|
||||||
|
} else {
|
||||||
|
setError('Erro inesperado. Tente novamente.')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="max-w-md w-full space-y-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-center">Escolha como deseja entrar</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-6">
|
||||||
|
{USER_ROLES.map((role) => {
|
||||||
|
const isActive = role === selectedRole
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={role}
|
||||||
|
type="button"
|
||||||
|
variant={isActive ? 'default' : 'outline'}
|
||||||
|
className={`w-full text-sm h-auto py-3 px-4 transition-all ${isActive ? 'shadow-md' : 'hover:bg-primary/5'}`}
|
||||||
|
onClick={() => handleSelectRole(role)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{roleConfig[role].title}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleLogin} className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Digite seu email"
|
||||||
|
value={credentials.email}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setCredentials({ ...credentials, email: event.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
||||||
|
Senha
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Digite sua senha"
|
||||||
|
value={credentials.password}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setCredentials({ ...credentials, password: event.target.value })
|
||||||
|
}
|
||||||
|
required
|
||||||
|
className="mt-1"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full cursor-pointer" disabled={loading}>
|
||||||
|
{loading ? 'Entrando...' : cta}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
asChild
|
||||||
|
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
|
||||||
|
>
|
||||||
|
<Link href="/">
|
||||||
|
Voltar ao Início
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/layout/marketing/Header"
|
||||||
import { HeroSection } from "@/components/hero-section"
|
import { HeroSection } from "@/features/marketing/components/hero-section"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/layout/marketing/Footer"
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
@ -2,8 +2,8 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import type { UserType } from '@/types/auth'
|
import type { UserType } from '@/features/autenticacao/types'
|
||||||
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
|
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/features/autenticacao/types'
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -1,12 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Bell, ChevronDown } from "lucide-react"
|
import { Bell } from "lucide-react"
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { SidebarTrigger } from "../ui/sidebar"
|
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||||
|
|
||||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||||
const { logout, user } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import {
|
import {
|
||||||
Sidebar as ShadSidebar,
|
Sidebar as ShadSidebar,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
@ -72,22 +71,23 @@ export function Sidebar() {
|
|||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{navigation.map((item) => {
|
{navigation.map((item) => {
|
||||||
const isActive = pathname === item.href ||
|
const isActive =
|
||||||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
|
pathname === item.href ||
|
||||||
|
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={item.name}>
|
<SidebarMenuItem key={item.name}>
|
||||||
<SidebarMenuButton asChild isActive={isActive}>
|
<SidebarMenuButton asChild isActive={isActive}>
|
||||||
<Link href={item.href} className="flex items-center">
|
<Link href={item.href} className="flex items-center">
|
||||||
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
||||||
<span className="truncate group-data-[collapsible=icon]:hidden">
|
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
@ -12,10 +12,8 @@ export function Footer() {
|
|||||||
<footer className="bg-background border-t border-border">
|
<footer className="bg-background border-t border-border">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
||||||
{}
|
|
||||||
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
|
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<nav className="flex items-center space-x-8">
|
<nav className="flex items-center space-x-8">
|
||||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||||
Termos
|
Termos
|
||||||
@ -28,7 +26,6 @@ export function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
import { SimpleThemeToggle } from "@/components/layout/SimpleThemeToggle";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@ -22,7 +22,6 @@ export function Header() {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{}
|
|
||||||
<nav className="hidden md:flex items-center gap-10">
|
<nav className="hidden md:flex items-center gap-10">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@ -42,31 +41,30 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="hidden md:flex items-center space-x-3">
|
<div className="hidden md:flex items-center space-x-3">
|
||||||
<SimpleThemeToggle />
|
<SimpleThemeToggle />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href="/login?role=paciente">Sou Paciente</Link>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
|
<Button
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=administrador">Sou Administrador de uma Clínica</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/login-admin">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground cursor-pointer"
|
|
||||||
>
|
|
||||||
Sou Administrador de uma Clínica
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
<button
|
||||||
className="md:hidden p-2"
|
className="md:hidden p-2"
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
@ -76,7 +74,6 @@ export function Header() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="md:hidden py-4 border-t border-border">
|
<div className="md:hidden py-4 border-t border-border">
|
||||||
<nav className="flex flex-col space-y-4">
|
<nav className="flex flex-col space-y-4">
|
||||||
@ -98,22 +95,24 @@ export function Header() {
|
|||||||
<SimpleThemeToggle />
|
<SimpleThemeToggle />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
<Link href="/login?role=paciente">Sou Paciente</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
|
<Button
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
className="bg-primary hover:bg-primary/90 text-primary-foreground w-full shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-primary bg-transparent w-full shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=administrador">Sou Administrador de uma Clínica</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/login-admin">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-primary border-primary bg-transparent w-full shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground cursor-pointer"
|
|
||||||
>
|
|
||||||
Sou Administrador de uma Clínica
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||