# MediConnect - Sistema de Agendamento Médico

Aplicação SPA (React + Vite + TypeScript) consumindo **Supabase** (Auth, PostgREST, Storage) diretamente do frontend, hospedada no **Cloudflare Pages**.

---

## 🚀 Acesso ao Sistema

- **URL Principal:** https://mediconnectbrasil.app/
- **URL Cloudflare:** https://mediconnect-5oz.pages.dev/

---

## 🏗️ Arquitetura Atual (Outubro 2025)

```
Frontend (Vite/React) → Supabase API
         ↓                 ├── Auth (JWT)
  Cloudflare Pages         ├── PostgREST (PostgreSQL)
                            └── Storage (Avatares)
```

**Mudança importante:** O sistema **não usa mais Netlify Functions**. Toda comunicação é direta entre frontend e Supabase via services (`authService`, `userService`, `patientService`, `avatarService`, etc.).

---

## 🚀 Guias de Início Rápido

**Primeira vez rodando o projeto?**

### Instalação Rápida (5 minutos)

```powershell
# 1. Instalar dependências
pnpm install

# 2. Iniciar servidor de desenvolvimento
pnpm dev

# 3. Acessar http://localhost:5173
```

### Build e Deploy

```powershell
# Build de produção
pnpm build

# Deploy para Cloudflare
npx wrangler pages deploy dist --project-name=mediconnect --branch=production
```

📚 **Documentação completa:** Veja o [README principal](../README.md) com arquitetura, API e serviços.

---

## ⚠️ SISTEMA DE AUTENTICAÇÃO E PERMISSÕES

### Autenticação JWT com Supabase

O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:

- `access_token` (JWT, expira em 1 hora)
- `refresh_token` (para renovação automática)

### Interceptors Automáticos

```typescript
// Adiciona token automaticamente em todas as requisições
axios.interceptors.request.use((config) => {
  const token = localStorage.getItem("access_token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Refresh automático quando token expira
axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    if (error.response?.status === 401) {
      const refreshToken = localStorage.getItem("refresh_token");
      const newTokens = await authService.refreshToken(refreshToken);
      // Retry request original
    }
  }
);
```

### Roles e Permissões (RLS)

#### 👑 Admin/Gestor:

- ✅ **Acesso completo a todos os recursos**
- ✅ Criar/editar/deletar usuários, médicos, pacientes
- ✅ Visualizar todos os agendamentos e prontuários

#### 👨‍⚕️ Médicos:

- ✅ Veem **todos os pacientes**
- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`)
- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`)
- ✅ Editam apenas **seus próprios laudos e agendamentos**

#### 👤 Pacientes:

- ✅ Veem apenas **seus próprios dados**
- ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`)
- ✅ Veem apenas **seus próprios agendamentos**
- ✅ Podem agendar consultas

#### 👩‍💼 Secretárias:

- ✅ Veem **todos os pacientes**
- ✅ Veem **todos os agendamentos**
- ✅ Veem **todos os laudos**
- ✅ Podem criar/editar agendamentos

---

## 📡 API e Serviços

### Estrutura de Services

O projeto usa uma arquitetura de **services** que encapsulam toda comunicação com o Supabase:

```
src/services/
├── api/
│   ├── client.ts          # Cliente HTTP (Axios configurado)
│   └── config.ts          # Configurações da API
├── auth/
│   ├── authService.ts     # Login, signup, recuperação de senha
│   └── types.ts
├── users/
│   └── userService.ts     # getUserInfo, createUser, deleteUser
├── patients/
│   └── patientService.ts  # CRUD de pacientes
├── doctors/
│   └── doctorService.ts   # CRUD de médicos
├── appointments/
│   └── appointmentService.ts  # Agendamentos
└── availability/
    └── availabilityService.ts  # Disponibilidade médica
```

### Principais Endpoints

#### 🔐 Autenticação (authService)

```typescript
// Login
await authService.login({ email, password });
// Retorna: { access_token, refresh_token, user }

// Recuperação de senha
await authService.requestPasswordReset(email);
// Envia email com link de reset

// Atualizar senha
await authService.updatePassword(accessToken, newPassword);
// Usado na página /reset-password

// Refresh token
await authService.refreshToken(refreshToken);
```

#### 👤 Usuários (userService)

```typescript
// Buscar informações do usuário autenticado (com roles)
const userInfo = await userService.getUserInfo();
// Retorna: { id, email, full_name, roles: ['medico', 'admin'] }

// Criar usuário com role
await userService.createUser({
  email: "user@example.com",
  full_name: "Nome Completo",
  role: "medico", // ou 'admin', 'paciente', 'secretaria'
});

// Deletar usuário
await userService.deleteUser(userId);
```

#### 🏥 Pacientes (patientService)

```typescript
// Listar pacientes
const patients = await patientService.list();

// Buscar por ID
const patient = await patientService.getById(id);

// Criar paciente
await patientService.create({
  email: "paciente@example.com",
  full_name: "Nome Paciente",
  cpf: "12345678900",
  phone_mobile: "11999999999",
});

// Atualizar paciente
await patientService.update(id, { phone_mobile: "11888888888" });

// Deletar paciente
await patientService.delete(id);
```

#### 👨‍⚕️ Médicos (doctorService)

```typescript
// Listar médicos
const doctors = await doctorService.list();

// Buscar por ID
const doctor = await doctorService.getById(id);

// Buscar disponibilidade
const slots = await doctorService.getAvailableSlots(doctorId, date);
```

#### 📅 Agendamentos (appointmentService)

```typescript
// Listar agendamentos (filtrado por role automaticamente)
const appointments = await appointmentService.list();

// Criar agendamento
await appointmentService.create({
  patient_id: "uuid-paciente",
  doctor_id: "uuid-medico",
  scheduled_at: "2025-10-25T10:00:00",
  reason: "Consulta de rotina",
});

// Atualizar status
await appointmentService.updateStatus(id, "confirmed");
// Status: requested, confirmed, completed, cancelled

// Cancelar
await appointmentService.cancel(id, "Motivo do cancelamento");
```

#### 📸 Avatares (avatarService)

```typescript
// Upload de avatar (usa FormData com x-upsert: true)
const file = event.target.files[0]; // File do input
const result = await avatarService.upload({
  userId: user.id,
  file: file,
});
// Retorna: { Key: "url-publica-do-avatar" }

// Obter URL pública do avatar
const url = avatarService.getPublicUrl({
  userId: user.id,
  ext: "png", // ou 'jpg', 'webp'
});
// Retorna: https://yuanqfswhberkoevtmfr.supabase.co/storage/v1/object/avatars/{userId}/avatar.png

// Auto-load de avatar (testa múltiplas extensões)
const extensions = ["png", "jpg", "webp"];
for (const ext of extensions) {
  const url = avatarService.getPublicUrl({ userId: user.id, ext });
  const response = await fetch(url, { method: "HEAD" });
  if (response.ok) {
    setAvatarUrl(url);
    break;
  }
}
```

**Detalhes importantes:**

- Upload usa **multipart/form-data** via FormData
- Header `x-upsert: true` permite sobrescrever avatares existentes
- Suporta formatos: PNG, JPG, WEBP (máx 2MB)
- URLs públicas não requerem autenticação
- Avatar é carregado automaticamente nos painéis

---

## 🔧 Configuração da API

```typescript
// src/services/api/config.ts
export const API_CONFIG = {
  SUPABASE_URL: "https://yuanqfswhberkoevtmfr.supabase.co",
  SUPABASE_ANON_KEY: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  AUTH_URL: `${SUPABASE_URL}/auth/v1`,
  REST_URL: `${SUPABASE_URL}/rest/v1`,
  APP_URL: "https://mediconnectbrasil.app",
  TIMEOUT: 30000,
  STORAGE_KEYS: {
    ACCESS_TOKEN: "mediconnect_access_token",
    REFRESH_TOKEN: "mediconnect_refresh_token",
    USER: "mediconnect_user",
  },
};
```

---

## 🚀 Deploy no Cloudflare Pages

### Via Wrangler CLI

```powershell
# Build
pnpm build

# Deploy para production
npx wrangler pages deploy dist --project-name=mediconnect --branch=production

# Deploy com mudanças não commitadas
npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
```

### Configuração do Projeto

- **Production Branch:** `production`
- **Build Command:** `pnpm build`
- **Build Output:** `dist`
- **Custom Domain:** `mediconnectbrasil.app`

### URLs

- **Production:** https://mediconnectbrasil.app/
- **Preview:** https://mediconnect-5oz.pages.dev/
- **Branch Preview:** https://[branch].mediconnect-5oz.pages.dev/

---

## 1. Variáveis de Ambiente (`.env` / `.env.local`)

| Variável                 | Obrigatória | Descrição                                                       |
| ------------------------ | ----------- | --------------------------------------------------------------- |
| `VITE_SUPABASE_URL`      | Sim         | URL base do projeto Supabase (`https://<ref>.supabase.co`)      |
| `VITE_SUPABASE_ANON_KEY` | Sim         | Chave pública (anon) usada para Auth password grant e PostgREST |
| `VITE_APP_ENV`           | Não         | Identifica ambiente (ex: `dev`, `staging`, `prod`)              |

**Nota:** As variáveis já estão configuradas no código em `src/services/api/config.ts`. Não é necessário criar arquivo `.env` para desenvolvimento local.

Boas práticas:

- Nunca exponha Service Role Key no frontend.
- Não comitar `.env` – usar `.env.example` como referência (se houver).

---

## 2. Fluxo de Login e Validação de Roles

### Exemplo: Login de Médico

```typescript
// pages/LoginMedico.tsx
const handleLogin = async (e: FormEvent) => {
  e.preventDefault();

  try {
    // 1. Autenticar com Supabase
    const loginResponse = await authService.login({ email, password });

    // 2. Salvar tokens
    localStorage.setItem("access_token", loginResponse.access_token);
    localStorage.setItem("refresh_token", loginResponse.refresh_token);

    // 3. Buscar informações do usuário com roles
    const userInfo = await userService.getUserInfo();
    const roles = userInfo.roles || [];

    // 4. Validar permissões
    const isAdmin = roles.includes("admin");
    const isGestor = roles.includes("gestor");
    const isMedico = roles.includes("medico");

    if (!isAdmin && !isGestor && !isMedico) {
      toast.error("Você não tem permissão para acessar esta área");
      await authService.logout();
      return;
    }

    // 5. Redirecionar para o painel
    navigate("/painel-medico");
  } catch (error) {
    toast.error("Email ou senha incorretos");
  }
};
```

---

## 3. Recuperação de Senha

### Fluxo Completo

1. **Usuário clica em "Esqueceu a senha?"**
2. **Sistema envia email com link de recuperação**
3. **Usuário clica no link → redireciona para `/reset-password`**
4. **Sistema extrai token do URL (#access_token=...)**
5. **Usuário define nova senha**
6. **Sistema atualiza senha e redireciona para login**

### Implementação

```typescript
// Solicitar recuperação (páginas de login)
const handlePasswordReset = async () => {
  try {
    await authService.requestPasswordReset(email);
    toast.success("Link de recuperação enviado para seu email");
  } catch (error) {
    toast.error("Erro ao enviar email de recuperação");
  }
};

// Página de reset (ResetPassword.tsx)
useEffect(() => {
  // Extrair token do URL
  const hash = window.location.hash;
  const params = new URLSearchParams(hash.substring(1));
  const token = params.get("access_token");

  if (token) {
    setAccessToken(token);
    setIsLoading(false);
  }
}, []);

const handleSubmit = async (e: FormEvent) => {
  try {
    await authService.updatePassword(accessToken, newPassword);
    toast.success("Senha atualizada com sucesso!");
    navigate("/login-paciente");
  } catch (error) {
    toast.error("Erro ao atualizar senha");
  }
};
```

---

## 5. Estrutura do Banco de Dados

### Tabelas Principais

#### `profiles`

```sql
- id (uuid, PK)
- email (text, unique)
- full_name (text)
- phone (text)
- created_at (timestamp)
- updated_at (timestamp)
```

#### `patients`

```sql
- id (uuid, PK, FK -> profiles)
- email (text, unique)
- full_name (text)
- cpf (text, unique)
- phone_mobile (text)
- birth_date (date)
- address (text)
- created_at (timestamp)
```

#### `doctors`

```sql
- id (uuid, PK, FK -> profiles)
- email (text, unique)
- full_name (text)
- specialty (text)
- crm (text, unique)
- phone (text)
- created_at (timestamp)
```

#### `appointments`

```sql
- id (uuid, PK)
- patient_id (uuid, FK -> patients)
- doctor_id (uuid, FK -> doctors)
- scheduled_at (timestamp)
- status (enum: requested, confirmed, completed, cancelled)
- reason (text)
- notes (text)
- created_at (timestamp)
```

#### `user_roles`

```sql
- user_id (uuid, FK -> profiles)
- role (text: admin, gestor, medico, secretaria, paciente)
- created_at (timestamp)
```

---

## 6. Armazenamento de Tokens

O sistema usa uma estratégia híbrida para máxima segurança:

| Tipo          | Local        | Expiração Natural                 |
| ------------- | ------------ | --------------------------------- |
| Access Token  | localStorage | 1 hora (renovado automaticamente) |
| Refresh Token | localStorage | 30 dias (ou revogação backend)    |
| User Snapshot | localStorage | Limpo em logout                   |

**Segurança:**

- Tokens são limpos automaticamente no logout
- Refresh automático quando access_token expira (401)
- Interceptors garantem tokens válidos em todas as requisições

Riscos remanescentes:

- XSS ainda pode ler refresh token dentro da mesma aba.
- Ataques supply-chain podem capturar tokens em runtime.

Mitigações planejadas:

1. CSP + bloqueio de inline script não autorizado.
2. Auditoria de dependências e lockfile imutável.
3. (Opcional) Migrar refresh para cookie httpOnly + rotacionamento curto (exige backend/proxy).

Fallback / Migração:

- Em primeira utilização o `tokenStore` migra chaves legacy (`authToken`, `refreshToken`, `authUser`) e remove do `localStorage`.

Operações:

- `tokenStore.setTokens(access, refresh?)` atualiza memória e session.
- `tokenStore.clear()` remove tudo (usado em logout e erro crítico de refresh).

Fluxo de Refresh:

1. Requisição falha com 401.
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.

---

## 7. Scripts Utilitários

### Gerenciamento de Usuários

```bash
# Listar todos os usuários
node scripts/manage-users.js list

# Criar usuário
node scripts/manage-users.js create email@example.com "Nome Completo"

# Deletar usuário
node scripts/manage-users.js delete user-id

# Limpar usuários de teste
node scripts/cleanup-users.js
```

### Testes de API

```bash
# Testar recuperação de senha
node test-password-recovery.js

# Criar usuário Fernando (exemplo)
node create-fernando.cjs

# Buscar usuário Fernando
node search-fernando.cjs
```

---

## 8. Padrões de Código

### Nomenclatura

- **Componentes:** PascalCase (`LoginPaciente.tsx`)
- **Serviços:** camelCase (`authService.ts`)
- **Hooks:** camelCase com prefixo `use` (`useAuth.ts`)
- **Tipos:** PascalCase (`LoginInput`, `AuthUser`)

### Estrutura de Componentes

```typescript
// Imports
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { serviceImport } from "../services";

// Types
interface Props {
  // ...
}

// Component
const ComponentName: React.FC<Props> = ({ ...props }) => {
  // Hooks
  const navigate = useNavigate();
  const [state, setState] = useState();

  // Effects
  useEffect(() => {
    // ...
  }, []);

  // Handlers
  const handleAction = async () => {
    // ...
  };

  // Render
  return <div>{/* JSX */}</div>;
};

export default ComponentName;
```

---

## 9. Tecnologias Utilizadas

### Frontend

- **React** 18.3.1 - Biblioteca UI
- **TypeScript** 5.9.3 - Tipagem estática
- **Vite** 7.1.10 - Build tool
- **React Router** 6.30.1 - Roteamento
- **Tailwind CSS** 3.4.17 - Estilização
- **Axios** 1.12.2 - Cliente HTTP
- **React Hot Toast** 2.4.1 - Notificações
- **date-fns** 4.1.0 - Manipulação de datas

### Backend

- **Supabase** - Backend as a Service
- **PostgreSQL** - Banco de dados relacional
- **Supabase Auth** - Autenticação JWT

### Deploy

- **Cloudflare Pages** - Hospedagem frontend
- **Wrangler** 4.44.0 - CLI Cloudflare

---

## 10. Suporte e Contato

- **Equipe:** Squad 18 - Rise Up
- **Repositório:** https://git.popcode.com.br/RiseUP/riseup-squad18.git
- **Trello:** [Squad 18 - Idealização/Planejamento](https://trello.com/b/CCl3Azxk/squad-18-idealizacao-planejamento)

---

**Desenvolvido com ❤️ pela Squad 18**
