Trade values are set by a manager. Use Trade-In Requests to send a trade.
);
};
const TaAppraisalDesk = ({ onClose, go }) => {
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => DMS.subscribe(force), []);
const [veh, setVeh] = React.useState({ vin: "", year: "", make: "", model: "", trim: "", type: "", mileage: "", condition: "Clean", color: "", options: "" });
// Fields a person typed (or that arrived from another screen): a VIN decode never replaces them.
const taTyped = React.useRef(new Set());
const set = (k, v) => { taTyped.current.add(k); setVeh(s => ({ ...s, [k]: v })); };
// Typing / pasting a 17-character VIN fills the still-blank year / make / model / trim / body type / color from our server
// (MarketCheck -> NHTSA -> the VIN itself; window.DMS.vin). Undo puts the filled values back.
const vinFill = (typeof VlUseVinFill === "function") ? VlUseVinFill({
vin: veh.vin, keys: { year: "year", make: "make", model: "model", trim: "trim", type: "body", color: "exteriorColor" },
read: () => veh, write: (p) => setVeh(s => ({ ...s, ...p })), typed: () => taTyped.current, shape: { year: (n) => String(n) },
}) : null;
const [quotes, setQuotes] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [acv, setAcv] = React.useState("");
const [allowance, setAllowance] = React.useState("");
const [payoff, setPayoff] = React.useState("");
const [dealId, setDealId] = React.useState("");
const [showCfg, setShowCfg] = React.useState(false);
// CRM hand-off: `jb_appraisal_lead` = { leadId, custNum, name, year, make,
// model, vin, miles, payoff } written by the CRM's "Trade Appraisal ▸"
// (read-once, removed here). While linked, Save writes the ACV / allowance /
// payoff back onto that lead (and its deal, if it's already been desked).
const [lead, setLead] = React.useState(null);
const [savedToLead, setSavedToLead] = React.useState(false);
React.useEffect(() => {
let ptr = null;
try { const raw = localStorage.getItem("jb_appraisal_lead"); if (raw) { ptr = JSON.parse(raw); localStorage.removeItem("jb_appraisal_lead"); } } catch (e) {}
if (!ptr && window.jbAppraisalLead) ptr = window.jbAppraisalLead;
try { delete window.jbAppraisalLead; } catch (e) {}
if (!ptr || (!ptr.leadId && !ptr.vin)) return;
if (ptr.leadId) setLead(ptr); // VIN Decode's "Send to Appraisal" hands over a vehicle without a lead: just the vehicle
const pre = { year: String(ptr.year || ""), make: String(ptr.make || ""), model: String(ptr.model || ""), trim: String(ptr.trim || ""), vin: String(ptr.vin || "").toUpperCase(), mileage: String(ptr.miles || ""), color: String(ptr.color || "") };
Object.keys(pre).forEach(k => { if (pre[k]) taTyped.current.add(k); else delete pre[k]; });
setVeh(s => ({ ...s, ...pre }));
if (+ptr.payoff > 0) setPayoff(String(+ptr.payoff));
// the lead's existing deal (if desked) is the natural "attach to" target
if (DMS.crmLeads) { const l = (DMS.crmLeads() || []).find(x => x.id === ptr.leadId); if (l && l.dealId) setDealId(l.dealId); }
}, []);
// Write the saved appraisal back onto the linked CRM lead (+ its deal).
const writeBackToLead = (apprId) => {
if (!lead || !lead.leadId || !DMS.crmLeads || !DMS.crmSetLeads) return false;
const list = (DMS.crmLeads() || []).slice();
const i = list.findIndex(x => x.id === lead.leadId);
if (i < 0) { window.k2bToast && window.k2bToast("Lead " + lead.leadId + " is no longer in the CRM — appraisal saved, not linked.", "warn"); return false; }
const l = { ...list[i] };
const yr = String(veh.year || "").trim().toUpperCase(), mk = String(veh.make || "").trim().toUpperCase(), mdl = String(veh.model || "").trim().toUpperCase();
const desc = [yr, mk, mdl].filter(Boolean).join(" ");
const nAcv = +acv || 0, nAllow = +allowance || 0, nPay = +payoff || 0;
const now = new Date().toISOString();
Object.assign(l, { tradeYr: yr, tradeMk: mk, tradeMdl: mdl, tradeVin: String(veh.vin || "").toUpperCase(), tradeMiles: String(veh.mileage || "").replace(/[^0-9]/g, ""),
tradeAllow: nAllow, tradeAcv: nAcv, tradePayoff: nPay ? String(nPay) : (l.tradePayoff || ""), tradeApprId: apprId,
tradeIn: desc, tradeVehicle: desc, lastTouch: now });
// no ACV in lead text: the lead is the salesperson's (the ACV stays in tradeAcv, which the server hides from Sales)
l.activities = [{ kind: "note", body: `Appraised - allowance ${_taMoney(nAllow)} - payoff ${_taMoney(nPay)} (${apprId})`, at: now }, ...(l.activities || [])];
list[i] = l;
DMS.crmSetLeads(list);
if (l.dealId && DMS.updateDeal) {
DMS.updateDeal(l.dealId, { tradeDesc: desc, tradeAllow: nAllow, tradeACV: nAcv, tradeAcv: nAcv, tradePayoff: nPay, tradeVin: l.tradeVin, tradeMiles: +l.tradeMiles || 0, tradeApprId: apprId });
}
setSavedToLead(true);
window.k2bToast && window.k2bToast(`Saved to lead ${lead.name || lead.leadId} — Desk It will carry this trade.`, "ok", 4200);
return true;
};
const deals = (DMS.deals() || []).filter(d => d.buyer);
const recent = DMS.appraisals();
// Decode button: the real decoder (our server), again - the VIN box also decodes by itself once 17 characters are in.
const decodeVin = () => {
if (!vinFill) { window.k2bToast && window.k2bToast("VIN look-up is not loaded. Reload the page.", "warn"); return; }
if (!(window.jbVin && window.jbVin.isVin(veh.vin))) { window.k2bToast && window.k2bToast("Enter a full 17-character VIN to decode (no I, O or Q).", "warn"); return; }
vinFill.again();
};
const pull = async () => {
if (!veh.make && !veh.model) { window.k2bToast && window.k2bToast("Enter at least make & model (or decode a VIN).", "warn"); return; }
setBusy(true);
const q = await window.TradeConnectors.quoteAll(veh);
setQuotes(q);
const rec = window.TradeConnectors.recommendedACV(q);
setAcv(String(rec));
setAllowance(String(Math.round(rec * 1.08 / 25) * 25)); // typical over-allow vs ACV
setBusy(false);
};
const saveAppraisal = () => {
const r = DMS.addAppraisal({ ...veh, acv: +acv || 0, allowance: +allowance || 0, payoff: +payoff || 0,
quotes: quotes || {}, recommended: +acv || 0,
leadId: lead ? lead.leadId : "", custNum: lead ? lead.custNum || "" : "", leadName: lead ? lead.name || "" : "" });
window.k2bToast && window.k2bToast(r.msg, "ok");
if (r && r.ok && lead) writeBackToLead(r.id);
return r;
};
const applyToDeal = () => {
if (!dealId) { window.k2bToast && window.k2bToast("Choose a deal to attach this trade to.", "warn"); return; }
saveAppraisal();
const r = DMS.applyTradeToDeal(dealId, { allowance: +allowance || 0, acv: +acv || 0, payoff: +payoff || 0,
vehicle: [veh.year, veh.make, veh.model].filter(Boolean).join(" "), vin: veh.vin });
window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn");
if (r.ok && go) go("deal");
};
const provIds = window.TradeConnectors.providers.map(p => p.id);
const fld = { width: "100%", boxSizing: "border-box", padding: "6px 8px", fontSize: 13 };
const lab = { fontSize: 10.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--jb-text-muted)", marginBottom: 3, display: "block" };
return (
{lead && (
Appraising for lead {lead.name || lead.leadId}{lead.custNum ? ({lead.custNum}) : null}
{" — "}saving writes the ACV / allowance / payoff back to the CRM lead{savedToLead ? · saved ✓ : null}.
{savedToLead && }
)}
{/* Left — vehicle entry */}
Trade Vehicle
VIN
set("vin", e.target.value.toUpperCase().replace(/[\s-]+/g, ""))} placeholder="1G1ZD5ST…" title="Type or paste the 17-character VIN - it decodes as you type" />
{vinFill && typeof VlVinNote === "function" && }
Year set("year", e.target.value)} />
Make set("make", e.target.value)} />
Model set("model", e.target.value)} />
Trim set("trim", e.target.value)} />
Body type set("type", e.target.value)} placeholder="SUV, Pickup, Sedan…" />
{[a.year, a.make, a.model].filter(Boolean).join(" ")}{a.leadName ? · lead {a.leadName} : null}
{a.mileage ? a.mileage + " mi" : ""}
ACV {_taMoney(a.acv)}
))}
)}
ESC-Close go && go("desking")}>F2-Desking go && go("deal")}>F3-Deals go && go("crm")}>CRM — LeadsValuations are estimates until provider API keys are connected via the server tier.
Enter each provider's API key to enable live valuations. Until a key is set — and routed through the K2B server proxy that holds it securely — the desk uses a deterministic local estimate (marked simulated). Browsers can't safely hold these keys directly, so live calls go through your server.