Skip to main content

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.findFutureBuilder → 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çãoPrósContras
A — Classe privada em cart_status_detail_screen.dartIgual a _ContentProducts; coesão da telaArquivo cresce
B — Arquivo separado em widgets/Separação físicaQuebra 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çãoPrósContras
A — Getter em CartDetailCentraliza regra; testável unitariamenteUm getter adicional
B — Condição inline no build do widgetMenos alteração no modelDuplicaçã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çãoPrósContras
A — asListOrEmptyList<CartItemRecommendation>Consistente com productCartDetails; simplifica visibilidade
B — List<CartItemRecommendation>? nullableDistinção semântica null vs vazioLó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çãoPrósContras
A — ZzListViewBuilderIdêntico a _ContentProducts; shrinkWrap + NeverScrollableScrollPhysics embutidos
B — ListView.builder (como CheckSellRecommended)Já usado no fluxo de sellPadrã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çãoPrósContras
A — Manter FutureBuilder + cartController.find AS-ISZero alteração no ciclo de vida; escopo mínimoSem update em tempo real
B — Polling ou pull-to-refreshUpdate sem sair da telaFora 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

CamadaComponenteTipoResponsabilidade
ScreenCartStatusDetailScreenModificadoInserir _ContentRecommendations após _ContentSummary no ListView (RF-02.1, RF-07.1)
Widget_ContentRecommendationsNovo (privado)Título, subtítulo e lista read-only de recomendações (RF-02, RF-04)
WidgetCartStatusProductCardNewDependência 196866Factory recommended para renderizar cada item (RF-04.2)
ModelCartDetailModificadoCampo cartItemRecommendations, getter de visibilidade, fromJson/toJson (RF-01, RF-03)
ModelCartItemRecommendationDependência 196866Tipagem dos itens de cartItemRecommendations (RF-03.3)
ControllerCartController.findExistenteRe-fetch ao reentrar na tela (RF-06.1)
APICart Detail endpointExistenteFonte de verdade para composição dos blocos (RF-06.3)

Components and Interfaces

Novos Arquivos

ArquivoCamadaTipo
test/models/cart/cart_detail_test.dartTestDeserialização de cartItemRecommendations e getter showRecommendationsSection (RF-03, RF-01)

Arquivos Modificados

ArquivoModificação
lib/models/cart/cart_detail.dartAdicionar cartItemRecommendations, getter showRecommendationsSection, mapeamento em fromJson/toJson (RF-01, RF-03)
lib/screens/cart_status/cart_status_detail_screen.dartCriar _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 pedidocartItemRecommendationsproductCartDetailsUI
Recomendação pendentecontém o produtonão contémCard em "Recomendados"
Após conversão via zzlinknão contémcontém com fromRecommendation: trueCard em "Itens" com tag
PI (saleEcommerce: true)ignorado na UIexibido normalmenteSeçã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árioComportamentoRF
cartItemRecommendations ausente ou null no JSONDefault []; seção ocultaRF-03.2, RF-01.2
saleEcommerce: true com dados na APIIgnorar dados; SizedBox.shrinkRF-01.3
Lista vazia após conversão de todos os itensSeção ocultaRF-05.4
Vendedora permanece na tela após conversãoSem atualização automáticaRF-05 (borda)
Erro de rede no cartController.findComportamento AS-IS do FutureBuilderRF-07.1
Campos vazios no item recomendadoCard renderiza campos vazios; layout preservadoRF-04.4
Imagem com URL inválidaZzUrlImage 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árioEntradaResultado esperadoRF
JSON completocart-detail.jsoncartItemRecommendations.length == 2RF-03.1
Chave ausenteJSON sem cartItemRecommendationscartItemRecommendations == []RF-03.2
Chave null"cartItemRecommendations": nullcartItemRecommendations == []RF-03.2
Visibilidade — loja com itenssaleEcommerce: false, 2 itensshowRecommendationsSection == trueRF-01.1
Visibilidade — loja vaziasaleEcommerce: false, []showRecommendationsSection == falseRF-01.2
Visibilidade — PIsaleEcommerce: true, 2 itensshowRecommendationsSection == falseRF-01.3
Roundtrip toJson/fromJsonObjeto com recommendationsLista preservadaRF-03

Testes de Widget (opcional, baixa prioridade)

CenárioVerificaçãoRF
showRecommendationsSection == false_ContentRecommendations retorna SizedBox.shrinkRF-01
showRecommendationsSection == trueTítulo "Recomendados" e subtítulo visíveisRF-02.2, RF-02.3
Lista com N itensN instâncias de CartStatusProductCardNewRF-04.1
Card recomendadoSem GestureDetector com callbackRF-04.5

Dependências

DependênciaStatusNota
Task 196866 (CartStatusProductCardNew.recommended, CartItemRecommendation, fromRecommendation)PendenteBloqueante para compilação
API Cart Detail (cartItemRecommendations, fromRecommendation)DisponívelVer cart-detail.json
Figma ZZAPP 2.0DisponívelPosicionamento, subtítulo e cards
US 196869DisponívelCA-1 a CA-4
ZzListViewBuilder, ZZTextNew, ZZColorsExistenteDesign system atual
CheckSellRecommendedExistente (referência negativa)Não replicar banner nem ações

Rastreabilidade Requisitos → Design

RFElemento do Design
RF-01Getter showRecommendationsSection, early return em _ContentRecommendations
RF-02_ContentRecommendations: posição no ListView, título, subtítulo, sem CartStatusDetailCard
RF-03Campo cartItemRecommendations em CartDetail.fromJson com asListOrEmpty
RF-04ZzListViewBuilder + CartStatusProductCardNew.recommended (read-only)
RF-05Reflexão via re-fetch; tag em orderItem delegada à 196866; sem polling
RF-06CartController.find AS-IS; confiança na API; sem lógica de deduplicação no front
RF-07Blocos 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"