Compare commits

..

12 Commits

Author SHA1 Message Date
107bba89d8 chore: save work 2025-10-08 20:56:35 -03:00
João Gustavo
0a7d3f3ae4 fix-laudos-module 2025-10-07 02:56:13 -03:00
João Gustavo
a43fdcc655 add-patient-page 2025-10-07 02:35:49 -03:00
João Gustavo
d7fbcab6a6 Merge branch 'develop' into feature/add-report-endpoints 2025-10-07 00:22:58 -03:00
João Gustavo
40bf3e0e6f fix-report-page 2025-10-07 00:16:39 -03:00
João Gustavo
a994a70d90 atualizing-api.ts 2025-10-06 23:24:57 -03:00
8955446bc7 Merge pull request 'feature/api-usuarios-perfis' (#40) from feature/api-usuarios-perfis into develop
Reviewed-on: #40
2025-10-07 01:51:25 +00:00
11c8b790ba Merge branch 'develop' into feature/api-usuarios-perfis 2025-10-06 22:47:22 -03:00
0510ef8a36 feat(api):add profile API 2025-10-06 02:35:21 -03:00
8284ccbadd Merge branch 'develop' into feature/api-usuarios-perfis 2025-10-04 23:44:02 -03:00
63e5a2ca9d chore(eslint): Configure and adjust
ESLint rules for the project
2025-10-03 14:38:54 -03:00
20f7d79474 feat: add email confirmation on user registration
Implements automatic creation in Supabase Auth with mandatory
email confirmation. Adds credentials popup and clear messages
about the confirmation process.

BREAKING CHANGE: Users must confirm email before login
2025-10-03 04:42:24 -03:00
181 changed files with 13439 additions and 14527 deletions

29
.gitignore vendored
View File

@ -1,29 +1,2 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
node_modules/
# 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,3 @@
{
"pages": {}
}

17
.next/build-manifest.json Normal file
View File

@ -0,0 +1,17 @@
{
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
"static/development/_ssgManifest.js"
],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
}

1
.next/cache/.previewinfo vendored Normal file
View File

@ -0,0 +1 @@
{"previewModeId":"ef42988a66c7facb8cacc41264c816a3","previewModeSigningKey":"a79be007b8fc9844687acde60b0aaa42535a074e1b398db709f1ba36bb394a70","previewModeEncryptionKey":"581405036cabc30bb42611700f8f34afae8b4345617d5f849f74468b1d1b38d5","expireAt":1761141052491}

1
.next/cache/.rscinfo vendored Normal file
View File

@ -0,0 +1 @@
{"encryption.key":"1+xyFz3ckt+qN0qpo15cccDdyvzpv2iYyAOjplkpmUQ=","encryption.expire_at":1761141052419}

1
.next/cache/next-devtools-config.json vendored Normal file
View File

@ -0,0 +1 @@
{}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
.next/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "commonjs"}

View File

@ -0,0 +1,11 @@
{
"version": 4,
"routes": {},
"dynamicRoutes": {},
"notFoundRoutes": [],
"preview": {
"previewModeId": "3a416f4d36dff8983ece0b1d649076fb",
"previewModeSigningKey": "5d2b597ca7226d41745e16f10542be9669d399be31d9ddb1d7d8740428afe5a2",
"previewModeEncryptionKey": "77da2f1b974f25583b0b427b5d5575be062291801610be0e6467ad97df7aaba3"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{"version":3,"caseSensitive":false,"basePath":"","rewrites":{"beforeFiles":[],"afterFiles":[],"fallback":[]},"redirects":[{"source":"/:path+/","destination":"/:path+","permanent":true,"internal":true,"regex":"^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$"}],"headers":[]}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@ -0,0 +1,19 @@
globalThis.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
};
globalThis.__BUILD_MANIFEST.lowPriorityFiles = [
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
];

View File

@ -0,0 +1,6 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@ -0,0 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@ -0,0 +1 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"

View File

@ -0,0 +1 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"

View File

@ -0,0 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "1+xyFz3ckt+qN0qpo15cccDdyvzpv2iYyAOjplkpmUQ="
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
self.__BUILD_MANIFEST = (function(a){return {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:a,__routerFilterDynamic:a,sortedPages:["\u002F_app"]}}(void 0));self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

View File

@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

4
.next/trace Normal file

File diff suppressed because one or more lines are too long

141
.next/types/cache-life.d.ts vendored Normal file
View File

@ -0,0 +1,141 @@
// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
revalidateTag,
revalidatePath,
unstable_expireTag,
unstable_expirePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "default"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 30 seconds
* revalidate: 1 seconds
* expire: 60 seconds (1 minute)
* ```
*
* This cache may be stale on clients for 30 seconds before checking with the server.
* If the server receives a new request after 1 seconds, start revalidating new values in the background.
* If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "seconds"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "minutes"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "hours"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "days"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (30 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 30 days it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "weeks"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (30 days)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 30 days, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "max"): void
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function unstable_cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number,
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number,
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number
}): void
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
}

1
.next/types/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "module"}

55
.next/types/routes.d.ts vendored Normal file
View File

@ -0,0 +1,55 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
type AppRoutes = never
type PageRoutes = never
type LayoutRoutes = never
type RedirectRoutes = never
type RewriteRoutes = never
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
interface ParamMap {
}
export type ParamsOf<Route extends Routes> = ParamMap[Route]
interface LayoutSlotMap {
}
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
declare global {
/**
* Props for Next.js App Router page components
* @example
* ```tsx
* export default function Page(props: PageProps<'/blog/[slug]'>) {
* const { slug } = await props.params
* return <div>Blog post: {slug}</div>
* }
* ```
*/
interface PageProps<AppRoute extends AppRoutes> {
params: Promise<ParamMap[AppRoute]>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
/**
* Props for Next.js App Router layout components
* @example
* ```tsx
* export default function Layout(props: LayoutProps<'/dashboard'>) {
* return <div>{props.children}</div>
* }
* ```
*/
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
params: Promise<ParamMap[LayoutRoute]>
children: React.ReactNode
} & {
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
}
}

16
.next/types/validator.ts Normal file
View File

@ -0,0 +1,16 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
// This file validates that all pages and layouts export the correct types

View File

@ -1,370 +0,0 @@
# ✅ 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

View File

@ -1,74 +0,0 @@
// 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,
];

9430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,93 +1,9 @@
{
"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": {
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/react": "^6.1.19",
"@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"
"@headlessui/react": "^2.2.7",
"@heroicons/react": "^2.2.0",
"date-fns": "^4.1.0",
"react-big-calendar": "^1.19.4",
"react-signature-canvas": "^1.1.0-alpha.2"
}
}

View File

@ -1,88 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { FileDown } from "lucide-react";
import jsPDF from "jspdf";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
export default function RelatoriosPage() {
// Dados fictícios para o gráfico financeiro
const financeiro = [
{ mes: "Jan", faturamento: 35000, despesas: 12000 },
{ mes: "Fev", faturamento: 29000, despesas: 15000 },
{ mes: "Mar", faturamento: 42000, despesas: 18000 },
{ mes: "Abr", faturamento: 38000, despesas: 14000 },
{ mes: "Mai", faturamento: 45000, despesas: 20000 },
{ mes: "Jun", faturamento: 41000, despesas: 17000 },
];
// ============================
// PASSO 3 - Funções de exportar
// ============================
const exportConsultasPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Consultas", 10, 10);
doc.text("Resumo das consultas realizadas.", 10, 20);
doc.save("relatorio-consultas.pdf");
};
const exportPacientesPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Pacientes", 10, 10);
doc.text("Informações gerais dos pacientes cadastrados.", 10, 20);
doc.save("relatorio-pacientes.pdf");
};
const exportFinanceiroPDF = () => {
const doc = new jsPDF();
doc.text("Relatório Financeiro", 10, 10);
doc.text("Receitas e despesas da clínica.", 10, 20);
doc.save("relatorio-financeiro.pdf");
};
return (
<div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-foreground">Relatórios</h1>
<div className="grid grid-cols-3 gap-6">
{/* Card Consultas */}
<div className="p-4 border border-border rounded-lg shadow bg-card">
<h2 className="font-semibold text-lg text-foreground">Relatório de Consultas</h2>
<p className="text-sm text-muted-foreground">Resumo das consultas realizadas.</p>
{/* PASSO 4 - Botão chama a função */}
<Button onClick={exportConsultasPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
</div>
{/* Card Pacientes */}
<div className="p-4 border border-border rounded-lg shadow bg-card">
<h2 className="font-semibold text-lg text-foreground">Relatório de Pacientes</h2>
<p className="text-sm text-muted-foreground">Informações gerais dos pacientes cadastrados.</p>
<Button onClick={exportPacientesPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
</div>
{/* Card Financeiro com gráfico */}
<div className="p-4 border border-border rounded-lg shadow col-span-3 md:col-span-3 bg-card">
<h2 className="font-semibold text-lg mb-2 text-foreground">Relatório Financeiro</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={financeiro} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="faturamento" fill="#10b981" name="Faturamento" />
<Bar dataKey="despesas" fill="#ef4444" name="Despesas" />
</BarChart>
</ResponsiveContainer>
<Button onClick={exportFinanceiroPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
</div>
</div>
</div>
);
}

View File

@ -1,11 +0,0 @@
'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

@ -1,92 +0,0 @@
'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

@ -1,11 +0,0 @@
'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>
)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,206 +0,0 @@
'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,181 +0,0 @@
import httpClient from "@/lib/http";
import { API_KEY } from "@/lib/config"; // Necessário para algumas chamadas de auth
import { ENV_CONFIG } from "@/lib/env-config"; // Configuração de ambiente completa
import { API_KEY } from "@/lib/config"; // Necessário para algumas chamadas de auth
// ===== TIPOS DE AUTENTICAÇÃO E PERFIL =====
export type Profile = {
id: string;
full_name: string;
email: string;
// Adicione outros campos do perfil aqui conforme necessário
};
export type CreateUserWithPasswordResponse = {
success: boolean;
user: any; // Objeto do usuário retornado pelo Supabase
email: string;
password?: string;
};
export type UserData = {
id: string;
email: string;
full_name?: string;
phone_mobile?: string;
// ... outros campos do usuário
};
export type LoginResponse = {
access_token: string;
refresh_token: string;
user: UserData;
};
export type RefreshTokenResponse = {
access_token: string;
refresh_token: string;
};
// ===== PERFIS DE USUÁRIOS =====
export async function listarPerfis(): Promise<Profile[]> {
// O endpoint original usava um mock.apidog.com. Vamos assumir que o endpoint real seria /rest/v1/profiles
// ou que o httpClient já está configurado para o Supabase.
const url = `/rest/v1/profiles`; // Usando caminho relativo para o httpClient
const res = await httpClient.get(url);
if (!res.ok) {
throw new Error(`Erro ao listar perfis: ${res.statusText}`);
}
return res.json();
}
// ===== CRIAÇÃO DE USUÁRIOS (PACIENTE/MÉDICO) =====
// Assumindo que o Supabase Auth tem um endpoint de signup ou que a criação é feita via API de admin
// Este é um placeholder. A implementação exata pode variar.
export async function criarUsuarioPaciente(input: {
email: string;
full_name: string;
phone_mobile?: string;
}): Promise<CreateUserWithPasswordResponse> {
// Endpoint de signup do Supabase Auth
const url = AUTH_ENDPOINTS.SIGNUP || `/auth/v1/signup`; // Usar AUTH_ENDPOINTS se definido, senão um padrão
const res = await httpClient.post(url, {
email: input.email,
password: Math.random().toString(36).slice(-8), // Gerar senha temporária
data: { full_name: input.full_name, phone_mobile: input.phone_mobile, role: 'paciente' },
}, {
headers: {
'apikey': API_KEY, // API Key é necessária para signup direto no Supabase Auth
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao criar usuário paciente: ${error.message}`);
}
const data = await res.json();
return { user_id: data.user.id, email: data.user.email, password: data.password };
}
export async function criarUsuarioMedico(input: {
email: string;
full_name: string;
phone_mobile?: string;
}): Promise<CreateUserWithPasswordResponse> {
const url = AUTH_ENDPOINTS.SIGNUP || `/auth/v1/signup`;
const res = await httpClient.post(url, {
email: input.email,
password: Math.random().toString(36).slice(-8), // Gerar senha temporária
data: { full_name: input.full_name, phone_mobile: input.phone_mobile, role: 'medico' },
}, {
headers: {
'apikey': API_KEY,
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao criar usuário médico: ${error.message}`);
}
const data = await res.json();
return { user_id: data.user.id, email: data.user.email, password: data.password };
}
// ===== FUNÇÕES DE AUTENTICAÇÃO PRINCIPAIS (MOVIDAS DE auth.ts) =====
// Estas funções serão movidas de src/lib/auth.ts para cá
export async function loginUser(
email: string,
password: string,
userType: 'profissional' | 'paciente' | 'administrador'
): Promise<LoginResponse> {
const url = AUTH_ENDPOINTS.LOGIN;
const payload = { email, password };
const res = await httpClient.post(url, payload, {
headers: {
'apikey': API_KEY,
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro no login: ${error.message}`);
}
const data = await res.json();
return { access_token: data.access_token, refresh_token: data.refresh_token, user: data.user };
}
export async function logoutUser(token: string): Promise<void> {
const url = AUTH_ENDPOINTS.LOGOUT;
const res = await httpClient.post(url, null, {
headers: {
'Authorization': `Bearer ${token}`,
'apikey': API_KEY,
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro no logout: ${error.message}`);
}
}
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
const url = AUTH_ENDPOINTS.REFRESH;
const res = await httpClient.post(url, { refresh_token: refreshToken }, {
headers: {
'apikey': API_KEY,
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao renovar token: ${error.message}`);
}
const data = await res.json();
return { access_token: data.access_token, refresh_token: data.refresh_token };
}
export async function getCurrentUser(token: string): Promise<UserData> {
const url = AUTH_ENDPOINTS.USER;
const res = await httpClient.get(url, {
headers: {
'Authorization': `Bearer ${token}`,
'apikey': API_KEY,
}
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao obter usuário atual: ${error.message}`);
}
const data = await res.json();
return data.user; // Assumindo que a resposta tem um objeto 'user'
}

View File

@ -1,179 +0,0 @@
// src/features/pacientes/api/index.ts
import httpClient from "@/lib/http";
import type { Paciente, PacienteInput } from "@/features/pacientes/types";
// A constante `REST` foi removida para usar a baseURL do httpClient.
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
if (!page || !limit) return {};
const start = (page - 1) * limit;
const end = start + limit - 1;
return { Range: `${start}-${end}`, "Range-Unit": "items" };
}
// ===== PACIENTES (CRUD) =====
export async function listarPacientes(params?: {
page?: number;
limit?: number;
q?: string;
}): Promise<Paciente[]> {
const qs = new URLSearchParams();
if (params?.q) qs.set("q", params.q);
// Utiliza caminho relativo, o httpClient adicionará a base.
const url = `/rest/v1/patients${qs.toString() ? `?${qs.toString()}` : ""}`;
const res = await httpClient.get(url, {
headers: {
...rangeHeaders(params?.page, params?.limit),
},
});
if (!res.ok) {
throw new Error(`Erro ao listar pacientes: ${res.statusText}`);
}
return res.json();
}
export async function buscarPacientes(termo: string): Promise<Paciente[]> {
if (!termo || termo.trim().length < 2) {
return [];
}
const searchTerm = termo.toLowerCase().trim();
const digitsOnly = searchTerm.replace(/\D/g, "");
const queries = [];
if (searchTerm.includes("-") && searchTerm.length > 10) {
queries.push(`id=eq.${searchTerm}`);
}
if (digitsOnly.length >= 11) {
queries.push(`cpf=eq.${digitsOnly}`);
} else if (digitsOnly.length >= 3) {
queries.push(`cpf=ilike.*${digitsOnly}*`);
}
if (searchTerm.length >= 2) {
queries.push(`full_name=ilike.*${searchTerm}*`);
queries.push(`social_name=ilike.*${searchTerm}*`);
}
if (searchTerm.includes("@")) {
queries.push(`email=ilike.*${searchTerm}*`);
}
const results: Paciente[] = [];
const seenIds = new Set<string>();
for (const query of queries) {
try {
const url = `/rest/v1/patients?${query}&limit=10`;
const res = await httpClient.get(url);
const arr: Paciente[] = await res.json();
if (arr?.length > 0) {
for (const paciente of arr) {
if (!seenIds.has(paciente.id)) {
seenIds.add(paciente.id);
results.push(paciente);
}
}
}
} catch (error) {
console.warn(`Erro na busca com query: ${query}`, error);
}
}
return results.slice(0, 20);
}
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
const url = `/rest/v1/patients?id=eq.${id}&limit=1`;
const res = await httpClient.get(url);
if (!res.ok) {
throw new Error("404: Paciente não encontrado");
}
const arr: Paciente[] = await res.json();
if (!arr?.length) throw new Error("404: Paciente não encontrado");
return arr[0];
}
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
const url = `/rest/v1/patients`;
const res = await httpClient.post(url, input, {
headers: {
Prefer: "return=representation",
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao criar paciente: ${error.message}`);
}
const arr: Paciente[] = await res.json();
return arr[0];
}
export async function atualizarPaciente(
id: string | number,
input: Partial<PacienteInput>
): Promise<Paciente> {
const url = `/rest/v1/patients?id=eq.${id}`;
const res = await httpClient.patch(url, input, {
headers: {
Prefer: "return=representation",
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao atualizar paciente: ${error.message}`);
}
const arr: Paciente[] = await res.json();
return arr[0];
}
export async function excluirPaciente(id: string | number): Promise<void> {
const url = `/rest/v1/patients?id=eq.${id}`;
const res = await httpClient.delete(url);
if (!res.ok && res.status !== 204) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao excluir paciente: ${error.message}`);
}
}
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
const clean = (cpf || "").replace(/\D/g, "");
if (!clean) return false;
const url = `/rest/v1/patients?cpf=eq.${clean}&select=id`;
const res = await httpClient.get(url);
if (!res.ok) {
console.error("Erro ao verificar CPF:", res.statusText);
return false;
}
const data: any[] = await res.json().catch(() => []);
return Array.isArray(data) && data.length > 0;
}
// Funções "stub" para pacientes
export const uploadFotoPacienteAPI = async (
pacienteId: string,
foto: File
) => {
console.log(`Upload da foto do paciente ${pacienteId}:`, foto.name);
return { success: true, message: "Foto do paciente enviada com sucesso." };
};
export const uploadAnexoPacienteAPI = async (
pacienteId: string,
anexo: File
) => {
console.log(`Upload do anexo do paciente ${pacienteId}:`, anexo.name);
return { success: true, message: "Anexo do paciente enviado com sucesso." };
};

View File

@ -1,41 +0,0 @@
// src/features/pacientes/types/index.ts
// ===== PACIENTES =====
export type Paciente = {
id: string;
full_name: string;
social_name?: string | null;
cpf?: string;
rg?: string | null;
sex?: string | null;
birth_date?: string | null;
phone_mobile?: string;
email?: string;
cep?: string | null;
street?: string | null;
number?: string | null;
complement?: string | null;
neighborhood?: string | null;
city?: string | null;
state?: string | null;
notes?: string | null;
};
export type PacienteInput = {
full_name: string;
social_name?: string | null;
cpf: string;
rg?: string | null;
sex?: string | null;
birth_date?: string | null;
phone_mobile?: string | null;
email?: string | null;
cep?: string | null;
street?: string | null;
number?: string | null;
complement?: string | null;
neighborhood?: string | null;
city?: string | null;
state?: string | null;
notes?: string | null;
};

View File

@ -1,168 +0,0 @@
// src/features/profissionais/api/index.ts
import httpClient from "@/lib/http";
import type { Medico, MedicoInput } from "@/features/profissionais/types";
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
if (!page || !limit) return {};
const start = (page - 1) * limit;
const end = start + limit - 1;
return { Range: `${start}-${end}`, "Range-Unit": "items" };
}
// ===== MÉDICOS (CRUD) =====
export async function listarMedicos(params?: {
page?: number;
limit?: number;
q?: string;
}): Promise<Medico[]> {
const qs = new URLSearchParams();
if (params?.q) qs.set("q", params.q);
const url = `/rest/v1/doctors${qs.toString() ? `?${qs.toString()}` : ""}`;
const res = await httpClient.get(url, {
headers: {
...rangeHeaders(params?.page, params?.limit),
},
});
if (!res.ok) {
throw new Error(`Erro ao listar médicos: ${res.statusText}`);
}
return res.json();
}
export async function buscarMedicos(termo: string): Promise<Medico[]> {
if (!termo || termo.trim().length < 2) {
return [];
}
const searchTerm = termo.toLowerCase().trim();
const digitsOnly = searchTerm.replace(/\D/g, '');
const queries = [];
if (searchTerm.includes('-') && searchTerm.length > 10) {
queries.push(`id=eq.${searchTerm}`);
}
if (digitsOnly.length >= 3) {
queries.push(`crm=ilike.*${digitsOnly}*`);
}
if (searchTerm.length >= 2) {
queries.push(`full_name=ilike.*${searchTerm}*`);
queries.push(`nome_social=ilike.*${searchTerm}*`);
}
if (searchTerm.includes('@')) {
queries.push(`email=ilike.*${searchTerm}*`);
}
if (searchTerm.length >= 2) {
queries.push(`specialty=ilike.*${searchTerm}*`);
}
const results: Medico[] = [];
const seenIds = new Set<string>();
for (const query of queries) {
try {
const url = `/rest/v1/doctors?${query}&limit=10`;
const res = await httpClient.get(url);
const arr: Medico[] = await res.json();
if (arr?.length > 0) {
for (const medico of arr) {
if (!seenIds.has(medico.id)) {
seenIds.add(medico.id);
results.push(medico);
}
}
}
} catch (error) {
console.warn(`Erro na busca com query: ${query}`, error);
}
}
return results.slice(0, 20);
}
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
const url = `/rest/v1/doctors?id=eq.${id}&limit=1`;
const res = await httpClient.get(url);
if (!res.ok) {
throw new Error("404: Médico não encontrado");
}
const arr: Medico[] = await res.json();
if (!arr?.length) throw new Error("404: Médico não encontrado");
return arr[0];
}
export async function criarMedico(input: MedicoInput): Promise<Medico> {
const url = `/rest/v1/doctors`;
const res = await httpClient.post(url, input, {
headers: {
Prefer: "return=representation",
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao criar médico: ${error.message}`);
}
const arr: Medico[] = await res.json();
return arr[0];
}
export async function atualizarMedico(
id: string | number,
input: Partial<MedicoInput>
): Promise<Medico> {
const url = `/rest/v1/doctors?id=eq.${id}`;
const res = await httpClient.patch(url, input, {
headers: {
Prefer: "return=representation",
},
});
if (!res.ok) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao atualizar médico: ${error.message}`);
}
const arr: Medico[] = await res.json();
return arr[0];
}
export async function excluirMedico(id: string | number): Promise<void> {
const url = `/rest/v1/doctors?id=eq.${id}`;
const res = await httpClient.delete(url);
if (!res.ok && res.status !== 204) {
const error = await res.json().catch(() => ({ message: res.statusText }));
throw new Error(`Erro ao excluir médico: ${error.message}`);
}
}
// Funções "stub" para profissionais
export const uploadFotoProfissionalAPI = async (
profissionalId: string,
foto: File
) => {
console.log(`Upload da foto do profissional ${profissionalId}:`, foto.name);
return {
success: true,
message: "Foto do profissional enviada com sucesso.",
};
};
export const uploadAnexoProfissionalAPI = async (
profissionalId: string,
anexo: File
) => {
console.log(`Upload do anexo do profissional ${profissionalId}:`, anexo.name);
return {
success: true,
message: "Anexo do profissional enviado com sucesso.",
};
};

View File

@ -1,78 +0,0 @@
// src/features/profissionais/types/index.ts
export type FormacaoAcademica = {
instituicao: string;
curso: string;
ano_conclusao: string;
};
export type DadosBancarios = {
banco: string;
agencia: string;
conta: string;
tipo_conta: string;
};
export type Medico = {
id: string;
full_name: string;
nome_social?: string | null;
cpf?: string;
rg?: string | null;
sexo?: string | null;
data_nascimento?: string | null;
telefone?: string;
celular?: string;
contato_emergencia?: string;
email?: string;
crm?: string;
estado_crm?: string;
rqe?: string;
formacao_academica?: FormacaoAcademica[];
curriculo_url?: string | null;
especialidade?: string;
observacoes?: string | null;
foto_url?: string | null;
tipo_vinculo?: string;
dados_bancarios?: DadosBancarios;
agenda_horario?: string;
valor_consulta?: number | string;
active?: boolean;
cep?: string;
city?: string;
complement?: string;
neighborhood?: string;
number?: string;
phone2?: string;
state?: string;
street?: string;
created_at?: string;
created_by?: string;
updated_at?: string;
updated_by?: string;
user_id?: string;
};
export type MedicoInput = {
user_id?: string | null;
crm: string;
crm_uf: string;
specialty: string;
full_name: string;
cpf: string;
email: string;
phone_mobile: string;
phone2?: string | null;
cep: string;
street: string;
number: string;
complement?: string;
neighborhood?: string;
city: string;
state: string;
birth_date: string | null;
rg?: string | null;
active?: boolean;
created_by?: string | null;
updated_by?: string | null;
};

View File

@ -1,24 +0,0 @@
// src/types/api.ts
export type ApiOk<T = any> = {
success?: boolean;
data: T;
message?: string;
pagination?: {
current_page?: number;
per_page?: number;
total_pages?: number;
total?: number;
};
};
// ===== TIPOS COMUNS =====
export type Endereco = {
cep?: string;
logradouro?: string;
numero?: string;
complemento?: string;
bairro?: string;
cidade?: string;
estado?: string;
};

29
susconecta/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# 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

@ -8,6 +8,8 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
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 {
mockAppointments,

View File

@ -54,7 +54,7 @@ import {
} from "@/components/ui/select";
import { mockAppointments, mockProfessionals } from "@/lib/mocks/appointment-mocks";
import { CalendarRegistrationForm } from "@/features/agendamento/components/forms/calendar-registration-form";
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
const formatDate = (date: string | Date) => {

View File

@ -0,0 +1,265 @@
"use client";
import { Button } from "@/components/ui/button";
import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react";
import jsPDF from "jspdf";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
// Dados fictícios para demonstração
const metricas = [
{ label: "Atendimentos", value: 1240, icon: <CalendarCheck className="w-6 h-6 text-blue-500" /> },
{ label: "Absenteísmo", value: "7,2%", icon: <UserCheck className="w-6 h-6 text-red-500" /> },
{ label: "Satisfação", value: "92%", icon: <ThumbsUp className="w-6 h-6 text-green-500" /> },
{ label: "Faturamento (Mês)", value: "R$ 45.000", icon: <DollarSign className="w-6 h-6 text-emerald-500" /> },
{ label: "No-show", value: "5,1%", icon: <User className="w-6 h-6 text-yellow-500" /> },
];
const consultasPorPeriodo = [
{ periodo: "Jan", consultas: 210 },
{ periodo: "Fev", consultas: 180 },
{ periodo: "Mar", consultas: 250 },
{ periodo: "Abr", consultas: 230 },
{ periodo: "Mai", consultas: 270 },
{ periodo: "Jun", consultas: 220 },
];
const faturamentoMensal = [
{ mes: "Jan", valor: 35000 },
{ mes: "Fev", valor: 29000 },
{ mes: "Mar", valor: 42000 },
{ mes: "Abr", valor: 38000 },
{ mes: "Mai", valor: 45000 },
{ mes: "Jun", valor: 41000 },
];
const taxaNoShow = [
{ mes: "Jan", noShow: 6.2 },
{ mes: "Fev", noShow: 5.8 },
{ mes: "Mar", noShow: 4.9 },
{ mes: "Abr", noShow: 5.5 },
{ mes: "Mai", noShow: 5.1 },
{ mes: "Jun", noShow: 4.7 },
];
const pacientesMaisAtendidos = [
{ nome: "Ana Souza", consultas: 18 },
{ nome: "Bruno Lima", consultas: 15 },
{ nome: "Carla Menezes", consultas: 13 },
{ nome: "Diego Alves", consultas: 12 },
{ nome: "Fernanda Dias", consultas: 11 },
];
const medicosMaisProdutivos = [
{ nome: "Dr. Carlos Andrade", consultas: 62 },
{ nome: "Dra. Paula Silva", consultas: 58 },
{ nome: "Dr. João Pedro", consultas: 54 },
{ nome: "Dra. Marina Costa", consultas: 51 },
];
const convenios = [
{ nome: "Unimed", valor: 18000 },
{ nome: "Bradesco", valor: 12000 },
{ nome: "SulAmérica", valor: 9000 },
{ nome: "Particular", valor: 15000 },
];
const performancePorMedico = [
{ nome: "Dr. Carlos Andrade", consultas: 62, absenteismo: 4.8 },
{ nome: "Dra. Paula Silva", consultas: 58, absenteismo: 6.1 },
{ nome: "Dr. João Pedro", consultas: 54, absenteismo: 7.5 },
{ nome: "Dra. Marina Costa", consultas: 51, absenteismo: 5.2 },
];
const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"];
function exportPDF(title: string, content: string) {
const doc = new jsPDF();
doc.text(title, 10, 10);
doc.text(content, 10, 20);
doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`);
}
export default function RelatoriosPage() {
return (
<div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-foreground">Dashboard Executivo de Relatórios</h1>
{/* Métricas principais */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
{metricas.map((m) => (
<div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
{m.icon}
<span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
<span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
</div>
))}
</div>
{/* Gráficos e Relatórios */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Consultas realizadas por período */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={consultasPorPeriodo}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="periodo" />
<YAxis />
<Tooltip />
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Faturamento mensal/anual */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={faturamentoMensal}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Taxa de no-show */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={taxaNoShow}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis unit="%" />
<Tooltip />
<Line type="monotone" dataKey="noShow" stroke="#ef4444" name="No-show (%)" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
{/* Indicadores de satisfação */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<div className="flex flex-col items-center justify-center h-[220px]">
<span className="text-5xl font-bold text-green-500">92%</span>
<span className="text-muted-foreground mt-2">Índice de satisfação geral</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Pacientes mais atendidos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Paciente</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{pacientesMaisAtendidos.map((p) => (
<tr key={p.nome}>
<td className="py-1">{p.nome}</td>
<td className="py-1">{p.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Médicos mais produtivos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{medicosMaisProdutivos.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Análise de convênios */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={convenios} dataKey="valor" nameKey="nome" cx="50%" cy="50%" outerRadius={80} label>
{convenios.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
{/* Performance por médico */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
<th className="text-left font-medium">Absenteísmo (%)</th>
</tr>
</thead>
<tbody>
{performancePorMedico.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
<td className="py-1">{m.absenteismo}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
);
}

View File

@ -9,7 +9,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, Di
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { DoctorRegistrationForm } from "@/features/profissionais/components/forms/doctor-registration-form";
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";

View File

@ -1,8 +1,8 @@
import type React from "react";
import ProtectedRoute from "@/components/layout/ProtectedRoute";
import { Sidebar } from "@/components/layout/app/Sidebar";
import { SidebarProvider } from "@/components/ui/sidebar";
import { PagesHeader } from "@/components/layout/app/Header";
import ProtectedRoute from "@/components/ProtectedRoute";
import { Sidebar } from "@/components/dashboard/sidebar";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { PagesHeader } from "@/components/dashboard/header";
export default function MainRoutesLayout({
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 { Paciente, Endereco, listarPacientes, buscarPacientes, buscarPacientePorId, excluirPaciente } from "@/lib/api";
import { PatientRegistrationForm } from "@/features/pacientes/components/forms/patient-registration-form";
import { PatientRegistrationForm } from "@/components/forms/patient-registration-form";
function normalizePaciente(p: any): Paciente {

View File

@ -1,7 +1,7 @@
"use client";
import { useRouter } from "next/navigation";
import { CalendarRegistrationForm } from "@/features/agendamento/components/forms/calendar-registration-form";
import { CalendarRegistrationForm } from "@/components/forms/calendar-registration-form";
import HeaderAgenda from "@/components/agenda/HeaderAgenda";
import FooterAgenda from "@/components/agenda/FooterAgenda";
import { useState } from "react";
@ -56,7 +56,6 @@ export default function NovoAgendamentoPage() {
/>
</main>
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
</div>
);
}

View File

@ -1,7 +1,7 @@
import type React from "react"
import type { Metadata } from "next"
import { AuthProvider } from "@/features/autenticacao/hooks/useAuth"
import { ThemeProvider } from "@/components/layout/ThemeProvider"
import { AuthProvider } from "@/hooks/useAuth"
import { ThemeProvider } from "@/components/theme-provider"
import "./globals.css"
export const metadata: Metadata = {

View File

@ -0,0 +1,124 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/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'
export default function LoginAdminPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
// Tentar fazer login usando o contexto com tipo administrador
const success = await login(credentials.email, credentials.password, 'administrador')
if (success) {
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
// Redirecionamento direto - solução que funcionou
window.location.href = '/dashboard'
}
} catch (err) {
console.error('[LOGIN-ADMIN] Erro no login:', err)
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">
Login Administrador de Clínica
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Entre com suas credenciais para acessar o sistema administrativo
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
</CardHeader>
<CardContent>
<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={(e) => setCredentials({...credentials, email: e.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={(e) => setCredentials({...credentials, password: e.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...' : 'Entrar no Sistema Administrativo'}
</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

@ -0,0 +1,131 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/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'
export default function LoginPacientePage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
// Tentar fazer login usando o contexto com tipo paciente
const success = await login(credentials.email, credentials.password, 'paciente')
if (success) {
// Redirecionar para a página do paciente
router.push('/paciente')
}
} catch (err) {
console.error('[LOGIN-PACIENTE] Erro no login:', err)
if (err instanceof AuthenticationError) {
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
} else {
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">
Sou Paciente
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Acesse sua área pessoal e gerencie suas consultas
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
</CardHeader>
<CardContent>
<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={(e) => setCredentials({...credentials, email: e.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={(e) => setCredentials({...credentials, password: e.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...' : 'Entrar na Minha Área'}
</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

@ -0,0 +1,133 @@
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useAuth } from '@/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'
export default function LoginPage() {
const [credentials, setCredentials] = useState({ email: '', password: '' })
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const router = useRouter()
const { login } = useAuth()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
try {
// Tentar fazer login usando o contexto com tipo profissional
const success = await login(credentials.email, credentials.password, 'profissional')
if (success) {
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
// Redirecionamento direto - solução que funcionou
window.location.href = '/profissional'
}
} catch (err) {
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
if (err instanceof AuthenticationError) {
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
} else {
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">
Login Profissional de Saúde
</h2>
<p className="mt-2 text-sm text-muted-foreground">
Entre com suas credenciais para acessar o sistema
</p>
</div>
<Card>
<CardHeader>
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
</CardHeader>
<CardContent>
<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={(e) => setCredentials({...credentials, email: e.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={(e) => setCredentials({...credentials, password: e.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...' : 'Entrar'}
</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

@ -0,0 +1,578 @@
'use client'
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
// Simulação de internacionalização básica
const strings = {
dashboard: 'Dashboard',
consultas: 'Consultas',
exames: 'Exames & Laudos',
mensagens: 'Mensagens',
perfil: 'Perfil',
sair: 'Sair',
proximaConsulta: 'Próxima Consulta',
ultimosExames: 'Últimos Exames',
mensagensNaoLidas: 'Mensagens Não Lidas',
agendar: 'Agendar',
reagendar: 'Reagendar',
cancelar: 'Cancelar',
detalhes: 'Detalhes',
adicionarCalendario: 'Adicionar ao calendário',
visualizarLaudo: 'Visualizar Laudo',
download: 'Download',
compartilhar: 'Compartilhar',
inbox: 'Caixa de Entrada',
enviarMensagem: 'Enviar Mensagem',
salvar: 'Salvar',
editarPerfil: 'Editar Perfil',
consentimentos: 'Consentimentos',
notificacoes: 'Preferências de Notificação',
vazio: 'Nenhum dado encontrado.',
erro: 'Ocorreu um erro. Tente novamente.',
carregando: 'Carregando...',
sucesso: 'Salvo com sucesso!',
erroSalvar: 'Erro ao salvar.',
}
export default function PacientePage() {
const { logout, user } = useAuth()
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
// Simulação de loaders, empty states e erro
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null)
// Acessibilidade: foco visível e ordem de tabulação garantidos por padrão nos botões e inputs
const handleLogout = async () => {
setLoading(true)
setError('')
try {
await logout()
} catch {
setError(strings.erro)
} finally {
setLoading(false)
}
}
// Estado para edição do perfil
const [isEditingProfile, setIsEditingProfile] = useState(false)
const [profileData, setProfileData] = useState({
nome: "Maria Silva Santos",
email: user?.email || "paciente@example.com",
telefone: "(11) 99999-9999",
endereco: "Rua das Flores, 123",
cidade: "São Paulo",
cep: "01234-567",
biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.",
})
const handleProfileChange = (field: string, value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }))
}
const handleSaveProfile = () => {
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
}
const handleCancelEdit = () => {
setIsEditingProfile(false)
}
function DashboardCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="flex flex-col items-center justify-center p-4">
<Calendar className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.proximaConsulta}</span>
<span className="text-2xl">12/10/2025</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<FileText className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.ultimosExames}</span>
<span className="text-2xl">2</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<MessageCircle className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
<span className="text-2xl">1</span>
</Card>
</div>
)
}
// Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
const consultasFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
especialidade: "Cardiologia",
local: "Clínica Coração Feliz",
data: new Date().toISOString().split('T')[0],
hora: "09:00",
status: "Confirmada"
},
{
id: 2,
medico: "Dra. Fernanda Lima",
especialidade: "Dermatologia",
local: "Clínica Pele Viva",
data: new Date().toISOString().split('T')[0],
hora: "14:30",
status: "Pendente"
},
{
id: 3,
medico: "Dr. João Silva",
especialidade: "Ortopedia",
local: "Hospital Ortopédico",
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
hora: "11:00",
status: "Cancelada"
},
];
function formatDatePt(dateStr: string) {
const date = new Date(dateStr);
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
function navigateDate(direction: 'prev' | 'next') {
const newDate = new Date(currentDate);
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
setCurrentDate(newDate);
}
function goToToday() {
setCurrentDate(new Date());
}
const todayStr = currentDate.toISOString().split('T')[0];
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
function Consultas() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Minhas Consultas</h2>
</div>
{/* Navegação de Data */}
<div className="flex items-center justify-between mb-6 p-4 bg-blue-50 rounded-lg dark:bg-muted">
<div className="flex items-center space-x-4">
<Button variant="outline" size="sm" onClick={() => navigateDate('prev')} className="p-2"><ChevronLeft className="h-4 w-4" /></Button>
<h3 className="text-lg font-medium text-foreground">{formatDatePt(todayStr)}</h3>
<Button variant="outline" size="sm" onClick={() => navigateDate('next')} className="p-2"><ChevronRight className="h-4 w-4" /></Button>
<Button variant="outline" size="sm" onClick={goToToday} className="ml-4 px-3 py-1 text-sm">Hoje</Button>
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
</div>
</div>
{/* Lista de Consultas do Dia */}
<div className="space-y-4">
{consultasDoDia.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma consulta agendada para este dia</p>
<p className="text-sm">Você pode agendar uma nova consulta</p>
<Button variant="default" className="mt-4">Agendar Consulta</Button>
</div>
) : (
consultasDoDia.map(consulta => (
<div key={consulta.id} className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
<div className="flex items-center">
<div className="w-3 h-3 rounded-full mr-3" style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}></div>
<div>
<div className="font-medium flex items-center">
<Stethoscope className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
{consulta.medico}
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consulta.especialidade} {consulta.local}
</div>
</div>
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<div className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>{consulta.status}</div>
</div>
<div className="flex items-center justify-end space-x-2">
<Button variant="outline" size="sm">Detalhes</Button>
{consulta.status !== 'Cancelada' && <Button variant="secondary" size="sm">Reagendar</Button>}
{consulta.status !== 'Cancelada' && <Button variant="destructive" size="sm">Cancelar</Button>}
</div>
</div>
</div>
))
)}
</div>
</section>
)
}
// Exames e laudos fictícios
const examesFicticios = [
{
id: 1,
nome: "Hemograma Completo",
data: "2025-09-20",
status: "Disponível",
prontuario: "Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
},
{
id: 2,
nome: "Raio-X de Tórax",
data: "2025-08-10",
status: "Disponível",
prontuario: "Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
},
{
id: 3,
nome: "Eletrocardiograma",
data: "2025-07-05",
status: "Disponível",
prontuario: "Ritmo sinusal, sem arritmias. Exame dentro da normalidade.",
},
];
const laudosFicticios = [
{
id: 1,
nome: "Laudo Hemograma Completo",
data: "2025-09-21",
status: "Assinado",
laudo: "Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
},
{
id: 2,
nome: "Laudo Raio-X de Tórax",
data: "2025-08-11",
status: "Assinado",
laudo: "Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
},
{
id: 3,
nome: "Laudo Eletrocardiograma",
data: "2025-07-06",
status: "Assinado",
laudo: "ECG normal. Não há sinais de isquemia ou sobrecarga.",
},
];
const [exameSelecionado, setExameSelecionado] = useState<null | typeof examesFicticios[0]>(null)
const [laudoSelecionado, setLaudoSelecionado] = useState<null | typeof laudosFicticios[0]>(null)
function ExamesLaudos() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Exames</h2>
<div className="mb-8">
<h3 className="text-lg font-semibold mb-2">Meus Exames</h3>
<div className="space-y-3">
{examesFicticios.map(exame => (
<div key={exame.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{exame.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(exame.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setExameSelecionado(exame)}>Ver Prontuário</Button>
<Button variant="secondary">Download</Button>
</div>
</div>
))}
</div>
</div>
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
<div>
<h3 className="text-lg font-semibold mb-2">Meus Laudos</h3>
<div className="space-y-3">
{laudosFicticios.map(laudo => (
<div key={laudo.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{laudo.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(laudo.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setLaudoSelecionado(laudo)}>Visualizar</Button>
<Button variant="secondary">Compartilhar</Button>
</div>
</div>
))}
</div>
</div>
{/* Modal Prontuário Exame */}
<Dialog open={!!exameSelecionado} onOpenChange={open => !open && setExameSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Prontuário do Exame</DialogTitle>
<DialogDescription>
{exameSelecionado && (
<>
<div className="font-semibold mb-2">{exameSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(exameSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{exameSelecionado.prontuario}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setExameSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal Visualizar Laudo */}
<Dialog open={!!laudoSelecionado} onOpenChange={open => !open && setLaudoSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{laudoSelecionado && (
<>
<div className="font-semibold mb-2">{laudoSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(laudoSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{laudoSelecionado.laudo}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setLaudoSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
)
}
// Mensagens fictícias recebidas do médico
const mensagensFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
data: "2025-10-06T15:30:00",
conteudo: "Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
lida: false
},
{
id: 2,
medico: "Dra. Fernanda Lima",
data: "2025-09-21T10:15:00",
conteudo: "Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
lida: true
},
{
id: 3,
medico: "Dr. João Silva",
data: "2025-08-12T09:00:00",
conteudo: "Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
lida: true
},
];
function Mensagens() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Mensagens Recebidas</h2>
<div className="space-y-3">
{mensagensFicticias.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<MessageCircle className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma mensagem recebida</p>
<p className="text-sm">Você ainda não recebeu mensagens dos seus médicos.</p>
</div>
) : (
mensagensFicticias.map(msg => (
<div key={msg.id} className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!msg.lida ? 'border-primary' : 'border-transparent'}`}>
<div>
<div className="font-medium text-foreground flex items-center gap-2">
<User className="h-4 w-4 text-primary" />
{msg.medico}
{!msg.lida && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
</div>
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.data).toLocaleString('pt-BR')}</div>
<div className="text-foreground whitespace-pre-line">{msg.conteudo}</div>
</div>
</div>
))
)}
</div>
</section>
)
}
function Perfil() {
return (
<div className="space-y-6 max-w-2xl mx-auto">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-foreground">Meu Perfil</h2>
{!isEditingProfile ? (
<Button onClick={() => setIsEditingProfile(true)} className="flex items-center gap-2">
Editar Perfil
</Button>
) : (
<div className="flex gap-2">
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Informações Pessoais */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Informações Pessoais</h3>
<div className="space-y-2">
<Label htmlFor="nome">Nome Completo</Label>
<p className="p-2 bg-muted rounded text-muted-foreground">{profileData.nome}</p>
<span className="text-xs text-muted-foreground">Este campo não pode ser alterado</span>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
{isEditingProfile ? (
<Input id="email" type="email" value={profileData.email} onChange={e => handleProfileChange('email', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
{isEditingProfile ? (
<Input id="telefone" value={profileData.telefone} onChange={e => handleProfileChange('telefone', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.telefone}</p>
)}
</div>
</div>
{/* Endereço e Contato */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Endereço</h3>
<div className="space-y-2">
<Label htmlFor="endereco">Endereço</Label>
{isEditingProfile ? (
<Input id="endereco" value={profileData.endereco} onChange={e => handleProfileChange('endereco', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.endereco}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label>
{isEditingProfile ? (
<Input id="cidade" value={profileData.cidade} onChange={e => handleProfileChange('cidade', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cidade}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cep">CEP</Label>
{isEditingProfile ? (
<Input id="cep" value={profileData.cep} onChange={e => handleProfileChange('cep', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="biografia">Biografia</Label>
{isEditingProfile ? (
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
) : (
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
)}
</div>
</div>
</div>
{/* Foto do Perfil */}
<div className="border-t border-border pt-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarFallback className="text-lg">
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
</AvatarFallback>
</Avatar>
{isEditingProfile && (
<div className="space-y-2">
<Button variant="outline" size="sm">Alterar Foto</Button>
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
</div>
)}
</div>
</div>
</div>
)
}
// Renderização principal
return (
<ProtectedRoute requiredUserType={["paciente"]}>
<div className="min-h-screen bg-background text-foreground flex flex-col">
{/* Header só com título e botão de sair */}
<header className="flex items-center justify-between px-4 py-2 border-b bg-card">
<div className="flex items-center gap-2">
<User className="h-6 w-6 text-primary" aria-hidden />
<span className="font-bold">Portal do Paciente</span>
<Button asChild variant="outline" className="ml-4">
<Link href="/">
<Home className="h-4 w-4 mr-1" /> Início
</Link>
</Button>
</div>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button onClick={handleLogout} variant="destructive" aria-label={strings.sair} disabled={loading} className="ml-2"><LogOut className="h-4 w-4" /> {strings.sair}</Button>
</div>
</header>
<div className="flex flex-1 min-h-0">
{/* Sidebar vertical */}
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
</nav>
{/* Conteúdo principal */}
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
{/* Toasts de feedback */}
{toast && (
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type==='success'?'bg-green-600 text-white':'bg-red-600 text-white'}`} role="alert">{toast.msg}</div>
)}
{/* Loader global */}
{loading && <div className="flex-1 flex items-center justify-center"><span>{strings.carregando}</span></div>}
{error && <div className="flex-1 flex items-center justify-center text-red-600"><span>{error}</span></div>}
{/* Conteúdo principal */}
{!loading && !error && (
<main className="flex-1">
{tab==='dashboard' && <DashboardCards />}
{tab==='consultas' && <Consultas />}
{tab==='exames' && <ExamesLaudos />}
{tab==='mensagens' && <Mensagens />}
{tab==='perfil' && <Perfil />}
</main>
)}
</div>
</div>
</div>
</ProtectedRoute>
)
}

View File

@ -1,6 +1,6 @@
import { Header } from "@/components/layout/marketing/Header"
import { HeroSection } from "@/features/marketing/components/hero-section"
import { Footer } from "@/components/layout/marketing/Footer"
import { Header } from "@/components/header"
import { HeroSection } from "@/components/hero-section"
import { Footer } from "@/components/footer"
export default function HomePage() {
return (

View File

@ -3,12 +3,14 @@
import React, { useState, useRef, useEffect } from "react";
import SignatureCanvas from "react-signature-canvas";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth";
import { buscarPacientes } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import {
Table,
TableBody,
@ -2031,14 +2033,40 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
}
}, [laudo, isNewLaudo]);
const formatText = (type: string) => {
// Histórico para desfazer/refazer
const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
// Atualiza histórico ao digitar
useEffect(() => {
if (history[historyIndex] !== content) {
const newHistory = history.slice(0, historyIndex + 1);
setHistory([...newHistory, content]);
setHistoryIndex(newHistory.length);
}
// eslint-disable-next-line
}, [content]);
const handleUndo = () => {
if (historyIndex > 0) {
setContent(history[historyIndex - 1]);
setHistoryIndex(historyIndex - 1);
}
};
const handleRedo = () => {
if (historyIndex < history.length - 1) {
setContent(history[historyIndex + 1]);
setHistoryIndex(historyIndex + 1);
}
};
// Formatação avançada
const formatText = (type: string, value?: any) => {
const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
let formattedText = "";
switch(type) {
case "bold":
@ -2048,13 +2076,44 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*";
break;
case "underline":
formattedText = selectedText ? `<u>${selectedText}</u>` : "<u>texto sublinhado</u>";
formattedText = selectedText ? `__${selectedText}__` : "__texto sublinhado__";
break;
case "list":
formattedText = selectedText ? `${selectedText}` : "• item da lista";
case "list-ul":
formattedText = selectedText ? selectedText.split('\n').map(l => `${l}`).join('\n') : "• item da lista";
break;
case "list-ol":
formattedText = selectedText ? selectedText.split('\n').map((l,i) => `${i+1}. ${l}`).join('\n') : "1. item da lista";
break;
case "indent":
formattedText = selectedText ? selectedText.split('\n').map(l => ` ${l}`).join('\n') : " ";
break;
case "outdent":
formattedText = selectedText ? selectedText.split('\n').map(l => l.replace(/^\s{1,4}/, "")).join('\n') : "";
break;
case "align-left":
formattedText = selectedText ? `[left]${selectedText}[/left]` : "[left]Texto à esquerda[/left]";
break;
case "align-center":
formattedText = selectedText ? `[center]${selectedText}[/center]` : "[center]Texto centralizado[/center]";
break;
case "align-right":
formattedText = selectedText ? `[right]${selectedText}[/right]` : "[right]Texto à direita[/right]";
break;
case "align-justify":
formattedText = selectedText ? `[justify]${selectedText}[/justify]` : "[justify]Texto justificado[/justify]";
break;
case "font-size":
formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`;
break;
case "font-family":
formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`;
break;
case "font-color":
formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`;
break;
default:
return;
}
const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
setContent(newText);
};
@ -2083,7 +2142,14 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/<u>(.*?)<\/u>/g, '<u>$1</u>')
.replace(/__(.*?)__/g, '<u>$1</u>')
.replace(/\[left\](.*?)\[\/left\]/gs, '<div style="text-align:left">$1</div>')
.replace(/\[center\](.*?)\[\/center\]/gs, '<div style="text-align:center">$1</div>')
.replace(/\[right\](.*?)\[\/right\]/gs, '<div style="text-align:right">$1</div>')
.replace(/\[justify\](.*?)\[\/justify\]/gs, '<div style="text-align:justify">$1</div>')
.replace(/\[size=(\d+)\](.*?)\[\/size\]/gs, '<span style="font-size:$1px">$2</span>')
.replace(/\[font=([^\]]+)\](.*?)\[\/font\]/gs, '<span style="font-family:$1">$2</span>')
.replace(/\[color=([^\]]+)\](.*?)\[\/color\]/gs, '<span style="color:$1">$2</span>')
.replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]')
.replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')
.replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]')
@ -2382,44 +2448,60 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<div className="flex-1 flex flex-col">
{/* Toolbar */}
<div className="p-3 border-b border-border">
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => formatText("bold")}
title="Negrito"
className="hover:bg-blue-50 dark:hover:bg-accent"
<div className="flex flex-wrap gap-2 items-center">
{/* Tamanho da fonte */}
<label className="text-xs mr-1">Tamanho</label>
<input
type="number"
min={8}
max={32}
defaultValue={14}
onBlur={e => formatText('font-size', e.target.value)}
className="w-14 border rounded px-1 py-0.5 text-xs mr-2"
title="Tamanho da fonte"
/>
{/* Família da fonte */}
<label className="text-xs mr-1">Fonte</label>
<select
defaultValue={'Arial'}
onBlur={e => formatText('font-family', e.target.value)}
className="border rounded px-1 py-0.5 text-xs mr-2"
title="Família da fonte"
>
<strong>B</strong>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("italic")}
title="Itálico"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
<em>I</em>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("underline")}
title="Sublinhado"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
<u>U</u>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("list")}
title="Lista"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
</Button>
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option>
</select>
{/* Cor da fonte */}
<label className="text-xs mr-1">Cor</label>
<input
type="color"
defaultValue="#222222"
onBlur={e => formatText('font-color', e.target.value)}
className="w-6 h-6 border rounded mr-2"
title="Cor da fonte"
/>
{/* Alinhamento */}
<Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1"><svg width="16" height="16" fill="none"><rect x="4" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="3" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1"><svg width="16" height="16" fill="none"><rect x="6" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="4" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-justify')} title="Justificar" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="12" height="2" rx="1" fill="currentColor"/></svg></Button>
{/* Listas */}
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1">1.</Button>
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1"></Button>
{/* Recuo */}
<Button variant="outline" size="sm" onClick={() => formatText('indent')} title="Aumentar recuo" className="px-1"></Button>
<Button variant="outline" size="sm" onClick={() => formatText('outdent')} title="Diminuir recuo" className="px-1"></Button>
{/* Desfazer/Refazer */}
<Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1"></Button>
<Button variant="outline" size="sm" onClick={handleRedo} title="Refazer" className="px-1"></Button>
{/* Negrito, itálico, sublinhado */}
<Button variant="outline" size="sm" onClick={() => formatText("bold") } title="Negrito" className="hover:bg-blue-50 dark:hover:bg-accent"><strong>B</strong></Button>
<Button variant="outline" size="sm" onClick={() => formatText("italic") } title="Itálico" className="hover:bg-blue-50 dark:hover:bg-accent"><em>I</em></Button>
<Button variant="outline" size="sm" onClick={() => formatText("underline") } title="Sublinhado" className="hover:bg-blue-50 dark:hover:bg-accent"><u>U</u></Button>
</div>
{/* Templates */}
@ -2442,12 +2524,13 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div>
{/* Editor */}
<div className="flex-1 p-4">
<div className="flex-1 p-4 overflow-auto max-h-[500px]">
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico, <u></u> para sublinhado."
className="h-full min-h-[400px] resize-none"
className="h-full min-h-[400px] resize-none scrollbar-thin scrollbar-thumb-blue-400 scrollbar-track-blue-100"
style={{ maxHeight: 400, overflow: 'auto' }}
/>
</div>
</div>
@ -3250,7 +3333,8 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
};
return (
<div className="container mx-auto px-4 py-8">
<ProtectedRoute requiredUserType={["profissional"]}>
<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">
<div className="flex items-center gap-4">
<Avatar className="h-12 w-12">
@ -3268,13 +3352,16 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
)}
</div>
</div>
<Button
variant="outline"
onClick={logout}
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
>
Sair
</Button>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button
variant="outline"
onClick={logout}
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
>
Sair
</Button>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">
@ -3508,6 +3595,7 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div>
)}
</div>
</ProtectedRoute>
);
};

View File

@ -1,6 +1,6 @@
import { Header } from "@/components/layout/marketing/Header"
import { AboutSection } from "@/features/marketing/components/about-section"
import { Footer } from "@/components/layout/marketing/Footer"
import { Header } from "@/components/header"
import { AboutSection } from "@/components/about-section"
import { Footer } from "@/components/footer"
export default function AboutPage() {
return (

View File

@ -2,8 +2,8 @@
import { useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '@/hooks/useAuth'
import type { UserType } from '@/features/autenticacao/types'
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/features/autenticacao/types'
import type { UserType } from '@/types/auth'
import { USER_TYPE_ROUTES, LOGIN_ROUTES, AUTH_STORAGE_KEYS } from '@/types/auth'
interface ProtectedRouteProps {
children: React.ReactNode

View File

@ -0,0 +1,177 @@
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
export interface CredentialsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
email: string;
password: string;
userName: string;
userType: "médico" | "paciente";
}
export function CredentialsDialog({
open,
onOpenChange,
email,
password,
userName,
userType,
}: CredentialsDialogProps) {
const [showPassword, setShowPassword] = useState(false);
const [copiedEmail, setCopiedEmail] = useState(false);
const [copiedPassword, setCopiedPassword] = useState(false);
function handleCopyEmail() {
navigator.clipboard.writeText(email);
setCopiedEmail(true);
setTimeout(() => setCopiedEmail(false), 2000);
}
function handleCopyPassword() {
navigator.clipboard.writeText(password);
setCopiedPassword(true);
setTimeout(() => setCopiedPassword(false), 2000);
}
function handleCopyBoth() {
const text = `Email: ${email}\nSenha: ${password}`;
navigator.clipboard.writeText(text);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com Sucesso!
</DialogTitle>
<DialogDescription>
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer login com as credenciais abaixo.
</DialogDescription>
</DialogHeader>
<Alert className="bg-amber-50 border-amber-200">
<AlertDescription className="text-amber-900">
<strong>Importante:</strong> Anote ou copie estas credenciais agora. Por segurança, essa senha não será exibida novamente.
</AlertDescription>
</Alert>
<Alert className="bg-blue-50 border-blue-200">
<AlertDescription className="text-blue-900">
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
</AlertDescription>
</Alert>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="email">Email de Acesso</Label>
<div className="flex gap-2">
<Input
id="email"
value={email}
readOnly
className="bg-muted"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyEmail}
title="Copiar email"
>
{copiedEmail ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha Temporária</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
readOnly
className="bg-muted pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyPassword}
title="Copiar senha"
>
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-900">
<strong>Próximos passos:</strong>
<ol className="list-decimal list-inside mt-2 space-y-1">
<li>Compartilhe estas credenciais com o {userType}</li>
<li>
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
<strong>{email}</strong> (verifique também a pasta de spam)
</li>
<li>
Após confirmar o email, o {userType} deve acessar:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/login" : "/login-paciente"}
</code>
</li>
<li>
Após o login, terá acesso à área:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/profissional" : "/paciente"}
</code>
</li>
<li>Recomende trocar a senha no primeiro acesso</li>
</ol>
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
type="button"
variant="outline"
onClick={handleCopyBoth}
className="w-full sm:w-auto"
>
<Copy className="mr-2 h-4 w-4" />
Copiar Tudo
</Button>
<Button
type="button"
onClick={() => onOpenChange(false)}
className="w-full sm:w-auto"
>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -1,11 +1,13 @@
"use client"
import { Bell } from "lucide-react"
import { Bell, ChevronDown } from "lucide-react"
import { useAuth } from "@/hooks/useAuth"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
import { SidebarTrigger } from "@/components/ui/sidebar"
import { SidebarTrigger } from "../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth();
@ -43,7 +45,12 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<Bell className="h-4 w-4" />
</Button>
<SimpleThemeToggle />
<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"
asChild
></Button>
{/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}>
<Button

View File

@ -2,6 +2,7 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
import {
Sidebar as ShadSidebar,
SidebarHeader,
@ -71,23 +72,22 @@ export function Sidebar() {
<SidebarGroupContent>
<SidebarMenu>
{navigation.map((item) => {
const isActive =
pathname === item.href ||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
const isActive = pathname === item.href ||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
return (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-3 h-4 w-4 shrink-0" />
<span className="truncate group-data-[collapsible=icon]:hidden">
{item.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
return (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild isActive={isActive}>
<Link href={item.href} className="flex items-center">
<item.icon className="mr-3 h-4 w-4 shrink-0" />
<span className="truncate group-data-[collapsible=icon]:hidden">
{item.name}
</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>

View File

@ -12,8 +12,10 @@ export function Footer() {
<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="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>
{}
<nav className="flex items-center space-x-8">
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
Termos
@ -26,6 +28,7 @@ export function Footer() {
</a>
</nav>
{}
<Button
variant="outline"
size="sm"

View File

@ -1,6 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { buscarPacientePorId } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -23,10 +24,13 @@ import {
removerAnexoMedico,
MedicoInput, // 👈 importado do lib/api
Medico, // 👈 adicionado import do tipo Medico
criarUsuarioMedico,
CreateUserWithPasswordResponse,
} from "@/lib/api";
;
import { buscarCepAPI } from "@/lib/api";
import { buscarCepAPI } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
type FormacaoAcademica = {
instituicao: string;
@ -149,6 +153,11 @@ export function DoctorRegistrationForm({
const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedDoctor, setSavedDoctor] = useState<Medico | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
@ -390,8 +399,69 @@ if (missingFields.length > 0) {
console.log("✅ Médico salvo com sucesso:", saved);
onSaved?.(saved);
setSubmitting(false);
// Se for criação de novo médico e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o médico...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.full_name);
console.log("📱 Telefone:", form.celular);
try {
const userCredentials = await criarUsuarioMedico({
email: form.email,
full_name: form.full_name,
phone_mobile: form.celular,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
setCredentials(userCredentials);
setShowCredentials(true);
setSavedDoctor(saved); // Salva médico para chamar onSaved depois
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// NÃO chama onSaved aqui! Isso fecha o formulário.
// O dialog vai chamar onSaved quando o usuário fechar
setSubmitting(false);
return; // ← IMPORTANTE: Impede que o código abaixo seja executado
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("💬 Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Médico cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
return;
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
}
} catch (err: any) {
console.error("❌ Erro ao salvar médico:", err);
console.error("❌ Detalhes do erro:", {
@ -934,18 +1004,90 @@ if (missingFields.length > 0) {
</>
);
if (inline) return <div className="space-y-6">{content}</div>;
if (inline) {
return (
<>
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (inline) onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
userName={form.full_name}
userType="médico"
/>
)}
</>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (dialog mode) onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e fechando modal principal");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
}
// Limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={form.full_name}
userType="médico"
/>
)}
</>
);
}

View File

@ -16,6 +16,7 @@ import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload,
import {
Paciente,
PacienteInput,
buscarCepAPI,
criarPaciente,
atualizarPaciente,
uploadFotoPaciente,
@ -24,14 +25,13 @@ import {
listarAnexos,
removerAnexo,
buscarPacientePorId,
verificarCpfDuplicado,
} from "@/features/pacientes/api";
import {
criarUsuarioPaciente,
CreateUserWithPasswordResponse,
} from "@/features/autenticacao/api";
} from "@/lib/api";
import { validarCPFLocal, buscarCepAPI } from "@/lib/utils";
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
@ -107,6 +107,11 @@ export function PatientRegistrationForm({
const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
@ -236,11 +241,9 @@ export function PatientRegistrationForm({
return;
}
}
} catch (err) {
// Log the validation error for debugging.
// Keeping the catch block intentionally minimal to avoid empty-block lint errors.
console.error("Erro ao validar CPF", err);
}
} catch (err) {
console.error("Erro ao validar CPF", err);
}
setSubmitting(true);
@ -258,32 +261,99 @@ export function PatientRegistrationForm({
if (form.photo && saved?.id) {
try {
await uploadFotoPaciente(saved.id, form.photo);
} catch (uploadError) {
// Não bloquear a criação/edição do paciente se a foto falhar.
console.warn("Falha ao enviar foto do paciente:", uploadError);
}
} catch {}
}
if (form.anexos.length && saved?.id) {
for (const f of form.anexos) {
try {
await adicionarAnexo(saved.id, f);
} catch (anexoError) {
// Registrar erro de anexo e continuar com os próximos arquivos.
console.warn("Falha ao adicionar anexo:", anexoError);
}
} catch {}
}
}
// Se for criação de novo paciente e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o paciente...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.nome);
console.log("📱 Telefone:", form.telefone);
try {
const userCredentials = await criarUsuarioPaciente({
email: form.email,
full_name: form.nome,
phone_mobile: form.telefone,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
setCredentials(userCredentials);
setShowCredentials(true);
console.log("📋 Depois de set - credentials:", userCredentials);
console.log("📋 Depois de set - showCredentials: true");
console.log("📋 Modo inline?", inline);
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
// Força re-render
setTimeout(() => {
console.log("⏰ Timeout - credentials:", credentials);
console.log("⏰ Timeout - showCredentials:", showCredentials);
}, 100);
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// Salva o paciente para chamar onSaved depois
setSavedPatient(saved);
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
// Se chamar agora, o formulário fecha e o dialog desaparece.
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
// RETORNA AQUI para não executar o código abaixo
return;
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("<22> Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Paciente cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
}
onSaved?.(saved);
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
} catch (err: any) {
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
} finally {
@ -622,18 +692,85 @@ export function PatientRegistrationForm({
</>
);
if (inline) return <div className="space-y-6">{content}</div>;
if (inline) {
return (
<>
<div className="space-y-6">{content}</div>
{/* Debug */}
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o paciente salvo
if (savedPatient) {
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
onSaved?.(savedPatient);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedPatient(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
userName={form.nome}
userType="paciente"
/>
)}
</>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
<>
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
setShowCredentials(open);
if (!open) {
// Quando fechar o dialog, limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={form.nome}
userType="paciente"
/>
)}
</>
);
}

View File

@ -5,7 +5,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Menu, X } from "lucide-react";
import { usePathname } from "next/navigation";
import { SimpleThemeToggle } from "@/components/layout/SimpleThemeToggle";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -22,6 +22,7 @@ export function Header() {
</span>
</Link>
{}
<nav className="hidden md:flex items-center gap-10">
<Link
href="/"
@ -41,30 +42,31 @@ export function Header() {
</Link>
</nav>
{}
<div className="hidden md:flex items-center space-x-3">
<SimpleThemeToggle />
<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"
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"
asChild
>
<Link href="/login?role=paciente">Sou Paciente</Link>
<Link href="/login-paciente">Sou Paciente</Link>
</Button>
<Button
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 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">
<Link href="/login">Sou Profissional de Saúde</Link>
</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>
{}
<button
className="md:hidden p-2"
onClick={() => setIsMenuOpen(!isMenuOpen)}
@ -74,6 +76,7 @@ export function Header() {
</button>
</div>
{}
{isMenuOpen && (
<div className="md:hidden py-4 border-t border-border">
<nav className="flex flex-col space-y-4">
@ -95,24 +98,22 @@ export function Header() {
<SimpleThemeToggle />
<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"
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"
asChild
>
<Link href="/login?role=paciente">Sou Paciente</Link>
<Link href="/login-paciente">Sou Paciente</Link>
</Button>
<Button
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 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">
<Link href="/login">Sou Profissional de Saúde</Link>
</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>
</nav>
</div>

View File

@ -7,6 +7,7 @@ export function HeroSection() {
<section className="py-8 lg:py-12 bg-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-8 items-center">
{}
<div className="space-y-6">
<div className="space-y-3">
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
@ -22,25 +23,27 @@ export function HeroSection() {
</div>
</div>
{}
<div className="flex flex-col sm:flex-row gap-4">
<Button
size="lg"
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
asChild
>
<Link href="/login?role=paciente">Portal do Paciente</Link>
<Link href="/login-paciente">Portal do Paciente</Link>
</Button>
<Button
size="lg"
variant="outline"
className="text-primary bg-transparent cursor-pointer 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"
className="text-primary border-primary bg-transparent cursor-pointer 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"
asChild
>
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
<Link href="/login">Sou Profissional de Saúde</Link>
</Button>
</div>
</div>
{}
<div className="relative">
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-6">
<img
@ -52,6 +55,7 @@ export function HeroSection() {
</div>
</div>
{}
<div className="mt-10 grid md:grid-cols-3 gap-6">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">

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