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:
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user