/* © 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. */ /* eslint-disable */ // K2B DMS — Owner Panel > Stores (RC35, lane brd; SPEC 4.5). The owner only (the route already is). Without a fresh code: the list. // + Add a store [data-store-form="new"] a blank store: names, address, phone, license, the chart choice (required - no default), // GM franchise items (off), first RO number, tax rates (+ the required tick), two colors with a // live preview of the stripe and of the print header, a logo, who may use it. // Edit branding [data-store-form=""] the same fields + the document header switches + the check header ("Changes every printout") // Logo [data-store-logo] PNG / JPG / SVG, at most 1 MB (checked here first, the server checks again) // Who can use it [data-store-access=""] one row per login: allowed (home and owner fixed) // Options GM franchise items, kind, first RO number, the first store's integration keys, turn off / on // The SERVER decides every rule (owner + fresh code + audited; routes /api/owner/stores...). Store names are text typed by the owner: React // prints them as text; the print preview goes through K2BPrint.esc. const osApi = async (path, method, body, headers) => { try { const d = await apiFetch("/owner" + path, { method: method || "GET", body, headers, noOwnerOverride: true }); return { ok: true, data: d || {} }; } catch (e) { return { ok: false, status: e.status || 0, data: e.body || {}, code: e.body && e.body.code, error: (e.body && e.body.error) || e.message || "Server unreachable" }; } }; const OS_STATES = ["AL","AK","AZ","AR","CA","CO","CT","DE","DC","FL","GA","HI","ID","IL","IN","IA","KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT","VA","WA","WV","WI","WY"]; const OS_STATE_NAME = { AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas", CA: "California", CO: "Colorado", CT: "Connecticut", DE: "Delaware", DC: "District of Columbia", FL: "Florida", GA: "Georgia", HI: "Hawaii", ID: "Idaho", IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas", KY: "Kentucky", LA: "Louisiana", ME: "Maine", MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi", MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey", NM: "New Mexico", NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio", OK: "Oklahoma", OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island", SC: "South Carolina", SD: "South Dakota", TN: "Tennessee", TX: "Texas", UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington", WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming" }; const osInitials = (s) => String(s || "").split(/\s+/).filter((w) => /^[A-Za-z0-9&]/.test(w)).map((w) => w[0].toUpperCase()).join("").replace(/[^A-Z0-9&]/g, "").slice(0, 3); const osIsMobile = () => (window.innerWidth || 1000) < 768; const OS_INPUT = { padding: "7px 9px", border: "1px solid #c0c8d4", fontSize: 13.5, fontFamily: "var(--font-ui)", borderRadius: 6, width: "100%", boxSizing: "border-box", minHeight: 36 }; const OS_LBL = { fontSize: 11.5, color: "#555", fontWeight: 700, display: "block", marginBottom: 3 }; const osKey = () => "store-create-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10); /* the modal: a centered card on a laptop, the whole screen on a phone */ const OsModal = ({ attrs, title, onClose, children, buttons, wide }) => { React.useEffect(() => { const k = (e) => { if (e.key === "Escape") onClose && onClose(); }; document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, [onClose]); return ReactDOM.createPortal(
{title}
{children}
{buttons ?
{buttons}
: null}
, document.body); }; /* ── the logo: client checks first (size, real PNG / JPEG / SVG), preview 22 / 38 / 110, upload as { mime, dataBase64 } ── */ const osReadLogo = (file) => new Promise((resolve) => { if (!file) return resolve({ ok: false, error: "Pick a file." }); if (file.size > 1048576) return resolve({ ok: false, error: "That logo is too large. The limit is 1 MB." }); const fr = new FileReader(); fr.onerror = () => resolve({ ok: false, error: "That file could not be read." }); fr.onload = () => { const bytes = new Uint8Array(fr.result); const png = bytes.length > 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47; const jpg = bytes.length > 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff; let svg = false, dtd = false; if (!png && !jpg) { try { svg = /^\s*(<\?xml[^>]*>\s*)?(\s*)*]/i.test(new TextDecoder().decode(bytes.slice(0, 4096))); } catch (e) { svg = false; } } // RC35 fix round (SPEC-CHANGES stc 18:20 item 1): an SVG with a DOCTYPE / ENTITY line (before or anywhere in it) is refused with // the server's own reason - not the general "Pick a PNG, JPG or SVG picture." - and is never previewed or sent if (!png && !jpg) { try { const t = new TextDecoder().decode(bytes); dtd = /^\s*(<\?xml|