Skip to content

Commit b0fe952

Browse files
PhyBrunoclaude
andcommitted
fix(007): tres bugs que so apareciam com o dedo, a tela estreita e a venda seguinte (AD-192)
Revisao da 007 logo apos a implementacao, com conferencia no navegador real em 390px. Os tres primeiros achados sao bugs de comportamento que a propria feature introduziu. 1. O campo de desconto de capa reabria **vazio** com um desconto valendo. O controle so vive na etapa 2, entao ir a revisao e voltar (ou cruzar o breakpoint) o remontava -- e como o `onBlur` trata campo vazio como "desisti do ajuste", bastava tabular por ele para o desconto sumir sem que ninguem o tivesse removido. Nao e perda cosmetica: e dinheiro negociado com o cliente na frente do caixa. 2. O wizard ficava preso na etapa 3 depois de finalizar. `data-model.md` §2 supunha que finalizar remontaria a arvore; nao remonta -- zera o carrinho e chama `abrirSessaoDeVenda` na mesma arvore. O reinicio segue a identidade da sessao (o `VENDA_INICIADA` que abre o historico), nunca o tamanho dele, que jogaria o operador a etapa 1 a cada bipagem. 3. A camera do scanner reiniciava sozinha no meio da mira: `onCodigoLido` estava nas dependencias do efeito e o slot cria uma funcao nova a cada render do pai. Lida por `ref` agora. Junto, a guarda que faltava depois do `await` do `detect` -- fechar a janela cancela o proximo frame, nunca a promessa em voo, e um codigo lido apos o gesto de sair virava item numa tela que ja nao estava la. Renderizacao: `flex-wrap` sozinho nao quebrava nada (toda celula era `flex-1 min-w-0`, e quem encolhe ate zero nunca "nao cabe") -- o piso por celula e o que faz a quebra; a notificacao do goey-toast saia pela direita da tela e a primeira correcao empatava em especificidade com o pacote, que carrega depois (medido: caixa em 358px, titulo em 801px indo ate 827); o cabecalho mobile mostrava o titulo do desktop e virava "Centrium ...", perdendo as duas informacoes -- o Pencil escreve so "Centrium Checkout" no no `YXaRZ`. Mais alvos de toque de 40-44px abaixo de `md`, total da lista em `font-mono` e o cabecalho do card de cliente quebrando em linhas. Fecha o item 47 de PENDENCIES.md de verdade: o gate da 014 em viewport compacta ganhou o par recusado/aceito -- sem o aceito, "nenhum pagamento aplicado" passaria tambem se o gate nao fosse consultado no mobile. Verificacao: tsc e eslint limpos; 1097 testes unit/integracao e 173 E2E; toast e cabecalho reconferidos no navegador em 390px, zero estouro horizontal medido em toda a arvore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Ki1CPfueZmf4M4t1s52Ps
1 parent 833cef6 commit b0fe952

20 files changed

Lines changed: 1569 additions & 76 deletions

.specs/project/STATE.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2520,4 +2520,26 @@ Com isso, `erro-recuperavel` passa a ter dois desfechos: **com** marca, `ErrorRe
25202520

25212521
**`FR-005` (sem atalhos no mobile) já estava cumprido pela 013**, e por ausência estrutural: `projetarAtalhos` recebe a plataforma e devolve lista vazia no compacto, então `DicaAtalhos` não registra tecla alguma no mapa central. A 007 acrescentou a verificação automatizada disso (`appShell.spec.tsx`), que antes não existia.
25222522

2523-
**Impact:** criados — `src/client/domain/layout/{classificarLayout,suportaScannerCamera}.ts`, `src/client/layout/{useIsMobile,obterPlataforma,AppShell}.ts(x)`, `src/client/layout/desktop/DesktopLayout.tsx`, `src/client/layout/mobile/{MobileWizard,EtapaClienteProdutos,EtapaPagamento,EtapaRevisao,ScannerCamera}.tsx`, `src/client/features/pagamento/ConfiguracaoPagamento.tsx`; alterados — `src/client/App.tsx` (perde `TelaDeVenda`, renderiza `AppShell`), `src/client/layout/usePlataforma.ts`, `src/client/features/pagamento/PainelPagamentoETotais.tsx`, `src/client/features/finalizacao-suspensao/AcoesFinaisVenda.tsx`, `src/client/features/carrinho/EntradaRapidaProduto.tsx` (slot `renderizarCaptura` e `flex-wrap` na linha de prévia); testes — 3 unitários de domínio, 6 de integração (`appShell`, `mobileWizard`, `pagamentoMobile`, `scannerCamera`, `ausenciaEstrutural`, `semDuplicacaoRegra`), 4 E2E (`layout-desktop`, `layout-mobile`, `layout-responsivo`, `layout-scanner`) e o helper `tests/support/layout.tsx`. Verificação: `tsc --noEmit` limpo; 1064 testes unit/integração e 171 E2E passando.
2523+
### AD-192: a 007 quebrou três coisas que só apareciam com o dedo, a tela estreita e a venda seguinte (rodada de revisão, 2026-09-09)
2524+
2525+
**Origem:** revisão da 007 logo após a implementação — cobertura de comportamento e conferência visual em 390px no navegador real. Os três primeiros achados são **bugs de comportamento** que a feature introduziu; os demais são de renderização.
2526+
2527+
**1. O campo de desconto de capa reabria vazio com um desconto valendo — e tabular por ele o apagava.** `ControleDescontoCapa` nasceu numa tela que nunca desmontava. A 007 criou dois caminhos de remonte que não existiam: ir à revisão e voltar (o controle só vive na etapa 2) e cruzar o breakpoint. Em ambos o campo reabria em branco enquanto o desconto seguia aplicado na venda — e, como o `onBlur` trata campo vazio como "desisti do ajuste", bastava o operador passar por ele com TAB para o desconto sumir sem que ninguém o tivesse removido. O TSDoc do `modo` já dizia a regra ("um desconto já aplicado continua mandando"); faltava o número obedecê-la. **Não é perda cosmética: é dinheiro negociado com o cliente na frente do caixa.**
2528+
2529+
**2. O wizard ficava preso na etapa 3 depois de finalizar.** `data-model.md` §2 supunha que finalizar trocaria de tela e remontaria o wizard (I1). Não é o que acontece: `useFinalizarOuSuspenderVenda` zera o carrinho e chama `abrirSessaoDeVenda('NOVA')` **na mesma árvore montada**. Sem reinício explícito, o operador terminava a venda e continuava na "Revisão e finalização" de uma venda vazia, com o campo de código do próximo cliente uma etapa atrás. A identidade da sessão é o `VENDA_INICIADA` que abre o histórico — objeto novo a cada `resetarAuditoria` e o mesmo durante toda a venda; pendurar o reinício no **tamanho** do histórico jogaria o operador à etapa 1 a cada bipagem.
2530+
2531+
**3. A câmera do scanner reiniciava sozinha no meio da mira.** `onCodigoLido` estava nas dependências do efeito que liga a câmera, e o slot `renderizarCaptura?.((codigo) => …)` cria uma função nova a cada render do pai. Qualquer re-render enquanto a janela estava aberta — uma query que assenta, um item que entra na lista — derrubava o efeito, encerrava a trilha de vídeo e chamava `getUserMedia` de novo. Resolvido lendo a callback por `ref`. Junto veio a guarda que faltava **depois** do `await` do `detect`: fechar a janela cancela o próximo `requestAnimationFrame`, nunca a promessa já em voo, e um código decodificado após o gesto de sair virava produto no carrinho de uma tela que não estava mais lá.
2532+
2533+
**4. `flex-wrap` sozinho não quebra linha nenhuma.** A correção original da barra de entrada rápida acrescentou `flex-wrap` e parou aí. Toda célula era `flex-1 min-w-0`, e um item que pode encolher até zero nunca chega a "não caber": as sete se espremiam na mesma linha de 326px, o campo de código ficava com 39px e quatro rótulos se sobrepunham. O que faz a quebra acontecer é o **piso por célula** (`min-w-*`, revogado em `md:`).
2534+
2535+
**5. A notificação saía pela direita da tela, e a primeira correção não pegou.** `goey-toast` monta a faixa compacta como `width: fit-content` com o título em `white-space: nowrap`: no desktop um aviso de 826px ainda sobra na tela de 1440; em 390px o operador lia "Esta venda já tem forma de pagamento aplicada e cada ven…". O teto de largura entrou na caixa de conteúdo (que é a que o pacote mede para desenhar a bolha) e funcionou. A quebra de linha do título **não**: o pacote declara exatamente `.gooey-header > .gooey-title`, a mesma especificidade (0,2,0) da nossa regra, e a folha dele carrega depois — num empate ganha quem vem por último. Medido no navegador: caixa em 358px, título ainda em 801px com a borda direita em 827px. Corrigido subindo para (0,3,0) com `.gooey-content` na frente, sem `!important` — que se reservaria para uma briga contra `!important`, e não é o caso.
2536+
2537+
**6. O cabeçalho mobile mostrava o título do desktop.** Reusar `tituloDoProduto` acrescentava a empresa ("Centrium Checkout - Organizações Tabajara") num cabeçalho de 358px, e o resultado era "Centrium …" — perdendo **as duas** informações. O Pencil escreve só "Centrium Checkout" no nó `YXaRZ`; a empresa não está lá por decisão de desenho. `NOME_DO_PRODUTO` passou a ser exportado do domínio. Encolher a pílula do operador não bastava: as duas metades do cabeçalho encolhem juntas e o flex tira mais de quem é maior, então um `UsuarioNome` longo ficava inteiro enquanto o título cedia — daí o teto de largura no nome do operador, com `title` guardando o valor completo.
2538+
2539+
**Além disso:** alvos de toque de 28px (lápis/lixeira da lista mobile, barras do indicador de etapa) subiram para 40–44px só abaixo de `md`, o utilitário `cc-alvo-toque` cresce a área sensível sem mexer num pixel do desenho, o total da lista mobile passou a `font-mono tabular-nums` (regra de tipografia do projeto, estava em Inter), e o cabeçalho do card de cliente quebra em linhas no compacto — antes "CONSUMIDOR FINAL" virava "CO" e o vendedor sumia sob a palavra "Recolhido".
2540+
2541+
**O item 47 de `PENDENCIES.md` fechou de verdade:** o cenário de recusa do gate da 014 em viewport compacta existe agora em `tests/e2e/validacao-previa.spec.ts`, com o par aceito/recusado — sem o aceito, "nenhum pagamento aplicado" passaria também se o gate simplesmente não fosse consultado no compacto.
2542+
2543+
**Impact:** alterados — `src/client/features/pagamento/ControleDescontoCapa.tsx`, `src/client/layout/mobile/{MobileWizard,ScannerCamera}.tsx`, `src/client/features/carrinho/{EntradaRapidaProduto,ListaItensMobile}.tsx`, `src/client/features/cliente/CampoClienteVenda.tsx`, `src/client/domain/sessao/identidadePdv.ts` (`NOME_DO_PRODUTO` exportado), `src/client/styles/global.css` (`cc-alvo-toque` + as duas regras do toast); testes — `tests/integration/layoutPreservacaoEstado.spec.tsx` (novo, 7 casos, incluindo o TAB que apagava o desconto), acréscimos em `appShell`/`mobileWizard`/`scannerCamera`/`ausenciaEstrutural`/`semDuplicacaoRegra` e o bloco compacto de `tests/e2e/validacao-previa.spec.ts`. Verificação: `tsc --noEmit` e `eslint` limpos; 1097 testes unit/integração e 173 E2E passando; toast e cabeçalho reconferidos no navegador em 390px (título do aviso passou a 338px de largura, borda direita em 364 sobre 390 — dentro da tela), e zero estouro horizontal medido em toda a árvore.
2544+
2545+
**Impact (AD-191):** criados — `src/client/domain/layout/{classificarLayout,suportaScannerCamera}.ts`, `src/client/layout/{useIsMobile,obterPlataforma,AppShell}.ts(x)`, `src/client/layout/desktop/DesktopLayout.tsx`, `src/client/layout/mobile/{MobileWizard,EtapaClienteProdutos,EtapaPagamento,EtapaRevisao,ScannerCamera}.tsx`, `src/client/features/pagamento/ConfiguracaoPagamento.tsx`; alterados — `src/client/App.tsx` (perde `TelaDeVenda`, renderiza `AppShell`), `src/client/layout/usePlataforma.ts`, `src/client/features/pagamento/PainelPagamentoETotais.tsx`, `src/client/features/finalizacao-suspensao/AcoesFinaisVenda.tsx`, `src/client/features/carrinho/EntradaRapidaProduto.tsx` (slot `renderizarCaptura` e `flex-wrap` na linha de prévia); testes — 3 unitários de domínio, 6 de integração (`appShell`, `mobileWizard`, `pagamentoMobile`, `scannerCamera`, `ausenciaEstrutural`, `semDuplicacaoRegra`), 4 E2E (`layout-desktop`, `layout-mobile`, `layout-responsivo`, `layout-scanner`) e o helper `tests/support/layout.tsx`. Verificação: `tsc --noEmit` limpo; 1064 testes unit/integração e 171 E2E passando.

src/client/App.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,3 @@ export function App({
212212
/>
213213
);
214214
}
215-

src/client/domain/layout/suportaScannerCamera.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ export function suportaScannerCamera(userAgent: string, hasBarcodeDetector: bool
3333
}
3434

3535
return (
36-
/Android/.test(userAgent) &&
37-
/Chrome\//.test(userAgent) &&
38-
!OUTROS_CHROMIUM.test(userAgent)
36+
/Android/.test(userAgent) && /Chrome\//.test(userAgent) && !OUTROS_CHROMIUM.test(userAgent)
3937
);
4038
}

src/client/domain/sessao/identidadePdv.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ export interface IdentidadePdvBruta {
2323
readonly CadMaqCod?: string | undefined;
2424
}
2525

26-
/** Como no Pencil: "Centrium Checkout - Organizações Tabajara" (nó `HSvSJ`). */
27-
const NOME_DO_PRODUTO = 'Centrium Checkout';
26+
/**
27+
* O nome do produto, sozinho — sem a empresa.
28+
*
29+
* Exportado porque o cabeçalho compacto o usa **assim**, cru: o Pencil escreve
30+
* "Centrium Checkout" no nó `YXaRZ` da tela mobile e deixa a empresa de fora,
31+
* ao contrário da barra do desktop (`HSvSJ`), que traz os dois. Não é economia
32+
* de espaço arbitrária — em 390px o título completo não cabe de jeito nenhum, e
33+
* truncá-lo custa as duas informações de uma vez ("Centrium ..."), em vez de
34+
* apenas a menos importante.
35+
*/
36+
export const NOME_DO_PRODUTO = 'Centrium Checkout';
2837

2938
/**
3039
* Nome fantasia na frente da razão social: é o nome pelo qual o operador

src/client/features/carrinho/EntradaRapidaProduto.tsx

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -777,12 +777,24 @@ export function EntradaRapidaProduto({
777777
{/* `flex-wrap`: no desktop a linha nunca quebra (sobra largura), mas na
778778
etapa 1 do wizard mobile as células caem umas sob as outras em vez de
779779
estourar a lateral da tela — é a mesma barra, reflowada, não um
780-
segundo componente (`SC-001`). */}
780+
segundo componente (`SC-001`).
781+
782+
**O `flex-wrap` sozinho não quebrava nada** (achado em 2026-09-08):
783+
toda célula era `flex-1 min-w-0`, e um item que pode encolher até zero
784+
nunca chega a "não caber" — as sete se espremiam na mesma linha de
785+
326px, o campo de código ficava com 39px e os rótulos "Quantidade",
786+
"Unidade", "Preço unitário" e "Desconto do item" se sobrepunham em
787+
três linhas ilegíveis. O piso por célula (`min-w-*`, revogado em
788+
`md:` para o desktop voltar a ser a linha única do Pencil) é o que
789+
faz a quebra acontecer de verdade, e reproduz o empilhamento do
790+
desenho mobile: código na primeira faixa com a lupa e o Scanner
791+
(`dfZEs`/`kU6Z5`), depois os pares de valores (`J5G7EE`/`aRe5V`) e o
792+
botão de inserir ocupando a largura toda (`q2NBVJ`). */}
781793
<div className="flex flex-wrap items-end gap-sm" data-testid="previa-insercao-produto">
782-
<label className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
794+
<label className="flex min-w-[9.5rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
783795
<span className="flex items-center gap-xs font-semibold text-muted-foreground">
784-
<Barcode className="size-4" aria-hidden="true" />
785-
{rotuloCampoCodigo}
796+
<Barcode className="size-4 shrink-0" aria-hidden="true" />
797+
<span className="truncate">{rotuloCampoCodigo}</span>
786798
</span>
787799
<input
788800
ref={campoCodigo}
@@ -837,7 +849,7 @@ export function EntradaRapidaProduto({
837849
ponto do campo acendia o "−" (achado do usuário, 2026-09-03).
838850
Com o rótulo apontando para o input por `htmlFor`, a associação
839851
acessível continua de pé e o hover do "−" volta a ser só o dele. */}
840-
<div className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
852+
<div className="flex min-w-[9.5rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
841853
<label className={classeRotulo} htmlFor={ID_CAMPO_QUANTIDADE}>
842854
Quantidade
843855
</label>
@@ -894,7 +906,7 @@ export function EntradaRapidaProduto({
894906
</div>
895907
</div>
896908

897-
<label className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
909+
<label className="flex min-w-[6rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
898910
<span className={classeRotulo}>Unidade</span>
899911
<input
900912
className={cn(classeCampoValor, semResolucao && 'text-muted-foreground')}
@@ -907,7 +919,7 @@ export function EntradaRapidaProduto({
907919
/>
908920
</label>
909921

910-
<label className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
922+
<label className="flex min-w-[9rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
911923
<span className={classeRotulo}>Preço unitário</span>
912924
<span className={classeMolduraValor}>
913925
<SimboloReal testId="previa-preco-unitario-simbolo" />
@@ -935,7 +947,7 @@ export function EntradaRapidaProduto({
935947
</span>
936948
</label>
937949

938-
<label className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
950+
<label className="flex min-w-[9rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
939951
<span className={classeRotulo}>Desconto do item</span>
940952
<span className={classeMolduraValor}>
941953
<SimboloReal testId="previa-desconto-item-simbolo" />
@@ -977,7 +989,7 @@ export function EntradaRapidaProduto({
977989
</span>
978990
</label>
979991

980-
<label className="flex min-w-0 flex-1 flex-col gap-xxs text-sm">
992+
<label className="flex min-w-[9rem] flex-1 flex-col gap-xxs text-sm md:min-w-0">
981993
<span className={classeRotulo}>Total item</span>
982994
<strong
983995
className={cn(
@@ -990,18 +1002,38 @@ export function EntradaRapidaProduto({
9901002
</strong>
9911003
</label>
9921004

1005+
{/* Largura cheia no compacto, os 70px do desenho desktop a partir de
1006+
`md:`. É o botão "Adicionar ao carrinho" do Pencil mobile (nó
1007+
`q2NBVJ`): pílula de largura total ao pé do cartão, ícone mais
1008+
rótulo. Um alvo de 70px perdido no fim de uma linha quebrada seria
1009+
o gesto mais difícil da etapa justamente para a ação que o caixa
1010+
repete a cada item.
1011+
1012+
**Um desvio declarado**: `q2NBVJ` é preto (`$surface-dark`) e este
1013+
botão continua na cor da marca. O cartão escuro de total já ocupa o
1014+
topo das três etapas, e um segundo preto — este, clicável — abriria
1015+
uma terceira cor de ação na mesma tela, ao lado do azul do "Ver
1016+
produtos e pagamento" logo abaixo.
1017+
1018+
O rótulo só aparece no compacto: no desktop a barra é uma linha só e
1019+
o `aria-label` já nomeia o botão para quem usa leitor de tela. Como
1020+
o `aria-label` vence o conteúdo no cálculo do nome acessível, o
1021+
texto visível não muda o nome anunciado em nenhum dos dois layouts. */}
9931022
<Button
9941023
ref={botaoConfirmar}
9951024
type="button"
996-
className="h-11.5 w-[70px] shrink-0 rounded-full"
1025+
className="h-11.5 w-full shrink-0 gap-xs rounded-full md:w-[70px]"
9971026
aria-label={
9981027
linhaEmEdicao === null ? 'Adicionar item à venda' : 'Confirmar edição do item'
9991028
}
10001029
data-testid="previa-confirmar"
10011030
{...atributosDeBloqueio(bloqueioDeInsercao)}
10021031
onClick={acaoBloqueavel(bloqueioDeInsercao, confirmar)}
10031032
>
1004-
<Plus className="size-5" aria-hidden="true" />
1033+
<Plus className="size-5 shrink-0" aria-hidden="true" />
1034+
<span className="text-md font-bold md:hidden">
1035+
{linhaEmEdicao === null ? 'Adicionar ao carrinho' : 'Confirmar edição'}
1036+
</span>
10051037
</Button>
10061038
</div>
10071039

0 commit comments

Comments
 (0)