Frontend completo (Etapas 11-18) e extensões de backend (Etapas 13 e 18)

Adiciona o frontend Next.js em frontend/ (setup, design system,
autenticação/tema, layout/navegação, área master, área tenant completa:
clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas,
equipe e perfil). Estende o backend de forma aditiva e autorizada:
cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe +
edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo
e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 09:41:13 -03:00
co-authored by Claude Sonnet 5
parent f9738e54da
commit 323912748f
179 changed files with 29312 additions and 22 deletions
+127
View File
@@ -0,0 +1,127 @@
import { apiClient } from '@/lib/api/client';
import { buildQuery, type PaginatedResult } from '@/lib/api/types';
import type { ProductService } from '@/lib/api/products';
export type VendaStatus = 'orcamento' | 'confirmada' | 'cancelada' | 'concluida';
export type ParcelaStatus = 'pendente' | 'pago' | 'atrasado' | 'cancelado';
export type FormaPagamento = 'pix' | 'boleto' | 'cartao' | 'dinheiro';
/** Transições válidas (espelha `TRANSICOES_VALIDAS`, `src/modules/sales/sales.service.ts`). */
export const TRANSICOES_VALIDAS: Record<VendaStatus, VendaStatus[]> = {
orcamento: ['confirmada', 'cancelada'],
confirmada: ['concluida', 'cancelada'],
concluida: [],
cancelada: [],
};
/** Espelha o model `sale_payments`. */
export interface SalePayment {
id: string;
sale_id: string;
numero_parcela: number;
valor: string;
vencimento: string;
pago_em: string | null;
forma_pagamento: string | null;
status: ParcelaStatus;
alerta_enviado_em: string | null;
criado_em: string;
}
/** Espelha o model `sale_items` + `include: { produto: true }`. */
export interface SaleItem {
id: string;
sale_id: string;
product_service_id: string;
quantidade: string;
valor_unitario: string;
valor_total: string;
produto: ProductService;
}
/**
* `SALE_INCLUDE` do backend é usado tanto em `findAll` quanto em
* `findOne` — listagem e detalhe têm exatamente o mesmo shape (itens com
* produto, parcelas ordenadas, customer resumido).
*/
export interface SaleListItem {
id: string;
tenant_id: string;
customer_id: string;
vendedor_id: string | null;
numero_venda: number;
status: VendaStatus;
valor_total: string;
desconto: string;
observacoes: string | null;
data_venda: string;
criado_em: string;
atualizado_em: string;
customer: { id: string; razao_social: string };
itens: SaleItem[];
parcelas: SalePayment[];
}
export interface ListSalesParams {
status?: VendaStatus;
customer_id?: string;
data_inicio?: string;
data_fim?: string;
page?: number;
limit?: number;
}
export function listSales(params: ListSalesParams) {
return apiClient.get<PaginatedResult<SaleListItem>>(`/tenant/vendas${buildQuery(params)}`);
}
export function getSale(id: string) {
return apiClient.get<SaleListItem>(`/tenant/vendas/${id}`);
}
export interface SaleItemInput {
product_service_id: string;
quantidade: number;
/** Se omitido, usa o `valor_venda` do catálogo. */
valor_unitario?: number;
}
export interface SaleParcelaInput {
valor: number;
vencimento: string;
}
/**
* Espelha `CreateSaleDto`/`UpdateSaleDto`. `numero_parcelas` OU `parcelas`
* — nunca os dois, nunca nenhum (validado em runtime pelo backend, não
* pelo DTO — o schema zod do frontend replica essa regra antes de bater
* na API).
*/
export interface SaleInput {
customer_id: string;
data_venda?: string;
desconto?: number;
observacoes?: string;
itens: SaleItemInput[];
numero_parcelas?: number;
parcelas?: SaleParcelaInput[];
}
export function createSale(input: SaleInput & { status?: 'orcamento' | 'confirmada' }) {
return apiClient.post<SaleListItem>('/tenant/vendas', input);
}
/** Só permitido com a venda em `orcamento` — o backend redefine itens/parcelas por inteiro. */
export function updateSale(id: string, input: SaleInput) {
return apiClient.put<SaleListItem>(`/tenant/vendas/${id}`, input);
}
/**
* Retorna a `sales` "crua" (sem `itens`/`parcelas`/`customer` populados,
* diferente de `create`/`update`/`getSale`) — por isso as telas sempre
* re-buscam via `getSale` depois de mudar o status, em vez de usar a
* resposta direto.
*/
export function changeSaleStatus(id: string, status: Exclude<VendaStatus, 'orcamento'>) {
return apiClient.patch<{ id: string; status: VendaStatus }>(`/tenant/vendas/${id}/status`, { status });
}