/* 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 });