// view-product.jsx — 제품 분석 (Product Analysis)
// Unique names: ProductView, ProdCatCard, ProdDetailPanel, ProdModelList,
//   PT_KO, PT_EN, PROD_CAT_ORDER
// Reuses from view-overview.jsx (global scope): moneyB, cssVar,
//   Placeholder, ErrBox. Layout chrome comes from app.css classes
//   (.surface-card, .view-head, .filter-pill, .category-card, etc).

const PROD_CAT_ORDER = [
  'CARBON PARTS', 'WING', 'BUMPER', 'GRILLE', 'WIDEBODY',
  'MERCH', 'SET', 'THIRD PARTY', 'MAINPLATES', 'OTHERS',
];

const PT_KO = {
  title: '제품 분석', loading: '불러오는 중…', empty: '데이터가 없습니다.',
  all: '전체', pctTotal: '전체 비중',
  detailModels: 'TOP MODELS', detailCountries: '국가 분포',
  detailMonthly: '월별 추이', detailSubcat: '카테고리 세부 구성',
  topModels: '전체 모델 TOP 10',
  us: 'US', row: 'ROW', kr: 'KR',
  qty: 'QTY', revenue: '매출',
  salesKrw: '매출 KRW', qtyLabel: '판매 수량', qtyUnit: '개',
};
const PT_EN = {
  title: 'Product Analysis', loading: 'Loading…', empty: 'No data.',
  all: 'All', pctTotal: '% of Total',
  detailModels: 'TOP MODELS', detailCountries: 'Country Breakdown',
  detailMonthly: 'Monthly Trend', detailSubcat: 'Sub-category Breakdown',
  topModels: 'Top 10 Models',
  us: 'US', row: 'ROW', kr: 'KR',
  qty: 'QTY', revenue: 'Revenue',
  salesKrw: 'Sales KRW', qtyLabel: 'Units Sold', qtyUnit: '',
};

// ─── Main view ────────────────────────────────────────────────────────────────
function ProductView({ lang, perms }) {
  const [loading, setLoading]     = React.useState(true);
  const [allRows, setAllRows]     = React.useState([]);
  const [kpiTargets, setKpiTargets] = React.useState([]);
  const [err, setErr]             = React.useState(null);
  const [selected, setSelected]   = React.useState(null);
  const [yearFilter, setYearFilter] = React.useState('all');

  // 차트별 매출액(krw)/수량(qty) 토글 — 세션 한정(새로고침 시 krw로 리셋).
  // 여기서는 하단 '전체 모델 TOP 10' 리스트만 관리. 상세 패널 4개 차트는
  // ProdDetailPanel이 자체 상태로 관리(선택 변경 시 remount).
  const [chartMetric, setChartMetric] = React.useState({ topModels: 'krw' });
  const setMetric = (key, val) => setChartMetric(prev => ({ ...prev, [key]: val }));

  const pt = lang === 'en' ? PT_EN : PT_KO;
  const currentYear = new Date().getFullYear();

  React.useEffect(() => {
    (async () => {
      try {
        setLoading(true);
        const [rows, targets] = await Promise.all([
          SALES_DB.fetchAllSalesData(),
          SALES_DB.fetchKpiTargets(currentYear).catch(() => []),
        ]);
        setAllRows(rows);
        setKpiTargets(targets || []);
      } catch (e) {
        setErr(e?.message || String(e));
      } finally {
        setLoading(false);
      }
    })();
  }, []);

  const filteredRows = React.useMemo(() =>
    yearFilter === 'all' ? allRows : allRows.filter(r => (r.order_date || '').startsWith(yearFilter)),
    [allRows, yearFilter]
  );

  // Aggregate per category_group. Every bucket holds {krw, qty} so the
  // per-chart 매출액/수량 토글 can switch metric without re-aggregating.
  const catAgg = React.useMemo(() => {
    const m = {};
    let total = 0, totalQty = 0;
    for (const r of filteredRows) {
      const cg  = r.category_group || 'OTHERS';
      const krw = r.sales_amount_krw || 0;
      const q   = r.qty || 0;
      if (!m[cg]) m[cg] = {
        krw: 0, qty: 0, models: {}, countries: {}, months: {}, cats: {},
        US: 0, ROW: 0, KR: 0, qtyUS: 0, qtyROW: 0, qtyKR: 0,
      };
      m[cg].krw += krw;
      m[cg].qty += q;
      total += krw;
      totalQty += q;
      const mdl = (r.make || '?') + ' ' + (r.model || '?');
      if (!m[cg].models[mdl]) m[cg].models[mdl] = { krw: 0, qty: 0 };
      m[cg].models[mdl].krw += krw; m[cg].models[mdl].qty += q;
      const cty = r.country || 'Unknown';
      if (!m[cg].countries[cty]) m[cg].countries[cty] = { krw: 0, qty: 0 };
      m[cg].countries[cty].krw += krw; m[cg].countries[cty].qty += q;
      const ym = (r.order_date || '').slice(0, 7);
      if (ym) {
        if (!m[cg].months[ym]) m[cg].months[ym] = {
          US: 0, ROW: 0, KR: 0, qtyUS: 0, qtyROW: 0, qtyKR: 0,
        };
        const ch = r.channel_group || '';
        if (ch === 'US' || ch === 'ROW' || ch === 'KR') {
          m[cg].months[ym][ch] += krw;       m[cg][ch] += krw;
          m[cg].months[ym]['qty' + ch] += q; m[cg]['qty' + ch] += q;
        }
      }
      const cat = r.category || 'Other';
      if (!m[cg].cats[cat]) m[cg].cats[cat] = { krw: 0, qty: 0 };
      m[cg].cats[cat].krw += krw; m[cg].cats[cat].qty += q;
    }
    for (const cg of Object.keys(m)) m[cg].pct = total > 0 ? m[cg].krw / total * 100 : 0;
    return { groups: m, total, totalQty };
  }, [filteredRows]);

  // Top 10 models across all categories — keep both metrics per model.
  const allTopModels = React.useMemo(() => {
    const m = {};
    for (const r of filteredRows) {
      const mdl = (r.make || '?') + ' ' + (r.model || '?');
      if (!m[mdl]) m[mdl] = { krw: 0, qty: 0 };
      m[mdl].krw += r.sales_amount_krw || 0;
      m[mdl].qty += r.qty || 0;
    }
    return m;  // sorting/slicing happens in ProdModelList per metric
  }, [filteredRows]);

  if (loading) return <Placeholder msg={pt.loading}/>;
  if (err)     return <ErrBox err={err} lang={lang}/>;
  if (!filteredRows.length) return <Placeholder msg={pt.empty}/>;

  const orderedCats = [
    ...PROD_CAT_ORDER.filter(c => catAgg.groups[c]),
    ...Object.keys(catAgg.groups).filter(c => !PROD_CAT_ORDER.includes(c)).sort(),
  ];

  return (
    <div>
      {/* Header + year filter */}
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:16 }}>
        <h2 className="view-head" style={{ marginBottom:0 }}>{pt.title}</h2>
        <div style={{ display:'flex', gap:4 }}>
          {['all', '2024', '2025', '2026'].map(y => (
            <button
              key={y}
              onClick={() => { setYearFilter(y); setSelected(null); }}
              className={'filter-pill filter-pill--sm' + (yearFilter === y ? ' is-active' : '')}
            >{y === 'all' ? pt.all : y}</button>
          ))}
        </div>
      </div>

      {/* Category cards */}
      <div style={{ display:'grid', gridTemplateColumns:'repeat(auto-fill, minmax(190px, 1fr))', gap:10, marginBottom:14 }}>
        {orderedCats.map(cg => (
          <ProdCatCard
            key={cg}
            name={cg}
            stat={catAgg.groups[cg]}
            selected={selected === cg}
            onClick={() => setSelected(selected === cg ? null : cg)}
            pt={pt}
          />
        ))}
      </div>

      {/* Detail panel — keyed by selection so charts remount cleanly */}
      {selected && catAgg.groups[selected] && (
        <ProdDetailPanel
          key={selected}
          catGroup={selected}
          stat={catAgg.groups[selected]}
          total={catAgg.total}
          pt={pt}
          lang={lang}
        />
      )}

      {/* Bottom: overall top 10 models */}
      <div className="surface-card" style={{ marginTop:14 }}>
        <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:12 }}>
          <div className="section-eyebrow">{pt.topModels}</div>
          <MetricToggle value={chartMetric.topModels} onChange={v => setMetric('topModels', v)} lang={lang}/>
        </div>
        <ProdModelList
          models={allTopModels}
          metric={chartMetric.topModels}
          total={chartMetric.topModels === 'krw' ? catAgg.total : catAgg.totalQty}
          pt={pt}
        />
      </div>
    </div>
  );
}

// ─── Category card ────────────────────────────────────────────────────────────
function ProdCatCard({ name, stat, selected, onClick, pt }) {
  return (
    <div
      onClick={onClick}
      className={'category-card' + (selected ? ' is-selected' : '')}
    >
      <div className="category-card__name">{name}</div>
      <div className="category-card__value">{moneyB(stat.krw)}</div>
      <div className="category-card__pct">{stat.pct.toFixed(1)}% {pt.pctTotal}</div>
      <div className="category-card__bar">
        <div className="category-card__bar-fill" style={{ width: Math.min(stat.pct, 100) + '%' }}/>
      </div>
    </div>
  );
}

// 차트 metric별 표시 설정 — 막대·도넛 공용. krw 데이터는 억 단위로 dataset에
// 넣으므로 barScale ×1e8, qty는 원자료라 ×1. (view-overview.jsx ovMetricCfg와 동일 패턴)
function pdMetricCfg(metric, pt) {
  if (metric === 'qty') {
    return {
      label: pt.qtyLabel, barScale: 1,
      axisTick: v => Number(v).toLocaleString(),
      fmt: v => Number(v).toLocaleString() + (pt.qtyUnit || ''),
    };
  }
  return {
    label: pt.salesKrw, barScale: 1e8,
    axisTick: v => (Math.round(v * 10) / 10).toLocaleString() + '억',  // float 오차 제거 + 1자리 반올림
    fmt: v => moneyB(v),
  };
}

// 정렬 헬퍼 — {k:{krw,qty}} 맵을 선택 metric 기준으로 내림차순 정렬·슬라이스.
function pdSortByMetric(obj, metric, limit) {
  const arr = Object.entries(obj)
    .map(([k, v]) => [k, metric === 'qty' ? (v.qty || 0) : (v.krw || 0)])
    .sort((a, b) => b[1] - a[1]);
  return limit ? arr.slice(0, limit) : arr;
}

// ─── Detail panel (4 charts) ──────────────────────────────────────────────────
function ProdDetailPanel({ catGroup, stat, total, pt, lang }) {
  const modelsRef    = React.useRef(null);
  const countriesRef = React.useRef(null);
  const monthlyRef   = React.useRef(null);
  const subcatRef    = React.useRef(null);
  const chartsRef    = React.useRef({});
  const safeId = catGroup.replace(/[^a-zA-Z0-9]/g, '-');

  // 차트별 매출액(krw)/수량(qty) 토글 — 세션 한정. 패널이 catGroup별로
  // remount(key={selected})되므로 카테고리 전환 시 자연스럽게 krw로 리셋.
  const [chartMetric, setChartMetric] = React.useState({
    models: 'krw', country: 'krw', monthly: 'krw', subcat: 'krw',
  });
  const setMetric = (key, val) => setChartMetric(prev => ({ ...prev, [key]: val }));

  // 1. Top-8 models horizontal bar — own effect, deps include only its metric.
  React.useEffect(() => {
    if (!modelsRef.current) return;
    const fg = cssVar('--fg'), fgMuted = cssVar('--fg-muted');
    const gridC = 'rgba(128,128,128,0.12)';
    const id = 'pd-m-' + safeId;
    const prev = chartsRef.current[id];
    if (prev) { try { prev.destroy(); } catch (_) {} }
    const metric = chartMetric.models;
    const cfg = pdMetricCfg(metric, pt);
    const topModels = pdSortByMetric(stat.models, metric, 8);
    if (!topModels.length) return;
    const c = new Chart(modelsRef.current, {
      type: 'bar',
      data: {
        labels: topModels.map(([k]) => k),
        datasets: [{
          label: cfg.label,
          data: topModels.map(([, v]) => metric === 'krw' ? Math.round(v / 1e7) / 10 : v),
          backgroundColor: '#007AFFcc', borderRadius: 4,
        }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        indexAxis: 'y',
        plugins: {
          legend: { display: false },
          tooltip: { callbacks: { label: ctx => ' ' + cfg.fmt(ctx.raw * cfg.barScale) } },
        },
        scales: {
          x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: cfg.axisTick } },
          y: { grid: { display: false }, ticks: { color: fg, font: { size: 10 } } },
        },
      },
    });
    chartsRef.current[id] = c;
    return () => { try { c.destroy(); } catch (_) {} };
  }, [catGroup, stat, lang, chartMetric.models]);

  // 2. Top-8 countries horizontal bar — own effect.
  React.useEffect(() => {
    if (!countriesRef.current) return;
    const fg = cssVar('--fg'), fgMuted = cssVar('--fg-muted');
    const gridC = 'rgba(128,128,128,0.12)';
    const id = 'pd-c-' + safeId;
    const prev = chartsRef.current[id];
    if (prev) { try { prev.destroy(); } catch (_) {} }
    const metric = chartMetric.country;
    const cfg = pdMetricCfg(metric, pt);
    const topCty = pdSortByMetric(stat.countries, metric, 8);
    if (!topCty.length) return;
    const c = new Chart(countriesRef.current, {
      type: 'bar',
      data: {
        labels: topCty.map(([k]) => k),
        datasets: [{
          label: cfg.label,
          data: topCty.map(([, v]) => metric === 'krw' ? Math.round(v / 1e7) / 10 : v),
          backgroundColor: '#34C759cc', borderRadius: 4,
        }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        indexAxis: 'y',
        plugins: {
          legend: { display: false },
          tooltip: { callbacks: { label: ctx => ' ' + cfg.fmt(ctx.raw * cfg.barScale) } },
        },
        scales: {
          x: { grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: cfg.axisTick } },
          y: { grid: { display: false }, ticks: { color: fg, font: { size: 10 } } },
        },
      },
    });
    chartsRef.current[id] = c;
    return () => { try { c.destroy(); } catch (_) {} };
  }, [catGroup, stat, lang, chartMetric.country]);

  // 3. Monthly stacked bar (US/ROW/KR), last 12 months — own effect.
  React.useEffect(() => {
    if (!monthlyRef.current) return;
    const fg = cssVar('--fg'), fgMuted = cssVar('--fg-muted');
    const gridC = 'rgba(128,128,128,0.12)';
    const id = 'pd-t-' + safeId;
    const prev = chartsRef.current[id];
    if (prev) { try { prev.destroy(); } catch (_) {} }
    const metric = chartMetric.monthly;
    const cfg = pdMetricCfg(metric, pt);
    const mos = Object.keys(stat.months).sort().slice(-12);
    if (!mos.length) return;
    // krw 채널값은 억 단위로, qty는 채널별 qty 키(qtyUS 등)를 원자료로.
    const chVal = (m, ch) => metric === 'qty'
      ? (stat.months[m]['qty' + ch] || 0)
      : Math.round((stat.months[m][ch] || 0) / 1e7) / 10;
    const c = new Chart(monthlyRef.current, {
      type: 'bar',
      data: {
        labels: mos,
        datasets: [
          { label: 'US',  data: mos.map(m => chVal(m, 'US')),  backgroundColor: 'rgba(0,122,255,0.8)', borderRadius: 2 },
          { label: 'ROW', data: mos.map(m => chVal(m, 'ROW')), backgroundColor: 'rgba(255,149,0,0.8)', borderRadius: 2 },
          { label: 'KR',  data: mos.map(m => chVal(m, 'KR')),  backgroundColor: 'rgba(52,199,89,0.8)', borderRadius: 2 },
        ],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { labels: { color: fg, font: { size: 10 }, boxWidth: 10 } },
          tooltip: { callbacks: { label: ctx => ' ' + ctx.dataset.label + ': ' + cfg.fmt(ctx.raw * cfg.barScale) } },
        },
        scales: {
          x: { stacked: true, grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 } } },
          y: { stacked: true, grid: { color: gridC }, ticks: { color: fgMuted, font: { size: 10 }, callback: cfg.axisTick } },
        },
      },
    });
    chartsRef.current[id] = c;
    return () => { try { c.destroy(); } catch (_) {} };
  }, [catGroup, stat, lang, chartMetric.monthly]);

  // 4. Sub-category donut — own effect. Only renders when > 1 sub-cat.
  React.useEffect(() => {
    if (!subcatRef.current) return;
    const fg = cssVar('--fg');
    const id = 'pd-s-' + safeId;
    const prev = chartsRef.current[id];
    if (prev) { try { prev.destroy(); } catch (_) {} }
    const metric = chartMetric.subcat;
    const cfg = pdMetricCfg(metric, pt);
    const subcatList = pdSortByMetric(stat.cats, metric);
    if (subcatList.length <= 1) return;
    const palette = ['#007AFF', '#34C759', '#FF9500', '#FF2D55', '#AF52DE', '#5AC8FA', '#FFCC00', '#FF6B6B'];
    const c = new Chart(subcatRef.current, {
      type: 'doughnut',
      data: {
        labels: subcatList.map(([k]) => k),
        datasets: [{ data: subcatList.map(([, v]) => v), backgroundColor: palette, borderWidth: 0 }],
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        cutout: '65%',
        plugins: {
          legend: { position: 'bottom', labels: { color: fg, font: { size: 10 }, boxWidth: 10, padding: 6 } },
          tooltip: { callbacks: { label: ctx => ' ' + ctx.label + ': ' + cfg.fmt(ctx.raw) } },
        },
      },
    });
    chartsRef.current[id] = c;
    return () => { try { c.destroy(); } catch (_) {} };
  }, [catGroup, stat, lang, chartMetric.subcat]);

  const catTotal  = stat.krw || 1;
  const usShare   = stat.US  / catTotal * 100;
  const rowShare  = stat.ROW / catTotal * 100;
  const krShare   = stat.KR  / catTotal * 100;
  const subcatCount = Object.keys(stat.cats).length;

  return (
    <div style={{ marginBottom: 14 }}>
      {/* Panel header — kept as a borderless strip; the inner chart cards now
          own the visual chrome (was previously a single outer cardStyle wrap
          containing 4 bare divs, which made the grid feel cramped). */}
      <div style={{ display:'flex', alignItems:'center', gap:8, marginBottom:10, padding:'0 4px' }}>
        <span style={{ fontSize:14, fontWeight:700 }}>{catGroup}</span>
        <span style={{ fontSize:12, color:'var(--fg-muted)' }}>— {moneyB(stat.krw)}</span>
        <div style={{ marginLeft:'auto', display:'flex', gap:16 }}>
          {[['US', usShare, '#007AFF'], ['ROW', rowShare, '#FF9500'], ['KR', krShare, '#34C759']].map(([ch, pct, col]) => (
            <span key={ch} style={{ fontSize:11, color: col, fontWeight:600 }}>
              {ch} {pct.toFixed(1)}%
            </span>
          ))}
        </div>
      </div>

      {/* 2x2 grid of detail charts. Equal columns; each card uses
          PD_CHART_HEIGHT for visual parity. Canvases are absolute-positioned
          inside the card body so Chart.js (responsive + maintainAspectRatio:
          false) can fill the available space precisely. */}
      <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:14 }}>
        {/* Models */}
        <PdChartCard title={pt.detailModels}
          right={<MetricToggle value={chartMetric.models} onChange={v => setMetric('models', v)} lang={lang}/>}>
          <canvas ref={modelsRef} id={'pdm-' + safeId} style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </PdChartCard>

        {/* Countries */}
        <PdChartCard title={pt.detailCountries}
          right={<MetricToggle value={chartMetric.country} onChange={v => setMetric('country', v)} lang={lang}/>}>
          <canvas ref={countriesRef} id={'pdc-' + safeId} style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </PdChartCard>

        {/* Monthly trend */}
        <PdChartCard title={pt.detailMonthly}
          right={<MetricToggle value={chartMetric.monthly} onChange={v => setMetric('monthly', v)} lang={lang}/>}>
          <canvas ref={monthlyRef} id={'pdt-' + safeId} style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
        </PdChartCard>

        {/* Sub-category donut or placeholder */}
        <PdChartCard title={pt.detailSubcat}
          right={subcatCount > 1
            ? <MetricToggle value={chartMetric.subcat} onChange={v => setMetric('subcat', v)} lang={lang}/>
            : null}>
          {subcatCount > 1
            ? <canvas ref={subcatRef} id={'pds-' + safeId} style={{ position:'absolute', inset:0, width:'100%', height:'100%' }}/>
            : <div style={{ padding:'18px 4px', fontSize:12, color:'var(--fg-subtle)' }}>단일 카테고리 — 세부 구성 없음</div>
          }
        </PdChartCard>
      </div>
    </div>
  );
}

// Unified chart-card sizing for the Product Analysis tab.
// Mirrors the OvChartCard pattern from view-overview.jsx — same flex
// layout so a child <canvas> styled with absolute inset:0 fills the body
// precisely (required for Chart.js maintainAspectRatio:false).
const PD_CHART_HEIGHT      = 280;
const PD_CHART_HEIGHT_TALL = 320; // reserved for any future full-width row

// `height` controls TOTAL card height. Body is `position:relative; flex:1;
// minHeight:0` so an absolute-positioned canvas can fill the remaining
// space below the title row. `right` renders an optional control (e.g.
// MetricToggle) on the title row's far edge.
function PdChartCard({ title, children, right, height = PD_CHART_HEIGHT }) {
  return (
    <div className="chart-card chart-card--snug" style={{ height }}>
      <div className="chart-card__title chart-card__title--sm"
        style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap:8 }}>
        <span>{title}</span>
        {right}
      </div>
      <div className="chart-card__body">
        {children}
      </div>
    </div>
  );
}

// ─── Model list (CSS bar, no Chart.js) ───────────────────────────────────────
// `models` is a { 'MAKE Model': {krw, qty} } map. Sorting, Top-10 slicing and
// value formatting all switch on `metric` ('krw' | 'qty').
function ProdModelList({ models, metric = 'krw', total, pt }) {
  const palette = ['#007AFF','#34C759','#FF9500','#FF2D55','#AF52DE','#5AC8FA','#FFCC00','#FF6B6B','#30D158','#FF375F'];
  const fmt = v => metric === 'qty'
    ? Number(v).toLocaleString() + (pt && pt.qtyUnit ? pt.qtyUnit : '')
    : moneyB(v);
  const rows = Object.entries(models)
    .map(([name, v]) => [name, metric === 'qty' ? (v.qty || 0) : (v.krw || 0)])
    .sort((a, b) => b[1] - a[1])
    .slice(0, 10);
  const denom = total || rows.reduce((s, [, v]) => s + v, 0) || 1;
  return (
    <div style={{ display:'flex', flexDirection:'column', gap:8 }}>
      {rows.map(([name, val], i) => {
        const pct = val / denom * 100;
        return (
          <div key={name} style={{ display:'flex', alignItems:'center', gap:10 }}>
            <div style={{ width:18, fontSize:10.5, color:'var(--fg-subtle)', textAlign:'right', fontVariantNumeric:'tabular-nums' }}>{i + 1}</div>
            <div style={{ width:160, fontSize:12, color:'var(--fg-2)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }} title={name}>{name}</div>
            <div style={{ flex:1, height:6, background:'var(--surface-2)', borderRadius:99, overflow:'hidden' }}>
              <div style={{ width: Math.max(0.5, pct) + '%', height:'100%', background: palette[i % palette.length], borderRadius:99, transition:'width .35s ease' }}/>
            </div>
            <div style={{ width:64, fontSize:11.5, color:'var(--fg-muted)', textAlign:'right', fontVariantNumeric:'tabular-nums' }}>{fmt(val)}</div>
          </div>
        );
      })}
    </div>
  );
}

window.ProductView = ProductView;
