/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // JBREWS DMS — JBField primitives. // Real elements that: // - allow normal highlight/select-all/delete-digit behavior // - keep a "raw" text state while focused so partial edits don't get reformatted // - auto-persist to localStorage keyed by (formId, field) // - format $ on blur when type="money" // // Use JBNum / JBText / JBMoney everywhere a span "fipro-field" was used before. const _jbParseNum = (s) => { const cleaned = String(s ?? "").replace(/[^0-9.\-]/g, ""); const n = parseFloat(cleaned); return isNaN(n) ? 0 : n; }; const _jbFmtMoney = (n) => { const v = _jbParseNum(n); return v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; /* Focus-select policy (audit A-03 / R-8). Select-all ONLY when focus arrives via the keyboard (Tab / Shift-Tab) — dealers like tabbing into a box and overtyping. When focus arrives from a mouse / touch click the caret must stay exactly where the user clicked, so Backspace and Delete edit a single digit instead of wiping the field. pointerdown/mousedown fire BEFORE focus, so we stamp the element with the press time and onFocus skips select() when a press happened just before it (the window also covers touch, where focus can land after pointerup). A click on an ALREADY-focused box fires no focus event at all, so the caret simply lands where the user clicked (R-8) — and because the stamp is time-based, a later Tab back into the box still selects all. The stamp is cleared on blur so it can never leak into the next focus. */ const _JB_PRESS_WINDOW_MS = 600; const _jbNow = () => (typeof performance !== "undefined" && performance.now ? performance.now() : Date.now()); const _jbPointerDown = (e) => { const el = e.currentTarget || e.target; if (el) el.__jbPressAt = _jbNow(); }; const _jbPointerUp = (e) => { const el = e.currentTarget || e.target; if (el) el.__jbPressAt = 0; }; // used on blur only const _jbFocusSelect = (e) => { const el = e.target; if (!el) return; const pressedAt = el.__jbPressAt || 0; if (pressedAt && (_jbNow() - pressedAt) < _JB_PRESS_WINDOW_MS) return; // mouse / touch → keep caret if (typeof el.select === "function") el.select(); // keyboard → select all }; // Props spread onto every JBField/JBLive* . const _jbFocusProps = { onPointerDown: _jbPointerDown, onMouseDown: _jbPointerDown, }; /* Generic text/number input that persists to (formId, field). */ const JBField = ({ formId, field, type = "text", // "text" | "number" | "money" width = 110, align, hi, sel, defaultValue = "", placeholder = "", uppercase, mono, style, onValueChange, readOnly, }) => { const persistedDefault = React.useMemo(() => { if (!formId || !field) return defaultValue; const stored = window.loadJbForm(formId)[field]; return stored !== undefined && stored !== null ? String(stored) : String(defaultValue ?? ""); }, [formId, field]); const [val, setVal] = React.useState(persistedDefault); const [focused, setFocused] = React.useState(false); React.useEffect(() => { // sync when external default changes (rare) if (defaultValue !== undefined && defaultValue !== "" && !focused) { // do not stomp user-saved data on remount — persistedDefault already accounts } }, []); const persist = (v) => { if (formId && field) window.saveJbFormField(formId, field, v); onValueChange && onValueChange(v); }; const onChange = (e) => { let v = e.target.value; if (uppercase) v = v.toUpperCase(); setVal(v); persist(v); }; const onBlur = () => { setFocused(false); if (type === "money") { const s = val.trim(); if (s === "" || s === "-" || s === ".") return; const formatted = _jbFmtMoney(s); setVal(formatted); persist(formatted); } }; const baseAlign = align || (type === "money" || type === "number" ? "right" : "left"); return ( { setFocused(true); _jbFocusSelect(e); }} onBlur={(e) => { _jbPointerUp(e); onBlur(); }} className={ "jb-field fipro-field" + (hi ? " hi" : "") + (sel ? " selected" : "") + (baseAlign === "left" ? " text" : "") + (mono ? " mono" : "") } style={{ width: typeof width === "number" ? width : width, minWidth: typeof width === "number" ? width : undefined, textAlign: baseAlign, font: mono ? "13px 'Courier Prime', monospace" : undefined, ...style, }} /> ); }; /* Convenience wrappers */ const JBText = (p) => ; const JBNum = (p) => ; const JBMoney = (p) => ; /* A "controlled money input" for the desking 4-square — same UX (highlight/delete single digits) but parent controls the numeric value. Holds local text state while focused so .toFixed never stomps mid-edit. */ const JBLiveMoney = ({ value, onChange, width = 90, style }) => { const [text, setText] = React.useState(() => { const n = Number(value); return isNaN(n) ? "0.00" : n.toFixed(2); }); const [focused, setFocused] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { if (focused) return; // don't stomp while typing const n = Number(value); setText(isNaN(n) ? "" : n.toFixed(2)); }, [value, focused]); return ( { setFocused(true); _jbFocusSelect(e); }} onChange={(e) => { const v = e.target.value; setText(v); // Push numeric value upward only when parseable, so totals re-compute live const cleaned = v.replace(/[^0-9.\-]/g, ""); const n = parseFloat(cleaned); if (!isNaN(n)) onChange(n); else if (cleaned === "" || cleaned === "-" || cleaned === ".") onChange(0); }} onBlur={(e) => { _jbPointerUp(e); setFocused(false); const cleaned = text.replace(/[^0-9.\-]/g, ""); const n = parseFloat(cleaned); const finalN = isNaN(n) ? 0 : n; setText(finalN.toFixed(2)); onChange(finalN); }} style={{ width, textAlign: "right", background: "var(--k2b-yellow, #FFFFE1)", border: "1px solid #808080", fontFamily: "'Courier Prime', monospace", fontSize: 13, color: "var(--k2b-blue-data-dk, #11337A)", padding: "0 4px", ...style, }} /> ); }; const JBLiveInt = ({ value, onChange, width = 50, style }) => { const [text, setText] = React.useState(() => String(value ?? 0)); const [focused, setFocused] = React.useState(false); React.useEffect(() => { if (!focused) setText(String(value ?? 0)); }, [value, focused]); return ( { setFocused(true); _jbFocusSelect(e); }} onChange={(e) => { const v = e.target.value; setText(v); const n = parseInt(v.replace(/[^0-9\-]/g, ""), 10); if (!isNaN(n)) onChange(n); else if (v === "" || v === "-") onChange(0); }} onBlur={(e) => { _jbPointerUp(e); setFocused(false); const n = parseInt(text.replace(/[^0-9\-]/g, ""), 10); const f = isNaN(n) ? 0 : n; setText(String(f)); onChange(f); }} style={{ width, textAlign: "right", background: "var(--k2b-yellow, #FFFFE1)", border: "1px solid #808080", fontFamily: "'Courier Prime', monospace", fontSize: 13, padding: "0 4px", ...style, }} /> ); }; const JBLiveDec = ({ value, onChange, width = 60, style, decimals = 2 }) => { const fmt = (n) => { const v = Number(n); if (isNaN(v)) return ""; return decimals == null ? String(v) : v.toFixed(decimals); }; const [text, setText] = React.useState(() => fmt(value)); const [focused, setFocused] = React.useState(false); React.useEffect(() => { if (!focused) setText(fmt(value)); }, [value, focused]); return ( { setFocused(true); _jbFocusSelect(e); }} onChange={(e) => { const v = e.target.value; setText(v); const n = parseFloat(v.replace(/[^0-9.\-]/g, "")); if (!isNaN(n)) onChange(n); else if (v === "" || v === "-" || v === ".") onChange(0); }} onBlur={(e) => { _jbPointerUp(e); setFocused(false); const n = parseFloat(text.replace(/[^0-9.\-]/g, "")); const f = isNaN(n) ? 0 : n; setText(fmt(f)); onChange(f); }} style={{ width, textAlign: "right", background: "var(--k2b-yellow, #FFFFE1)", border: "1px solid #808080", fontFamily: "'Courier Prime', monospace", fontSize: 13, padding: "0 4px", ...style, }} /> ); }; Object.assign(window, { JBField, JBText, JBNum, JBMoney, JBLiveMoney, JBLiveInt, JBLiveDec, // Focus-select policy helpers — reusable by other screens' numeric inputs. jbFocusSelect: _jbFocusSelect, jbFocusProps: _jbFocusProps });