/* ===================== Designer Tables (manager cockpit) ===================== */
(function () {
const { useState, useEffect, useMemo, useRef } = React;

/* ---- config ---- */
const TKEY = "nove8_tables_v1";
const KKEY = "nove8_kpi_v1";
const POS = {
  "Motion Designer":    { dot: "#6366f1", short: "Motion" },
  "Graphic Designer":   { dot: "#10b981", short: "Graphic" },
  "Innovation Manager": { dot: "#f59e0b", short: "Innovation" },
};
const TYPE_OPTS = [
  { v: "motion",  label: "motion",  color: "#6c79f6" },
  { v: "graphic", label: "graphic", color: "#16b277" },
];
const DEFAULT_TASK_TYPES = [
  { id: "cs",       label: "creatives S", color: "#f0625a" },
  { id: "cm",       label: "creatives M", color: "#6c79f6" },
  { id: "smm",      label: "SMM",         color: "#16b277" },
  { id: "product",  label: "Product",     color: "#f59e0b" },
  { id: "hrpr",     label: "HR/PR",       color: "#ec4899" },
  { id: "meetings", label: "Meetings",    color: "#0ea5e9" },
];
const TTKEY = "nove8_tasktypes_v1";
const FNKEY = "nove8_funnels_v1";
const TYKEY = "nove8_types_v1";
function loadTypesList() {
  try { const r = localStorage.getItem(TYKEY); if (r) { const a = JSON.parse(r); if (Array.isArray(a) && a.length) return a; } } catch (e) {}
  return TYPE_OPTS.map((o) => ({ id: o.v, label: o.label, color: o.color }));
}
const TYPE_PALETTE = ["#14b8a6", "#8b5cf6", "#ef4444", "#0ea5e9", "#f59e0b", "#ec4899", "#65a30d", "#6366f1", "#d946ef", "#0891b2"];
function loadTaskTypes() { try { const r = localStorage.getItem(TTKEY); if (r) { const a = JSON.parse(r); if (Array.isArray(a) && a.length) return a; } } catch (e) {} return DEFAULT_TASK_TYPES; }
function loadFunnels() { try { const r = localStorage.getItem(FNKEY); if (r) { const a = JSON.parse(r); if (Array.isArray(a)) return a; } } catch (e) {} return []; }
const ttId = () => "tt_" + Date.now().toString(36) + Math.floor(Math.random() * 1000).toString(36);
const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const YEARS = [2025, 2026];
const KPI_MIN = 30, KPI_GROWTH = 36; // weekly SP thresholds (defaults)
// Bonus thresholds derive from the designer's weekly MIN KPI:
//   month minimum = weekCount * min;  tier threshold = month minimum + offset.
const BONUS_PCTS = [10, 15, 20, 25, 30];
const DEFAULT_BONUS_OFFSETS = [20, 30, 40, 50, 60];
const DEFAULT_KPI = { min: KPI_MIN, growth: KPI_GROWTH, fullDays: 5, threshold: 3, bonusOffsets: DEFAULT_BONUS_OFFSETS, bonusEnabled: true };
function bonusTableFor(weekCount, kpi) {
  const monthMin = weekCount * (kpi.min || 0);
  const offs = (kpi.bonusOffsets && kpi.bonusOffsets.length === 5) ? kpi.bonusOffsets : DEFAULT_BONUS_OFFSETS;
  return { min: monthMin, tiers: BONUS_PCTS.map((p, i) => [monthMin + offs[i], p]) };
}
function loadKpis() { try { const r = localStorage.getItem(KKEY); if (r) return JSON.parse(r); } catch (e) {} return {}; }
function getKpi(kpis, dId) { const k = { ...DEFAULT_KPI, ...(kpis && kpis[dId] ? kpis[dId] : {}) }; if (!k.bonusOffsets || k.bonusOffsets.length !== 5) k.bonusOffsets = DEFAULT_BONUS_OFFSETS; return k; }

/* ---- month-versioned KPI ----
   A KPI edit takes effect from the month it was made in; months before keep
   the values that were in force back then (past stats are never recomputed).
   Storage: kpis[dId] = { versions: [{from: "YYYY-MM", ...kpi fields}] };
   a legacy plain object (no versions) applies to all time. */
const ymKey = (y, m) => y + "-" + String(m + 1).padStart(2, "0");
function normKpi(v) {
  const k = { ...DEFAULT_KPI, ...(v || {}) };
  if (!k.bonusOffsets || k.bonusOffsets.length !== 5) k.bonusOffsets = DEFAULT_BONUS_OFFSETS;
  delete k.from;
  return k;
}
function kpiFor(kpis, dId, year, month) {
  const raw = kpis && kpis[dId];
  if (!raw) return normKpi({});
  const oKey = ymKey(year, month);
  let k;
  if (raw.overrides && raw.overrides[oKey]) {
    // month-specific override (an edit made on a past month) wins over versions
    k = normKpi(raw.overrides[oKey]);
  } else if (!Array.isArray(raw.versions)) {
    k = normKpi(raw); // legacy — one value for all time
  } else {
    const sorted = raw.versions.slice().sort((a, b) => String(a.from).localeCompare(String(b.from)));
    let pick = null;
    for (const v of sorted) { if (String(v.from) <= oKey) pick = v; }
    // months before the first recorded change keep the defaults that were in
    // force back then — a new KPI must never repaint history
    k = normKpi(pick || {});
  }
  // the monthly bonus is designer-wide: on in every month or off in every
  // month — the root flag overrides whatever a version/override carries
  if (typeof raw.bonusEnabled === "boolean") k.bonusEnabled = raw.bonusEnabled;
  return k;
}

/* Effective weekly SP, normalised by worked days:
   - worked >= threshold days: project the daily average across a full week (avg * fullDays)
   - worked  < threshold days: actual SP + remaining days valued at the MIN daily rate (min / fullDays)
   - worked == 0: no result yet (null) */
function effectiveWeekSP(sp, wd, kpi) {
  const full = kpi.fullDays || 5, thr = kpi.threshold || 3;
  const d = Math.max(0, Math.min(full, parseInt(wd, 10) || 0));
  if (d === 0) return null;
  if (d >= thr) return Math.round((sp / d) * full * 10) / 10;
  return Math.round((sp + (full - d) * (kpi.min / full)) * 10) / 10;
}
function tierFromSP(eff, kpi) {
  if (eff == null) return null;
  // compare on the rounded value so the colour always matches the whole-number
  // display: 35.8 shows as 36 → with growth KPI 36 the week is green
  const e = Math.round(eff);
  if (e >= kpi.growth) return "g";
  if (e >= kpi.min) return "y";
  return "r";
}
/* An explicitly-entered 0 in Working days = sick leave / vacation ("off" week).
   An empty field (dash) is simply not filled in and stays neutral. */
function isOffWeek(week) {
  return !!week && (week.wd === 0 || week.wd === "0");
}

/* tier for a week — only once the week has ended (past weeks), else null.
   "off" (violet) shows immediately: vacations are usually known in advance. */
function weekTier(week, weekMeta, kpi, now) {
  if (!week) return null;
  if (isOffWeek(week)) return "off";
  if (weekMeta && weekMeta.end && weekMeta.end >= (now || Date.now())) return null;
  const a = weekAgg(week);
  const hasData = a.sp > 0 || (week.rows && week.rows.some((r) => r && r.app)) || (week.wd && +week.wd > 0);
  if (!hasData) return null;
  return tierFromSP(effectiveWeekSP(a.sp, week.wd, kpi), kpi);
}

const pad = (n) => String(n).padStart(2, "0");
const initials = (n) => n.split(" ").filter(Boolean).slice(0, 2).map((w) => w[0].toUpperCase()).join("");
function textOn(hex) {
  const c = hex.replace("#", ""); if (c.length < 6) return "#fff";
  const r = parseInt(c.substr(0,2),16), g = parseInt(c.substr(2,2),16), b = parseInt(c.substr(4,2),16);
  return (0.299*r + 0.587*g + 0.114*b) > 165 ? "#14171c" : "#fff";
}

/* weeks (Mon–Sun) clipped to the month */
/* work weeks (Mon–Fri) assigned to a month by majority of weekdays — a week
   belongs to the month containing its Wednesday. Weekends ignored. 4–5 per month. */
function weeksOfMonth(year, m) {
  const out = [];
  let mon = new Date(year, m, 1);
  mon.setDate(mon.getDate() - ((mon.getDay() + 6) % 7)); // Monday of the week containing the 1st
  for (let g = 0; g < 8; g++) {
    const wed = new Date(mon); wed.setDate(mon.getDate() + 2);
    const cmp = (wed.getFullYear() - year) * 12 + (wed.getMonth() - m);
    if (cmp > 0) break;
    if (cmp === 0) {
      const fri = new Date(mon); fri.setDate(mon.getDate() + 4);
      const same = mon.getMonth() === fri.getMonth();
      const label = same
        ? `${pad(mon.getDate())}–${pad(fri.getDate())}`
        : `${pad(mon.getDate())}.${pad(mon.getMonth() + 1)}–${pad(fri.getDate())}.${pad(fri.getMonth() + 1)}`;
      const endTs = new Date(fri); endTs.setHours(23, 59, 59, 999);
      out.push({ idx: out.length, label, end: endTs.getTime() });
    }
    mon = new Date(mon); mon.setDate(mon.getDate() + 7);
  }
  return out;
}

/* ---- storage ---- */
function loadTables() { try { const r = localStorage.getItem(TKEY); if (r) return JSON.parse(r); } catch (e) {} return null; }

/* One-time migration: old default zeros in Working days become "not filled in"
   (dash). From now on an explicitly typed 0 means sick/vacation. */
(function migrateWdZeros() {
  try {
    if (localStorage.getItem("nove8_wd0_migrated_v1")) return;
    const raw = localStorage.getItem(TKEY);
    if (raw) {
      const t = JSON.parse(raw);
      let touched = false;
      Object.values(t || {}).forEach((entry) => {
        Object.values((entry && entry.weeks) || {}).forEach((wk) => {
          if (wk && (wk.wd === 0 || wk.wd === "0")) { delete wk.wd; touched = true; }
        });
      });
      if (touched) localStorage.setItem(TKEY, JSON.stringify(t));
    }
    localStorage.setItem("nove8_wd0_migrated_v1", "1");
  } catch (e) {}
})();
const mkey = (dId, year, m) => `${dId}|${year}-${m}`;

function seedTables(designers) {
  // a modest example on the first designer's current month (June 2026) so the matrix
  // demonstrates the green/yellow/red weekly dynamics — like the screenshots.
  const t = {};
  const d0 = designers[0];
  if (d0) {
    const k = mkey(d0.id, 2026, 5);
    const app = d0.productId || "";
    const row = (task, sp, link) => ({ app, type: "motion", task, link: link || "", sp: String(sp) });
    t[k] = { weeks: {
      0: { wd: 5, rows: [ row("cm", 14, "5542"), row("cm", 12), row("cm", 10), row("smm", 6) ] }, // sp 42 → green
      1: { wd: 5, rows: [ row("cm", 16), row("cm", 16) ] },                                        // sp 32 → yellow
      2: { wd: 4, rows: [ row("cs", 10), row("cs", 10) ] },                                        // sp 20 → red
    } };
  }
  return t;
}

/* ---- aggregation ---- */
function weekAgg(week) {
  if (!week || !week.rows) return { task: 0, sp: 0 };
  let task = 0, sp = 0;
  week.rows.forEach((r) => {
    if (r && r.app) {
      // meetings earn SP but are not tasks (not calculations)
      if (!r.meet) task += 1;
      const n = parseFloat(r.sp); if (!isNaN(n)) sp += n;
    }
  });
  return { task, sp: Math.round(sp * 10) / 10 };
}
function monthAgg(tables, dId, year, m, weeks) {
  const entry = tables[mkey(dId, year, m)];
  let task = 0, sp = 0;
  weeks.forEach((w) => { const a = weekAgg(entry && entry.weeks ? entry.weeks[w.idx] : null); task += a.task; sp += a.sp; });
  return { task, sp: Math.round(sp * 10) / 10 };
}
const bonusTier = (sp, kpi) => sp >= kpi.growth ? 10 : sp >= kpi.min ? 5 : 0;
const statusColor = (sp, kpi) => sp >= kpi.growth ? "#10b981" : sp >= kpi.min ? "#f59e0b" : "#e0444b";

/* Sum of theoretical (worked-days-normalised) weekly results across the month. */
function monthEffective(tables, dId, year, m, weeks, kpi) {
  const entry = tables[mkey(dId, year, m)];
  let eff = 0;
  weeks.forEach((w) => {
    const wk = entry && entry.weeks ? entry.weeks[w.idx] : null;
    // Off week (explicit 0 working days — sick/vacation): credited with the
    // designer's minimal weekly KPI, so the monthly bonus isn't dragged down.
    if (isOffWeek(wk)) { eff += (kpi.min || 0); return; }
    const a = weekAgg(wk);
    const e = effectiveWeekSP(a.sp, wk && wk.wd, kpi);
    if (e != null) eff += e;
  });
  return Math.round(eff * 10) / 10;
}
/* Monthly bonus %, from the effective month total vs the week-count bonus table. */
function bonusForMonth(effSum, weekCount, kpi) {
  const tbl = bonusTableFor(weekCount, kpi);
  let pct = 0, nextThr = null, nextPct = null;
  for (const [thr, p] of tbl.tiers) {
    if (effSum >= thr) pct = p;
    else if (nextThr == null) { nextThr = thr; nextPct = p; }
  }
  return { pct, min: tbl.min, metMin: effSum >= tbl.min, nextThr, nextPct, max: tbl.tiers[tbl.tiers.length - 1] };
}

/* per task-type weekly rollup (for the matrix) */
function weekTypeAgg(week, typeId) {
  if (!week || !week.rows) return { task: 0, sp: 0 };
  let task = 0, sp = 0;
  week.rows.forEach((r) => { if (r && r.task === typeId) { task += 1; const n = parseFloat(r.sp); if (!isNaN(n)) sp += n; } });
  return { task, sp: Math.round(sp * 10) / 10 };
}
/* rows with no / unknown task type (e.g. historical calculations saved before
   the calc card context existed) — aggregated into the matrix "Other" column */
function weekUntypedAgg(week, knownIds) {
  if (!week || !week.rows) return { task: 0, sp: 0 };
  let task = 0, sp = 0;
  week.rows.forEach((r) => {
    if (r && r.app && (!r.task || !knownIds.has(r.task))) {
      task += 1; const n = parseFloat(r.sp); if (!isNaN(n)) sp += n;
    }
  });
  return { task, sp: Math.round(sp * 10) / 10 };
}
/* week colour: green = growth met, yellow = threshold met, red = below, null = no data */
const TIER_BG = { g: "#a7e8b0", y: "#fbe48a", r: "#f6a09a", off: "#d9c9f7" };

/* ---- chip select ---- */
function ChipSelect({ value, options, onChange, placeholder, disabled }) {
  const opt = options.find((o) => o.v === value);
  const filled = !!opt;
  const style = filled ? { background: opt.color, color: textOn(opt.color) } : {};
  if (disabled) {
    return (
      <div className={"csel" + (filled ? " filled" : "")}>
        <div className="csel-ro" style={style}>{filled ? opt.label : placeholder}</div>
      </div>
    );
  }
  return (
    <div className={"csel" + (filled ? " filled" : "")}>
      <select value={value || ""} onChange={(e) => onChange(e.target.value)} style={style}>
        <option value="" style={{ color: "#98a0ac", background: "#fff" }}>{placeholder}</option>
        {options.map((o) => <option key={o.v} value={o.v} style={{ color: "#14171c", background: "#fff" }}>{o.label}</option>)}
      </select>
    </div>
  );
}

/* ---- drag grip ---- */
function GripIcon() {
  return <svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor" aria-hidden="true"><circle cx="2.5" cy="3" r="1.25"/><circle cx="7.5" cy="3" r="1.25"/><circle cx="2.5" cy="7" r="1.25"/><circle cx="7.5" cy="7" r="1.25"/><circle cx="2.5" cy="11" r="1.25"/><circle cx="7.5" cy="11" r="1.25"/></svg>;
}

/* ---- week block ---- */
function WeekBlock({ week, data, products, taskTypes, funnels, kpi, readOnly, onCell, onWD, onDeleteRow, onEditLink, dragInfo, setDragInfo, onMoveRow, onAddMeet, typeOpts }) {
  const rows = (data && data.rows) ? data.rows.slice() : [];
  while (rows.length < 5) rows.push({});
  if (!readOnly && rows[rows.length - 1] && rows[rows.length - 1].app) rows.push({});
  const agg = weekAgg(data);
  const appOpts = products.map((p) => ({ v: p.id, label: p.name, color: p.color }));
  const taskOpts = taskTypes.map((t) => ({ v: t.id, label: t.label, color: t.color }));
  const funnelOpts = (funnels || []).map((f) => ({ v: f.id, label: f.label || f.name, color: f.color }));
  const labelOf = (opts, v) => { const o = opts.find((x) => x.v === v); return o ? o.label : ""; };
  const isUrl = (s) => /^https?:\/\//i.test(String(s || ""));
  const linkHref = (r) => (r.linkUrl && isUrl(r.linkUrl)) ? r.linkUrl : (isUrl(r.link) ? r.link : null);
  const tier = weekTier(data, week, kpi);
  const passed = week.end < Date.now();
  const off = isOffWeek(data);
  // off week: show the credited weekly average (the designer's min KPI)
  const eff = off ? (kpi.min || 0) : (passed ? effectiveWeekSP(agg.sp, data && data.wd, kpi) : null);
  const TIER_FILL = { g: { bg: "#b7ecc0", fg: "#0a6e44" }, y: { bg: "#fbe48a", fg: "#8a5e12" }, r: { bg: "#f6a09a", fg: "#8f2820" }, off: { bg: "#dcd0f9", fg: "#5b3fa8" } };
  const fill = tier ? TIER_FILL[tier] : null;
  const fillStyle = fill ? { boxShadow: "inset 0 0 0 200px " + fill.bg } : undefined;
  const fgStyle = fill ? { color: fill.fg } : undefined;
  const dateStyle = fill ? { background: fill.bg } : undefined;

  const isDropTarget = dragInfo && dragInfo.wIdx !== week.idx;
  const onDragOver = (e) => { if (isDropTarget) { e.preventDefault(); e.dataTransfer.dropEffect = "move"; } };
  const onDrop = (e) => { if (isDropTarget) { e.preventDefault(); onMoveRow(dragInfo.wIdx, dragInfo.rowIdx, week.idx); setDragInfo(null); } };

  return (
    <div className={"wk" + (readOnly ? " wk-ro" : "") + (tier ? " wk-tier-" + tier : "") + (isDropTarget ? " wk-drop" : "")} onDragOver={onDragOver} onDrop={onDrop}>
      <div className="wk-date">
        <div className="lab">Date Week</div>
        <div className="rng">{week.label}</div>
      </div>
      <div className="wk-mid">
        <div className="wk-grid">
          <div className="wk-griph"></div>
          <div className="colh">App Name</div>
          <div className="colh">Type</div>
          <div className="colh">Task type</div>
          <div className="colh">Name/Link</div>
          <div className="colh">SP</div>
          <div className="colh">Funnel</div>
          <div className="colh-x"></div>
        </div>
        {rows.map((r, i) => {
          const hasContent = !!(r.app || r.type || r.task || r.link || r.sp || r.funnel);
          const href = linkHref(r);
          const dragging = dragInfo && dragInfo.wIdx === week.idx && dragInfo.rowIdx === i;
          return (
          <div className={"wk-grid" + (dragging ? " wk-row-dragging" : "")} key={i}>
            {hasContent
              ? <span className="wk-grip" draggable title="Drag to move this task to another week"
                  onDragStart={(e) => { setDragInfo({ wIdx: week.idx, rowIdx: i }); e.dataTransfer.effectAllowed = "move"; try { e.dataTransfer.setData("text/plain", week.idx + ":" + i); } catch (_) {} }}
                  onDragEnd={() => setDragInfo(null)}><GripIcon /></span>
              : <span className="wk-grip wk-grip-empty"></span>}
            <ChipSelect value={r.app} options={appOpts} placeholder="-select-" disabled={readOnly} onChange={(v) => onCell(week.idx, i, "app", v)} />
            <ChipSelect value={r.type} options={(typeOpts && typeOpts.length) ? typeOpts : TYPE_OPTS} placeholder="-select-" disabled={readOnly} onChange={(v) => onCell(week.idx, i, "type", v)} />
            <ChipSelect value={r.task} options={taskOpts} placeholder="-select-" disabled={readOnly} onChange={(v) => onCell(week.idx, i, "task", v)} />
            <div className="cinput linkcell" onContextMenu={(e) => { e.preventDefault(); onEditLink(week.idx, i, r); }} title="Right-click to edit number / link">
              {href
                ? <a className="linkcell-a" href={href} target="_blank" rel="noopener noreferrer" title={href}>{r.link || href}</a>
                : <span className={"linkcell-t" + (r.link ? "" : " empty")}>{r.link || "—"}</span>}
            </div>
            <input className="cinput" value={r.sp || ""} placeholder="" inputMode="decimal" readOnly={readOnly} tabIndex={readOnly ? -1 : undefined} onChange={(e) => onCell(week.idx, i, "sp", e.target.value)} />
            <ChipSelect value={r.funnel} options={funnelOpts} placeholder={funnelOpts.length ? "-select-" : "—"} disabled={readOnly} onChange={(v) => onCell(week.idx, i, "funnel", v)} />
            {hasContent
              ? <button className="row-del" title="Delete task" onClick={() => onDeleteRow(week.idx, i)}>×</button>
              : <span className="row-del-sp"></span>}
          </div>
          );
        })}
        {onAddMeet && (
          <button type="button" className="wk-add-meet" onClick={onAddMeet}
            title="Add a meeting — earns SP but does not count as a task">+ Meeting</button>
        )}
      </div>
      <div className="wk-right">
        <div className="wk-wd">
          <div className="lab">Working days</div>
          <input type="number" min="0" max="31" value={data && data.wd != null ? data.wd : ""} placeholder="—" title="Empty = not filled in · 0 = sick/vacation (week counts as min KPI)" onChange={(e) => onWD(week.idx, e.target.value)} />
        </div>
        <div className="wk-results">
          <div className="wk-res">
            <div className="wk-res-in" style={fillStyle}>
              <div className="k" style={fgStyle}>Task result</div>
              <div className="v" style={fgStyle}>{agg.task}</div>
            </div>
          </div>
          <div className="wk-res">
            <div className="wk-res-in" style={fillStyle}>
              <div className="k" style={fgStyle}>SP result</div>
              <div className="v" style={fgStyle} title={agg.sp !== Math.round(agg.sp) ? "exact: " + agg.sp : undefined}>{Math.round(agg.sp)}</div>
              {eff != null && (
                off
                  ? <div className="wk-norm" style={fgStyle} title="Off week (sick/vacation) — credited with the minimal weekly KPI">≈{eff} / wk · min KPI</div>
                  : <div className="wk-norm" style={fgStyle} title="Normalised by worked days">≈{eff} / wk</div>
              )}
            </div>
          </div>
        </div>
        {!passed && !isOffWeek(data) && (agg.sp > 0 || (data && data.wd)) && <div className="wk-pending">Result colours after the week ends</div>}
      </div>
    </div>
  );
}

/* ---- matrix: one month ---- */
function MonthMatrix({ monthIdx, year, current, entry, taskTypes, kpi, showOther }) {
  const weeksData = (entry && entry.weeks) ? entry.weeks : {};
  const weeks = weeksOfMonth(year, monthIdx);
  const knownIds = new Set(taskTypes.map((t) => t.id));
  const sums = { wd: 0, types: taskTypes.map(() => ({ task: 0, sp: 0 })), other: { task: 0, sp: 0 } };
  weeks.forEach((wk) => {
    const w = weeksData[wk.idx];
    if (w) {
      if (w.wd) sums.wd += (+w.wd || 0);
      taskTypes.forEach((t, ti) => { const a = weekTypeAgg(w, t.id); sums.types[ti].task += a.task; sums.types[ti].sp += a.sp; });
      const o = weekUntypedAgg(w, knownIds); sums.other.task += o.task; sums.other.sp += o.sp;
    }
  });
  const r1 = (n) => Math.round(n * 10) / 10;
  return (
    <div className={"mx-card" + (current ? " mx-current" : "")} {...(current ? { "data-current": "1" } : {})}>
      <div className="mx-month">
        <div className="mx-month-h">{current ? "current month" : "month"}</div>
        <div className="mx-month-name">{MONTHS[monthIdx]}</div>
      </div>
      <div className="mx-table-wrap">
        <table className="mx-table">
          <thead>
            <tr>
              <th rowSpan={2} className="mx-th-weeks">weeks</th>
              <th rowSpan={2} className="mx-th-wd">working days</th>
              {taskTypes.map((t) => <th key={t.id} colSpan={2} className="mx-th-type">{t.label}</th>)}
              {showOther && <th colSpan={2} className="mx-th-type" title="Calculations without a task type (older history)">Other</th>}
            </tr>
            <tr>
              {taskTypes.map((t) => (
                <React.Fragment key={t.id}><th className="mx-sub">task</th><th className="mx-sub">sp</th></React.Fragment>
              ))}
              {showOther && <React.Fragment><th className="mx-sub">task</th><th className="mx-sub">sp</th></React.Fragment>}
            </tr>
          </thead>
          <tbody>
            {weeks.map((wk, i) => {
              const w = weeksData[wk.idx];
              const tier = weekTier(w, wk, kpi);
              return (
                <tr key={i}>
                  <td><span className="mx-week" style={tier ? { background: TIER_BG[tier], borderColor: "transparent" } : {}}>W{i + 1} · {wk.label}</span></td>
                  <td><span className="mx-cell">{isOffWeek(w) ? "0" : (w && w.wd ? w.wd : "")}</span></td>
                  {taskTypes.map((t) => {
                    const a = weekTypeAgg(w, t.id);
                    return <React.Fragment key={t.id}>
                      <td><span className="mx-cell">{a.task || ""}</span></td>
                      <td><span className="mx-cell" title={a.sp !== Math.round(a.sp) ? "exact: " + a.sp : undefined}>{Math.round(a.sp) || ""}</span></td>
                    </React.Fragment>;
                  })}
                  {showOther && (() => {
                    const o = weekUntypedAgg(w, knownIds);
                    return <React.Fragment>
                      <td><span className="mx-cell">{o.task || ""}</span></td>
                      <td><span className="mx-cell" title={o.sp !== Math.round(o.sp) ? "exact: " + o.sp : undefined}>{Math.round(o.sp) || ""}</span></td>
                    </React.Fragment>;
                  })()}
                </tr>
              );
            })}
            <tr className="mx-sum">
              <td><span className="mx-sumlabel">SUM</span></td>
              <td><span className="mx-cell sum">{sums.wd || ""}</span></td>
              {taskTypes.map((t, ti) => (
                <React.Fragment key={t.id}>
                  <td><span className="mx-cell sum">{sums.types[ti].task || ""}</span></td>
                  <td><span className="mx-cell sum" title={r1(sums.types[ti].sp) !== Math.round(sums.types[ti].sp) ? "exact: " + r1(sums.types[ti].sp) : undefined}>{Math.round(sums.types[ti].sp) || ""}</span></td>
                </React.Fragment>
              ))}
              {showOther && (
                <React.Fragment>
                  <td><span className="mx-cell sum">{sums.other.task || ""}</span></td>
                  <td><span className="mx-cell sum" title={r1(sums.other.sp) !== Math.round(sums.other.sp) ? "exact: " + r1(sums.other.sp) : undefined}>{Math.round(sums.other.sp) || ""}</span></td>
                </React.Fragment>
              )}
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  );
}

/* ---- matrix: full year ---- */
function MatrixView({ designer, tables, year, taskTypes, kpi, kpiOf }) {
  const wrapRef = useRef(null);
  const now = new Date();
  const curMonth = year === now.getFullYear() ? now.getMonth() : -1;
  // Show the "Other" column only when the year actually has rows without a
  // known task type (historical calculations predate the calc-card context).
  const showOther = useMemo(() => {
    const knownIds = new Set(taskTypes.map((t) => t.id));
    for (let m = 0; m < 12; m++) {
      const entry = tables[mkey(designer.id, year, m)];
      const wks = (entry && entry.weeks) || {};
      for (const wIdx of Object.keys(wks)) {
        if (weekUntypedAgg(wks[wIdx], knownIds).task > 0) return true;
      }
    }
    return false;
  }, [tables, designer && designer.id, year, taskTypes]);
  useEffect(() => {
    const wrap = wrapRef.current; if (!wrap) return;
    const card = wrap.querySelector("[data-current]");
    const scroller = wrap.closest(".dt-detail");
    if (card && scroller) {
      const cr = card.getBoundingClientRect(), sr = scroller.getBoundingClientRect();
      scroller.scrollTop += (cr.top - sr.top) - 14;
    }
  }, [designer && designer.id, year]);
  return (
    <div className="mx-wrap" ref={wrapRef}>
      <div className="mx-legend">
        <span className="mx-leg-title">Week result vs weekly KPI ({kpi.min} / {kpi.growth} SP)</span>
        <span><span className="mx-dot" style={{ background: TIER_BG.g }}></span>growth met</span>
        <span><span className="mx-dot" style={{ background: TIER_BG.y }}></span>threshold met</span>
        <span><span className="mx-dot" style={{ background: TIER_BG.r }}></span>below threshold</span>
        <span><span className="mx-dot" style={{ background: TIER_BG.off }}></span>off (0 wd) — counts as min KPI</span>
      </div>
      {MONTHS.map((mn, m) => (
        <MonthMatrix key={m} monthIdx={m} year={year} current={m === curMonth} entry={tables[mkey(designer.id, year, m)]} taskTypes={taskTypes} kpi={kpiOf ? kpiOf(m) : kpi} showOther={showOther} />
      ))}
    </div>
  );
}

/* ---- task-type manager (single source for both views) ---- */
function TaskTypeManager({ taskTypes, onAdd, onRemove }) {
  const [open, setOpen] = useState(false);
  const [val, setVal] = useState("");
  const add = () => { if (val.trim()) { onAdd(val.trim()); setVal(""); } };
  return (
    <div className="ttm">
      <button className="ttm-btn" onClick={() => setOpen((o) => !o)}>Task types · {taskTypes.length}</button>
      {open && (
        <div className="ttm-panel">
          <div className="ttm-h">Task types — applied to both views</div>
          <div className="ttm-chips">
            {taskTypes.map((t) => (
              <span className="ttm-chip" key={t.id}>
                <span className="pdot" style={{ background: t.color }}></span>{t.label}
                <button onClick={() => onRemove(t.id)} title="Remove">×</button>
              </span>
            ))}
          </div>
          <div className="ttm-add">
            <input value={val} placeholder="New type…" onChange={(e) => setVal(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") add(); if (e.key === "Escape") setOpen(false); }} />
            <button className="btn primary" disabled={!val.trim()} onClick={add}>Add</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ---- detail (one designer's monthly table) ---- */
function Detail({ designer, products, tables, year, month, viewMode, setView, taskTypes, funnels, typeOpts, onAddType, onRemoveType, kpi, kpiOf, readOnly, onEditKpi, onSetKpi, onCell, onWD, onDeleteRow, onEditLink, onMoveRow, addMeet }) {
  if (!designer) return <div className="placeholder"><div className="pl"><b>No designer selected</b>Pick someone from the list</div></div>;
  const weeks = useMemo(() => weeksOfMonth(year, month), [year, month]);
  const [dragInfo, setDragInfo] = useState(null);
  const entry = tables[mkey(designer.id, year, month)];
  const ma = monthAgg(tables, designer.id, year, month, weeks);
  const effMonth = monthEffective(tables, designer.id, year, month, weeks, kpi);
  const bonus = bonusForMonth(effMonth, weeks.length, kpi);
  const weekEff = weeks.map((w) => {
    const wk = entry && entry.weeks ? entry.weeks[w.idx] : null;
    // off week (explicit 0 wd) is credited with the minimal weekly KPI — show
    // that credit as a term in the bonus equation too
    if (isOffWeek(wk)) return kpi.min || 0;
    const a = weekAgg(wk);
    return effectiveWeekSP(a.sp, wk && wk.wd, kpi);
  });
  const effParts = weekEff.filter((e) => e != null && e > 0);
  const product = products.find((p) => p.id === designer.productId);
  const pinfo = POS[designer.position] || {};

  return (
    <div className="dt-detail">
      <div className="detail-head">
        <div className="avatar" style={{ background: "#eef0ff", color: "#4f46e5" }}>{initials(designer.name)}</div>
        <div>
          <div className="dh-name">{designer.name}</div>
          <div className="dh-sub">
            <span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
              <span style={{ width: 7, height: 7, borderRadius: "50%", background: pinfo.dot }}></span>{designer.position}
            </span>
            {product && <span className="dh-chip" style={{ background: product.color }}>{product.name}</span>}
            <span>{designer.email}</span>
          </div>
        </div>
        <div style={{ flex: 1 }}></div>
        <div className="right-controls">
          {!readOnly && <button className="kpi-btn" onClick={onEditKpi} title="Edit KPI & week-result logic">KPI / week</button>}
          {!readOnly && <TaskTypeManager taskTypes={taskTypes} onAdd={onAddType} onRemove={onRemoveType} />}
          <div className="vtoggle">
            <button className={viewMode === "weekly" ? "on" : ""} onClick={() => setView("weekly")}>Weekly</button>
            <button className={viewMode === "matrix" ? "on" : ""} onClick={() => setView("matrix")}>Matrix</button>
          </div>
          <div className="detail-period">{viewMode === "matrix" ? year : MONTHS[month] + " " + year}</div>
        </div>
      </div>

      {viewMode === "matrix" ? (
        <MatrixView designer={designer} tables={tables} year={year} taskTypes={taskTypes} kpi={kpi} kpiOf={kpiOf} />
      ) : (
      <React.Fragment>
      <div className="kpi-row">
        <div className="kpi-card">
          <div className="t">Month Result</div>
          <div className="kpi-twin">
            <div><div className="lab">task</div><div className="num">{ma.task}</div></div>
            <div><div className="lab">sp</div><div className="num">{ma.sp}</div></div>
          </div>
        </div>
        <div className={"kpi-card" + (readOnly ? "" : " kpi-card-clickable")} onClick={readOnly ? undefined : onEditKpi} title={readOnly ? "" : "Edit KPI"}>
          <div className="t">KPI / Week</div>
          <div className="kpi-twin">
            <div><div className="lab">min</div><div className="num">{kpi.min}</div></div>
            <div><div className="lab">growth</div><div className="num">{kpi.growth}</div></div>
          </div>
        </div>
        <div className="kpi-card kpi-card-clickable bonus-card" onClick={onEditKpi} title={readOnly ? "View bonus scale" : "Edit bonus system"}>
          <div className="t">Month Bonus <span className="kpi-wkn">{weeks.length}w</span>
            {!readOnly && <label className="bonus-switch" onClick={(e) => e.stopPropagation()} title={kpi.bonusEnabled ? "Bonus on — click to disable" : "Bonus off — click to enable"}>
              <input type="checkbox" checked={!!kpi.bonusEnabled} onChange={() => onSetKpi({ bonusEnabled: !kpi.bonusEnabled })} />
              <span className="bonus-switch-track"></span>
            </label>}
          </div>
          {kpi.bonusEnabled ? (
            <React.Fragment>
              <div className="bonus-card-row">
                <div className={"kpi-bonus t" + bonus.pct}>{bonus.pct}%</div>
                <div className="bonus-card-kpi">
                  <div><span className="bl">min</span><b>{bonus.min}</b></div>
                  <div><span className="bl">growth</span><b>{weeks.length * kpi.growth}</b></div>
                </div>
              </div>
              <div className="bonus-card-next">
                {bonus.nextThr != null
                  ? <React.Fragment><b>+{Math.max(0, Math.round((bonus.nextThr - effMonth) * 10) / 10)}</b> SP to {bonus.nextPct}%</React.Fragment>
                  : <React.Fragment>🎉 max bonus</React.Fragment>}
              </div>
              <div className="bonus-card-calc">{effParts.length ? effParts.join(" + ") : "0"} = <b>{effMonth}</b> SP</div>
            </React.Fragment>
          ) : (
            <div className="bonus-off">No bonus for this designer</div>
          )}
        </div>
      </div>

      {weeks.map((w) => (
        <WeekBlock key={w.idx} week={w} products={products} taskTypes={taskTypes} funnels={funnels} typeOpts={typeOpts} kpi={kpi} readOnly={readOnly}
          data={entry && entry.weeks ? entry.weeks[w.idx] : null}
          dragInfo={dragInfo} setDragInfo={setDragInfo} onMoveRow={onMoveRow}
          onCell={onCell} onWD={onWD} onDeleteRow={onDeleteRow} onEditLink={onEditLink}
          onAddMeet={addMeet ? () => addMeet(w.idx) : null} />
      ))}
      </React.Fragment>
      )}
    </div>
  );
}

/* ---- rail item ---- */
function RailItem({ designer, product, active, onClick, metrics, kpi }) {
  const pinfo = POS[designer.position] || {};
  return (
    <button className={"rail-item" + (active ? " active" : "")} onClick={onClick}>
      <div className="avatar" style={{ background: "#eef0ff", color: "#4f46e5" }}>{initials(designer.name)}</div>
      <div style={{ minWidth: 0 }}>
        <div className="ri-name">{designer.name}</div>
        <div className="ri-pos">
          <span style={{ width: 6, height: 6, borderRadius: "50%", background: pinfo.dot }}></span>
          {pinfo.short || designer.position}{product ? " · " + product.name : ""}
        </div>
      </div>
      <div className="ri-metrics">
        <div className="ri-metric"><div className="v">{metrics.sp}</div><div className="k">SP</div></div>
        <div className="ri-metric"><div className="v">{metrics.task}</div><div className="k">Task</div></div>
      </div>
      <span className="ri-status" style={{ background: statusColor(metrics.sp, kpi || DEFAULT_KPI) }} title="vs KPI"></span>
    </button>
  );
}

const SearchIcon = () => <svg width="15" height="15" viewBox="0 0 16 16"><circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.5" fill="none"/><path d="M10.5 10.5L14 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>;

/* ---- KPI editor modal ---- */
function KpiModal({ kpi, designerName, scopeNote, onSave, onClose }) {
  const [min, setMin] = useState(kpi.min);
  const [growth, setGrowth] = useState(kpi.growth);
  const [fullDays, setFullDays] = useState(kpi.fullDays);
  const [threshold, setThreshold] = useState(kpi.threshold);
  const [offsets, setOffsets] = useState(() => (kpi.bonusOffsets && kpi.bonusOffsets.length === 5 ? kpi.bonusOffsets.slice() : DEFAULT_BONUS_OFFSETS.slice()));
  const [bonusOn, setBonusOn] = useState(kpi.bonusEnabled !== false);
  const k = { min: +min || 0, growth: +growth || 0, fullDays: +fullDays || 5, threshold: +threshold || 1, bonusOffsets: offsets.map((o) => +o || 0), bonusEnabled: bonusOn };
  const setOffset = (i, v) => setOffsets((a) => { const n = a.slice(); n[i] = +v || 0; return n; });

  // live preview rows
  const ex = [{ sp: 26, wd: 3 }, { sp: 15, wd: 2 }, { sp: 40, wd: 5 }];
  const tierLabel = { g: "green", y: "yellow", r: "red" };

  return (
    <div className="kpi-modal-backdrop" onClick={onClose}>
      <div className="kpi-modal" onClick={(e) => e.stopPropagation()}>
        <div className="kpi-modal-head">
          <div>
            <h3>KPI / week</h3>
            <p>{designerName} · weekly story-point targets &amp; how the week result is computed</p>
            {scopeNote && <p style={{ marginTop: 4, fontWeight: 700, color: "#5b4bcf" }}>{scopeNote}</p>}
          </div>
          <button className="kpi-x" onClick={onClose}>×</button>
        </div>

        <div className="kpi-modal-body">
          <div className="kpi-fields">
            <div className="kpi-field">
              <label>Min KPI <span>SP / week — must be reached</span></label>
              <input type="number" min="0" value={min} onChange={(e) => setMin(e.target.value)} />
            </div>
            <div className="kpi-field">
              <label>Growth KPI <span>SP / week — target to aim for</span></label>
              <input type="number" min="0" value={growth} onChange={(e) => setGrowth(e.target.value)} />
            </div>
          </div>

          <div className="kpi-logic">
            <div className="kpi-logic-h">Week-result logic</div>
            <p className="kpi-logic-desc">
              The result is normalised by the days a designer actually worked, then compared to the KPIs above.
            </p>
            <div className="kpi-fields">
              <div className="kpi-field">
                <label>Full week <span>working days</span></label>
                <input type="number" min="1" max="7" value={fullDays} onChange={(e) => setFullDays(e.target.value)} />
              </div>
              <div className="kpi-field">
                <label>Averaging threshold <span>min. worked days</span></label>
                <input type="number" min="1" max="7" value={threshold} onChange={(e) => setThreshold(e.target.value)} />
              </div>
            </div>
            <ul className="kpi-rules">
              <li><b>≥ {k.threshold} days worked:</b> project the daily average over the full week → <code>SP / days × {k.fullDays}</code></li>
              <li><b>&lt; {k.threshold} days worked:</b> actual SP + remaining days at the min rate → <code>SP + (rest × {k.min}/{k.fullDays})</code></li>
              <li>Colours appear only after the week has ended.</li>
            </ul>
          </div>

          <div className="kpi-logic">
            <div className="kpi-bonus-toggle">
              <div>
                <div className="kpi-logic-h" style={{ marginBottom: 2 }}>Monthly bonus system</div>
                <span className="kpi-toggle-sub">{bonusOn ? "Enabled for this designer" : "Disabled — this designer has no bonus"}</span>
              </div>
              <label className="bonus-switch lg" title="Enable / disable bonus">
                <input type="checkbox" checked={bonusOn} onChange={() => setBonusOn((v) => !v)} />
                <span className="bonus-switch-track"></span>
              </label>
            </div>
            {bonusOn && <React.Fragment>
            <p className="kpi-logic-desc">Bonus uses the <b>sum of theoretical weekly results</b> over the month. The base derives from <b>Min KPI</b> ({k.min} SP): month minimum = weeks × {k.min}. Each tier adds an offset on top.</p>
            <div className="kpi-bonus-edit">
              <div className="kpi-bonus-col">
                <div className="kpi-bonus-coltitle">Tier offsets (+SP over month min)</div>
                {BONUS_PCTS.map((p, i) => (
                  <div className="kpi-bonus-line" key={i}><span>{p}%</span><input type="number" value={offsets[i]} onChange={(e) => setOffset(i, e.target.value)} /></div>
                ))}
              </div>
              <div className="kpi-bonus-col">
                <div className="kpi-bonus-coltitle">Resulting thresholds</div>
                {[4, 5].map((wc) => {
                  const tbl = bonusTableFor(wc, k);
                  return (
                    <div className="kpi-derived" key={wc}>
                      <div className="kpi-derived-h">{wc}-week · min {tbl.min}</div>
                      <div className="kpi-derived-row">{tbl.tiers.map(([thr, p]) => <span key={p}>{p}%<b>{thr}</b></span>)}</div>
                    </div>
                  );
                })}
              </div>
            </div>
            </React.Fragment>}
          </div>

          <div className="kpi-preview">
            <div className="kpi-logic-h">Preview</div>
            {ex.map((e, i) => {
              const eff = effectiveWeekSP(e.sp, e.wd, k);
              const t = tierFromSP(eff, k);
              return (
                <div className="kpi-prev-row" key={i}>
                  <span>{e.sp} SP · {e.wd}d worked</span>
                  <span className="kpi-prev-arrow">→</span>
                  <span>≈{eff} SP/wk</span>
                  <span className={"kpi-prev-tier tier-" + t}>{tierLabel[t]}</span>
                </div>
              );
            })}
          </div>
        </div>

        <div className="kpi-modal-foot">
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={() => onSave(k)}>Save KPI</button>
        </div>
      </div>
    </div>
  );
}

/* ---- read-only bonus scale viewer (designer side) ---- */
function BonusInfoModal({ kpi, weekCount, designerName, onClose }) {
  const tbl = bonusTableFor(weekCount, kpi);
  return (
    <div className="kpi-modal-backdrop" onClick={onClose}>
      <div className="kpi-modal" style={{ width: 420 }} onClick={(e) => e.stopPropagation()}>
        <div className="kpi-modal-head">
          <div>
            <h3>Bonus scale</h3>
            <p>{designerName} · how monthly story points map to your bonus</p>
          </div>
          <button className="kpi-x" onClick={onClose}>×</button>
        </div>
        <div className="kpi-modal-body">
          {kpi.bonusEnabled === false ? (
            <div className="bonus-off" style={{ minHeight: 80 }}>No bonus is set for you</div>
          ) : (
            <div className="kpi-logic" style={{ background: "#fff" }}>
              <div className="binfo-row binfo-min"><span>Minimum ({weekCount}-week month)</span><b>{tbl.min} SP</b></div>
              {tbl.tiers.map(([thr, p]) => (
                <div className="binfo-row" key={p}><span>{p}% bonus</span><b>{thr} SP</b></div>
              ))}
              <div className="bonus-d-note" style={{ marginTop: 10 }}>Based on the sum of your theoretical weekly results over the month.</div>
            </div>
          )}
        </div>
        <div className="kpi-modal-foot">
          <button className="btn primary" onClick={onClose}>Got it</button>
        </div>
      </div>
    </div>
  );
}

/* ---- name/link editor (right-click) ---- */
/* ---- "Add meeting" (designer's own table): name + SP, not a calculation ---- */
function MeetModal({ onSave, onClose }) {
  const { useState } = React;
  const [name, setName] = useState("");
  const [sp, setSp] = useState("");
  const valid = name.trim() && parseFloat(sp) > 0;
  const submit = () => { if (valid) onSave(name.trim(), parseFloat(sp)); };
  return (
    <div className="kpi-modal-backdrop" onClick={onClose}>
      <div className="kpi-modal" style={{ width: 420 }} onClick={(e) => e.stopPropagation()}>
        <div className="kpi-modal-head">
          <div>
            <h3>Add meeting</h3>
            <p>Earns story points · not counted as a task</p>
          </div>
          <button className="kpi-x" onClick={onClose}>×</button>
        </div>
        <div className="kpi-modal-body">
          <div className="kpi-field">
            <label>Meeting name <span>shown in the table</span></label>
            <input value={name} placeholder="e.g. Weekly sync" autoFocus
              onChange={(e) => setName(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") onClose(); }} />
          </div>
          <div className="kpi-field">
            <label>Story points <span>SP for this meeting</span></label>
            <input type="number" min="0" step="0.1" value={sp} placeholder="e.g. 1"
              onChange={(e) => setSp(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") submit(); if (e.key === "Escape") onClose(); }} />
          </div>
        </div>
        <div className="kpi-modal-foot">
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" disabled={!valid} onClick={submit}>Add</button>
        </div>
      </div>
    </div>
  );
}

function LinkEditModal({ initLink, initUrl, onSave, onClose }) {
  const { useState } = React;
  const [num, setNum] = useState(initLink || "");
  const [url, setUrl] = useState(initUrl || "");
  return (
    <div className="kpi-modal-backdrop" onClick={onClose}>
      <div className="kpi-modal" style={{ width: 420 }} onClick={(e) => e.stopPropagation()}>
        <div className="kpi-modal-head">
          <div>
            <h3>Name / Link</h3>
            <p>Replace the task number or its link</p>
          </div>
          <button className="kpi-x" onClick={onClose}>×</button>
        </div>
        <div className="kpi-modal-body">
          <div className="kpi-field">
            <label>Number / name <span>shown in the table cell</span></label>
            <input value={num} placeholder="e.g. WF-1024" autoFocus
              onChange={(e) => setNum(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") onSave(num.trim(), url.trim()); if (e.key === "Escape") onClose(); }} />
          </div>
          <div className="kpi-field">
            <label>Link (URL) <span>opens in a new tab when clicked</span></label>
            <input value={url} placeholder="https://…"
              onChange={(e) => setUrl(e.target.value)}
              onKeyDown={(e) => { if (e.key === "Enter") onSave(num.trim(), url.trim()); if (e.key === "Escape") onClose(); }} />
          </div>
        </div>
        <div className="kpi-modal-foot">
          <button className="btn ghost" onClick={onClose}>Cancel</button>
          <button className="btn primary" onClick={() => onSave(num.trim(), url.trim())}>Save</button>
        </div>
      </div>
    </div>
  );
}

/* ---- main view ---- */
function DesignerTables({ products, designers, soloId, readOnly }) {
  const solo = !!soloId;
  // Tables content is shared for everyone via the server (no demo seed —
  // real rows come from calculations; wd/manual edits sync below).
  const [tables, setTables] = useState(() => loadTables() || {});
  // Designer edits to calculation rows: { deleted: [evId], moved: { evId: {y,m,w} } }
  const [adjustments, setAdjustments] = useState(() => {
    try { return JSON.parse(localStorage.getItem("nove8_tbl_adj_v1")) || { deleted: [], moved: {} }; }
    catch (e) { return { deleted: [], moved: {} }; }
  });
  const tblDirty = useRef(false);
  const markTablesDirty = () => {
    tblDirty.current = true;
    try { localStorage.setItem("nove8_tbl_edit_ts", String(Date.now())); } catch (e) {}
  };
  const [filter, setFilter] = useState("__all__");
  const [posFilter, setPosFilter] = useState("__all__");
  const [q, setQ] = useState("");
  const [selId, setSelId] = useState(null);
  // open on the current month/year (was hardcoded to June 2026)
  const [year, setYear] = useState(() => new Date().getFullYear());
  const [month, setMonth] = useState(() => new Date().getMonth());
  const [viewMode, setViewMode] = useState("weekly");
  const [taskTypes, setTaskTypes] = useState(loadTaskTypes);
  const [funnels, setFunnels] = useState(loadFunnels);
  const [typesList, setTypesList] = useState(loadTypesList);
  const [kpis, setKpis] = useState(loadKpis);
  const [kpiModal, setKpiModal] = useState(false);
  const [bonusInfo, setBonusInfo] = useState(false);
  const [linkEdit, setLinkEdit] = useState(null);
  const [meetModal, setMeetModal] = useState(null); // { wIdx } — "Add meeting" dialog

  // Persist locally (cache) and publish user edits to the shared store —
  // everyone: designers set their working days / move / delete their tasks,
  // managers and admins edit any table. Server-applied updates don't echo.
  useEffect(() => {
    try { localStorage.setItem(TKEY, JSON.stringify(tables)); } catch (e) {}
    try { localStorage.setItem("nove8_tbl_adj_v1", JSON.stringify(adjustments)); } catch (e) {}
    if (!tblDirty.current) return;
    tblDirty.current = false;
    fetch("/api/tables", {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ tables, adjustments }),
    }).catch(() => {});
  }, [tables, adjustments]);
  // Load the shared tables on mount (server wins) and keep them fresh.
  useEffect(() => {
    let alive = true;
    const pull = () => {
      fetch("/api/tables", { cache: "no-store" })
        .then((r) => (r.ok ? r.json() : null))
        .then((j) => {
          if (!alive || !j || !j.state) return;
          try {
            const ts = Number(localStorage.getItem("nove8_tbl_edit_ts") || 0);
            if (Date.now() - ts < 45000) return; // don't stomp an in-flight edit
          } catch (e) {}
          const st = j.state;
          if (st.tables && typeof st.tables === "object") {
            setTables((cur) => (JSON.stringify(cur) === JSON.stringify(st.tables) ? cur : st.tables));
          }
          if (st.adjustments && typeof st.adjustments === "object") {
            const norm = { deleted: st.adjustments.deleted || [], moved: st.adjustments.moved || {} };
            setAdjustments((cur) => (JSON.stringify(cur) === JSON.stringify(norm) ? cur : norm));
          }
        })
        .catch(() => {});
    };
    pull();
    const id = setInterval(pull, 30000);
    return () => { alive = false; clearInterval(id); };
  }, []);
  useEffect(() => {
    const reload = (e) => {
      if (!e || !e.key || e.key === TKEY) { const t = loadTables(); if (t) setTables(t); }
      if (!e || !e.key || e.key === FNKEY) setFunnels(loadFunnels());
      if (!e || !e.key || e.key === TTKEY) setTaskTypes(loadTaskTypes());
      if (!e || !e.key || e.key === TYKEY) setTypesList(loadTypesList());
    };
    const onPush = () => { const t = loadTables(); if (t) setTables(t); };
    const onCatalog = () => { setFunnels(loadFunnels()); setTaskTypes(loadTaskTypes()); setTypesList(loadTypesList()); };
    window.addEventListener("storage", reload);
    window.addEventListener("nove8-tables-push", onPush);
    window.addEventListener("nove8-catalog-changed", onCatalog);
    window.addEventListener("focus", reload);
    return () => { window.removeEventListener("storage", reload); window.removeEventListener("nove8-tables-push", onPush); window.removeEventListener("nove8-catalog-changed", onCatalog); window.removeEventListener("focus", reload); };
  }, []);
  useEffect(() => { try { localStorage.setItem(TTKEY, JSON.stringify(taskTypes)); } catch (e) {} }, [taskTypes]);
  // KPI lives on the server (shared_state key 'kpi'). Local edits by an
  // admin/manager publish the whole map; everyone else reads it. A dirty flag
  // distinguishes user edits from server-applied updates (no echo).
  const kpiDirty = useRef(false);
  const kpiLoaded = useRef(false);
  useEffect(() => {
    try { localStorage.setItem(KKEY, JSON.stringify(kpis)); } catch (e) {}
    if (!kpiDirty.current) return;
    kpiDirty.current = false;
    try { localStorage.setItem("nove8_kpi_edit_ts", String(Date.now())); } catch (e) {}
    let role = "";
    try { role = localStorage.getItem("nove8_shell_role") || ""; } catch (e) {}
    if (role !== "admin" && role !== "manager") return;
    fetch("/api/kpi", {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(kpis),
    }).catch(() => {});
  }, [kpis]);
  useEffect(() => {
    let alive = true;
    const pull = (initial) => {
      fetch("/api/kpi", { cache: "no-store" })
        .then((r) => (r.ok ? r.json() : null))
        .then((j) => {
          if (!alive) return;
          if (initial) kpiLoaded.current = true;
          if (!j || !j.state || typeof j.state !== "object") return;
          try {
            const ts = Number(localStorage.getItem("nove8_kpi_edit_ts") || 0);
            if (Date.now() - ts < 45000) return; // don't stomp an in-flight edit
          } catch (e) {}
          setKpis((cur) => (JSON.stringify(cur) === JSON.stringify(j.state) ? cur : j.state));
        })
        .catch(() => { if (initial) kpiLoaded.current = true; });
    };
    pull(true);
    const id = setInterval(() => pull(false), 30000);
    return () => { alive = false; clearInterval(id); };
  }, []);

  // Apply a KPI change depending on the VIEWED month:
  // - current (or future) month → becomes the base from that month onward;
  // - a past month → a month-only override; every other month stays as it was.
  const applyKpiChange = (dId, change) => {
    kpiDirty.current = true;
    setKpis((m) => {
      const now = new Date();
      const nowKey = ymKey(now.getFullYear(), now.getMonth());
      const viewKey = ymKey(year, month);
      const cur = m[dId];
      // No stored KPI = the designer lived on the defaults — record them as the
      // baseline so history keeps them after this change.
      let versions = !cur
        ? [{ from: "0000-00", ...DEFAULT_KPI }]
        : (Array.isArray(cur.versions) ? cur.versions.slice() : [{ from: "0000-00", ...cur }]);
      const overrides = (cur && cur.overrides) ? { ...cur.overrides } : {};
      // The monthly bonus flag is designer-wide (all months at once) — it goes
      // to the root, never into a version or a month override.
      let rootBonus = (cur && typeof cur.bonusEnabled === "boolean") ? cur.bonusEnabled : undefined;
      const rest = { ...change };
      if (Object.prototype.hasOwnProperty.call(rest, "bonusEnabled")) {
        rootBonus = !!rest.bonusEnabled;
        delete rest.bonusEnabled;
      }
      const entry = { versions, overrides };
      if (rootBonus !== undefined) entry.bonusEnabled = rootBonus;
      if (Object.keys(rest).length === 0) {
        // bonus-only toggle — nothing month-scoped changes
        return { ...m, [dId]: entry };
      }
      const effView = kpiFor(m, dId, year, month);
      if (viewKey < nowKey) {
        entry.overrides = { ...overrides, [viewKey]: { ...effView, ...rest } };
        return { ...m, [dId]: entry };
      }
      const next = { ...effView, ...rest, from: viewKey };
      entry.versions = versions.filter((v) => String(v.from) !== viewKey);
      entry.versions.push(next);
      entry.versions.sort((a, b) => String(a.from).localeCompare(String(b.from)));
      return { ...m, [dId]: entry };
    });
  };

  // Task-type edits from this panel go to the shared catalog too (the Catalog
  // tab already publishes; this panel used to be local-only).
  const publishTaskTypes = (next) => {
    try { localStorage.setItem("nove8_catalog_edit_ts", String(Date.now())); } catch (e) {}
    let role = "";
    try { role = localStorage.getItem("nove8_shell_role") || ""; } catch (e) {}
    if (role !== "admin" && role !== "manager") return;
    const read = (k) => { try { const r = localStorage.getItem(k); if (r) { const a = JSON.parse(r); if (Array.isArray(a)) return a; } } catch (e) {} return []; };
    fetch("/api/catalog", {
      method: "PUT",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ taskTypes: next, types: read("nove8_types_v1"), funnels: read(FNKEY) }),
    }).catch(() => {});
  };
  const addType = (label) => {
    const next = [...taskTypes, { id: ttId(), label, color: TYPE_PALETTE[taskTypes.length % TYPE_PALETTE.length] }];
    setTaskTypes(next);
    publishTaskTypes(next);
  };
  const removeType = (id) => {
    const next = taskTypes.filter((t) => t.id !== id);
    setTaskTypes(next);
    publishTaskTypes(next);
  };

  const list = useMemo(() => {
    return designers
      .filter((d) => filter === "__all__" ? true : filter === "__none__" ? d.productId === null : d.productId === filter)
      .filter((d) => posFilter === "__all__" ? true : d.position === posFilter)
      .filter((d) => d.name.toLowerCase().includes(q.trim().toLowerCase()))
      .slice()
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [designers, filter, posFilter, q]);

  // keep a valid selection
  useEffect(() => {
    if (solo) return;
    if (list.length === 0) { if (selId !== null) setSelId(null); return; }
    if (!list.find((d) => d.id === selId)) setSelId(list[0].id);
  }, [list, selId, solo]);

  const weeks = useMemo(() => weeksOfMonth(year, month), [year, month]);
  const curId = solo ? soloId : selId;
  const designer = designers.find((d) => d.id === curId) || null;
  // KPI effective for the VIEWED month — history keeps its period's values
  const kpi = kpiFor(kpis, curId, year, month);

  // ---- Overlay real calculations from the server (task_evaluations) onto the
  // tables, for EVERY designer: the manager's Designers Tables and the solo
  // "My Table" both show actual history. Matched by each account's linked
  // calculator names (handles). Weeks with real calculations display them
  // (server wins); weeks without stay as locally entered.
  const [serverEvents, setServerEvents] = useState(null);
  useEffect(() => {
    let alive = true;
    fetch("/api/task-evaluations", { cache: "no-store" })
      .then((r) => (r.ok ? r.json() : null))
      .then((j) => { if (alive && j && Array.isArray(j.events)) setServerEvents(j.events); })
      .catch(() => {});
    return () => { alive = false; };
  }, []);

  // Year options: the static list extended with any year that actually has
  // calculations on the server, so old history is always reachable.
  const yearOptions = useMemo(() => {
    const ys = new Set(YEARS);
    ys.add(new Date().getFullYear());
    (serverEvents || []).forEach((ev) => {
      const y = new Date(Number(ev.ts) || 0).getFullYear();
      if (y >= 2020 && y <= 2100) ys.add(y);
    });
    return [...ys].sort();
  }, [serverEvents]);

  // Provisional attribution for historical (context-less) calculations:
  // a Motion Designer's untyped rows count as Type=motion, Task type=creative.
  // Uses an existing "creative" task type if the catalog has one, otherwise a
  // synthetic column is appended below.
  const creativeTT = taskTypes.find((t) => /^creatives?$/i.test(String(t.label || "").trim()));
  const CREATIVE_ID = creativeTT ? creativeTT.id : "__creative";

  const overlayResult = useMemo(() => {
    if (!serverEvents || !serverEvents.length) return { tables, usedCreative: false };
    let usedCreative = false;
    // designer adjustments to calculation rows (shared via /api/tables)
    const adjDeleted = new Set((adjustments && adjustments.deleted) || []);
    const adjMoved = (adjustments && adjustments.moved) || {};
    // index events by the designer name they were saved under
    const byName = new Map();
    for (const ev of serverEvents) {
      const k = String(ev.designer || "").trim().toLowerCase();
      if (!k) continue;
      if (!byName.has(k)) byName.set(k, []);
      byName.get(k).push(ev);
    }
    const next = { ...tables };
    for (const d of designers) {
      const handles = ((d.handles && d.handles.length) ? d.handles : [d.name])
        .map((h) => String(h).trim().toLowerCase());
      const mine = handles.flatMap((h) => byName.get(h) || []);
      if (!mine.length) continue;
      // place each event into its work week (weeks belong to a month by Wednesday;
      // ranges are disjoint, so checking the date's month ± 1 finds the owner)
      const byMonth = new Map(); // "y-m" -> { weekIdx: [events] }
      for (const ev of mine) {
        if (ev.id && adjDeleted.has(ev.id)) continue; // deleted by the designer
        const mv = ev.id ? adjMoved[ev.id] : null;
        let placed = (mv && mv.y != null) ? { y: mv.y, m: mv.m, w: mv.w } : null; // moved to another week
        if (!placed) {
          const ts = Number(ev.ts) || 0;
          const dt = new Date(ts);
          for (const off of [0, -1, 1]) {
            const ref = new Date(dt.getFullYear(), dt.getMonth() + off, 1);
            const y = ref.getFullYear(), m = ref.getMonth();
            for (const wk of weeksOfMonth(y, m)) {
              const start = wk.end - 4 * 86400000 - 86399999; // Monday 00:00
              const endSun = wk.end + 2 * 86400000;           // include weekend
              if (ts >= start && ts <= endSun) { placed = { y, m, w: wk.idx }; break; }
            }
            if (placed) break;
          }
        }
        if (!placed) continue;
        const key = placed.y + "-" + placed.m;
        if (!byMonth.has(key)) byMonth.set(key, {});
        const wks = byMonth.get(key);
        (wks[placed.w] = wks[placed.w] || []).push(ev);
      }
      const appId = d.productId || (products[0] && products[0].id) || "";
      const isMotion = d.position === "Motion Designer";
      for (const [key, wks] of byMonth) {
        const parts = key.split("-");
        const k = mkey(d.id, +parts[0], +parts[1]);
        const entry = { ...(next[k] || {}) };
        const weeksObj = { ...(entry.weeks || {}) };
        Object.keys(wks).forEach((wIdx) => {
          const prev = weeksObj[wIdx] || {};
          // manually entered rows (incl. meetings) survive the overlay —
          // appended after the calculation rows
          const meetRows = (prev.rows || []).filter((r) => r && !r.evId && (r.app || r.type || r.task || r.link || r.sp || r.funnel));
          weeksObj[wIdx] = {
            ...prev,
            rows: wks[wIdx].slice().sort((a, b) => (a.ts || 0) - (b.ts || 0)).map((ev) => {
              // newer calculations carry the calc card's context in a __meta items entry
              const meta = (Array.isArray(ev.items) && ev.items.find((it) => it && it.__meta === true)) || {};
              let task = meta.taskTypeId || "";
              let type = meta.typeId || "";
              // provisional rule: a Motion Designer's untyped history counts as
              // Type=motion, Task type=creative
              if (isMotion && !type) type = "motion";
              if (isMotion && !task) { task = CREATIVE_ID; if (!creativeTT) usedCreative = true; }
              return {
                evId: ev.id || "", // ties the row to its calculation for delete/move
                app: meta.productId || appId,
                type, task,
                funnel: meta.funnelId || "",
                link: meta.taskNumber || ev.taskLink || "",
                linkUrl: ev.taskLink || "",
                sp: String(ev.result != null ? ev.result : ""),
              };
            }).concat(meetRows),
          };
        });
        entry.weeks = weeksObj; next[k] = entry;
      }
    }
    return { tables: next, usedCreative };
  }, [serverEvents, tables, designers, products, taskTypes, adjustments]);

  const displayTables = overlayResult.tables;
  // If the catalog has no "creative" task type but the provisional rule used
  // one, append a synthetic entry so the weekly chips and matrix column render.
  // Layout preference: creative and SMM swap places, so creative (the bulk of
  // historical volume) sits where SMM was instead of at the far end.
  const effTaskTypes = useMemo(() => {
    const list = (overlayResult.usedCreative && !creativeTT)
      ? [...taskTypes, { id: "__creative", label: "creative", color: "#6c79f6" }]
      : taskTypes.slice();
    const si = list.findIndex((t) => /^smm$/i.test(String(t.label || "").trim()));
    const ci = list.findIndex((t) => t.id === "__creative" || /^creatives?$/i.test(String(t.label || "").trim()));
    if (si >= 0 && ci >= 0 && si !== ci) {
      const tmp = list[si]; list[si] = list[ci]; list[ci] = tmp;
    }
    return list;
  }, [overlayResult.usedCreative, creativeTT, taskTypes]);

  const setCell = (wIdx, rowIdx, field, value) => {
    if (!designer) return;
    markTablesDirty();
    const disp = displayedRow(wIdx, rowIdx);
    if (disp && disp.evId) return; // calculation rows are edited via the calculator
    setTables((t) => {
      const k = mkey(designer.id, year, month);
      const next = { ...t };
      const entry = { ...(next[k] || {}) };
      const wks = { ...(entry.weeks || {}) };
      const wk = { ...(wks[wIdx] || {}) };
      const rows = (wk.rows || []).slice();
      // In weeks that carry calculation rows the display index is shifted, so
      // resolve the stored row by identity; otherwise use the plain index.
      let target = disp ? rows.indexOf(disp) : -1;
      if (target < 0) {
        const dispWk = displayTables[k] && displayTables[k].weeks ? displayTables[k].weeks[wIdx] : null;
        const hasCalc = !!(dispWk && dispWk.rows && dispWk.rows.some((r) => r && r.evId));
        if (hasCalc) { rows.push({}); target = rows.length - 1; }
        else { while (rows.length <= rowIdx) rows.push({}); target = rowIdx; }
      }
      rows[target] = { ...rows[target], [field]: value };
      wk.rows = rows; wks[wIdx] = wk; entry.weeks = wks; next[k] = entry;
      return next;
    });
  };
  const setWD = (wIdx, value) => {
    if (!designer) return;
    markTablesDirty();
    setTables((t) => {
      const k = mkey(designer.id, year, month);
      const next = { ...t };
      const entry = { ...(next[k] || {}) };
      const wks = { ...(entry.weeks || {}) };
      const wk = { ...(wks[wIdx] || {}) };
      wk.wd = value === "" ? "" : Math.max(0, Math.min(31, parseInt(value, 10) || 0));
      wks[wIdx] = wk; entry.weeks = wks; next[k] = entry;
      return next;
    });
  };
  // Resolve the row the user actually sees (overlay rows carry an evId).
  const displayedRow = (wIdx, rowIdx) => {
    const k = mkey(designer.id, year, month);
    const entry = displayTables[k];
    const wk = entry && entry.weeks ? entry.weeks[wIdx] : null;
    return (wk && wk.rows && wk.rows[rowIdx]) || null;
  };

  const deleteRow = (wIdx, rowIdx) => {
    if (!designer) return;
    markTablesDirty();
    const row = displayedRow(wIdx, rowIdx);
    if (row && row.evId) {
      // calculation row — record the deletion as a shared adjustment
      setAdjustments((a) => ({ ...a, deleted: [...(a.deleted || []), row.evId] }));
      return;
    }
    setTables((t) => {
      const k = mkey(designer.id, year, month);
      const next = { ...t };
      const entry = { ...(next[k] || {}) };
      const wks = { ...(entry.weeks || {}) };
      const wk = { ...(wks[wIdx] || {}) };
      const rows = (wk.rows || []).slice();
      // resolve by identity (display index shifts in weeks with calc rows);
      // fall back to the plain index for plain weeks
      let target = row ? rows.indexOf(row) : -1;
      if (target < 0 && row && row.mid) target = rows.findIndex((r) => r && r.mid === row.mid);
      if (target < 0) target = rowIdx;
      rows.splice(target, 1);
      wk.rows = rows; wks[wIdx] = wk; entry.weeks = wks; next[k] = entry;
      return next;
    });
  };
  const moveRow = (fromW, fromR, toW) => {
    if (!designer || fromW === toW) return;
    markTablesDirty();
    const row = displayedRow(fromW, fromR);
    if (row && row.evId) {
      // calculation row — record the new week as a shared adjustment
      setAdjustments((a) => ({ ...a, moved: { ...(a.moved || {}), [row.evId]: { y: year, m: month, w: toW } } }));
      return;
    }
    setTables((t) => {
      const k = mkey(designer.id, year, month);
      const next = { ...t };
      const entry = { ...(next[k] || {}) };
      const wks = { ...(entry.weeks || {}) };
      const fromWk = { ...(wks[fromW] || {}) };
      const fromRows = (fromWk.rows || []).slice();
      // resolve by identity first (display index shifts in weeks with calc rows)
      let idx = row ? fromRows.indexOf(row) : -1;
      if (idx < 0 && row && row.mid) idx = fromRows.findIndex((r) => r && r.mid === row.mid);
      if (idx < 0) idx = fromR;
      if (idx < 0 || idx >= fromRows.length) return t;
      const moved = fromRows[idx];
      if (!moved || !(moved.app || moved.type || moved.task || moved.link || moved.sp)) return t;
      fromRows.splice(idx, 1);
      fromWk.rows = fromRows; wks[fromW] = fromWk;
      const toWk = { ...(wks[toW] || {}) };
      const toRows = (toWk.rows || []).slice();
      toRows.push(moved);
      toWk.rows = toRows; wks[toW] = toWk;
      entry.weeks = wks; next[k] = entry;
      return next;
    });
  };

  // Meetings: SP-earning rows a designer adds to their own table (not
  // calculations). Stored as manual rows flagged meet:true with a unique mid,
  // so they sync through /api/tables and survive the calculations overlay.
  const addMeeting = (wIdx, name, sp) => {
    if (!designer) return;
    markTablesDirty();
    const meetingsTT = taskTypes.find((t) => /meet/i.test(String(t.label || "")));
    const row = {
      meet: true,
      mid: "meet_" + Date.now().toString(36) + Math.floor(Math.random() * 1e6).toString(36),
      app: designer.productId || (products[0] && products[0].id) || "",
      type: "",
      task: meetingsTT ? meetingsTT.id : "",
      link: name,
      sp: String(sp),
    };
    setTables((t) => {
      const k = mkey(designer.id, year, month);
      const next = { ...t };
      const entry = { ...(next[k] || {}) };
      const wks = { ...(entry.weeks || {}) };
      const wk = { ...(wks[wIdx] || {}) };
      wk.rows = [...(wk.rows || []), row];
      wks[wIdx] = wk; entry.weeks = wks; next[k] = entry;
      return next;
    });
  };

  const filters = [{ id: "__all__", name: "All", color: "#98a0ac" }, ...products, { id: "__none__", name: "Unassigned", color: "#c2c8d0" }];

  return (
    <div className="dt">
      <div className="dt-toolbar">
        <h1>{solo ? "My Table" : "Designer Tables"}</h1>
        {!solo && (
          <div className="prodfilter">
            {filters.map((f) => (
              <button key={f.id} className={filter === f.id ? "on" : ""} onClick={() => setFilter(f.id)}>
                {f.id !== "__all__" && <span className="dot" style={{ background: f.color }}></span>}{f.name}
              </button>
            ))}
          </div>
        )}
        <div className="dt-spacer"></div>
        <div className="dt-monthsel">
          <select value={month} onChange={(e) => setMonth(+e.target.value)}>
            {MONTHS.map((mn, i) => <option key={i} value={i}>{mn}</option>)}
          </select>
          <select value={year} onChange={(e) => setYear(+e.target.value)}>
            {yearOptions.map((y) => <option key={y} value={y}>{y}</option>)}
          </select>
        </div>
        {!solo && (
          <div className="dt-search">
            <SearchIcon />
            <input placeholder="Search designer…" value={q} onChange={(e) => setQ(e.target.value)} />
          </div>
        )}
      </div>

      <div className="dt-body">
        {!solo && (
        <div className="dt-rail">
          <div className="rail-head"><span>Designers</span><span>{list.length}</span></div>
          <div className="rail-filters">
            <button className={"posfilter-chip" + (posFilter === "__all__" ? " on" : "")} onClick={() => setPosFilter("__all__")}>All roles</button>
            {Object.keys(POS).map((p) => (
              <button key={p} className={"posfilter-chip" + (posFilter === p ? " on" : "")} onClick={() => setPosFilter(p)}>
                <span className="pdot" style={{ background: POS[p].dot }}></span>{POS[p].short}
              </button>
            ))}
          </div>
          <div className="rail-list">
            {list.map((d) => (
              <RailItem key={d.id} designer={d} product={products.find((p) => p.id === d.productId)}
                active={d.id === selId} onClick={() => setSelId(d.id)}
                metrics={monthAgg(displayTables, d.id, year, month, weeks)} kpi={kpiFor(kpis, d.id, year, month)} />
            ))}
            {list.length === 0 && <div className="rail-empty">No designers match this filter.</div>}
          </div>
        </div>
        )}

        <Detail designer={designer} products={products} tables={displayTables}
          year={year} month={month}
          viewMode={viewMode} setView={setViewMode}
          taskTypes={effTaskTypes} funnels={funnels} typeOpts={typesList.map((t) => ({ v: t.id, label: t.label, color: t.color }))} onAddType={addType} onRemoveType={removeType}
          kpi={kpi} kpiOf={(m) => kpiFor(kpis, curId, year, m)} readOnly={readOnly}
          addMeet={solo ? (wIdx) => setMeetModal({ wIdx }) : null}
          onEditKpi={() => { if (readOnly) setBonusInfo(true); else if (designer) setKpiModal(true); }}
          onSetKpi={(patch) => !readOnly && designer && applyKpiChange(designer.id, patch)}
          onCell={setCell} onWD={setWD} onDeleteRow={deleteRow} onMoveRow={moveRow}
          onEditLink={(wIdx, rowIdx, row) => setLinkEdit({ wIdx, rowIdx, link: row.link || "", linkUrl: row.linkUrl || "" })} />
      </div>
      {bonusInfo && designer && (
        <BonusInfoModal kpi={kpi} weekCount={weeks.length} designerName={designer.name} onClose={() => setBonusInfo(false)} />
      )}
      {meetModal && (
        <MeetModal onClose={() => setMeetModal(null)}
          onSave={(name, sp) => { addMeeting(meetModal.wIdx, name, sp); setMeetModal(null); }} />
      )}
      {linkEdit && (
        <LinkEditModal initLink={linkEdit.link} initUrl={linkEdit.linkUrl}
          onClose={() => setLinkEdit(null)}
          onSave={(num, url) => {
            setCell(linkEdit.wIdx, linkEdit.rowIdx, "link", num);
            setCell(linkEdit.wIdx, linkEdit.rowIdx, "linkUrl", url);
            setLinkEdit(null);
          }} />
      )}
      {kpiModal && designer && (
        <KpiModal kpi={kpi} designerName={designer.name}
          scopeNote={(() => {
            const now = new Date();
            const nowKey = ymKey(now.getFullYear(), now.getMonth());
            return ymKey(year, month) < nowKey
              ? `Applies to ${MONTHS[month]} ${year} only (editing a past month)`
              : `Applies from ${MONTHS[month]} ${year} onward`;
          })()}
          onClose={() => setKpiModal(false)}
          onSave={(k) => { applyKpiChange(designer.id, k); setKpiModal(false); }} />
      )}
    </div>
  );
}

window.DesignerTables = DesignerTables;
})();
