/* ============================================================= CoreIQ Office — Floor & Shelf Planner: rail panels + scan modal Exposes window.PlannerRail and window.PlannerScanModal ============================================================= */ const { useState: prUseState, useMemo: prUseMemo, useEffect: prUseEffect, useRef: prUseRef } = React; // --------------------------------------------------------------- // Summary stat // --------------------------------------------------------------- function PlSum({ label, value, sub }) { return (
{label}
{value}
{sub &&
{sub}
}
); } // --------------------------------------------------------------- // Category tree (draggable) — Plan mode // --------------------------------------------------------------- function CategoryTree({ plan, storeId, basis, selBay, assignBay, setDragCat, dragCat }) { const placed = new Set(window.plannerPlanCats(plan)); const [open, setOpen] = prUseState(() => { const o = {}; window.OFFICE_DATA.CATEGORIES.forEach(d => { o[d.id] = true; }); return o; }); function onChipClick(catId) { if (selBay) assignBay(selBay.fixtureId, selBay.bayId, catId); } return (
{window.OFFICE_DATA.CATEGORIES.map(dept => { const color = window.plannerDeptColor(dept.id, 1); return (
setOpen(o => ({ ...o, [dept.id]: !o[dept.id] }))}> {dept.name}
{open[dept.id] && (
{dept.cats.map(cat => { const sku = window.plannerCatSkuCount(cat.id, storeId); const isPlaced = placed.has(cat.id); return (
{ e.dataTransfer.effectAllowed = "copy"; e.dataTransfer.setData("text/plain", cat.id); setDragCat(cat.id); }} onDragEnd={() => setDragCat(null)} onClick={() => onChipClick(cat.id)} title={selBay ? "Click to assign to selected bay" : "Drag onto a bay"} > {cat.name} {sku} SKU{isPlaced ? " · placed" : ""}
); })}
)}
); })}
); } // --------------------------------------------------------------- // Bay inspector — Plan mode // --------------------------------------------------------------- function BayInspector({ plan, storeId, basis, selBay, clearBay, money }) { const fixture = selBay && plan.fixtures.find(f => f.id === selBay.fixtureId); const bay = fixture && (fixture.bays || []).find(b => b.id === selBay.bayId); if (!bay) { return (
No bay selected
Click a bay on the floor plan, then drag or click a category to assign it.
); } const catId = bay.cat; const dept = catId ? window.plannerCatDept(catId) : null; const s2s = catId ? window.plannerSpaceToSales(plan, catId, "week") : null; const value = catId ? window.plannerCatInventoryValue(catId, storeId, basis) : 0; const sku = catId ? window.plannerCatSkuCount(catId, storeId) : 0; const sales = catId ? window.plannerCatSales(catId, storeId, "week") : 0; return (
{catId ? window.plannerCatName(catId) : "Empty bay"}
{fixture.label} · bay {(fixture.bays.indexOf(bay) + 1)}
Linear space{bay.m.toFixed(2)} m
{catId &&
Share of floor{(s2s.shareSpace * 100).toFixed(1)}%
} {catId &&
Inventory ({basis}){money(value)}
} {catId &&
SKUs stocked{sku}
} {catId &&
Sales · week{money(sales)}
} {catId &&
Space-to-sales{s2s.index.toFixed(2)}×
}
{catId ? :
This bay is empty. Drag a category from the list, or click one while this bay is selected.
}
); } // --------------------------------------------------------------- // Performance breakdown — Performance mode // --------------------------------------------------------------- function PerfPanel({ plan, storeId, period, metric, basis, setBasis, money }) { const cats = window.plannerPlanCats(plan); const PERIOD_LABEL = window.PLANNER_DATA.PERIOD_LABEL[period]; const rows = prUseMemo(() => cats.map(c => { const s2s = window.plannerSpaceToSales(plan, c, period); return { cat: c, sales: window.plannerCatSales(c, storeId, period), value: window.plannerCatInventoryValue(c, storeId, basis), units: window.plannerCatUnits(c, storeId, period), gp: window.plannerCatGP(c, storeId, period), delta: window.plannerCatDelta(c, storeId, period), idx: s2s.index, shareSales: s2s.shareSales, shareSpace: s2s.shareSpace, }; }).sort((a, b) => a.idx - b.idx), [plan, storeId, period, metric, basis]); const totals = prUseMemo(() => ({ sales: rows.reduce((s, r) => s + r.sales, 0), gp: rows.reduce((s, r) => s + r.gp, 0), value: rows.reduce((s, r) => s + r.value, 0), }), [rows]); const maxShare = Math.max(...rows.map(r => Math.max(r.shareSales, r.shareSpace)), 0.01); function idxClass(idx) { return idx >= 1.15 ? "pl-idx--under" : idx <= 0.85 ? "pl-idx--over" : "pl-idx--ok"; } function idxLabel(idx) { return idx >= 1.15 ? "grow" : idx <= 0.85 ? "trim" : "ok"; } // movement — top 4 movers + decliners by delta const movers = prUseMemo(() => [...rows].sort((a, b) => b.delta - a.delta), [rows]); return ( <>
Totals · {PERIOD_LABEL}
Value basis
{["cost", "retail"].map(b => ( ))}
{/* legend */}
Heatmap · {window.PLANNER_DATA.METRICS.find(m => m.key === metric).label}
{metric === "s2s" ? (
Over-spacedBalancedUnder-spaced
Space-to-sales = share of sales ÷ share of floor. Green earns more than its footprint (give it room); red takes more room than it earns (trim).
) : (
LowHigh
Each bay shaded by its {window.PLANNER_DATA.METRICS.find(m => m.key === metric).label.toLowerCase()} {PERIOD_LABEL}.
)}
{/* breakdown table */}
By category{rows.length}
Category · space vs sales Sales S2S
{rows.map(r => (
{window.plannerCatName(r.cat)}
{money(r.sales)} {r.idx.toFixed(2)}×
))}
Space Sales
{/* movement */}
Movement · {PERIOD_LABEL} vs prior
{movers.slice(0, 5).map(r => { const up = r.delta >= 0; const trend = window.plannerCatTrend(r.cat, storeId, period); const tmax = Math.max(...trend, 1); return (
{window.plannerCatName(r.cat)}
{trend.map((v, i) =>
)}
{Math.abs(r.delta * 100).toFixed(0)}%
); })}
); } // --------------------------------------------------------------- // RAIL // --------------------------------------------------------------- function PlannerRail(props) { const { plan, storeId, mode, period, metric, basis, setBasis, showLabels, setShowLabels, selBay, assignBay, clearBay, dragCat, setDragCat, money } = props; if (mode === "perf") { return ( ); } // PLAN mode const totalM = window.plannerTotalLinearM(plan); const allBays = window.plannerAllBays(plan); const empties = allBays.filter(b => !b.cat).length; const cats = window.plannerPlanCats(plan); const allocValue = cats.reduce((s, c) => s + window.plannerCatInventoryValue(c, storeId, basis), 0); const assignedBays = allBays.filter(b => b.cat).length; return ( ); } // --------------------------------------------------------------- // ROOMPLAN SCAN MODAL — staged "scan arriving from iPhone" // --------------------------------------------------------------- function PlannerScanModal({ store, plan, onClose, onApply }) { const [stage, setStage] = prUseState(0); // 0 connect · 1 scan · 2 walls · 3 fixtures · 4 done const [pct, setPct] = prUseState(0); const [shownFix, setShownFix] = prUseState(0); // fit plan geometry into the preview stage const SW = 460, SH = 240, pad = 16; const k = Math.min((SW - pad * 2) / plan.room.w, (SH - pad * 2) / plan.room.h); const offX = (SW - plan.room.w * k) / 2, offY = (SH - plan.room.h * k) / 2; prUseEffect(() => { const timers = []; timers.push(setTimeout(() => setStage(1), 700)); // progress during scan let p = 0; const prog = setInterval(() => { p = Math.min(100, p + 4); setPct(p); }, 70); timers.push(setTimeout(() => { clearInterval(prog); setPct(100); setStage(2); }, 2600)); timers.push(setTimeout(() => setStage(3), 4000)); return () => { timers.forEach(clearTimeout); clearInterval(prog); }; }, []); // reveal fixtures one-by-one during stage 3 prUseEffect(() => { if (stage !== 3) return; let i = 0; const id = setInterval(() => { i++; setShownFix(i); if (i >= plan.fixtures.length) { clearInterval(id); setTimeout(() => setStage(4), 400); } }, 110); return () => clearInterval(id); }, [stage]); const statusText = { 0: "Connecting to iPhone…", 1: `Capturing room geometry — ${pct}%`, 2: "Reconstructing walls & openings…", 3: `Detecting fixtures — ${shownFix}/${plan.fixtures.length}`, 4: "Plan ready", }[stage]; return (
e.stopPropagation()}>
Import scan · Apple RoomPlan
{store.name} · {store.address.split(",")[0]}
{stage <= 1 && <>
} {stage >= 2 && (
{stage >= 3 && plan.fixtures.slice(0, shownFix).map(f => ( ))}
)}
{stage < 4 ? : } {statusText}
Area
{plan.meta.area} m²
Fixtures
{stage >= 3 ? shownFix : 0}
Bays
{stage >= 4 ? plan.fixtures.reduce((s, f) => s + (f.bays || []).length, 0) : 0}
); } window.PlannerRail = PlannerRail; window.PlannerScanModal = PlannerScanModal;