// projects.jsx — Projects feature components, loaded BEFORE app.jsx
// Exposes everything via window.Projects so app.jsx can reach it.
//
// Babel-standalone: no imports/exports. Use React globals.

(function () {
  const { useState, useEffect, useRef, useCallback } = React;

  // ── Color presets ─────────────────────────────────────────────────────
  // Ported from new_tab_UI/new-screens-detail.jsx (lines 766-773).
  // PRESET_CATEGORY_COLORS must match app.jsx COLOR_OPTIONS (line 2054) exactly.
  const PRESET_PROJECT_COLORS = [
    '#2a6fdb', '#7c3aed', '#b54a8f', '#3a8a6b',
    '#d96a3a', '#c0a51f', '#7a7a7a',
  ];
  const PRESET_CATEGORY_COLORS = [
    '#0066cc', '#d96a3a', '#2a6fdb', '#b54a8f', '#c0a51f',
    '#3a8a6b', '#cc4747', '#7c3aed', '#7a7a7a',
  ];

  // ── HSV ↔ RGB ↔ Hex helpers ───────────────────────────────────────────
  function _hexToRgb(hex) {
    let h = String(hex || '').replace('#', '');
    // No 3-char shorthand support per README spec — but tolerate it from
    // an external `value` prop by expanding it (don't reject silently).
    if (h.length === 3) h = h.split('').map((c) => c + c).join('');
    return {
      r: parseInt(h.slice(0, 2), 16) || 0,
      g: parseInt(h.slice(2, 4), 16) || 0,
      b: parseInt(h.slice(4, 6), 16) || 0,
    };
  }
  function _rgbToHex({ r, g, b }) {
    const h = (n) => Math.round(Math.max(0, Math.min(255, n))).toString(16).padStart(2, '0');
    return '#' + h(r) + h(g) + h(b);
  }
  function _rgbToHsv({ r, g, b }) {
    r /= 255; g /= 255; b /= 255;
    const max = Math.max(r, g, b), min = Math.min(r, g, b);
    const d = max - min;
    let h = 0;
    if (d !== 0) {
      if (max === r) h = ((g - b) / d) % 6;
      else if (max === g) h = (b - r) / d + 2;
      else h = (r - g) / d + 4;
    }
    h = (h * 60 + 360) % 360;
    const s = max === 0 ? 0 : (d / max) * 100;
    const v = max * 100;
    return { h, s, v };
  }
  function _hsvToRgb({ h, s, v }) {
    s /= 100; v /= 100;
    const c = v * s;
    const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
    const m = v - c;
    let r, g, b;
    if (h < 60) [r, g, b] = [c, x, 0];
    else if (h < 120) [r, g, b] = [x, c, 0];
    else if (h < 180) [r, g, b] = [0, c, x];
    else if (h < 240) [r, g, b] = [0, x, c];
    else if (h < 300) [r, g, b] = [x, 0, c];
    else [r, g, b] = [c, 0, x];
    return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
  }

  // ── Recent swatches persistence ───────────────────────────────────────
  const RECENT_CAP = 6;
  function _loadRecent(key) {
    try {
      const raw = window.localStorage && window.localStorage.getItem(key);
      if (!raw) return [];
      const arr = JSON.parse(raw);
      if (!Array.isArray(arr)) return [];
      return arr
        .filter((c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c))
        .slice(0, RECENT_CAP);
    } catch (e) {
      return [];
    }
  }
  function _saveRecent(key, arr) {
    try {
      window.localStorage && window.localStorage.setItem(key, JSON.stringify(arr));
    } catch (e) {
      /* ignore quota / disabled storage */
    }
  }
  function _pushRecent(cur, hex) {
    const norm = String(hex || '').toLowerCase();
    if (!/^#[0-9a-f]{6}$/.test(norm)) return cur;
    const filtered = cur.filter((c) => c.toLowerCase() !== norm);
    return [norm, ...filtered].slice(0, RECENT_CAP);
  }

  // ── Pointer drag helper ───────────────────────────────────────────────
  // Attaches pointermove/pointerup to document; detaches on pointerup.
  // Returns the pointerdown handler.
  function _makeDragHandler(elRef, onUpdate) {
    return (e) => {
      if (!elRef.current) return;
      e.preventDefault();
      const update = (ev) => {
        const rect = elRef.current.getBoundingClientRect();
        if (!rect.width || !rect.height) return;
        const x = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));
        const y = Math.max(0, Math.min(1, (ev.clientY - rect.top) / rect.height));
        onUpdate(x, y);
      };
      update(e);
      const move = (ev) => update(ev);
      const up = () => {
        document.removeEventListener('pointermove', move);
        document.removeEventListener('pointerup', up);
        document.removeEventListener('pointercancel', up);
      };
      document.addEventListener('pointermove', move);
      document.addEventListener('pointerup', up);
      document.addEventListener('pointercancel', up);
    };
  }

  // ── ColorPicker component ─────────────────────────────────────────────
  function ColorPicker({
    value,
    onChange,
    presets,
    initialOpen = false,
    recentKey = 'ledger.colorpicker.recent.v1',
  }) {
    const presetList = Array.isArray(presets) && presets.length ? presets : PRESET_PROJECT_COLORS;

    const [open, setOpen] = useState(initialOpen);
    const [hsv, setHsv] = useState(() => _rgbToHsv(_hexToRgb(value)));
    const [recent, setRecent] = useState(() => _loadRecent(recentKey));
    const padRef = useRef(null);
    const hueRef = useRef(null);

    // Derived
    const rgb = _hsvToRgb(hsv);
    const hex = _rgbToHex(rgb);

    // Sync external value -> internal hsv when prop changes externally.
    useEffect(() => {
      const lower = String(value || '').toLowerCase();
      if (/^#[0-9a-f]{6}$/.test(lower) && lower !== hex.toLowerCase()) {
        setHsv(_rgbToHsv(_hexToRgb(value)));
      }
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [value]);

    const commit = useCallback(
      (newHex) => {
        if (typeof onChange === 'function') onChange(newHex);
      },
      [onChange]
    );

    // Push to recents (used when user "picks" a color: preset, recent, or
    // pressing 完成 on the custom panel). Not called on every HSV-drag tick.
    const recordRecent = useCallback(
      (newHex) => {
        setRecent((cur) => {
          const next = _pushRecent(cur, newHex);
          if (next !== cur) _saveRecent(recentKey, next);
          return next;
        });
      },
      [recentKey]
    );

    const startPadDrag = _makeDragHandler(padRef, (x, y) => {
      setHsv((cur) => {
        const next = { ...cur, s: x * 100, v: (1 - y) * 100 };
        commit(_rgbToHex(_hsvToRgb(next)));
        return next;
      });
    });
    const startHueDrag = _makeDragHandler(hueRef, (x) => {
      setHsv((cur) => {
        const next = { ...cur, h: x * 360 };
        commit(_rgbToHex(_hsvToRgb(next)));
        return next;
      });
    });

    const setHexInput = (s) => {
      const cleaned = '#' + s.replace(/[^0-9a-f]/gi, '').slice(0, 6);
      if (cleaned.length === 7) {
        setHsv(_rgbToHsv(_hexToRgb(cleaned)));
        commit(cleaned);
      }
    };
    const setRgbComponent = (comp, val) => {
      const n = Math.max(0, Math.min(255, parseInt(val, 10) || 0));
      const newRgb = { ...rgb, [comp]: n };
      setHsv(_rgbToHsv(newRgb));
      commit(_rgbToHex(newRgb));
    };
    const pickPreset = (col) => {
      const norm = String(col || '').toLowerCase();
      setHsv(_rgbToHsv(_hexToRgb(norm)));
      commit(norm);
      recordRecent(norm);
    };
    const finishCustom = () => {
      recordRecent(hex);
      setOpen(false);
    };

    const isPresetMatch = presetList.some((p) => p.toLowerCase() === hex.toLowerCase());
    const hueDeg = Math.round(hsv.h);
    const emptySlots = Math.max(0, RECENT_CAP - recent.length);

    return (
      <div className="cp-section">
        <div className="cp-section-label">
          <span>預設色</span>
          <span className="cp-section-label-r">已選 {hex.toUpperCase()}</span>
        </div>
        <div className="cp-presets color-swatches">
          {presetList.map((col) => (
            <button
              key={col}
              type="button"
              className={
                'color-swatch' +
                (col.toLowerCase() === hex.toLowerCase() ? ' is-on' : '')
              }
              style={{ ['--swatch']: col }}
              onClick={() => pickPreset(col)}
              title={col.toUpperCase()}
            />
          ))}
          <button
            type="button"
            className={
              'cp-trigger cp-add-trigger' +
              (open ? ' is-on' : '') +
              (!isPresetMatch ? ' has-color' : '')
            }
            onClick={() => setOpen(!open)}
            title="自訂顏色"
          >
            {!isPresetMatch && (
              <span className="cp-trigger-preview" style={{ ['--swatch']: hex }} />
            )}
            {isPresetMatch && <span className="cp-trigger-glyph">＋</span>}
          </button>
        </div>

        <React.Fragment>
          <div className="cp-section-label" style={{ marginTop: 4 }}>
            <span>最近使用</span>
          </div>
          <div className="cp-recent">
            {recent.map((c) => (
              <button
                key={c}
                type="button"
                className={
                  'cp-recent-swatch' +
                  (c.toLowerCase() === hex.toLowerCase() ? ' is-on' : '')
                }
                style={{ ['--swatch']: c }}
                onClick={() => pickPreset(c)}
                title={c.toUpperCase()}
              />
            ))}
            {Array.from({ length: emptySlots }).map((_, i) => (
              <span key={'e' + i} className="cp-recent-empty" />
            ))}
          </div>
        </React.Fragment>

        {open && (
          <div className="cp-custom-panel">
            <div
              ref={padRef}
              className="cp-pad"
              style={{ ['--cp-hue']: hueDeg }}
              onPointerDown={startPadDrag}
            >
              <div
                className="cp-pad-cursor cp-pad-thumb"
                style={{
                  left: hsv.s + '%',
                  top: (100 - hsv.v) + '%',
                  background: hex,
                }}
              />
            </div>
            <div
              ref={hueRef}
              className="cp-hue"
              onPointerDown={startHueDrag}
              style={{ ['--cp-hue']: hueDeg }}
            >
              <div
                className="cp-hue-cursor cp-hue-thumb"
                style={{ left: (hsv.h / 360 * 100) + '%' }}
              />
            </div>
            <div className="cp-inputs">
              <div className="cp-preview" style={{ ['--cp-current']: hex }} />
              <div className="cp-input cp-hex">
                <span className="cp-input-lbl">HEX</span>
                <input
                  value={hex.replace('#', '').toUpperCase()}
                  onChange={(e) => setHexInput(e.target.value)}
                  spellCheck={false}
                  maxLength={6}
                />
              </div>
              {['r', 'g', 'b'].map((comp) => (
                <div key={comp} className="cp-input cp-rgb">
                  <span className="cp-input-lbl">{comp.toUpperCase()}</span>
                  <input
                    type="number"
                    min="0"
                    max="255"
                    value={Math.round(rgb[comp])}
                    onChange={(e) => setRgbComponent(comp, e.target.value)}
                  />
                </div>
              ))}
            </div>
            <div className="cp-foot">
              <span>拖曳 ◯ 選飽和度／亮度 · 下方條選色相</span>
              <button type="button" onClick={finishCustom}>完成</button>
            </div>
          </div>
        )}
      </div>
    );
  }

  // ── Number formatting (NT$ #,###) ─────────────────────────────────────
  function _fmt(n) {
    const v = Number(n) || 0;
    return v.toLocaleString('en-US');
  }

  function _safeBudget(s) {
    if (s == null) return 0;
    const n = parseFloat(s);
    return Number.isFinite(n) ? n : 0;
  }

  function _fmtMD(d) {
    if (!d) return '';
    const dt = d instanceof Date ? d : new Date(d);
    return (dt.getMonth() + 1) + '/' + dt.getDate();
  }

  // ── Sparkline (12-month series) ───────────────────────────────────────
  function ProjSpark({ data }) {
    const W = 100, H = 28;
    const arr = (data && data.length) ? data : [0];
    const max = Math.max(1, ...arr);
    const stepX = W / Math.max(1, arr.length - 1);
    const pts = arr.map((v, i) => ({
      x: (i * stepX).toFixed(1),
      y: (H - (v / max) * (H - 4) - 2).toFixed(1),
    }));
    const path = pts.map((p, i) => (i ? 'L' : 'M') + p.x + ' ' + p.y).join(' ');
    const area = path + ' L ' + W + ' ' + H + ' L 0 ' + H + ' Z';
    const last = pts[pts.length - 1];
    return (
      <svg className="proj-spark" viewBox={'0 0 ' + W + ' ' + H} preserveAspectRatio="none">
        <path d={area} className="spark-area" />
        <path d={path} className="spark-line" />
        <circle cx={last.x} cy={last.y} r="1.8" className="spark-end" />
      </svg>
    );
  }

  // ── ProjectCard ───────────────────────────────────────────────────────
  function ProjectCard({ project: p, onOpen, onEdit }) {
    const budget = _safeBudget(p.target_budget);
    const spent = Number(p.spent_total) || 0;
    const noBudget = budget <= 0;
    const overBudget = !noBudget && spent > budget;
    const pct = !noBudget ? Math.min(100, Math.round((spent / budget) * 100)) : 0;
    const overPct = !noBudget ? Math.round((spent / budget) * 100) : 0;
    const archived = !!p.archived_at;
    return (
      <div className={'proj-tile' + (archived ? ' is-archived' : '')}
           style={p.color ? { ['--proj-color']: p.color } : null}>
        <div className="proj-head">
          <div className="proj-head-l" onClick={onOpen} style={{ cursor: 'pointer' }}>
            <span className="proj-name">{p.name}</span>
            <div className="proj-meta-row">
              <span className="proj-color-dot" style={p.color ? { background: p.color } : null} />
              <span>{p.records_count || 0} 筆紀錄</span>
              {p.last_tx_at && (
                <>
                  <span className="dot">·</span>
                  <span>最近 {_fmtMD(p.last_tx_at)}</span>
                </>
              )}
              {archived && <><span className="dot">·</span><span>已封存</span></>}
            </div>
          </div>
          <button
            className="icon-dots"
            aria-label="編輯"
            onClick={(e) => { e.stopPropagation(); onEdit(); }}
          >
            <svg width="16" height="4" viewBox="0 0 16 4">
              <circle cx="2" cy="2" r="1.5" fill="currentColor" />
              <circle cx="8" cy="2" r="1.5" fill="currentColor" />
              <circle cx="14" cy="2" r="1.5" fill="currentColor" />
            </svg>
          </button>
        </div>
        <div className="proj-amt-block">
          <div className="proj-amt">
            <span className="mark">NT$</span>
            <span className="num">{_fmt(spent)}</span>
            {budget > 0 && (
              <span className="over">/ <b>{_fmt(budget)}</b> 預算</span>
            )}
            {noBudget && <span className="over">未設預算</span>}
          </div>
          <div className={'proj-bar ' + (overBudget ? 'is-over' : '') + (noBudget ? ' no-budget' : '')}>
            <i style={{ width: (noBudget ? 100 : pct) + '%' }} />
          </div>
          <div className="proj-bar-legend">
            <span>{noBudget ? '無上限追蹤' : <>已用 <b>{overPct}%</b></>}</span>
            <span>
              {budget > 0 && !overBudget && <>剩 NT$ <b>{_fmt(budget - spent)}</b></>}
              {overBudget && <>超支 NT$ <b style={{ color: '#c0392b' }}>{_fmt(spent - budget)}</b></>}
            </span>
          </div>
        </div>
        <div className="proj-foot">
          <ProjSpark data={p.monthly_spend_12 || []} />
          <div className="proj-foot-r" onClick={onOpen} style={{ cursor: 'pointer' }}>看明細 →</div>
        </div>
      </div>
    );
  }

  // ── ProjectsPage (list view) ──────────────────────────────────────────
  function ProjectsPage({ onOpenProject, onCreateProject, onEditProject, refreshKey }) {
    const [tab, setTab] = useState('active');
    const [projects, setProjects] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      let cancelled = false;
      setLoading(true);
      window.LEDGER_API.apiListProjects(true)
        .then((rows) => { if (!cancelled) { setProjects(rows); setLoading(false); } })
        .catch((err) => { if (!cancelled) { setError(err); setLoading(false); } });
      return () => { cancelled = true; };
    }, [refreshKey]);

    const active = projects.filter((p) => !p.archived_at);
    const archived = projects.filter((p) => p.archived_at);
    const shown = tab === 'active' ? active : archived;

    return (
      <div className="projects-page">
        <div className="page">
          <div className="ph">
            <div className="ph-l">
              <span className="ph-eyebrow">PROJECTS · 時間性容器</span>
              <h2 className="ph-title">專案</h2>
              <span className="ph-sub">把分散在多分類的支出按主題集中追蹤 · 一筆記帳可同時屬於多個專案</span>
            </div>
            <div className="ph-r">
              <button className="btn-primary" onClick={onCreateProject}>
                <span className="plus">＋</span>新增專案
              </button>
            </div>
          </div>

          <div className="projects-toolbar">
            <div className="seg-toggle" role="tablist">
              <button
                className={tab === 'active' ? 'is-on' : ''}
                onClick={() => setTab('active')}
              >進行中<span className="seg-count">{active.length}</span></button>
              <button
                className={tab === 'archived' ? 'is-on' : ''}
                onClick={() => setTab('archived')}
              >已封存<span className="seg-count">{archived.length}</span></button>
            </div>
            <div style={{ fontSize: 12, color: 'var(--ink-48)' }}>
              點卡片查看單一專案明細 · 進度條使用 lifetime budget
            </div>
          </div>

          {loading && <div className="tx-empty"><div className="tx-empty-h">載入中…</div></div>}
          {error && <div className="tx-empty"><div className="tx-empty-h">載入失敗：{error.message || String(error)}</div></div>}

          {!loading && !error && (
            <div className="proj-grid">
              {shown.map((p) => (
                <ProjectCard
                  key={p.id}
                  project={p}
                  onOpen={() => onOpenProject(p.id)}
                  onEdit={() => onEditProject(p)}
                />
              ))}
            </div>
          )}

          {!loading && !error && shown.length === 0 && (
            <div className="tx-empty">
              <div className="tx-empty-mark">—</div>
              <div className="tx-empty-h">沒有{tab === 'active' ? '進行中' : '已封存'}的專案</div>
            </div>
          )}
        </div>
      </div>
    );
  }

  // ── ProjectEditPanel (side drawer) ────────────────────────────────────
  function ProjectEditPanel({ mode, project, onClose, onSaved }) {
    const isEdit = mode === 'edit' && project;
    const [name, setName] = useState(isEdit ? project.name : '');
    const [color, setColor] = useState(isEdit && project.color ? project.color : '#2a6fdb');
    // Preserve cents from DB NUMERIC(12,2) — don't Math.round here, that
    // silently truncates 123.45 → 123 on every re-save.
    const [budget, setBudget] = useState(
      isEdit && project.target_budget != null
        ? String(project.target_budget).replace(/\.00$/, '')
        : '',
    );
    const [busy, setBusy] = useState(false);
    const [err, setErr] = useState(null);

    const save = useCallback(async () => {
      setBusy(true);
      setErr(null);
      try {
        const trimmed = name.trim();
        if (!trimmed) { setErr('請輸入名稱'); setBusy(false); return; }
        const payload = {
          name: trimmed,
          color: color || null,
          target_budget: budget ? Number(budget) : null,
        };
        let saved;
        if (isEdit) {
          saved = await window.LEDGER_API.apiUpdateProject(project.id, payload);
        } else {
          saved = await window.LEDGER_API.apiCreateProject(payload);
        }
        onSaved(saved);
      } catch (e) {
        setErr(e.message || String(e));
        setBusy(false);
      }
    }, [name, color, budget, isEdit, project, onSaved]);

    const archive = useCallback(async () => {
      if (!isEdit) return;
      setBusy(true);
      try {
        const saved = await window.LEDGER_API.apiArchiveProject(project.id);
        onSaved(saved);
      } catch (e) {
        setErr(e.message || String(e));
        setBusy(false);
      }
    }, [isEdit, project, onSaved]);

    const unarchive = useCallback(async () => {
      if (!isEdit) return;
      setBusy(true);
      try {
        const saved = await window.LEDGER_API.apiUnarchiveProject(project.id);
        onSaved(saved);
      } catch (e) {
        setErr(e.message || String(e));
        setBusy(false);
      }
    }, [isEdit, project, onSaved]);

    const archived = isEdit && project.archived_at;

    return (
      <div className="projects-page proj-panel-frame">
        <div className="proj-panel">
          <div className="proj-panel-head">
            <button className="proj-panel-x" onClick={onClose} aria-label="關閉">✕</button>
            <span className="proj-panel-eyebrow">{isEdit ? '編輯專案' : '新增專案'}</span>
          </div>

          <div className="proj-panel-fields" style={{ paddingTop: 18, borderTop: 0 }}>
            <div className="fp-section">
              <span className="df-label">名稱</span>
              <input
                className="df-input"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="例：日本旅行 2026、婚禮籌備"
                autoFocus
              />
              <div className="fp-helper">在 record 上多選一個或多個專案來把支出歸戶</div>
            </div>

            <div className="fp-section">
              <span className="df-label">顏色</span>
              <ColorPicker
                value={color}
                onChange={setColor}
                presets={PRESET_PROJECT_COLORS}
              />
            </div>

            <div className="fp-section">
              <span className="df-label">目標預算（lifetime · 可選）</span>
              <div className="money-input-wrap">
                <span className="mark">NT$</span>
                <input
                  inputMode="decimal"
                  value={budget}
                  onChange={(e) => {
                    // Allow digits + one decimal point (NUMERIC(12,2) cents).
                    let v = e.target.value.replace(/[^\d.]/g, '');
                    const i = v.indexOf('.');
                    if (i !== -1) v = v.slice(0, i + 1) + v.slice(i + 1).replace(/\./g, '');
                    setBudget(v);
                  }}
                  placeholder="0 = 不設預算"
                />
              </div>
              <div className="fp-helper">Lifetime budget：專案總花費的目標上限</div>
            </div>

            {err && <div className="fp-section" style={{ color: '#c0392b' }}>錯誤：{err}</div>}
          </div>

          <div className="proj-panel-foot">
            {isEdit && !archived && (
              <button className="btn-text-danger" onClick={archive} disabled={busy}>封存專案</button>
            )}
            {isEdit && archived && (
              <button className="btn-text-danger" onClick={unarchive} disabled={busy}>取消封存</button>
            )}
            <div className="spacer" />
            <button className="btn-cancel" onClick={onClose} disabled={busy}>取消</button>
            <button className="btn-save" onClick={save} disabled={busy || !name.trim()}>
              {isEdit ? '儲存變更' : '建立專案'}
            </button>
          </div>
        </div>
      </div>
    );
  }

  // ── ProjectDetailView ──────────────────────────────────────────────────
  function ProjectDetailView({ projectId, onBack, refreshKey }) {
    const [summary, setSummary] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
      let cancelled = false;
      setLoading(true);
      window.LEDGER_API.apiGetProjectSummary(projectId)
        .then((s) => { if (!cancelled) { setSummary(s); setLoading(false); } })
        .catch((e) => { if (!cancelled) { setError(e); setLoading(false); } });
      return () => { cancelled = true; };
    }, [projectId, refreshKey]);

    if (loading) {
      return (
        <div className="projects-page"><div className="page">
          <button className="dv-back" onClick={onBack}>← 全部專案</button>
          <div className="tx-empty"><div className="tx-empty-h">載入中…</div></div>
        </div></div>
      );
    }
    if (error || !summary) {
      return (
        <div className="projects-page"><div className="page">
          <button className="dv-back" onClick={onBack}>← 全部專案</button>
          <div className="tx-empty"><div className="tx-empty-h">載入失敗</div></div>
        </div></div>
      );
    }

    const p = summary.project;
    const budget = _safeBudget(p.target_budget);
    const spent = Number(p.spent_total) || 0;
    const noBudget = budget <= 0;
    const pct = !noBudget ? Math.min(100, Math.round((spent / budget) * 100)) : 0;
    const max = Math.max(1, ...summary.monthly_series.map((m) => m.total));
    const currentMonth = new Date().getMonth() + 1;
    const currentYear = new Date().getFullYear();

    return (
      <div className="projects-page">
        <div className="page">
          <button className="dv-back" onClick={onBack}>← 全部專案</button>

          <div className="rd-hero" style={p.color ? { ['--proj-color']: p.color } : null}>
            <span className="rd-hero-icon" style={p.color ? { background: p.color, border: 'none' } : null}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M3 7l9-4 9 4-9 4z" /><path d="M3 12l9 4 9-4" /><path d="M3 17l9 4 9-4" />
              </svg>
            </span>
            <div className="rd-hero-mid">
              <span className="rd-hero-name">{p.name}</span>
              <div className="rd-hero-meta">
                <span>{p.records_count} 筆紀錄</span>
                {p.last_tx_at && <><span className="dot">·</span><span>最近 {p.last_tx_at}</span></>}
              </div>
              <div className="rd-hero-amt">
                <span className="mark">NT$</span>
                <span className="num">{_fmt(spent)}</span>
                {budget > 0 && <span className="per">/ NT$ {_fmt(budget)} 預算</span>}
              </div>
              {budget > 0 && (
                <>
                  <div className="proj-bar" style={{ marginTop: 12, maxWidth: 480 }}>
                    <i style={{ width: pct + '%', background: p.color || null }} />
                  </div>
                  <div className="proj-bar-legend" style={{ maxWidth: 480 }}>
                    <span>已用 <b>{pct}%</b></span>
                    <span>剩 NT$ <b>{_fmt(Math.max(0, budget - spent))}</b></span>
                  </div>
                </>
              )}
            </div>
          </div>

          <div className="stat-strip">
            <div className="ss-cell">
              <span className="ss-k">月均</span>
              <span className="ss-v"><span className="mark">NT$</span>{_fmt(Math.round(summary.stats.monthly_avg))}</span>
              <span className="ss-sub">有支出的月份平均</span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">最高單月</span>
              <span className="ss-v"><span className="mark">NT$</span>{_fmt(Math.round(summary.stats.max_month.total))}</span>
              <span className="ss-sub">{summary.stats.max_month.year} / {summary.stats.max_month.month} 月</span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">最大單筆</span>
              <span className="ss-v"><span className="mark">NT$</span>{_fmt(Math.round(summary.stats.max_record.amount))}</span>
              <span className="ss-sub">{summary.stats.max_record.item || '—'}</span>
            </div>
            <div className="ss-cell">
              <span className="ss-k">平均單筆</span>
              <span className="ss-v"><span className="mark">NT$</span>{_fmt(Math.round(summary.stats.avg_record))}</span>
              <span className="ss-sub">共 {p.records_count} 筆</span>
            </div>
          </div>

          <div className="pd-grid">
            <div className="pd-monthly-card" style={p.color ? { ['--proj-color']: p.color } : null}>
              <div className="chart-head">
                <div className="cal-eyebrow">月度走勢</div>
                <div className="chart-head-meta">近 12 個月</div>
              </div>
              <div className="pd-monthly-chart">
                {summary.monthly_series.map((m, i) => {
                  const isCurrent = m.year === currentYear && m.month === currentMonth;
                  return (
                    <div className="pdm-bar-col" key={i}>
                      <div
                        className={'pdm-bar ' + (isCurrent ? 'is-current' : '')}
                        style={{ height: (m.total / max) * 100 + '%' }}
                      />
                    </div>
                  );
                })}
              </div>
              <div className="pdm-lbl">
                {summary.monthly_series.map((m, i) => (
                  <div className="pdm-lbl-col" key={i}>{m.month}月</div>
                ))}
              </div>
            </div>

            <div className="pd-records-card">
              <div className="pd-records-head">
                <div className="sec-title">最近紀錄</div>
              </div>
              <div className="tx-list" style={{ padding: 0 }}>
                {summary.recent_records.length === 0 && (
                  <div className="tx-empty"><div className="tx-empty-h">尚無紀錄</div></div>
                )}
                {summary.recent_records.map((r, i) => (
                  <div
                    key={r.id}
                    style={{
                      display: 'grid',
                      gridTemplateColumns: '54px 1fr auto',
                      alignItems: 'center',
                      gap: 12,
                      padding: '12px 22px',
                      borderBottom: i < summary.recent_records.length - 1 ? '1px solid var(--hairline-soft)' : 'none',
                      fontSize: 13.5,
                    }}
                  >
                    <span style={{ color: 'var(--ink-48)', fontSize: 11.5, fontVariantNumeric: 'tabular-nums' }}>
                      {_fmtMD(r.date)}
                    </span>
                    <span style={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
                      <span style={{ color: 'var(--ink)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {r.item}
                      </span>
                      <span style={{ fontSize: 11.5, color: 'var(--ink-48)' }}>
                        {r.category}{r.sub_category ? ' · ' + r.sub_category : ''}
                      </span>
                    </span>
                    <span style={{ fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>
                      <span style={{ fontSize: 10.5, color: 'var(--ink-48)', marginRight: 3 }}>NT$</span>
                      {_fmt(r.amount)}
                    </span>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ── ProjectPicker (multi-select chip for RecordForm) ──────────────────
  function ProjectPicker({ value, onChange, projects }) {
    const [open, setOpen] = useState(false);
    const [query, setQuery] = useState('');
    const inputRef = useRef(null);
    const selected = (value || []).map((id) =>
      (projects || []).find((p) => p.id === id),
    ).filter(Boolean);
    const active = (projects || []).filter(
      (p) => !p.archived_at && !(value || []).includes(p.id),
    );
    const filtered = query
      ? active.filter((p) => p.name.toLowerCase().includes(query.toLowerCase()))
      : active;

    const add = (id) => {
      onChange([...(value || []), id]);
      setQuery('');
      setOpen(false);
    };
    const remove = (id) => onChange((value || []).filter((x) => x !== id));

    return (
      <div className="pp-picker">
        <div className="pp-chips" onClick={() => { setOpen(true); inputRef.current && inputRef.current.focus(); }}>
          {selected.map((p) => (
            <span className="pp-chip" key={p.id} style={p.color ? { ['--proj-color']: p.color } : null}>
              <span className="pp-chip-dot" style={p.color ? { background: p.color } : null} />
              {p.name}
              <button
                type="button"
                className="pp-chip-x"
                aria-label="移除"
                onClick={(e) => { e.stopPropagation(); remove(p.id); }}
              >✕</button>
            </span>
          ))}
          <input
            ref={inputRef}
            className="pp-input"
            value={query}
            onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
            onFocus={() => setOpen(true)}
            onBlur={() => setTimeout(() => setOpen(false), 120)}
            placeholder={selected.length ? '' : '加入專案…'}
          />
        </div>
        {open && filtered.length > 0 && (
          <div className="pp-menu">
            {filtered.slice(0, 8).map((p) => (
              <button
                key={p.id}
                type="button"
                className="pp-menu-item"
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => add(p.id)}
              >
                <span className="pp-chip-dot" style={p.color ? { background: p.color } : null} />
                {p.name}
              </button>
            ))}
          </div>
        )}
      </div>
    );
  }

  // ── Export to global ──────────────────────────────────────────────────
  window.Projects = window.Projects || {};
  Object.assign(window.Projects, {
    ColorPicker,
    PRESET_PROJECT_COLORS,
    PRESET_CATEGORY_COLORS,
    ProjectsPage,
    ProjectCard,
    ProjectEditPanel,
    ProjectDetailView,
    ProjectPicker,
  });
})();
