// ===== SPPG role split: Ahli Gizi / Procurement / Kepala SPPG =====
// Empat komponen baru yang menggantikan akses operasional penuh yang dulu dipunya
// role "induk"/"satelit" tunggal. Alur: gizi ajukan Perencanaan Porsi (menu 2 minggu)
// -> Kepala SPPG setuju -> procurement hitung kebutuhan bahan dari menu yang disetujui
// -> Kepala SPPG setuju -> baris SAFCC_REQUESTS nyata dibuat (window.ApprovalFlow, utils.jsx).
//
// Semua state di sini window.IDSS.MENU_CYCLES/PROCUREMENT_ORDERS in-memory + localStorage
// (pola yang sama dipakai seluruh app ini — lihat MenuOptimasi/DapurRequestKomoditas),
// plus write path Supabase nyata (window.sb, guard non-breaking bila belum aktif).
const { useState: useS_S, useMemo: useM_S, useEffect: useE_S } = React;

// ----- Helper UI: banner status & pill, konsisten dgn convention .card/.pill di styles.css
// (bukan borderColor ad-hoc di atas .card kosong — itu yang bikin tampilan lama kurang rapi,
// karena .card butuh .card-head/.card-body utk dapat padding & heading yang benar).
const SPPG_TONE = {
  info:    { bg: "var(--info-100)",   border: "var(--info)",   color: "var(--info)" },
  warn:    { bg: "var(--warn-100)",   border: "var(--warn)",   color: "oklch(0.45 0.16 55)" },
  danger:  { bg: "var(--danger-100)", border: "var(--danger)", color: "var(--danger)" },
  success: { bg: "var(--accent-100)", border: "var(--accent)", color: "var(--accent-700)" }
};
function SppgBanner({ tone = "info", title, children }) {
  const t = SPPG_TONE[tone] || SPPG_TONE.info;
  return (
    <div style={{
      padding: "12px 14px", background: t.bg, border: `1px solid ${t.border}`,
      borderRadius: "var(--radius-lg)", color: t.color, fontSize: 13, lineHeight: 1.5
    }}>
      {title && <strong style={{ display: "block", marginBottom: 2 }}>{title}</strong>}
      {children}
    </div>
  );
}
const SPPG_STATUS_META = {
  draft:              { label: "Draft",     tone: "neutral" },
  pending_approval:   { label: "Menunggu",  tone: "warn" },
  menunggu_kepala:    { label: "Menunggu",  tone: "warn" },
  active:             { label: "Aktif",     tone: "ok" },
  approved:           { label: "Disetujui", tone: "ok" },
  rejected:           { label: "Ditolak",   tone: "danger" },
  revisi:             { label: "Revisi",    tone: "danger" },
  disetujui:            { label: "Disetujui, menunggu supplier", tone: "warn" },
  dikonfirmasi_supplier: { label: "Dikonfirmasi supplier",       tone: "ok" },
  ditolak_supplier:      { label: "Ditolak supplier",            tone: "danger" },
  selesai:               { label: "Selesai",                     tone: "ok" }
};
function StatusPill({ status }) {
  const m = SPPG_STATUS_META[status] || { label: status, tone: "neutral" };
  return <span className={`pill ${m.tone}`}><span className="led"/>{m.label}</span>;
}
// Kartu ringkas utk 1 item pending di dalam Approval Inbox — sengaja bukan .card
// bersarang (dua border ganda terlihat berantakan), cukup 1 border ringan.
function SppgSubcard({ children }) {
  return (
    <div style={{ border: "1px solid var(--line)", borderRadius: "var(--radius-lg)", padding: 14, marginTop: 12 }}>
      {children}
    </div>
  );
}

// Hitung ulang report kepatuhan gizi/harga 1 cycle setelah 1 hari diedit manual (Edit Lauk) --
// dipakai supaya KPI & banner di atas tabel 10-hari langsung mencerminkan hasil edit, bukan
// angka lama dari generate otomatis.
function recomputeCycleReport(days) {
  const totalHari = days.length || 1;
  const hariGiziCompliant = days.filter(d => d.result?.nutrisiOK).length;
  const hariHargaCompliant = days.filter(d => d.result?.costOK).length;
  const totalCost = days.reduce((s, d) => s + (d.result?.totals.biaya || 0), 0);
  const avgCostPerPorsi = Math.round(totalCost / totalHari);
  const budgetMin = 9000, budgetMax = 12000;
  return {
    totalHari, hariGiziCompliant, hariHargaCompliant,
    avgCostPerPorsi,
    cycleBudgetCompliant: avgCostPerPorsi >= budgetMin && avgCostPerPorsi <= budgetMax,
    budgetMin,
    budgetMax,
    dagingHariKe: days.filter(d => d.hasDaging).map(d => d.hariKeCycle),
    susuHariKe: days.filter(d => d.hasSusu).map(d => d.hariKeCycle),
    // subsidiSilang TIDAK dihitung ulang di sini -- angka pool asli dari generate otomatis
    // (MenuEngine._placeDagingInCycle) tidak lagi valid setelah edit manual, jadi banner
    // subsidi silang sengaja hilang stlh edit (bukan bug) sampai generate ulang.
    subsidiSilang: null
  };
}
const SPPG_MILK_ING = { nama: "Susu UHT 200 ml", p: 3.4, l: 3.5, k: 4.8, s: 0, biayaPerG: 25, gMin: 200, gMax: 200, cat: "Susu" };

// Tanggal ke-n (1-indexed) dari HARI KERJA (Senin-Jumat, weekend dilewati) dihitung mulai dari
// baseDate -- dipakai kolom Tanggal di tabel Rincian 10 hari kerja, anchor-nya cycle.updatedAt
// (kapan draf ini digenerate/diperbarui terakhir), bukan tanggal submit (belum tentu diajukan).
function nthWorkingDayFrom(baseDate, n) {
  const d = new Date(baseDate);
  while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
  let count = 1;
  while (count < n) {
    d.setDate(d.getDate() + 1);
    if (d.getDay() !== 0 && d.getDay() !== 6) count++;
  }
  return d;
}

// Susun ings + resolve nama bahan dari editPicks (dropdown Edit Lauk) -> item MENU_DB nyata.
// Dipakai BERSAMA oleh pratinjau live (tiap dropdown berubah) dan handleSaveEdit, supaya
// keduanya selalu menghitung dari data yang persis sama (tidak ada rumus ganda yang bisa beda).
function buildIngsFromPicks(picks) {
  if (!picks) return null;
  const MENU_DB = window.IDSS.MENU_DB;
  const findIng = (arr, nama) => (arr || []).find(x => x.nama === nama);
  const carb = findIng(MENU_DB.carbohydrate, picks.carbohydrate);
  const protein = findIng(MENU_DB.proteinAnimal, picks.proteinAnimal);
  const plant = findIng(MENU_DB.proteinPlant, picks.proteinPlant);
  const veg = findIng(MENU_DB.vegetable, picks.vegetable);
  const fruit = findIng(MENU_DB.fruit, picks.fruit);
  if (!carb || !protein || !plant || !veg || !fruit) return null;
  const ings = [
    { ...carb, cat: "Karbohidrat" }, { ...protein, cat: "Protein Hewani" },
    { ...plant, cat: "Protein Nabati" }, { ...veg, cat: "Sayur" }, { ...fruit, cat: "Buah" }
  ];
  if (picks.susu) ings.push({ ...SPPG_MILK_ING });
  return { ings, carb, protein, plant, veg, fruit };
}

function MenuPlanner2Minggu({ role, porsi }) {
  const { KPI } = window.IDSSUI;
  const { NUTRISI_PRESETS, MENU_CYCLES, SEKOLAH_INDUK, SEKOLAH_SATELIT } = window.IDSS;
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';

  const [preset, setPreset] = useS_S("Siswa SD (4-6)");
  const presetObj = NUTRISI_PRESETS.find(p => p.label === preset) || NUTRISI_PRESETS[2];
  const [cycle, setCycle] = useS_S(null);
  const [generating, setGenerating] = useS_S(false);
  const [savedTick, setSavedTick] = useS_S(0);
  const [selectedDay, setSelectedDay] = useS_S(null);
  const [editMode, setEditMode] = useS_S(false);
  const [editPicks, setEditPicks] = useS_S(null);

  // 2026-08 perf: javascript-lp-solver (dipakai MenuEngine.optimizeDayLP) tak lagi dimuat
  // eager di semua halaman -- dimuat di background begitu layar ini dibuka (bukan blocking,
  // bukan nunggu klik "Generate") supaya siap sebelum user benar2 klik. optimizeDayLP sudah
  // punya fallback heuristik kalau solver belum sempat termuat saat diklik -- tidak ada
  // regresi, worst case cuma sekali fallback di percobaan generate pertama yang sangat cepat.
  useE_S(() => {
    if (window.solver) return;
    window.LazyScript.load('lp-solver', [
      { src: 'https://unpkg.com/javascript-lp-solver@0.4.24/prod/solver.js', fallback: 'https://cdn.jsdelivr.net/npm/javascript-lp-solver@0.4.24/prod/solver.js' }
    ], () => !!window.solver).catch(() => { /* diam -- optimizeDayLP fallback ke heuristik */ });
  }, []);

  const myCycles = useM_S(() =>
    (MENU_CYCLES || []).filter(c => c.dapurId === dapurId).slice().reverse(),
    [savedTick, dapurId]
  );
  const activeCycle = myCycles.find(c => c.status === "active");
  const pendingCycle = myCycles.find(c => c.status === "pending_approval");

  // Sekolah/posyandu AKTIF yang disuplai dapur ini (dikelola Kepala SPPG di Kelola
  // Sekolah) — hanya yang aktif ikut dihitung, supaya sekolah libur/nonaktif otomatis
  // tidak membebani target porsi menu maupun kalkulasi kebutuhan bahan di Procurement.
  const sekolahList = (dapurId === "D1" ? SEKOLAH_INDUK : SEKOLAH_SATELIT) || [];
  const sekolahAktifList = sekolahList.filter(s => s.aktif !== false);
  const totalPorsiDapur = sekolahAktifList.reduce((s, x) => s + x.siswa, 0);
  // Porsi & kebutuhan bahan HARUS dihitung dari sekolah yang segmennya cocok dgn kelompok usia
  // yang sedang di-generate (mis. preset "Siswa SMA" -> hanya SMAN 1+2 Cianjur, bukan seluruh
  // sekolah dapur termasuk SD/SMP/balita) -- generate utk 1 preset TIDAK mewakili populasi
  // dapur secara keseluruhan, karena target gizi per preset berbeda-beda per usia.
  const targetSegmen = window.PRESET_SEGMEN_MAP?.[preset] || null;
  const sekolahSegmenList = sekolahAktifList.filter(s => !targetSegmen || s.segmen === targetSegmen);
  const totalPorsiSekolah = sekolahSegmenList.reduce((s, x) => s + x.siswa, 0);

  const handleGenerate = () => {
    setGenerating(true);
    // setTimeout supaya state "Menghitung..." sempat ter-render sebelum exhaustive search
    // (ratusan optimizeDay() sinkron — lihat MenuEngine.generateWeeklyCycle di utils.jsx) jalan.
    setTimeout(() => {
      const target = { protein: presetObj.protein, lemak: presetObj.lemak, karbo: presetObj.karbo, gram: presetObj.gram, energyKcal: presetObj.energi };
      // budgetTarget TIDAK dikirim ke pencarian (biarkan default = budgetMax): mengirim target
      // lunak lebih rendah (mis. Rp8.000 utk SD 4-6) ke _scoreResult ternyata malah MENGHUKUM
      // kombinasi yang mendekati batas atas budget meski itu satu-satunya cara menutup kekurangan
      // gizi -- terverifikasi lewat smoke test bikin kepatuhan gizi turun, bukan naik. Target
      // lunak per usia tetap berguna sbg ANGKA REFERENSI (bukan penghambat pencarian). Pita harga
      // Rp9.000 (budgetMin) - Rp12.000 (budgetMax) dipakai konsisten oleh subsidi silang daging.
      const result = window.MenuEngine.generateWeeklyCycle({ target, budgetMax: 12000, budgetMin: 9000 });
      result.target = target; // disimpan agar Modal detail bisa bandingkan tanpa tergantung dropdown preset berubah
      result.updatedAt = new Date().toISOString(); // anchor tanggal per hari di tabel (hari kerja, weekend dilewati)
      setCycle(result);
      setGenerating(false);
      window.showToast?.("Perencanaan porsi 10 hari berhasil dibuat — tinjau lalu ajukan ke Kepala SPPG", "success", 4000);
    }, 30);
  };

  const handleSubmit = () => {
    if (!cycle) return;
    const id = `MC-${Date.now().toString(36).toUpperCase()}`;
    const today = new Date();
    const periodeMulai = today.toISOString().split("T")[0];
    const periodeSelesai = new Date(today.getTime() + 13 * 86400000).toISOString().split("T")[0];
    const entry = {
      id, dapurId, dapurNama, preset,
      periodeMulai, periodeSelesai,
      status: "pending_approval",
      days: cycle.days,
      report: cycle.report,
      createdBy: authedUser,
      createdAt: new Date().toISOString()
    };
    window.IDSS.MENU_CYCLES = [...(window.IDSS.MENU_CYCLES || []), entry];
    window.IDSSPersist?.scheduleSave();
    window.AuditLog?.log('menu_cycle_submitted', { cycleId: id, dapurId });
    window.MessageBus?.send(window.IDSS.MESSAGE_TYPES.MENU_CYCLE_PENDING_APPROVAL, { cycleId: id, dapurId });
    if (window.sb) {
      window.sb.from('menu_cycles').insert({
        id, dapur_id: dapurId, periode_mulai: periodeMulai, periode_selesai: periodeSelesai,
        preset, status: 'pending_approval', report: cycle.report, created_by: authedUser
      }).then(({ error }) => {
        if (error) { console.warn('[MenuPlanner2Minggu] Supabase insert menu_cycles gagal:', error.message); return; }
        const dayRows = cycle.days.map(d => ({
          cycle_id: id, hari_ke: d.hariKeCycle, tanggal: null,
          items: d.result?.ings || [], has_daging: d.hasDaging, has_susu: d.hasSusu,
          totals: d.result?.totals || {}, nutrisi_ok: !!d.result?.nutrisiOK, harga_ok: !!d.result?.costOK
        }));
        window.sb.from('menu_cycle_days').insert(dayRows).then(({ error: dErr }) => {
          if (dErr) console.warn('[MenuPlanner2Minggu] Supabase insert menu_cycle_days gagal:', dErr.message);
        });
      });
    }
    window.showToast?.("Perencanaan porsi diajukan ke Kepala SPPG untuk persetujuan", "success", 4000);
    setCycle(null);
    setSavedTick(t => t + 1);
  };

  const handleOpenDetail = (d) => { setSelectedDay(d); setEditMode(false); setEditPicks(null); };
  const handleCloseDetail = () => { setSelectedDay(null); setEditMode(false); setEditPicks(null); };
  const handleStartEdit = () => {
    setEditPicks({ ...selectedDay.picked, susu: selectedDay.hasSusu });
    setEditMode(true);
  };
  const handleCancelEdit = () => { setEditMode(false); setEditPicks(null); };

  // Pratinjau LIVE: dihitung ulang setiap dropdown Edit Lauk berubah (sebelum disimpan) supaya
  // Ahli Gizi langsung lihat dampak gizi & harga tiap kombinasi dan bisa menyesuaikan sampai
  // target terpenuhi, bukan baru tahu hasilnya setelah klik Simpan.
  const editPreview = useM_S(() => {
    if (!editMode || !editPicks || !cycle) return null;
    const built = buildIngsFromPicks(editPicks);
    if (!built) return null;
    const result = window.MenuEngine.optimizeDayLP(built.ings, cycle.target, cycle.report?.budgetMax || 12000, cycle.budgetTarget, cycle.report?.budgetMin || 9000);
    return { ...built, result };
  }, [editPicks, editMode, cycle]);

  // Edit manual lauk 1 hari (mis. ada anak alergi/pantangan khusus) — dropdown per kategori,
  // gram tetap DIOPTIMASI ULANG lewat optimizeDay yang sama dgn generate otomatis (bukan cuma
  // ganti nama bahan dgn gram lama), jadi status gizi/harga hari itu tetap akurat setelah edit.
  // Memakai editPreview yang sama dgn yang sudah ditampilkan (bukan hitung ulang) supaya angka
  // yang disimpan PERSIS sama dengan yang barusan dilihat gizi di pratinjau.
  const handleSaveEdit = () => {
    if (!editPreview || !selectedDay || !cycle) {
      window.showToast?.("Pilihan bahan tidak valid", "error");
      return;
    }
    const { result, carb, protein, plant, veg, fruit } = editPreview;
    const newDay = {
      ...selectedDay,
      picked: {
        carbohydrate: carb.nama, proteinAnimal: protein.nama, proteinPlant: plant.nama,
        vegetable: veg.nama, fruit: fruit.nama, milkML: editPicks.susu ? 200 : 0
      },
      result,
      hasDaging: protein.nama === "Daging Sapi Suwir",
      hasSusu: !!editPicks.susu,
      editedManually: true
    };
    const newDays = cycle.days.map(d => d.hariKeCycle === newDay.hariKeCycle ? newDay : d);
    const newReport = recomputeCycleReport(newDays);
    setCycle({ ...cycle, days: newDays, report: newReport });
    setSelectedDay(newDay);
    setEditMode(false);
    setEditPicks(null);

    // Daging: idealnya max 1x/siklus (10 hari, ~1 bulan) krn biayanya disubsidi silang dari
    // hari lain (lihat MenuEngine._placeDagingInCycle, utils.jsx) -- edit manual bisa
    // menyimpang dari itu, makanya diberi peringatan (bukan diblokir, keputusan tetap di gizi).
    const dagingCount = newDays.filter(d => d.hasDaging).length;
    const susuCount = newDays.filter(d => d.hasSusu).length;
    let warn = "";
    if (dagingCount > 1) warn += ` Daging kini muncul ${dagingCount}x/10 hari kerja (idealnya maks. 1x/siklus ~1 bulan, disubsidi silang dari hari lain — lihat catatan subsidi silang di ringkasan siklus).`;
    if (susuCount !== 2) warn += ` Susu kini muncul ${susuCount}x/10 hari (idealnya 1x/minggu = 2x).`;
    window.showToast?.(`Lauk hari ke-${newDay.hariKeCycle} diperbarui.${warn}`, warn ? "warn" : "success", 5000);
  };

  // Fase 2: 4 varian per hari (base/base+susu/daging/daging+susu) sudah dihitung sekali saat
  // generate (MenuEngine._buildSlotStates, sudah LP-refined) dan dibawa di d.alternatif -- ganti
  // varian tinggal pilih dari situ, TANPA optimasi ulang (beda dgn Edit Lauk manual di atas
  // yang benar2 bahan bebas & perlu re-optimize).
  const VARIANT_LABEL = { base: "Standar", baseMilk: "Standar + Susu", daging: "Daging", dagingMilk: "Daging + Susu" };
  const handleSwitchVariant = (variantKey) => {
    const v = selectedDay?.alternatif?.[variantKey];
    if (!v || !cycle) return;
    const newDay = { ...selectedDay, picked: v.picked, result: v.result, hasDaging: v.hasDaging, hasSusu: v.hasSusu, editedManually: false };
    const newDays = cycle.days.map(d => d.hariKeCycle === newDay.hariKeCycle ? newDay : d);
    const newReport = recomputeCycleReport(newDays);
    setCycle({ ...cycle, days: newDays, report: newReport });
    setSelectedDay(newDay);
    window.showToast?.(`Hari ke-${newDay.hariKeCycle} diganti ke varian "${VARIANT_LABEL[variantKey]}"`, "success", 3000);
  };

  // Rekomendasi penggantian bahan saat hari terpilih masih kurang gizi -- LP-solved beneran
  // per opsi (MenuEngine.suggestSwaps, utils.jsx), bukan tebakan. Dihitung dari kombinasi hari
  // yang sedang aktif ditampilkan (selectedDay.result.ings, sudah punya label cat yang cocok).
  const swapSuggestions = useM_S(() => {
    if (!selectedDay?.result || selectedDay.result.nutrisiOK || !cycle) return [];
    return window.MenuEngine.suggestSwaps(selectedDay.result.ings, cycle.target, cycle.report?.budgetMax || 12000, cycle.budgetTarget, 5);
  }, [selectedDay, cycle]);

  const CAT_TO_PICKED_KEY = { "Karbohidrat": "carbohydrate", "Protein Hewani": "proteinAnimal", "Protein Nabati": "proteinPlant", "Sayur": "vegetable", "Buah": "fruit" };
  const handleApplySwapSuggestion = (sugg) => {
    if (!selectedDay || !cycle) return;
    const key = CAT_TO_PICKED_KEY[sugg.category];
    const newDay = {
      ...selectedDay,
      picked: key ? { ...selectedDay.picked, [key]: sugg.to } : selectedDay.picked,
      result: sugg.trialResult,
      editedManually: true
    };
    const newDays = cycle.days.map(d => d.hariKeCycle === newDay.hariKeCycle ? newDay : d);
    const newReport = recomputeCycleReport(newDays);
    setCycle({ ...cycle, days: newDays, report: newReport });
    setSelectedDay(newDay);
    window.showToast?.(`${sugg.from} diganti ${sugg.to} untuk hari ke-${newDay.hariKeCycle}`, "success", 3000);
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Perencanaan Porsi — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>10 hari kerja (2 minggu), optimasi gizi & harga per hari sama seperti alat Optimasi Menu — rata-rata dalam pita Rp9.000-12.000/porsi per siklus, susu 1x/minggu, daging maksimal 1x/siklus (~1 bulan) dengan subsidi silang dari hari-hari lain.</p>
      </div>

      {pendingCycle && (
        <SppgBanner tone="warn" title="Menunggu persetujuan Kepala SPPG">
          Diajukan {new Date(pendingCycle.createdAt).toLocaleDateString('id-ID')} — {pendingCycle.report.hariHargaCompliant}/10 hari sesuai budget, {pendingCycle.report.hariGiziCompliant}/10 hari sesuai target gizi.
        </SppgBanner>
      )}
      {activeCycle && (
        <SppgBanner tone="success" title="Perencanaan porsi aktif">
          Periode {activeCycle.periodeMulai} s/d {activeCycle.periodeSelesai} — disetujui oleh {activeCycle.reviewedBy}.
        </SppgBanner>
      )}

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Sekolah &amp; Posyandu yang Disuplai</h2>
          <span className="tag mono">{totalPorsiSekolah.toLocaleString('id-ID')} porsi/hari untuk {preset}</span>
        </div>
        <div className="card-body" style={{ overflowX: "auto" }}>
          <p className="muted" style={{ fontSize: 12.5, marginTop: 0, marginBottom: 10 }}>
            Porsi &amp; kebutuhan bahan generate ini hanya dihitung dari sekolah bersegmen <strong>{targetSegmen || preset}</strong> (baris tebal) — sekolah segmen lain di dapur ini ({(totalPorsiDapur - totalPorsiSekolah).toLocaleString('id-ID')} porsi/hari) tidak ikut, karena target gizinya berbeda per kelompok usia.
          </p>
          <table className="data">
            <thead><tr><th>Nama</th><th>Segmen</th><th>Porsi/hari</th></tr></thead>
            <tbody>
              {sekolahAktifList.map(s => {
                const cocok = !targetSegmen || s.segmen === targetSegmen;
                return (
                  <tr key={s.id} style={cocok ? { fontWeight: 600 } : { opacity: 0.55 }}>
                    <td>{s.nama}</td><td>{s.segmen}</td><td className="num">{s.siswa.toLocaleString('id-ID')}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
          {porsi !== totalPorsiSekolah && (
            <p className="muted" style={{ fontSize: 12, marginTop: 10 }}>
              Catatan: target porsi produksi di Pengaturan ({porsi.toLocaleString('id-ID')}) berbeda dari total porsi sekolah segmen {targetSegmen || preset} ({totalPorsiSekolah.toLocaleString('id-ID')}) — Perencanaan Pemesanan memakai total sekolah segmen ini, kelola daftarnya di menu Kelola Sekolah (Kepala SPPG).
            </p>
          )}
        </div>
      </div>

      <div className="card">
        <div className="card-head"><h2 className="h2">Generate Perencanaan Porsi</h2></div>
        <div className="card-body">
          <div className="row between" style={{ flexWrap: "wrap", gap: 12 }}>
            <div className="field" style={{ marginBottom: 0, minWidth: 220 }}>
              <label>Kelompok usia</label>
              <select className="input" value={preset} onChange={e => setPreset(e.target.value)}>
                {NUTRISI_PRESETS.map(p => <option key={p.label} value={p.label}>{p.label}</option>)}
              </select>
            </div>
            <div className="row" style={{ gap: 8 }}>
              <button className="btn primary" onClick={handleGenerate} disabled={generating} style={{ minWidth: 200, justifyContent: "center" }}>
                {generating ? "Menghitung..." : "Generate 10 Hari"}
              </button>
              <button className="btn" onClick={handleSubmit} disabled={!cycle} style={{ minWidth: 160, justifyContent: "center" }}>
                Ajukan ke Kepala SPPG
              </button>
            </div>
          </div>
        </div>
      </div>

      {cycle && (
        <>
          <div className="grid cols-3" style={{ gap: 12 }}>
            <KPI label="Hari sesuai gizi" value={cycle.report.hariGiziCompliant} unit="/10" intent={cycle.report.hariGiziCompliant === 10 ? "success" : "warn"}/>
            <KPI label="Hari sesuai harga" value={cycle.report.hariHargaCompliant} unit="/10" intent={cycle.report.hariHargaCompliant === 10 ? "success" : "warn"}/>
            <KPI label="Rata-rata biaya/porsi" value={cycle.report.avgCostPerPorsi} unit="Rp" intent={cycle.report.cycleBudgetCompliant ? "success" : "danger"}/>
          </div>

          {cycle.report.subsidiSilang && (
            <SppgBanner tone="info" title={`Subsidi silang hari daging (hari ke-${cycle.report.dagingHariKe.join(',')})`}>
              Biaya hari daging Rp{cycle.report.subsidiSilang.biayaHariDagingRp.toLocaleString('id-ID')}/porsi (di atas batas bawah pita Rp{cycle.report.subsidiSilang.budgetMinRp.toLocaleString('id-ID')}) ditutupi dari tabungan Rp{cycle.report.subsidiSilang.poolDipakaiRp.toLocaleString('id-ID')} hasil hemat hari-hari yang biayanya di bawah Rp{cycle.report.subsidiSilang.budgetMinRp.toLocaleString('id-ID')} dalam siklus ini, dibatasi keras ke pagu atas Rp{cycle.report.subsidiSilang.budgetMaxRp.toLocaleString('id-ID')} — rata-rata 10 hari tetap Rp{cycle.report.avgCostPerPorsi.toLocaleString('id-ID')}/porsi ({cycle.report.cycleBudgetCompliant ? "sesuai" : "di luar"} pita Rp{cycle.report.budgetMin.toLocaleString('id-ID')}-{cycle.report.budgetMax.toLocaleString('id-ID')}).
            </SppgBanner>
          )}

          {cycle.report.hariGiziCompliant < cycle.report.totalHari && (
            <SppgBanner tone="warn" title="Sebagian hari belum penuh sesuai target gizi">
              Hasil ini sudah dioptimasi pakai LP (linear programming) — kalau tetap kurang, artinya target gizi preset ini memang tidak tercapai dalam batas Rp{(cycle.report.budgetMax || 12000).toLocaleString('id-ID')}/porsi dengan kombinasi bahan terbaik yang ada, bukan karena algoritma kurang optimal. Klik Detail pada hari yang kurang untuk lihat 4 varian lain (Standar/+Susu/Daging/Daging+Susu) atau Edit Lauk manual, atau pertimbangkan preset/katalog bahan lain.
            </SppgBanner>
          )}

          <div className="card">
            <div className="card-head">
              <h2 className="h2">Rincian 10 hari kerja</h2>
              <span className="tag mono">{preset}</span>
            </div>
            <div className="card-body" style={{ overflowX: "auto" }}>
              <p className="muted" style={{ fontSize: 11.5, marginTop: 0, marginBottom: 10 }}>Arahkan kursor ke menu untuk lihat gram tiap bahan. Klik Detail untuk rincian gizi lengkap per porsi & opsi edit lauk.</p>
              <table className="data" style={{ tableLayout: "fixed", width: "100%" }}>
                <colgroup>
                  <col style={{ width: 44 }}/><col style={{ width: 100 }}/><col/><col style={{ width: 80 }}/><col style={{ width: 90 }}/><col style={{ width: 140 }}/><col style={{ width: 76 }}/>
                </colgroup>
                <thead>
                  <tr>
                    <th>Hari</th><th>Tanggal</th><th>Menu</th><th className="num">Kalori</th><th className="num">Biaya</th><th>Status</th><th></th>
                  </tr>
                </thead>
                <tbody>
                  {cycle.days.map(d => {
                    const gramFor = (cat) => Math.round((d.result?.ings || []).find(i => i.cat === cat)?.gram || 0);
                    const menuLabel = [d.picked.carbohydrate, d.picked.proteinAnimal, d.picked.proteinPlant, d.picked.vegetable, d.picked.fruit].filter(Boolean).join(' + ');
                    const menuTitle = [
                      `Karbohidrat: ${d.picked.carbohydrate} (${gramFor("Karbohidrat")}g)`,
                      `Protein Hewani: ${d.picked.proteinAnimal} (${gramFor("Protein Hewani")}g)`,
                      `Protein Nabati: ${d.picked.proteinPlant} (${gramFor("Protein Nabati")}g)`,
                      `Sayur: ${d.picked.vegetable} (${gramFor("Sayur")}g)`,
                      `Buah: ${d.picked.fruit} (${gramFor("Buah")}g)`,
                      d.hasSusu ? `Susu: ${d.picked.milkML} ml` : null
                    ].filter(Boolean).join('\n');
                    const tanggal = nthWorkingDayFrom(cycle.updatedAt || new Date(), d.hariKeCycle);
                    return (
                      <tr key={d.hariKeCycle}>
                        <td>{d.hariKeCycle}</td>
                        <td>{tanggal.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' })}</td>
                        <td title={menuTitle} style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 0, cursor: "help" }}>
                          {menuLabel}
                        </td>
                        <td className="num">{Math.round(d.result?.totals.energiKkal || 0)}</td>
                        <td className="num">Rp{Math.round(d.result?.totals.biaya || 0).toLocaleString('id-ID')}</td>
                        <td style={{ whiteSpace: "nowrap" }}>
                          {d.result?.costOK && d.result?.nutrisiOK ? (
                            <span className="pill ok"><span className="led"/>OK</span>
                          ) : (
                            <span className="pill warn" title={d.result?.infeasibilityWarning || undefined}>
                              <span className="led"/>
                              {!d.result?.nutrisiOK
                                ? `Kurang ${[
                                    d.result?.deficits.protein >= 0.5 ? `${d.result.deficits.protein.toFixed(0)}g protein` : null,
                                    d.result?.deficits.lemak >= 0.5 ? `${d.result.deficits.lemak.toFixed(0)}g lemak` : null,
                                    d.result?.deficits.karbo >= 0.5 ? `${d.result.deficits.karbo.toFixed(0)}g karbo` : null
                                  ].filter(Boolean).join(', ')}`
                                : "Biaya lewat budget"}
                            </span>
                          )}
                        </td>
                        <td><button className="btn" style={{ padding: "4px 10px", fontSize: 12 }} onClick={() => handleOpenDetail(d)}>Detail</button></td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        </>
      )}

      {selectedDay && (
        <window.IDSSUI.Modal isOpen={true} onClose={handleCloseDetail} size="lg"
          title={`Detail Menu — Hari ke-${selectedDay.hariKeCycle} (Minggu ${selectedDay.mingguKe})`}
          footer={editMode ? (
            <>
              <button className="btn" onClick={handleCancelEdit}>Batal</button>
              <button className="btn primary" onClick={handleSaveEdit} disabled={!editPreview}>Simpan Perubahan</button>
            </>
          ) : (
            <>
              <button className="btn" onClick={handleStartEdit}>Edit Lauk</button>
              <button className="btn primary" onClick={handleCloseDetail}>Tutup</button>
            </>
          )}>
          {editMode ? (
            <div className="col" style={{ gap: 14 }}>
              <SppgBanner tone="info">Ubah bahan untuk kebutuhan khusus (mis. alergi/pantangan). Gram akan dioptimasi ulang otomatis mengikuti target gizi & budget hari ini — status gizi/harga diperbarui setelah disimpan.</SppgBanner>
              <div className="grid cols-2" style={{ gap: 12 }}>
                {[
                  { key: "carbohydrate", label: "Karbohidrat", opts: window.IDSS.MENU_DB.carbohydrate },
                  { key: "proteinAnimal", label: "Protein Hewani", opts: window.IDSS.MENU_DB.proteinAnimal },
                  { key: "proteinPlant", label: "Protein Nabati", opts: window.IDSS.MENU_DB.proteinPlant },
                  { key: "vegetable", label: "Sayur", opts: window.IDSS.MENU_DB.vegetable },
                  { key: "fruit", label: "Buah", opts: window.IDSS.MENU_DB.fruit }
                ].map(f => (
                  <div className="field" key={f.key} style={{ marginBottom: 0 }}>
                    <label>{f.label}</label>
                    <select className="input" value={editPicks?.[f.key] || ""} onChange={e => setEditPicks(p => ({ ...p, [f.key]: e.target.value }))}>
                      {f.opts.map(o => <option key={o.nama} value={o.nama}>{o.nama}</option>)}
                    </select>
                  </div>
                ))}
              </div>
              <label className="row" style={{ gap: 8, alignItems: "center", fontSize: 13.5 }}>
                <input type="checkbox" checked={!!editPicks?.susu} onChange={e => setEditPicks(p => ({ ...p, susu: e.target.checked }))}/>
                Sertakan susu (200 ml) hari ini
              </label>

              {editPreview && (
                <div className="col" style={{ gap: 12, borderTop: "1px solid var(--line)", paddingTop: 14 }}>
                  <div className="eyebrow">Pratinjau gizi &amp; harga dengan pilihan ini</div>

                  <div style={{ overflowX: "auto" }}>
                    <table className="data">
                      <thead><tr><th>Bahan</th><th>Gram/porsi</th><th>Protein</th><th>Lemak</th><th>Karbo</th><th>Biaya</th></tr></thead>
                      <tbody>
                        {(editPreview.result?.ings || []).map(ing => (
                          <tr key={ing.nama}>
                            <td>{ing.nama} <span className="muted" style={{ fontSize: 11 }}>({ing.cat})</span></td>
                            <td className="num">{Math.round(ing.gram)} g</td>
                            <td className="num">{(ing.gram * ing.p / 100).toFixed(1)} g</td>
                            <td className="num">{(ing.gram * ing.l / 100).toFixed(1)} g</td>
                            <td className="num">{(ing.gram * ing.k / 100).toFixed(1)} g</td>
                            <td className="num">Rp{Math.round(ing.gram * ing.biayaPerG).toLocaleString('id-ID')}</td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>

                  <div className="grid cols-2" style={{ gap: 12 }}>
                    <KPI label="Total energi" value={Math.round(editPreview.result?.totals.energiKkal || 0)} unit="kkal"
                      delta={`semula ${Math.round(selectedDay.result?.totals.energiKkal || 0)} kkal`}
                      deltaKind={(editPreview.result?.totals.energiKkal || 0) >= (selectedDay.result?.totals.energiKkal || 0) ? "up" : "down"}/>
                    <KPI label="Total biaya/porsi" value={Math.round(editPreview.result?.totals.biaya || 0)} unit="Rp"
                      intent={editPreview.result?.costOK ? "success" : "danger"}
                      delta={`semula Rp${Math.round(selectedDay.result?.totals.biaya || 0).toLocaleString('id-ID')}`}
                      deltaKind={(editPreview.result?.totals.biaya || 0) <= (selectedDay.result?.totals.biaya || 0) ? "down" : "up"}/>
                  </div>

                  <div style={{ overflowX: "auto" }}>
                    <table className="data">
                      <thead><tr><th>Nutrisi</th><th>Target</th><th>Semula</th><th>Dengan pilihan ini</th><th>Status</th></tr></thead>
                      <tbody>
                        {[
                          { label: "Protein", target: cycle?.target?.protein, before: selectedDay.result?.totals.protein, actual: editPreview.result?.totals.protein, deficit: editPreview.result?.deficits.protein },
                          { label: "Lemak", target: cycle?.target?.lemak, before: selectedDay.result?.totals.lemak, actual: editPreview.result?.totals.lemak, deficit: editPreview.result?.deficits.lemak },
                          { label: "Karbohidrat", target: cycle?.target?.karbo, before: selectedDay.result?.totals.karbo, actual: editPreview.result?.totals.karbo, deficit: editPreview.result?.deficits.karbo },
                          { label: "Berat porsi", target: cycle?.target?.gram, before: selectedDay.result?.totals.gram, actual: editPreview.result?.totals.gram, deficit: Math.abs((editPreview.result?.totals.gram || 0) - (cycle?.target?.gram || 0)) >= 25 ? 1 : 0 }
                        ].map(row => (
                          <tr key={row.label}>
                            <td>{row.label}</td>
                            <td className="num">{Number(row.target || 0).toFixed(1)} g</td>
                            <td className="num muted">{Number(row.before || 0).toFixed(1)} g</td>
                            <td className="num" style={{ fontWeight: 600 }}>{Number(row.actual || 0).toFixed(1)} g</td>
                            <td>{row.deficit < 0.5
                              ? <span className="pill ok"><span className="led"/>Terpenuhi</span>
                              : <span className="pill warn"><span className="led"/>Kurang {row.deficit.toFixed(1)}g</span>}
                            </td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>

                  <SppgBanner tone={editPreview.result?.nutrisiOK && editPreview.result?.costOK ? "success" : "warn"}>
                    {editPreview.result?.nutrisiOK && editPreview.result?.costOK
                      ? "Kombinasi ini memenuhi semua target gizi & harga — siap disimpan."
                      : "Kombinasi ini masih belum memenuhi sebagian target — coba ganti bahan lain di atas sebelum menyimpan."}
                  </SppgBanner>
                </div>
              )}
            </div>
          ) : (
            <div className="col" style={{ gap: 16 }}>
              {selectedDay.editedManually && (
                <SppgBanner tone="info">Lauk hari ini sudah diedit manual dari hasil generate otomatis.</SppgBanner>
              )}

              {selectedDay.alternatif && (
                <div>
                  <div className="eyebrow" style={{ marginBottom: 8 }}>Ganti varian hari ini (4 opsi, dihitung LP saat generate)</div>
                  <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
                    {Object.entries(VARIANT_LABEL).map(([key, label]) => {
                      const v = selectedDay.alternatif[key];
                      if (!v) return null;
                      const isActive = selectedDay.hasDaging === v.hasDaging && selectedDay.hasSusu === v.hasSusu && !selectedDay.editedManually;
                      return (
                        <button key={key} onClick={() => handleSwitchVariant(key)} disabled={isActive}
                          className={`btn ${isActive ? "primary" : ""}`} style={{ padding: "8px 12px", fontSize: 12, flexDirection: "column", alignItems: "flex-start", gap: 2, minWidth: 130 }}>
                          <span style={{ fontWeight: 600 }}>{isActive && "✓ "}{label}</span>
                          <span style={{ fontSize: 11, opacity: 0.85 }}>{Math.round(v.result?.totals.energiKkal || 0)} kkal · Rp{Math.round(v.result?.totals.biaya || 0).toLocaleString('id-ID')}</span>
                          <span style={{ fontSize: 11, opacity: 0.85 }}>{v.result?.nutrisiOK ? "✓ Gizi terpenuhi" : `⚠ Kurang gizi`}</span>
                        </button>
                      );
                    })}
                  </div>
                </div>
              )}

              <div style={{ overflowX: "auto" }}>
                <table className="data">
                  <thead><tr><th>Bahan</th><th>Gram/porsi</th><th>Protein</th><th>Lemak</th><th>Karbo</th><th>Biaya</th></tr></thead>
                  <tbody>
                    {(selectedDay.result?.ings || []).map(ing => (
                      <tr key={ing.nama}>
                        <td>{ing.nama} <span className="muted" style={{ fontSize: 11 }}>({ing.cat})</span></td>
                        <td className="num">{Math.round(ing.gram)} g</td>
                        <td className="num">{(ing.gram * ing.p / 100).toFixed(1)} g</td>
                        <td className="num">{(ing.gram * ing.l / 100).toFixed(1)} g</td>
                        <td className="num">{(ing.gram * ing.k / 100).toFixed(1)} g</td>
                        <td className="num">Rp{Math.round(ing.gram * ing.biayaPerG).toLocaleString('id-ID')}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>

              <div className="grid cols-2" style={{ gap: 12 }}>
                <KPI label="Total energi" value={Math.round(selectedDay.result?.totals.energiKkal || 0)} unit="kkal"/>
                <KPI label="Total biaya/porsi" value={Math.round(selectedDay.result?.totals.biaya || 0)} unit="Rp" intent={selectedDay.result?.costOK ? "success" : "danger"}/>
              </div>

              <div style={{ overflowX: "auto" }}>
                <table className="data">
                  <thead><tr><th>Nutrisi</th><th>Target</th><th>Tercapai</th><th>Status</th></tr></thead>
                  <tbody>
                    {[
                      { label: "Protein", target: cycle?.target?.protein, actual: selectedDay.result?.totals.protein, deficit: selectedDay.result?.deficits.protein },
                      { label: "Lemak", target: cycle?.target?.lemak, actual: selectedDay.result?.totals.lemak, deficit: selectedDay.result?.deficits.lemak },
                      { label: "Karbohidrat", target: cycle?.target?.karbo, actual: selectedDay.result?.totals.karbo, deficit: selectedDay.result?.deficits.karbo },
                      { label: "Berat porsi", target: cycle?.target?.gram, actual: selectedDay.result?.totals.gram, deficit: Math.abs((selectedDay.result?.totals.gram || 0) - (cycle?.target?.gram || 0)) >= 25 ? 1 : 0 }
                    ].map(row => (
                      <tr key={row.label}>
                        <td>{row.label}</td>
                        <td className="num">{Number(row.target || 0).toFixed(1)} g</td>
                        <td className="num">{Number(row.actual || 0).toFixed(1)} g</td>
                        <td>{row.deficit < 0.5
                          ? <span className="pill ok"><span className="led"/>Terpenuhi</span>
                          : <span className="pill warn"><span className="led"/>Kurang {row.deficit.toFixed(1)}g</span>}
                        </td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>

              <SppgBanner tone={selectedDay.result?.nutrisiOK && selectedDay.result?.costOK ? "success" : "warn"}>
                {selectedDay.result?.nutrisiOK && selectedDay.result?.costOK
                  ? "Semua target gizi & harga terpenuhi untuk hari ini."
                  : "Sebagian target belum terpenuhi — lihat rekomendasi penggantian bahan di bawah, atau gunakan Edit Lauk untuk mengganti bahan bebas."}
              </SppgBanner>

              {swapSuggestions.length > 0 && (
                <div style={{ border: "1px solid var(--line)", borderRadius: "var(--radius-lg)", padding: 12, background: "var(--bg-2)" }}>
                  <div style={{ fontSize: 12.5, fontWeight: 600, marginBottom: 8 }}>Rekomendasi penggantian bahan (dihitung ulang via LP, bukan tebakan)</div>
                  <div className="col" style={{ gap: 6 }}>
                    {swapSuggestions.map((sugg, i) => (
                      <div key={i} className="row between" style={{ alignItems: "center", gap: 8, fontSize: 12, padding: "6px 8px", background: "var(--surface)", borderRadius: 6 }}>
                        <div>
                          Ganti <b>{sugg.from}</b> → <b>{sugg.to}</b> ({sugg.category})
                          <div className="muted" style={{ fontSize: 11 }}>
                            Kekurangan gizi: {sugg.deficitBefore}g → {sugg.deficitAfter}g {sugg.fullyResolves && <span style={{ color: "var(--accent)" }}>(terpenuhi penuh)</span>}
                            {" · "}{sugg.costDelta >= 0 ? `+Rp${sugg.costDelta.toLocaleString('id-ID')}` : `-Rp${Math.abs(sugg.costDelta).toLocaleString('id-ID')}`}
                          </div>
                        </div>
                        <button className="btn" style={{ padding: "4px 10px", fontSize: 11.5, flexShrink: 0 }} onClick={() => handleApplySwapSuggestion(sugg)}>Terapkan</button>
                      </div>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}
        </window.IDSSUI.Modal>
      )}

      {myCycles.length > 0 && (
        <div className="card">
          <div className="card-head"><h2 className="h2">Riwayat pengajuan</h2></div>
          <div className="card-body">
            <table className="data">
              <thead><tr><th>Periode</th><th>Status</th><th>Diajukan</th><th>Catatan reviewer</th></tr></thead>
              <tbody>
                {myCycles.map(c => (
                  <tr key={c.id}>
                    <td>{c.periodeMulai} – {c.periodeSelesai}</td>
                    <td><StatusPill status={c.status}/></td>
                    <td>{new Date(c.createdAt).toLocaleDateString('id-ID')}</td>
                    <td>{c.reviewNote || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
      )}
    </div>
  );
}
window.MenuPlanner2Minggu = MenuPlanner2Minggu;

function ProcurementPlanner({ role, porsi }) {
  const { KPI } = window.IDSSUI;
  const { MENU_CYCLES, PROCUREMENT_ORDERS, NERACA, SEKOLAH_INDUK, SEKOLAH_SATELIT } = window.IDSS;
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';

  const [bufferPct, setBufferPct] = useS_S(0.1);
  const [savedTick, setSavedTick] = useS_S(0);
  const [recommended, setRecommended] = useS_S({}); // { [komoditas]: supplier_id }, dari Kepala SPPG
  const [pdfLoading, setPdfLoading] = useS_S(false); // memuat jsPDF+autotable on-demand (lazy)

  useE_S(() => {
    window.SupplierIntelligence.getRecommendations(dapurId).then(rows => {
      const map = {};
      rows.forEach(r => { map[r.komoditas] = r.supplier_id; });
      setRecommended(map);
    });
  }, [dapurId]);

  const activeCycle = useM_S(() =>
    (MENU_CYCLES || []).filter(c => c.dapurId === dapurId && c.status === "active")
      .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0] || null,
    [savedTick, dapurId]
  );

  const currentStockMap = useM_S(() => {
    const map = {};
    (NERACA || []).forEach(n => { map[n.komoditas] = n.tersedia; });
    return map;
  }, []);

  // Basis porsi utk kalkulasi kebutuhan bahan = total sekolah AKTIF sungguhan (dikelola
  // Kepala SPPG di Kelola Sekolah), BUKAN angka `porsi` manual di tweaks — supaya sekolah
  // baru/libur langsung mengubah usulan kg tanpa perlu sinkron manual di 2 tempat. DIFILTER
  // lagi ke segmen yang cocok dgn preset cycle aktif (mis. cycle "Siswa SMA" -> hanya sekolah
  // SMA) — cycle yang di-generate utk 1 kelompok usia TIDAK mewakili seluruh sekolah dapur,
  // jadi kebutuhan bahan tidak boleh dihitung dari total sekolah semua segmen.
  // (Tidak di-useMemo — array kecil, dan agar update SchoolRoster langsung ke-refresh.)
  const sekolahListForCalc = (dapurId === "D1" ? SEKOLAH_INDUK : SEKOLAH_SATELIT) || [];
  const targetSegmen = window.PRESET_SEGMEN_MAP?.[activeCycle?.preset] || null;
  const totalPorsiAktif = sekolahListForCalc
    .filter(s => s.aktif !== false && (!targetSegmen || s.segmen === targetSegmen))
    .reduce((s, x) => s + x.siswa, 0);

  const calc = useM_S(() => {
    if (!activeCycle) return null;
    return window.ProcurementCalculator.computeNeeds(activeCycle.days, totalPorsiAktif, currentStockMap, bufferPct);
  }, [activeCycle, totalPorsiAktif, currentStockMap, bufferPct]);

  const myOrders = useM_S(() =>
    (PROCUREMENT_ORDERS || []).filter(o => o.dapurId === dapurId).slice().reverse(),
    [savedTick, dapurId]
  );
  const pendingOrder = myOrders.find(o => o.status === "menunggu_kepala");

  // Titik putus yang diperbaiki: setelah Kepala approve, baris safcc_requests NYATA lahir
  // (lihat window.ApprovalFlow.approveProcurementOrder) tapi progresnya di SAFCC/Agro/Supplier
  // (pending -> pending_fulfillment -> fulfilled -> shipped) sebelumnya tak pernah terlihat
  // lagi dari sisi Procurement -- seolah menghilang begitu disetujui. Query langsung di sini.
  const [requestStatusMap, setRequestStatusMap] = useS_S({});
  const [requestDeadlineMap, setRequestDeadlineMap] = useS_S({});
  useE_S(() => {
    if (!window.sb) return;
    const allIds = myOrders.filter(o => o.status === "approved" && o.resultingRequestIds?.length)
      .flatMap(o => o.resultingRequestIds);
    if (allIds.length === 0) return;
    let cancelled = false;
    window.sb.from('safcc_requests').select('id, status, deadline').in('id', allIds).then(({ data, error }) => {
      if (cancelled || error) { if (error) console.warn('[ProcurementPlanner] gagal ambil status safcc_requests:', error.message); return; }
      const statusMap = {}, deadlineMap = {};
      (data || []).forEach(r => { statusMap[r.id] = r.status; deadlineMap[r.id] = r.deadline; });
      setRequestStatusMap(statusMap);
      setRequestDeadlineMap(deadlineMap);
    });
    return () => { cancelled = true; };
  }, [myOrders]);

  const SAFCC_STATUS_LABEL = { pending: "Menunggu SAFCC", pending_fulfillment: "Diproses supplier", fulfilled: "Dipenuhi", shipped: "Dikirim ke dapur" };
  const summarizeOrderProgress = (order) => {
    const ids = order.resultingRequestIds || [];
    if (order.status !== "approved" || ids.length === 0) return null;
    const counts = {};
    ids.forEach(id => {
      const st = requestStatusMap[id] || "…";
      counts[st] = (counts[st] || 0) + 1;
    });
    return Object.entries(counts).map(([st, n]) => `${n}× ${SAFCC_STATUS_LABEL[st] || st}`).join(', ');
  };
  // Rentang prediksi kedatangan (deadline safcc_requests, dihitung dari lead time per
  // komoditas saat Kepala SPPG menyetujui -- lihat ApprovalFlow.approveProcurementOrder) --
  // tampilkan rentang tanggal PALING CEPAT s/d PALING LAMBAT krn 1 usulan bisa berisi
  // beberapa komoditas dgn lead time berbeda.
  const summarizeOrderDeadline = (order) => {
    const ids = order.resultingRequestIds || [];
    if (order.status !== "approved" || ids.length === 0) return null;
    const dates = ids.map(id => requestDeadlineMap[id]).filter(Boolean).sort();
    if (dates.length === 0) return null;
    const fmt = (iso) => new Date(iso + "T00:00:00").toLocaleDateString('id-ID', { day: '2-digit', month: 'short' });
    return dates[0] === dates[dates.length - 1] ? fmt(dates[0]) : `${fmt(dates[0])} – ${fmt(dates[dates.length - 1])}`;
  };

  const handleSubmit = () => {
    if (!calc) return;
    const id = `PO-${Date.now().toString(36).toUpperCase()}`;
    const entry = {
      id, dapurId, dapurNama,
      menuCycleId: activeCycle.id,
      status: "menunggu_kepala",
      items: calc.rows,
      unmapped: calc.unmapped,
      bufferPct,
      createdBy: authedUser,
      createdAt: new Date().toISOString()
    };
    window.IDSS.PROCUREMENT_ORDERS = [...(window.IDSS.PROCUREMENT_ORDERS || []), entry];
    window.IDSSPersist?.scheduleSave();
    window.AuditLog?.log('procurement_order_submitted', { orderId: id, dapurId });
    window.MessageBus?.send(window.IDSS.MESSAGE_TYPES.PROCUREMENT_ORDER_PENDING_APPROVAL, { orderId: id, dapurId });
    if (window.sb) {
      window.sb.from('procurement_orders').insert({
        id, dapur_id: dapurId, menu_cycle_id: activeCycle.id, status: 'menunggu_kepala',
        items: calc.rows, buffer_pct: bufferPct, created_by: authedUser
      }).then(({ error }) => {
        if (error) console.warn('[ProcurementPlanner] Supabase insert procurement_orders gagal:', error.message);
      });
    }
    window.showToast?.("Usulan pemesanan diajukan ke Kepala SPPG", "success", 4000);
    setSavedTick(t => t + 1);
  };

  // Ekspor PDF ringkasan total bahan baku & biaya (Kepala SPPG/Procurement butuh dokumen fisik
  // utk arsip/lampiran pengajuan) -- jsPDF + autoTable TIDAK lagi dimuat di semua halaman
  // (2026-08 perf) -- dimuat on-demand persis saat tombol ini diklik pertama kali, sesudahnya
  // langsung terpakai dari cache browser.
  const handleExportPdf = async () => {
    if (!calc || !activeCycle) return;
    if (!window.jspdf) {
      setPdfLoading(true);
      try {
        await window.LazyScript.load('jspdf', [
          { src: 'https://unpkg.com/jspdf@2.5.2/dist/jspdf.umd.min.js', fallback: 'https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js' },
          { src: 'https://unpkg.com/jspdf-autotable@3.8.2/dist/jspdf.plugin.autotable.js', fallback: 'https://cdn.jsdelivr.net/npm/jspdf-autotable@3.8.2/dist/jspdf.plugin.autotable.js' }
        ], () => !!window.jspdf);
      } catch (e) {
        window.showToast?.("Gagal memuat pustaka PDF — cek koneksi internet", "error", 4000);
        setPdfLoading(false);
        return;
      }
      setPdfLoading(false);
    }
    const { jsPDF } = window.jspdf;
    const doc = new jsPDF();
    const marginX = 14;
    let y = 16;

    doc.setFontSize(14);
    doc.text("Kebutuhan Bahan Baku & Biaya", marginX, y);
    y += 7;
    doc.setFontSize(10);
    doc.setTextColor(90);
    doc.text(`${dapurNama} — Perencanaan Porsi ${activeCycle.periodeMulai} s/d ${activeCycle.periodeSelesai} (${activeCycle.preset})`, marginX, y);
    y += 5;
    doc.text(`${totalPorsiAktif.toLocaleString('id-ID')} porsi/hari · buffer stok ${Math.round(bufferPct * 100)}% · dicetak ${new Date().toLocaleDateString('id-ID')}`, marginX, y);
    y += 4;
    doc.setTextColor(0);

    doc.autoTable({
      startY: y + 4,
      margin: { left: marginX, right: marginX },
      head: [["Komoditas", "Kg/hari", "Kg/minggu", "Kg/bulan", "Kebutuhan siklus (kg)", "Stok saat ini", "Usulan order (kg)", "Prediksi tiba", "Estimasi biaya"]],
      body: calc.rows.map(r => [
        r.komoditas, r.kgHarianRataRata, r.kgMingguan, r.kgBulanan, r.kgTotalSiklus, r.stokSaatIni, r.usulanOrderKg,
        `${new Date(r.prediksiTibaTanggal + "T00:00:00").toLocaleDateString('id-ID', { day: '2-digit', month: 'short' })} (+${r.leadTimeHariKerja}h)`,
        r.totalBiayaUsulanOrderRp != null ? `Rp${r.totalBiayaUsulanOrderRp.toLocaleString('id-ID')}` : "—"
      ]),
      foot: [["Total", "", "", "", "", "", `${calc.totalKgUsulanOrder.toLocaleString('id-ID')} kg`, "", `Rp${calc.totalBiayaEstimasiRp.toLocaleString('id-ID')}`]],
      styles: { fontSize: 8.5 },
      headStyles: { fillColor: [40, 90, 70] },
      footStyles: { fillColor: [230, 230, 230], textColor: 20, fontStyle: "bold" }
    });

    let afterTableY = (doc.lastAutoTable?.finalY || y + 4) + 10;
    if (calc.unmapped.length > 0) {
      doc.setFontSize(9);
      doc.setTextColor(120);
      doc.text(`Tidak terhitung (belum ada di katalog komoditas): ${calc.unmapped.join(', ')}`, marginX, afterTableY, { maxWidth: 180 });
      doc.setTextColor(0);
    }

    const fileName = `kebutuhan-bahan-baku-${dapurId}-${activeCycle.periodeMulai}.pdf`;
    doc.save(fileName);
    window.AuditLog?.log('procurement_pdf_exported', { dapurId, cycleId: activeCycle.id });
    window.showToast?.("PDF kebutuhan bahan baku & biaya berhasil diunduh", "success", 3000);
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Perencanaan Pemesanan — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Kebutuhan dihitung dari Perencanaan Porsi yang sudah disetujui Kepala SPPG — mencegah over-order (waste).</p>
      </div>

      {!activeCycle && (
        <SppgBanner tone="danger" title="Belum ada perencanaan porsi aktif">
          Ajukan &amp; minta Ahli Gizi mendapat persetujuan Kepala SPPG dulu sebelum bisa menghitung kebutuhan bahan baku.
        </SppgBanner>
      )}
      {pendingOrder && (
        <SppgBanner tone="warn" title="Usulan menunggu persetujuan Kepala SPPG">
          Diajukan {new Date(pendingOrder.createdAt).toLocaleDateString('id-ID')}.
        </SppgBanner>
      )}

      {activeCycle && calc && (
        <>
          <div className="card">
            <div className="card-head">
              <h2 className="h2">Hitung Kebutuhan</h2>
              <span className="tag mono">{totalPorsiAktif.toLocaleString('id-ID')} porsi/hari (sekolah segmen {targetSegmen || activeCycle.preset})</span>
            </div>
            <div className="card-body">
              <p className="muted" style={{ fontSize: 12, marginTop: 0 }}>Perencanaan porsi aktif ini dibuat untuk <strong>{activeCycle.preset}</strong> — kebutuhan bahan di bawah hanya menghitung sekolah bersegmen itu, bukan seluruh sekolah dapur.</p>
              <div className="row between" style={{ flexWrap: "wrap", gap: 12 }}>
                <div className="field" style={{ marginBottom: 0, minWidth: 160 }}>
                  <label>Buffer stok (%)</label>
                  <input type="number" className="input" style={{ width: 90 }} value={Math.round(bufferPct * 100)}
                    onChange={e => setBufferPct(Math.max(0, Math.min(50, Number(e.target.value) || 0)) / 100)}/>
                  <span className="muted" style={{ fontSize: 11.5 }}>Periode {activeCycle.periodeMulai} – {activeCycle.periodeSelesai}</span>
                </div>
                <button className="btn primary" onClick={handleSubmit} disabled={!!pendingOrder} style={{ minWidth: 200, justifyContent: "center" }}>
                  Ajukan ke Kepala SPPG
                </button>
              </div>
              {porsi !== totalPorsiAktif && (
                <p className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>
                  Catatan: target porsi produksi di Pengaturan ({porsi.toLocaleString('id-ID')}) berbeda dari total porsi sekolah segmen {targetSegmen || activeCycle.preset} ({totalPorsiAktif.toLocaleString('id-ID')}) — kalkulasi di bawah memakai total sekolah segmen ini, kelola daftarnya di menu Kelola Sekolah (Kepala SPPG).
                </p>
              )}
            </div>
          </div>

          <div className="card">
            <div className="card-head">
              <h2 className="h2">1. Kebutuhan bahan baku per porsi</h2>
              <span className="tag mono">{calc.rows.length} komoditas</span>
            </div>
            <div className="card-body" style={{ overflowX: "auto" }}>
              <p className="muted" style={{ fontSize: 12.5, marginTop: 0 }}>
                Setiap 1 porsi makanan di siklus ini butuh (rata-rata, dari resep menu yang disetujui):
              </p>
              <table className="data">
                <thead>
                  <tr>
                    <th>Komoditas (bahan mentah)</th><th>Dari dish (× rendemen)</th><th>Kg mentah / porsi</th><th>Gram mentah / porsi</th>
                  </tr>
                </thead>
                <tbody>
                  {calc.rows.map(r => (
                    <tr key={r.komoditas}>
                      <td><strong>{r.komoditas}</strong></td>
                      <td style={{ fontSize: 11.5 }} className="muted">
                        {r.sumberDish.map(sd => `${sd.nama} (×${sd.rendemen})`).join(', ')}
                      </td>
                      <td className="num">{r.kgPerPorsiRataRata}</td>
                      <td className="num">{Math.round(r.kgPerPorsiRataRata * 1000)}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <p className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>
                "Kg mentah/porsi" = gram dish jadi di menu × faktor rendemen mentah→matang (mis. ayam goreng susut air saat digoreng, nasi menyerap air saat dimasak) — bukan gram dish disamakan langsung dengan bahan mentah. Faktor rendemen adalah estimasi standar dapur institusi; sebaiknya dikalibrasi ahli gizi sesuai data DKBM/Kemenkes.
              </p>
              {calc.unmapped.length > 0 && (
                <p className="muted" style={{ fontSize: 12, marginTop: 10 }}>
                  Tidak terhitung (belum ada di katalog komoditas yang bisa dipesan): {calc.unmapped.join(', ')}
                </p>
              )}
            </div>
          </div>

          <div className="card">
            <div className="card-head">
              <h2 className="h2">2. Akumulasi kebutuhan ({totalPorsiAktif.toLocaleString('id-ID')} porsi/hari)</h2>
              <span className="tag mono">per porsi × jumlah porsi</span>
            </div>
            <div className="card-body" style={{ overflowX: "auto" }}>
              <p className="muted" style={{ fontSize: 12.5, marginTop: 0 }}>
                Kg mentah/porsi (tabel 1) dikalikan jumlah porsi/hari, lalu dijumlah sesuai kebutuhan siklus — "Usulan order" adalah kebutuhan bersih (dikurangi stok berjalan) supaya tidak over-order. "Prediksi tiba" = perkiraan tanggal bahan sampai di dapur JIKA diajukan &amp; disetujui hari ini, dihitung dari lead time per komoditas (lihat catatan tabel EOQ di bawah) — tanggal final sesungguhnya baru terkunci saat Kepala SPPG benar-benar menyetujui usulan.
              </p>
              <table className="data">
                <thead>
                  <tr>
                    <th>Komoditas</th><th>Kg/hari</th><th>Kg/minggu</th><th>Kg/bulan</th><th>Kebutuhan siklus</th><th>Stok saat ini</th><th>Usulan order (kg)</th><th>Prediksi tiba</th><th>Estimasi biaya</th>
                  </tr>
                </thead>
                <tbody>
                  {calc.rows.map(r => (
                    <tr key={r.komoditas}>
                      <td>{r.komoditas}</td>
                      <td className="num">{r.kgHarianRataRata}</td>
                      <td className="num">{r.kgMingguan}</td>
                      <td className="num">{r.kgBulanan}</td>
                      <td className="num">{r.kgTotalSiklus}</td>
                      <td className="num">{r.stokSaatIni}</td>
                      <td className="num"><strong>{r.usulanOrderKg}</strong></td>
                      <td style={{ whiteSpace: "nowrap" }} title={`Lead time ${r.leadTimeHariKerja} hari kerja`}>
                        {new Date(r.prediksiTibaTanggal + "T00:00:00").toLocaleDateString('id-ID', { day: '2-digit', month: 'short' })}
                        <span className="muted" style={{ fontSize: 11 }}> (+{r.leadTimeHariKerja}h)</span>
                      </td>
                      <td className="num">{r.totalBiayaUsulanOrderRp != null ? `Rp${r.totalBiayaUsulanOrderRp.toLocaleString('id-ID')}` : "—"}</td>
                    </tr>
                  ))}
                </tbody>
                <tfoot>
                  <tr style={{ fontWeight: 600 }}>
                    <td colSpan={6}>Total</td>
                    <td className="num">{calc.totalKgUsulanOrder.toLocaleString('id-ID')} kg</td>
                    <td></td>
                    <td className="num">Rp{calc.totalBiayaEstimasiRp.toLocaleString('id-ID')}</td>
                  </tr>
                </tfoot>
              </table>
              {calc.totalBiayaTanpaHarga && (
                <p className="muted" style={{ fontSize: 11.5, marginTop: 8 }}>Sebagian komoditas belum punya harga/kg di katalog — total biaya di atas kemungkinan sedikit di bawah perkiraan sesungguhnya.</p>
              )}
            </div>
          </div>

          <div className="card">
            <div className="card-head">
              <h2 className="h2">3. Pendekatan EOQ (ukuran pesanan ekonomis)</h2>
              <span className="tag mono">biaya pesan Rp{calc.eoqParams.biayaPesanPerOrder.toLocaleString('id-ID')}/PO · simpan {Math.round(calc.eoqParams.holdingCostPct * 100)}%/tahun</span>
            </div>
            <div className="card-body" style={{ overflowX: "auto" }}>
              <p className="muted" style={{ fontSize: 12.5, marginTop: 0 }}>
                EOQ = √(2×D×S/H) menjawab pertanyaan BERBEDA dari "Usulan order" di atas: bukan "berapa kg dibutuhkan siklus ini" (itu tetap dari netting kebutuhan vs stok, supaya tidak over-order), melainkan "kalau memesan komoditas ini berulang ke SAFCC, berapa kg per PO yang paling murah totalnya (biaya pesan + biaya simpan)". D = perkiraan permintaan setahun, S &amp; H diatur di panel Tweaks. Interval pesan dibuat KONSISTEN per kebijakan: maks tiap 2 minggu (10 hari kerja) untuk komoditas reguler, maks tiap 1 hari kerja saja untuk komoditas sensitif (daging &amp; sayuran segar — lihat tanda ⚠ di bawah) supaya tidak disarankan beli lebih banyak dari yang bisa disimpan sebelum basi.
              </p>
              <table className="data">
                <thead>
                  <tr>
                    <th>Komoditas</th><th>Estimasi harga/kg</th><th>Permintaan/tahun (kg)</th><th>EOQ (kg/PO)</th><th>Interval pesan</th><th>PO/tahun</th><th>Lead time kirim</th>
                  </tr>
                </thead>
                <tbody>
                  {calc.rows.map(r => (
                    <tr key={r.komoditas}>
                      <td>{r.komoditas}</td>
                      <td className="num">{r.hargaPerKgMentah != null ? `Rp${r.hargaPerKgMentah.toLocaleString('id-ID')}` : "—"}</td>
                      <td className="num">{r.permintaanTahunanKg.toLocaleString('id-ID')}</td>
                      <td className="num"><strong>{r.eoqKg != null ? r.eoqKg.toLocaleString('id-ID') : "—"}</strong></td>
                      <td className="num">
                        {r.orderIntervalHari != null ? `~${r.orderIntervalHari} hari kerja` : "—"}
                        {r.eoqCappedByShelfLife && (
                          <span className="muted" title="Interval dipangkas ke kebijakan konsisten (2 minggu reguler / 1 hari kerja untuk komoditas sensitif) — bukan hasil murni rumus EOQ" style={{ marginLeft: 4 }}>⚠</span>
                        )}
                      </td>
                      <td className="num">{r.ordersPerTahun != null ? r.ordersPerTahun : "—"}</td>
                      <td className="num" title="Waktu dari permintaan diajukan ke SAFCC sampai bahan tiba di dapur">{r.leadTimeHariKerja} hari kerja</td>
                    </tr>
                  ))}
                </tbody>
              </table>
              <p className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>
                Estimasi harga/kg mengacu ke harga pasar bahan mentah (katalog Komoditas, bukan harga beli SAFCC sungguhan) — dipakai hanya utk memperkirakan biaya simpan. Kalibrasi biaya pesan/PO &amp; % biaya simpan lewat panel Tweaks (ikon gerigi). ⚠ = interval pesan dipangkas ke kebijakan konsisten (2 minggu reguler / 1 hari kerja untuk daging &amp; sayuran), bukan hasil murni rumus EOQ. "Lead time kirim" = waktu tunggu dari usulan disetujui Kepala SPPG sampai bahan tiba — dipakai menghitung kolom "Prediksi tiba" di tabel Akumulasi Kebutuhan (tabel 2) di atas dan deadline permintaan SAFCC saat disetujui.
              </p>
            </div>
          </div>

          <div className="card">
            <div className="card-head">
              <h2 className="h2">4. Total Bahan Baku &amp; Biaya</h2>
              <span className="tag mono">siklus {activeCycle.periodeMulai} – {activeCycle.periodeSelesai}</span>
            </div>
            <div className="card-body">
              <div className="grid cols-2" style={{ gap: 12 }}>
                <KPI label="Total bahan baku dibutuhkan" value={calc.totalKgUsulanOrder.toLocaleString('id-ID')} unit="kg"/>
                <KPI label="Total estimasi biaya" value={`Rp${calc.totalBiayaEstimasiRp.toLocaleString('id-ID')}`}/>
              </div>
              <p className="muted" style={{ fontSize: 12, marginTop: 12, marginBottom: 12 }}>
                Ringkasan usulan order ({calc.rows.length} komoditas) untuk diajukan ke Kepala SPPG — unduh sebagai PDF untuk arsip/dilampirkan ke dokumen pengajuan.
              </p>
              <button className="btn" onClick={handleExportPdf} disabled={pdfLoading}>
                {pdfLoading ? "Memuat pustaka PDF..." : "📄 Ekspor PDF"}
              </button>
            </div>
          </div>

          <div className="card">
            <div className="card-head">
              <h2 className="h2">Rekomendasi Supplier</h2>
              <span className="tag mono">AHP · rating, acceptance rate, riwayat kirim</span>
            </div>
            <div className="card-body col" style={{ gap: 12 }}>
              <p className="muted" style={{ fontSize: 12.5, margin: 0 }}>
                Skor sama persis dengan yang dipakai Agro saat memvalidasi kiriman — jadi supplier dengan skor tinggi di sini realistis untuk diharapkan performanya. Bobot kriteria (harga/mutu/waktu/jumlah) diatur lewat panel Tweaks.
              </p>
              {calc.rows.map(r => {
                const rekomendasi = window.SupplierIntelligence.recommendForKomoditas(r.komoditas, 3);
                return (
                  <div key={r.komoditas} style={{ borderTop: "1px solid var(--line)", paddingTop: 10 }}>
                    <div style={{ fontWeight: 600, fontSize: 13.5, marginBottom: 6 }}>{r.komoditas}</div>
                    {rekomendasi.length === 0 ? (
                      <span className="pill danger"><span className="led"/>Belum ada supplier terdaftar untuk komoditas ini</span>
                    ) : (
                      <div className="row" style={{ gap: 8, flexWrap: "wrap" }}>
                        {rekomendasi.map((s, i) => (
                          <span key={s.id} className={`pill ${i === 0 ? "ok" : "neutral"}`} title={`${s.lokasi} · rating ${s.rating} · acceptance ${Math.round(s.acceptanceRate * 100)}%`}>
                            <span className="led"/>{i === 0 && "★ "}{s.nama} · {s.safccScore}
                            {recommended[r.komoditas] === s.id && " · 👑 Rekomendasi Kepala"}
                          </span>
                        ))}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        </>
      )}

      {myOrders.length > 0 && (
        <div className="card">
          <div className="card-head"><h2 className="h2">Riwayat pengajuan</h2></div>
          <div className="card-body">
            <table className="data">
              <thead><tr><th>Diajukan</th><th>Status</th><th>Item</th><th>Progres di SAFCC</th><th>Prediksi tiba</th><th>Catatan reviewer</th></tr></thead>
              <tbody>
                {myOrders.map(o => (
                  <tr key={o.id}>
                    <td>{new Date(o.createdAt).toLocaleDateString('id-ID')}</td>
                    <td><StatusPill status={o.status}/></td>
                    <td>{o.items.length} komoditas</td>
                    <td className="muted" style={{ fontSize: 12 }}>{summarizeOrderProgress(o) || (o.status === "approved" ? "Memuat…" : "—")}</td>
                    <td className="muted" style={{ fontSize: 12 }}>{summarizeOrderDeadline(o) || (o.status === "approved" ? "Memuat…" : "—")}</td>
                    <td>{o.reviewNote || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
            {!window.sb && myOrders.some(o => o.status === "approved") && (
              <p className="muted" style={{ fontSize: 11.5, marginTop: 10 }}>
                Progres di SAFCC butuh koneksi Supabase — belum aktif di lingkungan ini.
              </p>
            )}
          </div>
        </div>
      )}
    </div>
  );
}
window.ProcurementPlanner = ProcurementPlanner;

function ApprovalInbox({ role }) {
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';
  const [tick, setTick] = useS_S(0);
  const [noteDraft, setNoteDraft] = useS_S({});
  const [etaDraft, setEtaDraft] = useS_S({}); // { [orderId]: { at, note } } -- prediksi kedatangan bahan

  const pendingCycles = useM_S(() =>
    (window.IDSS.MENU_CYCLES || []).filter(c => c.dapurId === dapurId && c.status === "pending_approval"),
    [tick, dapurId]
  );
  const pendingOrders = useM_S(() =>
    (window.IDSS.PROCUREMENT_ORDERS || []).filter(o => o.dapurId === dapurId && o.status === "menunggu_kepala"),
    [tick, dapurId]
  );

  // direct_purchase_orders tidak punya cermin in-memory -- selalu di-fetch langsung.
  const [pendingDirectOrders, setPendingDirectOrders] = useS_S([]);
  useE_S(() => {
    if (!window.sb) return;
    let cancelled = false;
    window.sb.from('direct_purchase_orders').select('*').eq('dapur_id', dapurId).eq('status', 'menunggu_kepala')
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) { console.warn('[ApprovalInbox] gagal ambil direct orders:', error.message); return; }
        setPendingDirectOrders(data || []);
      });
    return () => { cancelled = true; };
  }, [dapurId, tick]);
  const pool = window.DapurSupplierRegistry.getPoolSuppliers();
  const { SEKOLAH_INDUK, SEKOLAH_SATELIT } = window.IDSS;

  const refresh = () => setTick(t => t + 1);
  const getNote = (id) => noteDraft[id] || "";
  const setNote = (id, v) => setNoteDraft(d => ({ ...d, [id]: v }));

  // Checklist "Cek Menu" -- Kepala SPPG membuka pop up berisi 10 hari menu yang diajukan dan
  // mencentang tiap hari sudah ditinjau, SEBELUM tombol Setujui bisa diklik. Ini murni state
  // tinjauan lokal (bukan disimpan ke server) -- tujuannya memaksa peninjauan per-hari, bukan
  // cuma baca ringkasan 1 baris, sebelum menyetujui perencanaan porsi 2 minggu.
  const [checklistCycle, setChecklistCycle] = useS_S(null);
  const [checkedMap, setCheckedMap] = useS_S({}); // { [cycleId]: { [hariKeCycle]: true } }
  const isDayChecked = (cycleId, hariKeCycle) => !!checkedMap[cycleId]?.[hariKeCycle];
  const toggleDayChecked = (cycleId, hariKeCycle) => setCheckedMap(m => ({
    ...m, [cycleId]: { ...m[cycleId], [hariKeCycle]: !m[cycleId]?.[hariKeCycle] }
  }));
  const checkedCount = (cycleId, days) => (days || []).filter(d => isDayChecked(cycleId, d.hariKeCycle)).length;
  const allDaysChecked = (cycleId, days) => (days || []).length > 0 && checkedCount(cycleId, days) === days.length;
  const markAllChecked = (cycleId, days) => setCheckedMap(m => ({
    ...m, [cycleId]: Object.fromEntries((days || []).map(d => [d.hariKeCycle, true]))
  }));

  // Rekomendasi jam mulai/selesai masak (Kepala SPPG) -- durasi bisa diubah per cycle,
  // default 3 jam masak + 1 jam transit/pengemasan sebelum jam terima tercepat sekolah.
  const [jadwalDraft, setJadwalDraft] = useS_S({});
  const getJadwal = (id) => jadwalDraft[id] || { durasiMasakMenit: 180, durasiTransitMenit: 60 };
  const setJadwal = (id, field, v) => setJadwalDraft(d => ({ ...d, [id]: { ...getJadwal(id), [field]: Number(v) || 0 } }));

  const handleApproveCycle = async (c) => {
    window.ApprovalFlow.approveMenuCycle(c.id, authedUser, getNote(c.id));
    const sekolahList = (c.dapurId === "D1" ? SEKOLAH_INDUK : SEKOLAH_SATELIT || [])
      .filter(s => s.aktif !== false && (!window.PRESET_SEGMEN_MAP?.[c.preset] || s.segmen === window.PRESET_SEGMEN_MAP[c.preset]));
    const plan = await window.ProductionScheduler.generateFromMenuCycle(c, sekolahList, getJadwal(c.id));
    window.showToast?.(
      plan ? "Perencanaan porsi disetujui — jadwal masak dibuat untuk chef" : "Perencanaan porsi disetujui (jadwal masak gagal dibuat — cek koneksi Supabase)",
      plan ? "success" : "warn"
    );
    refresh();
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Persetujuan — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Setujui Perencanaan Porsi (gizi & harga) dan usulan pemesanan procurement.</p>
      </div>

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Perencanaan Porsi</h2>
          <span className="tag mono">{pendingCycles.length} menunggu</span>
        </div>
        <div className="card-body">
          {pendingCycles.length === 0 && <p className="muted" style={{ margin: 0 }}>Tidak ada yang menunggu.</p>}
          {pendingCycles.map(c => (
            <SppgSubcard key={c.id}>
              <div><strong>{c.periodeMulai} – {c.periodeSelesai}</strong> ({c.preset})</div>
              <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>
                Diajukan oleh {c.createdBy} · {c.report.hariGiziCompliant}/10 hari sesuai gizi · {c.report.hariHargaCompliant}/10 hari dalam pita Rp{(c.report.budgetMin || 9000).toLocaleString('id-ID')}-{(c.report.budgetMax || 12000).toLocaleString('id-ID')} · rata-rata Rp{c.report.avgCostPerPorsi.toLocaleString('id-ID')}/porsi
              </div>
              <textarea className="input" placeholder="Catatan (wajib jika tolak)" style={{ marginTop: 10, width: "100%" }}
                value={getNote(c.id)} onChange={e => setNote(c.id, e.target.value)}/>
              <div className="row" style={{ gap: 12, marginTop: 10, flexWrap: "wrap", alignItems: "flex-end" }}>
                <div className="field" style={{ marginBottom: 0 }}>
                  <label>Durasi masak (menit)</label>
                  <input type="number" min="0" className="input" style={{ width: 110 }}
                    value={getJadwal(c.id).durasiMasakMenit} onChange={e => setJadwal(c.id, 'durasiMasakMenit', e.target.value)}/>
                </div>
                <div className="field" style={{ marginBottom: 0 }}>
                  <label>Transit + kemas (menit)</label>
                  <input type="number" min="0" className="input" style={{ width: 110 }}
                    value={getJadwal(c.id).durasiTransitMenit} onChange={e => setJadwal(c.id, 'durasiTransitMenit', e.target.value)}/>
                </div>
                <span className="muted" style={{ fontSize: 11.5 }}>Dipakai hitung rekomendasi jam mulai/selesai masak untuk chef, mundur dari jam terima sekolah tercepat.</span>
              </div>
              <div className="row" style={{ gap: 8, marginTop: 10, alignItems: "center", flexWrap: "wrap" }}>
                <button className="btn" onClick={() => setChecklistCycle(c)}>
                  Cek Menu ({checkedCount(c.id, c.days)}/{(c.days || []).length})
                </button>
                <button className="btn primary" onClick={() => handleApproveCycle(c)} disabled={!allDaysChecked(c.id, c.days)}
                  title={!allDaysChecked(c.id, c.days) ? "Cek semua menu (tombol Cek Menu) dulu sebelum menyetujui" : undefined}>
                  Setujui
                </button>
                <button className="btn danger" onClick={() => {
                  if (!getNote(c.id).trim()) { window.showToast?.("Catatan wajib diisi saat menolak", "warn"); return; }
                  window.ApprovalFlow.rejectMenuCycle(c.id, authedUser, getNote(c.id)); window.showToast?.("Ditolak, dikembalikan ke ahli gizi", "info"); refresh();
                }}>
                  Tolak
                </button>
                {!allDaysChecked(c.id, c.days) && (
                  <span className="muted" style={{ fontSize: 11.5 }}>Belum semua menu dicek — buka "Cek Menu" dan centang tiap hari untuk mengaktifkan Setujui.</span>
                )}
              </div>
            </SppgSubcard>
          ))}
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Usulan Pemesanan</h2>
          <span className="tag mono">{pendingOrders.length} menunggu</span>
        </div>
        <div className="card-body">
          {pendingOrders.length === 0 && <p className="muted" style={{ margin: 0 }}>Tidak ada yang menunggu.</p>}
          {pendingOrders.map(o => (
            <SppgSubcard key={o.id}>
              <div><strong>{o.items.length} komoditas</strong> · buffer {Math.round(o.bufferPct * 100)}%</div>
              <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>Diajukan oleh {o.createdBy} pada {new Date(o.createdAt).toLocaleDateString('id-ID')}</div>
              <div style={{ overflowX: "auto", marginTop: 10 }}>
                <table className="data">
                  <thead><tr><th>Komoditas</th><th>Kg/bulan</th><th>Stok</th><th>Usulan order</th><th>Prediksi tiba jika disetujui sekarang</th></tr></thead>
                  <tbody>
                    {o.items.map(it => {
                      const leadTime = it.leadTimeHariKerja ?? window.getLeadTimeHariKerja(it.komoditas);
                      const prediksi = window.addWorkingDays(new Date(), leadTime);
                      return (
                        <tr key={it.komoditas}>
                          <td>{it.komoditas}</td>
                          <td className="num">{it.kgBulanan}</td>
                          <td className="num">{it.stokSaatIni}</td>
                          <td className="num"><strong>{it.usulanOrderKg}</strong> kg</td>
                          <td style={{ whiteSpace: "nowrap" }}>
                            {prediksi.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' })}
                            <span className="muted" style={{ fontSize: 11 }}> (+{leadTime}h kerja)</span>
                          </td>
                        </tr>
                      );
                    })}
                  </tbody>
                </table>
                <p className="muted" style={{ fontSize: 11, marginTop: 6 }}>Tanggal terkunci begitu diklik Setujui — deadline permintaan ke SAFCC dihitung dari saat itu, bukan dari tanggal pengajuan.</p>
              </div>
              <textarea className="input" placeholder="Catatan (wajib jika tolak)" style={{ marginTop: 10, width: "100%" }}
                value={getNote(o.id)} onChange={e => setNote(o.id, e.target.value)}/>
              <div className="row" style={{ gap: 8, marginTop: 10 }}>
                <button className="btn primary" onClick={() => { window.ApprovalFlow.approveProcurementOrder(o.id, authedUser, getNote(o.id)); window.showToast?.("Pemesanan disetujui, dikirim ke SAFCC", "success"); refresh(); }}>
                  Setujui
                </button>
                <button className="btn danger" onClick={() => {
                  if (!getNote(o.id).trim()) { window.showToast?.("Catatan wajib diisi saat menolak", "warn"); return; }
                  window.ApprovalFlow.rejectProcurementOrder(o.id, authedUser, getNote(o.id)); window.showToast?.("Usulan ditolak, dikembalikan ke procurement", "info"); refresh();
                }}>
                  Tolak
                </button>
              </div>
            </SppgSubcard>
          ))}
        </div>
      </div>

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Pesanan Langsung (di luar SAFCC)</h2>
          <span className="tag mono">{pendingDirectOrders.length} menunggu</span>
        </div>
        <div className="card-body">
          {pendingDirectOrders.length === 0 && <p className="muted" style={{ margin: 0 }}>Tidak ada yang menunggu.</p>}
          {pendingDirectOrders.map(o => {
            const supplier = pool.find(s => s.id === o.supplier_id);
            return (
              <SppgSubcard key={o.id}>
                <div><strong>{supplier?.nama || o.supplier_id}</strong> · {(o.items || []).length} komoditas</div>
                <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>Diajukan oleh {o.created_by} pada {new Date(o.created_at).toLocaleDateString('id-ID')}</div>
                <div style={{ marginTop: 8, padding: 10, background: "var(--warn-100)", border: "1px solid var(--warn)", borderRadius: "var(--radius-lg)" }}>
                  <div style={{ fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: 0.3, marginBottom: 4 }}>Alasan kekurangan SAFCC</div>
                  <div style={{ fontSize: 13 }}>{o.alasan_kekurangan || "(tidak dicantumkan)"}</div>
                  {(o.komoditas_defisit || []).length > 0 && (
                    <div className="muted" style={{ fontSize: 11.5, marginTop: 4 }}>Komoditas defisit dirujuk: {o.komoditas_defisit.join(', ')}</div>
                  )}
                </div>
                <table className="data" style={{ marginTop: 10 }}>
                  <thead><tr><th>Komoditas</th><th>Jumlah</th></tr></thead>
                  <tbody>
                    {(o.items || []).map(it => (
                      <tr key={it.komoditas}><td>{it.komoditas}</td><td className="num">{it.jumlah} {it.satuan}</td></tr>
                    ))}
                  </tbody>
                </table>
                <textarea className="input" placeholder="Catatan (wajib jika tolak)" style={{ marginTop: 10, width: "100%" }}
                  value={getNote(o.id)} onChange={e => setNote(o.id, e.target.value)}/>
                <div className="row" style={{ gap: 12, marginTop: 10, flexWrap: "wrap", alignItems: "flex-end" }}>
                  <div className="field" style={{ marginBottom: 0 }}>
                    <label>Prediksi waktu kedatangan (opsional)</label>
                    <input type="datetime-local" className="input" style={{ width: 200 }}
                      value={etaDraft[o.id]?.at || ""} onChange={e => setEtaDraft(d => ({ ...d, [o.id]: { ...d[o.id], at: e.target.value } }))}/>
                  </div>
                  <div className="field" style={{ marginBottom: 0, flex: 1, minWidth: 180 }}>
                    <label>Catatan ETA (opsional)</label>
                    <input className="input" placeholder="mis. tiba pagi sebelum jam masak" style={{ width: "100%" }}
                      value={etaDraft[o.id]?.note || ""} onChange={e => setEtaDraft(d => ({ ...d, [o.id]: { ...d[o.id], note: e.target.value } }))}/>
                  </div>
                </div>
                <div className="row" style={{ gap: 8, marginTop: 10 }}>
                  <button className="btn primary" onClick={async () => {
                    const eta = etaDraft[o.id] || {};
                    await window.ApprovalFlow.approveDirectOrder(o.id, authedUser, getNote(o.id), {
                      predictedArrivalAt: eta.at ? new Date(eta.at).toISOString() : null, etaNote: eta.note || ""
                    });
                    window.showToast?.("Pesanan disetujui, terkirim ke supplier", "success"); refresh();
                  }}>
                    Setujui
                  </button>
                  <button className="btn danger" onClick={async () => {
                    if (!getNote(o.id).trim()) { window.showToast?.("Catatan wajib diisi saat menolak", "warn"); return; }
                    await window.ApprovalFlow.rejectDirectOrder(o.id, authedUser, getNote(o.id));
                    window.showToast?.("Pesanan ditolak, dikembalikan ke procurement", "info"); refresh();
                  }}>
                    Tolak
                  </button>
                </div>
              </SppgSubcard>
            );
          })}
        </div>
      </div>

      {checklistCycle && (
        <window.IDSSUI.Modal isOpen={true} onClose={() => setChecklistCycle(null)} size="lg"
          title={`Cek Menu — ${checklistCycle.periodeMulai} s/d ${checklistCycle.periodeSelesai} (${checklistCycle.preset})`}
          footer={
            <>
              <button className="btn" onClick={() => markAllChecked(checklistCycle.id, checklistCycle.days)}>
                Tandai semua sudah dicek
              </button>
              <button className="btn primary" onClick={() => setChecklistCycle(null)}>Tutup</button>
            </>
          }>
          <div className="col" style={{ gap: 12 }}>
            <SppgBanner tone={allDaysChecked(checklistCycle.id, checklistCycle.days) ? "success" : "info"}>
              {checkedCount(checklistCycle.id, checklistCycle.days)}/{(checklistCycle.days || []).length} hari sudah dicek — centang tiap hari setelah ditinjau menunya, gizi & harganya. Tombol Setujui di daftar Perencanaan Porsi aktif setelah semua hari dicek.
            </SppgBanner>
            <div style={{ overflowX: "auto" }}>
              <table className="data" style={{ tableLayout: "fixed", width: "100%" }}>
                <colgroup>
                  <col style={{ width: 40 }}/><col/><col style={{ width: 90 }}/><col style={{ width: 130 }}/><col style={{ width: 90 }}/>
                </colgroup>
                <thead>
                  <tr><th></th><th>Menu (hari ke)</th><th className="num">Biaya</th><th>Status</th><th>Dicek</th></tr>
                </thead>
                <tbody>
                  {(checklistCycle.days || []).map(d => {
                    const menuLabel = [d.picked?.carbohydrate, d.picked?.proteinAnimal, d.picked?.proteinPlant, d.picked?.vegetable, d.picked?.fruit].filter(Boolean).join(' + ');
                    const checked = isDayChecked(checklistCycle.id, d.hariKeCycle);
                    return (
                      <tr key={d.hariKeCycle} style={checked ? { opacity: 0.7 } : undefined}>
                        <td>{d.hariKeCycle}</td>
                        <td>
                          {menuLabel}
                          {d.hasSusu && <span className="muted"> · susu</span>}
                          {d.hasDaging && <span className="muted"> · daging</span>}
                        </td>
                        <td className="num">Rp{Math.round(d.result?.totals.biaya || 0).toLocaleString('id-ID')}</td>
                        <td>
                          {d.result?.costOK && d.result?.nutrisiOK ? (
                            <span className="pill ok"><span className="led"/>OK</span>
                          ) : (
                            <span className="pill warn"><span className="led"/>Perlu tinjauan</span>
                          )}
                        </td>
                        <td>
                          <label className="row" style={{ gap: 6, alignItems: "center" }}>
                            <input type="checkbox" checked={checked} onChange={() => toggleDayChecked(checklistCycle.id, d.hariKeCycle)}/>
                          </label>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        </window.IDSSUI.Modal>
      )}
    </div>
  );
}
window.ApprovalInbox = ApprovalInbox;

const SPPG_JABATAN_LABEL = { gizi: "Ahli Gizi", procurement: "Procurement", kepala: "Kepala SPPG" };

function AccountManagement({ role }) {
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const dapurType = dapurId === "D1" ? "induk" : "satelit";
  const myUsername = sessionStorage.getItem('authedUser') || '';
  const [form, setForm] = useS_S({ username: "", password: "", nama: "", jabatan: "gizi" });
  const [busy, setBusy] = useS_S(false);
  const [accounts, setAccounts] = useS_S([]);
  const [loadingAccounts, setLoadingAccounts] = useS_S(false);
  const [listTick, setListTick] = useS_S(0);

  // Daftar akun tim dapur ini (gizi/procurement/kepala) -- app_users tidak dihidrasi ke
  // window.IDSS (cuma checklist/feedback/alerts yang di-hydrate saat login), jadi di-query
  // langsung dari Supabase di sini. Kepala punya is_coordinator()=true (bisa lihat semua
  // app_users lewat RLS au_self), tapi tampilan tetap difilter ke dapur_id sendiri saja.
  useE_S(() => {
    if (!window.sb) return;
    let cancelled = false;
    setLoadingAccounts(true);
    window.sb.from('app_users').select('username, nama, role, created_at, aktif')
      .eq('dapur_id', dapurId).order('role')
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) { console.warn('[AccountManagement] gagal ambil daftar akun:', error.message); return; }
        setAccounts(data || []);
      })
      .finally(() => { if (!cancelled) setLoadingAccounts(false); });
    return () => { cancelled = true; };
  }, [dapurId, listTick]);

  const handleCreate = async () => {
    if (!form.username || !form.password || !form.nama) {
      window.showToast?.("Lengkapi username, password, dan nama", "warn");
      return;
    }
    setBusy(true);
    try {
      if (!window.sb?.functions) {
        window.showToast?.("Fitur ini butuh koneksi Supabase (edge function create-dapur-staff) — belum aktif di lingkungan ini.", "warn", 6000);
        return;
      }
      const { data, error } = await window.sb.functions.invoke('create-dapur-staff', {
        body: { username: form.username, password: form.password, nama: form.nama, jabatan: form.jabatan }
      });
      if (error) throw error;
      window.AuditLog?.log('dapur_staff_created', { username: form.username, jabatan: form.jabatan, dapurId });
      window.showToast?.(`Akun ${form.username} (${form.jabatan}) berhasil dibuat`, "success", 4000);
      setForm({ username: "", password: "", nama: "", jabatan: "gizi" });
      setListTick(t => t + 1); // refresh daftar supaya akun baru langsung muncul
    } catch (e) {
      window.showToast?.(`Gagal membuat akun: ${await window.extractEdgeFunctionError(e)}`, "error", 6000);
    } finally {
      setBusy(false);
    }
  };

  // Nonaktifkan, BUKAN hapus -- username dipakai sbg audit trail (created_by/reviewed_by/dst)
  // di menu_cycles/procurement_orders/dapur_suppliers/direct_purchase_orders/
  // supplier_recommendations/supplier_complaints. Hard delete akan selalu gagal (FK
  // violation) begitu akun pernah membuat/menyetujui apa pun -- lihat deactivate-dapur-staff.
  const handleDeactivate = async (account) => {
    if (!window.confirm(`Nonaktifkan akun ${account.username} (${account.nama || account.username})? Akun tidak bisa login lagi, tapi riwayat aktivitasnya (menu/pesanan yang pernah dibuat) tetap tersimpan.`)) return;
    setBusy(true);
    try {
      if (!window.sb?.functions) {
        window.showToast?.("Fitur ini butuh koneksi Supabase (edge function deactivate-dapur-staff) — belum aktif di lingkungan ini.", "warn", 6000);
        return;
      }
      const { data, error } = await window.sb.functions.invoke('deactivate-dapur-staff', {
        body: { username: account.username }
      });
      if (error) throw error;
      window.AuditLog?.log('dapur_staff_deactivated', { username: account.username, dapurId });
      window.showToast?.(`Akun ${account.username} dinonaktifkan`, "success", 4000);
      setListTick(t => t + 1);
    } catch (e) {
      window.showToast?.(`Gagal menonaktifkan akun: ${await window.extractEdgeFunctionError(e)}`, "error", 6000);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Kelola Akun Tim — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Buat login untuk Ahli Gizi atau Procurement di dapur Anda. Akun dikunci ke dapur ini.</p>
      </div>

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Akun di Dapur Ini</h2>
          <span className="tag mono">{accounts.length} akun</span>
        </div>
        <div className="card-body">
          {!window.sb && <p className="muted" style={{ margin: 0 }}>Daftar akun butuh koneksi Supabase — belum aktif di lingkungan ini.</p>}
          {window.sb && loadingAccounts && <p className="muted" style={{ margin: 0 }}>Memuat daftar akun...</p>}
          {window.sb && !loadingAccounts && accounts.length === 0 && <p className="muted" style={{ margin: 0 }}>Belum ada akun tercatat untuk dapur ini.</p>}
          {window.sb && !loadingAccounts && accounts.length > 0 && (
            <table className="data">
              <thead><tr><th>Username</th><th>Nama</th><th>Jabatan</th><th>Status</th><th>Dibuat</th><th>Aksi</th></tr></thead>
              <tbody>
                {accounts.map(a => {
                  const jabatan = String(a.role || "").split("_")[1] || a.role;
                  const aktif = a.aktif !== false;
                  return (
                    <tr key={a.username} style={aktif ? undefined : { opacity: 0.6 }}>
                      <td>{a.username}{a.username === myUsername && <span className="muted" style={{ fontSize: 11 }}> (Anda)</span>}</td>
                      <td>{a.nama || "—"}</td>
                      <td><span className={`pill ${jabatan === "kepala" ? "info" : "neutral"}`}><span className="led"/>{SPPG_JABATAN_LABEL[jabatan] || jabatan}</span></td>
                      <td>{aktif ? <span className="pill ok"><span className="led"/>Aktif</span> : <span className="pill neutral"><span className="led"/>Nonaktif</span>}</td>
                      <td>{a.created_at ? new Date(a.created_at).toLocaleDateString('id-ID') : "—"}</td>
                      <td>
                        {aktif && jabatan !== "kepala" && a.username !== myUsername ? (
                          <button className="btn danger" style={{ padding: "4px 10px", fontSize: 12 }} disabled={busy} onClick={() => handleDeactivate(a)}>Nonaktifkan</button>
                        ) : (
                          <span className="muted" style={{ fontSize: 11 }}>—</span>
                        )}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </div>
      </div>

      <div className="card" style={{ maxWidth: 480 }}>
        <div className="card-head"><h2 className="h2">Buat Akun Baru</h2></div>
        <div className="card-body col" style={{ gap: 14 }}>
          <div className="field">
            <label>Jabatan</label>
            <select className="input" value={form.jabatan} onChange={e => setForm(f => ({ ...f, jabatan: e.target.value }))}>
              <option value="gizi">Ahli Gizi</option>
              <option value="procurement">Procurement</option>
            </select>
            <span className="muted" style={{ fontSize: 12 }}>Role yang dibuat: {dapurType}_{form.jabatan}</span>
          </div>
          <div className="field">
            <label>Nama lengkap</label>
            <input className="input" value={form.nama} onChange={e => setForm(f => ({ ...f, nama: e.target.value }))}/>
          </div>
          <div className="field">
            <label>Username</label>
            <input className="input" value={form.username} onChange={e => setForm(f => ({ ...f, username: e.target.value }))} placeholder={`${dapurType}_${form.jabatan}2`}/>
          </div>
          <div className="field">
            <label>Password</label>
            <input type="password" className="input" value={form.password} onChange={e => setForm(f => ({ ...f, password: e.target.value }))}/>
          </div>
          <button className="btn primary" onClick={handleCreate} disabled={busy}>{busy ? "Membuat..." : "Buat Akun"}</button>
        </div>
      </div>
    </div>
  );
}
window.AccountManagement = AccountManagement;

// Segmen usia baku (harus persis sama dgn idss.segmen.label di DB & NUTRISI_PRESETS) --
// dipakai dropdown "Tambah Sekolah" supaya nilai yg dikirim selalu valid.
const SPPG_SEGMEN_OPTIONS = ["TK/PAUD/RA", "Siswa SD", "Siswa SMP", "Siswa SMA", "Ibu Hamil & Menyusui", "Anak Balita"];

function SchoolManagement({ role }) {
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const [tick, setTick] = useS_S(0);
  const [showForm, setShowForm] = useS_S(false);
  const [form, setForm] = useS_S({ nama: "", segmen: SPPG_SEGMEN_OPTIONS[1], siswa: "", kontak: "" });
  const [editingKontak, setEditingKontak] = useS_S(null); // sekolahId sedang diedit kontaknya
  const [kontakDraft, setKontakDraft] = useS_S("");

  const sekolahList = useM_S(() =>
    (dapurId === "D1" ? window.IDSS.SEKOLAH_INDUK : window.IDSS.SEKOLAH_SATELIT) || [],
    [tick, dapurId]
  );
  const aktifList = sekolahList.filter(s => s.aktif !== false);
  const nonaktifList = sekolahList.filter(s => s.aktif === false);
  const totalPorsiAktif = aktifList.reduce((s, x) => s + x.siswa, 0);

  const refresh = () => setTick(t => t + 1);

  const handleAdd = () => {
    if (!form.nama.trim() || !form.siswa || Number(form.siswa) <= 0) {
      window.showToast?.("Lengkapi nama sekolah dan jumlah siswa/penerima", "warn");
      return;
    }
    window.SchoolRoster.addSchool(dapurId, { nama: form.nama.trim(), segmen: form.segmen, siswa: form.siswa, kontak: form.kontak.trim() });
    window.showToast?.(`${form.nama} ditambahkan ke daftar suplai`, "success");
    setForm({ nama: "", segmen: SPPG_SEGMEN_OPTIONS[1], siswa: "", kontak: "" });
    setShowForm(false);
    refresh();
  };

  const handleToggle = (sekolah) => {
    window.SchoolRoster.setActive(dapurId, sekolah.id, sekolah.aktif === false);
    window.showToast?.(
      sekolah.aktif === false ? `${sekolah.nama} diaktifkan kembali` : `${sekolah.nama} dinonaktifkan (mis. libur) — tidak dihitung di Perencanaan Porsi & Pemesanan`,
      "info"
    );
    refresh();
  };

  const startEditKontak = (sekolah) => {
    setEditingKontak(sekolah.id);
    setKontakDraft(sekolah.kontak || "");
  };

  const saveKontak = (sekolah) => {
    window.SchoolRoster.setKontak(dapurId, sekolah.id, kontakDraft.trim());
    window.showToast?.(`Kontak ${sekolah.nama} disimpan`, "success");
    setEditingKontak(null);
    refresh();
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Kelola Sekolah — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Tambah atau nonaktifkan sekolah/posyandu yang disuplai dapur ini. Perubahan langsung terhubung ke Perencanaan Porsi (Ahli Gizi) dan Perencanaan Pemesanan (Procurement).</p>
      </div>

      <div className="grid cols-2" style={{ gap: 12 }}>
        <window.IDSSUI.KPI label="Sekolah aktif" value={aktifList.length} unit={`/ ${sekolahList.length}`}/>
        <window.IDSSUI.KPI label="Total porsi/hari (aktif)" value={totalPorsiAktif.toLocaleString('id-ID')}/>
      </div>

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Daftar Sekolah &amp; Posyandu</h2>
          <button className="btn primary" style={{ padding: "5px 12px", fontSize: 12.5 }} onClick={() => setShowForm(s => !s)}>
            {showForm ? "Batal" : "+ Tambah Sekolah"}
          </button>
        </div>
        <div className="card-body">
          {showForm && (
            <div style={{ border: "1px solid var(--line)", borderRadius: "var(--radius-lg)", padding: 14, marginBottom: 14 }}>
              <div className="row" style={{ gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
                <div className="field" style={{ marginBottom: 0, minWidth: 200 }}>
                  <label>Nama sekolah/posyandu</label>
                  <input className="input" value={form.nama} onChange={e => setForm(f => ({ ...f, nama: e.target.value }))}/>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 160 }}>
                  <label>Segmen</label>
                  <select className="input" value={form.segmen} onChange={e => setForm(f => ({ ...f, segmen: e.target.value }))}>
                    {SPPG_SEGMEN_OPTIONS.map(s => <option key={s} value={s}>{s}</option>)}
                  </select>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 120 }}>
                  <label>Jumlah siswa/penerima</label>
                  <input type="number" className="input" value={form.siswa} onChange={e => setForm(f => ({ ...f, siswa: e.target.value }))}/>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 180 }}>
                  <label>Kontak (nama PIC & no. HP, opsional)</label>
                  <input className="input" value={form.kontak} onChange={e => setForm(f => ({ ...f, kontak: e.target.value }))} placeholder="Bu Rini, 0812-xxxx-xxxx"/>
                </div>
                <button className="btn primary" onClick={handleAdd}>Simpan</button>
              </div>
            </div>
          )}

          <table className="data">
            <thead><tr><th>Nama</th><th>Segmen</th><th>Porsi/hari</th><th>Kontak</th><th>Status</th><th></th></tr></thead>
            <tbody>
              {aktifList.map(s => (
                <tr key={s.id}>
                  <td>{s.nama}</td>
                  <td>{s.segmen}</td>
                  <td className="num">{s.siswa.toLocaleString('id-ID')}</td>
                  <td>
                    {editingKontak === s.id ? (
                      <div className="row gap-xs" style={{ alignItems: "center" }}>
                        <input className="input" style={{ padding: "4px 8px", fontSize: 12, width: 160 }} value={kontakDraft} onChange={e => setKontakDraft(e.target.value)} placeholder="Bu Rini, 0812-xxxx-xxxx" autoFocus/>
                        <button className="btn primary" style={{ padding: "3px 8px", fontSize: 11 }} onClick={() => saveKontak(s)}>Simpan</button>
                        <button className="btn" style={{ padding: "3px 8px", fontSize: 11 }} onClick={() => setEditingKontak(null)}>Batal</button>
                      </div>
                    ) : (
                      <span className="row gap-xs" style={{ alignItems: "center", cursor: "pointer" }} onClick={() => startEditKontak(s)} title="Klik untuk edit kontak">
                        {s.kontak ? <span style={{ fontSize: 12.5 }}>{s.kontak}</span> : <span className="muted" style={{ fontSize: 12 }}>Belum ada — klik isi</span>}
                      </span>
                    )}
                  </td>
                  <td><span className="pill ok"><span className="led"/>Aktif</span></td>
                  <td><button className="btn danger" style={{ padding: "4px 10px", fontSize: 12 }} onClick={() => handleToggle(s)}>Nonaktifkan</button></td>
                </tr>
              ))}
              {nonaktifList.map(s => (
                <tr key={s.id} style={{ opacity: 0.6 }}>
                  <td>{s.nama}</td>
                  <td>{s.segmen}</td>
                  <td className="num">{s.siswa.toLocaleString('id-ID')}</td>
                  <td className="muted" style={{ fontSize: 12 }}>{s.kontak || "—"}</td>
                  <td><span className="pill neutral"><span className="led"/>Nonaktif</span></td>
                  <td><button className="btn" style={{ padding: "4px 10px", fontSize: 12 }} onClick={() => handleToggle(s)}>Aktifkan</button></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  );
}
window.SchoolManagement = SchoolManagement;

// ===== Direct sourcing (40% dapur cari supplier sendiri, di luar SAFCC) =====
// Katalog komoditas dibatasi ke window.IDSS.KOMODITAS (bukan bebas ketik) supaya nama
// komoditas supplier baru selalu konsisten dgn katalog orderable -- ini yang bikin
// rekomendasi supplier (SupplierIntelligence) sempat kosong utk "Beras Premium" krn
// mismatch nama sebelum diperbaiki; kita tak mau menambah mismatch baru dari sini.

function KelolaSupplier({ role }) {
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';
  const KOMODITAS_OPTIONS = (window.IDSS.KOMODITAS || []).map(k => k.nama);

  const [tick, setTick] = useS_S(0);
  const [linkedIds, setLinkedIds] = useS_S(null); // null = belum dimuat
  const [showAddExisting, setShowAddExisting] = useS_S(false);
  const [showNew, setShowNew] = useS_S(false);
  const [pickExisting, setPickExisting] = useS_S("");
  const [busy, setBusy] = useS_S(false);
  const [newForm, setNewForm] = useS_S({ nama: "", type: "kelompok_tani", lokasi: "", kontak: "", komoditas: [] });

  const refresh = () => setTick(t => t + 1);

  useE_S(() => {
    if (!window.sb) { setLinkedIds([]); return; }
    let cancelled = false;
    window.sb.from('dapur_suppliers').select('supplier_id').eq('dapur_id', dapurId).then(({ data, error }) => {
      if (cancelled) return;
      if (error) { console.warn('[KelolaSupplier] gagal ambil daftar:', error.message); setLinkedIds([]); return; }
      setLinkedIds((data || []).map(r => r.supplier_id));
    });
    return () => { cancelled = true; };
  }, [dapurId, tick]);

  const pool = window.DapurSupplierRegistry.getPoolSuppliers();
  const linkedSuppliers = linkedIds ? pool.filter(s => linkedIds.includes(s.id)) : [];
  const unlinkedPool = linkedIds ? pool.filter(s => !linkedIds.includes(s.id)) : [];

  const handleLinkExisting = async () => {
    if (!pickExisting) return;
    setBusy(true);
    const ok = await window.DapurSupplierRegistry.linkExisting(dapurId, pickExisting, authedUser);
    setBusy(false);
    if (ok) { window.showToast?.("Supplier ditambahkan ke daftar dapur", "success"); setShowAddExisting(false); setPickExisting(""); refresh(); }
    else window.showToast?.("Gagal menambahkan supplier", "error");
  };

  const handleUnlink = async (s) => {
    setBusy(true);
    const ok = await window.DapurSupplierRegistry.unlink(dapurId, s.id);
    setBusy(false);
    if (ok) { window.showToast?.(`${s.nama} dihapus dari daftar dapur (entitas supplier tetap ada di pool)`, "info"); refresh(); }
    else window.showToast?.("Gagal menghapus dari daftar", "error");
  };

  const handleCreateNew = async () => {
    if (!newForm.nama.trim() || !newForm.lokasi.trim() || newForm.komoditas.length === 0) {
      window.showToast?.("Lengkapi nama, lokasi, dan minimal 1 komoditas", "warn");
      return;
    }
    setBusy(true);
    await window.DapurSupplierRegistry.createAndLink(dapurId, newForm, authedUser);
    setBusy(false);
    window.showToast?.(`${newForm.nama} terdaftar & ditambahkan ke daftar dapur`, "success");
    setNewForm({ nama: "", type: "kelompok_tani", lokasi: "", kontak: "", komoditas: [] });
    setShowNew(false);
    refresh();
  };

  const toggleKomoditas = (k) => setNewForm(f => ({
    ...f, komoditas: f.komoditas.includes(k) ? f.komoditas.filter(x => x !== k) : [...f.komoditas, k]
  }));

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Kelola Supplier — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Daftar supplier khusus dapur ini untuk jalur pemesanan langsung (di luar SAFCC). Menambah/menghapus di sini langsung terhubung ke pilihan supplier Procurement di Cari Supplier.</p>
      </div>

      {!window.sb && <SppgBanner tone="warn">Fitur ini butuh koneksi Supabase — belum aktif di lingkungan ini.</SppgBanner>}

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Daftar Supplier Dapur Ini</h2>
          <div className="row" style={{ gap: 8 }}>
            <button className="btn" style={{ padding: "5px 12px", fontSize: 12.5 }} onClick={() => { setShowAddExisting(s => !s); setShowNew(false); }}>+ Dari Pool</button>
            <button className="btn primary" style={{ padding: "5px 12px", fontSize: 12.5 }} onClick={() => { setShowNew(s => !s); setShowAddExisting(false); }}>+ Daftarkan Baru</button>
          </div>
        </div>
        <div className="card-body">
          {showAddExisting && (
            <div style={{ border: "1px solid var(--line)", borderRadius: "var(--radius-lg)", padding: 14, marginBottom: 14 }}>
              <div className="row" style={{ gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
                <div className="field" style={{ marginBottom: 0, minWidth: 260 }}>
                  <label>Pilih dari pool supplier bersama</label>
                  <select className="input" value={pickExisting} onChange={e => setPickExisting(e.target.value)}>
                    <option value="">-- pilih supplier --</option>
                    {unlinkedPool.map(s => <option key={s.id} value={s.id}>{s.nama} ({(s.komoditas || []).join(', ')})</option>)}
                  </select>
                </div>
                <button className="btn primary" onClick={handleLinkExisting} disabled={busy || !pickExisting}>Tambahkan</button>
              </div>
            </div>
          )}

          {showNew && (
            <div style={{ border: "1px solid var(--line)", borderRadius: "var(--radius-lg)", padding: 14, marginBottom: 14 }}>
              <div className="row" style={{ gap: 12, flexWrap: "wrap", alignItems: "flex-end" }}>
                <div className="field" style={{ marginBottom: 0, minWidth: 200 }}>
                  <label>Nama supplier</label>
                  <input className="input" value={newForm.nama} onChange={e => setNewForm(f => ({ ...f, nama: e.target.value }))}/>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 160 }}>
                  <label>Jenis</label>
                  <select className="input" value={newForm.type} onChange={e => setNewForm(f => ({ ...f, type: e.target.value }))}>
                    <option value="kelompok_tani">Kelompok Tani</option>
                    <option value="koperasi">Koperasi</option>
                    <option value="umkm">UMKM</option>
                    <option value="pengusaha_besar">Pengusaha Besar</option>
                  </select>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 160 }}>
                  <label>Lokasi</label>
                  <input className="input" value={newForm.lokasi} onChange={e => setNewForm(f => ({ ...f, lokasi: e.target.value }))}/>
                </div>
                <div className="field" style={{ marginBottom: 0, minWidth: 160 }}>
                  <label>Kontak (opsional)</label>
                  <input className="input" value={newForm.kontak} onChange={e => setNewForm(f => ({ ...f, kontak: e.target.value }))}/>
                </div>
              </div>
              <div style={{ marginTop: 12 }}>
                <label style={{ fontSize: 12.5, fontWeight: 600, display: "block", marginBottom: 6 }}>Komoditas yang dipasok</label>
                <div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
                  {KOMODITAS_OPTIONS.map(k => (
                    <span key={k} onClick={() => toggleKomoditas(k)}
                      className={`pill ${newForm.komoditas.includes(k) ? "ok" : "neutral"}`}
                      style={{ cursor: "pointer" }}>
                      <span className="led"/>{k}
                    </span>
                  ))}
                </div>
              </div>
              <button className="btn primary" style={{ marginTop: 12 }} onClick={handleCreateNew} disabled={busy}>Daftarkan &amp; Tambahkan</button>
            </div>
          )}

          {linkedIds === null ? (
            <p className="muted" style={{ margin: 0 }}>Memuat...</p>
          ) : linkedSuppliers.length === 0 ? (
            <p className="muted" style={{ margin: 0 }}>Belum ada supplier di daftar dapur ini.</p>
          ) : (
            <table className="data">
              <thead><tr><th>Nama</th><th>Jenis</th><th>Lokasi</th><th>Komoditas</th><th>Rating</th><th></th></tr></thead>
              <tbody>
                {linkedSuppliers.map(s => (
                  <tr key={s.id}>
                    <td>{s.nama}</td>
                    <td className="muted" style={{ fontSize: 12 }}>{s.type}</td>
                    <td>{s.lokasi}</td>
                    <td style={{ fontSize: 12 }}>{(s.komoditas || []).join(', ') || '—'}</td>
                    <td className="num">{s.rating}</td>
                    <td><button className="btn danger" style={{ padding: "4px 10px", fontSize: 12 }} onClick={() => handleUnlink(s)}>Hapus</button></td>
                  </tr>
                ))}
              </tbody>
            </table>
          )}
        </div>
      </div>
    </div>
  );
}
window.KelolaSupplier = KelolaSupplier;

// Kepala SPPG menandai 1 supplier sbg REKOMENDASI (prioritas/default, bukan mengikat --
// Procurement di Cari Supplier tetap bebas pilih supplier lain) per komoditas kebutuhan
// dapur, berbasis skor rule-based 4-kriteria (harga/mutu/jumlah/waktu) dari
// SupplierIntelligence.scoreSupplier. Kebutuhan bahan dihitung sama seperti ProcurementPlanner.
function RekomendasiSupplier({ role }) {
  const { MENU_CYCLES, NERACA, SEKOLAH_INDUK, SEKOLAH_SATELIT } = window.IDSS;
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';
  const [tick, setTick] = useS_S(0);
  const [existing, setExisting] = useS_S({}); // { [komoditas]: baris supplier_recommendations penuh (skor, criteria, recommended_by/at) }
  const [busyKom, setBusyKom] = useS_S(null);
  const [expandedKomoditas, setExpandedKomoditas] = useS_S(null);
  const [showMethodology, setShowMethodology] = useS_S(false);
  const w = window.__ahpWeights || { harga: 0.30, mutu: 0.35, waktu: 0.20, jumlah: 0.15 };

  const activeCycle = useM_S(() =>
    (MENU_CYCLES || []).filter(c => c.dapurId === dapurId && c.status === "active")
      .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0] || null,
    [dapurId]
  );
  const currentStockMap = useM_S(() => {
    const map = {};
    (NERACA || []).forEach(n => { map[n.komoditas] = n.tersedia; });
    return map;
  }, []);
  const sekolahListForCalc = (dapurId === "D1" ? SEKOLAH_INDUK : SEKOLAH_SATELIT) || [];
  const targetSegmen = window.PRESET_SEGMEN_MAP?.[activeCycle?.preset] || null;
  const totalPorsiAktif = sekolahListForCalc
    .filter(s => s.aktif !== false && (!targetSegmen || s.segmen === targetSegmen))
    .reduce((s, x) => s + x.siswa, 0);
  const calc = useM_S(() => {
    if (!activeCycle) return null;
    return window.ProcurementCalculator.computeNeeds(activeCycle.days, totalPorsiAktif, currentStockMap, 0.1);
  }, [activeCycle, totalPorsiAktif, currentStockMap]);

  useE_S(() => {
    window.SupplierIntelligence.getRecommendations(dapurId).then(rows => {
      const map = {};
      rows.forEach(r => { map[r.komoditas] = r; });
      setExisting(map);
    });
  }, [dapurId, tick]);

  const handleRecommend = async (komoditas, candidate) => {
    setBusyKom(komoditas);
    const ok = await window.SupplierIntelligence.setRecommendation(dapurId, komoditas, candidate, authedUser);
    setBusyKom(null);
    if (ok) { window.showToast?.(`${candidate.nama} dijadikan rekomendasi untuk ${komoditas}`, "success"); setTick(t => t + 1); }
    else window.showToast?.("Gagal menyimpan rekomendasi — butuh koneksi Supabase", "error");
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Rekomendasi Supplier — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Pilih supplier prioritas per komoditas berbasis skor rule-based (harga, mutu, jumlah, waktu). Ini rekomendasi/default — Procurement di Cari Supplier tetap bebas memilih supplier lain.</p>
      </div>

      <div className="card">
        <div className="card-head" style={{ cursor: "pointer" }} onClick={() => setShowMethodology(v => !v)}>
          <h2 className="h2">ℹ️ Bagaimana skor ini dihitung?</h2>
          <span className="muted" style={{ fontSize: 12 }}>{showMethodology ? "Sembunyikan ▲" : "Tampilkan ▼"}</span>
        </div>
        {showMethodology && (
          <div className="card-body" style={{ fontSize: 12.5, lineHeight: 1.7 }}>
            <p style={{ margin: "0 0 8px" }}>
              Skor dihitung <strong>otomatis oleh sistem</strong> (formula rule-based di <code>SupplierIntelligence.scoreSupplier</code>) — <strong>bukan penilaian manual manusia</strong>. Dihitung ulang setiap kali kebutuhan bahan berubah, bukan snapshot statis.
            </p>
            <table className="data" style={{ marginBottom: 8 }}>
              <thead><tr><th>Kriteria</th><th>Bobot saat ini</th><th>Sumber data</th></tr></thead>
              <tbody>
                <tr><td>Harga</td><td className="num">{Math.round(w.harga * 100)}%</td><td>Harga satuan yang diisi mandiri oleh supplier, dibandingkan relatif ke supplier lain untuk komoditas yang sama (lebih murah = skor lebih tinggi)</td></tr>
                <tr><td>Mutu</td><td className="num">{Math.round(w.mutu * 100)}%</td><td>Rating &amp; acceptance rate historis supplier</td></tr>
                <tr><td>Jumlah</td><td className="num">{Math.round(w.jumlah * 100)}%</td><td>Kapasitas kirim (diisi mandiri supplier) dibanding kebutuhan aktual komoditas ini</td></tr>
                <tr><td>Waktu</td><td className="num">{Math.round(w.waktu * 100)}%</td><td>Proksi dari jumlah riwayat kirim (belum ada data lead-time asli)</td></tr>
              </tbody>
            </table>
            <p className="muted" style={{ margin: 0 }}>Bobot diatur lewat panel Tweaks (AHP). Klik "Jadikan Rekomendasi" untuk menyimpan pilihan Kepala SPPG — baris yang sudah direkomendasikan mencatat siapa &amp; kapan (lihat kolom "Direkomendasikan oleh").</p>
          </div>
        )}
      </div>

      {!activeCycle && (
        <SppgBanner tone="danger" title="Belum ada perencanaan porsi aktif">
          Kebutuhan bahan (jadi dasar kriteria "jumlah") baru bisa dihitung setelah ada Perencanaan Porsi yang disetujui.
        </SppgBanner>
      )}

      {activeCycle && calc && (
        <div className="card">
          <div className="card-head">
            <h2 className="h2">Kebutuhan bahan &amp; rekomendasi</h2>
            <span className="tag mono">{calc.rows.length} komoditas</span>
          </div>
          <div className="card-body" style={{ overflowX: "auto" }}>
            <table className="data">
              <thead>
                <tr><th>Komoditas</th><th className="num">Kebutuhan</th><th>Rekomendasi saat ini</th><th>Direkomendasikan oleh</th><th></th></tr>
              </thead>
              <tbody>
                {calc.rows.map(r => {
                  const rekomendasi = window.SupplierIntelligence.recommendForKomoditas(r.komoditas, 5, r.usulanOrderKg);
                  const rec = existing[r.komoditas];
                  const top = rekomendasi[0];
                  const isExpanded = expandedKomoditas === r.komoditas;
                  return (
                    <React.Fragment key={r.komoditas}>
                      <tr style={{ cursor: rekomendasi.length > 0 ? "pointer" : undefined }}
                        onClick={() => rekomendasi.length > 0 && setExpandedKomoditas(isExpanded ? null : r.komoditas)}>
                        <td>{isExpanded ? "▾" : "▸"} {r.komoditas}</td>
                        <td className="num">~{r.usulanOrderKg} kg</td>
                        <td>
                          {rekomendasi.length === 0 ? (
                            <span className="muted">Belum ada supplier terdaftar</span>
                          ) : rec ? (
                            <span className="pill ok"><span className="led"/>★ {rekomendasi.find(s => s.id === rec.supplier_id)?.nama || rec.supplier_id} · {rec.score != null ? Math.round(rec.score * 100) / 100 : top.safccScore}</span>
                          ) : (
                            <span className="pill neutral">{top.nama} · skor {top.safccScore} <span className="muted">(belum dipilih Kepala)</span></span>
                          )}
                        </td>
                        <td className="muted" style={{ fontSize: 11.5 }}>
                          {rec ? <>{rec.recommended_by} <br/>{new Date(rec.recommended_at).toLocaleString('id-ID')}</> : "—"}
                        </td>
                        <td className="muted" style={{ fontSize: 11.5 }}>{rekomendasi.length > 0 ? (isExpanded ? "Tutup" : "Lihat ranking") : ""}</td>
                      </tr>
                      {isExpanded && (
                        <tr>
                          <td colSpan={5} style={{ padding: 0, background: "var(--bg-2)" }}>
                            <table className="data" style={{ margin: "6px 12px 12px" }}>
                              <thead>
                                <tr><th>Supplier</th><th className="num">Harga/satuan</th><th className="num">Kapasitas</th><th className="num">Mutu (acceptance)</th><th className="num">Skor</th><th></th></tr>
                              </thead>
                              <tbody>
                                {rekomendasi.map(s => (
                                  <tr key={s.id} style={rec?.supplier_id === s.id ? { background: "var(--accent-soft, var(--bg-2))" } : undefined}>
                                    <td>{s.nama} <span className="muted" style={{ fontSize: 11 }}>· {s.lokasi}</span></td>
                                    <td className="num">{s.hargaSatuan != null ? `Rp${Number(s.hargaSatuan).toLocaleString('id-ID')}` : "—"}</td>
                                    <td className="num">{s.kapasitas != null ? `${s.kapasitas} kg` : "—"}</td>
                                    <td className="num">{Math.round((s.acceptanceRate || 0) * 100)}%</td>
                                    <td className="num"><strong>{s.safccScore}</strong></td>
                                    <td onClick={e => e.stopPropagation()}>
                                      {rec?.supplier_id === s.id ? (
                                        <span className="pill ok"><span className="led"/>★ Direkomendasikan</span>
                                      ) : (
                                        <button className="btn" style={{ padding: "4px 10px", fontSize: 12 }} disabled={busyKom === r.komoditas}
                                          onClick={() => handleRecommend(r.komoditas, s)}>Jadikan Rekomendasi</button>
                                      )}
                                    </td>
                                  </tr>
                                ))}
                              </tbody>
                            </table>
                          </td>
                        </tr>
                      )}
                    </React.Fragment>
                  );
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}
    </div>
  );
}
window.RekomendasiSupplier = RekomendasiSupplier;

function CariSupplier({ role }) {
  const { usePagination, Pagination } = window.IDSSUI;
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';

  const [tick, setTick] = useS_S(0);
  const [linkedIds, setLinkedIds] = useS_S(null);
  const [query, setQuery] = useS_S("");
  const [selectedSupplierId, setSelectedSupplierId] = useS_S(null);
  const [orderItems, setOrderItems] = useS_S({}); // { [komoditas]: jumlah }
  const [busy, setBusy] = useS_S(false);
  const [myOrders, setMyOrders] = useS_S([]);
  const [loadingOrders, setLoadingOrders] = useS_S(false);
  const [recommended, setRecommended] = useS_S({}); // { [komoditas]: supplier_id }, dari Kepala SPPG
  // Jalur ini HANYA utk kekurangan bahan baku dari SAFCC -- procurement wajib melaporkan
  // komoditas apa yang defisit & alasannya sebelum Kepala SPPG bisa menyetujui belanja mandiri.
  const [komoditasDefisitSelected, setKomoditasDefisitSelected] = useS_S([]);
  const [alasanKekurangan, setAlasanKekurangan] = useS_S("");
  // 2026-08: entry point BARU -- diarahkan ke sini dari Review Bahan Masuk (BahanMasukReview)
  // saat 1 komoditas ditolak Keamanan Pangan (EWS blocks), supaya "decision cari supplier
  // sendiri" utk bahan tidak aman itu benar2 actionable, bukan komplain lalu diam. Prefill
  // dititip lewat window.IDSS (bukan URL/props -- app ini tak punya router) & langsung
  // dikonsumsi+dibersihkan sekali saat mount, supaya tidak nempel di kunjungan berikutnya.
  const [prefillInfo, setPrefillInfo] = useS_S(null);
  useE_S(() => {
    const prefill = window.IDSS.__cariSupplierPrefill;
    if (!prefill) return;
    window.IDSS.__cariSupplierPrefill = null;
    setPrefillInfo(prefill);
    setQuery(prefill.komoditas || "");
    if (prefill.komoditas) setKomoditasDefisitSelected(list => list.includes(prefill.komoditas) ? list : [...list, prefill.komoditas]);
    if (prefill.alasan) setAlasanKekurangan(prefill.alasan);
  }, []);

  const refresh = () => setTick(t => t + 1);

  useE_S(() => {
    window.SupplierIntelligence.getRecommendations(dapurId).then(rows => {
      const map = {};
      rows.forEach(r => { map[r.komoditas] = r.supplier_id; });
      setRecommended(map);
    });
  }, [dapurId, tick]);

  useE_S(() => {
    if (!window.sb) { setLinkedIds([]); return; }
    let cancelled = false;
    window.sb.from('dapur_suppliers').select('supplier_id').eq('dapur_id', dapurId).then(({ data, error }) => {
      if (cancelled) return;
      if (error) { console.warn('[CariSupplier] gagal ambil daftar:', error.message); setLinkedIds([]); return; }
      setLinkedIds((data || []).map(r => r.supplier_id));
    });
    return () => { cancelled = true; };
  }, [dapurId, tick]);

  useE_S(() => {
    if (!window.sb) return;
    let cancelled = false;
    setLoadingOrders(true);
    window.sb.from('direct_purchase_orders').select('*').eq('dapur_id', dapurId).order('created_at', { ascending: false })
      .then(({ data, error }) => {
        if (cancelled) return;
        if (error) console.warn('[CariSupplier] gagal ambil riwayat:', error.message);
        setMyOrders(data || []);
      }).finally(() => { if (!cancelled) setLoadingOrders(false); });
    return () => { cancelled = true; };
  }, [dapurId, tick]);

  // Komoditas NERACA yang berstatus kurang/defisit -- daftar rujukan wajib dipilih procurement
  // sebelum bisa mengajukan (bukti kekurangan sungguhan, bukan sekadar preferensi jalur cepat).
  const komoditasDefisitTersedia = (window.IDSS.NERACA || []).filter(n => n.status === 'kurang' || n.status === 'defisit');
  const toggleKomoditasDefisit = (k) => setKomoditasDefisitSelected(list => list.includes(k) ? list.filter(x => x !== k) : [...list, k]);

  const pool = window.DapurSupplierRegistry.getPoolSuppliers();
  const { pageItems: pagedOrders, page: ordersPage, totalPages: ordersTotalPages, setPage: setOrdersPage, total: ordersTotal, pageSize: ordersPageSize } = usePagination(myOrders, 15);
  const linkedSuppliers = linkedIds ? pool.filter(s => linkedIds.includes(s.id)) : [];
  const recommendedKomoditas = (s) => (s.komoditas || []).filter(k => recommended[k] === s.id);
  const filtered = linkedSuppliers.filter(s => {
    if (!query.trim()) return true;
    const q = query.toLowerCase();
    return s.nama.toLowerCase().includes(q) || (s.komoditas || []).some(k => k.toLowerCase().includes(q));
  }).sort((a, b) => recommendedKomoditas(b).length - recommendedKomoditas(a).length);
  const selectedSupplier = linkedSuppliers.find(s => s.id === selectedSupplierId) || null;

  const pendingOrder = myOrders.find(o => ["menunggu_kepala"].includes(o.status));

  const handleSelectSupplier = (s) => { setSelectedSupplierId(s.id); setOrderItems({}); };
  const setItemQty = (komoditas, v) => setOrderItems(o => ({ ...o, [komoditas]: v }));

  const handleSubmit = async () => {
    if (!selectedSupplier) return;
    const items = Object.entries(orderItems)
      .filter(([, v]) => Number(v) > 0)
      .map(([komoditas, v]) => ({ komoditas, jumlah: Number(v), satuan: "kg" }));
    if (items.length === 0) { window.showToast?.("Isi jumlah minimal 1 komoditas", "warn"); return; }
    if (!alasanKekurangan.trim()) { window.showToast?.("Jelaskan dulu kekurangan bahan baku dari SAFCC sebelum mengajukan belanja mandiri", "warn"); return; }
    if (!window.sb) { window.showToast?.("Fitur ini butuh koneksi Supabase — belum aktif di lingkungan ini.", "warn"); return; }
    setBusy(true);
    const id = `DPO-${Date.now().toString(36).toUpperCase()}`;
    const { error } = await window.sb.from('direct_purchase_orders').insert({
      id, dapur_id: dapurId, supplier_id: selectedSupplier.id, status: 'menunggu_kepala',
      items, created_by: authedUser,
      alasan_kekurangan: alasanKekurangan.trim(), komoditas_defisit: komoditasDefisitSelected
    });
    setBusy(false);
    if (error) { window.showToast?.(`Gagal mengajukan: ${error.message}`, "error"); return; }
    window.AuditLog?.log('direct_order_submitted', { id, dapurId, supplierId: selectedSupplier.id, komoditasDefisit: komoditasDefisitSelected });
    window.showToast?.(`Pesanan ke ${selectedSupplier.nama} diajukan ke Kepala SPPG`, "success");
    setSelectedSupplierId(null); setOrderItems({}); setKomoditasDefisitSelected([]); setAlasanKekurangan("");
    refresh();
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Cari Supplier — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Jalur utama bahan baku tetap SAFCC. Belanja mandiri ke supplier di luar SAFCC HANYA untuk menutup kekurangan bahan baku yang benar-benar terjadi — wajib melaporkan komoditas & alasan kekurangannya, lalu menunggu persetujuan Kepala SPPG. Supplier di sini adalah yang sudah ditambahkan Kepala SPPG ke daftar dapur — hubungi Kepala bila supplier yang dicari belum ada.</p>
      </div>

      {!window.sb && <SppgBanner tone="warn">Fitur ini butuh koneksi Supabase — belum aktif di lingkungan ini.</SppgBanner>}
      {prefillInfo && (
        <SppgBanner tone="danger" title={`Diarahkan dari Review Bahan Masuk — ${prefillInfo.komoditas}`}>
          Bahan ini ditolak saat Keamanan Pangan karena {prefillInfo.alasan?.toLowerCase() || "gagal skor keamanan pangan"} — cari &amp; ajukan supplier alternatif untuk komoditas ini di bawah.
        </SppgBanner>
      )}
      {pendingOrder && <SppgBanner tone="warn" title="Ada pesanan langsung menunggu persetujuan Kepala SPPG">Selesaikan/tunggu keputusan sebelum mengajukan pesanan baru.</SppgBanner>}

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Supplier Terdaftar</h2>
          <input className="input" style={{ width: 220 }} placeholder="Cari nama atau komoditas..." value={query} onChange={e => setQuery(e.target.value)}/>
        </div>
        <div className="card-body">
          {linkedIds === null ? (
            <p className="muted" style={{ margin: 0 }}>Memuat...</p>
          ) : filtered.length === 0 ? (
            <p className="muted" style={{ margin: 0 }}>Tidak ada supplier cocok. Minta Kepala SPPG menambahkan lewat Kelola Supplier.</p>
          ) : (
            <div className="col" style={{ gap: 10 }}>
              {filtered.map(s => {
                const ahp = window.SupplierIntelligence.scoreSupplier(s);
                const safccScore = window.SupplierIntelligence.toSAFCCScore(ahp);
                const rekKom = recommendedKomoditas(s);
                return (
                  <div key={s.id} style={{
                    border: `1px solid ${selectedSupplierId === s.id ? "var(--accent)" : rekKom.length > 0 ? "var(--ok, #2e7d32)" : "var(--line)"}`,
                    borderRadius: "var(--radius-lg)", padding: 14, cursor: "pointer",
                    background: selectedSupplierId === s.id ? "var(--accent-soft, var(--bg-2))" : "transparent"
                  }} onClick={() => handleSelectSupplier(s)}>
                    <div className="row between">
                      <div>
                        <strong>{s.nama}</strong> <span className="muted" style={{ fontSize: 12 }}>· {s.type} · {s.lokasi}</span>
                        <div style={{ fontSize: 12, marginTop: 4 }}>{(s.komoditas || []).join(', ')}</div>
                        {rekKom.length > 0 && (
                          <div style={{ marginTop: 6 }}>
                            <span className="pill ok"><span className="led"/>★ Direkomendasikan Kepala untuk: {rekKom.join(', ')}</span>
                          </div>
                        )}
                      </div>
                      <span className="pill ok"><span className="led"/>Skor {safccScore}</span>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>
      </div>

      {selectedSupplier && (
        <div className="card">
          <div className="card-head"><h2 className="h2">Ajukan Pesanan — {selectedSupplier.nama}</h2></div>
          <div className="card-body">
            <table className="data">
              <thead><tr><th>Komoditas</th><th style={{ width: 140 }}>Jumlah (kg)</th></tr></thead>
              <tbody>
                {(selectedSupplier.komoditas || []).map(k => (
                  <tr key={k}>
                    <td>{k}</td>
                    <td><input type="number" min="0" className="input" style={{ width: 100 }}
                      value={orderItems[k] || ""} onChange={e => setItemQty(k, e.target.value)}/></td>
                  </tr>
                ))}
              </tbody>
            </table>

            <div className="hr" style={{ margin: "14px 0" }}/>
            <div className="field" style={{ marginBottom: 10 }}>
              <label>Komoditas mana yang defisit dari SAFCC? (opsional, bantu rujukan Kepala SPPG)</label>
              {komoditasDefisitTersedia.length === 0 ? (
                <p className="muted" style={{ fontSize: 12, margin: "4px 0 0" }}>Tidak ada komoditas berstatus kurang/defisit di Neraca saat ini — tetap jelaskan alasannya di bawah.</p>
              ) : (
                <div className="row" style={{ flexWrap: "wrap", gap: 6, marginTop: 4 }}>
                  {komoditasDefisitTersedia.map(n => {
                    const sel = komoditasDefisitSelected.includes(n.komoditas);
                    return (
                      <button key={n.komoditas} type="button" onClick={() => toggleKomoditasDefisit(n.komoditas)}
                        className={`pill ${sel ? "danger" : "neutral"}`} style={{ cursor: "pointer", border: "none" }}>
                        <span className="led"/>{n.komoditas} ({n.status})
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
            <div className="field" style={{ marginBottom: 0 }}>
              <label>Alasan kekurangan bahan baku (wajib diisi — dibaca Kepala SPPG saat menyetujui)</label>
              <textarea className="input" rows={3} value={alasanKekurangan} onChange={e => setAlasanKekurangan(e.target.value)}
                placeholder="mis. Ayam Karkas defisit 120kg — SAFCC baru bisa penuhi 3 hari lagi, sementara stok dapur habis besok pagi."/>
            </div>

            <button className="btn primary" style={{ marginTop: 14 }} onClick={handleSubmit} disabled={busy || !!pendingOrder || !alasanKekurangan.trim()}>
              Ajukan ke Kepala SPPG
            </button>
          </div>
        </div>
      )}

      {myOrders.length > 0 && (
        <div className="card">
          <div className="card-head"><h2 className="h2">Riwayat Pesanan Langsung</h2></div>
          <div className="card-body" style={{ overflowX: "auto" }}>
            <table className="data">
              <thead><tr><th>Diajukan</th><th>Supplier</th><th>Status</th><th>Item</th><th>Alasan kekurangan</th><th>Prediksi kedatangan</th><th>Catatan</th></tr></thead>
              <tbody>
                {pagedOrders.map(o => {
                  const s = pool.find(p => p.id === o.supplier_id);
                  return (
                    <tr key={o.id}>
                      <td>{new Date(o.created_at).toLocaleDateString('id-ID')}</td>
                      <td>{s?.nama || o.supplier_id}</td>
                      <td><StatusPill status={o.status}/></td>
                      <td>{(o.items || []).length} komoditas</td>
                      <td style={{ fontSize: 11.5, maxWidth: 220 }} className="muted" title={o.alasan_kekurangan || undefined}>
                        {o.alasan_kekurangan || "—"}
                        {(o.komoditas_defisit || []).length > 0 && <div>({o.komoditas_defisit.join(', ')})</div>}
                      </td>
                      <td>{o.predicted_arrival_at ? new Date(o.predicted_arrival_at).toLocaleString('id-ID') : "—"}</td>
                      <td>{o.review_note || o.supplier_response_note || "—"}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
            <Pagination page={ordersPage} totalPages={ordersTotalPages} onChange={setOrdersPage} total={ordersTotal} pageSize={ordersPageSize}/>
          </div>
        </div>
      )}
    </div>
  );
}
window.CariSupplier = CariSupplier;

function BahanMasukReview({ role, onNavigate }) {
  const { KPI } = window.IDSSUI;
  const dapurId = window.getDapurId(role);
  const dapurNama = window.getDapurName(dapurId);
  const authedUser = sessionStorage.getItem('authedUser') || 'unknown';
  const [tick, setTick] = useS_S(0);
  const [noteDraft, setNoteDraft] = useS_S({});
  const [overrideChecked, setOverrideChecked] = useS_S({});

  const pendingDeliveries = useM_S(() =>
    (window.IDSS.SUPPLIER_FULFILLMENT_SUBMISSIONS || []).filter(f =>
      f.procurementReviewStatus === "pending" && window.resolveDapurId(f) === dapurId),
    [tick, dapurId]
  );

  const refresh = () => setTick(t => t + 1);
  const getNote = (id) => noteDraft[id] || "";
  const setNote = (id, v) => setNoteDraft(d => ({ ...d, [id]: v }));

  // Keamanan Pangan BUKAN lagi layar terpisah yang cuma informasi -- skor & alert EWS tahap
  // Penerimaan (T1) di sini BENAR-BENAR memfilter keputusan: skor di bawah ambang WASPADA
  // (< 3.5, sama persis dgn ambang AMAN/WASPADA/BERESIKO/KRITIS yang dipakai Agro & Supplier)
  // atau ada alert aktif akan MENGUNCI tombol Gunakan sampai Ahli Gizi sadar & menandai
  // override (dgn catatan wajib) -- bukan cuma badge yang bisa diabaikan begitu saja.
  const getEwsInfo = (komoditas) => {
    const t1 = window.IDSS.EWS_DATA_KOMODITAS?.T1?.[komoditas];
    if (!t1 || !Array.isArray(t1.skor) || t1.skor.length === 0) return null;
    const avg = t1.skor.reduce((a, b) => a + b, 0) / t1.skor.length;
    const status = avg >= 4.3 ? "AMAN" : avg >= 3.5 ? "WASPADA" : avg >= 2.5 ? "BERESIKO" : "KRITIS";
    return { avg, alert: t1.alert || 0, batasOverride: t1.batasOverride, status, blocks: avg < 3.5 || (t1.alert || 0) > 0 };
  };

  // Ringkasan aman vs tidak aman -- sebelumnya cuma badge per-item, tidak ada rekap jadi Kepala
  // SPPG/Procurement tidak bisa langsung lihat "berapa yang bermasalah hari ini" tanpa scroll
  // baca satu-satu. "Tidak aman" = blocked (WASPADA/BERESIKO/KRITIS atau ada alert aktif),
  // sama persis dgn definisi yang mengunci tombol Gunakan di bawah -- 1 sumber kebenaran.
  const ewsSummary = pendingDeliveries.reduce((acc, f) => {
    const ews = getEwsInfo(f.komoditas);
    if (!ews) acc.belumAda++;
    else if (ews.blocks) acc.tidakAman++;
    else acc.aman++;
    return acc;
  }, { aman: 0, tidakAman: 0, belumAda: 0 });

  // Decision utk bahan TIDAK AMAN: bukan cuma tolak+komplain lalu diam -- arahkan langsung ke
  // Cari Supplier (jalur belanja mandiri di luar SAFCC) dgn komoditas & alasan sudah terisi,
  // supaya "harus dipikirkan decision cari supplier sendiri" jadi 1 klik, bukan navigasi manual
  // yang gampang terlewat.
  const handleCariSupplierSendiri = (f, ews) => {
    window.IDSS.__cariSupplierPrefill = {
      komoditas: f.komoditas,
      alasan: `Bahan ${f.komoditas} dari SAFCC/ALC ditolak Keamanan Pangan (skor ${ews.status}${ews.alert > 0 ? `, ${ews.alert} alert aktif` : ''}) saat Review Bahan Masuk.`
    };
    if (onNavigate) {
      onNavigate('cari-supplier');
    } else {
      window.showToast?.("Buka menu Cari Supplier untuk melanjutkan", "info");
    }
  };

  const handleDecision = (f, decision, blocked) => {
    if (decision === "tolak" && !getNote(f.id).trim()) {
      window.showToast?.("Catatan wajib diisi saat menolak bahan", "warn");
      return;
    }
    if (decision === "gunakan" && blocked && !overrideChecked[f.id]) {
      window.showToast?.("Keamanan Pangan merekomendasikan tolak — centang override & isi alasan dulu jika tetap ingin menggunakan", "warn");
      return;
    }
    if (decision === "gunakan" && blocked && !getNote(f.id).trim()) {
      window.showToast?.("Catatan alasan wajib diisi saat override rekomendasi Keamanan Pangan", "warn");
      return;
    }
    window.ApprovalFlow.reviewDeliveryQuality(f.id, decision, authedUser, getNote(f.id));
    // Retur/komplain ke supplier -- hanya utk kiriman direct sourcing (sumber="DIRECT")
    // yang punya supplierId eksplisit; kiriman ALC (logistik internal Agro) di luar cakupan ini.
    if (decision === "tolak" && f.supplierId) {
      window.SupplierComplaints.create({
        supplierId: f.supplierId, dapurId, source: 'direct', refTable: 'fulfillment_submissions', refId: f.id,
        komoditas: f.komoditas, jumlahDitolak: f.volumeReceived, alasan: getNote(f.id), createdBy: authedUser
      });
    }
    window.showToast?.(
      decision === "gunakan" ? `${f.komoditas} disetujui, masuk stok dapur` : `${f.komoditas} ditolak — tidak masuk stok, komplain dikirim ke supplier`,
      decision === "gunakan" ? "success" : "info"
    );
    refresh();
  };

  return (
    <div className="col" style={{ gap: 16 }}>
      <div>
        <h2 className="page-title">Review Bahan Masuk (Keamanan Pangan) — {dapurNama}</h2>
        <p className="muted" style={{ marginTop: 4 }}>Bahan hasil pesanan Procurement yang sudah tiba fisik menunggu keputusan Anda. Skor Keamanan Pangan (EWS tahap Penerimaan) di bawah ini adalah FILTER keputusan — bahan dgn skor WASPADA/BERESIKO/KRITIS atau alert aktif tombol "Gunakan"-nya terkunci sampai Anda tandai override.</p>
      </div>

      {pendingDeliveries.length > 0 && (
        <div className="grid cols-3" style={{ gap: 12 }}>
          <KPI label="Menunggu review" value={pendingDeliveries.length} unit="bahan"/>
          <KPI label="Aman" value={ewsSummary.aman} unit="bahan" intent={ewsSummary.aman > 0 ? "success" : undefined}/>
          <KPI label="Tidak aman" value={ewsSummary.tidakAman} unit="bahan" intent={ewsSummary.tidakAman > 0 ? "danger" : undefined}/>
        </div>
      )}

      <div className="card">
        <div className="card-head">
          <h2 className="h2">Menunggu Review</h2>
          <span className="tag mono">{pendingDeliveries.length} bahan</span>
        </div>
        <div className="card-body">
          {pendingDeliveries.length === 0 && <p className="muted" style={{ margin: 0 }}>Tidak ada bahan menunggu review.</p>}
          {pendingDeliveries.map(f => {
            const ews = getEwsInfo(f.komoditas);
            const ewsTone = !ews ? "neutral" : ews.status === "AMAN" ? "ok" : ews.status === "WASPADA" ? "warn" : "danger";
            const blocked = !!ews?.blocks;
            const canUse = !blocked || overrideChecked[f.id];
            return (
              <SppgSubcard key={f.id}>
                <div className="row between" style={{ flexWrap: "wrap", gap: 8 }}>
                  <div>
                    <strong>{f.komoditas}</strong> — {f.volumeReceived} kg diterima ({f.jumlahDiberikan} kg diminta)
                    <div className="muted" style={{ fontSize: 12, marginTop: 2 }}>Diterima oleh {f.receivedBy} pada {f.receivedAt ? new Date(f.receivedAt).toLocaleString('id-ID') : "—"}</div>
                  </div>
                  {ews && <span className={`pill ${ewsTone}`}><span className="led"/>Keamanan Pangan (Penerimaan): {ews.status} · {ews.avg.toFixed(1)}/5</span>}
                </div>
                {blocked && (
                  <div style={{ marginTop: 10 }}>
                    <SppgBanner tone="danger" title={`Keamanan Pangan merekomendasikan TOLAK — ${f.komoditas}`}>
                      {ews.alert > 0 ? `${ews.alert} alert aktif pada tahap Penerimaan (T1). ` : ""}
                      Skor {ews.status.toLowerCase()} ({ews.avg.toFixed(1)}/5, ambang aman ≥3.5). Tombol Gunakan terkunci — centang override di bawah jika tetap ingin menggunakan bahan ini, disertai alasan.
                    </SppgBanner>
                    <button className="btn" style={{ marginTop: 8 }} onClick={() => handleCariSupplierSendiri(f, ews)}>
                      Cari Supplier Sendiri untuk {f.komoditas} →
                    </button>
                  </div>
                )}
                {f.qualityNotes && <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>Catatan penerimaan procurement: {f.qualityNotes}</div>}
                <textarea className="input" placeholder={blocked ? "Alasan override (wajib)" : "Catatan review (wajib jika tolak)"} style={{ marginTop: 10, width: "100%" }}
                  value={getNote(f.id)} onChange={e => setNote(f.id, e.target.value)}/>
                {blocked && (
                  <label className="row" style={{ gap: 8, alignItems: "center", fontSize: 12.5, marginTop: 8 }}>
                    <input type="checkbox" checked={!!overrideChecked[f.id]} onChange={e => setOverrideChecked(o => ({ ...o, [f.id]: e.target.checked }))}/>
                    Saya tetap ingin menggunakan bahan ini meski Keamanan Pangan merekomendasikan tolak
                  </label>
                )}
                <div className="row" style={{ gap: 8, marginTop: 10 }}>
                  <button className="btn primary" onClick={() => handleDecision(f, "gunakan", blocked)} disabled={!canUse} title={!canUse ? "Terkunci oleh Keamanan Pangan — centang override dulu" : undefined}>
                    Gunakan
                  </button>
                  <button className="btn danger" onClick={() => handleDecision(f, "tolak", blocked)}>Tolak</button>
                </div>
              </SppgSubcard>
            );
          })}
        </div>
      </div>
    </div>
  );
}
window.BahanMasukReview = BahanMasukReview;
