← back to Dw Activation Calendar

public/index.html

800 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DW Calendar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
  :root{
    /* Luxe DW palette (Figma tokens — verbatim) */
    --ground:#FAF8F3; --surface:#FFFFFF; --ink:#1F1B17; --muted:#8A8178;
    --faint:#C2BBAE; --hairline:#E7E1D6; --plum:#5E2B3C; --brass:#B0894F;
    --green:#3E7C5A; --red:#A23B3B;
    --out-month:#FBF9F5;
    /* chip tints */
    --chip-brass-bg:#F4ECDD; --chip-brass-fg:#7A571C;
    --chip-plum-bg:#F3E7EC;  --chip-plum-fg:#6E2A40;
    --chip-green-bg:#E8F0EA; --chip-green-fg:#2F5C43;
    /* not-connected banner */
    --warn-bg:#FBF3E3; --warn-bd:#E8D6A8; --warn-fg:#7A571C;
    --serif:"Cormorant Garamond",Georgia,"Times New Roman",serif;
    --ui:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{margin:0;background:var(--ground);color:var(--ink);font:14px/1.5 var(--ui);-webkit-font-smoothing:antialiased}
  .wrap{max-width:1200px;margin:32px auto;background:var(--surface);border:1px solid var(--hairline);border-radius:20px;padding:40px 48px 48px;box-shadow:0 1px 2px rgba(31,27,23,.03)}

  /* ---- header ---- */
  header{display:flex;align-items:flex-start;gap:20px;padding-bottom:24px;border-bottom:1px solid var(--hairline)}
  .brand{flex:1;min-width:0}
  .wordmark{font-family:var(--serif);font-weight:600;font-size:32px;line-height:1.05;color:var(--ink);letter-spacing:.2px}
  .sub{font-family:var(--ui);font-size:11px;font-weight:600;color:var(--brass);text-transform:uppercase;letter-spacing:4px;margin-top:4px}
  .spacer{flex:1}
  button{font:inherit;cursor:pointer;border:none}
  .btn-primary{background:var(--plum);color:#fff;font-weight:600;font-size:14px;padding:11px 20px;border-radius:10px;box-shadow:0 1px 2px rgba(94,43,60,.25)}
  .btn-primary:hover{background:#4d2230}
  .btn-ghost{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-size:13px;font-weight:500;padding:8px 16px;border-radius:999px}
  .btn-ghost:hover{border-color:var(--faint)}
  .btn-pill{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-size:13px;font-weight:500;padding:8px 18px;border-radius:999px}
  .btn-pill:hover{border-color:var(--faint)}
  /* SKU-view inline controls */
  #skuCtrls{display:none;align-items:center;gap:16px;margin-left:18px}
  .ctrl{display:flex;flex-direction:column;gap:3px;font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
  .ctrl input{font:inherit;font-size:13px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;width:74px}
  .ctrl input[type=date]{width:140px}
  #apply{background:var(--plum);color:#fff;font-weight:600;font-size:13px;padding:9px 16px;border-radius:8px;align-self:flex-end}

  /* ---- tab bar ---- */
  .tabbar{display:flex;align-items:center;gap:8px;margin:24px 0 4px;padding:5px;background:var(--ground);border:1px solid var(--hairline);border-radius:14px}
  .tab{background:transparent;color:var(--muted);font-weight:500;font-size:14px;padding:9px 18px;border-radius:10px;transition:all .12s}
  .tab:hover{color:var(--ink)}
  .tab.active{background:var(--surface);color:var(--ink);font-weight:600;box-shadow:0 1px 3px rgba(31,27,23,.08)}
  .conn-pill{margin-left:auto;display:flex;align-items:center;gap:8px;font-size:13px;color:var(--muted);background:var(--surface);border:1px solid var(--hairline);border-radius:999px;padding:7px 16px}
  .conn-pill .dot{width:8px;height:8px;border-radius:50%;background:var(--green);flex:0 0 auto}
  .conn-pill.off .dot{background:var(--faint)}
  .conn-pill code{font-family:var(--ui);font-size:12px;color:var(--ink)}
  .conn-pill button{background:none;color:var(--muted);font-size:12px;text-decoration:underline;padding:0 0 0 6px}
  .conn-pill a{text-decoration:none}
  .conn-pill a button{color:var(--plum);text-decoration:underline}

  .view{display:none}
  .view.active{display:block}

  /* ---- not-connected banner ---- */
  .banner{margin:18px 0 0;padding:13px 18px;border-radius:12px;border:1px solid var(--warn-bd);background:var(--warn-bg);color:var(--warn-fg);font-size:13.5px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
  .banner.hidden{display:none}
  .banner b{color:var(--warn-fg);font-weight:600}
  .banner a{text-decoration:none}

  /* ---- month toolbar ---- */
  .monthbar{display:flex;align-items:center;gap:16px;margin:22px 0 14px}
  .monthbar .nav{background:none;color:var(--muted);font-size:22px;line-height:1;padding:2px 6px;border-radius:8px}
  .monthbar .nav:hover{color:var(--ink);background:var(--ground)}
  .monthbar .ml{font-family:var(--serif);font-weight:500;font-size:26px;color:var(--ink);min-width:200px;letter-spacing:.2px}
  .monthbar .spacer{flex:1}

  /* ---- month card grid ---- */
  .cal{border:1px solid var(--hairline);border-radius:14px;overflow:hidden;background:var(--surface)}
  .cal .dow-row{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--hairline)}
  .dow{height:40px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px}
  .grid{display:grid;grid-template-columns:repeat(7,1fr)}
  .cell{min-height:112px;border-right:1px solid var(--hairline);border-bottom:1px solid var(--hairline);padding:8px 9px;display:flex;flex-direction:column;gap:5px;overflow:hidden}
  .cell:nth-child(7n){border-right:none}
  .grid .cell:nth-last-child(-n+7){border-bottom:none}
  .cell.out{background:var(--out-month)}
  .cell.out .dn{color:var(--faint)}
  .cell .dn{font-size:13px;font-weight:500;color:var(--ink);width:28px;height:28px;display:flex;align-items:center;justify-content:center}
  .cell.today .dn{background:var(--plum);color:#fff;border-radius:50%}
  .cell .cnt{font-size:10px;font-weight:600;color:var(--brass)}
  .cell .dhr{display:flex;align-items:baseline;gap:4px}
  .cell .span{font-size:8px;font-weight:600;color:var(--muted,#9a8f7d);letter-spacing:.2px;white-space:nowrap}
  .cell .dh{display:flex;align-items:center;justify-content:space-between}

  /* Google event chips */
  .gcell .evs{display:flex;flex-direction:column;gap:4px;overflow-y:auto}
  .ev{display:block;width:100%;text-align:left;border:none;border-radius:6px;padding:4px 8px;cursor:pointer;font-size:11.5px;line-height:1.3;overflow:hidden;
      background:var(--chip-brass-bg);color:var(--chip-brass-fg)}
  .ev.cat-plum{background:var(--chip-plum-bg);color:var(--chip-plum-fg)}
  .ev.cat-green{background:var(--chip-green-bg);color:var(--chip-green-fg)}
  .ev.cat-brass{background:var(--chip-brass-bg);color:var(--chip-brass-fg)}
  .ev:hover{filter:brightness(.97)}
  .ev .et{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .ev .ewhen{opacity:.85;font-size:10.5px}
  .ev .ecreated{opacity:.65;font-size:9px;margin-top:1px}
  .addbtn{margin-top:auto;align-self:flex-start;font-size:11px;color:var(--faint);background:none;padding:2px 0;opacity:0;transition:opacity .12s}
  .gcell:hover .addbtn{opacity:1}
  .addbtn:hover{color:var(--muted)}

  /* ---- SKU activation view ---- */
  .summary{display:flex;gap:32px;margin:18px 0 6px;flex-wrap:wrap}
  .summary .k{display:flex;flex-direction:column;gap:2px}
  .summary .k span{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}
  .summary .k b{font-family:var(--serif);font-weight:500;font-size:24px;color:var(--ink)}
  .note{font-size:12px;color:var(--muted);margin:6px 0 0;line-height:1.5}
  .note b{color:var(--ink);font-weight:600}
  .legend{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px;font-size:11px;color:var(--muted)}
  .legend .li{display:flex;align-items:center;gap:5px}
  .legend .sw{width:10px;height:10px;border-radius:3px;display:inline-block}
  .loading{padding:60px;text-align:center;color:var(--muted)}

  /* SKU chips (image tiles). Flex-wrap with FIXED squares — aspect-ratio chips
     inside an auto-row grid collapse the rows (derived height never reaches
     track sizing) and every row overlaps the next. */
  .scell .chips{display:flex;flex-wrap:wrap;gap:5px;align-content:flex-start;
      max-height:178px;overflow:hidden}          /* collapsed preview: ~3 chip rows */
  .cell.expanded .chips{max-height:none;overflow:visible}  /* expanded: the whole day column */
  .chip{position:relative;display:block;text-decoration:none;color:inherit;border-radius:6px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);width:56px;height:56px;flex:0 0 auto;padding:0;cursor:grab}
  .chip[draggable=true]:active{cursor:grabbing}
  .chip.dragging{opacity:.4}
  .scell.drop-target{outline:2px dashed var(--brass);outline-offset:-3px;background:var(--chip-brass-bg)}
  .chip:hover{border-color:var(--brass);box-shadow:0 0 0 1px var(--brass)}
  .chip:focus-visible{outline:2px solid var(--plum);outline-offset:1px}
  /* expand / collapse the day column */
  .scell .more{margin-top:4px;align-self:stretch;font-size:10px;font-weight:600;letter-spacing:.4px;color:var(--muted);
      background:var(--ground);border:1px dashed var(--hairline);border-radius:6px;padding:3px 6px}
  .scell .more:hover{color:var(--ink);border-color:var(--faint)}
  .chip img{width:100%;height:100%;object-fit:cover;display:block}
  .chip .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#fff}
  .chip .vn{position:absolute;left:0;right:0;bottom:0;font-size:7px;line-height:1.15;background:rgba(31,27,23,.72);color:#f3ead7;padding:2px 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:.2px}
  .chip .fx{position:absolute;top:2px;left:2px;width:6px;height:6px;border-radius:50%}
  .chip .fx.s{background:var(--green)}.chip .fx.r{background:var(--brass)}
  .chips::-webkit-scrollbar,.evs::-webkit-scrollbar{width:7px}
  .chips::-webkit-scrollbar-thumb,.evs::-webkit-scrollbar-thumb{background:var(--hairline);border-radius:4px}

  /* ===== modal (rig-built panel restyled to the luxe Figma look) ===== */
  /* The rig sets inline cssText on backdrop/panel/head/body; override with high
     specificity + !important so drag/resize/close wiring stays intact. */
  [id^="modalrig-bd-"]{background:rgba(31,27,23,.28)!important;backdrop-filter:blur(1px)}
  [id^="modalrig-bd-"] > div{           /* the panel */
    background:var(--surface)!important;border:1px solid var(--hairline)!important;
    border-radius:16px!important;box-shadow:0 24px 64px rgba(31,27,23,.22)!important;overflow:hidden!important}
  [id^="modalrig-bd-"] > div > div:first-child{  /* the rig head → grab-handle bar */
    background:var(--surface)!important;border-bottom:none!important;border-radius:16px 16px 0 0!important;
    justify-content:center!important;padding:10px 0 0!important;min-height:18px}
  [id^="modalrig-bd-"] > div > div:first-child span:first-child{display:none!important} /* hide rig title */
  [id^="modalrig-bd-"] > div > div:first-child::before{
    content:"";width:40px;height:4px;border-radius:2px;background:var(--hairline);display:block}
  [id^="modalrig-bd-"] [data-rig-btn]{display:none!important} /* hide rig × + maximize; we add our own close */
  [id^="modalrig-bd-"] > div > div:nth-child(2){padding:8px 28px 24px!important} /* body */

  .mform{font-family:var(--ui)}
  .mform .mhead{display:flex;align-items:center;justify-content:space-between;margin:4px 0 18px}
  .mform .mtitle{font-family:var(--serif);font-weight:600;font-size:24px;color:var(--ink)}
  .mform .mclose{width:30px;height:30px;border-radius:50%;background:var(--ground);color:var(--muted);font-size:16px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;border:none}
  .mform .mclose:hover{background:var(--hairline);color:var(--ink)}
  .mform label{display:block;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.7px;font-weight:600;margin:14px 0 5px}
  .mform input[type=text],.mform textarea,.mform input[type=datetime-local],.mform input[type=date]{
    width:100%;font:inherit;font-size:14px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:10px 12px}
  .mform input:focus,.mform textarea:focus{outline:none;border-color:var(--brass)}
  .mform textarea{min-height:86px;resize:vertical;line-height:1.45}
  .mform .row{display:flex;gap:16px}.mform .row>div{flex:1}
  /* all-day pill toggle */
  .mform .chk{display:flex;align-items:center;justify-content:space-between;margin-top:16px;font-size:14px;font-weight:500;color:var(--ink)}
  .mform .chk input{position:absolute;opacity:0;pointer-events:none}
  .mform .toggle{width:44px;height:24px;border-radius:999px;background:var(--hairline);position:relative;transition:background .15s;cursor:pointer;flex:0 0 auto}
  .mform .toggle::after{content:"";position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.2);transition:transform .15s}
  .mform .chk input:checked + .toggle{background:var(--plum)}
  .mform .chk input:checked + .toggle::after{transform:translateX(20px)}
  /* color picker dots */
  .mform .colorpick{display:flex;align-items:center;justify-content:space-between;margin-top:18px}
  .mform .colorpick .lab{font-size:14px;font-weight:500;color:var(--ink)}
  .mform .dots{display:flex;gap:10px}
  .mform .dot{width:22px;height:22px;border-radius:50%;cursor:pointer;border:2px solid transparent;box-shadow:0 0 0 1px rgba(31,27,23,.06) inset}
  .mform .dot.sel{box-shadow:0 0 0 2px var(--surface) inset,0 0 0 4px currentColor}
  /* footer */
  .mform .actions{display:flex;justify-content:space-between;align-items:center;margin-top:24px;padding-top:20px;border-top:1px solid var(--hairline)}
  .mform .actions .right{display:flex;gap:10px}
  .mform .btn-del{background:none;color:var(--red);font-weight:600;font-size:14px;padding:9px 4px}
  .mform .btn-del:hover{text-decoration:underline}
  .mform .btn-cancel{background:var(--surface);border:1px solid var(--hairline);color:var(--ink);font-weight:500;font-size:14px;padding:9px 20px;border-radius:10px}
  .mform .btn-cancel:hover{border-color:var(--faint)}
  .mform .btn-save{background:var(--plum);color:#fff;font-weight:600;font-size:14px;padding:9px 24px;border-radius:10px}
  .mform .btn-save:hover{background:#4d2230}
  .mform .msg{font-size:12.5px;min-height:16px;margin-top:10px}
  .mform .msg.err{color:var(--red)}.mform .msg.ok{color:var(--green)}

  /* ---- SKU detail modal ---- */
  .skud .hero{width:100%;aspect-ratio:4/3;border-radius:12px;overflow:hidden;background:var(--ground);border:1px solid var(--hairline);margin-bottom:16px}
  .skud .hero img{width:100%;height:100%;object-fit:cover;display:block}
  .skud .hero .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:var(--serif);font-size:34px;color:#fff}
  .skud dl{display:grid;grid-template-columns:118px 1fr;gap:7px 14px;margin:0;font-size:13.5px}
  .skud dt{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.6px;font-weight:600;align-self:center}
  .skud dd{margin:0;color:var(--ink)}
  .skud dd code{font-size:12.5px;background:var(--ground);border:1px solid var(--hairline);border-radius:6px;padding:2px 7px}
  .skud .fxpill{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:600;border-radius:999px;padding:3px 11px}
  .skud .fxpill .d{width:7px;height:7px;border-radius:50%}
  .skud .fxpill.s{background:var(--chip-green-bg);color:var(--chip-green-fg)}.skud .fxpill.s .d{background:var(--green)}
  .skud .fxpill.r{background:var(--chip-brass-bg);color:var(--chip-brass-fg)}.skud .fxpill.r .d{background:var(--brass)}
  .skud .links{display:flex;gap:10px;margin-top:20px;padding-top:18px;border-top:1px solid var(--hairline);flex-wrap:wrap}
  .skud .links a{flex:1;min-width:150px;text-align:center;text-decoration:none;font-weight:600;font-size:13.5px;padding:10px 16px;border-radius:10px}
  .skud .links a.adm{background:var(--plum);color:#fff}.skud .links a.adm:hover{background:#4d2230}
  .skud .links a.sf{background:var(--surface);border:1px solid var(--hairline);color:var(--ink)}.skud .links a.sf:hover{border-color:var(--faint)}
  /* reschedule row in the SKU modal */
  .skud .resched{display:flex;align-items:center;gap:8px;margin:6px 0 2px;padding:10px 0 0;border-top:1px solid var(--hairline)}
  .skud .resched label{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--muted);flex:1}
  .skud .resched input[type=date]{font:inherit;font-size:13px;border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;color:var(--ink);background:var(--surface)}
  .skud .resched .cta-btn{padding:9px 16px;font-size:11px}
  /* vendor "see all staged" list */
  #skuCtrls select#vendorFilter{font:inherit;font-size:13px;color:var(--ink);background:var(--surface);border:1px solid var(--hairline);border-radius:8px;padding:7px 9px;max-width:200px}
  .linklike{color:var(--plum);text-decoration:none;font-size:12px;font-weight:600}
  .linklike:hover{text-decoration:underline}
  .vlist .vrows{max-height:560px;overflow:auto;display:flex;flex-direction:column;gap:6px}
  .vrow{display:flex;align-items:center;gap:11px;padding:6px 8px;border:1px solid var(--hairline);border-radius:10px;background:var(--ground)}
  .vthumb{width:44px;height:44px;flex:0 0 auto;border-radius:7px;overflow:hidden;background:var(--surface);border:1px solid var(--hairline)}
  .vthumb img,.vthumb .ph{width:100%;height:100%;object-fit:cover;display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;font-weight:700}
  .vmeta{flex:1;min-width:0}
  .vlabel{font-size:13px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .vsub{font-size:11px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .vdate{font:inherit;font-size:12px;border:1px solid var(--hairline);border-radius:7px;padding:6px 8px;color:var(--ink);background:var(--surface);flex:0 0 auto}
</style>
</head>
<body>
<div class="wrap">
<header>
  <div class="brand">
    <div class="wordmark">Designer Wallcoverings</div>
    <div class="sub">Calendar</div>
  </div>
  <div class="spacer"></div>
  <!-- SKU-view controls (only shown on the SKU tab) -->
  <div id="skuCtrls">
    <div class="ctrl"><label>Per day</label><input id="perDay" type="number" value="500" min="1"></div>
    <div class="ctrl"><label>Per vendor/day</label><input id="perVendorDay" type="number" value="500" min="1"></div>
    <div class="ctrl"><label>Start</label><input id="start" type="date"></div>
    <button id="apply">Apply</button>
    <div class="ctrl"><label>Vendor</label><select id="vendorFilter"><option value="">All vendors</option></select></div>
    <button id="vendorAll" class="btn-pill" style="display:none">See all staged</button>
    <button id="refreshLive" class="btn-pill" title="re-pull the current staged worklist from dw_unified">↻ Refresh (live)</button>
  </div>
  <button class="btn-primary" id="gnew">+ New Event</button>
</header>

<div class="tabbar">
  <button class="tab" id="tab-gcal" data-view="gcal">Google Calendar</button>
  <button class="tab" id="tab-sku" data-view="sku">SKU Activation</button>
  <span class="conn-pill off" id="connPill"><span class="dot"></span><span id="connTxt">Checking…</span></span>
</div>

<!-- ====================== GOOGLE CALENDAR VIEW ====================== -->
<div class="view" id="view-gcal">
  <div class="banner hidden" id="gbanner"></div>
  <div class="note" id="gfallnote" style="display:none;margin:14px 0 0">Showing the <b>SKU activation rollout</b> below until the Google Calendar is connected — these are scheduled product go-lives, not calendar events. They become editable Google events once Steve authorizes.</div>
  <div class="monthbar">
    <button class="nav" id="gprev" aria-label="Previous month">‹</button>
    <div class="ml" id="gmlabel">—</div>
    <button class="nav" id="gnext" aria-label="Next month">›</button>
    <div class="spacer"></div>
    <button class="btn-pill" id="gtoday">Today</button>
  </div>
  <div id="gcal"><div class="loading">Loading events…</div></div>
</div>

<!-- ====================== SKU ACTIVATION VIEW ====================== -->
<div class="view" id="view-sku">
  <div class="summary" id="summary"></div>
  <div class="note">Every staged SKU waiting to go active carries a <b>durable go-live date AND time</b> — the exact drain slot it will activate in, matching the real hourly executor (<b>500/day</b>, fired across 24 hourly <b>:10</b> slots, ~30/slot, samples-first, round-robin across vendors, in rr order). Hover any chip for its go-active date+time; each day header shows that day's slot span. <span style="color:var(--green)">●</span> sample-add · <span style="color:var(--brass)">●</span> roll/price build.</div>
  <div class="monthbar">
    <button class="nav" id="prev" aria-label="Previous month">‹</button>
    <div class="ml" id="mlabel">—</div>
    <button class="nav" id="next" aria-label="Next month">›</button>
  </div>
  <div id="cal"><div class="loading">Loading schedule…</div></div>
  <div class="legend" id="legend"></div>
</div>
</div><!-- /wrap -->

<script src="/modal-rig.js"></script>
<script>
/* ======================= tabs ======================= */
function setTab(v){
  document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active',t.dataset.view===v));
  document.getElementById('view-gcal').classList.toggle('active',v==='gcal');
  document.getElementById('view-sku').classList.toggle('active',v==='sku');
  document.getElementById('skuCtrls').style.display = v==='sku' ? 'flex':'none';
  document.getElementById('gnew').style.display = v==='gcal' ? '' : 'none';
  localStorage.setItem('dwcal_tab',v);
  if(v==='gcal' && !GDATA) loadGcal();
  if(v==='sku' && !DATA) load();
}
document.querySelectorAll('.tab').forEach(t=>t.onclick=()=>setTab(t.dataset.view));

/* ======================= GOOGLE CALENDAR ======================= */
let GDATA=null, gMonth=null, GSTATUS={connected:false,oauth_configured:false};
function fmtMonth(d){return d.toLocaleString(undefined,{month:'long',year:'numeric'});}
function fmtCreated(iso){
  if(!iso) return '';
  try{return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}
  catch(e){return iso;}
}
function fmtWhen(ev){
  if(ev.allDay) return 'All day';
  try{return new Date(ev.start).toLocaleTimeString(undefined,{hour:'numeric',minute:'2-digit'});}
  catch(e){return '';}
}
/* category tint: map a Google colorId / a guessed bucket to a chip tint class */
function evCat(ev){
  const c=String(ev.colorId||'');
  // Google green-ish ids → green, red/plum-ish → plum, else brass (default)
  if(['2','10','5'].includes(c)) return 'cat-green';
  if(['3','4','11','6'].includes(c)) return 'cat-plum';
  return 'cat-brass';
}
async function refreshStatus(){
  try{ GSTATUS = await (await fetch('/api/calendar/status')).json(); }
  catch(e){ GSTATUS={connected:false,oauth_configured:false,error:e.message}; }
  const pill=document.getElementById('connPill'), txt=document.getElementById('connTxt');
  const b=document.getElementById('gbanner');
  if(GSTATUS.connected){
    pill.className='conn-pill';
    txt.innerHTML='Connected · <code>'+(GSTATUS.calendar_id||'info@designerwallcoverings.com')+'</code> '+
      '<button id="gdisc">Disconnect</button>';
    document.getElementById('gdisc').onclick=async()=>{await fetch('/api/oauth/revoke',{method:'POST'});refreshStatus();loadGcal();};
    b.className='banner hidden'; b.innerHTML='';
  }else{
    pill.className='conn-pill off';
    txt.textContent='Not connected';
    b.className='banner';
    const cfg=!!GSTATUS.oauth_configured;
    b.innerHTML='<b>Calendar not connected — Steve must authorize.</b> '+
      (cfg ? 'OAuth is configured — click to grant access. '
           : 'The Google OAuth credential has not been added yet (queued in pending-approval). ')+
      '<button class="btn-primary" id="gconnect">'+(cfg?'Connect':'How to connect')+'</button>';
    document.getElementById('gconnect').onclick=()=>{
      // Only navigate to the OAuth start route when there's a credential to use —
      // otherwise that route returns a raw 503 JSON page (looks like a crash).
      if(cfg){ window.location.href='/api/oauth/start'; return; }
      document.getElementById('gbanner').innerHTML=
        '<b>Connect the Google Calendar — one-time (Steve):</b>'+
        '<ol style="margin:8px 0 0;padding-left:20px;line-height:1.7">'+
        '<li>Google Cloud Console → enable Calendar API → create an OAuth <b>Web application</b> client; '+
          'add redirect URI <code>http://127.0.0.1:9765/api/oauth/callback</code>.</li>'+
        '<li>Paste the Client ID + Secret to Claude (secrets routes are pre-wired), '+
          'or run <code>node ~/Projects/secrets-manager/cli.js add GOOGLE_OAUTH_CLIENT_ID &lt;id&gt;</code> (and the secret).</li>'+
        '<li>Restart the app, reload this page, then click <b>Connect</b> to grant consent.</li>'+
        '</ol>';
    };
  }
}
// Fallback content for the Google tab when OAuth isn't connected: the SKU
// activation rollout. Kept in its own var (NOT DATA) so the SKU tab still
// lazy-loads itself via setTab's `!DATA` guard.
let GSCHED=null;
async function ensureSched(){
  if(GSCHED) return GSCHED;
  try{ GSCHED=await (await fetch('/api/schedule?perDay=500&perVendorDay=500')).json(); }
  catch(e){ GSCHED={days:[]}; }
  return GSCHED;
}
async function loadGcal(){
  await refreshStatus();
  if(!gMonth){const n=new Date();gMonth=new Date(n.getFullYear(),n.getMonth(),1);}
  document.getElementById('gmlabel').textContent=fmtMonth(gMonth);
  const fallNote=document.getElementById('gfallnote');
  if(!GSTATUS.connected){
    // Never show a blank grid: surface the SKU rollout as read-only fallback
    // (the "Steve must authorize" banner stays on top). Month nav re-renders it.
    fallNote.style.display='';
    document.getElementById('gcal').innerHTML='<div class="loading">Loading rollout…</div>';
    await ensureSched();
    renderSchedMonth('gcal', gMonth, GSCHED);
    return;
  }
  fallNote.style.display='none';
  document.getElementById('gcal').innerHTML='<div class="loading">Loading events…</div>';
  const y=gMonth.getFullYear(), m=gMonth.getMonth();
  const timeMin=new Date(y,m,1).toISOString(), timeMax=new Date(y,m+1,0,23,59,59).toISOString();
  let resp={connected:false,events:[]};
  try{ resp=await (await fetch('/api/events?timeMin='+encodeURIComponent(timeMin)+'&timeMax='+encodeURIComponent(timeMax))).json(); }
  catch(e){ resp={connected:false,events:[],error:e.message}; }
  GDATA=resp;
  renderGcal();
}
function renderGcal(){
  const byDate={};
  (GDATA.events||[]).forEach(ev=>{ const d=(ev.start||'').slice(0,10); (byDate[d]=byDate[d]||[]).push(ev); });
  const y=gMonth.getFullYear(), m=gMonth.getMonth();
  const cal=document.getElementById('gcal'); cal.className='cal'; cal.innerHTML='';
  const dowRow=document.createElement('div'); dowRow.className='dow-row';
  ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;dowRow.appendChild(h);});
  cal.appendChild(dowRow);
  const grid=document.createElement('div'); grid.className='grid'; cal.appendChild(grid);

  const first=new Date(y,m,1), startDow=first.getDay(), days=new Date(y,m+1,0).getDate();
  const prevDays=new Date(y,m,0).getDate();
  const todayISO=new Date().toISOString().slice(0,10);
  // leading out-of-month days
  for(let i=startDow-1;i>=0;i--){ grid.appendChild(outCell(prevDays-i)); }
  for(let dn=1;dn<=days;dn++){
    const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
    const cell=document.createElement('div'); cell.className='cell gcell'+(iso===todayISO?' today':'');
    const evs=byDate[iso]||[];
    cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${evs.length?`<span class="cnt">${evs.length}</span>`:''}</div>`;
    const body=document.createElement('div'); body.className='evs';
    evs.forEach(ev=>{
      const b=document.createElement('button'); b.className='ev '+evCat(ev);
      b.innerHTML=`<div class="et">${esc(ev.summary)}</div>`+
        `<div class="ewhen">${fmtWhen(ev)}</div>`+
        `<div class="ecreated" title="${ev.created||''}">🕓 ${fmtCreated(ev.created)}</div>`;
      b.onclick=(e)=>{e.stopPropagation();openEventModal(ev,iso);};
      body.appendChild(b);
    });
    const add=document.createElement('button'); add.className='addbtn'; add.textContent='+ add';
    add.onclick=(e)=>{e.stopPropagation();openEventModal(null,iso);};
    body.appendChild(add);
    cell.appendChild(body);
    grid.appendChild(cell);
  }
  // trailing out-of-month days to fill the last row
  const totalCells=startDow+days; const trail=(7-(totalCells%7))%7;
  for(let dn=1;dn<=trail;dn++){ grid.appendChild(outCell(dn)); }
}
function outCell(dn){
  const c=document.createElement('div'); c.className='cell out';
  c.innerHTML=`<div class="dh"><span class="dn">${dn}</span></div>`;
  return c;
}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}

/* ---- the editable modal (create + edit) ---- */
function toLocalInput(iso){ // ISO -> value for datetime-local (local tz)
  if(!iso) return '';
  const d=new Date(iso); if(isNaN(d)) return '';
  const p=n=>String(n).padStart(2,'0');
  return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}`;
}
const CAL_DOTS=[{id:'plum',c:'#5E2B3C'},{id:'brass',c:'#B0894F'},{id:'green',c:'#3E7C5A'},{id:'ink',c:'#1F1B17'}];
function openEventModal(ev, dayISO){
  const isEdit=!!ev;
  const startVal = ev ? (ev.allDay ? '' : toLocalInput(ev.start)) : (dayISO+'T09:00');
  const endVal   = ev ? (ev.allDay ? '' : toLocalInput(ev.end))   : (dayISO+'T10:00');
  const allDay = ev ? !!ev.allDay : false;
  const dots=CAL_DOTS.map((d,i)=>`<span class="dot${i===0?' sel':''}" data-color="${d.id}" style="background:${d.c};color:${d.c}"></span>`).join('');
  const bodyHtml=`<form class="mform" id="evForm" onsubmit="return false">
    <div class="mhead">
      <div class="mtitle">${isEdit?'Edit Event':'New Event'}</div>
      <button type="button" class="mclose" id="f_x" aria-label="Close">✕</button>
    </div>
    <label>Title *</label><input type="text" id="f_summary" value="${esc(ev?ev.summary:'')}" placeholder="Event title" required>
    <div class="row" id="timedRow" style="${allDay?'display:none':''}">
      <div><label>Starts</label><input type="datetime-local" id="f_start" value="${startVal}"></div>
      <div><label>Ends</label><input type="datetime-local" id="f_end" value="${endVal}"></div>
    </div>
    <div class="row" id="dayRow" style="${allDay?'':'display:none'}">
      <div><label>Date</label><input type="date" id="f_date" value="${(ev?ev.start:dayISO||'').slice(0,10)}"></div>
    </div>
    <label class="chk">All-day event<input type="checkbox" id="f_allday" ${allDay?'checked':''}><span class="toggle"></span></label>
    <label>Location</label><input type="text" id="f_loc" value="${esc(ev?ev.location:'')}" placeholder="Optional">
    <label>Description</label><textarea id="f_desc" placeholder="Notes">${esc(ev?ev.description:'')}</textarea>
    <div class="colorpick"><span class="lab">Calendar color</span><div class="dots" id="f_dots">${dots}</div></div>
    <div class="msg" id="f_msg"></div>
    <div class="actions">
      <div>${isEdit?'<button type="button" class="btn-del" id="f_del">Delete</button>':'<span></span>'}</div>
      <div class="right"><button type="button" class="btn-cancel" id="f_cancel">Cancel</button> <button type="button" class="btn-save" id="f_save">${isEdit?'Save':'Create'}</button></div>
    </div>
  </form>`;
  const M=ModalRig.open({key:'dwcal-event',title:isEdit?'Edit Event':'New Event',bodyHtml,width:460,height:660,bg:'var(--surface)'});
  const $=id=>M.body.querySelector('#'+id);
  $('f_x').onclick=M.close;
  // wire the all-day toggle (the visible toggle is the styled label; the checkbox drives it)
  $('f_allday').onchange=()=>{const on=$('f_allday').checked;$('timedRow').style.display=on?'none':'';$('dayRow').style.display=on?'':'none';};
  // color dot selection (cosmetic ring)
  M.body.querySelectorAll('#f_dots .dot').forEach(d=>d.onclick=()=>{
    M.body.querySelectorAll('#f_dots .dot').forEach(x=>x.classList.remove('sel'));d.classList.add('sel');});
  $('f_cancel').onclick=M.close;
  if(isEdit) $('f_del').onclick=async()=>{
    if(!confirm('Delete this event?')) return;
    const r=await fetch('/api/events/'+encodeURIComponent(ev.id),{method:'DELETE'});
    const j=await r.json().catch(()=>({}));
    if(r.ok && j.ok){M.close();loadGcal();} else showMsg($,'Delete failed'+(j.error?': '+j.error:''),true);
  };
  $('f_save').onclick=async()=>{
    const allday=$('f_allday').checked;
    const payload={summary:$('f_summary').value.trim(),description:$('f_desc').value,location:$('f_loc').value,allDay:allday};
    if(!payload.summary){showMsg($,'Title is required',true);return;}
    if(allday){const d=$('f_date').value;if(!d){showMsg($,'Date required',true);return;}payload.start=d;payload.end=d;}
    else{
      const s=$('f_start').value,e=$('f_end').value;
      if(!s||!e){showMsg($,'Start and end required',true);return;}
      payload.start=new Date(s).toISOString();payload.end=new Date(e).toISOString();
    }
    showMsg($,'Saving…',false);
    const url=isEdit?'/api/events/'+encodeURIComponent(ev.id):'/api/events';
    const r=await fetch(url,{method:isEdit?'PATCH':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
    const j=await r.json().catch(()=>({}));
    if(r.status===409){showMsg($,'Calendar not connected — Steve must authorize.',true);return;}
    if(r.ok && j.ok){M.close();loadGcal();}
    else showMsg($,'Save failed'+(j.error?': '+j.error:'')+(j.detail?' — '+j.detail:''),true);
  };
}
function showMsg($,t,err){const m=$('f_msg');m.textContent=t;m.className='msg '+(err?'err':'ok');}

document.getElementById('gprev').onclick=()=>{gMonth=new Date(gMonth.getFullYear(),gMonth.getMonth()-1,1);loadGcal();};
document.getElementById('gnext').onclick=()=>{gMonth=new Date(gMonth.getFullYear(),gMonth.getMonth()+1,1);loadGcal();};
document.getElementById('gtoday').onclick=()=>{const n=new Date();gMonth=new Date(n.getFullYear(),n.getMonth(),1);loadGcal();};
document.getElementById('gnew').onclick=()=>{const n=new Date();openEventModal(null,n.toISOString().slice(0,10));};

/* ======================= SKU ACTIVATION (existing logic) ======================= */
let DATA=null, viewMonth=null, vendorFilter='';
const VCOLORS={};
function vcolor(v){
  if(VCOLORS[v]) return VCOLORS[v];
  let h=0; for(const c of v) h=(h*31+c.charCodeAt(0))%360;
  return VCOLORS[v]=`hsl(${h},38%,52%)`;
}
async function load(fresh){
  const perDay=document.getElementById('perDay').value||500;
  const perVendorDay=document.getElementById('perVendorDay').value||500;
  const start=document.getElementById('start').value;
  const qs=new URLSearchParams({perDay,perVendorDay}); if(start) qs.set('start',start);
  if(fresh) qs.set('fresh','1');
  document.getElementById('cal').innerHTML='<div class="loading">'+(fresh?'Refreshing from live data …':'Scheduling …')+'</div>';
  const r=await fetch('/api/schedule?'+qs); DATA=await r.json();
  renderSummary(); renderLegend(); populateVendorFilter();
  const first=DATA.days[0]?new Date(DATA.days[0].date+'T00:00:00'):new Date();
  viewMonth=new Date(first.getFullYear(),first.getMonth(),1);
  renderMonth();
}
function renderSummary(){
  const s=DATA;
  if(vendorFilter){
    let count=0; const dates=[];
    s.days.forEach(d=>{ const n=d.skus.filter(x=>x.vendor===vendorFilter).length; if(n){ count+=n; dates.push(d.date); } });
    dates.sort();
    document.getElementById('summary').innerHTML=
      `<div class="k"><span>${esc(vendorFilter)} — staged</span><b>${count.toLocaleString()}</b></div>`+
      `<div class="k"><span>Days used</span><b>${dates.length}</b></div>`+
      `<div class="k"><span>First go-live</span><b>${dates[0]||'—'}</b></div>`+
      `<div class="k"><span>Last go-live</span><b>${dates[dates.length-1]||'—'}</b></div>`+
      `<div class="k"><span>&nbsp;</span><b><a href="#" class="linklike" onclick="clearVendorFilter();return false">← all vendors</a></b></div>`;
    return;
  }
  document.getElementById('summary').innerHTML=
    `<div class="k"><span>Staged for active</span><b>${s.totalSkus.toLocaleString()}</b></div>`+
    `<div class="k"><span>Vendors</span><b>${Object.keys(s.vendorTotals).length}</b></div>`+
    `<div class="k"><span>Days to clear</span><b>${s.totalDays}</b></div>`+
    `<div class="k"><span>Finishes</span><b>${s.endDate||'—'}</b></div>`+
    `<div class="k"><span>Cadence</span><b>${s.perDay}/day</b></div>`+
    (s.coverage?`<div class="k"><span>Date+time assigned</span><b title="every staged item has a durable go-live date and time">${s.coverage} ✓</b></div>`:'')+
    (s.scheduleGeneratedAt?`<div class="k"><span>Live as of</span><b title="pulled from the current dw_unified worklist">${fmtGoLive(s.scheduleGeneratedAt)}</b></div>`:'');
}
// ---- Vendor sort/filter + "see all staged for this vendor" ----
function populateVendorFilter(){
  const sel=document.getElementById('vendorFilter'); if(!sel||!DATA) return;
  const cur=sel.value;
  const vts=Object.entries(DATA.vendorTotals||{}).sort((a,b)=>b[1]-a[1]);
  sel.innerHTML='<option value="">All vendors ('+(DATA.totalSkus||0).toLocaleString()+')</option>'+
    vts.map(([v,n])=>`<option value="${esc(v)}">${esc(v)} (${n.toLocaleString()})</option>`).join('');
  sel.value=cur;
}
function applyVendorFilter(v){
  vendorFilter=v||'';
  const sel=document.getElementById('vendorFilter'); if(sel) sel.value=vendorFilter;
  const btn=document.getElementById('vendorAll');
  if(btn){ btn.style.display=vendorFilter?'':'none';
    btn.textContent=vendorFilter?('See all '+((DATA.vendorTotals||{})[vendorFilter]||0).toLocaleString()+' staged'):'See all staged'; }
  renderSummary(); renderMonth();
}
function clearVendorFilter(){ applyVendorFilter(''); }
// Flat list of EVERY staged item for a vendor (across all dates) with per-row reschedule.
function openVendorList(vendor){
  const items=DATA.days.flatMap(d=>d.skus.filter(x=>x.vendor===vendor))
    .sort((a,b)=> (a.go_live_at<b.go_live_at?-1:a.go_live_at>b.go_live_at?1:0));
  const rows=items.map(s=>{
    const key=s.shopify_id||s.dw_sku;
    const img=(s.image_url&&/^https?:/.test(s.image_url))
      ? `<img src="${esc(s.image_url)}" loading="lazy">`
      : `<div class="ph" style="background:${vcolor(vendor)}">${esc(vendor.slice(0,3).toUpperCase())}</div>`;
    return `<div class="vrow"><div class="vthumb">${img}</div>
      <div class="vmeta"><div class="vlabel">${esc(s.title||skuLabel(s))}</div>
        <div class="vsub">${esc(skuLabel(s))}${s.mat_tier===0?' · texture':(s.fix_type?' · '+esc(s.fix_type):'')}</div></div>
      <input type="date" class="vdate" data-key="${esc(key)}" value="${esc((s.go_live_at||'').slice(0,10))}" title="reschedule go-live"></div>`;
  }).join('');
  const body=`<div class="mform vlist"><div class="mhead"><div class="mtitle">${esc(vendor)} — ${items.length.toLocaleString()} staged</div><button type="button" class="mclose" id="vl_x" aria-label="Close">✕</button></div>
    <div class="note" style="margin:6px 0 8px">All staged items for this vendor, in go-live order. Change any date to reschedule that SKU.</div>
    <div class="vrows">${rows||'<div class="loading">None staged.</div>'}</div></div>`;
  const M=ModalRig.open({key:'dwcal-vendorlist',title:vendor,bodyHtml:body,width:560,height:720,bg:'var(--surface)'});
  M.body.querySelector('#vl_x').onclick=M.close;
  M.body.querySelectorAll('.vdate').forEach(inp=>{
    inp.onchange=async()=>{ const to=inp.value; if(!to) return; inp.disabled=true;
      await moveSku(inp.dataset.key, to, inp.dataset.key); M.close(); openVendorList(vendor); };
  });
}
function renderLegend(){
  const vt=DATA.vendorTotals;
  const top=Object.entries(vt).sort((a,b)=>b[1]-a[1]);
  document.getElementById('legend').innerHTML=top.map(([v,n])=>
    `<span class="li"><span class="sw" style="background:${vcolor(v)}"></span>${v} <span style="color:var(--ink)">${n.toLocaleString()}</span></span>`).join('');
}
/* go-live formatting helpers */
function slotLabel(h){ if(h==null) return ''; const ap=h<12?'AM':'PM'; let hh=h%12; if(hh===0)hh=12; return hh+' '+ap; }
function fmtGoLive(iso){ if(!iso) return '—'; try{ return new Date(iso).toLocaleString(undefined,{weekday:'short',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }catch(e){ return iso; } }
function skuLabel(s){ return s.dw_sku || s.mfr_sku || (s.shopify_id?('#'+String(s.shopify_id).split('/').pop()):'(no sku)'); }

/* ---- SKU detail modal (chip click) ---- */
function openSkuModal(s){
  const idNum = s.shopify_id ? String(s.shopify_id).split('/').pop() : null;
  const adminUrl = idNum ? 'https://admin.shopify.com/store/designer-laboratory-sandbox/products/'+idNum : null;
  const siteUrl  = s.handle ? 'https://www.designerwallcoverings.com/products/'+encodeURIComponent(s.handle) : null;
  const fx = s.fix_type==='add-sample' ? 's' : 'r';
  const label = s.title || skuLabel(s);
  const abbr  = s.vendor.slice(0,3).toUpperCase();
  const hero = (s.image_url && /^https?:/.test(s.image_url))
    ? `<img src="${esc(s.image_url)}" alt="${esc(label)}">`
    : `<div class="ph" style="background:${vcolor(s.vendor)}">${esc(abbr)}</div>`;
  const bodyHtml=`<div class="mform skud">
    <div class="mhead">
      <div class="mtitle">${esc(label)}</div>
      <button type="button" class="mclose" id="sk_x" aria-label="Close">✕</button>
    </div>
    <div class="hero">${hero}</div>
    <dl>
      <dt>Vendor</dt><dd>${esc(s.vendor)}</dd>
      ${s.dw_sku?`<dt>DW SKU</dt><dd><code>${esc(s.dw_sku)}</code></dd>`:''}
      ${s.mfr_sku?`<dt>Mfr SKU</dt><dd><code>${esc(s.mfr_sku)}</code></dd>`:''}
      ${s.mat_tier!=null
        ? `<dt>Tier</dt><dd><span class="fxpill ${s.mat_tier===0?'s':'r'}"><span class="d"></span>${s.mat_tier===0?'texture (goes first)':'standard'}</span></dd>`
        : (s.fix_type?`<dt>Build step</dt><dd><span class="fxpill ${fx}"><span class="d"></span>${esc(s.fix_type)}</span></dd>`:'')}
      <dt>Goes active</dt><dd title="${esc(s.go_live_at||'')}">⏱ ${fmtGoLive(s.go_live_at)}${s.slot_hour!=null?' · '+slotLabel(s.slot_hour)+' slot':''}</dd>
      ${idNum?`<dt>Shopify ID</dt><dd><code>${esc(idNum)}</code></dd>`:''}
    </dl>
    <div class="resched">
      <label for="sk_date">Reschedule go-live</label>
      <input type="date" id="sk_date" value="${esc((s.go_live_at||'').slice(0,10))}">
      <button type="button" id="sk_move" class="cta-btn">Move</button>
    </div>
    <div class="links">
      ${adminUrl?`<a class="adm" href="${adminUrl}" target="_blank" rel="noopener noreferrer">Open in Shopify Admin</a>`:''}
      ${siteUrl?`<a class="sf" href="${siteUrl}" target="_blank" rel="noopener noreferrer" title="staged items 404 on the storefront until they go active">View on site</a>`:''}
    </div>
  </div>`;
  const M=ModalRig.open({key:'dwcal-sku',title:'SKU',bodyHtml,width:440,height:660,bg:'var(--surface)'});
  M.body.querySelector('#sk_x').onclick=M.close;
  const mv=M.body.querySelector('#sk_move'), dinp=M.body.querySelector('#sk_date');
  if(mv) mv.onclick=async()=>{ const to=dinp.value; if(!to){ return; } mv.disabled=true; mv.textContent='Moving…';
    await moveSku(s.shopify_id||s.dw_sku, to, label); M.close(); };
}

/* Generic SKU-schedule month grid — shared by the SKU Activation tab AND the
   Google Calendar tab's not-connected fallback (renders into any container). */
function renderSchedMonth(containerId, monthDate, data){
  const cal=document.getElementById(containerId); cal.className='cal'; cal.innerHTML='';
  if(!data||!data.days){ cal.innerHTML='<div class="loading">No schedule.</div>'; return; }
  const byDate={}; data.days.forEach(d=>byDate[d.date]=d);
  const y=monthDate.getFullYear(), m=monthDate.getMonth();
  const dowRow=document.createElement('div'); dowRow.className='dow-row';
  ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d=>{const h=document.createElement('div');h.className='dow';h.textContent=d;dowRow.appendChild(h);});
  cal.appendChild(dowRow);
  const grid=document.createElement('div'); grid.className='grid'; cal.appendChild(grid);
  const first=new Date(y,m,1), startDow=first.getDay(), days=new Date(y,m+1,0).getDate();
  const prevDays=new Date(y,m,0).getDate();
  const todayISO=new Date().toISOString().slice(0,10);
  const expanders=[];
  for(let i=startDow-1;i>=0;i--){ grid.appendChild(outCell(prevDays-i)); }
  for(let dn=1;dn<=days;dn++){
    const iso=`${y}-${String(m+1).padStart(2,'0')}-${String(dn).padStart(2,'0')}`;
    const day=byDate[iso];
    const cell=document.createElement('div'); cell.className='cell scell'+(iso===todayISO?' today':'');
    // vendor filter: show only the selected vendor's chips (and count) on each day
    const dayskus = day ? (vendorFilter ? day.skus.filter(x=>x.vendor===vendorFilter) : day.skus) : null;
    const shownCount = dayskus ? dayskus.length : 0;
    let spanTxt='';
    if(shownCount){ const hrs=dayskus.map(x=>x.slot_hour); spanTxt=`<span class="span" title="drain slots that fire this day">${slotLabel(Math.min(...hrs))}–${slotLabel(Math.max(...hrs))}</span>`; }
    cell.innerHTML=`<div class="dh"><span class="dn">${dn}</span>${shownCount?`<span class="dhr"><span class="cnt">${shownCount}</span>${spanTxt}</span>`:''}</div>`;
    // --- drop target: dropping a chip here reschedules that SKU to this day ---
    cell.addEventListener('dragover',ev=>{ ev.preventDefault(); ev.dataTransfer.dropEffect='move'; cell.classList.add('drop-target'); });
    cell.addEventListener('dragleave',ev=>{ if(!cell.contains(ev.relatedTarget)) cell.classList.remove('drop-target'); });
    cell.addEventListener('drop',ev=>{
      ev.preventDefault(); cell.classList.remove('drop-target');
      let d; try{ d=JSON.parse(ev.dataTransfer.getData('text/plain')||'{}'); }catch(e){ return; }
      if(d.key && d.from!==iso) moveSku(d.key, iso, d.label);
    });
    if(shownCount){
      const chips=document.createElement('div'); chips.className='chips';
      dayskus.forEach(s=>{
        // Every data point is a REAL anchor to its actual staged product (Shopify
        // admin — staged items 404 on the storefront until active). So a chip is
        // openable every way: hover shows the URL, cmd/ctrl/middle/right-click opens
        // the product in a new tab. A plain left-click still opens the rich detail
        // modal (which also carries the admin + storefront links).
        const idNum=s.shopify_id?String(s.shopify_id).split('/').pop():null;
        const prodUrl=idNum?'https://admin.shopify.com/store/designer-laboratory-sandbox/products/'+idNum:null;
        const el=document.createElement(prodUrl?'a':'div'); el.className='chip';
        if(prodUrl){ el.href=prodUrl; el.target='_blank'; el.rel='noopener noreferrer'; }
        else { el.setAttribute('role','button'); }
        el.tabIndex=0;
        el.onkeydown=(e)=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();openSkuModal(s);}};
        el.title=`${skuLabel(s)} · ${s.vendor}${s.title?' · '+s.title:''}${s.mat_tier===0?' · texture':(s.fix_type?' · '+s.fix_type:'')}\n⏱ goes active ${fmtGoLive(s.go_live_at)}${prodUrl?'\n↗ opens the staged product in Shopify admin':''}`;
        const fx=(s.mat_tier!=null)?(s.mat_tier===0?'s':'r'):(s.fix_type==='add-sample'?'s':'r');
        const abbr=s.vendor.slice(0,3).toUpperCase();
        if(s.image_url && /^https?:/.test(s.image_url)){
          el.innerHTML=`<span class="fx ${fx}"></span><img loading="lazy" src="${s.image_url}" onerror="this.replaceWith(Object.assign(document.createElement('div'),{className:'ph',style:'background:${vcolor(s.vendor)}',textContent:'${abbr}'}))"><span class="vn">${s.vendor}</span>`;
        }else{
          el.innerHTML=`<span class="fx ${fx}"></span><div class="ph" style="background:${vcolor(s.vendor)}">${abbr}</div><span class="vn">${s.vendor}</span>`;
        }
        el.onclick=(e)=>{
          // modified / non-primary clicks follow the real href → straight to the product;
          // a plain left-click opens the detail modal (same admin link inside).
          if(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.button!==0) return;
          e.preventDefault(); e.stopPropagation(); openSkuModal(s);
        };
        // --- drag to reschedule: drop this chip on another day to change its go-live date ---
        const dragKey=s.shopify_id||s.dw_sku;
        if(dragKey){
          el.draggable=true;
          el.addEventListener('dragstart',ev=>{
            ev.dataTransfer.setData('text/plain',JSON.stringify({key:dragKey,from:iso,label:skuLabel(s)}));
            ev.dataTransfer.effectAllowed='move'; el.classList.add('dragging');
          });
          el.addEventListener('dragend',()=>el.classList.remove('dragging'));
        }
        chips.appendChild(el);
      });
      cell.appendChild(chips);
      // expand / collapse the whole day column (hidden later if everything fits)
      const more=document.createElement('button'); more.type='button'; more.className='more';
      const closedLabel='▾ show all '+shownCount;
      more.textContent=closedLabel;
      more.onclick=()=>{
        const open=cell.classList.toggle('expanded');
        more.textContent=open?'▴ collapse':closedLabel;
      };
      cell.appendChild(more);
      expanders.push({chips,more});
    }
    grid.appendChild(cell);
  }
  const totalCells=startDow+days; const trail=(7-(totalCells%7))%7;
  for(let dn=1;dn<=trail;dn++){ grid.appendChild(outCell(dn)); }
  // after layout, drop the expander on days whose chips already fit the preview
  requestAnimationFrame(()=>expanders.forEach(({chips,more})=>{
    if(chips.scrollHeight<=chips.clientHeight+2) more.remove();
  }));
}
// Persist a drag-to-reschedule: POST the move, then re-fetch + re-render so
// counts/summary reflect the SKU's new day. The move survives regenerate (server
// records it in data/schedule-overrides.json).
async function moveSku(key, toISO, label){
  try{
    const r=await fetch('/api/schedule/move',{method:'POST',headers:{'Content-Type':'application/json'},
      body:JSON.stringify({key,to_date:toISO})});
    const j=await r.json().catch(()=>({}));
    if(!r.ok||!j.ok){ alert('Reschedule failed: '+(j.error||('HTTP '+r.status))); return; }
    const rr=await fetch('/api/schedule'); DATA=await rr.json();
    if(typeof renderSummary==='function') renderSummary();
    if(typeof renderLegend==='function') renderLegend();
    renderMonth();
    console.log('rescheduled '+(label||key)+' → '+toISO);
  }catch(e){ alert('Reschedule error: '+e.message); }
}
function renderMonth(){
  document.getElementById('mlabel').textContent=fmtMonth(viewMonth);
  renderSchedMonth('cal', viewMonth, DATA);
}
document.getElementById('prev').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()-1,1);renderMonth();};
document.getElementById('next').onclick=()=>{viewMonth=new Date(viewMonth.getFullYear(),viewMonth.getMonth()+1,1);renderMonth();};
document.getElementById('apply').onclick=load;
document.getElementById('vendorFilter').onchange=(e)=>applyVendorFilter(e.target.value);
document.getElementById('vendorAll').onclick=()=>{ if(vendorFilter) openVendorList(vendorFilter); };
document.getElementById('refreshLive').onclick=async()=>{
  const b=document.getElementById('refreshLive'); const t=b.textContent; b.disabled=true; b.textContent='↻ Refreshing…';
  const keepVendor=vendorFilter;
  try{ await load(true); populateVendorFilter(); applyVendorFilter(keepVendor); }
  finally{ b.disabled=false; b.textContent=t; }
};

/* ======================= boot ======================= */
// Default to the populated SKU Activation tab so the page shows content on first
// load. The Google Calendar tab is empty until Steve connects OAuth (gated), so
// landing there first looks broken. After a successful connect (?connected=1) we
// land on gcal; a returning visitor's remembered tab still wins.
const initialTab = (new URLSearchParams(location.search).has('connected')) ? 'gcal'
  : (localStorage.getItem('dwcal_tab') || 'sku');
setTab(initialTab);
</script>
</body>
</html>