/* =============================================================
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)}
{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 < 4 ? : }
{statusText}
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;