// Outlet Database — semua outlet hasil scrape lintas brand di satu menu.
// Tujuan: pantau perkembangan tiap outlet (rating & jumlah review antar waktu).
//
// IA dashboard (Shneiderman: overview → filter → detail):
//   KPI row (klik = filter) → filter bar → chart grid (klik = filter,
//   cross-filter: tiap chart mengecualikan dimensi filternya sendiri) → tabel.
// Aturan chart: bar mulai dari nol, TANPA dual-axis (trend = 2 panel terpisah),
// semua mark berlabel langsung (identitas tidak pernah cuma warna), warna dari
// token tema (--rate-*, --tone-*) supaya dark mode benar.
//
// Reuse global dari brand-audit.jsx: ratingColor, ageStrAudit, OutletDetail,
// Sparkline, AuditToastHost, ConfirmButton — file ini dimuat SETELAHNYA.

// Ekstrak { province, city } dari alamat outlet via REGION_HIERARCHY
// (templates.jsx). Multi-match: kota = match ber-kotakab terpanjang (supaya
// "jakarta selatan" menang atas "jakarta"), provinsi ikut kota kalau ada.
// lazy: contains-match bisa salah kalau nama jalan mengandung nama kota lain
// ("Jl. Malang" di Bekasi) — cukup akurat karena alamat Google selalu
// diakhiri kota + provinsi asli; upgrade ke parsing per-koma kalau meleset.
function outletRegionOf(address) {
  const k = (address || '').toLowerCase();
  const H = window.REGION_HIERARCHY || {};
  let city = null, cityKeyLen = 0, cityProv = null;
  let prov = null, provKeyLen = 0;
  for (const key in H) {
    if (!k.includes(key)) continue;
    const info = H[key];
    if (info.kotakab) {
      if (key.length > cityKeyLen) { city = info.kotakab; cityProv = info.province; cityKeyLen = key.length; }
    } else if (key.length > provKeyLen) {
      prov = info.province; provKeyLen = key.length;
    }
  }
  return { province: cityProv || prov, city };
}

// Bucket distribusi rating — juga jadi nilai filter `bandFilter`.
const RATING_BUCKETS = [
  { key: 'lt2', label: '<2',      min: 0,   max: 2 },
  { key: 'b2',  label: '2–2.9',   min: 2,   max: 3 },
  { key: 'b3',  label: '3–3.4',   min: 3,   max: 3.5 },
  { key: 'b35', label: '3.5–3.9', min: 3.5, max: 4 },
  { key: 'b4',  label: '4–4.4',   min: 4,   max: 4.5 },
  { key: 'b45', label: '4.5–5',   min: 4.5, max: 5.01 },
];
function inRatingBand(rating, bandKey) {
  if (rating == null) return false;
  if (bandKey === 'critical') return rating < 3.5;
  const b = RATING_BUCKETS.find((x) => x.key === bandKey);
  return b ? rating >= b.min && rating < b.max : true;
}

// CSV ramah-Excel (BOM UTF-8, pola sama dengan export respons responses.ts).
function csvDownload(basename, headers, lines, toastMsg) {
  const esc = (v) => {
    if (v === null || v === undefined) return '';
    const s = String(v);
    return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
  };
  const csv = [headers, ...lines].map((r) => r.map(esc).join(',')).join('\n');
  const blob = new Blob(['﻿' + csv], { type: 'text/csv;charset=utf-8' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = `${basename}_${new Date().toISOString().slice(0, 10)}.csv`;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(a.href), 5000);
  pushAuditToast(toastMsg);
}

// Download list outlet (sudah terfilter + tersortir).
function downloadOutletsCsv(rows) {
  const fmtDate = (d) => d ? new Date(d).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) : '';
  csvDownload(
    'outlet-database',
    [
      'Nama Outlet', 'Alamat', 'Brand', 'Label', 'Kota', 'Provinsi', 'Wilayah Scan',
      'Score', 'Grade',
      'Rating', 'Rating Sebelumnya', 'Perubahan Rating',
      'Jumlah Review', 'Review Sebelumnya', 'Perubahan Review',
      'Review Jelek 90 Hari', 'Review Tersimpan', 'Jumlah Scan', 'Pertama Discan', 'Terakhir Discan',
    ],
    rows.map((o) => [
      o.name, o.address, o.brandKey, (o.labels || []).join('; '), o.city, o.province, o.region,
      o.score ?? '', o.grade ?? '',
      o.rating != null ? o.rating.toFixed(1) : '',
      o.prevRating != null ? o.prevRating.toFixed(1) : '',
      o.deltaRating != null ? o.deltaRating.toFixed(2) : '',
      o.reviewsCount, o.prevReviewsCount ?? '', o.deltaReviews ?? '',
      o.recentBad ?? 0, o.storedReviews, o.snapshots, fmtDate(o.firstSeen), fmtDate(o.lastSeen),
    ]),
    `⬇ ${rows.length} outlet di-download — buka langsung di Excel.`,
  );
}

function gradeTone(grade) {
  if (!grade) return { bg: 'var(--bg-2)', fg: 'var(--ink-3)' };
  if (grade === 'A' || grade === 'B') return { bg: 'var(--tone-good-bg)', fg: 'var(--tone-good-fg)' };
  if (grade === 'C') return { bg: 'var(--tone-warn-bg)', fg: 'var(--tone-warn-fg)' };
  return { bg: 'var(--tone-bad-bg)', fg: 'var(--tone-bad-fg)' };
}

function scoreTitle(o) {
  if (!o.scoreParts) return 'Belum ada rating — skor tidak dihitung';
  return `Skor ${o.score}/100 — Rating ${o.scoreParts.rating}/60 · Trend ${o.scoreParts.trend}/15 · Bebas komplain ${o.scoreParts.complaint}/25`
    + (o.scoreLowData ? ' · (review tersimpan masih sedikit — komponen komplain netral)' : '');
}

function OutletDeltaBadge({ delta, digits = 1, suffix = '' }) {
  // Tanpa perubahan / tanpa pembanding → jangan render apa-apa (kurangi noise)
  if (delta == null || delta === 0) return null;
  const up = delta > 0;
  return (
    <span className="mono" style={{
      fontSize: 10, fontWeight: 800, padding: '1px 6px', borderRadius: 999,
      background: up ? 'var(--tone-good-bg)' : 'var(--tone-bad-bg)',
      color: up ? 'var(--tone-good-fg)' : 'var(--tone-bad-fg)',
    }}>
      {up ? '▲' : '▼'} {up ? '+' : ''}{typeof delta === 'number' ? delta.toFixed(digits) : delta}{suffix}
    </span>
  );
}

// ─── KPI card — klik = toggle filter terkait ────────────────────────
function KpiCard({ icon, label, value, sub, tone, active, onClick }) {
  const toneColor = tone === 'good' ? 'var(--tone-good-fg)' : tone === 'bad' ? 'var(--tone-bad-fg)' : 'var(--ink)';
  const Tag = onClick ? 'button' : 'div';
  return (
    <Tag
      className="card"
      onClick={onClick}
      aria-pressed={onClick ? !!active : undefined}
      style={{
        padding: '12px 14px', textAlign: 'left', width: '100%',
        cursor: onClick ? 'pointer' : 'default',
        border: active ? '1px solid var(--primary)' : '1px solid var(--line)',
        background: active ? 'var(--primary-50)' : 'var(--surface)',
      }}
      title={onClick ? (active ? 'Klik untuk hapus filter ini' : 'Klik untuk filter tabel') : undefined}
    >
      <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
        {icon} {label}
      </div>
      <div className="mono" style={{ fontSize: 22, fontWeight: 800, color: toneColor, marginTop: 3, lineHeight: 1.1 }}>
        {value}
      </div>
      {sub && <div style={{ fontSize: 10.5, color: 'var(--ink-4)', marginTop: 3 }}>{sub}</div>}
    </Tag>
  );
}

// ─── Histogram distribusi rating (bar vertikal, klik = filter band) ─
function RatingHistogram({ outlets, activeBand, onPick }) {
  const counts = RATING_BUCKETS.map((b) => ({
    ...b,
    count: outlets.filter((o) => o.rating != null && o.rating >= b.min && o.rating < b.max).length,
    color: ratingColor((b.min + Math.min(b.max, 5)) / 2),
  }));
  const max = Math.max(1, ...counts.map((c) => c.count));
  return (
    <div className="card" style={{ padding: 16 }}>
      <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 2 }}>📊 Distribusi Rating</div>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 12 }}>Jumlah outlet per rentang rating — klik bar untuk filter</div>
      <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, height: 130 }}>
        {counts.map((b) => {
          const active = activeBand === b.key;
          const h = Math.max(b.count > 0 ? 6 : 2, (b.count / max) * 100);
          return (
            <button
              key={b.key}
              onClick={() => onPick(active ? 'all' : b.key)}
              disabled={b.count === 0}
              aria-pressed={active}
              title={`${b.label}: ${b.count} outlet`}
              style={{
                flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end',
                gap: 4, height: '100%', background: 'transparent', border: 'none', padding: 0,
                cursor: b.count === 0 ? 'default' : 'pointer',
                opacity: activeBand !== 'all' && activeBand !== 'critical' && !active ? 0.35 : 1,
              }}
            >
              <span className="mono" style={{ fontSize: 10.5, fontWeight: 700, color: 'var(--ink-2)' }}>{b.count}</span>
              <div style={{
                width: '100%', height: `${h}%`, minHeight: 2, background: b.color,
                borderRadius: '4px 4px 0 0',
                outline: active ? '2px solid var(--primary)' : 'none', outlineOffset: 1,
              }} />
              <span style={{ fontSize: 9.5, color: 'var(--ink-3)', fontWeight: 600 }}>{b.label}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// ─── Horizontal bar terurut (brand / provinsi), klik = filter ───────
function TopBarsCard({ title, subtitle, items, activeKey, onPick, maxRows = 8 }) {
  const shown = items.slice(0, maxRows);
  const max = Math.max(1, ...shown.map((i) => i.value));
  return (
    <div className="card" style={{ padding: 16 }}>
      <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 2 }}>{title}</div>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 12 }}>{subtitle}</div>
      {shown.length === 0 && (
        <div style={{ fontSize: 12, color: 'var(--ink-4)', textAlign: 'center', padding: 14 }}>Belum ada data.</div>
      )}
      <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
        {shown.map((it) => {
          const active = activeKey === it.key;
          return (
            <button
              key={it.key}
              onClick={() => onPick(active ? 'all' : it.key)}
              aria-pressed={active}
              title={`${it.label}: ${it.value} outlet — klik untuk filter`}
              style={{
                display: 'grid', gridTemplateColumns: 'minmax(90px, 130px) 1fr auto auto', gap: 8, alignItems: 'center',
                background: 'transparent', border: 'none', padding: '2px 0', cursor: 'pointer', textAlign: 'left',
                opacity: activeKey !== 'all' && !active ? 0.4 : 1,
              }}
            >
              <span style={{
                fontSize: 11.5, fontWeight: active ? 700 : 600,
                color: active ? 'var(--primary)' : 'var(--ink-2)',
                overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
              }}>{it.label}</span>
              <div style={{ height: 10, borderRadius: 999, background: 'var(--bg-2)', overflow: 'hidden' }}>
                <div style={{
                  width: `${(it.value / max) * 100}%`, height: '100%', minWidth: 2,
                  background: active ? 'var(--primary)' : 'var(--primary-100)', borderRadius: 999,
                }} />
              </div>
              <span className="mono" style={{ fontSize: 11, fontWeight: 700, color: 'var(--ink-2)', minWidth: 24, textAlign: 'right' }}>
                {it.value}
              </span>
              <span className="mono" style={{ fontSize: 11, fontWeight: 700, color: ratingColor(it.avg), minWidth: 34, textAlign: 'right' }}>
                {it.avg != null ? it.avg.toFixed(1) + '⭐' : '—'}
              </span>
            </button>
          );
        })}
      </div>
      {items.length > maxRows && (
        <div style={{ fontSize: 10.5, color: 'var(--ink-4)', marginTop: 8 }}>+{items.length - maxRows} lainnya — pakai dropdown filter untuk lihat semua</div>
      )}
    </div>
  );
}

// ─── Line chart kecil (satu seri, satu sumbu) untuk panel trend ─────
function MiniTrendLine({ points, color, fmt }) {
  const W = 260, H = 90, PAD = { top: 8, right: 44, bottom: 16, left: 8 };
  const vals = points.map((p) => p.y);
  let lo = Math.min(...vals), hi = Math.max(...vals);
  if (hi - lo < 0.001) { lo -= 1; hi += 1; }
  const span = hi - lo;
  lo -= span * 0.12; hi += span * 0.12;
  const xAt = (i) => PAD.left + (i / Math.max(1, points.length - 1)) * (W - PAD.left - PAD.right);
  const yAt = (v) => PAD.top + (1 - (v - lo) / (hi - lo)) * (H - PAD.top - PAD.bottom);
  const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${xAt(i).toFixed(1)} ${yAt(p.y).toFixed(1)}`).join(' ');
  const last = points[points.length - 1];
  const fmtDay = (d) => new Date(d).toLocaleDateString('id-ID', { day: '2-digit', month: 'short' });
  return (
    <svg viewBox={`0 0 ${W} ${H}`} style={{ width: '100%', height: 'auto', display: 'block' }}>
      {[0.25, 0.5, 0.75].map((t) => (
        <line key={t} x1={PAD.left} x2={W - PAD.right} y1={PAD.top + t * (H - PAD.top - PAD.bottom)} y2={PAD.top + t * (H - PAD.top - PAD.bottom)} stroke="var(--line-2)" strokeWidth="0.5" />
      ))}
      <path d={path} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      {points.map((p, i) => (
        <g key={i}>
          {/* hit target lebih besar dari mark + tooltip native */}
          <circle cx={xAt(i)} cy={yAt(p.y)} r="8" fill="transparent">
            <title>{`${fmtDay(p.x)}: ${fmt(p.y)}`}</title>
          </circle>
          <circle cx={xAt(i)} cy={yAt(p.y)} r="2.5" fill={color} stroke="var(--surface)" strokeWidth="1" style={{ pointerEvents: 'none' }} />
        </g>
      ))}
      {/* label langsung di titik terakhir */}
      <text x={xAt(points.length - 1) + 6} y={yAt(last.y) + 3.5} fontSize="10" fontWeight="700" fill="var(--ink)" className="mono">
        {fmt(last.y)}
      </text>
      <text x={PAD.left} y={H - 4} fontSize="8.5" fill="var(--ink-4)">{fmtDay(points[0].x)}</text>
      <text x={W - PAD.right} y={H - 4} fontSize="8.5" fill="var(--ink-4)" textAnchor="end">{fmtDay(last.x)}</text>
    </svg>
  );
}

// Panel trend: 2 chart terpisah (rata-rata rating & total review) — sengaja
// TIDAK digabung satu chart dua sumbu (dual axis menyesatkan pembacanya).
function TrendPanel({ trend, brandFilter }) {
  const rows = brandFilter === 'all' ? trend : trend.filter((t) => t.brandKey === brandFilter);
  // Agregasi per hari: avg rating dibobot outletCount, review dijumlah.
  const byDay = new Map();
  for (const r of rows) {
    const d = byDay.get(r.day) || { day: r.day, ratingSum: 0, ratingW: 0, reviews: 0, outlets: 0 };
    if (r.avgRating != null) { d.ratingSum += r.avgRating * r.outletCount; d.ratingW += r.outletCount; }
    d.reviews += r.totalReviews;
    d.outlets += r.outletCount;
    byDay.set(r.day, d);
  }
  const days = [...byDay.values()].sort((a, b) => a.day.localeCompare(b.day));
  const ratingPts = days.filter((d) => d.ratingW > 0).map((d) => ({ x: d.day, y: d.ratingSum / d.ratingW }));
  const reviewPts = days.map((d) => ({ x: d.day, y: d.reviews }));

  return (
    <div className="card" style={{ padding: 16 }}>
      <div style={{ fontSize: 13, fontWeight: 700, marginBottom: 2 }}>📈 Perkembangan{brandFilter !== 'all' ? ` — ${brandFilter}` : ' Portofolio'}</div>
      <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 12 }}>
        Per hari scan (WIB){brandFilter === 'all' ? ' — hari beda bisa meng-scan brand berbeda' : ''}
      </div>
      {days.length < 2 ? (
        <div style={{ fontSize: 12, color: 'var(--ink-3)', textAlign: 'center', padding: '20px 10px', lineHeight: 1.6 }}>
          🕒 Baru {days.length} hari scan — scan lagi di hari berbeda (misal mingguan)
          <br />untuk melihat garis perkembangan.
        </div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: 14 }}>
          <div>
            <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>⭐ Rata-rata Rating</div>
            {ratingPts.length >= 2
              ? <MiniTrendLine points={ratingPts} color="var(--rate-4)" fmt={(v) => v.toFixed(2)} />
              : <div style={{ fontSize: 11, color: 'var(--ink-4)', padding: 10 }}>Belum cukup data rating.</div>}
          </div>
          <div>
            <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 4 }}>💬 Total Review</div>
            <MiniTrendLine points={reviewPts} color="var(--tone-info-acc)" fmt={(v) => v.toLocaleString('id-ID')} />
          </div>
        </div>
      )}
    </div>
  );
}

// ─── Tab: Monitor Review Jelek — evaluasi tim lapangan ──────────────
// Feed review ≤2★ terbaru lintas semua toko + ranking outlet paling banyak
// dikomplain pada periode terpilih. Data dari review tersimpan (place_reviews)
// — kesegarannya mengikuti Update/Collect terakhir per outlet.
function BadReviewMonitor({ brands }) {
  const [days, setDays] = React.useState(30);
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [brandF, setBrandF] = React.useState('all');
  const [search, setSearch] = React.useState('');

  React.useEffect(() => {
    let cancelled = false;
    setLoading(true); setError(null);
    window.api.outletsBadReviews(days)
      .then((r) => { if (!cancelled) setData(r); })
      .catch((e) => { if (!cancelled) setError(e.message || 'Gagal memuat review'); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, [days]);

  const q = search.trim().toLowerCase();
  const list = React.useMemo(() => {
    let l = data?.reviews || [];
    if (brandF !== 'all') l = l.filter((r) => r.brandKey === brandF);
    if (q) l = l.filter((r) => r.text.toLowerCase().includes(q) || r.outletName.toLowerCase().includes(q));
    return l;
  }, [data, brandF, q]);

  // Ranking outlet paling banyak dikomplain — artefak utama evaluasi tim.
  const offenders = React.useMemo(() => {
    const m = new Map();
    for (const r of list) {
      const e = m.get(r.canonical) || { name: r.outletName, brandKey: r.brandKey, rating: r.outletRating, count: 0 };
      e.count++;
      m.set(r.canonical, e);
    }
    return [...m.values()].sort((a, b) => b.count - a.count).slice(0, 5);
  }, [list]);

  const exportFeed = () => {
    csvDownload(
      `review-jelek-${days}hari`,
      ['Tanggal', 'Rating', 'Review', 'Outlet', 'Brand', 'Rating Outlet', 'Alamat', 'Reviewer'],
      list.map((r) => [
        r.isoDate ? new Date(r.isoDate).toLocaleDateString('id-ID') : r.date,
        r.rating, r.text, r.outletName, r.brandKey,
        r.outletRating != null ? r.outletRating.toFixed(1) : '', r.address, r.user,
      ]),
      `⬇ ${list.length} review jelek di-download.`,
    );
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      {/* Filter bar */}
      <div className="card" style={{ padding: '10px 14px', display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
        <select className="select" value={days} onChange={(e) => setDays(Number(e.target.value))} style={{ fontSize: 12.5, width: 'auto' }}>
          <option value={7}>7 hari terakhir</option>
          <option value={30}>30 hari terakhir</option>
          <option value={90}>90 hari terakhir</option>
          <option value={180}>180 hari terakhir</option>
        </select>
        <select className="select" value={brandF} onChange={(e) => setBrandF(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
          <option value="all">Semua brand</option>
          {brands.map((b) => <option key={b} value={b}>{b}</option>)}
        </select>
        <input
          className="input"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder='Cari kata kunci / nama outlet — "lambat", "kasir"…'
          style={{ fontSize: 12.5, flex: 1, minWidth: 170 }}
        />
        <button className="btn btn-soft" style={{ padding: '7px 12px', fontSize: 11.5 }} onClick={exportFeed} disabled={list.length === 0}>
          ⬇ Excel
        </button>
      </div>

      {error && (
        <div style={{ padding: 12, borderRadius: 10, background: 'var(--danger-soft)', color: 'var(--danger)', fontSize: 13 }}>{error}</div>
      )}

      {loading ? (
        <>
          <div className="shimmer" style={{ height: 90, borderRadius: 12 }} />
          <div className="shimmer" style={{ height: 300, borderRadius: 12 }} />
        </>
      ) : (
        <>
          {/* Ranking outlet paling dikomplain */}
          {offenders.length > 0 && (
            <div className="card" style={{ padding: 16 }}>
              <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--tone-bad-fg)', marginBottom: 2 }}>
                🚨 Paling Banyak Dikomplain — {days} hari terakhir
              </div>
              <div style={{ fontSize: 11, color: 'var(--ink-3)', marginBottom: 10 }}>
                Prioritas kunjungan / evaluasi tim lapangan
              </div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                {offenders.map((o, i) => (
                  <div key={i} style={{
                    display: 'grid', gridTemplateColumns: '20px 1fr auto auto', gap: 10, alignItems: 'center',
                    padding: '7px 10px', background: 'var(--tone-bad-bg)', borderRadius: 8, fontSize: 12.5,
                  }}>
                    <span className="mono" style={{ fontWeight: 800, color: 'var(--tone-bad-fg)' }}>#{i + 1}</span>
                    <div style={{ minWidth: 0 }}>
                      <span style={{ fontWeight: 700, color: 'var(--ink)' }}>{o.name}</span>
                      <span style={{ fontSize: 11, color: 'var(--ink-3)', marginLeft: 6 }}>{o.brandKey}</span>
                    </div>
                    <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: ratingColor(o.rating) }}>
                      {o.rating != null ? o.rating.toFixed(1) + '⭐' : '—'}
                    </span>
                    <span className="mono" style={{ fontSize: 12, fontWeight: 800, color: 'var(--tone-bad-fg)' }}>
                      {o.count}× komplain
                    </span>
                  </div>
                ))}
              </div>
            </div>
          )}

          {/* Feed review jelek */}
          <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
            <div style={{ padding: '12px 16px', borderBottom: '1px solid var(--line)', display: 'flex', alignItems: 'baseline', gap: 8, flexWrap: 'wrap' }}>
              <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink-2)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
                Review ⭐1–2 Terbaru ({list.length})
              </span>
              <span style={{ fontSize: 10.5, color: 'var(--ink-4)', marginLeft: 'auto' }}>
                Dari review tersimpan — jalankan Collect/Update di outlet untuk data paling segar
              </span>
            </div>
            {list.length === 0 && (
              <div style={{ padding: 32, textAlign: 'center', color: 'var(--ink-3)', fontSize: 12.5, lineHeight: 1.6 }}>
                🎉 Tidak ada review ⭐1–2 tersimpan dalam {days} hari terakhir{brandF !== 'all' ? ` untuk ${brandF}` : ''}.
                <br /><span style={{ fontSize: 11, color: 'var(--ink-4)' }}>Coba perlebar periode, atau jalankan Collect Semua supaya review terbaru tertarik.</span>
              </div>
            )}
            <div style={{ maxHeight: 640, overflow: 'auto' }} className="scroll">
              {list.map((r, i) => (
                <div key={`${r.canonical}-${r.isoDate}-${i}`} style={{ padding: '12px 16px', borderTop: i === 0 ? 'none' : '1px solid var(--line-2)' }}>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4, flexWrap: 'wrap' }}>
                    <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: ratingColor(r.rating) }}>
                      ⭐ {r.rating}/5
                    </span>
                    <span style={{ fontSize: 12, fontWeight: 700, color: 'var(--ink)' }}>{r.outletName}</span>
                    <span style={{ fontSize: 10.5, padding: '1px 7px', borderRadius: 999, background: 'var(--bg-2)', color: 'var(--ink-3)', fontWeight: 600 }}>{r.brandKey}</span>
                    <span style={{ fontSize: 10.5, color: 'var(--ink-4)', marginLeft: 'auto' }}>
                      {r.isoDate ? new Date(r.isoDate).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }) : r.date} · {r.user || 'Anonim'}
                    </span>
                  </div>
                  <div style={{ fontSize: 13, color: 'var(--ink)', lineHeight: 1.55 }}>
                    {q ? highlightMatch(r.text, q) : r.text}
                  </div>
                </div>
              ))}
            </div>
          </div>
        </>
      )}
    </div>
  );
}

// ─── Panel perkembangan satu outlet (expand row tabel) ──────────────
function OutletTimeline({ outlet }) {
  const [history, setHistory] = React.useState(null);
  const [error, setError] = React.useState(null);
  const [showReviews, setShowReviews] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    window.api.outletHistory(outlet.dataId)
      .then((r) => { if (!cancelled) setHistory(r.history || []); })
      .catch((e) => { if (!cancelled) setError(e.message); });
    return () => { cancelled = true; };
  }, [outlet.dataId]);

  if (error) {
    return <div style={{ padding: 14, fontSize: 12, color: 'var(--danger)' }}>{error}</div>;
  }
  if (!history) {
    return <div style={{ padding: 14, fontSize: 12, color: 'var(--ink-3)' }}>⏳ Memuat riwayat…</div>;
  }

  const ratingTimeline = history.filter((h) => h.rating != null).map((h) => ({ rating: h.rating }));
  const reviewsTimeline = history.map((h) => ({ rating: h.reviewsCount }));
  const first = history[0];
  const last = history[history.length - 1];

  return (
    <div style={{ background: 'var(--bg-2)', borderTop: '1px solid var(--line-2)', padding: '12px 16px' }}>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12, marginBottom: 12 }}>
        <div style={{ padding: 10, background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--line)' }}>
          <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 6 }}>
            ⭐ Perkembangan Rating
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Sparkline timeline={ratingTimeline} />
            <div className="mono" style={{ fontSize: 12 }}>
              <span style={{ color: ratingColor(first?.rating) }}>{first?.rating?.toFixed(1) ?? '—'}</span>
              <span style={{ color: 'var(--ink-4)' }}> → </span>
              <span style={{ fontWeight: 700, color: ratingColor(last?.rating) }}>{last?.rating?.toFixed(1) ?? '—'}</span>
            </div>
          </div>
        </div>
        <div style={{ padding: 10, background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--line)' }}>
          <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: 6 }}>
            💬 Pertumbuhan Review
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Sparkline timeline={reviewsTimeline} />
            <div className="mono" style={{ fontSize: 12 }}>
              <span style={{ color: 'var(--ink-3)' }}>{first?.reviewsCount?.toLocaleString('id-ID') ?? 0}</span>
              <span style={{ color: 'var(--ink-4)' }}> → </span>
              <span style={{ fontWeight: 700, color: 'var(--ink)' }}>{last?.reviewsCount?.toLocaleString('id-ID') ?? 0}</span>
            </div>
          </div>
        </div>
      </div>

      {/* Tabel snapshot */}
      <div style={{ background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--line)', overflow: 'hidden', marginBottom: 12 }}>
        <div style={{ padding: '8px 12px', fontSize: 10.5, fontWeight: 700, color: 'var(--ink-3)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '1px solid var(--line-2)' }}>
          Riwayat Snapshot ({history.length})
        </div>
        <div style={{ maxHeight: 220, overflow: 'auto' }} className="scroll">
          {[...history].reverse().map((h, i, arr) => {
            const prev = arr[i + 1]; // baris berikut = snapshot lebih lama
            const dRating = h.rating != null && prev?.rating != null ? Math.round((h.rating - prev.rating) * 100) / 100 : null;
            const dReviews = prev != null ? h.reviewsCount - prev.reviewsCount : null;
            return (
              <div key={i} style={{
                display: 'grid', gridTemplateColumns: '110px auto auto 1fr', gap: 12, alignItems: 'center',
                padding: '7px 12px', fontSize: 12, borderTop: i === 0 ? 'none' : '1px solid var(--line-2)',
              }}>
                <span style={{ color: 'var(--ink-3)', fontSize: 11 }}>
                  {new Date(h.capturedAt).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: '2-digit' })}
                </span>
                <span className="mono" style={{ fontWeight: 700, color: ratingColor(h.rating), display: 'inline-flex', gap: 6, alignItems: 'center' }}>
                  {h.rating?.toFixed(1) ?? '—'} ⭐ <OutletDeltaBadge delta={dRating} />
                </span>
                <span className="mono" style={{ color: 'var(--ink-2)', display: 'inline-flex', gap: 6, alignItems: 'center' }}>
                  {h.reviewsCount.toLocaleString('id-ID')} review <OutletDeltaBadge delta={dReviews} digits={0} />
                </span>
                <span style={{ fontSize: 10.5, color: 'var(--ink-4)', textAlign: 'right' }}>{h.brandKey}</span>
              </div>
            );
          })}
        </div>
      </div>

      {/* Drill review — reuse OutletDetail */}
      {!showReviews ? (
        <button className="btn btn-soft" style={{ padding: '7px 14px', fontSize: 12 }} onClick={() => setShowReviews(true)}>
          💬 Lihat review outlet ini {outlet.storedReviews > 0 ? `(${outlet.storedReviews} tersimpan)` : ''}
        </button>
      ) : (
        <div style={{ background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--line)', overflow: 'hidden' }}>
          <OutletDetail
            place={{
              dataId: outlet.drillDataId || outlet.dataId,
              name: outlet.name,
              address: outlet.address,
              rating: outlet.rating,
              reviewsCount: outlet.reviewsCount,
            }}
            sampleReviews={[]}
          />
        </div>
      )}
    </div>
  );
}

function OutletDbView() {
  const useState_od = React.useState;
  const [outlets, setOutlets] = useState_od([]);
  const [trend, setTrend] = useState_od([]);
  const [loading, setLoading] = useState_od(true);
  const [error, setError] = useState_od(null);
  const [search, setSearch] = useState_od('');
  const [brandFilter, setBrandFilter] = useState_od('all');
  const [provFilter, setProvFilter] = useState_od('all');
  const [cityFilter, setCityFilter] = useState_od('all');
  const [bandFilter, setBandFilter] = useState_od('all');   // bucket rating / 'critical'
  const [deltaFilter, setDeltaFilter] = useState_od('all'); // 'up' | 'down'
  const [labelFilter, setLabelFilter] = useState_od('all');
  const [selected, setSelected] = useState_od(() => new Set()); // dataId terpilih (bulk action)
  const [labelInput, setLabelInput] = useState_od('');
  const [bulkBusy, setBulkBusy] = useState_od(false);
  const [sort, setSort] = useState_od('worst');
  const [page, setPage] = useState_od(1);
  const [expandedId, setExpandedId] = useState_od(null);
  const [tab, setTab] = useState_od('dash'); // 'dash' | 'bad'
  const PER_PAGE = 50;

  const load = () => {
    setLoading(true); setError(null);
    Promise.all([window.api.listOutlets(), window.api.outletsTrend().catch(() => ({ trend: [] }))])
      .then(([r, t]) => {
        setOutlets((r.outlets || []).map((o) => ({ ...o, ...outletRegionOf(o.address) })));
        setTrend(t.trend || []);
      })
      .catch((e) => setError(e.message || 'Gagal memuat outlet'))
      .finally(() => setLoading(false));
  };
  React.useEffect(() => { load(); }, []);

  const q = search.trim().toLowerCase();

  // Cross-filter: satu fungsi filter dengan pengecualian dimensi — chart
  // brand dihitung TANPA filter brand (supaya bisa pindah pilihan), dst.
  const applyFilters = React.useCallback((list, except = {}) => {
    let l = list;
    if (!except.brand && brandFilter !== 'all') l = l.filter((o) => o.brandKey === brandFilter);
    if (!except.prov && provFilter !== 'all') l = l.filter((o) => o.province === provFilter);
    if (!except.city && cityFilter !== 'all') l = l.filter((o) => o.city === cityFilter);
    if (!except.band && bandFilter !== 'all') l = l.filter((o) => inRatingBand(o.rating, bandFilter));
    if (!except.delta && deltaFilter !== 'all') {
      l = l.filter((o) => deltaFilter === 'up' ? (o.deltaRating ?? 0) > 0 : (o.deltaRating ?? 0) < 0);
    }
    if (labelFilter !== 'all') l = l.filter((o) => (o.labels || []).includes(labelFilter));
    if (q) l = l.filter((o) => o.name.toLowerCase().includes(q) || (o.address || '').toLowerCase().includes(q));
    return l;
  }, [brandFilter, provFilter, cityFilter, bandFilter, deltaFilter, labelFilter, q]);

  const brands = React.useMemo(() => [...new Set(outlets.map((o) => o.brandKey))].sort(), [outlets]);
  const provinces = React.useMemo(() => [...new Set(outlets.map((o) => o.province).filter(Boolean))].sort(), [outlets]);
  const cities = React.useMemo(
    () => [...new Set(outlets.filter((o) => provFilter === 'all' || o.province === provFilter).map((o) => o.city).filter(Boolean))].sort(),
    [outlets, provFilter],
  );
  React.useEffect(() => {
    if (cityFilter !== 'all' && !cities.includes(cityFilter)) setCityFilter('all');
  }, [cities, cityFilter]);

  const allLabels = React.useMemo(
    () => [...new Set(outlets.flatMap((o) => o.labels || []))].sort(),
    [outlets],
  );

  // ── Bulk selection + aksi massal ──
  const toggleSelect = (dataId) => {
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(dataId)) next.delete(dataId); else next.add(dataId);
      return next;
    });
  };
  const clearSelection = () => setSelected(new Set());

  const doLabel = async (mode) => {
    const label = labelInput.trim();
    if (!label) { pushAuditToast('Isi nama label dulu.', 'bad'); return; }
    setBulkBusy(true);
    try {
      const dataIds = [...selected];
      if (mode === 'add') {
        const r = await window.api.outletsAddLabel({ dataIds, label });
        pushAuditToast(`🏷 Label "${label}" dipasang ke ${r.labeled} outlet.`);
      } else {
        const r = await window.api.outletsRemoveLabel({ dataIds, label });
        pushAuditToast(`Label "${label}" dilepas dari ${r.unlabeled} outlet.`);
      }
      setLabelInput('');
      load();
    } catch (e) {
      pushAuditToast(e.message || 'Gagal memproses label', 'bad');
    } finally {
      setBulkBusy(false);
    }
  };

  const doBulkDelete = async () => {
    setBulkBusy(true);
    try {
      const r = await window.api.outletsBulkDelete({ dataIds: [...selected] });
      pushAuditToast(`🗑 ${r.deleted} outlet dihapus permanen dari database.`);
      clearSelection();
      load();
    } catch (e) {
      pushAuditToast(e.message || 'Gagal menghapus', 'bad');
    } finally {
      setBulkBusy(false);
    }
  };

  // Basis KPI: semua filter KECUALI band & delta (dimensi milik KPI sendiri) —
  // supaya angka KPI stabil saat KPI-nya di-toggle.
  const kpiBase = React.useMemo(() => applyFilters(outlets, { band: true, delta: true }), [outlets, applyFilters]);
  const kpi = React.useMemo(() => {
    const rated = kpiBase.filter((o) => o.rating != null);
    const avg = rated.length ? rated.reduce((s, o) => s + o.rating, 0) / rated.length : null;
    const reviewGrowth = kpiBase.reduce((s, o) => s + (o.deltaReviews ?? 0), 0);
    return {
      total: kpiBase.length,
      avg,
      up: kpiBase.filter((o) => (o.deltaRating ?? 0) > 0).length,
      down: kpiBase.filter((o) => (o.deltaRating ?? 0) < 0).length,
      reviews: kpiBase.reduce((s, o) => s + (o.reviewsCount || 0), 0),
      reviewGrowth,
      critical: kpiBase.filter((o) => o.rating != null && o.rating < 3.5).length,
    };
  }, [kpiBase]);

  // Data chart — masing-masing mengecualikan dimensi filternya sendiri.
  const histoData = React.useMemo(() => applyFilters(outlets, { band: true }), [outlets, applyFilters]);
  const brandChart = React.useMemo(() => {
    const base = applyFilters(outlets, { brand: true });
    const m = new Map();
    for (const o of base) {
      const e = m.get(o.brandKey) || { key: o.brandKey, label: o.brandKey, value: 0, ratingSum: 0, rated: 0 };
      e.value++;
      if (o.rating != null) { e.ratingSum += o.rating; e.rated++; }
      m.set(o.brandKey, e);
    }
    return [...m.values()].map((e) => ({ ...e, avg: e.rated ? e.ratingSum / e.rated : null }))
      .sort((a, b) => b.value - a.value);
  }, [outlets, applyFilters]);
  const provChart = React.useMemo(() => {
    const base = applyFilters(outlets, { prov: true, city: true });
    const m = new Map();
    for (const o of base) {
      const key = o.province || '(tidak terdeteksi)';
      const e = m.get(key) || { key: o.province || 'all', label: key, value: 0, ratingSum: 0, rated: 0, real: !!o.province };
      e.value++;
      if (o.rating != null) { e.ratingSum += o.rating; e.rated++; }
      m.set(key, e);
    }
    return [...m.values()].filter((e) => e.real).map((e) => ({ ...e, avg: e.rated ? e.ratingSum / e.rated : null }))
      .sort((a, b) => b.value - a.value);
  }, [outlets, applyFilters]);

  const filtered = React.useMemo(() => {
    const list = applyFilters(outlets);
    return [...list].sort((a, b) => {
      if (sort === 'worst') return (a.rating ?? 6) - (b.rating ?? 6);
      if (sort === 'best') return (b.rating ?? -1) - (a.rating ?? -1);
      if (sort === 'drop') return (a.deltaRating ?? 0) - (b.deltaRating ?? 0);
      if (sort === 'growth') return (b.deltaReviews ?? 0) - (a.deltaReviews ?? 0);
      if (sort === 'score') return (a.score ?? 101) - (b.score ?? 101);
      return new Date(b.lastSeen) - new Date(a.lastSeen); // recent
    });
  }, [outlets, applyFilters, sort]);

  React.useEffect(() => { setPage(1); }, [brandFilter, provFilter, cityFilter, bandFilter, deltaFilter, q, sort]);

  const totalPages = Math.max(1, Math.ceil(filtered.length / PER_PAGE));
  const currentPage = Math.min(page, totalPages);
  const start = (currentPage - 1) * PER_PAGE;
  const visible = filtered.slice(start, start + PER_PAGE);

  // Chip filter aktif dari KPI/chart (yang tidak kelihatan di dropdown)
  const extraChips = [];
  if (bandFilter !== 'all') {
    extraChips.push({
      label: bandFilter === 'critical' ? '⚠️ Rating < 3.5' : `⭐ ${RATING_BUCKETS.find((b) => b.key === bandFilter)?.label ?? bandFilter}`,
      clear: () => setBandFilter('all'),
    });
  }
  if (deltaFilter !== 'all') {
    extraChips.push({
      label: deltaFilter === 'up' ? '📈 Rating naik' : '📉 Rating turun',
      clear: () => setDeltaFilter('all'),
    });
  }

  return (
    <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
      <TopBar title="Outlet Database" subtitle="Semua outlet hasil scrape — pantau perkembangan rating & review per outlet" />
      <div className="scroll" style={{ flex: 1, overflow: 'auto', padding: 24, background: 'var(--bg)' }}>
        <div style={{ maxWidth: 1100, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 14 }}>

          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            <div className="tabs" style={{ flex: 1 }}>
              <button className={`tab ${tab === 'dash' ? 'active' : ''}`} onClick={() => setTab('dash')}>
                📊 Dashboard
              </button>
              <button className={`tab ${tab === 'bad' ? 'active' : ''}`} onClick={() => setTab('bad')}>
                🚨 Review Jelek
              </button>
            </div>
            <a
              href="/api/admin/outlets/report"
              target="_blank"
              rel="noreferrer"
              className="btn btn-primary"
              style={{ padding: '7px 14px', fontSize: 12, textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: 6 }}
              title="Laporan kinerja semua outlet (AI insight + ranking skor + komplain 30 hari) — buka di tab baru lalu Save as PDF"
            >
              🖨️ Laporan PDF
            </a>
          </div>

          {tab === 'bad' && <BadReviewMonitor brands={brands} />}

          {tab === 'dash' && (<>
          {error && (
            <div style={{ padding: 12, borderRadius: 10, background: 'var(--danger-soft)', color: 'var(--danger)', fontSize: 13 }}>{error}</div>
          )}

          {loading ? (
            // Skeleton meniru layout final (bukan spinner)
            <>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 10 }}>
                {[...Array(6)].map((_, i) => <div key={i} className="shimmer" style={{ height: 74, borderRadius: 12 }} />)}
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 14 }}>
                {[...Array(4)].map((_, i) => <div key={i} className="shimmer" style={{ height: 190, borderRadius: 12 }} />)}
              </div>
              <div className="shimmer" style={{ height: 320, borderRadius: 12 }} />
            </>
          ) : outlets.length === 0 ? (
            <div className="card" style={{ padding: 48, textAlign: 'center' }}>
              <div style={{ fontSize: 40, marginBottom: 10 }}>🏪</div>
              <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 6 }}>Belum ada outlet terekam</div>
              <div style={{ fontSize: 13, color: 'var(--ink-3)', maxWidth: 460, margin: '0 auto', lineHeight: 1.55 }}>
                Jalankan <b>Audit</b> atau <b>Cari Outlet</b> di menu Brand Audit — setiap scan otomatis
                merekam snapshot outlet ke database ini. Scan berkala (misal mingguan) untuk melihat trend.
              </div>
            </div>
          ) : (
            <>
              {/* ── 1. KPI row (klik = filter) ───────────────────── */}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 10 }}>
                <KpiCard icon="🏪" label="Total Outlet" value={kpi.total} sub={`${brands.length} brand`} />
                <KpiCard icon="⭐" label="Rata-rata Rating" value={kpi.avg != null ? kpi.avg.toFixed(2) : '—'} sub="rata-rata sederhana antar outlet" />
                <KpiCard
                  icon="📈" label="Rating Naik" value={kpi.up} tone="good"
                  sub="▲ vs scan sebelumnya"
                  active={deltaFilter === 'up'}
                  onClick={() => setDeltaFilter(deltaFilter === 'up' ? 'all' : 'up')}
                />
                <KpiCard
                  icon="📉" label="Rating Turun" value={kpi.down} tone="bad"
                  sub="▼ vs scan sebelumnya"
                  active={deltaFilter === 'down'}
                  onClick={() => setDeltaFilter(deltaFilter === 'down' ? 'all' : 'down')}
                />
                <KpiCard
                  icon="💬" label="Total Review" value={kpi.reviews.toLocaleString('id-ID')}
                  sub={kpi.reviewGrowth !== 0 ? `${kpi.reviewGrowth > 0 ? '▲ +' : '▼ '}${kpi.reviewGrowth.toLocaleString('id-ID')} sejak scan sebelumnya` : 'di Google Maps'}
                />
                <KpiCard
                  icon="⚠️" label="Kritis (<3.5)" value={kpi.critical} tone={kpi.critical > 0 ? 'bad' : 'good'}
                  sub="prioritas perbaikan"
                  active={bandFilter === 'critical'}
                  onClick={() => setBandFilter(bandFilter === 'critical' ? 'all' : 'critical')}
                />
              </div>

              {/* ── 2. Filter bar ────────────────────────────────── */}
              <div className="card" style={{ padding: '10px 14px', display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
                <input
                  className="input"
                  value={search}
                  onChange={(e) => setSearch(e.target.value)}
                  placeholder="Cari nama outlet atau alamat…"
                  style={{ fontSize: 12.5, flex: 1, minWidth: 170 }}
                />
                <select className="select" value={brandFilter} onChange={(e) => setBrandFilter(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
                  <option value="all">Semua brand ({brands.length})</option>
                  {brands.map((b) => <option key={b} value={b}>{b}</option>)}
                </select>
                <select className="select" value={provFilter} onChange={(e) => setProvFilter(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
                  <option value="all">Semua provinsi ({provinces.length})</option>
                  {provinces.map((p) => <option key={p} value={p}>{p}</option>)}
                </select>
                <select className="select" value={cityFilter} onChange={(e) => setCityFilter(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
                  <option value="all">Semua kota ({cities.length})</option>
                  {cities.map((c) => <option key={c} value={c}>{c}</option>)}
                </select>
                {allLabels.length > 0 && (
                  <select className="select" value={labelFilter} onChange={(e) => setLabelFilter(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
                    <option value="all">Semua label</option>
                    {allLabels.map((l) => <option key={l} value={l}>🏷 {l}</option>)}
                  </select>
                )}
                <select className="select" value={sort} onChange={(e) => setSort(e.target.value)} style={{ fontSize: 12.5, width: 'auto' }}>
                  <option value="worst">Rating terendah</option>
                  <option value="best">Rating tertinggi</option>
                  <option value="score">Score terendah</option>
                  <option value="drop">Drop rating terbesar</option>
                  <option value="growth">Review tumbuh tercepat</option>
                  <option value="recent">Terakhir di-scan</option>
                </select>
                <button
                  className="btn btn-soft"
                  style={{ padding: '7px 12px', fontSize: 11.5 }}
                  onClick={() => downloadOutletsCsv(filtered)}
                  disabled={filtered.length === 0}
                  title="Download daftar outlet (sesuai filter aktif) sebagai CSV — buka langsung di Excel"
                >⬇ Excel</button>
                <button className="btn btn-ghost" style={{ padding: '7px 12px', fontSize: 11.5 }} onClick={load} title="Muat ulang data">↻</button>
              </div>

              {/* Chip filter dari KPI/chart */}
              {extraChips.length > 0 && (
                <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginTop: -6 }}>
                  {extraChips.map((c, i) => (
                    <button key={i} onClick={c.clear} className="btn btn-soft" style={{ padding: '3px 10px', fontSize: 11, fontWeight: 700 }}>
                      {c.label} ✕
                    </button>
                  ))}
                </div>
              )}

              {/* ── 3. Chart grid (klik = filter) ────────────────── */}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))', gap: 14 }}>
                <RatingHistogram outlets={histoData} activeBand={bandFilter} onPick={setBandFilter} />
                <TrendPanel trend={trend} brandFilter={brandFilter} />
                <TopBarsCard
                  title="🏷️ Per Brand"
                  subtitle="Jumlah outlet + rata-rata rating — klik untuk filter"
                  items={brandChart}
                  activeKey={brandFilter}
                  onPick={setBrandFilter}
                />
                <TopBarsCard
                  title="🗺️ Per Provinsi"
                  subtitle="Jumlah outlet + rata-rata rating — klik untuk filter"
                  items={provChart}
                  activeKey={provFilter}
                  onPick={setProvFilter}
                />
              </div>

              {/* ── 4. Tabel detail ──────────────────────────────── */}
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div style={{ display: 'grid', gridTemplateColumns: '38px 1fr', borderBottom: '1px solid var(--line)' }}>
                  <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
                    title="Pilih semua outlet di halaman ini">
                    <input
                      type="checkbox"
                      checked={visible.length > 0 && visible.every((o) => selected.has(o.dataId))}
                      onChange={(e) => {
                        setSelected((prev) => {
                          const next = new Set(prev);
                          for (const o of visible) e.target.checked ? next.add(o.dataId) : next.delete(o.dataId);
                          return next;
                        });
                      }}
                    />
                  </label>
                  <div style={{
                    display: 'grid', gridTemplateColumns: '1fr 76px 96px 118px 82px 20px', gap: 10,
                    padding: '10px 16px 10px 0', fontSize: 10, fontWeight: 700, color: 'var(--ink-4)',
                    textTransform: 'uppercase', letterSpacing: '0.05em',
                  }}>
                    <span>Outlet ({filtered.length})</span>
                    <span>Score</span>
                    <span>Rating</span>
                    <span>Review</span>
                    <span style={{ textAlign: 'right' }}>Scan</span>
                    <span />
                  </div>
                </div>

                {visible.length === 0 && (
                  <div style={{ padding: 28, textAlign: 'center', color: 'var(--ink-3)', fontSize: 12.5 }}>
                    Tidak ada outlet cocok dengan filter — klik chip/chart aktif untuk menghapus filter.
                  </div>
                )}
                {visible.map((o) => {
                  const isOpen = expandedId === o.dataId;
                  const isSel = selected.has(o.dataId);
                  return (
                    <div key={o.dataId}>
                      <div style={{
                        display: 'grid', gridTemplateColumns: '38px 1fr', alignItems: 'stretch',
                        borderTop: '1px solid var(--line-2)',
                        background: isSel ? 'var(--primary-50)' : isOpen ? 'var(--bg-2)' : 'var(--bg)',
                      }}>
                        <label style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
                          <input type="checkbox" checked={isSel} onChange={() => toggleSelect(o.dataId)} />
                        </label>
                        <button
                          onClick={() => setExpandedId(isOpen ? null : o.dataId)}
                          aria-expanded={isOpen}
                          style={{
                            width: '100%', padding: '10px 16px 10px 0', textAlign: 'left',
                            background: 'transparent', border: 'none', cursor: 'pointer',
                            display: 'grid', gridTemplateColumns: '1fr 76px 96px 118px 82px 20px', gap: 10, alignItems: 'center',
                          }}
                        >
                          <div style={{ minWidth: 0 }}>
                            <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, minWidth: 0 }}>
                              <span style={{ fontSize: 13, fontWeight: 600, color: 'var(--ink)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{o.name}</span>
                              {(o.labels || []).map((l) => (
                                <span key={l} style={{ fontSize: 9.5, padding: '1px 7px', borderRadius: 999, background: 'var(--tone-info-bg)', color: 'var(--tone-info-fg)', fontWeight: 700, whiteSpace: 'nowrap' }}>
                                  🏷 {l}
                                </span>
                              ))}
                            </div>
                            <div style={{ fontSize: 11, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 2 }}>
                              <b style={{ color: 'var(--ink-2)' }}>{o.brandKey}</b>
                              {' · '}{o.city ? `${o.city}, ${o.province}` : o.region}
                              <span style={{ color: 'var(--ink-4)' }}>{o.address ? ` · ${o.address}` : ''}</span>
                            </div>
                          </div>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 5 }} title={scoreTitle(o)}>
                            <span className="mono" style={{
                              fontSize: 11, fontWeight: 800, padding: '2px 7px', borderRadius: 6,
                              background: gradeTone(o.grade).bg, color: gradeTone(o.grade).fg,
                            }}>{o.grade ?? '—'}</span>
                            <span className="mono" style={{ fontSize: 12.5, fontWeight: 700, color: gradeTone(o.grade).fg }}>
                              {o.score ?? ''}
                            </span>
                          </div>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
                            <span className="mono" style={{ fontSize: 13.5, fontWeight: 700, color: ratingColor(o.rating) }}>
                              {o.rating?.toFixed(1) ?? '—'}
                            </span>
                            <OutletDeltaBadge delta={o.deltaRating} />
                          </div>
                          <div style={{ display: 'flex', alignItems: 'center', gap: 5, flexWrap: 'wrap' }}>
                            <span className="mono" style={{ fontSize: 12, color: 'var(--ink-2)' }}>
                              {o.reviewsCount.toLocaleString('id-ID')}
                            </span>
                            <OutletDeltaBadge delta={o.deltaReviews} digits={0} />
                            {o.storedReviews > 0 && (
                              <span title={`${o.storedReviews} review tersimpan di database`} style={{ fontSize: 9.5, padding: '1px 5px', borderRadius: 4, background: 'var(--primary-50)', color: 'var(--primary)', fontWeight: 700 }}>
                                💾{o.storedReviews}
                              </span>
                            )}
                          </div>
                          <div style={{ textAlign: 'right', fontSize: 10.5, color: 'var(--ink-4)', lineHeight: 1.4 }}>
                            {o.snapshots}× · {ageStrAudit(Date.now() - new Date(o.lastSeen).getTime())}
                          </div>
                          <span style={{ fontSize: 10, color: 'var(--ink-4)' }}>{isOpen ? '▲' : '▼'}</span>
                        </button>
                      </div>
                      {isOpen && <OutletTimeline outlet={o} />}
                    </div>
                  );
                })}

                {totalPages > 1 && (
                  <div style={{ padding: '10px 16px', borderTop: '1px solid var(--line)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                    <button className="btn btn-ghost" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={currentPage === 1} style={{ padding: '6px 12px', fontSize: 11.5 }}>
                      ← Sebelumnya
                    </button>
                    <span style={{ fontSize: 11.5, color: 'var(--ink-3)' }}>
                      Halaman <b style={{ color: 'var(--ink-2)' }}>{currentPage}</b> dari {totalPages}
                    </span>
                    <button className="btn btn-ghost" onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} style={{ padding: '6px 12px', fontSize: 11.5 }}>
                      Selanjutnya →
                    </button>
                  </div>
                )}
              </div>
              {/* ── Action bar seleksi massal (sticky) ───────────── */}
              {selected.size > 0 && (
                <div className="card" style={{
                  position: 'sticky', bottom: 14, zIndex: 30,
                  display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap',
                  padding: '10px 14px', boxShadow: 'var(--shadow-lg)', border: '1px solid var(--primary-100)',
                }}>
                  <span style={{ fontSize: 12.5, fontWeight: 700 }}>
                    ✓ {selected.size} outlet dipilih
                  </span>
                  <button className="btn btn-ghost" style={{ padding: '4px 10px', fontSize: 11.5 }} onClick={clearSelection}>
                    ✕ batal
                  </button>
                  <span style={{ flex: 1 }} />
                  <input
                    className="input"
                    list="outlet-label-suggestions"
                    value={labelInput}
                    onChange={(e) => setLabelInput(e.target.value)}
                    placeholder='nama label — "Prioritas", "Sudah dikunjungi"…'
                    style={{ fontSize: 12, width: 220, padding: '7px 10px' }}
                    maxLength={40}
                  />
                  <datalist id="outlet-label-suggestions">
                    {allLabels.map((l) => <option key={l} value={l} />)}
                  </datalist>
                  <button className="btn btn-soft" style={{ padding: '7px 12px', fontSize: 11.5 }} onClick={() => doLabel('add')} disabled={bulkBusy}>
                    🏷 Kasih Label
                  </button>
                  <button className="btn btn-ghost" style={{ padding: '7px 12px', fontSize: 11.5 }} onClick={() => doLabel('remove')} disabled={bulkBusy} title="Lepas label bernama ini dari outlet terpilih">
                    Lepas Label
                  </button>
                  <ConfirmButton
                    label={`🗑 Hapus ${selected.size}`}
                    confirmLabel={`PERMANEN hapus ${selected.size} outlet?`}
                    onConfirm={doBulkDelete}
                    disabled={bulkBusy}
                    className="btn btn-ghost"
                    style={{ padding: '7px 12px', fontSize: 11.5 }}
                    title="Hapus permanen: snapshot, review tersimpan, dan label outlet terpilih"
                  />
                </div>
              )}
            </>
          )}
          </>)}
        </div>
      </div>
      <AuditToastHost />
    </div>
  );
}

window.OutletDbView = OutletDbView;
