# Newsletters — Views e UX

> **Módulo:** `newsletters` (tabela `modulos`, id 53)

---

## 1. Views do CMS

Diretório: `v6/resources/views/prefecture/contents/newsletter/`

| Arquivo | Função |
|---|---|
| `index.blade.php` | Tela "Lista de E-mails": cabeçalho com botões "Cadastrar E-mail" e "Exportar E-mails", botão de exclusão em lote e o esqueleto `<table id="tabela_newsletter">` preenchido por AJAX. |
| `create.blade.php` | Formulário de cadastro (`Form::open(['route' => ['newsletter.store'], 'files' => true, 'id' => 'form'])`). |
| `edit.blade.php` | Formulário de edição (`Form::model($newsletter, ['route' => ['newsletter.update', $newsletter->id], 'method' => 'PUT'])`). |
| `form.blade.php` | Partial com os campos, compartilhado por create e edit. |

Todas estendem `admin.layouts.app` (AdminLTE 1.22).

### Campos do formulário (`form.blade.php`)

| Campo | Tipo | Obrigatório | Observações |
|---|---|---|---|
| `active` | `hidden` (`0`) + `checkbox` (`1`) | não | Checkbox marcado quando `$newsletter->active` é verdadeiro; o `hidden` garante o envio de `0` quando desmarcado. |
| `email` | `Form::email` | **sim** | `maxlength` implícito do `Form::email`; mensagem de erro abaixo do campo. |
| `name` | `Form::text` | **sim** | `maxlength="120"` no HTML (o backend valida `max:255`). |
| `sent_at` | `text` com `datetimepicker` | visual | Campo **somente leitura de fato**: o `update()` ignora o valor enviado e a criação sempre usa `Carbon::now()`. Mostra `d/m/Y H:i`. |

Inclui `@include('components.layout.errors')` e `@include('components.layout.mandatory_help')`.
Carrega `assets/plugins/datetimepicker/jquery.datetimepicker.full.min.js` e aplica máscara
`00/00/0000 00:00` via jQuery.

### Colunas da DataTable (`index.blade.php`)

| Coluna | Origem no JSON |
|---|---|
| (checkbox mestre) | `deleteByLoteMasterCheckbox` (só com `has_access(['newsletter.delete'])`) |
| ID | `value` (row_number da view `newsletter`) |
| E-mail | `email` |
| Nome | `nome` |
| IP | `ip` |
| Data de inscrição | `enviado` |
| Ações | botão editar / excluir |

O botão de exclusão em lote usa
`@component('prefecture.components.deleteByLoteSubmitButton', ['route' => 'ajax.generic.content.deleteAll'])`
— ver `08-GAPS-E-DIVIDAS.md` (aponta para a rota de conteúdos, não para a de newsletters).

## 2. Views do Portal

O formulário público aparece em **cinco** dos seis layouts temáticos; o layout `default`
**não** possui newsletter.

| Layout | Arquivo | Posição na tela |
|---|---|---|
| `modelo1` | `v6/resources/views/portal/layouts/modelo1/layout/sidebar/newsletter.blade.php` | sidebar (incluído por `v6/resources/views/portal/layouts/modelo1/layout/sidebar/layout.blade.php:20`) |
| `modelo2` | `v6/resources/views/portal/layouts/modelo2/layout/sidebar/newsletter.blade.php` | sidebar (`v6/resources/views/portal/layouts/modelo2/layout/sidebar/layout.blade.php:22`) |
| `modelo3` | `v6/resources/views/portal/layouts/modelo3/layout/sidebar/newsletter.blade.php` | sidebar (`v6/resources/views/portal/layouts/modelo3/layout/sidebar/layout.blade.php:12`) |
| `modelo4` | `v6/resources/views/portal/layouts/modelo4/layout/sidebar/newsletter.blade.php` | sidebar (`v6/resources/views/portal/layouts/modelo4/layout/sidebar/layout.blade.php:11`) |
| `modelo5` | `v6/resources/views/portal/layouts/modelo5/layout/sidebar/newsletter.blade.php` | rodapé (`v6/resources/views/portal/layouts/modelo5/layout/footer/menu-footer.blade.php:95`) |
| `modelo6` | `v6/resources/views/portal/layouts/modelo6/layout/footer.blade.php:36-82` | rodapé (formulário embutido no footer, não há partial separado) |
| `default` | — | **sem newsletter** |

### Conteúdo do formulário (modelo1..modelo5)

```blade
@if(isset($modulos['newsletters']) && $modulos['newsletters'])
    <div id="newsletter-form" class="container-fluid col-lg-12 col-md-6 col-12 row justify-content-xs-center mx-auto w-100 pb-3">
        <h4 class="h4 text-uppercase txt-color-2 font-or">NEWSLETTER</h4>
        <form action="{{ route('newsletter.storeportal') }}" method="POST" class="newsletter-form w-100">
            <input type="hidden" name="_token" value="{{ csrf_token() }}">
            <input type="hidden" name="active" value="true">
            <input type="text" placeholder="Nome:" id="name" name="name" class="form-control" value="{{ old('name') }}" required>
            <input type="email" id="email" placeholder="E-mail:" name="email" class="form-control" value="{{ old('email') }}" required>
            <button type="submit" class="btn bg-color-3 txt-color-dinamic-3 mb-3 mt-3">Cadastrar</button>
        </form>
    </div>
@endif
```

- Título: "NEWSLETTER" (modelo1, 2, 5) ou "Newsletter" (modelo4) / "NEWSLETTER" (modelo3, h3).
- Texto: "Assine nossa newsletter e acompanhe as ações da prefeitura. Receba notícias e
  informações importantes diretamente no seu e-mail. Fique informado!"
- Botão: "Cadastrar" (modelo1..modelo5) e "Assine" (modelo6).
- `active` é enviado como `"true"` por um `hidden`.

### Condições de exibição (dupla trava)

| Verificação | Onde | Efeito |
|---|---|---|
| `isset($modulos['newsletters']) && $modulos['newsletters']` | dentro de cada partial | `modulos.status` do slug `newsletters` (id 53) precisa estar ativo |
| `getStatusDisplayNewsletters()['displayNewsletters'] == 0` | no ponto de `@include` (sidebar/footer) | a configuração `configurations.key = 'newsletters'` precisa estar em `0` |

As duas condições precisam ser satisfeitas **simultaneamente**. Quando o segundo
interruptor está em `1`, o `modelo5` ainda recalcula o CSS do rodapé
(`.col-newsletter-disabled`, `.compact-footer`) para ocupar o espaço deixado pelo bloco.

### Mensagens no portal

- Sucesso: `session('success')` → `<div class="alert alert-success" id="success-message">`.
- Erro de e-mail duplicado: `$errors->has('email')` → `<span class="text-danger">`.
- Após sucesso ou erro, um script faz `document.getElementById('newsletter-form').scrollIntoView({behavior: 'smooth'})`
  para rolar até o formulário.

## 3. JavaScript

| Arquivo | Papel |
|---|---|
| `v6/public/assets/js/custom/contents/newsletter/datatables.js` | Colunas e inicialização da DataTable do CMS. |
| `v6/public/assets/js/custom/utilities/table.js` | `runDatatables()`, `createBTN()`, `addDelete()`, `addDeleteAll()`, botões de export Excel/PDF. |
| `v6/public/assets/js/custom/utilities/deleteAll.js` | `submitDeleteAll()` — envia `toDelete[]` para a rota de exclusão em lote. |
| `v6/public/assets/js/custom/utilities/functions.js` | `myFetch()` — `fetch` com cabeçalho `X-CSRF-TOKEN`. |
| `v6/public/assets/js/custom/utilities/helpers/validacao-texto.js` | Carregado nas telas de create/edit. |

### `datatables.js` (fonte)

```js
let collums = [
    { data: 'email', render: function (data) { return `<div class='vertical-middle p-10-0' >${data}</div>`; } },
    { data: 'nome',  render: function (data) { return `<div class='font-weight-bold'>${data}</div>`; } },
    { data: 'ip',    render: function (data) { return `<div class='font-weight-bold'>${data}</div>`; } },
    { data: 'enviado', render: function (data) { /* new Date(data) + toLocaleDateString('pt-BR') */ } },
    {
        data: null,
        name: "actions",
        orderable: false,
        render: function (data) {
            return `<div class='flex justify-center vertical-middle'>`
                + createBTN(data.id, `newsletter/edit/${data.id}`, data.edit, data.delete, `newsletter/delete/${data.id}`)
                + `</div>`;
        }
    },
];

runDatatables('newsletter', collums, true, 'ASC', null, true);
```

- `runDatatables(url, colunas, ordenavel, ordem, callback, deleteAll)` monta o AJAX em
  `/cms/datatable/${url}` (`v6/public/assets/js/custom/utilities/table.js:181-185`).
- `deleteAll = true` insere a coluna de checkbox em lote (via `createDeleteAllButton`).
- Os botões de editar/excluir usam **URLs relativas** (`newsletter/edit/{id}`), que o
  navegador resolve a partir de `/cms/newsletter` → `/cms/newsletter/edit/{id}`.
- A coluna `value` (row_number) é inserida automaticamente por `runDatatables`.
- O botão "Excluir" usa o atributo `target` do botão como URL do DELETE
  (`table.js:466-489`), com `myFetch(url, 'DELETE')`.

Não há polling nem requisições periódicas: a DataTable só recarrega após uma ação
(`datatable.ajax.reload()`).

## 4. Exportação

### Exportação CSV do módulo (`/cms/newsletter/export`)

Implementada por `NewsLetterController@export` com `response()->stream()`:

```php
$newsletters = DB::table('newsletters')->get();

if ($newsletters->isEmpty()) {
    return redirect()->back()->with('error', 'Nenhuma inscrição encontrada para exportar.');
}

return response()->stream(function () use ($newsletters) {
    $handle = fopen('php://output', 'w');
    fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
    fputcsv($handle, ['ID', 'Nome', 'E-mail', 'Endereço IP', 'Data de Inscrição', 'Ativo']);
    foreach ($newsletters as $newsletter) {
        fputcsv($handle, [
            $newsletter->id,
            $newsletter->name,
            $newsletter->email,
            $newsletter->ip_address,
            $newsletter->sent_at ? Carbon::parse($newsletter->sent_at)->format('d/m/Y H:i:s') : '',
            $newsletter->active,
        ]);
    }
    fclose($handle);
}, 200, [
    "Content-Type" => "text/csv",
    "Content-Disposition" => "attachment; filename=newsletters.csv",
    "Pragma" => "no-cache",
    "Cache-Control" => "must-revalidate, post-check=0, pre-check=0",
    "Expires" => "0",
]);
```

Características:

- **BOM UTF-8** (`EF BB BF`) para o Excel reconhecer acentuação.
- Separador: vírgula (padrão de `fputcsv`).
- `sent_at` sai como `d/m/Y H:i:s`; vazio quando nulo.
- `active` sai como o booleano PHP serializado (`1` para `true`, string vazia para `false`).
- A base vem da **tabela** `newsletters`: registros em soft delete **são exportados**.
- Não há escape de fórmulas: um `name` ou `email` começando com `=`, `+`, `-` ou `@`
  é interpretado como fórmula pelo Excel ao abrir o arquivo (risco de CSV injection).

### Exportação Excel/PDF da DataTable

`runDatatables` adiciona botões **Excel** e **PDF** (DataTables Buttons) que exportam as
colunas visíveis da própria tabela — que, no caso do módulo, vem da **view** `newsletter`
(portanto já sem registros apagados). O título do arquivo é
`Prefeitura de {cidade} - {data}` (`table.js:30-35`). Diferente do CSV do controller,
essa exportação respeita o soft delete e não inclui IP.
