"use client"; import { useEffect, useState } from "react"; import type { CommercialTerms, PaymentMilestone, PricingModel } from "@capakraken/shared"; import { computeCommercialTermsSummary, computeMilestoneAmounts, validatePaymentMilestones, } from "@capakraken/engine"; import { clsx } from "clsx"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { formatMoney } from "~/lib/format.js"; import { trpc } from "~/lib/trpc/client.js"; interface Props { estimateId: string; baseCostCents: number; basePriceCents: number; baseCurrency: string; canEdit: boolean; } const PRICING_MODELS: Array<{ value: PricingModel; label: string }> = [ { value: "fixed_price", label: "Fixed Price" }, { value: "time_and_materials", label: "Time & Materials" }, { value: "hybrid", label: "Hybrid" }, ]; export function CommercialTermsEditor({ estimateId, baseCostCents, basePriceCents, baseCurrency, canEdit, }: Props) { const utils = trpc.useUtils(); const { data, isLoading } = trpc.estimate.getCommercialTerms.useQuery({ estimateId, }); const updateMutation = trpc.estimate.updateCommercialTerms.useMutation({ onSuccess: async () => { await utils.estimate.getCommercialTerms.invalidate({ estimateId }); }, }); const [terms, setTerms] = useState({ pricingModel: "fixed_price", contingencyPercent: 0, discountPercent: 0, paymentTermDays: 30, paymentMilestones: [], warrantyMonths: 0, }); const [dirty, setDirty] = useState(false); useEffect(() => { if (data?.terms) { setTerms(data.terms); setDirty(false); } }, [data]); function update(patch: Partial) { setTerms((prev) => ({ ...prev, ...patch })); setDirty(true); } function save() { updateMutation.mutate({ estimateId, terms, }); setDirty(false); } const summary = computeCommercialTermsSummary({ baseCostCents, basePriceCents, terms, }); const milestoneWarnings = validatePaymentMilestones(terms.paymentMilestones); const milestoneAmounts = computeMilestoneAmounts( summary.adjustedPriceCents, terms.paymentMilestones, ); if (isLoading) { return (
); } return (
{/* Adjusted financials summary */}

Adjusted Cost

{formatMoney(summary.adjustedCostCents, baseCurrency)}

{summary.contingencyCents > 0 && (

+{formatMoney(summary.contingencyCents, baseCurrency)} contingency

)}

Adjusted Price

{formatMoney(summary.adjustedPriceCents, baseCurrency)}

{summary.discountCents > 0 && (

-{formatMoney(summary.discountCents, baseCurrency)} discount

)}

Adjusted Margin

= 0 ? "text-emerald-700" : "text-red-700", )} > {formatMoney(summary.adjustedMarginCents, baseCurrency)}

{summary.adjustedMarginPercent.toFixed(1)}% of price

Pricing Model

{PRICING_MODELS.find((m) => m.value === terms.pricingModel)?.label ?? terms.pricingModel}

{terms.warrantyMonths > 0 && (

{terms.warrantyMonths} mo warranty

)}
{/* Terms editor */}

Commercial Terms

{canEdit && dirty && ( )}
{/* Pricing Model */}
{/* Contingency % */}
update({ contingencyPercent: parseFloat(e.target.value) || 0 }) } disabled={!canEdit} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tabular-nums disabled:bg-gray-50" />
{/* Discount % */}
update({ discountPercent: parseFloat(e.target.value) || 0 }) } disabled={!canEdit} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tabular-nums disabled:bg-gray-50" />
{/* Payment Terms */}
update({ paymentTermDays: parseInt(e.target.value) || 0 }) } disabled={!canEdit} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tabular-nums disabled:bg-gray-50" />
{/* Warranty */}
update({ warrantyMonths: parseInt(e.target.value) || 0 }) } disabled={!canEdit} className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tabular-nums disabled:bg-gray-50" />
{/* Notes */}