feat(mvp-rep): formas de pagamento do ERP + suporte offline completo

Formas de pagamento:
- Endpoint GET /catalog/payment-methods lendo vw_formas_pagamento
  filtrado por ativa=1 e integrar_sar=1
- FormaPagamento schema/type no shared api-interface
- Hook useFormasPagamento (staleTime 1h) substituindo lista hardcoded

Offline (FR-4.2 / NFR-2.1–2.4):
- IndexedDB queue: lib/offline/idb.ts + order-queue.ts sem deps externos
- NewOrderPage detecta !navigator.onLine → enqueueOrder() → toast + reset
- useOfflineSync: auto-sync ao reconectar (POST orders + PATCH transmit)
- usePendingOrders: fila reativa via CustomEvents
- AppShell: banner offline + useOfflineSync() global
- OrdersPage: seção de pedidos pendentes com retry/descartar
- sw.js: network-first para API GETs cacheáveis + stale-while-revalidate
  para assets + app shell navigate fallback

Docs:
- architecture.md: documento de decisões de arquitetura do SAR MVP

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 21:30:23 +00:00
parent 1647871a39
commit a3c68f9f05
33 changed files with 2175 additions and 173 deletions

View File

@@ -5,3 +5,4 @@ export * from './lib/order.contract';
export * from './lib/product.contract';
export * from './lib/dashboard.contract';
export * from './lib/notifications.contract';
export * from './lib/company.contract';

View File

@@ -20,6 +20,7 @@ export const ClientSummarySchema = z.object({
email: z.string().nullable(),
telefone: z.string().nullable(),
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable().optional(),
limiteCreditoStr: z.string().nullable(),
activityStatus: ActivityStatusSchema,
dtUltimaCompra: z.iso.datetime().nullable(),

View File

@@ -0,0 +1,21 @@
import { z } from 'zod';
// Dados legais da empresa matriz (a que fatura o pedido) — cabeçalho do PDF.
// Fonte: gestao.empresa (matriz) + vw_municipios.
export const EmpresaInfoSchema = z.object({
idEmpresa: z.number().int(),
razaoSocial: z.string(),
nomeFantasia: z.string().nullable(),
cnpj: z.string().nullable(),
inscricaoEstadual: z.string().nullable(),
endereco: z.string().nullable(),
numero: z.string().nullable(),
complemento: z.string().nullable(),
bairro: z.string().nullable(),
cidade: z.string().nullable(),
uf: z.string().nullable(),
cep: z.string().nullable(),
telefone: z.string().nullable(),
email: z.string().nullable(),
});
export type EmpresaInfo = z.infer<typeof EmpresaInfoSchema>;

View File

@@ -11,6 +11,31 @@ export const ClienteInativoSchema = z.object({
});
export type ClienteInativo = z.infer<typeof ClienteInativoSchema>;
// Dimensão de meta. O ERP (vw_metas.tipo) define como o cliente acompanha metas:
// GL = global, GR = por grupo. Motor único; outras dimensões (marca/subgrupo/
// produto) entram aqui depois sem mudar a forma.
export const MetaDimensaoSchema = z.enum(['global', 'grupo']);
export type MetaDimensao = z.infer<typeof MetaDimensaoSchema>;
// Linha de meta vs realizado por grupo, multi-medida (codigo=null = rollup global).
// fator = R$/kg (valor/peso); o ERP guarda vl_fator na meta.
export const MetaItemSchema = z.object({
codigo: z.number().int().nullable(),
rotulo: z.string(),
pedidos: z.number().int(), // qtd de pedidos faturados no grupo (realizado)
valorMeta: z.number(),
valorReal: z.number(),
qtdMeta: z.number(),
qtdReal: z.number(),
pesoMeta: z.number(),
pesoReal: z.number(),
fatorMeta: z.number(),
fatorReal: z.number(),
pct: z.number(), // % de valor (real/meta) — base da barra de progresso
falta: z.number(), // valor faltante p/ a meta
});
export type MetaItem = z.infer<typeof MetaItemSchema>;
export const RepDashboardSchema = z.object({
meta: z.object({
atingido: z.number(),
@@ -18,6 +43,9 @@ export const RepDashboardSchema = z.object({
pct: z.number(),
falta: z.number(),
}),
// Dimensão detectada do ERP e detalhamento por grupo (vazio quando global).
metaDimensao: MetaDimensaoSchema.default('global'),
metasPorGrupo: z.array(MetaItemSchema).default([]),
comissao: z.object({
fixa: z.number(),
flex: z.number(),
@@ -32,6 +60,7 @@ export type RepDashboard = z.infer<typeof RepDashboardSchema>;
export const RepInativosSummarySchema = z.object({
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable().optional(),
inativosCount: z.number().int(),
});
export type RepInativosSummary = z.infer<typeof RepInativosSummarySchema>;

View File

@@ -6,13 +6,24 @@ import { z } from 'zod';
// ─── Situa ────────────────────────────────────────────────────────────────────
// situa: 1=Pendente 2=Aprovado 3=Cancelado 4=Faturado
export const SituaPedidoSchema = z.union([z.literal(1), z.literal(2), z.literal(3), z.literal(4)]);
// Ciclo de vida do pedido SAR:
// 0=Orçamento → 1=Ag. Aprovação (se desconto > alçada) → 2=Transmitido
// Estados que o SAR controla: Orçamento e Transmitido (1 é o gate de desconto).
// Após Transmitido, o status passa a refletir o ERP (Emitido/Cancelado/Aguardando…)
// — espelhado quando a integração existir.
export const SituaPedidoSchema = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4),
]);
export type SituaPedido = z.infer<typeof SituaPedidoSchema>;
export const SITUA_LABEL: Record<number, string> = {
0: 'Orçamento',
1: 'Ag. Aprovação',
2: 'Aprovado',
2: 'Transmitido',
3: 'Cancelado',
4: 'Faturado',
};
@@ -54,6 +65,7 @@ export const PedidoSummarySchema = z.object({
nomeCliente: z.string().nullable().optional(),
razaoCliente: z.string().nullable().optional(),
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable().optional(),
situa: z.number().int(),
statusDescr: z.string().optional(), // descrição legível do status
dtPedido: z.string(),

View File

@@ -28,6 +28,16 @@ export const PautaSchema = z.object({
descricao: z.string(),
});
export type Pauta = z.infer<typeof PautaSchema>;
// ─── Forma de Pagamento (vw_formas_pagamento) ─────────────────────────────────
export const FormaPagamentoSchema = z.object({
codigo: z.number().int(),
descricao: z.string(),
numParcelas: z.number().int().nullable(),
txAcrescimo: z.string(),
});
export type FormaPagamento = z.infer<typeof FormaPagamentoSchema>;
export type ProdutoSummary = z.infer<typeof ProdutoSummarySchema>;
// ─── Produto Detail ───────────────────────────────────────────────────────────