{"id":19,"date":"2026-08-29T10:55:02","date_gmt":"2026-08-29T10:55:02","guid":{"rendered":"https:\/\/thatshop.kdlink.dev\/?page_id=19"},"modified":"2026-08-29T10:57:23","modified_gmt":"2026-08-29T10:57:23","slug":"stock","status":"publish","type":"page","link":"https:\/\/thatshop.kdlink.dev\/?page_id=19","title":{"rendered":"stock"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"19\" class=\"elementor elementor-19\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4ce04e8 e-con-full e-flex e-con e-parent\" data-id=\"4ce04e8\" data-element_type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-076e2ad elementor-widget elementor-widget-html\" data-id=\"076e2ad\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n<meta charset=\"utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n<title>StockFloor - Warehouse control<\/title>\r\n<style>\r\n  html, body { margin:0; padding:0; background:#E9EDF1; }\r\n  #boot { font-family: Arial, Helvetica, sans-serif; padding:56px 24px; text-align:center; color:#64757F; line-height:1.6; }\r\n  #boot b { display:block; font-size:22px; color:#14202C; margin-bottom:10px; letter-spacing:-.02em; }\r\n  #boot small { display:block; margin-top:6px; font-size:12px; }\r\n<\/style>\r\n<\/head>\r\n<body>\r\n<div id=\"root\"><div id=\"boot\"><b>StockFloor<\/b>Starting up<small>This takes a couple of seconds the first time.<\/small><\/div><\/div>\r\n\r\n<script type=\"text\/babel\" data-presets=\"react\" data-type=\"module\">\r\nconst { useState, useEffect, useRef, useMemo } = React;\r\n\r\n\/* ---- storage shim: data stays in this browser only ---- *\/\r\nconst store = {\r\n  get: (k) => Promise.resolve({ value: localStorage.getItem(k) }),\r\n  set: (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} return Promise.resolve(); },\r\n};\r\n\r\n\r\n\/* ============================================================\r\n   CODE 128-B \u2014 real, scannable barcode rendering\r\n   ============================================================ *\/\r\nconst C128 = [\r\n  \"212222\",\"222122\",\"222221\",\"121223\",\"121322\",\"131222\",\"122213\",\"122312\",\"132212\",\"221213\",\r\n  \"221312\",\"231212\",\"112232\",\"122132\",\"122231\",\"113222\",\"123122\",\"123221\",\"223211\",\"221132\",\r\n  \"221231\",\"213212\",\"223112\",\"312131\",\"311222\",\"321122\",\"321221\",\"312212\",\"322112\",\"322211\",\r\n  \"212123\",\"212321\",\"232121\",\"111323\",\"131123\",\"131321\",\"112313\",\"132113\",\"132311\",\"211313\",\r\n  \"231113\",\"231311\",\"112133\",\"112331\",\"132131\",\"113123\",\"113321\",\"133121\",\"313121\",\"211331\",\r\n  \"231131\",\"213113\",\"213311\",\"213131\",\"311123\",\"311321\",\"331121\",\"312113\",\"312311\",\"332111\",\r\n  \"314111\",\"221411\",\"431111\",\"111224\",\"111422\",\"121124\",\"121421\",\"141122\",\"141221\",\"112214\",\r\n  \"112412\",\"122114\",\"122411\",\"142112\",\"142211\",\"241211\",\"221114\",\"413111\",\"241112\",\"134111\",\r\n  \"111242\",\"121142\",\"121241\",\"114212\",\"124112\",\"124211\",\"411212\",\"421112\",\"421211\",\"212141\",\r\n  \"214121\",\"412121\",\"111143\",\"111341\",\"131141\",\"114113\",\"114311\",\"411113\",\"411311\",\"113141\",\r\n  \"114131\",\"311141\",\"411131\",\"211412\",\"211214\",\"211232\",\"2331112\",\r\n];\r\n\r\nfunction encode128B(data) {\r\n  const codes = [104];\r\n  for (const ch of String(data)) {\r\n    const v = ch.charCodeAt(0) - 32;\r\n    codes.push(v >= 0 && v < 95 ? v : 0);\r\n  }\r\n  let sum = 104;\r\n  for (let i = 1; i < codes.length; i++) sum += codes[i] * i;\r\n  codes.push(sum % 103);\r\n  codes.push(106);\r\n  return codes;\r\n}\r\n\r\nfunction Barcode({ value, height = 46, module = 1.5 }) {\r\n  const bars = [];\r\n  let x = 12;\r\n  encode128B(value).forEach((c) => {\r\n    let isBar = true;\r\n    for (const w of C128[c]) {\r\n      const width = parseInt(w, 10) * module;\r\n      if (isBar) bars.push(<rect key={`${x}-${width}`} x={x} y={0} width={width} height={height} \/>);\r\n      x += width;\r\n      isBar = !isBar;\r\n    }\r\n  });\r\n  return (\r\n    <svg viewBox={`0 0 ${x + 12} ${height}`} width=\"100%\" height={height}\r\n         preserveAspectRatio=\"xMidYMid meet\" fill=\"#000\" shapeRendering=\"crispEdges\">\r\n      {bars}\r\n    <\/svg>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   HELPERS\r\n   ============================================================ *\/\r\nconst ORIGINS = [\"India\", \"China\", \"Vietnam\", \"UAE\", \"Turkey\", \"Bangladesh\", \"Germany\"];\r\nconst ORIGIN_CODE = { India: \"IN\", China: \"CN\", Vietnam: \"VN\", UAE: \"AE\", Turkey: \"TR\", Bangladesh: \"BD\", Germany: \"DE\" };\r\nconst EXIT_REASONS = [\"Sale\", \"Damage\", \"Return to supplier\", \"Sample\", \"Internal use\"];\r\nconst STORE_KEY = \"stockfloor:v3\";\r\n\r\nconst pad = (n, l) => String(n).padStart(l, \"0\");\r\nconst nowISO = () => new Date().toISOString();\r\n\r\nfunction fmt(iso) {\r\n  return new Date(iso).toLocaleString(\"en-GB\", { day: \"2-digit\", month: \"short\", hour: \"2-digit\", minute: \"2-digit\", hour12: true });\r\n}\r\nfunction fmtDay(iso) {\r\n  return new Date(iso).toLocaleDateString(\"en-GB\", { day: \"2-digit\", month: \"short\", year: \"numeric\" });\r\n}\r\nfunction isToday(iso) {\r\n  return new Date(iso).toDateString() === new Date().toDateString();\r\n}\r\nfunction daysAgo(iso) {\r\n  return (Date.now() - new Date(iso).getTime()) \/ 86400000;\r\n}\r\nfunction makeSKU(name, origin, seq) {\r\n  const base = (name || \"ITM\").replace(\/[^a-zA-Z]\/g, \"\").toUpperCase().slice(0, 3).padEnd(3, \"X\");\r\n  return `${base}-${ORIGIN_CODE[origin] || \"XX\"}-${pad(seq, 4)}`;\r\n}\r\nfunction makeBarcode(seq) {\r\n  const d = new Date();\r\n  const stamp = `${String(d.getFullYear()).slice(2)}${pad(d.getMonth() + 1, 2)}${pad(d.getDate(), 2)}`;\r\n  return `SB${stamp}${pad(seq, 4)}`;\r\n}\r\n\r\n\/* seed data spread across a few days so the date filters are visible *\/\r\nconst SEED = (() => {\r\n  const t = Date.now();\r\n  let n = 0;\r\n  const mk = (sku, name, desc, wt, org, hoursAgo, status) => {\r\n    n++;\r\n    const created = new Date(t - hoursAgo * 3600000);\r\n    const stamp = `${String(created.getFullYear()).slice(2)}${pad(created.getMonth() + 1, 2)}${pad(created.getDate(), 2)}`;\r\n    return {\r\n      barcode: `SB${stamp}${pad(n, 4)}`, sku, name, description: desc, weight: wt, origin: org,\r\n      status, mode: n % 3 === 0 ? \"manual\" : \"auto\",\r\n      createdAt: created.toISOString(),\r\n      exitedAt: status === \"out\" ? new Date(t - (hoursAgo - 3) * 3600000).toISOString() : null,\r\n      exitReason: status === \"out\" ? \"Sale\" : null,\r\n    };\r\n  };\r\n  return [\r\n    mk(\"COT-IN-0001\", \"Cotton bedsheet set\", \"Double bed, king size, 210 TC\", \"2.4 kg\", \"India\", 2, \"in\"),\r\n    mk(\"COT-IN-0001\", \"Cotton bedsheet set\", \"Double bed, king size, 210 TC\", \"2.4 kg\", \"India\", 2, \"in\"),\r\n    mk(\"COT-IN-0001\", \"Cotton bedsheet set\", \"Double bed, king size, 210 TC\", \"2.4 kg\", \"India\", 2, \"out\"),\r\n    mk(\"ALU-TR-0004\", \"Aluminium foil roll\", \"9 metre, heavy duty catering grade\", \"0.7 kg\", \"Turkey\", 5, \"in\"),\r\n    mk(\"LED-CN-0002\", \"LED bulb 9W\", \"Carton of 50 bulbs, cool white\", \"6.1 kg\", \"China\", 30, \"in\"),\r\n    mk(\"LED-CN-0002\", \"LED bulb 9W\", \"Carton of 50 bulbs, cool white\", \"6.1 kg\", \"China\", 30, \"in\"),\r\n    mk(\"STE-AE-0003\", \"Steel water bottle\", \"1 litre, vacuum insulated\", \"0.9 kg\", \"UAE\", 96, \"in\"),\r\n    mk(\"STE-AE-0003\", \"Steel water bottle\", \"1 litre, vacuum insulated\", \"0.9 kg\", \"UAE\", 96, \"out\"),\r\n    mk(\"DEN-BD-0005\", \"Denim jacket\", \"Mens, mixed sizes M to XL, 12 pieces\", \"8.3 kg\", \"Bangladesh\", 260, \"in\"),\r\n  ];\r\n})();\r\n\r\nconst SEED_MOVES = SEED.flatMap((u) => {\r\n  const arr = [{ id: `${u.barcode}-in`, type: \"IN\", barcode: u.barcode, sku: u.sku, name: u.name, at: u.createdAt, note: u.mode === \"auto\" ? \"Automatic entry\" : \"Manual entry\" }];\r\n  if (u.status === \"out\") arr.push({ id: `${u.barcode}-out`, type: \"OUT\", barcode: u.barcode, sku: u.sku, name: u.name, at: u.exitedAt, note: \"Sale\" });\r\n  return arr;\r\n}).sort((a, b) => new Date(b.at) - new Date(a.at));\r\n\r\n\/* ============================================================\r\n   STYLES\r\n   ============================================================ *\/\r\nconst CSS = `\r\n@import url('https:\/\/fonts.googleapis.com\/css2?family=Archivo:wght@500;600;800&family=JetBrains+Mono:wght@400;700&display=swap');\r\n\r\n.sf * { box-sizing: border-box; margin: 0; padding: 0; }\r\n.sf {\r\n  --ink:#14202C; --line:#C9D2DA; --line2:#E2E8ED;\r\n  --bg:#E9EDF1; --surf:#FFFFFF; --mute:#64757F;\r\n  --amber:#FFB300; --amber-d:#B87C00;\r\n  --in:#0F7A4A; --out:#C1322C;\r\n  --disp:'Archivo','Helvetica Neue',Arial,sans-serif;\r\n  --mono:'JetBrains Mono',ui-monospace,'SF Mono',Menlo,monospace;\r\n  font-family: var(--disp); color: var(--ink); background: var(--bg);\r\n  min-height:100vh; display:flex; -webkit-font-smoothing:antialiased;\r\n}\r\n.sf button, .sf input, .sf select, .sf textarea { font: inherit; color: inherit; }\r\n.sf :focus-visible { outline:2px solid var(--amber); outline-offset:2px; }\r\n@media (prefers-reduced-motion: reduce){ .sf *{ transition:none !important; animation:none !important; } }\r\n\r\n\/* nav rail *\/\r\n.rail { width:214px; flex-shrink:0; background:var(--ink); color:#fff; display:flex; flex-direction:column;\r\n  position:sticky; top:0; height:100vh; }\r\n.brand { padding:20px 18px 16px; border-bottom:1px solid rgba(255,255,255,.1); }\r\n.brand b { font-size:19px; font-weight:800; letter-spacing:-.02em; display:block; }\r\n.brand span { font-family:var(--mono); font-size:9.5px; color:var(--amber); letter-spacing:.16em; text-transform:uppercase; }\r\n.nav { padding:10px 8px; flex:1; display:flex; flex-direction:column; gap:2px; }\r\n.nav button { display:flex; align-items:center; gap:9px; width:100%; text-align:left; background:none; border:0;\r\n  padding:9px 11px; border-radius:6px; color:#A9B8C4; font-size:13.5px; font-weight:500; cursor:pointer; }\r\n.nav button:hover { background:rgba(255,255,255,.07); color:#fff; }\r\n.nav button.on { background:var(--amber); color:var(--ink); font-weight:600; }\r\n.nav .glyph { font-family:var(--mono); font-size:11px; width:15px; }\r\n.railfoot { padding:12px 16px; border-top:1px solid rgba(255,255,255,.1); font-size:11.5px; color:#7D8F9C; }\r\n.railfoot button { background:none; border:0; color:#7D8F9C; cursor:pointer; text-decoration:underline; font-size:11.5px; padding:0; margin-top:3px; }\r\n\r\n\/* page *\/\r\n.main { flex:1; min-width:0; }\r\n.head { background:var(--surf); border-bottom:1px solid var(--line); padding:16px 26px; display:flex;\r\n  justify-content:space-between; align-items:flex-end; gap:16px; flex-wrap:wrap; }\r\n.head h1 { font-size:21px; font-weight:800; letter-spacing:-.02em; }\r\n.head p { font-size:12.5px; color:var(--mute); margin-top:2px; }\r\n.wrap { padding:22px 26px 60px; max-width:1220px; }\r\n\r\n\/* primitives *\/\r\n.card { background:var(--surf); border:1px solid var(--line); border-radius:8px; }\r\n.card > h3 { font-size:12px; text-transform:uppercase; letter-spacing:.09em; color:var(--mute);\r\n  padding:13px 16px; border-bottom:1px solid var(--line2); font-weight:600; }\r\n.pad { padding:16px; }\r\n.btn { border:1px solid var(--ink); background:var(--ink); color:#fff; padding:9px 16px; border-radius:6px;\r\n  font-size:13.5px; font-weight:600; cursor:pointer; }\r\n.btn:hover { background:#24384A; }\r\n.btn.amber { background:var(--amber); border-color:var(--amber); color:var(--ink); }\r\n.btn.amber:hover { background:#FFC233; }\r\n.btn.ghost { background:transparent; color:var(--ink); border-color:var(--line); }\r\n.btn.ghost:hover { background:#F1F5F8; }\r\n.btn.sm { padding:6px 11px; font-size:12px; }\r\n.btn:disabled { opacity:.4; cursor:not-allowed; }\r\n.tag { font-family:var(--mono); font-size:10.5px; font-weight:700; padding:2.5px 7px; border-radius:3px;\r\n  letter-spacing:.04em; display:inline-block; }\r\n.tag.in { background:#DFF3E9; color:var(--in); }\r\n.tag.out { background:#FBE4E3; color:var(--out); }\r\n.mono { font-family:var(--mono); font-size:12.5px; }\r\n\r\n\/* stats *\/\r\n.stats { display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; margin-bottom:20px; }\r\n.stat { background:var(--surf); border:1px solid var(--line); border-radius:8px; padding:15px 16px;\r\n  border-left:3px solid var(--amber); }\r\n.stat .k { font-size:10.5px; text-transform:uppercase; letter-spacing:.09em; color:var(--mute); font-weight:600; }\r\n.stat .v { font-size:31px; font-weight:800; letter-spacing:-.03em; line-height:1.15; margin-top:5px; }\r\n.stat .s { font-size:11.5px; color:var(--mute); }\r\n.stat.g { border-left-color:var(--in); }\r\n.stat.r { border-left-color:var(--out); }\r\n\r\n\/* table *\/\r\n.tbl { width:100%; border-collapse:collapse; }\r\n.tbl th { text-align:left; font-size:10.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--mute);\r\n  padding:9px 14px; border-bottom:1px solid var(--line); font-weight:600; white-space:nowrap; }\r\n.tbl td { padding:11px 14px; border-bottom:1px solid var(--line2); font-size:13.5px; vertical-align:middle; }\r\n.tbl tr:last-child td { border-bottom:0; }\r\n.tbl tbody tr:hover { background:#F6F9FB; }\r\n.scroll { overflow-x:auto; }\r\n.empty { padding:38px 16px; text-align:center; color:var(--mute); font-size:13.5px; }\r\n\r\n\/* form *\/\r\n.grid2 { display:grid; grid-template-columns:1fr 1fr; gap:14px; }\r\n.f { display:flex; flex-direction:column; gap:5px; }\r\n.f label { font-size:11px; text-transform:uppercase; letter-spacing:.07em; color:var(--mute); font-weight:600; }\r\n.f input, .f select, .f textarea { border:1px solid var(--line); border-radius:6px; padding:9px 11px;\r\n  font-size:13.5px; background:#fff; width:100%; }\r\n.f textarea { resize:vertical; min-height:64px; font-family:inherit; }\r\n.f input.code { font-family:var(--mono); }\r\n.f input:disabled { background:#F1F5F8; color:var(--mute); }\r\n.f .hint { font-size:11px; color:var(--mute); }\r\n.f .err { font-size:11px; color:var(--out); font-weight:600; }\r\n.span2 { grid-column:1 \/ -1; }\r\n\r\n\/* mode switch *\/\r\n.modes { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-bottom:18px; }\r\n.mode { text-align:left; background:var(--surf); border:1px solid var(--line); border-radius:8px;\r\n  padding:14px 15px; cursor:pointer; }\r\n.mode:hover { border-color:var(--amber-d); }\r\n.mode.on { border-color:var(--amber); border-width:2px; padding:13px 14px; background:#FFFBF0; }\r\n.mode b { display:block; font-size:14.5px; font-weight:700; margin-bottom:3px; }\r\n.mode span { font-size:12px; color:var(--mute); line-height:1.45; }\r\n\r\n\/* label \u2014 signature element *\/\r\n.labels { display:flex; flex-wrap:wrap; gap:16px; }\r\n.labelwrap { position:relative; }\r\n.label { width:280px; background:#fff; padding:11px 12px 9px; border-radius:2px;\r\n  border:1px dashed #9AA7B2; box-shadow:0 2px 0 rgba(20,32,44,.06), 0 6px 14px rgba(20,32,44,.09); }\r\n.label .lrow { display:flex; justify-content:space-between; align-items:baseline;\r\n  font-family:var(--mono); font-size:8.5px; letter-spacing:.1em; text-transform:uppercase; color:#5A6B76;\r\n  border-bottom:1px solid #DDE3E8; padding-bottom:5px; }\r\n.label .lname { font-size:14px; font-weight:800; letter-spacing:-.01em; margin:7px 0 1px; line-height:1.25; }\r\n.label .ldesc { font-size:10.5px; color:#5A6B76; line-height:1.4; min-height:15px; }\r\n.label .lmeta { display:flex; gap:14px; margin:7px 0 6px; }\r\n.label .lmeta div { font-size:9px; text-transform:uppercase; letter-spacing:.07em; color:#8494A0; }\r\n.label .lmeta b { display:block; font-family:var(--mono); font-size:11px; color:var(--ink); letter-spacing:0; text-transform:none; }\r\n.label .lcode { text-align:center; font-family:var(--mono); font-size:11.5px; font-weight:700;\r\n  letter-spacing:.22em; margin-top:2px; }\r\n.label .lsku { font-family:var(--mono); font-size:11px; font-weight:700; }\r\n.pick { position:absolute; top:-7px; left:-7px; width:24px; height:24px; border-radius:50%;\r\n  border:1px solid var(--line); background:#fff; cursor:pointer; font-size:13px; font-weight:800;\r\n  color:transparent; display:flex; align-items:center; justify-content:center; box-shadow:0 1px 4px rgba(0,0,0,.15); }\r\n.labelwrap.sel .pick { background:var(--in); border-color:var(--in); color:#fff; }\r\n.labelwrap.unsel .label { opacity:.42; }\r\n.groupbar { display:flex; align-items:center; gap:10px; margin:22px 0 12px; }\r\n.groupbar h4 { font-size:12px; text-transform:uppercase; letter-spacing:.09em; font-weight:700; white-space:nowrap; }\r\n.groupbar .rule { flex:1; height:1px; background:var(--line); }\r\n.groupbar .n { font-family:var(--mono); font-size:11px; color:var(--mute); }\r\n\r\n\/* scan *\/\r\n.scanbox { background:var(--ink); border-radius:10px; padding:26px; color:#fff; }\r\n.scanbox h2 { font-size:13px; text-transform:uppercase; letter-spacing:.1em; color:var(--amber); font-weight:600; }\r\n.scanbox p { font-size:12.5px; color:#A9B8C4; margin-top:5px; }\r\n.scaninput { width:100%; margin-top:16px; background:#0C1620; border:2px solid #34485A; border-radius:8px;\r\n  padding:16px 18px; font-family:var(--mono); font-size:22px; font-weight:700; color:var(--amber);\r\n  letter-spacing:.1em; }\r\n.scaninput::placeholder { color:#3E5265; letter-spacing:.06em; font-weight:400; font-size:17px; }\r\n.scaninput:focus { border-color:var(--amber); outline:none; }\r\n.reasons { display:flex; gap:7px; flex-wrap:wrap; margin-top:14px; }\r\n.reasons button { background:#243A4C; border:1px solid #34485A; color:#C3D0DA; padding:6px 12px;\r\n  border-radius:20px; font-size:12px; cursor:pointer; }\r\n.reasons button.on { background:var(--amber); border-color:var(--amber); color:var(--ink); font-weight:700; }\r\n.toast { margin-top:14px; padding:12px 15px; border-radius:7px; font-size:13.5px; font-weight:600; }\r\n.toast.ok { background:#0F7A4A; color:#fff; }\r\n.toast.bad { background:#C1322C; color:#fff; }\r\n.chips { display:flex; flex-wrap:wrap; gap:7px; }\r\n.chips button { font-family:var(--mono); font-size:11.5px; background:#F1F5F8; border:1px solid var(--line);\r\n  border-radius:5px; padding:6px 10px; cursor:pointer; }\r\n.chips button:hover { background:var(--amber); border-color:var(--amber); }\r\n\r\n\/* login *\/\r\n.login { flex:1; display:flex; align-items:center; justify-content:center; padding:24px;\r\n  background:var(--ink); background-image:repeating-linear-gradient(-45deg,transparent 0 22px,rgba(255,179,0,.05) 22px 44px); }\r\n.loginbox { width:100%; max-width:370px; background:var(--surf); border-radius:10px; padding:30px 28px;\r\n  box-shadow:0 20px 50px rgba(0,0,0,.3); }\r\n.loginbox b.lg { font-size:26px; font-weight:800; letter-spacing:-.03em; display:block; }\r\n.loginbox .sub { font-family:var(--mono); font-size:9.5px; letter-spacing:.18em; text-transform:uppercase;\r\n  color:var(--amber-d); margin-bottom:22px; display:block; }\r\n.loginbox .f { margin-bottom:13px; }\r\n.loginbox .btn { width:100%; margin-top:6px; }\r\n.loginbox .note { font-size:11.5px; color:var(--mute); margin-top:14px; text-align:center; line-height:1.5; }\r\n\r\n\/* filter bar *\/\r\n.bar { display:flex; gap:9px; align-items:center; flex-wrap:wrap; margin-bottom:14px; }\r\n.bar input, .bar select { border:1px solid var(--line); border-radius:6px; padding:8px 11px; font-size:13px; background:#fff; }\r\n.bar input.grow { flex:1; min-width:190px; }\r\n.bar .lbl { font-size:11px; text-transform:uppercase; letter-spacing:.07em; color:var(--mute); font-weight:600; }\r\n.bar .count { font-size:12.5px; color:var(--mute); margin-left:auto; }\r\n.callout { background:#FFF8E6; border:1px solid #F0D89A; border-left:3px solid var(--amber);\r\n  border-radius:6px; padding:11px 14px; font-size:12.5px; line-height:1.55; }\r\n.stickybar { position:sticky; top:0; z-index:5; background:var(--bg); padding:12px 0 6px; margin-top:-12px; }\r\n\r\n@media (max-width:820px){\r\n  .sf { flex-direction:column; }\r\n  .rail { width:100%; height:auto; position:static; }\r\n  .nav { flex-direction:row; overflow-x:auto; padding:8px; }\r\n  .nav button { white-space:nowrap; }\r\n  .railfoot { display:flex; justify-content:space-between; align-items:center; }\r\n  .wrap { padding:16px 14px 44px; }\r\n  .head { padding:14px 16px; }\r\n  .grid2, .modes { grid-template-columns:1fr; }\r\n}\r\n@media print {\r\n  .rail, .head, .noprint { display:none !important; }\r\n  .sf, .wrap { background:#fff; padding:0; }\r\n  .labelwrap.unsel { display:none !important; }\r\n  .pick { display:none !important; }\r\n  .label { box-shadow:none; page-break-inside:avoid; }\r\n  .groupbar { display:none !important; }\r\n}\r\n`;\r\n\r\n\/* ============================================================\r\n   APP\r\n   ============================================================ *\/\r\nfunction App() {\r\n  const [ready, setReady] = useState(false);\r\n  const [user, setUser] = useState(null);\r\n  const [view, setView] = useState(\"dash\");\r\n  const [units, setUnits] = useState([]);\r\n  const [moves, setMoves] = useState([]);\r\n  const [seq, setSeq] = useState(10);\r\n  const [lastBatch, setLastBatch] = useState([]);\r\n\r\n  useEffect(() => {\r\n    (async () => {\r\n      try {\r\n        const r = await store.get(STORE_KEY);\r\n        const d = JSON.parse(r.value);\r\n        setUnits(d.units); setMoves(d.moves); setSeq(d.seq);\r\n      } catch {\r\n        setUnits(SEED); setMoves(SEED_MOVES); setSeq(10);\r\n      }\r\n      setReady(true);\r\n    })();\r\n  }, []);\r\n\r\n  useEffect(() => {\r\n    if (!ready) return;\r\n    store.set(STORE_KEY, JSON.stringify({ units, moves, seq })).catch(() => {});\r\n  }, [units, moves, seq, ready]);\r\n\r\n  const inStock = useMemo(() => units.filter((u) => u.status === \"in\"), [units]);\r\n\r\n  function addUnits(newUnits, note) {\r\n    setUnits((p) => [...newUnits, ...p]);\r\n    setMoves((p) => [\r\n      ...newUnits.map((u) => ({ id: `${u.barcode}-in`, type: \"IN\", barcode: u.barcode, sku: u.sku, name: u.name, at: u.createdAt, note })),\r\n      ...p,\r\n    ]);\r\n  }\r\n\r\n  function exitUnit(barcode, reason) {\r\n    const u = units.find((x) => x.barcode === barcode.trim());\r\n    if (!u) return { ok: false, msg: `No box matches barcode ${barcode}. Check the code and scan again.` };\r\n    if (u.status === \"out\") return { ok: false, msg: `${u.name} already left on ${fmt(u.exitedAt)} \u2014 reason: ${u.exitReason}.` };\r\n    const at = nowISO();\r\n    setUnits((p) => p.map((x) => (x.barcode === u.barcode ? { ...x, status: \"out\", exitedAt: at, exitReason: reason } : x)));\r\n    setMoves((p) => [{ id: `${u.barcode}-out-${at}`, type: \"OUT\", barcode: u.barcode, sku: u.sku, name: u.name, at, note: reason }, ...p]);\r\n    return { ok: true, msg: `${u.name} removed from stock \u2014 ${reason}.` };\r\n  }\r\n\r\n  if (!ready) return <div className=\"sf\"><style>{CSS}<\/style><div style={{ margin: \"auto\", color: \"#64757F\" }}>Loading\u2026<\/div><\/div>;\r\n  if (!user) return <div className=\"sf\"><style>{CSS}<\/style><Login onIn={setUser} \/><\/div>;\r\n\r\n  const NAV = [\r\n    [\"dash\", \"\u25a4\", \"Dashboard\"],\r\n    [\"add\", \"\uff0b\", \"Add stock\"],\r\n    [\"labels\", \"\u25ad\", \"Labels\"],\r\n    [\"inv\", \"\u25a6\", \"Inventory\"],\r\n    [\"scan\", \"\u2317\", \"Scan out\"],\r\n    [\"log\", \"\u2261\", \"Movement log\"],\r\n  ];\r\n  const TITLES = {\r\n    dash: [\"Dashboard\", \"What is moving through the warehouse today\"],\r\n    add: [\"Add stock\", \"Book new boxes in and create their labels\"],\r\n    labels: [\"Labels\", \"Find any label, print it, stick it on the box\"],\r\n    inv: [\"Inventory\", \"Every box on record, in one place\"],\r\n    scan: [\"Scan out\", \"Scan a barcode to take a box out of stock\"],\r\n    log: [\"Movement log\", \"Every entry and exit, in order\"],\r\n  };\r\n\r\n  return (\r\n    <div className=\"sf\">\r\n      <style>{CSS}<\/style>\r\n      <nav className=\"rail noprint\">\r\n        <div className=\"brand\"><b>StockFloor<\/b><span>Warehouse control<\/span><\/div>\r\n        <div className=\"nav\">\r\n          {NAV.map(([k, g, l]) => (\r\n            <button key={k} className={view === k ? \"on\" : \"\"} onClick={() => setView(k)}>\r\n              <span className=\"glyph\">{g}<\/span>{l}\r\n            <\/button>\r\n          ))}\r\n        <\/div>\r\n        <div className=\"railfoot\">\r\n          Signed in as {user}\r\n          <button onClick={() => setUser(null)}>Sign out<\/button>\r\n        <\/div>\r\n      <\/nav>\r\n\r\n      <div className=\"main\">\r\n        <header className=\"head noprint\">\r\n          <div><h1>{TITLES[view][0]}<\/h1><p>{TITLES[view][1]}<\/p><\/div>\r\n          {view === \"dash\" && <button className=\"btn amber\" onClick={() => setView(\"add\")}>Add stock<\/button>}\r\n        <\/header>\r\n        <div className=\"wrap\">\r\n          {view === \"dash\" && <Dash units={units} inStock={inStock} moves={moves} go={setView} \/>}\r\n          {view === \"add\" && <AddStock units={units} seq={seq} setSeq={setSeq} addUnits={addUnits} setLastBatch={setLastBatch} go={setView} \/>}\r\n          {view === \"labels\" && <Labels units={units} lastBatch={lastBatch} go={setView} \/>}\r\n          {view === \"inv\" && <Inventory units={units} \/>}\r\n          {view === \"scan\" && <Scan inStock={inStock} exitUnit={exitUnit} moves={moves} \/>}\r\n          {view === \"log\" && <Log moves={moves} \/>}\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   LOGIN\r\n   ============================================================ *\/\r\nfunction Login({ onIn }) {\r\n  const [id, setId] = useState(\"rahul\");\r\n  const [pw, setPw] = useState(\"demo1234\");\r\n  const [err, setErr] = useState(\"\");\r\n  const submit = () => {\r\n    if (!id.trim() || !pw.trim()) return setErr(\"Enter both your staff ID and password.\");\r\n    onIn(id.trim());\r\n  };\r\n  return (\r\n    <div className=\"login\">\r\n      <div className=\"loginbox\">\r\n        <b className=\"lg\">StockFloor<\/b>\r\n        <span className=\"sub\">Warehouse control<\/span>\r\n        <div className=\"f\">\r\n          <label>Staff ID<\/label>\r\n          <input value={id} onChange={(e) => { setId(e.target.value); setErr(\"\"); }} onKeyDown={(e) => e.key === \"Enter\" && submit()} \/>\r\n        <\/div>\r\n        <div className=\"f\">\r\n          <label>Password<\/label>\r\n          <input type=\"password\" value={pw} onChange={(e) => { setPw(e.target.value); setErr(\"\"); }} onKeyDown={(e) => e.key === \"Enter\" && submit()} \/>\r\n        <\/div>\r\n        {err && <div className=\"f\"><span className=\"err\">{err}<\/span><\/div>}\r\n        <button className=\"btn amber\" onClick={submit}>Sign in<\/button>\r\n        <p className=\"note\">This is a prototype \u2014 any ID and password will let you in.<\/p>\r\n      <\/div>\r\n    <\/div>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   DASHBOARD\r\n   ============================================================ *\/\r\nfunction Dash({ units, inStock, moves, go }) {\r\n  const skus = useMemo(() => {\r\n    const m = {};\r\n    inStock.forEach((u) => {\r\n      if (!m[u.sku]) m[u.sku] = { sku: u.sku, name: u.name, count: 0 };\r\n      m[u.sku].count++;\r\n    });\r\n    return Object.values(m).sort((a, b) => a.count - b.count);\r\n  }, [inStock]);\r\n\r\n  const inToday = moves.filter((m) => m.type === \"IN\" && isToday(m.at)).length;\r\n  const outToday = moves.filter((m) => m.type === \"OUT\" && isToday(m.at)).length;\r\n\r\n  return (\r\n    <>\r\n      <div className=\"stats\">\r\n        <div className=\"stat\"><div className=\"k\">Boxes in stock<\/div><div className=\"v\">{inStock.length}<\/div><div className=\"s\">across {skus.length} items<\/div><\/div>\r\n        <div className=\"stat g\"><div className=\"k\">Booked in today<\/div><div className=\"v\">{inToday}<\/div><div className=\"s\">boxes<\/div><\/div>\r\n        <div className=\"stat r\"><div className=\"k\">Sent out today<\/div><div className=\"v\">{outToday}<\/div><div className=\"s\">boxes<\/div><\/div>\r\n        <div className=\"stat\"><div className=\"k\">All time<\/div><div className=\"v\">{units.length}<\/div><div className=\"s\">boxes on record<\/div><\/div>\r\n      <\/div>\r\n\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 16 }} className=\"dashgrid\">\r\n        <div className=\"card\">\r\n          <h3>Running low<\/h3>\r\n          {skus.length === 0 ? (\r\n            <div className=\"empty\">Nothing in stock yet.\r\n              <div style={{ marginTop: 12 }}><button className=\"btn sm amber\" onClick={() => go(\"add\")}>Add stock<\/button><\/div>\r\n            <\/div>\r\n          ) : (\r\n            <div className=\"scroll\"><table className=\"tbl\">\r\n              <thead><tr><th>Item<\/th><th>SKU<\/th><th style={{ textAlign: \"right\" }}>Boxes<\/th><\/tr><\/thead>\r\n              <tbody>{skus.slice(0, 6).map((s) => (\r\n                <tr key={s.sku}>\r\n                  <td>{s.name}<\/td>\r\n                  <td className=\"mono\">{s.sku}<\/td>\r\n                  <td style={{ textAlign: \"right\", fontWeight: 700, color: s.count <= 2 ? \"var(--out)\" : \"inherit\" }}>{s.count}<\/td>\r\n                <\/tr>))}<\/tbody>\r\n            <\/table><\/div>\r\n          )}\r\n        <\/div>\r\n\r\n        <div className=\"card\">\r\n          <h3>Latest movements<\/h3>\r\n          {moves.length === 0 ? <div className=\"empty\">No movements recorded yet.<\/div> : (\r\n            <div className=\"scroll\"><table className=\"tbl\">\r\n              <thead><tr><th>Type<\/th><th>Item<\/th><th>When<\/th><\/tr><\/thead>\r\n              <tbody>{moves.slice(0, 6).map((m) => (\r\n                <tr key={m.id}>\r\n                  <td><span className={`tag ${m.type === \"IN\" ? \"in\" : \"out\"}`}>{m.type}<\/span><\/td>\r\n                  <td>{m.name}<div className=\"mono\" style={{ color: \"var(--mute)\", fontSize: 11 }}>{m.barcode}<\/div><\/td>\r\n                  <td style={{ color: \"var(--mute)\", fontSize: 12.5, whiteSpace: \"nowrap\" }}>{fmt(m.at)}<\/td>\r\n                <\/tr>))}<\/tbody>\r\n            <\/table><\/div>\r\n          )}\r\n        <\/div>\r\n      <\/div>\r\n      <style>{`@media(max-width:820px){.dashgrid{grid-template-columns:1fr !important;}}`}<\/style>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   ADD STOCK\r\n   ============================================================ *\/\r\nfunction AddStock({ units, seq, setSeq, addUnits, setLastBatch, go }) {\r\n  const [mode, setMode] = useState(\"auto\");\r\n  const [f, setF] = useState({ barcode: \"\", sku: \"\", name: \"\", description: \"\", weight: \"\", origin: \"India\", count: 1 });\r\n  const [err, setErr] = useState({});\r\n  const set = (k) => (e) => { setF((p) => ({ ...p, [k]: e.target.value })); setErr((p) => ({ ...p, [k]: \"\" })); };\r\n\r\n  const previewSKU = mode === \"auto\" ? makeSKU(f.name, f.origin, seq) : f.sku;\r\n  const previewBC = mode === \"auto\" ? makeBarcode(seq) : f.barcode;\r\n  const count = Math.max(1, Math.min(500, parseInt(f.count, 10) || 1));\r\n\r\n  function submit() {\r\n    const e = {};\r\n    if (!f.name.trim()) e.name = \"Item name is required.\";\r\n    if (!f.weight.trim()) e.weight = \"Weight is required.\";\r\n    if (mode === \"manual\") {\r\n      if (!f.barcode.trim()) e.barcode = \"Enter the barcode printed on the box.\";\r\n      else if (units.some((u) => u.barcode === f.barcode.trim())) e.barcode = \"This barcode is already used by another box.\";\r\n      if (!f.sku.trim()) e.sku = \"Enter a SKU.\";\r\n    }\r\n    setErr(e);\r\n    if (Object.keys(e).length) return;\r\n\r\n    const at = nowISO();\r\n    const made = Array.from({ length: count }, (_, i) => ({\r\n      barcode: mode === \"auto\" ? makeBarcode(seq + i) : (count === 1 ? f.barcode.trim() : `${f.barcode.trim()}-${pad(i + 1, 2)}`),\r\n      sku: mode === \"auto\" ? makeSKU(f.name, f.origin, seq) : f.sku.trim(),\r\n      name: f.name.trim(), description: f.description.trim(), weight: f.weight.trim(), origin: f.origin,\r\n      status: \"in\", mode, createdAt: at, exitedAt: null, exitReason: null,\r\n    }));\r\n\r\n    addUnits(made, mode === \"auto\" ? \"Automatic entry\" : \"Manual entry\");\r\n    setSeq(seq + count);\r\n    setLastBatch(made.map((m) => m.barcode));\r\n    setF({ barcode: \"\", sku: \"\", name: \"\", description: \"\", weight: \"\", origin: \"India\", count: 1 });\r\n    go(\"labels\");\r\n  }\r\n\r\n  return (\r\n    <>\r\n      <div className=\"modes\">\r\n        <button className={`mode ${mode === \"auto\" ? \"on\" : \"\"}`} onClick={() => { setMode(\"auto\"); setErr({}); }}>\r\n          <b>Automatic<\/b>\r\n          <span>The system creates the barcode and SKU. You only fill in the item details. Use this for goods that arrive unlabelled.<\/span>\r\n        <\/button>\r\n        <button className={`mode ${mode === \"manual\" ? \"on\" : \"\"}`} onClick={() => { setMode(\"manual\"); setErr({}); }}>\r\n          <b>Manual<\/b>\r\n          <span>You type the barcode and SKU yourself. Use this when the supplier has already labelled the boxes.<\/span>\r\n        <\/button>\r\n      <\/div>\r\n\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 320px\", gap: 18, alignItems: \"start\" }} className=\"addgrid\">\r\n        <div className=\"card\">\r\n          <h3>{mode === \"auto\" ? \"Item details\" : \"Full details\"}<\/h3>\r\n          <div className=\"pad\">\r\n            <div className=\"grid2\">\r\n              <div className=\"f\">\r\n                <label>Barcode<\/label>\r\n                <input className=\"code\" value={mode === \"auto\" ? previewBC : f.barcode} onChange={set(\"barcode\")}\r\n                  disabled={mode === \"auto\"} placeholder=\"8901234567890\" \/>\r\n                {err.barcode ? <span className=\"err\">{err.barcode}<\/span>\r\n                  : <span className=\"hint\">{mode === \"auto\" ? \"Each box gets its own unique number.\" : \"The number printed on the box.\"}<\/span>}\r\n              <\/div>\r\n              <div className=\"f\">\r\n                <label>SKU<\/label>\r\n                <input className=\"code\" value={mode === \"auto\" ? previewSKU : f.sku} onChange={set(\"sku\")}\r\n                  disabled={mode === \"auto\"} placeholder=\"COT-IN-0004\" \/>\r\n                {err.sku ? <span className=\"err\">{err.sku}<\/span>\r\n                  : <span className=\"hint\">All boxes of the same product share one SKU.<\/span>}\r\n              <\/div>\r\n              <div className=\"f span2\">\r\n                <label>Item name<\/label>\r\n                <input value={f.name} onChange={set(\"name\")} placeholder=\"Cotton bedsheet set\" \/>\r\n                {err.name && <span className=\"err\">{err.name}<\/span>}\r\n              <\/div>\r\n              <div className=\"f span2\">\r\n                <label>Description<\/label>\r\n                <textarea value={f.description} onChange={set(\"description\")} placeholder=\"Double bed, king size, 210 TC\" \/>\r\n              <\/div>\r\n              <div className=\"f\">\r\n                <label>Weight<\/label>\r\n                <input value={f.weight} onChange={set(\"weight\")} placeholder=\"2.4 kg\" \/>\r\n                {err.weight && <span className=\"err\">{err.weight}<\/span>}\r\n              <\/div>\r\n              <div className=\"f\">\r\n                <label>Origin<\/label>\r\n                <select value={f.origin} onChange={set(\"origin\")}>{ORIGINS.map((o) => <option key={o}>{o}<\/option>)}<\/select>\r\n              <\/div>\r\n              <div className=\"f span2\">\r\n                <label>How many boxes arrived<\/label>\r\n                <input type=\"number\" min=\"1\" max=\"500\" value={f.count} onChange={set(\"count\")} style={{ maxWidth: 130 }} \/>\r\n                <span className=\"hint\">\r\n                  {count > 1\r\n                    ? `Books in ${count} boxes and creates ${count} separate barcodes and labels.`\r\n                    : \"One box, one barcode, one label.\"}\r\n                <\/span>\r\n              <\/div>\r\n            <\/div>\r\n            <div style={{ display: \"flex\", gap: 10, marginTop: 20 }}>\r\n              <button className=\"btn amber\" onClick={submit}>\r\n                Book in {count} {count > 1 ? \"boxes\" : \"box\"} and create labels\r\n              <\/button>\r\n            <\/div>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div>\r\n          <div className=\"card\" style={{ marginBottom: 14 }}>\r\n            <h3>Label preview<\/h3>\r\n            <div className=\"pad\" style={{ background: \"#F3F6F8\", display: \"flex\", justifyContent: \"center\" }}>\r\n              <LabelCard u={{\r\n                barcode: previewBC || \"SB000000000\", sku: previewSKU || \"SKU-XX-0000\",\r\n                name: f.name || \"Item name\", description: f.description || \"Description appears here\",\r\n                weight: f.weight || \"\u2014\", origin: f.origin,\r\n              }} \/>\r\n            <\/div>\r\n          <\/div>\r\n          <div className=\"callout\">\r\n            {mode === \"auto\"\r\n              ? \"In automatic mode every box gets a different barcode, so later you can tell exactly which box left the warehouse.\"\r\n              : \"If you enter more than one box in manual mode, the system adds -01, -02 to the barcode so each box stays unique.\"}\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <style>{`@media(max-width:900px){.addgrid{grid-template-columns:1fr !important;}}`}<\/style>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   LABEL \u2014 signature element\r\n   ============================================================ *\/\r\nfunction LabelCard({ u }) {\r\n  return (\r\n    <div className=\"label\">\r\n      <div className=\"lrow\"><span>Stockfloor<\/span><span className=\"lsku\">{u.sku}<\/span><\/div>\r\n      <div className=\"lname\">{u.name}<\/div>\r\n      <div className=\"ldesc\">{u.description || \"\u2014\"}<\/div>\r\n      <div className=\"lmeta\">\r\n        <div>Weight<b>{u.weight || \"\u2014\"}<\/b><\/div>\r\n        <div>Origin<b>{u.origin}<\/b><\/div>\r\n      <\/div>\r\n      <Barcode value={u.barcode} \/>\r\n      <div className=\"lcode\">{u.barcode}<\/div>\r\n    <\/div>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   LABELS \u2014 filter, sort, select, print\r\n   ============================================================ *\/\r\nfunction Labels({ units, lastBatch, go }) {\r\n  const [range, setRange] = useState(lastBatch.length ? \"batch\" : \"today\");\r\n  const [sort, setSort] = useState(\"new\");\r\n  const [q, setQ] = useState(\"\");\r\n  const [hideGone, setHideGone] = useState(true);\r\n  const [picked, setPicked] = useState(null); \/\/ null = everything visible is picked\r\n\r\n  const rows = useMemo(() => {\r\n    let r = units.slice();\r\n\r\n    if (range === \"batch\") r = r.filter((u) => lastBatch.includes(u.barcode));\r\n    else if (range === \"today\") r = r.filter((u) => isToday(u.createdAt));\r\n    else if (range === \"7\") r = r.filter((u) => daysAgo(u.createdAt) <= 7);\r\n    else if (range === \"30\") r = r.filter((u) => daysAgo(u.createdAt) <= 30);\r\n\r\n    if (hideGone) r = r.filter((u) => u.status === \"in\");\r\n\r\n    const s = q.trim().toLowerCase();\r\n    if (s) r = r.filter((u) => [u.name, u.sku, u.barcode, u.origin].join(\" \").toLowerCase().includes(s));\r\n\r\n    const by = {\r\n      new: (a, b) => new Date(b.createdAt) - new Date(a.createdAt),\r\n      old: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),\r\n      name: (a, b) => a.name.localeCompare(b.name) || a.barcode.localeCompare(b.barcode),\r\n      sku: (a, b) => a.sku.localeCompare(b.sku) || a.barcode.localeCompare(b.barcode),\r\n      barcode: (a, b) => a.barcode.localeCompare(b.barcode),\r\n    };\r\n    return r.sort(by[sort]);\r\n  }, [units, range, sort, q, hideGone, lastBatch]);\r\n\r\n  const isPicked = (bc) => (picked === null ? true : picked.has(bc));\r\n  const pickedCount = picked === null ? rows.length : rows.filter((u) => picked.has(u.barcode)).length;\r\n\r\n  function toggle(bc) {\r\n    setPicked((p) => {\r\n      const next = new Set(p === null ? rows.map((u) => u.barcode) : p);\r\n      next.has(bc) ? next.delete(bc) : next.add(bc);\r\n      return next;\r\n    });\r\n  }\r\n\r\n  \/\/ group headings only make sense when the sort creates real groups\r\n  const grouped = useMemo(() => {\r\n    const key = (u) =>\r\n      sort === \"name\" ? u.name\r\n      : sort === \"sku\" ? `${u.sku} \u00b7 ${u.name}`\r\n      : fmtDay(u.createdAt);\r\n    if (sort === \"barcode\") return [[\"\", rows]];\r\n    const map = new Map();\r\n    rows.forEach((u) => {\r\n      const k = key(u);\r\n      if (!map.has(k)) map.set(k, []);\r\n      map.get(k).push(u);\r\n    });\r\n    return [...map.entries()];\r\n  }, [rows, sort]);\r\n\r\n  return (\r\n    <>\r\n      <div className=\"stickybar noprint\">\r\n        <div className=\"bar\">\r\n          <span className=\"lbl\">Show<\/span>\r\n          <select value={range} onChange={(e) => { setRange(e.target.value); setPicked(null); }}>\r\n            {lastBatch.length > 0 && <option value=\"batch\">Just created ({lastBatch.length})<\/option>}\r\n            <option value=\"today\">Added today<\/option>\r\n            <option value=\"7\">Last 7 days<\/option>\r\n            <option value=\"30\">Last 30 days<\/option>\r\n            <option value=\"all\">All labels<\/option>\r\n          <\/select>\r\n\r\n          <span className=\"lbl\">Sort by<\/span>\r\n          <select value={sort} onChange={(e) => setSort(e.target.value)}>\r\n            <option value=\"new\">Newest first<\/option>\r\n            <option value=\"old\">Oldest first<\/option>\r\n            <option value=\"name\">Item name (A\u2013Z)<\/option>\r\n            <option value=\"sku\">SKU (A\u2013Z)<\/option>\r\n            <option value=\"barcode\">Barcode<\/option>\r\n          <\/select>\r\n\r\n          <input className=\"grow\" placeholder=\"Search name, SKU or barcode\u2026\" value={q}\r\n            onChange={(e) => { setQ(e.target.value); setPicked(null); }} \/>\r\n        <\/div>\r\n\r\n        <div className=\"bar\">\r\n          <label style={{ display: \"flex\", alignItems: \"center\", gap: 7, fontSize: 12.5, cursor: \"pointer\" }}>\r\n            <input type=\"checkbox\" checked={hideGone} style={{ width: \"auto\" }}\r\n              onChange={(e) => { setHideGone(e.target.checked); setPicked(null); }} \/>\r\n            Hide boxes that already left\r\n          <\/label>\r\n          <button className=\"btn ghost sm\" onClick={() => setPicked(null)}>Select all<\/button>\r\n          <button className=\"btn ghost sm\" onClick={() => setPicked(new Set())}>Clear selection<\/button>\r\n          <span className=\"count\">{pickedCount} of {rows.length} selected<\/span>\r\n          <button className=\"btn amber\" disabled={pickedCount === 0}\r\n            onClick={() => { if (picked === null) setPicked(new Set(rows.map((u) => u.barcode))); setTimeout(() => window.print(), 60); }}>\r\n            Print {pickedCount} {pickedCount === 1 ? \"label\" : \"labels\"}\r\n          <\/button>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      {rows.length === 0 ? (\r\n        <div className=\"card\"><div className=\"empty\">\r\n          No labels match these filters. Widen the date range, or clear the search.\r\n          <div style={{ marginTop: 12 }}><button className=\"btn sm amber\" onClick={() => go(\"add\")}>Add stock<\/button><\/div>\r\n        <\/div><\/div>\r\n      ) : (\r\n        grouped.map(([heading, list]) => (\r\n          <div key={heading || \"all\"}>\r\n            {heading && (\r\n              <div className=\"groupbar noprint\">\r\n                <h4>{heading}<\/h4><div className=\"rule\" \/><span className=\"n\">{list.length}<\/span>\r\n              <\/div>\r\n            )}\r\n            <div className=\"labels\">\r\n              {list.map((u) => (\r\n                <div key={u.barcode} className={`labelwrap ${isPicked(u.barcode) ? \"sel\" : \"unsel\"}`}>\r\n                  <button className=\"pick noprint\" onClick={() => toggle(u.barcode)}\r\n                    aria-label={`${isPicked(u.barcode) ? \"Remove\" : \"Add\"} ${u.barcode} ${isPicked(u.barcode) ? \"from\" : \"to\"} the print selection`}>\u2713<\/button>\r\n                  <LabelCard u={u} \/>\r\n                <\/div>\r\n              ))}\r\n            <\/div>\r\n          <\/div>\r\n        ))\r\n      )}\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   INVENTORY\r\n   ============================================================ *\/\r\nfunction Inventory({ units }) {\r\n  const [q, setQ] = useState(\"\");\r\n  const [st, setSt] = useState(\"in\");\r\n  const [sort, setSort] = useState(\"new\");\r\n\r\n  const rows = useMemo(() => {\r\n    let r = units.filter((u) => {\r\n      if (st !== \"all\" && u.status !== st) return false;\r\n      const s = q.trim().toLowerCase();\r\n      return !s || [u.barcode, u.sku, u.name, u.origin].join(\" \").toLowerCase().includes(s);\r\n    });\r\n    const by = {\r\n      new: (a, b) => new Date(b.createdAt) - new Date(a.createdAt),\r\n      old: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),\r\n      name: (a, b) => a.name.localeCompare(b.name),\r\n      sku: (a, b) => a.sku.localeCompare(b.sku),\r\n    };\r\n    return r.sort(by[sort]);\r\n  }, [units, q, st, sort]);\r\n\r\n  return (\r\n    <>\r\n      <div className=\"bar\">\r\n        <input className=\"grow\" placeholder=\"Search by barcode, SKU or item name\u2026\" value={q} onChange={(e) => setQ(e.target.value)} \/>\r\n        <select value={st} onChange={(e) => setSt(e.target.value)}>\r\n          <option value=\"in\">In stock<\/option>\r\n          <option value=\"out\">Already left<\/option>\r\n          <option value=\"all\">All boxes<\/option>\r\n        <\/select>\r\n        <select value={sort} onChange={(e) => setSort(e.target.value)}>\r\n          <option value=\"new\">Newest first<\/option>\r\n          <option value=\"old\">Oldest first<\/option>\r\n          <option value=\"name\">Item name (A\u2013Z)<\/option>\r\n          <option value=\"sku\">SKU (A\u2013Z)<\/option>\r\n        <\/select>\r\n        <span className=\"count\">{rows.length} boxes<\/span>\r\n      <\/div>\r\n      <div className=\"card scroll\">\r\n        {rows.length === 0 ? <div className=\"empty\">Nothing matches this search.<\/div> : (\r\n          <table className=\"tbl\">\r\n            <thead><tr><th>Status<\/th><th>Barcode<\/th><th>SKU<\/th><th>Item<\/th><th>Weight<\/th><th>Origin<\/th><th>Booked in<\/th><th>Left<\/th><\/tr><\/thead>\r\n            <tbody>{rows.map((u) => (\r\n              <tr key={u.barcode}>\r\n                <td><span className={`tag ${u.status}`}>{u.status === \"in\" ? \"IN STOCK\" : \"OUT\"}<\/span><\/td>\r\n                <td className=\"mono\">{u.barcode}<\/td>\r\n                <td className=\"mono\">{u.sku}<\/td>\r\n                <td>{u.name}<div style={{ fontSize: 11.5, color: \"var(--mute)\" }}>{u.description}<\/div><\/td>\r\n                <td className=\"mono\">{u.weight}<\/td>\r\n                <td>{u.origin}<\/td>\r\n                <td style={{ color: \"var(--mute)\", fontSize: 12.5, whiteSpace: \"nowrap\" }}>{fmt(u.createdAt)}<\/td>\r\n                <td style={{ color: \"var(--mute)\", fontSize: 12.5, whiteSpace: \"nowrap\" }}>\r\n                  {u.exitedAt ? <>{fmt(u.exitedAt)}<div style={{ fontSize: 11 }}>{u.exitReason}<\/div><\/> : \"\u2014\"}\r\n                <\/td>\r\n              <\/tr>))}<\/tbody>\r\n          <\/table>\r\n        )}\r\n      <\/div>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   SCAN OUT\r\n   ============================================================ *\/\r\nfunction Scan({ inStock, exitUnit, moves }) {\r\n  const [val, setVal] = useState(\"\");\r\n  const [reason, setReason] = useState(\"Sale\");\r\n  const [msg, setMsg] = useState(null);\r\n  const ref = useRef(null);\r\n  useEffect(() => { ref.current?.focus(); }, []);\r\n\r\n  function fire(code) {\r\n    const c = (code ?? val).trim();\r\n    if (!c) return;\r\n    setMsg(exitUnit(c, reason));\r\n    setVal(\"\");\r\n    ref.current?.focus();\r\n  }\r\n\r\n  const recentOut = moves.filter((m) => m.type === \"OUT\").slice(0, 5);\r\n\r\n  return (\r\n    <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 300px\", gap: 18, alignItems: \"start\" }} className=\"scangrid\">\r\n      <div>\r\n        <div className=\"scanbox\">\r\n          <h2>Scan out<\/h2>\r\n          <p>Point the scanner at the box and pull the trigger. The scanner types the code and presses Enter for you.<\/p>\r\n          <input ref={ref} className=\"scaninput\" value={val} placeholder=\"Waiting for a scan\u2026\"\r\n            onChange={(e) => { setVal(e.target.value); setMsg(null); }}\r\n            onKeyDown={(e) => e.key === \"Enter\" && fire()} onBlur={() => ref.current?.focus()} \/>\r\n          <div className=\"reasons\">\r\n            {EXIT_REASONS.map((r) => (\r\n              <button key={r} className={reason === r ? \"on\" : \"\"} onClick={() => { setReason(r); ref.current?.focus(); }}>{r}<\/button>\r\n            ))}\r\n          <\/div>\r\n          {msg && <div className={`toast ${msg.ok ? \"ok\" : \"bad\"}`}>{msg.msg}<\/div>}\r\n        <\/div>\r\n\r\n        <div className=\"card\" style={{ marginTop: 16 }}>\r\n          <h3>No scanner here? Tap a barcode to test<\/h3>\r\n          <div className=\"pad\">\r\n            {inStock.length === 0 ? <div style={{ color: \"var(--mute)\", fontSize: 13.5 }}>No boxes left in stock.<\/div> : (\r\n              <div className=\"chips\">\r\n                {inStock.slice(0, 14).map((u) => (\r\n                  <button key={u.barcode} onClick={() => fire(u.barcode)} title={u.name}>{u.barcode}<\/button>\r\n                ))}\r\n              <\/div>\r\n            )}\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      <div className=\"card\">\r\n        <h3>Just went out<\/h3>\r\n        {recentOut.length === 0 ? <div className=\"empty\">Nothing has left yet.<\/div> : (\r\n          <div className=\"pad\" style={{ display: \"flex\", flexDirection: \"column\", gap: 12 }}>\r\n            {recentOut.map((m) => (\r\n              <div key={m.id} style={{ borderLeft: \"2px solid var(--out)\", paddingLeft: 11 }}>\r\n                <div style={{ fontSize: 13.5, fontWeight: 600 }}>{m.name}<\/div>\r\n                <div className=\"mono\" style={{ color: \"var(--mute)\", fontSize: 11.5 }}>{m.barcode}<\/div>\r\n                <div style={{ fontSize: 11.5, color: \"var(--mute)\" }}>{m.note} \u00b7 {fmt(m.at)}<\/div>\r\n              <\/div>\r\n            ))}\r\n          <\/div>\r\n        )}\r\n      <\/div>\r\n      <style>{`@media(max-width:900px){.scangrid{grid-template-columns:1fr !important;}}`}<\/style>\r\n    <\/div>\r\n  );\r\n}\r\n\r\n\/* ============================================================\r\n   MOVEMENT LOG\r\n   ============================================================ *\/\r\nfunction Log({ moves }) {\r\n  const [t, setT] = useState(\"all\");\r\n  const [q, setQ] = useState(\"\");\r\n  const rows = moves.filter((m) => {\r\n    if (t !== \"all\" && m.type !== t) return false;\r\n    const s = q.trim().toLowerCase();\r\n    return !s || [m.barcode, m.sku, m.name, m.note].join(\" \").toLowerCase().includes(s);\r\n  });\r\n  return (\r\n    <>\r\n      <div className=\"bar\">\r\n        <input className=\"grow\" placeholder=\"Search barcode, SKU, item or reason\u2026\" value={q} onChange={(e) => setQ(e.target.value)} \/>\r\n        <select value={t} onChange={(e) => setT(e.target.value)}>\r\n          <option value=\"all\">All movements<\/option>\r\n          <option value=\"IN\">Entries only<\/option>\r\n          <option value=\"OUT\">Exits only<\/option>\r\n        <\/select>\r\n        <span className=\"count\">{rows.length} entries<\/span>\r\n      <\/div>\r\n      <div className=\"card scroll\">\r\n        {rows.length === 0 ? <div className=\"empty\">Nothing matches this filter.<\/div> : (\r\n          <table className=\"tbl\">\r\n            <thead><tr><th>Type<\/th><th>When<\/th><th>Barcode<\/th><th>SKU<\/th><th>Item<\/th><th>Reason<\/th><\/tr><\/thead>\r\n            <tbody>{rows.map((m) => (\r\n              <tr key={m.id}>\r\n                <td><span className={`tag ${m.type === \"IN\" ? \"in\" : \"out\"}`}>{m.type}<\/span><\/td>\r\n                <td style={{ color: \"var(--mute)\", fontSize: 12.5, whiteSpace: \"nowrap\" }}>{fmt(m.at)}<\/td>\r\n                <td className=\"mono\">{m.barcode}<\/td>\r\n                <td className=\"mono\">{m.sku}<\/td>\r\n                <td>{m.name}<\/td>\r\n                <td style={{ fontSize: 12.5 }}>{m.note}<\/td>\r\n              <\/tr>))}<\/tbody>\r\n          <\/table>\r\n        )}\r\n      <\/div>\r\n    <\/>\r\n  );\r\n}\r\n\r\nReactDOM.createRoot(document.getElementById(\"root\")).render(<App \/>);\r\n<\/script>\r\n\r\n<script>\r\n(function () {\r\n  var SOURCES = {\r\n    react: [\r\n      \"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/react\/18.3.1\/umd\/react.production.min.js\",\r\n      \"https:\/\/unpkg.com\/react@18\/umd\/react.production.min.js\",\r\n      \"https:\/\/cdn.jsdelivr.net\/npm\/react@18\/umd\/react.production.min.js\"\r\n    ],\r\n    reactdom: [\r\n      \"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/react-dom\/18.3.1\/umd\/react-dom.production.min.js\",\r\n      \"https:\/\/unpkg.com\/react-dom@18\/umd\/react-dom.production.min.js\",\r\n      \"https:\/\/cdn.jsdelivr.net\/npm\/react-dom@18\/umd\/react-dom.production.min.js\"\r\n    ],\r\n    babel: [\r\n      \"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/babel-standalone\/7.25.6\/babel.min.js\",\r\n      \"https:\/\/unpkg.com\/@babel\/standalone@7\/babel.min.js\",\r\n      \"https:\/\/cdn.jsdelivr.net\/npm\/@babel\/standalone@7\/babel.min.js\"\r\n    ]\r\n  };\r\n\r\n  function loadOne(url) {\r\n    return new Promise(function (resolve, reject) {\r\n      var s = document.createElement(\"script\");\r\n      s.src = url;\r\n      s.async = false;\r\n      s.onload = resolve;\r\n      s.onerror = function () { reject(new Error(url)); };\r\n      document.head.appendChild(s);\r\n    });\r\n  }\r\n\r\n  function loadAny(urls, check) {\r\n    var i = 0;\r\n    function next() {\r\n      if (i >= urls.length) return Promise.reject(new Error(\"all sources failed\"));\r\n      return loadOne(urls[i++]).then(function () {\r\n        return check() ? true : next();\r\n      }, next);\r\n    }\r\n    return next();\r\n  }\r\n\r\n  function fail(msg) {\r\n    document.getElementById(\"root\").innerHTML =\r\n      '<div id=\"boot\"><b>Could not start<\/b>' + msg +\r\n      '<small>Connect to the internet and refresh the page.<\/small><\/div>';\r\n  }\r\n\r\n  loadAny(SOURCES.react, function () { return !!window.React; })\r\n    .then(function () { return loadAny(SOURCES.reactdom, function () { return !!window.ReactDOM; }); })\r\n    .then(function () { return loadAny(SOURCES.babel, function () { return !!window.Babel; }); })\r\n    .then(function () { window.Babel.transformScriptTags(); })\r\n    .catch(function () { fail(\"The app needs to download a few files the first time it runs.\"); });\r\n})();\r\n<\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>StockFloor &#8211; Warehouse control StockFloorStarting upThis takes a couple of seconds the first time.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"class_list":["post-19","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/pages\/19","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=19"}],"version-history":[{"count":7,"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/pages\/19\/revisions"}],"predecessor-version":[{"id":27,"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=\/wp\/v2\/pages\/19\/revisions\/27"}],"wp:attachment":[{"href":"https:\/\/thatshop.kdlink.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=19"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}