/* global React, ReactDOM, Header, Sidebar, BatchList, BatchPicker, BatchDetail, OracleList, OracleDetail, ProductMasterList, PriceHistoryList, AdjList, AdjPicker, AdjDetail, PriceAdjList, PriceAdjPicker, PriceAdjDetail, AdjStatusList, AdjStatusPicker, AdjStatusDetail, AdjOracleList, AdjOracleDetail, ApprovalList, ApprovalForm, CollectionOfferList, CollectionOfferDetail, EatsOfferList, EoCreatePage, ClearanceList, ClearanceDetail, ToastProvider, Icon, Button, ROLES, SEED_FORMS, ADJ_BATCHES, adjRow, PRICE_ADJ_BATCHES, ADJ_STATUS_BATCHES, SEED_FLOWS, CO_MODULE_CONFIG, SEED_COLLECTION_OFFERS, PROMO_STUB_SEEDS, EO_MODULE_CONFIG, SEED_EATS_OFFERS, CL_MODULE_CONFIG, SEED_CLEARANCE_OFFERS, baseSku, useTweaks, TweaksPanel, TweakSection, TweakRadio, TweakToggle, TweakButton, SIDEBAR, LoginPage, ChangePasswordPage, UserList, RoleList, DeptList, LineAssignPage, MailPreviewPage, MainIntroPage, SEED_USERS, AUTH_ROLES, AUTH_ROLE_LABELS, AUTH_ROLE_PERMISSIONS, AUTH_DEPARTMENTS, AUTH_JOB_POSITIONS, SEED_LINE_ASSIGNMENTS, genTempPassword, StockQueryPage, StZoneList, StGradeList, DCatPage, ZONE_MASTER_SEED, STORE_GRADE_SEED, DISPLAY_CAT_SEED, STORE_MASTER, ADJ_STATUS_BATCHES, SOURCES, SEED_MENU_PREVIEWS, MenuPreviewList, MenuPreviewDetail, DeltaFileList, SEED_DELTA_FILES, VendorList, VendorDetail, VendorDeliveryList, emptyVendor, SEED_VENDORS, SEED_VENDOR_DELIVERIES, BasedataList, BD_PAGE_CONFIG, StHoursPage, IncaFileList, INCA_LIST_CONFIG, AuthPermList, PosSettingsList, DisplayItemPage, DisplayMergePage, PromoStubList, PROMO_STUB_CONFIG, SysIncaSettings, SysEffectivePlaceholder, BD_REGION_SEED, BD_STORE_SEED, BD_TIER_SEED, BD_TEMP_SEED, ST_HOURS_SEED, AUTH_PERM_SEED, DISPLAY_ITEM_SEED, DISPLAY_MERGE_SEED, SEED_INCA_CATALOG_FILES, SEED_INCA_PROMO_FILES, SYS_INCA_DEFAULT, isPrototypeSuperUser, canActAsPurchase, canActAsJoanna, canActAsEditor */
// Root app — state container, hash router, tweaks panel

const { useState, useEffect, useMemo } = React;

const TWEAK_DEFAULTS = {
  role: (() => {
    const fromSearch = new URLSearchParams(window.location.search).get("role");
    if (fromSearch) return fromSearch;
    const hash = window.location.hash || "";
    const qIdx = hash.indexOf("?");
    if (qIdx !== -1) {
      return new URLSearchParams(hash.slice(qIdx + 1)).get("role") || "samantha";
    }
    return "samantha";
  })(),
  sidebarCollapsed: false,
  coHasApproval: false,
};

// 將誤放在 hash 的 query（#/product/new?role=samantha）移回 ?role=…#/product/new
const normalizeHashUrl = () => {
  const hash = window.location.hash || "";
  const qIdx = hash.indexOf("?");
  if (qIdx === -1) return;
  const qs = new URLSearchParams(hash.slice(qIdx + 1));
  const cleanHash = hash.slice(0, qIdx);
  const search = new URLSearchParams(window.location.search);
  qs.forEach((v, k) => search.set(k, v));
  const q = search.toString();
  const next = `${window.location.pathname}${q ? `?${q}` : ""}${cleanHash}`;
  history.replaceState(null, "", next);
};

// ── Hash routing helpers ────────────────────────────────────────────────────
// Format: #/<groupId>/<itemId>   or   #/<groupId>/<itemId>/<formId>
// Role 請用 ?role=samantha#/product/new（search）；hash 內 ? 仍會被剝除並正規化
const parseHash = () => {
  let hash = window.location.hash || "#/product/new";
  const qIdx = hash.indexOf("?");
  if (qIdx !== -1) hash = hash.slice(0, qIdx);
  if (hash.startsWith("#/main-intro")) {
    const sub = hash.replace("#/main-intro", "").replace(/^\//, "").split("/")[0]?.split("?")[0] || null;
    return { groupId: "main-intro", itemId: "main-intro", formId: sub || null };
  }
  if (hash.startsWith("#/mail-preview")) return { groupId: "mail-preview", itemId: "mail-preview", formId: null };
  const parts = hash.replace(/^#\//, "").split("/").map(p => p.split("?")[0]);
  const raw = parts[2] || null;
  return { groupId: parts[0] || "product", itemId: parts[1] || "new", formId: raw ? decodeURIComponent(raw) : null };
};
const pushHash = (groupId, itemId, formId) => {
  const seg = formId ? encodeURIComponent(formId) : null;
  const h = seg ? `#/${groupId}/${itemId}/${seg}` : `#/${groupId}/${itemId}`;
  if (window.location.hash !== h) window.location.hash = h;
};

// Look up label from SIDEBAR config
const sidebarLabel = (itemId) => {
  for (const g of SIDEBAR) {
    const item = g.children.find(c => c.id === itemId);
    if (item) return { groupId: g.id, label: item.label };
  }
  return { groupId: "", label: itemId };
};

// ── Placeholder for unbuilt pages ──────────────────────────────────────────
const PROMO_SPEC_IDS          = new Set([]);
const AUTH_SPEC_IDS           = new Set([]);
const MASTER_SPEC_IDS         = new Set([]);
const SYS_SPEC_IDS            = new Set(["sys-effective"]);
const STOCK_SPEC_IDS          = new Set([]);
const DISPLAY_SPEC_IDS        = new Set([]);
const EATS_OFFER_SPEC_IDS     = new Set([]);
const DELTA_SPEC_IDS          = new Set([]);

const Placeholder = ({ itemId }) => {
  const { label } = sidebarLabel(itemId);
  const isPromoSpec         = PROMO_SPEC_IDS.has(itemId);
  const isAuthSpec          = AUTH_SPEC_IDS.has(itemId);
  const isMasterSpec        = MASTER_SPEC_IDS.has(itemId);
  const isSysSpec           = SYS_SPEC_IDS.has(itemId);
  const isStockSpec         = STOCK_SPEC_IDS.has(itemId);
  const isDisplaySpec       = DISPLAY_SPEC_IDS.has(itemId);
  const isEatsOfferSpec     = EATS_OFFER_SPEC_IDS.has(itemId);
  const isDeltaSpec         = DELTA_SPEC_IDS.has(itemId);
  const isSpecPage          = isPromoSpec || isAuthSpec || isMasterSpec || isSysSpec || isStockSpec || isDisplaySpec || isEatsOfferSpec || isDeltaSpec;
  return (
    <>
      <div className="page__head">
        <div>
          <div className="crumb"><span className="cur">{label}</span></div>
          <div className="page__title">{label}</div>
        </div>
      </div>
      <div className="card">
        <div className="empty" style={{ padding: "100px 20px" }}>
          <div className="empty__icon">
            <Icon name={isSpecPage ? "clipboard" : "grid"} size={56} />
          </div>
          <div className="empty__title">
            {isPromoSpec
              ? "版型與「滿額折／滿件折」相同"
              : isAuthSpec
              ? "版型與「部門設定」或「角色存取權限設定」相同"
              : isMasterSpec
              ? "版型與「部門設定」相同（wireframe 9.x 基礎主檔 CRUD）"
              : isSysSpec
              ? "wireframe 10.x 系統設定（待實作）"
              : isStockSpec
              ? "wireframe 4.4 販售時間表（待實作）"
              : isDisplaySpec
              ? "版型與「重複陳列類別」相同"
              : isEatsOfferSpec
              ? "版型與「商品優惠總表 Eatsoffer」相同"
              : isDeltaSpec
              ? "版型與「Delta file 商家價格、庫存檔案 INCA」相同"
              : label}
          </div>
          <div className="empty__desc" style={isSpecPage ? { maxWidth: 520, margin: "0 auto" } : undefined}>
            {isPromoSpec
              ? "欄位規格另以文字補充。有／無審核流程的介面差異，可切換「測試設定 → 審核流程」後至「滿額折／滿件折」對照。"
              : isAuthSpec || isMasterSpec || isSysSpec || isStockSpec || isDisplaySpec || isEatsOfferSpec || isDeltaSpec
              ? "欄位與 flow 依 Figma wireframe；本頁待對齊實作。"
              : "此頁面設計進行中，敬請期待。"}
          </div>
        </div>
      </div>
    </>
  );
};

// ── App ────────────────────────────────────────────────────────────────────
normalizeHashUrl();
const App = () => {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  // Derive initial state from hash
  const initHash = parseHash();
  const [currentGroupId, setCurrentGroupId] = useState(initHash.groupId);
  const [currentItemId, setCurrentItemId] = useState(initHash.itemId);
  const [view, setView] = useState(
    initHash.formId && initHash.itemId !== "a-flow" && initHash.itemId !== "adj-image"
      ? { kind: "detail", formId: initHash.formId }
      : { kind: "list" }
  );

  const [forms, setForms] = useState(SEED_FORMS);
  const [pickerOpen, setPickerOpen] = useState(false);
  const [deleteCandidate, setDeleteCandidate] = useState(null);

  // ── ADJ-A state ──────────────────────────────────────────────────────────
  const [adjBatches, setAdjBatches] = useState(ADJ_BATCHES);
  const [adjPickerOpen, setAdjPickerOpen] = useState(false);
  const [adjDeleteCandidate, setAdjDeleteCandidate] = useState(null);
  const [isDirty, setIsDirty] = useState(false);
  const [pendingNav, setPendingNav] = useState(null);

  const [coSettingOpen, setCoSettingOpen] = useState(false);

  // ── Auth state ───────────────────────────────────────────────────────────
  const [authState,        setAuthState]        = useState("app"); // "login" | "forceChangePassword" | "app"
  const [users,            setUsers]            = useState(SEED_USERS);
  const [authRoles,        setAuthRoles]        = useState(AUTH_ROLES);
  const [authRoleLabels,   setAuthRoleLabels]   = useState(AUTH_ROLE_LABELS);
  const [authRolePerms,    setAuthRolePerms]    = useState(AUTH_ROLE_PERMISSIONS);
  const [loginSuccessBanner, setLoginSuccessBanner] = useState(false);

  const updateUser  = (updated) => setUsers(us => us.map(u => u.id === updated.id ? updated : u));
  const addUser     = (newUser) => setUsers(us => [...us, newUser]);

  const addRole    = (key, label, permissions) => {
    setAuthRoles(rs => [...rs, key]);
    setAuthRoleLabels(ls => ({ ...ls, [key]: label }));
    setAuthRolePerms(ps => ({ ...ps, [key]: permissions }));
  };
  const updateRole = (key, label, permissions) => {
    setAuthRoleLabels(ls => ({ ...ls, [key]: label }));
    setAuthRolePerms(ps => ({ ...ps, [key]: permissions }));
  };
  const deleteRole = (key) => {
    setAuthRoles(rs => rs.filter(r => r !== key));
    setAuthRoleLabels(ls => { const n = { ...ls }; delete n[key]; return n; });
    setAuthRolePerms(ps => { const n = { ...ps }; delete n[key]; return n; });
  };

  const [authDepts, setAuthDepts] = useState(AUTH_DEPARTMENTS);
  const addDept    = (name)             => setAuthDepts(ds => [...ds, name]);
  const updateDept = (oldName, newName) => {
    setAuthDepts(ds => ds.map(d => d === oldName ? newName : d));
    setUsers(us => us.map(u => u.department === oldName ? { ...u, department: newName } : u));
  };
  const deleteDept = (name) => setAuthDepts(ds => ds.filter(d => d !== name));

  const [lineAssignments, setLineAssignments] = useState(SEED_LINE_ASSIGNMENTS);

  const handleLoginSuccess = () => { setLoginSuccessBanner(false); setAuthState("app"); };
  const handleForceChange  = () => { setAuthState("forceChangePassword"); };
  const handlePwChanged    = () => { setLoginSuccessBanner(true); setAuthState("login"); };

  const goMailPreview = (type, userId) => {
    window.location.hash = `#/mail-preview?type=${type}&userId=${userId}`;
  };

  // ── Clearance Offer state ────────────────────────────────────────────────
  const [clearanceOffers, setClearanceOffers] = useState(SEED_CLEARANCE_OFFERS);
  const addClearanceOffer   = (o) => setClearanceOffers(list => [o, ...list]);
  const deleteClearanceOffer = (o) => setClearanceOffers(list => list.filter(x => x.id !== o.id));
  const openClOffer = (o) => setView({ kind: "detail", formId: o.id });

  // ── Zone state ───────────────────────────────────────────────────────────
  const [zones, setZones] = useState(ZONE_MASTER_SEED);

  const handleZoneCreate = ({ zoneName, storeIds, note }) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const today = now.toISOString().slice(0, 10);
    const newZone = { id: `Z-${Date.now()}`, zoneName, storeIds, note, createdAt: today, updatedAt: stamp };
    setZones(prev => {
      const next = [...prev, newZone];
      SOURCES.region = next.map(z => z.zoneName);
      return next;
    });
  };

  const handleZoneUpdate = (id, { zoneName, storeIds, note }) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    setZones(prev => {
      const next = prev.map(z => z.id === id ? { ...z, zoneName, storeIds, note, updatedAt: stamp } : z);
      SOURCES.region = next.map(z => z.zoneName);
      return next;
    });
  };

  const handleZoneDelete = (id) => {
    setZones(prev => {
      const next = prev.filter(z => z.id !== id);
      SOURCES.region = next.map(z => z.zoneName);
      return next;
    });
  };

  // ── Collection Offer state ───────────────────────────────────────────────
  const [collectionOffers, setCollectionOffers] = useState(SEED_COLLECTION_OFFERS);

  // ── Eats Offer state ─────────────────────────────────────────────────────
  const [eatsOffers, setEatsOffers] = useState(SEED_EATS_OFFERS);

  const [editEoItem, setEditEoItem] = useState(null);
  const [viewEoItem, setViewEoItem] = useState(null);
  const deleteEoItem = (item) => setEatsOffers(list => list.filter(x => x.uid !== item.uid));
  const updateEoItem = (updatedItem) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    setEatsOffers(list => list.map(o => o.uid === updatedItem.uid ? { ...updatedItem, updatedAt: stamp } : o));
  };
  const [coDeleteCandidate, setCoDeleteCandidate] = useState(null);

  const openCoCreate = () => setView({ kind: "detail", formId: "__new__" });
  const openCoOffer  = (o) => setView({ kind: "detail", formId: o.id });
  const deleteCoOffer = (o) => setCollectionOffers(list => list.filter(x => x.id !== o.id));
  const toggleCoStatus = (o, status) => setCollectionOffers(list => list.map(x => x.id === o.id ? { ...x, status } : x));
  const saveCoOffer = (updatedOffer, isNew) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const dateStr = now.toISOString().slice(0, 10);
    if (isNew) {
      const nextId = "CO-" + String(collectionOffers.length + 100).slice(-3);
      setCollectionOffers(list => [{ ...updatedOffer, id: nextId, createdAt: dateStr, updatedAt: stamp }, ...list]);
    } else {
      setCollectionOffers(list => list.map(o => o.id === updatedOffer.id ? { ...updatedOffer, updatedAt: stamp } : o));
    }
    setView({ kind: "list" });
  };

  // ── ADJ-B (price-adj) state ───────────────────────────────────────────────
  const [priceAdjBatches, setPriceAdjBatches] = useState(PRICE_ADJ_BATCHES);
  const [priceAdjPickerOpen, setPriceAdjPickerOpen] = useState(false);
  const [priceAdjDeleteCandidate, setPriceAdjDeleteCandidate] = useState(null);

  // ── ADJ-C/D (adj-status) state ────────────────────────────────────────────
  const [adjStatusBatches, setAdjStatusBatches] = useState(ADJ_STATUS_BATCHES);

  // ── 圖片調整 state ────────────────────────────────────────────────────────
  const [imgAdjView,    setImgAdjView]    = useState(
    initHash.itemId === "adj-image" && initHash.formId === "worksheet" ? "worksheet" : "list"
  ); // "list" | "worksheet"
  const [imgAdjInitSkus,setImgAdjInitSkus]= useState(null);  // 從 L1 帶入的 SKUs
  const [imgDrawerSku,  setImgDrawerSku]  = useState(null);  // L2-B 抽屜的 SKU
  const [imgViewDrawerSku, setImgViewDrawerSku] = useState(null); // L2-B 唯讀抽屜

  // ── 菜單預覽 state ────────────────────────────────────────────────────────
  const [menuPreviews, setMenuPreviews] = useState(SEED_MENU_PREVIEWS);
  const [menuPreviewDetail, setMenuPreviewDetail] = useState(null); // preview object or null
  const [menuDeleteCandidate, setMenuDeleteCandidate] = useState(null);

  // ── 廠商基礎資料 state ────────────────────────────────────────────────────
  const [vendors,          setVendors]          = useState(SEED_VENDORS);
  const [vendorDeliveries, setVendorDeliveries] = useState(SEED_VENDOR_DELIVERIES);
  const [vendorDetail,     setVendorDetail]     = useState(null); // null | vendor object | "__new__"

  const handleVendorSaveDraft = (updated, delivs, isNew) => {
    if (isNew) {
      setVendors(prev => [updated, ...prev]);
    } else {
      setVendors(prev => prev.map(v => v.uid === updated.uid ? updated : v));
    }
    setVendorDeliveries(prev => {
      const filtered = prev.filter(d => d.vendorNo !== updated.vendorNo);
      return [...filtered, ...delivs];
    });
    setVendorDetail(updated);
  };

  const handleVendorSubmit = (updated, delivs, isNew) => {
    if (isNew) {
      setVendors(prev => [updated, ...prev]);
    } else {
      setVendors(prev => prev.map(v => v.uid === updated.uid ? updated : v));
    }
    setVendorDeliveries(prev => {
      const filtered = prev.filter(d => d.vendorNo !== updated.vendorNo);
      return [...filtered, ...delivs];
    });
    setVendorDetail(null);
  };

  const handleVendorDelete = (vendor) => {
    setVendors(prev => prev.filter(v => v.uid !== vendor.uid));
    setVendorDeliveries(prev => prev.filter(d => d.vendorNo !== vendor.vendorNo));
  };

  // 無法刪除（已同步 Oracle / 有關聯資料）時的替代動作：改為停止合作
  const handleVendorSuspend = (vendor) => {
    setVendors(prev => prev.map(v => v.uid === vendor.uid ? { ...v, vendorStatus: "suspended" } : v));
  };

  // 廠商配送資料 L1 模組：單列新增/編輯/刪除（與基礎資料 L2 子表共用同一份 vendorDeliveries）
  const handleVendorDelivSaveRow = (row, isNew) => {
    setVendorDeliveries(prev => isNew ? [row, ...prev] : prev.map(d => d.uid === row.uid ? row : d));
  };
  const handleVendorDelivDeleteRow = (uid) => {
    setVendorDeliveries(prev => prev.filter(d => d.uid !== uid));
  };

  // ── Delta File 產出 state ─────────────────────────────────────────────────
  const [deltaFiles, setDeltaFiles] = useState(SEED_DELTA_FILES);
  const [deltaDeleteCandidate, setDeltaDeleteCandidate] = useState(null);

  // ── 基礎主檔 9.x state ────────────────────────────────────────────────────
  const [bdRegions, setBdRegions] = useState(BD_REGION_SEED);
  const [bdStores, setBdStores] = useState(BD_STORE_SEED);
  const [bdTiers, setBdTiers] = useState(BD_TIER_SEED);
  const [bdTemps, setBdTemps] = useState(BD_TEMP_SEED);

  const bdAdd = (setter, prefix) => (form) =>
    setter(prev => [...prev, { id: `${prefix}-${Date.now()}`, ...form }]);
  const bdUpdate = (setter) => (id, form) =>
    setter(prev => prev.map(r => r.id === id ? { ...r, ...form } : r));
  const bdDelete = (setter) => (id) =>
    setter(prev => prev.filter(r => r.id !== id));

  // ── 販售時間表 state ──────────────────────────────────────────────────────
  const [stHoursRows, setStHoursRows] = useState(ST_HOURS_SEED);

  // ── INCA Catalog / Promotion state ────────────────────────────────────────
  const [catalogFiles, setCatalogFiles] = useState(SEED_INCA_CATALOG_FILES);
  const [promoFiles, setPromoFiles] = useState(SEED_INCA_PROMO_FILES);
  const [incaDeleteCandidate, setIncaDeleteCandidate] = useState(null);

  // ── 權限列表 / 職位設定 state ─────────────────────────────────────────────
  const [authPerms, setAuthPerms] = useState(AUTH_PERM_SEED);
  const [authPositions, setAuthPositions] = useState(
    AUTH_JOB_POSITIONS.map((name, i) => ({ id: `pos-${i + 1}`, name }))
  );

  const addAuthPerm = ({ code, name }) =>
    setAuthPerms(prev => [...prev, { id: `ap-${Date.now()}`, code, name }]);
  const updateAuthPerm = (id, { code, name }) =>
    setAuthPerms(prev => prev.map(p => p.id === id ? { ...p, code, name } : p));
  const deleteAuthPerm = (id) =>
    setAuthPerms(prev => prev.filter(p => p.id !== id));

  const addAuthPosition = (name) =>
    setAuthPositions(prev => [...prev, { id: `pos-${Date.now()}`, name }]);
  const updateAuthPosition = (id, name) =>
    setAuthPositions(prev => prev.map(p => p.id === id ? { ...p, name } : p));
  const deleteAuthPosition = (id) =>
    setAuthPositions(prev => prev.filter(p => p.id !== id));

  // ── INCA 排程設定 state ───────────────────────────────────────────────────
  const [sysIncaSettings, setSysIncaSettings] = useState(SYS_INCA_DEFAULT);

  // 同步建立新版本並加入列表，回傳新 preview 物件（進度由 MenuPreviewList 管理）
  const handleMenuGenerate = () => {
    const now = new Date();
    const mm = String(now.getMonth() + 1).padStart(2, "0");
    const dd = String(now.getDate()).padStart(2, "0");
    const hh = String(now.getHours()).padStart(2, "0");
    const mi = String(now.getMinutes()).padStart(2, "0");
    const todayPrefix = mm + dd;
    const sameDay = menuPreviews.filter(p => p.name.startsWith(todayPrefix));
    const suffix = sameDay.length === 0 ? "" : `_v${sameDay.length + 1}`;
    const newPreview = {
      id: `MP-${Date.now()}`,
      name: `${todayPrefix}上傳版本_原始檔${suffix}`,
      createdAt: `${now.getFullYear()}${mm}${dd} ${hh}:${mi}`,
      items: SEED_MENU_PREVIEWS[0].items,
    };
    setMenuPreviews(list => [newPreview, ...list]);
    return newPreview;
  };

  const handleDeltaGenerate = () => {
    const now = new Date();
    const mm = String(now.getMonth() + 1).padStart(2, "0");
    const dd = String(now.getDate()).padStart(2, "0");
    const hh = String(now.getHours()).padStart(2, "0");
    const mi = String(now.getMinutes()).padStart(2, "0");
    const todayPrefix = mm + dd;
    const sameDay = deltaFiles.filter(f => f.name.startsWith(todayPrefix));
    const suffix = sameDay.length === 0 ? "" : `_v${sameDay.length + 1}`;
    const newFile = {
      id: `DF-${Date.now()}`,
      name: `${todayPrefix}_Delta_File${suffix}`,
      createdAt: `${now.getFullYear()}${mm}${dd} ${hh}:${mi}`,
    };
    setDeltaFiles(list => [newFile, ...list]);
  };

  const makeIncaGenerate = (setter, idPrefix, nameSuffix) => () => {
    const now = new Date();
    const mm = String(now.getMonth() + 1).padStart(2, "0");
    const dd = String(now.getDate()).padStart(2, "0");
    const hh = String(now.getHours()).padStart(2, "0");
    const mi = String(now.getMinutes()).padStart(2, "0");
    const todayPrefix = mm + dd;
    setter(prev => {
      const sameDay = prev.filter(f => f.name.startsWith(todayPrefix));
      const suffix = sameDay.length === 0 ? "" : `_v${sameDay.length + 1}`;
      const newFile = {
        id: `${idPrefix}-${Date.now()}`,
        name: `${todayPrefix}_${nameSuffix}${suffix}`,
        createdAt: `${now.getFullYear()}${mm}${dd} ${hh}:${mi}`,
      };
      return [newFile, ...prev];
    });
  };

  const handleCatalogGenerate = makeIncaGenerate(setCatalogFiles, "CF", "Catalog_File");
  const handlePromoFileGenerate = makeIncaGenerate(setPromoFiles, "PF", "Promotion_File");

  // ── 審核流程設定 state ────────────────────────────────────────────────────
  const [flows, setFlows] = useState(SEED_FLOWS);
  const [afCloneSource, setAfCloneSource] = useState(null);
  const [afView, setAfView] = useState(() => { // { kind: "list"|"form", flowId?: string }
    if (initHash.itemId !== "a-flow" || !initHash.formId) return { kind: "list" };
    if (initHash.formId === "new") return { kind: "form", flowId: null };
    return { kind: "form", flowId: initHash.formId };
  });

  // ── 審核流程設定 handlers ──────────────────────────────────────────────────
  const makeStamp = () => {
    const now = new Date();
    return now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
  };
  const makeDate = () => new Date().toISOString().slice(0, 10);

  const handleAfNew     = () => setAfView({ kind: "form", flowId: null });
  const handleAfEdit    = (flow) => setAfView({ kind: "form", flowId: flow.id });
  const handleAfCancel  = () => { setAfView({ kind: "list" }); setAfCloneSource(null); setIsDirty(false); };

  const handleAfSave = (saved, isEdit) => {
    const stamp = makeStamp();
    if (isEdit) {
      setFlows(fs => fs.map(f => f.id === saved.id ? { ...saved, updated: stamp } : f));
    } else {
      const nextNum = String(flows.length + 100).padStart(3, "0").slice(-3);
      const newId = "AF-" + nextNum;
      setFlows(fs => [...fs, { ...saved, id: newId, created: makeDate(), updated: stamp }]);
    }
    setAfView({ kind: "list" });
    setAfCloneSource(null);
    setIsDirty(false);
  };

  const handleAfToggle    = (flow) => setFlows(fs => fs.map(f => f.id === flow.id ? { ...f, enabled: !f.enabled, updated: makeStamp() } : f));
  const handleAfDuplicate = (flow) => {
    setAfCloneSource(flow);
    setAfView({ kind: "form", flowId: null });
  };

  const role = t.role;
  const currentUser = ROLES[role].name.toUpperCase();

  // ── Sidebar notification dots ─────────────────────────────────────────────
  const sidebarNotifications = useMemo(() => {
    const n = {};
    const hasPending  = (batches) => batches.some(b => b.rows.some(r => r.rowStatus === "adjPending"));
    const hasRejected = (batches, user) => batches.some(b => b.applicant === user && b.rows.some(r => r.rowStatus === "adjRejected"));
    if (isPrototypeSuperUser(role) || role === "joanna") {
      n["adj-vendor"] = hasPending(adjBatches);
      n["adj-price"]  = hasPending(priceAdjBatches);
      n["adj-status"] = hasPending(adjStatusBatches);
      n["p-eats"]     = eatsOffers.some(e => e.status === "eoUnderReview");
      n["p-coll"]     = collectionOffers.some(o => o.status === "coUnderReview");
    }
    if (isPrototypeSuperUser(role) || role === "purchase") {
      n["adj-vendor"] = hasRejected(adjBatches, currentUser);
      n["adj-price"]  = hasRejected(priceAdjBatches, currentUser);
      n["adj-status"] = hasRejected(adjStatusBatches, currentUser);
      n["p-coll"]     = collectionOffers.some(o => o.applicant === currentUser && o.status === "coRejected");
      n["p-eats"]     = eatsOffers.some(o => o.applicant === currentUser && o.status === "eoRejected");
    }
    return n;
  }, [role, currentUser, adjBatches, priceAdjBatches, adjStatusBatches, eatsOffers, collectionOffers]);

  // 初次載入：正規化 hash 內誤放的 ?role=，並同步路由 state（避免 itemId 變成 new?role=samantha）
  useEffect(() => {
    normalizeHashUrl();
    const { groupId, itemId, formId } = parseHash();
    setCurrentGroupId(groupId);
    setCurrentItemId(itemId);
    if (itemId === "a-flow") {
      if (!formId) setAfView({ kind: "list" });
      else if (formId === "new") setAfView({ kind: "form", flowId: null });
      else setAfView({ kind: "form", flowId: formId });
      setView({ kind: "list" });
    } else if (itemId === "adj-image") {
      setImgAdjView(formId === "worksheet" ? "worksheet" : "list");
      setView({ kind: "list" });
    } else {
      setView(formId ? { kind: "detail", formId } : { kind: "list" });
    }
  }, []);

  // Sync hash when navigation state changes
  useEffect(() => {
    if (currentGroupId === "main-intro") {
      const sub = view.kind === "detail" ? view.formId : null;
      const h = sub ? `#/main-intro/${sub}` : "#/main-intro";
      if (window.location.hash !== h) window.location.hash = h;
      return;
    }
    let sub = null;
    if (currentItemId === "a-flow") {
      if (afView.kind === "form") sub = afView.flowId || "new";
    } else if (currentItemId === "adj-image") {
      if (imgAdjView === "worksheet") sub = "worksheet";
    } else {
      sub = view.kind === "detail" ? view.formId : null;
    }
    pushHash(currentGroupId, currentItemId, sub);
  }, [currentGroupId, currentItemId, view, afView, imgAdjView]);

  // Browser back/forward
  useEffect(() => {
    const onHashChange = () => {
      normalizeHashUrl();
      const hash = window.location.hash || "";
      if (hash.startsWith("#/main-intro")) {
        const { formId } = parseHash();
        setCurrentGroupId("main-intro");
        setCurrentItemId("main-intro");
        setView(formId ? { kind: "detail", formId } : { kind: "list" });
        return;
      }
      if (hash.startsWith("#/mail-preview")) {
        setCurrentGroupId("mail-preview");
        setCurrentItemId("mail-preview");
        return;
      }
      const { groupId, itemId, formId } = parseHash();
      setCurrentGroupId(groupId);
      setCurrentItemId(itemId);
      if (itemId === "a-flow") {
        if (!formId) setAfView({ kind: "list" });
        else if (formId === "new") setAfView({ kind: "form", flowId: null });
        else setAfView({ kind: "form", flowId: formId });
        setView({ kind: "list" });
      } else if (itemId === "adj-image") {
        setImgAdjView(formId === "worksheet" ? "worksheet" : "list");
        setView({ kind: "list" });
      } else {
        setView(formId ? { kind: "detail", formId } : { kind: "list" });
      }
    };
    window.addEventListener("hashchange", onHashChange);
    return () => window.removeEventListener("hashchange", onHashChange);
  }, []);

  const guardedNav = (action) => {
    if (isDirty) setPendingNav(() => action);
    else action();
  };

  const navigate = (groupId, itemId) => {
    guardedNav(() => {
      setCurrentGroupId(groupId);
      setCurrentItemId(itemId);
      setView({ kind: "list" });
      setAfView({ kind: "list" });
      setIsDirty(false);
    });
  };

  // ── Create flow ────────────────────────────────────────────────────────
  const openCreate = () => setPickerOpen(true);
  const confirmCreate = (batch) => {
    const id = "CF-" + String(forms.length + 100).slice(-3);
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const newForm = {
      id, applicant: currentUser, batch,
      status: "editing", imgStatus: "no", syncStatus: "no",
      updated: stamp, created: stamp, items: [],
    };
    setForms(fs => [newForm, ...fs]);
    setPickerOpen(false);
    setView({ kind: "detail", formId: id });
  };

  const openForm = (form) => guardedNav(() => {
    setView({ kind: "detail", formId: form.id });
    setIsDirty(false);
  });
  const back = () => guardedNav(() => {
    setView({ kind: "list" });
    setIsDirty(false);
  });

  // ── Patch form items ───────────────────────────────────────────────────
  const patchForm = (formId, uid, key, value) => {
    setForms(fs => fs.map(f => {
      if (f.id !== formId) return f;
      let items = f.items;
      if (key === "_add") {
        items = [...items, value];
      } else if (key === "_delete") {
        items = items.filter(it => it.uid !== uid);
      } else if (key === "_replace") {
        items = value.map(it => {
          const next = { ...it };
          const c = +next.cost, l = +next.listPrice;
          if (c > 0 && l > 0) next.marginPct = (((l - c) / l) * 100).toFixed(2) + "%";
          if (next.taxRate && next.minOrder && next.shelfDays) {
            next.specSummary = `${next.taxRate} 最低訂量${next.minOrder} 效期${next.shelfDays}`;
          }
          return next;
        });
      } else {
        items = items.map(it => {
          if (it.uid !== uid) return it;
          const next = { ...it, [key]: value };
          if (key === "cost" || key === "listPrice") {
            const c = +next.cost, l = +next.listPrice;
            if (c > 0 && l > 0) next.marginPct = (((l - c) / l) * 100).toFixed(2) + "%";
          }
          if (key === "mainCat") next.subCat = "";
          // Formula 🔒 specSummary — always recompute from source fields
          if (next.taxRate && next.minOrder && next.shelfDays) {
            next.specSummary = `${next.taxRate} 最低訂量${next.minOrder} 效期${next.shelfDays}`;
          } else {
            next.specSummary = "";
          }
          return next;
        });
      }
      const now = new Date();
      const stamp = now.getFullYear().toString()
        + String(now.getMonth() + 1).padStart(2, "0")
        + String(now.getDate()).padStart(2, "0") + " "
        + String(now.getHours()).padStart(2, "0") + ":"
        + String(now.getMinutes()).padStart(2, "0");
      return { ...f, items, updated: stamp };
    }));
  };

  const submitForm = (formId) => {
    setForms(fs => fs.map(f => f.id === formId
      ? { ...f, status: "submitted", imgStatus: "no", syncStatus: "no" }
      : f));
    setIsDirty(false);
    setView({ kind: "list" });
  };

  const askDelete = (form) => setDeleteCandidate(form);
  const doDelete = () => {
    setForms(fs => fs.filter(f => f.id !== deleteCandidate.id));
    setDeleteCandidate(null);
  };

  // ── ADJ-A callbacks ──────────────────────────────────────────────────────
  const openAdjCreate = () => setAdjPickerOpen(true);
  const confirmAdjCreate = (batchName) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const id = "ADJ-" + String(adjBatches.length + 100).slice(-3);
    const newBatch = { id, batchName, applicant: currentUser, submitted: false,
      createdAt: stamp, updatedAt: stamp, rows: [] };
    setAdjBatches(bs => [newBatch, ...bs]);
    setAdjPickerOpen(false);
    setView({ kind: "detail", formId: id });
  };
  const openAdjBatch = (batch) => guardedNav(() => {
    setView({ kind: "detail", formId: batch.id });
    setIsDirty(false);
  });
  const patchAdjBatch = (batchId, rowUid, key, value) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    setAdjBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (key === "_addRow")    return { ...b, rows: [...b.rows, value], updatedAt: stamp };
      if (key === "_deleteRow") return { ...b, rows: b.rows.filter(r => r.uid !== rowUid), updatedAt: stamp };
      if (key === "_resubmit") {
        // 重新送出：被退回(adjRejected)的列改回「待審核」(adjPending)，重新進入審核
        const rows = b.rows.map(r => r.rowStatus === "adjRejected" ? { ...r, rowStatus: "adjPending", reviewPass: "", rejectReason: "" } : r);
        return { ...b, rows, submitted: true, updatedAt: stamp };
      }
      if (key === "_multiPatch") {
        const rows = b.rows.map(r => r.uid === rowUid ? { ...r, ...value } : r);
        return { ...b, rows, updatedAt: stamp };
      }
      const rows = b.rows.map(r => r.uid === rowUid ? { ...r, [key]: value, updatedAt: stamp } : r);
      return { ...b, rows, updatedAt: stamp };
    }));
  };
  const submitAdjBatch = (batchId) => {
    setAdjBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (b.rows.length === 0) return b;
      const rows = b.rows.map(r => ({ ...r, rowStatus: "adjPending" }));
      return { ...b, rows, submitted: true };
    }));
    setView({ kind: "list" });
  };
  const askDeleteAdj = (batch) => setAdjDeleteCandidate(batch);
  const doDeleteAdj = () => {
    setAdjBatches(bs => bs.filter(b => b.id !== adjDeleteCandidate.id));
    setAdjDeleteCandidate(null);
  };

  // ── Price-adj callbacks ──────────────────────────────────────────────────
  const openPriceAdjCreate = () => setPriceAdjPickerOpen(true);
  const confirmPriceAdjCreate = (batchName) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const id = "PADJ-" + String(priceAdjBatches.length + 100).slice(-3);
    const newBatch = { id, batchName, applicant: currentUser, submitted: false,
      createdAt: stamp, updatedAt: stamp, rows: [] };
    setPriceAdjBatches(bs => [newBatch, ...bs]);
    setPriceAdjPickerOpen(false);
    setView({ kind: "detail", formId: id });
  };
  const openPriceAdjBatch = (batch) => guardedNav(() => {
    setView({ kind: "detail", formId: batch.id });
    setIsDirty(false);
  });
  const patchPriceAdjBatch = (batchId, rowUid, key, value) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    setPriceAdjBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (key === "_addRow")    return { ...b, rows: [...b.rows, value], updatedAt: stamp };
      if (key === "_deleteRow") return { ...b, rows: b.rows.filter(r => r.uid !== rowUid), updatedAt: stamp };
      if (key === "_resubmit") {
        const rows = b.rows.map(r => r.rowStatus === "adjRejected" ? { ...r, rowStatus: "adjPending", reviewPass: "", rejectReason: "" } : r);
        return { ...b, rows, submitted: true, updatedAt: stamp };
      }
      if (key === "_multiPatch") {
        const rows = b.rows.map(r => r.uid === rowUid ? { ...r, ...value } : r);
        return { ...b, rows, updatedAt: stamp };
      }
      const rows = b.rows.map(r => r.uid === rowUid ? { ...r, [key]: value, updatedAt: stamp } : r);
      return { ...b, rows, updatedAt: stamp };
    }));
  };
  const submitPriceAdjBatch = (batchId) => {
    setPriceAdjBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (b.rows.length === 0) return b;
      const rows = b.rows.map(r => ({ ...r, rowStatus: "adjPending" }));
      return { ...b, rows, submitted: true };
    }));
    setView({ kind: "list" });
  };
  const askDeletePriceAdj = (batch) => setPriceAdjDeleteCandidate(batch);
  const doDeletePriceAdj = () => {
    setPriceAdjBatches(bs => bs.filter(b => b.id !== priceAdjDeleteCandidate.id));
    setPriceAdjDeleteCandidate(null);
  };

  // ── adj-status callbacks ──────────────────────────────────────────────────
  const confirmAdjStatusCreate = (batchName) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    const id = "AS-" + String(adjStatusBatches.length + 100).slice(-3);
    const newBatch = { id, batchName, applicant: currentUser, submitted: false,
      createdAt: stamp, updatedAt: stamp, rows: [] };
    setAdjStatusBatches(bs => [newBatch, ...bs]);
    return id;
  };
  const patchAdjStatusBatch = (batchId, rowUid, key, value) => {
    const now = new Date();
    const stamp = now.getFullYear().toString()
      + String(now.getMonth() + 1).padStart(2, "0")
      + String(now.getDate()).padStart(2, "0") + " "
      + String(now.getHours()).padStart(2, "0") + ":"
      + String(now.getMinutes()).padStart(2, "0");
    setAdjStatusBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (key === "_addRow")    return { ...b, rows: [...b.rows, value], updatedAt: stamp };
      if (key === "_deleteRow") return { ...b, rows: b.rows.filter(r => r.uid !== rowUid), updatedAt: stamp };
      if (key === "_resubmit") {
        const rows = b.rows.map(r => r.rowStatus === "adjRejected" ? { ...r, rowStatus: "adjPending", reviewPass: "", rejectReason: "" } : r);
        return { ...b, rows, submitted: true, updatedAt: stamp };
      }
      if (key === "_multiPatch") {
        const rows = b.rows.map(r => r.uid === rowUid ? { ...r, ...value } : r);
        return { ...b, rows, updatedAt: stamp };
      }
      const rows = b.rows.map(r => r.uid === rowUid ? { ...r, [key]: value, updatedAt: stamp } : r);
      return { ...b, rows, updatedAt: stamp };
    }));
  };
  const submitAdjStatusBatch = (batchId) => {
    setAdjStatusBatches(bs => bs.map(b => {
      if (b.id !== batchId) return b;
      if (b.rows.length === 0) return b;
      const rows = b.rows.map(r => ({ ...r, rowStatus: "adjPending" }));
      return { ...b, rows, submitted: true };
    }));
  };
  const deleteAdjStatusBatch = (batchId) => {
    setAdjStatusBatches(bs => bs.filter(b => b.id !== batchId));
  };

  const getSidebarLabels = (groupId, itemId) => {
    const group = SIDEBAR.find(g => g.id === groupId);
    const item  = group?.children?.find(c => c.id === itemId);
    return { section: group?.label || "", title: item?.label || "" };
  };

  const isMailPreviewPage   = currentGroupId === "mail-preview";
  const isMainIntroPage       = currentGroupId === "main-intro";
  const isUserListPage      = currentItemId === "a-user";
  const isRoleMgmtPage      = currentItemId === "a-perm";
  const isDeptSettingsPage  = currentItemId === "a-dept";
  const isLineAssignPage    = currentItemId === "a-line";
  const isStockQueryPage    = currentItemId === "st-query";
  const isStZonePage        = currentItemId === "st-zone";
  const isStGradePage       = currentItemId === "st-grade";
  const isDCatPage          = currentItemId === "d-cat";
  const isMenuPreviewPage   = currentItemId === "m-prev";
  const isDeltaPage         = currentItemId === "m-delta";
  const isVendorBasePage    = currentItemId === "v-base";
  const isVendorDelivPage   = currentItemId === "v-deliv";
  const isNewProductPage    = currentItemId === "new";
  const isOracleExportPage  = currentItemId === "oracleExport";
  const isProductMasterPage = currentItemId === "master";
  const isPriceHistPage     = currentItemId === "priceHist";
  const isAdjVendorPage     = currentItemId === "adj-vendor";
  const isPriceAdjPage      = currentItemId === "adj-price";
  const isAdjStatusPage     = currentItemId === "adj-status";
  const isAdjOraclePage     = currentItemId === "adj-oracle";
  const isAdjImagePage          = currentItemId === "adj-image";
  const isApprovalFlowPage      = currentItemId === "a-flow";
  const isCollectionOfferPage   = currentItemId === "p-coll";
  const isEatsOfferPage         = currentItemId === "p-eats";
  const isClearancePage         = currentItemId === "p-clear";
  const isBdRegionPage          = currentItemId === "bd-region";
  const isBdStorePage           = currentItemId === "bd-store";
  const isBdTierPage            = currentItemId === "bd-tier";
  const isBdTempPage            = currentItemId === "bd-temp";
  const isStHoursPage           = currentItemId === "st-hours";
  const isAuthPermListPage      = currentItemId === "a-auth-list";
  const isPosSettingsPage       = currentItemId === "a-pos";
  const isDisplayItemPage       = currentItemId === "d-item";
  const isDisplayMergePage      = currentItemId === "d-merge";
  const isIncaCatalogPage       = currentItemId === "m-cat";
  const isIncaPromoPage         = currentItemId === "m-promo";
  const isPromoStubPage         = PROMO_STUB_CONFIG[currentItemId] != null;
  const isSysIncaPage           = currentItemId === "sys-inca";
  const isSysEffectivePage      = currentItemId === "sys-effective";
  const clRole = isPrototypeSuperUser(role) ? role
    : CL_MODULE_CONFIG.editUsers.includes(currentUser) ? "purchase" : "viewer";
  const coRole = isPrototypeSuperUser(role) ? role
    : CO_MODULE_CONFIG.reviewUsers?.includes(currentUser)
      ? "joanna"
      : CO_MODULE_CONFIG.editUsers.includes(currentUser)
        ? "purchase"
        : "viewer";
  const eoRole = isPrototypeSuperUser(role) ? role
    : EO_MODULE_CONFIG.reviewUsers?.includes(currentUser)
      ? "joanna"
      : EO_MODULE_CONFIG.editUsers.includes(currentUser)
        ? "purchase"
        : "viewer";

  const currentForm = view.kind === "detail" ? forms.find(f => f.id === view.formId) : null;
  const currentAdjBatch = (isAdjVendorPage && view.kind === "detail") ? adjBatches.find(b => b.id === view.formId) : null;
  const currentCoOffer = (isCollectionOfferPage && view.kind === "detail" && view.formId !== "__new__")
    ? collectionOffers.find(o => o.id === view.formId) : null;
  const currentClearanceOffer = (isClearancePage && view.kind === "detail")
    ? clearanceOffers.find(o => o.id === view.formId) : null;
  const currentPriceAdjBatch = (isPriceAdjPage && view.kind === "detail") ? priceAdjBatches.find(b => b.id === view.formId) : null;

  // ADJ Oracle — look up batch from correct source by ID prefix
  const currentAdjOracleBatch = useMemo(() => {
    if (!isAdjOraclePage || view.kind !== "detail" || !view.formId) return null;
    const id = view.formId;
    if (id.startsWith("ADJ-")) {
      const b = adjBatches.find(b => b.id === id);
      return b ? { ...b, sourceType: "ADJ-A" } : null;
    }
    if (id.startsWith("PADJ-")) {
      const b = priceAdjBatches.find(b => b.id === id);
      return b ? { ...b, sourceType: "ADJ-B" } : null;
    }
    if (id.startsWith("AS-")) {
      const b = adjStatusBatches.find(b => b.id === id);
      return b ? { ...b, sourceType: "ADJ-C/D" } : null;
    }
    return null;
  }, [adjBatches, priceAdjBatches, adjStatusBatches, isAdjOraclePage, view]);

  // Oracle operates at Phase level — aggregate all submitted forms sharing the same batch name
  const currentPhase = useMemo(() => {
    if (!isOracleExportPage || view.kind !== "detail" || !view.formId) return null;
    const submitted = forms.filter(f => f.status === "submitted" && f.batch === view.formId);
    if (!submitted.length) return null;
    const items = submitted.flatMap(f => f.items);
    const created = submitted.map(f => f.created).sort()[0];
    const updated = submitted.map(f => f.updated).sort().reverse()[0];
    return { id: view.formId, batch: view.formId, items, created, updated };
  }, [forms, view.formId, view.kind, isOracleExportPage]);

  // ── Shared: 測試設定 FAB + mini-panel (always visible) ──────────────────
  const goMainIntro = () => {
    setIsDirty(false);
    setCurrentGroupId("main-intro");
    setCurrentItemId("main-intro");
    setView({ kind: "list" });
    window.location.hash = "#/main-intro";
  };

  useEffect(() => {
    window.__diOnBack = goMainIntro;
    return () => { delete window.__diOnBack; };
  }, []);

  const settingPanel = (
    <>
      <button
        onClick={() => setCoSettingOpen(v => !v)}
        title="測試設定"
        style={{
          position: "fixed", bottom: 20, right: 20, zIndex: 9000,
          width: 36, height: 36, borderRadius: "50%",
          border: "1px solid var(--border-1)", background: "#fff",
          boxShadow: "0 2px 8px rgba(0,0,0,.18)",
          cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
          color: coSettingOpen ? "var(--brand-primary)" : "var(--fg-3)",
        }}
      >
        <Icon name="cog" size={16} />
      </button>

      {coSettingOpen && (
        <div style={{
          position: "fixed", bottom: 64, right: 20, zIndex: 9000,
          width: 280, background: "#fff",
          border: "1px solid var(--border-1)", borderRadius: 8,
          boxShadow: "0 4px 16px rgba(0,0,0,.15)", overflow: "hidden",
        }}>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", borderBottom: "1px solid var(--border-1)" }}>
            <span style={{ fontSize: 12, fontWeight: 600, color: "var(--fg-2)" }}>測試設定</span>
            <button onClick={() => setCoSettingOpen(false)} style={{ border: "none", background: "none", cursor: "pointer", color: "var(--fg-3)", fontSize: 16, lineHeight: 1, padding: 2 }}>✕</button>
          </div>
          <div style={{ padding: "12px 14px" }}>

            {/* 示範角色 */}
            <div style={{ fontSize: 11, fontWeight: 600, color: "var(--fg-3)", marginBottom: 8, textTransform: "uppercase", letterSpacing: ".05em" }}>示範角色</div>
            {[
              { value: "purchase", label: "採購（Neilson）" },
              { value: "samantha", label: "系統管理員（Samantha）" },
              { value: "johnny",   label: "CRM / 廠商（Johnny）" },
              { value: "joanna",   label: "採購主管（Joanna）" },
            ].map(opt => (
              <label key={opt.value} style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 0", cursor: "pointer" }}>
                <input
                  type="radio"
                  name="demo-role"
                  value={opt.value}
                  checked={t.role === opt.value}
                  onChange={() => setTweak("role", opt.value)}
                  style={{ accentColor: "var(--brand-primary)", flexShrink: 0 }}
                />
                <span style={{ fontSize: 13, color: "var(--fg-1)" }}>{opt.label}</span>
              </label>
            ))}

            <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border-1)" }}>
              {/* 模擬首次登入 */}
              <div style={{ fontSize: 11, fontWeight: 600, color: "var(--fg-3)", marginBottom: 8, textTransform: "uppercase", letterSpacing: ".05em" }}>登入流程</div>
              <button
                onClick={() => { setCoSettingOpen(false); setAuthState("forceChangePassword"); }}
                style={{
                  width: "100%", height: 28, padding: "0 12px",
                  font: "500 12px var(--font-sans)", background: "#fff",
                  border: "1px solid var(--border-2)", borderRadius: 4,
                  cursor: "pointer", color: "var(--fg-1)", textAlign: "left",
                }}
              >
                模擬首次登入（強制改密碼）
              </button>
              <button
                onClick={() => { setCoSettingOpen(false); setLoginSuccessBanner(false); setAuthState("login"); }}
                style={{
                  width: "100%", height: 28, padding: "0 12px", marginTop: 6,
                  font: "500 12px var(--font-sans)", background: "#fff",
                  border: "1px solid var(--border-2)", borderRadius: 4,
                  cursor: "pointer", color: "var(--fg-1)", textAlign: "left",
                }}
              >
                回登入頁
              </button>
            </div>

            <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border-1)" }}>
              {/* 顯示審核流程 */}
              <div style={{ fontSize: 11, fontWeight: 600, color: "var(--fg-3)", marginBottom: 8, textTransform: "uppercase", letterSpacing: ".05em" }}>滿額折／滿件折</div>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
                <span style={{ fontSize: 13, color: "var(--fg-1)" }}>顯示審核流程</span>
                <button
                  onClick={() => setTweak("coHasApproval", !t.coHasApproval)}
                  style={{
                    position: "relative", width: 32, height: 18, flexShrink: 0,
                    border: "none", borderRadius: 999,
                    background: t.coHasApproval ? "var(--brand-primary)" : "var(--gray-300)",
                    cursor: "pointer", transition: "background .15s",
                  }}
                >
                  <span style={{
                    position: "absolute", top: 2, left: t.coHasApproval ? 16 : 2,
                    width: 14, height: 14, borderRadius: "50%", background: "#fff",
                    transition: "left .15s",
                  }} />
                </button>
              </div>
            </div>

            <div style={{ marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--border-1)" }}>
              <div style={{ fontSize: 11, fontWeight: 600, color: "var(--fg-3)", marginBottom: 8, textTransform: "uppercase", letterSpacing: ".05em" }}>通知信預覽</div>
              {[
                { label: "建立帳號通知信", type: "create" },
                { label: "重設密碼通知信", type: "reset" },
              ].map(({ label, type }) => (
                <button
                  key={type}
                  onClick={() => { setCoSettingOpen(false); const u = users[0]; if (u) goMailPreview(type, u.id); }}
                  style={{
                    width: "100%", height: 28, padding: "0 12px", marginTop: type === "reset" ? 6 : 0,
                    font: "500 12px var(--font-sans)", background: "#fff",
                    border: "1px solid var(--border-2)", borderRadius: 4,
                    cursor: "pointer", color: "var(--fg-1)", textAlign: "left",
                  }}
                >
                  {label}
                </button>
              ))}
            </div>
          </div>
        </div>
      )}
    </>
  );

  // ── Main intro hub (no shell / no login) ─────────────────────────────────
  if (isMainIntroPage) {
    const introSubView = view.kind === "detail" ? view.formId : null;
    return (
      <ToastProvider>
        <MainIntroPage
          subView={introSubView}
          onOpenSub={(sub) => setView(sub ? { kind: "detail", formId: sub } : { kind: "list" })}
          onOpenRoute={(groupId, itemId) => {
            setCurrentGroupId(groupId);
            setCurrentItemId(itemId);
            setView({ kind: "list" });
            setIsDirty(false);
          }}
          onEnterApp={() => {
            setCurrentGroupId("product");
            setCurrentItemId("new");
            setView({ kind: "list" });
            setIsDirty(false);
          }}
        />
        {settingPanel}
      </ToastProvider>
    );
  }

  // ── Mail-preview full-page (no shell) ────────────────────────────────────
  if (isMailPreviewPage) {
    return (
      <ToastProvider>
        <MailPreviewPage
          users={users}
          onBack={() => {
            window.history.back();
            setTimeout(() => {
              const { groupId, itemId } = parseHash();
              setCurrentGroupId(groupId);
              setCurrentItemId(itemId);
            }, 0);
          }}
        />
        {settingPanel}
      </ToastProvider>
    );
  }

  // ── Login / Force-change-password ────────────────────────────────────────
  if (authState === "login") {
    return (
      <ToastProvider>
        <LoginPage
          users={users}
          successBanner={loginSuccessBanner}
          onLoginSuccess={handleLoginSuccess}
          onForceChangePassword={handleForceChange}
        />
        {settingPanel}
      </ToastProvider>
    );
  }
  if (authState === "forceChangePassword") {
    return (
      <ToastProvider>
        <ChangePasswordPage onSuccess={handlePwChanged} />
        {settingPanel}
      </ToastProvider>
    );
  }

  return (
    <ToastProvider>
      <div className={"app" + (t.sidebarCollapsed ? " app--collapsed" : "")}>
        <div className="app__header">
          <Header role={role} onLogout={() => { setLoginSuccessBanner(false); setAuthState("login"); }} />
        </div>
        <div className="app__sidebar">
          <Sidebar
            currentPage={currentItemId}
            onNavigate={navigate}
            collapsed={t.sidebarCollapsed}
            onToggle={() => setTweak("sidebarCollapsed", !t.sidebarCollapsed)}
            role={role}
            notifications={sidebarNotifications}
          />
        </div>
        <main className="app__main">
          <div className="page">
            {/* 新商品建立 — fully implemented */}
            {isNewProductPage && view.kind === "list" && (
              <BatchList
                forms={forms}
                role={role}
                currentUser={currentUser}
                onCreate={openCreate}
                onOpenForm={openForm}
                onDelete={askDelete}
              />
            )}
            {isNewProductPage && view.kind === "detail" && currentForm && (
              <BatchDetail
                key={currentForm.id}
                form={currentForm}
                role={role}
                onBack={back}
                onSubmit={submitForm}
                onPatch={patchForm}
                onDelete={() => askDelete(currentForm)}
                onDirtyChange={setIsDirty}
              />
            )}
            {/* Oracle 匯出 — fully implemented */}
            {isOracleExportPage && view.kind === "list" && (
              <OracleList
                forms={forms}
                onOpenForm={openForm}
              />
            )}
            {isOracleExportPage && view.kind === "detail" && currentPhase && (
              <OracleDetail
                key={currentPhase.id}
                phase={currentPhase}
                onBack={back}
              />
            )}
            {/* 商品主檔 — fully implemented */}
            {isProductMasterPage && <ProductMasterList />}

            {/* 進價歷史總表 — fully implemented */}
            {isPriceHistPage && <PriceHistoryList />}

            {/* 轉廠商調整 — fully implemented */}
            {isAdjVendorPage && view.kind === "list" && (
              <AdjList
                adjBatches={adjBatches}
                role={role}
                currentUser={currentUser}
                onCreate={openAdjCreate}
                onOpen={openAdjBatch}
                onDelete={askDeleteAdj}
              />
            )}
            {isAdjVendorPage && view.kind === "detail" && currentAdjBatch && (
              <AdjDetail
                key={currentAdjBatch.id}
                batch={currentAdjBatch}
                role={role}
                onBack={back}
                onPatchBatch={patchAdjBatch}
                onSubmitBatch={submitAdjBatch}
                onDeleteBatch={askDeleteAdj}
                onDirtyChange={setIsDirty}
              />
            )}

            {/* 價格調整 — fully implemented */}
            {isPriceAdjPage && view.kind === "list" && (
              <PriceAdjList
                priceAdjBatches={priceAdjBatches}
                role={role}
                currentUser={currentUser}
                onCreate={openPriceAdjCreate}
                onOpen={openPriceAdjBatch}
                onDelete={askDeletePriceAdj}
              />
            )}
            {isPriceAdjPage && view.kind === "detail" && currentPriceAdjBatch && (
              <PriceAdjDetail
                key={currentPriceAdjBatch.id}
                batch={currentPriceAdjBatch}
                role={role}
                onBack={back}
                onPatchBatch={patchPriceAdjBatch}
                onSubmitBatch={submitPriceAdjBatch}
                onDeleteBatch={askDeletePriceAdj}
                onDirtyChange={setIsDirty}
              />
            )}

            {/* 商品狀態調整 — fully implemented */}
            {isAdjStatusPage && (
              <AdjStatusList
                role={role}
                currentUser={currentUser}
                adjStatusBatches={adjStatusBatches}
                onPatchBatch={patchAdjStatusBatch}
                onSubmitBatch={submitAdjStatusBatch}
                onCreateBatch={confirmAdjStatusCreate}
                onDeleteBatch={deleteAdjStatusBatch}
                onDirtyChange={setIsDirty}
              />
            )}

            {/* 商品調整 Oracle 匯出 — fully implemented */}
            {isAdjOraclePage && view.kind === "list" && (
              <AdjOracleList
                adjBatches={adjBatches}
                priceAdjBatches={priceAdjBatches}
                adjStatusBatches={adjStatusBatches}
                onOpenBatch={(batch) => guardedNav(() => {
                  setView({ kind: "detail", formId: batch.id });
                  setIsDirty(false);
                })}
              />
            )}
            {isAdjOraclePage && view.kind === "detail" && currentAdjOracleBatch && (
              <AdjOracleDetail
                key={currentAdjOracleBatch.id}
                batch={currentAdjOracleBatch}
                onBack={back}
              />
            )}

            {/* 圖片調整 — fully implemented */}
            {isAdjImagePage && imgAdjView === "list" && (
              <ImageAdjustList
                skus={IMG_SKUS}
                role={role}
                onOpenDrawer={(sku) => setImgDrawerSku(sku)}
                onViewDrawer={(sku) => setImgViewDrawerSku(sku)}
                onGoWorksheet={(filteredSkus) => {
                  setImgAdjInitSkus(filteredSkus);
                  setImgAdjView("worksheet");
                }}
              />
            )}
            {isAdjImagePage && imgAdjView === "worksheet" && (
              <ImageWorksheet
                initialSkus={imgAdjInitSkus}
                currentUser={currentUser}
                onBack={() => { setImgAdjView("list"); setImgAdjInitSkus(null); }}
                onDirtyChange={setIsDirty}
              />
            )}
            {isAdjImagePage && imgDrawerSku && (
              <ImageEditDrawer
                sku={imgDrawerSku}
                onClose={() => setImgDrawerSku(null)}
                onSave={(skuId, drafts) => {
                  const sku = IMG_SKUS.find(s => s.skuId === skuId);
                  if (sku) {
                    Object.entries(drafts).forEach(([k, v]) => { sku[k] = v || ""; });
                    const now = new Date().toLocaleString("zh-TW", { hour12: false }).replace(",", "");
                    sku.lastUpdated = now.slice(0, 16);
                    sku.updatedBy = currentUser;
                  }
                  setImgDrawerSku(null);
                }}
              />
            )}
            {isAdjImagePage && imgViewDrawerSku && (
              <ImageEditDrawer
                sku={imgViewDrawerSku}
                readOnly
                onClose={() => setImgViewDrawerSku(null)}
                onSave={() => {}}
              />
            )}

            {/* 審核流程設定 — fully implemented */}
            {isApprovalFlowPage && afView.kind === "list" && (
              <ApprovalList
                flows={flows}
                onCreate={handleAfNew}
                onEdit={handleAfEdit}
                onToggle={handleAfToggle}
                onDuplicate={handleAfDuplicate}
              />
            )}
            {isApprovalFlowPage && afView.kind === "form" && (
              <ApprovalForm
                initial={
                  afView.flowId
                    ? flows.find(f => f.id === afView.flowId)
                    : afCloneSource
                      ? { ...afCloneSource, id: null, name: afCloneSource.name + "（副本）" }
                      : null
                }
                onCancel={handleAfCancel}
                onSave={handleAfSave}
                onDirtyChange={setIsDirty}
              />
            )}

            {/* 滿額折／滿件折 Collection Offer */}
            {isCollectionOfferPage && view.kind === "list" && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <CollectionOfferList
                  offers={collectionOffers}
                  hasApproval={t.coHasApproval}
                  role={coRole}
                  currentUser={currentUser}
                  sectionLabel={section}
                  titleLabel={title}
                  onCreate={openCoCreate}
                  onOpen={openCoOffer}
                  onDelete={deleteCoOffer}
                  onToggleStatus={toggleCoStatus}
                />
              );
            })()}
            {isCollectionOfferPage && view.kind === "detail" && (view.formId === "__new__" || currentCoOffer) && (
              <CollectionOfferDetail
                key={view.formId}
                offer={currentCoOffer}
                hasApproval={t.coHasApproval}
                role={coRole}
                currentUser={currentUser}
                onBack={back}
                onSaveOffer={saveCoOffer}
                onToggleStatus={toggleCoStatus}
                onDelete={deleteCoOffer}
                onDirtyChange={setIsDirty}
              />
            )}

            {/* 單品優惠 Eats Offer — 單層平面列表 */}
            {isEatsOfferPage && editEoItem === "__new__" && (
              <EoCreatePage
                currentUser={currentUser}
                initialItems={eatsOffers.filter(o => o.status === "eoDraft" && o.applicant === currentUser)}
                onBack={() => setEditEoItem(null)}
                onSave={(savedItems) => {
                  const now = new Date();
                  const stamp = now.getFullYear().toString()
                    + String(now.getMonth() + 1).padStart(2, "0")
                    + String(now.getDate()).padStart(2, "0") + " "
                    + String(now.getHours()).padStart(2, "0") + ":"
                    + String(now.getMinutes()).padStart(2, "0");
                  // 只替換「當前使用者自己的草稿」；保留他人草稿與所有非草稿（避免吞掉別人的草稿）
                  setEatsOffers(list => [
                    ...savedItems.map(it => ({ ...it, updatedAt: stamp })),
                    ...list.filter(o => !(o.status === "eoDraft" && o.applicant === currentUser)),
                  ]);
                  setEditEoItem(null);
                }}
              />
            )}
            {isEatsOfferPage && (editEoItem === null || (editEoItem && editEoItem !== "__new__")) && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <EatsOfferList
                  offers={eatsOffers.filter(o => o.status !== "eoDraft")}
                  hasApproval={EO_MODULE_CONFIG.hasApproval}
                  role={eoRole}
                  currentUser={currentUser}
                  sectionLabel={section}
                  titleLabel={title}
                  subtitleLabel="填寫商品品號與促銷定價、折扣種類及菜單促銷期間；系統依毛利條件（< 20%）決定是否送交審核。"
                  onAdd={() => setEditEoItem("__new__")}
                  onEdit={setEditEoItem}
                  onDelete={deleteEoItem}
                  onApprove={(item) => updateEoItem({ ...item, status: "eoApproved" })}
                  onReject={(item, reason) => updateEoItem({ ...item, status: "eoRejected", rejectReason: reason || "" })}
                  onView={setViewEoItem}
                />
              );
            })()}
            {isEatsOfferPage && editEoItem && editEoItem !== "__new__" && (
              <EoRowEditDrawer
                item={editEoItem}
                onClose={() => setEditEoItem(null)}
                onSave={(updated) => { updateEoItem(updated); setEditEoItem(null); }}
              />
            )}
            {isEatsOfferPage && viewEoItem && (
              <EoRowEditDrawer
                item={viewEoItem}
                readOnly
                onClose={() => setViewEoItem(null)}
                onSave={() => {}}
              />
            )}

            {/* 清倉折扣 Clearance Offer */}
            {isClearancePage && view.kind === "list" && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <ClearanceList
                  offers={clearanceOffers}
                  role={clRole}
                  currentUser={currentUser}
                  sectionLabel={section}
                  titleLabel={title}
                  onCreate={addClearanceOffer}
                  onOpen={openClOffer}
                  onDelete={deleteClearanceOffer}
                />
              );
            })()}
            {isClearancePage && view.kind === "detail" && currentClearanceOffer && (
              <ClearanceDetail
                key={currentClearanceOffer.id}
                offer={currentClearanceOffer}
                role={clRole}
                currentUser={currentUser}
                onBack={back}
                onDelete={deleteClearanceOffer}
              />
            )}

            {/* 使用者管理 */}
            {isUserListPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const isSysAdmin = t.role === "samantha";
              return (
                <UserList
                  users={users}
                  roles={authRoles}
                  roleLabels={authRoleLabels}
                  rolePermissions={authRolePerms}
                  departments={authDepts}
                  isSysAdmin={isSysAdmin}
                  onUpdateUser={updateUser}
                  onAddUser={addUser}
                  sectionLabel={section}
                  titleLabel={title}
                  onGoMailPreview={goMailPreview}
                />
              );
            })()}

            {/* 部門設定 */}
            {isDeptSettingsPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const isSysAdmin = t.role === "samantha";
              return (
                <DeptList
                  depts={authDepts}
                  users={users}
                  isSysAdmin={isSysAdmin}
                  onAddDept={addDept}
                  onUpdateDept={updateDept}
                  onDeleteDept={deleteDept}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 採購線別指派 */}
            {isLineAssignPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const isSysAdmin = t.role === "samantha";
              return (
                <LineAssignPage
                  users={users}
                  lineAssignments={lineAssignments}
                  onSave={setLineAssignments}
                  onDirtyChange={setIsDirty}
                  isSysAdmin={isSysAdmin}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 角色存取權限設定 */}
            {isRoleMgmtPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const isSysAdmin = t.role === "samantha";
              return (
                <RoleList
                  roles={authRoles}
                  roleLabels={authRoleLabels}
                  rolePermissions={authRolePerms}
                  isSysAdmin={isSysAdmin}
                  users={users}
                  onAddRole={addRole}
                  onUpdateRole={updateRole}
                  onDeleteRole={deleteRole}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 庫存查詢 */}
            {isStockQueryPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <StockQueryPage
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 分區設定 */}
            {isStZonePage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const stZoneCanEdit = canActAsEditor(role);
              return (
                <StZoneList
                  zones={zones}
                  storeMaster={STORE_MASTER}
                  canEdit={stZoneCanEdit}
                  adjStatusBatches={adjStatusBatches}
                  sectionLabel={section}
                  titleLabel={title}
                  onCreate={handleZoneCreate}
                  onUpdate={handleZoneUpdate}
                  onDelete={handleZoneDelete}
                />
              );
            })()}

            {/* 店級指派 */}
            {isStGradePage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const stGradeCanEdit = canActAsEditor(role);
              return (
                <StGradeList
                  storeMaster={STORE_MASTER}
                  canEdit={stGradeCanEdit}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 類別重複陳列 */}
            {isDCatPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              const dCatCanEdit = canActAsEditor(role);
              return (
                <DCatPage
                  canEdit={dCatCanEdit}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 菜單預覽 */}
            {isMenuPreviewPage && !menuPreviewDetail && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
              <MenuPreviewList
                previews={menuPreviews}
                onGenerate={handleMenuGenerate}
                onView={p => setMenuPreviewDetail(p)}
                onDelete={p => setMenuDeleteCandidate(p)}
                sectionLabel={section}
                titleLabel={title}
              />
              );
            })()}
            {isMenuPreviewPage && menuPreviewDetail && (
              <MenuPreviewDetail
                preview={menuPreviewDetail}
                onBack={() => setMenuPreviewDetail(null)}
              />
            )}

            {/* Delta File 產出 */}
            {isDeltaPage && (
              <DeltaFileList
                deltaFiles={deltaFiles}
                onGenerate={handleDeltaGenerate}
                onDelete={f => setDeltaDeleteCandidate(f)}
              />
            )}

            {/* 廠商基礎資料 */}
            {isVendorBasePage && !vendorDetail && (() => {
              const canEdit = canActAsEditor(role);
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <VendorList
                  vendors={vendors}
                  deliveries={vendorDeliveries}
                  canEdit={canEdit}
                  onAdd={() => setVendorDetail("__new__")}
                  onEdit={v => setVendorDetail(v)}
                  onView={v => setVendorDetail(v)}
                  onDelete={handleVendorDelete}
                  onSuspend={handleVendorSuspend}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isVendorBasePage && vendorDetail && (() => {
              const canEdit = canActAsEditor(role);
              const isNew   = vendorDetail === "__new__";
              return (
                <VendorDetail
                  vendor={isNew ? null : vendorDetail}
                  deliveries={vendorDeliveries}
                  allVendors={vendors}
                  readOnly={!canEdit}
                  onBack={() => guardedNav(() => setVendorDetail(null))}
                  onSaveDraft={handleVendorSaveDraft}
                  onSubmit={handleVendorSubmit}
                  onDirtyChange={setIsDirty}
                />
              );
            })()}

            {isVendorDelivPage && (() => {
              const canEdit = canActAsEditor(role);
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <VendorDeliveryList
                  deliveries={vendorDeliveries}
                  vendors={vendors}
                  canEdit={canEdit}
                  onSaveRow={handleVendorDelivSaveRow}
                  onDeleteRow={handleVendorDelivDeleteRow}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 基礎主檔 9.x */}
            {isBdRegionPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <BasedataList
                  pageId="bd-region"
                  rows={bdRegions}
                  onAdd={bdAdd(setBdRegions, "bd-r")}
                  onUpdate={bdUpdate(setBdRegions)}
                  onDelete={bdDelete(setBdRegions)}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isBdStorePage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <BasedataList
                  pageId="bd-store"
                  rows={bdStores}
                  regionNames={bdRegions.map(r => r.name)}
                  onAdd={bdAdd(setBdStores, "bd-s")}
                  onUpdate={bdUpdate(setBdStores)}
                  onDelete={bdDelete(setBdStores)}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isBdTierPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <BasedataList
                  pageId="bd-tier"
                  rows={bdTiers}
                  onAdd={bdAdd(setBdTiers, "bd-t")}
                  onUpdate={bdUpdate(setBdTiers)}
                  onDelete={bdDelete(setBdTiers)}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isBdTempPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <BasedataList
                  pageId="bd-temp"
                  rows={bdTemps}
                  onAdd={bdAdd(setBdTemps, "bd-tp")}
                  onUpdate={bdUpdate(setBdTemps)}
                  onDelete={bdDelete(setBdTemps)}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 販售時間表 */}
            {isStHoursPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <StHoursPage
                  rows={stHoursRows}
                  onSave={setStHoursRows}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 陳列單品 / 陳列合併 */}
            {isDisplayItemPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <DisplayItemPage seed={DISPLAY_ITEM_SEED} sectionLabel={section} titleLabel={title} />
              );
            })()}
            {isDisplayMergePage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <DisplayMergePage seed={DISPLAY_MERGE_SEED} sectionLabel={section} titleLabel={title} />
              );
            })()}

            {/* INCA Catalog / Promotion */}
            {isIncaCatalogPage && (
              <IncaFileList
                files={catalogFiles}
                config={INCA_LIST_CONFIG["m-cat"]}
                onGenerate={handleCatalogGenerate}
                onDelete={f => setIncaDeleteCandidate({ kind: "catalog", file: f })}
              />
            )}
            {isIncaPromoPage && (
              <IncaFileList
                files={promoFiles}
                config={INCA_LIST_CONFIG["m-promo"]}
                onGenerate={handlePromoFileGenerate}
                onDelete={f => setIncaDeleteCandidate({ kind: "promo", file: f })}
              />
            )}

            {/* 優惠與活動 stub 列表 */}
            {isPromoStubPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <PromoStubList
                  pageId={currentItemId}
                  config={PROMO_STUB_CONFIG[currentItemId]}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 權限列表 / 職位設定 */}
            {isAuthPermListPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <AuthPermList
                  perms={authPerms}
                  onAdd={addAuthPerm}
                  onUpdate={updateAuthPerm}
                  onDelete={deleteAuthPerm}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isPosSettingsPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <PosSettingsList
                  positions={authPositions}
                  onAdd={addAuthPosition}
                  onUpdate={updateAuthPosition}
                  onDelete={deleteAuthPosition}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}

            {/* 系統設定 */}
            {isSysIncaPage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <SysIncaSettings
                  settings={sysIncaSettings}
                  onSave={setSysIncaSettings}
                  sectionLabel={section}
                  titleLabel={title}
                />
              );
            })()}
            {isSysEffectivePage && (() => {
              const { section, title } = getSidebarLabels(currentGroupId, currentItemId);
              return (
                <SysEffectivePlaceholder sectionLabel={section} titleLabel={title} />
              );
            })()}

            {/* All other pages — placeholder */}
            {!isNewProductPage && !isOracleExportPage && !isProductMasterPage && !isPriceHistPage && !isAdjVendorPage && !isPriceAdjPage && !isAdjStatusPage && !isAdjOraclePage && !isAdjImagePage && !isApprovalFlowPage && !isCollectionOfferPage && !isEatsOfferPage && !isClearancePage && !isUserListPage && !isRoleMgmtPage && !isDeptSettingsPage && !isLineAssignPage && !isStockQueryPage && !isStZonePage && !isStGradePage && !isDCatPage && !isMenuPreviewPage && !isDeltaPage && !isVendorBasePage && !isVendorDelivPage && !isBdRegionPage && !isBdStorePage && !isBdTierPage && !isBdTempPage && !isStHoursPage && !isDisplayItemPage && !isDisplayMergePage && !isIncaCatalogPage && !isIncaPromoPage && !isPromoStubPage && !isAuthPermListPage && !isPosSettingsPage && !isSysIncaPage && !isSysEffectivePage && <Placeholder itemId={currentItemId} />}
          </div>
        </main>
      </div>

      <BatchPicker
        open={pickerOpen}
        existingForms={forms}
        currentUser={currentUser}
        onClose={() => setPickerOpen(false)}
        onConfirm={confirmCreate}
      />

      <AdjPicker
        open={adjPickerOpen}
        adjBatches={adjBatches}
        currentUser={currentUser}
        onClose={() => setAdjPickerOpen(false)}
        onConfirm={confirmAdjCreate}
      />

      <PriceAdjPicker
        open={priceAdjPickerOpen}
        priceAdjBatches={priceAdjBatches}
        currentUser={currentUser}
        onClose={() => setPriceAdjPickerOpen(false)}
        onConfirm={confirmPriceAdjCreate}
      />

      {/* Price-adj Delete confirm */}
      {priceAdjDeleteCandidate && (
        <div className="dialog-mask" onClick={() => setPriceAdjDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">刪除調整單</div></div>
            <div className="dialog__body">
              將刪除 <b>{priceAdjDeleteCandidate.batchName}</b>（{priceAdjDeleteCandidate.applicant}）的{" "}
              <b>{priceAdjDeleteCandidate.rows.length}</b> 筆品項。<br />此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setPriceAdjDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={doDeletePriceAdj}>確認刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* ADJ-A Delete confirm */}
      {adjDeleteCandidate && (
        <div className="dialog-mask" onClick={() => setAdjDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">刪除調整單</div></div>
            <div className="dialog__body">
              將刪除 <b>{adjDeleteCandidate.batchName}</b>（{adjDeleteCandidate.applicant}）的{" "}
              <b>{adjDeleteCandidate.rows.length}</b> 筆品項。<br />此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setAdjDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={doDeleteAdj}>確認刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* Delete confirm */}
      {deleteCandidate && (
        <div className="dialog-mask" onClick={() => setDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">刪除建立單</div></div>
            <div className="dialog__body">
              將刪除 <b>{deleteCandidate.batch}</b>（{deleteCandidate.applicant}）的{" "}
              <b>{deleteCandidate.items.length}</b> 筆品項。<br />此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={doDelete}>確認刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* Menu Preview — delete confirm */}
      {menuDeleteCandidate && (
        <div className="dialog-mask" onClick={() => setMenuDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">確認刪除此版本菜單？</div></div>
            <div className="dialog__body">
              將刪除 <b>{menuDeleteCandidate.name}</b>（共 <b>{(menuDeleteCandidate.items?.length ?? 0).toLocaleString()}</b> 筆品項）。<br />此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setMenuDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={() => {
                setMenuPreviews(list => list.filter(p => p.id !== menuDeleteCandidate.id));
                if (menuPreviewDetail?.id === menuDeleteCandidate.id) setMenuPreviewDetail(null);
                setMenuDeleteCandidate(null);
              }}>刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* Delta File — delete confirm */}
      {deltaDeleteCandidate && (
        <div className="dialog-mask" onClick={() => setDeltaDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">確認刪除此版本 Delta File？</div></div>
            <div className="dialog__body">
              將刪除 <b>{deltaDeleteCandidate.name}</b>（共 13 個檔案）。此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setDeltaDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={() => {
                setDeltaFiles(list => list.filter(f => f.id !== deltaDeleteCandidate.id));
                setDeltaDeleteCandidate(null);
              }}>刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* INCA Catalog / Promotion — delete confirm */}
      {incaDeleteCandidate && (
        <div className="dialog-mask" onClick={() => setIncaDeleteCandidate(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">確認刪除此版本檔案？</div></div>
            <div className="dialog__body">
              將刪除 <b>{incaDeleteCandidate.file.name}</b>。此動作無法復原。
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setIncaDeleteCandidate(null)}>取消</Button>
              <Button variant="danger" onClick={() => {
                const { kind, file } = incaDeleteCandidate;
                if (kind === "catalog") {
                  setCatalogFiles(list => list.filter(f => f.id !== file.id));
                } else {
                  setPromoFiles(list => list.filter(f => f.id !== file.id));
                }
                setIncaDeleteCandidate(null);
              }}>刪除</Button>
            </div>
          </div>
        </div>
      )}

      {/* Unsaved-changes guard */}
      {pendingNav && (
        <div className="dialog-mask" onClick={() => setPendingNav(null)}>
          <div className="dialog" onClick={e => e.stopPropagation()}>
            <div className="dialog__head"><div className="dialog__title">尚未儲存的變更</div></div>
            <div className="dialog__body">
              {isVendorBasePage
                ? "這筆廠商資料有未儲存的編輯。離開頁面前，要怎麼處理？"
                : role === "joanna"
                  ? "這張調整單有未送出的審核結果，離開後本次審核勾選將遺失。"
                  : isLineAssignPage
                    ? "採購線別指派有未儲存的變更，離開後將遺失。"
                    : <>這張{isAdjImagePage ? "工作表" : (isAdjVendorPage || isPriceAdjPage || isAdjStatusPage) ? "調整單" : "建立單"}有未儲存的編輯。離開頁面前，要怎麼處理？</>
              }
            </div>
            <div className="dialog__foot">
              <Button variant="text" onClick={() => setPendingNav(null)}>
                {(role === "joanna" && !isVendorBasePage) ? "繼續審核" : "繼續編輯"}
              </Button>
              <Button variant="text" onClick={() => {
                const go = pendingNav; setIsDirty(false); setPendingNav(null); go();
              }} style={{ color: "var(--danger)" }}>放棄變更</Button>
              {role !== "joanna" && !isLineAssignPage && !isVendorBasePage && (
                <Button variant="primary" onClick={() => {
                  const go = pendingNav; setIsDirty(false); setPendingNav(null); go();
                }}>儲存草稿並離開</Button>
              )}
            </div>
          </div>
        </div>
      )}

      {settingPanel}

      <TweaksPanel title="Tweaks">
        <TweakSection title="示範角色">
          <TweakRadio
            value={t.role}
            onChange={(v) => setTweak("role", v)}
            options={[
              { value: "purchase", label: "採購" },
              { value: "samantha", label: "Samantha" },
              { value: "johnny",   label: "Johnny" },
              { value: "joanna",   label: "Joanna" },
            ]}
          />
          <div style={{ fontSize: 11, color: "#9E9E9E", marginTop: 4, lineHeight: 1.5 }}>
            切換不同角色看權限／工具差異。
          </div>
        </TweakSection>
        <TweakSection title="側邊欄">
          <TweakToggle label="收合側邊欄" value={t.sidebarCollapsed} onChange={(v) => setTweak("sidebarCollapsed", v)} />
        </TweakSection>
        <TweakSection title="快速跳轉">
          <TweakButton onClick={() => { setIsDirty(false); navigate("product", "new"); }} label="L1 · 建立單列表" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("product"); setCurrentItemId("new");
            const editing = forms.find(f => f.status === "editing");
            if (editing) setView({ kind: "detail", formId: editing.id });
          }} label="L2 · 編輯中明細" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("product"); setCurrentItemId("new");
            const submitted = forms.find(f => f.status === "submitted");
            if (submitted) setView({ kind: "detail", formId: submitted.id });
          }} label="L2 · 已送出明細（唯讀）" />
          <TweakButton onClick={() => {
            setIsDirty(false); navigate("product", "new"); setPickerOpen(true);
          }} label="新增建立單 dialog" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("product", "oracleExport"); }} label="Oracle 匯出 L1" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("product", "master"); }} label="商品主檔 L1" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("product", "priceHist"); }} label="進價歷史總表 L1" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("product"); setCurrentItemId("oracleExport");
            const submitted = forms.find(f => f.status === "submitted");
            if (submitted) setView({ kind: "detail", formId: submitted.batch });
          }} label="Oracle 匯出 L2" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("adjust", "adj-vendor"); }} label="ADJ-A L1 批次列表" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-vendor");
            const draft = adjBatches.find(b => !b.submitted);
            if (draft) setView({ kind: "detail", formId: draft.id });
          }} label="ADJ-A L2 編輯中" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-vendor");
            const pending = adjBatches.find(b => b.submitted && b.rows.every(r => r.rowStatus === "adjPending"));
            if (pending) setView({ kind: "detail", formId: pending.id });
          }} label="ADJ-A L2 已送審" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-vendor");
            const mixed = adjBatches.find(b => b.submitted && b.rows.some(r => r.rowStatus === "adjRejected"));
            if (mixed) setView({ kind: "detail", formId: mixed.id });
          }} label="ADJ-A L2 混合審核" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-vendor");
            const approved = adjBatches.find(b => b.submitted && b.rows.every(r => r.rowStatus === "adjApproved"));
            if (approved) setView({ kind: "detail", formId: approved.id });
          }} label="ADJ-A L2 全數通過" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("adjust", "adj-price"); }} label="ADJ-B L1 價格調整" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-price");
            const draft = priceAdjBatches.find(b => !b.submitted);
            if (draft) setView({ kind: "detail", formId: draft.id });
          }} label="ADJ-B L2 編輯中" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-price");
            const pending = priceAdjBatches.find(b => b.submitted && b.rows.every(r => r.rowStatus === "adjPending"));
            if (pending) setView({ kind: "detail", formId: pending.id });
          }} label="ADJ-B L2 已送審" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-price");
            const approved = priceAdjBatches.find(b => b.submitted && b.rows.every(r => r.rowStatus === "adjApproved"));
            if (approved) setView({ kind: "detail", formId: approved.id });
          }} label="ADJ-B L2 全數通過" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("adjust", "adj-oracle"); }} label="商品調整 Oracle 匯出 L1" />
          <TweakButton onClick={() => {
            setIsDirty(false); setCurrentGroupId("adjust"); setCurrentItemId("adj-oracle");
            const b = adjBatches.find(b => b.submitted && b.rows.some(r => r.rowStatus === "adjApproved"));
            if (b) setView({ kind: "detail", formId: b.id });
          }} label="商品調整 Oracle 匯出 L2" />
          <TweakButton onClick={() => { setIsDirty(false); navigate("adjust", "adj-status"); }} label="ADJ-C/D L1 商品狀態調整" />
          <TweakButton onClick={() => {
            setIsDirty(false); navigate("adjust", "adj-status");
          }} label="ADJ-C/D L1（草稿批次）" />
          <TweakButton onClick={() => {
            setIsDirty(false); navigate("adjust", "adj-status");
          }} label="ADJ-C/D L1（待審核批次）" />
        </TweakSection>
      </TweaksPanel>
    </ToastProvider>
  );
};

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
