Design Document: [Front][App] Ajuste de layout nova vitrine
Overview
Adaptar o fluxo Minhas Vitrines no App Flutter (coezzion_vendas_app) para suportar a modalidade Vitrine de Estoque da Loja (showCaseType = "SellerStock"), coexistindo com a vitrine personalizada atual (showCaseType = "Store").
A task cobre 4 frentes de mudança:
- Migração para endpoint v2 da listagem com flags
hasSellerStockehasNext - Bottom sheet de escolha de modalidade ao criar vitrine
- Criação da Vitrine de Estoque da Loja sem tela intermediária — a tela de detalhe assume um modo de criação (POST + GET encadeados)
- Detalhe condicional ao
showCaseType(layout, regras de edição, contagem de produtos, rewards)
Além disso, uma 5ª frente foi incorporada na revisão de 03/07/2026: aviso de alterações não salvas ao sair, visualizar, compartilhar ou copiar link (RF-06).
Nota de revisão (03/07/2026): este documento foi revisado numa sessão de grilling que cruzou cada decisão com o código atual do app. Várias decisões do documento original foram corrigidas ou substituídas — os achados detalhados (com referências de arquivo/linha) estão em
context/*.md. Este documento reflete o estado final acordado.
Contexto
AS-IS (Validado em 25/06/2026; atualizado em 03/07/2026)
Nota de revisão (03/07/2026): Este documento foi revisado em uma sessão de grilling que cruzou cada decisão com o código atual do app e identificou várias lacunas, comportamentos não-mapeados e possíveis bugs. Os achados detalhados (com referências de arquivo/linha e roadmap de correção) estão em
context/*.md. Este documento reflete o estado final acordado após revisão.
| Aspecto | Estado atual |
|---|---|
| Botão CRIAR VITRINE | Navega direto para ShowCaseSearchProducts (fluxo de seleção de produtos). Sem bottom sheet de escolha de modalidade. |
| Endpoint listagem | GET /api/show-case-new/store/{storeId} (v1) retorna items: [] + pagination. Sem hasSellerStock. |
| DTOs Create/Update | CreateShowCaseRequest e UpdateShowCaseRequest não incluem showCaseType. |
| Detalhe | ShowCaseDetailResponse não inclui showCaseType nem totalItems. Contagem sempre via showCaseItems.length. |
| Componente detalhe | ShowCaseDetailLayout renderiza nome editável, grid com até 4 itens, botão ADICIONAR PRODUTOS, ícone de remoção por card. Sem branching por tipo. Heading da seção ("Confira os produtos adicionados") é hardcoded, não parametrizado. |
| Enum ShowCaseType | Não existe; valores hardcoded em testes/mocks (magic numbers 1, 4). |
Lacunas (TO-BE)
- Bottom sheet "Escolha uma opção" ao clicar em CRIAR VITRINE (RF-02)
- Parsing de
hasSellerStock/hasNextdo endpoint v2; bloqueio de criação SellerStock se já existe (RF-01) - Criação da Vitrine de Estoque da Loja sem tela intermediária: tela de detalhe ganha modo de criação (POST + GET encadeados) (RF-03)
- Detalhe adaptado: nome fixo "Estoque da loja", contagem via
totalItems, sem edição/link "Todos os produtos", PUT só efetivadateExpiration, sem reward events nativos (RF-04) - Suporte aos novos campos em DTOs:
showCaseTypeobrigatório em requests,totalItems/showCaseTypeem response de detalhe,hasNext/hasSellerStockna listagem v2 (RF-05) - Aviso de alterações não salvas ao sair, visualizar, compartilhar ou copiar link — hoje só existe (parcialmente) para "sair"; falta para as outras 3 ações (RF-06)
Decisões de Design
1. Bottom Sheet de Escolha de Modalidade
Decisão: Novo widget ShowCaseTypeSelectionBottomSheet invocado via GetxBottomSheet.showBottomSheet(), seguindo padrão já estabelecido em ShowCaseBottomSheet.
Justificativa:
- Reutiliza infraestrutura existente (
GetxBottomSheet) → menos código, comportamento consistente. - Widget independente → testável e reutilizável.
- Centraliza lógica de visibilidade/disable da opção SellerStock (condicionada a
hasSellerStock).
Implementação:
createFastCatalog()noMyShowCaseControllerpassa a chamarshowBottomSheetem vez de navegar direto.- Bottom sheet recebe
hasSellerStockcomo prop (valor síncrono no momento da abertura — não precisa ser reativo, já que é uma UI efêmera). - Opção "Vitrine do estoque da loja" renderizada com disable visual (opacidade, sem tap) se
hasSellerStock=true. - Mensagem "Você já possui uma Vitrine de Estoque ativa..." exibida abaixo do título quando desabilitada.
- Opção "Vitrine do estoque da loja" habilitada (
hasSellerStock=false) chamaMyShowCaseController.createSellerStock()(ver Decisão #3) — não navega para nenhuma tela de formulário. - Badge "Nova" da opção usa
ZzTag(theme: ZzTagTheme.newFeature, text: 'Nova')— novo valor de enum emZzTagTheme(theme_widgets/tag/tag.dart), já que os temas existentes (primary,red,yellow,success) não têm a combinação de cores pedida pelo Figma (fundo sólido, não o tom pastel usado hoje pelo badge "Nova" de vitrines de marca).newFeature:backgroundColor: ZZColors.successMedium,foregroundColor: ZZColors.neutralLightest. (NomenewFeatureem vez denew—newé palavra reservada em Dart, não pode ser identificador.)
// lib/theme_widgets/tag/tag.dart (DIFF)
enum ZzTagTheme { primary, red, yellow, success, newFeature }
extension ZzTagColorExtension on ZzTagTheme {
Color get backgroundColor {
switch (this) {
// ... casos existentes ...
case ZzTagTheme.newFeature:
return ZZColors.successMedium;
}
}
Color get foregroundColor {
switch (this) {
// ... casos existentes ...
case ZzTagTheme.newFeature:
return ZZColors.neutralLightest;
}
}
}
2. Endpoint v2 e Flags hasSellerStock / hasNext
Decisão: Nova URL getStoreShowCaseV2Url(...) em ApiUtils. Parse manual do response v2 em MyShowCaseController._load() em vez de delegar para DefaultApiController.findPagedList(). ApiUtils.getStoreShowCaseUrl (v1) é removido — fica sem nenhum outro chamador no app após a migração.
Justificativa:
- v2 muda estrutura de paginação:
items→data, sem objetopaginationaninhado (estrutura plana). findPagedList()esperaresponse['pagination'](aninhado) — incompatível com o formato plano da v2. Parse manual é necessário, não apenas preferência de estilo.- Parse manual evita quebrar outros fluxos que ainda usam
DefaultApiControllercom v1. hasSellerStockarmazenado comoRxBoolno controller → observável, dispara rebuild do bottom sheet.hasNexté exposto diretamente pelo backend na v2 (confirmado) — o App não precisa calcular a partir detotal/pageNumber/pageSize.
Implementação:
ApiUtils.getStoreShowCaseV2Url(storeId, pageNumber, search, pageSize=20)retorna URL com caminho/v2/.MyShowCaseController._load()chama URL v2, faz parse manual deShowCaseListV2Response, extraidata,hasNextehasSellerStock.hasSellerStockatualizado em cada página carregada (útil se vendedor cria vitrine em outra aba e volta).ApiUtils.getStoreShowCaseUrl(v1) removido doApiUtils.
3. Criação da Vitrine de Estoque da Loja — Sem Tela Intermediária
Decisão: Não existe tela/controller/rota nova de criação. Ao tocar em "Vitrine do estoque da loja" no bottom sheet (com hasSellerStock == false), o app navega diretamente para ShowCaseDetailScreen (rota já existente, AppRoutes.storeShowCaseDetail), em um novo modo de criação. A própria tela/controller de detalhe assume a responsabilidade de criar a vitrine (POST) e, em seguida, carregar seus dados completos (GET) — usando a infraestrutura de loading/erro que já existe no app.
Justificativa:
- O único dado variável na criação é a data de expiração — e ela já é editável na tela de detalhe existente (
ATUALIZAR VITRINE, RF-04 AC10/11). Não há necessidade de uma tela extra só para confirmar isso antes de criar; o vendedor pode ajustar a data depois de ver a vitrine criada. - Elimina uma tela, um controller e uma rota inteiros (
ShowCaseSellerStockScreen,ShowCaseSellerStockController,AppRoutes.showCaseSellerStock), reduzindo esforço de implementação e manutenção. - Reaproveita 100% a UI de loading e erro já usada no app (skeleton de
ShowCaseDetailLayout; estado de erro no padrão doUpsellRecommendationScreen). POST /api/show-case-newretorna apenas{id, url}(CreateShowCaseResponse) — nunca o shape completo deShowCaseDetailResponse. Um GET subsequente é inevitável para preencher a tela; encadear os dois dentro do controller de detalhe é o caminho mais direto.- Contrato atualizado: a API não retorna mais erro quando já existe uma vitrine SellerStock ativa para
(userId, storeId)— o POST retorna200 OKcom os dados da vitrine existente ("get-or-create"). Do ponto de vista do App, não há distinção entre "criada agora" e "já existia"; o único caminho de erro tratado é falha inesperada (rede, 5xx). - Nenhum reward event é registrado nesse fluxo (nem na criação, nem nas ações de visualizar/compartilhar/copiar do detalhe) enquanto a task 197192 (eventos específicos de Rewards para SellerStock) estiver pendente — evita rotular incorretamente ações de SellerStock com eventos "Store".
Implementação:
ShowCaseDetailScreenArguments ganha dois campos novos (catalogId passa a ser opcional; isCreating novo, default false — não quebra o único call site existente, goToDetailFastCatalog()):
class ShowCaseDetailScreenArguments {
final String catalogName;
final String? catalogId;
final bool isCreating;
ShowCaseDetailScreenArguments({
required this.catalogName,
this.catalogId,
this.isCreating = false,
});
}
MyShowCaseController ganha um novo método, chamado pelo callback do bottom sheet:
Future<void> createSellerStock() async {
final args = ShowCaseDetailScreenArguments(
catalogName: 'Estoque da loja',
isCreating: true,
);
await Get.toNamed(AppRoutes.storeShowCaseDetail, arguments: args);
// mesmo padrão de goToDetailFastCatalog(): recarrega a listagem ao voltar
// (mostra a vitrine recém-criada e atualiza hasSellerStock)
_hasNextPage = true;
pagingController.value = PagingState();
}
Nenhum reward event de criação é registrado (MyShowCaseController não precisa ganhar RewardEventController como dependência nova por causa disso).
ShowCaseDetailController ramifica por arguments.isCreating no onInit():
final isLoading = false.obs;
final hasError = false.obs;
void onInit() {
arguments = Get.arguments as ShowCaseDetailScreenArguments;
nameController.text = arguments.catalogName;
nameController.addListener(() {
hasNameChanged.value = nameController.text != detail.value?.name;
update();
});
expirationDate.listen((nd) => update());
if (arguments.isCreating) {
createAndLoad();
} else {
loadData(arguments);
}
super.onInit();
}
Future<void> createAndLoad() async {
isLoading.value = true;
hasError.value = false;
update();
try {
final request = await _buildSellerStockCreateRequest();
// name: 'Estoque da loja', showCaseType: ShowCaseType.sellerStock,
// showCaseItems: [], dateExpiration: hoje + 30 dias (limite desta modalidade), + demais campos
// obrigatórios (schema, storeId, storeName, maxInstallments, userId,
// userName, userPhone, codeBrand) — mesmo padrão de postFastCatalog().
final response = await post(
request.toJson(),
customUrl: await ApiUtils.postShowCaseUrl(),
showLoading: false, // loading próprio via isLoading/skeleton
);
final created = CreateShowCaseResponse.fromJson(response);
// POST só retorna {id, url} — encadeia com o GET normal para
// popular a tela por completo (showCaseType, totalItems, preview, etc.)
arguments = ShowCaseDetailScreenArguments(
catalogName: arguments.catalogName,
catalogId: created.id,
);
await loadData(arguments);
} catch (e) {
hasError.value = true;
} finally {
isLoading.value = false;
update();
}
}
/// Chamado pelo botão "TENTE NOVAMENTE" do estado de erro.
void retryCreate() => createAndLoad();
ShowCaseDetailScreen passa a checar controller.hasError.value (antes de isLoading), renderizando um ZzErrorState — mesma dinâmica do UpsellRecommendationScreen (title, description genéricos, retryText: 'TENTE NOVAMENTE', onRetry: controller.retryCreate) — no lugar do ShowCaseDetailLayout enquanto hasError == true. Mensagem sempre genérica, sem diferenciar tipo de erro (a "duplicata" deixou de ser um erro, ver acima).
4. Detalhe Condicional por showCaseType
Decisão: Branching no ShowCaseDetailController (lógica) e ShowCaseDetailScreen (renderização). Parametrização do ShowCaseDetailLayout com props opcionais.
Justificativa:
- Evita duplicação de tela (uma única
ShowCaseDetailScreen, não duas variantes). ShowCaseDetailLayoutjá é compartilhado por todas as variantes de detalhe (brand, recommend, store) → estender é natural.- Controller lê
showCaseTypee aplica regras: PUT sódateExpirationpara type=4, contagem viatotalItems, etc.
Implementação:
ShowCaseDetailController.loadData()lêdetail.value!.showCaseTypeapós GET.showCaseType == ShowCaseType.store(Store): comportamento atual, sem mudança (exceto a correção deenableEditButton, ver abaixo, que se aplica aos dois tipos).showCaseType == ShowCaseType.sellerStock(SellerStock):namemarcado como read-only (não editável via form).totalItemsusado para contagem em vez deshowCaseItems.length.- Botão
ADICIONAR PRODUTOSocultado. - Ícones de remoção de produto ocultados.
- Sem link "Todos os produtos", mesmo quando
totalItems > 4— não existe endpoint para carregar a lista completa por vitrine (só o preview de 4 itens vem no GET de detalhe); a grid sempre mostra no máximo os 4 itens do preview. updateShowCase()envia PUT comshowCaseType: ShowCaseType.sellerStock, forçandoshowCaseItems: []explicitamente (mesmo a API ignorando esse campo, deixa claro no código que os productIds do preview não representam a vitrine real).- Nenhum reward event é registrado em
viewShowCase()/shareShowCase()/copyLink()quandoshowCaseType == ShowCaseType.sellerStock(chamadas arewardEventController.register(...)condicionadas ashowCaseType == ShowCaseType.store).
ShowCaseDetailScreenpassa props aoShowCaseDetailLayout:hideProductActions = (showCaseType == ShowCaseType.sellerStock)→ remove botão ADICIONAR e ícones de remove.sectionTitle = (showCaseType == ShowCaseType.sellerStock) ? "Confira os produtos" : "Confira os produtos adicionados"(novo parâmetro — o heading é hoje hardcoded no layout, não parametrizado).productCountText = (showCaseType == ShowCaseType.sellerStock) ? "${detail.totalItems} produtos recomendados" : "${items.length} produtos adicionados"(parâmetro já existente, só muda o cálculo).productCount = items.length > 4 ? 4 : items.length— sempre, independente doshowCaseType. Nunca usardetail.totalItemsaqui:productCountalimenta oitemCountdoGridView.builder, cujoitemBuilderindexa a lista localitems(no máximo 4 elementos para SellerStock); usartotalItemsdiretamente causariaRangeErrorsempre quetotalItems > 4.onViewAllProducts = nullquandoshowCaseType == ShowCaseType.sellerStock(sem link "Todos os produtos" para esta modalidade).appBarTitle = (showCaseType == ShowCaseType.sellerStock) ? "Estoque da loja" : catalogName. Como o backend garantename = "Estoque da loja"já na listagem v2 para vitrines SellerStock,arguments.catalogNamejá nasce correto antes do GET de detalhe resolver — sem risco de "flash" de texto incorreto.
ShowCaseDetailLayoutrecebe novos params opcionais:hideProductActions(bool, defaultfalse),sectionTitle(String, default'Confira os produtos adicionados', substituindo oZZTextNewhardcoded) — sem quebrar assinaturas existentes (productCountText/productCountjá eram obrigatórios, não são novos).
Limite de data de expiração condicionado ao tipo: o campo ZZDateFormField em _HeaderContent (show_case_detail_screen.dart) hoje tem finalDateRange fixo em DateTime.now().add(const Duration(days: 31)) para todos os tipos. SellerStock tem um limite de negócio diferente — 30 dias, não 31 — conforme Figma (context/image-3.png) e US 196053 (CA-3). Passa a ser condicional:
finalDateRange: (detail.value?.showCaseType == ShowCaseType.sellerStock)
? DateTime.now().add(const Duration(days: 30))
: DateTime.now().add(const Duration(days: 31)),
O mesmo limite de 30 dias vale para o dateExpiration inicial enviado no POST de criação (Decisão #3).
Correção de enableEditButton (aplicada a Store e SellerStock):
Comportamento atual: expirationDate.value != null habilita o botão só por o vendedor ter tocado no campo de data, mesmo escolhendo a mesma data já salva — não compara com o valor original. Corrigido para os dois tipos:
bool get enableEditButton {
final currentDetail = detail.value;
if (currentDetail == null) return false;
final dateChanged = expirationDate.value != null &&
expirationDate.value != currentDetail.dateExpiration;
final baseCondition = hasNameChanged.value || hasProductsChanged.value || dateChanged;
if (currentDetail.showCaseType == ShowCaseType.sellerStock) {
// Sem exigir showCaseItems não-vazio: uma loja com estoque zerado
// ainda deve poder atualizar a data de expiração.
return baseCondition;
}
return baseCondition && currentDetail.showCaseItems.isNotEmpty;
}
5. Enum ShowCaseType
Decisão: Novo enum ShowCaseType em lib/shared/enum/show_case_type.dart, espelhando o enum C# da API, com serialização string via extension + parser (mesmo padrão de cart_item_discount_origin.dart).
Valores (C# → JSON → Dart):
| C# | JSON (API) | Dart |
|---|---|---|
Brand | "Brand" | ShowCaseType.brand |
Store | "Store" | ShowCaseType.store |
Recommendation | "Recommendation" | ShowCaseType.recommendation |
BrandSeller | "BrandSeller" | ShowCaseType.brandSeller |
SellerStock | "SellerStock" | ShowCaseType.sellerStock |
Justificativa:
- A API serializa o enum C# como string PascalCase, não como inteiro.
- Elimina magic strings/numbers espalhados no código.
- Type-safe: comparações via
showCaseType == ShowCaseType.sellerStock. - Padrão já consolidado no app (
CartItemDiscountOrigin).
Implementação:
// lib/shared/enum/show_case_type.dart
enum ShowCaseType { brand, store, recommendation, brandSeller, sellerStock }
extension ShowCaseTypeApi on ShowCaseType {
String toApiString() {
switch (this) {
case ShowCaseType.brand: return 'Brand';
case ShowCaseType.store: return 'Store';
case ShowCaseType.recommendation: return 'Recommendation';
case ShowCaseType.brandSeller: return 'BrandSeller';
case ShowCaseType.sellerStock: return 'SellerStock';
}
}
}
abstract class ShowCaseTypeParser {
static ShowCaseType fromJson(String? value) {
switch (value) {
case 'Brand': return ShowCaseType.brand;
case 'Store': return ShowCaseType.store;
case 'Recommendation': return ShowCaseType.recommendation;
case 'BrandSeller': return ShowCaseType.brandSeller;
case 'SellerStock': return ShowCaseType.sellerStock;
default: return ShowCaseType.store; // fallback seguro
}
}
}
Nota de revisão: a subtask 01 implementou inicialmente
store(1)/sellerStock(4)comfromValue(int). Corrigido na subtask 01b.
6. DTOs — Suporte a Novos Campos
Decisão: Adicionar showCaseType: ShowCaseType obrigatório (sem default) em CreateShowCaseRequest, UpdateShowCaseRequest. Adicionar showCaseType/totalItems em ShowCaseDetailResponse (com default para retrocompatibilidade, já que vêm de um GET que pode ainda não ter os campos). Novo model ShowCaseListV2Response para response paginada v2 (incluindo hasNext).
Justificativa:
- Requisito RF-05: DTOs devem refletir contrato da API (string enum, não inteiro).
showCaseTypeobrigatório (não defaultstore): como dois fluxos diferentes constroemCreateShowCaseRequest/UpdateShowCaseRequest(Store viaShowCaseSearchProductsController, SellerStock viaShowCaseDetailController.createAndLoad()), um default silencioso esconderia a intenção em cada call site.ShowCaseSearchProductsController.postFastCatalog()/putFastCatalog()precisam passarshowCaseType: ShowCaseType.storeexplicitamente.ShowCaseDetailResponse/ShowCaseListV2Response: campos com default nofromJson, pois vêm de leitura de API (retrocompatibilidade caso o backend ainda não tenha entregue o campo).
Implementação:
// lib/models/show_case/create_show_case_request.dart
class CreateShowCaseRequest implements DefaultModelInterface {
// ... campos existentes ...
+ final ShowCaseType showCaseType; // obrigatório, sem default
Map<String, dynamic> toJson() => {
// ... existing fields ...
'showCaseType': showCaseType.toApiString(),
};
}
// lib/models/show_case/update_show_case_request.dart
class UpdateShowCaseRequest {
// ... campos existentes ...
+ final ShowCaseType showCaseType; // obrigatório, sem default
Map<String, dynamic> toJson() => {
// ... existing fields ...
'showCaseType': showCaseType.toApiString(),
};
}
// lib/models/show_case/show_case_detail_response.dart
class ShowCaseDetailResponse {
// ... campos existentes ...
+ final ShowCaseType showCaseType; // default store se ausente
+ final int totalItems; // default showCaseItems.length se ausente
factory ShowCaseDetailResponse.fromJson(Map<String, dynamic> json) {
final picker = pick(json);
return ShowCaseDetailResponse(
// ... existing fields ...
showCaseType: ShowCaseTypeParser.fromJson(
picker('showCaseType').asStringOrNull(),
),
totalItems: picker('totalItems').asIntOrNull() ??
(picker('showCaseitems').asList().length),
);
}
}
// lib/models/show_case/show_case_list_v2_response.dart (NOVO)
class ShowCaseListV2Response {
final List<ShowCaseResponseItem> data;
final int total;
final int pageNumber;
final int pageSize;
final bool hasSellerStock;
final bool hasNext; // exposto diretamente pelo backend, sem cálculo no client
ShowCaseListV2Response({
required this.data,
required this.total,
required this.pageNumber,
required this.pageSize,
required this.hasSellerStock,
required this.hasNext,
});
factory ShowCaseListV2Response.fromJson(Map<String, dynamic> json) {
final picker = pick(json);
return ShowCaseListV2Response(
data: picker('data').asListOrEmpty<Map<String, dynamic>>()
.map((e) => ShowCaseResponseItem.fromJson(e))
.toList(),
total: picker('total').asIntOrThrow(),
pageNumber: picker('pageNumber').asIntOrThrow(),
pageSize: picker('pageSize').asIntOrThrow(),
hasSellerStock: picker('hasSellerStock').asBoolOrNull() ?? false,
hasNext: picker('hasNext').asBoolOrNull() ?? false,
);
}
}
7. Aviso de Alterações Não Salvas (Sair / Visualizar / Compartilhar / Copiar Link)
Decisão: Um único método privado no ShowCaseDetailController, _confirmProceedWithPendingChanges(String actionDescription), cobre as 4 situações (sair, visualizar, compartilhar, copiar link), reaproveitando o BottomSheetAskLauncher já usado hoje só para "sair".
Justificativa:
onWillPop()já implementa esse aviso para "sair da tela" — masviewShowCase()/shareShowCase()/copyLink()abrem/compartilham/copiam o link salvo no backend, que fica desatualizado se houver alteração pendente (nome, data ou itens) ainda não persistida viaupdateShowCase().- Reaproveitar o mesmo widget (
BottomSheetAskLauncher) e o mesmo critério (enableEditButton) evita 4 implementações divergentes. - Vale para os dois tipos (
StoreeSellerStock) — mesmo critérioenableEditButtonjá corrigido na Decisão #4.
Implementação:
/// Retorna true se a ação que motivou a chamada deve prosseguir (sem
/// alterações pendentes, ou o vendedor escolheu prosseguir mesmo assim —
/// com ou sem salvar antes). Retorna false se a modal foi fechada sem
/// escolha, ou se "Salvar" falhou (validação ou erro de API).
Future<bool> _confirmProceedWithPendingChanges(String actionDescription) async {
if (!enableEditButton) return true;
final answer = await BottomSheetAskLauncher.ask(
icon: PhosphorIcons.info,
title: 'A vitrine possui alterações não salvas',
description: 'Deseja salvar as alterações antes de $actionDescription?',
confirmButton: BottomSheetLauncherConfirmButton(title: 'Salvar'),
cancelButton: BottomSheetLauncherCancelButton(title: 'Não salvar'),
theme: BottomSheetLauncherTheme.info,
);
if (answer == null) return false; // fechou sem escolher
if (answer) {
final saved = await updateShowCase();
if (!saved) return false; // falhou ao salvar
}
return true; // "Não salvar" OU salvou com sucesso
}
Os 4 pontos de entrada passam a chamar esse helper no início:
Future<bool> onWillPop() => _confirmProceedWithPendingChanges('sair');
Future<void> viewShowCase() async {
if (!await _confirmProceedWithPendingChanges('visualizar')) return;
// ...corpo atual sem mudança...
}
Future<void> shareShowCase() async {
if (!await _confirmProceedWithPendingChanges('compartilhar')) return;
// ...corpo atual sem mudança...
}
Future<void> copyLink() async {
if (!await _confirmProceedWithPendingChanges('copiar o link')) return;
// ...corpo atual sem mudança...
}
updateShowCase() muda de Future<void> para Future<bool> (retorna se salvou com sucesso — false em validação inválida ou erro de API). Não quebra o botão ATUALIZAR VITRINE existente (onPressed: controller.updateShowCase), já que Dart aceita uma função Future<bool> Function() onde se espera void Function().
Efeito colateral: a versão atual do onWillPop() chama Get.back() manualmente e retorna true (potencial double-pop). A nova versão só retorna true/false, deixando o WillPopScope cuidar do pop — corrige isso de graça.
Casos cobertos sem código adicional:
- Campo mantém o valor pendente após "Não salvar" (RF-06 AC11): não mexemos em
nameController/expirationDateno caminho "Não salvar" — só pulamos a chamada aupdateShowCase(). - Sem aviso durante modo de criação em loading/erro (RF-06 AC12):
enableEditButtonjá retornafalsequandodetail.valueé nulo (Decisão #4). EXCLUIR VITRINEfica fora do escopo — usa somente a confirmação de exclusão já existente (ConfirmBottomSheet), sem checagem de alterações pendentes antes.
Fora de escopo (débito técnico à parte): o botão de voltar visível da ZzAppBar chama Navigator.of(context).pop() diretamente, que não consulta onWillPop (confirmado via docs oficiais do Flutter — só Navigator.maybePop()/botão físico Android consultam). Isso significa que, mesmo com a implementação acima, o aviso de "sair" só dispara hoje pelo botão/gesto físico do Android, não pela seta visível da AppBar. Ver context/bug-appbar-back-button-ignora-onwillpop.md.
Architecture
Diagrama de Fluxo — Criar Vitrine (Novo)
Sequência: CRIAR VITRINE com Bottom Sheet
Vendedor toca em "CRIAR VITRINE"
↓
MyShowCaseController.createFastCatalog()
├─→ GetxBottomSheet.showBottomSheet<ShowCaseTypeSelectionBottomSheet>
│ (hasSellerStock: _hasSellerStock.value)
│
├─ Opção: "Criar nova vitrine"
│ └─→ Get.toNamed(AppRoutes.showCaseSearchProducts)
│ → ShowCaseSearchProductsScreen (fluxo existente, sem mudança)
│
└─ Opção: "Vitrine do estoque da loja"
├─ Se hasSellerStock == true
│ └─→ Opção disabled + mensagem visual (sem tap)
│
└─ Se hasSellerStock == false
└─→ MyShowCaseController.createSellerStock()
└─→ Get.toNamed(AppRoutes.storeShowCaseDetail,
arguments: ShowCaseDetailScreenArguments(
catalogName: 'Estoque da loja', isCreating: true))
↓
ShowCaseDetailController (modo criação)
├─→ skeleton (isLoading = true)
├─→ POST /api/show-case-new
│ (showCaseType='SellerStock', name='Estoque da loja',
│ showCaseItems=[], dateExpiration=hoje+31)
│
├─ 200 OK (criada OU já existente — get-or-create)
│ └─→ GET /api/show-case-new/app/{id} (encadeado)
│ └─→ Tela de detalhe renderiza normalmente
│ (branching por showCaseType, ver Decisão #4)
│
└─ Erro inesperado (rede, 5xx)
└─→ ZzErrorState (retry → createAndLoad() de novo)
Ao voltar da tela de detalhe (qualquer caminho): MyShowCaseController
recarrega a listagem (_hasNextPage=true; pagingController.value =
PagingState()) — igual ao padrão já usado em goToDetailFastCatalog().
Diagrama de Fluxo — Detalhe (Condicional)
Sequência: DETALHE por showCaseType
Vendedor toca em item da listagem (ou vem do fluxo de criação)
↓
ShowCaseDetailController.loadData(showCaseId)
├─→ GET /api/show-case-new/app/{showCaseId}
│ → ShowCaseDetailResponse (agora inclui showCaseType, totalItems)
│
├─ Se showCaseType == ShowCaseType.store (Store / Personalizada)
│ └─→ Layout atual: nome editável, seleção de produtos, botão ADICIONAR
│ PUT envia: name, showCaseItems, dateExpiration, showCaseType='Store'
│ Reward events nativos (view/share/copy) registrados normalmente
│
└─ Se showCaseType == ShowCaseType.sellerStock (SellerStock)
└─→ Layout adaptado:
· Título AppBar: "Estoque da loja"
· Nome: "Estoque da loja" (desabilitado)
· Heading da seção: "Confira os produtos" (novo param sectionTitle)
· Contagem: "{totalItems} produtos recomendados" (não showCaseItems.length)
· Grid: sempre no máximo 4 items preview (productCount = min(4, items.length))
· SEM link "Todos os produtos" (não há endpoint p/ lista completa por vitrine)
· Sem botão ADICIONAR PRODUTOS · Sem ícones de remoção
· PUT envia: showCaseType='SellerStock', showCaseItems=[] forçado (API ignora tudo
exceto dateExpiration)
· ATUALIZAR VITRINE habilitado só com mudança real de data (sem exigir
showCaseItems não-vazio)
· Nenhum reward event registrado em Visualizar/Compartilhar/Copiar
(aguardando task 197192)
Componentes e Responsabilidades
| Componente | Camada | Tipo | Responsabilidade |
|---|---|---|---|
MyShowCaseListScreen | Screen | existente (sem mudança) | Renderiza lista; delega lógica ao controller |
MyShowCaseController | Controller | existente + modificado | Migra para v2; gerencia hasSellerStock; chama bottom sheet em createFastCatalog; + createSellerStock() |
ShowCaseTypeSelectionBottomSheet | Widget | novo | Renderiza 2 opções (Personalizada / SellerStock) com lógica de disable |
ShowCaseDetailScreen | Screen | existente + modificado | Branching visual por showCaseType (título, nome, botões, textos); estado de erro no modo criação |
ShowCaseDetailController | Controller | existente + modificado | Lê showCaseType/totalItems; modo criação (isCreating → POST + GET encadeados); enableEditButton corrigido; PUT type=4 só efetiva dateExpiration; suprime reward events para type=4; aviso de alterações pendentes ao sair/visualizar/compartilhar/copiar (_confirmProceedWithPendingChanges) |
ShowCaseDetailLayout | Widget | existente + modificado | Novos params opcionais: hideProductActions, sectionTitle |
ShowCaseBottomSheet | Widget | existente (sem mudança) | Ações pós-criação da vitrine personalizada (Visualizar/Compartilhar/Copiar); não é usado no fluxo de criação SellerStock |
ApiUtils | Util | existente + modificado | + getStoreShowCaseV2Url(...); - getStoreShowCaseUrl (v1, removido — sem uso após migração) |
AppRoutes / AppPages | Routes | existente (sem mudança) | Nenhuma rota nova — o fluxo de criação reaproveita AppRoutes.storeShowCaseDetail |
Components and Interfaces
Novos Arquivos
| Arquivo | Camada | Tipo | Descrição |
|---|---|---|---|
lib/screens/show_case/widgets/show_case_type_selection_bottom_sheet.dart | Widget | novo | Bottom sheet de escolha de modalidade (Personalizada / SellerStock) |
lib/shared/enum/show_case_type.dart | Enum | novo | Enum com 5 valores C#; toApiString() + ShowCaseTypeParser.fromJson() |
lib/models/show_case/show_case_list_v2_response.dart | Model | novo | Response da listagem v2 com data, hasSellerStock, hasNext |
Arquivos Modificados
| Arquivo | Modificação | Impacto |
|---|---|---|
lib/screens/show_case/show_case/my_show_case_controller.dart | createFastCatalog() chama bottom sheet; _load() migra para v2; + RxBool hasSellerStock; + createSellerStock() (navega pro detalhe em modo criação, sem POST aqui); recarrega listagem ao voltar | Compatível com listagem infinita existente; novo estado observável |
lib/shared/utils/api_utils.dart | + getStoreShowCaseV2Url(storeId, pageNumber, search, pageSize); - getStoreShowCaseUrl (v1, removido) | Novo endpoint; remove código morto |
lib/models/show_case/create_show_case_request.dart | + ShowCaseType showCaseType (obrigatório, sem default); toJson() usa toApiString() | Call sites existentes (ShowCaseSearchProductsController) precisam passar o campo explicitamente |
lib/models/show_case/update_show_case_request.dart | + ShowCaseType showCaseType (obrigatório, sem default); toJson() usa toApiString() | Idem acima |
lib/models/show_case/show_case_detail_response.dart | + ShowCaseType showCaseType (default store), + int totalItems (default showCaseItems.length) | Retrocompatível (defaults mantêm vitrines antigas) |
lib/screens/show_case/show_case_detail/show_case_detail_screen.dart | ShowCaseDetailScreenArguments ganha catalogId opcional e isCreating; appBarTitle/sectionTitle/productCountText/productCount/onViewAllProducts condicionados a showCaseType; renderiza ZzErrorState quando controller.hasError | Renderização condicional; sem quebra de prop existente no único call site (goToDetailFastCatalog) |
lib/screens/show_case/show_case_detail/show_case_detail_controller.dart | + isLoading/hasError (RxBool); + createAndLoad()/retryCreate(); enableEditButton corrigido (comparação real de data, guard de showCaseItems só para type=1); updateShowCase() força showCaseItems: [] quando type=4 e passa a retornar Future<bool> (era Future<void>); suprime reward events quando type=4; + _confirmProceedWithPendingChanges() chamado por onWillPop()/viewShowCase()/shareShowCase()/copyLink() (RF-06) | Sem quebra de contrato para o fluxo de visualização existente; mudança de retorno de updateShowCase() é compatível com o botão existente |
lib/screens/show_case/widgets/show_case_detail_layout.dart | + hideProductActions (bool, default false), + sectionTitle (String, default 'Confira os produtos adicionados', substitui o ZZTextNew hardcoded) | Novos params opcionais; sem quebra de assinatura existente |
lib/screens/show_case/search/show_case_search_products_controller.dart | postFastCatalog()/putFastCatalog() passam showCaseType: ShowCaseType.store explicitamente (novo campo obrigatório no DTO) | Necessário só porque showCaseType deixou de ter default |
lib/theme_widgets/tag/tag.dart | + ZzTagTheme.newFeature (backgroundColor: ZZColors.successMedium, foregroundColor: ZZColors.neutralLightest) | Novo valor de enum; não quebra usos existentes de ZzTag |
Data Models
Novo Enum
// lib/shared/enum/show_case_type.dart
enum ShowCaseType { brand, store, recommendation, brandSeller, sellerStock }
extension ShowCaseTypeApi on ShowCaseType {
String toApiString() { /* 'Brand', 'Store', 'SellerStock', ... */ }
}
abstract class ShowCaseTypeParser {
static ShowCaseType fromJson(String? value) { /* fallback: store */ }
}
Novo Model — Listagem v2
// lib/models/show_case/show_case_list_v2_response.dart
class ShowCaseListV2Response {
final List<ShowCaseResponseItem> data;
final int total;
final int pageNumber;
final int pageSize;
final bool hasSellerStock;
final bool hasNext;
ShowCaseListV2Response({
required this.data,
required this.total,
required this.pageNumber,
required this.pageSize,
required this.hasSellerStock,
required this.hasNext,
});
factory ShowCaseListV2Response.fromJson(Map<String, dynamic> json) {
final picker = pick(json);
final dataList = picker('data').asListOrEmpty<Map<String, dynamic>>();
return ShowCaseListV2Response(
data: dataList.map((e) => ShowCaseResponseItem.fromJson(e)).toList(),
total: picker('total').asIntOrThrow(),
pageNumber: picker('pageNumber').asIntOrThrow(),
pageSize: picker('pageSize').asIntOrThrow(),
hasSellerStock: picker('hasSellerStock').asBoolOrNull() ?? false,
hasNext: picker('hasNext').asBoolOrNull() ?? false,
);
}
}
Modificados — ShowCaseDetailScreenArguments
// lib/screens/show_case/show_case_detail/show_case_detail_screen.dart (DIFF)
class ShowCaseDetailScreenArguments {
final String catalogName;
- final String catalogId;
+ final String? catalogId; // nulo enquanto a vitrine ainda não foi criada
+ final bool isCreating; // default false
ShowCaseDetailScreenArguments({
required this.catalogName,
this.catalogId,
this.isCreating = false,
});
}
Modificados — DTOs Existentes
// lib/models/show_case/create_show_case_request.dart (DIFF)
class CreateShowCaseRequest implements DefaultModelInterface {
// ... campos existentes (schema, storeId, name, etc) ...
+ final ShowCaseType showCaseType; // OBRIGATÓRIO, sem default
// toJson() adicionará 'showCaseType': showCaseType.toApiString()
}
// lib/models/show_case/update_show_case_request.dart (DIFF)
class UpdateShowCaseRequest {
// ... campos existentes ...
+ final ShowCaseType showCaseType; // OBRIGATÓRIO, sem default
// toJson() adicionará 'showCaseType': showCaseType.toApiString()
}
// lib/models/show_case/show_case_detail_response.dart (DIFF)
class ShowCaseDetailResponse {
// ... campos existentes (id, name, showCaseItems, etc) ...
+ final ShowCaseType showCaseType; // default store se ausente
+ final int totalItems; // Count total do estoque (SellerStock) ou length de items; default items.length se ausente
factory ShowCaseDetailResponse.fromJson(Map<String, dynamic> json) {
// ... código existente ...
final itemsCount = picker('showCaseitems').asListOrEmpty().length;
final totalItemsValue = picker('totalItems').asIntOrNull() ?? itemsCount;
return ShowCaseDetailResponse(
// ... campos existentes ...
showCaseType: ShowCaseTypeParser.fromJson(
picker('showCaseType').asStringOrNull(),
),
totalItems: totalItemsValue,
);
}
}
Diagrama ER — Relacionamento de Models
Error Handling
Tabela de Erros
| Cenário | Status HTTP | Mensagem | Comportamento | Requisito |
|---|---|---|---|---|
| POST SellerStock — vitrine já existe | 200 | (N/A) | Não é mais um erro. API retorna dados da vitrine existente (get-or-create); App trata como sucesso normal, carrega o detalhe pelo id retornado | RF-03 AC4 |
| POST SellerStock (modo criação) — erro inesperado | 4xx/5xx / falha de rede | "Ocorreu um problema" / "Tivemos um problema ao carregar os dados..." | Tela de detalhe exibe ZzErrorState com retryText: 'TENTE NOVAMENTE'; onRetry chama createAndLoad() de novo (mesma mensagem genérica para qualquer causa) | RF-03 AC5 |
GET v2 listing — hasSellerStock/hasNext ausentes | 200 | (N/A) | Parse com default false para ambos | RF-01 |
GET detalhe — showCaseType ausente | 200 | (N/A) | Parse com default ShowCaseType.store → layout atual | RF-04 AC1 |
GET detalhe — totalItems ausente | 200 | (N/A) | Parse com default showCaseItems.length | RF-04 AC1 |
| PUT SellerStock — nome ou items alterados | 200 | (N/A) | API ignora campos; só aplica dateExpiration. App já envia showCaseItems: [] explicitamente para type=4 | RF-04 AC8 |
Testing Strategy
Testes Unitários
| Cenário | Entrada | Resultado esperado | Requisito |
|---|---|---|---|
Bottom sheet — hasSellerStock=false | createFastCatalog() com flag false | Opção "Vitrine do estoque da loja" habilitada, sem disable visual | RF-02 AC4/5 |
Bottom sheet — hasSellerStock=true | createFastCatalog() com flag true | Opção desabilitada (visual disabled), mensagem de limite exibida | RF-02 AC6/7 |
| Bottom sheet — badge "Nova" | Opção "Vitrine do estoque da loja" renderizada | ZzTag com theme: ZzTagTheme.newFeature (fundo successMedium, texto neutralLightest) | RF-02 AC2 |
| Bottom sheet — tap "Criar nova vitrine" | Bottom sheet aberto | Navega para showCaseSearchProducts com fluxo de criação | RF-02 AC4 |
| Bottom sheet — tap "Vitrine do estoque" (enabled) | Bottom sheet aberto, flag=false | Chama MyShowCaseController.createSellerStock(), navegando para ShowCaseDetailScreen com isCreating=true | RF-02 AC5 |
| Criação SellerStock — POST + GET encadeados | ShowCaseDetailController com arguments.isCreating=true | createAndLoad() faz POST, extrai id de CreateShowCaseResponse, encadeia loadData() com esse id; tela renderiza dados completos | RF-03 AC2/AC4 |
| Criação SellerStock — get-or-create (vitrine já existia) | POST retorna 200 com dados de vitrine existente | App trata como sucesso; nenhuma mensagem de duplicata exibida | RF-03 AC4 |
| Criação SellerStock — erro inesperado | POST falha (rede/5xx) | hasError=true; ZzErrorState exibido; retryCreate() chama createAndLoad() de novo | RF-03 AC5 |
| Criação SellerStock — sem seleção de produtos | Fluxo completo | Nenhuma tela ShowCaseSearchProducts nem tela intermediária de formulário exibida | RF-03 AC6 |
| Criação SellerStock — sem reward event | createAndLoad() executado com sucesso | Nenhuma chamada a rewardEventController.register(...) | RF-03 AC7 |
Detalhe — showCaseType=Store | loadData(id) com "Store" | Layout atual: nome editável, grid de produtos, botão ADICIONAR, ícones de remoção, reward events registrados normalmente | RF-04 AC2 |
Detalhe — showCaseType=SellerStock | loadData(id) com "SellerStock" | Título "Estoque da loja"; nome "Estoque da loja" (disabled); heading "Confira os produtos"; sem ADICIONAR; contagem = "{totalItems} produtos recomendados" | RF-04 AC3/4/5 |
| Detalhe SellerStock — grid nunca excede 4 | totalItems=87, showCaseItems=[4 items] | productCount == 4 (não 87); nenhum RangeError | RF-04 AC6 |
| Detalhe SellerStock — sem link "Todos os produtos" | totalItems=87 | onViewAllProducts == null, independente de totalItems | RF-04 AC6 |
| Detalhe SellerStock — PUT força lista vazia | Altera só dateExpiration | PUT com {showCaseType: "SellerStock", dateExpiration: ..., name: "...", showCaseItems: []} (vazio, não os 4 preview) | RF-04 AC8 |
Detalhe SellerStock — enableEditButton com data igual | Usuário reabre o date picker e escolhe a mesma data já salva | enableEditButton == false (comparação real, não só "campo tocado") — vale para Store e SellerStock | RF-04 AC11 |
Detalhe SellerStock — enableEditButton com estoque zerado | showCaseItems=[] (loja sem estoque), data alterada | enableEditButton == true (guard de lista não-vazia não se aplica a type=4) | RF-04 AC10 |
| Detalhe SellerStock — sem reward events | viewShowCase()/shareShowCase()/copyLink() chamados com type=4 | Nenhuma chamada a rewardEventController.register(...) | RF-04 AC12 |
| Detalhe SellerStock — limite de data diferente | showCaseType=SellerStock no campo de data | finalDateRange == hoje + 30 dias (não 31, que continua valendo só para showCaseType=Store) | RF-04 AC13 |
| Criação SellerStock — data padrão do POST | createAndLoad() monta a request | dateExpiration == hoje + 30 dias | RF-03 AC2 |
| Detalhe — aviso ao sair com alteração pendente | onWillPop() com enableEditButton=true | Modal "A vitrine possui alterações não salvas" / "Deseja salvar as alterações antes de sair?" | RF-06 AC1 |
| Detalhe — aviso ao visualizar/compartilhar/copiar | viewShowCase()/shareShowCase()/copyLink() com enableEditButton=true | Mesma modal, com descrição variando por ação ("...antes de visualizar?" / "...compartilhar?" / "...copiar o link?") | RF-06 AC2/3/4 |
| Detalhe — "Salvar" na modal de aviso | Vendedor escolhe Salvar | updateShowCase() chamado; em caso de sucesso, ação original prossegue com dado atualizado | RF-06 AC5 |
| Detalhe — "Não salvar" na modal de aviso | Vendedor escolhe Não salvar | Ação original prossegue com o dado já salvo; campo editado continua exibindo o valor pendente na tela | RF-06 AC6/AC11 |
| Detalhe — sem alteração pendente | enableEditButton=false | Ação (sair/visualizar/compartilhar/copiar) executa direto, sem modal | RF-06 AC7 |
| Detalhe — modal fechada sem escolha | answer == null | Permanece na tela; ação original não executa | RF-06 AC8 |
| Detalhe — "Salvar" falha | updateShowCase() retorna false (validação ou erro de API) | Ação original não prossegue; vendedor permanece na tela | RF-06 AC9 |
| Detalhe — sem aviso durante criação (loading/erro) | isCreating=true, detail.value == null | enableEditButton == false; sair não exibe modal | RF-06 AC12 |
Parse v2 — hasSellerStock/hasNext ausentes | JSON sem os campos | Ambos default false | Sem erro de parsing |
Parse detalhe — showCaseType ausente | JSON sem campo | ShowCaseType.store (default) | Retrocompatível |
Parse detalhe — totalItems ausente | JSON sem campo | totalItems = showCaseItems.length (default) | Retrocompatível |
Enum ShowCaseType | fromJson('SellerStock') | ShowCaseType.sellerStock | Type-safe |
Enum ShowCaseType | fromJson('Unknown') / fromJson(null) | ShowCaseType.store (fallback) | Default seguro |
Dependências
| Dependência | Status | Nota |
|---|---|---|
Task 197190 — Backend v2 com showCaseType, hasSellerStock, hasNext, comportamento get-or-create no POST | Pendente | Bloqueia integração v2 e criação SellerStock. Contrato atualizado em 03/07 — ver context/frontend-contract-diff.md |
frontend-contract-diff.md — Contrato de API | Disponível | Atualizado em 03/07/2026: get-or-create no POST (sem erro de duplicata), hasNext na listagem v2 |
AS-IS.md — Mapeamento de código App | Disponível | Validado em 25/06/2026; atualizado em 03/07/2026 |
| Referências visuais | Disponível | context/image.png a image-3.png |
context/gap-arquivos-e-props-nao-mapeados.md — Lacunas de mapeamento | Disponível | Componentes e props não capturadas no AS-IS inicial; impacto baixo no design |
context/gap-paginacao-v2-hasnext.md — Incerteza hasNext vs hasSellerStock | Disponível | Contrato v2 pode incluir hasNext; roadmap de solução |
context/gap-todos-produtos-sellerstock.md — Comportamento "Todos os produtos" SellerStock | Disponível | Rota showCaseAllProducts não diferencia tipo; requer validação de UX |
context/bug-productcount-crash-grid-detalhe.md — Crash em grid com totalItems | Investigado | MainAxisExtent pode quebrar se totalItems >> showCaseItems.length; sugestão: usar count efetivo de items renderizados |
context/bug-textos-secao-produtos-conflito.md — Conflito de rótulos | Investigado | Textos da seção de produtos podem conflitar; sugestão: usar dois parâmetros distintos (sectionTitle + productCountText) |
context/comportamento-controller-detalhe-sellerstock.md — Lógica PUT SellerStock | Investigado | PUT type=4 deve enviar apenas dateExpiration; resto é ignorado pela API; código atual envia tudo |
context/fluxo-pos-criacao-e-dependencia-rewards.md — Integração Rewards pós-criação | Depende de 197192 | RewardEvent CreateShowcaseRewardEvent deve incluir showCaseType; não bloqueia layout |
| Task 197192 — Eventos Rewards SellerStock | Pendente | Não bloqueia layout; enquanto pendente, nenhum reward event é registrado para ações de SellerStock |
Checklist de Qualidade
- Todos os requisitos funcionais cobertos (RF-01 a RF-05, com RF-03 reescrito)
- Tratamento de erro genérico (com retry) só para falhas inesperadas — duplicata deixou de ser um caminho de erro
-
productCountnunca deriva detotalItemsdiretamente (grid sempre capada em 4) - Textos da seção de produtos corretos por tipo (
sectionTitle+productCountText, dois parâmetros distintos) - Novo fluxo não quebra vitrines personalizadas existentes (retrocompatibilidade)
- Nenhuma rota/tela/controller novo para a criação SellerStock — reaproveita
ShowCaseDetailScreen -
enableEditButtoncom comparação real de data (Store e SellerStock) e sem guard de estoque vazio para SellerStock - Nenhum reward event registrado para ações de SellerStock (criação, visualizar, compartilhar, copiar) enquanto a task 197192 estiver pendente
-
showCaseTypeobrigatório (sem default) emCreateShowCaseRequest/UpdateShowCaseRequest; call sites existentes atualizados -
ApiUtils.getStoreShowCaseUrl(v1) removido - Enum
ShowCaseTypeelimina magic numbers - Parse de v2 não quebra
DefaultApiControllerpara outros fluxos - Testes cobrindo branching por tipo, defaults de parse, desabilitar opção com mensagem, POST+GET encadeados, retry
- Aviso de alterações não salvas cobrindo as 4 ações (sair, visualizar, compartilhar, copiar link), com o mesmo critério (
enableEditButton) e helper compartilhado (_confirmProceedWithPendingChanges) - Padrão visual (bottom sheet, skeleton, estado de erro) consistente com app existente