{"id":87,"date":"2026-06-24T14:55:40","date_gmt":"2026-06-24T14:55:40","guid":{"rendered":"https:\/\/akku.cozyfluffy.com\/?page_id=87"},"modified":"2026-06-30T09:22:34","modified_gmt":"2026-06-30T09:22:34","slug":"pkt-inventory-stock-dashboard","status":"publish","type":"page","link":"https:\/\/akku.cozyfluffy.com\/index.php\/pkt-inventory-stock-dashboard\/","title":{"rendered":"PKT \u5e93\u5b58\u6570\u636e\u770b\u677f"},"content":{"rendered":"<p><!-- PKT inventory dashboard managed by Codex. --><\/p>\n<div id=\"pkt-inventory-wordpress-root\">\n<style>\n:root {\n  --bg: #f6f7f9;\n  --surface: #ffffff;\n  --surface-soft: #f9fafb;\n  --text: #111827;\n  --muted: #667085;\n  --line: #d8dee8;\n  --blue: #2563eb;\n  --green: #138a4d;\n  --amber: #b45309;\n  --red: #c2410c;\n  --ink: #0f172a;\n  --shadow: 0 14px 30px rgba(15, 23, 42, 0.08);\n}<\/p>\n<p>* {\n  box-sizing: border-box;\n}<\/p>\n<p>body {\n  margin: 0;\n  color: var(--text);\n  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n  background: var(--bg);\n}<\/p>\n<p>button,\ninput,\nselect,\ntextarea {\n  font: inherit;\n}<\/p>\n<p>button {\n  cursor: pointer;\n}<\/p>\n<p>.app-shell {\n  width: min(1440px, 100%);\n  min-height: 100vh;\n  margin: 0 auto;\n  padding: 24px;\n}<\/p>\n<p>.topbar {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 20px;\n  margin-bottom: 18px;\n}<\/p>\n<p>.brand {\n  display: flex;\n  align-items: center;\n  gap: 14px;\n  min-width: 0;\n}<\/p>\n<p>.brand img {\n  width: 48px;\n  height: 48px;\n  object-fit: contain;\n}<\/p>\n<p>.eyebrow {\n  margin: 0 0 4px;\n  color: var(--muted);\n  font-size: 13px;\n  font-weight: 700;\n}<\/p>\n<p>h1,\nh2,\np {\n  margin: 0;\n}<\/p>\n<p>h1 {\n  font-size: 28px;\n  line-height: 1.15;\n  font-weight: 800;\n  letter-spacing: 0;\n}<\/p>\n<p>h2 {\n  font-size: 18px;\n  line-height: 1.25;\n}<\/p>\n<p>.source-actions {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  flex-wrap: wrap;\n  justify-content: flex-end;\n}<\/p>\n<p>.sync-strip {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n  margin-bottom: 14px;\n}<\/p>\n<p>.sync-strip span {\n  display: inline-flex;\n  align-items: center;\n  min-height: 30px;\n  border: 1px solid var(--line);\n  border-radius: 999px;\n  background: var(--surface);\n  padding: 5px 10px;\n  color: #475467;\n  font-size: 12px;\n  font-weight: 750;\n}<\/p>\n<p>.sheet-link,\n.primary-btn,\n.secondary-btn {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  min-height: 40px;\n  border-radius: 8px;\n  border: 1px solid transparent;\n  padding: 9px 14px;\n  font-weight: 750;\n  text-decoration: none;\n}<\/p>\n<p>.sheet-link,\n.secondary-btn {\n  background: var(--surface);\n  color: var(--ink);\n  border-color: var(--line);\n}<\/p>\n<p>.primary-btn {\n  background: var(--ink);\n  color: #fff;\n}<\/p>\n<p>.notice {\n  display: none;\n  margin-bottom: 16px;\n  border: 1px solid var(--line);\n  border-left: 4px solid var(--blue);\n  border-radius: 8px;\n  background: var(--surface);\n  padding: 12px 14px;\n  color: #344054;\n  line-height: 1.45;\n}<\/p>\n<p>.notice.show {\n  display: block;\n}<\/p>\n<p>.notice.error {\n  border-left-color: var(--red);\n}<\/p>\n<p>.notice.success {\n  border-left-color: var(--green);\n}<\/p>\n<p>.summary-grid {\n  display: grid;\n  grid-template-columns: repeat(4, minmax(0, 1fr));\n  gap: 12px;\n  margin-bottom: 16px;\n}<\/p>\n<p>.metric-card {\n  min-height: 100px;\n  border: 1px solid var(--line);\n  border-radius: 8px;\n  background: var(--surface);\n  padding: 16px;\n  box-shadow: var(--shadow);\n}<\/p>\n<p>.metric-card span {\n  display: block;\n  color: var(--muted);\n  font-size: 13px;\n  font-weight: 700;\n}<\/p>\n<p>.metric-card strong {\n  display: block;\n  margin-top: 12px;\n  color: var(--ink);\n  font-size: 30px;\n  line-height: 1;\n  letter-spacing: 0;\n}<\/p>\n<p>.toolbar {\n  display: grid;\n  grid-template-columns: minmax(260px, 1fr) 220px 240px;\n  gap: 12px;\n  margin-bottom: 16px;\n  border: 1px solid var(--line);\n  border-radius: 8px;\n  background: var(--surface);\n  padding: 14px;\n}<\/p>\n<p>.toolbar label {\n  display: grid;\n  gap: 7px;\n}<\/p>\n<p>.toolbar span {\n  color: var(--muted);\n  font-size: 12px;\n  font-weight: 750;\n}<\/p>\n<p>input,\nselect,\ntextarea {\n  width: 100%;\n  border: 1px solid #c9d3df;\n  border-radius: 8px;\n  background: #fff;\n  color: var(--text);\n}<\/p>\n<p>input,\nselect {\n  min-height: 40px;\n  padding: 8px 10px;\n}<\/p>\n<p>textarea {\n  resize: vertical;\n  min-height: 130px;\n  padding: 10px;\n}<\/p>\n<p>.content-grid {\n  display: block;\n  margin-bottom: 16px;\n}<\/p>\n<p>.panel {\n  border: 1px solid var(--line);\n  border-radius: 8px;\n  background: var(--surface);\n  box-shadow: var(--shadow);\n}<\/p>\n<p>.panel-head {\n  display: flex;\n  align-items: flex-start;\n  justify-content: space-between;\n  gap: 12px;\n  border-bottom: 1px solid var(--line);\n  padding: 16px;\n}<\/p>\n<p>.panel-head p {\n  margin-top: 4px;\n  color: var(--muted);\n  font-size: 13px;\n}<\/p>\n<p>.chart-list {\n  display: grid;\n  gap: 14px;\n  max-height: none;\n  overflow: visible;\n  padding: 16px;\n}<\/p>\n<p>.chart-group {\n  display: grid;\n  gap: 10px;\n}<\/p>\n<p>.chart-equivalent-group {\n  border-left: 4px solid #0f766e;\n  padding: 8px 0 8px 12px;\n  background: #f8fafc;\n}<\/p>\n<p>.chart-group-label {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 12px;\n  color: #0f766e;\n  font-size: 12px;\n  font-weight: 800;\n  line-height: 1.35;\n}<\/p>\n<p>.chart-group-label strong {\n  color: var(--ink);\n  font-weight: 800;\n}<\/p>\n<p>.chart-row {\n  display: grid;\n  grid-template-columns: minmax(360px, 520px) minmax(320px, 1fr) 150px;\n  align-items: center;\n  gap: 18px;\n  min-height: 56px;\n}<\/p>\n<p>.chart-name {\n  overflow: visible;\n  font-weight: 750;\n  line-height: 1.2;\n  text-overflow: clip;\n  white-space: normal;\n  overflow-wrap: anywhere;\n}<\/p>\n<p>.chart-product,\n.table-product {\n  display: grid;\n  gap: 5px;\n}<\/p>\n<p>.chart-meta,\n.table-meta {\n  color: var(--muted);\n  font-size: 12px;\n  font-weight: 650;\n  line-height: 1.35;\n}<\/p>\n<p>.alternative-note {\n  color: #0f766e;\n  font-size: 13px;\n  font-weight: 750;\n  line-height: 1.35;\n}<\/p>\n<p>.cover-warning {\n  color: var(--amber);\n  font-size: 13px;\n  font-weight: 800;\n  line-height: 1.35;\n}<\/p>\n<p>.bar-stack {\n  display: grid;\n  gap: 5px;\n}<\/p>\n<p>.bar-line {\n  display: grid;\n  grid-template-columns: 70px minmax(0, 1fr);\n  align-items: center;\n  gap: 8px;\n}<\/p>\n<p>.bar-label {\n  color: var(--muted);\n  font-size: 12px;\n  font-weight: 750;\n}<\/p>\n<p>.bar-track {\n  height: 10px;\n  overflow: hidden;\n  border-radius: 999px;\n  background: #edf1f6;\n}<\/p>\n<p>.bar-fill {\n  width: var(--bar-width);\n  height: 100%;\n  border-radius: inherit;\n}<\/p>\n<p>.bar-fill.current {\n  background: var(--green);\n}<\/p>\n<p>.bar-fill.sea {\n  background: #0ea5e9;\n}<\/p>\n<p>.bar-fill.production {\n  background: #7c3aed;\n}<\/p>\n<p>.bar-fill.estimated {\n  background: var(--blue);\n}<\/p>\n<p>.chart-values {\n  display: grid;\n  gap: 3px;\n  color: var(--muted);\n  font-size: 12px;\n  text-align: right;\n}<\/p>\n<p>.chart-values strong {\n  color: var(--ink);\n}<\/p>\n<p>.import-box {\n  display: grid;\n  grid-template-columns: minmax(220px, 320px) minmax(320px, 1fr) 180px;\n  align-items: start;\n  gap: 12px;\n  padding: 16px;\n}<\/p>\n<p>.table-panel {\n  margin-bottom: 16px;\n  overflow: visible;\n}<\/p>\n<p>.rules-panel {\n  margin-bottom: 16px;\n  overflow: hidden;\n}<\/p>\n<p>.equivalent-panel {\n  margin-bottom: 16px;\n  overflow: visible;\n}<\/p>\n<p>.equivalent-table {\n  min-width: 1080px;\n}<\/p>\n<p>.equivalent-family {\n  color: var(--muted);\n  font-size: 12px;\n  font-weight: 800;\n  margin-bottom: 4px;\n}<\/p>\n<p>.equivalent-products {\n  color: var(--ink);\n  font-size: 12px;\n  font-weight: 700;\n  line-height: 1.35;\n  overflow-wrap: anywhere;\n}<\/p>\n<p>.equivalent-product-name,\n.equivalent-total-label {\n  color: var(--ink);\n  font-size: 14px;\n  font-weight: 800;\n  line-height: 1.35;\n  overflow-wrap: anywhere;\n}<\/p>\n<p>.equivalent-group-start td {\n  border-top: 2px solid #d8dee8;\n}<\/p>\n<p>.equivalent-total-row td {\n  background: #f8fafc;\n  font-weight: 850;\n}<\/p>\n<p>.equivalent-total-row .equivalent-products {\n  color: var(--muted);\n  margin-top: 3px;\n}<\/p>\n<p>.rules-list {\n  display: grid;\n  grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));\n  column-gap: 24px;\n  padding: 0 20px 16px;\n}<\/p>\n<p>.rule-row {\n  display: grid;\n  grid-template-columns: 76px minmax(0, 1fr);\n  align-items: center;\n  gap: 12px;\n  border-top: 1px solid #e5e9f0;\n  padding: 10px 0;\n}<\/p>\n<p>.rule-family {\n  color: var(--muted);\n  font-size: 12px;\n  font-weight: 800;\n  white-space: nowrap;\n}<\/p>\n<p>.rule-pair {\n  display: grid;\n  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n  align-items: center;\n  gap: 8px;\n  color: var(--ink);\n  font-size: 13px;\n  font-weight: 750;\n}<\/p>\n<p>.rule-pair span {\n  min-width: 0;\n  overflow-wrap: anywhere;\n}<\/p>\n<p>.rule-pair strong {\n  color: #0f766e;\n  font-size: 14px;\n}<\/p>\n<p>.table-wrap {\n  overflow: visible;\n  max-height: none;\n}<\/p>\n<p>table {\n  min-width: 1280px;\n  width: 100%;\n  border-collapse: separate;\n  border-spacing: 0;\n}<\/p>\n<p>th,\ntd {\n  border-bottom: 1px solid #e5e9f0;\n  padding: 13px 16px;\n  text-align: left;\n}<\/p>\n<p>th {\n  position: sticky;\n  top: 0;\n  z-index: 20;\n  background: var(--surface-soft);\n  box-shadow: 0 1px 0 #e5e9f0, 0 8px 18px rgba(15, 23, 42, 0.08);\n  color: #475467;\n  font-size: 12px;\n  font-weight: 800;\n  text-transform: uppercase;\n}<\/p>\n<p>th small {\n  display: block;\n  margin-top: 4px;\n  color: #667085;\n  font-size: 10px;\n  font-weight: 750;\n  line-height: 1.3;\n  text-transform: none;\n  white-space: normal;\n}<\/p>\n<p>.th-title {\n  display: block;\n}<\/p>\n<p>td {\n  font-size: 14px;\n}<\/p>\n<p>td.number-cell {\n  font-variant-numeric: tabular-nums;\n  font-weight: 800;\n}<\/p>\n<p>.stock-eta-cell {\n  min-width: 118px;\n  vertical-align: top;\n}<\/p>\n<p>.eta-qty {\n  color: var(--ink);\n  font-size: 14px;\n  font-weight: 850;\n  line-height: 1.2;\n}<\/p>\n<p>.eta-lines {\n  display: grid;\n  gap: 3px;\n  margin-top: 5px;\n}<\/p>\n<p>.eta-line {\n  display: grid;\n  grid-template-columns: minmax(0, 1fr) auto;\n  align-items: baseline;\n  gap: 8px;\n  color: var(--muted);\n  font-size: 11px;\n  font-weight: 700;\n  line-height: 1.25;\n  text-transform: none;\n}<\/p>\n<p>.eta-line span {\n  min-width: 0;\n  overflow-wrap: anywhere;\n}<\/p>\n<p>.eta-line strong {\n  color: #0f766e;\n  font-size: 12px;\n}<\/p>\n<p>.total-cell {\n  color: var(--ink);\n}<\/p>\n<p>.risk-cell {\n  color: var(--red);\n}<\/p>\n<p>.cover-cell-over4 {\n  color: var(--amber);\n}<\/p>\n<p>.cover-cell-level1,\n.cover-cell-replenish {\n  color: var(--red);\n}<\/p>\n<p>.purchase-cell {\n  white-space: nowrap;\n}<\/p>\n<p>.purchase-needed {\n  color: var(--red);\n}<\/p>\n<p>.purchase-ok {\n  color: #0f766e;\n}<\/p>\n<p>.alt-cell {\n  color: #0f766e;\n  font-size: 13px;\n  font-weight: 750;\n  line-height: 1.35;\n}<\/p>\n<p>.has-alternative td {\n  background: #f0fdfa;\n}<\/p>\n<p>.cover-over4 td {\n  background: #fffbeb;\n}<\/p>\n<p>.cover-level1 td {\n  background: #fff1e8;\n}<\/p>\n<p>.cover-replenish td {\n  background: #fff7ed;\n}<\/p>\n<p>.cover-level1 .cover-warning,\n.cover-replenish .cover-warning {\n  color: var(--red);\n}<\/p>\n<p>.status-pill {\n  display: inline-flex;\n  align-items: center;\n  min-height: 26px;\n  border-radius: 999px;\n  padding: 4px 10px;\n  font-size: 12px;\n  font-weight: 800;\n}<\/p>\n<p>.status-covered {\n  background: #e8f7ee;\n  color: #126d3c;\n}<\/p>\n<p>.status-short {\n  background: #fff4e6;\n  color: var(--amber);\n}<\/p>\n<p>.status-empty {\n  background: #fff0eb;\n  color: var(--red);\n}<\/p>\n<p>.empty-state {\n  padding: 28px 16px;\n  color: var(--muted);\n  text-align: center;\n}<\/p>\n<p>@media (max-width: 980px) {\n  .topbar,\n  .source-actions {\n    align-items: flex-start;\n    justify-content: flex-start;\n  }<\/p>\n<p>  .topbar,\n  .topbar {\n    display: grid;\n  }<\/p>\n<p>  .summary-grid {\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n  }<\/p>\n<p>  .toolbar {\n    grid-template-columns: 1fr;\n  }<\/p>\n<p>  .import-box {\n    grid-template-columns: 1fr;\n  }<\/p>\n<p>  .table-wrap {\n    overflow-x: auto;\n    overflow-y: visible;\n  }\n}<\/p>\n<p>@media (max-width: 640px) {\n  .app-shell {\n    padding: 16px;\n  }<\/p>\n<p>  .brand {\n    align-items: flex-start;\n  }<\/p>\n<p>  h1 {\n    font-size: 23px;\n  }<\/p>\n<p>  .summary-grid {\n    grid-template-columns: 1fr;\n  }<\/p>\n<p>  .chart-row {\n    grid-template-columns: 1fr;\n    gap: 8px;\n  }<\/p>\n<p>  .chart-values {\n    text-align: left;\n  }<\/p>\n<p>  .rules-list {\n    grid-template-columns: 1fr;\n    padding: 0 16px 14px;\n  }<\/p>\n<p>  .rule-row {\n    grid-template-columns: 1fr;\n    gap: 6px;\n  }<\/p>\n<p>  .rule-pair {\n    grid-template-columns: 1fr;\n    gap: 4px;\n  }<\/p>\n<p>  th,\n  td {\n    padding: 12px;\n  }\n}<\/p>\n<\/style>\n<p>  <main class=\"app-shell\"><\/p>\n<header class=\"topbar\">\n<div class=\"brand\">\n          <img decoding=\"async\" src=\".\/logo.png?v=20260630b\" alt=\"Perfektium\" \/><\/p>\n<div>\n<p class=\"eyebrow\">2026 \u5fb7\u4ed3 PKT \u5b9e\u9645\u5e93\u5b58<\/p>\n<h1>PKT \u5e93\u5b58\u6570\u636e\u770b\u677f<\/h1>\n<\/p><\/div>\n<\/p><\/div>\n<div class=\"source-actions\">\n          <a\n            class=\"sheet-link\"\n            href=\"https:\/\/docs.google.com\/spreadsheets\/d\/1ubVZzyZAo0KNwNh21W8Jut8o66cbtduVScGACGGJd-Q\/edit?gid=0#gid=0\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n          ><br \/>\n            \u6253\u5f00\u539f\u59cb\u8868\u683c<br \/>\n          <\/a><br \/>\n          <button id=\"refreshBtn\" class=\"primary-btn\" type=\"button\">\u5237\u65b0\u6570\u636e<\/button>\n        <\/div>\n<\/header>\n<section class=\"sync-strip\" aria-label=\"\u81ea\u52a8\u540c\u6b65\u72b6\u6001\">\n        <span id=\"syncMode\">\u81ea\u52a8\u540c\u6b65\uff1a\u6bcf 30 \u79d2<\/span><br \/>\n        <span id=\"lastSync\">\u6700\u540e\u540c\u6b65\uff1a\u7b49\u5f85\u8bfb\u53d6<\/span><br \/>\n        <span id=\"nextSync\">\u4e0b\u6b21\u5237\u65b0\uff1a&#8211;<\/span><br \/>\n      <\/section>\n<section class=\"notice\" id=\"sourceNotice\" aria-live=\"polite\"><\/section>\n<section class=\"notice planning-note show success\">\n        \u91c7\u8d2d\u8ba1\u5212\u53e3\u5f84\uff1a\u73b0\u8d27\u5e93\u5b58\u7528\u4e8e\u5224\u65ad\u4eca\u5929\u53ef\u9500\u552e\u6570\u91cf\uff1b\u603b\u5e93\u5b58 = \u73b0\u8d27 + \u6d77\u4e0a\u5e93\u5b58 + \u751f\u4ea7\u4e2d\uff1b\u6708\u5747\u9500\u552e\u6309\u4eca\u5e74\u5df2\u6709\u9500\u552e\u6570\u636e\u7684\u6708\u4efd\u5e73\u5747\u8ba1\u7b97\u3002\u91c7\u8d2d\u5efa\u8bae = \u672a\u6765 3 \u4e2a\u6708\u9700\u6c42 &#8211; \u73b0\u8d27 &#8211; \u6d77\u4e0a\u5e93\u5b58\uff0c\u4e0d\u542b\u751f\u4ea7\u4e2d\u3002\u4f4e\u4e8e 3 \u4e2a\u6708\u63d0\u793a\u8865\u8d27\uff1b\u8d85\u8fc7 4 \u4e2a\u6708\u63d0\u793a\u5e93\u5b58\u504f\u9ad8\uff1b\u8d85\u8fc7 6 \u4e2a\u6708\u6807\u4e3a\u4e00\u7ea7\u8b66\u544a\u3002<br \/>\n      <\/section>\n<section class=\"summary-grid\" aria-label=\"\u5e93\u5b58\u6c47\u603b\">\n<article class=\"metric-card\">\n          <span>\u4ea7\u54c1\u6570\u91cf<\/span><br \/>\n          <strong id=\"productCount\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u73b0\u8d27\u5e93\u5b58<\/span><br \/>\n          <strong id=\"currentTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u6d77\u4e0a\u5e93\u5b58<\/span><br \/>\n          <strong id=\"seaTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u751f\u4ea7\u4e2d<\/span><br \/>\n          <strong id=\"productionTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u603b\u5e93\u5b58<\/span><br \/>\n          <strong id=\"totalStockTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>PI\u767b\u8bb0\u6570\u91cf<\/span><br \/>\n          <strong id=\"estimatedTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u52a0\u6743\u53ef\u552e\u6708\u6570<\/span><br \/>\n          <strong id=\"monthsCoverTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<article class=\"metric-card\">\n          <span>\u73b0\u8d27\u8986\u76d6<\/span><br \/>\n          <strong id=\"coverageTotal\">&#8212;<\/strong><br \/>\n        <\/article>\n<\/section>\n<section class=\"toolbar\" aria-label=\"\u5e93\u5b58\u7b5b\u9009\">\n        <label class=\"search-box\"><br \/>\n          <span>\u641c\u7d22\u4ea7\u54c1<\/span><br \/>\n          <input id=\"searchInput\" type=\"search\" placeholder=\"\u8f93\u5165\u4ea7\u54c1\u578b\u53f7\u6216\u540d\u79f0\" \/><br \/>\n        <\/label><br \/>\n        <label><br \/>\n          <span>\u5e93\u5b58\u72b6\u6001<\/span><br \/>\n          <select id=\"statusFilter\"><option value=\"all\">\u5168\u90e8\u4ea7\u54c1<\/option><option value=\"covered\">\u73b0\u8d27\u53ef\u8986\u76d6 PI<\/option><option value=\"short\">\u73b0\u8d27\u4e0d\u8db3<\/option><option value=\"stock-only\">\u4ec5\u6709\u73b0\u8d27<\/option><option value=\"empty\">\u65e0\u73b0\u8d27<\/option><\/select><br \/>\n        <\/label><br \/>\n        <label><br \/>\n          <span>\u6392\u5e8f<\/span><br \/>\n          <select id=\"sortSelect\"><option value=\"series\">\u6309 PB \/ PF \/ PBG \/ IB \/ IA \u987a\u5e8f<\/option><option value=\"product\">\u6309\u4ea7\u54c1\u540d\u79f0<\/option><option value=\"current-desc\">Current stock \u4ece\u9ad8\u5230\u4f4e<\/option><option value=\"total-desc\">\u603b\u5e93\u5b58\u4ece\u9ad8\u5230\u4f4e<\/option><option value=\"cover-asc\">\u53ef\u552e\u6708\u6570\u4ece\u4f4e\u5230\u9ad8<\/option><option value=\"sales-desc\">\u4eca\u5e74\u6708\u5747\u9500\u91cf\u4ece\u9ad8\u5230\u4f4e<\/option><option value=\"purchase-desc\">\u91c7\u8d2d\u5efa\u8bae\u4ece\u9ad8\u5230\u4f4e<\/option><option value=\"estimated-desc\">Estimated Quantity \u4ece\u9ad8\u5230\u4f4e<\/option><option value=\"gap-asc\">\u5e93\u5b58\u7f3a\u53e3\u4f18\u5148<\/option><\/select><br \/>\n        <\/label><br \/>\n      <\/section>\n<section class=\"panel table-panel\">\n<div class=\"panel-head\">\n<div>\n<h2>\u4ea7\u54c1\u5e93\u5b58\u660e\u7ec6<\/h2>\n<p id=\"tableSubhead\">\u7b49\u5f85\u6570\u636e\u52a0\u8f7d<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div class=\"table-wrap\">\n<table>\n<thead>\n<tr>\n<th>\u4ea7\u54c1<\/th>\n<th>\u73b0\u8d27<\/th>\n<th>\n                  <span class=\"th-title\">\u6d77\u4e0a<\/span><br \/>\n                  <small>PKT-2026-05-02 ETA 7\u67086\u65e5 \/ IA-2026-05-01 ETA 6\/29<\/small>\n                <\/th>\n<th>\n                  <span class=\"th-title\">\u751f\u4ea7\u4e2d<\/span><br \/>\n                  <small>PKT-2026-06-01 ETA 8\u6708\u521d<\/small>\n                <\/th>\n<th>\u603b\u5e93\u5b58<\/th>\n<th>\u4eca\u5e74\u6708\u5747\u9500\u552e<\/th>\n<th>\u53ef\u552e\u6708\u6570<\/th>\n<th>\u91c7\u8d2d\u5efa\u8bae<\/th>\n<th>PI\u767b\u8bb0<\/th>\n<th>\u5e93\u5b58\u72b6\u6001<\/th>\n<th>\u66ff\u4ee3\u578b\u53f7<\/th>\n<\/tr>\n<\/thead>\n<tbody id=\"inventoryRows\"><\/tbody>\n<\/table><\/div>\n<\/section>\n<section class=\"panel equivalent-panel\">\n<div class=\"panel-head\">\n<div>\n<h2>\u578b\u53f7\u603b\u5e93\u5b58\u6c47\u603b<\/h2>\n<p>\u540c\u6b3e\u53ef\u6362\u8d34\u7eb8\u578b\u53f7\u5408\u5e76\u7edf\u8ba1\uff1b\u6ca1\u6709\u53ef\u6362\u578b\u53f7\u7684\u4ea7\u54c1\u4e5f\u5355\u72ec\u663e\u793a\uff0c\u65b9\u4fbf\u67e5\u770b\u6bcf\u4e2a\u578b\u53f7\u7684\u603b\u5e93\u5b58\u3002<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div class=\"table-wrap equivalent-table-wrap\">\n<table class=\"equivalent-table\">\n<thead>\n<tr>\n<th>\u578b\u53f7 \/ \u5408\u8ba1<\/th>\n<th>\u73b0\u8d27<\/th>\n<th>\n                  <span class=\"th-title\">\u6d77\u4e0a<\/span><br \/>\n                  <small>PKT-2026-05-02 ETA 7\u67086\u65e5 \/ IA-2026-05-01 ETA 6\/29<\/small>\n                <\/th>\n<th>\n                  <span class=\"th-title\">\u751f\u4ea7\u4e2d<\/span><br \/>\n                  <small>PKT-2026-06-01 ETA 8\u6708\u521d<\/small>\n                <\/th>\n<th>\u603b\u5e93\u5b58<\/th>\n<th>\u4eca\u5e74\u6708\u5747\u9500\u552e<\/th>\n<th>\u53ef\u552e\u6708\u6570<\/th>\n<th>\u91c7\u8d2d\u5efa\u8bae<\/th>\n<th>PI\u5408\u8ba1<\/th>\n<\/tr>\n<\/thead>\n<tbody id=\"equivalentRows\"><\/tbody>\n<\/table><\/div>\n<\/section>\n<section class=\"panel rules-panel\">\n<div class=\"panel-head\">\n<div>\n<h2>\u66ff\u4ee3\u578b\u53f7\u89c4\u5219<\/h2>\n<p>\u540c\u4e00\u884c\u5de6\u53f3\u578b\u53f7\u53ef\u4e92\u76f8\u66ff\u4ee3\uff0c\u5e93\u5b58\u4e0d\u8db3\u65f6\u4f1a\u5728\u660e\u7ec6\u91cc\u63d0\u793a\u6709\u8d27\u578b\u53f7\u3002<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div id=\"rulesList\" class=\"rules-list\"><\/div>\n<\/section>\n<section class=\"content-grid\">\n<section class=\"panel chart-panel\">\n<div class=\"panel-head\">\n<div>\n<h2>\u4ea7\u54c1\u5e93\u5b58\u5bf9\u6bd4<\/h2>\n<p>\u6309\u4ea7\u54c1\u5c55\u793a\u73b0\u8d27\u3001\u6d77\u4e0a\u3001\u751f\u4ea7\u4e2d\u4e0e PI \u767b\u8bb0\u6570\u91cf\u3002<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div id=\"chartList\" class=\"chart-list\" aria-label=\"\u4ea7\u54c1\u5e93\u5b58\u6761\u5f62\u56fe\"><\/div>\n<\/section>\n<\/section>\n<section class=\"panel import-panel\">\n<div class=\"panel-head\">\n<div>\n<h2>\u6570\u636e\u63a5\u5165<\/h2>\n<p>\u81ea\u52a8\u8bfb\u53d6\u5931\u8d25\u65f6\uff0c\u53ef\u4e34\u65f6\u5bfc\u5165\u539f\u8868 CSV\u3002<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<div class=\"import-box\">\n          <input id=\"csvFile\" type=\"file\" accept=\".csv,text\/csv\" \/><br \/>\n          <textarea\n            id=\"csvPaste\"\n            rows=\"4\"\n            placeholder=\"\u4e5f\u53ef\u4ee5\u628a\u5305\u542b Product \/ Current stock \/ \u6d77\u4e0a\u5e93\u5b58 \/ \u751f\u4ea7\u4e2d \/ Estimated Quantity \u7684 CSV \u5185\u5bb9\u7c98\u8d34\u5230\u8fd9\u91cc\"\n          ><\/textarea><br \/>\n          <button id=\"loadCsvBtn\" class=\"secondary-btn\" type=\"button\">\u5bfc\u5165 CSV<\/button>\n        <\/div>\n<\/section>\n<p>    <\/main><br \/>\n  <script>\nconst SHEET = {\n  id: \"1ubVZzyZAo0KNwNh21W8Jut8o66cbtduVScGACGGJd-Q\",\n  gid: \"0\",\n  title: \"2026\u5fb7\u4ed3PKT\u5b9e\u9645\u5e93\u5b58\"\n};<\/p>\n<p>const AUTO_REFRESH_MS = 30000;<\/p>\n<p>const state = {\n  rawRows: [],\n  rows: [],\n  source: \"\",\n  search: \"\",\n  status: \"all\",\n  sort: \"series\",\n  loading: false,\n  error: \"\",\n  lastSyncAt: null,\n  nextSyncAt: null\n};<\/p>\n<p>let autoRefreshTimer = null;\nlet countdownTimer = null;<\/p>\n<p>const els = {\n  refreshBtn: document.getElementById(\"refreshBtn\"),\n  sourceNotice: document.getElementById(\"sourceNotice\"),\n  lastSync: document.getElementById(\"lastSync\"),\n  nextSync: document.getElementById(\"nextSync\"),\n  productCount: document.getElementById(\"productCount\"),\n  currentTotal: document.getElementById(\"currentTotal\"),\n  estimatedTotal: document.getElementById(\"estimatedTotal\"),\n  coverageTotal: document.getElementById(\"coverageTotal\"),\n  seaTotal: document.getElementById(\"seaTotal\"),\n  productionTotal: document.getElementById(\"productionTotal\"),\n  totalStockTotal: document.getElementById(\"totalStockTotal\"),\n  monthsCoverTotal: document.getElementById(\"monthsCoverTotal\"),\n  searchInput: document.getElementById(\"searchInput\"),\n  statusFilter: document.getElementById(\"statusFilter\"),\n  sortSelect: document.getElementById(\"sortSelect\"),\n  chartList: document.getElementById(\"chartList\"),\n  tableSubhead: document.getElementById(\"tableSubhead\"),\n  inventoryRows: document.getElementById(\"inventoryRows\"),\n  equivalentRows: document.getElementById(\"equivalentRows\"),\n  rulesList: document.getElementById(\"rulesList\"),\n  csvFile: document.getElementById(\"csvFile\"),\n  csvPaste: document.getElementById(\"csvPaste\"),\n  loadCsvBtn: document.getElementById(\"loadCsvBtn\")\n};<\/p>\n<p>function normalizeHeader(value) {\n  return String(value || \"\")\n    .toLowerCase()\n    .replace(\/\\s+\/g, \"\")\n    .replace(\/[_\\-()\uff08\uff09\/\\\\:\uff1a.]\/g, \"\");\n}<\/p>\n<p>function parseNumber(value) {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value;\n  const text = String(value ?? \"\").trim();\n  if (!text || text === \"-\" || text.toLowerCase() === \"n\/a\") return 0;\n  const cleaned = text.replace(\/,\/g, \"\").replace(\/[^\\d.-]\/g, \"\");\n  const parsed = Number(cleaned);\n  return Number.isFinite(parsed) ? parsed : 0;\n}<\/p>\n<p>function parseNumberOrNull(value) {\n  if (typeof value === \"number\" && Number.isFinite(value)) return value;\n  const text = String(value ?? \"\").trim();\n  if (!text || text === \"-\" || text === \"\/\" || text.toLowerCase() === \"n\/a\") return null;\n  const cleaned = text.replace(\/,\/g, \"\").replace(\/[^\\d.-]\/g, \"\");\n  const parsed = Number(cleaned);\n  return Number.isFinite(parsed) ? parsed : null;\n}<\/p>\n<p>function formatNumber(value) {\n  return Math.round(value).toLocaleString(\"en-US\");\n}<\/p>\n<p>function formatDecimal(value, digits = 1) {\n  if (!Number.isFinite(value)) return \"--\";\n  return value.toLocaleString(\"en-US\", {\n    maximumFractionDigits: digits,\n    minimumFractionDigits: value % 1 ? Math.min(digits, 1) : 0\n  });\n}<\/p>\n<p>function formatClock(date) {\n  if (!date) return \"--\";\n  return date.toLocaleTimeString(\"zh-CN\", {\n    hour: \"2-digit\",\n    minute: \"2-digit\",\n    second: \"2-digit\"\n  });\n}<\/p>\n<p>function statusFor(row) {\n  if (row.current <= 0) return \"empty\";\n  if (row.estimated <= 0) return \"stock-only\";\n  return row.current >= row.estimated ? \"covered\" : \"short\";\n}<\/p>\n<p>function statusText(status) {\n  return {\n    covered: \"\u73b0\u8d27\u53ef\u8986\u76d6 PI\",\n    short: \"\u73b0\u8d27\u4e0d\u8db3\",\n    empty: \"\u65e0\u73b0\u8d27\",\n    \"stock-only\": \"\u4ec5\u6709\u73b0\u8d27\"\n  }[status] || \"\u5f85\u786e\u8ba4\";\n}<\/p>\n<p>const equivalentGroups = [\n  [\"PF-Underseat-12-100\", \"IA-12-100\"],\n  [\"PF-Underseat-12-100H\", \"IA-12-100H\"],\n  [\"PF-Underseat-12-150H\", \"IA-12-150H\"],\n  [\"PF-Underseat-12-180H\", \"IA-12-180H\"],\n  [\"PF-Underseat-12-180-CAN\", \"IA-12-180-CAN\"],\n  [\"PF-Underseat-12-200H\", \"IA-12-200H\"],\n  [\"PF-Underseat-12-250H\", \"IA-12-250H\"],\n  [\"PF-Underseat-12-310H\", \"IA-12-310H\"],\n  [\"PF-Underseat-12-500H\", \"PF-Underseat-12-500H (sans logo)\", \"IA-12-500H\"],\n  [\"PF-Underseat-12-100-LN3\", \"IA-12-100-MINI\"],\n  [\"PF-Underseat-12-100H-LN3-PRO\", \"IA-12-100H-MINI-PRO\"]\n];<\/p>\n<p>function productKey(product) {\n  return String(product || \"\")\n    .trim()\n    .normalize(\"NFKC\")\n    .replace(\/[\u2010\u2011\u2012\u2013\u2014\u2212\ufe63\uff0d]\/g, \"-\")\n    .toUpperCase()\n    .replace(\/\\s+\/g, \"\");\n}<\/p>\n<p>const excludedProductKeys = new Set([\"PF-Underseat-12-280H\"].map(productKey));<\/p>\n<p>function isExcludedProduct(product) {\n  return excludedProductKeys.has(productKey(product));\n}<\/p>\n<p>function alternativeKeysFor(product) {\n  const key = productKey(product);\n  const group = equivalentGroups.find((items) => items.some((item) => productKey(item) === key));\n  const explicitKeys = group ? group.map(productKey).filter((itemKey) => itemKey !== key) : [];\n  return uniqueValues([...explicitKeys, ...inferredPbIbAlternativeKeys(key)]);\n}<\/p>\n<p>function uniqueValues(items) {\n  return [...new Set(items.filter(Boolean))];\n}<\/p>\n<p>function inferredPbIbAlternativeKeys(key) {\n  if (key.startsWith(\"PB-\")) {\n    const pbBase = key.slice(3);\n    const baseWithoutPro = pbBase.endsWith(\"-PRO\") ? pbBase.slice(0, -4) : pbBase;\n    return uniqueValues([`IB-${pbBase}`, `IB-${baseWithoutPro}`]);\n  }\n  if (key.startsWith(\"IB-\")) {\n    const ibBase = key.slice(3);\n    return uniqueValues([`PB-${ibBase}`, `PB-${ibBase}-PRO`]);\n  }\n  return [];\n}<\/p>\n<p>function brandSourceForProduct(product) {\n  const key = productKey(product);\n  if (key.startsWith(\"P\") || key.includes(\"PF-\") || key.includes(\"PB-\") || key.includes(\"PBG-\")) return \"PKT\";\n  if (key.startsWith(\"I\") || key.includes(\"IA-\") || key.includes(\"IB-\")) return \"IA\";\n  return \"\";\n}<\/p>\n<p>function needsAlternative(row) {\n  return row.current <= 0 || (row.estimated > 0 && row.current < row.estimated);\n}\n\nfunction attachAlternatives(rows) {\n  const byProduct = new Map(rows.map((row) => [productKey(row.product), row]));\n  return rows.map((row) => {\n    const alternatives =\n      needsAlternative(row)\n        ? alternativeKeysFor(row.product)\n            .map((key) => byProduct.get(key))\n            .filter((alternative) => alternative && alternative.current > 0)\n            .map((alternative) => ({\n              product: alternative.product,\n              current: alternative.current\n            }))\n        : [];\n    return {\n      ...row,\n      alternatives\n    };\n  });\n}<\/p>\n<p>function replacementFamily(product) {\n  const key = productKey(product);\n  if (key.startsWith(\"PF-\") || key.startsWith(\"IA-\")) return \"PF \/ IA\";\n  if (key.startsWith(\"PB-\") || key.startsWith(\"IB-\")) return \"PB \/ IB\";\n  return \"\u5176\u4ed6\";\n}<\/p>\n<p>function orientReplacementPair(left, right) {\n  const leftKey = productKey(left);\n  const rightKey = productKey(right);\n  if ((leftKey.startsWith(\"IA-\") && rightKey.startsWith(\"PF-\")) || (leftKey.startsWith(\"IB-\") && rightKey.startsWith(\"PB-\"))) {\n    return [right, left];\n  }\n  return [left, right];\n}<\/p>\n<p>function addReplacementPair(pairs, seen, left, right) {\n  const oriented = orientReplacementPair(left, right);\n  const leftProduct = oriented[0];\n  const rightProduct = oriented[1];\n  const leftKey = productKey(leftProduct);\n  const rightKey = productKey(rightProduct);\n  if (!leftKey || !rightKey || leftKey === rightKey) return;\n  const pairKey = [leftKey, rightKey].sort().join(\"|\");\n  if (seen.has(pairKey)) return;\n  seen.add(pairKey);\n  const family = replacementFamily(leftProduct) !== \"\u5176\u4ed6\" ? replacementFamily(leftProduct) : replacementFamily(rightProduct);\n  pairs.push({ family, left: leftProduct, right: rightProduct });\n}<\/p>\n<p>function replacementPairsForRows(rows) {\n  const byProduct = new Map(rows.map((row) => [productKey(row.product), row.product]));\n  const pairs = [];\n  const seen = new Set();<\/p>\n<p>  equivalentGroups.forEach((group) => {\n    const presentProducts = group.map((item) => byProduct.get(productKey(item))).filter(Boolean);\n    for (let index = 1; index < presentProducts.length; index += 1) {\n      addReplacementPair(pairs, seen, presentProducts[0], presentProducts[index]);\n    }\n  });\n\n  rows.forEach((row) => {\n    alternativeKeysFor(row.product).forEach((key) => {\n      const alternativeProduct = byProduct.get(key);\n      if (alternativeProduct) addReplacementPair(pairs, seen, row.product, alternativeProduct);\n    });\n  });<\/p>\n<p>  return pairs.sort((a, b) => {\n    return seriesRank(a.left) - seriesRank(b.left) || a.left.localeCompare(b.left, undefined, {\n      numeric: true,\n      sensitivity: \"base\"\n    });\n  });\n}<\/p>\n<p>function equivalentStockGroupsForRows(rows) {\n  const byProduct = new Map(rows.map((row) => [productKey(row.product), row]));\n  const groups = [];\n  const seen = new Set();\n  const groupedProductKeys = new Set();<\/p>\n<p>  function addGroup(productNames, family, allowSingle = false) {\n    const members = [];\n    const memberKeys = new Set();\n    productNames.forEach((product) => {\n      const key = productKey(product);\n      const row = byProduct.get(key);\n      if (!row || memberKeys.has(key)) return;\n      memberKeys.add(key);\n      members.push(row);\n    });\n    members.sort((a, b) => seriesRank(a.product) - seriesRank(b.product) || productSort(a, b));\n    if (members.length < 2 &#038;&#038; !allowSingle) return;\n\n    const groupKey = members.map((row) => productKey(row.product)).sort().join(\"|\");\n    if (seen.has(groupKey)) return;\n    seen.add(groupKey);\n    members.forEach((row) => groupedProductKeys.add(productKey(row.product)));<\/p>\n<p>    const current = members.reduce((sum, row) => sum + row.current, 0);\n    const sea = members.reduce((sum, row) => sum + row.sea, 0);\n    const production = members.reduce((sum, row) => sum + row.production, 0);\n    const seaEtas = combineEtaEntries(members.flatMap((row) => row.seaEtas || []));\n    const productionEtas = combineEtaEntries(members.flatMap((row) => row.productionEtas || []));\n    const totalStock = members.reduce((sum, row) => sum + row.totalStock, 0);\n    const estimated = members.reduce((sum, row) => sum + row.estimated, 0);\n    const salesAverage = members.reduce((sum, row) => sum + (Number.isFinite(row.salesAvg3) ? row.salesAvg3 : 0), 0);\n    const hasSalesAverage = members.some((row) => Number.isFinite(row.salesAvg3));\n    const monthsCover = hasSalesAverage && salesAverage > 0 ? totalStock \/ salesAverage : null;\n    const purchaseRecommendation = hasSalesAverage\n      ? Math.max(0, Math.ceil(salesAverage * 3 - (current + sea)))\n      : null;<\/p>\n<p>    groups.push({\n      family,\n      products: members.map((row) => row.product),\n      members,\n      current,\n      sea,\n      production,\n      seaEtas,\n      productionEtas,\n      totalStock,\n      estimated,\n      salesAverage,\n      monthsCover,\n      purchaseRecommendation,\n      standalone: members.length === 1\n    });\n  }<\/p>\n<p>  equivalentGroups.forEach((group) => addGroup(group, replacementFamily(group[0])));<\/p>\n<p>  rows.forEach((row) => {\n    inferredPbIbAlternativeKeys(productKey(row.product)).forEach((key) => {\n      const alternative = byProduct.get(key);\n      if (alternative) addGroup([row.product, alternative.product], replacementFamily(row.product));\n    });\n  });<\/p>\n<p>  rows.forEach((row) => {\n    if (groupedProductKeys.has(productKey(row.product))) return;\n    addGroup([row.product], replacementFamily(row.product), true);\n  });<\/p>\n<p>  return groups.sort((a, b) => {\n    return seriesRank(a.products[0]) - seriesRank(b.products[0]) || productSort({ product: a.products[0] }, { product: b.products[0] });\n  });\n}<\/p>\n<p>function groupedSeriesRows(rows) {\n  return equivalentStockGroupsForRows(rows).flatMap((group) => group.members);\n}<\/p>\n<p>function seriesRank(product) {\n  const key = productKey(product);\n  if (key.startsWith(\"PB-\")) return 1;\n  if (key.startsWith(\"PF-\")) return 2;\n  if (key.startsWith(\"PBG-\")) return 3;\n  if (key.startsWith(\"IB-\")) return 4;\n  if (key.startsWith(\"IA-\")) return 5;\n  if (key.startsWith(\"BRANDLABEL-PB\") || key.startsWith(\"BRANDLABELPB\")) return 6;\n  if (key.startsWith(\"BRANDLABEL-PF\") || key.startsWith(\"BRANDLABELPF\")) return 7;\n  if (key.startsWith(\"BRANDLABEL-PBG\") || key.startsWith(\"BRANDLABELPBG\")) return 8;\n  const name = String(product || \"\").trim().toLowerCase();\n  if (name.includes(\"charger\") || name.includes(\"charge\") || name.includes(\"\u5145\u7535\u5668\")) return 9;\n  return 99;\n}<\/p>\n<p>function productSort(a, b) {\n  return a.product.localeCompare(b.product, undefined, {\n    numeric: true,\n    sensitivity: \"base\"\n  });\n}<\/p>\n<p>function findHeader(headers, candidates, fallback) {\n  let best = { key: fallback || headers[0], score: -1 };\n  headers.forEach((header) => {\n    const normalized = normalizeHeader(header);\n    let score = 0;\n    candidates.forEach((candidate) => {\n      const target = normalizeHeader(candidate);\n      if (normalized === target) score = Math.max(score, 100);\n      if (normalized.includes(target) || target.includes(normalized)) score = Math.max(score, 70);\n    });\n    if (score > best.score) best = { key: header, score };\n  });\n  if (best.score < 0 &#038;&#038; fallback === undefined) return \"\";\n  return best.key;\n}\n\nfunction valueFor(record, key) {\n  return key ? record[key] : undefined;\n}\n\nfunction snapshotRows() {\n  const rows = Array.isArray(window.PKT_INVENTORY_SNAPSHOT) ? window.PKT_INVENTORY_SNAPSHOT : [];\n  return rows.filter((row) => !isExcludedProduct(row.product));\n}<\/p>\n<p>function snapshotMap() {\n  return new Map(snapshotRows().map((row) => [productKey(row.product), row]));\n}<\/p>\n<p>function valueFromRecordOrSnapshot(record, recordKey, snapshot, snapshotKey, fallback = 0) {\n  const fromRecord = parseNumberOrNull(valueFor(record, recordKey));\n  if (fromRecord !== null) return fromRecord;\n  const fromSnapshot = parseNumberOrNull(snapshot?.[snapshotKey]);\n  return fromSnapshot !== null ? fromSnapshot : fallback;\n}<\/p>\n<p>function valueFromSnapshotOrRecord(record, recordKey, snapshot, snapshotKey, fallback = 0) {\n  const fromSnapshot = parseNumberOrNull(snapshot?.[snapshotKey]);\n  if (fromSnapshot !== null) return fromSnapshot;\n  const fromRecord = parseNumberOrNull(valueFor(record, recordKey));\n  return fromRecord !== null ? fromRecord : fallback;\n}<\/p>\n<p>function textFromRecordOrSnapshot(record, recordKey, snapshot, snapshotKey, fallback = \"\") {\n  const fromRecord = String(valueFor(record, recordKey) ?? \"\").trim();\n  if (fromRecord) return fromRecord;\n  const fromSnapshot = String(snapshot?.[snapshotKey] ?? \"\").trim();\n  return fromSnapshot || fallback;\n}<\/p>\n<p>function normalizeEtaEntries(entries) {\n  if (!Array.isArray(entries)) return [];\n  return entries\n    .map((entry) => ({\n      label: String(entry?.label || \"\").trim(),\n      eta: String(entry?.eta || \"\").trim(),\n      sourceSheet: String(entry?.sourceSheet || \"\").trim(),\n      qty: parseNumber(entry?.qty)\n    }))\n    .filter((entry) => entry.qty > 0);\n}<\/p>\n<p>function combineEtaEntries(entries) {\n  const combined = new Map();\n  normalizeEtaEntries(entries).forEach((entry) => {\n    const key = [entry.label, entry.eta, entry.sourceSheet].join(\"|\");\n    if (!combined.has(key)) {\n      combined.set(key, { ...entry, qty: 0 });\n    }\n    combined.get(key).qty += entry.qty;\n  });\n  return [...combined.values()];\n}<\/p>\n<p>function renderStockWithEta(quantity, entries) {\n  const etaEntries = normalizeEtaEntries(entries);\n  const etaMarkup = etaEntries.length\n    ? `<\/p>\n<div class=\"eta-lines\">${etaEntries.map(renderEtaLine).join(\"\")}<\/div>\n<p>`\n    : \"\";\n  return `<\/p>\n<div class=\"eta-qty\">${formatNumber(quantity)}<\/div>\n<p>${etaMarkup}`;\n}<\/p>\n<p>function renderEtaLine(entry) {\n  const label = entry.label ? `${entry.label} \u00b7 ` : \"\";\n  const eta = entry.eta || \"ETA\u5f85\u786e\u8ba4\";\n  const title = [entry.sourceSheet, entry.label, entry.eta, `${formatNumber(entry.qty)} pcs`].filter(Boolean).join(\" \/ \");\n  return `<\/p>\n<div class=\"eta-line\" title=\"${escapeHtml(title)}\">\n      <span>${escapeHtml(`${label}ETA ${eta}`)}<\/span>\n      <strong>${formatNumber(entry.qty)}<\/strong>\n    <\/div>\n<p>  `;\n}<\/p>\n<p>function normalizeInventoryRow(row) {\n  const current = parseNumber(row.current);\n  const sea = parseNumber(row.sea);\n  const production = parseNumber(row.production);\n  const totalStock = parseNumberOrNull(row.totalStock) ?? current + sea + production;\n  const salesFromMonths = averageFromSalesMonths(row.salesMonths);\n  const explicitSalesAverage = parseNumberOrNull(row.salesAvg3);\n  const salesAvg3 = salesFromMonths.average ?? explicitSalesAverage;\n  const monthsCover = salesAvg3 && salesAvg3 > 0 ? totalStock \/ salesAvg3 : parseNumberOrNull(row.monthsCover);\n  const purchaseRecommendation =\n    (salesFromMonths.hasData || explicitSalesAverage !== null) && salesAvg3 !== null\n      ? Math.max(0, Math.ceil(salesAvg3 * 3 - (current + sea)))\n      : null;<\/p>\n<p>  return {\n    ...row,\n    sourceBrand: row.sourceBrand || brandSourceForProduct(row.product),\n    current,\n    sea,\n    production,\n    seaEtas: normalizeEtaEntries(row.seaEtas),\n    productionEtas: normalizeEtaEntries(row.productionEtas),\n    totalStock,\n    salesAvg3,\n    salesAvgMonths: salesFromMonths.months,\n    monthsCover,\n    purchaseRecommendation,\n    estimated: parseNumber(row.estimated),\n    salesMtd: parseNumber(row.salesMtd),\n    latestSalesQty: parseNumberOrNull(row.latestSalesQty),\n    gap: current - parseNumber(row.estimated),\n    status: statusFor({ current, estimated: parseNumber(row.estimated) })\n  };\n}<\/p>\n<p>function averageFromSalesMonths(salesMonths) {\n  if (!salesMonths || typeof salesMonths !== \"object\") {\n    return { average: null, months: [], hasData: false };\n  }<\/p>\n<p>  const entries = Object.entries(salesMonths)\n    .map(([month, value]) => ({\n      month: Number(month),\n      value: parseNumberOrNull(value)\n    }))\n    .filter((entry) => Number.isInteger(entry.month) && entry.month >= 1 && entry.month <= 12 &#038;&#038; entry.value !== null)\n    .sort((a, b) => a.month - b.month);<\/p>\n<p>  if (!entries.length) return { average: null, months: [], hasData: false };<\/p>\n<p>  const total = entries.reduce((sum, entry) => sum + entry.value, 0);\n  return {\n    average: total \/ entries.length,\n    months: entries.map((entry) => entry.month),\n    hasData: true\n  };\n}<\/p>\n<p>function extractInventoryRows(records) {\n  const snapshots = snapshotMap();\n  if (!records.length) {\n    const snapshotOnlyRows = snapshotRows().map((snapshot) =>\n      normalizeInventoryRow({\n        product: snapshot.product,\n        sourceBrand: snapshot.sourceBrand || brandSourceForProduct(snapshot.product),\n        series: snapshot.series || \"\",\n        voltage: snapshot.voltage || \"\",\n        current: snapshot.current,\n        sea: snapshot.sea,\n        production: snapshot.production,\n        seaEtas: snapshot.seaEtas || [],\n        productionEtas: snapshot.productionEtas || [],\n        totalStock: snapshot.totalStock,\n        estimated: 0,\n        salesAvg3: snapshot.salesAvg3,\n        salesMonths: snapshot.salesMonths,\n        monthsCover: snapshot.monthsCover,\n        salesMtd: snapshot.salesMtd,\n        latestSalesLabel: snapshot.latestSalesLabel || \"\",\n        latestSalesQty: snapshot.latestSalesQty,\n        sourceSheets: snapshot.sourceSheets || []\n      })\n    );\n    return attachAlternatives(snapshotOnlyRows);\n  }<\/p>\n<p>  const headers = Object.keys(records[0]);\n  const productHeaderKey = findHeader(\n    headers,\n    [\"Product\", \"Product Name\", \"Model\", \"Item\", \"SKU\", \"\u4ea7\u54c1\", \"\u4ea7\u54c1\u540d\u79f0\", \"\u4ea7\u54c1\u7f16\u7801\", \"\u578b\u53f7\", \"\u54c1\u540d\", \"\u7269\u6599\"],\n    headers[0]\n  );\n  const currentKey = findHeader(headers, [\n    \"Current stock\",\n    \"Current Stock\",\n    \"\u73b0\u8d27\u6570\u91cf\",\n    \"\u73b0\u8d27\",\n    \"\u5b9e\u9645\u5e93\u5b58\",\n    \"\u5f53\u524d\u5e93\u5b58\",\n    \"\u5e93\u5b58\u6570\u91cf\"\n  ]);\n  const seaKey = findHeader(headers, [\n    \"\u6d77\u4e0a\u5e93\u5b58\",\n    \"\u5728\u9014\u5e93\u5b58\",\n    \"On Sea\",\n    \"Sea Stock\",\n    \"In Transit\"\n  ]);\n  const productionKey = findHeader(headers, [\n    \"\u751f\u4ea7\u4e2d\",\n    \"\u751f\u4ea7\u5e93\u5b58\",\n    \"In Production\",\n    \"Production\"\n  ]);\n  const totalStockKey = findHeader(headers, [\n    \"\u603b\u5e93\u5b58\",\n    \"Total Stock\",\n    \"Total Inventory\",\n    \"\u73b0\u8d27+\u6d77\u4e0a+\u751f\u4ea7\"\n  ]);\n  const salesAvg3Key = findHeader(headers, [\n    \"\u4eca\u5e74\u6708\u5747\u9500\u91cf\",\n    \"\u4eca\u5e74\u6708\u5747\u9500\u552e\",\n    \"\u5168\u5e74\u6708\u5747\u9500\u91cf\",\n    \"\u5168\u5e74\u6708\u5747\u9500\u552e\",\n    \"\u524d\u4e09\u4e2a\u6708\u5e73\u5747\u9500\u91cf\",\n    \"\u524d\u4e09\u4e2a\u6708 \u6708\u5747\u9500\u91cf\",\n    \"3\u4e2a\u6708\u5e73\u5747\u9500\u91cf\",\n    \"Average Sales\",\n    \"Avg Sales\",\n    \"Sales Avg 3\"\n  ]);\n  const monthsCoverKey = findHeader(headers, [\n    \"\u603b\u5e93\u5b58\u53ef\u552e\u65f6\u95f4\/\u6708\",\n    \"\u53ef\u552e\u65f6\u95f4\/\u6708\",\n    \"\u53ef\u552e\u6708\u6570\",\n    \"Months Cover\",\n    \"Inventory Months\"\n  ]);\n  const salesMtdKey = findHeader(headers, [\n    \"\u672c\u6708\u9500\u552e\u51fa\u5e93\",\n    \"\u672c\u6708\u9500\u91cf\",\n    \"\u9500\u552e\u51fa\u5e93\",\n    \"Sales MTD\"\n  ]);\n  const seriesKey = findHeader(headers, [\"\u4ea7\u54c1\u7cfb\u5217\", \"Series\"]);\n  const voltageKey = findHeader(headers, [\"\u7535\u538b\", \"Voltage\"]);\n  const estimatedKey = findHeader(headers, [\n    \"Estimated Quantity\",\n    \"Estimated Qty\",\n    \"Estimate Quantity\",\n    \"PI Quantity\",\n    \"PI\u6570\u91cf\",\n    \"\u505a\u4e86PI\",\n    \"\u767b\u8bb0\u6570\u91cf\",\n    \"\u9884\u4f30\u6570\u91cf\",\n    \"\u9884\u8ba1\u6570\u91cf\"\n  ]);<\/p>\n<p>  const rows = records\n    .map((record) => {\n      const product = String(record[productHeaderKey] ?? \"\").trim();\n      const snapshot = snapshots.get(productKey(product));\n      return {\n        product,\n        sourceBrand: textFromRecordOrSnapshot(record, \"\", snapshot, \"sourceBrand\", brandSourceForProduct(product)),\n        series: textFromRecordOrSnapshot(record, seriesKey, snapshot, \"series\"),\n        voltage: textFromRecordOrSnapshot(record, voltageKey, snapshot, \"voltage\"),\n        current: valueFromSnapshotOrRecord(record, currentKey, snapshot, \"current\"),\n        sea: valueFromSnapshotOrRecord(record, seaKey, snapshot, \"sea\"),\n        production: valueFromSnapshotOrRecord(record, productionKey, snapshot, \"production\"),\n        seaEtas: snapshot?.seaEtas || [],\n        productionEtas: snapshot?.productionEtas || [],\n        totalStock: valueFromSnapshotOrRecord(record, totalStockKey, snapshot, \"totalStock\", null),\n        estimated: parseNumber(valueFor(record, estimatedKey)),\n        salesAvg3: valueFromSnapshotOrRecord(record, salesAvg3Key, snapshot, \"salesAvg3\", null),\n        salesMonths: snapshot?.salesMonths || null,\n        monthsCover: valueFromSnapshotOrRecord(record, monthsCoverKey, snapshot, \"monthsCover\", null),\n        salesMtd: valueFromSnapshotOrRecord(record, salesMtdKey, snapshot, \"salesMtd\"),\n        latestSalesLabel: snapshot?.latestSalesLabel || \"\",\n        latestSalesQty: snapshot?.latestSalesQty ?? null,\n        sourceSheets: snapshot?.sourceSheets || []\n      };\n    })\n    .filter((row) => row.product && !isExcludedProduct(row.product))\n    .map(normalizeInventoryRow);<\/p>\n<p>  const seen = new Set(rows.map((row) => productKey(row.product)));\n  snapshotRows().forEach((snapshot) => {\n    if (seen.has(productKey(snapshot.product))) return;\n    rows.push(\n      normalizeInventoryRow({\n        product: snapshot.product,\n        sourceBrand: snapshot.sourceBrand || brandSourceForProduct(snapshot.product),\n        series: snapshot.series || \"\",\n        voltage: snapshot.voltage || \"\",\n        current: snapshot.current,\n        sea: snapshot.sea,\n        production: snapshot.production,\n        seaEtas: snapshot.seaEtas || [],\n        productionEtas: snapshot.productionEtas || [],\n        totalStock: snapshot.totalStock,\n        estimated: 0,\n        salesAvg3: snapshot.salesAvg3,\n        salesMonths: snapshot.salesMonths,\n        monthsCover: snapshot.monthsCover,\n        salesMtd: snapshot.salesMtd,\n        latestSalesLabel: snapshot.latestSalesLabel || \"\",\n        latestSalesQty: snapshot.latestSalesQty,\n        sourceSheets: snapshot.sourceSheets || []\n      })\n    );\n  });<\/p>\n<p>  return attachAlternatives(rows);\n}<\/p>\n<p>function parseCsv(text) {\n  const rows = [];\n  let current = \"\";\n  let row = [];\n  let inQuotes = false;<\/p>\n<p>  for (let index = 0; index < text.length; index += 1) {\n    const char = text[index];\n    const next = text[index + 1];\n\n    if (char === '\"' &#038;&#038; inQuotes &#038;&#038; next === '\"') {\n      current += '\"';\n      index += 1;\n    } else if (char === '\"') {\n      inQuotes = !inQuotes;\n    } else if (char === \",\" &#038;&#038; !inQuotes) {\n      row.push(current);\n      current = \"\";\n    } else if ((char === \"\\n\" || char === \"\\r\") &#038;&#038; !inQuotes) {\n      if (char === \"\\r\" &#038;&#038; next === \"\\n\") index += 1;\n      row.push(current);\n      rows.push(row);\n      row = [];\n      current = \"\";\n    } else {\n      current += char;\n    }\n  }\n\n  if (current || row.length) {\n    row.push(current);\n    rows.push(row);\n  }\n\n  const cleanRows = rows.filter((entry) => entry.some((cell) => String(cell).trim()));\n  if (!cleanRows.length) return [];<\/p>\n<p>  const headers = cleanRows[0].map((cell, index) => String(cell || `Column ${index + 1}`).trim());\n  return cleanRows.slice(1).map((entry) => {\n    const record = {};\n    headers.forEach((header, index) => {\n      record[header] = entry[index] ?? \"\";\n    });\n    return record;\n  });\n}<\/p>\n<p>function visualizationToRecords(payload) {\n  const table = payload && payload.table;\n  if (!table || !Array.isArray(table.cols) || !Array.isArray(table.rows)) return [];\n  const headers = table.cols.map((col, index) => col.label || col.id || `Column ${index + 1}`);\n  return table.rows.map((row) => {\n    const record = {};\n    headers.forEach((header, index) => {\n      const cell = row.c && row.c[index];\n      record[header] = cell ? cell.f ?? cell.v ?? \"\" : \"\";\n    });\n    return record;\n  });\n}<\/p>\n<p>function loadSheetJsonp() {\n  return new Promise((resolve, reject) => {\n    const requestId = Date.now();\n    const callbackName = `__pktInventorySheet_${requestId}`;\n    const timeout = window.setTimeout(() => {\n      cleanup();\n      reject(new Error(\"\u8bfb\u53d6 Google Sheet \u8d85\u65f6\"));\n    }, 20000);<\/p>\n<p>    const script = document.createElement(\"script\");\n    const base = `https:\/\/docs.google.com\/spreadsheets\/d\/${SHEET.id}\/gviz\/tq`;\n    const tqx = `out:json;responseHandler:${callbackName}`;<\/p>\n<p>    function cleanup() {\n      window.clearTimeout(timeout);\n      delete window[callbackName];\n      script.remove();\n    }<\/p>\n<p>    window[callbackName] = (payload) => {\n      cleanup();\n      if (payload && payload.status === \"error\") {\n        reject(new Error(payload.errors?.[0]?.detailed_message || \"Google Sheet \u8fd4\u56de\u9519\u8bef\"));\n        return;\n      }\n      resolve(visualizationToRecords(payload));\n    };<\/p>\n<p>    window.google = window.google || {};\n    window.google.visualization = window.google.visualization || {};\n    window.google.visualization.Query = window.google.visualization.Query || {};\n    window.google.visualization.Query.setResponse = window[callbackName];<\/p>\n<p>    script.onerror = () => {\n      cleanup();\n      reject(new Error(\"\u65e0\u6cd5\u8fde\u63a5 Google Sheet\"));\n    };\n    script.src = `${base}?gid=${encodeURIComponent(SHEET.gid)}&tqx=${encodeURIComponent(tqx)}`;\n    script.src += `&tq=${encodeURIComponent(\"select *\")}&cacheBust=${requestId}`;\n    document.head.appendChild(script);\n  });\n}<\/p>\n<p>async function loadSheetCsv() {\n  const url = `https:\/\/docs.google.com\/spreadsheets\/d\/${SHEET.id}\/export?format=csv&gid=${SHEET.gid}&cacheBust=${Date.now()}`;\n  const response = await fetch(url, { cache: \"no-store\" });\n  if (!response.ok) throw new Error(`CSV \u8bfb\u53d6\u5931\u8d25\uff1a${response.status}`);\n  return parseCsv(await response.text());\n}<\/p>\n<p>function setNotice(message, type = \"\") {\n  els.sourceNotice.textContent = message;\n  els.sourceNotice.className = `notice ${message ? \"show\" : \"\"} ${type}`.trim();\n}<\/p>\n<p>function setRows(records, source) {\n  state.rawRows = extractInventoryRows(records);\n  state.source = source;\n  state.error = \"\";\n  state.lastSyncAt = new Date();\n  applyFilters();\n}<\/p>\n<p>function scheduleNextRefresh() {\n  window.clearTimeout(autoRefreshTimer);\n  state.nextSyncAt = new Date(Date.now() + AUTO_REFRESH_MS);\n  autoRefreshTimer = window.setTimeout(() => {\n    refreshFromSheet({ silent: true });\n  }, AUTO_REFRESH_MS);\n  renderSyncState();\n}<\/p>\n<p>async function refreshFromSheet(options = {}) {\n  const silent = Boolean(options.silent);\n  if (state.loading) return;\n  state.loading = true;\n  state.error = \"\";\n  render();\n  if (!silent) setNotice(\"\u6b63\u5728\u8bfb\u53d6 Google Sheet \u6570\u636e...\", \"\");<\/p>\n<p>  try {\n    const records = await loadSheetJsonp();\n    setRows(records, \"Google Sheet\");\n    setNotice(`\u5df2\u540c\u6b65 ${SHEET.title}\uff0c\u5e76\u8865\u5145\u6d77\u4e0a\u5e93\u5b58\u3001\u751f\u4ea7\u4e2d\u3001\u9500\u552e\u4e0e\u53ef\u552e\u6708\u6570\u3002`, \"success\");\n  } catch (jsonpError) {\n    try {\n      const records = await loadSheetCsv();\n      setRows(records, \"Google Sheet CSV\");\n      setNotice(`\u5df2\u901a\u8fc7 CSV \u540c\u6b65 ${SHEET.title}\uff0c\u5e76\u8865\u5145\u6d77\u4e0a\u5e93\u5b58\u3001\u751f\u4ea7\u4e2d\u3001\u9500\u552e\u4e0e\u53ef\u552e\u6708\u6570\u3002`, \"success\");\n    } catch {\n      if (!state.rawRows.length) {\n        state.rawRows = [];\n        state.rows = [];\n      }\n      state.error = jsonpError.message;\n      setNotice(\n        \"\u6682\u65f6\u65e0\u6cd5\u540c\u6b65 Google Sheet\u3002\u8bf7\u786e\u8ba4\u8868\u683c\u5171\u4eab\u6743\u9650\uff1b\u770b\u677f\u4e0d\u4f1a\u6df7\u7528\u9500\u552e\u6570\u636e\uff0c\u4e0b\u4e00\u8f6e\u4f1a\u7ee7\u7eed\u81ea\u52a8\u91cd\u8bd5\u3002\",\n        \"error\"\n      );\n      render();\n    }\n  } finally {\n    state.loading = false;\n    scheduleNextRefresh();\n    render();\n  }\n}<\/p>\n<p>function applyFilters() {\n  const query = state.search.trim().toLowerCase();\n  let rows = [...state.rawRows];<\/p>\n<p>  if (query) {\n    rows = rows.filter((row) => row.product.toLowerCase().includes(query));\n  }<\/p>\n<p>  if (state.status !== \"all\") {\n    rows = rows.filter((row) => row.status === state.status);\n  }<\/p>\n<p>  rows.sort((a, b) => {\n    if (state.sort === \"current-desc\") return b.current - a.current || productSort(a, b);\n    if (state.sort === \"total-desc\") return b.totalStock - a.totalStock || productSort(a, b);\n    if (state.sort === \"cover-asc\") return comparableCover(a) - comparableCover(b) || productSort(a, b);\n    if (state.sort === \"sales-desc\") return (b.salesAvg3 || 0) - (a.salesAvg3 || 0) || productSort(a, b);\n    if (state.sort === \"purchase-desc\") return comparablePurchase(b) - comparablePurchase(a) || productSort(a, b);\n    if (state.sort === \"estimated-desc\") return b.estimated - a.estimated || productSort(a, b);\n    if (state.sort === \"gap-asc\") return a.gap - b.gap || productSort(a, b);\n    if (state.sort === \"product\") return productSort(a, b);\n    return seriesRank(a.product) - seriesRank(b.product) || productSort(a, b);\n  });<\/p>\n<p>  if (state.sort === \"series\") {\n    rows = groupedSeriesRows(rows);\n  }<\/p>\n<p>  state.rows = rows;\n  render();\n}<\/p>\n<p>function comparableCover(row) {\n  return Number.isFinite(row.monthsCover) ? row.monthsCover : 999999;\n}<\/p>\n<p>function comparablePurchase(row) {\n  return Number.isFinite(row.purchaseRecommendation) ? row.purchaseRecommendation : -1;\n}<\/p>\n<p>function renderSummary() {\n  const totalCurrent = state.rawRows.reduce((sum, row) => sum + row.current, 0);\n  const totalSea = state.rawRows.reduce((sum, row) => sum + row.sea, 0);\n  const totalProduction = state.rawRows.reduce((sum, row) => sum + row.production, 0);\n  const totalStock = state.rawRows.reduce((sum, row) => sum + row.totalStock, 0);\n  const totalEstimated = state.rawRows.reduce((sum, row) => sum + row.estimated, 0);\n  const totalSalesAvg = state.rawRows.reduce((sum, row) => sum + (row.salesAvg3 || 0), 0);\n  const weightedMonthsCover = totalSalesAvg > 0 ? totalStock \/ totalSalesAvg : null;\n  const coveredCount = state.rawRows.filter((row) => row.status === \"covered\" || row.status === \"stock-only\").length;<\/p>\n<p>  els.productCount.textContent = state.loading ? \"...\" : formatNumber(state.rawRows.length);\n  els.currentTotal.textContent = state.loading ? \"...\" : formatNumber(totalCurrent);\n  if (els.seaTotal) els.seaTotal.textContent = state.loading ? \"...\" : formatNumber(totalSea);\n  if (els.productionTotal) els.productionTotal.textContent = state.loading ? \"...\" : formatNumber(totalProduction);\n  if (els.totalStockTotal) els.totalStockTotal.textContent = state.loading ? \"...\" : formatNumber(totalStock);\n  if (els.monthsCoverTotal) els.monthsCoverTotal.textContent = state.loading ? \"...\" : formatDecimal(weightedMonthsCover, 1);\n  els.estimatedTotal.textContent = state.loading ? \"...\" : formatNumber(totalEstimated);\n  els.coverageTotal.textContent = state.loading\n    ? \"...\"\n    : state.rawRows.length\n      ? `${coveredCount}\/${state.rawRows.length}`\n      : \"--\";\n}<\/p>\n<p>function renderChart() {\n  if (!state.rows.length) {\n    els.chartList.innerHTML = `<\/p>\n<div class=\"empty-state\">${state.loading ? \"\u6b63\u5728\u52a0\u8f7d\u5e93\u5b58\u6570\u636e\" : \"\u6682\u65e0\u53ef\u663e\u793a\u7684\u5e93\u5b58\u6570\u636e\"}<\/div>\n<p>`;\n    return;\n  }<\/p>\n<p>  const maxValue = Math.max(...state.rows.flatMap((row) => [row.current, row.sea, row.production, row.estimated]), 1);\n  const chartGroups =\n    state.sort === \"series\"\n      ? equivalentStockGroupsForRows(state.rows)\n      : state.rows.map((row) => ({\n          family: replacementFamily(row.product),\n          products: [row.product],\n          members: [row],\n          current: row.current,\n          totalStock: row.totalStock,\n          standalone: true\n        }));<\/p>\n<p>  function renderChartRow(row) {\n      const currentWidth = `${Math.max((row.current \/ maxValue) * 100, row.current ? 4 : 0)}%`;\n      const seaWidth = `${Math.max((row.sea \/ maxValue) * 100, row.sea ? 4 : 0)}%`;\n      const productionWidth = `${Math.max((row.production \/ maxValue) * 100, row.production ? 4 : 0)}%`;\n      const estimatedWidth = `${Math.max((row.estimated \/ maxValue) * 100, row.estimated ? 4 : 0)}%`;\n      const alternativeNote = alternativeText(row);\n      const coverWarning = coverWarningText(row);\n      return `<\/p>\n<div class=\"chart-row\">\n<div class=\"chart-product\">\n<div class=\"chart-name\" title=\"${escapeHtml(row.product)}\">${escapeHtml(row.product)}<\/div>\n<div class=\"chart-meta\">${escapeHtml([row.sourceBrand, row.voltage, coverText(row)].filter(Boolean).join(\" \u00b7 \"))}<\/div>\n<p>            ${coverWarning ? `<\/p>\n<div class=\"cover-warning\">${escapeHtml(coverWarning)}<\/div>\n<p>` : \"\"}\n            ${alternativeNote ? `<\/p>\n<div class=\"alternative-note\">${escapeHtml(alternativeNote)}<\/div>\n<p>` : \"\"}\n          <\/p><\/div>\n<div class=\"bar-stack\">\n<div class=\"bar-line\">\n              <span class=\"bar-label\">\u73b0\u8d27<\/span><\/p>\n<div class=\"bar-track\">\n<div class=\"bar-fill current\" style=\"--bar-width:${currentWidth}\"><\/div>\n<\/div><\/div>\n<div class=\"bar-line\">\n              <span class=\"bar-label\">\u6d77\u4e0a<\/span><\/p>\n<div class=\"bar-track\">\n<div class=\"bar-fill sea\" style=\"--bar-width:${seaWidth}\"><\/div>\n<\/div><\/div>\n<div class=\"bar-line\">\n              <span class=\"bar-label\">\u751f\u4ea7<\/span><\/p>\n<div class=\"bar-track\">\n<div class=\"bar-fill production\" style=\"--bar-width:${productionWidth}\"><\/div>\n<\/div><\/div>\n<div class=\"bar-line\">\n              <span class=\"bar-label\">PI<\/span><\/p>\n<div class=\"bar-track\">\n<div class=\"bar-fill estimated\" style=\"--bar-width:${estimatedWidth}\"><\/div>\n<\/div><\/div>\n<\/p><\/div>\n<div class=\"chart-values\">\n            <span>\u73b0\u8d27 <strong>${formatNumber(row.current)}<\/strong><\/span>\n            <span>\u603b\u5e93\u5b58 <strong>${formatNumber(row.totalStock)}<\/strong><\/span>\n            <span>\u6708\u5747\u9500\u552e <strong>${formatDecimal(row.salesAvg3, 1)}<\/strong><\/span>\n          <\/div>\n<\/p><\/div>\n<p>      `;\n  }<\/p>\n<p>  els.chartList.innerHTML = chartGroups\n    .map((group) => {\n      const groupLabel = group.standalone\n        ? \"\"\n        : `<\/p>\n<div class=\"chart-group-label\">\n            <span>${escapeHtml(group.family)} \u540c\u578b\u53f7<\/span>\n            <strong>\u5408\u8ba1\uff1a\u73b0\u8d27 ${formatNumber(group.current)} \u00b7 \u603b\u5e93\u5b58 ${formatNumber(group.totalStock)}<\/strong>\n          <\/div>\n<p>        `;\n      return `<\/p>\n<div class=\"${group.standalone ? \"chart-group\" : \"chart-group chart-equivalent-group\"}\">\n          ${groupLabel}\n          ${group.members.map(renderChartRow).join(\"\")}\n        <\/div>\n<p>      `;\n    })\n    .join(\"\");\n}<\/p>\n<p>function alternativeText(row) {\n  if (!row.alternatives || !row.alternatives.length) return \"\";\n  return `\u66ff\u4ee3\u6709\u8d27\uff1a${row.alternatives\n    .map((alternative) => `${alternative.product} (${formatNumber(alternative.current)})`)\n    .join(\" \/ \")}`;\n}<\/p>\n<p>function coverText(row) {\n  if (!Number.isFinite(row.monthsCover)) return \"\";\n  return `\u53ef\u552e ${formatDecimal(row.monthsCover, 1)} \u6708`;\n}<\/p>\n<p>function coverWarningLevel(row) {\n  if (!Number.isFinite(row.monthsCover)) return \"\";\n  if (row.monthsCover < 3) return \"replenish\";\n  if (row.monthsCover > 6) return \"level1\";\n  if (row.monthsCover > 4) return \"over4\";\n  return \"\";\n}<\/p>\n<p>function coverWarningText(row) {\n  const level = coverWarningLevel(row);\n  if (level === \"replenish\") return \"\u8865\u8d27\u63d0\u9192\uff1a\u5e93\u5b58\u53ef\u552e\u4f4e\u4e8e 3 \u4e2a\u6708\";\n  if (level === \"level1\") return \"\u4e00\u7ea7\u8b66\u544a\uff1a\u5e93\u5b58\u53ef\u552e\u8d85\u8fc7\u534a\u5e74\";\n  if (level === \"over4\") return \"\u5e93\u5b58\u53ef\u552e\u8d85\u8fc7 4 \u4e2a\u6708\";\n  return \"\";\n}<\/p>\n<p>function purchaseRecommendationText(row) {\n  if (!Number.isFinite(row.purchaseRecommendation)) return \"--\";\n  return formatNumber(row.purchaseRecommendation);\n}<\/p>\n<p>function alternativeStatusText(row) {\n  if (!row.alternatives || !row.alternatives.length) return statusText(row.status);\n  return row.current <= 0 ? \"\u65e0\u73b0\u8d27\uff0c\u53ef\u66ff\u4ee3\" : \"\u5e93\u5b58\u4e0d\u8db3\uff0c\u53ef\u66ff\u4ee3\";\n}\n\nfunction renderReplacementRules() {\n  if (!els.rulesList) return;\n  const pairs = replacementPairsForRows(state.rawRows);\n  if (!pairs.length) {\n    els.rulesList.innerHTML = `\n\n<div class=\"empty-state\">${state.loading ? \"\u6b63\u5728\u52a0\u8f7d\u66ff\u4ee3\u89c4\u5219\" : \"\u6682\u65e0\u53ef\u5339\u914d\u7684\u66ff\u4ee3\u578b\u53f7\"}<\/div>\n<p>`;\n    return;\n  }<\/p>\n<p>  els.rulesList.innerHTML = pairs\n    .map(\n      (pair) => `<\/p>\n<div class=\"rule-row\">\n          <span class=\"rule-family\">${escapeHtml(pair.family)}<\/span><\/p>\n<div class=\"rule-pair\">\n            <span>${escapeHtml(pair.left)}<\/span>\n            <strong>=<\/strong>\n            <span>${escapeHtml(pair.right)}<\/span>\n          <\/div>\n<\/p><\/div>\n<p>      `\n    )\n    .join(\"\");\n}<\/p>\n<p>function renderEquivalentSummary() {\n  if (!els.equivalentRows) return;\n  const groups = equivalentStockGroupsForRows(state.rawRows);\n  if (!groups.length) {\n    els.equivalentRows.innerHTML = `<\/p>\n<tr>\n<td class=\"empty-state\" colspan=\"9\">${state.loading ? \"\u6b63\u5728\u52a0\u8f7d\u578b\u53f7\u603b\u5e93\u5b58\" : \"\u6682\u65e0\u578b\u53f7\u5e93\u5b58\u6570\u636e\"}<\/td>\n<\/tr>\n<p>    `;\n    return;\n  }<\/p>\n<p>  els.equivalentRows.innerHTML = groups\n    .map((group) => {\n      if (group.standalone) {\n        const row = group.members[0];\n        const purchaseClass = group.purchaseRecommendation > 0 ? \"purchase-needed\" : \"purchase-ok\";\n        return `<\/p>\n<tr class=\"equivalent-group-start equivalent-standalone-row\">\n<td>\n<div class=\"equivalent-family\">${escapeHtml(group.family)}<\/div>\n<div class=\"equivalent-product-name\">${escapeHtml(row.product)}<\/div>\n<\/td>\n<td class=\"number-cell\">${formatNumber(group.current)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.sea)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.production)}<\/td>\n<td class=\"number-cell total-cell\">${formatNumber(group.totalStock)}<\/td>\n<td class=\"number-cell\">${formatDecimal(group.salesAverage, 1)}<\/td>\n<td class=\"number-cell\">${formatDecimal(group.monthsCover, 1)}<\/td>\n<td class=\"number-cell purchase-cell ${purchaseClass}\">${purchaseRecommendationText(group)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.estimated)}<\/td>\n<\/tr>\n<p>        `;\n      }<\/p>\n<p>      const memberRows = group.members\n        .map((row, index) => {\n          const purchaseClass = row.purchaseRecommendation > 0 ? \"purchase-needed\" : \"purchase-ok\";\n          return `<\/p>\n<tr class=\"${index === 0 ? \"equivalent-group-start\" : \"\"}\">\n<td>\n<div class=\"equivalent-family\">${index === 0 ? escapeHtml(group.family) : \"\"}<\/div>\n<div class=\"equivalent-product-name\">${escapeHtml(row.product)}<\/div>\n<\/td>\n<td class=\"number-cell\">${formatNumber(row.current)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.sea)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.production)}<\/td>\n<td class=\"number-cell total-cell\">${formatNumber(row.totalStock)}<\/td>\n<td class=\"number-cell\">${formatDecimal(row.salesAvg3, 1)}<\/td>\n<td class=\"number-cell\">${formatDecimal(row.monthsCover, 1)}<\/td>\n<td class=\"number-cell purchase-cell ${purchaseClass}\">${purchaseRecommendationText(row)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.estimated)}<\/td>\n<\/tr>\n<p>          `;\n        })\n        .join(\"\");\n      const purchaseClass = group.purchaseRecommendation > 0 ? \"purchase-needed\" : \"purchase-ok\";\n      return `\n        ${memberRows}<\/p>\n<tr class=\"equivalent-total-row\">\n<td>\n<div class=\"equivalent-total-label\">\u5408\u8ba1<\/div>\n<div class=\"equivalent-products\">${group.products.map((product) => escapeHtml(product)).join(\" + \")}<\/div>\n<\/td>\n<td class=\"number-cell\">${formatNumber(group.current)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.sea)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.production)}<\/td>\n<td class=\"number-cell total-cell\">${formatNumber(group.totalStock)}<\/td>\n<td class=\"number-cell\">${formatDecimal(group.salesAverage, 1)}<\/td>\n<td class=\"number-cell\">${formatDecimal(group.monthsCover, 1)}<\/td>\n<td class=\"number-cell purchase-cell ${purchaseClass}\">${purchaseRecommendationText(group)}<\/td>\n<td class=\"number-cell\">${formatNumber(group.estimated)}<\/td>\n<\/tr>\n<p>      `;\n    })\n    .join(\"\");\n}<\/p>\n<p>function escapeHtml(value) {\n  return String(value)\n    .replace(\/&\/g, \"&amp;\")\n    .replace(\/<\/g, \"&lt;\")\n    .replace(\/>\/g, \"&gt;\")\n    .replace(\/\"\/g, \"&quot;\");\n}<\/p>\n<p>function renderTable() {\n  els.tableSubhead.textContent = state.rawRows.length\n    ? `${state.rows.length} \/ ${state.rawRows.length} \u4e2a\u4ea7\u54c1`\n    : \"\u7b49\u5f85 Google Sheet \u6216 CSV \u6570\u636e\";<\/p>\n<p>  if (!state.rows.length) {\n    els.inventoryRows.innerHTML = `<\/p>\n<tr>\n<td class=\"empty-state\" colspan=\"11\">${state.loading ? \"\u6b63\u5728\u52a0\u8f7d\u5e93\u5b58\u6570\u636e\" : \"\u6682\u65e0\u5e93\u5b58\u6570\u636e\"}<\/td>\n<\/tr>\n<p>    `;\n    return;\n  }<\/p>\n<p>  els.inventoryRows.innerHTML = state.rows\n    .map(\n      (row) => {\n        const alternativeNote = alternativeText(row);\n        const coverWarning = coverWarningText(row);\n        const coverLevel = coverWarningLevel(row);\n        const purchaseClass = row.purchaseRecommendation > 0 ? \"purchase-needed\" : \"purchase-ok\";\n        return `<\/p>\n<tr class=\"${[alternativeNote ? \"has-alternative\" : \"\", coverLevel ? `cover-${coverLevel}` : \"\"].filter(Boolean).join(\" \")}\">\n<td>\n<div class=\"table-product\">${escapeHtml(row.product)}<\/div>\n<div class=\"table-meta\">${escapeHtml([row.sourceBrand || brandSourceForProduct(row.product), row.series, row.voltage, sourceText(row)].filter(Boolean).join(\" \u00b7 \"))}<\/div>\n<p>            ${coverWarning ? `<\/p>\n<div class=\"cover-warning\">${escapeHtml(coverWarning)}<\/div>\n<p>` : \"\"}\n            ${alternativeNote ? `<\/p>\n<div class=\"alternative-note\">${escapeHtml(alternativeNote)}<\/div>\n<p>` : \"\"}\n          <\/td>\n<td class=\"number-cell\">${formatNumber(row.current)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.sea)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.production)}<\/td>\n<td class=\"number-cell total-cell\">${formatNumber(row.totalStock)}<\/td>\n<td class=\"number-cell\">${formatDecimal(row.salesAvg3, 1)}<\/td>\n<td class=\"number-cell ${coverLevel ? `cover-cell-${coverLevel}` : \"\"}\">${formatDecimal(row.monthsCover, 1)}<\/td>\n<td class=\"number-cell purchase-cell ${purchaseClass}\">${purchaseRecommendationText(row)}<\/td>\n<td class=\"number-cell\">${formatNumber(row.estimated)}<\/td>\n<td><span class=\"status-pill status-${alternativeNote ? \"short\" : row.status === \"stock-only\" ? \"covered\" : row.status}\">${alternativeNote ? alternativeStatusText(row) : statusText(row.status)}<\/span><\/td>\n<td class=\"alt-cell\">${alternativeNote ? escapeHtml(row.alternatives.map((alternative) => `${alternative.product} (${formatNumber(alternative.current)})`).join(\" \/ \")) : \"--\"}<\/td>\n<\/tr>\n<p>      `;\n      }\n    )\n    .join(\"\");\n}<\/p>\n<p>function sourceText(row) {\n  return row.sourceSheets && row.sourceSheets.length ? row.sourceSheets.join(\"\/\") : \"\";\n}<\/p>\n<p>function render() {\n  renderSummary();\n  renderTable();\n  renderEquivalentSummary();\n  renderReplacementRules();\n  renderChart();\n  renderSyncState();\n  els.refreshBtn.disabled = state.loading;\n  els.refreshBtn.textContent = state.loading ? \"\u8bfb\u53d6\u4e2d...\" : \"\u5237\u65b0\u6570\u636e\";\n}<\/p>\n<p>function renderSyncState() {\n  if (els.lastSync) {\n    els.lastSync.textContent = state.lastSyncAt\n      ? `\u6700\u540e\u540c\u6b65\uff1a${formatClock(state.lastSyncAt)}`\n      : \"\u6700\u540e\u540c\u6b65\uff1a\u7b49\u5f85\u8bfb\u53d6\";\n  }\n  if (els.nextSync) {\n    if (state.loading) {\n      els.nextSync.textContent = \"\u6b63\u5728\u540c\u6b65\";\n    } else if (state.nextSyncAt) {\n      const seconds = Math.max(0, Math.ceil((state.nextSyncAt.getTime() - Date.now()) \/ 1000));\n      els.nextSync.textContent = `\u4e0b\u6b21\u5237\u65b0\uff1a${seconds}s`;\n    } else {\n      els.nextSync.textContent = \"\u4e0b\u6b21\u5237\u65b0\uff1a--\";\n    }\n  }\n}<\/p>\n<p>async function readSelectedCsvFile() {\n  const file = els.csvFile.files && els.csvFile.files[0];\n  if (!file) return \"\";\n  return file.text();\n}<\/p>\n<p>async function loadManualCsv() {\n  const fileText = await readSelectedCsvFile();\n  const text = fileText || els.csvPaste.value;\n  if (!text.trim()) {\n    setNotice(\"\u8bf7\u5148\u9009\u62e9 CSV \u6587\u4ef6\uff0c\u6216\u7c98\u8d34 CSV \u5185\u5bb9\u3002\", \"error\");\n    return;\n  }\n  const records = parseCsv(text);\n  setRows(records, \"CSV\");\n  setNotice(\"\u5df2\u5bfc\u5165 CSV\uff0c\u5e76\u8865\u5145\u6d77\u4e0a\u5e93\u5b58\u3001\u751f\u4ea7\u4e2d\u3001\u9500\u552e\u4e0e\u53ef\u552e\u6708\u6570\u3002\", \"success\");\n}<\/p>\n<p>els.refreshBtn.addEventListener(\"click\", refreshFromSheet);\nels.searchInput.addEventListener(\"input\", (event) => {\n  state.search = event.target.value;\n  applyFilters();\n});\nels.statusFilter.addEventListener(\"change\", (event) => {\n  state.status = event.target.value;\n  applyFilters();\n});\nels.sortSelect.addEventListener(\"change\", (event) => {\n  state.sort = event.target.value;\n  applyFilters();\n});\nels.loadCsvBtn.addEventListener(\"click\", loadManualCsv);<\/p>\n<p>countdownTimer = window.setInterval(renderSyncState, 1000);\nrefreshFromSheet();<\/p>\n<p>  <\/script>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>2026 \u5fb7\u4ed3 PKT \u5b9e\u9645\u5e93\u5b58 PKT \u5e93\u5b58\u6570\u636e\u770b\u677f \u6253\u5f00\u539f\u59cb\u8868\u683c \u5237\u65b0\u6570\u636e \u81ea\u52a8\u540c\u6b65\uff1a\u6bcf 30 \u79d2 \u6700\u540e\u540c\u6b65\uff1a\u7b49\u5f85\u8bfb\u53d6 \u4e0b\u6b21\u5237\u65b0\uff1a&#8211; \u91c7\u8d2d\u8ba1\u5212\u53e3\u5f84\uff1a\u73b0\u8d27\u5e93\u5b58\u7528\u4e8e\u5224\u65ad\u4eca\u5929\u53ef\u9500\u552e\u6570\u91cf\uff1b\u603b\u5e93\u5b58 = \u73b0\u8d27 + \u6d77\u4e0a\u5e93\u5b58 + \u751f\u4ea7\u4e2d\uff1b\u6708\u5747\u9500\u552e\u6309\u4eca\u5e74\u5df2\u6709\u9500\u552e\u6570\u636e\u7684\u6708\u4efd\u5e73\u5747\u8ba1\u7b97\u3002\u91c7\u8d2d\u5efa\u8bae = \u672a\u6765 3 \u4e2a\u6708\u9700\u6c42 &#8211; \u73b0\u8d27 &#8211; \u6d77\u4e0a\u5e93\u5b58\uff0c\u4e0d\u542b\u751f\u4ea7\u4e2d\u3002\u4f4e\u4e8e 3 \u4e2a\u6708\u63d0\u793a\u8865\u8d27\uff1b\u8d85\u8fc7 4 \u4e2a\u6708\u63d0\u793a\u5e93\u5b58\u504f\u9ad8\uff1b\u8d85\u8fc7 6 \u4e2a\u6708\u6807\u4e3a\u4e00\u7ea7\u8b66\u544a\u3002 \u4ea7\u54c1\u6570\u91cf &#8212; \u73b0\u8d27\u5e93\u5b58 &#8212; \u6d77\u4e0a\u5e93\u5b58 &#8212; \u751f\u4ea7\u4e2d &#8212; \u603b\u5e93\u5b58 &#8212; PI\u767b\u8bb0\u6570\u91cf &#8212; \u52a0\u6743\u53ef\u552e\u6708\u6570 &#8212; \u73b0\u8d27\u8986\u76d6 &#8212; \u641c\u7d22\u4ea7\u54c1 \u5e93\u5b58\u72b6\u6001 \u5168\u90e8\u4ea7\u54c1\u73b0\u8d27\u53ef\u8986\u76d6 PI\u73b0\u8d27\u4e0d\u8db3\u4ec5\u6709\u73b0\u8d27\u65e0\u73b0\u8d27 \u6392\u5e8f [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-87","page","type-page","status-publish","hentry"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/pages\/87","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/comments?post=87"}],"version-history":[{"count":10,"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/pages\/87\/revisions"}],"predecessor-version":[{"id":118,"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/pages\/87\/revisions\/118"}],"wp:attachment":[{"href":"https:\/\/akku.cozyfluffy.com\/index.php\/wp-json\/wp\/v2\/media?parent=87"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}