# Globais — site inteiro

| Campo | Valor |
|---|---|
| **status** | aprofundada — pronta para fatiar em tasks |
| **rota pública** | nenhuma (quando integrar: props shared em todas as páginas) |
| **tela CMS** | Sistema → Configurações (`/admin/configuracoes`) |
| **referência primo** | `ManageSettingsPage`, model `Setting`, tabela `settings` `group=global`, trait `ClearsReplacementUploads` |
| **fonte do seeder** | `SiteFooter.tsx`, `SiteLayout.tsx` (títulos `\| Omni`), `layout/assets.ts` (quais redes existem) |

## Premissas (sem pergunta extra)

Se algo estiver errado, corrigir nesta spec **antes** das tasks.

| Tema | Decisão |
|---|---|
| Store key-value | Igual ao primo: tabela `settings` (`key` unique, `value` text nullable, `group`). Esta spec só grava `group=global`. As Manage* de página (`010+`) reutilizam a mesma tabela com outros groups. |
| Tela | Uma `ManageSettingsPage` (não copiar o `SettingResource` vazio/escondido do primo) |
| Permissão | Já existe: `sistema.configuracoes.editar`. Root sempre entra. Operador **não** (módulo Sistema fora do perfil) |
| Campos | Só o que o front **já mostra** (ou tem contrato claro de fallback). Lição do primo: nada de campo morto |
| Cookies / política | **Fora.** Não há `CookieBanner`, nem rota `/politica-de-privacidade`, nem menção nos briefs de origem |
| Front | **Não** ligar `HandleInertiaRequests` / `siteSettings` nesta entrega |

Não é desta spec: header, mega menu, colunas do footer (menu, transparência, canais/telefones) — isso é `003-layout`. Conteúdo de landing (`010+`).

---

## 1. O que o editor vê

1. Menu **Sistema → Configurações** (ícone `heroicon-o-cog-6-tooth`, sort **3** — entre Usuários e Logs, como o primo).
2. Página com abas e botão **Salvar alterações** (Blade igual ao primo: `filament.pages.manage-settings-page`).
3. Sem item em **Páginas**. Sem widgets no Dashboard.

Quem não tem a permissão: `canAccess` false e item some da nav (`shouldRegisterNavigation = canAccess`).

---

## 2. Recorte contra o layout (`003`) e contra o primo

### 2.1 O que é global neste site (hoje)

No `SiteFooter.tsx` / `SiteLayout`, fora da estrutura de navegação:

| Superfície hoje | Onde |
|---|---|
| 4 ícones sociais (Facebook, Instagram, LinkedIn, YouTube), `href="#"` | footer, ao lado do logo |
| Endereço (só mobile no layout) | rodapé institucional |
| CNPJ | rodapé institucional |
| Copyright `© 2022 Omni. Todos os direitos reservados.` | rodapé institucional |
| Sufixo de título `Omni` (`SiteLayout title="… \| Omni"`; Home = `Omni`) | `<Head>` |

Canais, telefones, links de menu/transparência, bloco “Baixe nosso app” (imagens das lojas) = **estrutura** → `003` / SuperApp (`024`).

### 2.2 O que o primo tem e **não** copiamos

Campos mortos ou sem superfície na Omni:

| Campo do primo | Por quê não |
|---|---|
| `global_company_name/email/phone` | Front não lê; telefone/e-mail institucionais estão nos canais (`003`) |
| `global_footer_tagline` | Footer Omni não tem tagline sob o logo |
| `global_footer_email` | Não há mailto no footer Omni |
| `global_footer_complaints_url` | Morto no primo; aqui a Ouvidoria é canal do footer (`003`) |
| `global_social_spotify` | Não há ícone Spotify no footer |
| `global_cookie_banner_text` | Sem banner no React / Figma / `docs/origin` |
| `global_privacy_policy_content` | Sem rota pública. “Privacidade Omni” é **link** da coluna Transparência (`003`) |

Quando existir banner de cookies ou página de privacidade (rota + componente), aí sim abrir campo — não nesta spec.

---

## 3. Modelo de dados

### Tabela `settings` (nova, igual primo)

| Coluna | Notas |
|---|---|
| `id` | |
| `key` | string unique |
| `value` | text nullable |
| `group` | string, default `general`; globais usam `global` |
| timestamps | |

Model `Setting`: `get($key, $default = null)`, `set($key, $value, $group = 'general')` via `updateOrCreate` na `key`. Fillable `key`, `value`, `group`.

Não criar resource Filament CRUD de linhas soltas.

---

## 4. Chaves `group=global`

Prefixo `global_`. Upload de OG: disco `public`, pasta `global`, JPEG/PNG/WebP, max 2 MB, preview 1200×630 (helper). Padrão de **substituição** do primo: `ReplacementImage::fields()` (Placeholder da atual + FileUpload FilePond para enviar/substituir). Imagem opcional: `onDelete` com lixeira no hover do preview (confirmação; upload vazio não apaga). Trait `ClearsReplacementUploads` (outras Manage* reusam). Todo campo de imagem no Filament segue esse par — ver `.cursor/rules/neocms-imagens-filament.mdc`.

### 4.1 Institucional

| Key | Tipo no form | Seed (copy atual) |
|---|---|---|
| `global_footer_address` | Textarea, 2 linhas | `Av. São Gabriel, 555 - 5º andar, Jardim Paulista, São Paulo - SP CEP 01435-001` |
| `global_footer_cnpj` | TextInput | `92.228.410/0001-02` (sem o prefixo “CNPJ:” — isso é apresentação) |
| `global_footer_copyright` | TextInput | `© 2022 Omni. Todos os direitos reservados.` Helper: `{year}` opcional, como o primo; **não** trocar 2022 por `{year}` no seed |

### 4.2 Redes

No painel: repeater `SocialLinksRepeater` — o editor **adiciona** 1..4 redes (Facebook, Instagram, LinkedIn, YouTube), cada uma uma vez. Não mostrar os 4 campos vazios.

No banco: as mesmas 4 keys. Rede ausente do repeater grava `''`. Seed **vazio** (o `#` do front é placeholder, não URL válida).

| Key | Label |
|---|---|
| `global_social_facebook` | Facebook |
| `global_social_instagram` | Instagram |
| `global_social_linkedin` | LinkedIn |
| `global_social_youtube` | YouTube |

Contrato **quando** o front integrar (não nesta fase): URL vazia → não renderiza o ícone (igual primo). Hoje o React mostra os 4 sempre, com `#`.

### 4.3 SEO padrão

| Key | Tipo | Seed |
|---|---|---|
| `global_seo_site_name` | TextInput, max 100 | `Omni` |
| `global_seo_description` | TextInput, max 200 | **vazio** — o site não tem meta description hoje; não copiar copy da landing A Omni |
| `global_seo_og_image` | FileUpload de substituição + Placeholder da atual | **sem arquivo** |

Helper do nome: sufixo do `<title>` (hoje `Página \| Omni`). Description/OG: fallback quando a página não tiver os próprios (as specs `010+` terão SEO de página). Não inventar OG.

---

## 5. Abas do form

Espelhar o primo em estrutura, só com as abas vivas:

1. **Institucional** — endereço, CNPJ, copyright.
2. **Redes sociais** — repeater 1..4 (não os 4 inputs vazios).
3. **SEO Global** — nome, description, preview OG + substituir.

`statePath('data')`. `mount()`: `Setting::where('group', 'global')->pluck('value', 'key')` + defaults iguais ao seeder + `withoutReplacementUploads`.

`save()`: para cada key do state, pular `null` e upload de substituição vazio; `Setting::set($key, $value, 'global')`; `clearReplacementUploads()`; `ActivityLogger::log('settings.updated', …, details: ['keys' => $keys])`; notification “Configurações salvas!”.

Defaults no `mount` = valores da tabela §4 (para form útil mesmo sem seed).

---

## 6. Permissão e log

Já no catálogo (`001` §5.1). Esta entrega **enforce**:

```text
canAccess  ⇔  root  OU  hasPermission('sistema.configuracoes.editar')
```

Não criar chave nova. Administrador já tem todas; Operador não tem Sistema.

Log: `settings.updated` (já usado no primo; o resource de logs lista `action` distinto).

---

## 7. Seeder

`GlobalSettingsSeeder`: `Setting::set` de **todas** as keys da §4 (OG omitida ou value null — não inventar path). Idempotente (`updateOrCreate` via `set`).

`DatabaseSeeder` chama esse seeder **junto** de `PermissaoSeeder` / `PerfilSeeder`, **antes** do `if (production) return`. Conteúdo institucional vale em produção; só o user root continua restrito a local.

Rodar seed deixa o painel com o mesmo institucional que o footer hardcoded hoje. Redes e description/OG vazios, como o front de verdade.

---

## 8. Integração front — **não** nesta fase

Proibido nesta entrega:

- `HandleInertiaRequests` share `siteSettings`
- `Seo.tsx` / `CookieBanner`
- `SiteFooter` / `SiteLayout` lerem o banco

O React segue `SiteFooter.tsx` e `title` nas features. Ligar shared props só quando o time pedir.

Contrato **futuro** (não implementar): mapa `Record<string, string>` lazy, OG como URL pública (`Storage::disk('public')->url`), `{year}` no copyright.

---

## 9. Testes

PHPUnit (padrão do repo em `tests/Feature/Cms/`), `RefreshDatabase`, `withoutVite`. Pest só se o arquivo vizinho já for Pest.

Mínimo:

- Seeder grava endereço, CNPJ, copyright e `global_seo_site_name` = `Omni`; redes vazias; sem path de OG inventado.
- Root: `ManageSettingsPage::canAccess()` true; GET da página 200.
- `usuario` com perfil **sem** `sistema.configuracoes.editar`: `canAccess` false.
- `usuario` com a chave: `canAccess` true.
- Livewire save altera um campo (ex. copyright) e gera `activity_logs.action = settings.updated`.
- OG: gravar um path, salvar o form com FileUpload vazio **não** apaga o path (padrão substituição).

Não testar `siteSettings` no Inertia (ainda não existe).

---

## 10. Fora de escopo

- Header, mega menu, colunas e canais do footer (`003`).
- URLs das lojas (Google Play / App Store): imagens existem, mas sem `href`; decidir em `003` ou `024`.
- Banner de cookies, HTML de política, canal de denúncias.
- `SettingResource` CRUD genérico.
- Share Inertia, componente `Seo`, GTM consent.
- Logo do footer (SVG em `layout/assets.ts`).

---

## 11. Tasks

Não criar task dentro desta pasta. Implementação: [`docs/tasks/000-index.md`](../../tasks/000-index.md) (`CMS-002-01`, `CMS-002-02`).
