/* =============================================================
CoreIQ Office — Stock Line Editor
Replaces the old "New stock line" form with a comprehensive
tabbed editor that doubles as the Edit view for existing lines.
Tabs: Details · Pricing · Suppliers (PDEs) · Barcodes · Promotions
· Printing · History · Movements · Notes · Multi-store
============================================================= */
const { useState: sleUseState, useMemo: sleUseMemo } = React;
const SLE_TABS = [
{ id: "details", label: "Details" },
{ id: "pricing", label: "Pricing" },
{ id: "suppliers", label: "Suppliers (PDEs)" },
{ id: "barcodes", label: "Barcodes" },
{ id: "promos", label: "Promotions & Clubs" },
{ id: "printing", label: "Printing" },
{ id: "multistore", label: "Multi-store" },
{ id: "movements", label: "Movements" },
{ id: "history", label: "History" },
{ id: "notes", label: "Notes" },
];
function StockLineEditor() {
const { setScreen, screenState, pushToast } = window.useOffice();
const editing = screenState.activeProductId
? OD.PRODUCTS.find(p => p.id === screenState.activeProductId)
: null;
const isNew = !editing;
// ---------------------------------------------------------------
// Form state — seed from an existing product or sensible defaults.
// Field names mirror Minfos/FRED conventions where reasonable so
// the model maps cleanly to migration.
// ---------------------------------------------------------------
const [form, setForm] = sleUseState(() => sleSeedForm(editing));
const [tab, setTab] = sleUseState("details");
const [dirty, setDirty] = sleUseState(false);
function set(patch) { setForm(f => ({ ...f, ...patch })); setDirty(true); }
function setField(k, v) { set({ [k]: v }); }
// Derived pricing
const costEx = parseFloat(form.costEx) || 0;
const supplierEx = parseFloat(form.supplierEx) || 0;
const taxRate = form.taxRate || 0;
const costInc = costEx * (1 + taxRate);
const sellEx = parseFloat(form.sellEx) || 0;
const sellInc = sellEx * (1 + taxRate);
const gstAmount = sellInc - sellEx;
const markup = costEx > 0 ? ((sellEx - costEx) / costEx) * 100 : 0;
const gp = sellEx > 0 ? ((sellEx - costEx) / sellEx) * 100 : 0;
// Validation
const valid =
!!form.name &&
!!form.sku &&
!!form.dept &&
!!form.sub &&
sellEx > 0;
function save() {
pushToast({
kind: "paid",
icon: "check-circle",
title: `${isNew ? "Stock line created" : "Stock line saved"} · ${form.name}`,
meta: `SKU ${form.sku} · ${oMoney(sellEx)} ex GST`,
});
setDirty(false);
setScreen(isNew ? "stockOverview" : "productDetail");
}
function cancel() {
setScreen(isNew ? "stockOverview" : "productDetail");
}
return (
{/* Tab strip */}
{tab === "details" &&
}
{tab === "pricing" && }
{tab === "suppliers" && }
{tab === "barcodes" && }
{tab === "promos" && }
{tab === "printing" && }
{tab === "multistore" && }
{tab === "movements" && }
{tab === "history" && }
{tab === "notes" && }
);
}
// =================================================================
// SEED — turn an existing product into a full editor form, or
// produce sensible defaults for a new line.
// =================================================================
function sleSeedForm(p) {
if (!p) {
return {
// Identity
name: "", fullName: "", sku: "",
strength: "", pack: "",
// Classification
dept: "", cat: "", sub: "", manufacturer: "",
schedule: "—", stockGroups: [], locations: [],
// Stock rules
cartonSize: 1, minOrdCartons: 1, minStock: 0, maxStock: 0,
// Flags
active: true, autoOrder: true, orderOnlyBelowMin: false,
ethical: false, discountable: true, authRequired: false,
bulkFood: false, custDisplay: true, inRobot: false, online: false,
controlled: false, coldChain: false, ndss: false, ageVerify: false, counsel: false,
// Pricing
costEx: "", supplierEx: "", avgCost: "", prevCost: "",
sellEx: "", rrp: "",
taxRate: 0.10, gstFlag: "Y - Taxable",
pricingPolicy: "Standard", roundingPolicy: "Rounding None",
expectedDiscount: 0,
// Lists
suppliers: [],
barcodes: [],
clubs: [],
promos: [],
// Multi-store
storeStock: OD.STORES.reduce((a, s) => ({...a, [s.id]: { onHand:0, min:0, max:0, location:"", active:true }}), {}),
// Printing
labelTemplate: "shelf-50x30",
printPrice: true, printBarcode: true, printStrength: true,
// Notes
internalNote: "", customerNote: "", clinicalNote: "",
};
}
// From a real product (synthetic but plausible PDEs / barcodes)
const seed = (p.id || "").split("").reduce((a, c) => a + c.charCodeAt(0), 0);
const taxRate = p.taxRate || 0;
const supplierEx = +(p.cost * 1.08).toFixed(2);
return {
name: p.name,
fullName: `${p.name} ${p.strength !== "—" ? p.strength : ""} ${p.pack}`.trim(),
sku: p.sku,
strength: p.strength === "—" ? "" : p.strength,
pack: p.pack,
dept: p.dept, cat: p.cat, sub: p.sub,
manufacturer: ["GSK","Pfizer","Sanofi","Bayer","AstraZeneca","Mylan","Sandoz","Apotex"][seed % 8],
schedule: p.schedule,
stockGroups: p.controlled ? ["S8 register"] : p.pbs ? ["PBS"] : [],
locations: p.controlled ? ["S8 Safe"] : ["Main shelf"],
cartonSize: [1,1,6,12][seed % 4],
minOrdCartons: 1,
minStock: 0, maxStock: 0,
active: true, autoOrder: true, orderOnlyBelowMin: false,
ethical: false, discountable: !p.pbs, authRequired: !!p.controlled || !!p.ageVerify,
bulkFood: false, custDisplay: true, inRobot: false, online: !p.controlled,
controlled: !!p.controlled, coldChain: !!p.coldChain, ndss: !!p.ndss,
ageVerify: !!p.ageVerify, counsel: !!p.controlled,
costEx: p.cost.toFixed(2),
supplierEx: supplierEx.toFixed(2),
avgCost: p.cost.toFixed(2),
prevCost: (p.cost * 0.96).toFixed(2),
sellEx: p.price.toFixed(2),
rrp: (p.price * 1.05).toFixed(2),
taxRate,
gstFlag: taxRate > 0 ? "Y - Taxable" : "N - Free to Customers",
pricingPolicy: p.pbs ? "PBS" : "Standard",
roundingPolicy: "Rounding None",
expectedDiscount: 0,
suppliers: sleSeedSuppliers(p, seed),
barcodes: [{ code: p.sku, default: true, type: "EAN-13" }],
clubs: p.pbs ? [{ name: "Concession", ratio: "1.0", fixed: "" }] : [],
promos: [],
storeStock: OD.STORES.reduce((a, s) => {
const st = OD.STOCK[s.id]?.[p.id];
return { ...a, [s.id]: {
onHand: st?.onHand || 0,
min: st?.min || 0,
max: st?.max || 0,
location: st?.location || "",
active: !!st,
}};
}, {}),
labelTemplate: "shelf-50x30",
printPrice: true, printBarcode: true, printStrength: p.strength !== "—",
internalNote: "", customerNote: "", clinicalNote: "",
};
}
function sleSeedSuppliers(p, seed) {
const sups = OD.SUPPLIERS.slice(0, 4);
return sups.map((s, i) => ({
pde: (1000000 + (seed * 31 + i * 117) % 9000000).toString(),
supplierId: s.id,
description: p.name.toUpperCase().slice(0, 32),
moq: i === 0 ? "" : "1",
cart: i === 0 ? "" : "1",
ws1: (p.cost * (0.95 + i * 0.05)).toFixed(2),
default: i === Math.min(1, sups.length - 1),
}));
}
// =================================================================
// DETAILS TAB — identity, classification, stock rules, flags
// =================================================================
function DetailsTab({ form, set, setField }) {
return (
{/* Basic details */}
setField("name", e.target.value)} placeholder="e.g. Panadol Liquid Caps 16s"/>
setField("fullName", e.target.value)} placeholder="Long form including strength + pack"/>
setField("strength", e.target.value)} placeholder="500mg"/>
setField("pack", e.target.value)} placeholder="16 capsules"/>
setField("sku", e.target.value)} placeholder="13-digit barcode"/>
setField("manufacturer", e.target.value)} placeholder="e.g. GSK"/>
{/* Classification */}
setField("stockGroups", e.target.value.split(",").map(x => x.trim()).filter(Boolean))} placeholder="e.g. PBS, Webster eligible"/>
setField("locations", e.target.value.split(",").map(x => x.trim()).filter(Boolean))} placeholder="e.g. Main shelf, S8 Safe"/>
{form.dept && form.cat && form.sub && (
{OD.DEPT_BY_ID[form.dept]?.name} › {OD.CAT_BY_ID[form.cat]?.name} › {OD.SUB_BY_ID[form.sub]?.name}
)}
{/* Stock rules */}
{/* Behaviour toggles */}
{/* Clinical flags */}
);
}
// =================================================================
// PRICING TAB — full cost ladder + sell prices + GST + policies
// =================================================================
function PricingTab({ form, set, setField, costEx, costInc, sellEx, sellInc, gstAmount, markup, gp, supplierEx }) {
return (
Margin summary
60 ? "good" : null}/>
40 ? "good" : null}/>
);
}
// =================================================================
// SUPPLIERS / PDEs TAB
// Pharmacy Data Exchange codes — multiple per stock line
// =================================================================
function SuppliersTab({ form, set }) {
function update(i, patch) {
const next = form.suppliers.map((s, j) => j === i ? { ...s, ...patch } : (patch.default ? { ...s, default: false } : s));
set({ suppliers: next });
}
function add() {
set({ suppliers: [...form.suppliers, { pde: "", supplierId: OD.SUPPLIERS[0]?.id, description: form.name, moq: "", cart: "", ws1: "", default: form.suppliers.length === 0 }] });
}
function remove(i) {
set({ suppliers: form.suppliers.filter((_, j) => j !== i) });
}
return (
PDE #
Supplier
Description
MOQ
Cart
WS1 ($)
Default
{form.suppliers.map((s, i) => (
))}
{form.suppliers.length === 0 && (
No supplier codes yet. Add one to enable ordering.
)}
);
}
// =================================================================
// BARCODES TAB
// =================================================================
function BarcodesTab({ form, set }) {
function update(i, patch) {
const next = form.barcodes.map((b, j) => j === i ? { ...b, ...patch } : (patch.default ? { ...b, default: false } : b));
set({ barcodes: next });
}
function add() {
set({ barcodes: [...form.barcodes, { code: "", default: form.barcodes.length === 0, type: "EAN-13" }] });
}
function remove(i) {
set({ barcodes: form.barcodes.filter((_, j) => j !== i) });
}
return (
Barcode
Type
Default
{form.barcodes.map((b, i) => (
))}
{form.barcodes.length === 0 &&
No barcodes registered.
}
);
}
// =================================================================
// PROMOTIONS & CLUBS TAB
// =================================================================
function PromosTab({ form, set }) {
function addClub() { set({ clubs: [...form.clubs, { name: "", ratio: "1.0", fixed: "" }] }); }
function updateClub(i, patch) { set({ clubs: form.clubs.map((c, j) => j === i ? { ...c, ...patch } : c) }); }
function removeClub(i) { set({ clubs: form.clubs.filter((_, j) => j !== i) }); }
function addPromo() {
const today = "28/05/2026";
set({ promos: [...form.promos, { description: "", type: "% off", start: today, end: "" }] });
}
function updatePromo(i, patch) { set({ promos: form.promos.map((p, j) => j === i ? { ...p, ...patch } : p) }); }
function removePromo(i) { set({ promos: form.promos.filter((_, j) => j !== i) }); }
return (
Club name
Ratio
Fixed $
{form.clubs.map((c, i) => (
))}
{form.clubs.length === 0 &&
No club pricing rules.
}
Description
Type
Start
End
{form.promos.map((p, i) => (
))}
{form.promos.length === 0 &&
No active promotions.
}
);
}
// =================================================================
// PRINTING TAB
// =================================================================
function PrintingTab({ form, setField }) {
return (
Label preview
{form.name || "Product name"}
{form.printStrength && form.strength &&
{form.strength} · {form.pack}
}
{!form.printStrength && form.pack &&
{form.pack}
}
{form.printBarcode &&
}
{form.printPrice &&
${(parseFloat(form.sellEx) * (1 + form.taxRate) || 0).toFixed(2)}
}
{form.printBarcode && form.sku &&
{form.sku}
}
);
}
// =================================================================
// MULTI-STORE TAB
// =================================================================
function MultiStoreTab({ form, set }) {
function updateStore(id, patch) {
set({ storeStock: { ...form.storeStock, [id]: { ...form.storeStock[id], ...patch } } });
}
return (
Store
Active
On hand
Min
Max
Location
{OD.STORES.map(s => {
const v = form.storeStock[s.id];
return (
);
})}
);
}
// =================================================================
// MOVEMENTS TAB — read-only ledger
// =================================================================
function MovementsTab({ editing }) {
const items = [
{ date: "28/05/2026 14:32", kind: "Sale", qty: -1, bal: 8, ref: "Sale 1042831", user: "Sarah K." },
{ date: "28/05/2026 09:01", kind: "Receipt", qty: +30, bal: 9, ref: "PO 4421", user: "Auto" },
{ date: "27/05/2026 17:48", kind: "Sale", qty: -2, bal: -21, ref: "Rx 80442", user: "Mark T." },
{ date: "26/05/2026 11:15", kind: "Transfer", qty: -6, bal: -19, ref: "TX-2026-088", user: "Alex J." },
{ date: "24/05/2026 16:30", kind: "Adjustment", qty: -1, bal: -13, ref: "Damaged · disposed", user: "Sarah K." },
{ date: "20/05/2026 10:05", kind: "Stocktake", qty: 0, bal: -12, ref: "Cycle count", user: "Alex J." },
];
return (
Date / time
Kind
Qty
Balance
Reference
User
{items.map((m, i) => (
{m.date}
{m.kind}
0 ? 'var(--paid-text)' : 'var(--text-subtle)'}}>
{m.qty > 0 ? "+" : ""}{m.qty}
{m.bal}
{m.ref}
{m.user}
))}
);
}
// =================================================================
// HISTORY TAB — audit log
// =================================================================
function HistoryTab({ editing }) {
const items = [
{ date: "28/05/2026 09:00", user: "Auto", action: "Cost price updated", detail: "$4.20 → $4.42 (from PO 4421)" },
{ date: "21/05/2026 14:21", user: "Sarah K.", action: "Retail price changed", detail: "$4.65 → $4.99" },
{ date: "18/05/2026 11:10", user: "Mark T.", action: "Min stock raised", detail: "0 → 5 (Boronia store)" },
{ date: "10/05/2026 08:42", user: "Alex J.", action: "Barcode added", detail: "9300673893680" },
{ date: "02/05/2026 16:30", user: "Sarah K.", action: "Stock line created", detail: editing?.name || "" },
];
return (
{items.map((h, i) => (
))}
);
}
// =================================================================
// NOTES TAB
// =================================================================
function NotesTab({ form, setField }) {
return (
);
}
// =================================================================
// SHARED ATOMS
// =================================================================
function Field({ label, hint, required, full, children }) {
return (
{children}
);
}
function Toggle({ k, label, hint, tone, form, setField }) {
return (
);
}
function SleStat({ label, value, tone }) {
return (
);
}
function SleLadderRow({ label, value, emph, muted }) {
return (
{label}
{oMoney(value)}
);
}
// =================================================================
// REGISTER
// =================================================================
window.OFFICE_SCREENS = Object.assign(window.OFFICE_SCREENS || {}, {
newStock: StockLineEditor,
editStock: StockLineEditor,
});