import { useCallback, useEffect, useState } from "react"; import type { FormEvent } from "react"; import { useParams } from "react-router-dom"; import { Link } from "react-router-dom"; import { Alert, Badge, Button, Card, Input, PageHeader, Select } from "@eden/ui"; import type { BadgeVariant } from "@eden/ui"; import { useAuth } from "../../lib/auth/AuthContext.js"; import { ApiError } from "../../lib/api/http.js"; import { closeQuoteDeal, decideQuoteApproval, getQuote } from "../../lib/api/quotes.js"; import type { ApprovalStatus, DealStatus, Quote } from "../../lib/api/quotes.js"; import { listResellers } from "../../lib/api/resellers.js"; import type { Reseller } from "../../lib/api/resellers.js"; import { listClientRegistrations } from "../../lib/api/client-registrations.js"; import type { ClientRegistration } from "../../lib/api/client-registrations.js"; import { formatCurrencyBRL, formatDateBR } from "../../lib/format.js"; import { PRIMARY_LINK_BUTTON_CLASS } from "../../components/linkButtonClasses.js"; import { Breadcrumb } from "../../components/Breadcrumb.js"; /** * Layout reference: DreamsERP theme `.tmp/theme-reference/sales/salesQuotesDetails.tsx` * (breadcrumb + title/status header, bill-to summary block, item table, * right-aligned financial totals) plus `.tmp/theme-reference/crm/dealsDetails.tsx` * (summary sidebar's `flex justify-between` key/value rows for the numbers * that don't fit an invoice-style table). Rebuilt with `@eden/ui` + plain * Tailwind — no antd/Preline dependency. */ const APPROVAL_LABEL: Record = { none: "Sem aprovação", pending: "Pendente", approved: "Aprovada", rejected: "Rejeitada", }; const APPROVAL_VARIANT: Record = { none: "neutral", pending: "warning", approved: "success", rejected: "danger", }; const DEAL_LABEL: Record = { orcamento: "Orçamento", fechado: "Fechado", perdido: "Perdido", }; const DEAL_VARIANT: Record = { orcamento: "info", fechado: "success", perdido: "danger", }; export function QuoteDetailPage() { const { id } = useParams<{ id: string }>(); const { accessToken } = useAuth(); const [quote, setQuote] = useState(null); const [reseller, setReseller] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = useCallback(() => { if (!id || !accessToken) return; setLoading(true); setError(null); Promise.all([getQuote(id, accessToken), listResellers(accessToken)]) .then(([quoteResult, resellers]) => { setQuote(quoteResult); setReseller(resellers.find((r) => r.id === quoteResult.reseller_id) ?? null); }) .catch((err) => setError(err instanceof ApiError ? err.message : "Não foi possível carregar a oferta.")) .finally(() => setLoading(false)); }, [id, accessToken]); useEffect(() => { load(); }, [load]); if (loading) { return (

Carregando oferta…

); } if (error || !quote) { return (
{error ?? "Oferta não encontrada."}
); } return (
{APPROVAL_LABEL[quote.approval_status]} {DEAL_LABEL[quote.deal_status]} {quote.deal_status === "fechado" ? ( Criar contrato a partir desta oferta ) : null}
} />
{quote.items.map((item, index) => ( ))}
Produto Qtd. Preço unit. Total Impl. unit. Impl. total
{item.productName} {item.quantity} {formatCurrencyBRL(item.unitPrice)} {formatCurrencyBRL(item.total)} {formatCurrencyBRL(item.implUnitPrice)} {formatCurrencyBRL(item.implTotal)}
{quote.proposed_monthly_total != null ? ( ) : null}
{quote.notes ? (

Notas

{quote.notes}

) : null}
{quote.approval_status === "pending" ? ( ) : quote.approval_status !== "none" && quote.approval_notes ? (

{quote.approval_notes}

) : null} {quote.deal_status === "orcamento" ? ( ) : quote.client_registration_id ? (
Cadastro de cliente
Ver cadastro →
) : null}
); } function Row({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function SummaryRow({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) { return (
{label} {value}
); } function ApprovalCard({ quoteId, accessToken, onDecided, }: { quoteId: string; accessToken: string | null; onDecided: () => void; }) { const [notes, setNotes] = useState(""); const [submitting, setSubmitting] = useState<"approved" | "rejected" | null>(null); const [error, setError] = useState(null); async function decide(decision: "approved" | "rejected") { if (!accessToken) return; setError(null); setSubmitting(decision); try { await decideQuoteApproval(quoteId, { decision, notes: notes || undefined }, accessToken); onDecided(); } catch (err) { setError(err instanceof ApiError ? err.message : "Não foi possível registrar a decisão."); } finally { setSubmitting(null); } } return ( {error ? ( {error} ) : null}

Esta oferta tem desconto e/ou fidelidade reduzida e precisa de aprovação antes de seguir.

setNotes(e.target.value)} className="mb-3" />
); } /** * Was a free-text UUID field the user had to paste manually — replaced with * a real ` setClientRegistrationId(e.target.value)} placeholder={registrations === null ? "Carregando cadastros…" : "Selecione um cadastro"} hint="Cadastro de cliente que receberá este negócio" className="min-w-72 flex-1" required /> )} ); }