/* ==========================================================================
 * ADMIN NEXT — core.jsx  (แยกจาก index.html เดิม; โหลดผ่าน <script type="text/babel">)
 * โหลดตามลำดับใน index.html — ทุกไฟล์แชร์ scope เดียวกัน (babel-standalone)
 * ========================================================================== */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

/* ============================================================================
 *  ADMIN NEXT — UI ใหม่ (เบต้า) วางคู่ขนานที่ /admin/next/
 *  ต่อ API เดิม /api/admin/* + ใช้ token เดิมจาก localStorage['adminToken']
 *  ไม่แตะ backend / route / auth เดิมเลย  (frontend ล้วน + adapter)
 * ========================================================================== */

/* ---------- adapter: ใช้ auth เดิม ---------- */
const getToken = () => { try { return localStorage.getItem("adminToken") || ""; } catch { return ""; } };
const getAdminKey = () => { try { return localStorage.getItem("ADMIN_KEY") || ""; } catch { return ""; } };
async function api(path, opt = {}) {
  const headers = { "Content-Type": "application/json", ...(opt.headers || {}) };
  const t = getToken(); if (t) headers.Authorization = "Bearer " + t;
  const k = getAdminKey(); if (k) headers["x-admin-key"] = k;
  let r;
  try {
    r = await fetch("/api/admin" + path, {
      method: opt.method || "GET",
      credentials: "include",
      headers,
      body: opt.body != null ? JSON.stringify(opt.body) : undefined,
    });
  } catch (_netErr) {
    throw nxApiError(0, null);
  }
  const data = await r.json().catch(() => null);
  if (r.status === 401 || r.status === 403) {
    throw nxApiError(r.status, data || { error: "unauthorized" });
  }
  if (!r.ok) throw nxApiError(r.status, data);
  return data;
}

/* ---------- ข้อความ error ภาษาไทย (ชุดเดียวกับหลังบ้านเก่า admin/assets/js/admin/core.js) ---------- */
const NX_ERROR_TH = {
  invalid_credentials: 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง',
  /* ---- กรอกข้อมูลไม่ครบ / ไม่ถูกต้อง ---- */
  username_required: 'กรุณากรอกชื่อผู้ใช้',
  amount_must_be_positive: 'จำนวนเงินต้องมากกว่า 0',
  invalid_turnover: 'ยอดเทิร์นต้องมากกว่า 0',
  invalid_category: 'ประเภทไม่ถูกต้อง',
  category_required: 'กรุณาระบุประเภท',
  date_range_required: 'กรุณาเลือกช่วงวันที่',
  session_id_required: 'ไม่พบรหัสเซสชัน',
  ref_and_username_required: 'ต้องระบุทั้งเลขอ้างอิงและชื่อผู้ใช้',
  bigwin_ref_required: 'ต้องระบุเลขอ้างอิงและเวลาของรายการ',
  no_updates: 'ไม่มีข้อมูลที่เปลี่ยนแปลง',
  url_too_long: 'ลิงก์ยาวเกินกำหนด',
  /* ---- รูปภาพ / ไฟล์ ---- */
  invalid_image: 'ไฟล์รูปไม่ถูกต้อง',
  invalid_data_url: 'ข้อมูลรูปไม่ถูกต้อง',
  unsupported_image_type_or_convert_failed: 'ไฟล์รูปชนิดนี้ใช้ไม่ได้ หรือแปลงไฟล์ไม่สำเร็จ',
  no_slip_image: 'ยังไม่ได้แนบรูปสลิป',
  slip_file_invalid: 'ไฟล์สลิปไม่ถูกต้อง รองรับเฉพาะ JPG/PNG',
  /* ---- บัญชี / การตั้งค่า ---- */
  deposit_account_not_found: 'ไม่พบบัญชีรับเงินที่เลือก',
  withdraw_destination_not_configured: 'ยังไม่ได้ตั้งค่าบัญชีสำหรับโอนออก',
  marketing_org_not_found: 'ไม่พบสายการตลาดนี้',
  prediction_not_found: 'ไม่พบรายการทายผลนี้',
  /* ---- โดเมน ---- */
  too_many_domains: 'ใส่โดเมนได้ไม่เกิน 30 รายการ',
  primary_not_in_list: 'โดเมนหลักต้องอยู่ในรายการโดเมนด้วย',
  role_domain_not_in_list: 'โดเมนที่กำหนดให้ตำแหน่งนี้ ต้องอยู่ในรายการโดเมนด้วย',
  domain_manage_denied: 'ไม่มีสิทธิ์จัดการโดเมน',
  admin_domain_not_allowed: 'โดเมนนี้ไม่ได้รับอนุญาตให้เข้าหลังบ้าน',
  /* ---- เจ้าของระบบ ---- */
  owner_username_required: 'กรุณากรอกชื่อผู้ใช้ของเจ้าของระบบ',
  owner_username_invalid: 'ชื่อผู้ใช้ของเจ้าของระบบไม่ถูกต้อง',
  owner_password_required: 'กรุณากรอกรหัสผ่านของเจ้าของระบบ',
  owner_password_too_short: 'รหัสผ่านสั้นเกินไป',
  /* ---- ค่ายเกม / บริการภายนอก ---- */
  ufa_not_configured: 'ยังไม่ได้ตั้งค่า UFA',
  rb7_not_configured: 'ยังไม่ได้ตั้งค่า RB7',
  oneallsport_not_configured: 'ยังไม่ได้ตั้งค่า ONEALLSPORT',
  invalid_sms_key: 'SMS Key ไม่ถูกต้อง',
  sms_key_server_unavailable: 'เชื่อมต่อเซิร์ฟเวอร์ SMS ไม่ได้ กรุณาลองใหม่',
  invalid_admin_key: 'ADMIN API KEY ไม่ถูกต้อง',
  admin_already_exists: 'มีผู้ดูแลระบบอยู่แล้ว',
  account_suspended: 'บัญชีนี้ถูกระงับการใช้งาน',
  unauthorized: 'กรุณาเข้าสู่ระบบอีกครั้ง',
  invalid_token: 'เซสชันหมดอายุ กรุณาเข้าสู่ระบบใหม่',
  invalid_sms_secret: 'SMS Secret ไม่ถูกต้อง',
  invalid_callback_key: 'Callback Key ไม่ถูกต้อง',
  forbidden: 'ไม่มีสิทธิ์ดำเนินการนี้',
  admin_only: 'สงวนสิทธิ์เฉพาะผู้ดูแลระบบ',
  owner_only: 'สงวนสิทธิ์เฉพาะผู้มีสิทธิ์สูงสุด',
  page_permission_denied: 'ไม่มีสิทธิ์เข้าหน้านี้',
  setting_permission_denied: 'ไม่มีสิทธิ์แก้ไขการตั้งค่านี้',
  impersonate_member_permission_denied: 'ไม่มีสิทธิ์เข้าสู่ระบบแทนสมาชิก',
  admin_2fa_required: 'กรุณายืนยัน 2FA ก่อนใช้งาน',
  invalid_2fa_code: 'รหัส 2FA ไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง',
  invalid_2fa_challenge: 'รายการยืนยัน 2FA หมดอายุ กรุณาเข้าสู่ระบบใหม่',
  admin_2fa_not_configured: 'บัญชียังไม่ได้ตั้งค่า 2FA',
  invalid_username: 'ชื่อผู้ใช้ไม่ถูกต้อง',
  admin_dev_username_missing: 'กรุณากรอก username ของยูส DEV',
  admin_dev_only: 'ไม่มีสิทธิ์ทำรายการนี้',
  username_reserved_for_env_admin_dev: 'ชื่อผู้ใช้นี้ใช้ไม่ได้ กรุณาใช้ชื่ออื่น',
  username_already_exists: 'ชื่อผู้ใช้นี้ถูกใช้งานแล้ว',
  password_min_8: 'รหัสผ่านต้องมีอย่างน้อย 8 ตัวอักษร',
  password_min_6: 'รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร',
  password_too_short: 'รหัสผ่านสั้นเกินไป',
  invalid_phone: 'เบอร์โทรศัพท์ไม่ถูกต้อง',
  phone_required: 'กรุณากรอกเบอร์โทรศัพท์',
  phone_already_registered: 'เบอร์โทรศัพท์นี้ถูกใช้งานแล้ว',
  user_not_found: 'ไม่พบสมาชิกนี้',
  wallet_not_found: 'ไม่พบกระเป๋าเงินของสมาชิก',
  admin_not_found: 'ไม่พบผู้ดูแลระบบนี้',
  owner_password_dev_only: 'รหัสผ่านของยูส Admin แรกเปลี่ยนได้เฉพาะเจ้าของยูสเท่านั้น',
  owner_permissions_dev_only: 'ไม่สามารถแก้สิทธิ์ของยูส Admin แรกได้',
  owner_permissions_required: 'ต้องเหลือสิทธิ์อย่างน้อย 1 รายการ',
  cannot_delete_self: 'ไม่สามารถลบบัญชีของตัวเองได้',
  admin_has_all_permissions: 'บัญชีนี้ได้รับสิทธิ์ครบทุกหน้า',
  invalid_amount: 'จำนวนเงินไม่ถูกต้อง',
  missing_fields: 'กรุณากรอกข้อมูลให้ครบถ้วน',
  missing_required_fields: 'กรุณากรอกข้อมูลที่จำเป็นให้ครบถ้วน',
  validation_error: 'ข้อมูลไม่ถูกต้อง กรุณาตรวจสอบอีกครั้ง',
  no_fields: 'ไม่มีข้อมูลให้บันทึก',
  nothing_to_update: 'ไม่มีข้อมูลที่เปลี่ยนแปลง',
  need_filter: 'กรุณาเลือกเงื่อนไขก่อนดำเนินการ',
  need_at_least_one_field: 'กรุณากรอกข้อมูลอย่างน้อย 1 ช่อง',
  empty_ids: 'กรุณาเลือกรายการก่อนดำเนินการ',
  empty_items: 'ไม่มีรายการให้บันทึก',
  too_many_items: 'จำนวนรายการมากเกินไป',
  not_found: 'ไม่พบข้อมูลนี้',
  promotion_not_found: 'ไม่พบโปรโมชั่นนี้',
  code_not_found: 'ไม่พบโค้ดนี้',
  batch_not_found: 'ไม่พบชุดคูปองนี้',
  withdrawal_not_found: 'ไม่พบรายการถอนเงินนี้',
  withdrawal_not_found_or_processed: 'ไม่พบรายการถอน หรือรายการถูกดำเนินการแล้ว',
  payout_account_required: 'กรุณาเลือกบัญชีที่ใช้โอนก่อนอนุมัติ',
  tmnwd_account_required: 'กรุณาเลือกบัญชี TMNWD (TrueWallet) ที่จะใช้โอน',
  invalid_payout_account: 'บัญชีถอนที่เลือกไม่ถูกต้อง',
  only_paid_withdrawals: 'แก้ไขได้เฉพาะรายการถอนที่จ่ายแล้วหรืออนุมัติแล้ว',
  already_corrected_once: 'รายการนี้เคยแก้ไขบัญชีแล้ว',
  deposit_not_found: 'ไม่พบรายการฝากเงินนี้',
  game_not_found: 'ไม่พบเกมนี้',
  image_url_too_long: 'ลิงก์รูปภาพยาวเกินไป',
  invalid_image_data: 'ข้อมูลรูปภาพไม่ถูกต้อง',
  unsupported_image_type: 'รองรับเฉพาะไฟล์รูปภาพ PNG, JPG, WEBP หรือ GIF',
  empty_image: 'ไฟล์รูปภาพว่างเปล่า',
  image_too_large_max_2mb:  'ไฟล์รูปต้องไม่เกิน 10MB',
  image_too_large_max_10mb: 'ไฟล์รูปต้องไม่เกิน 10MB',
  provider_required: 'กรุณาเลือกผู้ให้บริการ',
  amb_endpoint_not_supported: 'ปลายทาง AMB นี้ยังไม่รองรับ',
  askmepay_not_configured: 'ยังไม่ได้ตั้งค่าผู้ให้บริการฝากเงิน',
  line_login_not_configured: 'ยังไม่ได้ตั้งค่า LINE Login API',
  line_login_invalid_callback: 'ข้อมูลตอบกลับจาก LINE ไม่ถูกต้อง',
  line_login_state_invalid: 'รายการเชื่อมต่อ LINE หมดอายุ กรุณาลองใหม่',
  line_login_token_failed: 'เชื่อมต่อ LINE ไม่สำเร็จ กรุณาตรวจสอบ API',
  line_login_profile_failed: 'ดึงข้อมูล LINE ไม่สำเร็จ กรุณาลองใหม่',
  tmnone_not_configured: 'ยังไม่ได้ตั้งค่า TMNOne',
  name_required: 'กรุณากรอกชื่อ',
  slug_required: 'กรุณากรอก slug',
  channel_not_found: 'ไม่พบช่องทางนี้',
  bank_code_required: 'กรุณาเลือกธนาคาร',
  bank_account_required: 'กรุณากรอกเลขบัญชี',
  user_session_required: 'กรุณาเข้าสู่ระบบด้วยบัญชีผู้ดูแล',
  invalid_ip: 'รูปแบบ IP ไม่ถูกต้อง',
  ip_not_found: 'ไม่พบ IP นี้',
  no_active_turnover: 'สมาชิกไม่มีเทิร์นค้างอยู่',
  duplicate_bank_account: 'เลขบัญชีนี้ซ้ำกับสมาชิกอื่นที่เปิดใช้งานอยู่',
  /* ---- เพิ่มสำหรับหลังบ้านใหม่ ---- */
  slip_already_used: 'สลิปนี้เคยใช้แล้ว',
  deposit_already_used: 'รายการฝากนี้ถูกใช้ไปแล้ว',
  bank_account_already_registered: 'เลขบัญชีนี้มีสมาชิกใช้แล้ว',
  turnover_not_cleared: 'ยังทำเทิร์นไม่ครบ',
  autopeer_not_configured: 'ยังไม่ได้ตั้งค่า Autopeer',
  wallet_account_not_configured: 'ยังไม่ได้ตั้งค่าบัญชีวอเลท',
  phone_not_registered: 'ไม่พบเบอร์นี้ในระบบ',
  otp_rate_limited: 'ขอ OTP บ่อยเกินไป กรุณารอสักครู่',
  promo_limit_reached: 'โปรโมชั่นนี้ครบจำนวนแล้ว',
  invalid_login: 'ข้อมูลเข้าสู่ระบบไม่ถูกต้อง',
  slip_expired: 'สลิปหมดอายุแล้ว',
  slip_service_error: 'ระบบตรวจสลิปขัดข้อง กรุณาลองใหม่',
  slip_service_not_configured: 'ยังไม่ได้ตั้งค่าระบบตรวจสลิป',
  request_not_found: 'ไม่พบรายการนี้',
  ufa_disabled: 'ปิดใช้งาน UFA อยู่',
  tmnone_disabled: 'ปิดใช้งาน TMNOne อยู่',
  smskub_not_configured: 'ยังไม่ได้ตั้งค่า SMS-KUB',
  invalid_minutes: 'จำนวนนาทีไม่ถูกต้อง',
  admin_session_expired: 'เซสชันหมดอายุ กรุณาเข้าสู่ระบบใหม่',
  error: 'ทำรายการไม่สำเร็จ',
  internal_error: 'ระบบขัดข้อง กรุณาลองใหม่อีกครั้ง',
};
const NX_ERROR_STATUS_TH = {
  0: 'เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ ตรวจสอบอินเทอร์เน็ตแล้วลองใหม่',
  404: 'ไม่พบเมนูนี้บนเซิร์ฟเวอร์ (404) — ลองรีเฟรชหน้าเว็บ',
  413: 'ไฟล์หรือข้อมูลใหญ่เกินไป',
  429: 'ทำรายการถี่เกินไป กรุณารอสักครู่แล้วลองใหม่',
  502: 'เซิร์ฟเวอร์ไม่ตอบสนอง (502) กรุณาลองใหม่',
  503: 'เซิร์ฟเวอร์ไม่พร้อมใช้งานชั่วคราว (503) กรุณาลองใหม่',
  504: 'เซิร์ฟเวอร์ตอบช้าเกินไป (504) กรุณาลองใหม่',
};
/* error ล่าสุดจาก api() — ให้ toast รู้ว่าข้อความไหนเป็น error (แสดงสีแดง) แม้หน้าต่าง ๆ จะเรียก toast(e.message) เฉย ๆ */
let NX_LAST_API_ERR = { msg: "", at: 0 };

function nxErrorText(raw) {
  const msg = String(raw == null ? "" : raw).trim();
  if (!msg) return "ทำรายการไม่สำเร็จ";
  if (NX_ERROR_TH[msg]) return NX_ERROR_TH[msg];
  const key = msg.split(":")[0].trim();
  if (NX_ERROR_TH[key]) return NX_ERROR_TH[key] + " (" + msg.slice(msg.indexOf(":") + 1).trim() + ")";
  if (/^[a-z][a-z0-9_]*$/.test(key)) return "ทำรายการไม่สำเร็จ (รหัส: " + msg + ")";
  return msg;
}

function nxIsErrorMessage(raw) {
  const msg = String(raw == null ? "" : raw);
  if (NX_LAST_API_ERR.msg && msg === NX_LAST_API_ERR.msg && Date.now() - NX_LAST_API_ERR.at < 10000) return true;
  if (NX_ERROR_TH[msg] || NX_ERROR_TH[msg.split(":")[0]]) return true;
  return /ไม่สำเร็จ|ผิดพลาด|ขัดข้อง|ไม่มีสิทธิ์|ไม่พบ|ซ้ำกับ|ไม่ถูกต้อง|ล้มเหลว|ไม่ได้ตั้งค่า|internal_error|error|failed|denied/i.test(msg);
}

function nxApiErrorMessage(status, data) {
  const raw = data && (data.error || data.message);
  const ref = data && data.ref ? " (รหัสอ้างอิง " + data.ref + " — ส่งรหัสนี้ให้ทีมงาน)" : "";
  if (raw === "internal_error") return "ระบบขัดข้อง" + (ref || " กรุณาลองใหม่อีกครั้ง");
  if (raw) return raw + ref;
  return NX_ERROR_STATUS_TH[status] || ("ทำรายการไม่สำเร็จ (HTTP " + status + ")");
}

function nxApiError(status, data) {
  const e = new Error(nxApiErrorMessage(status, data));
  e.status = status;
  e.data = data;
  NX_LAST_API_ERR = { msg: e.message, at: Date.now() };
  return e;
}


/* ---------- สิทธิ์ (perm) — จาก /permissions/me ---------- */
let NX_PERMS = { set: new Set(), isSuper: false, isOwner: false };
/* owner ไม่ข้ามสิทธิ์แล้ว — backend ส่งรายการสิทธิ์จริงของ owner มาให้ใน /permissions/me · key เป็น array = มีข้อใดข้อหนึ่งก็พอ */
function can(key) { if (!key) return true; if (Array.isArray(key)) return key.some(k => can(k)); if (NX_PERMS.isSuper) return true; return NX_PERMS.set.has(key); }
/* เมนู API Game / API ฝาก เห็นได้เมื่อมีสิทธิ์ของแท็บใดแท็บหนึ่ง — ใช้ร่วมกันทั้งเมนูข้าง, การเข้าหน้า และแท็บในหน้า */
const NX_API_GAME_PERMS = ["settings_agent","settings_ufa","settings_oneallsport","settings_rb7","settings_pghard"];
const NX_API_DEPOSIT_PERMS = ["settings_truewallet","settings_tmnone","settings_webhook"];
const NX_API_DEPOSIT_SPECIAL_PERMS = ["settings_deposit_special"];
function canAny(keys) { return (keys||[]).some(k => can(k)); }
const NX_PAGES = new Set(["home","overview","members","deposits","withdrawals","bankreceive","bankpayout","bankcentral","logs","fraud","manualreport","suspicious","topdep","wheelhist","referralhist","cashbackhist","otplog","smshist","allhist","bonushist","predhist","promoreq","profitloss","promosummary","affiliate","affiliatehist","codes","checkin","gamemanager","gamesort","sitesettings","admins","promo","prediction","mktreport","mktorgs","mktlinks","domains","notifications","popupcontact","providerlogos","featuredproviders","featuredgames","memberlevels","wheel","cashback","whitelabel","serverhealth","apigame","apideposit","apidepositspecial","apisms","leaderboard","providerreports","staffkpi","creditmanage","slipapprove","autopeer","payment","depositchannels","cryptodeposit","abdconsole","lineconnect"]);

/* ---------- helper ---------- */
const baht = (satang) => "฿" + (Number(satang || 0) / 100).toLocaleString("th-TH", { maximumFractionDigits: 2 });
const initials = (n) => String(n || "?").trim().slice(0, 1) || "?";
const AVA = ["#4F46E5","#0EA5E9","#059669","#D97706","#DB2777","#7C3AED","#0891B2","#DC2626"];
const avc = (s) => AVA[((String(s||"?").charCodeAt(0) || 0) + String(s||"").length) % AVA.length];
/* พอร์ตจาก _bankLogoSlug ของแอดมินเดิม — โลโก้จริงที่ /assets/banks/<slug>.png */
const BANK_ALIASES = {
  "002":"bbl","01002":"bbl",bbl:"bbl","004":"kbank","01004":"kbank",kbank:"kbank",
  "006":"ktb","01006":"ktb",ktb:"ktb","011":"ttb","01011":"ttb",ttb:"ttb",tmb:"tmb",tbank:"tbank",
  "014":"scb","01014":"scb",scb:"scb",sc:"sc","025":"bay","01025":"bay",bay:"bay",
  "069":"kk","01069":"kk",kk:"kk",kkp:"kk","022":"cimb","01022":"cimb",cimb:"cimb",
  "067":"tisco","01067":"tisco",tisco:"tisco","024":"uob","01024":"uob",uob:"uob",
  "071":"tcrb","01071":"tcrb",tcrb:"tcrb","073":"lhb","01073":"lhb",lh:"lhb",lhb:"lhb",lhbank:"lhb",
  "070":"icbc","01070":"icbc",icbc:"icbc","034":"baac","01034":"baac",baac:"baac",
  "030":"gsb","01030":"gsb",gsb:"gsb","033":"ghb","01033":"ghb",ghb:"ghb",
  "066":"ibank","01066":"ibank",isbt:"ibank",ibank:"ibank",bcel:"bcel",truewallet:"truewallet",
};
const BANK_NAME = { kbank:"กสิกรไทย", scb:"ไทยพาณิชย์", ktb:"กรุงไทย", bbl:"กรุงเทพ", bay:"กรุงศรี", gsb:"ออมสิน", ttb:"ทีทีบี", tmb:"ทีเอ็มบี", tbank:"ธนชาต", kk:"เกียรตินาคิน", cimb:"ซีไอเอ็มบี", tisco:"ทิสโก้", uob:"ยูโอบี", tcrb:"ไทยเครดิต", lhb:"แลนด์ฯ", icbc:"ไอซีบีซี", baac:"ธ.ก.ส.", ghb:"อาคารสงเคราะห์", ibank:"อิสลาม", sc:"สแตนดาร์ด", bcel:"BCEL", truewallet:"ทรูวอลเล็ท" };
const BANK_COLOR = { kbank:"#138F2D", scb:"#4E2A84", ktb:"#00A1E0", bbl:"#1E4598", bay:"#FEC70D", gsb:"#EB198D", ttb:"#1279BE", tmb:"#1279BE", tbank:"#F7941E", kk:"#635052", cimb:"#7E1916", tisco:"#12246A", uob:"#005EB8", tcrb:"#00A950", lhb:"#6D6E71", icbc:"#C8102E", baac:"#4B9B1D", ghb:"#F26A24", ibank:"#1A7F5A", truewallet:"#FF6A00" };
function bankSlug(code) { const raw = String(code||"").trim().toLowerCase(); return BANK_ALIASES[raw] || raw || ""; }
function bankName(code) { return BANK_NAME[bankSlug(code)] || (code ? String(code) : "ธนาคาร"); }
function bankColor(code) { return BANK_COLOR[bankSlug(code)] || "#8A93A3"; }
const TRUEWALLET_LOGO_URL = "/admin/assets/unnamed.png";
function BankLogo({ code, size=26 }) {
  const [err, setErr] = useState(false);
  const slug = bankSlug(code);
  if (slug === "truewallet" || String(code||"").toLowerCase() === "truewallet") {
    if (err) return <span className="grid shrink-0 place-items-center rounded-md text-[8px] font-bold text-white" style={{width:size,height:size,background:"#FF6A00"}}>TW</span>;
    return <img src={TRUEWALLET_LOGO_URL} onError={()=>setErr(true)} alt="TrueWallet" title="ทรูวอลเล็ท"
      className="shrink-0" style={{width:size,height:size,objectFit:"contain",background:"#fff",borderRadius:6,padding:2}}/>;
  }
  if (err || !slug) { const c = bankColor(code);
    return <span className="grid shrink-0 place-items-center rounded-md" style={{width:size,height:size,background:c+"22"}}><span className="h-2 w-2 rounded-full" style={{background:c}}/></span>; }
  return <img src={"/assets/banks/"+slug+".png"} onError={()=>setErr(true)} alt={bankName(code)} title={bankName(code)}
    className="shrink-0" style={{width:size,height:size,objectFit:"contain",background:"#fff",borderRadius:6,padding:2}}/>;
}
function last7() {
  const fmt = new Intl.DateTimeFormat("en-CA", { timeZone: "Asia/Bangkok" });
  const out = [];
  for (let i = 6; i >= 0; i--) out.push(fmt.format(new Date(Date.now() - i * 86400000)));
  return out;
}
const DAYS7 = last7();

/* ---------- ไอคอน ---------- */
const Ic = ({ d, size=18, sw=1.7, cls="" }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={sw}
       strokeLinecap="round" strokeLinejoin="round" className={cls} aria-hidden="true">
    {(Array.isArray(d)?d:[d]).map((p,i)=><path key={i} d={p} />)}
  </svg>
);
const I = {
  grid:["M4 4h7v7H4z","M13 4h7v7h-7z","M13 13h7v7h-7z","M4 13h7v7H4z"],
  search:["M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16","M21 21l-4.3-4.3"],
  bell:["M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9","M13.7 21a2 2 0 0 1-3.4 0"],
  sun:["M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10","M12 1v2","M12 21v2","M4.2 4.2l1.4 1.4","M18.4 18.4l1.4 1.4","M1 12h2","M21 12h2","M4.2 19.8l1.4-1.4","M18.4 5.6l1.4-1.4"],
  moon:["M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"], mon:["M3 4h18v12H3z","M8 20h8","M12 16v4"],
  filter:["M3 5h18","M6 12h12","M10 19h4"], plus:["M12 5v14","M5 12h14"], x:["M6 6l12 12","M18 6L6 18"],
  chevR:["M9 6l6 6-6 6"], chevD:["M6 9l6 6 6-6"], check:["M5 12l5 5L20 6"], corner:["M9 6l-4 6 4 6"],
  logout:["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4","M16 17l5-5-5-5","M21 12H9"],
  refresh:["M20 11a8 8 0 1 0-2 5","M20 5v6h-6"], ban:["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18","M5.6 5.6l12.8 12.8"],
  coins:["M8 14a5 5 0 1 0 0-10 5 5 0 0 0 0 10","M16 20a5 5 0 1 0 0-10 5 5 0 0 0 0 10"],
  eye:["M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z","M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6"], eyeOff:["M2 12s3.5-7 10-7c1.7 0 3.2.4 4.6 1","M22 12s-3.5 7-10 7c-1.8 0-3.4-.4-4.8-1.1","M3 3l18 18","M9.9 9.9a3 3 0 0 0 4.2 4.2"],
  lock:["M6 11h12v10H6z","M8 11V7a4 4 0 0 1 8 0v4"],
  copy:["M9 9h10v11H9z","M5 15H4V4h11v1"], edit:["M12 20h9","M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"],
  down:["M12 4v12","M8 12l4 4 4-4","M4 20h16"], back:["M15 18l-6-6 6-6"], warn2:["M12 9v4","M12 17h.01","M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"],
  up:["M12 19V5","M6 11l6-6 6 6"], dn:["M12 5v14","M6 13l6 6 6-6"], dep:["M12 3v12","M7 10l5 5 5-5","M5 21h14"], wdr:["M12 15V3","M7 8l5-5 5 5","M5 21h14"], people:["M17 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1","M10 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7","M21 20v-1a4 4 0 0 0-3-3.9"], dot:["M12 12h.01"],
  chart:["M4 19V5","M4 19h16","M8 15l3-4 3 2 4-6"], clock:["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18","M12 7v5l3 2"],
  key:["M15.5 7.5a4 4 0 1 0-4 4h.4v2.1h2.1V16h2.5v-2.6l-1.6-1.6","M11.5 11.5 4 19v2h2v-2h2v-2h2"], login2:["M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4","M10 17l5-5-5-5","M15 12H3"], star:["M12 3.2l2.7 5.5 6 .9-4.35 4.25 1.03 6-5.38-2.83-5.38 2.83 1.03-6L3.3 9.6l6-.9z"], person:["M16 20v-1a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v1","M12 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7"],
  trash:["M4 7h16","M6 7l1 13h10l1-13","M9 7V4h6v3","M10 11v6","M14 11v6"], broom:["M19 5l-7 7","M14 6l4 4","M9.5 10.5 3 17l4 4 6.5-6.5","M7 21l-4-4"], users:["M16 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1","M9.5 11a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7","M21 20v-1a4 4 0 0 0-3-3.87","M16.5 4.13a4 4 0 0 1 0 7.75"], gift:["M20 12v8H5v-8","M3 8h18v4H3z","M12 8v12","M12 8S9.5 3 7 4.5 8 8 12 8Z","M12 8s2.5-5 5-3.5S16 8 12 8Z"], game:["M8 11h3","M9.5 9.5v3","M15.5 11h.01","M17.5 13h.01","M17 8H7a4 4 0 0 0-4 4 4 4 0 0 0 4 4c1.3 0 2-1 3-1h4c1 0 1.7 1 3 1a4 4 0 0 0 4-4 4 4 0 0 0-4-4z"], wheel:["M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18","M12 3v18","M3 12h18","M5.6 5.6l12.8 12.8","M18.4 5.6L5.6 18.4"],
};
const DUO = {
  home:(<><path d="M4 11.2 12 4.5l8 6.7V19a1.2 1.2 0 0 1-1.2 1.2H5.2A1.2 1.2 0 0 1 4 19z" fill="currentColor" opacity=".28"/><path d="M2.6 11.6 12 3.7l9.4 7.9" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/><rect x="10" y="13.5" width="4" height="6.7" rx=".7" fill="currentColor"/></>),
  grid:(<><rect x="2.5" y="3.5" width="19" height="17" rx="3.5" fill="currentColor" opacity=".28"/><rect x="6" y="12" width="2.6" height="5" rx="1.1" fill="currentColor"/><rect x="10.7" y="8.8" width="2.6" height="8.2" rx="1.1" fill="currentColor"/><rect x="15.4" y="6.4" width="2.6" height="10.6" rx="1.1" fill="currentColor"/></>),
  chart:(<><rect x="4.5" y="4" width="15" height="17" rx="2.6" fill="currentColor" opacity=".28"/><rect x="8.3" y="2.6" width="7.4" height="3.8" rx="1.6" fill="currentColor"/><rect x="7.6" y="10" width="8.8" height="1.8" rx=".9" fill="currentColor"/><rect x="7.6" y="13.6" width="6" height="1.8" rx=".9" fill="currentColor"/></>),
  clock:(<><circle cx="12" cy="12" r="8.6" fill="currentColor" opacity=".28"/><circle cx="12" cy="12" r="8.6" fill="none" stroke="currentColor" strokeWidth="1.7"/><path d="M12 7v5.2l3.6 2.1" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></>),
  users:(<><path d="M4.8 20a7.2 7.2 0 0 1 14.4 0v.4H4.8z" fill="currentColor" opacity=".28"/><circle cx="12" cy="8.2" r="3.7" fill="currentColor"/></>),
  money:(<><rect x="3.8" y="4.2" width="16.4" height="15.6" rx="2.4" fill="currentColor" opacity=".28"/><rect x="7.3" y="8" width="9.4" height="1.8" rx=".9" fill="currentColor"/><rect x="7.3" y="11.6" width="6.2" height="1.8" rx=".9" fill="currentColor"/><path d="M14.5 15.2h3.2M16.1 13.6v3.2" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></>),
  bank:(<><rect x="3" y="10.2" width="18" height="9" rx="1.2" fill="currentColor" opacity=".28"/><path d="M12 3.2l9.2 5.4a.9.9 0 0 1-.5 1.7H3.3a.9.9 0 0 1-.5-1.7z" fill="currentColor"/><rect x="6" y="11.4" width="1.9" height="6" rx=".6" fill="currentColor"/><rect x="11.05" y="11.4" width="1.9" height="6" rx=".6" fill="currentColor"/><rect x="16.1" y="11.4" width="1.9" height="6" rx=".6" fill="currentColor"/><rect x="4.5" y="18.4" width="15" height="1.8" rx=".7" fill="currentColor"/></>),
  game:(<><rect x="2.5" y="7.8" width="19" height="9.4" rx="4.7" fill="currentColor" opacity=".28"/><rect x="6" y="11.7" width="4.2" height="1.6" rx=".8" fill="currentColor"/><rect x="7.3" y="10.4" width="1.6" height="4.2" rx=".8" fill="currentColor"/><circle cx="16" cy="11.4" r="1.15" fill="currentColor"/><circle cx="18.1" cy="13.5" r="1.15" fill="currentColor"/></>),
  mega:(<><path d="M4 10v4l3 .6V9.4z" fill="currentColor" opacity=".28"/><path d="M7 9.4l9.5-3.6v12.4L7 14.6z" fill="currentColor"/><path d="M18 10.2a2.4 2.4 0 0 1 0 3.6" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round"/></>),
  wheel:(<><circle cx="12" cy="12" r="8.8" fill="currentColor" opacity=".28"/><circle cx="12" cy="12" r="8.8" fill="none" stroke="currentColor" strokeWidth="1.8"/><path d="M12 3.4v17.2M3.4 12h17.2M6 6l12 12M18 6 6 18" stroke="currentColor" strokeWidth="1.3"/><circle cx="12" cy="12" r="2.3" fill="currentColor"/></>),
  gift:(<><rect x="4.2" y="10.4" width="15.6" height="9.8" rx="1.6" fill="currentColor" opacity=".28"/><rect x="3" y="7.4" width="18" height="3.8" rx="1.1" fill="currentColor"/><rect x="10.8" y="7.4" width="2.4" height="12.8" fill="currentColor"/><path d="M12 7.4C10.2 4.4 6.6 5.2 7.6 7.4zM12 7.4C13.8 4.4 17.4 5.2 16.4 7.4z" fill="currentColor"/></>),
  ticket:(<><path d="M4 8.5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2 1.6 1.6 0 0 0 0 3.2 1.6 1.6 0 0 0 0 3.2 2 2 0 0 1-2 2H6a2 2 0 0 1-2-2 1.6 1.6 0 0 0 0-3.2 1.6 1.6 0 0 0 0-3.2z" fill="currentColor" opacity=".28"/><path d="M13.5 7v10" stroke="currentColor" strokeWidth="1.7" strokeDasharray="1.6 2.2" strokeLinecap="round"/></>),
  cal:(<><rect x="3.6" y="5" width="16.8" height="15.2" rx="2.6" fill="currentColor" opacity=".28"/><rect x="3.6" y="5" width="16.8" height="4.4" rx="2.6" fill="currentColor"/><rect x="7" y="3" width="1.9" height="4" rx=".9" fill="currentColor"/><rect x="15.1" y="3" width="1.9" height="4" rx=".9" fill="currentColor"/><path d="M9 14.5l2 2 4-4.2" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/></>),
  refund:(<><circle cx="14.5" cy="13.5" r="5.6" fill="currentColor" opacity=".28"/><path d="M9 8 5 12l4 4" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round"/><path d="M5.2 12H13a4.5 4.5 0 0 1 4.5 4.5" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round"/></>),
  cog:(<><path d="M10.6 2.6h2.8l.55 2.2 1.85.77 1.98-1.05 1.98 1.98-1.05 1.98.77 1.85 2.2.55v2.8l-2.2.55-.77 1.85 1.05 1.98-1.98 1.98-1.98-1.05-1.85.77-.55 2.2h-2.8l-.55-2.2-1.85-.77-1.98 1.05-1.98-1.98 1.05-1.98-.77-1.85-2.2-.55v-2.8l2.2-.55.77-1.85L4.84 6.5l1.98-1.98 1.98 1.05 1.85-.77z" fill="currentColor" opacity=".28"/><circle cx="12" cy="12" r="3.5" fill="currentColor"/></>),
  api:(<><rect x="3" y="6" width="7.2" height="12" rx="1.8" fill="currentColor" opacity=".28"/><rect x="12" y="6" width="9" height="5" rx="1.6" fill="currentColor"/><rect x="12" y="13" width="6.4" height="4" rx="1.4" fill="currentColor"/><rect x="5.2" y="9.2" width="2.8" height="1.6" rx=".7" fill="currentColor"/><rect x="5.2" y="12.4" width="2.8" height="1.6" rx=".7" fill="currentColor"/></>),
};
const Duo = ({ name, size=21 }) => (<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">{DUO[name] || DUO.grid}</svg>);

const Badge = ({ tone="info", children }) => (
  <span className="inline-flex items-center gap-1.5 whitespace-nowrap rounded-full px-2 py-0.5 text-xs font-medium" style={{background:`var(--${tone}-soft)`,color:`var(--${tone})`}}>
    <span className="h-1.5 w-1.5 rounded-full" style={{background:`var(--${tone})`}} />{children}
  </span>
);
const IconBtn = ({ title, onClick, children, active }) => (
  <button title={title} onClick={onClick} className={"grid h-9 w-9 place-items-center rounded-lg border transition-colors "+(active?"border-accent/40 bg-accent-soft text-accent":"border-line bg-surface text-fg-2 hover:bg-sunken hover:text-fg")}>{children}</button>
);
function Checkbox({ checked, indeterminate, onChange }) {
  const ref = useRef(null); useEffect(()=>{ if(ref.current) ref.current.indeterminate=!!indeterminate; },[indeterminate]);
  return <input ref={ref} type="checkbox" checked={checked} onChange={onChange}
    className="h-4 w-4 cursor-pointer appearance-none rounded-[5px] border border-line bg-surface checked:border-accent checked:bg-accent"
    style={{backgroundImage:(checked||indeterminate)?"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'><path d='M5 12l5 5L20 6'/></svg>\")":"none",backgroundSize:"12px",backgroundPosition:"center",backgroundRepeat:"no-repeat"}}/>;
}

/* ---------- shared: CSV export + pagination ---------- */
function csvDownload(filename, rows) {
  const esc = (v) => { const s = v==null?"":String(v); return /[",\n]/.test(s) ? '"'+s.replace(/"/g,'""')+'"' : s; };
  const body = rows.map(r => r.map(esc).join(",")).join("\r\n");
  try {
    const blob = new Blob(["﻿"+body], { type:"text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob); const a = document.createElement("a");
    a.href = url; a.download = filename; document.body.appendChild(a); a.click();
    setTimeout(()=>{ document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
  } catch (_) {}
}
function Pager({ page, setPage, total, size, setSize }) {
  const pages = Math.max(1, Math.ceil(total / size));
  const p = Math.min(Math.max(1, page), pages);
  const Btn = ({ d, dis, on }) => (<button disabled={dis} onClick={on} className="grid h-7 w-7 place-items-center rounded-lg border border-line bg-surface text-fg-2 transition-colors hover:bg-sunken disabled:opacity-40"><Ic d={d} size={14}/></button>);
  if (total === 0) return null;
  return (<div className="flex flex-wrap items-center gap-2 border-t border-line px-4 py-2.5 text-xs">
    <span className="text-muted tnum">ทั้งหมด {total.toLocaleString("th-TH")} · หน้า {p}/{pages}</span>
    {setSize && <select value={size} onChange={(e)=>{setSize(Number(e.target.value)); setPage(1);}} className="h-7 rounded-lg border border-line bg-surface px-1.5 text-xs">{[25,50,100,200].map(n=><option key={n} value={n}>{n}/หน้า</option>)}</select>}
    <div className="ml-auto flex items-center gap-1">
      <Btn d={["M11 6l-6 6 6 6","M18 6l-6 6 6 6"]} dis={p<=1} on={()=>setPage(1)}/>
      <Btn d={I.back} dis={p<=1} on={()=>setPage(p-1)}/>
      <Btn d={I.chevR} dis={p>=pages} on={()=>setPage(p+1)}/>
      <Btn d={["M13 6l6 6-6 6","M6 6l6 6-6 6"]} dis={p>=pages} on={()=>setPage(pages)}/>
    </div>
  </div>);
}
const ExportBtn = ({ onClick }) => can("perm_export_excel") ? (<button onClick={onClick} className="flex h-9 items-center gap-1.5 rounded-lg border border-line bg-surface px-3 text-sm font-medium text-fg-2 hover:bg-sunken"><Ic d={I.down} size={16}/> Export</button>) : null;

/* ---------- แถบเครดิต AGENT/PGHARD/UFA/SMS + แอดมินออนไลน์ (ทุกหน้า) ---------- */
const cash = (v)=> v==null ? "—" : Number(v).toLocaleString("th-TH",{minimumFractionDigits:2,maximumFractionDigits:2});
const cashInt = (v)=> v==null ? "—" : Number(v).toLocaleString("th-TH");

/* ---------- แดชบอร์ดภาพรวม (ข้อมูลจริง /summary/period) ---------- */
const bkkDate = (offsetDays=0) => new Intl.DateTimeFormat("en-CA", { timeZone:"Asia/Bangkok" }).format(new Date(Date.now() - offsetDays*86400000));
const RANGES = [["today","วันนี้"],["yesterday","เมื่อวาน"],["7d","7 วัน"],["15d","15 วัน"],["30d","30 วัน"],["month","เดือนนี้"],["custom","กำหนดเอง"]];
function rangeToDates(key, custom) {
  const t = bkkDate(0);
  switch (key) {
    case "yesterday": { const y=bkkDate(1); return {from:y,to:y}; }
    case "7d": return {from:bkkDate(6),to:t};
    case "15d": return {from:bkkDate(14),to:t};
    case "30d": return {from:bkkDate(29),to:t};
    case "month": return {from:t.slice(0,8)+"01",to:t};
    case "custom": return {from:(custom&&custom.from)||t,to:(custom&&custom.to)||t};
    default: return {from:t,to:t};
  }
}
const N = (v)=>Number(v||0);
const money2 = (satang)=>(N(satang)/100).toLocaleString("th-TH",{minimumFractionDigits:2,maximumFractionDigits:2})+" บาท";
const dBaht = (c,p)=>(N(c)-N(p))/100;
const dCnt = (c,p)=>N(c)-N(p);
const CHANNEL_LABEL = { slip:"สลิป (อัพโหลด)", manual:"แมนวล", sms:"SMS", auto:"ออโต้", autopeer:"AutoPeer", askmepay:"AskMePay", truewallet:"ทรูวอลเล็ท", tmnone:"ทรูมันนี่", code:"โค้ด/คูปอง", angpao:"อั่งเปา" };

/* ---------- หน้าหลัก (home) — /home ---------- */
const DEP_STATUS = { success:{t:"สำเร็จ",c:"success"}, approved:{t:"สำเร็จ",c:"success"}, paid:{t:"สำเร็จ",c:"success"}, pending:{t:"รอดำเนินการ",c:"warn"}, processing:{t:"กำลังทำ",c:"warn"}, failed:{t:"ไม่สำเร็จ",c:"danger"}, rejected:{t:"ปฏิเสธ",c:"danger"}, timeout:{t:"หมดเวลา",c:"warn"}, expired:{t:"หมดอายุ",c:"info"} };
const depStatus = (s)=> DEP_STATUS[String(s||"").toLowerCase()] || {t:s||"-",c:"info"};
const fmtDT = (iso)=>{ if(!iso) return "-"; try { return new Intl.DateTimeFormat("th-TH",{timeZone:"Asia/Bangkok",day:"2-digit",month:"2-digit",hour:"2-digit",minute:"2-digit"}).format(new Date(iso)); } catch { return "-"; } };
const slipUrl = (r)=>{ const m=(r&&r.meta)||{}; return m.slip_image_url||m.slipImageUrl||m.image_url||m.imageUrl||""; };

/* ---------- รายการฝาก / รายการถอน — /deposits, /withdrawals ---------- */
const FIN_RANGES = [["","ทั้งหมด"],["today","วันนี้"],["yesterday","เมื่อวาน"],["7d","7 วัน"],["30d","30 วัน"],["month","เดือนนี้"],["custom","กำหนดเอง"]];

/* ---------- บัญชีธนาคาร — /deposit-accounts/stats, /central-accounts ---------- */
const ACCT_TYPE_LABEL = { bank:"ธนาคาร", wallet:"ทรูวอลเล็ท", TMNDepo:"ทรูมันนี่ (ฝาก)", WHdepo:"ทรูวอลเล็ท (WebHook)", TMNWD:"ทรูมันนี่ (ถอน)", LINEAbd:"LINE Connect" };
