const STORAGE_KEY = "roomie-console-state-v3";
const SESSION_KEY = "roomie-console-session";
const REMEMBER_KEY = "roomie-console-remember-admin";
const CLOUD_CONFIG_KEY = "roomie-cloud-sync-config";

const catStatuses = ["入住中", "预约", "已退房"];
const paymentStatuses = ["已结清", "仅押金", "待付款"];
const appointmentStatuses = ["已确认", "待确认", "已取消"];
const ledgerKinds = ["收入", "支出"];
const genders = ["未填写", "男", "女"];
const employeeStatuses = ["试用期", "兼职", "全职", "长休假", "停职", "离职"];
const neuterStatuses = ["未填写", "已绝育", "未绝育", "不确定"];
const inventoryCategories = ["猫咪零售商品", "门店消耗物资", "下午茶罐头"];
const inventoryUnits = ["件", "包", "袋", "盒", "罐", "瓶", "卷"];

const roomTypes = [
  { type: "大方间", rooms: ["VIC01", "VIC02", "VIC03", "VIC04"], price: 328, rank: 1 },
  { type: "自在间", rooms: ["VIC05", "VIC06", "VIC07"], price: 288, rank: 2 },
  { type: "竹边间", rooms: ["BBV01", "BBV02", "BBV03", "BBV04"], price: 258, rank: 3 },
  { type: "二段间-L", rooms: ["RM01", "RM02", "RM03", "RM04"], price: 228, rank: 4 },
  { type: "二段间-M", rooms: ["RM07", "RM08"], price: 198, rank: 5 },
  { type: "二段间-S", rooms: ["RM05", "RM06"], price: 168, rank: 6 },
  { type: "小小间", rooms: ["RM09", "RM10", "RM11", "RM12"], price: 128, rank: 7, emergency: true }
];

const roomOptions = roomTypes.flatMap((type) => type.rooms.map((room) => ({ room, type: type.type, price: type.price, emergency: Boolean(type.emergency) })));

const labels = {
  dashboard: ["今日总览", "grid"],
  profile: ["我的账号", "user"],
  calendar: ["预约日历", "calendar"],
  rooms: ["房型房态", "house"],
  customers: ["客户信息", "user"],
  cats: ["猫咪入住", "paw"],
  reports: ["门店日报", "clipboard"],
  roster: ["排班考勤", "user"],
  docs: ["内部资料", "files"],
  inventory: ["物资库存", "box"],
  reminders: ["提醒管理", "bell"],
  accounts: ["账号审核", "user"],
  ledger: ["管理员账本", "wallet"]
};

const roleViews = {
  admin: ["dashboard", "profile", "calendar", "rooms", "customers", "cats", "reports", "roster", "docs", "inventory", "reminders", "accounts", "ledger"],
  staff: ["dashboard", "profile", "calendar", "rooms", "customers", "cats", "reports", "roster", "docs", "inventory"]
};

const roleText = {
  admin: "管理员",
  staff: "员工"
};

const rosterEmployees = ["双双", "饭饭"];
const rosterShiftText = {
  full: "全班 8:30-19:30",
  morning: "早班 8:30-13:30",
  evening: "晚班 13:30-19:30",
  rest: "休息"
};
const rosterTemplates = {
  low: {
    name: "低入住版",
    range: "猫咪 1-5 只适用",
    rows: [
      ["周一", "full", "rest"],
      ["周二", "full", "rest"],
      ["周三", "rest", "full"],
      ["周四", "evening", "morning"],
      ["周五", "morning", "evening"],
      ["周六", "evening", "morning"],
      ["周日", "rest", "full"]
    ]
  },
  high: {
    name: "高入住版",
    range: "猫咪 6 只及以上适用",
    rows: [
      ["周一", "full", "rest"],
      ["周二", "full", "rest"],
      ["周三", "rest", "full"],
      ["周四", "full", "morning"],
      ["周五", "full", "evening"],
      ["周六", "full", "morning"],
      ["周日", "rest", "full"]
    ]
  }
};

const ADMIN_INVITE_CODE = "ROOMIE-ADMIN";
const STAFF_APPLICATION_FILE_TYPE = "roomieStaffApplication";
const STAFF_ACCOUNTS_FILE_TYPE = "roomieStaffAccounts";

let currentView = "dashboard";
let loginRole = "staff";
let loginMode = "login";
let authNotice = "";
let catSearch = "";
let calendarCursor = startOfMonth(new Date());
let selectedDate = isoDate(new Date());
let state = normalizeState(loadState());
let session = readSession();
let weatherState = { label: "天气同步中", humidity: "--", temperature: "--" };
const nativeCallbacks = new Map();
let cloudPushTimer = null;
let cloudSyncBusy = false;
let cloudSyncOperation = null;
const CLOUD_REQUEST_TIMEOUT_MS = 12000;

const screen = document.getElementById("screen");
const loginScreen = document.getElementById("loginScreen");
const appShell = document.getElementById("appShell");
const saveIndicator = document.getElementById("saveIndicator");
const catDialog = document.getElementById("catDialog");
const catForm = document.getElementById("catForm");
const catDialogTitle = document.getElementById("catDialogTitle");
const toastRegion = document.getElementById("toastRegion");

function showToast(message, tone = "info", duration = 4200) {
  if (!toastRegion || !message) return;
  const toast = document.createElement("div");
  toast.className = `app-toast ${tone}`;
  toast.textContent = message;
  toastRegion.appendChild(toast);
  while (toastRegion.children.length > 3) toastRegion.firstElementChild?.remove();
  window.setTimeout(() => {
    toast.classList.add("is-leaving");
    window.setTimeout(() => toast.remove(), 180);
  }, duration);
}

function cloudErrorMessage(error, fallback) {
  const message = String(error?.message || "").trim();
  if (message === "Load failed" || message === "Failed to fetch") {
    return "无法从 App 连接阿里云，请确认服务器在线、防火墙已开放，并使用最新测试兼容版。";
  }
  return message || fallback;
}

window.roomieNativeCallback = function roomieNativeCallback(payload) {
  const requestId = payload?.requestId;
  if (!requestId || !nativeCallbacks.has(requestId)) return;
  const handlers = nativeCallbacks.get(requestId);
  nativeCallbacks.delete(requestId);
  if (payload.success) {
    handlers.resolve(payload);
  } else {
    handlers.reject(new Error(payload.message || "微信发送失败"));
  }
};

function icon(name) {
  const svg = {
    grid: '<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>',
    paw: '<svg class="icon" viewBox="0 0 24 24"><path d="M7.5 12.5c-2.1 1.4-3 4.9-.4 6.3 2.1 1.2 3.7-.5 4.9-.5s2.8 1.7 4.9.5c2.6-1.4 1.7-4.9-.4-6.3-1.6-1-2.5-.1-4.5-.1s-2.9-.9-4.5.1Z"/><circle cx="6.7" cy="7.4" r="2.1"/><circle cx="12" cy="5.4" r="2.1"/><circle cx="17.3" cy="7.4" r="2.1"/></svg>',
    clipboard: '<svg class="icon" viewBox="0 0 24 24"><path d="M9 5h6"/><path d="M9 4h6a2 2 0 0 1 2 2v1H7V6a2 2 0 0 1 2-2Z"/><path d="M7 6H5.5A1.5 1.5 0 0 0 4 7.5v12A1.5 1.5 0 0 0 5.5 21h13a1.5 1.5 0 0 0 1.5-1.5v-12A1.5 1.5 0 0 0 18.5 6H17"/><path d="m8 13 2 2 4-5"/><path d="M8 18h8"/></svg>',
    wallet: '<svg class="icon" viewBox="0 0 24 24"><path d="M4 7.5A2.5 2.5 0 0 1 6.5 5H19v4H6.5A2.5 2.5 0 0 1 4 6.5v11A2.5 2.5 0 0 0 6.5 20H20V9H6.5"/><path d="M17 14h.01"/></svg>',
    plus: '<svg class="icon" viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg>',
    edit: '<svg class="icon" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z"/></svg>',
    trash: '<svg class="icon" viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5"/><path d="M14 11v5"/></svg>',
    search: '<svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>',
    house: '<svg class="icon" viewBox="0 0 24 24"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10.5V20h13v-9.5"/><path d="M9.5 20v-6h5v6"/></svg>',
    bell: '<svg class="icon" viewBox="0 0 24 24"><path d="M18 8a6 6 0 1 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9"/><path d="M10 21h4"/></svg>',
    check: '<svg class="icon" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></svg>',
    close: '<svg class="icon" viewBox="0 0 24 24"><path d="M6 6l12 12"/><path d="M18 6 6 18"/></svg>',
    card: '<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18"/><path d="M7 15h4"/></svg>',
    trend: '<svg class="icon" viewBox="0 0 24 24"><path d="M4 19V5"/><path d="M4 19h16"/><path d="m7 15 4-4 3 3 5-7"/></svg>',
    user: '<svg class="icon" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M5 20a7 7 0 0 1 14 0"/></svg>',
    calendar: '<svg class="icon" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4"/><path d="M8 3v4"/><path d="M3 11h18"/></svg>',
    upload: '<svg class="icon" viewBox="0 0 24 24"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M5 20h14"/></svg>',
    download: '<svg class="icon" viewBox="0 0 24 24"><path d="M12 4v12"/><path d="m7 11 5 5 5-5"/><path d="M5 20h14"/></svg>',
    logout: '<svg class="icon" viewBox="0 0 24 24"><path d="M10 17 15 12 10 7"/><path d="M15 12H3"/><path d="M14 4h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3h-4"/></svg>',
    files: '<svg class="icon" viewBox="0 0 24 24"><path d="M7 3h7l4 4v14H7z"/><path d="M14 3v5h5"/><path d="M5 7H4a1 1 0 0 0-1 1v13h10v-1"/></svg>',
    box: '<svg class="icon" viewBox="0 0 24 24"><path d="m3 7 9 5 9-5"/><path d="M12 22V12"/><path d="M21 7v10l-9 5-9-5V7l9-5Z"/></svg>'
  };
  return svg[name] || svg.grid;
}

function setup() {
  document.querySelectorAll(".nav-item").forEach((button) => {
    const view = button.dataset.view;
    button.innerHTML = `${icon(labels[view][1])}<span>${labels[view][0]}</span>`;
    button.addEventListener("click", () => {
      currentView = view;
      render();
    });
  });

  document.getElementById("exportButton").innerHTML = `${icon("upload")}<span>导出数据</span>`;
  document.getElementById("exportButton").addEventListener("click", exportData);

  document.querySelectorAll("[data-close-dialog]").forEach((button) => {
    button.innerHTML = button.classList.contains("icon-button") ? icon("close") : button.innerHTML;
    button.addEventListener("click", () => catDialog.close());
  });

  populateCatFormOptions();
  catForm.addEventListener("submit", saveCatFromForm);
  render();
  updateLiveClock();
  setInterval(updateLiveClock, 1000);
  fetchWenzhouWeather();
  setInterval(fetchWenzhouWeather, 30 * 60 * 1000);
  setInterval(checkScheduledWechatDrafts, 60 * 1000);
  setInterval(() => pullCloudState({ silent: true }), 15000);
  pullCloudState({ silent: true });
}

function readSession() {
  try {
    const shortSession = JSON.parse(sessionStorage.getItem(SESSION_KEY));
    if (shortSession) return shortSession;
    const remembered = JSON.parse(localStorage.getItem(REMEMBER_KEY));
    if (remembered && remembered.role === "admin" && remembered.expiresAt && new Date(remembered.expiresAt) > new Date()) {
      return remembered;
    }
    if (remembered) localStorage.removeItem(REMEMBER_KEY);
    return null;
  } catch {
    return null;
  }
}

function setSession(nextSession, rememberDays = 0) {
  session = nextSession;
  if (session) {
    sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
    if (session.role === "admin" && rememberDays > 0) {
      localStorage.setItem(REMEMBER_KEY, JSON.stringify({
        ...session,
        expiresAt: addDays(new Date(), rememberDays).toISOString()
      }));
    }
  } else {
    sessionStorage.removeItem(SESSION_KEY);
    localStorage.removeItem(REMEMBER_KEY);
  }
}

function loadState() {
  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    return saved ? JSON.parse(saved) : seedState();
  } catch {
    return seedState();
  }
}

function normalizeState(raw) {
  const next = raw || seedState();
  if (!next.meta || typeof next.meta !== "object") next.meta = {};
  next.meta.updatedAt = next.meta.updatedAt || new Date().toISOString();
  next.meta.deviceName = next.meta.deviceName || "Roomie Console";
  if (!Array.isArray(next.cats)) next.cats = [];
  next.cats = next.cats.map(normalizeStay);
  if (!Array.isArray(next.customers)) next.customers = seedCustomers();
  next.customers = next.customers.map(normalizeCustomer);
  if (!Array.isArray(next.reports)) next.reports = [];
  next.reports = next.reports.map((report) => ({ roomType: "", room: "", afternoonTeaDone: false, ...report }));
  if (!Array.isArray(next.ledger)) next.ledger = [];
  if (!Array.isArray(next.appointments)) next.appointments = seedAppointments();
  if (!Array.isArray(next.schedules)) next.schedules = seedSchedules();
  if (!Array.isArray(next.attendance)) next.attendance = [];
  if (!Array.isArray(next.documents)) next.documents = seedDocuments();
  if (!Array.isArray(next.readReceipts)) next.readReceipts = [];
  if (!Array.isArray(next.printReceipts)) next.printReceipts = [];
  if (!Array.isArray(next.inventory)) next.inventory = seedInventory();
  next.inventory = next.inventory
    .filter((item) => item?.category !== "能耗与固定成本")
    .map(normalizeInventoryItem);
  if (!Array.isArray(next.users)) next.users = seedUsers();
  next.users = next.users.map(normalizeUser);
  if (!Array.isArray(next.cleaningTasks)) next.cleaningTasks = seedCleaningTasks();
  next.cleaningTasks = next.cleaningTasks.map(normalizeCleaningTask);
  if (!Array.isArray(next.reminders)) next.reminders = seedReminders();
  next.reminders = next.reminders.map(normalizeManagedReminder);
  if (!Array.isArray(next.wechatLogs)) next.wechatLogs = [];
  if (!next.wechatConfig || typeof next.wechatConfig !== "object") next.wechatConfig = { targetName: "员工工作群", autoOpen: true };
  next.wechatConfig.targetName = next.wechatConfig.targetName || "员工工作群";
  next.wechatConfig.autoOpen = next.wechatConfig.autoOpen !== false;
  return next;
}

function normalizeUser(user) {
  const gender = user.gender === "弟弟" ? "男" : (user.gender === "妹妹" ? "女" : (user.gender || "未填写"));
  return {
    avatar: "",
    gender,
    birthDate: "",
    phone: "",
    idNumber: "",
    hireDate: "",
    emergencyPhone: "",
    employeeStatus: "全职",
    ...user,
    gender
  };
}

function normalizeCustomer(customer) {
  return {
    id: customer.id || crypto.randomUUID(),
    customerName: customer.customerName || "",
    idNumber: customer.idNumber || "",
    phone: customer.phone || "",
    emergencyPhone: customer.emergencyPhone || "",
    checkInDate: customer.checkInDate || isoDate(new Date()),
    cats: Array.isArray(customer.cats) ? customer.cats.map(normalizeCatProfile) : [],
    createdAt: customer.createdAt || new Date().toISOString()
  };
}

function normalizeStay(cat) {
  const roomMeta = roomMetaFor(cat.room);
  const familyCats = Array.isArray(cat.familyCats) && cat.familyCats.length
    ? cat.familyCats.map(normalizeCatProfile)
    : [normalizeCatProfile({
        name: cat.catName,
        gender: cat.gender,
        ageMonthsAtFirstRegistration: parseAgeMonths(cat.age),
        breed: cat.breed,
        firstStayDate: cat.checkInDate,
        specialNotes: cat.healthNotes,
        medicationNotes: "",
        feeding: {
          dryFoodGrams: "",
          wetFoodGrams: "",
          supplements: "",
          dietNotes: cat.feedingPlan || ""
        },
        pastStayDates: cat.checkInDate ? [cat.checkInDate] : [],
        spendingHistory: [],
        roomServiceOrders: []
      })];
  return {
    roomType: cat.roomType || roomMeta?.type || "",
    familyCats,
    ...cat,
    familyCats,
    catName: familyCats.map((item) => item.name).filter(Boolean).join(" / ") || cat.catName || "",
    dailyRate: Number(cat.dailyRate || roomMeta?.price || 0)
  };
}

function normalizeCatProfile(profile) {
  const firstStayDate = profile.firstStayDate || isoDate(new Date());
  const gender = profile.gender === "弟弟" ? "男" : (profile.gender === "妹妹" ? "女" : (profile.gender || "未填写"));
  return {
    id: profile.id || crypto.randomUUID(),
    name: profile.name || "",
    gender,
    birthDate: profile.birthDate || "",
    ageMonthsAtFirstRegistration: profile.ageMonthsAtFirstRegistration || "",
    ageYearsAtFirstRegistration: profile.ageYearsAtFirstRegistration || "",
    breed: profile.breed || "",
    coatColor: profile.coatColor || "",
    vaccineDate: profile.vaccineDate || "",
    neuterStatus: profile.neuterStatus || "未填写",
    specialNotes: profile.specialNotes || "",
    medicationNotes: profile.medicationNotes || "",
    feeding: {
      dryFoodGrams: profile.feeding?.dryFoodGrams || "",
      wetFoodGrams: profile.feeding?.wetFoodGrams || "",
      supplements: profile.feeding?.supplements || "",
      dietNotes: profile.feeding?.dietNotes || ""
    },
    firstStayDate,
    pastStayDates: Array.isArray(profile.pastStayDates) ? profile.pastStayDates : [],
    spendingHistory: Array.isArray(profile.spendingHistory) ? profile.spendingHistory : [],
    roomServiceOrders: normalizeRoomServiceOrders(profile.roomServiceOrders)
  };
}

function normalizeRoomServiceOrders(orders) {
  if (!Array.isArray(orders)) return [];
  return orders.map((order) => {
    if (typeof order === "string") {
      return {
        id: crypto.randomUUID(),
        purchaseDate: "",
        productName: order,
        unitPrice: "",
        totalAmount: "",
        quantity: "",
        usage: "",
        note: ""
      };
    }
    return {
      id: order.id || crypto.randomUUID(),
      purchaseDate: order.purchaseDate || "",
      productName: order.productName || "",
      unitPrice: order.unitPrice || order.price || "",
      totalAmount: order.totalAmount || "",
      quantity: order.quantity || "",
      usage: order.usage || "",
      note: order.note || ""
    };
  }).filter((order) => order.productName || order.purchaseDate || order.unitPrice || order.totalAmount || order.quantity || order.usage || order.note);
}

function normalizeCleaningTask(item) {
  return {
    id: item.id || crypto.randomUUID(),
    cadence: item.cadence || "每日",
    area: item.area || "",
    content: item.content || "",
    time: item.time || "",
    sendTime: item.sendTime || item.time || "",
    enabled: item.enabled !== false
  };
}

function normalizeManagedReminder(item) {
  return {
    id: item.id || crypto.randomUUID(),
    type: item.type || "额外提醒",
    time: item.time || "",
    sendTime: item.sendTime || item.time || "",
    content: item.content || "",
    enabled: item.enabled !== false
  };
}

function normalizeInventoryItem(item) {
  return {
    id: item.id || crypto.randomUUID(),
    name: item.name || "",
    category: inventoryCategories.includes(item.category) ? item.category : inventoryCategories[0],
    quantity: Number(item.quantity || 0),
    unit: item.unit || "件",
    purchaseDate: item.purchaseDate || isoDate(new Date()),
    expiryDate: item.expiryDate || "",
    safetyStock: Number(item.safetyStock || 0),
    unitCost: Number(item.unitCost || 0),
    supplier: item.supplier || "",
    location: item.location || "",
    note: item.note || "",
    updatedAt: item.updatedAt || new Date().toISOString(),
    updatedBy: item.updatedBy || "system"
  };
}

function saveState() {
  state.meta = {
    ...(state.meta || {}),
    updatedAt: new Date().toISOString(),
    deviceName: "Roomie Console"
  };
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
  saveIndicator.textContent = `已保存 ${timeText(new Date())}`;
  scheduleCloudPush();
}

function readCloudConfig() {
  try {
    const config = JSON.parse(localStorage.getItem(CLOUD_CONFIG_KEY));
    return {
      enabled: Boolean(config?.enabled),
      endpoint: String(config?.endpoint || "").trim().replace(/\/+$/, ""),
      token: String(config?.token || "").trim(),
      verifiedAt: String(config?.verifiedAt || "")
    };
  } catch {
    return { enabled: false, endpoint: "", token: "", verifiedAt: "" };
  }
}

function saveCloudConfig(config) {
  localStorage.setItem(CLOUD_CONFIG_KEY, JSON.stringify({
    enabled: Boolean(config.enabled),
    endpoint: String(config.endpoint || "").trim().replace(/\/+$/, ""),
    token: String(config.token || "").trim(),
    verifiedAt: String(config.verifiedAt || "")
  }));
}

function cloudSyncEnabled() {
  const config = readCloudConfig();
  return Boolean(config.enabled && config.endpoint && config.token);
}

function cloudStatusText() {
  const config = readCloudConfig();
  if (!config.enabled) return "未启用，当前为本机保存";
  if (!config.endpoint || !config.token) return "已开启，但缺少阿里云同步地址或密钥";
  return config.verifiedAt
    ? `最近验证通过：${config.endpoint}`
    : `已配置，等待连接验证：${config.endpoint}`;
}

function scheduleCloudPush() {
  if (!cloudSyncEnabled() || cloudSyncBusy) return;
  clearTimeout(cloudPushTimer);
  cloudPushTimer = setTimeout(() => pushCloudState({ silent: true }), 900);
}

async function cloudRequest(path, options = {}, operation = null) {
  const config = readCloudConfig();
  if (!config.endpoint || !config.token) throw new Error("未配置阿里云同步地址或密钥");
  const controller = new AbortController();
  if (operation) operation.controller = controller;
  const timeoutId = window.setTimeout(() => controller.abort(), CLOUD_REQUEST_TIMEOUT_MS);
  let response;
  try {
    response = await fetch(`${config.endpoint}${path}`, {
      ...options,
      signal: controller.signal,
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${config.token}`,
        ...(options.headers || {})
      }
    });
  } catch (error) {
    if (error?.name === "AbortError") {
      throw new Error("云端响应超时，同步状态已自动释放，请稍后重试");
    }
    throw error;
  } finally {
    window.clearTimeout(timeoutId);
    if (operation?.controller === controller) operation.controller = null;
  }
  if (!response.ok) {
    const message = await response.text().catch(() => "");
    if (response.status === 401) throw new Error("同步密钥不正确，请重新核对");
    throw new Error(message || `云同步请求失败：${response.status}`);
  }
  return response.status === 204 ? null : response.json();
}

async function pullCloudState(options = {}) {
  if (!cloudSyncEnabled()) {
    if (!options.silent) showToast("请先开启联网同步并填写完整的地址和密钥。", "error");
    return false;
  }
  if (cloudSyncOperation) {
    if (options.silent || !cloudSyncOperation.silent) {
      if (!options.silent) showToast("另一项手动同步正在进行，请稍候再试。", "info");
      return false;
    }
    cloudSyncOperation.controller?.abort();
  }
  const operation = { type: "pull", silent: Boolean(options.silent), controller: null };
  cloudSyncOperation = operation;
  cloudSyncBusy = true;
  try {
    const payload = await cloudRequest("/state", { method: "GET" }, operation);
    if (!payload?.state && !payload?.cats && !payload?.users) {
      if (!options.silent) showToast("云端暂无数据。确认本机资料无误后，可手动点击“推送到云端”。", "info", 5600);
      return false;
    }
    const remoteState = normalizeState(payload?.state || payload);
    const remoteTime = new Date(remoteState.meta?.updatedAt || 0).getTime();
    const localTime = new Date(state.meta?.updatedAt || 0).getTime();
    if (remoteTime > localTime) {
      state = remoteState;
      localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
      saveIndicator.textContent = `已同步 ${timeText(new Date())}`;
      render();
      if (!options.silent) showToast("已从云端拉取最新资料。", "success");
    } else if (!options.silent) {
      showToast("本机已经是最新数据，云端没有更新内容。", "info");
    }
    return true;
  } catch (error) {
    if (!options.silent) showToast(cloudErrorMessage(error, "云端同步失败"), "error", 6200);
    return false;
  } finally {
    if (cloudSyncOperation === operation) {
      cloudSyncOperation = null;
      cloudSyncBusy = false;
    }
  }
}

async function pushCloudState(options = {}) {
  if (!cloudSyncEnabled()) {
    if (!options.silent) showToast("请先开启联网同步并填写完整的地址和密钥。", "error");
    return false;
  }
  if (cloudSyncOperation) {
    if (options.silent || !cloudSyncOperation.silent) {
      if (!options.silent) showToast("另一项手动同步正在进行，请稍候再试。", "info");
      return false;
    }
    cloudSyncOperation.controller?.abort();
  }
  const operation = { type: "push", silent: Boolean(options.silent), controller: null };
  cloudSyncOperation = operation;
  cloudSyncBusy = true;
  try {
    await cloudRequest("/state", {
      method: "PUT",
      body: JSON.stringify({ state })
    }, operation);
    saveIndicator.textContent = `已云端同步 ${timeText(new Date())}`;
    if (!options.silent) showToast("本机资料已安全推送到云端。", "success");
    return true;
  } catch (error) {
    if (!options.silent) showToast(cloudErrorMessage(error, "云端保存失败"), "error", 6200);
    return false;
  } finally {
    if (cloudSyncOperation === operation) {
      cloudSyncOperation = null;
      cloudSyncBusy = false;
    }
  }
}

async function saveCloudSyncSettings(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const enabled = form.elements.enabled.value === "true";
  saveCloudConfig({
    enabled,
    endpoint: form.elements.endpoint.value,
    token: form.elements.token.value,
    verifiedAt: ""
  });
  render();
  if (!enabled) {
    showToast("已保存设置，并关闭联网同步。", "success");
    return;
  }
  if (!cloudSyncEnabled()) {
    showToast("设置已保存，但同步地址或密钥尚未填写完整。", "error", 5600);
    return;
  }
  showToast("设置已保存，正在验证阿里云连接…", "info");
  try {
    await cloudRequest("/state", { method: "GET" });
    const config = readCloudConfig();
    saveCloudConfig({ ...config, verifiedAt: new Date().toISOString() });
    render();
    showToast("阿里云连接验证成功。资料不会自动上传，请按需手动推送。", "success", 5600);
  } catch (error) {
    showToast(`设置已保存，但连接验证失败：${cloudErrorMessage(error, "请检查地址和密钥")}`, "error", 6800);
  }
}

function saveLoginCloudSyncSettings(event) {
  event.preventDefault();
  const form = event.currentTarget;
  saveCloudConfig({
    enabled: Boolean(form.elements.enabled.checked),
    endpoint: form.elements.endpoint.value,
    token: form.elements.token.value
  });
  authNotice = cloudSyncEnabled() ? "阿里云同步设置已保存，正在尝试从云端拉取最新数据。" : "已关闭联网同步，当前使用本机数据。";
  render();
  if (cloudSyncEnabled()) {
    pullCloudState({ silent: false });
  }
}

function seedState() {
  const today = new Date();
  const day = 24 * 60 * 60 * 1000;
  return {
    cats: [
      {
        id: crypto.randomUUID(),
        catName: "布丁",
        breed: "英短银渐层",
        age: "2 岁",
        gender: "妹妹",
        temperament: "亲人，怕吹风机",
        ownerName: "林小姐",
        phone: "138 0000 1024",
        emergencyContact: "微信同号",
        roomType: "自在间",
        room: "VIC05",
        status: "入住中",
        checkInDate: isoDate(new Date(today - 3 * day)),
        expectedCheckoutDate: isoDate(new Date(today.getTime() + 2 * day)),
        dailyRate: 288 * 5,
        deposit: 500,
        paymentStatus: "仅押金",
        feedingPlan: "早晚各一次，主食罐半罐 + 干粮 35g",
        healthNotes: "肠胃敏感，不吃冻干",
        lastUpdated: today.toISOString()
      },
      {
        id: crypto.randomUUID(),
        catName: "拿铁",
        breed: "布偶",
        age: "4 岁",
        gender: "弟弟",
        temperament: "慢热，喜欢高处",
        ownerName: "陈先生",
        phone: "139 0000 2048",
        emergencyContact: "陈太太 136 0000 3321",
        roomType: "大方间",
        room: "VIC01",
        status: "入住中",
        checkInDate: isoDate(new Date(today - day)),
        expectedCheckoutDate: isoDate(new Date(today.getTime() + 6 * day)),
        dailyRate: 328 * 7,
        deposit: 700,
        paymentStatus: "已结清",
        feedingPlan: "只吃自带粮，每餐 40g",
        healthNotes: "每天梳毛，注意泪痕",
        lastUpdated: today.toISOString()
      },
      {
        id: crypto.randomUUID(),
        catName: "芝麻",
        breed: "田园猫",
        age: "8 个月",
        gender: "妹妹",
        temperament: "活泼，好奇心强",
        ownerName: "王小姐",
        phone: "137 0000 7788",
        emergencyContact: "妈妈 135 0000 8899",
        roomType: "二段间-S",
        room: "RM05",
        status: "预约",
        checkInDate: isoDate(new Date(today.getTime() + 4 * day)),
        expectedCheckoutDate: isoDate(new Date(today.getTime() + 11 * day)),
        dailyRate: 168 * 7,
        deposit: 200,
        paymentStatus: "仅押金",
        feedingPlan: "幼猫粮，一日三餐",
        healthNotes: "首次寄养，需多观察",
        lastUpdated: today.toISOString()
      }
    ],
    reports: [
      {
        id: crypto.randomUUID(),
        date: today.toISOString(),
        shift: "早班",
        staffName: "Mia",
        temperature: "24.5C",
        humidity: "48%",
        feedingDone: true,
        litterDone: true,
        cleaningDone: true,
                medicineDone: false,
                afternoonTeaDone: true,
                roomType: "自在间",
                room: "VIC05",
                occupancyNote: "A 区 3 间，B 区 2 间，隔离房空置",
        incidents: "布丁上午软便一次，已备注并观察。",
        handover: "晚班重点看布丁饮水和排便；拿铁 20:00 梳毛。",
        todo: "补拍今日入住照，检查 A05 摄像头。",
        createdAt: today.toISOString()
      }
    ],
    ledger: [
      { id: crypto.randomUUID(), date: today.toISOString(), kind: "收入", category: "住宿费", amount: 1316, method: "微信", relatedCatName: "拿铁", operatorName: "Mia", note: "7 晚一次结清" },
      { id: crypto.randomUUID(), date: today.toISOString(), kind: "收入", category: "押金", amount: 500, method: "支付宝", relatedCatName: "布丁", operatorName: "Mia", note: "入住押金" },
      { id: crypto.randomUUID(), date: today.toISOString(), kind: "支出", category: "耗材", amount: 126, method: "企业卡", relatedCatName: "", operatorName: "Mia", note: "猫砂补货" }
    ],
    appointments: seedAppointments(),
    schedules: seedSchedules(),
    attendance: [],
    documents: seedDocuments(),
    readReceipts: [],
    printReceipts: [],
    users: seedUsers(),
    customers: seedCustomers(),
    inventory: seedInventory(),
    cleaningTasks: seedCleaningTasks(),
    reminders: seedReminders(),
    wechatLogs: [],
    wechatConfig: { targetName: "员工工作群", autoOpen: true }
  };
}

function seedUsers() {
  const now = new Date().toISOString();
  return [
    {
      id: crypto.randomUUID(),
      role: "admin",
      name: "Admin",
      username: "admin",
      phone: "",
      gender: "未填写",
      birthDate: "",
      avatar: "",
      idNumber: "",
      hireDate: "",
      emergencyPhone: "",
      employeeStatus: "全职",
      position: "管理员",
      pin: "0000",
      status: "active",
      createdAt: now,
      approvedAt: now,
      approvedBy: "system",
      note: "默认管理员账号"
    },
    {
      id: crypto.randomUUID(),
      role: "staff",
      name: "Mia",
      username: "mia",
      phone: "",
      gender: "未填写",
      birthDate: "",
      avatar: "",
      idNumber: "330300000000000000",
      hireDate: isoDate(new Date()),
      emergencyPhone: "138 0000 0000",
      employeeStatus: "全职",
      position: "前台接待",
      pin: "1111",
      status: "active",
      createdAt: now,
      approvedAt: now,
      approvedBy: "Admin",
      note: "演示员工账号"
    }
  ];
}

function seedCustomers() {
  const today = new Date();
  return [
    {
      id: crypto.randomUUID(),
      customerName: "林小姐",
      idNumber: "330302199901010000",
      phone: "138 0000 1024",
      emergencyPhone: "138 0000 1025",
      checkInDate: isoDate(addDays(today, -3)),
      cats: [normalizeCatProfile({
        name: "布丁",
        gender: "女",
        ageMonthsAtFirstRegistration: 24,
        breed: "英短银渐层",
        coatColor: "银白",
        vaccineDate: "",
        neuterStatus: "已绝育",
        specialNotes: "肠胃敏感",
        medicationNotes: "",
        feeding: { dryFoodGrams: 70, wetFoodGrams: 80, supplements: "", dietNotes: "不吃冻干" },
        firstStayDate: isoDate(addDays(today, -3)),
        pastStayDates: [isoDate(addDays(today, -3))],
        spendingHistory: ["2026-06 住宿押金 500"],
        roomServiceOrders: ["梳毛护理 1 次"]
      })],
      createdAt: new Date().toISOString()
    }
  ];
}

function seedCleaningTasks() {
  return [
    { id: crypto.randomUUID(), cadence: "每日", area: "客房", content: "猫砂盆、地面、饮水碗、食碗清洁消毒", time: "10:30", sendTime: "10:00", enabled: true },
    { id: crypto.randomUUID(), cadence: "每日", area: "公共区", content: "前台地面、空气净化器滤网外部擦拭", time: "14:00", sendTime: "13:30", enabled: true },
    { id: crypto.randomUUID(), cadence: "每周", area: "房间深清", content: "空房客房深度消杀、软垫检查、摄像头擦拭", time: "周一 11:00", sendTime: "10:30", enabled: true },
    { id: crypto.randomUUID(), cadence: "每月", area: "仓储", content: "猫砂、粮食、药品、清洁耗材盘点", time: "每月1日 16:00", sendTime: "15:30", enabled: true }
  ];
}

function seedReminders() {
  return [
    { id: crypto.randomUUID(), type: "上班提醒", time: "08:00", sendTime: "08:00", content: "今日 08:30 上班，请提前确认门店钥匙、排班和预约。", enabled: true },
    { id: crypto.randomUUID(), type: "卫生提醒", time: "10:30", sendTime: "10:30", content: "请完成今日固定卫生区域打扫并在日报中交接。", enabled: true }
  ];
}

function seedAppointments() {
  const today = new Date();
  return [
    {
      id: crypto.randomUUID(),
      date: isoDate(today),
      time: "15:30",
      catName: "糯米",
      ownerName: "赵小姐",
      phone: "136 0000 1234",
      service: "入住看房",
      roomPreference: "安静房",
      status: "已确认",
      note: "第一次寄养，想看 B 区房间"
    },
    {
      id: crypto.randomUUID(),
      date: isoDate(addDays(today, 2)),
      time: "11:00",
      catName: "小满",
      ownerName: "周先生",
      phone: "135 0000 5678",
      service: "预约入住",
      roomPreference: "阳光房",
      status: "待确认",
      note: "两只猫可能同行，需要确认空房"
    }
  ];
}

function seedSchedules() {
  const today = new Date();
  return [
    { id: crypto.randomUUID(), date: isoDate(today), employeeName: "Mia", status: "上班", shiftStart: "09:30", shiftEnd: "18:30", role: "前台接待", note: "早班交接、预约接待" },
    { id: crypto.randomUUID(), date: isoDate(today), employeeName: "Ava", status: "休息", shiftStart: "", shiftEnd: "", role: "护理师", note: "休息" },
    { id: crypto.randomUUID(), date: isoDate(addDays(today, 1)), employeeName: "Ava", status: "上班", shiftStart: "13:00", shiftEnd: "22:00", role: "护理师", note: "晚班喂养与清洁" }
  ];
}

function seedDocuments() {
  const now = new Date().toISOString();
  return [
    {
      id: crypto.randomUUID(),
      title: "猫咪寄养合同模板",
      category: "寄养合同",
      fileName: "roomie-boarding-contract.txt",
      mimeType: "text/plain",
      size: 1260,
      uploadedBy: "Admin",
      updatedAt: now,
      contentText: "Roomie 猫咪寄养合同模板\\n\\n一、寄养信息：猫咪姓名、年龄、品种、健康情况、入住日期、预计退房日期。\\n二、护理约定：喂养计划、用药要求、异常联系流程。\\n三、费用约定：房费、押金、加项服务与结算方式。\\n四、风险告知：应激反应、既往病史、紧急就医授权。\\n\\n员工只能查看、确认已读和打印，正式合同以管理员上传版本为准。",
      dataUrl: ""
    },
    {
      id: crypto.randomUUID(),
      title: "员工入职合同与守则",
      category: "员工合同",
      fileName: "roomie-staff-guide.txt",
      mimeType: "text/plain",
      size: 980,
      uploadedBy: "Admin",
      updatedAt: now,
      contentText: "Roomie 员工守则\\n\\n1. 到店后完成交接，确认入住猫咪状态。\\n2. 喂食、清洁、用药必须在门店日报中记录。\\n3. 不私自修改价格、合同、账目。\\n4. 发现异常时先安抚猫咪，再通知管理员和主人。",
      dataUrl: ""
    },
    {
      id: crypto.randomUUID(),
      title: "门店最新价目表",
      category: "价目表",
      fileName: "roomie-price-list.txt",
      mimeType: "text/plain",
      size: 760,
      uploadedBy: "Admin",
      updatedAt: now,
      contentText: "Roomie 门店价目表\\n\\n安静房：168 元/晚\\n阳光房：188 元/晚\\n套房：228 元/晚\\n梳毛护理：38 元/次\\n喂药服务：20 元/次\\n节假日价格以管理员发布版本为准。",
      dataUrl: ""
    }
  ];
}

function seedInventory() {
  const today = new Date();
  return [
    {
      id: crypto.randomUUID(),
      name: "主食罐 85g 鸡肉",
      category: "下午茶罐头",
      quantity: 42,
      unit: "罐",
      purchaseDate: isoDate(addDays(today, -12)),
      expiryDate: isoDate(addDays(today, 75)),
      safetyStock: 18,
      unitCost: 7.8,
      supplier: "固定供货商",
      location: "仓储 A 架",
      note: "下午茶优先用近效期批次",
      updatedAt: today.toISOString(),
      updatedBy: "Admin"
    },
    {
      id: crypto.randomUUID(),
      name: "豆腐猫砂",
      category: "门店消耗物资",
      quantity: 9,
      unit: "袋",
      purchaseDate: isoDate(addDays(today, -20)),
      expiryDate: "",
      safetyStock: 10,
      unitCost: 38,
      supplier: "耗材供应商",
      location: "后仓",
      note: "低于 10 袋需补货",
      updatedAt: today.toISOString(),
      updatedBy: "Admin"
    },
    {
      id: crypto.randomUUID(),
      name: "猫抓板",
      category: "猫咪零售商品",
      quantity: 16,
      unit: "件",
      purchaseDate: isoDate(addDays(today, -8)),
      expiryDate: "",
      safetyStock: 6,
      unitCost: 12,
      supplier: "零售商品供应商",
      location: "前台展示柜",
      note: "可售商品",
      updatedAt: today.toISOString(),
      updatedBy: "Admin"
    }
  ];
}

function render() {
  if (!session) {
    appShell.classList.add("is-hidden");
    loginScreen.classList.remove("is-hidden");
    loginScreen.innerHTML = renderLogin();
    bindLoginEvents();
    return;
  }

  loginScreen.classList.add("is-hidden");
  appShell.classList.remove("is-hidden");

  const allowed = allowedViews();
  if (!allowed.includes(currentView)) currentView = "dashboard";

  document.querySelectorAll(".nav-item").forEach((button) => {
    const isAllowed = allowed.includes(button.dataset.view);
    button.classList.toggle("is-hidden", !isAllowed);
    button.classList.toggle("active", button.dataset.view === currentView);
  });

  if (currentView === "dashboard") screen.innerHTML = renderDashboard();
  if (currentView === "profile") screen.innerHTML = renderProfile();
  if (currentView === "calendar") screen.innerHTML = renderCalendar();
  if (currentView === "rooms") screen.innerHTML = renderRooms();
  if (currentView === "customers") screen.innerHTML = renderCustomers();
  if (currentView === "cats") screen.innerHTML = renderCats();
  if (currentView === "reports") screen.innerHTML = renderReports();
  if (currentView === "roster") screen.innerHTML = renderRoster();
  if (currentView === "docs") screen.innerHTML = renderDocs();
  if (currentView === "inventory") screen.innerHTML = renderInventory();
  if (currentView === "reminders") screen.innerHTML = renderReminderManager();
  if (currentView === "accounts") screen.innerHTML = renderAccounts();
  if (currentView === "ledger") screen.innerHTML = renderLedger();

  bindViewEvents();
}

function allowedViews() {
  return roleViews[session?.role || "staff"];
}

function renderLogin() {
  return `
    <div class="login-card">
      <div class="login-brand">
        <div>
          <img class="login-logo" src="roomie-logo.png" alt="Roomie">
          <h1>Roomie</h1>
          <p>猫咪寄养酒店内部后台</p>
        </div>
        <div class="stack">
          <span class="pill teal">员工注册需管理员审核</span>
          <span class="pill teal">管理员可维护账号权限</span>
        </div>
      </div>
      <div class="login-form">
        <div>
          <h2>${authTitle()}</h2>
          <p>${authSubtitle()}</p>
        </div>
        <div class="auth-tabs">
          ${authTab("login", "登录")}
          ${authTab("staffRegister", "员工注册")}
          ${authTab("adminRegister", "管理员注册")}
        </div>
        ${renderLoginCloudSyncBox()}
        ${authNotice ? `<div class="auth-notice">${escapeHtml(authNotice)}</div>` : ""}
        ${loginMode === "login" ? renderLoginForm() : ""}
        ${loginMode === "staffRegister" ? renderStaffRegisterForm() : ""}
        ${loginMode === "adminRegister" ? renderAdminRegisterForm() : ""}
      </div>
    </div>
  `;
}

function authTitle() {
  if (loginMode === "staffRegister") return "员工注册申请";
  if (loginMode === "adminRegister") return "管理员注册";
  return "登录操作台";
}

function authSubtitle() {
  if (loginMode === "staffRegister") return "提交后需管理员审核，通过后才能登录";
  if (loginMode === "adminRegister") return "需要管理员注册码，用于创建后台管理账号";
  return "使用已审核账号进入对应工作视图";
}

function authTab(mode, title) {
  return `<button class="auth-tab ${loginMode === mode ? "active" : ""}" type="button" data-auth-mode="${mode}">${title}</button>`;
}

function renderLoginCloudSyncBox() {
  const config = readCloudConfig();
  return `
    <form id="loginCloudSyncForm" class="sync-box">
      <strong>阿里云联网同步</strong>
      <span>${escapeHtml(cloudStatusText())}</span>
      <label>同步地址<input name="endpoint" value="${escapeAttr(config.endpoint)}" placeholder="http://服务器公网IP:8787"></label>
      <label>同步密钥<input name="token" type="password" value="${escapeAttr(config.token)}" placeholder="管理员提供"></label>
      <label class="remember-line"><input name="enabled" type="checkbox" ${config.enabled ? "checked" : ""}>启用联网同步</label>
      <div class="actions">
        <button class="plain-button" type="submit">${icon("check")}<span>保存并同步</span></button>
      </div>
    </form>
  `;
}

function renderLoginForm() {
  return `
    <form id="loginForm" class="auth-form">
      <div class="role-switch">
        ${roleOption("staff", "员工登录", "预约、入住、日报")}
        ${roleOption("admin", "管理员登录", "全部视图与账本")}
      </div>
      ${loginRole === "staff" ? `
        <div class="sync-box">
          <strong>跨电脑登录</strong>
          <span>如果管理员已审核通过，请先导入管理员发来的员工登录文件，再输入账号和 PIN。</span>
          <label class="plain-button file-button">
            ${icon("files")}<span>导入员工登录文件</span>
            <input id="staffLoginImportInput" type="file" accept="application/json,.json">
          </label>
        </div>
      ` : ""}
      <label>账号<input name="username" required placeholder="例如：mia"></label>
      <label>PIN<input name="pin" required type="password" inputmode="numeric" placeholder="输入 PIN"></label>
      ${loginRole === "admin" ? `<label class="remember-line"><input name="rememberAdmin" type="checkbox">30 天自动登录此设备</label>` : ""}
      <div class="login-error" id="loginError"></div>
      <button class="primary-button" type="submit">${icon("user")}<span>登录</span></button>
    </form>
  `;
}

function renderStaffRegisterForm() {
  return `
    <form id="staffRegisterForm" class="auth-form">
      <label>姓名<input name="name" required placeholder="例如：Mia"></label>
      <label>登录账号<input name="username" required placeholder="建议用英文或工号"></label>
      <label>手机号<input name="phone" placeholder="用于管理员确认身份"></label>
      <label>岗位<input name="position" placeholder="前台、护理师、店长助理"></label>
      <label>设置 PIN<input name="pin" required minlength="4" type="password" inputmode="numeric" placeholder="至少 4 位"></label>
      <label>申请备注<textarea name="note" rows="3" placeholder="入职日期、所属门店或说明"></textarea></label>
      <div class="sync-box">
        <strong>跨电脑审核说明</strong>
        <span>${cloudSyncEnabled() ? "已启用联网同步，提交后管理员会自动看到待审核申请。" : "尚未启用联网同步。提交后会下载员工申请文件，请把该文件发给管理员导入审核。"}</span>
      </div>
      <div class="login-error" id="staffRegisterError"></div>
      <button class="primary-button" type="submit">${icon("plus")}<span>提交审核</span></button>
    </form>
  `;
}

function renderAdminRegisterForm() {
  return `
    <form id="adminRegisterForm" class="auth-form">
      <label>管理员姓名<input name="name" required placeholder="例如：店长"></label>
      <label>登录账号<input name="username" required placeholder="例如：admin2"></label>
      <label>设置 PIN<input name="pin" required minlength="4" type="password" inputmode="numeric" placeholder="至少 4 位"></label>
      <label>管理员注册码<input name="inviteCode" required placeholder="请输入管理员注册码"></label>
      <div class="login-error" id="adminRegisterError"></div>
      <button class="primary-button" type="submit">${icon("plus")}<span>注册管理员</span></button>
    </form>
  `;
}

function roleOption(role, title, detail) {
  return `<button class="role-option ${loginRole === role ? "active" : ""}" type="button" data-role="${role}"><strong>${title}</strong><span>${detail}</span></button>`;
}

function bindLoginEvents() {
  document.querySelectorAll("[data-auth-mode]").forEach((button) => {
    button.addEventListener("click", () => {
      loginMode = button.dataset.authMode;
      authNotice = "";
      render();
    });
  });

  document.querySelectorAll("[data-role]").forEach((button) => {
    button.addEventListener("click", () => {
      loginRole = button.dataset.role;
      render();
    });
  });

  const loginForm = document.getElementById("loginForm");
  if (loginForm) loginForm.addEventListener("submit", handleLogin);

  const loginCloudSyncForm = document.getElementById("loginCloudSyncForm");
  if (loginCloudSyncForm) loginCloudSyncForm.addEventListener("submit", saveLoginCloudSyncSettings);

  const staffLoginImportInput = document.getElementById("staffLoginImportInput");
  if (staffLoginImportInput) staffLoginImportInput.addEventListener("change", importStaffSyncFile);

  const staffRegisterForm = document.getElementById("staffRegisterForm");
  if (staffRegisterForm) staffRegisterForm.addEventListener("submit", handleStaffRegister);

  const adminRegisterForm = document.getElementById("adminRegisterForm");
  if (adminRegisterForm) adminRegisterForm.addEventListener("submit", handleAdminRegister);
}

function handleLogin(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const username = normalizeUsername(form.elements.username.value);
  const pin = form.elements.pin.value.trim();
  const user = findUser(username);
  const error = document.getElementById("loginError");

  if (!user || user.role !== loginRole || user.pin !== pin) {
    error.textContent = "账号或 PIN 不正确";
    return;
  }
  if (user.status === "pending") {
    error.textContent = "该员工账号仍在等待管理员审核";
    return;
  }
  if (user.status === "rejected") {
    error.textContent = "该员工申请已被拒绝，请联系管理员";
    return;
  }
  if (user.status === "paused") {
    error.textContent = "该员工账号已暂停，请联系管理员";
    return;
  }
  if (user.status !== "active") {
    error.textContent = "账号状态不可登录";
    return;
  }

  const rememberDays = user.role === "admin" && form.elements.rememberAdmin?.checked ? 30 : 0;
  setSession({ role: user.role, name: user.name, username: user.username, loginAt: new Date().toISOString() }, rememberDays);
  currentView = "dashboard";
  authNotice = "";
  render();
}

function handleStaffRegister(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const username = normalizeUsername(form.elements.username.value);
  const error = document.getElementById("staffRegisterError");

  if (usernameTaken(username)) {
    error.textContent = "这个登录账号已被使用";
    return;
  }

  const staffUser = normalizeUser({
    id: crypto.randomUUID(),
    role: "staff",
    name: form.elements.name.value.trim(),
    username,
    phone: form.elements.phone.value.trim(),
    position: form.elements.position.value.trim(),
    pin: form.elements.pin.value.trim(),
    status: "pending",
    createdAt: new Date().toISOString(),
    approvedAt: "",
    approvedBy: "",
    note: form.elements.note.value.trim()
  });
  state.users.unshift(staffUser);
  saveState();
  if (!cloudSyncEnabled()) downloadStaffApplication(staffUser);
  loginMode = "login";
  loginRole = "staff";
  authNotice = cloudSyncEnabled()
    ? "员工注册申请已提交到云端，请等待管理员审核通过后登录。"
    : "员工注册申请已提交，并已下载员工申请文件。请把该文件发给管理员导入审核；审核通过后，再导入管理员发回的员工登录文件。";
  render();
}

function handleAdminRegister(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const username = normalizeUsername(form.elements.username.value);
  const error = document.getElementById("adminRegisterError");

  if (form.elements.inviteCode.value.trim() !== ADMIN_INVITE_CODE) {
    error.textContent = "管理员注册码不正确";
    return;
  }
  if (usernameTaken(username)) {
    error.textContent = "这个登录账号已被使用";
    return;
  }

  state.users.unshift({
    id: crypto.randomUUID(),
    role: "admin",
    name: form.elements.name.value.trim(),
    username,
    phone: "",
    position: "管理员",
    pin: form.elements.pin.value.trim(),
    status: "active",
    createdAt: new Date().toISOString(),
    approvedAt: new Date().toISOString(),
    approvedBy: "invite-code",
    note: "管理员注册页面创建"
  });
  saveState();
  loginMode = "login";
  loginRole = "admin";
  authNotice = "管理员账号已创建，可以登录。";
  render();
}

function normalizeUsername(value) {
  return String(value || "").trim().toLowerCase();
}

function findUser(username) {
  return state.users.find((user) => user.username === normalizeUsername(username));
}

function usernameTaken(username) {
  return state.users.some((user) => user.username === normalizeUsername(username));
}

function renderHeader(title, subtitle, iconName, action = "") {
  const chip = session ? `<span class="user-chip">${roleText[session.role]} · ${escapeHtml(session.name)}</span>` : "";
  return `
    <div class="page-head">
      <div class="page-title">
        <div class="title-icon icon-${iconName}">${icon(iconName)}</div>
        <div>
          <h2>${title}</h2>
          <p>${subtitle}</p>
        </div>
      </div>
      <div class="actions">
        ${action}
        ${chip}
        <button class="plain-button" id="logoutButton" type="button">${icon("logout")}<span>退出</span></button>
      </div>
    </div>
  `;
}

function renderDashboard() {
  const activeCats = state.cats.filter((cat) => cat.status === "入住中");
  const soonCats = checkoutSoonCats();
  const pending = state.cats.filter((cat) => cat.status !== "已退房" && cat.paymentStatus !== "已结清");
  const latest = [...state.reports].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0];
  const todayAppointments = appointmentsOn(isoDate(new Date())).filter((item) => item.status !== "已取消");
  const todayWorking = schedulesOn(isoDate(new Date())).filter((item) => item.status === "上班");
  const todayIncome = sumLedger("收入", true);
  const inventoryAlerts = inventoryAlertCount();
  const stockAlerts = dashboardInventoryAlerts();

  const stats = [
    statCard("当前入住", `${activeCats.length} 只`, `${soonCats.length} 只猫咪明后天退房`, "house", "teal"),
    statCard("今日预约", `${todayAppointments.length} 个`, "预约日历自动汇总", "calendar", "blue"),
    statCard("今日上班", `${todayWorking.length} 人`, "来自排班表", "user", "gold"),
    statCard("库存提醒", `${inventoryAlerts} 项`, "低库存或临期物资", "box", inventoryAlerts ? "coral" : "teal")
  ];

  if (session.role === "admin") {
    const pendingUsers = state.users.filter((user) => user.role === "staff" && user.status === "pending");
    stats.push(statCard("今日收入", yuan(todayIncome), "管理员账本汇总", "card", "teal"));
    stats.push(statCard("待审员工", `${pendingUsers.length} 个`, "员工注册申请", "user", "gold"));
    stats.push(statCard("待跟进账款", `${pending.length} 单`, "未退房且未结清", "bell", "coral"));
  }

  return `
    ${renderHeader("今日门店总览", "入住、预约、交接实时汇总", "grid", `<div class="live-strip"><span id="liveDateTime">${dateTimeSecondText(new Date())}</span><span>温州鹿城区</span><span id="weatherText">${escapeHtml(weatherDisplayText())}</span></div>`)}
    <section class="mascot-hero">
      <div>
        <h3>${dashboardGreeting()}，${escapeHtml(session.name)}</h3>
        <p>${rooStatusLine()}</p>
      </div>
      ${rooMascotMarkup()}
    </section>
    <section class="panel urgent-panel">
      ${panelHead("今日强提醒", "bell")}
      <div class="reminder-grid">
        ${todayReminderItems().map(reminderItem).join("")}
      </div>
      ${session.role === "admin" ? `
        <form id="extraReminderForm" class="inline-reminder-form">
          <input name="content" placeholder="添加额外提醒事项，例如：今天重点检查 RM05 摄像头">
          <button class="primary-button" type="submit">${icon("plus")}<span>添加提醒</span></button>
          <button class="plain-button" type="button" id="openReminderManager">${icon("edit")}<span>编辑提醒</span></button>
          <button class="plain-button" type="button" id="sendWechatButton">${icon("upload")}<span>一键转发到微信</span></button>
        </form>
      ` : ""}
    </section>
    <section class="panel stock-alert-panel">
      <div class="panel-head-with-action">
        ${panelHead("库存补货预警", "box")}
        <button class="plain-button" type="button" id="openInventoryButton">${icon("box")}<span>查看库存</span></button>
      </div>
      <div class="stock-alert-list">
        ${stockAlerts.length ? stockAlerts.map(inventoryAlertCard).join("") : emptyBlock("库存状态正常", "目前没有低库存、临期或过期物资。")}
      </div>
    </section>
    ${dashboardRosterPanel()}
    <div class="stat-grid">${stats.join("")}</div>

    <div class="grid-two">
      <section class="panel">
        ${panelHead("今日日程", "calendar")}
        <div class="stack">
          ${todayAppointments.length ? todayAppointments.map(appointmentRowSmall).join("") : emptyBlock("今日暂无预约", "新增预约后会显示在这里。")}
        </div>
      </section>
      <section class="panel">
        ${panelHead("最新交接", "clipboard")}
        ${latest ? reportSummary(latest) : emptyBlock("还没有日报", "员工提交日报后会显示在这里。")}
      </section>
    </div>

    <section class="panel">
      ${panelHead("入住提醒", "bell")}
      <div class="stack">
        ${soonCats.length ? soonCats.map((cat) => reminderRow(cat)).join("") : emptyBlock("暂无临近退房", "有猫咪接近退房日期时会显示在这里。")}
      </div>
    </section>

    <section class="panel">
      ${panelHead("入住猫咪看板", "paw")}
      ${activeCats.length ? `<div class="cat-grid">${activeCats.map((cat) => catMiniCard(cat)).join("")}</div>` : emptyBlock("当前无入住猫咪", "在猫咪入住里登记后，这里会自动更新。")}
    </section>
  `;
}

function dashboardGreeting() {
  const hour = new Date().getHours();
  if (hour < 11) return "早安";
  if (hour < 14) return "中午好";
  if (hour < 19) return "下午好";
  return "晚上好";
}

function rooMood() {
  const now = new Date();
  const minutes = now.getHours() * 60 + now.getMinutes();
  if (minutes >= 8 * 60 + 30 && minutes < 13 * 60 + 30) return "morning";
  if (minutes >= 13 * 60 + 30 && minutes < 19 * 60 + 30) return "afternoon";
  return "quiet";
}

function rooStatusLine() {
  return "小 roo 在看板旁待命，今天先看预约、房态、库存预警和交接重点。";
}

function rooMascotMarkup() {
  return `
    <figure class="roo-stage" role="img" aria-label="小 roo 吉祥物">
      <img class="roo-image" src="roo-mascot-transparent.png" alt="">
    </figure>
  `;
}

function renderProfile() {
  const user = currentUser();
  if (!user) return "";
  return `
    ${renderHeader("我的账号", "编辑头像、用户名、生日、手机号和员工资料", "user")}
    <section class="panel profile-panel">
      <div class="profile-preview">
        ${avatarMarkup(user, "large")}
        <div>
          <h3>${escapeHtml(user.name)}</h3>
          <p>${roleText[user.role]} · ${escapeHtml(user.position || "未填岗位")} · ${accountStatusText(user.status)}</p>
        </div>
      </div>
      <form id="profileForm" class="form-panel">
        <div class="form-grid">
          <label>头像<input name="avatar" type="file" accept="image/*"></label>
          <label>用户名<input name="username" value="${escapeAttr(user.username)}" required></label>
          <label>姓名<input name="name" value="${escapeAttr(user.name)}" required></label>
          <label>性别<select name="gender">${genders.map((item) => `<option ${item === user.gender ? "selected" : ""}>${item}</option>`).join("")}</select></label>
          <label>出生日期<input name="birthDate" type="date" value="${escapeAttr(user.birthDate || "")}"></label>
          <label>手机号<input name="phone" value="${escapeAttr(user.phone || "")}"></label>
          <label>岗位<input name="position" value="${escapeAttr(user.position || "")}"></label>
          <label>修改 PIN<input name="pin" type="password" inputmode="numeric" value="${escapeAttr(user.pin || "")}"></label>
          ${user.role === "staff" ? `
            <label>身份证号<input name="idNumber" value="${escapeAttr(user.idNumber || "")}"></label>
            <label>入职日期<input name="hireDate" type="date" value="${escapeAttr(user.hireDate || "")}"></label>
            <label>紧急联系号码<input name="emergencyPhone" value="${escapeAttr(user.emergencyPhone || "")}"></label>
          ` : ""}
        </div>
        <div class="actions"><button class="primary-button" type="submit">${icon("check")}<span>保存账号资料</span></button></div>
      </form>
    </section>
  `;
}

function renderRooms() {
  const activeRooms = new Set(state.cats.filter((cat) => cat.status === "入住中").map((cat) => cat.room).filter(Boolean));
  return `
    ${renderHeader("房型房态", "按房间大到小、价格高至低排序", "house")}
    <section class="panel room-summary">
      ${roomTypes.map((type) => {
        const occupied = type.rooms.filter((room) => activeRooms.has(room)).length;
        return `
          <article class="room-type-card">
            <div class="row-title"><strong>${escapeHtml(type.type)}</strong>${type.emergency ? pill("应急用房", "coral") : pill(`${yuan(type.price)}/晚`, "blue")}</div>
            <div class="meta">${type.rooms.length} 间 · 当前入住 ${occupied} 间</div>
            <div class="room-chip-grid">
              ${type.rooms.map((room) => `<span class="room-chip ${activeRooms.has(room) ? "occupied" : ""}">${room}${activeRooms.has(room) ? " · 入住" : ""}</span>`).join("")}
            </div>
          </article>
        `;
      }).join("")}
    </section>
  `;
}

function renderCustomers() {
  const customers = [...state.customers].sort((a, b) => new Date(b.createdAt || b.checkInDate) - new Date(a.createdAt || a.checkInDate));
  return `
    ${renderHeader("客户信息登记", "按登记时间倒序，最新资料置顶", "user")}
    <section class="panel form-panel">
      ${panelHead("新增客户资料", "edit")}
      <form id="customerForm" class="form-panel">
        <div class="form-grid">
          <label>客户姓名<input name="customerName" required placeholder="林小姐"></label>
          <label>身份证号/外国人居住证号<input name="idNumber" placeholder="证件号码"></label>
          <label>客户手机号<input name="phone" required placeholder="手机号"></label>
          <label>紧急联系号码<input name="emergencyPhone" placeholder="紧急联系人手机号"></label>
          <label>本次入住时间<input name="checkInDate" type="date" value="${isoDate(new Date())}"></label>
        </div>
        <div class="family-cats-editor">
          ${[1, 2, 3].map((index) => customerCatFields(index)).join("")}
        </div>
        <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>保存客户资料</span></button></div>
      </form>
    </section>
    <section class="panel">
      ${panelHead("客户资料列表", "clipboard")}
      <div class="customer-list">
        ${customers.length ? customers.map(customerCard).join("") : emptyBlock("暂无客户资料", "新增客户后会显示在这里。")}
      </div>
    </section>
  `;
}

function customerCatFields(index) {
  const empty = normalizeCatProfile({ firstStayDate: isoDate(new Date()) });
  return catProfileFields(index, empty).replaceAll(`cat${index}`, `customerCat${index}`);
}

function renderCalendar() {
  const monthTitle = new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "long" }).format(calendarCursor);
  const selectedItems = appointmentsOn(selectedDate).sort((a, b) => (a.time || "").localeCompare(b.time || ""));
  return `
    ${renderHeader("预约日历", "记录看房、入住、退房和回访日程", "calendar")}
    <div class="calendar-layout">
      <section class="panel calendar-panel">
        <div class="calendar-head">
          <button class="plain-button" id="prevMonth" type="button">上月</button>
          <strong>${monthTitle}</strong>
          <button class="plain-button" id="nextMonth" type="button">下月</button>
        </div>
        <div class="weekday-row">
          <span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span>
        </div>
        <div class="calendar-grid">
          ${calendarDays(calendarCursor).map(dayCell).join("")}
        </div>
      </section>
      <aside class="panel schedule-panel">
        ${panelHead(`${dateText(selectedDate)} 日程`, "clipboard")}
        <form id="appointmentForm" class="form-panel">
          <div class="form-grid one-column">
            <label>日期<input name="date" type="date" value="${selectedDate}"></label>
            <label>时间<input name="time" type="time" value="10:00"></label>
            <label>猫名<input name="catName" required placeholder="糯米"></label>
            <label>主人<input name="ownerName" required placeholder="赵小姐"></label>
            <label>电话<input name="phone" placeholder="手机号"></label>
            <label>事项<input name="service" value="预约入住"></label>
            <label>房型偏好<input name="roomPreference" placeholder="阳光房、安静房"></label>
            <label>状态<select name="status">${appointmentStatuses.map((item) => `<option>${item}</option>`).join("")}</select></label>
          </div>
          <input name="note" placeholder="备注">
          <button class="primary-button" type="submit">${icon("plus")}<span>新增日程</span></button>
        </form>
        <div class="appointment-list">
          ${selectedItems.length ? selectedItems.map(appointmentRow).join("") : emptyBlock("当天暂无日程", "选择日期后新增预约。")}
        </div>
      </aside>
    </div>
  `;
}

function renderCats() {
  const query = catSearch.trim().toLowerCase();
  const cats = [...state.cats]
    .sort((a, b) => new Date(b.lastUpdated) - new Date(a.lastUpdated))
    .filter((cat) => {
      if (!query) return true;
      return [cat.catName, cat.ownerName, cat.phone, cat.room, cat.breed].some((value) => String(value || "").toLowerCase().includes(query));
    });

  return `
    ${renderHeader("猫咪入住登记", "支持一个家庭最多 3 只猫，自动计算猫咪年龄", "paw", `<button class="primary-button" id="addCatButton">${icon("plus")}<span>新增家庭入住</span></button>`)}
    <div class="toolbar">
      <div class="search">${icon("search")}<input id="catSearch" value="${escapeAttr(catSearch)}" placeholder="搜索猫名、主人、电话或房间"></div>
    </div>
    <div class="cat-list">
      ${cats.length ? cats.map((cat) => catRow(cat)).join("") : emptyBlock("没有匹配的猫咪", "换个关键词，或新增一条入住记录。")}
    </div>
  `;
}

function renderReports() {
  const reports = [...state.reports].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  return `
    ${renderHeader("门店日报", "员工每日记录环境、喂养、清洁、异常和交接事项", "clipboard")}
    <section class="panel form-panel">
      ${panelHead("提交今日状态", "edit")}
      <form id="reportForm" class="form-panel">
        <div class="form-grid">
          <label>班次<select name="shift"><option>早班</option><option>晚班</option><option>全天</option></select></label>
          <label>员工<input name="staffName" required value="${escapeAttr(session.name)}"></label>
          <label>温度<input name="temperature" placeholder="24.5C"></label>
          <label>湿度<input name="humidity" placeholder="48%"></label>
        </div>
        <div class="toggle-line">
          <label><input type="checkbox" name="feedingDone" checked>喂食完成</label>
          <label><input type="checkbox" name="litterDone" checked>猫砂清理完成</label>
          <label><input type="checkbox" name="cleaningDone" checked>客房清洁完成</label>
          <label><input type="checkbox" name="medicineDone">用药完成</label>
          <label><input type="checkbox" name="afternoonTeaDone">下午茶提供完成</label>
        </div>
        <div class="form-grid">
          <label>本次入住房型<select name="roomType">${roomTypes.map((item) => `<option>${item.type}</option>`).join("")}</select></label>
          <label>本次入住房号<select name="room">${roomOptions.map((item) => `<option>${item.room}</option>`).join("")}</select></label>
        </div>
        <input name="occupancyNote" placeholder="入住与房间情况">
        <input name="incidents" placeholder="异常情况">
        <input name="handover" placeholder="交接备注">
        <input name="todo" placeholder="待办事项">
        <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>提交日报</span></button></div>
      </form>
    </section>
    <section class="panel">
      ${panelHead("最近日报", "calendar")}
      <div class="report-list">
        ${reports.length ? reports.map(reportRow).join("") : emptyBlock("还没有门店日报", "提交第一条后会保存在本机。")}
      </div>
    </section>
  `;
}

function renderRoster() {
  const today = isoDate(new Date());
  const weekSchedules = weekScheduleList(today);
  const visibleSchedules = session.role === "admin"
    ? weekSchedules
    : weekSchedules.filter((item) => item.employeeName === session.name);
  const todaySchedules = schedulesOn(today);
  const myToday = schedulesOn(today).find((item) => item.employeeName === session.name);
  const myClock = state.attendance.find((item) => item.date === today && item.employeeName === session.name);
  const attendance = session.role === "admin"
    ? [...state.attendance]
    : state.attendance.filter((item) => item.employeeName === session.name);

  const adminForm = session.role === "admin" ? `
    <section class="panel form-panel">
      ${panelHead("新增排班", "edit")}
      <form id="scheduleForm" class="form-panel">
        <div class="form-grid">
          <label>日期<input name="date" type="date" value="${today}"></label>
          <label>员工<input name="employeeName" required placeholder="员工姓名"></label>
          <label>状态<select name="status"><option>上班</option><option>休息</option></select></label>
          <label>岗位<input name="role" placeholder="前台、护理师"></label>
          <label>开始<input name="shiftStart" type="time" value="09:30"></label>
          <label>结束<input name="shiftEnd" type="time" value="18:30"></label>
        </div>
        <input name="note" placeholder="备注">
        <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>保存排班</span></button></div>
      </form>
    </section>
  ` : "";

  return `
    ${renderHeader("排班考勤", "员工排班、到店记录和今日提醒", "user")}
    ${renderRosterTemplatePanel()}
    <div class="grid-two">
      <section class="panel">
        ${panelHead("今日排班", "calendar")}
        <div class="stack">
          ${todaySchedules.length ? todaySchedules.map(scheduleRow).join("") : emptyBlock("今日暂无排班", "管理员新增后会显示在这里。")}
        </div>
      </section>
      <section class="panel">
        ${panelHead("我的到店状态", "user")}
        ${myToday ? `
          <div class="row-card">
            <div class="row-title"><strong>${escapeHtml(myToday.employeeName)}</strong>${pill(myToday.status, myToday.status === "上班" ? "teal" : "gold")}</div>
            <p>${myToday.status === "上班" ? `${escapeHtml(myToday.shiftStart)} - ${escapeHtml(myToday.shiftEnd)} · ${escapeHtml(myToday.role || "未填岗位")}` : "今日休息"}</p>
            ${myClock ? `<p class="meta">已记录到店：${escapeHtml(myClock.time)} · ${escapeHtml(myClock.source)}</p>` : `<button class="primary-button" id="autoClockButton" type="button">${icon("check")}<span>模拟到店记录</span></button>`}
          </div>
        ` : emptyBlock("今天没有你的排班", "管理员排班后会自动出现在这里。")}
      </section>
    </div>
    ${adminForm}
    <section class="panel">
      ${panelHead("排班表", "clipboard")}
      <div class="report-list">
        ${visibleSchedules.length ? visibleSchedules.sort((a, b) => a.date.localeCompare(b.date)).map(scheduleRow).join("") : emptyBlock("暂无排班", "管理员新增排班后显示。")}
      </div>
    </section>
    <section class="panel">
      ${panelHead("到店记录", "check")}
      <div class="report-list">
        ${attendance.length ? attendance.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)).map(attendanceRow).join("") : emptyBlock("暂无到店记录", "手机端打开 App 并到达门店范围后记录。")}
      </div>
    </section>
  `;
}

function renderRosterTemplatePanel() {
  const mode = activeRosterMode();
  return `
    <section class="panel roster-template-panel">
      <div class="panel-head-with-action">
        ${panelHead(`自动排班表 · ${mode.name}`, "calendar")}
        <span class="pill ${mode.key === "high" ? "coral" : "teal"}">当前入住 ${mode.count} 只 · ${mode.range}</span>
      </div>
      ${rosterTemplateTable(mode)}
    </section>
  `;
}

function rosterTemplateTable(mode) {
  return `
    <div class="roster-template-table">
      <div class="roster-template-title">${escapeHtml(mode.name)}（${escapeHtml(mode.range)}）</div>
      <div class="roster-template-head">星期</div>
      ${rosterEmployees.map((employee, index) => `<div class="roster-template-head employee-${index}">${escapeHtml(employee)}</div>`).join("")}
      ${mode.rows.map((row) => `
        <div class="roster-day">${escapeHtml(row[0])}</div>
        ${row.slice(1).map((shiftKey) => `<div class="roster-shift shift-${shiftKey}">${escapeHtml(rosterShiftText[shiftKey])}</div>`).join("")}
      `).join("")}
    </div>
  `;
}

function renderDocs() {
  const adminUpload = session.role === "admin" ? `
    <section class="panel form-panel">
      ${panelHead("上传资料", "upload")}
      <form id="documentForm" class="form-panel">
        <div class="form-grid">
          <label>标题<input name="title" required placeholder="例如：春节价目表"></label>
          <label>分类<select name="category"><option>寄养合同</option><option>员工合同</option><option>价目表</option><option>护理标准</option><option>门店制度</option></select></label>
          <label class="wide-label">文件<input name="file" required type="file" accept=".pdf,.txt,.md,.csv,.png,.jpg,.jpeg,.webp,.numbers,.pages,.doc,.docx,.xls,.xlsx"></label>
        </div>
        <div class="actions"><button class="primary-button" type="submit">${icon("upload")}<span>上传到资料库</span></button></div>
      </form>
    </section>
  ` : "";

  return `
    ${renderHeader("内部资料", "寄养合同、员工合同、价目表和门店制度", "files")}
    ${adminUpload}
    <section class="panel">
      ${panelHead("资料列表", "files")}
      <div class="doc-list">
        ${state.documents.length ? state.documents.map(documentCard).join("") : emptyBlock("暂无资料", "管理员上传后会显示在这里。")}
      </div>
    </section>
    <div id="printPayload" class="print-payload"></div>
  `;
}

function renderInventory() {
  const items = [...state.inventory].sort(inventorySort);
  const lowStock = items.filter((item) => inventoryStatus(item).status === "低库存").length;
  const expiring = items.filter((item) => inventoryStatus(item).status === "临期").length;
  const expired = items.filter((item) => inventoryStatus(item).status === "已过期").length;
  const totalValue = items.reduce((sum, item) => sum + Number(item.quantity || 0) * Number(item.unitCost || 0), 0);

  return `
    ${renderHeader("物资库存", "门店商品、消耗物资和下午茶罐头管理", "box")}
    <div class="stat-grid">
      ${statCard("库存品类", `${items.length} 项`, "当前登记的库存记录", "box", "blue")}
      ${statCard("低库存", `${lowStock} 项`, "低于安全库存线", "bell", lowStock ? "coral" : "teal")}
      ${statCard("临期/过期", `${expiring + expired} 项`, "30 天内到期或已过期", "calendar", expiring + expired ? "coral" : "teal")}
      ${statCard("库存估值", yuan(totalValue), "数量乘以单件成本", "wallet", "gold")}
    </div>

    <section class="panel form-panel">
      ${panelHead("新增库存记录", "plus")}
      <form id="inventoryForm" class="form-panel">
        <div class="form-grid">
          <label>名称<input name="name" required placeholder="例如：主食罐 85g 鸡肉"></label>
          <label>类别<select name="category">${inventoryCategories.map((item) => `<option>${item}</option>`).join("")}</select></label>
          <label>数量<input name="quantity" type="number" min="0" step="0.1" required placeholder="0"></label>
          <label>单位<select name="unit">${inventoryUnits.map((item) => `<option>${item}</option>`).join("")}</select></label>
          <label>进货/记录日期<input name="purchaseDate" type="date" value="${isoDate(new Date())}"></label>
          <label>保质期/到期日<input name="expiryDate" type="date"></label>
          <label>安全库存<input name="safetyStock" type="number" min="0" step="0.1" placeholder="低于此数量提醒"></label>
          <label>单件成本<input name="unitCost" type="number" min="0" step="0.01" placeholder="用于估值"></label>
          <label>供应商<input name="supplier" placeholder="供货商、平台或采购渠道"></label>
          <label>存放位置<input name="location" placeholder="前台展示柜、后仓、冰箱"></label>
        </div>
        <input name="note" placeholder="备注，例如：优先使用近效期批次、月底盘点">
        <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>保存库存</span></button></div>
      </form>
    </section>

    <section class="panel">
      ${panelHead("库存列表", "box")}
      <div class="inventory-list">
        ${items.length ? items.map(inventoryRow).join("") : emptyBlock("暂无库存记录", "新增商品、消耗物资或下午茶罐头后会显示在这里。")}
      </div>
    </section>
  `;
}

function renderReminderManager() {
  if (session.role !== "admin") return "";
  const logs = [...state.wechatLogs].sort((a, b) => new Date(b.sentAt) - new Date(a.sentAt)).slice(0, 20);
  const targetName = state.wechatConfig?.targetName || "员工工作群";
  const autoOpen = state.wechatConfig?.autoOpen !== false;

  return `
    ${renderHeader("提醒管理", "编辑今日强提醒、固定卫生任务和普通微信转发", "bell", `<button class="primary-button" id="sendWechatManagerButton" type="button">${icon("upload")}<span>一键转发到微信</span></button>`)}
    <section class="panel form-panel">
      ${panelHead("普通微信转发设置", "upload")}
      <form id="wechatConfigForm" class="form-panel">
        <label class="wide-label">默认转发对象备注
          <input name="targetName" value="${escapeAttr(targetName)}" placeholder="例如：员工工作群 / 早班群">
        </label>
        <label class="remember-line"><input name="autoOpen" type="checkbox" ${autoOpen ? "checked" : ""}>到达提醒发送时间时，自动复制提醒文案并打开微信</label>
        <p class="helper-text">普通微信不开放后台静默发群消息。Roomie 会复制提醒内容并打开微信，管理员选择聊天后发送；手机正式版会调用系统分享/微信分享面板，仍需人工确认发送。</p>
        <div class="actions"><button class="primary-button" type="submit">${icon("check")}<span>保存微信配置</span></button></div>
      </form>
    </section>

    <div class="grid-two">
      <section class="panel form-panel">
        ${panelHead("新增固定卫生任务", "plus")}
        <form id="cleaningTaskForm" class="form-panel">
          <div class="form-grid">
            <label>频率<select name="cadence"><option>每日</option><option>每周</option><option>每月</option><option>临时</option></select></label>
            <label>执行时间<input name="time" placeholder="10:30 / 周一 11:00 / 每月1日 16:00"></label>
            <label>微信发送时间<input name="sendTime" type="time" value="09:30"></label>
            <label>区域<input name="area" required placeholder="客房 / 公共区 / 仓储"></label>
            <label>提醒内容<input name="content" required placeholder="需要员工执行的具体内容"></label>
          </div>
          <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>添加卫生任务</span></button></div>
        </form>
      </section>

      <section class="panel form-panel">
        ${panelHead("新增额外提醒", "plus")}
        <form id="managedReminderForm" class="form-panel">
          <div class="form-grid">
            <label>类型<input name="type" value="额外提醒"></label>
            <label>提醒时间<input name="time" type="time" value="${timeText(new Date())}"></label>
            <label>微信发送时间<input name="sendTime" type="time" value="${clockValue(new Date())}"></label>
          </div>
          <input name="content" required placeholder="例如：今天重点检查 RM05 摄像头">
          <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>添加提醒</span></button></div>
        </form>
      </section>
    </div>

    <section class="panel">
      ${panelHead("固定卫生任务编辑", "clipboard")}
      <div class="editable-list">
        ${state.cleaningTasks.length ? state.cleaningTasks.map(cleaningTaskEditor).join("") : emptyBlock("暂无固定任务", "新增后会自动进入今日强提醒。")}
      </div>
    </section>

    <section class="panel">
      ${panelHead("提醒事项编辑", "bell")}
      <div class="editable-list">
        ${state.reminders.length ? state.reminders.map(managedReminderEditor).join("") : emptyBlock("暂无额外提醒", "新增后会显示在首页强提醒。")}
      </div>
    </section>

    <section class="panel">
      ${panelHead("微信发送记录", "files")}
      <div class="report-list">
        ${logs.length ? logs.map(wechatLogRow).join("") : emptyBlock("暂无发送记录", "点击一键发送至微信后会记录时间、操作人和结果。")}
      </div>
    </section>
  `;
}

function renderAccounts() {
  if (session.role !== "admin") return "";
  const pendingStaff = state.users.filter((user) => user.role === "staff" && user.status === "pending");
  const activeStaff = state.users.filter((user) => user.role === "staff" && user.status === "active");
  const pausedStaff = state.users.filter((user) => user.role === "staff" && user.status === "paused");
  const rejectedStaff = state.users.filter((user) => user.role === "staff" && user.status === "rejected");
  const admins = state.users.filter((user) => user.role === "admin");
  const cloudConfig = readCloudConfig();
  const accountActions = `
    <label class="plain-button file-button">
      ${icon("files")}<span>导入员工申请/账号文件</span>
      <input id="staffSyncImportInput" type="file" accept="application/json,.json">
    </label>
    <button class="primary-button" id="exportStaffAccountsButton" type="button">${icon("upload")}<span>导出员工登录文件</span></button>
  `;

  return `
    ${renderHeader("账号审核", "管理员审核员工注册申请，并查看内部账号状态", "user", accountActions)}
    <section class="panel compact-panel">
      <div class="sync-box sync-box-wide">
        <strong>联网同步状态</strong>
        <span>${escapeHtml(cloudStatusText())}。启用阿里云同步后，员工注册、管理员审核和门店数据会自动同步到其他电脑和手机端。</span>
      </div>
    </section>
    <section class="panel form-panel">
      ${panelHead("阿里云同步设置", "upload")}
      <form id="cloudSyncForm" class="form-panel">
        <div class="form-grid">
          <label>启用联网同步<select name="enabled"><option value="false" ${cloudConfig.enabled ? "" : "selected"}>关闭</option><option value="true" ${cloudConfig.enabled ? "selected" : ""}>开启</option></select></label>
          <label>阿里云同步地址<input name="endpoint" value="${escapeAttr(cloudConfig.endpoint)}" placeholder="例如：https://roomie-api.example.com"></label>
          <label>同步密钥<input name="token" type="password" value="${escapeAttr(cloudConfig.token)}" placeholder="由阿里云后端生成"></label>
        </div>
        <div class="actions">
          <button class="primary-button" type="submit">${icon("check")}<span>保存同步设置</span></button>
          <button class="plain-button" id="pullCloudButton" type="button">${icon("download")}<span>从云端拉取</span></button>
          <button class="plain-button" id="pushCloudButton" type="button">${icon("upload")}<span>推送到云端</span></button>
        </div>
      </form>
      ${cloudConfig.endpoint.startsWith("http://") ? `<div class="transport-warning">当前使用 HTTP 测试连接，通信尚未加密。请勿推送正式身份证、合同或客户资料；完成 HTTPS 配置后再投入正式使用。</div>` : ""}
    </section>
    <div class="stat-grid">
      ${statCard("待审核", `${pendingStaff.length} 个`, "员工注册后需管理员通过", "bell", "coral")}
      ${statCard("已通过员工", `${activeStaff.length} 人`, "可登录员工端", "user", "teal")}
      ${statCard("已暂停", `${pausedStaff.length} 人`, "暂时不能登录系统", "close", "coral")}
      ${statCard("管理员", `${admins.length} 人`, "拥有全部后台权限", "wallet", "blue")}
    </div>
    <section class="panel">
      ${panelHead("待审核员工", "bell")}
      <div class="account-list">
        ${pendingStaff.length ? pendingStaff.map(accountRow).join("") : emptyBlock("暂无待审核申请", "员工提交注册申请后会显示在这里。")}
      </div>
    </section>
    <section class="panel">
      ${panelHead("已通过员工", "user")}
      <div class="account-list">
        ${activeStaff.length ? activeStaff.map(accountRow).join("") : emptyBlock("暂无已通过员工", "通过员工申请后会显示在这里。")}
      </div>
    </section>
    ${pausedStaff.length ? `
      <section class="panel">
        ${panelHead("已暂停员工", "close")}
        <div class="account-list">${pausedStaff.map(accountRow).join("")}</div>
      </section>
    ` : ""}
    <section class="panel">
      ${panelHead("管理员账号", "wallet")}
      <div class="account-list">
        ${admins.length ? admins.map(accountRow).join("") : emptyBlock("暂无管理员账号", "请先注册管理员。")}
      </div>
    </section>
    ${rejectedStaff.length ? `
      <section class="panel">
        ${panelHead("已拒绝申请", "clipboard")}
        <div class="account-list">${rejectedStaff.map(accountRow).join("")}</div>
      </section>
    ` : ""}
  `;
}

function renderLedger() {
  if (session.role !== "admin") return "";
  const todayIncome = sumLedger("收入", true);
  const todayExpense = sumLedger("支出", true);
  const monthNet = state.ledger
    .filter((entry) => isThisMonth(entry.date))
    .reduce((sum, entry) => sum + Number(entry.amount || 0) * (entry.kind === "收入" ? 1 : -1), 0);
  const ledger = [...state.ledger].sort((a, b) => new Date(b.date) - new Date(a.date));

  return `
    ${renderHeader("管理员账本", "记录收入、支出、收款方式和关联猫咪", "wallet")}
    <div class="stat-grid">
      ${statCard("今日收入", yuan(todayIncome), "来自今日收入记录", "card", "teal")}
      ${statCard("今日支出", yuan(todayExpense), "耗材、人工、维修等", "card", "coral")}
      ${statCard("本月净额", yuan(monthNet), "收入减支出", "trend", "gold")}
      ${statCard("账目数量", `${state.ledger.length} 笔`, "当前本机账本", "clipboard", "blue")}
    </div>
    <section class="panel form-panel">
      ${panelHead("新增账目", "edit")}
      <form id="ledgerForm" class="form-panel">
        <div class="form-grid">
          <label>类型<select name="kind">${ledgerKinds.map((item) => `<option>${item}</option>`).join("")}</select></label>
          <label>分类<input name="category" value="住宿费"></label>
          <label>金额<input name="amount" type="number" min="0" step="1" required></label>
          <label>方式<input name="method" value="微信"></label>
          <label>关联猫咪<input name="relatedCatName" placeholder="可选"></label>
          <label>经手人<input name="operatorName" value="${escapeAttr(session.name)}"></label>
        </div>
        <input name="note" placeholder="备注">
        <div class="actions"><button class="primary-button" type="submit">${icon("plus")}<span>记一笔</span></button></div>
      </form>
    </section>
    <section class="panel">
      ${panelHead("账目明细", "clipboard")}
      <div class="ledger-list">
        ${ledger.length ? ledger.map(ledgerRow).join("") : emptyBlock("还没有账目", "新增第一笔收入或支出后会显示在这里。")}
      </div>
    </section>
  `;
}

function bindViewEvents() {
  const logoutButton = document.getElementById("logoutButton");
  if (logoutButton) logoutButton.addEventListener("click", () => {
    setSession(null);
    render();
  });

  const addCatButton = document.getElementById("addCatButton");
  if (addCatButton) addCatButton.addEventListener("click", () => openCatDialog());

  const profileForm = document.getElementById("profileForm");
  if (profileForm) profileForm.addEventListener("submit", saveProfile);

  const customerForm = document.getElementById("customerForm");
  if (customerForm) customerForm.addEventListener("submit", saveCustomer);

  const extraReminderForm = document.getElementById("extraReminderForm");
  if (extraReminderForm) extraReminderForm.addEventListener("submit", saveExtraReminder);

  const sendWechatButton = document.getElementById("sendWechatButton");
  if (sendWechatButton) sendWechatButton.addEventListener("click", sendWechatReminders);

  const openReminderManager = document.getElementById("openReminderManager");
  if (openReminderManager) openReminderManager.addEventListener("click", () => {
    currentView = "reminders";
    render();
  });

  const openInventoryButton = document.getElementById("openInventoryButton");
  if (openInventoryButton) openInventoryButton.addEventListener("click", () => {
    currentView = "inventory";
    render();
  });

  const wechatConfigForm = document.getElementById("wechatConfigForm");
  if (wechatConfigForm) wechatConfigForm.addEventListener("submit", saveWechatConfig);

  const sendWechatManagerButton = document.getElementById("sendWechatManagerButton");
  if (sendWechatManagerButton) sendWechatManagerButton.addEventListener("click", sendWechatReminders);

  const cleaningTaskForm = document.getElementById("cleaningTaskForm");
  if (cleaningTaskForm) cleaningTaskForm.addEventListener("submit", saveCleaningTask);

  const managedReminderForm = document.getElementById("managedReminderForm");
  if (managedReminderForm) managedReminderForm.addEventListener("submit", saveManagedReminder);

  const catSearchInput = document.getElementById("catSearch");
  if (catSearchInput) {
    catSearchInput.addEventListener("input", (event) => {
      catSearch = event.target.value;
      render();
    });
  }

  const prevMonth = document.getElementById("prevMonth");
  if (prevMonth) prevMonth.addEventListener("click", () => {
    calendarCursor = addMonths(calendarCursor, -1);
    render();
  });

  const nextMonth = document.getElementById("nextMonth");
  if (nextMonth) nextMonth.addEventListener("click", () => {
    calendarCursor = addMonths(calendarCursor, 1);
    render();
  });

  document.querySelectorAll("[data-day]").forEach((button) => {
    button.addEventListener("click", () => {
      selectedDate = button.dataset.day;
      render();
    });
  });

  document.querySelectorAll("[data-edit-cat]").forEach((button) => {
    button.addEventListener("click", () => openCatDialog(button.dataset.editCat));
  });
  document.querySelectorAll("[data-delete-cat]").forEach((button) => {
    button.addEventListener("click", () => deleteCat(button.dataset.deleteCat));
  });
  document.querySelectorAll("[data-delete-report]").forEach((button) => {
    button.addEventListener("click", () => deleteReport(button.dataset.deleteReport));
  });
  document.querySelectorAll("[data-delete-ledger]").forEach((button) => {
    button.addEventListener("click", () => deleteLedger(button.dataset.deleteLedger));
  });
  document.querySelectorAll("[data-delete-appointment]").forEach((button) => {
    button.addEventListener("click", () => deleteAppointment(button.dataset.deleteAppointment));
  });
  document.querySelectorAll("[data-appointment-to-cat]").forEach((button) => {
    button.addEventListener("click", () => appointmentToCat(button.dataset.appointmentToCat));
  });
  document.querySelectorAll("[data-delete-schedule]").forEach((button) => {
    button.addEventListener("click", () => deleteSchedule(button.dataset.deleteSchedule));
  });
  document.querySelectorAll("[data-read-doc]").forEach((button) => {
    button.addEventListener("click", () => markDocumentRead(button.dataset.readDoc));
  });
  document.querySelectorAll("[data-print-doc]").forEach((button) => {
    button.addEventListener("click", () => printDocument(button.dataset.printDoc));
  });
  document.querySelectorAll("[data-open-doc]").forEach((button) => {
    button.addEventListener("click", () => openDocumentFile(button.dataset.openDoc));
  });
  document.querySelectorAll("[data-delete-doc]").forEach((button) => {
    button.addEventListener("click", () => deleteDocument(button.dataset.deleteDoc));
  });
  document.querySelectorAll("[data-consume-inventory]").forEach((button) => {
    button.addEventListener("click", () => adjustInventory(button.dataset.consumeInventory, -1));
  });
  document.querySelectorAll("[data-restock-inventory]").forEach((button) => {
    button.addEventListener("click", () => adjustInventory(button.dataset.restockInventory, 1));
  });
  document.querySelectorAll("[data-delete-inventory]").forEach((button) => {
    button.addEventListener("click", () => deleteInventoryItem(button.dataset.deleteInventory));
  });
  document.querySelectorAll("[data-approve-user]").forEach((button) => {
    button.addEventListener("click", () => reviewUser(button.dataset.approveUser, "active"));
  });
  document.querySelectorAll("[data-reject-user]").forEach((button) => {
    button.addEventListener("click", () => reviewUser(button.dataset.rejectUser, "rejected"));
  });
  document.querySelectorAll("[data-staff-profile-form]").forEach((form) => {
    form.addEventListener("submit", saveAdminStaffProfile);
  });
  document.querySelectorAll("[data-update-staff-pin]").forEach((button) => {
    button.addEventListener("click", () => updateStaffPin(button.dataset.updateStaffPin));
  });
  document.querySelectorAll("[data-update-employee-status]").forEach((button) => {
    button.addEventListener("click", () => updateEmployeeStatus(button.dataset.updateEmployeeStatus));
  });
  document.querySelectorAll("[data-pause-user]").forEach((button) => {
    button.addEventListener("click", () => reviewUser(button.dataset.pauseUser, "paused"));
  });
  document.querySelectorAll("[data-resume-user]").forEach((button) => {
    button.addEventListener("click", () => reviewUser(button.dataset.resumeUser, "active"));
  });
  document.querySelectorAll("[data-delete-user]").forEach((button) => {
    button.addEventListener("click", () => deleteUserAccount(button.dataset.deleteUser));
  });
  const staffSyncImportInput = document.getElementById("staffSyncImportInput");
  if (staffSyncImportInput) staffSyncImportInput.addEventListener("change", importStaffSyncFile);
  const exportStaffAccountsButton = document.getElementById("exportStaffAccountsButton");
  if (exportStaffAccountsButton) exportStaffAccountsButton.addEventListener("click", exportStaffAccounts);
  const cloudSyncForm = document.getElementById("cloudSyncForm");
  if (cloudSyncForm) cloudSyncForm.addEventListener("submit", saveCloudSyncSettings);
  const pullCloudButton = document.getElementById("pullCloudButton");
  if (pullCloudButton) pullCloudButton.addEventListener("click", () => pullCloudState({ silent: false }));
  const pushCloudButton = document.getElementById("pushCloudButton");
  if (pushCloudButton) pushCloudButton.addEventListener("click", () => pushCloudState({ silent: false }));
  document.querySelectorAll("[data-save-cleaning]").forEach((button) => {
    button.addEventListener("click", () => saveCleaningTaskEdit(button.dataset.saveCleaning));
  });
  document.querySelectorAll("[data-toggle-cleaning]").forEach((button) => {
    button.addEventListener("click", () => toggleCleaningTask(button.dataset.toggleCleaning));
  });
  document.querySelectorAll("[data-delete-cleaning]").forEach((button) => {
    button.addEventListener("click", () => deleteCleaningTask(button.dataset.deleteCleaning));
  });
  document.querySelectorAll("[data-save-managed-reminder]").forEach((button) => {
    button.addEventListener("click", () => saveManagedReminderEdit(button.dataset.saveManagedReminder));
  });
  document.querySelectorAll("[data-toggle-managed-reminder]").forEach((button) => {
    button.addEventListener("click", () => toggleManagedReminder(button.dataset.toggleManagedReminder));
  });
  document.querySelectorAll("[data-delete-managed-reminder]").forEach((button) => {
    button.addEventListener("click", () => deleteManagedReminder(button.dataset.deleteManagedReminder));
  });

  const autoClockButton = document.getElementById("autoClockButton");
  if (autoClockButton) autoClockButton.addEventListener("click", autoClockIn);

  const appointmentForm = document.getElementById("appointmentForm");
  if (appointmentForm) appointmentForm.addEventListener("submit", saveAppointment);

  const scheduleForm = document.getElementById("scheduleForm");
  if (scheduleForm) scheduleForm.addEventListener("submit", saveSchedule);

  const documentForm = document.getElementById("documentForm");
  if (documentForm) documentForm.addEventListener("submit", saveDocument);

  const inventoryForm = document.getElementById("inventoryForm");
  if (inventoryForm) inventoryForm.addEventListener("submit", saveInventoryItem);

  const reportForm = document.getElementById("reportForm");
  if (reportForm) reportForm.addEventListener("submit", saveReport);

  const ledgerForm = document.getElementById("ledgerForm");
  if (ledgerForm) ledgerForm.addEventListener("submit", saveLedger);
}

function statCard(title, value, note, iconName, color) {
  return `
    <article class="stat-card">
      <div class="stat-top">
        <div class="stat-icon ${color}">${icon(iconName)}</div>
      </div>
      <div class="stat-value">${value}</div>
      <div class="stat-title">${title}</div>
      <div class="stat-note">${note}</div>
    </article>
  `;
}

function panelHead(title, iconName) {
  return `<div class="panel-head icon-${iconName}">${icon(iconName)}<h3>${title}</h3></div>`;
}

function emptyBlock(title, message) {
  return `<div class="empty"><div><strong>${title}</strong><span>${message}</span></div></div>`;
}

function reminderRow(cat) {
  return `
    <div class="row-card dense">
      <div class="row-icon">${icon("calendar")}</div>
      <div>
        <div class="row-title"><strong>${escapeHtml(cat.catName)} · ${escapeHtml(cat.room || "未分房")}</strong>${pill(cat.paymentStatus, paymentTone(cat.paymentStatus))}</div>
        <div class="meta">已住 ${nights(cat.checkInDate)} 晚，预计 ${dateText(cat.expectedCheckoutDate)} 退房</div>
      </div>
      <div class="amount">${yuan(Number(cat.dailyRate || 0))}</div>
    </div>
  `;
}

function reportSummary(report) {
  return `
    <div class="stack">
      <div class="row-title">${pill(report.shift, "teal")}<strong>${escapeHtml(report.staffName)}</strong><span class="meta">${dateTimeText(report.createdAt)}</span></div>
      <p>${escapeHtml(report.handover || "暂无交接备注")}</p>
      ${checks(report)}
    </div>
  `;
}

function appointmentRowSmall(item) {
  return `
    <div class="row-card dense">
      <div class="row-icon">${icon("calendar")}</div>
      <div>
        <div class="row-title"><strong>${escapeHtml(item.time)} · ${escapeHtml(item.catName)}</strong>${pill(item.status, appointmentTone(item.status))}</div>
        <div class="meta">${escapeHtml(item.service)} · ${escapeHtml(item.ownerName)} · ${escapeHtml(item.phone || "未填电话")}</div>
      </div>
      <span class="pill teal">${escapeHtml(item.roomPreference || "未定房型")}</span>
    </div>
  `;
}

function reminderItem(item) {
  return `
    <article class="reminder-card">
      <div class="row-title"><strong>${escapeHtml(item.title)}</strong>${pill(item.time || item.cadence || "今日", "blue")}${item.sendTime ? pill(`微信 ${item.sendTime}`, "teal") : ""}</div>
      <p>${escapeHtml(item.content)}</p>
    </article>
  `;
}

function dashboardInventoryAlerts() {
  return [...state.inventory]
    .filter((item) => inventoryStatus(item).status !== "正常")
    .sort(inventorySort)
    .slice(0, 6);
}

function inventoryAlertCard(item) {
  const info = inventoryStatus(item);
  const quantity = Number(item.quantity || 0);
  const safetyStock = Number(item.safetyStock || 0);
  const restockAmount = Math.max(0, Math.ceil(safetyStock - quantity));
  const expiryText = item.expiryDate ? `${dateText(item.expiryDate)} 到期` : "无保质期";
  const message = info.status === "低库存"
    ? `建议至少补入 ${restockAmount} ${item.unit || "件"}`
    : `${expiryText}，请优先使用或下架确认`;
  return `
    <article class="stock-alert-card">
      <div>
        <div class="row-title">
          <strong>${escapeHtml(item.name)}</strong>
          ${pill(item.category, "blue")}
          ${pill(info.status, info.tone)}
        </div>
        <div class="meta">${escapeHtml(item.location || "未填位置")} · ${expiryText}</div>
        <p>${escapeHtml(message)}</p>
      </div>
      <div class="stock-alert-qty">
        <strong>${formatInventoryQuantity(item)}</strong>
        <span>安全线 ${formatInventoryQuantity({ quantity: item.safetyStock, unit: item.unit })}</span>
      </div>
    </article>
  `;
}

function dashboardRosterPanel() {
  const today = isoDate(new Date());
  const mode = activeRosterMode();
  const todaySchedules = schedulesOn(today);
  return `
    <section class="panel roster-dashboard-panel">
      <div class="panel-head-with-action">
        ${panelHead(`今日排班 · ${mode.name}`, "calendar")}
        <span class="pill ${mode.key === "high" ? "coral" : "teal"}">当前入住 ${mode.count} 只 · ${mode.range}</span>
      </div>
      <div class="dashboard-roster-list">
        ${todaySchedules.map(dashboardRosterCard).join("")}
      </div>
    </section>
  `;
}

function dashboardRosterCard(item) {
  const tone = item.status === "上班" ? shiftTone(item.role) : "gold";
  const time = item.status === "上班" ? `${escapeHtml(item.shiftStart)}-${escapeHtml(item.shiftEnd)}` : "今日休息";
  return `
    <article class="dashboard-roster-card">
      <div class="row-title">
        <strong>${escapeHtml(item.employeeName)}</strong>
        ${pill(item.role || item.status, tone)}
      </div>
      <p>${time}</p>
      <span class="meta">${escapeHtml(item.note || "手动排班")}</span>
    </article>
  `;
}

function cleaningTaskEditor(item) {
  return `
    <article class="editable-card" data-cleaning-editor="${escapeAttr(item.id)}">
      <div class="editable-card-head">
        <strong>${escapeHtml(item.cadence)}卫生 · ${escapeHtml(item.area || "未填区域")}</strong>
        ${pill(item.enabled ? "已启用" : "已停用", item.enabled ? "teal" : "gold")}
      </div>
      <div class="form-grid">
        <label>频率<select data-cleaning-field="cadence">${["每日", "每周", "每月", "临时"].map((option) => `<option ${option === item.cadence ? "selected" : ""}>${option}</option>`).join("")}</select></label>
        <label>执行时间<input data-cleaning-field="time" value="${escapeAttr(item.time || "")}"></label>
        <label>微信发送时间<input data-cleaning-field="sendTime" type="time" value="${escapeAttr(clockValueFromText(item.sendTime || item.time || ""))}"></label>
        <label>区域<input data-cleaning-field="area" value="${escapeAttr(item.area || "")}"></label>
        <label>内容<input data-cleaning-field="content" value="${escapeAttr(item.content || "")}"></label>
      </div>
      <div class="actions">
        <button class="primary-button" type="button" data-save-cleaning="${escapeAttr(item.id)}">${icon("check")}<span>保存</span></button>
        <button class="plain-button" type="button" data-toggle-cleaning="${escapeAttr(item.id)}">${item.enabled ? "停用" : "启用"}</button>
        <button class="danger-button" type="button" data-delete-cleaning="${escapeAttr(item.id)}">${icon("trash")}<span>删除</span></button>
      </div>
    </article>
  `;
}

function managedReminderEditor(item) {
  return `
    <article class="editable-card" data-reminder-editor="${escapeAttr(item.id)}">
      <div class="editable-card-head">
        <strong>${escapeHtml(item.type || "额外提醒")}</strong>
        ${pill(item.enabled ? "已启用" : "已停用", item.enabled ? "teal" : "gold")}
      </div>
      <div class="form-grid">
        <label>类型<input data-reminder-field="type" value="${escapeAttr(item.type || "")}"></label>
        <label>提醒时间<input data-reminder-field="time" value="${escapeAttr(item.time || "")}"></label>
        <label>微信发送时间<input data-reminder-field="sendTime" type="time" value="${escapeAttr(clockValueFromText(item.sendTime || item.time || ""))}"></label>
        <label class="wide-label">内容<input data-reminder-field="content" value="${escapeAttr(item.content || "")}"></label>
      </div>
      <div class="actions">
        <button class="primary-button" type="button" data-save-managed-reminder="${escapeAttr(item.id)}">${icon("check")}<span>保存</span></button>
        <button class="plain-button" type="button" data-toggle-managed-reminder="${escapeAttr(item.id)}">${item.enabled ? "停用" : "启用"}</button>
        <button class="danger-button" type="button" data-delete-managed-reminder="${escapeAttr(item.id)}">${icon("trash")}<span>删除</span></button>
      </div>
    </article>
  `;
}

function wechatLogRow(log) {
  const tone = log.status === "已复制待发送" ? "teal" : "coral";
  return `
    <div class="row-card dense">
      <div class="row-icon">${icon(log.status === "已复制待发送" ? "check" : "bell")}</div>
      <div>
        <div class="row-title"><strong>${escapeHtml(log.status || "未发送")}</strong>${pill(`${log.count || 0} 条提醒`, tone)}</div>
        <div class="meta">${dateTimeText(log.sentAt)} · ${escapeHtml(log.operatorName || "未知操作人")} · ${escapeHtml(log.message || "")}</div>
      </div>
      <span class="pill ${tone}">${escapeHtml(log.status || "记录")}</span>
    </div>
  `;
}

function todayReminderItems() {
  const today = isoDate(new Date());
  const mySchedule = schedulesOn(today).find((item) => item.employeeName === session.name && item.status === "上班");
  const scheduleReminder = mySchedule ? [{
    title: "上班提醒",
    time: `${mySchedule.shiftStart || "08:30"} 上班`,
    sendTime: timeBefore(mySchedule.shiftStart || "08:30", 30),
    content: `今天你是${mySchedule.role || "门店员工"}，班次 ${mySchedule.shiftStart || "--"} - ${mySchedule.shiftEnd || "--"}。`
  }] : [];
  const cleaning = state.cleaningTasks.filter((item) => item.enabled && isCleaningTaskDue(item)).map((item) => ({
    title: `${item.cadence}卫生 · ${item.area}`,
    time: item.time,
    sendTime: item.sendTime || item.time,
    content: item.content
  }));
  const extra = state.reminders.filter((item) => item.enabled).map((item) => ({
    title: item.type,
    time: item.time,
    sendTime: item.sendTime || item.time,
    content: item.content
  }));
  return [...scheduleReminder, ...cleaning, ...extra].slice(0, 8);
}

function isCleaningTaskDue(item) {
  if (item.cadence === "每日" || item.cadence === "临时") return true;
  const now = new Date();
  const text = `${item.time || ""} ${item.content || ""}`;
  if (item.cadence === "每周") {
    const weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
    const matched = weekdays.findIndex((day) => text.includes(day));
    return matched < 0 ? true : matched === now.getDay();
  }
  if (item.cadence === "每月") {
    const matched = text.match(/每月\s*(\d{1,2})\s*日/);
    return matched ? Number(matched[1]) === now.getDate() : true;
  }
  return true;
}

function catMiniCard(cat) {
  const stay = normalizeStay(cat);
  const progress = Math.min(nights(cat.checkInDate) / 14, 1) * 100;
  return `
    <article class="cat-card">
      <div class="cat-card-head"><h4>${escapeHtml(stay.catName)}</h4>${pill(`${stay.roomType || "未定房型"} ${stay.room || ""}`, "blue")}</div>
      <div class="meta">${stay.familyCats.length} 只猫 · ${escapeHtml(stay.ownerName)}</div>
      <div class="meta">预计 ${dateText(cat.expectedCheckoutDate)} 退房</div>
      <div class="progress"><span style="width:${progress}%"></span></div>
    </article>
  `;
}

function customerCard(customer) {
  return `
    <article class="customer-card">
      <div class="row-title">
        <strong>${escapeHtml(customer.customerName)}</strong>
        ${pill(dateText(customer.checkInDate), "blue")}
        ${pill(`${customer.cats.length} 只猫`, "teal")}
      </div>
      <div class="meta">证件：${escapeHtml(customer.idNumber || "未填")} · 手机：${escapeHtml(customer.phone)} · 紧急联系：${escapeHtml(customer.emergencyPhone || "未填")}</div>
      <div class="family-cat-list">
        ${customer.cats.map((profile) => `
          <div class="family-cat-chip">
            <strong>${escapeHtml(profile.name)}</strong>
            <span>${escapeHtml(profile.gender)} · ${smartAgeText(profile)} · ${escapeHtml(profile.breed || "未填品种")} · ${escapeHtml(profile.coatColor || "未填毛色")}</span>
            <span>疫苗：${escapeHtml(profile.vaccineDate || "选填未填")} · 绝育：${escapeHtml(profile.neuterStatus)} · 首次入住：${escapeHtml(profile.firstStayDate || "未填")}</span>
            <span>喂食：干粮 ${escapeHtml(profile.feeding.dryFoodGrams || "0")}g / 湿粮 ${escapeHtml(profile.feeding.wetFoodGrams || "0")}g · ${escapeHtml(profile.feeding.supplements || "无保健品")}</span>
            <span>客房服务：${escapeHtml(roomServiceSummary(profile))}</span>
            <span>${escapeHtml(profile.specialNotes || "无特殊备注")} ${escapeHtml(profile.medicationNotes || "")}</span>
          </div>
        `).join("")}
      </div>
      <div class="meta">登记时间：${dateTimeText(customer.createdAt)}</div>
    </article>
  `;
}

function roomServiceSummary(profile) {
  const orders = normalizeRoomServiceOrders(profile.roomServiceOrders);
  if (!orders.length) return "无记录";
  return orders.map((order) => {
    const calculatedTotal = order.totalAmount || (order.unitPrice && order.quantity ? Number(order.unitPrice || 0) * Number(order.quantity || 0) : "");
    const parts = [
      order.purchaseDate,
      order.productName,
      order.unitPrice ? `单价 ${yuan(Number(order.unitPrice || 0))}` : "",
      order.quantity ? `${order.quantity} 件` : "",
      calculatedTotal ? `总金额 ${yuan(Number(calculatedTotal || 0))}` : "",
      order.usage
    ].filter(Boolean);
    return parts.join(" · ");
  }).join("；");
}

function catRow(cat) {
  const stay = normalizeStay(cat);
  const deleteButton = session.role === "admin" ? `<button class="danger-button" data-delete-cat="${cat.id}">${icon("trash")}<span>删除</span></button>` : "";
  return `
    <article class="cat-row">
      <div class="cat-avatar">${icon("paw")}</div>
      <div class="cat-main">
        <div class="row-title">
          <strong>${escapeHtml(stay.catName)}</strong>
          ${pill(stay.status, statusTone(stay.status))}
          ${pill(stay.paymentStatus, paymentTone(stay.paymentStatus))}
          ${pill(`${stay.roomType || "未定房型"} ${stay.room || ""}`, "blue")}
        </div>
        <div class="cat-meta">
          <span>${stay.familyCats.length} 只猫</span>
          <span>${escapeHtml(stay.ownerName)}</span>
          <span>${escapeHtml(stay.phone)}</span>
          <span>本次寄养总金额 ${yuan(Number(stay.dailyRate || 0))}</span>
        </div>
        <div class="family-cat-list">${stay.familyCats.map((profile) => `
          <div class="family-cat-chip">
            <strong>${escapeHtml(profile.name)}</strong>
            <span>${escapeHtml(profile.gender)} · ${smartAgeText(profile)} · ${escapeHtml(profile.breed || "未填品种")} · ${escapeHtml(profile.coatColor || "未填毛色")}</span>
            <span>干粮 ${escapeHtml(profile.feeding.dryFoodGrams || "0")}g / 湿粮 ${escapeHtml(profile.feeding.wetFoodGrams || "0")}g · ${escapeHtml(profile.feeding.dietNotes || "无饮食备注")}</span>
            <span>客房服务：${escapeHtml(roomServiceSummary(profile))}</span>
          </div>
        `).join("")}</div>
        <div class="meta">入住 ${dateText(stay.checkInDate)} · 已住 ${nights(stay.checkInDate)} 晚 · 预计退房 ${dateText(stay.expectedCheckoutDate)}</div>
      </div>
      <div class="row-actions">
        <button class="plain-button" data-edit-cat="${stay.id}">${icon("edit")}<span>编辑</span></button>
        ${deleteButton}
      </div>
    </article>
  `;
}

function reportRow(report) {
  const deleteButton = session.role === "admin" ? `<button class="danger-button" data-delete-report="${report.id}">${icon("trash")}<span>删除</span></button>` : "";
  return `
    <article class="row-card">
      <div class="row-title">
        ${pill(report.shift, "teal")}
        <strong>${escapeHtml(report.staffName)}</strong>
        <span class="meta">${dateTimeText(report.createdAt)}</span>
        ${deleteButton}
      </div>
      ${checks(report)}
      ${report.occupancyNote ? `<p>房间：${escapeHtml(report.occupancyNote)}</p>` : ""}
      ${report.roomType || report.room ? `<p>本次房型：${escapeHtml(report.roomType || "未填")} · ${escapeHtml(report.room || "未填房号")}</p>` : ""}
      ${report.incidents ? `<p><strong>异常：</strong>${escapeHtml(report.incidents)}</p>` : ""}
      ${report.handover ? `<p>交接：${escapeHtml(report.handover)}</p>` : ""}
      ${report.todo ? `<p>待办：${escapeHtml(report.todo)}</p>` : ""}
    </article>
  `;
}

function ledgerRow(entry) {
  const tone = entry.kind === "收入" ? "income" : "expense";
  return `
    <article class="row-card dense">
      <div class="row-icon ${entry.kind === "收入" ? "" : "badge coral"}">${icon("wallet")}</div>
      <div>
        <div class="row-title">
          <strong>${escapeHtml(entry.category)}</strong>
          ${pill(entry.method || "未填方式", "blue")}
          ${entry.relatedCatName ? pill(entry.relatedCatName, "teal") : ""}
        </div>
        <div>${escapeHtml(entry.note || "无备注")}</div>
        <div class="meta">${dateTimeText(entry.date)} · ${escapeHtml(entry.operatorName || "未填经手人")}</div>
      </div>
      <div class="row-title">
        <span class="amount ${tone}">${entry.kind === "收入" ? "+" : "-"}${yuan(Number(entry.amount || 0))}</span>
        <button class="danger-button" data-delete-ledger="${entry.id}">${icon("trash")}<span>删除</span></button>
      </div>
    </article>
  `;
}

function appointmentRow(item) {
  const deleteButton = session.role === "admin" ? `<button class="danger-button" data-delete-appointment="${item.id}">${icon("trash")}<span>删除</span></button>` : "";
  return `
    <article class="row-card appointment-card">
      <div class="row-title">
        <strong>${escapeHtml(item.time)} · ${escapeHtml(item.catName)}</strong>
        ${pill(item.status, appointmentTone(item.status))}
      </div>
      <div class="meta">${escapeHtml(item.service)} · ${escapeHtml(item.ownerName)} · ${escapeHtml(item.phone || "未填电话")}</div>
      <div>${escapeHtml(item.note || "无备注")}</div>
      <div class="row-title">
        ${pill(item.roomPreference || "未定房型", "blue")}
        <button class="plain-button" data-appointment-to-cat="${item.id}">${icon("paw")}<span>登记入住</span></button>
        ${deleteButton}
      </div>
    </article>
  `;
}

function scheduleRow(item) {
  const deleteButton = session.role === "admin" && !item.generated ? `<button class="danger-button" data-delete-schedule="${item.id}">${icon("trash")}<span>删除</span></button>` : "";
  const time = item.status === "上班" ? `${escapeHtml(item.shiftStart || "--:--")} - ${escapeHtml(item.shiftEnd || "--:--")}` : "休息";
  return `
    <article class="row-card dense">
      <div class="row-icon">${icon("user")}</div>
      <div>
        <div class="row-title">
          <strong>${escapeHtml(item.employeeName)}</strong>
          ${pill(item.status, item.status === "上班" ? "teal" : "gold")}
          ${pill(dateText(item.date), "blue")}
          ${item.generated ? pill("自动", "teal") : ""}
        </div>
        <div>${time} · ${escapeHtml(item.role || "未填岗位")}</div>
        <div class="meta">${escapeHtml(item.note || "无备注")}</div>
      </div>
      <div class="row-title">${deleteButton}</div>
    </article>
  `;
}

function attendanceRow(item) {
  return `
    <article class="row-card dense">
      <div class="row-icon">${icon("check")}</div>
      <div>
        <div class="row-title"><strong>${escapeHtml(item.employeeName)}</strong>${pill(item.date, "blue")}</div>
        <div>${escapeHtml(item.time)} 到店</div>
        <div class="meta">${escapeHtml(item.source)} · ${escapeHtml(item.note || "无备注")}</div>
      </div>
      <span class="pill teal">已记录</span>
    </article>
  `;
}

function documentCard(doc) {
  const receipt = readReceiptFor(doc.id);
  const readAudit = state.readReceipts.filter((item) => item.documentId === doc.id);
  const printAudit = state.printReceipts.filter((item) => item.documentId === doc.id);
  const readButton = receipt
    ? `<span class="pill teal">已读 ${dateTimeText(receipt.readAt)}</span>`
    : `<button class="plain-button" data-read-doc="${doc.id}">${icon("check")}<span>确认已读</span></button>`;
  const deleteButton = session.role === "admin" ? `<button class="danger-button" data-delete-doc="${doc.id}">${icon("trash")}<span>删除</span></button>` : "";
  const fileLink = doc.dataUrl ? `<button class="plain-button" type="button" data-open-doc="${doc.id}">${icon("files")}<span>查看原文件</span></button>` : "";
  const preview = doc.contentText
    ? `<pre>${escapeHtml(doc.contentText)}</pre>`
    : `<div class="empty compact"><div><strong>${escapeHtml(doc.fileName)}</strong><span>${escapeHtml(doc.mimeType || "文件")}</span></div></div>`;

  return `
    <article class="doc-card">
      <div class="doc-head">
        <div>
          <div class="row-title"><strong>${escapeHtml(doc.title)}</strong>${pill(doc.category, "blue")}</div>
          <div class="meta">${escapeHtml(doc.fileName)} · ${formatBytes(doc.size)} · ${escapeHtml(doc.uploadedBy)} · ${dateTimeText(doc.updatedAt)}</div>
        </div>
        <div class="row-title">
          ${session.role === "admin" ? pill(`${readCountFor(doc.id)} 人已读`, "teal") : readButton}
          <button class="plain-button" data-print-doc="${doc.id}">${icon("clipboard")}<span>打印</span></button>
          ${fileLink}
          ${deleteButton}
        </div>
      </div>
      <div class="doc-preview">${preview}</div>
      ${session.role === "admin" ? `
        <div class="doc-audit">
          <strong>查看记录</strong>
          ${readAudit.length ? readAudit.map((item) => `<span>${escapeHtml(item.employeeName)} · ${dateTimeText(item.readAt)}</span>`).join("") : "<span>暂无查看记录</span>"}
          <strong>打印记录</strong>
          ${printAudit.length ? printAudit.map((item) => `<span>${escapeHtml(item.employeeName)} · ${dateTimeText(item.printedAt)}</span>`).join("") : "<span>暂无打印记录</span>"}
        </div>
      ` : ""}
    </article>
  `;
}

function inventoryRow(item) {
  const info = inventoryStatus(item);
  const expiryLabel = item.expiryDate ? `${dateText(item.expiryDate)} 到期` : "无保质期";
  const deleteButton = session.role === "admin"
    ? `<button class="danger-button" type="button" data-delete-inventory="${escapeAttr(item.id)}">${icon("trash")}<span>删除</span></button>`
    : "";
  return `
    <article class="inventory-card">
      <div class="inventory-main">
        <div class="row-title">
          <strong>${escapeHtml(item.name)}</strong>
          ${pill(item.category, "blue")}
          ${pill(info.status, info.tone)}
        </div>
        <div class="inventory-meta">
          <span>进货：${dateText(item.purchaseDate)}</span>
          <span>${expiryLabel}</span>
          <span>安全库存：${formatInventoryQuantity({ quantity: item.safetyStock, unit: item.unit })}</span>
          <span>位置：${escapeHtml(item.location || "未填")}</span>
          <span>供应商：${escapeHtml(item.supplier || "未填")}</span>
        </div>
        ${item.note ? `<p>${escapeHtml(item.note)}</p>` : ""}
        <div class="meta">最近更新：${dateTimeText(item.updatedAt)} · ${escapeHtml(item.updatedBy || "system")}</div>
      </div>
      <div class="inventory-qty">
        <strong>${formatInventoryQuantity(item)}</strong>
        <span>${yuan(Number(item.quantity || 0) * Number(item.unitCost || 0))}</span>
      </div>
      <div class="inventory-actions">
        <button class="plain-button" type="button" data-consume-inventory="${escapeAttr(item.id)}">${icon("close")}<span>消耗 1</span></button>
        <button class="plain-button" type="button" data-restock-inventory="${escapeAttr(item.id)}">${icon("plus")}<span>补入 1</span></button>
        ${deleteButton}
      </div>
    </article>
  `;
}

function inventoryAlertCount() {
  return state.inventory.filter((item) => {
    const status = inventoryStatus(item).status;
    return status === "低库存" || status === "临期" || status === "已过期";
  }).length;
}

function inventoryStatus(item) {
  const quantity = Number(item.quantity || 0);
  const safetyStock = Number(item.safetyStock || 0);
  const days = daysUntil(item.expiryDate);
  if (days !== null && days < 0) return { status: "已过期", tone: "coral", rank: 0 };
  if (quantity <= safetyStock && safetyStock > 0) return { status: "低库存", tone: "coral", rank: 1 };
  if (days !== null && days <= 30) return { status: "临期", tone: "gold", rank: 2 };
  return { status: "正常", tone: "teal", rank: 3 };
}

function inventorySort(a, b) {
  const statusDiff = inventoryStatus(a).rank - inventoryStatus(b).rank;
  if (statusDiff) return statusDiff;
  const categoryDiff = inventoryCategories.indexOf(a.category) - inventoryCategories.indexOf(b.category);
  if (categoryDiff) return categoryDiff;
  return new Date(b.updatedAt) - new Date(a.updatedAt);
}

function formatInventoryQuantity(item) {
  const quantity = Number(item.quantity || 0);
  const text = Number.isInteger(quantity) ? String(quantity) : quantity.toFixed(1).replace(/\.0$/, "");
  return `${text} ${item.unit || "件"}`;
}

function daysUntil(dateValue) {
  if (!dateValue) return null;
  const target = new Date(`${dateValue}T00:00:00`);
  if (Number.isNaN(target.getTime())) return null;
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return Math.ceil((target - today) / (24 * 60 * 60 * 1000));
}

function accountRow(user) {
  const actions = user.role === "staff" && user.status === "pending"
    ? `
      <button class="primary-button" data-approve-user="${user.id}">${icon("check")}<span>通过</span></button>
      <button class="danger-button" data-reject-user="${user.id}">${icon("close")}<span>拒绝</span></button>
    `
    : "";
  const staffManage = user.role === "staff"
    ? `
      <form class="staff-admin-tools" data-staff-profile-form="${user.id}">
        <div class="staff-admin-grid">
          <label>用户名<input name="username" value="${escapeAttr(user.username || "")}" required></label>
          <label>姓名<input name="name" value="${escapeAttr(user.name || "")}" required></label>
          <label>性别<select name="gender">${genders.map((item) => `<option ${item === user.gender ? "selected" : ""}>${item}</option>`).join("")}</select></label>
          <label>出生日期<input name="birthDate" type="date" value="${escapeAttr(user.birthDate || "")}"></label>
          <label>手机号<input name="phone" value="${escapeAttr(user.phone || "")}"></label>
          <label>岗位<input name="position" value="${escapeAttr(user.position || "")}"></label>
          <label>身份证号/居住证号<input name="idNumber" value="${escapeAttr(user.idNumber || "")}"></label>
          <label>入职日期<input name="hireDate" type="date" value="${escapeAttr(user.hireDate || "")}"></label>
          <label>紧急联系号码<input name="emergencyPhone" value="${escapeAttr(user.emergencyPhone || "")}"></label>
          <label>员工 PIN<input name="pin" type="password" inputmode="numeric" value="${escapeAttr(user.pin || "")}"></label>
          <label>员工状态<select name="employeeStatus">${employeeStatuses.map((item) => `<option ${item === user.employeeStatus ? "selected" : ""}>${item}</option>`).join("")}</select></label>
        </div>
        <div class="staff-admin-actions">
          <button class="primary-button" type="submit">${icon("check")}<span>保存员工资料</span></button>
          ${user.status === "paused"
            ? `<button class="plain-button" type="button" data-resume-user="${user.id}">${icon("check")}<span>恢复登录</span></button>`
            : `<button class="plain-button" type="button" data-pause-user="${user.id}">${icon("close")}<span>暂停登录</span></button>`}
          <button class="danger-button" type="button" data-delete-user="${user.id}">${icon("trash")}<span>删除账号</span></button>
        </div>
      </form>
    `
    : "";
  return `
    <article class="account-card">
      ${avatarMarkup(user, "small")}
      <div>
        <div class="row-title">
          <strong>${escapeHtml(user.name)}</strong>
          ${pill(roleText[user.role], user.role === "admin" ? "blue" : "teal")}
          ${pill(accountStatusText(user.status), accountStatusTone(user.status))}
        </div>
        <div class="meta">账号：${escapeHtml(user.username)} · PIN：${escapeHtml(user.pin || "")} · 岗位：${escapeHtml(user.position || "未填")} · 手机：${escapeHtml(user.phone || "未填")}</div>
        ${user.role === "staff" ? `<div class="meta">身份证：${escapeHtml(user.idNumber || "未填")} · 入职：${escapeHtml(user.hireDate || "未填")} · 紧急联系：${escapeHtml(user.emergencyPhone || "未填")}</div>` : ""}
        <div class="meta">申请：${dateTimeText(user.createdAt)}${user.approvedAt ? ` · 审核：${dateTimeText(user.approvedAt)} · ${escapeHtml(user.approvedBy || "")}` : ""}</div>
        ${user.note ? `<div>${escapeHtml(user.note)}</div>` : ""}
        ${staffManage}
      </div>
      <div class="row-title">${actions}</div>
    </article>
  `;
}

function accountStatusText(status) {
  if (status === "active") return "已通过";
  if (status === "pending") return "待审核";
  if (status === "rejected") return "已拒绝";
  if (status === "paused") return "已暂停";
  return "未知";
}

function accountStatusTone(status) {
  if (status === "active") return "teal";
  if (status === "pending") return "gold";
  if (status === "rejected") return "coral";
  if (status === "paused") return "coral";
  return "";
}

function checks(report) {
  return `
    <div class="check-grid">
      ${checkItem("喂食", report.feedingDone)}
      ${checkItem("猫砂", report.litterDone)}
      ${checkItem("客房清洁", report.cleaningDone)}
      ${checkItem("用药", report.medicineDone)}
      ${checkItem("下午茶", report.afternoonTeaDone)}
    </div>
  `;
}

function checkItem(label, done) {
  return `<div class="check-item ${done ? "done" : ""}">${icon("check")}<span>${label}</span></div>`;
}

function pill(text, tone = "") {
  return `<span class="pill ${tone}">${escapeHtml(text)}</span>`;
}

function statusTone(status) {
  if (status === "入住中") return "teal";
  if (status === "预约") return "gold";
  return "";
}

function paymentTone(status) {
  if (status === "已结清") return "teal";
  if (status === "仅押金") return "gold";
  return "coral";
}

function appointmentTone(status) {
  if (status === "已确认") return "teal";
  if (status === "待确认") return "gold";
  return "coral";
}

function shiftTone(role) {
  if (role === "早班") return "gold";
  if (role === "晚班") return "blue";
  if (role === "全班") return "teal";
  return "";
}

function checkoutSoonCats() {
  const endOfTomorrow = new Date();
  endOfTomorrow.setHours(0, 0, 0, 0);
  endOfTomorrow.setDate(endOfTomorrow.getDate() + 2);
  return state.cats
    .filter((cat) => cat.status === "入住中" && new Date(cat.expectedCheckoutDate) < endOfTomorrow)
    .sort((a, b) => new Date(a.expectedCheckoutDate) - new Date(b.expectedCheckoutDate));
}

function calendarDays(cursor) {
  const first = startOfMonth(cursor);
  const offset = (first.getDay() + 6) % 7;
  const start = addDays(first, -offset);
  return Array.from({ length: 42 }, (_, index) => addDays(start, index));
}

function dayCell(date) {
  const day = isoDate(date);
  const count = appointmentsOn(day).filter((item) => item.status !== "已取消").length;
  const classes = [
    "day-cell",
    date.getMonth() !== calendarCursor.getMonth() ? "other-month" : "",
    day === selectedDate ? "selected" : "",
    day === isoDate(new Date()) ? "today" : ""
  ].join(" ");
  return `
    <button class="${classes}" data-day="${day}" type="button">
      <span>${date.getDate()}</span>
      ${count ? `<b>${count}</b>` : ""}
    </button>
  `;
}

function appointmentsOn(day) {
  return state.appointments.filter((item) => item.date === day);
}

function schedulesOn(day) {
  const manual = state.schedules.filter((item) => item.date === day);
  const generated = rosterTemplateSchedules(day).filter((item) => (
    !manual.some((schedule) => schedule.employeeName === item.employeeName)
  ));
  return [...generated, ...manual];
}

function activeCatCount() {
  return state.cats.filter((cat) => cat.status === "入住中").length;
}

function activeRosterMode() {
  const count = activeCatCount();
  const key = count >= 6 ? "high" : "low";
  return { key, count, ...rosterTemplates[key] };
}

function rosterTemplateSchedules(day) {
  const date = new Date(`${day}T00:00:00`);
  const weekdayIndex = (date.getDay() + 6) % 7;
  const mode = activeRosterMode();
  const row = mode.rows[weekdayIndex] || mode.rows[0];
  return rosterEmployees.map((employee, index) => {
    const shiftKey = row[index + 1];
    const parsed = parseRosterShift(shiftKey);
    return {
      id: `auto-${mode.key}-${day}-${employee}`,
      date: day,
      employeeName: employee,
      status: parsed.status,
      shiftStart: parsed.shiftStart,
      shiftEnd: parsed.shiftEnd,
      role: parsed.role,
      note: `${mode.name}自动排班 · 当前入住 ${mode.count} 只`,
      generated: true
    };
  });
}

function weekScheduleList(startDay) {
  return Array.from({ length: 7 }, (_, index) => isoDate(addDays(new Date(`${startDay}T00:00:00`), index)))
    .flatMap((day) => schedulesOn(day));
}

function parseRosterShift(shiftKey) {
  if (shiftKey === "full") return { status: "上班", shiftStart: "08:30", shiftEnd: "19:30", role: "全班" };
  if (shiftKey === "morning") return { status: "上班", shiftStart: "08:30", shiftEnd: "13:30", role: "早班" };
  if (shiftKey === "evening") return { status: "上班", shiftStart: "13:30", shiftEnd: "19:30", role: "晚班" };
  return { status: "休息", shiftStart: "", shiftEnd: "", role: "休息" };
}

function currentUser() {
  if (!session) return null;
  return state.users.find((user) => user.username === session.username) || state.users.find((user) => user.name === session.name && user.role === session.role) || null;
}

function avatarMarkup(user, size = "small") {
  const cls = size === "large" ? "avatar avatar-large" : "avatar";
  if (user.avatar) return `<img class="${cls}" src="${user.avatar}" alt="${escapeAttr(user.name)}">`;
  return `<div class="${cls}">${escapeHtml((user.name || user.username || "?").slice(0, 1).toUpperCase())}</div>`;
}

function roomMetaFor(room) {
  return roomOptions.find((item) => item.room === room);
}

function populateCatFormOptions() {
  catForm.elements.status.innerHTML = catStatuses.map((status) => `<option>${status}</option>`).join("");
  catForm.elements.paymentStatus.innerHTML = paymentStatuses.map((status) => `<option>${status}</option>`).join("");
  catForm.elements.roomType.innerHTML = roomTypes.map((item) => `<option>${item.type}</option>`).join("");
  catForm.elements.roomType.addEventListener("change", () => updateRoomSelect(catForm.elements.roomType.value));
  catForm.elements.checkInDate.addEventListener("change", updateBoardingTotalEstimate);
  catForm.elements.expectedCheckoutDate.addEventListener("change", updateBoardingTotalEstimate);
  updateRoomSelect(roomTypes[0].type);
}

function updateRoomSelect(roomType, selectedRoom = "") {
  const rooms = roomTypes.find((item) => item.type === roomType)?.rooms || [];
  catForm.elements.room.innerHTML = rooms.map((room) => `<option ${room === selectedRoom ? "selected" : ""}>${room}</option>`).join("");
  const meta = roomTypes.find((item) => item.type === roomType);
  if (meta && catForm.elements.dailyRate) updateBoardingTotalEstimate();
}

function updateBoardingTotalEstimate() {
  if (!catForm.elements.dailyRate) return;
  catForm.elements.dailyRate.value = estimateBoardingTotal(
    catForm.elements.roomType.value,
    catForm.elements.checkInDate.value,
    catForm.elements.expectedCheckoutDate.value
  );
}

function estimateBoardingTotal(roomType, checkInDate, expectedCheckoutDate) {
  const price = roomTypes.find((item) => item.type === roomType)?.price || 0;
  const start = new Date(`${checkInDate || isoDate(new Date())}T00:00:00`);
  const end = new Date(`${expectedCheckoutDate || checkInDate || isoDate(addDays(new Date(), 1))}T00:00:00`);
  const nights = Math.max(1, Math.round((end - start) / (24 * 60 * 60 * 1000)));
  return price * nights;
}

function renderFamilyCatEditor(familyCats = []) {
  const cats = [0, 1, 2].map((index) => familyCats[index] || normalizeCatProfile({ firstStayDate: isoDate(new Date()) }));
  document.getElementById("familyCatsEditor").innerHTML = cats.map((profile, index) => catProfileFields(index + 1, profile)).join("");
}

function catProfileFields(index, profile) {
  return `
    <section class="cat-profile-box">
      <div class="row-title"><strong>猫咪 ${index}</strong>${index === 1 ? pill("必填", "teal") : pill("可选", "blue")}</div>
      <div class="form-grid">
        <label>猫猫名字<input name="cat${index}Name" ${index === 1 ? "required" : ""} value="${escapeAttr(profile.name)}"></label>
        <label>猫猫性别<select name="cat${index}Gender">${genders.map((item) => `<option ${item === profile.gender ? "selected" : ""}>${item}</option>`).join("")}</select></label>
        <label>出生日期（选填）<input name="cat${index}BirthDate" type="date" value="${escapeAttr(profile.birthDate || "")}"></label>
        <label>月龄（选填）<input name="cat${index}AgeMonths" type="number" min="0" step="1" value="${escapeAttr(profile.ageMonthsAtFirstRegistration || "")}" placeholder="例如：6"></label>
        <label>年龄/岁（选填）<input name="cat${index}AgeYears" type="number" min="0" step="0.1" value="${escapeAttr(profile.ageYearsAtFirstRegistration || "")}" placeholder="例如：2"></label>
        <label>猫猫品种<input name="cat${index}Breed" value="${escapeAttr(profile.breed)}"></label>
        <label>猫猫毛色<input name="cat${index}CoatColor" value="${escapeAttr(profile.coatColor)}"></label>
        <label>疫苗日期<input name="cat${index}VaccineDate" type="date" value="${escapeAttr(profile.vaccineDate || "")}"></label>
        <label>绝育情况<select name="cat${index}NeuterStatus">${neuterStatuses.map((item) => `<option ${item === profile.neuterStatus ? "selected" : ""}>${item}</option>`).join("")}</select></label>
        <label>每日干粮 g<input name="cat${index}DryFood" type="number" min="0" step="1" value="${escapeAttr(profile.feeding.dryFoodGrams)}"></label>
        <label>每日湿粮 g<input name="cat${index}WetFood" type="number" min="0" step="1" value="${escapeAttr(profile.feeding.wetFoodGrams)}"></label>
        <label>保健品<input name="cat${index}Supplements" value="${escapeAttr(profile.feeding.supplements)}"></label>
        <label>首次入住日期<input name="cat${index}FirstStayDate" type="date" value="${escapeAttr(profile.firstStayDate || "")}"></label>
      </div>
      <label class="wide-label">特殊备注<textarea name="cat${index}SpecialNotes" rows="2">${escapeHtml(profile.specialNotes)}</textarea></label>
      <label class="wide-label">用药备注<textarea name="cat${index}MedicationNotes" rows="2">${escapeHtml(profile.medicationNotes)}</textarea></label>
      <label class="wide-label">饮食注意事项<textarea name="cat${index}DietNotes" rows="2">${escapeHtml(profile.feeding.dietNotes)}</textarea></label>
      <label class="wide-label">往期入住日期<textarea name="cat${index}PastStayDates" rows="2" placeholder="每行一个日期">${escapeHtml(profile.pastStayDates.join("\n"))}</textarea></label>
      <label class="wide-label">往期寄养消费记录<textarea name="cat${index}SpendingHistory" rows="2">${escapeHtml(profile.spendingHistory.join("\n"))}</textarea></label>
      <div class="room-service-editor">
        <div class="row-title"><strong>客房服务下单记录</strong>${pill("选填", "blue")}</div>
        ${roomServiceOrderFields(`cat${index}`, profile.roomServiceOrders)}
      </div>
    </section>
  `;
}

function roomServiceOrderFields(prefix, orders = []) {
  const rows = [0, 1, 2].map((orderIndex) => orders[orderIndex] || {});
  return rows.map((order, orderIndex) => `
    <div class="room-service-row">
      <label>购买日期<input name="${prefix}Service${orderIndex + 1}Date" type="date" value="${escapeAttr(order.purchaseDate || "")}"></label>
      <label>产品名称<input name="${prefix}Service${orderIndex + 1}Name" value="${escapeAttr(order.productName || "")}" placeholder="例如：梳毛护理"></label>
      <label>单价<input name="${prefix}Service${orderIndex + 1}UnitPrice" type="number" min="0" step="0.01" value="${escapeAttr(order.unitPrice || order.price || "")}"></label>
      <label>数量<input name="${prefix}Service${orderIndex + 1}Quantity" type="number" min="0" step="1" value="${escapeAttr(order.quantity || "")}"></label>
      <label>总金额<input name="${prefix}Service${orderIndex + 1}TotalAmount" type="number" min="0" step="0.01" value="${escapeAttr(order.totalAmount || "")}"></label>
      <label>用法<input name="${prefix}Service${orderIndex + 1}Usage" value="${escapeAttr(order.usage || "")}" placeholder="每日一次/一次性"></label>
      <label>备注<input name="${prefix}Service${orderIndex + 1}Note" value="${escapeAttr(order.note || "")}"></label>
    </div>
  `).join("");
}

function openCatDialog(id = "", prefill = {}) {
  const cat = state.cats.find((item) => item.id === id);
  const normalizedCat = cat ? normalizeStay(cat) : null;
  catDialogTitle.textContent = normalizedCat ? `编辑 ${normalizedCat.catName}` : "新增家庭入住";
  catForm.reset();

  const defaultRoomType = prefill.roomType || normalizedCat?.roomType || roomTypes[0].type;
  const defaultRoom = prefill.room || normalizedCat?.room || roomTypes[0].rooms[0];
  const defaults = {
    id: "",
    ownerName: "",
    phone: "",
    emergencyContact: "",
    roomType: defaultRoomType,
    room: defaultRoom,
    status: "入住中",
    checkInDate: isoDate(new Date()),
    expectedCheckoutDate: isoDate(addDays(new Date(), 7)),
    dailyRate: estimateBoardingTotal(defaultRoomType, isoDate(new Date()), isoDate(addDays(new Date(), 7))),
    deposit: 300,
    paymentStatus: "仅押金"
  };
  const values = { ...defaults, ...(normalizedCat || {}), ...prefill };
  Object.entries(values).forEach(([key, value]) => {
    if (catForm.elements[key]) catForm.elements[key].value = value;
  });
  updateRoomSelect(values.roomType, values.room);
  renderFamilyCatEditor(values.familyCats || prefill.familyCats || []);

  catDialog.showModal();
}

function saveCatFromForm(event) {
  event.preventDefault();
  const data = Object.fromEntries(new FormData(catForm).entries());
  const familyCats = collectFamilyCats(data, data.checkInDate);
  const roomMeta = roomMetaFor(data.room);
  const cat = {
    id: data.id || crypto.randomUUID(),
    catName: familyCats.map((item) => item.name).join(" / "),
    breed: familyCats.map((item) => item.breed).filter(Boolean).join(" / "),
    age: familyCats.map((item) => smartAgeText(item, data.checkInDate)).join(" / "),
    gender: familyCats.map((item) => item.gender).filter(Boolean).join(" / "),
    temperament: "",
    ownerName: data.ownerName.trim(),
    phone: data.phone.trim(),
    emergencyContact: data.emergencyContact.trim(),
    room: data.room.trim(),
    roomType: data.roomType,
    status: data.status,
    checkInDate: data.checkInDate || isoDate(new Date()),
    expectedCheckoutDate: data.expectedCheckoutDate || isoDate(addDays(new Date(), 7)),
    dailyRate: Number(data.dailyRate || estimateBoardingTotal(data.roomType, data.checkInDate, data.expectedCheckoutDate) || roomMeta?.price || 0),
    deposit: Number(data.deposit || 0),
    paymentStatus: data.paymentStatus,
    feedingPlan: familyCats.map((item) => `${item.name}: 干粮${item.feeding.dryFoodGrams || 0}g 湿粮${item.feeding.wetFoodGrams || 0}g ${item.feeding.dietNotes || ""}`).join("；"),
    healthNotes: familyCats.map((item) => `${item.name}: ${item.specialNotes || ""} ${item.medicationNotes || ""}`).join("；"),
    familyCats,
    lastUpdated: new Date().toISOString()
  };

  const index = state.cats.findIndex((item) => item.id === cat.id);
  if (index >= 0) state.cats[index] = cat;
  else state.cats.unshift(cat);

  saveState();
  catDialog.close();
  render();
}

function collectFamilyCats(data, checkInDate) {
  const cats = [];
  for (let index = 1; index <= 3; index += 1) {
    const name = String(data[`cat${index}Name`] || "").trim();
    if (!name) continue;
    const firstStayDate = data[`cat${index}FirstStayDate`] || checkInDate || isoDate(new Date());
    const ageMonths = data[`cat${index}AgeMonths`] || "";
    const ageYears = data[`cat${index}AgeYears`] || "";
    const birthDate = data[`cat${index}BirthDate`] || "";
    cats.push(normalizeCatProfile({
      name,
      gender: data[`cat${index}Gender`],
      birthDate,
      ageMonthsAtFirstRegistration: ageMonths,
      ageYearsAtFirstRegistration: ageYears,
      breed: data[`cat${index}Breed`],
      coatColor: data[`cat${index}CoatColor`],
      vaccineDate: data[`cat${index}VaccineDate`],
      neuterStatus: data[`cat${index}NeuterStatus`],
      specialNotes: data[`cat${index}SpecialNotes`],
      medicationNotes: data[`cat${index}MedicationNotes`],
      feeding: {
        dryFoodGrams: data[`cat${index}DryFood`],
        wetFoodGrams: data[`cat${index}WetFood`],
        supplements: data[`cat${index}Supplements`],
        dietNotes: data[`cat${index}DietNotes`]
      },
      firstStayDate,
      pastStayDates: textLines(data[`cat${index}PastStayDates`]),
      spendingHistory: textLines(data[`cat${index}SpendingHistory`]),
      roomServiceOrders: collectRoomServiceOrders(data, `cat${index}`)
    }));
  }
  return cats.slice(0, 3);
}

function collectCustomerCats(data, checkInDate) {
  const cats = [];
  for (let index = 1; index <= 3; index += 1) {
    const prefix = `customerCat${index}`;
    const name = String(data[`${prefix}Name`] || "").trim();
    if (!name) continue;
    const firstStayDate = data[`${prefix}FirstStayDate`] || checkInDate || isoDate(new Date());
    const ageMonths = data[`${prefix}AgeMonths`] || "";
    const ageYears = data[`${prefix}AgeYears`] || "";
    cats.push(normalizeCatProfile({
      name,
      gender: data[`${prefix}Gender`],
      birthDate: data[`${prefix}BirthDate`] || "",
      ageMonthsAtFirstRegistration: ageMonths,
      ageYearsAtFirstRegistration: ageYears,
      breed: data[`${prefix}Breed`],
      coatColor: data[`${prefix}CoatColor`],
      vaccineDate: data[`${prefix}VaccineDate`],
      neuterStatus: data[`${prefix}NeuterStatus`],
      specialNotes: data[`${prefix}SpecialNotes`],
      medicationNotes: data[`${prefix}MedicationNotes`],
      feeding: {
        dryFoodGrams: data[`${prefix}DryFood`],
        wetFoodGrams: data[`${prefix}WetFood`],
        supplements: data[`${prefix}Supplements`],
        dietNotes: data[`${prefix}DietNotes`]
      },
      firstStayDate,
      pastStayDates: textLines(data[`${prefix}PastStayDates`]),
      spendingHistory: textLines(data[`${prefix}SpendingHistory`]),
      roomServiceOrders: collectRoomServiceOrders(data, prefix)
    }));
  }
  return cats.slice(0, 3);
}

function collectRoomServiceOrders(data, prefix) {
  const orders = [];
  for (let index = 1; index <= 3; index += 1) {
    const order = {
      id: crypto.randomUUID(),
      purchaseDate: data[`${prefix}Service${index}Date`] || "",
      productName: String(data[`${prefix}Service${index}Name`] || "").trim(),
      unitPrice: data[`${prefix}Service${index}UnitPrice`] || data[`${prefix}Service${index}Price`] || "",
      quantity: data[`${prefix}Service${index}Quantity`] || "",
      totalAmount: data[`${prefix}Service${index}TotalAmount`] || "",
      usage: String(data[`${prefix}Service${index}Usage`] || "").trim(),
      note: String(data[`${prefix}Service${index}Note`] || "").trim()
    };
    if (order.purchaseDate || order.productName || order.unitPrice || order.quantity || order.totalAmount || order.usage || order.note) {
      orders.push(order);
    }
  }
  return orders;
}

function saveCustomer(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  const cats = collectCustomerCats(data, data.checkInDate);
  state.customers.unshift({
    id: crypto.randomUUID(),
    customerName: data.customerName.trim(),
    idNumber: data.idNumber.trim(),
    phone: data.phone.trim(),
    emergencyPhone: data.emergencyPhone.trim(),
    checkInDate: data.checkInDate || isoDate(new Date()),
    cats,
    createdAt: new Date().toISOString()
  });
  saveState();
  form.reset();
  render();
}

function deleteCat(id) {
  if (session.role !== "admin") return;
  state.cats = state.cats.filter((cat) => cat.id !== id);
  saveState();
  render();
}

function saveAppointment(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  const appointment = {
    id: crypto.randomUUID(),
    date: data.date || selectedDate,
    time: data.time || "10:00",
    catName: data.catName.trim(),
    ownerName: data.ownerName.trim(),
    phone: data.phone.trim(),
    service: data.service.trim() || "预约入住",
    roomPreference: data.roomPreference.trim(),
    status: data.status,
    note: data.note.trim()
  };
  state.appointments.unshift(appointment);
  selectedDate = appointment.date;
  calendarCursor = startOfMonth(new Date(`${appointment.date}T00:00:00`));
  saveState();
  form.reset();
  render();
}

function deleteAppointment(id) {
  if (session.role !== "admin") return;
  state.appointments = state.appointments.filter((item) => item.id !== id);
  saveState();
  render();
}

function appointmentToCat(id) {
  const appointment = state.appointments.find((item) => item.id === id);
  if (!appointment) return;
  openCatDialog("", {
    catName: appointment.catName,
    ownerName: appointment.ownerName,
    phone: appointment.phone,
    room: appointment.roomPreference,
    checkInDate: appointment.date,
    expectedCheckoutDate: isoDate(addDays(new Date(`${appointment.date}T00:00:00`), 7)),
    healthNotes: appointment.note
  });
}

function saveSchedule(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  state.schedules.push({
    id: crypto.randomUUID(),
    date: data.date || isoDate(new Date()),
    employeeName: data.employeeName.trim(),
    status: data.status,
    shiftStart: data.status === "上班" ? data.shiftStart : "",
    shiftEnd: data.status === "上班" ? data.shiftEnd : "",
    role: data.role.trim(),
    note: data.note.trim()
  });
  saveState();
  form.reset();
  form.elements.date.value = isoDate(new Date());
  form.elements.shiftStart.value = "09:30";
  form.elements.shiftEnd.value = "18:30";
  render();
}

function deleteSchedule(id) {
  if (session.role !== "admin") return;
  state.schedules = state.schedules.filter((item) => item.id !== id);
  saveState();
  render();
}

function autoClockIn() {
  const today = isoDate(new Date());
  const exists = state.attendance.some((item) => item.date === today && item.employeeName === session.name);
  if (exists) return;
  state.attendance.unshift({
    id: crypto.randomUUID(),
    date: today,
    employeeName: session.name,
    time: timeText(new Date()),
    source: "移动端到店自动打卡",
    note: "电脑版原型中为模拟记录；手机版会用门店定位范围触发",
    createdAt: new Date().toISOString()
  });
  saveState();
  render();
}

function saveExtraReminder(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const content = form.elements.content.value.trim();
  if (!content) return;
  state.reminders.unshift({
    id: crypto.randomUUID(),
    type: "额外提醒",
    time: timeText(new Date()),
    content,
    enabled: true
  });
  saveState();
  form.reset();
  render();
}

function saveWechatConfig(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  state.wechatConfig = {
    targetName: form.elements.targetName.value.trim() || "员工工作群",
    autoOpen: Boolean(form.elements.autoOpen.checked)
  };
  saveState();
  alert("微信配置已保存。");
  render();
}

function saveCleaningTask(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  state.cleaningTasks.unshift(normalizeCleaningTask({
    id: crypto.randomUUID(),
    cadence: form.elements.cadence.value,
    area: form.elements.area.value.trim(),
    content: form.elements.content.value.trim(),
    time: form.elements.time.value.trim(),
    sendTime: form.elements.sendTime.value,
    enabled: true
  }));
  saveState();
  form.reset();
  render();
}

function saveCleaningTaskEdit(id) {
  if (session.role !== "admin") return;
  const task = state.cleaningTasks.find((item) => item.id === id);
  const card = document.querySelector(`[data-cleaning-editor="${id}"]`);
  if (!task || !card) return;
  task.cadence = editorValue(card, "cleaning", "cadence");
  task.time = editorValue(card, "cleaning", "time");
  task.sendTime = editorValue(card, "cleaning", "sendTime");
  task.area = editorValue(card, "cleaning", "area");
  task.content = editorValue(card, "cleaning", "content");
  saveState();
  render();
}

function toggleCleaningTask(id) {
  if (session.role !== "admin") return;
  const task = state.cleaningTasks.find((item) => item.id === id);
  if (!task) return;
  task.enabled = !task.enabled;
  saveState();
  render();
}

function deleteCleaningTask(id) {
  if (session.role !== "admin") return;
  state.cleaningTasks = state.cleaningTasks.filter((item) => item.id !== id);
  saveState();
  render();
}

function saveManagedReminder(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  state.reminders.unshift(normalizeManagedReminder({
    id: crypto.randomUUID(),
    type: form.elements.type.value.trim(),
    time: form.elements.time.value.trim(),
    sendTime: form.elements.sendTime.value,
    content: form.elements.content.value.trim(),
    enabled: true
  }));
  saveState();
  form.reset();
  render();
}

function saveManagedReminderEdit(id) {
  if (session.role !== "admin") return;
  const reminder = state.reminders.find((item) => item.id === id);
  const card = document.querySelector(`[data-reminder-editor="${id}"]`);
  if (!reminder || !card) return;
  reminder.type = editorValue(card, "reminder", "type");
  reminder.time = editorValue(card, "reminder", "time");
  reminder.sendTime = editorValue(card, "reminder", "sendTime");
  reminder.content = editorValue(card, "reminder", "content");
  saveState();
  render();
}

function toggleManagedReminder(id) {
  if (session.role !== "admin") return;
  const reminder = state.reminders.find((item) => item.id === id);
  if (!reminder) return;
  reminder.enabled = !reminder.enabled;
  saveState();
  render();
}

function deleteManagedReminder(id) {
  if (session.role !== "admin") return;
  state.reminders = state.reminders.filter((item) => item.id !== id);
  saveState();
  render();
}

function editorValue(card, group, field) {
  const input = card.querySelector(`[data-${group}-field="${field}"]`);
  return input ? input.value.trim() : "";
}

function checkScheduledWechatDrafts() {
  if (!session || session.role !== "admin" || state.wechatConfig?.autoOpen === false) return;
  const now = new Date();
  const current = clockValue(now);
  const items = todayReminderItems().filter((item) => clockValueFromText(item.sendTime) === current);
  if (!items.length) return;
  const key = `roomie-wechat-${isoDate(now)}-${current}`;
  if (sessionStorage.getItem(key)) return;
  sessionStorage.setItem(key, "1");
  sendWechatReminders(items);
}

async function sendWechatReminders(itemsOverride) {
  if (session.role !== "admin") return;
  const items = Array.isArray(itemsOverride) ? itemsOverride : todayReminderItems();
  if (!items.length) {
    alert("现在没有需要转发的提醒。");
    return;
  }

  const sentAt = new Date().toISOString();
  try {
    const result = await openWechatDraft(wechatReminderText(items));
    state.wechatLogs.unshift({
      id: crypto.randomUUID(),
      sentAt,
      operatorName: session.name,
      count: items.length,
      status: "已复制待发送",
      message: result.message || "已复制提醒内容并打开微信"
    });
    saveState();
    alert("已复制提醒内容并尝试打开微信，请在微信里选择聊天后发送。");
  } catch (error) {
    state.wechatLogs.unshift({
      id: crypto.randomUUID(),
      sentAt,
      operatorName: session.name,
      count: items.length,
      status: "发送失败",
      message: error.message || "微信跳转失败"
    });
    saveState();
    alert(`微信跳转失败：${error.message || "请检查电脑是否安装微信"}`);
  }
  render();
}

function wechatReminderText(items) {
  const lines = [
    `Roomie 今日强提醒 ${isoDate(new Date())}`,
    `发送人：${session.name}`,
    `建议发送到：${state.wechatConfig?.targetName || "员工工作群"}`,
    ""
  ];
  items.forEach((item, index) => {
    lines.push(`${index + 1}. ${item.title} ${item.time ? `(${item.time})` : ""}${item.sendTime ? ` · 微信提醒 ${item.sendTime}` : ""}`);
    lines.push(`   ${item.content}`);
  });
  return lines.join("\n");
}

function openWechatDraft(text) {
  const nativeHandler = window.webkit?.messageHandlers?.roomieNative;
  if (nativeHandler) {
    return new Promise((resolve, reject) => {
      const requestId = crypto.randomUUID();
      nativeCallbacks.set(requestId, { resolve, reject });
      nativeHandler.postMessage({
        action: "openWechat",
        requestId,
        text
      });
      setTimeout(() => {
        if (!nativeCallbacks.has(requestId)) return;
        nativeCallbacks.delete(requestId);
        reject(new Error("微信跳转超时，请稍后重试"));
      }, 12000);
    });
  }

  return navigator.clipboard.writeText(text).then(() => {
    window.location.href = "weixin://";
    return { success: true, message: "已复制提醒内容并尝试打开微信" };
  });
}

async function saveDocument(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const file = form.elements.file.files[0];
  if (!file) return;

  const dataUrl = await readFileAsDataUrl(file);
  const isText = file.type.startsWith("text/") || /\.(txt|md|csv)$/i.test(file.name);
  const contentText = isText ? await readFileAsText(file) : "";

  state.documents.unshift({
    id: crypto.randomUUID(),
    title: form.elements.title.value.trim(),
    category: form.elements.category.value,
    fileName: file.name,
    mimeType: file.type || "application/octet-stream",
    size: file.size,
    uploadedBy: session.name,
    updatedAt: new Date().toISOString(),
    contentText,
    dataUrl
  });
  saveState();
  form.reset();
  render();
}

function markDocumentRead(id) {
  const exists = readReceiptFor(id);
  if (exists) return;
  state.readReceipts.unshift({
    id: crypto.randomUUID(),
    documentId: id,
    employeeName: session.name,
    role: session.role,
    readAt: new Date().toISOString()
  });
  saveState();
  render();
}

function deleteDocument(id) {
  if (session.role !== "admin") return;
  state.documents = state.documents.filter((doc) => doc.id !== id);
  state.readReceipts = state.readReceipts.filter((item) => item.documentId !== id);
  saveState();
  render();
}

async function openDocumentFile(id) {
  const doc = state.documents.find((item) => item.id === id);
  if (!doc || !doc.dataUrl) return;
  if (!readReceiptFor(id)) {
    state.readReceipts.unshift({
      id: crypto.randomUUID(),
      documentId: id,
      employeeName: session.name,
      role: session.role,
      readAt: new Date().toISOString()
    });
    saveState();
  }

  try {
    const nativeHandler = window.webkit?.messageHandlers?.roomieNative;
    if (nativeHandler) {
      await new Promise((resolve, reject) => {
        const requestId = crypto.randomUUID();
        nativeCallbacks.set(requestId, { resolve, reject });
        nativeHandler.postMessage({
          action: "openDocument",
          requestId,
          fileName: doc.fileName || doc.title || "roomie-document",
          dataUrl: doc.dataUrl
        });
        setTimeout(() => {
          if (!nativeCallbacks.has(requestId)) return;
          nativeCallbacks.delete(requestId);
          reject(new Error("打开文件超时，请稍后重试"));
        }, 12000);
      });
    } else {
      const anchor = document.createElement("a");
      anchor.href = doc.dataUrl;
      anchor.target = "_blank";
      anchor.rel = "noreferrer";
      anchor.download = doc.fileName || "roomie-document";
      document.body.append(anchor);
      anchor.click();
      anchor.remove();
    }
  } catch (error) {
    alert(`原文件打开失败：${error.message || "请重新上传文件后再试"}`);
  }
  render();
}

function saveInventoryItem(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  state.inventory.unshift(normalizeInventoryItem({
    id: crypto.randomUUID(),
    name: data.name.trim(),
    category: data.category,
    quantity: data.quantity,
    unit: data.unit,
    purchaseDate: data.purchaseDate || isoDate(new Date()),
    expiryDate: data.expiryDate,
    safetyStock: data.safetyStock,
    unitCost: data.unitCost,
    supplier: data.supplier.trim(),
    location: data.location.trim(),
    note: data.note.trim(),
    updatedAt: new Date().toISOString(),
    updatedBy: session.name
  }));
  saveState();
  form.reset();
  form.elements.purchaseDate.value = isoDate(new Date());
  render();
}

function adjustInventory(id, delta) {
  const item = state.inventory.find((entry) => entry.id === id);
  if (!item) return;
  item.quantity = Math.max(0, Number(item.quantity || 0) + delta);
  item.updatedAt = new Date().toISOString();
  item.updatedBy = session.name;
  saveState();
  render();
}

function deleteInventoryItem(id) {
  if (session.role !== "admin") return;
  state.inventory = state.inventory.filter((item) => item.id !== id);
  saveState();
  render();
}

function reviewUser(id, status) {
  if (session.role !== "admin") return;
  const user = state.users.find((item) => item.id === id && item.role === "staff");
  if (!user) return;
  user.status = status;
  user.approvedAt = new Date().toISOString();
  user.approvedBy = session.name;
  saveState();
  render();
}

async function saveProfile(event) {
  event.preventDefault();
  const user = currentUser();
  if (!user) return;
  const form = event.currentTarget;
  const nextUsername = normalizeUsername(form.elements.username.value);
  const otherUser = state.users.find((item) => item.username === nextUsername && item.id !== user.id);
  if (otherUser) {
    alert("用户名已被其他账号使用");
    return;
  }
  user.username = nextUsername;
  user.name = form.elements.name.value.trim();
  user.gender = form.elements.gender.value;
  user.birthDate = form.elements.birthDate.value;
  user.phone = form.elements.phone.value.trim();
  user.position = form.elements.position.value.trim();
  user.pin = form.elements.pin.value.trim();
  if (user.role === "staff") {
    user.idNumber = form.elements.idNumber.value.trim();
    user.hireDate = form.elements.hireDate.value;
    user.emergencyPhone = form.elements.emergencyPhone.value.trim();
  }
  const avatarFile = form.elements.avatar.files[0];
  if (avatarFile) {
    user.avatar = await readFileAsDataUrl(avatarFile);
  }
  session.name = user.name;
  session.username = user.username;
  setSession(session);
  saveState();
  render();
}

function saveAdminStaffProfile(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const user = state.users.find((item) => item.id === form.dataset.staffProfileForm && item.role === "staff");
  if (!user) return;
  const nextUsername = normalizeUsername(form.elements.username.value);
  const otherUser = state.users.find((item) => item.username === nextUsername && item.id !== user.id);
  if (otherUser) {
    alert("用户名已被其他账号使用");
    return;
  }
  const nextPin = form.elements.pin.value.trim();
  if (!nextPin) {
    alert("员工 PIN 不能为空");
    return;
  }
  user.username = nextUsername;
  user.name = form.elements.name.value.trim();
  user.gender = form.elements.gender.value;
  user.birthDate = form.elements.birthDate.value;
  user.phone = form.elements.phone.value.trim();
  user.position = form.elements.position.value.trim();
  user.idNumber = form.elements.idNumber.value.trim();
  user.hireDate = form.elements.hireDate.value;
  user.emergencyPhone = form.elements.emergencyPhone.value.trim();
  user.pin = nextPin;
  user.employeeStatus = form.elements.employeeStatus.value;
  user.updatedAt = new Date().toISOString();
  user.updatedBy = session.name;
  saveState();
  render();
}

function updateStaffPin(id) {
  if (session.role !== "admin") return;
  const user = state.users.find((item) => item.id === id && item.role === "staff");
  const input = [...document.querySelectorAll("[data-staff-pin]")].find((item) => item.dataset.staffPin === id);
  if (!user || !input) return;
  user.pin = input.value.trim();
  saveState();
  render();
}

function updateEmployeeStatus(id) {
  if (session.role !== "admin") return;
  const user = state.users.find((item) => item.id === id && item.role === "staff");
  const select = [...document.querySelectorAll("[data-employee-status]")].find((item) => item.dataset.employeeStatus === id);
  if (!user || !select) return;
  user.employeeStatus = select.value;
  saveState();
  render();
}

function deleteUserAccount(id) {
  if (session.role !== "admin") return;
  state.users = state.users.filter((item) => item.id !== id || item.role !== "staff");
  saveState();
  render();
}

function staffUserPayload(user) {
  const normalized = normalizeUser(user);
  return {
    id: normalized.id,
    role: "staff",
    name: normalized.name,
    username: normalized.username,
    gender: normalized.gender,
    birthDate: normalized.birthDate,
    phone: normalized.phone,
    position: normalized.position,
    idNumber: normalized.idNumber,
    hireDate: normalized.hireDate,
    emergencyPhone: normalized.emergencyPhone,
    pin: normalized.pin,
    status: normalized.status,
    employeeStatus: normalized.employeeStatus,
    createdAt: normalized.createdAt,
    approvedAt: normalized.approvedAt,
    approvedBy: normalized.approvedBy,
    note: normalized.note
  };
}

function downloadStaffApplication(user) {
  downloadJson({
    roomieType: STAFF_APPLICATION_FILE_TYPE,
    version: 1,
    exportedAt: new Date().toISOString(),
    user: staffUserPayload({ ...user, status: "pending", approvedAt: "", approvedBy: "" })
  }, `roomie-staff-application-${safeFilePart(user.username)}-${isoDate(new Date())}.json`);
}

function exportStaffAccounts() {
  if (session.role !== "admin") return;
  const users = state.users
    .filter((user) => user.role === "staff" && user.status !== "rejected")
    .map(staffUserPayload);
  downloadJson({
    roomieType: STAFF_ACCOUNTS_FILE_TYPE,
    version: 1,
    exportedAt: new Date().toISOString(),
    users
  }, `roomie-staff-login-${isoDate(new Date())}.json`);
}

async function importStaffSyncFile(event) {
  const input = event.currentTarget;
  const file = input.files?.[0];
  if (!file) return;
  try {
    const payload = JSON.parse(await readFileAsText(file));
    let message = "";
    if (payload.roomieType === STAFF_APPLICATION_FILE_TYPE && payload.user) {
      const count = mergeStaffUsers([payload.user], { asApplication: true });
      message = `已导入 ${count} 个员工申请，管理员可在账号审核中通过。`;
    } else if (payload.roomieType === STAFF_ACCOUNTS_FILE_TYPE && Array.isArray(payload.users)) {
      const count = mergeStaffUsers(payload.users);
      message = `已同步 ${count} 个员工账号，可以使用已通过账号登录。`;
    } else if (Array.isArray(payload.users)) {
      const count = mergeStaffUsers(payload.users.filter((user) => user.role === "staff"));
      message = `已从数据文件中同步 ${count} 个员工账号。`;
    } else {
      throw new Error("unsupported");
    }
    saveState();
    if (session) {
      alert(message);
      render();
    } else {
      loginMode = "login";
      loginRole = "staff";
      authNotice = message;
      render();
    }
  } catch {
    const message = "无法识别这个文件，请确认导入的是 Roomie 员工申请文件或员工登录文件。";
    if (session) {
      alert(message);
    } else {
      authNotice = message;
      render();
    }
  } finally {
    input.value = "";
  }
}

function mergeStaffUsers(users, options = {}) {
  let count = 0;
  users.forEach((rawUser) => {
    if (!rawUser || rawUser.role !== "staff") return;
    const existingIndex = state.users.findIndex((user) => (
      user.role === "staff" && (user.id === rawUser.id || user.username === normalizeUsername(rawUser.username))
    ));
    const existing = existingIndex >= 0 ? state.users[existingIndex] : null;
    const next = normalizeUser({
      ...existing,
      ...rawUser,
      role: "staff",
      username: normalizeUsername(rawUser.username || existing?.username),
      status: options.asApplication && existing?.status === "active" ? "active" : (options.asApplication ? "pending" : rawUser.status || existing?.status || "pending"),
      approvedAt: options.asApplication && existing?.status !== "active" ? "" : rawUser.approvedAt || existing?.approvedAt || "",
      approvedBy: options.asApplication && existing?.status !== "active" ? "" : rawUser.approvedBy || existing?.approvedBy || ""
    });
    if (!next.username || !next.pin) return;
    if (existingIndex >= 0) {
      state.users[existingIndex] = next;
    } else {
      state.users.unshift(next);
    }
    count += 1;
  });
  return count;
}

function printDocument(id) {
  const doc = state.documents.find((item) => item.id === id);
  if (!doc) return;
  if (!readReceiptFor(id)) {
    state.readReceipts.unshift({
      id: crypto.randomUUID(),
      documentId: id,
      employeeName: session.name,
      role: session.role,
      readAt: new Date().toISOString()
    });
  }
  state.printReceipts.unshift({
    id: crypto.randomUUID(),
    documentId: id,
    employeeName: session.name,
    role: session.role,
    printedAt: new Date().toISOString()
  });
  saveState();
  const printPayload = document.getElementById("printPayload");
  if (!printPayload) return;
  printPayload.innerHTML = `
    <h1>${escapeHtml(doc.title)}</h1>
    <p>${escapeHtml(doc.category)} · ${escapeHtml(doc.fileName)} · ${dateTimeText(doc.updatedAt)}</p>
    <pre>${escapeHtml(doc.contentText || "此文件为上传附件，请在资料库中打开原文件查看。")}</pre>
  `;
  window.print();
  render();
}

function readReceiptFor(documentId) {
  return state.readReceipts.find((item) => item.documentId === documentId && item.employeeName === session.name);
}

function readCountFor(documentId) {
  return state.readReceipts.filter((item) => item.documentId === documentId).length;
}

function readFileAsDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

function readFileAsText(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsText(file);
  });
}

function saveReport(event) {
  event.preventDefault();
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  state.reports.unshift({
    id: crypto.randomUUID(),
    date: new Date().toISOString(),
    shift: data.shift,
    staffName: data.staffName.trim(),
    temperature: data.temperature.trim(),
    humidity: data.humidity.trim(),
    feedingDone: Boolean(data.feedingDone),
    litterDone: Boolean(data.litterDone),
    cleaningDone: Boolean(data.cleaningDone),
    medicineDone: Boolean(data.medicineDone),
    afternoonTeaDone: Boolean(data.afternoonTeaDone),
    roomType: data.roomType || "",
    room: data.room || "",
    occupancyNote: data.occupancyNote.trim(),
    incidents: data.incidents.trim(),
    handover: data.handover.trim(),
    todo: data.todo.trim(),
    createdAt: new Date().toISOString()
  });
  saveState();
  form.reset();
  render();
}

function deleteReport(id) {
  if (session.role !== "admin") return;
  state.reports = state.reports.filter((report) => report.id !== id);
  saveState();
  render();
}

function saveLedger(event) {
  event.preventDefault();
  if (session.role !== "admin") return;
  const form = event.currentTarget;
  const data = Object.fromEntries(new FormData(form).entries());
  state.ledger.unshift({
    id: crypto.randomUUID(),
    date: new Date().toISOString(),
    kind: data.kind,
    category: data.category.trim(),
    amount: Number(data.amount || 0),
    method: data.method.trim(),
    relatedCatName: data.relatedCatName.trim(),
    operatorName: data.operatorName.trim(),
    note: data.note.trim()
  });
  saveState();
  form.reset();
  form.elements.category.value = "住宿费";
  form.elements.method.value = "微信";
  form.elements.operatorName.value = session.name;
  render();
}

function deleteLedger(id) {
  if (session.role !== "admin") return;
  state.ledger = state.ledger.filter((entry) => entry.id !== id);
  saveState();
  render();
}

function exportData() {
  downloadJson(state, `roomie-data-${isoDate(new Date())}.json`);
}

async function downloadJson(payload, filename) {
  const content = JSON.stringify(payload, null, 2);
  const nativeHandler = window.webkit?.messageHandlers?.roomieNative;
  if (nativeHandler) {
    const requestId = crypto.randomUUID();
    showToast("正在打开保存位置…", "info");
    try {
      const result = await new Promise((resolve, reject) => {
        nativeCallbacks.set(requestId, { resolve, reject });
        nativeHandler.postMessage({
          action: "saveFile",
          requestId,
          fileName: filename,
          content
        });
      });
      showToast(result.message || "数据备份已保存", result.message === "已取消保存" ? "info" : "success");
    } catch (error) {
      showToast(error.message || "文件保存失败", "error", 6200);
    }
    return;
  }

  const blob = new Blob([content], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = filename;
  document.body.append(anchor);
  anchor.click();
  anchor.remove();
  URL.revokeObjectURL(url);
  showToast("数据备份已下载。", "success");
}

function safeFilePart(value) {
  return String(value || "staff").replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "staff";
}

function sumLedger(kind, todayOnly) {
  return state.ledger
    .filter((entry) => entry.kind === kind && (!todayOnly || isToday(entry.date)))
    .reduce((sum, entry) => sum + Number(entry.amount || 0), 0);
}

function nights(dateValue) {
  const start = new Date(dateValue);
  start.setHours(0, 0, 0, 0);
  const end = new Date();
  end.setHours(0, 0, 0, 0);
  return Math.max(1, Math.round((end - start) / (24 * 60 * 60 * 1000)));
}

function isToday(dateValue) {
  const date = new Date(dateValue);
  const today = new Date();
  return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth() && date.getDate() === today.getDate();
}

function isThisMonth(dateValue) {
  const date = new Date(dateValue);
  const today = new Date();
  return date.getFullYear() === today.getFullYear() && date.getMonth() === today.getMonth();
}

function startOfMonth(date) {
  return new Date(date.getFullYear(), date.getMonth(), 1);
}

function addDays(date, days) {
  const copy = new Date(date);
  copy.setDate(copy.getDate() + days);
  return copy;
}

function addMonths(date, months) {
  const copy = new Date(date);
  copy.setMonth(copy.getMonth() + months);
  return startOfMonth(copy);
}

function isoDate(date) {
  return new Date(date).toISOString().slice(0, 10);
}

function dateText(dateValue) {
  return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric" }).format(new Date(dateValue));
}

function dateTimeText(dateValue) {
  return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(dateValue));
}

function timeText(dateValue) {
  return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit" }).format(new Date(dateValue));
}

function clockValue(dateValue) {
  const date = new Date(dateValue);
  return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
}

function clockValueFromText(value) {
  const match = String(value || "").match(/(\d{1,2})[:：](\d{2})/);
  if (!match) return "";
  return `${String(Math.min(Number(match[1]), 23)).padStart(2, "0")}:${match[2]}`;
}

function timeBefore(value, minutesBefore) {
  const clock = clockValueFromText(value);
  if (!clock) return "";
  const [hour, minute] = clock.split(":").map(Number);
  const date = new Date();
  date.setHours(hour, minute, 0, 0);
  date.setMinutes(date.getMinutes() - minutesBefore);
  return clockValue(date);
}

function yuan(amount) {
  return new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY", maximumFractionDigits: amount % 1 ? 2 : 0 }).format(amount || 0);
}

function dateTimeSecondText(dateValue) {
  return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" }).format(new Date(dateValue));
}

function updateLiveClock() {
  const node = document.getElementById("liveDateTime");
  if (node) node.textContent = dateTimeSecondText(new Date());
}

async function fetchWenzhouWeather() {
  try {
    const response = await fetch("https://api.open-meteo.com/v1/forecast?latitude=28.01&longitude=120.65&current=temperature_2m,relative_humidity_2m,weather_code&timezone=Asia%2FShanghai");
    const data = await response.json();
    weatherState = {
      label: weatherCodeText(data.current?.weather_code),
      humidity: data.current?.relative_humidity_2m ?? "--",
      temperature: data.current?.temperature_2m ?? "--"
    };
  } catch {
    weatherState = { label: "天气离线", humidity: "--", temperature: "--" };
  }
  const node = document.getElementById("weatherText");
  if (node) node.textContent = weatherDisplayText();
}

function weatherDisplayText() {
  return `${weatherState.label} · ${weatherState.temperature}C · 湿度 ${weatherState.humidity}%`;
}

function weatherCodeText(code) {
  const map = {
    0: "晴", 1: "多云", 2: "多云", 3: "阴",
    45: "雾", 48: "雾",
    51: "小雨", 53: "小雨", 55: "小雨",
    61: "雨", 63: "雨", 65: "大雨",
    80: "阵雨", 81: "阵雨", 82: "强阵雨",
    95: "雷雨"
  };
  return map[code] || "天气同步中";
}

function parseAgeMonths(value) {
  const text = String(value || "");
  const number = Number((text.match(/\d+(\.\d+)?/) || [])[0] || 0);
  if (!number) return "";
  if (text.includes("岁")) return Math.round(number * 12);
  return Math.round(number);
}

function birthDateFromAgeMonths(months, referenceDate) {
  const count = Number(months || 0);
  if (!count) return "";
  const date = new Date(`${referenceDate || isoDate(new Date())}T00:00:00`);
  date.setMonth(date.getMonth() - count);
  return isoDate(date);
}

function smartAgeText(profile, atDate = new Date()) {
  let months = "";
  if (profile.birthDate) {
    months = monthsBetween(profile.birthDate, atDate);
  } else {
    const initialMonths = Number(profile.ageMonthsAtFirstRegistration || 0) || Math.round(Number(profile.ageYearsAtFirstRegistration || 0) * 12);
    if (!initialMonths) return "未填年龄";
    months = initialMonths + monthsBetween(profile.firstStayDate || new Date(), atDate);
  }
  months = Math.max(0, months);
  if (months < 12) return `${months} 个月`;
  const years = Math.floor(months / 12);
  const rest = months % 12;
  return rest ? `${years} 岁 ${rest} 个月` : `${years} 岁`;
}

function monthsBetween(startDate, endDate) {
  const start = new Date(startDate);
  const end = new Date(endDate);
  if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return 0;
  let months = (end.getFullYear() - start.getFullYear()) * 12 + end.getMonth() - start.getMonth();
  if (end.getDate() < start.getDate()) months -= 1;
  return months;
}

function formatBytes(bytes) {
  const size = Number(bytes || 0);
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
  return `${(size / 1024 / 1024).toFixed(1)} MB`;
}

function textLines(value) {
  return String(value || "").split(/\n+/).map((item) => item.trim()).filter(Boolean);
}

function escapeHtml(value) {
  return String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

function escapeAttr(value) {
  return escapeHtml(value).replaceAll("`", "&#096;");
}

setup();
