# Fase 2 — os avisos do mural da agenda

25/10/2026, a fatia 37 da fila das telas que faltam (MENSAGENS-API). O desenho é
`docs/f3-telas-restantes/tela-mensagens.md`: a F1 (a API, seção 2) e o que faltava da F0 (o lado D259 do M5, a forma
do D256 no M3 e o M9). A tela nova é a fatia 38 (MENSAGENS-TELA), e a faixa na agenda nova a 39 (MENSAGENS-MURAL);
esta é só o servidor -- a API nova e a tela antiga consertada -- e os tipos em `frontend/src/api/mensagens.ts`.

**É o mural da agenda, e não marketing** (a resposta 211 (a) do dono, a pergunta 1 do desenho): avisos internos para a
equipe, impressos no topo da agenda no dia marcado. Não usa canal nenhum e não tem dado de cliente. Os textos que a
unidade manda ao cliente (Unidades › mensageria) são outra coisa, e ficam fora. A resposta 210 (a) diz que os avisos
continuam na agenda nova, numa faixa acima da grade -- é a fatia 39, e nada aqui a faz.

---

## O recorte

| endpoint | verbo | origem no ERP | o que faz |
|---|---|---|---|
| `/adm/api/mensagens` | `GET` | `index` | a lista: `titulo`, `estado` (`ativo`, o padrão, `inativo`, `todos`), `momento` (`de_hoje_em_diante`, o padrão, `no_ar`, `agendado`, `passou`, `todos`), `unidade`, `grupo`, `dia` (`AAAA-MM-DD`), `pagina`; e `ultimoQueFoiAoAr` no envelope |
| `/adm/api/mensagens/opcoes` | `GET` | os `<select>` do formulário | as unidades que a tela oferece e os grupos ativos, com `abreAgenda` e `temPessoaAtiva` |
| `/adm/api/mensagens/:id` | `GET` | `editar`, leitura | o aviso inteiro; 404 no que não chega à unidade de quem só lê |
| `/adm/api/mensagens` | `POST` | `novo` | 201; nasce ativo |
| `/adm/api/mensagens/:id` | `PUT` | `editar`, gravação | campo a campo; `{}` é 400; sem `unidades` ou `grupos`, os alvos ficam |
| `.../:id/desativacao` e `.../:id/ativacao` | `POST` | `ativarRemover` | o estado pedido; 409 `mesmo_estado` |

Toda escrita devolve o aviso inteiro, como ficou (`MensagemJson::completo`). **A linha da lista é o recurso inteiro**: o
selo e o "para quem" da lista e do detalhe saem da mesma conta (a lição dos cupons).

**Quem.** Ler pede `configuracoes/mensagens/ler`. As quatro escritas pedem o `escrever` **e** ser administrador (403 "Só o
administrador altera aviso: ele aparece na agenda de outras franquias."), como a tela antiga desde o D404. Quem tem `ler`
sem ser administrador (hoje ninguém) vê só o aviso "para todas" e o ligado à unidade dele: o resto não vem na lista e é 404
no detalhe; as opções dele são a própria unidade. E das unidades ligadas ao aviso que ele vê, só a dele vem pelo nome: as
outras de fora vêm só contadas, em `outrasUnidades` (a leitura da decisão 2, "vê o que chega à própria unidade", na
verificação da fatia; a tela antiga nem lhe mostra os alvos). Desfaz: o `MensagemJson::completo` sem o `$soAUnidade`. A guarda é inline em cada ação ou no ajudante da rota (um nível, o que o
`conferir-acl-da-api.php` segue), com a funcionalidade no construtor (o D123). A barreira de origem é a do
`ApiAdmController` (o D290).

## O corpo, e as duas traduções na fronteira

```json
{"titulo": "Reunião de equipe", "texto": "Às 18h.\nNa unidade.", "cor": "atencao",
 "dia": "2026-11-03", "unidades": "todas" | [ids], "grupos": "todos" | [ids]}
```

- **A cor** sai com o nome do que diz (`informacao`, `atencao`, `urgente`) e a coluna continua gravando a classe que a
  agenda antiga imprime (`note-info`, `note-warning`, `note-danger`). A gravada crua de antes sai como `informacao`, como no
  mural.
- **O dia** é ISO na API e `d/m/Y` no ERP (o setter da casa).
- **O texto** vai em `texto` (a coluna é `conteudo`), e o `conteudo`, o `data`, a `unidade` e o `grupo` no corpo são 422
  dizendo para onde foram -- como o `status`, o `estado`, o `id`, o `momento` e as datas de registro: o hidratador do serviço
  chamaria um setter para cada chave.

O que serve (a `RegraDaMensagem::conferir`): título de 1 a 100 depois do `trim`, texto de 1 a 500 com quebra de linha, os
dois sem emoji (`utf8mb3`, o D6) e sem caractere de controle -- **sem proibir `<` e `>`**, que saem escapados (a decisão 7;
pela `RegraDoTexto::conferirComSinalDeTag`, a exceção registrada na seção 3 de `docs/convencao-api.md`); a cor da lista; o
dia que existe, de hoje em diante ao cadastrar e ao trocar (a decisão 8, pela `RegraDaMensagem::diaQuePassou`; o mesmo dia
de volta no `PUT` serve); `"todas"` ou a lista não vazia das unidades oferecidas, `"todos"` ou a lista não vazia
dos grupos ativos -- e, no `PUT`, também os já ligados ao aviso, inativos inclusive (o D256). A lista vazia é recusada: o
"para todos" é escolha, e não o efeito de não mexer.

**O formulário da tela antiga ganhou os mesmos validadores** do título e do texto (`RegraDaMensagem::validadoresDoFormulario`,
a `RegraDoTexto` sem o `<` e o `>`): a tabulação passou a ser recusada nas duas portas. **E a mesma recusa do dia que
passou** (o `MensagemFilter`, pela `diaQuePassou`, com o dia de hoje e o dia que o aviso já tem): o cadastro de ontem e a
troca para um dia passado são recusados no campo, e o aviso que já passou continua editável com o mesmo dia de volta. Até
a verificação da fatia, a tela antiga gravava o aviso de ontem, que nascia ativo e nunca ia ao ar. **A única diferença
entre as portas é o dia vazio** (a pergunta 4, abaixo).

## O momento, pelo relógio do servidor

O selo (`momento`) e o filtro saem do dia de hoje lido pelo PHP do servidor (`date('Y-m-d')`), o mesmo do mural da agenda
antiga -- e não do dia escolhido na grade:

- `no_ar`: ativo e com o dia de hoje; `agendado`: ativo e com dia futuro; `passou`: ativo e com dia passado (são os 65
  "ativos" da cópia); `desativado`;
- `todo_dia`: o ativo **sem dia**, que o ERP ainda deixa gravar e que aparece todo dia (a pergunta 4, sem resposta; na
  cópia, 0 dos 74). Entra no filtro `de_hoje_em_diante` e no `no_ar`, e vem primeiro nos dois bancos.

O filtro `momento` é o do dia; o `estado`, o da situação. A lista abre em `ativo` e `de_hoje_em_diante`, do mais perto ao mais
longe; `passou` e `todos` vão do mais recente ao mais antigo, com o desempate pelo id. `ultimoQueFoiAoAr` é o dia do ativo
que passou mais recente **com os filtros da lista** -- o título, a unidade, o grupo e o recorte de quem pede --, pela mesma
consulta (`listagemDaApi` com `estado=ativo`, `momento=passou` e sem o `dia`): a lista vazia diz "o último foi ao ar em
...", e não só "nenhum", e a lista de uma unidade não diz o dia de um aviso só de outra. O `estado`, o `momento` e o `dia`
do filtro não valem para ele. Até a verificação da fatia, ele só levava o recorte, e o recorte não tinha caso.

## Para quem

`unidades` é `"todas"` -- sem linha nenhuma: todas, inclusive as que abrirem depois -- ou `[{id, nome, ativa}]`; `grupos` é
`"todos"` ou `[{id, nome, ativo, abreAgenda, temPessoaAtiva}]`. `abreAgenda` é falso no 9 e no 11 (a agenda antiga os manda a
outra tela antes de ler o mural); `temPessoaAtiva`, falso no grupo sem ninguém ativo (na cópia, o 5, o 6 e o 9). Os dois
continuam oferecidos (a decisão 4). As opções trazem as unidades ativas ao administrador (a própria aos outros) e os grupos
ativos.

## A escrita

Pelo `MensagemService`, o mesmo da tela antiga: o aviso e os alvos numa transação, com a linha travada na alteração (o
D258); **só o que mudou nos alvos** (o M9) -- os ids das linhas que ficam não giram, e a chave que não vem não mexe neles;
o id que não existe é erro dentro da transação. O estado (`definirEstado`) é relido com `PESSIMISTIC_WRITE`, e o mesmo
estado é o 409 `mesmo_estado` -- lido depois da trava: duas abas recebem um 200 e um 409. A tela antiga usa o mesmo
`definirEstado`, e o editar dela não toca mais no estado (o D600).

## O que fica de fora, e o "enquanto isso"

Sem resposta do dono, a fatia segue o "O que decidi para seguir" da seção 4 do desenho, sem pergunta nova:

- **Quem escreve** (a pergunta 5): só o administrador, na API e no ERP. Desfaz: tirar a guarda das ações e as entradas do
  `conferir-recorte-por-unidade.php`.
- **O dia vazio** (a pergunta 4): obrigatório na API nova; ainda permitido no ERP, onde o aviso sem dia aparece todo dia. Se
  a resposta for (a) ou (c), é uma regra a mais.
- **O "re-datar"** (a pergunta 3): o de hoje, pelo `PUT` do `dia` -- sem "usar de novo" nem de/até, que esperam a resposta
  (a F4 do desenho). O dia anterior continua se perdendo a cada re-data (o M10).
- **Sem autor:** pede coluna, e coluna nova espera autorização e o passo 0 (a decisão 5).

E fora desta fatia: a tela (a 38), a faixa na agenda nova e o `GET /adm/api/agendas/avisos` (a 39).

## Provado

- `MensagemApiTest`, nos dois bancos: o 401 das sete ações; o 403 de ler e de escrever, e o "Só o administrador" das quatro
  escritas, com a metade que grava; a outra origem; o 404 do que não existe e do id fora do `int4`; o 409 dos dois estados;
  o 422 de cada campo e de cada filtro, com o nome; o 400 do `{}`; o momento pelo relógio, com os dias relativos a hoje; o
  aviso sem dia; os filtros que acham; o recorte de quem só lê, na lista, no detalhe, nas unidades ligadas e no
  `ultimoQueFoiAoAr` (com um aviso passado só de outra unidade, mais recente); o cadastro, o detalhe, as opções e os
  ajudantes do repositório.
- `RegraDaMensagemTest` (a regra pura), `MensagemAlvosTest` (o M9 e o estado relido), `MensagensPeloControllerTest` (o
  D600, o D256, a tabulação e o dia que passou no formulário antigo) e `TelasDasMensagensDoErpTest` (o editar sem a situação e o "Ativar" da
  lista).
- Pelo HTTP, `scripts/ensaiar-mensagens-adm-api.php`, contra o banco de contrato no MySQL e no PostgreSQL: 31 de 31 (22
  recusas e 9 controles, cada um conferido no banco), montado na faixa 90037900-90037999 e desfeito no `finally`.

## A fatia 39: o mural na agenda nova

25/10/2026 (MENSAGENS-MURAL; a F3 do desenho, pela resposta 210 (a); o D604). Um endereço a mais, só de leitura:

| endpoint | verbo | origem no ERP | o que faz |
|---|---|---|---|
| `/adm/api/agendas/avisos` | `GET` | o mural do `AgendasController::indexAction` | `{"dia", "avisos": [{id, titulo, texto, cor}]}`: os de hoje da sessão |

- **Quem:** `agendas/visualizar/ler`, a guarda da grade e do `indexAction` do ERP; 401 sem sessão.
- **O quê:** a mesma consulta do mural antigo, `MensagemRepository::listagemIndexUsuarios` -- os ativos com o dia de hoje
  ou sem dia, que chegam à unidade e ao grupo DA SESSÃO --, com o desempate pelo id. Nenhum parâmetro: a query é ignorada.
  Quem enxerga a rede vê o mural da própria unidade, como na agenda antiga.
- **Hoje** é o do relógio do servidor, lido uma vez para a consulta e o envelope, e não o dia da grade (a decisão 10).
- **O aviso** vai como texto (o React escapa) e a cor pelo nome (`MensagemJson::doMural`); a gravada crua de antes, como a
  neutra.

A tela (a `AvisosDoDia`, na `AgendaDeHoje`) e o que ela pede e quando estão na F3 do desenho.
