Design Document: [Front] Dinâmica de itens recomendados
Overview
Esta task integra o bloco "Recomendados" na tela CartStatusDetailScreen do zzapp, exibindo os itens pendentes de cartItemRecommendations retornados pela API de detalhe do pedido. A seção é somente leitura: a vendedora acompanha quais recomendações ainda aguardam conversão pela cliente via zzlink, sem ações de editar, remover ou adicionar.
A abordagem estende o model CartDetail com deserialização de cartItemRecommendations, adiciona o widget privado _ContentRecommendations após "Resumo do pedido" e reutiliza CartStatusProductCardNew.recommended (task 196866) para renderizar cada card. O fluxo segue o padrão existente da tela: CartController.find → FutureBuilder → widgets de conteúdo — sem polling nem pull-to-refresh.
A conversão de recomendado em item do pedido é server-side (zzlink + API Cart). O front apenas reflete o estado atualizado ao reentrar na tela: itens convertidos saem de cartItemRecommendations e aparecem em productCartDetails com fromRecommendation: true (tag delegada ao card da task 196866).
Contexto
A US 196869 (MVP upsell na tela de Status do Pedido — PT. 2) exige exibir recomendações pendentes e refletir conversões via zzlink (CA-1 a CA-4). A task 196866 entrega o componente de card (CartStatusProductCardNew.recommended) e o model CartItemRecommendation; esta task (196865) integra a dinâmica do bloco na tela de Status.
Contrato de referência: cart-detail.json
Tela alvo: CartStatusDetailScreen em lib/screens/cart_status/cart_status_detail_screen.dart
Referência negativa: CheckSellRecommended — fluxo de criação de carrinho com banner de 1h e ações interativas (fora de escopo).
Requisitos EARS: task-196865-front-dinamica-de-itens-recomendados-requisitos.md
Decisões de Design
1. Widget privado _ContentRecommendations no mesmo arquivo da tela
Contexto: RF-02 exige estrutura visual igual à seção "Itens" (Column com título + lista, sem CartStatusDetailCard).
Opções consideradas:
| Opção | Prós | Contras |
|---|---|---|
A — Classe privada em cart_status_detail_screen.dart | Igual a _ContentProducts; coesão da tela | Arquivo cresce |
B — Arquivo separado em widgets/ | Separação física | Quebra padrão atual da tela |
Decisão: Opção A — criar _ContentRecommendations como StatelessWidget privado no mesmo arquivo.
Racional: _ContentProducts, _ContentSummary e demais seções já seguem esse padrão; a nova seção é específica desta tela.
2. Getter showRecommendationsSection em CartDetail
Contexto: RF-01 define três condições de visibilidade (saleEcommerce, lista vazia, pedido de loja).
Opções consideradas:
| Opção | Prós | Contras |
|---|---|---|
A — Getter em CartDetail | Centraliza regra; testável unitariamente | Um getter adicional |
B — Condição inline no build do widget | Menos alteração no model | Duplicação; difícil de testar |
Decisão: Opção A.
Racional: Espelha o padrão de getters existentes em CartDetail (canCopyLinkUrl, orderTicket) e permite cobertura de teste sem widget test.
3. Lista vazia como default na deserialização
Contexto: RF-03.2 exige tratar cartItemRecommendations ausente ou null como lista vazia.
Opções consideradas:
| Opção | Prós | Contras |
|---|---|---|
A — asListOrEmpty → List<CartItemRecommendation> | Consistente com productCartDetails; simplifica visibilidade | — |
B — List<CartItemRecommendation>? nullable | Distinção semântica null vs vazio | Lógica duplicada em RF-01 |
Decisão: Opção A — campo não-nullable com default [].
Racional: Alinha com pick(json, 'productCartDetails').asListOrEmpty(...) já usado em CartDetail.fromJson.
4. ZzListViewBuilder em vez de ListView.builder
Contexto: RF-04.1 exige listar itens na ordem da API; RF-02.4 exige padrão da seção "Itens".
Opções consideradas:
| Opção | Prós | Contras |
|---|---|---|
A — ZzListViewBuilder | Idêntico a _ContentProducts; shrinkWrap + NeverScrollableScrollPhysics embutidos | — |
B — ListView.builder (como CheckSellRecommended) | Já usado no fluxo de sell | Padrão diferente da Status |
Decisão: Opção A.
Racional: Consistência visual e de scroll com a seção "Itens" na mesma tela.
5. Sem polling — re-fetch ao reentrar na tela
Contexto: RF-05 (borda) e RF-06.1 definem atualização apenas ao reabrir a tela.
Opções consideradas:
| Opção | Prós | Contras |
|---|---|---|
A — Manter FutureBuilder + cartController.find AS-IS | Zero alteração no ciclo de vida; escopo mínimo | Sem update em tempo real |
| B — Polling ou pull-to-refresh | Update sem sair da tela | Fora de escopo; complexidade desnecessária |
Decisão: Opção A.
Racional: Requisitos explicitam ausência de polling nesta task; backend é fonte de verdade (RF-06.3).
6. Model CartItemRecommendation como dependência da task 196866
Contexto: RF-03.3 exige deserializar com o model definido no contrato; a task 196866 já cria CartItemRecommendation.
Decisão: Task 196865 não cria o model — apenas importa e referencia em CartDetail. Bloqueante de compilação até merge da 196866.
Racional: Evita duplicação de models e mantém tipagem única para factory recommended.
Architecture
Diagrama de Fluxo
Camadas e Responsabilidades
| Camada | Componente | Tipo | Responsabilidade |
|---|---|---|---|
| Screen | CartStatusDetailScreen | Modificado | Inserir _ContentRecommendations após _ContentSummary no ListView (RF-02.1, RF-07.1) |
| Widget | _ContentRecommendations | Novo (privado) | Título, subtítulo e lista read-only de recomendações (RF-02, RF-04) |
| Widget | CartStatusProductCardNew | Dependência 196866 | Factory recommended para renderizar cada item (RF-04.2) |
| Model | CartDetail | Modificado | Campo cartItemRecommendations, getter de visibilidade, fromJson/toJson (RF-01, RF-03) |
| Model | CartItemRecommendation | Dependência 196866 | Tipagem dos itens de cartItemRecommendations (RF-03.3) |
| Controller | CartController.find | Existente | Re-fetch ao reentrar na tela (RF-06.1) |
| API | Cart Detail endpoint | Existente | Fonte de verdade para composição dos blocos (RF-06.3) |
Components and Interfaces
Novos Arquivos
| Arquivo | Camada | Tipo |
|---|---|---|
test/models/cart/cart_detail_test.dart | Test | Deserialização de cartItemRecommendations e getter showRecommendationsSection (RF-03, RF-01) |
Arquivos Modificados
| Arquivo | Modificação |
|---|---|
lib/models/cart/cart_detail.dart | Adicionar cartItemRecommendations, getter showRecommendationsSection, mapeamento em fromJson/toJson (RF-01, RF-03) |
lib/screens/cart_status/cart_status_detail_screen.dart | Criar _ContentRecommendations; inserir após _ContentSummary no ListView.children (RF-02, RF-04) |
Interfaces
// RF-03 — lib/models/cart/cart_detail.dart
class CartDetail implements DefaultModelInterface {
// ... campos existentes ...
List<CartItemRecommendation> cartItemRecommendations;
// RF-01 — visibilidade centralizada
bool get showRecommendationsSection =>
!saleEcommerce && cartItemRecommendations.isNotEmpty;
factory CartDetail.fromJson(Map<String, dynamic> json) {
return CartDetail(
// ... campos existentes ...
cartItemRecommendations: pick(json, 'cartItemRecommendations')
.asListOrEmpty(
(pick) => CartItemRecommendation.fromJson(pick.asMapOrEmpty()),
), // RF-03.2: ausente/null → []
);
}
}
// RF-02, RF-04 — lib/screens/cart_status/cart_status_detail_screen.dart
class _ContentRecommendations extends StatelessWidget {
final CartDetail cartDetail;
const _ContentRecommendations(this.cartDetail);
Widget build(BuildContext context) {
if (!cartDetail.showRecommendationsSection) {
return const SizedBox.shrink(); // RF-01
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const ZZTextNew(
'Recomendados',
fontSize: ZZFontSize.xs,
fontWeight: ZZFontWeight.semibold,
), // RF-02.2
const Padding(
padding: EdgeInsets.only(top: 4, bottom: 8),
child: ZZTextNew(
'Produtos que você recomendou para a cliente.',
fontSize: ZZFontSize.xxs,
fontWeight: ZZFontWeight.medium,
color: ZZColors.neutralDark,
),
), // RF-02.3
ZzListViewBuilder(
itemCount: cartDetail.cartItemRecommendations.length,
itens: cartDetail.cartItemRecommendations,
padding: const EdgeInsets.only(bottom: 8),
itemBuilder: (_, index) {
final recommendation =
cartDetail.cartItemRecommendations[index];
return CartStatusProductCardNew.recommended(recommendation);
}, // RF-04.1, RF-04.2, RF-04.5 — sem onTap
),
],
);
}
}
// RF-02.1 — inserção no ListView da tela
children: [
_ContentStore(cartDetail),
_ContentCustomer(cartDetail),
_ContentDelivery(cartDetail, cartStatusDetailController),
_ContentOrder(cartDetail, storePaymentConfigController, ...),
_ContentPayment(cartLinkController),
_ContentProducts(cartDetail),
_ContentSummary(cartDetail, cartLinkController),
_ContentRecommendations(cartDetail), // NOVO
],
Data Models
Entidades envolvidas
// RF-03.3 — lib/models/cart/cart_item_recommendation.dart (criado na task 196866)
class CartItemRecommendation implements DefaultModelInterface {
int productId;
String name;
String image;
double price;
String sku;
String size;
int discount;
double discountValue;
double fullPrice;
bool hasEmployeeDiscount;
factory CartItemRecommendation.fromJson(Map<String, dynamic> json) { /* ... */ }
}
// RF-03 — lib/models/cart/cart_detail.dart (campo adicionado nesta task)
class CartDetail implements DefaultModelInterface {
List<ProductCartDetail> productCartDetails;
List<CartItemRecommendation> cartItemRecommendations; // NOVO
bool saleEcommerce;
// ...
}
// RF-05.3 — lib/models/product/product_cart_detail.dart (campo da task 196866)
class ProductCartDetail implements DefaultModelInterface {
bool fromRecommendation; // controla tag no card orderItem
// ...
}
Diagrama de Relacionamento
Regras de composição dos blocos (RF-05, RF-06)
| Estado do pedido | cartItemRecommendations | productCartDetails | UI |
|---|---|---|---|
| Recomendação pendente | contém o produto | não contém | Card em "Recomendados" |
| Após conversão via zzlink | não contém | contém com fromRecommendation: true | Card em "Itens" com tag |
PI (saleEcommerce: true) | ignorado na UI | exibido normalmente | Seção "Recomendados" oculta |
| Sem recomendações | [] | — | Seção oculta |
A garantia de não-duplicidade (RF-06.2) é responsabilidade do backend; o front confia na resposta da API (RF-06.3).
Error Handling
Tabela de Cenários
| Cenário | Comportamento | RF |
|---|---|---|
cartItemRecommendations ausente ou null no JSON | Default []; seção oculta | RF-03.2, RF-01.2 |
saleEcommerce: true com dados na API | Ignorar dados; SizedBox.shrink | RF-01.3 |
| Lista vazia após conversão de todos os itens | Seção oculta | RF-05.4 |
| Vendedora permanece na tela após conversão | Sem atualização automática | RF-05 (borda) |
Erro de rede no cartController.find | Comportamento AS-IS do FutureBuilder | RF-07.1 |
| Campos vazios no item recomendado | Card renderiza campos vazios; layout preservado | RF-04.4 |
| Imagem com URL inválida | ZzUrlImage exibe placeholder (comportamento existente) | — |
Fluxo de Renderização (Pseudocódigo)
// RF-01, RF-02, RF-04 — _ContentRecommendations.build
Widget build(BuildContext context) {
// RF-01.3: PI nunca exibe, mesmo com dados na API
// RF-01.2: lista vazia oculta seção inteira
if (!cartDetail.showRecommendationsSection) {
return const SizedBox.shrink();
}
// RF-02.4: sem CartStatusDetailCard; padrão _ContentProducts
// RF-02.5: sem banner de 1h (diferente de CheckSellRecommended)
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title('Recomendados'), // RF-02.2
subtitle('Produtos...'), // RF-02.3
ZzListViewBuilder(
itens: cartDetail.cartItemRecommendations,
itemBuilder: (_, i) => CartStatusProductCardNew.recommended(
cartDetail.cartItemRecommendations[i],
), // RF-04.2 — read-only, sem onTap/editar/remover
),
],
);
}
// RF-03 — CartDetail.fromJson
cartItemRecommendations: pick(json, 'cartItemRecommendations')
.asListOrEmpty((p) => CartItemRecommendation.fromJson(p.asMapOrEmpty())),
// RF-05, RF-06 — sem lógica front de movimentação entre blocos
// Ao reentrar: FutureBuilder reexecuta cartController.find → estado atualizado
Testing Strategy
Testes Unitários — CartDetail
| Cenário | Entrada | Resultado esperado | RF |
|---|---|---|---|
| JSON completo | cart-detail.json | cartItemRecommendations.length == 2 | RF-03.1 |
| Chave ausente | JSON sem cartItemRecommendations | cartItemRecommendations == [] | RF-03.2 |
| Chave null | "cartItemRecommendations": null | cartItemRecommendations == [] | RF-03.2 |
| Visibilidade — loja com itens | saleEcommerce: false, 2 itens | showRecommendationsSection == true | RF-01.1 |
| Visibilidade — loja vazia | saleEcommerce: false, [] | showRecommendationsSection == false | RF-01.2 |
| Visibilidade — PI | saleEcommerce: true, 2 itens | showRecommendationsSection == false | RF-01.3 |
| Roundtrip toJson/fromJson | Objeto com recommendations | Lista preservada | RF-03 |
Testes de Widget (opcional, baixa prioridade)
| Cenário | Verificação | RF |
|---|---|---|
showRecommendationsSection == false | _ContentRecommendations retorna SizedBox.shrink | RF-01 |
showRecommendationsSection == true | Título "Recomendados" e subtítulo visíveis | RF-02.2, RF-02.3 |
| Lista com N itens | N instâncias de CartStatusProductCardNew | RF-04.1 |
| Card recomendado | Sem GestureDetector com callback | RF-04.5 |
Dependências
| Dependência | Status | Nota |
|---|---|---|
Task 196866 (CartStatusProductCardNew.recommended, CartItemRecommendation, fromRecommendation) | Pendente | Bloqueante para compilação |
API Cart Detail (cartItemRecommendations, fromRecommendation) | Disponível | Ver cart-detail.json |
| Figma ZZAPP 2.0 | Disponível | Posicionamento, subtítulo e cards |
| US 196869 | Disponível | CA-1 a CA-4 |
ZzListViewBuilder, ZZTextNew, ZZColors | Existente | Design system atual |
CheckSellRecommended | Existente (referência negativa) | Não replicar banner nem ações |
Rastreabilidade Requisitos → Design
| RF | Elemento do Design |
|---|---|
| RF-01 | Getter showRecommendationsSection, early return em _ContentRecommendations |
| RF-02 | _ContentRecommendations: posição no ListView, título, subtítulo, sem CartStatusDetailCard |
| RF-03 | Campo cartItemRecommendations em CartDetail.fromJson com asListOrEmpty |
| RF-04 | ZzListViewBuilder + CartStatusProductCardNew.recommended (read-only) |
| RF-05 | Reflexão via re-fetch; tag em orderItem delegada à 196866; sem polling |
| RF-06 | CartController.find AS-IS; confiança na API; sem lógica de deduplicação no front |
| RF-07 | Blocos existentes inalterados; _ContentSummary AS-IS para PI; refresh de listagem preservado |
Checklist de Qualidade
- Todos os RFs cobertos (RF-01 a RF-07)
- Tratamento de cenários de borda documentado (null, PI, conversão, permanência na tela)
- Segue padrão existente do projeto (
_ContentProducts,asListOrEmpty, widgets privados na tela) - Sem polling / pull-to-refresh (escopo respeitado)
- Cards somente leitura (sem onTap, editar ou remover)
- Dependência da task 196866 explicitada
- Seção "Recomendados" não impacta totalizadores do "Resumo do pedido"