# Prompt — Claude Code: MVP Plataforma ONG
# Stack: PHP 8.2 / MySQL 8.0 / Laravel 11
# Hospedagem: VPS Hostgator | Multi-tenant desde o início | API-ready para mobile

---

## LEIA ANTES DE COMEÇAR

Este é um projeto real que vai para produção numa VPS Hostgator com PHP 8.2 e MySQL 8.0.
A arquitetura precisa estar correta desde o início — multi-tenancy, segurança e API REST
são requisitos estruturais, não features futuras.

Trabalhe em etapas. Ao concluir cada Tarefa, informe o que foi feito e aguarde
confirmação antes de avançar para a próxima, exceto se eu disser "execute tudo".

---

## 1. CONTEXTO DO PRODUTO

Plataforma SaaS de gestão para ONGs brasileiras.
Múltiplas ONGs (tenants) compartilham a mesma instalação.
Cada ONG tem seus próprios usuários, beneficiários, projetos e dados — completamente isolados.

**Fase 1 — MVP (este prompt):**
- Módulo A: Cadastro e gestão de beneficiários
- Módulo B: Projetos e atividades
- Módulo C: App offline / PWA para uso em campo
- Módulo D: Geração de relatórios em PDF

**Futuro (não implementar agora, mas arquitetura deve suportar):**
- App mobile nativo (React Native ou Flutter) consumindo a mesma API REST
- Módulo financeiro
- Módulo de doadores e captação
- WhatsApp integrado

---

## 2. STACK E DEPENDÊNCIAS

### Core
```
PHP 8.2+
Laravel 11
MySQL 8.0
Nginx (a VPS usa Nginx — não Apache)
```

### Composer (instale via `composer require` se não existir)
```
laravel/breeze                  # autenticação base
laravel/sanctum                 # tokens de API para mobile futuro
barryvdh/laravel-dompdf         # geração de PDF
spatie/laravel-permission        # controle de perfis e permissões
intervention/image               # redimensionamento de fotos de beneficiários
```

### NPM
```
tailwindcss
alpinejs
chart.js
workbox-window                   # PWA / Service Worker
```

### PHP extensions necessárias (verificar disponibilidade na VPS)
```
php-gd, php-intl, php-mbstring, php-xml, php-zip, php-mysql
```

Gere um arquivo `SETUP.md` com as instruções de configuração para a VPS Hostgator.

---

## 3. ARQUITETURA MULTI-TENANT

### Estratégia: Single Database com tenant_id em todas as tabelas

Escolha deliberada para simplificar backup, migração e manutenção numa VPS única.
Isolamento garantido por middleware e global scopes — não por banco separado.

### Regras invioláveis de multi-tenancy

1. **Toda tabela de dados** tem coluna `tenant_id (FK → tenants.id, NOT NULL)`
2. **Todo Model** aplica um Global Scope filtrando pelo tenant do usuário autenticado
3. **Todo Controller** nunca confia no `tenant_id` vindo do request — sempre usa `auth()->user()->tenant_id`
4. **Todo relacionamento** valida que os registros relacionados pertencem ao mesmo tenant
5. **Toda query direta** (DB::table) inclui `->where('tenant_id', currentTenantId())`
6. **API REST** valida tenant via token Sanctum — o token carrega o `tenant_id`

### Helper global
Crie `app/Helpers/Tenant.php` com função `currentTenantId()`:
```php
function currentTenantId(): int {
    return auth()->user()?->tenant_id
        ?? request()->user()?->tenant_id
        ?? throw new \RuntimeException('Tenant não identificado');
}
```

Registre em `composer.json > autoload > files`.

### TenantScope (Global Scope reutilizável)
Crie `app/Scopes/TenantScope.php`:
```php
class TenantScope implements Scope {
    public function apply(Builder $builder, Model $model): void {
        $builder->where($model->getTable().'.tenant_id', currentTenantId());
    }
}
```

Aplique via trait em todos os models:
```php
trait BelongsToTenant {
    protected static function bootBelongsToTenant(): void {
        static::addGlobalScope(new TenantScope());
        static::creating(function ($model) {
            $model->tenant_id ??= currentTenantId();
        });
    }
}
```

---

## 4. BANCO DE DADOS — MIGRATIONS

Crie na ordem exata abaixo (respeitar FKs).

### 4.1 `tenants`
```sql
id                  BIGINT UNSIGNED AUTO_INCREMENT PK
nome                VARCHAR(150) NOT NULL
slug                VARCHAR(100) UNIQUE NOT NULL        -- usado na URL: app.dominio.com/ong-slug
cnpj                VARCHAR(18) NULLABLE UNIQUE
email_contato       VARCHAR(150)
telefone            VARCHAR(20)
logo_path           VARCHAR(255) NULLABLE
plano               ENUM('gratuito','basico','profissional') DEFAULT 'gratuito'
ativo               TINYINT(1) DEFAULT 1
configuracoes       JSON NULLABLE                       -- configs específicas da ONG (cores, campos custom)
created_at, updated_at
```

### 4.2 `users`
```sql
id                  BIGINT UNSIGNED AUTO_INCREMENT PK
tenant_id           FK → tenants.id
name                VARCHAR(150) NOT NULL
email               VARCHAR(150) UNIQUE NOT NULL
password            VARCHAR(255)
perfil              ENUM('super_admin','admin','gestor','tecnico','educador')
avatar_path         VARCHAR(255) NULLABLE
ativo               TINYINT(1) DEFAULT 1
ultimo_acesso       DATETIME NULLABLE
email_verified_at   DATETIME NULLABLE
remember_token      VARCHAR(100)
created_at, updated_at
```

### 4.3 `beneficiarios`
```sql
id                  BIGINT UNSIGNED AUTO_INCREMENT PK
tenant_id           FK → tenants.id
uuid                CHAR(36) UNIQUE NOT NULL             -- exposto na URL em vez do id

-- Identificação
nome_completo       VARCHAR(200) NOT NULL
nome_social         VARCHAR(200) NULLABLE
cpf                 VARCHAR(14) NULLABLE                 -- único por tenant (unique index composto)
data_nascimento     DATE NOT NULL
genero              ENUM('masculino','feminino','nao_binario','prefiro_nao_informar')
cor_raca            ENUM('branca','preta','parda','amarela','indigena','nao_informado')
foto_path           VARCHAR(255) NULLABLE
documento_tipo      ENUM('rg','certidao_nascimento','passaporte','cnh','nenhum') DEFAULT 'nenhum'
documento_numero    VARCHAR(50) NULLABLE

-- Contato
cep                 VARCHAR(9) NULLABLE
logradouro          VARCHAR(200) NULLABLE
numero              VARCHAR(10) NULLABLE
complemento         VARCHAR(100) NULLABLE
bairro              VARCHAR(100) NULLABLE
cidade              VARCHAR(100) NULLABLE
estado              CHAR(2) NULLABLE
telefone            VARCHAR(20) NULLABLE
whatsapp            VARCHAR(20) NULLABLE
email               VARCHAR(150) NULLABLE

-- Socioeconômico
escolaridade        ENUM('sem_escolaridade','fund_incompleto','fund_completo',
                         'medio_incompleto','medio_completo','superior_incompleto',
                         'superior_completo','pos_graduacao') NULLABLE
faixa_renda         ENUM('ate_1sm','1_a_2sm','2_a_3sm','3_a_5sm','acima_5sm') NULLABLE
situacao_trabalho   ENUM('empregado','desempregado','autonomo','aposentado',
                         'estudante','nao_informado') NULLABLE
pessoas_na_casa     TINYINT UNSIGNED NULLABLE
tipo_moradia        ENUM('propria','alugada','cedida','irregular','nao_informado') NULLABLE
cadunico            VARCHAR(30) NULLABLE
beneficios_sociais  TEXT NULLABLE

-- Saúde (dados sensíveis — LGPD)
pcd                 TINYINT(1) DEFAULT 0
tipo_deficiencia    VARCHAR(200) NULLABLE
condicao_saude      TEXT NULLABLE

-- Gestão interna
status              ENUM('ativo','inativo','em_espera','encerrado') DEFAULT 'ativo'
data_entrada        DATE NOT NULL
data_saida          DATE NULLABLE
motivo_saida        VARCHAR(200) NULLABLE
canal_captacao      VARCHAR(100) NULLABLE
responsavel_id      FK → users.id NULLABLE
observacoes         TEXT NULLABLE
lgpd_aceito         TINYINT(1) DEFAULT 0
lgpd_aceito_em      DATETIME NULLABLE

created_at, updated_at, deleted_at  -- softDeletes

INDEX: (tenant_id, cpf) UNIQUE WHERE cpf IS NOT NULL
INDEX: (tenant_id, status)
INDEX: (tenant_id, responsavel_id)
FULLTEXT INDEX: (nome_completo)      -- para busca rápida
```

### 4.4 `projetos`
```sql
id, tenant_id, uuid CHAR(36) UNIQUE,
nome                VARCHAR(200) NOT NULL
descricao           TEXT NULLABLE
objetivo            TEXT NULLABLE
publico_alvo        VARCHAR(200) NULLABLE
data_inicio         DATE NOT NULL
data_fim            DATE NULLABLE
status              ENUM('planejado','em_andamento','concluido','suspenso') DEFAULT 'planejado'
responsavel_id      FK → users.id
meta_beneficiarios  INT UNSIGNED NULLABLE
meta_atendimentos   INT UNSIGNED NULLABLE
carga_horaria_meta  INT UNSIGNED NULLABLE               -- em horas
fonte_recurso       VARCHAR(200) NULLABLE
cor                 VARCHAR(7) DEFAULT '#7F77DD'         -- cor do projeto no dashboard
created_at, updated_at, deleted_at
```

### 4.5 `beneficiario_projeto` (pivot)
```sql
id, tenant_id,
beneficiario_id     FK → beneficiarios.id
projeto_id          FK → projetos.id
data_vinculo        DATE NOT NULL
status_vinculo      ENUM('ativo','encerrado') DEFAULT 'ativo'
vinculado_por       FK → users.id
observacao          TEXT NULLABLE
created_at, updated_at
UNIQUE: (beneficiario_id, projeto_id)
```

### 4.6 `atividades`
```sql
id, tenant_id, uuid CHAR(36) UNIQUE,
projeto_id          FK → projetos.id
nome                VARCHAR(200) NOT NULL
descricao           TEXT NULLABLE
tipo                ENUM('oficina','aula','grupo','evento','atendimento_coletivo','reuniao')
data_hora_inicio    DATETIME NOT NULL
data_hora_fim       DATETIME NOT NULL
local               VARCHAR(200) NULLABLE
responsavel_id      FK → users.id
max_participantes   SMALLINT UNSIGNED NULLABLE
status              ENUM('agendada','realizada','cancelada') DEFAULT 'agendada'
created_at, updated_at, deleted_at
INDEX: (tenant_id, projeto_id, data_hora_inicio)
```

### 4.7 `presencas`
```sql
id, tenant_id,
atividade_id        FK → atividades.id
beneficiario_id     FK → beneficiarios.id
presente            TINYINT(1) NOT NULL DEFAULT 0
justificativa       VARCHAR(200) NULLABLE
registrado_por      FK → users.id
registrado_em       DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
offline_uuid        CHAR(36) NULLABLE UNIQUE            -- chave de idempotência offline
sincronizado        TINYINT(1) DEFAULT 1
created_at, updated_at
UNIQUE: (atividade_id, beneficiario_id)                 -- um registro por beneficiário por atividade
```

### 4.8 `atendimentos`
```sql
id, tenant_id, uuid CHAR(36) UNIQUE,
beneficiario_id     FK → beneficiarios.id
responsavel_id      FK → users.id
projeto_id          FK → projetos.id NULLABLE
tipo                ENUM('individual','grupo','visita_domiciliar',
                         'encaminhamento','telefonico','online')
data_hora           DATETIME NOT NULL
duracao_minutos     SMALLINT UNSIGNED NULLABLE
descricao           TEXT NOT NULL
encaminhamento_para VARCHAR(200) NULLABLE
status_encaminhamento ENUM('pendente','realizado','nao_realizado') NULLABLE
anexo_path          VARCHAR(255) NULLABLE
offline_uuid        CHAR(36) NULLABLE UNIQUE
created_at, updated_at
INDEX: (tenant_id, beneficiario_id, data_hora)
```

### 4.9 `audit_logs` (LGPD)
```sql
id, tenant_id,
user_id             FK → users.id NULLABLE
acao                VARCHAR(100) NOT NULL               -- ex: 'beneficiario.visualizado'
modelo              VARCHAR(100) NOT NULL               -- ex: 'Beneficiario'
modelo_id           BIGINT UNSIGNED NULLABLE
dados_anteriores    JSON NULLABLE
dados_novos         JSON NULLABLE
ip                  VARCHAR(45) NULLABLE
user_agent          VARCHAR(255) NULLABLE
created_at
-- SEM updated_at — log é imutável
```

### 4.10 `personal_access_tokens` (Sanctum — já vem com o pacote)

---

## 5. MODELS

Para cada model, implemente:

```php
// Exemplo completo — Beneficiario.php
class Beneficiario extends Model {
    use SoftDeletes, BelongsToTenant, HasUuids;   // HasUuids auto-gera uuid

    protected $fillable = [...];  // todos os campos exceto id, tenant_id, uuid

    protected $casts = [
        'data_nascimento' => 'date',
        'lgpd_aceito' => 'boolean',
        'pcd' => 'boolean',
        'data_entrada' => 'date',
        'data_saida' => 'date',
        'lgpd_aceito_em' => 'datetime',
    ];

    // Nunca expor id nas URLs — usar uuid
    public function getRouteKeyName(): string { return 'uuid'; }

    // Relacionamentos
    public function projetos(): BelongsToMany { ... }
    public function atendimentos(): HasMany { ... }
    public function presencas(): HasMany { ... }
    public function responsavel(): BelongsTo { ... }
    public function tenant(): BelongsTo { ... }

    // Accessors
    public function getIdadeAttribute(): int {
        return $this->data_nascimento->age;
    }

    public function getNomeAbreviadoAttribute(): string {
        $partes = explode(' ', $this->nome_completo);
        return count($partes) > 1
            ? $partes[0].' '.end($partes)
            : $partes[0];
    }

    // Scopes
    public function scopeAtivos($q) { return $q->where('status', 'ativo'); }
    public function scopeBusca($q, string $termo) {
        return $q->whereFullText('nome_completo', $termo)
                 ->orWhere('cpf', 'like', "%{$termo}%");
    }
}
```

Crie models para: `Tenant`, `User`, `Beneficiario`, `Projeto`, `Atividade`, `Presenca`, `Atendimento`, `AuditLog`.

---

## 6. MIDDLEWARES

### TenantMiddleware
```php
// Verifica autenticação + tenant válido
// Injeta tenant na view global: view()->share('tenant', currentTenant())
// Registra no grupo 'web' e 'api'
```

### AuditMiddleware
```php
// Para rotas de dados sensíveis (beneficiários com dados de saúde)
// Registra em audit_logs: quem acessou, qual registro, quando, IP
```

Registre ambos em `bootstrap/app.php`.

---

## 7. API REST (para mobile futuro)

Configure Laravel Sanctum. Crie prefixo `/api/v1/` com middleware `auth:sanctum`.

### Endpoints obrigatórios na Fase 1

```
POST   /api/v1/auth/login              # retorna token Sanctum
POST   /api/v1/auth/logout
GET    /api/v1/auth/me

GET    /api/v1/beneficiarios           # lista paginada, filtros via query string
POST   /api/v1/beneficiarios           # criar
GET    /api/v1/beneficiarios/{uuid}    # detalhe
PUT    /api/v1/beneficiarios/{uuid}    # atualizar
GET    /api/v1/beneficiarios/{uuid}/atendimentos

GET    /api/v1/projetos
GET    /api/v1/projetos/{uuid}
GET    /api/v1/projetos/{uuid}/atividades

GET    /api/v1/atividades/{uuid}/presenca
POST   /api/v1/atividades/{uuid}/presenca

# Sincronização offline
POST   /api/v1/sync/presencas          # array de registros com offline_uuid
POST   /api/v1/sync/atendimentos

# Cache para uso offline
GET    /api/v1/beneficiarios/cache-export   # retorna beneficiários do tenant em JSON compacto
```

### Padrão de resposta da API
```json
{
  "success": true,
  "data": { ... },
  "meta": { "page": 1, "per_page": 20, "total": 150 },
  "message": null
}
```

Crie `app/Http/Resources/` com API Resources para cada model.
Crie `app/Http/Responses/ApiResponse.php` como helper de resposta padrão.

---

## 8. CONTROLLERS E ROTAS WEB

### Rotas (web.php)
```php
Route::middleware(['auth', 'tenant'])->group(function () {

    Route::get('/', fn() => redirect('/dashboard'));
    Route::get('/dashboard', [DashboardController::class, 'index']);

    // Beneficiários — usar UUID nas URLs
    Route::resource('beneficiarios', BeneficiarioController::class)
         ->parameters(['beneficiarios' => 'beneficiario:uuid']);
    Route::get('beneficiarios/{beneficiario:uuid}/atendimentos/novo',
         [AtendimentoController::class, 'create']);
    Route::post('beneficiarios/{beneficiario:uuid}/atendimentos',
         [AtendimentoController::class, 'store']);

    // Projetos
    Route::resource('projetos', ProjetoController::class)
         ->parameters(['projetos' => 'projeto:uuid']);
    Route::get('projetos/{projeto:uuid}/dashboard',
         [ProjetoController::class, 'dashboard']);
    Route::post('projetos/{projeto:uuid}/beneficiarios/{beneficiario:uuid}',
         [ProjetoController::class, 'vincularBeneficiario']);

    // Atividades e presenças
    Route::resource('atividades', AtividadeController::class)
         ->parameters(['atividades' => 'atividade:uuid']);
    Route::get('atividades/{atividade:uuid}/presenca',
         [PresencaController::class, 'index']);
    Route::post('atividades/{atividade:uuid}/presenca',
         [PresencaController::class, 'store']);

    // Relatórios
    Route::get('relatorios', [RelatorioController::class, 'index']);
    Route::get('relatorios/projeto/{projeto:uuid}',
         [RelatorioController::class, 'projeto']);
    Route::get('relatorios/beneficiarios',
         [RelatorioController::class, 'beneficiarios']);

    // AJAX helpers
    Route::get('api-web/check-cpf',
         [BeneficiarioController::class, 'checkCpf']);
    Route::get('api-web/busca-cep/{cep}',
         [BeneficiarioController::class, 'buscaCep']);
});
```

---

## 9. VIEWS (Blade + Alpine.js + Tailwind)

### Paleta de cores (configure no tailwind.config.js)
```js
colors: {
  primary: { DEFAULT: '#7F77DD', dark: '#534AB7', light: '#EEEDFE' },
  teal:    { DEFAULT: '#1D9E75', dark: '#0F6E56', light: '#E1F5EE' },
  amber:   { DEFAULT: '#BA7517', dark: '#854F0B', light: '#FAEEDA' },
}
```

### Layout principal (`layouts/app.blade.php`)
- Sidebar fixa 240px: logo da ONG (do tenant), nav com ícones, collapse no mobile
- Header: breadcrumb, avatar + nome do usuário, badge de notificações
- Main content com padding adequado
- Footer mínimo com versão
- Variáveis disponíveis em todas as views: `$tenant`, `$authUser`

### Tela 1: `/beneficiarios` — Lista
- Busca em tempo real (Alpine.js + fetch para `/api-web/busca`)
- Filtros colapsáveis: status, projeto, responsável, data de entrada
- Tabela responsiva: avatar/iniciais | nome | projetos (badges) | status | responsável | ações
- Status com badge colorido: ativo=verde, inativo=cinza, em_espera=amarelo, encerrado=vermelho
- Paginação de 20/página
- Botão "Novo beneficiário" fixo no canto inferior direito no mobile

### Tela 2: `/beneficiarios/novo` — Wizard de cadastro
Alpine.js com `x-data="{ etapa: 1, form: {} }"`. 3 etapas com barra de progresso:

**Etapa 1 — Quem é?**
- Nome completo (obrigatório)
- CPF com máscara (##.###.###-##) + verificação AJAX de duplicata
- Data de nascimento + cálculo automático de idade exibido
- Gênero (radio estilizado)
- Cor/raça (dropdown)
- Upload de foto com preview imediato

**Etapa 2 — Onde mora e qual o perfil?**
- CEP com autopreenchimento via ViaCEP (`https://viacep.com.br/ws/{cep}/json/`)
- Telefone e WhatsApp com máscara
- Escolaridade, faixa de renda, situação de trabalho (selects)
- Nº de pessoas na casa, tipo de moradia

**Etapa 3 — Vinculação e LGPD**
- Selecionar projeto(s) com checkboxes
- Responsável técnico (select dos users do tenant)
- Canal de captação
- Aceite do termo LGPD (checkbox obrigatório com texto do termo inline)
- Botão "Cadastrar" — desabilitado até LGPD marcado

### Tela 3: `/beneficiarios/{uuid}` — Perfil
- Cabeçalho: foto grande, nome, idade, status badge, projeto(s)
- Abas (Alpine.js): Dados pessoais | Atendimentos | Projetos | Documentos
- **Aba Atendimentos:** linha do tempo cronológica reversa, cada item com: data, tipo, responsável, descrição, encaminhamento
- Botão flutuante "Registrar atendimento" (abre modal inline — não usar `position:fixed` em contextos de iframe)
- Seção de dados sensíveis (saúde) protegida: só visível para perfis técnico/gestor/admin

### Tela 4: `/projetos/{uuid}/dashboard`
4 cards métricas no topo:
- Beneficiários ativos no projeto
- % da meta de beneficiários atingida
- Atendimentos no mês atual
- Presença média nas últimas 4 semanas

Gráfico de linha (Chart.js via CDN): frequência diária dos últimos 30 dias.
Lista de próximas atividades (5 mais próximas).
Lista de beneficiários recentes vinculados.

### Tela 5: `/atividades/{uuid}/presenca` — Lista de presença
- Cabeçalho: nome da atividade, data, local, responsável
- Contador em tempo real: "X de Y presentes" (Alpine.js)
- Lista de beneficiários do projeto ordenada por nome
- Toggle presente/ausente por beneficiário (salva via fetch AJAX instantaneamente)
- Campo de justificativa para ausência (expansível ao marcar ausente)
- Botão "Exportar lista de presença" → PDF
- **Indicador offline:** badge amarelo "Modo offline — X registros pendentes"

---

## 10. PWA E FUNCIONALIDADE OFFLINE

### manifest.json (`public/manifest.json`)
```json
{
  "name": "Plataforma ONG",
  "short_name": "ONGApp",
  "description": "Gestão de beneficiários e atividades",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#7F77DD",
  "lang": "pt-BR",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/icon-512-maskable.png", "sizes": "512x512",
      "type": "image/png", "purpose": "maskable" }
  ]
}
```

Crie ícones placeholder (pode ser um SVG simples convertido) nas 3 resoluções.

### Service Worker (`public/sw.js`)
Implemente com Workbox via CDN (`importScripts`):

```js
// Cache first para assets estáticos (CSS, JS, imagens, fontes)
// Network first com fallback para páginas da aplicação
// Background Sync para /api/v1/sync/presencas e /api/v1/sync/atendimentos
// Estratégia: se falhar, enfileira no Background Sync Queue 'ong-sync'
```

### Módulo offline (`resources/js/offline.js`)
Implemente como classe ES6:

```js
class OfflineManager {
    // IndexedDB: banco 'ong_offline', stores: 'presencas_pendentes', 'atendimentos_pendentes', 'beneficiarios_cache'

    async carregarBeneficiarios(projetoUuid)  // busca /api/v1/beneficiarios/cache-export e salva localmente
    async registrarPresenca(dados)             // salva no IndexedDB + gera offline_uuid (crypto.randomUUID)
    async sincronizar()                         // envia pendentes para a API, remove os bem-sucedidos
    async getPendentes()                        // retorna contagem de registros pendentes
    isOnline()                                  // navigator.onLine
}

// Event listeners
window.addEventListener('online', () => offlineManager.sincronizar())
window.addEventListener('offline', () => mostrarBannerOffline())
```

### Endpoint de sync (idempotente)
```php
// POST /api/v1/sync/presencas
// Recebe: [ { offline_uuid, atividade_id, beneficiario_id, presente, registrado_em }, ... ]
// Para cada item:
//   - Se offline_uuid já existe na tabela: ignora (idempotência)
//   - Se não existe: insere
// Retorna: { salvos: N, ignorados: M, erros: [] }
```

---

## 11. GERAÇÃO DE RELATÓRIO PDF

Instalar: `composer require barryvdh/laravel-dompdf`

### RelatorioController@projeto
Parâmetros via query string: `?periodo_inicio=2024-01-01&periodo_fim=2024-03-31`

Gerar PDF com view Blade `resources/views/relatorios/projeto.blade.php`:

**Estrutura do PDF:**
1. Cabeçalho: logo da ONG + nome do projeto + período + data de geração
2. Sumário executivo (4 números grandes): beneficiários atendidos, total de atendimentos, horas de atividade, presença média
3. Perfil dos atendidos: tabelas com distribuição de gênero, escolaridade, faixa de renda
4. Frequência por atividade: tabela com nome da atividade, data, presentes/total, %
5. Atendimentos por tipo e por responsável
6. Rodapé: "Gerado em {data} · Plataforma ONG · {nome da ONG}"

Salvar em `storage/app/relatorios/{tenant_id}/` com nome único.
Retornar como download (`Content-Disposition: attachment`).

---

## 12. PERMISSÕES E PERFIS (Spatie)

```php
// Perfis e o que cada um pode fazer:

super_admin  → tudo (cross-tenant — usado pela Yby para suporte)
admin        → tudo dentro do seu tenant
gestor       → CRUD de projetos, atividades, relatórios; ver todos beneficiários
tecnico      → ver/editar beneficiários do seu projeto; registrar atendimentos
educador     → apenas lista de presença e cadastro básico de beneficiário

// Permissões granulares a criar:
beneficiario.criar        beneficiario.ver         beneficiario.editar
beneficiario.ver_saude    beneficiario.excluir
projeto.criar             projeto.ver              projeto.editar
atividade.criar           presenca.registrar
relatorio.gerar           audit_log.ver
```

Crie seeder de permissões em `PermissaoSeeder.php`.
Aplique `$this->middleware('can:permissao')` nos controllers.

---

## 13. DEDUPLICAÇÃO DE BENEFICIÁRIOS

### Verificação em tempo real (no wizard)
```php
// GET /api-web/check-cpf?cpf=123.456.789-00
// Retorna: { existe: true, uuid: 'xxx', nome: 'João Silva' }
// ou:      { existe: false }
// Filtra SEMPRE pelo tenant_id do usuário autenticado
```

### Detecção de nomes similares (no BeneficiarioObserver)
```php
// Ao criar um beneficiário, verificar similar_text() >= 85% com todos os nomes do tenant
// Se encontrar, adicionar flag 'possivel_duplicata' na tabela (coluna booleana)
// Exibir badge "Verificar duplicata" na listagem
```

---

## 14. SEEDERS PARA DESENVOLVIMENTO

### TenantSeeder
- 2 tenants: "Instituto Esperança" (slug: instituto-esperanca) e "ONG Futuro" (slug: ong-futuro)

### UserSeeder (por tenant)
- admin@instituto-esperanca.com.br / password → perfil: admin
- gestor@instituto-esperanca.com.br / password → perfil: gestor
- tecnico@instituto-esperanca.com.br / password → perfil: tecnico
- educador@instituto-esperanca.com.br / password → perfil: educador
- (mesma estrutura para ONG Futuro)

### BeneficiarioSeeder
- 60 beneficiários para "Instituto Esperança" (Faker pt_BR)
- 30 beneficiários para "ONG Futuro"
- Variedade real nos campos socioeconômicos

### ProjetoSeeder + AtividadeSeeder + PresencaSeeder + AtendimentoSeeder
- 3 projetos por tenant, com atividades dos últimos 60 dias
- Presenças e atendimentos realistas para preencher dashboards

Execute: `php artisan migrate:fresh --seed`

---

## 15. CONFIGURAÇÕES DE PRODUÇÃO (VPS HOSTGATOR)

Gere o arquivo `DEPLOY.md` com:

### Nginx — configuração do vhost
```nginx
server {
    listen 80;
    server_name seudominio.com.br www.seudominio.com.br;
    root /var/www/ong-plataforma/public;
    index index.php;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```

### .env.production (template — sem valores reais)
```env
APP_NAME="Plataforma ONG"
APP_ENV=production
APP_DEBUG=false
APP_URL=https://seudominio.com.br

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ong_plataforma
DB_USERNAME=
DB_PASSWORD=

CACHE_DRIVER=file       # VPS sem Redis no início — usar file
QUEUE_CONNECTION=sync   # sem fila assíncrona no MVP
SESSION_DRIVER=file
SESSION_LIFETIME=480    # 8 horas

SANCTUM_STATEFUL_DOMAINS=seudominio.com.br

FILESYSTEM_DISK=local
```

### Script de deploy (`deploy.sh`)
```bash
#!/bin/bash
set -e
git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan storage:link
chmod -R 775 storage bootstrap/cache
chown -R www-data:www-data storage bootstrap/cache
echo "Deploy concluído — $(date)"
```

### Considerações específicas da Hostgator VPS
- Usar `CACHE_DRIVER=file` (sem Redis)
- Usar `QUEUE_CONNECTION=sync` (sem Supervisor no início)
- Logs em `storage/logs/` — configurar rotação com logrotate
- PHP-FPM: ajustar `pm.max_children` conforme RAM disponível
- MySQL: `innodb_buffer_pool_size` = 70% da RAM disponível
- Habilitar `opcache` no php.ini

---

## 16. TESTES BÁSICOS

Crie testes de feature para os cenários críticos de segurança:

```php
// TenantIsolationTest.php
// Testa que usuário do tenant A não consegue acessar dados do tenant B
// Testa em: beneficiários, projetos, atividades, atendimentos

// BeneficiarioTest.php
// Testa criação, busca, deduplicação por CPF, filtros

// PresencaOfflineTest.php
// Testa endpoint de sync com offline_uuid duplicado (idempotência)
// Testa sync de múltiplos registros em uma chamada
```

Execute: `php artisan test`

---

## 17. ORDEM DE EXECUÇÃO

Execute nesta ordem exata:

```bash
# 1. Instalar dependências
composer install
npm install

# 2. Configurar ambiente
cp .env.example .env
php artisan key:generate
# ⚠️ Configure DB_* no .env antes de continuar

# 3. Banco de dados
php artisan migrate
php artisan db:seed

# 4. Assets
npm run build

# 5. Storage
php artisan storage:link

# 6. Permissões (Spatie)
php artisan permission:cache-reset

# 7. Verificar
php artisan route:list
php artisan test

# 8. Servidor local
php artisan serve
```

---

## 18. AO FINALIZAR — RELATÓRIO

Ao concluir cada tarefa, mostre:

1. Arquivos criados ou modificados (lista)
2. Comandos executados e resultado
3. Qualquer decisão técnica tomada e o motivo
4. Pendências que precisam de confirmação minha
5. Próxima tarefa que vai executar

Ao finalizar tudo:
- URL local: `http://localhost:8000`
- Credenciais admin: admin@instituto-esperanca.com.br / password
- Resumo de rotas: `php artisan route:list --columns=method,uri,name`
- Lista de features prontas vs pendentes

---

*Projeto: Plataforma SaaS para ONGs — Yby Comunicação e Marketing*
*Fase 1 MVP | Stack: PHP 8.2 + Laravel 11 + MySQL 8.0*
*Hospedagem: VPS Hostgator | Multi-tenant | API-ready para mobile*
