/* ============================================================================
   ReMeDe Weight Journey — หน้าจอฝั่งคลินิก (C1–C9)

   ไฟล์นี้ทำหน้าที่ "แสดงผล + state ของหน้าจอ" เท่านั้น
     - ข้อมูลทั้งหมดมาจาก window.RMD_API (ui_kits/api.js) — ห้ามเรียก supabase ตรงนี้
     - กฎธุรกิจ (แปลงคลิก→mg, สัญญาณเตือน, จัดรูปแบบตัวเลข) อยู่ที่ window.RMDLogic
     - ทุกหน้าที่ดึงข้อมูลต้องมีครบสามสถานะ: กำลังโหลด / ว่าง / error พร้อมปุ่มลองใหม่
       ตอนนี้ฐานข้อมูลจริงยังแทบว่าง สถานะ "ว่าง" คือสิ่งที่คนจะเห็นบ่อยที่สุด
   ============================================================================ */

const NS = window.ReMeDeWeightJourneyDesignSystem_3d7adc;
const { Button, IconButton, Card, Badge, Icon,
  FormField, Input, Select, Switch, SegmentedControl, NumberStepper,
  MetricStat, DoseReadout, PenGauge, WeightDoseChart, DataTable, TitrationTimeline,
  Callout, Toast, EmptyState, DangerConfirm, Dialog, SideNav, Tabs } = NS;

const L = window.RMDLogic;
const API = window.RMD_API;

const page = { flex: 1, minWidth: 0, overflowY: "auto", padding: "var(--page-gutter-desktop)", display: "flex", flexDirection: "column", gap: "var(--space-6)" };

/* ---------------------------------------------------------------------------
   1. คำแปลค่า enum ของฐานข้อมูล → ภาษาไทย
   ผู้ใช้ต้องไม่เห็นค่าดิบอย่าง "abdomen_left" หรือ "penlow" เด็ดขาด
--------------------------------------------------------------------------- */
const SYMPTOM_TH = {
  nausea: "คลื่นไส้", vomiting: "อาเจียน", constipation: "ท้องผูก", diarrhea: "ท้องเสีย",
  appetite_loss: "เบื่ออาหาร", fatigue: "อ่อนเพลีย", injection_site_reaction: "ปฏิกิริยาที่ตำแหน่งฉีด", other: "อาการอื่น",
};
const SITE_TH = {
  abdomen_left: "หน้าท้องซ้าย", abdomen_right: "หน้าท้องขวา",
  thigh_left: "ต้นขาซ้าย", thigh_right: "ต้นขาขวา", upper_arm: "ต้นแขน",
};
const OFF_RX_TH = {
  side_effect: "มีผลข้างเคียง", not_enough_drug: "ยาไม่พอ", doctor_advice: "แพทย์แนะนำ",
  miscount: "นับคลิกผิด", other: "เหตุผลอื่น",
};
const PEN_STATUS_TH = { in_use: "กำลังใช้อยู่", finished: "ใช้หมดแล้ว", discarded: "ทิ้งแล้ว", expired: "หมดอายุ" };
const PROGRAM_ICON = { medication: "syringe", diet_only: "utensils" };
const VISIT_KIND_TH = { initial: "ครั้งแรก", refill: "รับยาใหม่", followup: "ติดตามอาการ" };
const ROLE_TH = { nurse: "ผู้ช่วย/พยาบาล", doctor: "แพทย์", admin: "แอดมิน/เจ้าของ", patient: "ผู้ป่วย" };
const ACTION_TH = { read: "เปิดดูข้อมูล", create: "เพิ่มข้อมูล", update: "แก้ไขข้อมูล", delete: "ลบข้อมูล", export: "ส่งออกข้อมูล", login: "เข้าสู่ระบบ" };
const ENTITY_TH = {
  patient: "ข้อมูลผู้ป่วย", patients: "ข้อมูลผู้ป่วย", weight_logs: "บันทึกน้ำหนัก", side_effects: "อาการ",
  injections: "การฉีดยา", prescriptions: "ใบสั่งโดส", pens: "ปากกา", visits: "การมาคลินิก",
  consents: "คำยินยอม", pen_models: "รุ่นปากกา",
};

/* คำขอใช้สิทธิตาม PDPA (data_requests) */
const REQUEST_KIND_TH = { export: "ขอสำเนาข้อมูล", erase: "ขอลบข้อมูล" };
const REQUEST_STATUS_TH = { open: "รอดำเนินการ", done: "ดำเนินการแล้ว", rejected: "ปฏิเสธคำขอ" };

/* เมนูตามบทบาทจริงจากตาราง staff — ไม่ผูกกับรายชื่อคนอีกต่อไป */
const ROLE_NAV = {
  nurse: ["list", "refill"],
  doctor: ["list", "refill", "dash"],
  admin: ["list", "refill", "dash", "requests", "settings", "audit"],
};

/* ---------------------------------------------------------------------------
   2. ตัวช่วยจัดรูปแบบสำหรับการแสดงผล (ไม่ใช่กฎธุรกิจ จึงอยู่ที่หน้าจอ)
--------------------------------------------------------------------------- */
const DASH = "—";

function fmtDate(value) {
  if (!value) return DASH;
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  return d.toLocaleDateString("th-TH", { day: "2-digit", month: "short", year: "numeric" });
}

function fmtDateTime(value) {
  if (!value) return DASH;
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  return d.toLocaleDateString("th-TH", { day: "2-digit", month: "short" })
    + " · " + d.toLocaleTimeString("th-TH", { hour: "2-digit", minute: "2-digit" });
}

/** "วันนี้ / เมื่อวาน / N วันที่แล้ว" — ไม่มีข้อมูลคือไม่มี ไม่ใช่ 0 วัน */
function fmtSince(value) {
  if (!value) return "ยังไม่มีบันทึก";
  const d = new Date(value);
  if (isNaN(d.getTime())) return DASH;
  const days = Math.floor((Date.now() - d.getTime()) / 86400000);
  if (days <= 0) return "วันนี้";
  if (days === 1) return "เมื่อวาน";
  return days + " วันที่แล้ว";
}

function daysBetween(from, to) {
  return Math.floor((new Date(to).getTime() - new Date(from).getTime()) / 86400000);
}

/**
 * รวมข้อมูลดิบเป็นซีรีส์รายสัปดาห์สำหรับกราฟ
 * สัปดาห์ที่ไม่มีบันทึก ต้องได้ weight = null เพื่อให้กราฟ "ขาดเส้น"
 * ห้ามเติม 0 หรือลากค่าเดิมต่อ — นั่นคือการเดาค่าที่ผู้ป่วยไม่ได้บันทึก
 */
function buildWeekSeries({ programStartAt, weights, injections, sideEffects }) {
  const all = []
    .concat((weights || []).map((w) => w.measured_at))
    .concat((injections || []).map((i) => i.injected_at))
    .concat((sideEffects || []).map((s) => s.reported_at))
    .map((v) => new Date(v).getTime())
    .filter((t) => Number.isFinite(t));
  if (!all.length) return [];

  const startCandidates = all.slice();
  if (programStartAt) {
    const ps = new Date(programStartAt).getTime();
    if (Number.isFinite(ps)) startCandidates.push(ps);
  }
  const start = Math.min.apply(null, startCandidates);
  const weekOf = (value) => Math.max(0, Math.floor((new Date(value).getTime() - start) / 604800000));

  const lastWeek = Math.max(weekOf(Date.now()), weekOf(Math.max.apply(null, all)));
  const series = [];
  for (let i = 0; i <= lastWeek; i++) {
    series.push({ label: "ส." + (i + 1), weight: null, mg: null, se: null });
  }

  // weights เรียงเก่า → ใหม่อยู่แล้ว ค่าท้ายสุดของสัปดาห์จึงเป็นค่าที่ทับลงไปเป็นตัวสุดท้าย
  (weights || []).forEach((w) => {
    const slot = series[weekOf(w.measured_at)];
    if (slot) slot.weight = Number(w.weight_kg);
  });
  (injections || []).forEach((inj) => {
    const slot = series[weekOf(inj.injected_at)];
    if (!slot) return;
    slot.mg = (slot.mg || 0) + Number(inj.mg || 0);
  });
  (sideEffects || []).forEach((se) => {
    const slot = series[weekOf(se.reported_at)];
    if (!slot) return;
    const level = Number(se.severity);
    if (Number.isFinite(level)) slot.se = Math.max(slot.se || 0, level);
  });
  return series;
}

/* ---------------------------------------------------------------------------
   3. ตัวช่วยเรื่องสถานะการโหลด — ใช้ซ้ำทุกหน้า จะได้ไม่มีหน้าไหนลืมสถานะใดสถานะหนึ่ง
--------------------------------------------------------------------------- */

function useAsync(run, deps) {
  const [state, setState] = React.useState({ loading: true, error: null, data: null });
  const [tick, setTick] = React.useState(0);
  const keys = (deps || []).concat([tick]);

  React.useEffect(() => {
    let alive = true;
    setState({ loading: true, error: null, data: null });
    Promise.resolve()
      .then(run)
      .then(
        (data) => { if (alive) setState({ loading: false, error: null, data: data }); },
        (err) => { if (alive) setState({ loading: false, error: (err && err.message) || "โหลดข้อมูลไม่สำเร็จ", data: null }); }
      );
    return () => { alive = false; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, keys);

  return { loading: state.loading, error: state.error, data: state.data, reload: () => setTick((t) => t + 1) };
}

function LoadingBlock({ label }) {
  return <EmptyState icon="loader" title={label || "กำลังโหลดข้อมูล"} description="กำลังดึงข้อมูลจากระบบ สักครู่" />;
}

function ErrorBlock({ message, onRetry }) {
  return (
    <EmptyState icon="triangle-alert" title="โหลดข้อมูลไม่สำเร็จ"
      description={message || "เกิดปัญหาระหว่างเชื่อมต่อระบบ กรุณาลองใหม่อีกครั้ง"}
      action={onRetry ? <Button variant="secondary" icon="refresh-cw" onClick={onRetry}>ลองใหม่</Button> : null} />
  );
}

/** ห่อเนื้อหาที่ต้องรอข้อมูล: โหลด → error (มีปุ่มลองใหม่) → ว่าง → เนื้อหาจริง */
function AsyncBlock({ state, isEmpty, empty, children }) {
  if (state.loading) return <LoadingBlock />;
  if (state.error) return <ErrorBlock message={state.error} onRetry={state.reload} />;
  if (isEmpty) return empty;
  return children;
}

function PageHead({ eyebrow, title, subtitle, actions }) {
  return (
    <header style={{ display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: "var(--space-6)" }}>
      <div>
        {eyebrow ? <p className="rmd-eyebrow">{eyebrow}</p> : null}
        <h1 style={{ font: "var(--type-page-title)", marginTop: 2 }}>{title}</h1>
        {subtitle ? <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)", marginTop: 4 }}>{subtitle}</p> : null}
      </div>
      <div style={{ display: "flex", gap: "var(--space-4)" }}>{actions}</div>
    </header>
  );
}

/**
 * ป้ายบอกโปรแกรมการรักษา — ต้องเห็นได้ทันทีว่าคนนี้ใช้ยาฉีดหรือปรับอาหารอย่างเดียว
 * เพราะทุกหน้าจอที่เกี่ยวกับยาจะเปลี่ยนไปตามค่านี้
 */
function ProgramBadge({ programType, size }) {
  if (!programType) return null;
  const dietOnly = L.isDietOnly(programType);
  return (
    <Badge tone={dietOnly ? "accent" : "brand"} size={size || "sm"} icon={PROGRAM_ICON[programType]}>
      {L.programLabel(programType)}
    </Badge>
  );
}

/** ป้ายสัญญาณเตือนจาก flags ที่ view v_patient_alerts คำนวณมาให้ */
function FlagBadges({ flags }) {
  if (!flags || !flags.length) return <Badge tone="ok" size="sm">ปกติ</Badge>;
  return (
    <span style={{ display: "inline-flex", gap: "var(--space-2)", flexWrap: "wrap" }}>
      {flags.map((key) => {
        const meta = L.FLAG_META[key];
        if (!meta) return null;
        return <Badge key={key} tone={meta[0]} size="sm" icon={meta[0] === "act" ? "triangle-alert" : "clock"}>{meta[1]}</Badge>;
      })}
    </span>
  );
}

/* ===========================================================================
   C2 — รายชื่อผู้ป่วย
   ใช้เป็นหน้าเลือกผู้ป่วยของ "รับยาใหม่" ด้วย (pickMode)
=========================================================================== */
/* ---------------------------------------------------------------------------
   C10 — เปิดบัญชีให้ผู้ป่วย / ตั้งรหัสใหม่ให้

   ผู้ป่วยล็อกอินด้วย HN + รหัสผ่าน ไม่มีอีเมลจริงในระบบ จึงกดลิงก์ "ลืมรหัสผ่าน"
   ด้วยตัวเองไม่ได้ — คลินิกเป็นทางเดียวที่ตั้งรหัสใหม่ให้ได้
   รหัสถูกสร้างที่เซิร์ฟเวอร์และส่งกลับมา "ครั้งเดียว" ไม่ถูกเก็บไว้ที่ไหนอีกเลย
   หน้าจอจึงต้องบอกให้ชัดว่าปิดหน้าต่างนี้แล้วดูย้อนหลังไม่ได้
--------------------------------------------------------------------------- */
function CredentialsPanel({ result }) {
  const [copied, setCopied] = React.useState(null);

  async function copy(kind, value) {
    try {
      await navigator.clipboard.writeText(value);
      setCopied(kind);
      setTimeout(() => setCopied(null), 1800);
    } catch (err) {
      // เบราว์เซอร์บางตัวไม่ให้เขียนคลิปบอร์ด — ค่ายังอยู่บนจอให้จดเองได้
      setCopied("fail");
      setTimeout(() => setCopied(null), 2600);
    }
  }

  const row = (label, value, kind) => (
    <div style={{ display: "flex", alignItems: "center", gap: "var(--space-4)" }}>
      <span style={{ font: "var(--type-caption)", color: "var(--text-muted)", width: 96, flex: "0 0 auto" }}>{label}</span>
      <code style={{ flex: 1, minWidth: 0, font: "var(--type-mono-lg, var(--type-label))", fontFamily: "var(--font-mono, ui-monospace, monospace)",
        letterSpacing: ".04em", color: "var(--text-strong)", background: "var(--surface-card)",
        border: "1px solid var(--border-hairline)", borderRadius: "var(--radius-sm, 6px)", padding: "8px 12px", overflowWrap: "anywhere" }}>{value}</code>
      <Button size="sm" variant="secondary" icon={copied === kind ? "check" : "copy"}
        onClick={() => copy(kind, value)}>{copied === kind ? "คัดลอกแล้ว" : "คัดลอก"}</Button>
    </div>
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
      <Callout tone="act" icon="triangle-alert" title="รหัสนี้แสดงครั้งเดียวเท่านั้น">
        ระบบไม่ได้เก็บรหัสผ่านไว้ที่ไหน — ปิดหน้าต่างนี้แล้วจะดูย้อนหลังไม่ได้
        ถ้าลืมจดต้องกดตั้งรหัสใหม่อีกครั้ง · ส่งให้ผู้ป่วยแล้วอย่าเก็บไว้ในแชต
      </Callout>
      {row("รหัสผู้ป่วย", result.login_id, "id")}
      {row("รหัสผ่าน", result.password, "pw")}
      {copied === "fail" ? (
        <p style={{ font: "var(--type-caption)", color: "var(--text-muted)" }}>
          คัดลอกอัตโนมัติไม่ได้ในเบราว์เซอร์นี้ กรุณาลากคลุมแล้วคัดลอกเอง
        </p>
      ) : null}
      <p style={{ font: "var(--type-caption)", color: "var(--text-muted)" }}>
        ผู้ป่วยจะถูกบังคับให้ตั้งรหัสใหม่ของตัวเองทันทีที่เข้าใช้งานครั้งแรก
      </p>
    </div>
  );
}

function NewPatientDialog({ open, onClose, onCreated }) {
  const [hn, setHn] = React.useState("");
  const [nickname, setNickname] = React.useState("");
  const [programType, setProgramType] = React.useState("medication");
  const [startWeight, setStartWeight] = React.useState("");
  const [targetWeight, setTargetWeight] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [result, setResult] = React.useState(null);

  // ล้างฟอร์มทุกครั้งที่เปิดใหม่ ไม่ให้ค่าของคนก่อนหน้าค้างมาจนบันทึกผิดคน
  React.useEffect(() => {
    if (!open) return;
    setHn(""); setNickname(""); setProgramType("medication");
    setStartWeight(""); setTargetWeight(""); setError(null); setResult(null); setSaving(false);
  }, [open]);

  const cleanHN = API.normalizeHN(hn);
  const hnValid = API.HN_RE.test(cleanHN);
  const canSubmit = hnValid && nickname.trim() && !saving;

  async function submit() {
    if (!canSubmit) return;
    setSaving(true);
    setError(null);
    try {
      const res = await API.createPatientAccount({
        hn: cleanHN, nickname: nickname,
        program_type: programType,
        start_weight_kg: startWeight, target_weight_kg: targetWeight,
      });
      setResult(res);
      onCreated();
    } catch (err) {
      setError((err && err.message) || "เปิดบัญชีไม่สำเร็จ");
    } finally {
      setSaving(false);
    }
  }

  return (
    <Dialog open={open}
      title={result ? "เปิดบัญชีเรียบร้อย" : "เปิดบัญชีผู้ป่วยใหม่"}
      description={result
        ? "ส่งรหัสด้านล่างให้ผู้ป่วยเพื่อเข้าใช้งานครั้งแรก"
        : "ระบบเก็บเฉพาะ HN กับชื่อเล่นเท่านั้น ห้ามกรอกชื่อจริง เบอร์โทร หรือเลขบัตรประชาชน"}
      onClose={onClose}
      footer={result
        ? <Button onClick={onClose}>จดรหัสแล้ว ปิดหน้าต่าง</Button>
        : <React.Fragment>
            <Button variant="secondary" onClick={onClose} disabled={saving}>ยกเลิก</Button>
            <Button onClick={submit} disabled={!canSubmit}>{saving ? "กำลังเปิดบัญชี" : "เปิดบัญชี"}</Button>
          </React.Fragment>}>
      {result ? <CredentialsPanel result={result} /> : (
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
          <FormField label="HN (ใช้เป็นรหัสเข้าสู่ระบบของผู้ป่วย)" required
            hint="ตัวอักษรอังกฤษ ตัวเลข . _ - เท่านั้น · ผู้ป่วยจะใช้ค่านี้ล็อกอิน"
            error={hn && !hnValid ? "รูปแบบ HN ไม่ถูกต้อง (3–32 ตัว ห้ามเว้นวรรคหรืออักษรไทย)" : null}>
            <Input placeholder="HN-24118" value={hn} onChange={(e) => setHn(e.target.value)} />
          </FormField>

          <FormField label="ชื่อเล่น" required hint="ใช้เรียกในแอปเท่านั้น ไม่ต้องเป็นชื่อจริง">
            <Input placeholder="เช่น มิ้นท์" value={nickname} onChange={(e) => setNickname(e.target.value)} />
          </FormField>

          <FormField label="โปรแกรมการรักษา" required>
            <SegmentedControl block value={programType} onChange={setProgramType}
              options={[
                { value: "medication", label: "ใช้ยาฉีด" },
                { value: "diet_only", label: "ปรับอาหารอย่างเดียว" },
              ]} />
          </FormField>

          <div style={{ display: "flex", gap: "var(--space-5)" }}>
            <div style={{ flex: 1 }}>
              <FormField label="น้ำหนักเริ่มต้น (กก.)" hint="ไม่บังคับ">
                <Input type="number" inputMode="decimal" step="0.1" placeholder="80.0"
                  value={startWeight} onChange={(e) => setStartWeight(e.target.value)} />
              </FormField>
            </div>
            <div style={{ flex: 1 }}>
              <FormField label="น้ำหนักเป้าหมาย (กก.)" hint="ไม่บังคับ">
                <Input type="number" inputMode="decimal" step="0.1" placeholder="70.0"
                  value={targetWeight} onChange={(e) => setTargetWeight(e.target.value)} />
              </FormField>
            </div>
          </div>

          {error ? <Callout tone="act" title="เปิดบัญชีไม่สำเร็จ">{error}</Callout> : null}
        </div>
      )}
    </Dialog>
  );
}

/** ตั้งรหัสใหม่ให้ผู้ป่วยที่ลืมรหัส — อยู่ในหน้ารายละเอียดผู้ป่วย */
function ResetPasswordAction({ patient }) {
  const [open, setOpen] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [result, setResult] = React.useState(null);

  function close() { setOpen(false); setResult(null); setError(null); }

  async function submit() {
    setSaving(true);
    setError(null);
    try {
      setResult(await API.resetPatientPassword(patient.id));
    } catch (err) {
      setError((err && err.message) || "ตั้งรหัสใหม่ไม่สำเร็จ");
    } finally {
      setSaving(false);
    }
  }

  return (
    <React.Fragment>
      <Button variant="secondary" icon="key-round" onClick={() => setOpen(true)}>ตั้งรหัสผ่านใหม่</Button>
      <Dialog open={open}
        title={result ? "ตั้งรหัสใหม่เรียบร้อย" : "ตั้งรหัสผ่านใหม่ให้ผู้ป่วย"}
        description={(patient.nickname || "ผู้ป่วย") + " · " + patient.hn}
        onClose={close}
        footer={result
          ? <Button onClick={close}>จดรหัสแล้ว ปิดหน้าต่าง</Button>
          : <React.Fragment>
              <Button variant="secondary" onClick={close} disabled={saving}>ยกเลิก</Button>
              <Button onClick={submit} disabled={saving}>{saving ? "กำลังตั้งรหัส" : "ตั้งรหัสใหม่"}</Button>
            </React.Fragment>}>
        {result ? <CredentialsPanel result={result} /> : (
          <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
            <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
              รหัสเดิมจะใช้ไม่ได้ทันทีที่กดยืนยัน ถ้าผู้ป่วยกำลังเปิดแอปอยู่จะถูกให้เข้าสู่ระบบใหม่
              ทำเมื่อยืนยันตัวผู้ป่วยได้แน่ชัดแล้วเท่านั้น
            </p>
            {error ? <Callout tone="act" title="ตั้งรหัสใหม่ไม่สำเร็จ">{error}</Callout> : null}
          </div>
        )}
      </Dialog>
    </React.Fragment>
  );
}

function PatientList({ onOpen, pickMode, canManageAccounts }) {
  const state = useAsync(() => API.listPatients(), []);
  const [filter, setFilter] = React.useState("all");
  const [query, setQuery] = React.useState("");
  const [adding, setAdding] = React.useState(false);
  // ระหว่างเลือกผู้ป่วยเพื่อเริ่ม refill ไม่ควรมีปุ่มเปิดบัญชีมาแทรก — คนละงานกัน
  const showAdd = canManageAccounts && !pickMode;

  const patients = state.data || [];
  const q = query.trim().toLowerCase();
  const rows = patients
    .filter((p) => (filter === "flag" ? p.flags.length > 0
      : filter === "med" ? !L.isDietOnly(p.program_type)
      : filter === "diet" ? L.isDietOnly(p.program_type) : true))
    .filter((p) => !q || (p.nickname || "").toLowerCase().includes(q) || (p.hn || "").toLowerCase().includes(q))
    .map((p) => Object.assign({}, p, { id: p.patient_id }));

  const countWith = (key) => patients.filter((p) => p.flags.includes(key)).length;
  const dietOnlyCount = patients.filter((p) => L.isDietOnly(p.program_type)).length;

  return (
    <div style={page}>
      <PageHead eyebrow={pickMode ? "REFILL VISIT" : "PATIENT LIST"}
        title={pickMode ? "เลือกผู้ป่วยที่มาคลินิกวันนี้" : "รายชื่อผู้ป่วย"}
        subtitle={pickMode
          ? "เลือกจากรายชื่อเพื่อเริ่มขั้นตอนชั่งน้ำหนัก · คนที่ปรับอาหารอย่างเดียวจะไม่มีขั้นตอนจ่ายยา"
          : "ผู้ป่วยที่ยังอยู่ในโปรแกรม พร้อมสัญญาณที่ควรดู"}
        actions={<React.Fragment>
          <Input size="md" prefixIcon="search" placeholder="ค้นหาชื่อเล่น หรือ HN" style={{ width: 260 }}
            value={query} onChange={(e) => setQuery(e.target.value)} />
          {showAdd ? <Button icon="user-plus" onClick={() => setAdding(true)}>เปิดบัญชีผู้ป่วย</Button> : null}
        </React.Fragment>} />

      {showAdd ? <NewPatientDialog open={adding} onClose={() => setAdding(false)} onCreated={state.reload} /> : null}

      <AsyncBlock state={state}
        isEmpty={patients.length === 0}
        empty={<Card padding="lg"><EmptyState icon="users" title="ยังไม่มีผู้ป่วยในโปรแกรม"
          description={showAdd
            ? "กด “เปิดบัญชีผู้ป่วย” เพื่อสร้างบัญชีแรก ระบบจะสร้างรหัสผ่านให้อัตโนมัติ"
            : "เมื่อมีการเพิ่มผู้ป่วยเข้าโปรแกรมแล้ว รายชื่อและสัญญาณเตือนจะแสดงที่นี่"}
          action={showAdd ? <Button icon="user-plus" onClick={() => setAdding(true)}>เปิดบัญชีผู้ป่วย</Button> : null} /></Card>}>
        <React.Fragment>
          <div style={{ display: "flex", gap: "var(--space-5)" }}>
            <Card padding="sm" style={{ flex: 1 }}><MetricStat size="sm" icon="users" label="ผู้ป่วยในโปรแกรม" value={String(patients.length)} /></Card>
            <Card padding="sm" style={{ flex: 1 }}><MetricStat size="sm" icon="triangle-alert" label="มีสัญญาณที่ควรดู" value={String(patients.filter((p) => p.flags.length > 0).length)} /></Card>
            <Card padding="sm" style={{ flex: 1 }}><MetricStat size="sm" icon="clock" label="ขาดบันทึกน้ำหนัก" value={String(countWith("missing"))} footnote="เกิน 12 วัน" /></Card>
            <Card padding="sm" style={{ flex: 1 }}><MetricStat size="sm" icon="utensils" label="ปรับอาหารอย่างเดียว" value={String(dietOnlyCount)} footnote="ไม่ใช้ยาฉีด" /></Card>
            <Card padding="sm" style={{ flex: 1 }}><MetricStat size="sm" icon="syringe" label="ยาใกล้หมด" value={String(countWith("penlow"))} footnote="ยังไม่มีนัด" /></Card>
          </div>

          <SegmentedControl value={filter} onChange={setFilter}
            options={[
              { value: "all", label: "ทั้งหมด" },
              { value: "flag", label: "มีสัญญาณเตือน" },
              { value: "med", label: "ใช้ยาฉีด" },
              { value: "diet", label: "ปรับอาหารอย่างเดียว" },
            ]} />

          <Card padding="none">
            <DataTable onRowClick={(r) => onOpen(r.patient_id)}
              emptyLabel={q ? "ไม่พบผู้ป่วยที่ตรงกับคำค้นหา" : "ไม่มีผู้ป่วยที่ตรงกับตัวกรองนี้"}
              columns={[
                { key: "nickname", header: "ผู้ป่วย", render: (r) => (
                  <div>
                    <div style={{ font: "var(--type-label)", color: "var(--text-strong)" }}>{r.nickname || "ไม่ได้ตั้งชื่อเล่น"}</div>
                    <div style={{ font: "var(--type-caption)", color: "var(--text-subtle)" }}>{r.hn}</div>
                  </div>) },
                { key: "program_type", header: "โปรแกรม", render: (r) => <ProgramBadge programType={r.program_type} /> },
                { key: "last_measured_at", header: "บันทึกน้ำหนักล่าสุด", render: (r) => fmtSince(r.last_measured_at) },
                // คนที่ปรับอาหารอย่างเดียวไม่มีใบสั่งยาและไม่มีปากกา — แสดงขีดจะทำให้เข้าใจว่า "ยังไม่ได้สั่ง"
                { key: "prescribed_clicks", header: "โดสตามใบสั่ง", numeric: true, align: "right",
                  render: (r) => (L.isDietOnly(r.program_type) ? "ไม่ใช้ยาฉีด"
                    : r.prescribed_clicks == null ? DASH : r.prescribed_clicks + " คลิก") },
                { key: "clicks_remaining", header: "ยาคงเหลือ", numeric: true, align: "right",
                  render: (r) => (L.isDietOnly(r.program_type) ? DASH
                    : r.clicks_remaining == null ? DASH : r.clicks_remaining + " คลิก") },
                { key: "next_visit_at", header: "นัดถัดไป", render: (r) => (r.next_visit_at ? fmtDate(r.next_visit_at) : "ยังไม่มีนัด") },
                { key: "flags", header: "สัญญาณ", render: (r) => <FlagBadges flags={r.flags} /> },
                { key: "go", header: "", align: "right", render: () => <Icon name="chevron-right" size={16} style={{ color: "var(--text-subtle)" }} /> },
              ]}
              rows={rows} />
          </Card>
        </React.Fragment>
      </AsyncBlock>
    </div>
  );
}

/**
 * เปลี่ยนโปรแกรมการรักษาของผู้ป่วย (ใช้ยาฉีด ↔ ปรับอาหารอย่างเดียว)
 * กฎว่าเปลี่ยนได้หรือไม่อยู่ที่ฐานข้อมูล (เช่นยังมีใบสั่งยาเปิดอยู่)
 * หน้าจอมีหน้าที่แค่ถามยืนยันและแสดงข้อความที่ฐานข้อมูลตอบกลับมาตรง ๆ
 */
function ProgramSwitch({ patient, onChanged }) {
  const [open, setOpen] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  const dietOnly = L.isDietOnly(patient.program_type);
  const next = dietOnly ? "medication" : "diet_only";

  async function apply() {
    setSaving(true);
    setError(null);
    try {
      await API.setProgramType(patient.id, next);
      setOpen(false);
      onChanged(next);
    } catch (err) {
      setError((err && err.message) || "เปลี่ยนโปรแกรมไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <React.Fragment>
      <Button variant="ghost" icon="repeat" onClick={() => { setError(null); setOpen(true); }}>เปลี่ยนโปรแกรม</Button>
      <Dialog open={open} title="เปลี่ยนโปรแกรมการรักษา"
        description={"จาก “" + L.programLabel(patient.program_type) + "” เป็น “" + L.programLabel(next) + "”"}
        onClose={() => setOpen(false)}
        footer={<React.Fragment>
          <Button variant="secondary" onClick={() => setOpen(false)} disabled={saving}>ยกเลิก</Button>
          <Button onClick={apply} disabled={saving}>{saving ? "กำลังบันทึก" : "ยืนยันการเปลี่ยน"}</Button>
        </React.Fragment>}>
        <p style={{ font: "var(--type-body-sm)", color: "var(--text-muted)" }}>
          {dietOnly
            ? "หลังเปลี่ยนแล้วจะกำหนดโดส จ่ายปากกา และบันทึกการฉีดยาให้ผู้ป่วยรายนี้ได้ตามปกติ"
            : "หลังเปลี่ยนแล้วผู้ป่วยจะไม่เห็นส่วนที่เกี่ยวกับยาฉีดในแอปอีก และระบบจะไม่ให้บันทึกใบสั่งยา ปากกา หรือการฉีดยาของรายนี้"}
        </p>
        {error ? <div style={{ marginTop: "var(--space-5)" }}><Callout tone="act" title="เปลี่ยนโปรแกรมไม่ได้">{error}</Callout></div> : null}
      </Dialog>
    </React.Fragment>
  );
}

/* ===========================================================================
   C3 — หน้าผู้ป่วยรายบุคคล (หน้าจอสำคัญที่สุดของระบบ)
=========================================================================== */
function PatientDetail({ patientId, profile, onBack, onRefill, canRefill, canManageAccounts, onToast }) {
  const [tab, setTab] = React.useState("trend");
  const [range, setRange] = React.useState("12w");

  const state = useAsync(() => Promise.all([
    API.getPatient(patientId),
    API.getWeightLogs(patientId),
    API.getInjections(patientId),
    API.getSideEffects(patientId),
    API.getPrescriptions(patientId),
    API.getPens(patientId),
    API.getVisits(patientId),
  ]).then(([patient, weights, injections, sideEffects, prescriptions, pens, visits]) =>
    ({ patient, weights, injections, sideEffects, prescriptions, pens, visits })), [patientId]);

  // การเปิดดูข้อมูลผู้ป่วยต้องลง audit log — แต่ถ้าลงไม่สำเร็จ ห้ามบังหน้าจอที่แพทย์กำลังใช้
  React.useEffect(() => {
    if (!patientId) return;
    API.logPatientRead(patientId, "patient", "read").catch((err) => {
      console.error("[clinic] ลง audit log การเปิดดูข้อมูลไม่สำเร็จ", err && err.message);
    });
  }, [patientId]);

  if (state.loading) return <div style={page}><LoadingBlock label="กำลังโหลดข้อมูลผู้ป่วย" /></div>;
  if (state.error) {
    return (
      <div style={page}>
        <PageHead title="หน้าผู้ป่วย" actions={<Button variant="ghost" icon="chevron-left" onClick={onBack}>รายชื่อ</Button>} />
        <Card padding="lg"><ErrorBlock message={state.error} onRetry={state.reload} /></Card>
      </div>
    );
  }

  const { patient, weights, injections, sideEffects, prescriptions, pens, visits } = state.data;
  // ผู้ป่วยโปรแกรมปรับอาหารอย่างเดียวไม่มีใบสั่งยา/ปากกา/การฉีดเลย (ฐานข้อมูลบล็อกไว้)
  // จึงต้องซ่อนทั้งตัวเลขโดส แท็บการจ่ายยา และแท่งโดสในกราฟ ไม่ใช่แสดงเป็นขีด
  const dietOnly = L.isDietOnly(patient.program_type);
  const canEditProgram = profile && (profile.role === "doctor" || profile.role === "admin");
  const series = buildWeekSeries({ programStartAt: patient.program_start_at, weights, injections, sideEffects });
  const shown = range === "6w" ? series.slice(-6) : series.slice(-12);

  const latest = weights.length ? weights[weights.length - 1] : null;
  const previous = weights.length > 1 ? weights[weights.length - 2] : null;
  const delta = latest && previous ? L.weightDelta(latest.weight_kg, previous.weight_kg) : null;
  const startWeight = patient.start_weight_kg != null
    ? Number(patient.start_weight_kg)
    : (weights.length ? Number(weights[0].weight_kg) : null);
  const lossPct = latest ? L.lossPercentFrom(startWeight, latest.weight_kg) : null;
  const currentRx = prescriptions.find((r) => r.is_current) || null;
  const weeksWithLog = series.filter((w) => w.weight != null).length;
  const targetWeight = patient.target_weight_kg != null ? Number(patient.target_weight_kg) : undefined;
  const nextVisit = visits.filter((v) => new Date(v.visit_at).getTime() > Date.now()).slice(-1)[0] || null;

  const programWeek = patient.program_start_at ? Math.floor(daysBetween(patient.program_start_at, Date.now()) / 7) + 1 : null;
  const tabItems = [
    { value: "trend", label: "แนวโน้ม" },
    { value: "se", label: "อาการ" },
  ].concat(dietOnly ? [] : [
    { value: "disp", label: "การจ่ายยา" },
    { value: "hist", label: "ประวัติการปรับโดส" },
  ]);
  // กันแท็บค้าง: ถ้าเพิ่งย้ายผู้ป่วยไปโปรแกรมปรับอาหาร แท็บที่เลือกไว้เดิมอาจไม่มีแล้ว
  const activeTab = tabItems.some((t) => t.value === tab) ? tab : "trend";

  return (
    <div style={page}>
      <PageHead eyebrow={patient.hn + (programWeek ? " · สัปดาห์ที่ " + programWeek : "")}
        title={<span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-4)", flexWrap: "wrap" }}>
          {patient.nickname || "ไม่ได้ตั้งชื่อเล่น"}
          <ProgramBadge programType={patient.program_type} size="md" />
        </span>}
        subtitle={"เริ่มโปรแกรม " + fmtDate(patient.program_start_at)
          + (nextVisit ? " · นัดถัดไป " + fmtDate(nextVisit.visit_at) : " · ยังไม่มีนัดถัดไป")}
        actions={<React.Fragment>
          <Button variant="ghost" icon="chevron-left" onClick={onBack}>รายชื่อ</Button>
          {canManageAccounts ? <ResetPasswordAction patient={patient} /> : null}
          {canEditProgram ? (
            <ProgramSwitch patient={patient} onChanged={(next) => {
              if (onToast) onToast("เปลี่ยนเป็นโปรแกรม" + L.programLabel(next) + "แล้ว");
              state.reload();
            }} />
          ) : null}
          {canRefill ? (
            <Button icon="clipboard-list" onClick={() => onRefill(patientId)}>
              {dietOnly ? "เริ่มหน้าติดตามผล" : "เริ่มหน้ารับยาใหม่"}
            </Button>
          ) : null}
        </React.Fragment>} />

      <div style={{ display: "flex", gap: "var(--space-6)" }}>
        <Card padding="sm" style={{ flex: 1 }}>
          <MetricStat size="sm" label="น้ำหนักล่าสุด" value={latest ? L.formatKg(latest.weight_kg) : DASH} unit={latest ? "กก." : undefined}
            delta={delta == null ? undefined : delta} deltaLabel={delta == null ? undefined : "จากครั้งก่อน"}
            footnote={latest ? fmtSince(latest.measured_at) : "ยังไม่มีบันทึกน้ำหนัก"} />
        </Card>
        <Card padding="sm" style={{ flex: 1 }}>
          <MetricStat size="sm" label="ลดลงสะสม" value={lossPct == null ? DASH : L.formatPercent(lossPct)} unit={lossPct == null ? undefined : "%"}
            footnote={startWeight == null ? "ยังไม่มีน้ำหนักตั้งต้น" : "จาก " + L.formatKg(startWeight) + " กก."} />
        </Card>
        <Card padding="sm" style={{ flex: 1 }}>
          {dietOnly ? (
            <MetricStat size="sm" icon="target" label="น้ำหนักเป้าหมาย"
              value={targetWeight == null ? DASH : L.formatKg(targetWeight)} unit={targetWeight == null ? undefined : "กก."}
              footnote={targetWeight == null || !latest ? "ยังไม่ได้ตั้งเป้าหมาย"
                : "เหลืออีก " + L.formatKg(Math.max(0, Number(latest.weight_kg) - targetWeight)) + " กก."} />
          ) : (
            <MetricStat size="sm" icon="syringe" label="โดสปัจจุบัน" value={currentRx ? L.formatMg(currentRx.prescribed_mg) : DASH} unit={currentRx ? "mg" : undefined}
              footnote={currentRx ? currentRx.prescribed_clicks + " คลิก · " + currentRx.pen_label : "ยังไม่มีใบสั่งโดส"} />
          )}
        </Card>
        <Card padding="sm" style={{ flex: 1 }}>
          <MetricStat size="sm" label="ความสม่ำเสมอ" value={series.length ? weeksWithLog + "/" + series.length : DASH} footnote="สัปดาห์ที่มีบันทึกน้ำหนัก" />
        </Card>
      </div>

      <Tabs value={activeTab} onChange={setTab} items={tabItems} />

      {activeTab === "trend" ? (
        series.length === 0 ? (
          <Card padding="lg">
            <EmptyState icon="chart-line" title="ยังไม่มีข้อมูลสำหรับวาดกราฟ"
              description="เมื่อผู้ป่วยเริ่มบันทึกน้ำหนักหรือมีการฉีดยา กราฟแนวโน้มจะขึ้นที่นี่" />
          </Card>
        ) : (
          <Card padding="md" title={dietOnly ? "น้ำหนัก · อาการ" : "น้ำหนัก · โดส · อาการ"}
            action={<SegmentedControl size="sm" value={range} onChange={setRange}
              options={[{ value: "6w", label: "6 สัปดาห์" }, { value: "12w", label: "12 สัปดาห์" }]} />}>
            <WeightDoseChart data={shown} width={1000} height={380} targetWeight={targetWeight} showDose={!dietOnly} />
            <div style={{ display: "flex", gap: "var(--space-7)", marginTop: "var(--space-5)", font: "var(--type-caption)", color: "var(--text-muted)", flexWrap: "wrap" }}>
              <span style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 26, height: 3, background: "var(--chart-weight-line)", borderRadius: 2 }} />น้ำหนัก</span>
              {dietOnly ? null : <span style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 12, height: 18, background: "var(--chart-dose-bar)", borderRadius: 3 }} />โดส mg ต่อสัปดาห์</span>}
              <span style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 9, height: 9, borderRadius: "50%", background: "var(--chart-se-2)" }} />อาการที่รายงาน</span>
              {targetWeight != null ? <span style={{ display: "flex", alignItems: "center", gap: 6 }}><span style={{ width: 26, borderTop: "2px dashed var(--chart-target-line)" }} />เป้าหมาย {L.formatKg(targetWeight)} กก.</span> : null}
              <span>สัปดาห์ที่ไม่มีบันทึกจะไม่มีจุดบนเส้น</span>
            </div>
          </Card>
        )
      ) : activeTab === "se" ? (
        <Card title="อาการที่บันทึกไว้" subtitle="รายงานโดยผู้ป่วยหรือบันทึกที่คลินิก">
          <DataTable dense emptyLabel="ยังไม่มีการบันทึกอาการ"
            columns={[
              { key: "reported_at", header: "วันที่", render: (r) => fmtDate(r.reported_at) },
              { key: "symptom", header: "อาการ", render: (r) => SYMPTOM_TH[r.symptom] || r.symptom },
              { key: "severity", header: "ระดับ", align: "right", numeric: true, render: (r) => r.severity + " · " + L.severityLabel(r.severity) },
              { key: "source", header: "ที่มา", render: (r) => (r.source === "clinic" ? "บันทึกที่คลินิก" : "ผู้ป่วยรายงานเอง") },
              { key: "note", header: "บันทึกเพิ่มเติม", render: (r) => r.note || DASH },
            ]}
            rows={sideEffects} />
        </Card>
      ) : activeTab === "disp" ? (
        <React.Fragment>
          <Card title="ปากกาที่จ่ายให้ผู้ป่วย" subtitle="ทุกด้ามบันทึก lot และวันหมดอายุไว้เพื่อตามรอยยา">
            <DataTable dense emptyLabel="ยังไม่มีการจ่ายปากกา"
              columns={[
                { key: "dispensed_at", header: "วันที่จ่าย", render: (r) => fmtDate(r.dispensed_at) },
                { key: "pen", header: "รุ่นปากกา", render: (r) => (r.pen_models ? r.pen_models.label : DASH) },
                { key: "lot", header: "Lot" },
                { key: "expiry", header: "หมดอายุ", render: (r) => fmtDate(r.expiry) },
                { key: "left", header: "คลิกคงเหลือ", numeric: true, align: "right", render: (r) => r.clicks_remaining + " / " + r.clicks_total },
                { key: "status", header: "สถานะ", render: (r) => PEN_STATUS_TH[r.status] || r.status },
              ]}
              rows={pens} />
          </Card>
          <Card title="ประวัติการฉีด" subtitle="mg คำนวณจากจำนวนคลิกคูณ mg ต่อคลิกของรุ่นปากกา">
            <DataTable dense emptyLabel="ยังไม่มีการบันทึกการฉีดยา"
              columns={[
                { key: "injected_at", header: "วันที่", render: (r) => fmtDate(r.injected_at) },
                { key: "clicks", header: "คลิก", numeric: true, align: "right" },
                { key: "mg", header: "mg", numeric: true, align: "right", render: (r) => L.formatMg(r.mg) },
                { key: "pen_label", header: "รุ่นปากกา" },
                { key: "site", header: "ตำแหน่งฉีด", render: (r) => SITE_TH[r.site] || r.site },
                { key: "off_rx_reason", header: "ต่างจากใบสั่ง", render: (r) => (r.off_rx_reason ? (OFF_RX_TH[r.off_rx_reason] || r.off_rx_reason) : DASH) },
              ]}
              rows={injections.slice().reverse()} />
          </Card>
        </React.Fragment>
      ) : (
        <Card title="ไทม์ไลน์การปรับโดส" subtitle="ทุกครั้งที่ปรับโดสต้องมีเหตุผลทางคลินิกกำกับ">
          {prescriptions.length === 0 ? (
            <EmptyState icon="clipboard-list" compact title="ยังไม่มีใบสั่งโดส"
              description="เมื่อแพทย์กำหนดโดสครั้งแรกในหน้ารับยาใหม่ ประวัติจะแสดงที่นี่" />
          ) : (
            <TitrationTimeline steps={prescriptions.map((r) => ({
              mg: L.formatMg(r.prescribed_mg),
              clicks: r.prescribed_clicks,
              date: fmtDate(r.effective_from),
              reason: r.clinical_reason,
              by: r.decided_by_nickname || "ไม่ระบุผู้ตัดสินใจ",
              current: !!r.is_current,
            }))} />
          )}
        </Card>
      )}
    </div>
  );
}

/* ===========================================================================
   C4 + C5 + C6 — หน้ารับยาใหม่ จบในหน้าเดียว
   ลำดับการบันทึก: createVisit → saveClinicWeight → setPrescription → dispensePen
=========================================================================== */
const CLINICAL_REASONS = [
  "เริ่มการรักษา",
  "ทนยาได้ดี น้ำหนักลดต่อเนื่อง",
  "มีผลข้างเคียง จึงคงหรือลดโดส",
  "น้ำหนักนิ่ง จึงเพิ่มโดส",
  "ผู้ป่วยขอปรับโดส",
];

function RefillVisit({ patientId, profile, onBack, onSaved }) {
  const state = useAsync(() => Promise.all([
    API.getPatient(patientId),
    API.getWeightLogs(patientId),
    API.getInjections(patientId),
    API.getSideEffects(patientId),
    API.getPrescriptions(patientId),
    API.getPens(patientId),
    API.getPenModels(),
  ]).then(([patient, weights, injections, sideEffects, prescriptions, pens, penModels]) =>
    ({ patient, weights, injections, sideEffects, prescriptions, pens, penModels })), [patientId]);

  const [weight, setWeight] = React.useState("");
  const [waist, setWaist] = React.useState("");
  const [bodyFat, setBodyFat] = React.useState("");
  const [rxMode, setRxMode] = React.useState("keep");
  const [rxPenModelId, setRxPenModelId] = React.useState("");
  const [clicks, setClicks] = React.useState(10);
  const [reason, setReason] = React.useState("");
  const [dispensing, setDispensing] = React.useState(false);
  const [penModelId, setPenModelId] = React.useState("");
  const [lot, setLot] = React.useState("");
  const [expiry, setExpiry] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [saveError, setSaveError] = React.useState(null);
  const [ready, setReady] = React.useState(false);

  const canPrescribe = profile.role === "doctor" || profile.role === "admin";

  // ตั้งค่าเริ่มต้นจากใบสั่งปัจจุบันหลังโหลดเสร็จ — ทำครั้งเดียว ไม่ทับสิ่งที่ผู้ใช้พิมพ์ไปแล้ว
  React.useEffect(() => {
    if (!state.data || ready) return;
    const current = state.data.prescriptions.find((r) => r.is_current) || null;
    const firstModel = state.data.penModels[0];
    if (current) {
      setRxPenModelId(current.pen_model_id);
      setClicks(current.prescribed_clicks);
      setPenModelId(current.pen_model_id);
      setRxMode(canPrescribe ? "keep" : "keep");
    } else {
      // ยังไม่มีใบสั่งเลย — แพทย์ต้องกำหนดโดสครั้งแรกในการมาครั้งนี้
      if (firstModel) { setRxPenModelId(firstModel.id); setPenModelId(firstModel.id); }
      setRxMode(canPrescribe ? "set" : "keep");
      setReason(canPrescribe ? "เริ่มการรักษา" : "");
    }
    setReady(true);
  }, [state.data, ready, canPrescribe]);

  if (state.loading) return <div style={page}><LoadingBlock label="กำลังเตรียมหน้ารับยาใหม่" /></div>;
  if (state.error) {
    return (
      <div style={page}>
        <PageHead title="รับยาใหม่" actions={<Button variant="ghost" icon="chevron-left" onClick={onBack}>ย้อนกลับ</Button>} />
        <Card padding="lg"><ErrorBlock message={state.error} onRetry={state.reload} /></Card>
      </div>
    );
  }

  const { patient, weights, injections, sideEffects, prescriptions, pens, penModels } = state.data;
  // ปรับอาหารอย่างเดียว = การมาครั้งนี้มีแค่ชั่งน้ำหนักกับทบทวนอาการ
  // ส่วนตัดสินใจโดสและจ่ายปากกาต้องไม่มีให้กดเลย (ฐานข้อมูลก็ปฏิเสธอยู่แล้ว)
  const dietOnly = L.isDietOnly(patient.program_type);
  const currentRx = prescriptions.find((r) => r.is_current) || null;
  const activePens = pens.filter((p) => p.status === "in_use");
  const clicksLeft = activePens.reduce((sum, p) => sum + Number(p.clicks_remaining || 0), 0);
  const series = buildWeekSeries({ programStartAt: patient.program_start_at, weights, injections, sideEffects });
  const recentSideEffects = sideEffects.filter((s) => daysBetween(s.reported_at, Date.now()) <= 28);

  const modelOptions = penModels.filter((m) => m.is_active)
    // บอกเป็น "โดสเต็มกี่ mg ใช้กี่คลิก" ไม่ใช่ mg ต่อคลิก เพราะเลขนั้นเป็นทศนิยมยาว
    // (ปากกา 2.5 mg ได้ 0.0417 mg/คลิก) อ่านแล้วไม่ช่วยตัดสินใจอะไรเลย
    .map((m) => ({ value: m.id, label: m.label + " · เต็มโดส " + Number(m.mg_per_dose).toFixed(1) + " mg = " + m.clicks_per_dose + " คลิก" }));
  const rxModel = penModels.find((m) => m.id === rxPenModelId) || null;
  const dispenseModel = penModels.find((m) => m.id === penModelId) || null;

  const weightCheck = L.validateWeight(weight);
  const rxOk = dietOnly || rxMode !== "set" || (!!rxPenModelId && clicks > 0 && reason.trim().length >= 3);
  const dispenseOk = dietOnly || !dispensing || (!!penModelId && lot.trim() !== "" && expiry !== "");
  const canSave = weightCheck.valid && rxOk && dispenseOk && !saving;

  async function handleSave() {
    setSaving(true);
    setSaveError(null);
    try {
      const visit = await API.createVisit({
        patientId: patientId,
        staffId: profile.id,
        kind: dietOnly ? "followup" : currentRx ? "refill" : "initial",
      });
      await API.saveClinicWeight({
        patientId: patientId,
        visitId: visit.id,
        weightKg: weightCheck.value,
        waistCm: waist,
        bodyFatPct: bodyFat,
      });
      if (!dietOnly && rxMode === "set") {
        await API.setPrescription({
          patientId: patientId,
          penModelId: rxPenModelId,
          clicks: clicks,
          clinicalReason: reason,
          visitId: visit.id,
        });
      }
      if (!dietOnly && dispensing) {
        await API.dispensePen({
          patientId: patientId,
          penModelId: penModelId,
          lot: lot,
          expiry: expiry,
          visitId: visit.id,
        });
      }
      onSaved();
    } catch (err) {
      setSaveError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <div style={page}>
      <PageHead eyebrow={(dietOnly ? "FOLLOW-UP VISIT · " : "REFILL VISIT · ") + fmtDate(Date.now())}
        title={<span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-4)", flexWrap: "wrap" }}>
          {(patient.nickname || patient.hn) + (dietOnly ? " · ติดตามผล" : " · รับยาใหม่")}
          <ProgramBadge programType={patient.program_type} size="md" />
        </span>}
        subtitle={dietOnly
          ? "ผู้ป่วยรายนี้อยู่ในโปรแกรมปรับอาหารอย่างเดียว การมาครั้งนี้มีแค่ชั่งน้ำหนักและทบทวนอาการ"
          : "ชั่งน้ำหนัก ทบทวนอาการ ตัดสินใจโดส และจ่ายปากกา จบในหน้าเดียว"}
        actions={<Button variant="ghost" icon="chevron-left" onClick={onBack}>กลับหน้าผู้ป่วย</Button>} />

      {/* minWidth:0 ทั้งสองคอลัมน์ — ไม่งั้น min-content ของฟอร์ม/กราฟดันแทร็กจนล้นขอบขวา
          และปุ่มบันทึกหลุดออกนอกจอจนกดไม่ได้ */}
      <div style={{ display: "grid", gridTemplateColumns: "1.05fr 1fr", gap: "var(--space-6)", alignItems: "start" }}>
        <div style={{ minWidth: 0, display: "flex", flexDirection: "column", gap: "var(--space-6)" }}>

          <Card title="1 · ชั่งด้วยตาชั่งคลินิก" subtitle="น้ำหนักนี้จะบันทึกเป็นการชั่งที่คลินิก">
            <div style={{ display: "flex", gap: "var(--space-5)", flexWrap: "wrap" }}>
              <FormField style={{ flex: "1 1 150px" }} label="น้ำหนัก" required
                error={weightCheck.message || undefined}>
                <Input big size="md" suffix="กก." inputMode="decimal" placeholder="0.0"
                  value={weight} onChange={(e) => setWeight(e.target.value)} invalid={!!weightCheck.message} />
              </FormField>
              <FormField style={{ flex: "1 1 130px" }} label="รอบเอว" optional>
                <Input size="md" suffix="ซม." inputMode="decimal" placeholder="0.0" value={waist} onChange={(e) => setWaist(e.target.value)} />
              </FormField>
              <FormField style={{ flex: "1 1 130px" }} label="มวลไขมัน" optional>
                <Input size="md" suffix="%" inputMode="decimal" placeholder="0.0" value={bodyFat} onChange={(e) => setBodyFat(e.target.value)} />
              </FormField>
            </div>
          </Card>

          <Card title="2 · ทบทวนอาการ" subtitle="อาการที่บันทึกไว้ในช่วง 28 วันล่าสุด">
            {recentSideEffects.length === 0 ? (
              <EmptyState icon="clipboard-list" compact title="ไม่มีอาการที่บันทึกไว้ในช่วงนี้"
                description="ถามผู้ป่วยเพิ่มเติมได้ตามปกติ การบันทึกอาการทำผ่านแอปของผู้ป่วย" />
            ) : (
              <DataTable dense columns={[
                { key: "reported_at", header: "วันที่", render: (r) => fmtDate(r.reported_at) },
                { key: "symptom", header: "อาการ", render: (r) => SYMPTOM_TH[r.symptom] || r.symptom },
                { key: "severity", header: "ระดับ", align: "right", numeric: true, render: (r) => r.severity + " · " + L.severityLabel(r.severity) },
              ]} rows={recentSideEffects} />
            )}
          </Card>

          {dietOnly ? null : (
          <Card title="4 · จ่ายด้ามใหม่" subtitle="lot และวันหมดอายุบังคับทุกครั้งที่จ่ายยา">
            <Switch checked={dispensing} onChange={setDispensing} label="จ่ายปากกาใหม่ในการมาครั้งนี้"
              description={activePens.length ? "ตอนนี้มีปากกาที่ใช้อยู่ " + activePens.length + " ด้าม เหลือรวม " + clicksLeft + " คลิก" : "ตอนนี้ยังไม่มีปากกาที่ใช้งานอยู่"} />
            {dispensing ? (
              <React.Fragment>
                <div style={{ display: "flex", gap: "var(--space-5)", flexWrap: "wrap", marginTop: "var(--space-5)" }}>
                  <FormField style={{ flex: "1.4 1 190px" }} label="รุ่นปากกา" required>
                    <Select size="md" value={penModelId} onChange={(e) => setPenModelId(e.target.value)} options={modelOptions} placeholder="เลือกรุ่นปากกา" />
                  </FormField>
                  <FormField style={{ flex: "1 1 130px" }} label="Lot" required>
                    <Input size="md" value={lot} onChange={(e) => setLot(e.target.value)} invalid={lot.trim() === ""} placeholder="เช่น T4-88999" />
                  </FormField>
                  <FormField style={{ flex: "1 1 150px" }} label="วันหมดอายุ" required hint="ต้องเป็นวันในอนาคต">
                    <Input size="md" type="date" value={expiry} onChange={(e) => setExpiry(e.target.value)} invalid={expiry === ""} />
                  </FormField>
                </div>
                {dispenseModel ? (
                  <div style={{ marginTop: "var(--space-5)" }}>
                    <PenGauge label="คลิกในด้ามใหม่" remaining={dispenseModel.clicks_total} total={dispenseModel.clicks_total}
                      expiry={expiry ? fmtDate(expiry) : undefined} />
                  </div>
                ) : null}
              </React.Fragment>
            ) : null}
          </Card>
          )}
        </div>

        <div style={{ minWidth: 0, display: "flex", flexDirection: "column", gap: "var(--space-6)" }}>
          <Card padding="sm" title={dietOnly ? "แนวโน้มน้ำหนัก" : "แนวโน้มก่อนตัดสินใจ"}>
            {series.length ? (
              <WeightDoseChart data={series.slice(-12)} width={480} height={230} showDose={!dietOnly}
                targetWeight={patient.target_weight_kg != null ? Number(patient.target_weight_kg) : undefined} />
            ) : (
              <EmptyState icon="chart-line" compact title="ยังไม่มีข้อมูลย้อนหลัง"
                description={dietOnly ? "ผู้ป่วยรายนี้ยังไม่มีบันทึกน้ำหนัก" : "ผู้ป่วยรายนี้ยังไม่มีบันทึกน้ำหนักหรือการฉีดยา"} />
            )}
          </Card>

          {dietOnly ? null : (
          <Card title="3 · ตัดสินใจโดส">
            {!canPrescribe ? (
              <Callout tone="watch" title="บัญชีนี้ปรับโดสไม่ได้">
                การกำหนดหรือเปลี่ยนโดสต้องทำโดยแพทย์หรือแอดมิน คุณยังบันทึกน้ำหนักและจ่ายปากกาได้ตามปกติ
              </Callout>
            ) : (
              <React.Fragment>
                <SegmentedControl block value={rxMode} onChange={setRxMode} options={[
                  { value: "keep", label: currentRx ? "คงโดสเดิม" : "ยังไม่กำหนดโดส" },
                  { value: "set", label: currentRx ? "ปรับโดส" : "กำหนดโดสครั้งแรก" },
                ]} />
                {rxMode === "keep" ? (
                  <div style={{ marginTop: "var(--space-5)" }}>
                    {currentRx ? (
                      <DoseReadout compact clicks={currentRx.prescribed_clicks} mgPerClick={Number(currentRx.mg_per_click)}
                        penLabel={currentRx.pen_label} prescribed={currentRx.prescribed_clicks} />
                    ) : (
                      <Callout tone="watch" title="ผู้ป่วยรายนี้ยังไม่มีใบสั่งโดส">
                        เลือก “กำหนดโดสครั้งแรก” เพื่อบันทึกโดสตั้งต้นพร้อมเหตุผลทางคลินิก
                      </Callout>
                    )}
                  </div>
                ) : (
                  <div style={{ marginTop: "var(--space-5)", display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
                    <FormField label="รุ่นปากกาที่ใช้คำนวณโดส" required>
                      <Select size="md" value={rxPenModelId} onChange={(e) => setRxPenModelId(e.target.value)} options={modelOptions} placeholder="เลือกรุ่นปากกา" />
                    </FormField>
                    <FormField label="จำนวนคลิกต่อสัปดาห์" required hint="ค่าจริงคือจำนวนคลิก ส่วน mg เป็นค่าที่คำนวณให้">
                      <NumberStepperSafe value={clicks} onChange={setClicks} min={1} max={rxModel ? rxModel.clicks_total : 200} unit="คลิก" />
                    </FormField>
                    {/* ไม่แสดง mg เลยถ้ายังไม่รู้ว่าใช้ปากการุ่นไหน — ค่า mg ที่เดามาคือค่าที่ผิดแล้วคนเจ็บ */}
                    {rxModel ? (
                      <DoseReadout compact clicks={clicks} mgPerClick={Number(rxModel.mg_per_click)}
                        penLabel={rxModel.label} prescribed={currentRx ? currentRx.prescribed_clicks : undefined} />
                    ) : (
                      <Callout tone="info" title="ยังคำนวณ mg ไม่ได้">เลือกรุ่นปากกาก่อน ระบบจึงจะแปลงจำนวนคลิกเป็น mg ให้</Callout>
                    )}
                    <FormField label="เหตุผลทางคลินิก" required hint="บันทึกนี้ปรากฏในไทม์ไลน์และ audit log">
                      <Select size="md" value={reason} onChange={(e) => setReason(e.target.value)} placeholder="เลือกเหตุผล"
                        options={CLINICAL_REASONS.map((r) => ({ value: r, label: r }))} invalid={rxMode === "set" && reason.trim().length < 3} />
                    </FormField>
                  </div>
                )}
              </React.Fragment>
            )}
          </Card>
          )}

          <Card title={dietOnly ? "3 · บันทึกการมาคลินิก" : "บันทึกการมาคลินิก"}
            subtitle={dietOnly ? "บันทึกน้ำหนักที่ชั่งวันนี้เข้าประวัติของผู้ป่วย" : undefined}>
            {saveError ? <Callout tone="act" title="บันทึกไม่สำเร็จ">{saveError}</Callout> : null}
            <div style={{ display: "flex", gap: "var(--space-4)", marginTop: saveError ? "var(--space-5)" : 0, justifyContent: "flex-end" }}>
              <Button variant="secondary" onClick={onBack} disabled={saving}>ยกเลิก</Button>
              <Button disabled={!canSave} onClick={handleSave}>
                {saving ? "กำลังบันทึก" : dietOnly ? "บันทึกการมาคลินิก" : "บันทึกการนัดและจ่ายยา"}
              </Button>
            </div>
          </Card>

          {!dietOnly && currentRx && clicksLeft < currentRx.prescribed_clicks ? (
            <Callout tone="watch" title={"ยาคงเหลือ " + clicksLeft + " คลิก"}>
              น้อยกว่าโดสตามใบสั่ง ({currentRx.prescribed_clicks} คลิก) ควรจ่ายด้ามใหม่ในการมาครั้งนี้
            </Callout>
          ) : null}
        </div>
      </div>
    </div>
  );
}

/* NumberStepper ของ design system รับ/คืนตัวเลขตรง ๆ — ห่อไว้เพื่อกันค่าที่ไม่ใช่ตัวเลข
   หลุดเข้าไปตอนผู้ใช้กดเร็ว ๆ (คลิกเป็นค่าที่ผิดแล้วคนเจ็บ ไม่ใช่แค่หน้าจอเพี้ยน) */
function NumberStepperSafe({ value, onChange, min, max, unit }) {
  return <NumberStepper value={Number(value) || min || 0} onChange={(v) => onChange(Math.round(Number(v) || 0))} min={min} max={max} unit={unit} />;
}

/* ===========================================================================
   C7 — ภาพรวมคลินิก
   ตัวเลขทุกตัวในหน้านี้นับจากข้อมูลจริงเท่านั้น ค่าที่คำนวณไม่ได้ให้บอกตรง ๆ ว่ายังไม่มี
=========================================================================== */
function ClinicDashboard() {
  const state = useAsync(() => API.listPatients(), []);
  const patients = state.data || [];
  const countWith = (key) => patients.filter((p) => p.flags.includes(key)).length;

  return (
    <div style={page}>
      <PageHead eyebrow="CLINIC OVERVIEW" title="ภาพรวมคลินิก" subtitle="นับจากข้อมูลที่บันทึกไว้ในระบบจริง" />
      <AsyncBlock state={state}
        isEmpty={patients.length === 0}
        empty={<Card padding="lg"><EmptyState icon="chart-pie" title="ยังไม่มีข้อมูลพอสำหรับสรุปภาพรวม"
          description="เมื่อมีผู้ป่วยในโปรแกรมและเริ่มมีบันทึกน้ำหนักแล้ว ภาพรวมจะคำนวณให้อัตโนมัติ" /></Card>}>
        <React.Fragment>
          <div style={{ display: "flex", gap: "var(--space-6)" }}>
            <Card padding="md" style={{ flex: 1 }}><MetricStat label="ผู้ป่วยในโปรแกรม" value={String(patients.length)} unit="คน" /></Card>
            <Card padding="md" style={{ flex: 1 }}><MetricStat label="มีสัญญาณที่ควรดู" value={String(patients.filter((p) => p.flags.length > 0).length)} unit="คน" /></Card>
            <Card padding="md" style={{ flex: 1 }}><MetricStat label="ขาดบันทึกน้ำหนัก" value={String(countWith("missing"))} unit="คน" footnote="เกิน 12 วัน" /></Card>
            <Card padding="md" style={{ flex: 1 }}><MetricStat label="ยาใกล้หมด ยังไม่มีนัด" value={String(countWith("penlow"))} unit="คน" /></Card>
          </div>

          <Card title="สัญญาณเตือนแยกตามประเภท" subtitle="นับจำนวนผู้ป่วยที่เข้าเงื่อนไขแต่ละข้อในตอนนี้">
            <DataTable dense emptyLabel="ยังไม่มีสัญญาณเตือน"
              columns={[
                { key: "label", header: "สัญญาณ" },
                { key: "level", header: "ระดับ" },
                { key: "count", header: "จำนวนผู้ป่วย", numeric: true, align: "right" },
              ]}
              rows={L.FLAG_ORDER.map((key) => ({
                id: key,
                label: L.FLAG_META[key][1],
                level: L.FLAG_META[key][0] === "act" ? "ควรจัดการ" : "ควรเฝ้าดู",
                count: countWith(key),
              }))} />
          </Card>

          <Callout tone="info" title="ตัวเลขผลลัพธ์เชิงลึกยังคำนวณไม่ได้">
            สัดส่วนคนที่ลดถึง 5 / 10 / 15 เปอร์เซ็นต์ อัตราคงอยู่รายสัปดาห์ และความสัมพันธ์ระหว่างโดสกับผลลัพธ์
            ต้องรวมข้อมูลย้อนหลังของผู้ป่วยทุกคน ระบบยังไม่มีสรุปชุดนี้ให้ดึง จึงยังไม่แสดงตัวเลขเพื่อไม่ให้เข้าใจผิด
          </Callout>
        </React.Fragment>
      </AsyncBlock>
    </div>
  );
}

/* ===========================================================================
   C8 — ตั้งค่ารุ่นปากกา: แก้ขนาดยาต่อโดสตามกล่อง (แอดมินเท่านั้น)
   จงใจให้ทำยาก: เลือกรุ่น → กรอกค่าใหม่ → เลือกผู้อนุมัติคนที่สอง → พิมพ์ยืนยัน
=========================================================================== */
function PenSettings({ profile, onSaved }) {
  const state = useAsync(() => Promise.all([API.getPenModels(), API.listStaff()])
    .then(([models, staff]) => ({ models, staff })), []);

  const [modelId, setModelId] = React.useState("");
  const [mg, setMg] = React.useState("");
  const [approverId, setApproverId] = React.useState("");
  const [typed, setTyped] = React.useState("");
  const [ack, setAck] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const [saveError, setSaveError] = React.useState(null);

  const models = (state.data && state.data.models) || [];
  const staff = (state.data && state.data.staff) || [];
  const admins = staff.filter((s) => s.is_active && s.role === "admin" && s.id !== profile.id);
  const selected = models.find((m) => m.id === modelId) || null;
  const approver = admins.find((s) => s.id === approverId) || null;

  // แก้ได้เฉพาะ "ขนาดที่พิมพ์บนกล่อง" — mg ต่อคลิกเป็นค่าที่ระบบหารเอง (ขนาดต่อโดส ÷ 60)
  const currentMg = selected ? Number(selected.mg_per_dose) : null;
  const mgNum = Number(mg);
  const mgValid = mg.trim() !== "" && Number.isFinite(mgNum) && mgNum > 0 && mgNum <= 30 && mgNum !== currentMg;
  const canSave = !!selected && mgValid && !!approver && typed === "ยืนยัน" && ack && !saving;

  function reset() {
    setMg(""); setTyped(""); setAck(false); setApproverId(""); setSaveError(null);
  }

  async function handleSave() {
    setSaving(true);
    setSaveError(null);
    try {
      await API.updatePenModelMg({
        penModelId: modelId,
        newMgPerDose: mgNum,
        approverStaffId: approverId,
        confirmText: typed,
      });
      reset();
      setModelId("");
      state.reload();
      onSaved(mgNum);
    } catch (err) {
      setSaveError((err && err.message) || "บันทึกไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <div style={{ ...page, maxWidth: 960 }}>
      <PageHead eyebrow="ADMIN ONLY" title="ตั้งค่ารุ่นปากกา"
        subtitle="แก้ได้เฉพาะขนาดยาต่อโดสตามที่พิมพ์บนกล่อง · mg ต่อคลิกระบบหารให้เอง · มีผลย้อนหลังกับผู้ป่วยทุกคน" />

      <AsyncBlock state={state}
        isEmpty={models.length === 0}
        empty={<Card padding="lg"><EmptyState icon="pill" title="ยังไม่มีรุ่นปากกาในระบบ"
          description="ต้องมีรุ่นปากกาอย่างน้อยหนึ่งรุ่นก่อน จึงจะตั้งค่าขนาดยาได้" /></Card>}>
        <React.Fragment>
          <Card padding="none">
            <DataTable onRowClick={(r) => { setModelId(r.id); reset(); }}
              columns={[
                { key: "label", header: "รุ่นปากกา" },
                { key: "mg_per_dose", header: "เต็มโดส (mg)", numeric: true, align: "right", render: (r) => Number(r.mg_per_dose).toFixed(1) },
                { key: "clicks_per_dose", header: "คลิกต่อโดส", numeric: true, align: "right" },
                // ทศนิยม 4 ตำแหน่ง เพราะปากกา 2.5 mg ได้ 0.0417 ถ้าปัดสองตำแหน่งจะเหลือ 0.04 ซึ่งคูณกลับไม่ตรง
                { key: "mg_per_click", header: "mg ต่อคลิก", numeric: true, align: "right",
                  render: (r) => <span style={{ color: "var(--text-muted)" }}>{Number(r.mg_per_click).toFixed(4)}</span> },
                { key: "clicks_total", header: "คลิกทั้งด้าม", numeric: true, align: "right",
                  render: (r) => r.clicks_total + " (" + r.doses_per_pen + " โดส)" },
                { key: "mg_updated_at", header: "แก้ขนาดยาล่าสุด", render: (r) => (r.mg_updated_at ? fmtDate(r.mg_updated_at) : "ยังไม่เคยแก้") },
                { key: "is_active", header: "สถานะ", render: (r) => (r.is_active ? <Badge tone="ok" size="sm">ใช้งานอยู่</Badge> : <Badge tone="neutral" size="sm">ปิดใช้งาน</Badge>) },
                { key: "pick", header: "", align: "right", render: (r) => (r.id === modelId ? <Badge tone="brand" size="sm">กำลังแก้</Badge> : <Icon name="chevron-right" size={16} style={{ color: "var(--text-subtle)" }} />) },
              ]}
              rows={models} />
          </Card>

          {admins.length === 0 ? (
            <Callout tone="watch" title="ยังไม่มีผู้อนุมัติคนที่สอง">
              การแก้ขนาดยาต่อโดสต้องมีแอดมินอีกคนที่ยังใช้งานอยู่เป็นผู้อนุมัติ ตอนนี้ระบบยังไม่มีบัญชีดังกล่าว จึงยังแก้ค่าไม่ได้
            </Callout>
          ) : !selected ? (
            <Card padding="lg">
              <EmptyState icon="shield-alert" compact title="เลือกรุ่นปากกาที่ต้องการแก้"
                description="แตะแถวในตารางด้านบนเพื่อเปิดขั้นตอนยืนยันสองชั้น" />
            </Card>
          ) : (
            <DangerConfirm title={"แก้ขนาดยาต่อโดส — " + selected.label}
              warning="การเปลี่ยนค่านี้มีผลย้อนหลังกับการคำนวณโดสของผู้ป่วยทุกคนที่ใช้ปากการุ่นนี้ ตรวจสอบกับกล่องยาและเอกสารกำกับยาก่อนบันทึก"
              value={typed} onChange={setTyped}
              secondApprover={approver ? approver.nickname : undefined}
              acknowledged={ack} onAcknowledge={setAck}
              footer={<React.Fragment>
                <Button variant="secondary" onClick={() => { setModelId(""); reset(); }} disabled={saving}>ยกเลิก</Button>
                <Button variant="danger" disabled={!canSave} onClick={handleSave}>{saving ? "กำลังบันทึก" : "บันทึกค่าใหม่"}</Button>
              </React.Fragment>}>
              <div style={{ display: "flex", gap: "var(--space-5)", flexWrap: "wrap" }}>
                <FormField style={{ flex: "1 1 180px" }} label="ขนาดเดิมต่อโดสเต็ม">
                  <Input size="md" disabled value={currentMg.toFixed(1) + " mg (" + selected.clicks_per_dose + " คลิก)"} />
                </FormField>
                <FormField style={{ flex: "1 1 180px" }} label="ขนาดใหม่ต่อโดสเต็ม" required
                  hint={mg.trim() !== "" && !mgValid
                    ? "ต้องเป็นตัวเลข 0–30 และต่างจากค่าเดิม"
                    : (mgValid ? "จะได้ " + (mgNum / selected.clicks_per_dose).toFixed(4) + " mg ต่อคลิก" : "ตามที่พิมพ์บนกล่องยา")}>
                  <Input size="md" invalid={mg.trim() !== "" && !mgValid} suffix="mg" placeholder="2.5"
                    inputMode="decimal" value={mg} onChange={(e) => setMg(e.target.value)} />
                </FormField>
                <FormField style={{ flex: "1 1 220px" }} label="ผู้อนุมัติคนที่สอง" required hint="ต้องเป็นแอดมินคนอื่นที่ยังใช้งานอยู่">
                  <Select size="md" value={approverId} onChange={(e) => setApproverId(e.target.value)} placeholder="เลือกผู้อนุมัติ"
                    options={admins.map((s) => ({ value: s.id, label: s.nickname + " · " + s.staff_code }))} />
                </FormField>
              </div>
              {saveError ? <Callout tone="act" title="บันทึกไม่สำเร็จ">{saveError}</Callout> : null}
            </DangerConfirm>
          )}
        </React.Fragment>
      </AsyncBlock>
    </div>
  );
}

/* ===========================================================================
   C9 — ประวัติการเข้าถึงและแก้ไขข้อมูล
=========================================================================== */
function AuditLog() {
  const state = useAsync(() => API.getAuditLog(200), []);
  const [query, setQuery] = React.useState("");

  const rows = (state.data || []).filter((r) => {
    const q = query.trim().toLowerCase();
    if (!q) return true;
    return [r.actor_nickname, r.patient_hn, ACTION_TH[r.action], ENTITY_TH[r.entity] || r.entity, r.summary]
      .filter(Boolean).join(" ").toLowerCase().includes(q);
  });

  return (
    <div style={page}>
      <PageHead eyebrow="AUDIT" title="ประวัติการเข้าถึงและแก้ไขข้อมูล"
        subtitle="บันทึกทั้งการอ่านและการแก้ไข ลบไม่ได้ · แสดง 200 รายการล่าสุด"
        actions={<Input size="md" prefixIcon="search" placeholder="ค้นหาผู้ใช้ หรือ HN" style={{ width: 240 }}
          value={query} onChange={(e) => setQuery(e.target.value)} />} />

      <AsyncBlock state={state}
        isEmpty={(state.data || []).length === 0}
        empty={<Card padding="lg"><EmptyState icon="history" title="ยังไม่มีประวัติการใช้งาน"
          description="เมื่อมีการเปิดดูหรือแก้ไขข้อมูลผู้ป่วย รายการจะบันทึกไว้ที่นี่โดยอัตโนมัติ" /></Card>}>
        <Card padding="none">
          <DataTable dense emptyLabel="ไม่พบรายการที่ตรงกับคำค้นหา"
            columns={[
              { key: "at", header: "เวลา", render: (r) => fmtDateTime(r.at) },
              { key: "actor", header: "ผู้ใช้", render: (r) => r.actor_nickname || "ไม่ระบุ" },
              { key: "actor_role", header: "บทบาท", render: (r) => ROLE_TH[r.actor_role] || DASH },
              { key: "action", header: "การกระทำ", render: (r) => ACTION_TH[r.action] || r.action },
              { key: "entity", header: "ข้อมูลที่เกี่ยวข้อง", render: (r) => (ENTITY_TH[r.entity] || r.entity) + (r.patient_hn ? " · " + r.patient_hn : "") },
              { key: "summary", header: "รายละเอียด", render: (r) => r.summary || DASH },
            ]}
            rows={rows} />
        </Card>
      </AsyncBlock>
    </div>
  );
}

/* ===========================================================================
   คำขอใช้สิทธิตาม PDPA (เฉพาะแอดมิน)
   ผู้ป่วยยื่นคำขอจากแอปของตัวเอง (P11) — หน้านี้คือฝั่งที่ปิดเรื่อง
   หน้าจอไม่ได้ลบข้อมูลเอง เป็นการบันทึกผลการพิจารณาเท่านั้น
=========================================================================== */
function DataRequests({ onToast }) {
  // คำขอเก็บแค่ patient_id — ต้องเทียบกับรายชื่อผู้ป่วยเพื่อให้เจ้าหน้าที่รู้ว่าเป็นของใคร
  const state = useAsync(() => Promise.all([
    API.listDataRequests("open"),
    API.listPatients(),
  ]).then(([requests, patients]) => {
    const byId = {};
    patients.forEach((p) => { byId[p.patient_id] = p; });
    return requests.map((r) => Object.assign({}, r, { patient: byId[r.patient_id] || null }));
  }), []);

  const [active, setActive] = React.useState(null);
  const [decision, setDecision] = React.useState("done");
  const [note, setNote] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [error, setError] = React.useState(null);

  const rows = state.data || [];
  // ปฏิเสธคำขอต้องมีเหตุผลเสมอ ผู้ป่วยจะได้รู้ว่าทำไม
  const canSubmit = !!active && !saving && (decision === "done" || note.trim() !== "");

  function open(row) {
    setActive(row);
    setDecision("done");
    setNote("");
    setError(null);
  }

  async function submit() {
    setSaving(true);
    setError(null);
    try {
      await API.handleDataRequest(active.id, decision, note);
      setActive(null);
      state.reload();
      onToast(decision === "done" ? "ปิดเรื่องเป็นดำเนินการแล้ว" : "บันทึกการปฏิเสธคำขอแล้ว");
    } catch (err) {
      setError((err && err.message) || "ปิดเรื่องไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      setSaving(false);
    }
  }

  return (
    <div style={page}>
      <PageHead eyebrow="PDPA" title="คำขอใช้สิทธิของผู้ป่วย"
        subtitle="คำขอสำเนาข้อมูลและคำขอลบข้อมูลที่ยังไม่ปิดเรื่อง · ผู้ป่วยยื่นเองจากแอป"
        actions={<Button variant="secondary" icon="refresh-cw" onClick={state.reload}>โหลดใหม่</Button>} />

      <AsyncBlock state={state}
        isEmpty={rows.length === 0}
        empty={<Card padding="lg"><EmptyState icon="inbox" title="ไม่มีคำขอที่ค้างอยู่"
          description="เมื่อผู้ป่วยยื่นขอสำเนาหรือขอลบข้อมูลจากแอป คำขอจะมารออยู่ที่นี่" /></Card>}>
        <React.Fragment>
          <Callout tone="info" title="ต้องตอบกลับภายในกรอบเวลาตามกฎหมาย">
            ตรวจตัวตนผู้ยื่นก่อนดำเนินการทุกครั้ง · การลบข้อมูลย้อนกลับไม่ได้
            ส่วนที่กฎหมายกำหนดให้เก็บต่อ ให้ระบุไว้ในหมายเหตุ
          </Callout>

          <Card padding="none">
            <DataTable emptyLabel="ไม่มีคำขอที่ค้างอยู่"
              columns={[
                { key: "requested_at", header: "ยื่นเมื่อ", render: (r) => fmtDateTime(r.requested_at) },
                { key: "patient", header: "ผู้ป่วย", render: (r) => (
                  <div>
                    <div style={{ font: "var(--type-label)", color: "var(--text-strong)" }}>
                      {r.patient ? (r.patient.nickname || "ไม่ได้ตั้งชื่อเล่น") : "ไม่พบข้อมูลผู้ป่วย"}
                    </div>
                    <div style={{ font: "var(--type-caption)", color: "var(--text-subtle)" }}>
                      {r.patient ? r.patient.hn : DASH}
                    </div>
                  </div>) },
                { key: "kind", header: "ประเภทคำขอ", render: (r) => (
                  <Badge tone={r.kind === "erase" ? "act" : "brand"} icon={r.kind === "erase" ? "trash-2" : "download"}>
                    {REQUEST_KIND_TH[r.kind] || r.kind}
                  </Badge>) },
                { key: "status", header: "สถานะ", render: (r) => REQUEST_STATUS_TH[r.status] || r.status },
                { key: "go", header: "", align: "right", render: (r) => (
                  <Button size="sm" variant="secondary" onClick={() => open(r)}>ปิดเรื่อง</Button>) },
              ]}
              rows={rows} />
          </Card>
        </React.Fragment>
      </AsyncBlock>

      <Dialog open={!!active} title="ปิดเรื่องคำขอ"
        description={active
          ? (REQUEST_KIND_TH[active.kind] || active.kind)
            + " · " + (active.patient ? active.patient.hn : "ไม่พบข้อมูลผู้ป่วย")
            + " · ยื่นเมื่อ " + fmtDateTime(active.requested_at)
          : ""}
        onClose={() => setActive(null)}
        footer={<React.Fragment>
          <Button variant="secondary" onClick={() => setActive(null)} disabled={saving}>ยกเลิก</Button>
          <Button onClick={submit} disabled={!canSubmit}>{saving ? "กำลังบันทึก" : "บันทึกผลการพิจารณา"}</Button>
        </React.Fragment>}>
        <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
          <FormField label="ผลการพิจารณา" required>
            <SegmentedControl block value={decision} onChange={setDecision}
              options={[
                { value: "done", label: "ดำเนินการแล้ว" },
                { value: "rejected", label: "ปฏิเสธคำขอ" },
              ]} />
          </FormField>
          <FormField label="หมายเหตุ" required={decision === "rejected"}
            hint={decision === "rejected"
              ? "ระบุเหตุผลที่ปฏิเสธ ผู้ป่วยจะเห็นข้อความนี้ในแอป"
              : "เช่น ส่งไฟล์ทางอีเมลแล้ว หรือลบข้อมูลส่วนที่ลบได้แล้ว"}>
            <Input size="md" value={note} onChange={(e) => setNote(e.target.value)}
              placeholder={decision === "rejected" ? "เหตุผลที่ปฏิเสธคำขอ" : "สิ่งที่ทำไปแล้ว"} />
          </FormField>
          {error ? <Callout tone="act" title="ปิดเรื่องไม่สำเร็จ">{error}</Callout> : null}
        </div>
      </Dialog>
    </div>
  );
}

/* ===========================================================================
   C1 — เข้าสู่ระบบฝั่งคลินิก (Supabase Auth จริง)
   บทบาทมาจากตาราง staff เท่านั้น ไม่มีรายชื่อคนอยู่ในโค้ดอีกแล้ว
=========================================================================== */
function ClinicLogin({ onSignedIn }) {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);

  const canSubmit = email.trim() !== "" && password !== "" && !busy;

  async function submit(e) {
    if (e) e.preventDefault();
    if (!canSubmit) return;
    setBusy(true);
    setError(null);
    try {
      await API.auth.signInWithPassword(email, password);
      if (onSignedIn) await onSignedIn();
    } catch (err) {
      setError((err && err.message) || "เข้าสู่ระบบไม่สำเร็จ กรุณาลองใหม่");
    } finally {
      // ถ้าโปรไฟล์โหลดไม่ผ่าน (เช่นบัญชียังไม่ผูกกับตาราง staff) ผู้ใช้ต้องกดลองใหม่ได้
      setBusy(false);
    }
  }

  return (
    <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", background: "var(--surface-page)" }}>
      <form onSubmit={submit} style={{ width: 380, display: "flex", flexDirection: "column", gap: "var(--space-6)" }}>
        <div style={{ textAlign: "center" }}>
          <p className="rmd-wordmark" style={{ fontSize: 38 }}>ReMeDe</p>
          <p style={{ font: "var(--type-caption)", color: "var(--bronze-700)", letterSpacing: "var(--ls-eyebrow)", textTransform: "uppercase", marginTop: 2 }}>Weight Journey · Clinic</p>
        </div>
        <Card padding="lg">
          <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-5)" }}>
            <FormField label="อีเมลของบุคลากร" required>
              <Input size="md" type="email" autoComplete="username" placeholder="name@remede.co.th"
                value={email} onChange={(e) => setEmail(e.target.value)} />
            </FormField>
            <FormField label="รหัสผ่าน" required>
              <Input size="md" type="password" autoComplete="current-password" placeholder="รหัสผ่าน"
                value={password} onChange={(e) => setPassword(e.target.value)} />
            </FormField>
            {error ? <Callout tone="act" title="เข้าสู่ระบบไม่สำเร็จ">{error}</Callout> : null}
            <Button block size="lg" icon="log-in" type="submit" disabled={!canSubmit}>
              {busy ? "กำลังเข้าสู่ระบบ" : "เข้าสู่ระบบ"}
            </Button>
          </div>
        </Card>
        <p style={{ font: "var(--type-caption)", color: "var(--text-subtle)", textAlign: "center" }}>
          การเข้าดูข้อมูลผู้ป่วยทุกครั้งถูกบันทึกไว้ในประวัติการใช้งาน
        </p>
      </form>
    </div>
  );
}

/* ===========================================================================
   โครงแอปฝั่งคลินิก — ดูแลเซสชัน บทบาท และการนำทาง
=========================================================================== */
const NAV_ALL = [
  { value: "list", label: "รายชื่อผู้ป่วย", icon: "users" },
  { value: "refill", label: "รับยาใหม่", icon: "clipboard-list" },
  { value: "dash", label: "ภาพรวมคลินิก", icon: "chart-pie" },
  { value: "requests", label: "คำขอใช้สิทธิ PDPA", icon: "file-text" },
  { value: "settings", label: "ตั้งค่ารุ่นปากกา", icon: "shield-alert" },
  { value: "audit", label: "ประวัติการแก้ไข", icon: "history" },
];

function ClinicApp() {
  const [profile, setProfile] = React.useState(null);
  const [authReady, setAuthReady] = React.useState(false);
  const [authError, setAuthError] = React.useState(null);
  const [nav, setNav] = React.useState("list");
  const [screen, setScreen] = React.useState("list");
  const [activePatientId, setActivePatientId] = React.useState(null);
  const [toast, setToast] = React.useState(null);

  React.useEffect(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3600); return () => clearTimeout(t); }, [toast]);

  const mounted = React.useRef(true);
  React.useEffect(() => () => { mounted.current = false; }, []);

  // แปลงเซสชันเป็นโปรไฟล์ + บทบาทจริงจากตาราง staff
  const applySession = React.useCallback(async (session) => {
    if (!mounted.current) return;
    if (!session) {
      setProfile(null);
      setAuthError(null);
      setAuthReady(true);
      return;
    }
    try {
      const p = await API.auth.getProfile();
      if (!mounted.current) return;
      setProfile(p);
      setAuthError(null);
    } catch (err) {
      if (!mounted.current) return;
      setAuthError((err && err.message) || "โหลดข้อมูลผู้ใช้ไม่สำเร็จ");
      setProfile(null);
    } finally {
      if (mounted.current) setAuthReady(true);
    }
  }, []);

  // ติดตามเซสชันของ Supabase — ทั้งตอนเปิดหน้าครั้งแรกและตอน token หมุน
  React.useEffect(() => {
    let stop = null;
    API.auth.getSession()
      .then(applySession)
      .then(() => { stop = API.auth.onChange((event, session) => { applySession(session); }); })
      .catch((err) => {
        if (!mounted.current) return;
        setAuthError((err && err.message) || "เชื่อมต่อระบบไม่สำเร็จ");
        setAuthReady(true);
      });
    return () => { if (stop) stop(); };
  }, [applySession]);

  const allowed = profile && profile.kind === "staff" ? (ROLE_NAV[profile.role] || []) : [];
  const go = (value) => { setNav(value); setScreen(value); setActivePatientId(null); };

  async function handleLogout() {
    try { await API.auth.logout(); } catch (err) { setToast((err && err.message) || "ออกจากระบบไม่สำเร็จ"); }
    setProfile(null);
    setActivePatientId(null);
    go("list");
  }

  const shell = (children) => (
    <div style={{ display: "flex", height: "100%", minHeight: 0, background: "var(--surface-page)", fontFamily: "var(--font-ui)" }}>{children}</div>
  );

  if (!authReady) {
    return shell(<div style={page}><LoadingBlock label="กำลังตรวจสอบสิทธิ์การเข้าใช้งาน" /></div>);
  }

  if (!profile) {
    return shell(
      <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
        {authError ? (
          <div style={{ padding: "var(--space-6) var(--page-gutter-desktop) 0" }}>
            <Callout tone="act" title="เข้าใช้งานไม่ได้">{authError}</Callout>
          </div>
        ) : null}
        {/* โหลดโปรไฟล์เองหลังล็อกอินสำเร็จ ไม่รอเฉพาะ event ของ Supabase
            ถ้า event มาไม่ถึงด้วยเหตุใดก็ตาม ผู้ใช้จะได้ไม่ค้างอยู่หน้าล็อกอินทั้งที่ล็อกอินผ่านแล้ว */}
        <ClinicLogin onSignedIn={async () => {
          setNav("list");
          setScreen("list");
          const session = await API.auth.getSession();
          await applySession(session);
        }} />
      </div>
    );
  }

  // ผู้ป่วยที่บังเอิญเปิดลิงก์นี้ต้องไม่เห็นอะไรของฝั่งคลินิกเลย แม้แต่ชื่อเมนู
  if (profile.kind !== "staff") {
    return shell(
      <div style={page}>
        <EmptyState icon="shield-alert" title="หน้านี้สำหรับบุคลากรของคลินิก"
          description="บัญชีของคุณเป็นบัญชีผู้ป่วย กรุณาใช้แอปสำหรับผู้ป่วยแทน"
          action={<Button variant="secondary" icon="log-out" onClick={handleLogout}>ออกจากระบบ</Button>} />
      </div>
    );
  }

  const canRefill = allowed.includes("refill");
  /* เปิดบัญชี/ตั้งรหัสใหม่ให้ผู้ป่วย — เฉพาะแอดมิน (ตัดสินใจแล้ว 3 ส.ค. 2569)
     ด่านจริงอยู่ที่ Edge Function patient-accounts ซึ่งเช็ค role = admin ด้วยตัวเอง
     บรรทัดนี้แค่ซ่อนปุ่มไม่ให้พยาบาลกดแล้วเจอ 403 เปล่า ๆ
     ถ้าจะเปลี่ยนสิทธิ์ ต้องแก้ทั้งสองที่เสมอ แก้แค่ที่นี่ = ซ่อนปุ่มแต่ทางยังเปิด */
  const canManageAccounts = profile.kind === "staff" && profile.role === "admin";
  const openPatient = (id) => { setActivePatientId(id); setScreen("detail"); };
  const openRefill = (id) => { setActivePatientId(id); setScreen("refill-form"); };

  let body;
  if (screen === "detail" && activePatientId) {
    body = <PatientDetail patientId={activePatientId} profile={profile} canRefill={canRefill}
      canManageAccounts={canManageAccounts}
      onBack={() => { setActivePatientId(null); setScreen(nav === "refill" ? "refill" : "list"); }}
      onRefill={openRefill} onToast={setToast} />;
  } else if (screen === "refill-form" && activePatientId && canRefill) {
    body = <RefillVisit patientId={activePatientId} profile={profile}
      onBack={() => setScreen("detail")}
      onSaved={() => { setToast("บันทึกการมาคลินิกเรียบร้อย"); setScreen("detail"); }} />;
  } else if (screen === "refill" && canRefill) {
    body = <PatientList pickMode onOpen={openRefill} />;
  } else if (screen === "dash" && allowed.includes("dash")) {
    body = <ClinicDashboard />;
  } else if (screen === "requests") {
    // กันอีกชั้นนอกเหนือจากการซ่อนเมนู — สิทธิ์จริงบังคับที่ RLS ของ data_requests อยู่แล้ว
    body = allowed.includes("requests")
      ? <DataRequests onToast={setToast} />
      : <div style={page}><EmptyState icon="shield-alert" title="เฉพาะแอดมินเท่านั้น" description="บัญชีของคุณไม่มีสิทธิ์ดูคำขอใช้สิทธิของผู้ป่วย" /></div>;
  } else if (screen === "settings") {
    // กันอีกชั้นนอกเหนือจากการซ่อนเมนู — สิทธิ์จริงบังคับที่ DB อยู่แล้ว
    body = allowed.includes("settings")
      ? <PenSettings profile={profile} onSaved={(v) => setToast("บันทึกขนาดยาต่อโดสเป็น " + Number(v).toFixed(1) + " mg แล้ว")} />
      : <div style={page}><EmptyState icon="shield-alert" title="เฉพาะแอดมินเท่านั้น" description="บัญชีของคุณไม่มีสิทธิ์เข้าหน้านี้" /></div>;
  } else if (screen === "audit") {
    body = allowed.includes("audit")
      ? <AuditLog />
      : <div style={page}><EmptyState icon="shield-alert" title="เฉพาะแอดมินเท่านั้น" description="บัญชีของคุณไม่มีสิทธิ์ดูประวัติการแก้ไข" /></div>;
  } else {
    body = <PatientList onOpen={openPatient} canManageAccounts={canManageAccounts} />;
  }

  return (
    <div style={{ display: "flex", height: "100%", minHeight: 0, background: "var(--surface-page)", fontFamily: "var(--font-ui)", position: "relative" }}>
      <SideNav value={nav} onChange={go} items={NAV_ALL.filter((n) => allowed.includes(n.value))}
        footer={<div style={{ padding: "0 var(--space-4)", display: "flex", alignItems: "center", gap: "var(--space-4)", color: "var(--navy-200)" }}>
          <span style={{ width: 34, height: 34, borderRadius: "50%", background: "rgba(255,255,255,.10)", display: "inline-flex", alignItems: "center", justifyContent: "center", flex: "0 0 auto" }}><Icon name="user" size={17} /></span>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ font: "var(--type-body-sm)", color: "var(--warm-000)" }}>{profile.nickname}</div>
            <div style={{ font: "var(--type-caption)", color: "var(--bronze-300)" }}>{ROLE_TH[profile.role] || profile.role}</div>
          </div>
          <IconButton icon="log-out" label="ออกจากระบบ" onClick={handleLogout} style={{ color: "var(--navy-200)", flex: "0 0 auto" }} />
        </div>} />
      <div style={{ flex: 1, minWidth: 0, display: "flex" }}>{body}</div>
      {toast ? <div style={{ position: "absolute", right: 24, bottom: 24, zIndex: 60 }}><Toast tone="ok" title={toast} onClose={() => setToast(null)} /></div> : null}
    </div>
  );
}

Object.assign(window, { ClinicApp, ClinicLogin, PatientList, PatientDetail, RefillVisit, ClinicDashboard, PenSettings, AuditLog, DataRequests, PageHead });
