`; } async function poPackingPrint(id){ setLoading(true); try{ const j=await call({action:"po_doc",id}); const d=new Date(); j.today=(""+d.getDate()).padStart(2,"0")+"/"+(""+(d.getMonth()+1)).padStart(2,"0")+"/"+d.getFullYear(); const w=window.open("","_blank"); if(!w){ flash("Allow pop-ups to open the packing list.","err"); setLoading(false); return; } w.document.write(packingListHtml(j)); w.document.close(); w.focus(); }catch(err){ flash(err.message,"err"); } setLoading(false); } // PO documents (upload PDF + custom description) function pickPoDoc(){ if(!poEdit.id){ flash("Save the PO first.","err"); return; } const desc=prompt("Description for this document (e.g. Proforma invoice, Commercial invoice):",""); if(desc===null) return; poDocDesc.current=desc; poDocFileRef.current.click(); } async function onPoDoc(ev){ const f=ev.target.files[0]; ev.target.value=""; if(!f) return; if(f.type!=="application/pdf" && !/\.pdf$/i.test(f.name)){ flash("Please choose a PDF file.","err"); return; } setLoading(true); try{ const rid=(window.crypto&&crypto.randomUUID)?crypto.randomUUID():(Date.now()+"-"+Math.random().toString(36).slice(2)); const path="po-"+poEdit.id+"/"+rid+".pdf"; const {error}=await sbc.storage.from("po-documents").upload(path,f,{upsert:true,contentType:"application/pdf"}); if(error) throw error; await call({action:"po_doc_add",po_id:poEdit.id,path,filename:f.name,description:poDocDesc.current||""}); flash("Document uploaded."); await openPO(poEdit.id); }catch(err){ flash("Upload failed: "+err.message,"err"); } setLoading(false); } async function dlPoDoc(path){ try{ const {data,error}=await sbc.storage.from("po-documents").createSignedUrl(path,120); if(error) throw error; window.open(data.signedUrl,"_blank"); }catch(err){ flash("Download failed: "+err.message,"err"); } } async function delPoDoc(id){ if(!confirm("Delete this document?")) return; setLoading(true); try{ await call({action:"po_doc_delete",id}); await openPO(poEdit.id); }catch(err){ flash(err.message,"err"); } setLoading(false); } const lblByProduct=useMemo(()=>{ const m={}; lblLabels.forEach(r=>{ const gk=r.group_key||"__none"; (m[gk]=m[gk]||[]).push(r); }); return m; },[lblLabels]); const lblChanged=useMemo(()=>{ const out=[]; lblLabels.forEach(r=>{ const e=lblEdit[r.id]||{}, o=lblOrig[r.id]||{}; const changedF=LBL_FIELDS.filter(f=>(e[f]||"")!==(o[f]||"")); const schg=!!lblStatE[r.id]!==!!lblStatO[r.id]; const mchg=(lblMktE[r.id]||[]).join("+")!==(lblMktO[r.id]||[]).join("+"); if(changedF.length||schg||mchg){ const row={id:r.id, status:lblStatE[r.id]?"discontinued":"active"}; changedF.forEach(f=>row[f]=e[f]||""); if(mchg) row.market=(lblMktE[r.id]||[]).join("+"); out.push(row); } }); return out; },[lblLabels,lblEdit,lblOrig,lblStatE,lblStatO,lblMktE,lblMktO]); async function saveLabels(){ if(!lblChanged.length){ flash("No changes to save."); return; } setLoading(true); try{ const j=await call({action:"label_save",rows:lblChanged}); setLblOrig(o=>{const n={...o}; lblChanged.forEach(r=>{ n[r.id]={...(lblEdit[r.id]||{})}; }); return n;}); setLblStatO(o=>{const n={...o}; lblChanged.forEach(r=>{ n[r.id]=r.status==="discontinued"; }); return n;}); setLblMktO(o=>{const n={...o}; lblChanged.forEach(r=>{ if(r.market!==undefined) n[r.id]=lblMkArr(r.market); }); return n;}); flash(`Saved ${j.saved} label(s).`); }catch(err){ flash(err.message,"err"); } setLoading(false); } async function autofillSkus(){ const over=confirm("Auto-fill Product SKU on every label from the Amazon SKUs (matched by region + flavour + size).\n\nOK = overwrite existing SKUs too · Cancel = only fill blank ones."); setLoading(true); try{ const j=await call({action:"label_autofill_sku",overwrite:over}); await loadLabels(true); flash(`Filled ${j.filled} label SKU(s).`); }catch(err){ flash(err.message,"err"); } setLoading(false); } async function backfillArtwork(){ const over=confirm("Auto-generate the Artwork name for existing labels — format OL - Market - Brand - Product - Flavour - Size - Label ID.\n\nOK = regenerate ALL (overwrite) · Cancel = only fill blank ones."); setLoading(true); try{ const j=await call({action:"label_backfill_artwork",overwrite:over}); await loadLabels(true); flash(`Set ${j.filled} artwork name(s).`); }catch(err){ flash(err.message,"err"); } setLoading(false); } async function delLabel(id){ if(!confirm("Delete this label ID permanently? This removes its record and any uploaded PDF.")) return; setLoading(true); try{ await call({action:"label_delete",id}); await loadLabels(true); flash("Label deleted."); } catch(err){ flash(err.message,"err"); } setLoading(false); } const toggleLblMkt=(id,mk)=>setLblMktE(s=>{ const cur=new Set(s[id]||[]); if(cur.has(mk)) cur.delete(mk); else cur.add(mk); return {...s,[id]:["EU","UK","US"].filter(m=>cur.has(m))}; }); async function addLabel(p, market){ const flavour=prompt("Flavour for the new "+market+" label (leave blank if this product has no flavours):",""); if(flavour===null) return; const size=prompt("Size for the new "+market+" label (e.g. 100g, 360g, 30ml):",""); if(size===null) return; setLoading(true); try{ const j=await call({action:"label_add",group_key:(p.group_keys||[])[0],market,brand:p.brand,flavour:(flavour||"").trim(),size:(size||"").trim()}); await loadLabels(true); setLblOpen(s=>({...s,[p.pkey]:true})); flash(`Created ${j.label_number} · artwork name: ${j.artwork_name}. Upload the PDF next.`); }catch(err){ flash(err.message,"err"); } setLoading(false); } const lblFmtSz=(l)=>{ let s=String(l.size||"").trim(); if(!s) return ""; s=s.replace(/\.0+$/,""); const f=String(l.format||"").toLowerCase(); if(/^[0-9.]+$/.test(s)){ if(f.includes("gram")) s+="g"; else if(f.includes("ml")) s+="ml"; else if(f.includes("unit")||f.includes("sach")) s+=" units"; else if(f.includes("caps")) s+=" caps"; else if(f.includes("gumm")) s+=" gummies"; } return s; }; const lblFlav=(l)=>String(l.flavour||"").trim(); const lblSetF=(id,f,v)=>setLblEdit(s=>({...s,[id]:{...s[id],[f]:v}})); const LBL_DETAIL_FIELDS=[["product_sku","Product SKU"],["spec_sheet_ref","Spec sheet ref"],["dimensions","Dimensions"],["cutter","Cutter"],["winding","Winding"],["substrate_adhesive","Substrate / adhesive"],["finish","Finish"],["core_size","Core size"],["max_reel_size","Max reel size"]]; const lblProductLabels=(p)=>{ const arr=[]; (p.group_keys||[]).forEach(gk=>{ (lblByProduct[gk]||[]).forEach(l=>arr.push(l)); }); arr.sort((a,b)=>String(b.date_added||"").localeCompare(String(a.date_added||""))||(b.id-a.id)); return arr; }; function pickLblPdf(id){ lblUploadId.current=id; lblFileRef.current.click(); } async function onLblPdf(ev){ const f=ev.target.files[0]; ev.target.value=""; const id=lblUploadId.current; if(!f||!id) return; if(f.type!=="application/pdf" && !/\.pdf$/i.test(f.name)){ flash("Please choose a PDF file.","err"); return; } setLoading(true); try{ const path=id+".pdf"; const {error}=await sbc.storage.from("label-artwork").upload(path,f,{upsert:true,contentType:"application/pdf"}); if(error) throw error; await call({action:"label_save",rows:[{id,artwork_pdf:path}]}); setLblArt(a=>({...a,[id]:path})); flash("Artwork PDF uploaded."); }catch(err){ flash("Upload failed: "+err.message,"err"); } setLoading(false); } async function dlLblPdf(id,name){ const path=lblArt[id]; if(!path) return; const fn=((name||"").trim()||("label-"+id)).replace(/[\\/:*?"<>|]+/g,"-").replace(/\.pdf$/i,"")+".pdf"; try{ const {data,error}=await sbc.storage.from("label-artwork").createSignedUrl(path,120,{download:fn}); if(error) throw error; const a=document.createElement("a"); a.href=data.signedUrl; a.rel="noopener"; document.body.appendChild(a); a.click(); a.remove(); }catch(err){ flash("Download failed: "+err.message,"err"); } } const imgCtysFor=(mkts)=>(mkts||[]).flatMap(m=>IMG_CTY[m]||[]); const imgCurCty=(l)=>{ const cs=imgCtysFor(lblMktE[l.id]); return lblImgCty[l.id]||(cs[0]?cs[0][0]:"DE"); }; const imgCountCty=(id,cty)=>IMG_SLOTS.filter(x=>lblImg[imgKey(id,cty,x.k)]).length; const imgOrdered=(id,cty)=>IMG_SLOTS.map(x=>({slot:x.k,g:lblImg[imgKey(id,cty,x.k)]})).filter(x=>x.g); const imgExt=(f)=>((f.name.split(".").pop()||"jpg").toLowerCase().replace(/[^a-z0-9]/g,""))||"jpg"; const imgDims=(f)=>new Promise(res=>{ const u=URL.createObjectURL(f); const im=new Image(); im.onload=()=>{res({w:im.naturalWidth,h:im.naturalHeight});URL.revokeObjectURL(u);}; im.onerror=()=>res({w:0,h:0}); im.src=u; }); async function imgUploadOne(id,cty,slot,f){ const dim=await imgDims(f); const path=id+"/"+cty+"/"+slot+"_"+Date.now()+"."+imgExt(f); const up=await sbc.storage.from("label-images").upload(path,f,{upsert:true,contentType:f.type||"image/jpeg"}); if(up.error) throw up.error; const {data:pub}=sbc.storage.from("label-images").getPublicUrl(path); const url=pub.publicUrl; const status=(dim.w>=1000&&dim.h>=1000)?"stored":"bad"; const old=lblImg[imgKey(id,cty,slot)]; if(old&&old.path&&old.path!==path){ try{ await sbc.storage.from("label-images").remove([old.path]); }catch(_){} } const db=await sbc.from("label_image").upsert({label_id:id,market:cty,position:slot,path,url,width:dim.w,height:dim.h,status,updated_at:new Date().toISOString()},{onConflict:"label_id,market,position"}); if(db.error) throw db.error; return {slot,rec:{path,url,width:dim.w,height:dim.h,status}}; } function pickImg(id,cty,slot){ lblImgTgt.current={id,cty,slot,multi:false}; lblImgFileRef.current.value=""; lblImgFileRef.current.removeAttribute("multiple"); lblImgFileRef.current.click(); } function pickImgMulti(id,cty){ lblImgTgt.current={id,cty,multi:true}; lblImgFileRef.current.value=""; lblImgFileRef.current.setAttribute("multiple","multiple"); lblImgFileRef.current.click(); } async function onImgFile(ev){ const files=Array.from(ev.target.files||[]); const t=lblImgTgt.current; if(!files.length||!t) return; const id=t.id, cty=t.cty; setLoading(true); try{ if(t.multi){ const empty=IMG_SLOTS.map(x=>x.k).filter(k=>!lblImg[imgKey(id,cty,k)]); const use=files.slice(0,empty.length); let bad=0; const acc={}; for(let i=0;i({...z,...acc})); const skipped=files.length-use.length; flash("Uploaded "+use.length+" image"+(use.length===1?"":"s")+(bad?" ("+bad+" under 1000px)":"")+(skipped>0?" — "+skipped+" skipped ("+IMG_SLOTS.length+" slots max)":".")); } else { const r=await imgUploadOne(id,cty,t.slot,files[0]); setLblImg(z=>({...z,[imgKey(id,cty,r.slot)]:r.rec})); flash(r.rec.status==="bad"?"Uploaded, but under 1000px — fix before it can go live":"Image saved."); } }catch(err){ flash("Upload failed: "+err.message,"err"); } setLoading(false); } async function delImg(id,cty,slot){ const g=lblImg[imgKey(id,cty,slot)]; setLoading(true); try{ if(g&&g.path) await sbc.storage.from("label-images").remove([g.path]); await sbc.from("label_image").delete().eq("label_id",id).eq("market",cty).eq("position",slot); setLblImg(z=>{const n={...z}; delete n[imgKey(id,cty,slot)]; return n;}); flash("Image removed."); }catch(err){ flash("Delete failed: "+err.message,"err"); } setLoading(false); } async function reorderImg(id,cty,fromSlot,toSlot){ if(fromSlot===toSlot) return; const order=imgOrdered(id,cty).map(x=>x.slot); const fi=order.indexOf(fromSlot); if(fi<0) return; order.splice(fi,1); const ti=order.indexOf(toSlot); order.splice(ti<0?order.length:ti,0,fromSlot); const recs=order.map(sl=>lblImg[imgKey(id,cty,sl)]); const newMap={}, rows=[]; recs.forEach((rec,i)=>{ const pos=IMG_SLOTS[i].k; newMap[imgKey(id,cty,pos)]={...rec}; rows.push({label_id:id,market:cty,position:pos,path:rec.path,url:rec.url,width:rec.width,height:rec.height,status:rec.status,updated_at:new Date().toISOString()}); }); setLblImg(z=>{ const n={...z}; IMG_SLOTS.forEach(x=>{ delete n[imgKey(id,cty,x.k)]; }); return {...n,...newMap}; }); setLoading(true); try{ const d=await sbc.from("label_image").delete().eq("label_id",id).eq("market",cty); if(d.error) throw d.error; if(rows.length){ const ins=await sbc.from("label_image").insert(rows); if(ins.error) throw ins.error; } flash("Order saved."); }catch(err){ flash("Reorder failed: "+err.message,"err"); loadLabels(true); } setLoading(false); } async function dlZipCty(id,cty,labelNo){ const items=imgOrdered(id,cty); if(!items.length){ flash("No images to download for "+cty,"err"); return; } if(!window.JSZip){ flash("Zip library still loading — try again in a moment.","err"); return; } setLoading(true); try{ const zip=new window.JSZip(); for(let i=0;i{URL.revokeObjectURL(a.href);a.remove();},1000); flash("Downloaded "+items.length+" image"+(items.length===1?"":"s")+" for "+cty+"."); }catch(err){ flash("Download failed: "+err.message,"err"); } setLoading(false); } const lblMkList=(p)=>{ const set=new Set((p.markets||[]).filter(Boolean)); lblProductLabels(p).forEach(l=>(lblMktE[l.id]||[]).forEach(m=>set.add(m))); return ["EU","UK","US"].filter(m=>set.has(m)); }; // true if any CURRENT label (newest active per market+flavour+size) has no PDF uploaded yet const lblHasMissing=(p)=>{ const all=lblProductLabels(p); return lblMkList(p).some(mk=>{ const labs=all.filter(l=>(lblMktE[l.id]||[]).includes(mk)); const seen={}; labs.forEach(l=>{ const key=lblFlav(l)+"|"+(lblFmtSz(l)||"—"); if(!(key in seen) && !lblStatE[l.id]) seen[key]=l.id; }); return Object.values(seen).some(id=>!String(lblArt[id]||"").trim()); }); }; const lblMissingCount=useMemo(()=>lblProducts?lblProducts.filter(lblHasMissing).length:0,[lblProducts,lblByProduct,lblMktE,lblStatE,lblArt]); const lblProdVisible=useMemo(()=>{ if(!lblProducts) return []; const s=lblQ.trim().toLowerCase(); let list=lblProducts.slice(); if(s) list=list.filter(p=>{ if([p.name,p.brand].some(x=>String(x||"").toLowerCase().includes(s))) return true; return lblProductLabels(p).some(l=>String(l.label_number||"").toLowerCase().includes(s)); }); if(lblMissingOnly) list=list.filter(lblHasMissing); return list.sort((a,b)=>String((a.brand||"")+(a.name||"")).localeCompare(String((b.brand||"")+(b.name||"")))); },[lblProducts,lblQ,lblByProduct,lblMktE,lblMissingOnly,lblStatE,lblArt]); const lblUnlinked=useMemo(()=>lblByProduct["__none"]||[],[lblByProduct]); if(booting && !AUTH_ACTION){ return (
Loading…
); } if(recovery){ return (

Set your password

Choose a password for your account{session?.user?.email?" ("+session.user.email+")":""}.

setNewPw(e.target.value)} onKeyDown={e=>e.key==="Enter"&&saveNewPassword()} autoFocus/>
{msg &&
{msg.text}
}
); } if(!authed){ return (

Operations

Sign in with your email and password.

setEmail(e.target.value)} onKeyDown={e=>e.key==="Enter"&&email&&acctPw&&accountLogin()} autoFocus/> setAcctPw(e.target.value)} onKeyDown={e=>e.key==="Enter"&&email&&acctPw&&accountLogin()}/>

{e.preventDefault();resetPw();}} style={{color:"var(--navy)"}}>Forgot password?

{msg &&
{msg.text}
}
); } const exclCount=products.filter(p=>editA[p.group_key]).length, skuCount=products.reduce((a,p)=>a+p.skus.length,0); const NAV=[ {label:"Products", items:[["products","Products"],["names","Product names"]]}, {label:"Supply Chain", items:[["stock","Stock"],["po","Purchasing"],["packing","Packing"],["inbound","Inbound"],["recon","Reconciliation"],["suppliers","Suppliers"]]}, {label:"Labels", items:[["labels","Labels"],["lblorders","Label orders"]]}, {label:"Issues", items:[["expiry","Expiration Risk"],["vat","VAT check"],["paneu","Pan-EU"]]}, ]; return (

Operations

{session?.user?.email && {session.user.email}} {session && }
{NAV.map(g=>{ const active=g.items.some(([v])=>v===view); return
{g.items.map(([v,lbl])=>)}
; })}
{view==="costs" && {filtered.length} products · {skuCount} SKUs · cost per SKU (size/pack); shipping by region} {view==="products" && {hubRows?hubGroups.length:"…"} products · {hubRows?hubRows.length:"…"} SKUs · identity + cost (with history, updated from POs) + supplier + labels — expand a SKU (▸) for the rest} {view==="stock" && {stkSkus?stkGroups.length:"…"} products · warehouse + FBA stock by batch · FIFO cost layers (oldest cost used first)} {view==="po" && {poRows?poRows.length:"…"} purchase orders · auto label + cost per line · receive → stock · deposits, FX (HMRC monthly), cash-due} {view==="inbound" && {inbData?inbData.shipments.length:"…"} FBA inbound shipments · status, destination, units shipped vs received · expand a row for line items + expiry} {view==="labels" && {lblProducts?lblProducts.length:"…"} products · label IDs by market (newest = CURRENT) · add the next label & upload its PDF} {view==="names" && {prAsins?prAsins.length:"…"} ASINs · short display name + excluded flag (drives the Days Cover tab)} {view==="suppliers" && {vRows?vRows.length:"…"} suppliers · lead time in weeks + full record} {view==="vat" && applied vs expected VAT per product × marketplace · click a red cell for evidence} {view==="paneu" && estimated local vs actual FBA fee per product × EU market · red = paying cross-border (not Pan-EU)} {view==="recon" && {rcData?rcData.length:"…"} SKUs · stock valued at live Amazon quantity (self-healing) · our records vs live anchor · {eur0(rcTotal)} on hand} {view==="packing" && {pkRows?pkGroups.length:"…"} products · tariff code + net & gross weight per unit + box layout → powers the 📦 Packing list on each purchase order}
{view==="costs" &&
setQ(e.target.value)} style={{minWidth:200}}/> CurrencysetCcy(e.target.value.toUpperCase())} style={{width:58}} /> Effective fromsetEff(e.target.value)} /> {msg &&
{msg.text}
}
Cost is per SKU — each size/pack has its own. Same-priced flavours: fill one, then hit Copy ↓ on the product to apply to all its SKUs. Shipping is per region (EU = one cost, via DE Pan-EU FBA). Untick Active to exclude a discontinued product. Effective from dates a price change; blank = first entry / correction.
{REGIONS.map(r=>)} {filtered.map(p=>{ const arch=!!editA[p.group_key]; const vis=p.skus.filter(k=> showExcl || !editX[k.sku]); if(!vis.length) return null; return ( {vis.map(k=>{ const e=editU[k.sku]||{ship:{}}, o=origU[k.sku]||{ship:{}}, uChg=(e.unit||"")!==(o.unit||""), xExc=!!editX[k.sku]; return ( {REGIONS.map(r=>{ const sChg=(e.ship[r]||"")!==((o.ship||{})[r]||""); return ; })} ); })} ); })}
SKU / ProductUnit cost ({ccy})Ship {r}
{p.brand}{p.product_name} {p.markets.map(m=>{m})} {p.skus.length>1 && } setDisp(p.group_key,e.target.value)} title="Short English name shown in the Sales & Profit dashboard's grouped view. Give two products the SAME name to merge them into one line; different names keep them separate."/>
{k.sku} {k.asins.join(", ")} {k.asins[0] && setVar(k.asins[0],ev.target.value)} title="Short label shown per-variation in the Sales & Profit dashboard's expanded rows. This ASIN is shared across marketplaces, so one label shows everywhere. Blank = auto (size pulled from the listing title)."/>} setSkuBox(k.sku,ev.target.value)} title="Units per box/carton from the supplier (often = MOQ). Optional — leave blank where not relevant. Used to round PO quantities to whole boxes."/> setUnit(k.sku,ev.target.value)}/>setShip(k.sku,r,ev.target.value)}/>
} {view==="packing" &&
setPkQ(e.target.value)} style={{minWidth:220}}/> {msg &&
{msg.text}
}
These fields feed the 📦 Packing list button on each purchase order. Description = the short customs line (e.g. “organic Oregano 200 g”) — hit Auto-fill descriptions for a starting point from the product name, then tidy. Net = weight of the product in one retail pack; Gross = that pack including its own packaging (both per single unit, in kg). HS / tariff is the commodity code — fill one size, then Copy HS ↓ to the product’s other sizes. Units/box is the box layout (shared with the Costs & Suppliers tabs).
{!pkRows &&
{loading?"Loading…":""}
} {pkRows &&
{pkGroups.map(g=>( {g.skus.map(r=>{ const e=pkEdit[r.sku]||{}, o=pkOrig[r.sku]||{}; const chg=(f)=> (e[f]||"")!==(o[f]||"") ? " changed":""; const boxChg=(pkBox[r.sku]||"")!==(pkBoxOrig[r.sku]||""); return (); })} ))} {!pkGroups.length && }
SKU / productPacking descriptionHS / tariffNet kg/unitGross kg/unitUnits/box
{g.brand}{g.name} {g.skus.length>1 && }
{r.sku} {r.market_scope&&{r.market_scope}} setPkF(r.sku,"desc",ev.target.value)}/> setPkF(r.sku,"hs",ev.target.value)}/> setPkF(r.sku,"net",ev.target.value)}/> setPkF(r.sku,"gross",ev.target.value)}/> setPkBx(r.sku,ev.target.value)}/>
No products match your search.
}
} {view==="stock" &&
setStkQ(e.target.value)} style={{minWidth:220}}/> Market {msg &&
{msg.text}
}
Each inbound shipment is a batch / cost layer (qty + unit cost + BBE). On-hand and the FIFO cost (cost of the oldest layer still in stock) show per SKU — expand for the layers. FIFO is a cost-flow assumption (oldest cost used first); it doesn't require Amazon to ship oldest-first. Expiry is exact for your warehouses; approximate at FBA (commingled).
{rcv &&
Receive stock
{(stkSkus||[]).map(r=>)}
} {!stkSkus ?
{loading?"Loading…":""}
:
{stkGroups.map(g=>( {g.skus.map(r=>{ const st=stkBySku[r.sku]||{layers:[],onhand:0}; const op=!!stkOpen[r.sku]; return ( {op && } ); })} ))}
SKUMarketOn handSold*FIFO costSoonest BBELayers
{g.disc ? Discontinued with stock : {g.brand}{g.name}} {g.skus.reduce((a,r)=>a+((stkBySku[r.sku]||{}).onhand||0),0)} on hand
setStkOpen(s=>({...s,[r.sku]:!s[r.sku]}))}>{op?"▾":"▸"}{r.sku}{g.disc&&{(r.brand?r.brand+" · ":"")+(r.product_name||"")}} {r.market_scope||""} {st.onhand||0} {st.sold||0} {st.fifo_cost!=null?`${st.fifo_cost} ${st.fifo_ccy||""}`:"—"} {st.soonest_bbe?{st.soonest_bbe}:} {(st.layers||[]).filter(l=>l.remaining>0).length} live / {(st.layers||[]).length}
{(st.layers||[]).length? {st.layers.map(l=>(0?1:.45}}> ))}
ReceivedBatchRemaining / recvUnit costBBELocationPO
{l.received_date||"—"}{l.batch_code||"—"} {l.remaining} / {l.received_qty} {l.unit_cost!=null?`${l.unit_cost} ${l.currency||""}`:"—"} 0?"var(--red)":"inherit"}}>{l.bbe||"—"} {l.location_id&&locById[l.location_id]?locById[l.location_id].code:"—"} {l.po_ref||"—"}
:
No stock received yet — use + Receive.
}
}
} {view==="inbound" && !inbData &&
Loading inbound shipments…
} {view==="inbound" && inbData && {(()=>{ const act=inbData.shipments.filter(s=>INB_ACTIVE.indexOf(s.shipment_status)>=0); const units=act.reduce((a,s)=>a+inbAgg(s).shp,0), recv=act.reduce((a,s)=>a+inbAgg(s).rcv,0); const linked=inbData.shipments.filter(s=>inbData.po[s.shipment_id]).length; return
In transit / prep
{act.length}
Units shipped
{units.toLocaleString()}
Units received
{recv.toLocaleString()}
Linked to a PO
{linked} / {inbData.shipments.length}
; })()}
{["active","READY_TO_SHIP","SHIPPED","IN_TRANSIT","RECEIVING","CLOSED","CANCELLED","all"].map(k=> )}
setInbQ(e.target.value)}/> {msg &&
{msg.text}
}
{inbRows.map(s=>{ const a=inbAgg(s); const op=inbOpen[s.shipment_id]; const c=inbStCol[s.shipment_status]||["#eef1f5","#5f6b7a"]; const pct=a.shp>0?Math.min(100,Math.round(a.rcv/a.shp*100)):0; const po=inbData.po[s.shipment_id]; return setInbOpen(o=>({...o,[s.shipment_id]:!o[s.shipment_id]}))}> {op && } ; })} {!inbRows.length && }
ShipmentDestinationStatusPOLinesShippedReceivedSynced
{op?"▾":"▸"}{s.shipment_id} {regionFlag(inbRegionOf(s))} {s.destination_fc||"—"}{inbIsAWD(s)&&AWD→FBA} {(s.shipment_status||"").replace(/_/g," ")} {e.stopPropagation(); openPOByRef(po);}}>{po?{po.po_ref}:} {a.items} {a.shp.toLocaleString()} {a.rcv.toLocaleString()}/{a.shp.toLocaleString()} {fmtD(s.synced_at)}
{(()=>{ const R=s.raw||{}; const d=R.destination||{},da=d.address||{},sa=((R.source||{}).address)||R.ShipFromAddress||{},dw=R.selectedDeliveryWindow||{},rw=(R.dates||{}).readyToShipWindow||{}; const addr=(a)=>{ a=a||{}; return [a.name||a.companyName||a.Name,a.addressLine1||a.AddressLine1,a.addressLine2||a.AddressLine2,[a.city||a.City,a.stateOrProvinceCode||a.StateOrProvinceCode].filter(Boolean).join(", "),a.postalCode||a.PostalCode,a.countryCode||a.CountryCode].filter(Boolean).join(" · "); }; const F=(lbl,val)=> val?
{lbl}
{val}
: null; const awd=inbIsAWD(s); const any=R.amazonReferenceId||addr(da)||addr(sa)||dw.startDate||awd; return any?
{F("Type", awd? AWD → FBA replenishment : "Direct to FBA")} {F("Amazon ref", R.amazonReferenceId?{R.amazonReferenceId}:null)} {F("Shipment name", R.name||s.shipment_name)} {F("Destination"+(d.warehouseId||s.destination_fc?" · "+(d.warehouseId||s.destination_fc):""), addr(da))} {F("Ship from", addr(sa))} {F("Delivery window", dw.startDate? fmtD(dw.startDate)+" → "+fmtD(dw.endDate)+(dw.editableUntil?" (editable until "+fmtD(dw.editableUntil)+")":"") : null)} {F("Ready-to-ship", rw.start? fmtD(rw.start)+(rw.end&&rw.end!==rw.start?" → "+fmtD(rw.end):"") : null)} {F("Need by", s.confirmed_need_by_date? fmtD(s.confirmed_need_by_date):null)}
: null; })()} {a.its.length? {a.its.map((it,ii)=>)}
SKUASINFNSKUShippedReceivedBest-beforeLot
{it.sku}{it.asin||"—"}{it.fnsku||"—"}{Number(it.quantity_shipped||0).toLocaleString()}{it.quantity_received!=null?Number(it.quantity_received).toLocaleString():"—"}{it.expiration?{fmtD(it.expiration)}:}{it.manufacturing_lot_code||}
:
No line-item detail stored for this shipment.
}
No shipments match.
} {view==="expiry" &&
Expiration risk flags stock whose days-of-cover run past the sell-by date — each product's earliest best-before minus 3 months (stock must clear 3 months before it expires). Best-before dates come from inbound-shipment BBEs, so coverage grows as more shipments sync. Units at risk = units unlikely to sell before the sell-by at the current 30-day rate.
{!expData ?
{loading?"Loading…":""}
: (()=>{ const q=expQ.trim().toLowerCase(); const lv={past:0,risk:1,ok:2,none:3}; const rows=expData.filter(r=>{ const l=expLevel(r); if(expFilt==="risk" && !(l==="past"||l==="risk")) return false; if(expFilt==="past" && l!=="past") return false; if(expMkt && r.pool!==expMkt) return false; if(q && !((r.name||"").toLowerCase().includes(q)||(r.asin||"").toLowerCase().includes(q))) return false; return true; }).sort((a,b)=>{ const la=lv[expLevel(a)],lb=lv[expLevel(b)]; if(la!==lb) return la-lb; return (Number(b.units_at_risk)||0)-(Number(a.units_at_risk)||0) || (Number(a.days_to_sellby)||0)-(Number(b.days_to_sellby)||0); }); const atRisk=expData.filter(r=>{const l=expLevel(r);return l==="past"||l==="risk";}); const unitsRisk=atRisk.reduce((s,r)=>s+(Number(r.units_at_risk)||0),0); const pastN=expData.filter(r=>expLevel(r)==="past").length; const badge=(r)=>{ const m={past:["Past sell-by","#fdeceb","#B42318"],risk:["At risk","#fdf1e3","#8a6d1a"],ok:["On track","#e7f6ec","#1E7B34"],none:["—","#eef1f5","#5f6b7a"]}[expLevel(r)]; return {m[0]}; }; return
Products at risk
{atRisk.length}
Units at risk
{Math.round(unitsRisk).toLocaleString()}
Past sell-by
0?"var(--red)":undefined}}>{pastN}
Tracked (with expiry)
{expData.length}
{[["risk","At risk"],["past","Past sell-by"],["all","All tracked"]].map(([k,lbl])=>)}
setExpQ(e.target.value)}/>
{rows.map((r,i)=>{ const l=expLevel(r); return ; })} {!rows.length && }
ProductRegionStock30d salesCoverSoonest expirySell-by (−3mo)Units at riskStatus
{r.name||r.asin} {regionFlag(r.pool)} {r.pool} {Math.round(Number(r.stock)||0).toLocaleString()} {Math.round(Number(r.d30)||0).toLocaleString()} {r.cover_days!=null?Number(r.cover_days).toLocaleString()+"d":no sales} {r.soonest_exp?fmtD(r.soonest_exp):"—"} {r.sell_by?fmtD(r.sell_by):"—"} {r.days_to_sellby!=null&&{Number(r.days_to_sellby)<0?Math.abs(r.days_to_sellby)+"d ago":"in "+r.days_to_sellby+"d"}} 0?"var(--red)":undefined}}>{Number(r.units_at_risk)>0?Math.round(Number(r.units_at_risk)).toLocaleString():"—"} {badge(r)}
{expData.length?"Nothing at risk — all tracked stock will clear before its sell-by.":"No expiry data yet. Best-before dates arrive with Send-to-Amazon shipments; this fills in as they sync."}
; })()}
} {view==="recon" &&
Stock valuation is anchored on the live Amazon quantity (FBA fulfillable + reserved, Pan-EU deduped, + AWD + warehouse), not on replaying every movement. We walk the received cost layers newest → oldest and stop once the live quantity is filled — so the value self-heals if Amazon loses, finds, removes or reconciles units. The table also reconciles our movement records against that anchor: Δ is our records minus live. A small positive Δ is normal (sales feed lags a few days). Drift or data-gap rows are worth a look — a data gap means we hold stock Amazon confirms but have no cost/receipt recorded for it.
{!rcData ?
{loading?"Loading…":""}
: (()=>{ const q=rcQ.trim().toLowerCase(); const ord={drift:0,watch:1,ok:2}; const rows=rcData.filter(r=>{ if(rcFilt==="attn" && !(r.flag==="drift"||r.flag==="watch"||Number(r.data_gap_units)>0)) return false; if(rcFilt==="drift" && r.flag!=="drift") return false; if(rcFilt==="gap" && !(Number(r.data_gap_units)>0)) return false; if(q && !((r.name||"").toLowerCase().includes(q)||(r.sku||"").toLowerCase().includes(q))) return false; return true; }).sort((a,b)=>{ const oa=ord[a.flag]??3,ob=ord[b.flag]??3; if(oa!==ob) return oa-ob; return (Number(b.abs_delta)||0)-(Number(a.abs_delta)||0); }); const driftN=rcData.filter(r=>r.flag==="drift").length; const gapU=rcData.reduce((s,r)=>s+(Number(r.data_gap_units)||0),0); const valU=rcData.reduce((s,r)=>s+(Number(r.valued_units)||0),0); const badge=(r)=>{ const m={drift:["Drift","#fdeceb","#B42318"],watch:["Watch","#fdf1e3","#8a6d1a"],ok:["OK","#e7f6ec","#1E7B34"]}[r.flag]||["—","#eef1f5","#5f6b7a"]; return {m[0]}; }; return
Stock on hand (value)
{eur0(rcTotal)}
Units valued
{Math.round(valU).toLocaleString()}
Drift SKUs
0?"var(--red)":undefined}}>{driftN}
Data-gap units
0?"var(--red)":undefined}}>{Math.round(gapU).toLocaleString()}
{[["attn","Needs attention"],["drift","Drift only"],["gap","Data gaps"],["all","All SKUs"]].map(([k,lbl])=>)}
setRcQ(e.target.value)}/>
{rows.map((r,i)=>{ const gap=Number(r.data_gap_units)>0; return ; })} {!rows.length && }
ProductSKULive (Amazon)Our recordsΔValuedAvg costNext FIFOHolding €Status
{r.name||r.sku} {r.sku} {Math.round(Number(r.anchor_units)||0).toLocaleString()} {Math.round(Number(r.computed_onhand)||0).toLocaleString()} {Number(r.delta)>0?"+":""}{Math.round(Number(r.delta)||0).toLocaleString()}{r.pct!=null&& {Number(r.pct)>0?"+":""}{r.pct}%} {r.valued_units!=null?Math.round(Number(r.valued_units)).toLocaleString():"—"}{gap&& +{Math.round(Number(r.data_gap_units))} no cost} {r.avg_unit_cost_eur!=null?"€"+Number(r.avg_unit_cost_eur).toFixed(2):"—"} {r.next_fifo_cost_eur!=null?"€"+Number(r.next_fifo_cost_eur).toFixed(2):"—"} {eur0(r.holding_cost_eur)} {gap?Data gap:badge(r)}
{rcData.length?"Nothing flagged — our records match the live Amazon quantity within tolerance.":"No stock data yet."}
; })()}
} {view==="labels" &&
setLblQ(e.target.value)} style={{minWidth:240}}/> {msg &&
{msg.text}
}
Labels by product, split by market and size. Newest active label per market+size is CURRENT — the one to send to Superfast / the supplier for a PO. + Add label asks for the size, mints the next brand ID (e.g. APS129) and auto-names the artwork (OL - US - APS - Basil - 100g - APS044) for the designer to name the file. Toggle EU/UK/US to make one label ID serve several markets. Untick Active to discontinue, to delete. Discontinued products are hidden. Save after market/name edits.
{!lblProducts ?
{loading?"Loading…":""}
:
{lblProdVisible.map(p=>{ const pk=p.pkey, op=!!lblOpen[pk], mkts=lblMkList(p), all=lblProductLabels(p); return ( {op && mkts.map(mk=>{ const labs=all.filter(l=>(lblMktE[l.id]||[]).includes(mk)); const seen={}; labs.forEach(l=>{ const key=lblFlav(l)+"|"+(lblFmtSz(l)||"—"); if(!(key in seen) && !lblStatE[l.id]) seen[key]=l.id; }); return ( {labs.map(l=>{ const e=lblEdit[l.id]||{}, o=lblOrig[l.id]||{}, disc=!!lblStatE[l.id], sz=(lblFmtSz(l)||"—"), fl=lblFlav(l), isCur=seen[fl+"|"+sz]===l.id, mkts2=lblMktE[l.id]||[], mchg=(mkts2.join("+")!==(lblMktO[l.id]||[]).join("+")); const det=!!lblDet[l.id], ndet=LBL_DETAIL_FIELDS.filter(([f])=>(e[f]||"").trim()).length, dchg=LBL_DETAIL_FIELDS.some(([f])=>(e[f]||"")!==(o[f]||"")); const timg=imgCtysFor(lblMktE[l.id]).reduce((n,cd)=>n+imgCountCty(l.id,cd[0]),0); return ( {det && } {(!!lblImgOpen[l.id]) && } ); })} ); })} ); })} {lblShowUnlinked && lblUnlinked.length>0 && {lblUnlinked.map(l=>{ const disc=!!lblStatE[l.id]; return (); })} }
Product / LabelMarketDateArtwork namePDFActive
setLblOpen(s=>({...s,[pk]:!s[pk]}))}>{op?"▾":"▸"} {p.brand}{p.name} {mkts.map(mk=>{mk})} {all.length} label{all.length===1?"":"s"}
{mk} {!labs.length && no labels yet}
{l.label_number} {fl&&{fl}} {sz}{isCur&&CURRENT}{(e.product_sku||"").trim()&&
SKU {e.product_sku}
}
{["EU","UK","US"].map(m=>)} {l.date_added||"—"} lblSetF(l.id,"artwork_name",ev.target.value)}/> {lblArt[l.id] ? : } setLblStatE(s=>({...s,[l.id]:!ev.target.checked}))}/>
{LBL_DETAIL_FIELDS.map(([f,lab])=>())}
All optional — spec sheet ref links to the supplier's spec (food supplements); print specs can change per version. Remember to press Save.
{(()=>{ const ctys=imgCtysFor(lblMktE[l.id]); if(!ctys.length) return Toggle a market (EU / UK / US) on this label to add its images.; const cur=imgCurCty(l); const cd=ctys.find(x=>x[0]===cur)||ctys[0]; const cty=cd[0]; const nfilled=imgCountCty(l.id,cty); return
{ctys.map(cc=>{ const c=cc[0],nn=imgCountCty(l.id,c),stt=nn===0?"":(nn>=IMG_SLOTS.length?"done":"some"); return ; })}
{cd[1]} {cd[2]} · {cty} bound to {l.label_number} {nfilled}/{IMG_SLOTS.length} stored {!disc && }
{IMG_SLOTS.map(sl=>{ const g=lblImg[imgKey(l.id,cty,sl.k)]; const isMain=sl.k==="main"; return
{lblImgDrag.current={id:l.id,cty:cty,slot:sl.k};}):undefined} onDragOver={!disc?(ev=>{ev.preventDefault();}):undefined} onDrop={!disc?(ev=>{ev.preventDefault(); const d=lblImgDrag.current; if(d&&d.id===l.id&&d.cty===cty) reorderImg(l.id,cty,d.slot,sl.k); lblImgDrag.current=null;}):undefined} onClick={()=>{ if(!g&&!disc) pickImg(l.id,cty,sl.k); }}> {isMain?"MAIN":"#"+sl.k.slice(1)} {g?
{g.width}×{g.height}{g.status==="bad"?" ⚠":""}{!disc&&}
: {isMain?"Main":"Add"}}
; })}
Bound to label {l.label_number} · {cd[2]}. Genuinely separate images per country — drag a thumbnail to reorder (first = Main). Pushed to Amazon {cty} on the next listing publish for this market. Main image needs a pure-white background; every image must be ≥ 1000px.
; })()}
Unlinked legacy labels ({lblUnlinked.length}) — not tied to a product (mostly old Demon Labz / New Nature). Set the Product SKU in the workbook / assign later.
{l.label_number} {l.product||l.brand||""} {l.market||l.country||""} {l.date_added||"—"} {l.artwork_name||""} {lblArt[l.id]?:} setLblStatE(s=>({...s,[l.id]:!ev.target.checked}))}/>
}
} {view==="po" && {!poSel &&
{["open","due","all"].map(f=>)}
{msg &&
{msg.text}
}
Open balance
£{Math.round(poSummary.openBal).toLocaleString()}
Overdue
0?"var(--red)":undefined}}>£{Math.round(poSummary.overdue).toLocaleString()}
Due ≤30 days
£{Math.round(poSummary.d30).toLocaleString()}
Due 31–60 days
£{Math.round(poSummary.d60).toLocaleString()}
{poCash && poCash.codes.length>0 &&
Cash requirement forecast · GBP, goods only · per entity · outstanding timed by payment terms (no-terms POs by ETA)
{poCash.codes.map(c=>{ const e=poCash.ent[c]; return ; })}
EntityPrepaidOverdue≤1 wk≤2 wks≤4 wksLaterCash needed
{c} {gbp0(e.paid)} 0?"var(--red)":undefined,fontWeight:e.overdue>0?600:400}}>{gbp0(e.overdue)} {gbp0(e.w1)} {gbp0(e.w2)} {gbp0(e.w4)} {gbp0(e.later)} {gbp0(e.due)}
Total {gbp0(poCash.tot.paid)} 0?"var(--red)":undefined}}>{gbp0(poCash.tot.overdue)} {gbp0(poCash.tot.w1)} {gbp0(poCash.tot.w2)} {gbp0(poCash.tot.w4)} {gbp0(poCash.tot.later)} {gbp0(poCash.tot.due)}
} {poVisible.map(r=>(openPO(r.id)}> ))} {!poVisible.length && }
PO refSupplierEntityShip toStatusPO dateETATotalPaidDueNext due
{r.po_ref}{r.recv_pct>0&&{r.recv_pct===100?"received":"part"}} {poSupName(r.supplier_id)||} {r.buyer_code?{r.buyer_code}:} {r.region?{regionFlag(r.region)} {r.region}:} {r.status} {fmtD(r.po_date)||"—"} {fmtD(r.eta_date)||"—"} {money(r.total,r.currency)}{r.currency!=="GBP"&&
£{Math.round(r.total_gbp).toLocaleString()}
}
{r.paid>0.01?money(r.paid,r.currency):"—"} 0.01?"var(--orange)":"var(--green)"}}>{money(r.balance,r.currency)} {r.next_due_date?{fmtD(r.next_due_date)}
{money(r.next_due_amount,r.currency)}
:(r.balance>0.01?"no terms":"—")}
No purchase orders yet. Click + New PO.
} {poSel &&
{poPick &&
setPoPick(null)}>
e.stopPropagation()}>
Add products
{["EU","UK","US"].map(rg=>)}
{poSupName(poEdit.supplier_id)} · {poPick.region} products
setPoPick(p=>({...p,q:e.target.value}))}/> {!poPick.items &&
Loading…
} {poPick.items && poPick.items.filter(it=>{const q=(poPick.q||"").toLowerCase(); return !q||it.name.toLowerCase().includes(q)||it.sku.toLowerCase().includes(q);}).map(it=>( ))} {poPick.items && !poPick.items.length &&
No products for this supplier + region.
}
}
{poEdit.po_ref||"New PO"} {(()=>{ const rg=(poLocs.find(l=>l.id===Number(poEdit.ship_to_location_id))||{}).region; return rg?{regionFlag(rg)} {rg}:null; })()} {poEdit.id && } {poEdit.id && } {msg &&
{msg.text}
}
FBA shipments (one PO can ship in batches)
{(poSel.shipments||[]).map(s=>{s.ship_ref} {e.preventDefault();delPoShipment(s.id);}} style={{color:"#B42318",textDecoration:"none"}}>✕)} {poEdit.id ? setPoShipNew(e.target.value)} onKeyDown={e=>e.key==="Enter"&&addPoShipment()}/> : save PO first}
Linesgoods {money(poLineTotal,poEdit.currency)} · +charges {money(Number(poEdit.freight_amount||0)+Number(poEdit.duty_amount||0)+Number(poEdit.other_charges||0),poEdit.currency)}
{poLines.map((l,i)=>{ const cartons=l.units_per_box?Math.ceil(Number(l.qty||0)/l.units_per_box):null; return ; })} {!poLines.length && }
SKU / productQtyCartonsUnit priceLineLabelReceived
{l.sku||"(free)"} poLineField(i,"description",e.target.value)}/> poLineField(i,"qty",e.target.value)}/> {cartons!=null?cartons+" × "+l.units_per_box:"—"} poLineField(i,"unit_price",e.target.value)}/> {money(Number(l.qty||0)*Number(l.unit_price||0),poEdit.currency)} {l.label_number?{l.label_number}:} {l.id?{l.received_qty||0}/{l.qty}{l.sku&&poInbRecv[l.sku]!=null&& · Amz {poInbRecv[l.sku]}}{l.sku&&poInbLot[l.sku]&&(poInbLot[l.sku].lot||poInbLot[l.sku].exp)&&{poInbLot[l.sku].lot?" · lot "+poInbLot[l.sku].lot:""}{poInbLot[l.sku].exp?" · BBE "+poInbLot[l.sku].exp:""}} {Number(l.received_qty||0)receiveLine(l)}>Receive} {Number(l.received_qty||0)>0&&}:save first}
No lines — add a product.
{poEdit.id && (poSel.inbound||[]).length>0 &&
Inbound (Amazon) {(poSel.inbound||[]).map((sh,ix)=> sh.missing ? {sh.shipment_id} not synced yet : {(sh.shipment_status||"").replace(/_/g," ")}{sh.shipment_id}{sh.destination_fc?" → "+sh.destination_fc:""})}
} {poEdit.id &&
Payments paid {money(poSel.payments.reduce((a,p)=>a+Number(p.amount||0),0),poEdit.currency)}
{(poSel.payments||[]).map(p=>())} {!(poSel.payments||[]).length && }
DateTypeAmountRef
{fmtD(p.pay_date)}{p.kind}{money(p.amount,p.currency)}{p.ref||""}
No payments recorded.
{(()=>{ const rr=(poRows||[]).find(x=>x.id===poEdit.id); const out=(rr&&rr.outstanding)||[]; if(!out.length) return
Payment schedule: no terms set for this supplier (add them on the supplier) or nothing outstanding.
; return
Payment schedulefrom supplier terms · {rr.region} · anchored on {poEdit.invoice_date?"invoice date":(poEdit.eta_date?"ETA":"PO date")}
{out.map((o,i)=>())}
Due dateTermsAmount due
{fmtD(o.due_date)||"—"}{o.pct}% · {o.basis}{o.months?" +"+o.months+"m":""}{o.days?" +"+o.days+"d":""}{money(o.due,poEdit.currency)}
; })()}
Documents
{(poSel.documents||[]).map(d=>())} {!(poSel.documents||[]).length && }
DescriptionFileAdded
{d.description||}{fmtD(d.created_at)}
No documents. Upload proforma / commercial invoices etc.
} {!poEdit.id &&
Save the PO to add payments and receive stock.
}
}
} {view==="lblorders" && {!loData ?
{loading?"Loading…":""}
: (()=>{ const q=loQ.trim().toLowerCase(); const open=loData.orders.filter(o=>!o.archived); const rows=loData.orders.filter(o=>{ if(loFilt==="open" && o.archived) return false; if(loFilt==="proof" && !(o.artwork_sent_date && !o.proof_approved_date && !o.archived)) return false; if(loFilt==="transit" && !(o.ups_sent_date && !o.collection_booked_date && !o.archived)) return false; if(loFilt==="archived" && !o.archived) return false; if(q){ const hit=(o.po_ref||"").toLowerCase().includes(q)||loSupName(o.ship_to_supplier_id).toLowerCase().includes(q)||(o.lines||[]).some(l=>(l.label_number||"").toLowerCase().includes(q)||(l.artwork_name||"").toLowerCase().includes(q)); if(!hit) return false; } return true; }); const awaitingProof=open.filter(o=>o.artwork_sent_date&&!o.proof_approved_date).length; const inTransit=open.filter(o=>o.ups_sent_date&&!o.collection_booked_date).length; const overdueN=open.filter(o=>loOverdue(o)>0).length; const pq2=loProof.trim().toLowerCase(); const proofHit=pq2? loData.orders.find(o=>!o.archived && (o.lines||[]).some(l=>(l.label_number||"").toLowerCase()===pq2)) : null; const stepper=(o)=>{ const d=loLastDone(o); return {LO_STEPS.map((s,i)=>{ const done=i<=d, cur=i===d+1&&!o.archived; return {i>0&&}; })}; }; return
Order printed labels from a printer, approve the proof, and ship them to the factory. Only active factories where "we ship labels to supplier = Yes" appear. Creating an order also opens a linked printer service PO in Purchasing (cost blank until you're quoted). Lifecycle: Ordered → Artwork sent → Proof approved → Labels shipped → Collection booked.
Open orders
{open.length}
Awaiting proof
{awaitingProof}
In transit to factory
{inTransit}
Overdue / stuck
0?"var(--red)":undefined}}>{overdueN}
{[["open","Active"],["proof","Awaiting proof"],["transit","In transit"],["archived","Archived"]].map(kv=>)}
setLoQ(e.target.value)}/> {msg &&
{msg.text}
}
{loNew &&
New label order
Ship to factory Printer
Show on PO: {LO_SPECS.map(s=>{ const on=(loNew.spec_include||[]).includes(s[0]); return ; })}
{(() => { const opts=(loData.labels||[]).filter(l=>!loNew.factory || (l.supplier_ids||[]).includes(Number(loNew.factory))); return (loNew.lines||[]).map((ln,i)=>
{const v=e.target.value.replace(/[^0-9]/g,""); setLoNew(n=>{const L=[...n.lines]; L[i]={...L[i],quantity:v}; return {...n,lines:L};});}}/>
); })()} {loNew.factory && !(loData.labels||[]).some(l=>(l.supplier_ids||[]).includes(Number(loNew.factory))) &&
No labels are linked to this factory's products yet (set the factory as the SKUs' supplier in Products).
}
}
{rows.map(o=>{ const op=loOpen[o.id]; const st=loStage(o); const ov=loOverdue(o); const d=loLastDone(o); const qty=(o.lines||[]).reduce((a,l)=>a+Number(l.quantity||0),0); return setLoOpen(s=>({...s,[o.id]:!s[o.id]}))}> {op && } ; })} {!rows.length && }
PO refFactoryPrinterLabelsQtyProgressOrderedNext actionStatus
{op?"▾":"▸"}{o.po_ref} {loFlag(o.ship_to_supplier_id)} {loSupName(o.ship_to_supplier_id)} {loSupName(o.printer_supplier_id)} {(o.lines||[]).length} {qty.toLocaleString()} {stepper(o)} {fmtD(o.order_date)||"—"} 0?"var(--red)":"var(--orange)"}}>{o.archived?"—":loNext(o)}{ov>0?" · "+ov+"d":""} {st.lbl}
{LO_STEPS.map((s,i)=>
{s[1]}
{o[s[0]]?fmtD(o[s[0]]):"—"}
)}
{o.ups_tracking &&
UPS tracking: {o.ups_tracking}
} {ov>0 &&
⚠ Proof not approved for {ov} days — chase the printer.
} {(o.lines||[]).map((l,ii)=>)}
LabelArtworkQty
{l.label_number||"—"}{l.artwork_name||"—"}{Number(l.quantity||0).toLocaleString()}
Proof & documents
{(o.documents||[]).length? (o.documents||[]).map(dc=>) : No proof uploaded yet.}
{o.artwork_sent_date && !o.proof_approved_date && !o.archived &&
Proof check {(o.lines||[]).map(l=>{ const sp=(loData.labels||[]).find(x=>x.id===l.label_id)||{}; const a=pcAns[l.id]||l.proof_check||{}; const setA=(k,v)=>setPcAns(s=>({...s,[l.id]:{...(s[l.id]||l.proof_check||{}),[k]:v}})); const ok=proofOk(a); return
{l.label_number} {l.artwork_name} {l.proof_result && {l.proof_result.toUpperCase()}}
✓ Label is on order  ·  ✓ Awaiting approval (auto)
{PROOF_Q.map(q=>{ let txt=q.txt; if(q.k==="artwork"&&l.artwork_name) txt="Is this the correct artwork — “"+l.artwork_name+"”?"; if(q.k==="size"&&sp.dimensions) txt="Does the size match the spec — “"+sp.dimensions+"”?"; return
{txt}
; })}
Outcome: {ok?"PASS":"not yet"}
; })}
Approve the proof (button below) once every label passes.
}
{!o.archived && !o.artwork_sent_date && } {!o.archived && o.artwork_sent_date && !o.proof_approved_date && (()=>{ const allPass=(o.lines||[]).length>0 && (o.lines||[]).every(l=>l.proof_result==="pass"); return ; })()} {!o.archived && o.proof_approved_date && !o.ups_sent_date && {o._trk=e.target.value;}}/> } {!o.archived && o.ups_sent_date && !o.collection_booked_date && } {!o.archived && o.collection_booked_date && } {o.archived && } {o.linked_po_id && }
{loData.orders.length?"No orders match.":"No label orders yet — click + New label order."}
Proof check setLoProof(e.target.value)}/> {loProof.trim() && (proofHit ? ✓ On order — {proofHit.po_ref} ({loStage(proofHit).lbl}) : ✗ Not on any open order)}
; })()}
} {view==="names" &&
setPq(e.target.value)} style={{minWidth:200}}/> {msg &&
{msg.text}
}
One row per ASIN: the short name shown in the Days Cover tab (e.g. "APS - Rosemary - 100g") and a per-market Included flag. The same ASIN sells in both UK and EU, so include/exclude it separately per region — untick EU, UK or US to hide it from that pool's Days Cover only. Changes apply to everyone and take effect immediately on save.
{!prAsins ?
{loading?"Loading…":""}
:
{prVisible.map(a=>{ const e=editP[a], o=origP[a]; const chg=(k)=>e[k]!==o[k]; return (); })}
EUUKUSShort nameASINNote
setPr(a,"eu",!ev.target.checked)}/> setPr(a,"uk",!ev.target.checked)}/> setPr(a,"us",!ev.target.checked)}/> setPr(a,"short_name",ev.target.value)}/> {a} setPr(a,"note",ev.target.value)}/>
}
} {view==="suppliers" &&
setVQ(e.target.value)} style={{minWidth:200}}/> {msg &&
{msg.text}
}
Lead time is the stock lead time in weeks (used for reordering decisions). Click to edit the full record — address, VAT/EORI, FDA details, label notes. Supplier assignments per SKU are set in the Costs view.
{!vRows ?
{loading?"Loading…":""}
:
{vVisible.map(r=>{ const open=!!vOpen[r.id]; const inp=(f,w,ta)=>setSupField(r.id,f,ev.target.value)}/>; return ( {open && } ); })}
ActiveCodeFull nameLead time (wks)CurrencyCountryLabels byEmail
setVOpen(o=>({...o,[r.id]:!o[r.id]}))}>{open?"▾":"▸"} setSupField(r.id,"active",ev.target.checked)}/> {inp("code",150)} {inp("full_name",220)} {inp("lead_time_weeks",56,"right")} {inp("currency",52)} {inp("country_code",52)} {inp("labels_supplied_by",170)} {inp("notification_email",220)}
{SUP_MORE.map(([f,label])=>)}
Payment terms — installments used to forecast cash-due on POs. A region (EU/UK/US) overrides “All regions”. Basis = counts from the PO date or the invoice date, plus a months / days offset.
{[["default","All regions"],["EU","EU"],["UK","UK"],["US","US"]].filter(([rk])=>rk==="default"||supTerms(r)[rk]).map(([rk,rlabel])=>(
{rlabel}{rk!=="default"&&}
{(supTerms(r)[rk]||[]).map((it,ix)=>(
updInstall(r,rk,ix,"pct",Number(e.target.value))}/>% +updInstall(r,rk,ix,"months",Number(e.target.value))}/>mo updInstall(r,rk,ix,"days",Number(e.target.value))}/>days
))}
))}
{["EU","UK","US"].filter(rk=>!supTerms(r)[rk]).map(rk=>)}
{(()=>{ const t=supTerms(r); const sum=(t.default||[]).reduce((a,x)=>a+(Number(x.pct)||0),0); return (t.default&&sum!==100)?
All-regions installments add up to {sum}% (usually should be 100%).
:null; })()}
Documents {String(r.id).startsWith("new")&&save the supplier first}
{(vDocs[r.id]||[]).map(d=>(
{d.description||"—"} {fmtD(d.created_at)}
))} {!(vDocs[r.id]||[]).length &&
No documents (e.g. EU organic certificate, FDA registration, insurance).
}
}
} {view==="vat" &&
setVatQ(e.target.value)} style={{minWidth:200}}/> {vatRows && {vatTotalOver} overcharge flag(s)} {msg &&
{msg.text}
}
Applied VAT per product & marketplace, derived from order lines (zero-rated included) and snapped to legal bands. Rules — UK: food/oil 0%, supplements 20% · DE 7% · FR 5.5% · IT/ES 10% (olive oil 4%) · NL 9%.  Red = overcharged (click for 5 example orders) · amber = under/zero · green = correct · — = no taxed sales. Category is auto-set from brand (APSOGO = food); verify a flagged ASIN's tax code in Seller Central before filing.
{!vatRows ?
{loading?"Loading…":""}
:
{MK.map(m=>
{MKLBL[m]}
{vatOverByMk[m]}
overcharge flags
)}
{MK.map(m=>)} {vatPivot.map(p=>( {MK.map(m=>{ const c=p.cells[m]; if(!c) return ; const st=c.status, cls=st==="ok"?"vok":st==="over"?"vover":"vunder"; const title=`applied ${c.applied_rate}% vs expected ${c.expected_rate}% · ${c.total_lines} lines · ${c.conf_pct}% agree`; return ; })} ))}
ProductType{MKLBL[m]}
{p.name} {p.asin} {catLabel(p.category)}openVat(c):undefined}> {c.applied_rate}%{st!=="ok" && want {c.expected_rate}%}
} {vatModal &&
{ if(e.target.className==="vmodal-ovl") setVatModal(null); }}>
setVatModal(null)}>✕
{vatModal.product_name}
{MKLBL[vatModal.market]} · charged ~{vatModal.applied_rate}% · should be {vatModal.expected_rate}%  Amazon: MarketplaceFacilitator
{vatExLoad ?
Loading example orders…
: (!vatEx || !vatEx.length) ?
No example orders found at that rate.
: (()=>{ const c=CURSYM[vatModal.market]||""; let tot=0; const body=vatEx.map((r,i)=>{ const net=Number(r.item_price)-Number(r.item_tax); const correct=net*Number(vatModal.expected_rate)/100; const oc=Number(r.item_tax)-correct; tot+=oc; return {r.order_id} {r.qty} {c}{Number(r.item_price).toFixed(2)} {c}{Number(r.item_tax).toFixed(2)} {r.rate}% {c}{correct.toFixed(2)} {c}{oc.toFixed(2)} ; }); return {body}
Order IDQtyPaidVATRateVAT @{vatModal.expected_rate}%Over
~{c}{tot.toFixed(2)} overcharged across these {vatEx.length} orders. All Amazon-collected (MarketplaceFacilitator) — an Amazon product-tax-code setting. Order IDs are copy-ready for a Seller Central case.
; })()}
}
} {view==="paneu" &&
setPeuQ(e.target.value)} style={{minWidth:200}}/> {peuRows && {peuTotalEfn} not-Pan-EU flag(s)} {msg &&
{msg.text}
}
Per product & EU marketplace: Amazon's estimated local FBA fee vs the typical (median) fee actually paid (last {PEU_DAYS} days, from settlements). When stock is held locally (Pan-EU) the two match; a persistent per-unit gap means Amazon is shipping cross-border (EFN) — i.e. the item is not enrolled in Pan-EU there and you're overpaying.  green = Pan-EU (local) · red = cross-border (click for the €/unit penalty) · amber "est?" = Amazon's fee estimate is unreliable for this item (off even in the home market — can't judge) · — = no recent sales. Tick Excl for items not meant to be Pan-EU (oils/liquids, market-limited lines). Median is used so a one-off batch of mis-charged units doesn't skew the picture. UK/US/IE excluded by design.  The badge next to each product is Amazon's own Pan-EU eligibility report (refreshed daily): Pan-EU ✓ = enrolled · eligible · not enrolled = money on the table · grey = ineligible (hover a cell for Amazon's per-market reason, e.g. "Food").
{!peuRows ?
{loading?"Loading…":""}
:
{PMK.map(m=>
{PMKLBL[m]}
{peuEfnByMk[m]}
not Pan-EU
)}
{PMK.map(m=>)} {peuPivot.map(p=>{ const ex=isExcl(p.asin); return ( {PMK.map(m=>{ const c=p.cells[m]; if(!c || c.status==="nodata") return ; const efn=c.status==="efn", chk=c.status==="checkest", cls=efn?"vover":chk?"vunder":"vok"; const amz=c.mk_offer?` · Amazon: ${c.mk_offer}`:""; const title=(chk ? `Amazon's fee estimate (€${c.est_fee}) looks wrong for this item — even in the home market the actual is €${c.actual_fee}. Can't judge Pan-EU from a bad estimate.` : `actual €${c.actual_fee}/unit vs est €${c.est_fee} · ${c.units} units · gap €${c.gap}`)+amz; return ; })} ); })}
ProductExcl{PMKLBL[m]}
{p.name} {p.asin}{peuBadge(p)} togglePeuExcl(p.asin,e.target.checked)} title="Exclude this product from Pan-EU flags"/>setPeuModal({...c}):undefined}> €{c.actual_fee}{efn && +€{c.gap}}{chk && est?}
} {peuModal &&
{ if(e.target.className==="vmodal-ovl") setPeuModal(null); }}>
setPeuModal(null)}>✕
{peuModal.product_name}
{PMKLBL[peuModal.market]} · not Pan-EU · cross-border
{peuModal.enrol_status!=null && } {peuModal.mk_offer && }
Estimated local FBA fee€{peuModal.est_fee}
Actual fee paid / unit€{peuModal.actual_fee}
Cross-border penalty / unit+€{peuModal.gap}
Units (last {PEU_DAYS} days){peuModal.units}
Est. extra fees paid€{(Number(peuModal.gap)*Number(peuModal.units)).toFixed(2)}
Amazon Pan-EU report{peuModal.enrol_status}{peuModal.enrolled===true?" · enrolled":" · not enrolled"}
Offer status here{peuModal.mk_offer}
You're paying ~€{peuModal.gap}/unit more than the local {PMKLBL[peuModal.market]} fulfilment fee — the EFN cross-border surcharge. This ASIN is eligible but not enrolled in Pan-EU for {PMKLBL[peuModal.market]}. Enrol it (Seller Central → Pan-European FBA → offers) so stock is held locally and the surcharge disappears. If this product isn't meant to be Pan-EU, tick Excl to stop flagging it.
}
} {view==="products" &&
setHubQ(e.target.value)} style={{minWidth:220}}/> Cost currencysetHubCcy(e.target.value.toUpperCase())} style={{width:58}}/> Cost fromsetHubEff(e.target.value)}/> {msg &&
{msg.text}
}
One row per SKU, grouped by product — the single place for identity, cost and logistics. Editable product name per header (same name on two products merges them on reload). Untick the header checkbox to discontinue a product, or a SKU’s checkbox for one variant. Group an ungrouped SKU with the dropdown by its code, or move any SKU between products. Set the Cost + a “Cost from” date to add a dated entry — each change keeps history (profit uses the cost in effect on the order date). Click on a SKU for its cost history, per-region shipping, and supplier.
{!hubRows ?
{loading?"Loading…":""}
:
{hubVisible.map(g=>{ const gks=g.gks, skus=g.skus.map(r=>r.sku), grouped=gks.length>0; const vrows = hubShowDisc ? g.skus : g.skus.filter(r=>!hubSkuExE[r.sku]); if(!vrows.length) return null; const disc = grouped ? gks.every(k=>hubStatE[k]) : (skus.length>0 && skus.every(s=>hubSkuExE[s])); const dispVal = grouped? (hubDispE[gks[0]]??"") : ""; const dispChg = gks.some(k=>(hubDispE[k]||"")!==(hubDispO[k]||"")); const setDisc=v=> grouped ? setHubStatE(s=>{const n={...s};gks.forEach(k=>n[k]=v);return n;}) : setHubSkuExE(s=>{const n={...s};skus.forEach(k=>n[k]=v);return n;}); const setDisp=v=>setHubDispE(s=>{const n={...s};gks.forEach(k=>n[k]=v);return n;}); const assignedGk=(skus.length&&hubGroupE[skus[0]])||""; const assignGroup=gk=>setHubGroupE(s=>{const n={...s};skus.forEach(k=>{ if(gk) n[k]=gk; else delete n[k]; });return n;}); return ( {vrows.map(r=>{ const e=hubEdit[r.sku]||{}, o=hubOrig[r.sku]||{}; const sx=!!hubSkuExE[r.sku]; const cell=(f,w)=>setHub(r.sku,f,ev.target.value)}/>; const cd=(hubCost[r.sku]&&hubCost[r.sku].from&&hubCost[r.sku].from!=="2000-01-01")?hubCost[r.sku].from:""; const op=!!hubOpen[r.sku]; return ( {op && } ); })} ); })}
SKUASINMarketEANPackagingSize / FlavourLabel codeLabel byCostUpdated
{g.brand} {grouped ? setDisp(e.target.value)} title="Product-level name. Give two products the same name to merge them (on reload)."/> : {g.name} ungrouped } {disc && discontinued} {g.skus.length>1 && } {g.skus.length} SKU{g.skus.length>1?"s":""}
setHubOpen(s=>({...s,[r.sku]:!s[r.sku]}))}>{op?"▾":"▸"}{r.sku} {cell("asin",108)} {cell("market_scope",66)} {cell("ean",118)} {cell("packaging",150)} {cell("size_flavour",130)} {cell("label_code",84)} setHub(r.sku,"unit_cost",ev.target.value)}/> {hubCost[r.sku]?hubCost[r.sku].ccy:""} {cd||"—"}
Cost history
{(hubHist[r.sku]||[]).length? (hubHist[r.sku]).map((h,i)=>
{(h.from&&h.from!=="2000-01-01")?h.from:"(all history)"}: {h.cost} {h.ccy}
) :
— no cost yet —
}
Change the Cost cell + set “Cost from” above to add a dated entry.
Supplier
Box layout (units/box) is now in Supply Chain → Packing.
}
}
); } ReactDOM.createRoot(document.getElementById("root")).render();