Compare commits

...

4 Commits

Author SHA1 Message Date
b0ab1e86ca refactor(core): Restructure the projectfor feature-sliced architecture feature-sliced
- Move components, hooks, and types to their respective directories in /features.
  - Update all imports to reflect the new structure.
  - Fixes the path alias in tsconfig and removes legacy files.
2025-10-07 17:23:02 -03:00
498d6c80c1 feature(refactor): reorganize project structure and routes 2025-10-07 05:26:13 -03:00
b50050a545 feat: Refactor and modularize the API layer
- The API logic for Patients and
   Professionals has been extracted to the
   new src/features folder.
  - The patient API functions
  have been refactored to use the
  httpClient.
  - Types have been centralized by
  feature for better organization.
2025-10-07 01:19:08 -03:00
6aef2b4910 chore: Reorganize project file structure. 2025-10-06 23:44:04 -03:00
140 changed files with 14337 additions and 11226 deletions

29
.gitignore vendored
View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 801 KiB

After

Width:  |  Height:  |  Size: 801 KiB

View File

Before

Width:  |  Height:  |  Size: 8.3 MiB

After

Width:  |  Height:  |  Size: 8.3 MiB

View File

Before

Width:  |  Height:  |  Size: 568 B

After

Width:  |  Height:  |  Size: 568 B

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

@ -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) => {

View File

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

View File

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

View File

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

View File

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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

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

File diff suppressed because it is too large Load Diff

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More