# Spec: Bottom Sheet de Ações por Parada + Observação Individual — Reordenar Rota

**Data:** 2026-05-27  
**Status:** Aprovado

## Contexto

A tela de reordenação de rota (`editar_rota.php`) exibe um botão `⋮` (`btn-otimizar-daqui`) em cada card de parada. Atualmente ele chama `otimizarDaqui()` diretamente ao clicar. O objetivo é substituir esse comportamento direto por um bottom sheet com duas ações, e adicionar a possibilidade de o motorista registrar observações por parada.

## Objetivo

1. O botão `⋮` abre um **bottom sheet** com duas opções:
   - ✏️ **Otimizar rota daqui** — abre modal de confirmação, depois executa a otimização existente
   - 📝 **Observação da parada** — abre modal de texto livre para observação individual da entrega

2. A observação fica visível no **card da rota** (abaixo do nome do cliente) e no **Acompanhamento da rota do motorista** (`central.php`), abaixo do nome do cliente na tabela de pedidos.

## Arquitetura

- **Zero mudanças de backend** — as observações são serializadas dentro do array `rota_proposta` (campo `observacao` por parada), que já é retornado por `ConsultarEdicaoRotaLaboratorio`
- **Zero migration** — nenhuma nova coluna
- As observações persistem apenas após o submit; se o motorista fechar antes de enviar, são perdidas (aceitável para MVP)

## Fora do escopo

- Persistência de rascunho (observações salvas antes do submit)
- Validação de tamanho máximo da observação
- i18n: textos somente em pt-br nesta iteração
- Edição/exclusão de observações pelo gestor

---

## 1. HTML — Bottom sheet

Um único elemento no DOM de `editar_rota.php`, após o `.enviar-modal-overlay`:

```html
<div class="parada-sheet-overlay oculto" id="paradaSheetOverlay" role="dialog" aria-modal="true">
  <div class="parada-sheet" id="paradaSheet">
    <div class="parada-sheet-titulo" id="paradaSheetTitulo">— —</div>
    <button type="button" class="parada-sheet-btn" id="paradaSheetOtimizar">
      ✏️ Otimizar rota daqui
    </button>
    <button type="button" class="parada-sheet-btn" id="paradaSheetObs">
      📝 Observação da parada
    </button>
    <button type="button" class="parada-sheet-cancelar" id="paradaSheetCancelar">
      Cancelar
    </button>
  </div>
</div>
```

---

## 2. HTML — Modal de confirmação de otimização

Reutiliza as classes `.enviar-modal-*` já existentes:

```html
<div class="enviar-modal-overlay oculto" id="otimizarConfirmOverlay"
     role="dialog" aria-modal="true">
  <div class="enviar-modal" role="document">
    <div class="enviar-modal-titulo">Otimizar a partir daqui?</div>
    <div class="enviar-modal-sub" id="otimizarConfirmSub">
      A ordem das entregas a partir de <strong id="otimizarConfirmCliente">—</strong>
      será recalculada pelo mapa.
    </div>
    <div class="enviar-modal-acoes">
      <button type="button" class="enviar-modal-btn enviar-modal-btn-cancelar"
              id="otimizarConfirmCancelar">Cancelar</button>
      <button type="button" class="enviar-modal-btn enviar-modal-btn-ok"
              id="otimizarConfirmOk">Otimizar</button>
    </div>
  </div>
</div>
```

---

## 3. HTML — Modal de observação da parada

Reutiliza as classes `.enviar-modal-*`:

```html
<div class="enviar-modal-overlay oculto" id="paradaObsOverlay"
     role="dialog" aria-modal="true">
  <div class="enviar-modal" role="document">
    <div class="enviar-modal-titulo">📝 Observação da parada</div>
    <div class="enviar-modal-sub" id="paradaObsCliente">—</div>
    <textarea id="paradaObsTexto" class="enviar-modal-textarea"
              placeholder="Ex: entregar no fundo da loja, interfone 12…"
              rows="3"></textarea>
    <div class="enviar-modal-acoes">
      <button type="button" class="enviar-modal-btn enviar-modal-btn-cancelar"
              id="paradaObsCancelar">Cancelar</button>
      <button type="button" class="enviar-modal-btn enviar-modal-btn-ok"
              id="paradaObsSalvar">Salvar</button>
    </div>
  </div>
</div>
```

---

## 4. HTML — Texto de observação no card da parada

Dentro do `<li>` de cada parada, após o nome do cliente (`.parada-cliente`), inserir placeholder vazio que o JS popula:

```html
<div class="parada-obs" id="parada-obs-<?= $idx ?>"></div>
```

Quando preenchida pelo JS: `el.textContent = texto`.  
Quando vazia: elemento oculto via CSS (`.parada-obs:empty { display: none }`).

---

## 5. CSS (em `_layout.php`)

### Bottom sheet

```css
.parada-sheet-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: rgba(0,0,0,0.55);
  display: flex;
  align-items: flex-end;
  animation: seq-modal-fade 0.18s ease;
}
.parada-sheet-overlay.oculto { display: none; }
.parada-sheet {
  width: 100%;
  background: #13103a;
  border-radius: 18px 18px 0 0;
  padding: 20px 16px 28px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  animation: seq-modal-pop 0.22s ease;
}
.parada-sheet-titulo {
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--primary-soft);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: 4px;
}
.parada-sheet-btn {
  width: 100%;
  text-align: left;
  background: rgba(139,92,246,0.1);
  border: 1px solid var(--border-strong);
  border-radius: 12px;
  padding: 14px 16px;
  font-size: 1rem;
  color: #ffffff;
  cursor: pointer;
}
.parada-sheet-btn:active { background: rgba(139,92,246,0.22); }
.parada-sheet-cancelar {
  width: 100%;
  background: transparent;
  border: none;
  color: var(--muted-2);
  font-size: 0.95rem;
  padding: 10px;
  margin-top: 4px;
  cursor: pointer;
}
```

### Observação no card

```css
.parada-obs {
  font-size: 0.78rem;
  color: rgba(251,191,36,0.85);
  font-style: italic;
  margin-top: 3px;
  word-break: break-word;
  white-space: pre-wrap;
}
.parada-obs:empty { display: none; }
```

### Indicador âmbar no ⋮ quando há observação

```css
.btn-otimizar-daqui.tem-obs {
  border-color: var(--warning);
  background: rgba(245,158,11,0.15);
  color: var(--warning);
}
```

### Observação no Acompanhamento (central.php)

```css
.acompanhar-parada-obs {
  font-size: 0.78rem;
  color: rgba(251,191,36,0.8);
  font-style: italic;
  margin-top: 2px;
  white-space: normal;
  word-break: break-word;
}
```

---

## 6. JavaScript (IIFE em `editar_rota.php`)

### Mudança no handler existente do `⋮`

```js
// Substituir o listener atual de .btn-otimizar-daqui:
lista.addEventListener('click', function (e) {
  var btn = e.target.closest('.btn-otimizar-daqui');
  if (!btn || btn.disabled) return;
  e.stopPropagation();
  var li = btn.closest('li');
  if (li) abrirParadaSheet(btn, li);
});
```

### Bottom sheet

```js
var _paradaSheetBtn  = null;
var _paradaSheetLi   = null;

function abrirParadaSheet(btn, li) {
  _paradaSheetBtn = btn;
  _paradaSheetLi  = li;
  document.getElementById('paradaSheetTitulo').textContent =
    li.querySelector('.parada-cliente')?.textContent?.trim() || 'Parada';
  document.getElementById('paradaSheetOverlay').classList.remove('oculto');
}

function fecharParadaSheet() {
  document.getElementById('paradaSheetOverlay').classList.add('oculto');
}

document.getElementById('paradaSheetCancelar').addEventListener('click', fecharParadaSheet);
document.getElementById('paradaSheetOverlay').addEventListener('click', function(e) {
  if (e.target === this) fecharParadaSheet();
});
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape') fecharParadaSheet();
});
```

### Modal de confirmação de otimização

```js
document.getElementById('paradaSheetOtimizar').addEventListener('click', function () {
  fecharParadaSheet();
  var cliente = _paradaSheetLi?.querySelector('.parada-cliente')?.textContent?.trim() || '—';
  document.getElementById('otimizarConfirmCliente').textContent = cliente;
  document.getElementById('otimizarConfirmOverlay').classList.remove('oculto');
});

document.getElementById('otimizarConfirmCancelar').addEventListener('click', function () {
  document.getElementById('otimizarConfirmOverlay').classList.add('oculto');
});
document.getElementById('otimizarConfirmOk').addEventListener('click', function () {
  document.getElementById('otimizarConfirmOverlay').classList.add('oculto');
  if (_paradaSheetBtn && _paradaSheetLi) otimizarDaqui(_paradaSheetBtn, _paradaSheetLi);
});
document.getElementById('otimizarConfirmOverlay').addEventListener('click', function(e) {
  if (e.target === this) this.classList.add('oculto');
});
```

### Modal de observação

```js
document.getElementById('paradaSheetObs').addEventListener('click', function () {
  fecharParadaSheet();
  var cliente = _paradaSheetLi?.querySelector('.parada-cliente')?.textContent?.trim() || '—';
  document.getElementById('paradaObsCliente').textContent = cliente;
  document.getElementById('paradaObsTexto').value = _paradaSheetLi?.dataset.observacao || '';
  document.getElementById('paradaObsOverlay').classList.remove('oculto');
  setTimeout(function () { document.getElementById('paradaObsTexto').focus(); }, 80);
});

document.getElementById('paradaObsCancelar').addEventListener('click', function () {
  document.getElementById('paradaObsOverlay').classList.add('oculto');
});
document.getElementById('paradaObsOverlay').addEventListener('click', function(e) {
  if (e.target === this) this.classList.add('oculto');
});
document.getElementById('paradaObsSalvar').addEventListener('click', function () {
  var texto = document.getElementById('paradaObsTexto').value.trim();
  if (_paradaSheetLi) {
    _paradaSheetLi.dataset.observacao = texto;
    var obsEl = _paradaSheetLi.querySelector('.parada-obs');
    if (obsEl) obsEl.textContent = texto;
    _paradaSheetBtn.classList.toggle('tem-obs', texto.length > 0);
  }
  document.getElementById('paradaObsOverlay').classList.add('oculto');
});
```

### Submit — incluir observação na serialização

```js
// No handler de formRota submit, substituir:
return { id: li.dataset.id, pedido_id: li.dataset.pedido || null };
// por:
return {
  id: li.dataset.id,
  pedido_id: li.dataset.pedido || null,
  observacao: li.dataset.observacao || null,
};
```

---

## 7. central.php — Acompanhamento

### CSS (bloco `<style>` inline)

```css
.acompanhar-parada-obs {
  font-size: 0.78rem;
  color: rgba(251,191,36,0.8);
  font-style: italic;
  margin-top: 2px;
  white-space: normal;
  word-break: break-word;
}
```

### JS — célula "Cliente" na tabela

```js
// No forEach que monta os <tr>, substituir a linha do cliente:
const obs = m.observacao
  ? `<div class="acompanhar-parada-obs">${$('<div>').text(m.observacao).html()}</div>`
  : '';
// célula:
<td class="truncate max-w-[180px]">${cliente}${obs}</td>
```

O `$('<div>').text(m.observacao).html()` escapa o texto para prevenir XSS.

---

## Fluxo completo

```
Motorista clica ⋮ em "COISAS DA FAMILIA"
  → bottom sheet abre com título "COISAS DA FAMILIA"
  
  → [✏️ Otimizar rota daqui]
    → sheet fecha, modal confirmação abre
    → [Otimizar] → otimizarDaqui() executa normalmente
    → [Cancelar] → fecha, nada muda
    
  → [📝 Observação da parada]
    → sheet fecha, modal observação abre (pré-preenchido se já há texto)
    → motorista digita "entregar no fundo da loja"
    → [Salvar] → data-observacao="entregar no fundo da loja" no <li>
               → texto aparece abaixo de "COISAS DA FAMILIA" no card
               → ⋮ ganha borda âmbar (.tem-obs)
               
Motorista clica "Confirmar envio"
  → rota_proposta = [..., {id, pedido_id, observacao: "entregar no fundo da loja"}, ...]
  → POST salvo no banco

Gestor abre Acompanhamento
  → polling retorna rota_proposta com observacao por parada
  → tabela exibe: COISAS DA FAMILIA
                  entregar no fundo da loja   ← âmbar, itálico
```
