# Carta de Serviços — Plano de Implementação (Fases 2 e 3)

> Complemento de `06-MIGRACAO-MODERNA.md` (schema, models, requests).

## 5. Policies / autorização

```php
// app/Policies/ServiceLetterPolicy.php
class ServiceLetterPolicy
{
    public function viewAny(User $user): bool { return $user->can('carta-de-servicos.index'); }
    public function view(User $user, ServiceLetter $s): bool { return true; }        // leitura no portal é pública
    public function create(User $user): bool { return $user->can('carta-de-servicos.create'); }
    public function update(User $user, ServiceLetter $s): bool { return $user->can('carta-de-servicos.edit'); }
    public function delete(User $user, ServiceLetter $s): bool { return $user->can('carta-de-servicos.delete'); }
    public function generatePdf(User $user): bool { return $user->can('carta-de-servicos.pdf'); }
}
```

- Registrar em `AuthServiceProvider::policies` / `Gate::policy`.
- Substituir `has_access(['carta-de-servicos.index'])` nos blades por `@can('carta-de-servicos.index')` ou `Gate::allows(...)`.
- Criar as permissões no seeder de papéis (hoje **nenhum papel** tem chaves `carta-de-servicos.*`).

## 6. Endpoints REST/JSON sugeridos

### 6.1 Portal (público)

| Método | URI | Descrição |
|---|---|---|
| GET | `/api/carta-de-servicos/assuntos` | Lista de assuntos ativos com contagem de serviços |
| GET | `/api/carta-de-servicos/{subject:slug}/servicos` | Lista paginada, com `?texto=`, `?secretaria=`, `?page=` |
| GET | `/api/carta-de-servicos/servicos/{service:slug}` | Ficha completa |
| GET | `/api/carta-de-servicos/pdf` | Download do PDF vigente |

### 6.2 CMS (autenticado)

| Método | URI | Descrição |
|---|---|---|
| GET | `/admin/api/carta-de-servicos/servicos` | Listagem paginada (substitui a view SQL + DataTables) |
| POST | `/admin/api/carta-de-servicos/servicos` | Criar |
| PUT | `/admin/api/carta-de-servicos/servicos/{id}` | Atualizar |
| DELETE | `/admin/api/carta-de-servicos/servicos/{id}` | Excluir |
| POST | `/admin/api/carta-de-servicos/servicos/{id}/publicar` | Publicar/despublicar (substitui `set-status`) |
| POST | `/admin/api/carta-de-servicos/servicos/{id}/destacar` | Destacar (substitui `set-destaque`) |
| POST | `/admin/api/carta-de-servicos/{id}/canais` | etc. |
| POST | `/admin/api/carta-de-servicos/{id}/etapas` | etc. |
| POST | `/admin/api/carta-de-servicos/{id}/etapas/reordenar` | Recebe `[{id, numero}]` (substitui `etapas/order`) |
| POST | `/admin/api/carta-de-servicos/{id}/documentos` | etc. |
| POST | `/admin/api/carta-de-servicos/{id}/anexos` | etc. |
| POST | `/admin/api/carta-de-servicos/{id}/responsaveis` | etc. |
| PUT | `/admin/api/carta-de-servicos/configuracoes` | Config singleton |
| POST | `/admin/api/carta-de-servicos/pdfs` | Enfileira a geração |
| GET | `/admin/api/carta-de-servicos/pdfs` | Lista |
| GET | `/admin/api/carta-de-servicos/pdfs/{id}/download` | Download |

## 7. API Resources

```php
// app/Http/Resources/ServiceLetterResource.php
class ServiceLetterResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nome' => $this->nome,
            'slug' => $this->slug,
            'descricao_curta' => $this->descricao_curta,
            'secretaria' => $this->whenLoaded('secretaria', fn () => [
                'id' => $this->secretaria->id, 'nome' => $this->secretaria->title,
            ]),
            'subjects' => ServiceLetterSubjectResource::collection($this->whenLoaded('subjects')),
            'tipo_exibicao' => $this->tipo_exibicao->value,
            'link_externo' => $this->tipo_exibicao === TipoExibicao::LinkExterno ? $this->link_externo : null,
            'acesso' => [
                'online' => $this->acesso_online,
                'telefone' => $this->acesso_telefone,
                'presencial' => $this->acesso_presencial,
            ],
            'gratuito' => $this->gratuito,
            'valor_taxa' => $this->valor_taxa,
            'etapas' => ServiceLetterStepResource::collection($this->whenLoaded('steps')),
            'canais' => ServiceLetterChannelResource::collection($this->whenLoaded('channels')),
            'documentos' => ServiceLetterDocumentResource::collection($this->whenLoaded('documents')),
            'responsaveis' => ServiceLetterResponsavelResource::collection($this->whenLoaded('responsaveis')),
            'visualizacoes' => $this->visualizacoes,
        ];
    }
}
```

Resources sugeridos: `ServiceLetterResource`, `ServiceLetterSummaryResource` (listagem, sem o HTML pesado), `ServiceLetterSubjectResource`, `ServiceLetterChannelResource`, `ServiceLetterStepResource`, `ServiceLetterDocumentResource`, `ServiceLetterAttachmentResource`, `ServiceLetterResponsavelResource`, `ServiceLetterPdfResource`, `ServiceLetterConfigResource`.

> **Princípio:** `ServiceLetterSummaryResource` deve omitir os campos `jsonb` de conteúdo rico. A listagem atual carrega `sobre`, `documentacoes`, `custos` etc. de cada linha — desperdícioEvidente.

## 8. Views Blade modernas

| Atual | Moderno |
|---|---|
| `services/index.blade.php` com DataTables | **Livewire** (`ServiceList`) com paginação server-side e filtros; ou `<x-data-table>` reutilizável |
| `services/form.blade.php` (390 linhas, Summernote) | Blade Components `<x-service-form>` + `<x-rich-editor>` (Trix ou Lexical) |
| `services/partials/*.blade.php` (5 tabelas + modais) | **Livewire** por recurso: `<x-service-channels>`, `<x-service-steps>` (com reordenação drag-and-drop), `<x-service-documents>`, `<x-service-attachments>`, `<x-service-responsaveis>` — elimina o JavaScript duplicado e o bug do `tbody#lista_canals` |
| `portal/.../carta-servicos/show.blade.php` com JS AJAX inline | **Livewire** (`ServiceListBySubject`) com `wire:model.live.debounce.300ms` no campo de busca; ou paginação por Turbo/HTMX |
| `portal/.../carta-servicos/view.blade.php` | Blade Component `<x-service-sheet>` reutilizado por todos os modelos |
| 6 modelos × 5 arquivos = 30 blades | **Componente único** + tema por CSS variable. Os 6 modelos diferem apenas em classes/ícones → parametrizar por `<x-portal-layout :template="$template">` |

**Ganho de manutenibilidade:** a duplicação atual entre `modulos.js` (`renderCanalRow`…) e os partials Blade desaparece: passa a haver **um** template por recurso, renderizado pelo Livewire.

## 9. Fila, cache e storage

### 9.1 Fila

| Job | Descrição |
|---|---|
| `GenerateServiceLetterPdf` | Gera o PDF consolidado fora do request. **Obrigatório** — a geração é pesada (dezenas de serviços, imagens de capa) e hoje roda sincronamente, com risco de timeout |
| `RegisterServiceLetterAccess` | Incrementa `visualizacoes` e (opcionalmente) grava o log diário |

```php
// app/Jobs/GenerateServiceLetterPdf.php
class GenerateServiceLetterPdf implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(public ServiceLetterConfig $config) {}

    public function handle(ServiceLetterPdfService $pdfService): void
    {
        $services = ServiceLetter::publicados()
            ->with(['subjects', 'secretaria', 'channels', 'steps', 'documents'])
            ->when($this->config->ordenacao_servicos === OrdemServicos::Secretaria,
                   fn ($q) => $q->orderBy('secretaria_id'))
            ->when($this->config->ordenacao_servicos === OrdemServicos::Assunto,
                   fn ($q) => $q->orderBy('nome'))
            ->get();

        try {
            $result = $pdfService->generate($this->config, $services);
            $this->config->pdfs()->create([...]);
        } catch (Throwable $e) {
            $this->config->pdfs()->create(['status' => PdfStatus::Erro, 'erro_mensagem' => $e->getMessage()]);
            throw $e;
        }
    }
}
```

### 9.2 Cache

| Chave | TTL | Motivo |
|---|---|---|
| `carta-servicos:assuntos:ativos` | 10 min | Menu lateral de assuntos |
| `carta-de-servicos:pdf:vigente` | 10 min | Evita consultar o PDF a cada renderização do `nav.blade.php` (hoje é uma query por página) |
| `carta-de-servicos:servico:{id}` | 5 min | Ficha completa do serviço |
| Invalidação | — | `ServiceLetterSaved` / `ServiceLetterDeleted` / `ServiceLetterStepSaved` → `Cache::tags([...])->flush()` |

### 9.3 Storage

| Uso | Disco |
|---|---|
| Anexos do serviço | `local` (privado) com `Storage::temporaryUrl()` ou rota assinada — **não** `public` |
| Foto do responsável | `public` (exibida no portal) |
| Capa do PDF | `public` |
| PDF gerado | `private` com download autenticado, ou `public` se o download for público |

## 10. Riscos e decisões de equivalência funcional

| # | Risco / decisão | Impacto | Mitigação |
|---|---|---|---|
| R1 | **PDF não gerado hoje** (classe ausente) | Alto | Implementar `ServiceLetterPdfService` antes de migrar; validar o resultado contra um PDF gerado manualmente pelo negócio |
| R2 | **Duplicação `etapas` (HTML) × `steps` (estruturado)** | Alto — conteúdo incoerente no portal | Decisão de produto; recomendo remover o HTML |
| R3 | **Duplicação `responsavel_*` × `service_letter_responsaveis`** | Médio — campos órfãos | Migrar os dados e remover os campos |
| R4 | **`service_letter_access_logs` nunca gravada** | Baixo | Remover ou implementar |
| R5 | **Busca Postgres-específica** (`translate`/`ilike`) | Médio | Instalar a extensão `unaccent` no Postgres 15+ (já é padrão) e usar `unaccent(lower(x)) ILIKE ?` |
| R6 | **Ordenação dupla das etapas** | Médio | `unique(['service_letter_id','numero'])` + ordenação só por `numero` |
| R7 | **Views SQL não filtram soft delete** | Médio | Substituir por queries Eloquent com escopo `scopePublicados` |
| R8 | **`tipo_exibicao` sem validação de `link_externo`** | Médio | `required_if` no Form Request |
| R9 | **Campos de canal de tipos diferentes coexistem** | Baixo | `StoreChannelRequest` com `required_if` + limpeza no model |
| R10 | **Sem permissões cadastradas** para o módulo | Alto (operacional) | Seeder de permissões + `Gate::define` |
| R11 | **Coluna `id` da categoria de secretarias hardcoded (7)** | Médio | Buscar por `category_contents.slug = 'secretarias'` |
| R12 | **Template do portal hardcoded por `configurations.layout`** | Alto | Resolver via injeção de dependência / `View::composer`; garantir que todo layout ativo tenha as views da Carta de Serviços |
| R13 | **`slug` único sem tratamento de colisão** | Médio | Slug único gerado com sufixo numérico em caso de conflito, ou slug **editável** pelo operador com validação de unicidade |
| R14 | **HTML de Summernote armazenado sem sanitização** | Alto (segurança) | Sanitizar com `htmlspecialchars`/allowlist no servidor ou migrar para editor que emita Markdown sanitizado |
| R15 | **Exclusão de anexo apaga o arquivo mas a exclusão de responsável não** | Baixo | Hook de `deleting` no model que remove o arquivo |

## 11. Estimativa de esforço (ordem de grandeza)

| Frente | Itens |
|---|---|
| Schema + models + factories | 11 tabelas, 10 models, 10 factories |
| Requests + Policies | 8 requests, 1 policy |
| API + Resources | 10 resources, ~25 endpoints |
| Views | 5 componentes Livewire (CMS), 3 componentes Blade (portal) |
| Services | `ServiceLetterPdfService` (novo), `ServiceLetterSearchService` (opcional) |
| Jobs | 2 |
| Testes | ~60 casos (ver `07-TESTES.md`) |
| Migração de dados | 1 comando `php artisan migrate:carta-de-servicos` idempotente, com relatório de divergências |
