# Liberar rota para motorista — UX (Reenviar + telefone com bandeira) — Design

**Data:** 2026-05-08
**Escopo:** No modal "Liberar rota para motorista" (`Logistica/sinais/Central/html/central.php`):
1. Substituir o input de telefone por um campo com `intl-tel-input` (bandeira do país, DDI separado, default Brasil +55, validação por país).
2. Após o primeiro envio bem-sucedido **no canal `exibir_tela`**, o botão "Enviar para o motorista" passa a se chamar **"Reenviar para o motorista"**, e seu clique re-gera apenas o código OTP (mesma edição/token), sem criar uma edição nova.

Sem mudança de backend.

## Contexto

Hoje o modal (`central.php` L.2460-2513, JS L.~3970-4126) tem:

- **Telefone** como `<input type="text">` com `placeholder="+55 11 99999-0000"`. O atendente digita texto livre; o frontend só faz `.trim()` e manda no body como `telefone_motorista` / `destinatario`. Não há validação de formato nem indicação visual de país. Em operações multi-país (cliente ocasionalmente em PT/AR/UY) o atendente erra DDI ou esquece o "+".
- **Botão de envio** sempre rotulado "Enviar para o motorista". No canal `exibir_tela` o modal **não** fecha sozinho (decisão da spec `2026-05-08-canal-otp-exibir-tela-design.md`) — o atendente fica olhando para um botão chamado "Enviar" depois de já ter enviado, sem cue visual claro de "já mandou; clique de novo se quiser regerar". Nos outros canais o modal fecha em 1.2s e a questão não aparece.

## Decisões (alinhadas com o usuário)

- **Telefone**: usar plugin `intl-tel-input` (bandeira + busca de país, default Brasil). Formato enviado pra API é E.164 sem espaços (`iti.getNumber()`), padrão que adapters de WhatsApp/SMS esperam. Sem mudar contrato da API — `telefone_motorista` continua sendo string.
- **"Reenviar"**: aparece **apenas** no canal `exibir_tela`. Nos outros canais, o modal fecha automaticamente em 1.2s (comportamento atual), então o label nunca muda visualmente.
- **Reenviar = re-gerar OTP**: re-chama só `POST /api/v1/edicao-rota/{token}/otp/gerar` reusando o `token` salvo em `edicoesPorRota[rotaIdEmEdicao]`. Não cria nova `edicao_id` (não chama `POST /api/v1/edicao-rota`). Backend invalida o código anterior e devolve um novo.

## Arquitetura

Mudanças concentradas em **um único arquivo**:
- `componentes/Missoes/Logistica/sinais/Central/html/central.php`
  - HTML do modal: troca do input + bloco de loading do plugin (CSS/JS) inline no topo do arquivo.
  - JS do modal (mesmo arquivo, dentro do `<script>` que começa em L.~3855): init do plugin, leitura/validação do telefone, gerenciamento do label do botão e do branch de "reenviar".

Sem alterações em backend (`Mensageria/Otp`, `Plataforma/Api`) — o handler `GerarOtpHandler` já aceita re-chamada com mesmo token.

### Por que jsdelivr e não asset local

O codebase tem assets antigos do `intl-tel-input` em `controladores/_Src/galaxia_themes/app/assets/js/intlTelInput.min.js` e `.../css/intlTelInput.css`, mas:
- Versão indeterminada (sem cabeçalho de versão; `f` array com countries é a v15-v17 ish).
- Tema `app` não é o que serve a Central (Central usa tema `genezes`, ver `controladores/_Src/galaxia_themes/genezes/_theme.php` que carrega `kitgenesis.css` em L.138).
- O tema `genezes` já usa CDN (`bootstrap@5.0.2`, `quill@2.0.3`) via jsdelivr (L.75, L.83). Carregar `intl-tel-input@18.x` do mesmo CDN é coerente.
- Versão 18.x é a última que ainda funciona com jQuery dropdown sem ESM, alinhada ao stack atual da view (jQuery + Tailwind classes + FontAwesome).

## Componentes

### 1. Loading do plugin (CSS + JS)

Adicionar **imediatamente acima** do `<style>` que abre `central.php` na linha 1:

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/css/intlTelInput.css">
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/intlTelInput.min.js"></script>
<style>
    /* ... estilos atuais ... */
</style>
```

A view é renderizada via `galaxiaRoute->visual('html/central', ...)` dentro do template do tema `genezes`, mas o tema só envolve com `<html>/<head>/<body>` — `<link>` e `<script>` inline no corpo são respeitados pelo browser (já é o caso do `<style>` e `<script>` inline existentes).

`utils.js` é carregado pelo próprio plugin via opção `utilsScript` (lazy load), então não precisa de tag separada.

### 2. HTML do modal — substituição do campo telefone

Em `central.php` L.2473-2476, **substituir**:

```html
<div>
    <label class="g-texto-rotulo mb-1 block">Telefone do motorista</label>
    <input type="text" id="liberar-motorista-telefone" class="g-input w-full" placeholder="+55 11 99999-0000" autocomplete="off">
</div>
```

por:

```html
<div>
    <label class="g-texto-rotulo mb-1 block">Telefone do motorista</label>
    <input type="tel" id="liberar-motorista-telefone" class="g-input w-full" autocomplete="off">
</div>
```

Mudanças:
- `type="text"` → `type="tel"` (semântica + teclado mobile correto).
- Remove `placeholder` fixo (o plugin injeta `autoPlaceholder` dinâmico baseado no país).
- Tudo o mais (id, classe `g-input w-full`, `autocomplete="off"`) preservado.

### 3. Init e ciclo de vida do plugin

Adicionar perto do início do bloco JS do modal (antes do `$(document).on('click', '.btn-liberar-motorista', ...)` em L.~3972):

```js
let liberarMotoristaIti = null;

function ensureItiInit() {
    if (liberarMotoristaIti) return liberarMotoristaIti;
    if (typeof window.intlTelInput !== 'function') return null; // plugin não carregou (CDN offline)
    const input = document.getElementById('liberar-motorista-telefone');
    if (!input) return null;
    liberarMotoristaIti = window.intlTelInput(input, {
        initialCountry: 'br',
        preferredCountries: ['br', 'pt', 'us', 'ar', 'uy', 'py'],
        separateDialCode: true,
        nationalMode: true,
        autoPlaceholder: 'aggressive',
        utilsScript: 'https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js',
    });
    return liberarMotoristaIti;
}
```

Chamada dentro do handler de abertura do modal (`$(document).on('click', '.btn-liberar-motorista', ...)`), na ordem:
1. `enviadoExibirTela = false; resetBotaoLiberar();` (reset de estado/label)
2. `$('#liberar-motorista-modal').removeClass('hidden');` (mostra o modal — input precisa estar no DOM e visível)
3. `ensureItiInit();` (cria instância na 1ª vez; reusa nas subsequentes)

A instância é singleton — não destrói entre aberturas. País preferido do usuário **não persiste** entre sessões (out of scope).

### 4. Leitura e validação do telefone no submit

No handler `$(document).on('click', '#liberar-motorista-confirmar', ...)` (L.~4007), substituir:

```js
const telefone = ($('#liberar-motorista-telefone').val() || '').trim();
```

por:

```js
let telefone = '';
const inputRaw = ($('#liberar-motorista-telefone').val() || '').trim();
if (liberarMotoristaIti && inputRaw) {
    if (!liberarMotoristaIti.isValidNumber()) {
        mostrarFeedback($feedback, 'erro', 'Telefone inválido. Confira DDD e número.');
        return;
    }
    telefone = liberarMotoristaIti.getNumber(); // E.164, ex: +5513997919204
} else {
    telefone = inputRaw; // fallback: plugin não carregou (CDN offline)
}
```

Comportamento:
- Vazio é OK (o `if (!telefone && !email)` mais abaixo continua bloqueando os dois vazios).
- Se preencheu mas é inválido pelo plugin, bloqueia com erro claro.
- Se plugin não carregou, manda raw (preserva fluxo atual sem mascarar a falha de carregamento — o atendente continua conseguindo enviar; eventual problema de formatação fica visível no canal).

`telefone` é usado em duas chamadas downstream sem mudança:
- `body.telefone_motorista = telefone || email;`
- `gerarResp` body: `destinatario: canal === 'email' ? (email || telefone) : (telefone || email),`

### 5. Estado e label do botão "Reenviar"

Variável de estado no escopo do bloco JS do modal (junto com `let rotaIdEmEdicao = null;` em L.3971):

```js
let enviadoExibirTela = false;
```

Helper:
```js
function resetBotaoLiberar() {
    $('#liberar-motorista-confirmar')
        .html('<i class="fas fa-paper-plane mr-2"></i>Enviar para o motorista');
}
```

**Reset** em dois pontos:
1. Ao abrir o modal (`$(document).on('click', '.btn-liberar-motorista', ...)` antes de `removeClass('hidden')`):
   ```js
   enviadoExibirTela = false;
   resetBotaoLiberar();
   ```
2. No fechar/cancelar (`$(document).on('click', '#liberar-motorista-fechar, #liberar-motorista-cancelar', ...)`):
   ```js
   enviadoExibirTela = false;
   resetBotaoLiberar();
   ```

**Set** após sucesso no branch `if (canal === 'exibir_tela' && codigoExibido)` (atualmente L.4061+, depois do `$feedback.html(...)` que monta o painel):

```js
enviadoExibirTela = true;
$('#liberar-motorista-confirmar')
    .html('<i class="fas fa-paper-plane mr-2"></i>Reenviar para o motorista');
```

### 6. Branch "reenviar" no handler do botão confirmar

No início do `$(document).on('click', '#liberar-motorista-confirmar', async function () { ... })`, adicionar **antes** do fluxo atual de criar edição:

```js
if (enviadoExibirTela && edicoesPorRota[rotaIdEmEdicao]?.token) {
    const $btn = $(this);
    const $feedback = $('#liberar-motorista-feedback');
    const token = edicoesPorRota[rotaIdEmEdicao].token;

    // Lê telefone/email só pra montar destinatario; validação completa não é necessária
    // (já passou na 1ª chamada) mas mantemos isValidNumber pra evitar mandar lixo se editou.
    let telefone = '';
    const inputRaw = ($('#liberar-motorista-telefone').val() || '').trim();
    if (liberarMotoristaIti && inputRaw) {
        if (!liberarMotoristaIti.isValidNumber()) {
            mostrarFeedback($feedback, 'erro', 'Telefone inválido. Confira DDD e número.');
            return;
        }
        telefone = liberarMotoristaIti.getNumber();
    } else {
        telefone = inputRaw;
    }
    const email = ($('#liberar-motorista-email').val() || '').trim();

    $btn.prop('disabled', true).addClass('opacity-50');
    try {
        const gerarResp = await api('POST', `/api/v1/edicao-rota/${encodeURIComponent(token)}/otp/gerar`, {
            canal: 'exibir_tela',
            destinatario: telefone || email,
        });
        const novoCodigo = gerarResp?.data?.codigo_exibido;
        if (novoCodigo) {
            // Atualiza in-place o <code> do código e o botão Copiar correspondente
            const $codigoEl = $feedback.find('code').first();
            $codigoEl.text(novoCodigo);
            $feedback.find('.copiar-exibir-tela[data-copiar-rotulo="Código"]')
                .attr('data-copiar-valor', novoCodigo);
            // Flash visual rápido pra sinalizar update
            $codigoEl.addClass('ring-2 ring-yellow-300');
            setTimeout(() => $codigoEl.removeClass('ring-2 ring-yellow-300'), 1200);
        } else {
            mostrarFeedback($feedback, 'erro', 'Resposta inesperada do servidor — código não atualizado.');
        }
    } catch (e) {
        mostrarFeedback($feedback, 'erro', e.message || 'Falha ao reenviar código.');
    } finally {
        $btn.prop('disabled', false).removeClass('opacity-50');
    }
    return; // não passa pro fluxo de criar edição
}
```

O resto do handler (criar edição + gerar OTP inicial) permanece inalterado.

## Data flow

### Primeiro envio (canal `exibir_tela`)
```
1. Atendente clica .btn-liberar-motorista
   → reset enviadoExibirTela=false, label "Enviar para o motorista"
   → ensureItiInit() (1ª vez) cria instância iti com BR default
   → modal abre
2. Digita número (ex.: 13997919204) — plugin formata (13) 99791-9204
3. Clica "Enviar para o motorista"
   → iti.isValidNumber() true → telefone = "+5513997919204"
   → POST /api/v1/edicao-rota { telefone_motorista, ... }     → cria edicao_id + token
   → POST /api/v1/edicao-rota/{token}/otp/gerar { canal:'exibir_tela', destinatario }
                                                              → backend devolve codigo_exibido
   → painel mostra código/token/link com botões Copiar
   → enviadoExibirTela=true, label "Reenviar para o motorista"
```

### Reenvio (mesmo modal aberto)
```
4. Atendente clica "Reenviar para o motorista"
   → enviadoExibirTela===true && token existe → entra no branch reenvio
   → POST /api/v1/edicao-rota/{token}/otp/gerar { canal:'exibir_tela', destinatario }
                                                              → backend invalida código antigo, gera novo
   → painel: <code> do Código atualiza in-place + flash ring-yellow
   → label permanece "Reenviar para o motorista" (continua válido pra novo reenvio)
```

### Outros canais (whatsapp/email/sms)
```
1-3 igual ao primeiro envio (com canal diferente).
3'. Backend não devolve codigo_exibido — pula branch enviadoExibirTela.
4'. setTimeout 1200ms fecha o modal (comportamento atual preservado).
   → label nunca muda visualmente; enviadoExibirTela permanece false.
```

## Error handling

| Cenário | Tratamento |
|---|---|
| `intl-tel-input` (CDN) não carrega | `ensureItiInit()` retorna `null`. `liberarMotoristaIti` fica `null`. Submit usa `inputRaw` puro, sem validação `isValidNumber`. UX é a atual; backend recebe o que o atendente digitar. |
| Telefone inválido pelo plugin (`!isValidNumber()` com input não vazio) | `mostrarFeedback` "Telefone inválido. Confira DDD e número." Não chama API. |
| Telefone vazio + e-mail vazio | Validação atual ("Informe ao menos um contato...") permanece. |
| `/otp/gerar` falha no reenvio (rede/4xx/5xx) | `mostrarFeedback` com mensagem de erro. Botão volta a ficar habilitado. Label permanece "Reenviar" pra permitir nova tentativa. |
| `/otp/gerar` retorna 4xx por token expirado no reenvio | Backend devolve mensagem clara; atendente vê em `$feedback` e fecha/reabre o modal pra criar edição nova. Não tentamos auto-recuperar (criar nova edição automaticamente) — evita criar `edicao_id` "fantasma" que o atendente não percebe. |
| Resposta de `/otp/gerar` sem `codigo_exibido` | Mensagem genérica "Resposta inesperada do servidor". |

## Out of scope (não vai ser feito nesse spec)

- I18n do label "Reenviar para o motorista" (manter pt-br inline, igual aos outros textos do modal). Quando `linguagens/{pt-br,en,es}.json` for varrido pra esse modal, ambos os labels entram juntos.
- Persistir país preferido por motorista (cache local, "Salvar como padrão").
- Validação/format do campo e-mail.
- Auto-close do modal nos canais não-exibir_tela (continua 1.2s).
- Mexer no checkbox "Salvar como padrão" — segue salvando `{telefone, email, canal, tempo}`. Pequena observação: agora `telefone` salvo é E.164 (`+5513...`); o `salvarPadraoLocalmente` só guarda string, então tudo continua funcionando.

## Files touched

- `componentes/Missoes/Logistica/sinais/Central/html/central.php`
  - L.2473-2476 — HTML do input telefone.
  - Topo do arquivo (junto com `<style>` ou logo acima) — `<link>` + `<script src>` do plugin.
  - L.~3855-4126 — bloco `<script>` do JS do modal: init, validação, label do botão, branch de reenvio.

Nenhum outro arquivo é tocado. Backend, migrations, testes de integração existentes não mudam.
