Prioriza a parte web a pedido do usuário. 2 subagentes eden-frontend em paralelo sobre o shell autenticado já pronto: - Empresas/Produtos/Revendas: CRUD completo (lista + criar/editar), layout modelado nas páginas reais do tema DreamsERP comprado (purchase/vendors.tsx, inventory/products.tsx) — máscara de moeda para as 5 faixas de preço + implantação. Novo componente Table<T> genérico em packages/ui. - CRM: Leads (lista, criar/editar, converter em oportunidade) e Oportunidades, modelados em crm/leads.tsx/dealsDetails.tsx. - Ofertas/Quotes: o módulo mais rico — criação com lista dinâmica de itens produto/quantidade, fidelidade reduzida opcional, desconto opcional, totais calculados pelo servidor; detalhe com aprovar/rejeitar e fechar negócio. Modelado em sales/salesQuotes*.tsx. Novo componente Select em packages/ui. Correção de instrução no meio da leva: usuário pediu para seguir de perto o layout real do tema comprado, não só os tokens visuais já extraídos — as páginas React relevantes do tema foram extraídas para .tmp/theme-reference/ (nunca versionado) em cada worktree e os agentes já em execução foram redirecionados via mensagem (não relançados). Bug real do próprio agente, corrigindo meu brief: Leads/Opportunities respondem em camelCase (mapLead/mapOpportunity), não snake_case como o resto da API — verificado contra o código-fonte real antes de aceitar. Integração manual: um dos 2 agentes não conseguiu git merge no próprio sandbox e restaurou o shell existente por leitura+escrita — cada arquivo restaurado foi comparado byte a byte contra main (diff -rq) antes de aceitar. App.tsx/AuthenticatedLayout.tsx/packages/ui/index.ts reconciliados manualmente juntando as rotas/exports dos dois agentes. Verificado: pnpm turbo run typecheck build (28/28); containers eden-api/eden-core reconstruídos; bundle de produção contém todas as novas telas; login simulando Origin de browser real; listagens retornando dados reais; criação de lead confirmando o formato camelCase. Claude-Session: https://claude.ai/code/session_01BcWWuKfykGtUweXmHD6cKx Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
125 lines
3.9 KiB
TypeScript
125 lines
3.9 KiB
TypeScript
/**
|
|
* Client for `apps/api` `/v1/quotes` (see apps/api/src/quotes/quotes.controller.ts,
|
|
* quotes.service.ts, dto/*, and packages/database/src/repositories/quotes.ts
|
|
* for the source of truth).
|
|
*
|
|
* Unlike Leads/Opportunities, Quotes pass the DB row straight through with
|
|
* no repository-level mapper — the top-level response is snake_case. The
|
|
* one exception is `items`, a JSONB column whose element shape
|
|
* (`QuoteItem`) IS camelCase — a real inconsistency in the API, not a typo
|
|
* here. Money fields (`implementation_fee`, `impl_geral`, `monthly_total`,
|
|
* `contract_total`, `proposed_monthly_total`) come back as NUMERIC-typed
|
|
* strings from Postgres — always `Number(...)` before formatting/math.
|
|
*/
|
|
import { apiRequest } from "./http.js";
|
|
|
|
export const CONTRACT_PERIODS = [0, 12, 24, 36, 48] as const;
|
|
export type ContractPeriod = (typeof CONTRACT_PERIODS)[number];
|
|
|
|
export const IMPL_PAYMENT_CONDITIONS = ["À vista", "1+1", "1+2"] as const;
|
|
export type ImplPaymentCondition = (typeof IMPL_PAYMENT_CONDITIONS)[number];
|
|
|
|
export type ApprovalStatus = "none" | "pending" | "approved" | "rejected";
|
|
export type DealStatus = "orcamento" | "fechado" | "perdido";
|
|
|
|
export interface QuoteItem {
|
|
productId: string;
|
|
productName: string;
|
|
quantity: number;
|
|
unitPrice: number;
|
|
total: number;
|
|
implUnitPrice: number;
|
|
implTotal: number;
|
|
price0: number | null;
|
|
price12: number | null;
|
|
price24: number | null;
|
|
price36: number | null;
|
|
price48: number | null;
|
|
}
|
|
|
|
export interface Quote {
|
|
id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
created_by: string | null;
|
|
quote_number: string;
|
|
reseller_id: string;
|
|
client_name: string;
|
|
client_company: string | null;
|
|
client_email: string | null;
|
|
client_phone: string;
|
|
contract_period: number;
|
|
fidelity_period: number | null;
|
|
items: QuoteItem[];
|
|
implementation_fee: string;
|
|
impl_geral: string;
|
|
monthly_total: string;
|
|
contract_total: string;
|
|
proposed_monthly_total: string | null;
|
|
approval_status: ApprovalStatus;
|
|
approval_notes: string | null;
|
|
approved_by_id: string | null;
|
|
notes: string | null;
|
|
deal_status: DealStatus;
|
|
impl_payment_condition: ImplPaymentCondition;
|
|
client_registration_id: string | null;
|
|
}
|
|
|
|
export interface QuoteItemInput {
|
|
productId: string;
|
|
quantity: number;
|
|
}
|
|
|
|
export interface CreateQuoteInput {
|
|
resellerId: string;
|
|
clientName: string;
|
|
clientCompany?: string;
|
|
clientEmail?: string;
|
|
clientPhone: string;
|
|
contractPeriod: ContractPeriod;
|
|
fidelityPeriod?: number;
|
|
items: QuoteItemInput[];
|
|
implGeral?: number;
|
|
proposedMonthlyTotal?: number;
|
|
implPaymentCondition?: ImplPaymentCondition;
|
|
notes?: string;
|
|
}
|
|
|
|
export interface DecideApprovalInput {
|
|
decision: "approved" | "rejected";
|
|
notes?: string;
|
|
}
|
|
|
|
export interface CloseDealInput {
|
|
clientRegistrationId: string;
|
|
}
|
|
|
|
export function listQuotes(accessToken: string, dealStatus?: string): Promise<Quote[]> {
|
|
const query = dealStatus ? `?dealStatus=${encodeURIComponent(dealStatus)}` : "";
|
|
return apiRequest<Quote[]>(`/quotes${query}`, { method: "GET" }, { accessToken });
|
|
}
|
|
|
|
export function getQuote(id: string, accessToken: string): Promise<Quote> {
|
|
return apiRequest<Quote>(`/quotes/${id}`, { method: "GET" }, { accessToken });
|
|
}
|
|
|
|
export function createQuote(input: CreateQuoteInput, accessToken: string): Promise<Quote> {
|
|
return apiRequest<Quote>("/quotes", { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
|
}
|
|
|
|
export function decideQuoteApproval(
|
|
id: string,
|
|
input: DecideApprovalInput,
|
|
accessToken: string,
|
|
): Promise<Quote> {
|
|
return apiRequest<Quote>(
|
|
`/quotes/${id}/approval`,
|
|
{ method: "PATCH", body: JSON.stringify(input) },
|
|
{ accessToken },
|
|
);
|
|
}
|
|
|
|
export function closeQuoteDeal(id: string, input: CloseDealInput, accessToken: string): Promise<Quote> {
|
|
return apiRequest<Quote>(`/quotes/${id}/close`, { method: "POST", body: JSON.stringify(input) }, { accessToken });
|
|
}
|