/* ============================================================
   dispatch.jsx — Calibration dispatch (ส่งสอบเทียบ) workflow.
   Lifecycle:  รอส่ง → ส่งแล้ว → กำลังสอบเทียบ → รับกลับแล้ว
   Groups instruments into dispatch rounds (batch), prints a
   cover note, and on return writes new cal/exp back to the
   instrument (via onUpdateInstrument) for ISO 15189 traceability.
   ============================================================ */

const CJ_STAGE = {
  pending:    { i: 0, label: 'รอส่ง',         color: 'var(--ink-3)',  soft: 'var(--none-soft)',  icon: I.clock },
  sent:       { i: 1, label: 'ส่งแล้ว',        color: '#3b6ea5',       soft: 'rgba(59,110,165,.12)', icon: I.send },
  inprogress: { i: 2, label: 'กำลังสอบเทียบ',  color: 'var(--warn)',   soft: 'var(--warn-soft)',  icon: I.shield },
  returned:   { i: 3, label: 'รับกลับแล้ว',     color: 'var(--ok)',     soft: 'var(--ok-soft)',    icon: I.check },
};
const CJ_ORDER = ['pending', 'sent', 'inprogress', 'returned'];
const CJ_RESULTS = ['ผ่าน', 'ผ่านโดยมีเงื่อนไข', 'ไม่ผ่าน'];

function cjAddDays(iso, n) {
  if (!iso) return '';
  const d = new Date(iso + 'T00:00:00');
  if (isNaN(d.getTime())) return '';
  d.setDate(d.getDate() + (Number(n) || 0));
  const p = (x) => String(x).padStart(2, '0');
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
}

/* a batch's overall stage = the least-advanced stage among its jobs */
function batchStage(jobs) {
  let min = 3;
  jobs.forEach(j => { const s = (CJ_STAGE[j.status] || CJ_STAGE.sent).i; if (s < min) min = s; });
  return CJ_ORDER[min];
}

function CJField({ label, children, hint }) {
  return (
    <label style={{ display: 'block', marginBottom: 12 }}>
      <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--ink-2)', marginBottom: 5 }}>{label}</div>
      {children}
      {hint && <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 4 }}>{hint}</div>}
    </label>
  );
}
const cjInput = { width: '100%', padding: '9px 11px', borderRadius: 9, border: '1px solid var(--line)', background: 'var(--surface)', color: 'var(--ink)', fontSize: 13.5, fontFamily: 'inherit' };

function CJModal({ title, sub, icon, onClose, children, footer, wide }) {
  const Ico = icon;
  // Render at <body> via a portal so the overlay's position:fixed always
  // resolves to the viewport — never to a transformed ancestor (e.g. the
  // detail page wraps its body in .enter which animates `transform`, which
  // would otherwise make this modal a child of *that* container, putting
  // it off-screen below the fold).
  const modal = (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(15,18,22,.5)', zIndex: 60, backdropFilter: 'blur(2px)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '4vh 16px' }}>
      <div className="card" onClick={e => e.stopPropagation()} style={{ width: 'min(' + (wide ? 760 : 520) + 'px, 96vw)', maxHeight: '92vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '16px 20px', borderBottom: '1px solid var(--line)', flex: 'none' }}>
          <span style={{ width: 36, height: 36, borderRadius: 10, background: 'var(--accent-soft)', color: 'var(--accent-strong)', display: 'grid', placeItems: 'center', flex: 'none' }}>{Ico ? <Ico size={19} /> : null}</span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontWeight: 700, fontSize: 16 }}>{title}</div>
            {sub && <div className="page-sub">{sub}</div>}
          </div>
          <button className="btn btn-icon btn-ghost" onClick={onClose}><I.close size={18} /></button>
        </div>
        <div style={{ padding: '18px 20px', overflowY: 'auto', minHeight: 0, flex: 1 }}>{children}</div>
        {footer && <div style={{ padding: '12px 20px', borderTop: '1px solid var(--line)', flex: 'none', background: 'var(--surface)' }}>{footer}</div>}
      </div>
    </div>
  );
  return (window.ReactDOM && window.ReactDOM.createPortal) ? window.ReactDOM.createPortal(modal, document.body) : modal;
}

/* ---------------- create-dispatch modal ---------------- */
function CreateDispatch({ instruments, activeCodes, onCreate, onClose }) {
  const [q, setQ] = useState('');
  const [sel, setSel] = useState(() => new Set());
  const [provider, setProvider] = useState('');
  const [sentDate, setSentDate] = useState(window.TODAY_ISO);
  const [expectedReturn, setExpectedReturn] = useState(cjAddDays(window.TODAY_ISO, 30));
  const [note, setNote] = useState('');

  const providers = useMemo(() => {
    const s = new Set(['สถาบันมาตรวิทยาแห่งชาติ (NIMT)', 'ศูนย์สอบเทียบเครื่องมือแพทย์ เขต 10', 'หน่วยวิศวกรรมการแพทย์ รพ.', 'บริษัทผู้แทนจำหน่าย (Authorized)']);
    instruments.forEach(i => { const m = window.instrumentMeta ? window.instrumentMeta(i) : {}; const p = i.provider || m.provider; if (p) s.add(p); });
    return [...s];
  }, [instruments]);

  const candidates = useMemo(() => {
    const t = q.trim().toLowerCase();
    return instruments
      .filter(i => !window.isDecommissioned(i))
      .filter(i => !activeCodes.has(i.code))
      .filter(i => !t || (i.code + ' ' + i.type + ' ' + (i.brand || '') + ' ' + (i.dept || '')).toLowerCase().includes(t))
      .map(i => ({ i, s: window.statusOf(i) }))
      .sort((a, b) => {
        const da = a.s.days == null ? 99999 : a.s.days, db = b.s.days == null ? 99999 : b.s.days;
        return da - db;
      })
      .slice(0, 400);
  }, [instruments, q, activeCodes]);

  const toggle = (code) => setSel(s => { const n = new Set(s); n.has(code) ? n.delete(code) : n.add(code); return n; });
  const selList = instruments.filter(i => sel.has(i.code));

  const submit = (markSent) => {
    if (!sel.size) return;
    onCreate(selList, { provider: provider.trim(), sentDate, expectedReturn, note: note.trim() }, markSent);
  };

  const footer = (
    <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end', flexWrap: 'wrap', alignItems: 'center' }}>
      <span style={{ fontSize: 12.5, color: 'var(--ink-3)', marginRight: 'auto' }}>เลือกแล้ว <b style={{ color: 'var(--accent-strong)' }}>{sel.size}</b> เครื่อง</span>
      <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
      <button className="btn" disabled={!sel.size} onClick={() => submit(false)} title="บันทึกไว้ก่อน ยังไม่ส่งจริง"><I.clock size={15} /> บันทึกร่าง (รอส่ง)</button>
      <button className="btn btn-primary" disabled={!sel.size || !provider.trim()} onClick={() => submit(true)}><I.send size={15} /> ยืนยันส่ง &amp; พิมพ์ใบนำส่ง</button>
    </div>
  );

  return (
    <CJModal wide title="สร้างรอบส่งสอบเทียบ" sub="เลือกเครื่องมือที่จะนำส่ง · ระบบจะออกใบนำส่งให้" icon={I.send} onClose={onClose} footer={footer}>
      <div style={{ display: 'grid', gridTemplateColumns: '1.3fr 1fr', gap: 18 }} className="cj-create-grid">
        {/* picker */}
        <div style={{ minWidth: 0 }}>
          <div style={{ position: 'relative', marginBottom: 10 }}>
            <I.search size={15} style={{ position: 'absolute', left: 11, top: 11, color: 'var(--ink-3)' }} />
            <input value={q} onChange={e => setQ(e.target.value)} placeholder="ค้นหา รหัส / ชื่อ / แผนก…" style={{ ...cjInput, paddingLeft: 32 }} />
          </div>
          <div style={{ border: '1px solid var(--line)', borderRadius: 10, maxHeight: 'min(340px, 40vh)', overflowY: 'auto' }}>
            {candidates.length === 0 && <div style={{ padding: 18, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>ไม่พบเครื่องมือที่พร้อมส่ง</div>}
            {candidates.map(({ i, s }) => {
              const on = sel.has(i.code);
              const col = { ok: 'var(--ok)', warn: 'var(--warn)', danger: 'var(--danger)', none: 'var(--ink-3)' }[s.key];
              return (
                <div key={i.code} onClick={() => toggle(i.code)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 11px', borderBottom: '1px solid var(--line-soft)', cursor: 'pointer', background: on ? 'var(--accent-soft)' : 'transparent' }}>
                  <input type="checkbox" checked={on} readOnly style={{ flex: 'none' }} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 12.5, fontWeight: 600, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}><span className="mono">{i.code}</span> · {i.type}</div>
                    <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>{(window.DEPT_META[i.dept] || {}).short || i.dept}{i.exp ? ' · ครบกำหนด ' + window.fmtDateFull(i.exp) : ' · ยังไม่มีนัด'}</div>
                  </div>
                  <span style={{ fontSize: 11, fontWeight: 700, color: col, flex: 'none' }}>{s.days == null ? '—' : (s.days < 0 ? 'เกิน ' + Math.abs(s.days) + 'ว' : s.days + 'ว')}</span>
                </div>
              );
            })}
          </div>
          <div style={{ fontSize: 12, color: 'var(--ink-3)', marginTop: 8 }}>เลือกแล้ว <b style={{ color: 'var(--accent-strong)' }}>{sel.size}</b> เครื่อง{activeCodes.size > 0 ? ` · ซ่อนเครื่องที่กำลังส่งอยู่ ${activeCodes.size} เครื่อง` : ''}</div>
        </div>

        {/* details */}
        <div style={{ minWidth: 0 }}>
          <CJField label="หน่วยงานสอบเทียบ" hint="พิมพ์ใหม่ หรือเลือกจากรายการที่เคยใช้">
            <input list="cj-providers" value={provider} onChange={e => setProvider(e.target.value)} placeholder="เช่น สถาบันมาตรวิทยาฯ (NIMT)" style={cjInput} />
            <datalist id="cj-providers">{providers.map(p => <option key={p} value={p} />)}</datalist>
          </CJField>
          <CJField label="วันที่นำส่ง">
            <input type="date" value={sentDate} onChange={e => setSentDate(e.target.value)} style={cjInput} />
          </CJField>
          <CJField label="กำหนดรับกลับ (ประมาณ)" hint="ใช้เตือนเมื่อเลยกำหนดยังไม่ได้รับเครื่องกลับ">
            <input type="date" value={expectedReturn} onChange={e => setExpectedReturn(e.target.value)} style={cjInput} />
          </CJField>
          <CJField label="หมายเหตุ (ถ้ามี)">
            <textarea value={note} onChange={e => setNote(e.target.value)} rows={2} placeholder="เช่น ขอใบรับรองพร้อมผลย้อนกลับได้ (traceable)" style={{ ...cjInput, resize: 'vertical' }} />
          </CJField>
        </div>
      </div>
    </CJModal>
  );
}

/* ---------------- return-intake modal ---------------- */
function ReturnJob({ job, onSave, onClose, go }) {
  const [returnedDate, setReturnedDate] = useState(window.TODAY_ISO);
  const [result, setResult] = useState('ผ่าน');
  const [certNo, setCertNo] = useState(job.certNo || '');
  const [newCal, setNewCal] = useState(window.TODAY_ISO);
  const [newExp, setNewExp] = useState(cjAddDays(window.TODAY_ISO, job.intervalDays || 365));
  const [note, setNote] = useState('');
  const [touchedExp, setTouchedExp] = useState(false);
  const [certDoc, setCertDoc] = useState(null);   // uploaded certificate file (linked to instrument)
  const [upBusy, setUpBusy] = useState(false);
  const [upErr, setUpErr] = useState('');
  const certRef = useRef(null);

  // keep next-due in sync with cal date until the user edits it directly
  useEffect(() => { if (!touchedExp) setNewExp(cjAddDays(newCal, job.intervalDays || 365)); }, [newCal]);

  const uploadCert = async (files) => {
    setUpErr('');
    const file = files && files[0];
    if (!file) return;
    if (file.size > 12 * 1024 * 1024) { setUpErr('ไฟล์ใหญ่เกิน 12MB'); return; }
    if (window.Backend.mode !== 'gas') { setUpErr('โหมด local อัปโหลดไฟล์จริงไม่ได้'); return; }
    setUpBusy(true);
    try {
      const dataBase64 = await window.fileToBase64(file);
      const res = await window.Backend.fileUpload({ code: job.code, name: file.name, mime: file.type || 'application/octet-stream', type: 'cert', dataBase64 });
      if (!res || !res.ok || !res.doc) throw new Error((res && res.message) || 'อัปโหลดไม่สำเร็จ');
      window.Docs.addLocal(res.doc);
      setCertDoc(res.doc);
      if (window.Audit) window.Audit.add({ action: 'doc_add', code: job.code, target: file.name, detail: 'แนบใบรับรองสอบเทียบ (cert)' });
    } catch (e) { setUpErr(e.message || String(e)); }
    setUpBusy(false);
    if (certRef.current) certRef.current.value = '';
  };

  const save = () => {
    onSave(job, { returnedDate, result, certNo: certNo.trim(), newCal, newExp, note: note.trim(), certDocUrl: certDoc ? certDoc.url : '', certDocName: certDoc ? certDoc.name : '' });
  };

  const footer = (
    <div style={{ display: 'flex', gap: 10, justifyContent: 'space-between', flexWrap: 'wrap', alignItems: 'center' }}>
      <button className="btn btn-ghost" onClick={() => { onClose(); go && go('detail', { code: job.code }); }}><I.copy size={14} /> ดูเอกสารทั้งหมด</button>
      <div style={{ display: 'flex', gap: 10 }}>
        <button className="btn btn-ghost" onClick={onClose}>ยกเลิก</button>
        <button className="btn btn-primary" onClick={save}><I.check size={15} /> บันทึกการรับกลับ</button>
      </div>
    </div>
  );

  return (
    <CJModal title="รับเครื่องกลับจากสอบเทียบ" sub={job.code + ' · ' + job.instType} icon={I.inbox} onClose={onClose} footer={footer}>
      <CJField label="วันที่รับเครื่องกลับ">
        <input type="date" value={returnedDate} onChange={e => setReturnedDate(e.target.value)} style={cjInput} />
      </CJField>
      <CJField label="ผลการสอบเทียบ">
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {CJ_RESULTS.map(r => (
            <button key={r} type="button" onClick={() => setResult(r)}
              style={{ padding: '8px 14px', borderRadius: 9, fontSize: 13, fontWeight: 600, cursor: 'pointer', border: '1px solid ' + (result === r ? (r === 'ไม่ผ่าน' ? 'var(--danger)' : 'var(--ok)') : 'var(--line)'), background: result === r ? (r === 'ไม่ผ่าน' ? 'var(--danger-soft)' : 'var(--ok-soft)') : 'var(--surface)', color: result === r ? (r === 'ไม่ผ่าน' ? 'var(--danger)' : 'var(--ok)') : 'var(--ink-2)' }}>
              {r}
            </button>
          ))}
        </div>
      </CJField>
      <CJField label="เลขที่ใบรับรองผลการสอบเทียบ (Certificate No.)" hint="เลขตามใบรับรองจริงที่หน่วยงานออกให้">
        <input value={certNo} onChange={e => setCertNo(e.target.value)} placeholder="เช่น CAL-2569-0123" style={{ ...cjInput, fontFamily: 'var(--mono, monospace)' }} />
      </CJField>
      <CJField label="แนบไฟล์ใบรับรอง (Certificate)" hint="ไฟล์จะถูกเก็บใน Drive และผูกกับเครื่องนี้อัตโนมัติ (ดูได้ที่หน้าเครื่อง > เอกสารแนบ)">
        <input type="file" ref={certRef} hidden accept="image/*,.pdf,.doc,.docx" onChange={e => uploadCert(e.target.files)} />
        {certDoc ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, background: 'var(--ok-soft)', border: '1px solid var(--ok)', borderRadius: 9, padding: '9px 12px' }}>
            <I.check size={16} style={{ color: 'var(--ok)', flex: 'none' }} />
            <a href={certDoc.url} target="_blank" rel="noopener noreferrer" style={{ flex: 1, minWidth: 0, fontSize: 13, fontWeight: 600, color: 'var(--ink)', textDecoration: 'none', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{certDoc.name}</a>
            <button type="button" className="btn btn-icon btn-ghost" title="เปลี่ยนไฟล์" onClick={() => { setCertDoc(null); certRef.current && certRef.current.click(); }}><I.refresh size={15} /></button>
          </div>
        ) : (
          <button type="button" className="btn" disabled={upBusy} onClick={() => certRef.current && certRef.current.click()} style={{ width: '100%', justifyContent: 'center' }}>
            {upBusy ? <><I.refresh size={15} style={{ animation: 'spin 1s linear infinite' }} /> กำลังอัปโหลด…</> : <><I.download size={15} style={{ transform: 'rotate(180deg)' }} /> เลือกไฟล์ใบรับรอง (PDF / รูป)</>}
          </button>
        )}
        {upErr && <div style={{ fontSize: 12, color: 'var(--danger)', marginTop: 5 }}>{upErr}</div>}
      </CJField>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
        <CJField label="วันสอบเทียบ (รอบนี้)">
          <input type="date" value={newCal} onChange={e => setNewCal(e.target.value)} style={cjInput} />
        </CJField>
        <CJField label="ครบกำหนดรอบถัดไป" hint="คำนวณอัตโนมัติจากรอบเดิม ปรับได้">
          <input type="date" value={newExp} onChange={e => { setTouchedExp(true); setNewExp(e.target.value); }} style={cjInput} />
        </CJField>
      </div>
      <CJField label="เหตุผล / เกณฑ์การยอมรับ (ถ้ามี)" hint="ระบุเหตุผลที่สรุปผลนี้ เพื่อการตรวจสอบย้อนกลับ (ISO 15189)">
        <textarea value={note} onChange={e => setNote(e.target.value)} rows={2} placeholder="เช่น ค่า Uncertainty (37, 62, 100 °C) อยู่ในช่วง MPE (±2 °C)" style={{ ...cjInput, resize: 'vertical' }} />
      </CJField>

      <div style={{ background: 'var(--accent-soft)', borderRadius: 10, padding: '10px 13px', fontSize: 12.5, color: 'var(--ink-2)', marginBottom: 14, display: 'flex', gap: 8, alignItems: 'flex-start' }}>
        <I.shield size={15} style={{ flex: 'none', marginTop: 1, color: 'var(--accent-strong)' }} />
        <span>เมื่อบันทึก ระบบจะอัปเดต <b>วันสอบเทียบ/ครบกำหนด</b> และ <b>ผล/เลขใบรับรอง</b> ของเครื่องนี้ให้อัตโนมัติ พร้อมลงประวัติ (Audit)</span>
      </div>
    </CJModal>
  );
}

/* ---------------- stage stepper ---------------- */
function Stepper({ stage }) {
  const cur = (CJ_STAGE[stage] || CJ_STAGE.sent).i;
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 0, flexWrap: 'wrap' }}>
      {CJ_ORDER.map((k, idx) => {
        const st = CJ_STAGE[k]; const done = idx <= cur; const Ico = st.icon;
        return (
          <React.Fragment key={k}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11.5, fontWeight: 600, color: done ? st.color : 'var(--ink-3)', opacity: done ? 1 : .55 }}>
              <span style={{ width: 19, height: 19, borderRadius: 99, display: 'grid', placeItems: 'center', background: done ? st.soft : 'var(--line-soft)', color: done ? st.color : 'var(--ink-3)' }}><Ico size={12} /></span>
              {st.label}
            </span>
            {idx < CJ_ORDER.length - 1 && <span style={{ width: 16, height: 2, background: idx < cur ? CJ_STAGE[CJ_ORDER[idx + 1]].color : 'var(--line)', margin: '0 6px', borderRadius: 2 }} />}
          </React.Fragment>
        );
      })}
    </div>
  );
}

/* ---------------- one dispatch batch card ---------------- */
function BatchCard({ batchId, jobs, admin, onAdvance, onReturn, onPrint, onCancel }) {
  const stage = batchStage(jobs);
  const st = CJ_STAGE[stage];
  const returnedN = jobs.filter(j => j.status === 'returned').length;
  const meta0 = jobs[0] || {};
  const overdueReturn = meta0.expectedReturn && returnedN < jobs.length && meta0.expectedReturn < window.TODAY_ISO
    ? Math.round((new Date(window.TODAY_ISO) - new Date(meta0.expectedReturn)) / 86400000) : 0;

  return (
    <div className="card enter" style={{ overflow: 'hidden' }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 14, padding: '15px 18px', borderBottom: '1px solid var(--line)', flexWrap: 'wrap' }}>
        <span style={{ width: 40, height: 40, borderRadius: 11, background: st.soft, color: st.color, display: 'grid', placeItems: 'center', flex: 'none' }}><st.icon size={20} /></span>
        <div style={{ flex: 1, minWidth: 180 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
            <span className="mono" style={{ fontWeight: 700, fontSize: 14 }}>{batchId}</span>
            <span className="pill" style={{ background: st.soft, color: st.color, fontWeight: 700 }}>{st.label}</span>
            {overdueReturn > 0 && <span className="pill danger" style={{ fontWeight: 700 }}>เลยกำหนดรับกลับ {overdueReturn} วัน</span>}
          </div>
          <div style={{ fontSize: 12.5, color: 'var(--ink-3)', marginTop: 4 }}>
            {meta0.provider || '—'} · ส่ง {meta0.sentDate ? window.fmtDateFull(meta0.sentDate) : '—'}
            {meta0.expectedReturn ? ' · กำหนดรับกลับ ' + window.fmtDateFull(meta0.expectedReturn) : ''}
          </div>
        </div>
        <div style={{ textAlign: 'right', flex: 'none' }}>
          <div className="figure" style={{ fontSize: 22, color: st.color }}>{returnedN}/{jobs.length}</div>
          <div style={{ fontSize: 10.5, color: 'var(--ink-3)' }}>รับกลับแล้ว</div>
        </div>
      </div>

      <div style={{ padding: '12px 18px', borderBottom: '1px solid var(--line-soft)', display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
        <Stepper stage={stage} />
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <button className="btn btn-sm" onClick={() => onPrint(jobs, meta0, batchId)}><I.print size={14} /> ใบนำส่ง</button>
          {stage === 'pending' && <button className="btn btn-sm btn-primary" onClick={() => onAdvance(batchId, 'sent')}><I.send size={14} /> ทำเครื่องหมายส่งแล้ว</button>}
          {stage === 'sent' && <button className="btn btn-sm" onClick={() => onAdvance(batchId, 'inprogress')}><I.shield size={14} /> หน่วยงานเริ่มสอบเทียบ</button>}
          {admin && returnedN < jobs.length && <button className="btn btn-sm" style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }} onClick={() => onCancel(batchId)}><I.trash size={14} /> ยกเลิกรอบ</button>}
        </div>
      </div>

      <div>
        {jobs.map(j => {
          const done = j.status === 'returned';
          const fail = done && j.result === 'ไม่ผ่าน';
          return (
            <div key={j.id} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 18px', borderBottom: '1px solid var(--line-soft)' }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontWeight: 600 }}><span className="mono">{j.code}</span> · {j.instType}</div>
                <div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
                  {(window.DEPT_META[j.dept] || {}).short || j.dept}
                  {j.calPoint ? ' · จุดสอบเทียบ ' + j.calPoint : ''}
                  {done && j.certNo ? ' · ใบรับรอง ' + j.certNo : ''}
                </div>
              </div>
              {done ? (
                <div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 'none' }}>
                  {j.certDocUrl && <a href={j.certDocUrl} target="_blank" rel="noopener noreferrer" className="btn btn-icon btn-ghost" title="เปิดใบรับรอง" style={{ textDecoration: 'none', color: 'var(--accent-strong)' }}><I.shield size={15} /></a>}
                  <div style={{ textAlign: 'right' }}>
                    <span className="pill" style={{ background: fail ? 'var(--danger-soft)' : 'var(--ok-soft)', color: fail ? 'var(--danger)' : 'var(--ok)', fontWeight: 700 }}>{j.result || 'รับกลับแล้ว'}</span>
                    <div style={{ fontSize: 10.5, color: 'var(--ink-3)', marginTop: 3 }}>รับกลับ {window.fmtDateFull(j.returnedDate)} · ครบใหม่ {window.fmtDateFull(j.newExp)}</div>
                  </div>
                </div>
              ) : (
                <button className="btn btn-sm btn-primary" style={{ flex: 'none' }} onClick={() => onReturn(j)}><I.inbox size={14} /> รับกลับ</button>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ---------------- main page ---------------- */
function Dispatch({ instruments, go, admin, onUpdateInstrument, currentUser }) {
  const [, force] = useState(0);
  useEffect(() => window.CalJobs.subscribe(() => force(x => x + 1)), []);
  const [seg, setSeg] = useState('active');
  const [createOpen, setCreateOpen] = useState(false);
  const [returnJob, setReturnJob] = useState(null);

  const allJobs = window.CalJobs.all();
  // restrict to instruments the current user can see (dept scoping handled upstream)
  const visibleCodes = useMemo(() => new Set(instruments.map(i => i.code)), [instruments]);

  const batches = useMemo(() => {
    const m = {};
    allJobs.forEach(j => { if (!visibleCodes.has(j.code)) return; (m[j.batchId] = m[j.batchId] || []).push(j); });
    return Object.keys(m).map(id => ({ id, jobs: m[id] }))
      .sort((a, b) => String(b.jobs[0].createdAt || b.id).localeCompare(String(a.jobs[0].createdAt || a.id)));
  }, [allJobs, visibleCodes]);

  const active = batches.filter(b => b.jobs.some(j => j.status !== 'returned'));
  const history = batches.filter(b => b.jobs.every(j => j.status === 'returned'));
  const activeCodes = useMemo(() => { const s = new Set(); active.forEach(b => b.jobs.forEach(j => { if (j.status !== 'returned') s.add(j.code); })); return s; }, [active]);

  // KPIs
  const outCount = activeCodes.size;
  const overdueReturns = active.filter(b => { const m = b.jobs[0]; return m.expectedReturn && m.expectedReturn < window.TODAY_ISO && b.jobs.some(j => j.status !== 'returned'); }).length;
  const dueSoon = useMemo(() => instruments.filter(i => !window.isDecommissioned(i) && !activeCodes.has(i.code)).filter(i => { const s = window.statusOf(i); return s.key === 'danger' || s.key === 'warn'; }).length, [instruments, activeCodes]);

  const doCreate = (selInsts, opts, markSent) => {
    const batchId = 'CJ-' + window.TODAY_ISO.replace(/-/g, '') + '-' + Date.now().toString(36).slice(-4).toUpperCase();
    const created = selInsts.map(i => {
      const m = window.instrumentMeta ? window.instrumentMeta(i) : {};
      return {
        id: batchId + '-' + i.code,
        batchId, code: i.code,
        instType: i.type || '', brand: i.brand || '', model: i.model || '', serial: i.serial || '', dept: i.dept || '',
        provider: opts.provider, calPoint: i.calPoint || '', mpe: i.mpe || '', useRange: i.useRange || '',
        calSnapshot: i.cal || '', expSnapshot: i.exp || '', intervalDays: m.interval || 365,
        status: markSent ? 'sent' : 'pending',
        createdAt: new Date().toISOString(),
        sentDate: markSent ? opts.sentDate : '', expectedReturn: opts.expectedReturn, note: opts.note,
        returnedDate: '', result: '', certNo: '', newCal: '', newExp: '',
        sentBy: currentUser || '',
      };
    });
    created.forEach(j => window.CalJobs.upsert(j));
    if (window.Audit) window.Audit.add({ action: 'caljob_create', target: batchId, detail: 'สร้างรอบส่งสอบเทียบ ' + created.length + ' เครื่อง → ' + opts.provider });
    setCreateOpen(false);
    if (markSent) setTimeout(() => window.printDispatchNote(created.map(j => ({ ...j, calSnapshot: j.calSnapshot, expSnapshot: j.expSnapshot })), { batchId, provider: opts.provider, sentDate: opts.sentDate, expectedReturn: opts.expectedReturn, note: opts.note, sentBy: currentUser }), 80);
  };

  const advance = (batchId, toStatus) => {
    window.CalJobs.forBatch(batchId).forEach(j => {
      if (j.status === 'returned') return;
      const patch = { ...j, status: toStatus };
      if (toStatus === 'sent' && !j.sentDate) patch.sentDate = window.TODAY_ISO;
      window.CalJobs.upsert(patch);
    });
  };

  const cancelBatch = (batchId) => {
    if (!window.confirm('ยกเลิกรอบส่งนี้? (เฉพาะรายการที่ยังไม่รับกลับจะถูกลบ)')) return;
    window.CalJobs.forBatch(batchId).filter(j => j.status !== 'returned').forEach(j => window.CalJobs.remove(j.id));
  };

  const doReturn = (job, r) => {
    window.CalJobs.upsert({ ...job, status: 'returned', returnedDate: r.returnedDate, result: r.result, certNo: r.certNo, newCal: r.newCal, newExp: r.newExp, note: r.note || job.note, certDocUrl: r.certDocUrl || '' });
    // write results back to the instrument (audited + persisted via app)
    onUpdateInstrument(job.code, { cal: r.newCal, exp: r.newExp, calResult: r.result, certNo: r.certNo, provider: job.provider });
    if (window.Audit) window.Audit.add({ action: 'caljob_return', code: job.code, target: job.batchId, field: 'ผลสอบเทียบ', to: r.result + (r.certNo ? ' · ' + r.certNo : ''), detail: 'รับเครื่องกลับจากสอบเทียบ' });
    setReturnJob(null);
  };

  const printNote = (jobs, meta0, batchId) => window.printDispatchNote(jobs, { batchId, provider: meta0.provider, sentDate: meta0.sentDate, expectedReturn: meta0.expectedReturn, note: meta0.note, sentBy: meta0.sentBy });

  const list = seg === 'active' ? active : history;

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
      {/* KPI strip */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit,minmax(160px,1fr))', gap: 12 }}>
        {[
          { n: outCount, label: 'กำลังส่งสอบเทียบ', color: '#3b6ea5', icon: I.send },
          { n: overdueReturns, label: 'รอบที่เลยกำหนดรับกลับ', color: 'var(--danger)', icon: I.warning },
          { n: dueSoon, label: 'ถึงรอบ ควรเตรียมส่ง', color: 'var(--warn)', icon: I.clock },
          { n: history.length, label: 'รอบที่เสร็จสิ้น', color: 'var(--ok)', icon: I.check },
        ].map((k, idx) => (
          <div key={idx} className="card" style={{ padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12 }}>
            <span style={{ width: 38, height: 38, borderRadius: 10, background: 'var(--surface-2,var(--line-soft))', color: k.color, display: 'grid', placeItems: 'center', flex: 'none' }}><k.icon size={19} /></span>
            <div>
              <div className="figure" style={{ fontSize: 24, color: k.color, lineHeight: 1 }}>{k.n}</div>
              <div style={{ fontSize: 11.5, color: 'var(--ink-3)', marginTop: 2 }}>{k.label}</div>
            </div>
          </div>
        ))}
      </div>

      {/* toolbar */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
        <div className="seg">
          <button className={seg === 'active' ? 'on' : ''} onClick={() => setSeg('active')}>กำลังดำเนินการ ({active.length})</button>
          <button className={seg === 'history' ? 'on' : ''} onClick={() => setSeg('history')}>เสร็จสิ้น ({history.length})</button>
        </div>
        <button className="btn btn-primary" style={{ marginLeft: 'auto' }} onClick={() => setCreateOpen(true)}><I.send size={16} /> สร้างรอบส่งสอบเทียบ</button>
      </div>

      {list.length === 0 ? (
        <div className="card"><EmptyState icon={I.send} title={seg === 'active' ? 'ยังไม่มีรอบส่งที่กำลังดำเนินการ' : 'ยังไม่มีประวัติการส่งสอบเทียบ'} sub={seg === 'active' ? 'กด “สร้างรอบส่งสอบเทียบ” เพื่อเลือกเครื่องมือและออกใบนำส่ง' : 'รอบที่รับเครื่องกลับครบแล้วจะแสดงที่นี่'} /></div>
      ) : (
        list.map(b => <BatchCard key={b.id} batchId={b.id} jobs={b.jobs} admin={admin} onAdvance={advance} onReturn={setReturnJob} onPrint={printNote} onCancel={cancelBatch} />)
      )}

      {createOpen && <CreateDispatch instruments={instruments} activeCodes={activeCodes} onCreate={doCreate} onClose={() => setCreateOpen(false)} />}
      {returnJob && <ReturnJob job={returnJob} onSave={doReturn} onClose={() => setReturnJob(null)} go={go} />}
    </div>
  );
}
window.Dispatch = Dispatch;

/* ---------------- calibration history — pop-up modal (opened from the cal card) ---------------- */
function CalHistoryModal({ inst, onClose }) {
  const [, bump] = useState(0);
  useEffect(() => window.CalJobs.subscribe(() => bump(x => x + 1)), []);
  const rows = window.CalJobs.forCode(inst.code).filter(j => j.status === 'returned').slice()
    .sort((a, b) => String(b.newCal || b.returnedDate || '').localeCompare(String(a.newCal || a.returnedDate || '')));

  return (
    <CJModal wide title="ประวัติการสอบเทียบ" sub={inst.code + ' · ' + inst.type + (rows.length ? ` · รวม ${rows.length} ครั้ง` : '')} icon={I.history} onClose={onClose}>
      {rows.length === 0 ? (
        <div style={{ textAlign: 'center', padding: '32px 12px', color: 'var(--ink-3)' }}>
          <I.history size={36} style={{ opacity: .5, marginBottom: 10 }} />
          <div style={{ fontSize: 13.5 }}>ยังไม่มีประวัติการสอบเทียบ</div>
          <div style={{ fontSize: 11.5, marginTop: 4 }}>ประวัติจะถูกบันทึกเมื่อ "รับกลับ" จากการส่งสอบเทียบ</div>
        </div>
      ) : (
        <div className="tbl-wrap" style={{ border: '1px solid var(--line)', borderRadius: 10 }}>
          <table className="tbl">
            <thead>
              <tr>
                <th style={{ width: 120 }}>วันที่สอบเทียบ</th>
                <th style={{ width: 150 }}>ผลการสอบเทียบ</th>
                <th>หน่วยงานสอบเทียบ</th>
                <th style={{ width: 150 }}>เลขใบรับรอง</th>
                <th style={{ width: 70, textAlign: 'center' }}>ใบรับรอง</th>
              </tr>
            </thead>
            <tbody>
              {rows.map(r => {
                const fail = r.result === 'ไม่ผ่าน';
                const cond = r.result === 'ผ่านโดยมีเงื่อนไข';
                const col = fail ? 'var(--danger)' : cond ? 'var(--warn)' : 'var(--ok)';
                const soft = fail ? 'var(--danger-soft)' : cond ? 'var(--warn-soft)' : 'var(--ok-soft)';
                const meta = [r.newExp ? 'ครบกำหนดถัดไป ' + window.fmtDateFull(r.newExp) : '', r.sentBy ? 'บันทึกโดย ' + r.sentBy : ''].filter(Boolean).join(' · ');
                return (
                  <React.Fragment key={r.id}>
                    <tr style={(r.note || meta) ? { borderBottom: 'none' } : null}>
                      <td className="tnum" style={{ fontWeight: 600 }}>{window.fmtDateFull(r.newCal || r.returnedDate)}</td>
                      <td><span className="pill" style={{ background: soft, color: col, fontSize: 11, fontWeight: 700 }}><span className="dot" />{r.result || '—'}</span></td>
                      <td>{r.provider || <span className="muted">—</span>}</td>
                      <td className="mono" style={{ fontSize: 12 }}>{r.certNo || <span className="muted">—</span>}</td>
                      <td style={{ textAlign: 'center' }}>
                        {r.certDocUrl
                          ? <a href={r.certDocUrl} target="_blank" rel="noopener noreferrer" className="btn btn-icon btn-ghost" title="เปิดใบรับรอง" style={{ textDecoration: 'none', color: 'var(--accent-strong)' }}><I.shield size={15} /></a>
                          : <span className="muted">—</span>}
                      </td>
                    </tr>
                    {(r.note || meta) && (
                      <tr>
                        <td colSpan={5} style={{ paddingTop: 0, fontSize: 12, color: 'var(--ink-3)', lineHeight: 1.5 }}>
                          {r.note && <div><b style={{ color: 'var(--ink-2)', fontWeight: 600 }}>เหตุผล / เกณฑ์การยอมรับ:</b> {r.note}</div>}
                          {meta && <div style={{ color: 'var(--ink-4)', marginTop: 2 }}>{meta}</div>}
                        </td>
                      </tr>
                    )}
                  </React.Fragment>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </CJModal>
  );
}
window.CalHistoryModal = CalHistoryModal;
