/* ============================================================= CoreIQ Office — Floor & Shelf Planner (main screen) Registers window.OFFICE_SCREENS.planner Rail + scan modal live in office-screens-planner-panels.jsx (referenced via window.PlannerRail / window.PlannerScanModal). ============================================================= */ const { useState: plUseState, useMemo: plUseMemo, useRef: plUseRef, useEffect: plUseEffect } = React; const PL = window.PLANNER_DATA; const SCALE = 56; // px per metre (top-down) // ---- heat colour helpers -------------------------------------- function lerp(a, b, t) { return a + (b - a) * t; } function clamp01(x) { return Math.max(0, Math.min(1, x)); } // sequential: light slate → teal function heatSeq(t) { t = clamp01(t); const h = lerp(210, 186, t), s = lerp(34, 68, t), l = lerp(86, 40, t); return `hsl(${h}, ${s}%, ${l}%)`; } // diverging for space-to-sales: <1 over-spaced (rose) · ~1 balanced (slate) · >1 under-spaced (green) function heatS2S(idx) { if (idx >= 1) { const t = clamp01((idx - 1) / 0.8); // 1 → 1.8 return `hsl(${lerp(205, 152, t)}, ${lerp(20, 55, t)}%, ${lerp(72, 42, t)}%)`; } const t = clamp01((1 - idx) / 0.6); // 1 → 0.4 return `hsl(${lerp(205, 350, t)}, ${lerp(20, 68, t)}%, ${lerp(72, 55, t)}%)`; } // per-bay metric value (category metric attributed to the bay by linear share) function bayMetricValue(plan, bay, metric, period, basis) { if (!bay.cat) return null; const storeId = plan.storeId; if (metric === "s2s") return window.plannerSpaceToSales(plan, bay.cat, period).index; const catM = window.plannerCatLinearM(plan, bay.cat) || bay.m || 1; const share = (bay.m || 0) / catM; if (metric === "sales") return window.plannerCatSales(bay.cat, storeId, period) * share; if (metric === "value") return window.plannerCatInventoryValue(bay.cat, storeId, basis) * share; if (metric === "units") return window.plannerCatUnits(bay.cat, storeId, period) * share; if (metric === "gp") return window.plannerCatGP(bay.cat, storeId, period) * share; return null; } function fmtMetric(metric, v, money) { if (v == null) return "—"; if (metric === "s2s") return v.toFixed(2) + "×"; if (metric === "units") return Math.round(v).toLocaleString(); return money(v); } // ---- single fixture ------------------------------------------ function Fixture({ plan, fixture, ctx }) { const { mode, metric, period, basis, showLabels, selBay, onSelectBay, dragCat, dropBay, setDropBay, onDrop, range, money } = ctx; const vertical = fixture.h >= fixture.w; const isService = !(window.PLANNER_DATA.ASSIGNABLE_TYPES.includes(fixture.type)) || (fixture.bays || []).length === 0; const style = { left: fixture.x * SCALE, top: fixture.y * SCALE, width: fixture.w * SCALE, height: fixture.h * SCALE, }; if (isService) { return (
{fixture.label}
); } return (
{(fixture.bays || []).map(bay => { const selected = selBay && selBay.fixtureId === fixture.id && selBay.bayId === bay.id; const isDrop = dropBay && dropBay.fixtureId === fixture.id && dropBay.bayId === bay.id; let bg, tag = null, content = null; if (!bay.cat) { // empty content = ; } else if (mode === "plan") { bg = window.plannerDeptColor(window.plannerCatDept(bay.cat), 0.92); tag = {window.plannerCatName(bay.cat)}; } else { const v = bayMetricValue(plan, bay, metric, period, basis); if (metric === "s2s") bg = heatS2S(v); else { const t = range.max > range.min ? (v - range.min) / (range.max - range.min) : 0.5; bg = heatSeq(t); } tag = {fmtMetric(metric, v, money)}; } return (
onSelectBay(fixture.id, bay.id)} onDragOver={dragCat ? (e) => { e.preventDefault(); setDropBay({ fixtureId: fixture.id, bayId: bay.id }); } : undefined} onDragLeave={dragCat ? () => setDropBay(null) : undefined} onDrop={dragCat ? (e) => { e.preventDefault(); onDrop(fixture.id, bay.id); } : undefined} title={bay.cat ? window.plannerCatName(bay.cat) : "Empty bay — assign a category"} > {content}{tag}
); })} {showLabels && mode === "plan" && (
{fixture.label}
)}
); } // ---- the floor plan canvas ----------------------------------- function FloorPlan({ plan, ctx }) { const { view } = ctx; const W = plan.room.w * SCALE, H = plan.room.h * SCALE; return (
{plan.zones.map(z => (
{z.label}
))} {/* entrance */} {plan.room.entrance && ( <>
Entrance
)} {plan.fixtures.map(f => )}
); } // ---- segmented control ---------------------------------------- function Seg({ value, onChange, options, accent }) { return (
{options.map(o => ( ))}
); } // ================================================================= // PLANNER SCREEN // ================================================================= function FloorPlanner() { const { scope, pushToast } = window.useOffice(); const initStore = (scope && scope !== "all") ? scope : "S-001"; const [storeId, setStoreId] = plUseState(initStore); const [mode, setMode] = plUseState("plan"); // plan | perf const [period, setPeriod] = plUseState("week"); const [view, setView] = plUseState("top"); // top | 3d const [metric, setMetric] = plUseState("s2s"); const [basis, setBasis] = plUseState("cost"); // cost | retail const [showLabels, setShowLabels] = plUseState(true); const [selBay, setSelBay] = plUseState(null); const [dragCat, setDragCat] = plUseState(null); const [dropBay, setDropBay] = plUseState(null); const [scanOpen, setScanOpen] = plUseState(false); // mutable copies of all plans const [plans, setPlans] = plUseState(() => JSON.parse(JSON.stringify(PL.PLANS))); const plan = plans[storeId]; const money = window.officeMoney; // expose tweak hooks (read external tweak state if present) plUseEffect(() => { const t = window.__plannerTweaks; if (!t) return; if (t.metric) setMetric(t.metric); if (t.period) setPeriod(t.period); if (t.basis) setBasis(t.basis); if (typeof t.showLabels === "boolean") setShowLabels(t.showLabels); }, []); function assignBay(fixtureId, bayId, catId) { setPlans(prev => { const next = { ...prev, [storeId]: JSON.parse(JSON.stringify(prev[storeId])) }; const f = next[storeId].fixtures.find(x => x.id === fixtureId); const b = f && f.bays.find(x => x.id === bayId); if (b) b.cat = catId; return next; }); setDropBay(null); setDragCat(null); pushToast({ kind: "paid", icon: "check-circle", title: `${window.plannerCatName(catId)} assigned`, meta: "Bay updated · plan saved" }); } function clearBay(fixtureId, bayId) { setPlans(prev => { const next = { ...prev, [storeId]: JSON.parse(JSON.stringify(prev[storeId])) }; const f = next[storeId].fixtures.find(x => x.id === fixtureId); const b = f && f.bays.find(x => x.id === bayId); if (b) b.cat = null; return next; }); } function onSelectBay(fixtureId, bayId) { // click-to-assign: if dragging not active but a category is "armed" via selBay+chip, handled in rail. setSelBay({ fixtureId, bayId }); } // metric range across bays (perf mode shading) const range = plUseMemo(() => { if (mode !== "perf" || metric === "s2s") return { min: 0, max: 1 }; let min = Infinity, max = -Infinity; window.plannerAllBays(plan).forEach(b => { const v = bayMetricValue(plan, b, metric, period, basis); if (v == null) return; if (v < min) min = v; if (v > max) max = v; }); return min === Infinity ? { min: 0, max: 1 } : { min, max }; }, [plan, mode, metric, period, basis]); const ctx = { mode, metric, period, basis, showLabels, view, selBay, onSelectBay, dragCat, dropBay, setDropBay, onDrop: (fid, bid) => dragCat && assignBay(fid, bid, dragCat), range, money, }; const store = window.findOfficeStore(storeId); return (
Stock · Space management

Floor & Shelf Planner

{plan.meta.scanned}
{/* toolbar */}
{window.OFFICE_DATA.STORES.map(s => ( ))}
{mode === "perf" && (
Heatmap
)} {(mode === "perf" || true) && (
Period
)}
{/* body */}
{window.PlannerRail ? :
}
{scanOpen && window.PlannerScanModal && ( setScanOpen(false)} onApply={() => { setScanOpen(false); pushToast({ kind: "paid", icon: "check-circle", title: "Floor plan imported", meta: `${plan.fixtures.length} fixtures detected · ${store.name}` }); }}/> )}
); } window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, { planner: FloorPlanner }); // shared for the rail file window.PlannerShared = { SCALE, heatSeq, heatS2S, bayMetricValue, fmtMetric };