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>
128 lines
3.7 KiB
TypeScript
128 lines
3.7 KiB
TypeScript
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 });
|
|
}
|