// Internal consignment dashboard — NOT linked from the public nav.
// Reachable only at #/consignment, gated by a password checked server-side.

const RP = new Intl.NumberFormat("id-ID");
const rp = (n) => "Rp" + RP.format(Math.round(Number(n) || 0));
const PW_KEY = "consignment_pw";

const MONTH_LABEL = (m) => {
  if (!m) return "—";
  const [y, mm] = m.split("-");
  const names = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  return `${names[Number(mm)] || mm} ${y}`;
};

// ── shared API call ──────────────────────────────────────────
async function consignmentApi(params, opts = {}) {
  const pw = sessionStorage.getItem(PW_KEY) || "";
  if (opts.method === "POST") {
    const r = await fetch("/api/consignment", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ...opts.body, password: pw }),
    });
    return { ok: r.ok, status: r.status, data: await r.json() };
  }
  const qs = new URLSearchParams({ ...params, password: pw }).toString();
  const r = await fetch("/api/consignment?" + qs);
  return { ok: r.ok, status: r.status, data: await r.json() };
}

// ── password gate ────────────────────────────────────────────
const PasswordGate = ({ onUnlock }) => {
  const [pw, setPw] = React.useState("");
  const [err, setErr] = React.useState("");
  const [busy, setBusy] = React.useState(false);

  const submit = async (e) => {
    e.preventDefault();
    setBusy(true); setErr("");
    sessionStorage.setItem(PW_KEY, pw);
    const { ok, status } = await consignmentApi({});
    setBusy(false);
    if (ok) onUnlock();
    else { sessionStorage.removeItem(PW_KEY); setErr(status === 401 ? "Password salah." : "Gagal terhubung."); }
  };

  return (
    <div style={{ minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
      <form onSubmit={submit} style={{ width: "100%", maxWidth: 360 }}>
        <div className="caption" style={{ marginBottom: 8 }}>Internal</div>
        <h1 className="d-m" style={{ margin: "0 0 24px" }}>Consignment</h1>
        <input
          type="password" value={pw} autoFocus placeholder="Password"
          onChange={(e) => setPw(e.target.value)}
          style={{ width: "100%", padding: "14px 16px", border: "1px solid var(--ink)", background: "transparent", fontFamily: "var(--mono)", fontSize: 13, letterSpacing: "0.04em", borderRadius: 0 }}
        />
        {err && <div style={{ color: "#a3402f", fontFamily: "var(--mono)", fontSize: 11, marginTop: 10 }}>{err}</div>}
        <button className="btn solid" type="submit" disabled={busy} style={{ width: "100%", marginTop: 16, justifyContent: "center" }}>
          {busy ? "…" : "Masuk"}
        </button>
      </form>
    </div>
  );
};

// ── small UI bits ────────────────────────────────────────────
const Stat = ({ label, value, sub }) => (
  <div style={{ borderTop: "1px solid var(--hair)", paddingTop: 14 }}>
    <div className="caption" style={{ marginBottom: 8 }}>{label}</div>
    <div style={{ fontFamily: "var(--serif)", fontSize: 32, lineHeight: 1 }}>{value}</div>
    {sub && <div className="mono" style={{ fontSize: 11, color: "var(--ink-mute)", marginTop: 6 }}>{sub}</div>}
  </div>
);

const DashHeader = ({ navigate, crumb }) => (
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "20px var(--pad-x)", borderBottom: "1px solid var(--hair)" }}>
    <div style={{ display: "flex", alignItems: "baseline", gap: 12 }}>
      <a href="#/consignment" onClick={(e) => { e.preventDefault(); navigate("/consignment"); }} style={{ fontFamily: "var(--serif)", fontSize: 22 }}>Consignment</a>
      {crumb && <span className="mono" style={{ fontSize: 11, color: "var(--ink-mute)" }}>/ {crumb}</span>}
    </div>
    <button className="lnk" onClick={() => { sessionStorage.removeItem(PW_KEY); window.location.reload(); }}>Logout</button>
  </div>
);

// ── overview ─────────────────────────────────────────────────
const Overview = ({ navigate }) => {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState("");
  React.useEffect(() => {
    consignmentApi({}).then(({ ok, data }) => ok ? setData(data) : setErr(data.error || "error"));
  }, []);

  if (err) return <div style={{ padding: "var(--pad-x)" }} className="mono">{err}</div>;
  if (!data) return <div style={{ padding: "var(--pad-x)" }} className="mono">Loading…</div>;

  const t = data.totals;

  // fixed display order: muku → tomorrow → larusso/facility → others (unknowns before "others")
  const consignmentRank = (c) => {
    const k = `${c.id} ${c.name} ${c.partner || ""}`.toLowerCase();
    if (k.includes("muku")) return 0;
    if (k.includes("tomorrow")) return 1;
    if (k.includes("larusso") || k.includes("facility")) return 2;
    if (k.includes("others")) return 99;
    return 90;
  };
  const consignments = [...data.consignments].sort((a, b) => consignmentRank(a) - consignmentRank(b));

  return (
    <div className="wrap" style={{ paddingTop: 48, paddingBottom: 80 }}>
      <div className="caption">Ringkasan semua consignment</div>
      <h1 className="d-l" style={{ margin: "8px 0 40px" }}>Overview</h1>

      <div className="grid12" style={{ gap: 24, marginBottom: 56 }}>
        <div className="col-span-3"><Stat label="Total Omzet (net)" value={rp(t.gross)} /></div>
        <div className="col-span-3"><Stat label="Total Qty" value={RP.format(t.qty)} sub="botol terjual" /></div>
        <div className="col-span-3"><Stat label="Bagian Onethird" value={rp(t.onethird)} /></div>
        <div className="col-span-3"><Stat label="Komisi Ko Stef (8%)" value={rp(t.kostef)} /></div>
      </div>

      <div className="caption" style={{ marginBottom: 16 }}>Per consignment</div>
      <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13 }}>
        <thead>
          <tr style={{ textAlign: "left", color: "var(--ink-mute)", fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase" }}>
            <th style={{ padding: "10px 8px 10px 0", borderBottom: "1px solid var(--hair)" }}>Consignment</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>Qty</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>Omzet</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>Onethird</th>
            <th style={{ padding: "10px 0 10px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>Ko Stef</th>
          </tr>
        </thead>
        <tbody>
          {consignments.map((c) => (
            <tr key={c.id} style={{ cursor: "pointer" }} onClick={() => navigate("/consignment/" + c.id)}>
              <td style={{ padding: "14px 8px 14px 0", borderBottom: "1px solid var(--hair)" }}>
                <span style={{ fontFamily: "var(--serif)", fontSize: 18 }}>{c.name}</span>
                {c.partner && <span style={{ color: "var(--ink-mute)", marginLeft: 8 }}>· {c.partner}</span>}
              </td>
              <td style={{ padding: "14px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{RP.format(c.qty)}</td>
              <td style={{ padding: "14px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(c.gross)}</td>
              <td style={{ padding: "14px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(c.onethird)}</td>
              <td style={{ padding: "14px 0 14px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(c.kostef)}</td>
            </tr>
          ))}
          {!consignments.length && (
            <tr><td colSpan={5} style={{ padding: 24, color: "var(--ink-mute)" }}>Belum ada consignment. Jalankan schema.sql di Supabase.</td></tr>
          )}
        </tbody>
      </table>
    </div>
  );
};

// ── upload panel ─────────────────────────────────────────────
// onethird products are the same across every consignment, so variant is derived from
// whatever a partner's export provides: COLOUR, the product title, the SKU barcode, or ITEM.
const VARIANT_BY_COLOUR = { PINK: "Holy Dust", ORANGE: "Land of The Sun", GREEN: "Eve's Garden" };
const VARIANT_BY_ITEM = { "68380045": "Holy Dust", "68380063": "Land of The Sun", "68380054": "Eve's Garden" };
const VARIANT_BY_SKU = { "01": "Holy Dust", "02": "Land of The Sun", "03": "Eve's Garden" }; // 953331<NN>0130 barcode
const VARIANT_NAMES = ["Holy Dust", "Land of The Sun", "Eve's Garden"];
const normName = (s) => String(s || "").toUpperCase().replace(/[^A-Z0-9 ]/g, " ").replace(/\s+/g, " ").trim();

function deriveVariant({ variant, colour, title, sku, item }) {
  if (variant) return String(variant).trim();
  const col = String(colour || "").trim().toUpperCase();
  if (VARIANT_BY_COLOUR[col]) return VARIANT_BY_COLOUR[col];
  const t = normName(title);
  if (t) {
    const byName = VARIANT_NAMES.find((n) => t.includes(normName(n)));
    if (byName) return byName;
    for (const c in VARIANT_BY_COLOUR) if (t.includes(c)) return VARIANT_BY_COLOUR[c];
  }
  for (const s of [sku, item]) {
    const bar = String(s || "").match(/953331(\d{2})/); // SKU barcode → colour code
    if (bar && VARIANT_BY_SKU[bar[1]]) return VARIANT_BY_SKU[bar[1]];
  }
  const it = String(item || "").replace(/\.0$/, "").trim();
  if (VARIANT_BY_ITEM[it]) return VARIANT_BY_ITEM[it];
  // last resort: an SKU/title that literally holds the variant name (e.g. summary rows)
  const byNameAny = VARIANT_NAMES.find((n) => normName(sku).includes(normName(n)));
  return byNameAny || null;
}

const MONTHS_MAP = { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, mei: 5, jun: 6, jul: 7, aug: 8, agu: 8, agt: 8, sep: 9, oct: 10, okt: 10, nov: 11, dec: 12, des: 12 };
function toISO(v) {
  if (v instanceof Date && !isNaN(v)) return `${v.getFullYear()}-${String(v.getMonth() + 1).padStart(2, "0")}-${String(v.getDate()).padStart(2, "0")}`;
  const s = String(v == null ? "" : v).trim();
  let m = s.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/);       // dd/mm/yyyy or dd-mm-yyyy
  if (m) return `${m[3]}-${m[2].padStart(2, "0")}-${m[1].padStart(2, "0")}`;
  m = s.match(/^(\d{1,2})\s+([A-Za-z]{3,})\s+(\d{4})/);            // "05 May 2026"
  if (m) { const mn = MONTHS_MAP[m[2].slice(0, 3).toLowerCase()]; if (mn) return `${m[3]}-${String(mn).padStart(2, "0")}-${m[1].padStart(2, "0")}`; }
  m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);                          // already ISO
  if (m) return m[0];
  return null;
}

const money = (v) => {
  if (v == null) return 0;
  if (typeof v === "number") return v;
  const s = String(v).replace(/[^\d-]/g, ""); // strips "Rp", thousand-dots, spaces
  return s === "" || s === "-" ? 0 : Number(s);
};
const cleanStr = (v) => (v == null || v === "" ? null : String(v).replace(/\.0$/, "").trim());
const cleanStore = (v) => { const s = cleanStr(v); return s ? s.replace(/^Tomorrow\s*-\s*/i, "").trim() : s; };

// Column aliases across the formats we accept:
//   MuKu POS (pipe CSV)        — NET_AMT, TRANSACT_DATE, STORE_NAME, QTY, COLOUR/ITEM…
//   Tomorrow "orders" export   — Total Penjualan, Tanggal, Toko, SKU, Qty, Judul Produk…
//   Tomorrow "kasir" export    — Penjualan Bersih, Hari, Kasir, Unit terjual, Judul Produk…
const FIELDS = {
  date: ["TRANSACT_DATE", "Tanggal", "Hari", "PROCESS_DATE"],
  store: ["STORE_NAME", "Toko", "Kasir", "Location"],
  store_code: ["STORE_NO"],
  qty: ["QTY", "Qty", "Unit terjual"],
  net: ["NET_AMT", "Total Penjualan", "Penjualan Bersih", "Total Price"],
  gross: ["GROSS_AMT", "Penjualan", "Penjualan Kotor"],
  price: ["ITEM_PRICE", "Harga Dasar", "Harga", "Price"],
  trno: ["TRANSACT_NO", "Nomor Pesanan"],
  posno: ["POS_NO"],
  title: ["Judul Produk", "SKU NAME", "ITEM_DESC", "PRODUCT"],
  colour: ["COLOUR"],
  sku: ["SKU", "BARCODE"],
  item: ["ITEM"],
  variant: ["Variant"],
};

// Larusso monthly summary format (2-row header, one row per variant, no store/date).
function parseGrandTotal(aoa, row0, name) {
  const idx = (re) => row0.findIndex((h) => re.test(h));
  const skuNameCol = idx(/SKU\s*NAME/i);
  const skuCol = idx(/^SKU$/i);
  const gtCol = idx(/GRAND\s*TOTAL/i);
  const row1 = (aoa[1] || []).map((x) => String(x == null ? "" : x).trim().toUpperCase());
  let qtyCol = row1.indexOf("QTY"); if (qtyCol < 0) qtyCol = gtCol;
  let valCol = row1.indexOf("VALUE"); if (valCol < 0) valCol = gtCol + 1;

  const rows = [];
  const unknown = {};
  for (let i = 1; i < aoa.length; i++) {
    const r = aoa[i] || [];
    const skuName = r[skuNameCol];
    if (skuName == null || String(skuName).trim() === "") continue; // sub-header / TOTAL / blank rows
    const variant = deriveVariant({ title: skuName, sku: r[skuCol] });
    if (!variant) { unknown[cleanStr(skuName) || "?"] = (unknown[cleanStr(skuName) || "?"] || 0) + 1; continue; }
    const qty = money(r[qtyCol]);
    const net = money(r[valCol]);
    if (!qty && !net) continue;
    rows.push({
      store: null, store_code: null, transact_date: null, month: null, variant,
      qty, item_price: qty ? Math.round(net / qty) : 0, gross_amt: net, net_amt: net, discount: 0,
      pos_no: null, transact_no: null, source_file: name,
    });
  }
  return { rows, unknown, noDate: 0, months: [], needsMonth: rows.length > 0 };
}

async function parseSalesFile(file) {
  const name = file.name || "";
  let wb;
  if (/\.csv$/i.test(name) || file.type === "text/csv") {
    const text = await file.text();
    const firstLine = text.split(/\r?\n/)[0] || "";
    const FS = (firstLine.match(/\|/g) || []).length ? "|" : (firstLine.match(/;/g) || []).length ? ";" : ",";
    wb = XLSX.read(text, { type: "string", FS, raw: true });
  } else {
    wb = XLSX.read(await file.arrayBuffer(), { type: "array", cellDates: true });
  }
  const ws = wb.Sheets[wb.SheetNames[0]];

  // Larusso monthly summary: 2-row header (BRAND|SKU|SKU NAME|SIZE|GRAND TOTAL[QTY,VALUE]|MARGIN|TERHUTANG),
  // one aggregated row per variant, a TOTAL row, and no store/date columns.
  const aoa = XLSX.utils.sheet_to_json(ws, { header: 1, defval: null });
  const row0 = (aoa[0] || []).map((x) => String(x == null ? "" : x).trim());
  if (row0.some((h) => /GRAND\s*TOTAL/i.test(h)) && row0.some((h) => /SKU\s*NAME/i.test(h))) {
    return parseGrandTotal(aoa, row0, name);
  }

  const json = XLSX.utils.sheet_to_json(ws, { defval: null });

  // some formats (e.g. Larusso "SKU NAME|Location|QTY|Price|Total Price") have no date column;
  // those rows get month=null and the wizard asks the user to assign a month per file.
  const headers = json.length ? Object.keys(json[0]).map((k) => String(k).trim()) : [];
  const hasDateCol = FIELDS.date.some((a) => headers.includes(a));

  const rows = [];
  const unknown = {};
  let noDate = 0;
  for (const r of json) {
    const norm = {};
    for (const k in r) norm[String(k).trim()] = r[k];
    const pick = (key) => { for (const a of FIELDS[key]) if (norm[a] != null && norm[a] !== "") return norm[a]; return null; };

    if (pick("qty") == null) continue;
    const date = toISO(pick("date"));
    if (hasDateCol && !date) { noDate++; continue; } // summary/blank rows in a dated file

    const variant = deriveVariant({ variant: pick("variant"), colour: pick("colour"), title: pick("title"), sku: pick("sku"), item: pick("item") });
    if (!variant) { const c = cleanStr(pick("colour")) || cleanStr(pick("title")) || cleanStr(pick("sku")) || "?"; unknown[c] = (unknown[c] || 0) + 1; continue; }

    const net = money(pick("net"));
    const gross = money(pick("gross")) || net;
    rows.push({
      store: cleanStore(pick("store")),
      store_code: cleanStr(pick("store_code")),
      transact_date: date,
      month: date ? date.slice(0, 7) : null, // null → wizard assigns a month for this file
      variant,
      qty: money(pick("qty")),
      item_price: money(pick("price")),
      gross_amt: gross,
      net_amt: net,
      discount: Math.max(0, gross - net),
      pos_no: cleanStr(pick("posno")),
      transact_no: cleanStr(pick("trno")),
      source_file: name,
    });
  }
  const months = [...new Set(rows.map((r) => r.month).filter(Boolean))].sort();
  return { rows, unknown, noDate, months, needsMonth: !hasDateCol && rows.length > 0 };
}

// Multi-file drag & drop wizard. Each file is parsed in the browser; month is derived
// per-row from the transaction date. Uploading appends (dedup is per filename on the server).
const UploadPanel = ({ consignmentId, onDone }) => {
  const [items, setItems] = React.useState([]); // { fileName, rows, qty, net, months, unknown, noDate, error }
  const [drag, setDrag] = React.useState(false);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState("");
  const inputRef = React.useRef(null);

  const nowMonth = `${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, "0")}`;

  const addFiles = async (fileList) => {
    setMsg("");
    const files = [...fileList];
    const parsed = await Promise.all(files.map(async (file) => {
      try {
        const { rows, unknown, noDate, months, needsMonth } = await parseSalesFile(file);
        return {
          fileName: file.name,
          rows,
          qty: rows.reduce((s, r) => s + r.qty, 0),
          net: rows.reduce((s, r) => s + r.net_amt, 0),
          months,
          needsMonth,
          month: needsMonth ? nowMonth : null, // user-assigned month for dateless files
          unknownCount: Object.values(unknown).reduce((s, n) => s + n, 0),
          unknownKeys: Object.keys(unknown),
          noDate,
        };
      } catch (e) { return { fileName: file.name, rows: [], error: e.message }; }
    }));
    // de-dupe by filename within the staging list (last one wins)
    setItems((prev) => {
      const map = new Map(prev.map((it) => [it.fileName, it]));
      for (const p of parsed) map.set(p.fileName, p);
      return [...map.values()];
    });
  };

  const onDrop = (e) => { e.preventDefault(); setDrag(false); if (e.dataTransfer.files?.length) addFiles(e.dataTransfer.files); };
  const removeItem = (name) => setItems((prev) => prev.filter((it) => it.fileName !== name));
  const setItemMonth = (name, month) => setItems((prev) => prev.map((it) => (it.fileName === name ? { ...it, month, months: [month] } : it)));

  // dateless files get their month stamped from the user-picked month
  const rowsOf = (it) => (it.needsMonth ? it.rows.map((r) => ({ ...r, month: it.month })) : it.rows);
  const allRows = items.flatMap(rowsOf);
  const totQty = allRows.reduce((s, r) => s + r.qty, 0);
  const totNet = allRows.reduce((s, r) => s + r.net_amt, 0);
  const allMonths = [...new Set(allRows.map((r) => r.month).filter(Boolean))].sort();

  const submit = async () => {
    if (!allRows.length) return;
    setBusy(true); setMsg("");
    const { ok, data } = await consignmentApi({}, { method: "POST", body: { action: "upload", consignmentId, rows: allRows } });
    setBusy(false);
    if (ok) {
      setMsg(`✓ ${data.inserted} baris dari ${data.files} file masuk (${(data.months || []).map(MONTH_LABEL).join(", ")}).`);
      setItems([]); onDone && onDone();
    } else setMsg("Gagal: " + (data.error || ""));
  };

  return (
    <div style={{ marginTop: 32 }}>
      <div className="caption" style={{ marginBottom: 14 }}>Upload penjualan</div>

      <div
        onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
        onDragLeave={() => setDrag(false)}
        onDrop={onDrop}
        onClick={() => inputRef.current?.click()}
        style={{
          border: `1px dashed ${drag ? "var(--ink)" : "var(--hair-strong)"}`,
          background: drag ? "rgba(15,14,12,0.03)" : "transparent",
          padding: "40px 20px", textAlign: "center", cursor: "pointer", transition: "all .15s ease",
        }}
      >
        <div style={{ fontFamily: "var(--serif)", fontSize: 22, marginBottom: 6 }}>Drop file di sini</div>
        <div className="mono" style={{ fontSize: 11, color: "var(--ink-mute)" }}>
          atau klik untuk pilih · bisa banyak sekaligus · .csv / .xlsx
        </div>
        <input ref={inputRef} type="file" accept=".csv,.xlsx,.xls" multiple style={{ display: "none" }}
          onChange={(e) => { addFiles(e.target.files); e.target.value = ""; }} />
      </div>

      {items.length > 0 && (
        <div style={{ marginTop: 16 }}>
          {items.map((it) => (
            <div key={it.fileName} style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 12, padding: "10px 0", borderBottom: "1px solid var(--hair)", fontFamily: "var(--mono)", fontSize: 12 }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{it.fileName}</div>
                {it.error ? (
                  <div style={{ color: "#a3402f" }}>Gagal: {it.error}</div>
                ) : (
                  <div style={{ color: "var(--ink-mute)", display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                    <span>{it.rows.length} baris · {RP.format(it.qty)} botol · {rp(it.net)}</span>
                    {it.needsMonth ? (
                      <span style={{ display: "inline-flex", alignItems: "center", gap: 6, color: "#a3402f" }}>
                        · file tanpa tanggal → bulan:
                        <input type="month" value={it.month || ""} onClick={(e) => e.stopPropagation()} onChange={(e) => setItemMonth(it.fileName, e.target.value)}
                          style={{ fontFamily: "var(--mono)", fontSize: 12, padding: "4px 6px", border: "1px solid var(--hair-strong)", background: "transparent" }} />
                      </span>
                    ) : (
                      <span>· {it.months.map(MONTH_LABEL).join(", ") || "—"}</span>
                    )}
                    {it.unknownCount ? <span style={{ color: "#a3402f" }}> · {it.unknownCount} dilewati ({it.unknownKeys.join(", ")})</span> : null}
                    {!it.needsMonth && it.noDate ? <span style={{ color: "#a3402f" }}> · {it.noDate} tanpa tanggal</span> : null}
                  </div>
                )}
              </div>
              <button className="lnk" onClick={(e) => { e.stopPropagation(); removeItem(it.fileName); }}>Hapus</button>
            </div>
          ))}

          <div style={{ marginTop: 16, fontFamily: "var(--mono)", fontSize: 12 }}>
            <div><b>Total: {allRows.length} baris · {RP.format(totQty)} botol · {rp(totNet)}</b></div>
            <div style={{ color: "var(--ink-mute)", marginTop: 4 }}>Bulan terdampak: {allMonths.map(MONTH_LABEL).join(", ") || "—"}</div>
            <div style={{ color: "var(--ink-mute)", marginTop: 4 }}>File dengan nama sama akan <b>mengganti</b> datanya; file baru <b>ditambah</b>.</div>
            <button className="btn solid" style={{ marginTop: 14 }} disabled={busy || !allRows.length} onClick={submit}>
              {busy ? "Mengupload…" : `Upload ${items.filter((i) => i.rows.length).length} file`}
            </button>
          </div>
        </div>
      )}
      {msg && <div className="mono" style={{ fontSize: 12, marginTop: 14 }}>{msg}</div>}
    </div>
  );
};

// ── rules editor ─────────────────────────────────────────────
const RULE_HINT = {
  pct_of_gross: "% dari gross",
  pct_of_remainder: "% dari sisa",
  per_unit: "Rp / botol",
  fixed: "Rp (tetap)",
};

const RulesEditor = ({ consignmentId, rules, onSaved }) => {
  const [open, setOpen] = React.useState(false);
  const [draft, setDraft] = React.useState(rules);
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState("");

  React.useEffect(() => { setDraft(rules); }, [rules]);

  const setVal = (i, v) => setDraft((d) => d.map((r, idx) => (idx === i ? { ...r, value: v === "" ? 0 : Number(v) } : r)));

  const save = async () => {
    setBusy(true); setMsg("");
    const { ok, data } = await consignmentApi({}, { method: "POST", body: { action: "update_rules", consignmentId, revenue_rules: draft } });
    setBusy(false);
    if (ok) { setMsg("✓ Tersimpan."); onSaved && onSaved(); setOpen(false); }
    else setMsg("Gagal: " + (data.error || ""));
  };

  if (!open) {
    return <button className="lnk" style={{ marginTop: 14, display: "inline-block" }} onClick={() => setOpen(true)}>Edit bagi hasil →</button>;
  }
  return (
    <div style={{ border: "1px solid var(--hair)", padding: 20, marginTop: 16, maxWidth: 520 }}>
      <div className="caption" style={{ marginBottom: 14 }}>Edit aturan bagi hasil</div>
      {draft.map((r, i) => (
        <div key={r.id} style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, marginBottom: 12 }}>
          <div className="mono" style={{ fontSize: 12 }}>
            {r.label}
            <span style={{ color: "var(--ink-mute)", marginLeft: 8 }}>{RULE_HINT[r.type] || r.type}</span>
          </div>
          <input type="number" value={r.value} onChange={(e) => setVal(i, e.target.value)}
            style={{ width: 140, padding: "8px 10px", border: "1px solid var(--hair)", background: "transparent", fontFamily: "var(--mono)", fontSize: 13, textAlign: "right" }} />
        </div>
      ))}
      <div style={{ display: "flex", gap: 12, alignItems: "center", marginTop: 16 }}>
        <button className="btn solid" disabled={busy} onClick={save}>{busy ? "Menyimpan…" : "Simpan"}</button>
        <button className="lnk" onClick={() => { setDraft(rules); setOpen(false); setMsg(""); }}>Batal</button>
        {msg && <span className="mono" style={{ fontSize: 12 }}>{msg}</span>}
      </div>
    </div>
  );
};

// ── monthly chart (lightweight inline SVG — no chart lib) ────
const SHORT_MONTH = (m) => ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][Number((m || "0-0").split("-")[1])] || m;

const MonthlyChart = ({ months }) => {
  if (!months || !months.length) return null;
  const W = 600, H = 250, padL = 14, padR = 14, padT = 26, padB = 30;
  const plotW = W - padL - padR, plotH = H - padT - padB, base = padT + plotH;
  const n = months.length;
  const maxG = Math.max(...months.map((m) => m.gross), 1);
  const maxQ = Math.max(...months.map((m) => m.qty), 1);
  const slot = plotW / n;
  const bw = Math.min(slot * 0.5, 44);
  const cx = (i) => padL + slot * i + slot / 2;
  const gy = (g) => base - (g / maxG) * plotH;
  const qy = (q) => base - (q / maxQ) * plotH;
  const linePts = months.map((m, i) => `${cx(i)},${qy(m.qty)}`).join(" ");
  const jt = (v) => (v >= 1e6 ? (v / 1e6).toFixed(v >= 1e7 ? 0 : 1) + "jt" : Math.round(v / 1e3) + "rb");

  return (
    <div>
      <div className="caption" style={{ marginBottom: 12, display: "flex", gap: 18 }}>
        <span><span style={{ display: "inline-block", width: 10, height: 10, background: "rgba(15,14,12,0.14)", marginRight: 6, verticalAlign: "middle" }} />Omzet</span>
        <span><span style={{ display: "inline-block", width: 14, borderTop: "1.5px solid var(--ink)", marginRight: 6, verticalAlign: "middle" }} />Qty</span>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="xMidYMid meet" style={{ width: "100%", height: "auto", display: "block", overflow: "visible" }}>
        <line x1={padL} y1={base} x2={W - padR} y2={base} style={{ stroke: "var(--hair)" }} />
        {months.map((m, i) => (
          <rect key={"b" + m.month} x={cx(i) - bw / 2} y={gy(m.gross)} width={bw} height={Math.max(0, base - gy(m.gross))} fill="rgba(15,14,12,0.12)" />
        ))}
        {months.map((m, i) => (
          <text key={"g" + m.month} x={cx(i)} y={base - 8} textAnchor="middle" fontSize="9" style={{ fill: "var(--ink-mute)", fontFamily: "var(--mono)" }}>{jt(m.gross)}</text>
        ))}
        <polyline points={linePts} fill="none" strokeWidth="1.5" style={{ stroke: "var(--ink)" }} />
        {months.map((m, i) => (
          <g key={"q" + m.month}>
            <circle cx={cx(i)} cy={qy(m.qty)} r="3" style={{ fill: "var(--ink)" }} />
            <text x={cx(i)} y={qy(m.qty) - 9} textAnchor="middle" fontSize="10" style={{ fill: "var(--ink)", fontFamily: "var(--mono)" }}>{m.qty}</text>
            <text x={cx(i)} y={H - 8} textAnchor="middle" fontSize="10" style={{ fill: "var(--ink-mute)", fontFamily: "var(--mono)" }}>{SHORT_MONTH(m.month)}</text>
          </g>
        ))}
      </svg>
    </div>
  );
};

// editable per-month admin fee (saved on blur / Enter)
const AdminCell = ({ consignmentId, month, value, onSaved }) => {
  const [v, setV] = React.useState(String(value || 0));
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => { setV(String(value || 0)); }, [value]);
  const save = async () => {
    if (Number(v || 0) === Number(value || 0)) return;
    setBusy(true);
    const { ok } = await consignmentApi({}, { method: "POST", body: { action: "set_admin", consignmentId, month, amount: Number(v) || 0 } });
    setBusy(false);
    if (ok) onSaved && onSaved();
  };
  return (
    <input
      value={v} disabled={busy} inputMode="numeric"
      onChange={(e) => setV(e.target.value.replace(/[^\d]/g, ""))}
      onBlur={save}
      onKeyDown={(e) => { if (e.key === "Enter") e.target.blur(); }}
      title="Admin fee bulan ini (Rp)"
      style={{ width: 96, padding: "6px 8px", border: "1px solid var(--hair)", background: Number(v) ? "transparent" : "rgba(163,64,47,0.06)", fontFamily: "var(--mono)", fontSize: 12, textAlign: "right", color: "var(--ink)" }}
    />
  );
};

// ── running stock ────────────────────────────────────────────
const STOCK_LOW = 5; // ≤ this (and > 0) = hampir habis
const stockStyle = (left) => {
  if (left <= 0) return { bg: "rgba(163,64,47,0.16)", fg: "#8a2f22" };       // habis
  if (left <= STOCK_LOW) return { bg: "rgba(176,127,30,0.16)", fg: "#8a6a16" }; // hampir habis
  return { bg: "transparent", fg: "var(--ink)" };
};

const StockSection = ({ consignmentId, stock }) => {
  const [open, setOpen] = React.useState(false);
  const [form, setForm] = React.useState({ store: "", variant: "", qty: "", date: "" });
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState("");

  // store-less sales (e.g. Larusso monthly summary upload) can't be attributed to a store
  const hasStore = (s) => s.store && s.store !== "—";
  const stores = [...new Set(stock.filter(hasStore).map((s) => s.store))].sort();
  const variants = [...new Set(stock.map((s) => s.variant))].sort(
    (a, b) => VARIANT_NAMES.indexOf(a) - VARIANT_NAMES.indexOf(b)
  );
  const cell = (store, variant) => stock.find((s) => s.store === store && s.variant === variant);
  const displayed = stores.flatMap((st) => variants.map((v) => cell(st, v)).filter(Boolean));
  const outCount = displayed.filter((s) => s.left <= 0).length;
  const lowCount = displayed.filter((s) => s.left > 0 && s.left <= STOCK_LOW).length;
  const noStoreSold = stock.filter((s) => !hasStore(s)).reduce((a, s) => a + s.sold, 0);

  const addShipment = async () => {
    if (!form.store || !form.variant || !form.qty) { setMsg("Isi toko, varian, qty."); return; }
    setBusy(true); setMsg("");
    const { ok, data } = await consignmentApi({}, { method: "POST", body: { action: "add_shipment", consignmentId, store: form.store, variant: form.variant, qty: Number(form.qty), ship_date: form.date || null } });
    setBusy(false);
    if (ok) { setForm({ store: "", variant: "", qty: "", date: "" }); window.location.reload(); }
    else setMsg("Gagal: " + (data.error || ""));
  };

  if (!stock.length) return null;

  return (
    <div style={{ marginTop: 48 }}>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 16, flexWrap: "wrap" }}>
        <div className="caption">Stok berjalan (sisa = dikirim − terjual)</div>
        <div className="mono" style={{ fontSize: 11 }}>
          {outCount > 0 && <span style={{ color: "#8a2f22" }}>● {outCount} habis</span>}
          {outCount > 0 && lowCount > 0 && <span style={{ color: "var(--ink-mute)" }}> · </span>}
          {lowCount > 0 && <span style={{ color: "#8a6a16" }}>● {lowCount} hampir habis (≤{STOCK_LOW})</span>}
          {outCount === 0 && lowCount === 0 && <span style={{ color: "var(--ink-mute)" }}>semua aman</span>}
        </div>
      </div>

      <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13, marginTop: 12 }}>
        <thead>
          <tr style={{ color: "var(--ink-mute)", fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase" }}>
            <th style={{ padding: "10px 8px 10px 0", borderBottom: "1px solid var(--hair)", textAlign: "left" }}>Toko</th>
            {variants.map((v) => <th key={v} style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{v}</th>)}
          </tr>
        </thead>
        <tbody>
          {stores.map((store) => (
            <tr key={store}>
              <td style={{ padding: "10px 8px 10px 0", borderBottom: "1px solid var(--hair)" }}>{store}</td>
              {variants.map((v) => {
                const c = cell(store, v);
                const left = c ? c.left : 0;
                const st = stockStyle(left);
                return (
                  <td key={v} title={c ? `dikirim ${c.shipped} · terjual ${c.sold}` : ""} style={{ padding: "8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>
                    <span style={{ display: "inline-block", minWidth: 34, padding: "3px 8px", background: st.bg, color: st.fg, fontWeight: left <= STOCK_LOW ? 600 : 400 }}>
                      {c ? left : "—"}
                    </span>
                  </td>
                );
              })}
            </tr>
          ))}
        </tbody>
      </table>
      {noStoreSold > 0 && (
        <div className="mono" style={{ fontSize: 11, color: "var(--ink-mute)", marginTop: 10 }}>
          ⚠ {RP.format(noStoreSold)} botol terjual dari file summary (tanpa info toko) — tidak dikurangi dari stok per toko.
        </div>
      )}

      {!open ? (
        <button className="lnk" style={{ marginTop: 14, display: "inline-block" }} onClick={() => setOpen(true)}>+ Tambah pengiriman →</button>
      ) : (
        <div style={{ border: "1px solid var(--hair)", padding: 16, marginTop: 14, display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
          <select value={form.store} onChange={(e) => setForm({ ...form, store: e.target.value })} style={selStyle}>
            <option value="">Toko…</option>{stores.map((s) => <option key={s} value={s}>{s}</option>)}
          </select>
          <select value={form.variant} onChange={(e) => setForm({ ...form, variant: e.target.value })} style={selStyle}>
            <option value="">Varian…</option>{variants.map((v) => <option key={v} value={v}>{v}</option>)}
          </select>
          <input type="number" placeholder="Qty" value={form.qty} onChange={(e) => setForm({ ...form, qty: e.target.value })} style={{ ...selStyle, width: 80 }} />
          <input type="date" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} style={selStyle} />
          <button className="btn solid" disabled={busy} onClick={addShipment}>{busy ? "…" : "Tambah"}</button>
          <button className="lnk" onClick={() => { setOpen(false); setMsg(""); }}>Batal</button>
          {msg && <span className="mono" style={{ fontSize: 12, color: "#a3402f" }}>{msg}</span>}
        </div>
      )}
    </div>
  );
};
const selStyle = { fontFamily: "var(--mono)", fontSize: 12, padding: "8px 10px", border: "1px solid var(--hair)", background: "transparent" };

// manual sale entry (e.g. Ko Stef direct / "Others" consignment)
const ManualSaleForm = ({ consignmentId, onDone }) => {
  const now = new Date();
  const defMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
  const [f, setF] = React.useState({ item: "", qty: "", price: "", month: defMonth });
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState("");
  const total = (Number(f.qty) || 0) * (Number(f.price) || 0);
  const save = async () => {
    if (!f.item || !f.qty || !f.price) { setMsg("Isi item, qty, harga."); return; }
    setBusy(true); setMsg("");
    const { ok, data } = await consignmentApi({}, { method: "POST", body: { action: "add_manual_sale", consignmentId, item: f.item, qty: Number(f.qty), price: Number(f.price), month: f.month } });
    setBusy(false);
    if (ok) { setF({ item: "", qty: "", price: "", month: f.month }); onDone && onDone(); }
    else setMsg("Gagal: " + (data.error || ""));
  };
  return (
    <div style={{ marginTop: 32 }}>
      <div className="caption" style={{ marginBottom: 14 }}>Tambah penjualan manual</div>
      <div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
        <input placeholder="Item (mis. Discovery Set)" value={f.item} onChange={(e) => setF({ ...f, item: e.target.value })} style={{ ...selStyle, width: 220 }} />
        <input type="number" placeholder="Qty" value={f.qty} onChange={(e) => setF({ ...f, qty: e.target.value })} style={{ ...selStyle, width: 70 }} />
        <input type="number" placeholder="Harga satuan" value={f.price} onChange={(e) => setF({ ...f, price: e.target.value })} style={{ ...selStyle, width: 130 }} />
        <input type="month" value={f.month} onChange={(e) => setF({ ...f, month: e.target.value })} style={selStyle} />
        <span className="mono" style={{ fontSize: 12, color: "var(--ink-mute)" }}>Total: {rp(total)}</span>
        <button className="btn solid" disabled={busy} onClick={save}>{busy ? "…" : "Tambah"}</button>
        {msg && <span className="mono" style={{ fontSize: 12, color: "#a3402f" }}>{msg}</span>}
      </div>
    </div>
  );
};

// ── detail ───────────────────────────────────────────────────
const Detail = ({ id, navigate }) => {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState("");
  const [openMonth, setOpenMonth] = React.useState(null);
  const load = React.useCallback(() => {
    setData(null);
    consignmentApi({ id }).then(({ ok, data }) => ok ? setData(data) : setErr(data.error || "error"));
  }, [id]);
  React.useEffect(load, [load]);

  const deleteSale = async (sid) => {
    if (!window.confirm("Hapus baris penjualan ini?")) return;
    const { ok } = await consignmentApi({}, { method: "POST", body: { action: "delete_sale", id: sid } });
    if (ok) load();
  };

  if (err) return <div style={{ padding: "var(--pad-x)" }} className="mono">{err}</div>;
  if (!data) return <div style={{ padding: "var(--pad-x)" }} className="mono">Loading…</div>;

  const c = data.consignment;
  const t = data.total;
  // consignment-agnostic: partner = first %-of-gross cut (MDS 32% / Tomorrow 35% / …)
  const partnerDed = t.deductions.find((d) => d.type === "pct_of_gross");
  const kostef = t.deductions.find((d) => d.id === "kostef" || d.type === "pct_of_remainder")?.amount || 0;
  const sharePct = 100 - (partnerDed?.value || 0); // Onethird's gross share %
  const onethirdBersih = t.onethird;               // Net Profit − Ko Stef (yang diterima)

  return (
    <div className="wrap" style={{ paddingTop: 48, paddingBottom: 80 }}>
      <button className="lnk" onClick={() => navigate("/consignment")}>← Overview</button>
      <h1 className="d-l" style={{ margin: "16px 0 4px" }}>{c.name}</h1>
      <div className="mono" style={{ fontSize: 12, color: "var(--ink-mute)", marginBottom: 40 }}>{c.partner ? c.partner + " · " : ""}{c.note}</div>

      <div className="grid12" style={{ gap: 24, marginBottom: 16 }}>
        <div className="col-span-3"><Stat label="Total Omzet (net)" value={rp(t.gross)} /></div>
        <div className="col-span-3"><Stat label="Total Qty" value={RP.format(t.qty)} sub="botol" /></div>
        <div className="col-span-3"><Stat label="Onethird bersih" value={rp(onethirdBersih)} sub="net profit − Ko Stef" /></div>
        <div className="col-span-3"><Stat label="Komisi Ko Stef (8%)" value={rp(kostef)} /></div>
      </div>

      {/* breakdown (left) + monthly chart (right) */}
      <div className="grid12" style={{ gap: 24, marginTop: 40, alignItems: "start" }}>
        <div className="col-span-5">
          <div className="caption">Rincian bagi hasil (total)</div>
          <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13, marginTop: 12 }}>
            <tbody>
              <tr><td style={{ padding: "8px 0" }}>Gross (net penjualan)</td><td style={{ padding: "8px 0", textAlign: "right" }}>{rp(t.gross)}</td></tr>
              {(() => {
                let running = t.gross;
                const out = [];
                const sub = (key, label, val) => out.push(
                  <tr key={key} style={{ borderTop: "1px solid var(--hair)", fontWeight: 500 }}>
                    <td style={{ padding: "10px 0" }}>{label}</td>
                    <td style={{ padding: "10px 0", textAlign: "right" }}>{rp(val)}</td>
                  </tr>
                );
                t.deductions.forEach((d) => {
                  out.push(
                    <tr key={d.id} style={{ color: "var(--ink-mute)" }}>
                      <td style={{ padding: "8px 0" }}>− {d.label} {d.type === "pct_of_gross" ? `(${d.value}% gross)` : d.type === "pct_of_remainder" ? `(${d.value}% net profit)` : d.type === "per_unit" ? `(${rp(d.value)}/botol)` : ""}</td>
                      <td style={{ padding: "8px 0", textAlign: "right" }}>−{rp(d.amount)}</td>
                    </tr>
                  );
                  running -= d.amount;
                  if (d.type === "pct_of_gross") sub("o68", `Onethird (${sharePct}%)`, running); // bagian kotor Onethird
                  if (d.id === "cogs" || d.type === "per_unit") sub("np", "Net Profit", running); // Onethird − COGS
                });
                return out;
              })()}
              <tr style={{ borderTop: "1px solid var(--ink)", fontWeight: 500 }}>
                <td style={{ padding: "10px 0" }}>Onethird bersih</td><td style={{ padding: "10px 0", textAlign: "right" }}>{rp(onethirdBersih)}</td>
              </tr>
            </tbody>
          </table>
          {t.deductions.some((d) => d.id === "cogs" && d.value === 0) && (
            <div className="mono" style={{ fontSize: 11, color: "#a3402f", marginTop: 10 }}>⚠ Modal (COGS) masih 0 — klik "Edit bagi hasil" buat set modal per botol.</div>
          )}
          <RulesEditor consignmentId={id} rules={c.revenue_rules || []} onSaved={load} />
        </div>
        <div className="col-span-7">
          <div className="caption" style={{ marginBottom: 14 }}>Tren per bulan</div>
          <MonthlyChart months={data.months} />
        </div>
      </div>

      {/* running stock — only when shipments exist (e.g. not the manual "Others") */}
      {(data.shipments || []).length > 0 && <StockSection consignmentId={id} stock={data.stock || []} />}

      {/* monthly */}
      <div style={{ marginTop: 48 }} className="caption">Per bulan</div>
      <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13, marginTop: 12 }}>
        <thead>
          <tr style={{ textAlign: "right", color: "var(--ink-mute)", fontSize: 11, letterSpacing: "0.1em", textTransform: "uppercase" }}>
            <th style={{ padding: "10px 8px 10px 0", borderBottom: "1px solid var(--hair)", textAlign: "left" }}>Bulan</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Qty</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Omzet</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Onethird ({sharePct}%)</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Admin</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Net Profit</th>
            <th style={{ padding: "10px 8px", borderBottom: "1px solid var(--hair)" }}>Ko Stef</th>
            <th style={{ padding: "10px 0 10px 8px", borderBottom: "1px solid var(--hair)" }}>Onethird bersih</th>
          </tr>
        </thead>
        <tbody>
          {data.months.map((m) => {
            const mdd = (pred) => m.share.deductions.find(pred)?.amount || 0;
            const ks = mdd((d) => d.id === "kostef" || d.type === "pct_of_remainder");
            const o68 = m.gross - mdd((d) => d.type === "pct_of_gross");   // bagian kotor Onethird
            const np = m.share.onethird + ks;                             // net profit (Onethird − admin − COGS)
            const isOpen = openMonth === m.month;
            const txns = (data.sales || []).filter((s) => s.month === m.month);
            return (
              <React.Fragment key={m.month}>
              <tr>
                <td style={{ padding: "12px 8px 12px 0", borderBottom: "1px solid var(--hair)", cursor: "pointer", userSelect: "none" }} onClick={() => setOpenMonth(isOpen ? null : m.month)} title="Lihat rincian transaksi">
                  <span style={{ display: "inline-block", width: 12, color: "var(--ink-mute)" }}>{isOpen ? "▾" : "▸"}</span> {MONTH_LABEL(m.month)}
                </td>
                <td style={{ padding: "12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{RP.format(m.qty)}</td>
                <td style={{ padding: "12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(m.gross)}</td>
                <td style={{ padding: "12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(o68)}</td>
                <td style={{ padding: "8px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}><AdminCell consignmentId={id} month={m.month} value={m.admin || 0} onSaved={load} /></td>
                <td style={{ padding: "12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(np)}</td>
                <td style={{ padding: "12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(ks)}</td>
                <td style={{ padding: "12px 0 12px 8px", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(m.share.onethird)}</td>
              </tr>
              {isOpen && (
                <tr>
                  <td colSpan={8} style={{ padding: "0 0 16px", borderBottom: "1px solid var(--hair)" }}>
                    <div style={{ background: "rgba(15,14,12,0.03)", padding: "10px 14px" }}>
                      <div className="caption" style={{ marginBottom: 8 }}>Rincian transaksi {MONTH_LABEL(m.month)} · {txns.length} baris</div>
                      <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 12 }}>
                        <thead>
                          <tr style={{ color: "var(--ink-mute)", fontSize: 10, letterSpacing: "0.08em", textTransform: "uppercase" }}>
                            <th style={{ padding: "4px 8px 4px 0", textAlign: "left" }}>Tgl</th>
                            <th style={{ padding: "4px 8px", textAlign: "left" }}>Toko</th>
                            <th style={{ padding: "4px 8px", textAlign: "left" }}>Varian</th>
                            <th style={{ padding: "4px 8px", textAlign: "right" }}>Qty</th>
                            <th style={{ padding: "4px 8px", textAlign: "right" }}>Net</th>
                            <th style={{ padding: "4px 8px", textAlign: "left" }}>No. transaksi</th>
                            <th style={{ padding: "4px 0 4px 8px", textAlign: "right" }}></th>
                          </tr>
                        </thead>
                        <tbody>
                          {txns.map((s, i) => (
                            <tr key={i} style={{ borderTop: "1px solid var(--hair)" }}>
                              <td style={{ padding: "5px 8px 5px 0" }}>{s.transact_date || "—"}</td>
                              <td style={{ padding: "5px 8px" }}>{s.store || (s.source_file === "manual" ? "manual" : "—")}</td>
                              <td style={{ padding: "5px 8px" }}>{s.variant}</td>
                              <td style={{ padding: "5px 8px", textAlign: "right" }}>{RP.format(s.qty)}</td>
                              <td style={{ padding: "5px 8px", textAlign: "right" }}>{rp(s.net_amt)}</td>
                              <td style={{ padding: "5px 8px", color: "var(--ink-mute)" }}>{s.transact_no || s.pos_no || (s.source_file === "manual" ? "manual" : "—")}</td>
                              <td style={{ padding: "5px 0 5px 8px", textAlign: "right" }}>{s.source_file === "manual" && <button className="lnk" onClick={() => deleteSale(s.id)} title="Hapus baris">×</button>}</td>
                            </tr>
                          ))}
                          {!txns.length && <tr><td colSpan={7} style={{ padding: 10, color: "var(--ink-mute)" }}>Tidak ada baris.</td></tr>}
                        </tbody>
                      </table>
                    </div>
                  </td>
                </tr>
              )}
              </React.Fragment>
            );
          })}
          {!data.months.length && <tr><td colSpan={8} style={{ padding: 24, color: "var(--ink-mute)" }}>Belum ada penjualan. Upload file di bawah.</td></tr>}
        </tbody>
      </table>

      {/* by variant + stock */}
      <div className="grid12" style={{ gap: 24, marginTop: 48 }}>
        <div className="col-span-6">
          <div className="caption" style={{ marginBottom: 12 }}>Per varian</div>
          <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13 }}>
            <tbody>
              {data.byVariant.map((v) => (
                <tr key={v.key}>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)" }}>{v.key}</td>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{RP.format(v.qty)} btl</td>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(v.gross)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <div className="col-span-6">
          <div className="caption" style={{ marginBottom: 12 }}>Per toko</div>
          <table style={{ width: "100%", borderCollapse: "collapse", fontFamily: "var(--mono)", fontSize: 13 }}>
            <tbody>
              {data.byStore.map((s) => (
                <tr key={s.key}>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)" }}>{s.key}</td>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{RP.format(s.qty)} btl</td>
                  <td style={{ padding: "10px 0", borderBottom: "1px solid var(--hair)", textAlign: "right" }}>{rp(s.gross)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      <ManualSaleForm consignmentId={id} onDone={load} />
      {id !== "others" && <UploadPanel consignmentId={id} onDone={load} />}
    </div>
  );
};

// ── root ─────────────────────────────────────────────────────
const Consignment = ({ id, navigate }) => {
  const [unlocked, setUnlocked] = React.useState(!!sessionStorage.getItem(PW_KEY));
  if (!unlocked) return <PasswordGate onUnlock={() => setUnlocked(true)} />;
  return (
    <div>
      <DashHeader navigate={navigate} crumb={id || null} />
      {id ? <Detail id={id} navigate={navigate} /> : <Overview navigate={navigate} />}
    </div>
  );
};
