Skip to main content

Design Document — US 197021 (PR2: coezzion_vendas_app)

Requisitos: 197021-requisitos.md
Pesquisa: design-research-zzapp.md
Design API (contrato): 197021-design-showcase-api.md
RFs cobertos: RF-01, RF-02, RF-03, RF-04, RF-05, RF-08, RF-11


Overview

O PR2 adapta o zzapp para que o vendedor possa criar e editar vitrines Minhas e Histórico com prazo padrão (+31 dias) ou sem data de expiração (clear no picker → dateExpiration: null no payload). A API (PR1) persiste never-expire; o app apenas exibe "Sem data de expiração" quando a resposta traz null.

Fluxo Flutter: Screen → Controller (GetX) → Model → ApiUtils/Provider. Mudanças concentram-se em ZZDateFormField, models nullable, remoção de fallbacks +31 no update e labels na listagem Minhas.


Contexto

Recommend list e Brand list já usam nullExpirationLabel. Minhas quebra parse com asDateTimeOrThrow e esconde campo no detail quando null. Recommend create/edit bloqueia submit sem data via toast.


Decisões de Design

IDDecisãoAlternativa rejeitadaRacionalRF
D-APP-1Estender ZZDateFormField com allowClear + onClear + label parametrizável quando nullWidget só em show_caseReuso; copy não hardcoded no widget (D-APP-9)RF-01, RF-03
D-APP-2Default maxDaysFromNow no controller na abertura (D-APP-14)Esperar default da API no createD4: default é UX, não serverRF-02
D-APP-3Chave dateExpiration sempre no JSON; valor null quando sem prazo (D-APP-10)Omitir chaveContrato explícito com API PR1RF-02, RF-03
D-APP-4Remover fallbacks ?? now+31 no update MinhasManter fallback clientConflita com clear→neverRF-03
D-APP-5nullExpirationLabel na lista MinhasLinha vazia sem expiraçãoCA-4 / padrão RecommendRF-04
D-APP-6Detail Minhas sempre mostra pickerOcultar se nullUsuário precisa adicionar prazo depoisRF-03, RF-04
D-APP-7Recommend edit: min amanhã + max +31 (D-APP-12)Manter min hoje no editParidade create/edit e APIRF-08
D-APP-8Minhas edit: _initialExpirationDate + hasExpirationDateChanged (padrão Histórico)Corrigir só enableEditButtonAS-IS não detecta clear→null; Histórico já tem _isSameDateRF-03, RF-04
D-APP-9Clear UX: texto legível parametrizável no campo + ícone XCampo vazio com hint genéricoParidade portal; caller passa ex. 'Sem data de expiração'RF-04
D-APP-10JSON: chave dateExpiration sempre presente; valor null literalOmitir chaveContrato explícito PR1RF-02, RF-03
D-APP-11Fallback +31 removido em detalhe e busca de produtos MinhasSó detalheDois PUT paths no AS-ISRF-03
D-APP-12Histórico edit: firstExpirationDate = amanhã (como create)Manter min hojeAssimetria AS-IS; API exige futuraRF-08
D-APP-13Deploy simultâneo API + appAPI primeiro com app antigoApp novo + API antiga quebra clearRF-11
D-APP-14ShowCaseExpirationDefaults.maxDaysFromNow = 31 centralizadoMagic numbers inlineAlinha API; facilita PO 30↔31RF-02, RF-08
D-APP-15Testes: models obrigatórios; controllers se baixo acoplamentoWidget tests / controllers forçadosAutoclosure GetX pode inviabilizar unit de controllerRF-11
D-APP-16loadData espelha dateExpiration da API em expirationDate + _initialExpirationDateFallback no PUT ?? detailEvita PUT de nome-only virar never-expireRF-03

Architecture

Diagrama de Fluxo

Camadas e Responsabilidades

CamadaComponenteTipoResponsabilidadeRF
Widgetzz_date_form_field.dartModificadoClearableRF-01
Minhas create/edit buscashow_case_search_products_controller.dartModificadoDefault + clear; PUT sem ?? +31; setEditMode(DateTime?)RF-02, RF-03, D-APP-11
Minhas createcheck_fast_catalog_product_list.dartModificadoallowClearRF-02
Minhas editshow_case_detail_screen.dartModificadoCampo sempre visívelRF-03, RF-04
Minhas editshow_case_detail_controller.dartModificadoD-APP-16 sync loadData; hasExpirationDateChanged; sem fallback +31RF-03
Minhas listmy_show_case_list_screen.dartModificadonullExpirationLabelRF-04
Recommend createrecommend_show_case_check_products_*ModificadoSem toast obrigatórioRF-01, RF-02
Recommend editrecommend_show_case_detail_*ModificadoClear + max pickerRF-03, RF-08
Modelsupdate_show_case_request.dartModificadoDateTime?RF-03
Modelsshow_case_response_item.dartModificadoParse nullableRF-04
Testsmodels show_case + recommendModificadoSerialização nullRF-11

Components and Interfaces

Arquivos modificados (inventário)

Detalhe em design-research-zzapp.md.

Interface ZZDateFormField (proposta)

class ZZDateFormField extends StatefulWidget {
final DateTime? selectedDate;
final bool allowClear; // default: false — RF-01
final VoidCallback? onClear; // RF-03
final String? emptyDateLabel; // D-APP-9: texto no campo quando selectedDate == null
final void Function(DateTime) onChanged;
// hint permanece para fluxos sem emptyDateLabel (ex.: férias, CRM)
}

Comportamento (D-APP-9):

  • Com data: exibe dd/MM/yyyy + ícone X no suffix (se allowClear)
  • Após clear (selectedDate == null): exibe emptyDateLabel do caller (ex. vitrines: 'Sem data de expiração'), não o hint dd/mm/aaaa
  • Sem emptyDateLabel: comportamento legado (campo vazio + hint)
  • Vitrines Minhas + Histórico: allowClear: true, emptyDateLabel: ShowCaseExpirationDefaults.noExpirationLabel
  • Create: default +31 pré-preenchido; clear opcional antes de salvar

Data Models

ShowCaseExpirationDefaults (D-APP-14)

abstract final class ShowCaseExpirationDefaults {
static const int maxDaysFromNow = 31; // D3: alinhar com PO
static const String noExpirationLabel = 'Sem data de expiração';
}

Usar em: default create, initialDateRange/finalDateRange, nullExpirationLabel na lista Minhas, emptyDateLabel do picker.

UpdateShowCaseRequest (RF-03)

class UpdateShowCaseRequest {
final DateTime? dateExpiration; // era DateTime required

Map<String, dynamic> toJson() => {
// ...
// D-APP-10: chave sempre presente; null literal no Map → "dateExpiration":null no JSON
'dateExpiration': dateExpiration?.toIso8601String(),
};
}

ShowCaseResponseItem (RF-04)

class ShowCaseResponseItem {
final DateTime? expirationDate; // era required

factory ShowCaseResponseItem.fromJson(Map<String, dynamic> json) {
return ShowCaseResponseItem(
expirationDate: picker('dateExpiration').asDateTimeOrNull(),
);
}
}

Submit Minhas update (RF-03 — sem fallback, D-APP-8)

// D-APP-16: loadData — após detail.value = result:
expirationDate.value = result.dateExpiration;
_initialExpirationDate = result.dateExpiration;

bool get hasExpirationDateChanged =>
!_isSameDate(expirationDate.value, _initialExpirationDate);

bool get enableEditButton =>
hasNameChanged.value ||
hasProductsChanged.value ||
hasExpirationDateChanged; // inclui clear → null

// PUT — sem fallback:
dateExpiration: expirationDate.value,

Recommend controllers (RF-01)

// ANTES:
if (expirationDate == null) {
ZzToast.showError('Selecione a data de expiração');
return;
}

// DEPOIS: permitir null; enviar no request

Error Handling

CenárioComportamentoRF
API retorna 400 cap excedidoToast/erro via fluxo DefaultApiController existenteRF-08
Parse list com nullasDateTimeOrNull + labelRF-04
Usuário limpa e salvaEnvia null; sem toast local bloqueandoRF-02
Share/copy never-expireFluxo inalteradoRF-05

Testing Strategy

Testes unitários — models (obrigatório, D-APP-15)

Arquivo de testeCenário novoRF
show_case_response_item_test (criar se ausente)fromJson com dateExpiration: nullRF-04
update_show_case_request_test (criar se ausente)toJson"dateExpiration":nullRF-03, D-APP-10
create_recommend_show_case_request_testtoJson com nullRF-02
update_recommend_show_case_request_testtoJson com nullRF-03

Testes unitários — controllers (desejável, D-APP-15)

Extrair e testar funções puras quando possível (ex.: _isSameDate, normalização de payload) sem montar GetX completo.

CenárioControllerSe acoplamento alto
hasExpirationDateChanged detecta clear→nullshow_case_detail_controllerCobrir no smoke manual
PUT sem ?? +31show_case_search_products_controllerCobrir no smoke manual
Submit Recommend com nullrecommend_show_case_*Cobrir no smoke manual

Regra: se o teste exigir mock pesado de GetxController + services, não criar — documentar no PR e reforçar checklist manual abaixo.

Smoke manual

CenárioRF
Create Minhas sem tocar data → lista com data ~+31RF-02
Create Minhas clear → "Sem data de expiração"RF-04
Edit remove/adiciona data Minhas e RecommendRF-03
Share link never-expireRF-05

Deploy (D-APP-13)

Release simultâneo API + app no mesmo train. PR1 mergeado e disponível em produção antes do PR2. Sem feature flag nesta US. QA só com par API+app alinhados (RF-11).


Dependências

DependênciaStatusNota
PR1 showcase APIBloqueante recomendadoContrato null=never
Copy "Sem data de expiração"ConfirmadoPortal + Recommend list
ZZDateFormFieldDisponívelEstender, não substituir
Brand flowsFora escopoSem alteração

Checklist de Qualidade

  • RF-01 a RF-04, RF-08, RF-11 cobertos no app
  • Nenhum fallback client ?? now+31 no update após clear
  • Lista Minhas não quebra com null da API
  • Recommend create/edit aceita null
  • Picker max +31 alinhado em create e edit Recommend
  • Brand e share inalterados (RF-05)