<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手术统计管理</title>
<script src="http://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="http://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<style>
  :root {
    --red: #8b1a1a;
    --red-light: #fcebeb;
    --bg: #f5f4ef;
    --white: #fff;
    --border: #c8c2b0;
    --text: #222;
    --muted: #666;
    --surface: #faf8f2;
    --accent-bg: #f0ebe0;
    --danger: #a32d2d;
    --green: #1d6e3c;
    --green-bg: #eaf3de;
    --blue: #185fa5;
    --blue-bg: #e6f1fb;
    --amber: #854f0b;
    --amber-bg: #faeeda;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body {
    font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
    background: var(--bg);
    color: var(--text);
    font-size: 13px;
    min-height: 100vh;
  }

  /* ---- 顶部导航 ---- */
  .nav-bar {
    background: var(--red);
    color: #fff;
    padding: 0 20px;
    display: flex;
    align-items: center;
    gap: 4px;
    position: sticky;
    top: 0;
    z-index: 100;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  }
  .nav-bar .logo {
    font-size: 16px;
    font-weight: 500;
    letter-spacing: 2px;
    padding: 12px 16px 12px 0;
    border-right: 1px solid rgba(255,255,255,0.2);
    margin-right: 8px;
    white-space: nowrap;
  }
  .nav-tab {
    padding: 12px 16px;
    cursor: pointer;
    border: none;
    background: none;
    color: rgba(255,255,255,0.7);
    font-size: 13px;
    transition: all 0.15s;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
  }
  .nav-tab:hover { color: #fff; background: rgba(255,255,255,0.08); }
  .nav-tab.active { color: #fff; border-bottom-color: #fff; font-weight: 500; }

  /* ---- 页面容器 ---- */
  .page { display: none; }
  .page.active { display: block; }
  .page-content {
    max-width: 1600px;
    margin: 0 auto;
    padding: 20px 30px;
    overflow-x: auto;
  }

  /* ---- 通用组件 ---- */
  .card {
    background: var(--white);
    border: 1px solid #d4cfc0;
    border-radius: 4px;
    padding: 20px;
    margin-bottom: 16px;
  }
  .card h2 {
    font-size: 15px;
    font-weight: 500;
    color: var(--red);
    margin-bottom: 14px;
    padding-bottom: 10px;
    border-bottom: 1px solid #e8e4d4;
  }
  .btn {
    padding: 7px 14px;
    border: 1px solid var(--border);
    background: var(--white);
    color: var(--text);
    border-radius: 3px;
    cursor: pointer;
    font-size: 13px;
    transition: all 0.15s;
    font-family: inherit;
  }
  .btn:hover { background: var(--accent-bg); }
  .btn.primary { background: var(--red); color: #fff; border-color: var(--red); }
  .btn.primary:hover { background: #6f1414; }
  .btn.danger { color: var(--danger); border-color: #d6a8a8; }
  .btn.danger:hover { background: var(--red-light); }
  .btn.small { padding: 4px 10px; font-size: 12px; }

  .stat-cards { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
  .stat-card {
    flex: 1; min-width: 120px;
    background: var(--white);
    border: 1px solid var(--border);
    border-radius: 4px;
    padding: 14px 16px;
    text-align: center;
  }
  .stat-card .value { font-size: 28px; font-weight: 500; color: var(--text); }
  .stat-card .label { font-size: 12px; color: var(--muted); margin-top: 2px; }
  .stat-card.alert .value { color: var(--danger); }
  .stat-card.accent { border-left: 3px solid var(--red); }

  .filter-row {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-bottom: 12px;
  }
  .filter-row input, .filter-row select {
    padding: 6px 10px;
    border: 1px solid var(--border);
    border-radius: 3px;
    font-size: 13px;
    background: var(--white);
    font-family: inherit;
  }
  .filter-row input[type="date"] { width: 130px; }
  .filter-row select { min-width: 100px; }

  .search-box {
    padding: 7px 12px;
    border: 1px solid var(--border);
    border-radius: 3px;
    font-size: 13px;
    width: 200px;
    font-family: inherit;
  }

  /* ---- 登记表 ---- */
  .header {
    text-align: center;
    margin-bottom: 20px;
    padding-bottom: 16px;
    border-bottom: 2px solid var(--red);
  }
  .header h1 {
    font-size: 22px;
    font-weight: 500;
    letter-spacing: 8px;
    color: var(--text);
  }
  .header h2 {
    font-size: 28px;
    font-weight: 500;
    letter-spacing: 12px;
    margin-top: 8px;
  }
  .header .meta {
    margin-top: 10px;
    font-size: 13px;
    color: var(--muted);
    display: flex;
    justify-content: space-between;
    padding: 0 10px;
  }
  .header .meta input {
    border: none;
    border-bottom: 1px dotted #aaa;
    background: transparent;
    text-align: center;
    font-size: 13px;
    color: var(--text);
    width: 200px;
  }

  table.ledger {
    width: max-content; min-width: 100%;
    border-collapse: collapse;
    font-size: 12px;
    table-layout: auto;
  }
  table.ledger th, table.ledger td {
    border: 1px solid #2c2c2a;
    text-align: center;
    padding: 0;
    min-height: 34px; height: 34px;       // 默认 34px，分值列加交接分支时可自动撑高
    vertical-align: middle;
  }
  table.ledger th {
    background: var(--accent-bg);
    font-weight: 500;
    height: 36px;
  }
  table.ledger td input, table.ledger td select {
    width: 100%; height: 100%;
    border: none; background: transparent;
    text-align: center; font-size: 12px;
    padding: 2px 4px;
    font-family: inherit;
  }
  table.ledger td input:focus, table.ledger td select:focus {
    outline: 2px solid var(--red);
    outline-offset: -2px;
    background: #fffbe6;
  }
  table.ledger td.dur { color: var(--green); font-weight: 500; }
  table.ledger td.dur.zero { color: #999; }
  /* 手术名称列：保证最小宽度，长文本正常换行完整显示 */
  table.ledger td.surgery-cell,
  table.ledger th:nth-child(8) {
    min-width: 180px;
    width: 180px;
    word-break: normal;
    overflow-wrap: break-word;
    white-space: normal;
    line-height: 1.4;
  }
  /* 分值列：去掉 number 输入框自带的上下微调箭头，呈现为空白手填单元格 */
  /* 分值列：textarea 支持留痕表达式（如 3+4+6=13），框宽=单元格宽，内容多时竖向换行 */
  table.ledger textarea.score-inp {
    box-sizing: border-box;
    width: 100%;
    min-height: 22px;
    border: 1px solid #e8dcc0; border-radius: 3px;
    background: #fff8e1;
    font-family: inherit; font-size: 12px; line-height: 1.35;
    text-align: center;
    padding: 2px 3px;
    resize: none; overflow: hidden;
  }
  /* 无编辑分值权限时，分值框置灰只读 */
  table.ledger textarea.score-inp:disabled {
    background: #f0f0f0; color: #999; cursor: not-allowed;
  }
  /* 分值列包裹容器：护士名标签（上）+ 分值框（下），竖排，框撑满单元格宽度 */
  table.ledger .score-wrap {
    display: flex; flex-direction: column; align-items: stretch; gap: 2px;
  }
  table.ledger .score-label {
    font-size: 12px; color: #333; white-space: nowrap;
  }
  /* 分值列单元格内：主岗分值 + 交接分支（垂直堆叠，仅在已选交接护士时存在） */
  table.ledger .score-cell-stack {
    display: flex; flex-direction: column; gap: 2px; align-items: stretch;
  }
  table.ledger .score-wrap.score-branch {
    border-top: 1px dashed #b8d4ef; padding-top: 2px; margin-top: 2px;
  }
  table.ledger .score-wrap.score-branch .score-label {
    color: #2a7; font-weight: 600;
  }
  table.ledger .score-wrap.score-branch textarea.score-inp {
    background: #fffaf0;
  }
  /* 交接护士：底部触发项 + 单元格内横向子面板（[主岗▾] [时间__] [交接▾] [✕]，时间夹在两位护士之间） */
  table.ledger select option.ho-trigger-opt {
    color: #2a7; font-weight: 600; background: #f7fbff;
  }
  table.ledger td.nurse-cell {
    min-width: 95px;
  }
  table.ledger .nurse-wrap {
    display: flex; align-items: center; gap: 3px;
    flex-wrap: wrap; padding: 1px 3px;
  }
  table.ledger .nurse-wrap > select.nurse-primary {
    flex: 0 0 76px; min-width: 64px;
  }
  table.ledger .ho-row {
    display: inline-flex; align-items: center; gap: 3px;
    flex: 0 0 auto;
  }
  table.ledger input.ho-time {
    flex: 0 0 44px; width: 44px; font-size: 11px; padding: 1px 2px; text-align: center;
    border: 1px solid #d9d9d9; border-radius: 4px;
  }
  table.ledger select.ho-person {
    flex: 0 0 76px; min-width: 64px; font-size: 11px; padding: 1px 2px; color: #2a7;
    border: 1px dashed #b8d4ef; background: #f7fbff; border-radius: 4px;
  }
  table.ledger .row-chk {
    width: 22px; height: 22px; cursor: pointer; margin: 0;
  }
  table.ledger td:first-child,
  table.ledger th:first-child { min-width: 60px; text-align: center; }

  /* ---- OCR 页 ---- */
  .ocr-zone {
    border: 2px dashed var(--border);
    border-radius: 8px;
    padding: 40px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
  }
  .ocr-zone:hover { border-color: var(--red); background: var(--surface); }
  .ocr-zone .icon { font-size: 48px; margin-bottom: 12px; }
  .ocr-preview { max-height: 300px; margin: 12px auto; display: none; border: 1px solid var(--border); border-radius: 4px; }

  /* ---- 统计看板 ---- */
  .chart-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
  .chart-box { background: var(--white); border: 1px solid var(--border); border-radius: 4px; padding: 16px; min-height: 200px; display: flex; flex-direction: column; }
  .chart-box canvas { height: auto; }
  .chart-full { background: var(--white); border: 1px solid var(--border); border-radius: 4px; padding: 16px; margin-bottom: 16px; }
  .chart-full canvas { max-height: 320px; }

  /* 日统计简单表格 */
  .daily-stat-section { display: grid; grid-template-columns: 1fr; gap: 12px; }
  .stat-table-box { background: var(--white); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; }
  .stat-table-box h3 { font-size: 13px; font-weight: 500; padding: 10px 14px; margin: 0; background: var(--accent-bg); border-bottom: 1px solid var(--border); }
  table.stat-table { width: 100%; border-collapse: collapse; font-size: 13px; }
  table.stat-table th { background: #f8f3f3; font-weight: 500; padding: 7px 12px; border-bottom: 1px solid var(--border); text-align: center; font-size: 12px; color: var(--muted); }
  table.stat-table td { padding: 6px 12px; border-bottom: 1px solid #f0ecec; text-align: center; }
  table.stat-table td:first-child { text-align: left; font-weight: 500; }
  table.stat-table tr:last-child td { border-bottom: none; }
  table.stat-table .zero { color: #ccc; }
  table.stat-table tr:hover td { background: #faf6f6; }

  /* 可视化表格（带数据条） */
  .viz-table-box { background: var(--white); border: 1px solid var(--border); border-radius: 4px; overflow: hidden; margin-bottom: 12px; }
  .viz-table-box h3 { font-size: 13px; font-weight: 500; padding: 10px 14px; margin: 0; background: var(--accent-bg); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
  .viz-table-box h3 .hint { font-size: 11px; color: var(--muted); font-weight: normal; }
  .viz-table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .viz-table-scroll::-webkit-scrollbar { height: 4px; }
  .viz-table-scroll::-webkit-scrollbar-thumb { background: #ccc; border-radius: 2px; }
  table.viz-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 320px; }
  table.viz-table th { background: #faf5f5; font-weight: 500; padding: 8px 10px; border-bottom: 1px solid var(--border); text-align: center; font-size: 12px; color: var(--muted); white-space: nowrap; }
  table.viz-table td { padding: 7px 10px; border-bottom: 1px solid #f0ecec; text-align: center; font-size: 13px; }
  table.viz-table td.name { text-align: left; font-weight: 500; color: var(--text); white-space: nowrap; }
  table.viz-table td.num { font-variant-numeric: tabular-nums; }
  table.viz-table tr:last-child td { border-bottom: none; }
  table.viz-table tr:hover td { background: #faf6f6; }
  table.viz-table tr.zero-row td { color: #ccc; }
  table.viz-table .bar-cell { position: relative; padding: 0; min-width: 140px; }
  table.viz-table .bar-bg { display: block; height: 22px; line-height: 22px; padding: 0 8px; color: #fff; font-size: 12px; font-weight: 500; text-align: right; border-radius: 3px; white-space: nowrap; box-sizing: border-box; }
  table.viz-table .rank { display: inline-block; width: 22px; height: 22px; line-height: 22px; border-radius: 50%; color: #fff; font-size: 12px; font-weight: 600; background: #c4b8b8; }
  table.viz-table .rank.r1 { background: #c8911a; }
  table.viz-table .rank.r2 { background: #8b8b8b; }
  table.viz-table .rank.r3 { background: #b87333; }
  table.viz-table .rank.r4 { background: #5a8a9a; }
  table.viz-table .score-bar { background: linear-gradient(90deg, #8b1a1a, #b8302f); }
  table.viz-table .time-bar { background: linear-gradient(90deg, #185fa5, #2e7eb6); }
  table.viz-table .count-bar { background: linear-gradient(90deg, #2e7d32, #4a9d4e); }
  table.viz-table .zero-bar { background: #ececec; color: #aaa; }
  table.viz-table tfoot td { background: #faf5f5; font-weight: 500; border-top: 2px solid var(--border); }
  .stat-table-box .empty { padding: 20px; text-align: center; color: var(--muted); font-size: 13px; }

  /* ---- 绩效表 ---- */
  table.data-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
  }
  table.data-table th {
    background: var(--accent-bg);
    font-weight: 500;
    padding: 8px 12px;
    border: 1px solid var(--border);
    text-align: center;
  }
  table.data-table td {
    padding: 7px 12px;
    border: 1px solid var(--border);
    text-align: center;
  }
  table.data-table tr:hover td { background: var(--surface); }

  /* ---- 模态框 ---- */
  .modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.4);
    align-items: center;
    justify-content: center;
    z-index: 10000;
  }
  .modal.show { display: flex; }
  .modal-content {
    background: var(--white);
    padding: 24px;
    border-radius: 6px;
    min-width: 360px;
    max-width: 600px;
    max-height: 80vh;
    overflow: auto;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2);
  }
  .modal-content h3 { font-weight: 500; margin-bottom: 14px; color: var(--red); }
  .modal-content label { display: block; margin: 8px 0 4px; font-size: 13px; color: #555; }
  .modal-content input, .modal-content select {
    width: 100%;
    padding: 7px 10px;
    border: 1px solid var(--border);
    border-radius: 3px;
    font-size: 13px;
    font-family: inherit;
  }
  .modal-actions { margin-top: 18px; display: flex; gap: 8px; justify-content: flex-end; }
  .form-row { display: flex; gap: 8px; }
  .form-row > * { flex: 1; }
  /* 人员新增/编辑弹窗：响应式 2 列网格，手机端单列 */
  .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; margin-bottom: 4px; }
  .form-grid .fg-item { display: flex; flex-direction: column; }
  .form-grid label { font-size: 13px; color: #555; margin-bottom: 4px; }
  .form-grid input, .form-grid select {
    width: 100%; padding: 7px 10px; border: 1px solid var(--border);
    border-radius: 3px; font-size: 13px; font-family: inherit; box-sizing: border-box;
  }
  .form-grid .fg-full { grid-column: 1 / -1; }

  .toast {
    position: fixed;
    bottom: 30px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(40,40,40,0.92);
    color: #fff;
    padding: 10px 20px;
    border-radius: 4px;
    font-size: 13px;
    z-index: 10001;
    opacity: 0;
    transition: opacity 0.25s;
    pointer-events: none;
  }
  .toast.show { opacity: 1; }

  /* ---- 修改密码弹窗 ---- */
  .cp-box {
    background: #fff;
    border-radius: 12px;
    padding: 32px 28px;
    width: 100%;
    max-width: 400px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.35);
    box-sizing: border-box;
  }
  .cp-box h3 {
    margin: 0 0 6px;
    text-align: center;
    font-size: 20px;
    font-weight: 500;
    color: #8b1a1a;
  }
  .cp-subtitle {
    text-align: center;
    font-size: 13px;
    color: #888;
    margin-bottom: 24px;
  }
  .cp-field { margin-bottom: 16px; }
  .cp-field label {
    display: block;
    font-size: 13px;
    color: #555;
    margin-bottom: 6px;
  }
  .cp-field input {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    box-sizing: border-box;
    transition: border-color 0.2s;
  }
  .cp-field input:focus {
    outline: none;
    border-color: #8b1a1a;
  }
  .cp-err {
    min-height: 20px;
    font-size: 12px;
    color: #c33;
    margin: 4px 0 12px;
  }
  .cp-actions { display: flex; gap: 12px; }
  .cp-actions button {
    flex: 1;
    padding: 11px 0;
    border: none;
    border-radius: 6px;
    font-size: 14px;
    cursor: pointer;
    transition: opacity 0.2s;
  }
  .cp-actions button:hover { opacity: 0.9; }
  .cp-btn-primary {
    background: #8b1a1a;
    color: #fff;
  }
  .cp-btn-secondary {
    background: #f0f0f0;
    color: #555;
  }

  /* ---- 登录页 ---- */
  .login-overlay {
    position: fixed; inset: 0;
    background: linear-gradient(135deg, #5c1a1a 0%, #8b1a1a 40%, #a32d2d 100%);
    display: flex; align-items: center; justify-content: center;
    z-index: 9999;
  }
  .login-overlay.hidden { display: none; }
  .login-box {
    background: #fff; border-radius: 12px; padding: 40px 36px;
    width: 380px; box-shadow: 0 16px 48px rgba(0,0,0,0.25);
  }
  .login-box h1 {
    text-align: center; font-size: 20px; font-weight: 500;
    color: #8b1a1a; margin-bottom: 6px; letter-spacing: 4px;
  }
  .login-box .subtitle {
    text-align: center; font-size: 13px; color: #888; margin-bottom: 28px;
  }
  .login-box label {
    display: block; font-size: 13px; color: #555; margin: 12px 0 4px;
  }
  .login-box input {
    width: 100%; padding: 10px 12px; border: 1px solid #c8c2b0;
    border-radius: 6px; font-size: 14px; font-family: inherit;
    transition: border-color 0.2s;
  }
  .login-box input:focus { outline: none; border-color: #8b1a1a; }
  .login-box .login-btn {
    width: 100%; padding: 11px; margin-top: 22px;
    background: #8b1a1a; color: #fff; border: none;
    border-radius: 6px; font-size: 15px; cursor: pointer;
    font-family: inherit; letter-spacing: 2px;
  }
  .login-box .login-btn:hover { background: #6f1414; }
  .login-box .login-err {
    color: #a32d2d; font-size: 12px; text-align: center;
    margin-top: 10px; min-height: 18px;
  }
  .login-box .login-hint {
    text-align: center; font-size: 11px; color: #aaa; margin-top: 16px;
  }

  /* ---- 顶部用户区 ---- */
  .nav-bar .user-area {
    margin-left: auto; display: flex; align-items: center; gap: 10px;
    font-size: 12px; color: rgba(255,255,255,0.85);
  }
  .nav-bar .user-area .role-badge {
    background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 10px;
    font-size: 11px;
  }
  .nav-bar .user-area button {
    background: none; border: 1px solid rgba(255,255,255,0.4); color: #fff;
    padding: 3px 10px; border-radius: 3px; cursor: pointer; font-size: 11px;
    font-family: inherit;
  }
  .nav-bar .user-area button:hover { background: rgba(255,255,255,0.15); }

  /* 登记表纸面表头 + 翻页器 */
  .reg-paper-head { text-align: center; margin: 0 0 8px; padding: 6px 0; border-bottom: 2px solid #333; }
  .reg-paper-head .reg-paper-hospital { font-size: 14px; font-weight: 600; color: #333; margin-bottom: 2px; }
  .reg-paper-head .reg-paper-title { font-size: 18px; font-weight: 700; color: #000; letter-spacing: 2px; }
  .reg-pager { display: flex; justify-content: center; align-items: center; gap: 8px; padding: 10px 0; font-size: 13px; color: var(--muted); }
  .reg-pager button { padding: 4px 12px; font-size: 13px; border: 1px solid var(--border); background: #fff; border-radius: 4px; cursor: pointer; }
  .reg-pager button:disabled { opacity: 0.4; cursor: not-allowed; }
  .reg-pager .reg-pager-info { min-width: 110px; text-align: center; }
  tr.pg-hidden { display: none; }

  @media print {
    .nav-bar, .filter-row, .reg-pager, .btn, .search-box, .col-act, .user-area { display: none !important; }
    body { background: #fff; padding: 0; }
    .page-content { padding: 0; }
    .card { box-shadow: none; border: none; padding: 0; margin: 0; }
    .reg-paper-head { margin: 0 0 6px; padding: 4px 0; border-bottom: 1px solid #000; }
    .reg-paper-head .reg-paper-hospital { font-size: 12px; }
    .reg-paper-head .reg-paper-title { font-size: 16px; }
    /* A4 横向铺满 */
    #carm-table, #infection-table { width: 100% !important; font-size: 10px !important; }
    #carm-table th, #carm-table td,
    #infection-table th, #infection-table td { padding: 2px 4px !important; }
    /* 让每 15 行自动翻页（用 page-break-after on every 15th row） */
    #carm-table tr.break-page,
    #infection-table tr.break-page { page-break-after: always; }
    /* 隐藏所有非打印页的容器 */
    #carm-table-wrap, #infection-table-wrap { overflow: visible !important; max-height: none !important; height: auto !important; }
    /* 打印时取消分页隐藏 — 所有行都要打出来 */
    tr.pg-hidden { display: table-row !important; }
    /* 登记表打印：使用专用打印表格 #ledger-print，避免主表的复杂样式冲突 */
    #ledger { display: none !important; }
    .ledger-scroll { display: none !important; }
    #ledger-print {
      display: table !important;
      width: 100% !important;
      table-layout: fixed !important;
      border-collapse: collapse !important;
      font-size: 11px !important;
      margin: 0 !important;
    }
    #ledger-print th, #ledger-print td {
      border: 1px solid #2c2c2a;
      text-align: center;
      padding: 2px 3px;
      font-size: 11px;
      height: 22px;
      overflow: hidden;
      box-sizing: border-box;
    }
    #ledger-print th {
      background: var(--accent-bg);
      font-weight: 500;
    }
    #ledger-print td.surgery-cell {
      text-align: left !important;
      word-break: normal !important;
      overflow-wrap: break-word !important;
      white-space: normal !important;
    }
    /* 区间打印时标题显示（内容为空则自动隐藏）；弹窗不打印 */
    #print-range-title:not(:empty) { display: block !important; font-size: 13px !important; margin: 0 0 6px !important; }
    .modal { display: none !important; }
    /* 打印时不显示 C 臂/感染表操作列 */
    #carm-table th:last-child, #carm-table td:last-child,
    #infection-table th:last-child, #infection-table td:last-child { display: none !important; }
  }

  /* ---- 手机端自适应 ---- */
  @media (max-width: 768px) {
    body { padding: 0; }
    .page-content { padding: 6px; }
    .nav-bar { padding: 0 6px; gap: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
    .nav-bar .logo { font-size: 13px; padding: 10px 8px 10px 0; letter-spacing: 0; }
    .nav-tab { padding: 10px 8px; font-size: 11px; }
    .card { padding: 8px; margin-bottom: 6px; }
    .card h2 { font-size: 13px; margin-bottom: 6px; }
    /* 统计卡片 */
    .stat-cards { gap: 4px; grid-template-columns: repeat(2, 1fr); }
    .stat-card { min-width: 0; padding: 8px 6px; }
    .stat-card .value { font-size: 18px; }
    .stat-card .label { font-size: 10px; }
    /* 图表 */
    .chart-row { grid-template-columns: 1fr; gap: 8px; }
    .chart-box { min-height: 160px; height: auto; padding: 10px; }
    .chart-box canvas { height: auto !important; max-height: none !important; }
    .chart-box h3 { font-size: 11px !important; margin-bottom: 4px !important; }
    /* 日统计表格 */
    table.stat-table { font-size: 12px; }
    table.stat-table th, table.stat-table td { padding: 5px 8px; }
    .stat-table-box h3 { font-size: 12px; padding: 8px 10px; }
    /* 可视化表格手机端 */
    .viz-table-box h3 { font-size: 12px; padding: 8px 10px; }
    .viz-table-box h3 .hint { font-size: 10px; }
    table.viz-table { font-size: 11px; }
    table.viz-table th, table.viz-table td { padding: 5px 6px; }
    table.viz-table td.name { font-size: 12px; }
    table.viz-table .bar-cell { min-width: 80px; }
    table.viz-table .bar-bg { height: 18px; line-height: 18px; font-size: 10px; padding: 0 5px; }
    table.viz-table .rank { width: 18px; height: 18px; line-height: 18px; font-size: 10px; }
    /* 登记表表格 */
    table.ledger { font-size: 11px; }
    table.ledger th, table.ledger td { height: 36px; padding: 4px 3px; }
    table.ledger td input, table.ledger td select { font-size: 11px; padding: 3px 4px; min-height: 24px; }
    table.ledger .row-chk { width: 24px; height: 24px; }
    .toolbar { gap: 6px; padding: 8px 10px; flex-wrap: wrap; }
    .toolbar button { padding: 8px 10px; font-size: 13px; min-height: 36px; }
    .toolbar .stats { font-size: 11px; padding: 0 4px; width: 100%; }
    /* 统计表格 */
    table.data-table { font-size: 10px; }
    table.data-table th, table.data-table td { padding: 4px 6px; }
    /* 筛选栏 */
    .filter-row { gap: 4px; flex-wrap: wrap; padding: 6px 8px; }
    .filter-row input, .filter-row select { font-size: 12px; padding: 5px 6px; }
    .filter-row button { font-size: 12px; padding: 6px 10px; }
    .header h1 { font-size: 16px; letter-spacing: 4px; }
    .header h2 { font-size: 20px; letter-spacing: 6px; }
    .header .meta { font-size: 11px; flex-wrap: wrap; gap: 4px; }
    .modal-content { min-width: 0; width: 95%; margin: 10px; padding: 14px; }
    .form-grid { grid-template-columns: 1fr; gap: 10px; }
    .login-box { width: 90%; padding: 24px 20px; }
    table.data-table th, table.data-table td { padding: 4px 6px; }
    .ocr-zone { padding: 20px; }
    /* 登记表卡片形式滚动 */
    .page-content { overflow-x: hidden; }
    /* C臂/感染登记的日期列在手机端加宽，确保完整显示 2026-07-29（仅作用于这两张表，不改动登记表） */
    #carm-table th:first-child, #carm-table td:first-child,
    #infection-table th:first-child, #infection-table td:first-child {
      width: 84px !important;
      min-width: 84px;
    }
    #carm-table td:first-child input, #infection-table td:first-child input {
      font-size: 11px;
      white-space: nowrap;
    }
    /* C臂/感染登记页手机端去掉卡片外框，让表格区域撑满、显示更多行 */
    #page-carm .card, #page-infection .card {
      padding: 0;
      border: none;
      background: transparent;
      margin-bottom: 6px;
    }
    #page-carm .filter-row, #page-infection .filter-row {
      padding: 8px;
      background: var(--white);
      border: 1px solid #d4cfc0;
      border-radius: 4px;
    }
    #page-carm #carm-table-wrap, #page-infection #infection-table-wrap {
      max-height: calc(100vh - 180px) !important;
    }
  }
</style>
</head>
<body>

<!-- 登录页 -->
<div class="login-overlay" id="login-overlay">
  <div class="login-box">
    <h1>手术统计管理系统</h1>
    <div class="subtitle">手术室</div>
    <label>用户名</label>
    <input type="text" id="login-user" placeholder="请输入用户名" autocomplete="off">
    <label>登录密码</label>
    <input type="password" id="login-pass" placeholder="请输入密码">
    <button class="login-btn" onclick="doLogin()">登 录</button>
    <div class="login-err" id="login-err"></div>
  </div>
</div>

<!-- 修改密码弹窗 -->
<div class="modal" id="cp-modal">
  <div class="modal-box cp-box">
    <h3 id="cp-title">🔒 修改密码</h3>
    <div class="cp-subtitle" id="cp-subtitle">首次登录需修改密码，请设置不少于 6 位的新密码</div>
    <div class="cp-field">
      <label for="cp-old">原密码</label>
      <input type="password" id="cp-old" placeholder="请输入原密码" autocomplete="off">
    </div>
    <div class="cp-field">
      <label for="cp-new">新密码</label>
      <input type="password" id="cp-new" placeholder="请设置新密码（至少 6 位）" autocomplete="off">
    </div>
    <div class="cp-field">
      <label for="cp-conf">确认新密码</label>
      <input type="password" id="cp-conf" placeholder="再次输入新密码" autocomplete="off">
    </div>
    <div class="cp-err" id="cp-err"></div>
    <div class="cp-actions">
      <button class="cp-btn-primary" onclick="submitChangePwd()">确定</button>
      <button class="cp-btn-secondary" id="cp-cancel" onclick="cancelChangePwd()">取消</button>
    </div>
  </div>
</div>

<!-- 顶部导航 -->
<nav class="nav-bar">
  <div class="logo">手术统计</div>
  <button class="nav-tab active" data-page="ledger">登记表</button>
  <button class="nav-tab" data-page="ocr">手术上传</button>
  <button class="nav-tab" data-page="daily">日统计</button>
  <button class="nav-tab" data-page="monthly">月统计</button>
  <button class="nav-tab" data-page="carm">C臂登记</button>
  <button class="nav-tab" data-page="infection">感染登记</button>
  <button class="nav-tab" data-page="perf">绩效分值</button>
  <button class="nav-tab" data-page="personnel" data-role="admin">管理</button>
  <button class="nav-tab" data-page="audit" data-role="admin">操作日志</button>
  <div class="user-area">
    <span id="sync-status" style="font-size:11px; opacity:0.8; cursor:pointer;" onclick="apiLoad().then(()=>{renderLedger();refreshDaily();refreshMonthly();refreshPerf();refreshCArm();refreshInfection();renderNurses();t('数据已刷新');})">连接中...</span>
    <span id="user-name">管理员</span>
    <span class="role-badge" id="user-role">管理员</span>
    <button onclick="changeOwnPwd()">修改密码</button>
    <button onclick="doLogout()">退出</button>
  </div>
</nav>

<!-- ================ 登记表页 ================ -->
<div class="page active" id="page-ledger">
<div class="page-content">
  <div class="card" style="padding:8px 12px; margin-bottom:8px;">
    <div class="filter-row">
      <span style="font-weight:500;">工具栏</span>
      <button class="btn primary" onclick="addRow()">+ 新增一行</button>
      <select id="add-row-count" style="padding:7px 8px;border:1px solid var(--border);border-radius:3px;font-size:13px;font-family:inherit;">
        <option value="1">新增1行</option>
        <option value="2">新增2行</option>
        <option value="3">新增3行</option>
        <option value="4">新增4行</option>
        <option value="5">新增5行</option>
      </select>
      <button class="btn" onclick="addRow(parseInt(document.getElementById('add-row-count').value))">执行</button>
      <button class="btn danger" onclick="deleteSelectedRows()">删除选中</button>
      <button class="btn" onclick="printLedger()">🖨 打印本日</button>
      <button class="btn" onclick="openPrintRangeModal()">🖨 区间打印</button>
      <button class="btn primary" onclick="exportFullCSV()">📤 导出表格</button>
      <span style="margin-left:auto; font-size:12px; color:var(--muted);">
        手术总数：<strong id="stat-total">0</strong>
      </span>
    </div>
  </div>

  <div class="header" style="margin-bottom:10px;">
    <h2>手 术 病 人 登 记</h2>
    <div class="meta">
      <span>登记日期：<input type="date" id="reg-date" onchange="safeStorage.set('surgery_active_date',this.value);renderLedger()"></span>
      <span style="font-size:12px; color:var(--muted);">科室：<input type="text" id="reg-dept" value="医专手术室" style="width:120px; border:none; border-bottom:1px dotted #aaa; text-align:center;"></span>
      <span>页码：第 <input type="text" id="reg-page" value="1" style="width:36px"> 页 / 共 <input type="text" id="reg-total-pages" value="1" style="width:36px"> 页</span>
    </div>
    <span id="score-hint" style="display:none; font-size:12px; color:#b8860b; margin-top:4px;"></span>
  </div>

  <div style="overflow-x:auto;-webkit-overflow-scrolling:touch;" class="ledger-scroll">
  <table class="ledger" id="ledger">
    <colgroup>
      <col class="col-no" style="width:60px"><col class="col-adm" style="width:64px"><col class="col-dept" style="width:76px">
      <col class="col-bed" style="width:52px"><col class="col-name" style="width:68px"><col class="col-sex" style="width:42px">
      <col class="col-age" style="width:42px"><col class="col-surgery" style="width:160px"><col class="col-circu" style="width:95px">
      <col class="col-scrub" style="width:95px"><col class="col-in" style="width:62px"><col class="col-out" style="width:62px">
      <col class="col-dur" style="width:62px"><col class="col-cscore" style="width:90px"><col class="col-sscore" style="width:90px">
      <col class="col-act" style="width:70px">
      <col class="col-carm" style="width:70px"><col class="col-inf" style="width:70px">
    </colgroup>
    <thead>
      <tr>
        <th style="text-align:center;">序号</th>
        <th>住院号</th>
        <th>科室</th>
        <th>床号</th>
        <th>姓名</th>
        <th>性别</th>
        <th>年龄</th>
        <th>手术名称</th>
        <th>巡回护士</th>
        <th>器械护士</th>
        <th>入室</th>
        <th>出室</th>
        <th>耗时</th>
        <th>巡回护士分值</th>
        <th>器械护士分值</th>
        <th style="text-align:center;">操作</th>
        <th style="text-align:center;">导入C臂</th>
        <th style="text-align:center;">导入感染</th>
      </tr>
    </thead>
    <tbody id="tbody"></tbody>
  </table>
  </div>
  <div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
    <button class="btn" style="background:#43a047;color:#fff;" onclick="reviewSelected()" id="btn-review-all">审核选中项</button>
    <button class="btn" style="background:#e53935;color:#fff;" onclick="unreviewSelected()" id="btn-unreview-all">取消审核选中项</button>
    <span style="font-size:11px;color:var(--muted);" id="review-count"></span>
  </div>
  <div style="margin-top:8px; font-size:11px; color:var(--muted);">
    ※ 入室/出室4位数字→时间
  </div>
  <!-- 打印专用表格：平时隐藏，打印时由 JS 填充数据并显示，独立列宽避免与主表冲突 -->
  <div id="print-range-title" style="display:none; font-size:13px; font-weight:500; margin:4px 0 6px; text-align:center;"></div>
  <table class="ledger" id="ledger-print" style="display:none;">
    <colgroup id="ledger-print-cg"></colgroup>
    <thead id="ledger-print-thead"></thead>
    <tbody id="ledger-print-body"></tbody>
  </table>
</div>
</div>

<!-- ================ 手术上传页（Excel / CSV 批量导入） ================ -->
<div class="page" id="page-ocr">
<div class="page-content">
  <div class="card">
    <h2>手术上传（Excel / CSV 批量导入）</h2>
    <div style="display:flex; flex-wrap:wrap; gap:16px;">
      <div style="flex:1; min-width:260px;">
        <div class="ocr-zone" onclick="document.getElementById('excel-input').click()">
          <div class="icon">📥</div>
          <div>点击上传 或 拖拽 Excel / CSV 文件</div>
          <div style="color:var(--muted); font-size:12px; margin-top:4px;">支持：.xlsx / .xls / .csv</div>
        </div>
        <input type="file" id="excel-input" accept=".xlsx,.xls,.csv" style="display:none" onchange="handleExcelUpload(event)">
        <div style="margin-top:12px; display:flex; gap:8px; align-items:center;">
          <span style="font-size:12px; color:var(--muted);">状态：</span>
          <span id="excel-status" style="font-size:12px;">等待上传</span>
          <button class="btn small" id="excel-retry-btn" style="display:none;" onclick="resetExcel()">🔄 重新上传</button>
        </div>
        <div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
          <label style="font-size:12px; color:var(--muted);">导入日期：</label>
          <input type="date" id="excel-sync-date" style="padding:4px 8px; border:1px solid var(--border); border-radius:4px; font-size:12px;">
        </div>
        <div style="margin-top:8px; font-size:11px; color:var(--muted);">
          上传后在此页把 Excel 列映射到系统字段，勾选要导入的行，点「导入选中行到登记表」。<br>
          ※ 需要提取哪些列？把字段名发给我，我帮你预设好映射。
        </div>
      </div>
      <div style="flex:2; min-width:280px;">
        <div id="excel-map" style="border:1px solid var(--border); border-radius:4px; padding:12px; display:none;">
          <div style="font-size:12px; font-weight:600; margin-bottom:8px;">字段映射（Excel 列 → 系统字段）</div>
          <div id="excel-map-list"></div>
        </div>
        <div id="excel-preview" style="border:1px solid var(--border); border-radius:4px; padding:12px; margin-top:10px;">
          <p style="color:var(--muted);">请先上传 Excel / CSV 文件</p>
        </div>
        <div style="margin-top:10px; text-align:right;">
          <button class="btn primary" id="excel-import-btn" style="display:none;" onclick="saveExcelResult()">导入选中行到登记表</button>
          <span id="excel-saved-ok" style="display:none; color:var(--green); font-size:13px;">&#10003; 已导入</span>
        </div>
      </div>
    </div>
  </div>
  <div class="card">
    <h2>手动快速录入</h2>
    <div style="display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px;">
    </div>
    <button class="btn primary" onclick="document.querySelector('[data-page=ledger]').click();addRow()">+ 快速新增一台手术</button>
  </div>
</div>
</div>

<!-- ================ 日统计页 ================ -->
<div class="page" id="page-daily">
<div class="page-content">
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:500;">日统计</span>
      <input type="date" id="daily-date" onchange="safeStorage.set('surgery_active_date',this.value);refreshDaily()">
      <button class="btn" onclick="refreshDaily()">刷新</button>
    </div>
  </div>

  <div class="stat-cards" id="daily-cards"></div>

  <div class="daily-stat-section" id="daily-chart-row">
    <div class="viz-table-box" id="daily-person-box"><h3>人员绩效统计 <span class="hint">分值与场次按巡回/器械双岗分离</span></h3><div id="daily-person-table"></div></div>
    <div class="viz-table-box" id="daily-dept-box"><h3>科室手术分布</h3><div id="daily-dept-table"></div></div>
    <div class="viz-table-box" id="daily-duration-box"><h3>人员单日总时长 <span class="hint">含全部录入人员（不含管理员）</span></h3><div id="daily-duration-table"></div></div>
  </div>
</div>
</div>

<!-- ================ 月统计页 ================ -->
<div class="page" id="page-monthly">
<div class="page-content">
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:500;">区间统计</span>
      <input type="date" id="monthly-start" onchange="refreshMonthly()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <span style="color:var(--muted);">至</span>
      <input type="date" id="monthly-end" onchange="refreshMonthly()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <button class="btn" onclick="setRange('monthly','month')">本月</button>
      <button class="btn" onclick="setRange('monthly','30d')">近30天</button>
      <select id="monthly-person" onchange="refreshMonthly()" style="min-width:120px;">
        <option value="">全部人员</option>
      </select>
      <button class="btn" onclick="refreshMonthly()">刷新</button>
      <button class="btn primary" onclick="exportMonthly()">📤 导出月报表</button>
    </div>
  </div>

  <div class="stat-cards" id="monthly-cards"></div>

  <div class="daily-stat-section">
    <div class="viz-table-box"><h3>人员绩效分值 <span class="hint">巡回/器械双岗分别核算</span></h3><div id="monthly-person-table"></div></div>
    <div class="viz-table-box"><h3>每日手术台数分布 <span class="hint">点击列头查看明细</span></h3><div id="monthly-daily-table"></div></div>
    <div class="viz-table-box"><h3>科室手术台数分布</h3><div id="monthly-dept-table"></div></div>
  </div>

  <div class="card" style="margin-top:16px;">
    <h2>人员月度明细 <span class="hint">双岗分值</span></h2>
    <div id="monthly-table-wrap">
      <table class="data-table" id="monthly-table"><tbody><tr><td style="padding:24px; color:var(--muted);">暂无数据</td></tr></tbody></table>
    </div>
  </div>
</div>
</div>

<!-- ================ C 臂登记页 ================ -->
<div class="page" id="page-carm">
<div class="page-content">
  <!-- 打印用表头（屏幕也显示） -->
  <div class="reg-paper-head" id="carm-paper-head">
    <div class="reg-paper-title">C型臂使用登记本</div>
  </div>
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:600;font-size:15px;">C型臂使用登记本</span>
      <input type="date" id="carm-start" onchange="refreshCArm()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <span style="color:var(--muted);">至</span>
      <input type="date" id="carm-end" onchange="refreshCArm()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <button class="btn" onclick="setRange('carm','month')">本月</button>
      <button class="btn" onclick="setRange('carm','30d')">近30天</button>
      <button class="btn" onclick="refreshCArm()">刷新</button>
      <button class="btn primary" onclick="window.print()">🖨 打印</button>
      <button class="btn primary" onclick="exportCArm()">📤 导出 CSV</button>
    </div>
  </div>
  <div class="card">
    <div id="carm-table-wrap" style="overflow:auto;max-height:calc(100vh - 300px);">
      <table class="ledger" id="carm-table">
        <thead>
          <tr style="background:var(--accent-bg);">
            <th style="width:84px;">日期</th>
            <th style="width:90px;">科室</th>
            <th style="width:45px;">床号</th>
            <th style="width:70px;">住院号</th>
            <th style="width:65px;">病人姓名</th>
            <th style="width:200px;">手术名称</th>
            <th style="width:75px;">运行情况</th>
            <th style="width:80px;">巡回护士</th>
            <th style="width:80px;">洗手护士</th>
            <th style="width:60px;" id="carm-act-col">操作</th>
          </tr>
        </thead>
        <tbody id="carm-tbody"></tbody>
      </table>
    </div>
    <!-- 翻页 -->
    <div id="carm-pager" class="reg-pager" style="display:none;"></div>
  </div>
</div>
</div>

<!-- ================ 感染登记页 ================ -->
<div class="page" id="page-infection">
<div class="page-content">
  <!-- 打印用表头（屏幕也显示） -->
  <div class="reg-paper-head" id="infection-paper-head">
    <div class="reg-paper-hospital">感染、急诊手术登记表</div>
    <div class="reg-paper-title" id="infection-paper-year"></div>
  </div>
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:600;font-size:15px;">感染、急诊手术登记表</span>
      <input type="date" id="inf-start" onchange="refreshInfection()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <span style="color:var(--muted);">至</span>
      <input type="date" id="inf-end" onchange="refreshInfection()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <button class="btn" onclick="setRange('infection','month')">本月</button>
      <button class="btn" onclick="setRange('infection','30d')">近30天</button>
      <button class="btn" onclick="refreshInfection()">刷新</button>
      <button class="btn primary" onclick="window.print()">🖨 打印</button>
      <button class="btn primary" onclick="exportInfection()">📤 导出 CSV</button>
    </div>
    <div style="font-size:12px;color:var(--muted);margin-top:6px;">说明：感染类型一栏填写甲肝、乙肝、丙肝、梅毒、艾滋、结核、多耐、急诊（无化验者）等</div>
  </div>
  <div class="card">
    <div id="infection-table-wrap" style="overflow:auto;max-height:calc(100vh - 340px);">
      <table class="ledger" id="infection-table">
        <thead>
          <tr style="background:var(--accent-bg);">
            <th rowspan="2" style="width:84px;vertical-align:middle;">日期</th>
            <th rowspan="2" style="width:90px;vertical-align:middle;">科室</th>
            <th rowspan="2" style="width:55px;vertical-align:middle;">床号</th>
            <th rowspan="2" style="width:70px;vertical-align:middle;">姓名</th>
            <th rowspan="2" style="width:80px;vertical-align:middle;">住院号</th>
            <th rowspan="2" style="width:90px;vertical-align:middle;">感染类型</th>
            <th rowspan="2" style="width:60px;vertical-align:middle;">手术间号</th>
            <th rowspan="2" style="width:140px;vertical-align:middle;">手术名称</th>
            <th rowspan="2" style="width:60px;vertical-align:middle;">人员防护</th>
            <th colspan="2" style="vertical-align:middle;">术后处置</th>
            <th rowspan="2" style="width:90px;vertical-align:middle;">签名</th>
            <th rowspan="2" style="width:60px;vertical-align:middle;" id="infection-act-col">操作</th>
          </tr>
          <tr style="background:var(--accent-bg);">
            <th style="width:70px;">手术间空置消毒时间（30分钟）</th>
            <th style="width:70px;">物体表面及环境消毒液擦拭消毒</th>
          </tr>
        </thead>
        <tbody id="infection-tbody"></tbody>
      </table>
    </div>
    <!-- 翻页 -->
    <div id="infection-pager" class="reg-pager" style="display:none;"></div>
  </div>
</div>
</div>

<!-- ================ 绩效页 ================ -->
<div class="page" id="page-perf">
<div class="page-content">
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:500;">绩效分值汇总</span>
      <input type="date" id="perf-start" onchange="refreshPerf()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <span style="color:var(--muted);">至</span>
      <input type="date" id="perf-end" onchange="refreshPerf()" style="padding:5px;border:1px solid var(--border);border-radius:3px;font-size:13px;">
      <button class="btn" onclick="setRange('perf','month')">本月</button>
      <button class="btn" onclick="setRange('perf','30d')">近30天</button>
      <button class="btn" onclick="refreshPerf()">刷新</button>
      <button class="btn primary" onclick="exportPerf()">📤 导出绩效表</button>
    </div>
  </div>

  <div class="stat-cards" id="perf-cards"></div>

  <div class="daily-stat-section">
    <div class="viz-table-box"><h3>绩效分值排名 <span class="hint">含全部录入人员（不含管理员）；C臂每次5分，感染每次1分，仅统计已保存记录</span></h3><div id="perf-rank-table"></div></div>
  </div>

  <div class="card" style="margin-top:16px;">
    <h2>绩效明细 <span class="hint">双岗分值</span></h2>
    <div id="perf-table-wrap">
      <table class="data-table" id="perf-table">
        <thead>
          <tr><th>排名</th><th>姓名</th><th>巡回分值</th><th>器械分值</th><th>C臂分值</th><th>感染分值</th><th>台数</th><th>累计时长</th><th>总分</th></tr>
        </thead>
        <tbody id="perf-tbody"><tr><td colspan="9" style="padding:24px; color:var(--muted);">暂无数据</td></tr></tbody>
      </table>
    </div>
  </div>
</div>
</div>

<!-- ================ 操作日志页 ================ -->
<div class="page" id="page-audit">
  <div class="page-content">
    <div class="card">
      <div class="filter-row">
        <span style="font-weight:500;">操作日志</span>
        <input class="search-box" placeholder="搜索工号/姓名/操作..." id="audit-search" oninput="renderAuditTable()">
        <input type="date" id="audit-date" onchange="loadAuditLog()">
        <button class="btn" onclick="document.getElementById('audit-date').value='';loadAuditLog()">全部日期</button>
        <button class="btn" onclick="exportAuditCSV()">📤 导出CSV</button>
      </div>
      <div id="audit-table" style="margin-top:12px;overflow:auto;max-height:70vh;"></div>
    </div>
  </div>
</div>

<!-- ================ 管理页 ================ -->
<div class="page" id="page-personnel">
<div class="page-content">
  <div class="card">
    <div class="filter-row">
      <span style="font-weight:500;">人员基础库</span>
      <input class="search-box" placeholder="搜索姓名/工号..." id="nurse-search" oninput="renderNurses()">
      <button class="btn primary" onclick="toggleNurseForm()">+ 新增人员</button>
      <button class="btn" onclick="importNursesExcel()">📥 批量导入</button>
      <button class="btn" onclick="exportNursesExcel()">📤 导出名单</button>
      <button class="btn" onclick="rebuildAccounts()" title="为所有人员基础库里的护士按工号重建登录账号（默认密码123456，已存在的账号不覆盖）">🔑 重建登录账号</button>
    </div>

    <!-- 新增/编辑人员弹窗 -->
    <div class="modal" id="nurse-modal" onclick="if(event.target===this)toggleNurseForm()">
      <div class="modal-content" style="max-width:460px;">
        <h3 id="nurse-modal-title">新增人员</h3>
        <div class="form-grid">
          <div class="fg-item"><label>姓名 *</label><input id="nf-name" type="text" placeholder="必填"></div>
          <div class="fg-item"><label>工号</label><input id="nf-no" type="text" placeholder="留空默认未设"></div>
          <div class="fg-item"><label>系统角色</label><select id="nf-sysrole"><option value="nurse">护士</option><option value="admin">管理员</option></select></div>
          <div class="fg-item"><label>职称</label><input id="nf-title" type="text" placeholder="如：主管护师"></div>
          <div class="fg-item"><label>登录密码</label><input id="nf-pass" type="text" placeholder="留空不设"></div>
          <div class="fg-item fg-full">
            <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;color:#555;">
              <input id="nf-score" type="checkbox" style="width:auto;margin:0;"> 允许该护士在登记表中编辑分值
            </label>
          </div>
        </div>
        <div class="modal-actions">
          <button class="btn" onclick="toggleNurseForm()">取消</button>
          <button class="btn primary" onclick="saveNurse()">保存</button>
        </div>
      </div>
    </div>

    <!-- 批量录入弹窗 -->
    <div class="modal" id="batch-modal" onclick="if(event.target===this)this.classList.remove('show')">
      <div class="modal-content" style="max-width:480px;">
        <h3>批量录入人员</h3>
        <p style="font-size:12px;color:var(--muted);margin:0 0 8px;line-height:1.5;">每行填写「工号 姓名」（空格分隔），系统自动按姓名 / 工号去重，仅新增不存在的人员。已存在的同名 / 同号人员不会重复添加。</p>
        <textarea id="batch-text" rows="12" style="width:100%;box-sizing:border-box;padding:8px;border:1px solid var(--border);border-radius:4px;font-size:13px;font-family:monospace;line-height:1.7;resize:vertical;"></textarea>
        <div class="modal-actions">
          <button class="btn" onclick="document.getElementById('batch-modal').classList.remove('show')">取消</button>
          <button class="btn primary" onclick="doBatchImport()">录入</button>
        </div>
      </div>
    </div>

    <table class="data-table" id="nurse-table">
      <thead>
        <tr><th>序号</th><th>姓名</th><th>工号</th><th>系统角色</th><th>可编辑分值</th><th>登录账号</th><th>操作</th></tr>
      </thead>
      <tbody id="nurse-tbody"></tbody>
    </table>
  </div>

  <div class="card">
    <h2>科室管理</h2>
    <div style="display:flex; gap:8px; flex-wrap:wrap;">
      <button class="btn primary" onclick="showDeptEditor()">📋 编辑科室列表</button>
      <span style="font-size:12px; color:var(--muted); align-self:center;">增删科室选项，登记表和OCR中的科室下拉同步更新</span>
    </div>
  </div>

  <div class="card">
    <h2>数据管理</h2>
    <div style="display:flex; gap:8px; flex-wrap:wrap;">
      <button class="btn" onclick="exportJSON()">📤 导出 JSON 备份</button>
      <button class="btn" onclick="document.getElementById('import-json').click()">📥 导入 JSON 备份</button>
      <input type="file" id="import-json" accept=".json" style="display:none" onchange="importJSON(event)">
      <button class="btn" onclick="exportCSV()">📤 导出全部数据 CSV</button>
      <button class="btn primary" onclick="importStaff()">📥 导入29人名单</button>
    </div>
  </div>
</div>
</div>


<!-- Toast -->
<!-- 科室编辑弹窗 -->
<div class="modal" id="dept-modal"><div class="modal-content"></div></div>

<div class="toast" id="toast"></div>

<script>
/* ===============================================================
   手术统计管理系统 · 完整版 Web App
   数据表：rows(手术记录) + nurses(人员库) + settings(系统设置)
   =============================================================== */

const K_ROWS = 'cz_ledger_v2';
const K_NURSE = 'cz_nurse_v2';
const K_SETTINGS = 'cz_settings_v2';
const K_ACCOUNTS = 'cz_accounts_v2';
const K_DEPTS = 'cz_depts_v2';
const K_CARM = 'cz_carm_v1';
const K_INFECTION = 'cz_infection_v1';

const DEFAULT_DEPTS = ['乳腺外科一区','关节(运医)骨科三区','创伤骨科二区','妇科C','手足外科一区','泌尿外科(医专)','甲状腺头颈外科一区','甲状腺头颈外科二区','眼科(医专)','神经外科','肝胆胰(微创)外科','胃肠外科','胸外科一区','脊柱骨科三区'];

let rows = [];
let nurses = [];
let cArmRows = [];
let infectionRows = [];
let settings = {
  overtime: '16:00',
  defaultBaseScore: 10
};

/* ===========================================================
   云端同步层 — API 调用 + localStorage 本地缓存
   =========================================================== */
const API = ''; // 同源，空字符串即可
let _syncTimer = null;
let _online = true;
let _serverUpdatedAt = {};

// ---- 认证：Bearer Token 持久化（localStorage），不主动退出、不过期 ----
// Edge 等浏览器的"跟踪防护"可能禁用 localStorage；用安全包装避免抛异常。
const safeStorage = {
  get(k) { try { return localStorage.getItem(k); } catch (e) { return null; } },
  set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
  remove(k) { try { localStorage.removeItem(k); } catch (e) {} }
};
let authToken = safeStorage.get('surgery_token') || '';
function setAuthToken(t) {
  authToken = t || '';
  if (t) safeStorage.set('surgery_token', t); else safeStorage.remove('surgery_token');
}
function authHeader() { return authToken ? { 'Authorization': 'Bearer ' + authToken } : {}; }
// 统一封装带鉴权的 API 请求
async function apiFetch(url, opts = {}) {
  opts.headers = Object.assign({ 'Content-Type': 'application/json' }, authHeader(), opts.headers || {});
  return fetch(API + url, opts);
}
// 取当前登录用户信息（恢复会话 / 判断是否强制改密）
async function fetchMe() {
  try {
    const res = await apiFetch('/api/me');
    if (!res.ok) return null;
    const j = await res.json();
    return j.user;
  } catch (e) { return null; }
}

async function apiLoad() {
  try {
    const res = await apiFetch('/api/load');
    if (!res.ok) throw new Error('load failed');
    const d = await res.json();
    if (d.rows) { rows = d.rows; ensureRowIds(); safeStorage.set(K_ROWS, JSON.stringify(rows)); }
    if (d.nurses) { nurses = filterHiddenNurses(d.nurses); safeStorage.set(K_NURSE, JSON.stringify(nurses)); }
    if (d.settings) { settings = d.settings; safeStorage.set(K_SETTINGS, JSON.stringify(settings)); }
    if (d.cArmRows) { cArmRows = d.cArmRows; safeStorage.set(K_CARM, JSON.stringify(cArmRows)); }
    if (d.infectionRows) { infectionRows = d.infectionRows; safeStorage.set(K_INFECTION, JSON.stringify(infectionRows)); }
    // 账号缓存：服务器返回已剥离密码，直接作为本地账号缓存（不再存明文密码）
    if (d.accounts) {
      safeStorage.set(K_ACCOUNTS, JSON.stringify(d.accounts));
    }
    if (d.depts) { safeStorage.set(K_DEPTS, JSON.stringify(d.depts)); }
    _serverUpdatedAt = d.updatedAt || {};
    _online = true;
    updateSyncStatus();
    return true;
  } catch (e) {
    console.warn('云端加载失败，使用本地缓存:', e.message);
    _online = false;
    updateSyncStatus();
    return false;
  }
}

function syncToServer(immediate) {
  if (!_online) return Promise.resolve(false);
  clearTimeout(_syncTimer);
  const doSync = async () => {
    try {
      const res = await apiFetch('/api/save', {
        method: 'POST',
        body: JSON.stringify({ rows, nurses, settings, depts: getDeptsRaw(), cArmRows, infectionRows })
      });
      if (res.ok) { _online = true; updateSyncStatus('已同步'); return true; }
      return false;
    } catch (e) {
      _online = false; updateSyncStatus('同步失败');
      // 失败后 5 秒重试
      setTimeout(() => syncToServer(true), 5000);
      return false;
    }
  };
  if (immediate) return doSync();
  _syncTimer = setTimeout(doSync, 800);
  return Promise.resolve(true);
}

// 页面卸载 / 刷新时兜底发送最新数据，防止「刚保存就刷新」导致服务器未及时收到而数据丢失
function flushOnUnload() {
  if (!currentUser) return;
  const payload = JSON.stringify({ rows, nurses, settings, depts: getDeptsRaw(), cArmRows, infectionRows });
  try {
    // 优先用 keepalive 的 fetch：页面卸载后仍会发送完成，不受刷新中断影响（支持大负载）
    if (typeof Request !== 'undefined' && 'keepalive' in Request.prototype && typeof fetch === 'function') {
      fetch(API + '/api/save', { method: 'POST', headers: Object.assign({ 'Content-Type': 'application/json' }, authHeader()), body: payload, keepalive: true }).catch(() => {});
      return;
    }
  } catch (e) {}
  try {
    // 旧浏览器兜底：sendBeacon 在卸载时也能送达（但有 ~64KB 负载限制）
    if (navigator.sendBeacon) navigator.sendBeacon(API + '/api/save', new Blob([payload], { type: 'application/json' }));
  } catch (e) {}
}
window.addEventListener('pagehide', flushOnUnload);
window.addEventListener('beforeunload', flushOnUnload);

function updateSyncStatus(state) {
  const el = document.getElementById('sync-status');
  if (!el) return;
  if (!_online) { el.textContent = '离线模式'; el.style.color = '#a32d2d'; return; }
  el.textContent = state || '已连接'; el.style.color = state === '同步失败' ? '#a32d2d' : '#1d6e3c';
}

// 定时拉取最新数据（每 20 秒）
let _lastLocalChangeAt = 0;  // 本地最近一次改动时间（编辑/保存/删除/导入），5 秒内轮询跳过数据更新避免覆盖未同步的本地编辑
setInterval(async () => {
  if (!currentUser) return;
  // 本地刚改过 → 跳过本轮，避免 800ms 同步窗口内的本地数据被服务器旧版本覆盖
  if (Date.now() - _lastLocalChangeAt < 5000) return;
  try {
    const res = await apiFetch('/api/load');
    if (!res.ok) return;
    const d = await res.json();
    const sUA = d.updatedAt || {};
    // 检查 rows 是否有更新
    if (sUA.rows && sUA.rows !== _serverUpdatedAt.rows) {
      rows = d.rows; ensureRowIds(); safeStorage.set(K_ROWS, JSON.stringify(rows));
      _serverUpdatedAt.rows = sUA.rows;
      const activePage = document.querySelector('.nav-tab.active')?.dataset.page;
      // 若当前在登记表且用户正编辑输入框 → 跳过重绘避免打断输入
      const isEditing = activePage === 'ledger' && document.activeElement && document.activeElement.closest('#ledger');
      if (activePage === 'ledger' && !isEditing) renderLedger();
      if (activePage === 'daily') refreshDaily();
      if (activePage === 'monthly') refreshMonthly();
      if (activePage === 'perf') refreshPerf();
      if (!isEditing) t('数据已刷新');
    }
    if (sUA.nurses && sUA.nurses !== _serverUpdatedAt.nurses) {
      nurses = d.nurses; safeStorage.set(K_NURSE, JSON.stringify(nurses));
      _serverUpdatedAt.nurses = sUA.nurses;
    }
    if (sUA.cArmRows && sUA.cArmRows !== _serverUpdatedAt.cArmRows) {
      cArmRows = d.cArmRows || []; safeStorage.set(K_CARM, JSON.stringify(cArmRows));
      _serverUpdatedAt.cArmRows = sUA.cArmRows;
      const ap = document.querySelector('.nav-tab.active')?.dataset.page;
      if (ap === 'carm') refreshCArm();
    }
    if (sUA.infectionRows && sUA.infectionRows !== _serverUpdatedAt.infectionRows) {
      infectionRows = d.infectionRows || []; safeStorage.set(K_INFECTION, JSON.stringify(infectionRows));
      _serverUpdatedAt.infectionRows = sUA.infectionRows;
      const ap = document.querySelector('.nav-tab.active')?.dataset.page;
      if (ap === 'infection') refreshInfection();
    }
  } catch {}
}, 20000);

/* ---- 登录 & 权限 ---- */
let currentUser = null; // { name, role }

const ROLES = {
  admin: { label: '管理员', pages: ['ledger','ocr','daily','monthly','perf','carm','infection','personnel','audit'], canEdit: true, canDelete: true, canManage: true },
  nurse:  { label: '护士',   pages: ['ledger','daily','monthly','perf','carm','infection'], canEdit: true, canDelete: false, canManage: false },
  viewer: { label: '查看者', pages: ['daily','monthly','perf','carm','infection'], canEdit: false, canDelete: false, canManage: false }
};

function loadAccounts() {
  try {
    const raw = safeStorage.get(K_ACCOUNTS);
    return raw ? JSON.parse(raw) : {};
  } catch { return {}; }
}
function saveAccounts(acc) {
  // 本地不存明文密码
  const localSafe = {};
  for (const k in acc) { const a = acc[k] || {}; localSafe[k] = { role: a.role, name: a.name, mustChangePwd: a.mustChangePwd }; }
  safeStorage.set(K_ACCOUNTS, JSON.stringify(localSafe));
  // 同步到服务器（服务器会哈希明文密码）
  apiFetch('/api/save', {
    method: 'POST',
    body: JSON.stringify({ accounts: acc })
  }).catch(() => {});
}

function initAccounts() {
  let acc = loadAccounts();
  if (!acc.admin) {
    acc.admin = { password: 'admin123', role: 'admin', name: '管理员' };
    saveAccounts(acc);
  }
  return acc;
}

// 页面加载时自动为人员名单里有工号的护士补建登录账号（工号 + 默认密码123456）。
// 已有的账号绝不覆盖；仅在确实存在缺失时才写库并同步服务器。
// 这样无需手动点「重建」也能登录，且刷新不会再被清空（账号已持久化到本地与服务器）。
function ensureNurseAccounts() {
  const acc = loadAccounts();
  let changed = false;
  nurses.forEach(n => {
    if (n.systemRole === 'admin') return; // 管理员账号由 initAccounts 保证
    if (n.empNo && n.empNo !== '未设' && !acc[n.empNo]) {
      acc[n.empNo] = { password: '123456', role: 'nurse', name: n.name };
      changed = true;
    }
  });
  if (changed) saveAccounts(acc);
}
// 清理残留登录账号（双向）：某人当前角色的正确登录key为
//   - 护士 → 工号；若其名下仍存在以「姓名」为key的旧账号（曾设管理员遗留）→ 删除
//   - 管理员 → 姓名；若其名下仍存在以「工号」为key的旧账号（曾为护士遗留）→ 删除
// 否则用旧登录名登录会命中残留账号、仍按旧角色进入（如护士升管理员后仍以工号登上护士权限）。
function cleanupAccounts() {
  try {
    const acc = loadAccounts();
    let changed = false;
    nurses.forEach(n => {
      const targetKey = (n.systemRole === 'admin') ? (n.name || '') : (n.empNo && n.empNo !== '未设' ? n.empNo : '');
      const keys = [n.name, (n.empNo && n.empNo !== '未设' ? n.empNo : '')].filter(k => k);
      keys.forEach(k => {
        if (k !== targetKey && acc[k] && acc[k].name === n.name) { delete acc[k]; changed = true; }
      });
    });
    if (changed) saveAccounts(acc);
  } catch(e) {}
}

async function doLogin() {
  const user = document.getElementById('login-user').value.trim();
  const pass = document.getElementById('login-pass').value.trim();
  const err = document.getElementById('login-err');
  if (!user || !pass) { err.textContent = '请输入用户名和密码'; return; }
  err.textContent = '登录中...';
  try {
    const res = await fetch(API + '/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username: user, password: pass })
    });
    if (!res.ok) {
      let msg = '用户名或密码错误';
      try { const j = await res.json(); if (j.error) msg = j.error; } catch (e) {}
      err.textContent = msg; return;
    }
    const j = await res.json();
    setAuthToken(j.token);
    currentUser = { name: j.user.name, role: j.user.role, loginKey: j.user.loginKey, mustChangePwd: j.user.mustChangePwd };
    try { safeStorage.set('surgery_currentUser', JSON.stringify(currentUser)); } catch (e) {}
    err.textContent = '';
    await bootAppData();
    if (currentUser.mustChangePwd) {
      showChangePwd(true); // 强制改密，暂不入系统
    } else {
      enterApp();
    }
  } catch (e) {
    err.textContent = '登录失败，请检查网络';
  }
}

function doLogout() {
  try { apiFetch('/api/logout', { method: 'POST' }).catch(() => {}); } catch (e) {}
  currentUser = null;
  setAuthToken('');
  try { safeStorage.remove('surgery_currentUser'); safeStorage.remove('surgery_currentPage'); } catch (e) {}
  document.getElementById('login-overlay').classList.remove('hidden');
  document.getElementById('login-user').value = '';
  document.getElementById('login-pass').value = '';
  document.getElementById('login-err').textContent = '';
}

// 登录成功后从服务器加载数据（含兜底演示数据）
async function bootAppData() {
  const ok = await apiLoad();
  if (!ok || !rows.length) { if (!rows.length) createDemoData(); }
  if (!nurses.length) loadDefaultNurses();
}

// 进入系统主界面
function enterApp() {
  document.getElementById('login-overlay').classList.add('hidden');
  applyPermissions(true);
  try {
    renderLedger(); refreshDaily(); refreshMonthly(); refreshPerf(); refreshCArm(); refreshInfection(); renderNurses();
  } catch (e) {}
  t('欢迎，' + (currentUser ? currentUser.name : ''));
}

// 初始化收尾：角色重算 + 月份下拉 + 恢复上次页面
function finalizeInit() {
  try {
    if (currentUser && nurses.length) {
      const me = nurses.find(n => n.name === currentUser.name
        || (currentUser.loginKey && n.empNo && n.empNo === currentUser.loginKey));
      if (me && me.systemRole) {
        if (currentUser.role !== me.systemRole) {
          currentUser.role = me.systemRole;
          try { safeStorage.set('surgery_currentUser', JSON.stringify(currentUser)); } catch (e) {}
        }
        applyPermissions(false);
      }
    }
  } catch (e) {}
  initMonthSelects();
  try {
    const savedPage = safeStorage.get('surgery_currentPage') || 'ledger';
    const target = document.querySelector('.nav-tab[data-page="' + savedPage + '"]');
    if (target && target.style.display !== 'none') target.click();
  } catch (e) {}
}

// ---- 修改密码弹窗 ----
let _cpForced = false;
function showChangePwd(forced) {
  _cpForced = !!forced;
  document.getElementById('cp-err').textContent = '';
  document.getElementById('cp-old').value = '';
  document.getElementById('cp-new').value = '';
  document.getElementById('cp-conf').value = '';
  document.getElementById('cp-title').textContent = forced ? '🔒 首次登录，请修改密码' : '🔒 修改密码';
  document.getElementById('cp-subtitle').textContent = forced
    ? '为了账号安全，首次登录需要设置新密码（不少于 6 位）'
    : '请设置新密码（不少于 6 位）';
  document.getElementById('cp-cancel').style.display = forced ? 'none' : '';
  document.getElementById('cp-modal').classList.add('show');
  try { document.getElementById('cp-old').focus(); } catch (e) {}
}
function cancelChangePwd() {
  document.getElementById('cp-modal').classList.remove('show');
  if (_cpForced) doLogout();
}
async function submitChangePwd() {
  const oldP = document.getElementById('cp-old').value;
  const newP = document.getElementById('cp-new').value;
  const conf = document.getElementById('cp-conf').value;
  const err = document.getElementById('cp-err');
  if (!newP || newP.length < 6) { err.textContent = '新密码至少 6 位'; return; }
  if (newP !== conf) { err.textContent = '两次输入的新密码不一致'; return; }
  try {
    const res = await apiFetch('/api/change-password', {
      method: 'POST',
      body: JSON.stringify({ oldPassword: oldP, newPassword: newP })
    });
    if (res.ok) {
      currentUser.mustChangePwd = false;
      try { safeStorage.set('surgery_currentUser', JSON.stringify(currentUser)); } catch (e) {}
      document.getElementById('cp-modal').classList.remove('show');
      if (_cpForced) { _cpForced = false; enterApp(); }
      else t('密码已修改');
    } else {
      let m = '修改失败';
      try { const j = await res.json(); if (j.error) m = j.error; } catch (e) {}
      err.textContent = m;
    }
  } catch (e) { err.textContent = '网络错误，请重试'; }
}

function applyPermissions(fromLogin) {
  if (!currentUser) return;
  const cfg = ROLES[currentUser.role] || ROLES.nurse;
  document.getElementById('user-name').textContent = currentUser.name;
  document.getElementById('user-role').textContent = cfg.label;

  document.querySelectorAll('.nav-tab').forEach(btn => {
    const page = btn.dataset.page;
    if (!page) return;
    if (cfg.pages.includes(page)) btn.style.display = '';
    else btn.style.display = 'none';
  });

  // 仅在登录时（非刷新恢复）且没有激活tab时，才激活第一个可见tab
  if (fromLogin && !document.querySelector('.nav-tab.active:not([style*="display:none"])')) {
    const firstTab = document.querySelector('.nav-tab:not([style*="display:none"])');
    if (firstTab) firstTab.click();
  }
}

function canEdit() {
  if (!currentUser) return false;
  return (ROLES[currentUser.role] || {}).canEdit !== false;
}
function canDelete() {
  if (!currentUser) return false;
  return (ROLES[currentUser.role] || {}).canDelete === true;
}
function canManage() {
  if (!currentUser) return false;
  return (ROLES[currentUser.role] || {}).canManage === true;
}
// 是否拥有「编辑分值」权限：管理员默认可编辑；护士需在其人员设置中勾选「允许编辑分值」
function canEditScore() {
  if (!currentUser) return false;
  if (currentUser.role === 'admin') return true;
  const nurse = nurses.find(n => n.name === currentUser.name)
    || (currentUser.loginKey ? nurses.find(n => n.empNo === currentUser.loginKey) : null);
  return !!(nurse && nurse.canEditScore);
}
// 行级编辑权限：未保存的行任何可编辑账号均可填写；一旦本行被「保存」锁定，
// 仅「当初保存该行的本人账号」与「管理员」可再次编辑，其余账号只读，防止误改他人已登记数据。
function canEditRow(r) {
  if (!currentUser) return false;
  if (currentUser.role === 'admin') return true;       // 管理员恒可编辑
  const savedBy = (r && r.savedBy) ? r.savedBy : '';
  if (!savedBy) return true;                            // 尚未保存 → 可编辑
  if (savedBy === currentUser.loginKey || savedBy === currentUser.name) return true; // 保存者本人
  const me = currentUser.name || currentUser.loginKey;
  if (r.circu === me) return true;                      // 本行巡回护士
  if (r.circuHoName === me) return true;                // 本行交接巡回护士
  return false;
}


/* ---- 科室管理 ---- */
function getDeptsRaw() {
  try {
    const raw = safeStorage.get(K_DEPTS);
    const arr = raw ? JSON.parse(raw) : null;
    return (arr && arr.length) ? arr : [...DEFAULT_DEPTS];
  } catch { return [...DEFAULT_DEPTS]; }
}
function getDepts() { return getDeptsRaw(); }
// 科室归一化：取「科室/床号」斜杠前部分作为分组依据，下辖所有床号累加合并
function normDept(d) {
  if (!d) return '其他';
  const s = String(d).trim();
  if (!s) return '其他';
  const idx = s.indexOf('/');
  return idx > 0 ? s.slice(0, idx).trim() : s;
}
function saveDepts(arr) {
  safeStorage.set(K_DEPTS, JSON.stringify(arr));
  syncToServer();
}
function refreshDeptCol() {
  const idx = COLS.findIndex(c => c.k === 'dept');
  if (idx >= 0) COLS[idx].op = getDepts();
}
function showDeptEditor() {
  if (!canManage()) { t('仅管理员可编辑科室'); return; }
  const depts = getDepts();
  let html = '<h3>科室管理</h3><div style="max-height:300px;overflow:auto;margin:12px 0;">';
  depts.forEach((d,i) => {
    html += `<div style="display:flex;align-items:center;gap:8px;margin:4px 0;padding:4px 8px;border:1px solid var(--border);border-radius:3px;">` +
      `<input value="${d}" onchange="editDeptName(${i},this.value)" style="flex:1;padding:5px;border:1px solid var(--border);border-radius:3px;">` +
      `<button class="btn small danger" onclick="removeDept(${i})">删除</button></div>`;
  });
  html += '</div><div class="form-row" style="margin-top:8px;">' +
    '<input id="new-dept-name" placeholder="新科室名称" style="flex:1;padding:7px;border:1px solid var(--border);border-radius:3px;">' +
    '<button class="btn primary" onclick="addDept()">添加</button></div>' +
    '<div class="modal-actions"><button class="btn" onclick="closeDeptEditor()">完成</button></div>';
  const modal = document.getElementById('dept-modal');
  modal.querySelector('.modal-content').innerHTML = html;
  modal.classList.add('show');
}
function closeDeptEditor() { document.getElementById('dept-modal').classList.remove('show'); renderLedger(); }
function addDept() {
  const inp = document.getElementById('new-dept-name');
  const name = inp.value.trim();
  if (!name) return;
  const depts = getDepts();
  if (depts.includes(name)) { t('科室已存在'); return; }
  depts.push(name);
  saveDepts(depts); refreshDeptCol(); renderLedger();
  showDeptEditor(); t('已添加');
}
function removeDept(i) {
  const depts = getDepts();
  if (depts.length <= 2) { t('至少保留2个科室'); return; }
  depts.splice(i,1);
  saveDepts(depts); refreshDeptCol(); renderLedger();
  showDeptEditor(); t('已删除');
}
function editDeptName(i, val) {
  const depts = getDepts();
  depts[i] = val.trim() || depts[i];
  saveDepts(depts); refreshDeptCol(); renderLedger();
}

/* ---- 字段定义 ---- */
const COLS = [
  {k:'date',l:'日期',t:'date',w:100},
  {k:'admNo',l:'住院号',t:'text',w:90,ph:'住院号'},
  {k:'dept',l:'科室',t:'dept',w:80},
  {k:'bedNo',l:'床号',t:'text',w:54},
  {k:'name',l:'姓名',t:'text',w:72},
  {k:'sex',l:'性别',t:'select',w:44,op:['男','女'],nb:true},
  {k:'age',l:'年龄',t:'number',w:44},
  {k:'surgery',l:'手术名称',t:'text',w:280,ph:'手术名称'},
  {k:'circu',l:'巡回护士',t:'nurse',w:72},
  {k:'scrub',l:'器械护士',t:'nurse',w:72},
  {k:'inTime',l:'入室',t:'time',w:66},
  {k:'outTime',l:'出室',t:'time',w:66},
  {k:'duration',l:'耗时',t:'calc',w:80},
  {k:'circuScore',l:'巡回护士分值',t:'number',w:100,min:0,step:1,ph:'分值'},
  {k:'scrubScore',l:'器械护士分值',t:'number',w:100,min:0,step:1,ph:'分值'},
];
// 给列加序号
const allCols = COLS.slice(0, 15); // 仅登记表使用的列（date~器械护士分值）

/* ---- 持久化（本地缓存 + 云端同步） ---- */

/** 排除名单：不再出现在下拉/人员库中（删除历史数据中已不用的护士条目）
 *  集中过滤可避免历史数据中残留的旧人名污染新登记 */
/** 排除名单：不再加载进人员库的护士名（全员清除，后台管理也看不到） */
const HIDDEN_NURSES = new Set(['马春婷', '张金焕']);
/** 下拉排除名单：人员库内可见（后台管理），但不在巡回/器械/交接下拉中选择 */
const DROPDOWN_HIDDEN = new Set(['邱空军', '孙婷婷', '王晴']);
function filterHiddenNurses(list) { return (list || []).filter(n => n && !HIDDEN_NURSES.has(n.name)); }

function loadAll() {
  try { rows = JSON.parse(safeStorage.get(K_ROWS)) || []; } catch { rows = []; }
  try { nurses = filterHiddenNurses(JSON.parse(safeStorage.get(K_NURSE))) || []; } catch { nurses = []; }
  try { cArmRows = JSON.parse(safeStorage.get(K_CARM)) || []; } catch { cArmRows = []; }
  try { infectionRows = JSON.parse(safeStorage.get(K_INFECTION)) || []; } catch { infectionRows = []; }
  buildNurseNameMap();
  try { const s = JSON.parse(safeStorage.get(K_SETTINGS));
    settings = { overtime: s?.overtime||'16:00', defaultBaseScore: s?.defaultBaseScore||10 };
  } catch { settings = { overtime: '16:00', defaultBaseScore: 10 }; }
  let migrated = false;
  nurses.forEach(n => {
    if (!n.hasOwnProperty('systemRole')) { n.systemRole = 'nurse'; migrated = true; }
    if (!n.hasOwnProperty('canEditScore')) { n.canEditScore = false; migrated = true; }
  });
  if (migrated) saveNurses();
}
// 行级时间戳：每次本地修改/保存/取消保存都打戳，配合后端按行合并，
// 保证「最新改动」在多点同步冲突中胜出，已保存行绝不被旧副本冲掉。
function touchRow(r) { if (r) r.updatedAt = Date.now(); }

function saveRows() {
  _lastLocalChangeAt = Date.now();
  ensureRowIds(); // 给历史行补 id，保证服务端按行合并稳定
  safeStorage.set(K_ROWS, JSON.stringify(rows)); syncToServer();
}
function saveNurses() { _lastLocalChangeAt = Date.now(); safeStorage.set(K_NURSE, JSON.stringify(nurses)); buildNurseNameMap(); syncToServer(); }
function saveCArmRows() { _lastLocalChangeAt = Date.now(); safeStorage.set(K_CARM, JSON.stringify(cArmRows)); syncToServer(); }
function saveInfectionRows() { _lastLocalChangeAt = Date.now(); safeStorage.set(K_INFECTION, JSON.stringify(infectionRows)); syncToServer(); }
function saveAll() { saveRows(); saveNurses(); }

/* ---- 工具 ---- */
const today = () => { const d = new Date(); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0'); };
function t(msg) { const e = document.getElementById('toast'); e.textContent = msg; e.classList.add('show'); clearTimeout(t._id); t._id = setTimeout(() => e.classList.remove('show'),1800); }
// 分值解析：支持「留痕表达式」，统计时只取最终数值
//   '3+4+6=13' -> 13（等号右侧）   '3+4+6' -> 13（自动求和）   '13' -> 13   '' -> 0
function parseScore(raw) {
  if (raw === null || raw === undefined) return 0;
  let s = String(raw).trim();
  if (s === '') return 0;
  const eq = s.lastIndexOf('=');
  if (eq !== -1) {
    const right = s.slice(eq + 1).trim();
    if (right !== '' && /^\d+(\.\d+)?$/.test(right)) return parseFloat(right);
    s = s.slice(0, eq); // 等号右侧非数字（仍在输入中），回退用左侧表达式
  }
  s = s.replace(/[^\d+\-*/().\s]/g, '').trim();
  if (s === '') return 0;
  try {
    const v = Function('"use strict";return (' + s + ')')();
    return (typeof v === 'number' && isFinite(v)) ? v : 0;
  } catch (e) { return 0; }
}
function parse4Digit(v) {
  if (!v) return '';
  v = String(v).trim();
  // 兼容全角冒号（中文输入法常见）+ 句点/短横线/中文逗号
  v = v.replace(/[：﹕。\.\-，,]/g, ':');
  // 兼容「8:15」前面没补 0 的情况
  if (/^\d{1,2}:\d{2}$/.test(v)) return v.padStart(5, '0').slice(-5);
  if (/^\d{4}$/.test(v)) { const h=+v.slice(0,2),m=+v.slice(2,4); if(h<24&&m<60) return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0'); }
  return v;
}
// 出室时间上限：仅当记录属于「今天」时才限制（出室时间不得晚于当前本地时间 +30 分钟），
// 防止把尚未发生的当天手术出室时间提前填到未来。补登记历史手术（登记日期≠今天）放行，
// 否则会被「当前时间」误拦（例如第二天补登昨天的手术，填昨天的出室时间反而超限）。
function nowHM() { const n = new Date(); return String(n.getHours()).padStart(2,'0') + ':' + String(n.getMinutes()).padStart(2,'0'); }
function outTimeLimitExceeded(outTime, rowDate) {
  if (rowDate && rowDate !== today()) return false;   // 非今天的记录不限制（补登记历史手术）
  if (!outTime) return false;
  const m = String(outTime).match(/^(\d{1,2}):(\d{2})/);
  if (!m) return false;
  const ot = (+m[1]) * 60 + (+m[2]);
  const now = new Date();
  const nowMin = now.getHours() * 60 + now.getMinutes();
  let limit = nowMin + 30;
  if (limit <= 1440) return ot > limit;            // 当天：晚于 now+30 即超限
  const l2 = limit - 1440;                          // 跨午夜：允许 now 之后到午夜 + 次日 0~l2；区间 (l2, nowMin) 为超限
  return ot > l2 && ot < nowMin;
}
function diffMin(a,b) {
  if (!a||!b) return 0;
  a = String(a).replace(/[：﹕]/g, ':');
  b = String(b).replace(/[：﹕]/g, ':');
  // 截取时间和小数点之后多余字符（如「17:00（正常）」），只保留首段 HH:MM
  const ap = a.match(/^(\d{1,2}):(\d{2})/);
  const bp = b.match(/^(\d{1,2}):(\d{2})/);
  if (!ap || !bp) return 0;
  const ah = +ap[1], am = +ap[2], bh = +bp[1], bm = +bp[2];
  let s = ah*60+am, e = bh*60+bm;
  if(e<s) e+=24*60;
  return e-s;
}
function fmtDur(m) { if(!m||m<=0)return'';const h=Math.floor(m/60),r=m%60;return h+'h'+(r?r+'m':'');}
function isOver(t) { return t && t > settings.overtime; }
// 拆分主岗 / 交接护士各自的时长；有交接时主岗 = 入室→交接时刻，交接人 = 交接时刻→出室
function splitTime(r, who) {
  const hoName = r[who + 'HoName'];
  const hoTime = r[who + 'HoTime'];
  if (hoName && hoTime) {
    return { main: diffMin(r.inTime, hoTime), hoName: hoName, hoDur: diffMin(hoTime, r.outTime) };
  }
  return { main: diffMin(r.inTime, r.outTime), hoName: null, hoDur: 0 };
}

/* ---- 导航 ---- */
document.querySelectorAll('.nav-tab').forEach(btn => {
  btn.addEventListener('click', function() {
    const page = this.dataset.page;
    try { safeStorage.set('surgery_currentPage', page); } catch(e) {}
    document.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
    this.classList.add('active');
    const pageId = 'page-'+page;
    document.getElementById(pageId).classList.add('active');
    if (page === 'ledger') renderLedger();          // 登录/切回时刷新权限（分值编辑等）
    if (page === 'daily') refreshDaily();
    if (page === 'monthly') refreshMonthly();
    if (page === 'carm') {
      // 每次切换都拉一次服务端，确保不依赖本地缓存
      apiFetch('/api/load').then(r=>r.json()).then(d => {
        if (d.cArmRows) { cArmRows = d.cArmRows; safeStorage.set(K_CARM, JSON.stringify(cArmRows)); refreshCArm(); }
      }).catch(() => {});
      refreshCArm();
    }
    if (page === 'infection') {
      apiFetch('/api/load').then(r=>r.json()).then(d => {
        if (d.infectionRows) { infectionRows = d.infectionRows; safeStorage.set(K_INFECTION, JSON.stringify(infectionRows)); refreshInfection(); }
      }).catch(() => {});
      refreshInfection();
    }
    if (page === 'perf') refreshPerf();
    if (this.dataset.page === 'personnel') renderNurses();
    if (page === 'audit') loadAuditLog();
  });
});

/* ===========================================================
   模块 1：登记表
   =========================================================== */
// 每行稳定唯一 id（uuid），用于服务端按行合并，避免多人/同患者同日互相覆盖
function genId() {
  if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
  return 'r' + Date.now() + Math.random().toString(16).slice(2);
}
// 给本地 rows 中缺失 id 的历史行补 id（兼容升级前的数据）
function ensureRowIds() {
  if (!Array.isArray(rows)) return;
  rows.forEach(r => { if (r && !r.id) r.id = genId(); });
}
function blankRow() {
  return {
    id: genId(),
    date: (document.getElementById('reg-date').value || today()), admNo:'', dept: (getDepts()[0]||''), bedNo:'', name:'', sex:'', age:'',
    surgery:'', circu:'', scrub:'', inTime:'', outTime:'',
    duration:'', circuScore:'', scrubScore:'',
    // 交接巡回/器械：展示信息（姓名/时间）+ 独立分值（分支框，自动带入姓名）
    circuHoName:'', circuHoTime:'', circuHoScore:'',
    scrubHoName:'', scrubHoTime:'', scrubHoScore:'',
    status: 'pending', enteredBy: (currentUser?.name || ''), reviewedBy: '', reviewedAt: '',
    savedBy: '', savedByName: '', savedAt: ''
  };
}

/** 患者姓名脱敏：末字（4 字及以上为末两字）替换为 *
 *   2字 张伟     → 张*
 *   3字 张伟明   → 张伟*
 *   4字 欧阳明月 → 欧阳**
 *   5字及以上    → 保留前 n-2 字 + **（如「买买提明江」→「买买提**」）
 * 幂等：已含 * 的原样返回，避免「张*」被二次脱敏成「张**」
 * 说明：单字姓名保持不变（再脱敏信息量为 0，护士无法核对）
 */
function maskName(v) {
  const s = String(v == null ? '' : v).trim();
  if (!s || s.indexOf('×') >= 0) return s;
  const n = Array.from(s);                 // Array.from 正确处理生僻字代理对
  if (n.length <= 1) return s;             // 单字：不脱敏
  if (n.length <= 3) return n.slice(0, -1).join('') + '×';
  return n.slice(0, -2).join('') + '××';
}

/** 姓名输入框「失焦脱敏」：把脱敏结果写回对应数据源
 *  store: 'ledger' | 'carm' | 'infection'
 *  必须在 onblur 触发，不能在 oninput 里实时脱敏——否则输入「张伟明」时，
 *  打到第 2 字就变「张×」，继续输入第 3 字成「张×明」，中间字已被吃掉，结果错误 */
function maskNameBlur(inp, store, idx) {
  if (inp.style) inp.style.background = 'transparent';
  const masked = maskName(inp.value);
  if (masked === inp.value) return;
  inp.value = masked;
  if (store === 'carm') {
    if (cArmRows[idx]) { cArmRows[idx].name = masked; touchRow(cArmRows[idx]); saveCArmRows(); }
  } else if (store === 'infection') {
    if (infectionRows[idx]) { infectionRows[idx].name = masked; touchRow(infectionRows[idx]); saveInfectionRows(); }
  } else {
    if (rows[idx]) { rows[idx].name = masked; touchRow(rows[idx]); saveRows(); }
  }
}

/** 护士下拉顺序：将「金鑫」固定在第六位（索引 5），其余保持原顺序
 *  同时排除 DROPDOWN_HIDDEN 中的护士名——她们仍在人员库内，仅不出现在选择框 */
function orderedNurseList() {
  const PIN_NAME = '金鑫', PIN_POS = 5;
  const arr = nurses.slice().filter(n => !DROPDOWN_HIDDEN.has(n.name));
  const i = arr.findIndex(n => n.name === PIN_NAME);
  if (i !== -1) { const [p] = arr.splice(i, 1); arr.splice(Math.min(PIN_POS, arr.length), 0, p); }
  return arr;
}

/** 交接展示文本：初始护士 时分 交接巡回/器械 交接人员
 *  例：柳伟豪 16:05 交接巡回肖珊 ；无交接时返回原始姓名 */
function nurseCellText(baseName, hoName, hoTime, kind) {
  const base = baseName || '';
  if (!hoName) return base;
  const word = (kind === 'circu') ? '交接巡回' : '交接器械';
  return [base, (hoTime || ''), word + hoName].filter(Boolean).join(' ');
}

/** 记录哪些单元格的交接子面板已展开（点开下拉选中「交接巡回/交接器械」后） */
const hoPanelOpen = new Set();
const HO_TRIGGER = '__HO__';

/** 创建交接子面板（返回行内容器，不自行 append）：[时间] [交接人员] [✕] 三件，
 *  横向嵌入到主岗下拉右侧，组装后单元格显示：[主岗▾] [时间__] [交接▾] [✕]，
 *  "时间夹在两位护士之间"；锁定/展示态用 nurseCellText 合成文本 */
function createHandoverPanel(r, realIdx, col, baseName) {
  const kind = col.k;
  const hoNameKey = (kind === 'circu') ? 'circuHoName' : 'scrubHoName';
  const hoTimeKey = (kind === 'circu') ? 'circuHoTime' : 'scrubHoTime';
  const hoScoreKey = (kind === 'circu') ? 'circuHoScore' : 'scrubHoScore'; // 分支分值字段
  const curHo = r[hoNameKey] || '';
  const row = document.createElement('span');
  row.className = 'ho-row';
  // 顺序：先时间（夹在主岗和交接人员之间），再交接人员，最后清除
  if (curHo) {
    const timeInp = document.createElement('input');
    timeInp.type = 'text'; timeInp.className = 'ho-time';
    timeInp.value = r[hoTimeKey] || ''; timeInp.placeholder = '时间';
    timeInp.onblur = function() {
      let t = (this.value||'').trim();
      const d = t.replace(/\D/g,'');
      if (/^\d{3,4}$/.test(d)) t = d.padStart(4,'0').replace(/(\d{2})(\d{2})/, '$1:$2');
      rows[realIdx][hoTimeKey] = t; saveRows(); renderLedger();
    };
    timeInp.oninput = function() { rows[realIdx][hoTimeKey] = this.value; saveRows(); };
    row.appendChild(timeInp);
  }
  // 交接人员下拉（与主岗完全共用同一人员池）
  const hoSel = document.createElement('select');
  hoSel.className = 'ho-person';
  hoSel.innerHTML = '<option value=""></option>'
    + orderedNurseList().map(n => `<option value="${n.name}"${curHo===n.name?' selected':''}>${n.name}</option>`).join('');
  if (curHo && !orderedNurseList().find(n => n.name === curHo)) { hoSel.innerHTML += '<option value="'+curHo+'" selected>'+curHo+' (历史)</option>'; }
  hoSel.value = curHo;
  hoSel.onchange = () => {
    const nv = hoSel.value;
    rows[realIdx][hoNameKey] = nv;
    if (!nv) { rows[realIdx][hoTimeKey] = ''; rows[realIdx][hoScoreKey] = ''; }
    hoPanelOpen.delete(realIdx + '_' + kind);
    saveRows(); renderLedger();
  };
  row.appendChild(hoSel);
  // 取消 ✕ 清除按钮：清空交接仅通过把人员下拉改回空选项触发（hoSel.onchange 已联动清空时间+分值）
  return row;
}

/** 姓名自动同步：巡回/器械护士完成一次登记后，同日期后续空行自动复用该姓名 */
function syncNurseNames(fd) {
  let lastC = '', lastS = '';
  rows.forEach(r => {
    if (r.date !== fd) return;
    if (r.circu) lastC = r.circu; else if (lastC) r.circu = lastC;
    const sVal = (r.scrub && r.scrub !== '无') ? r.scrub : '';
    if (sVal) lastS = sVal; else if (lastS) r.scrub = lastS;
  });
}

function renderLedger() {
  const filterDate = document.getElementById('reg-date').value || today();
  const tb = document.getElementById('tbody');
  tb.innerHTML = '';
  const isAdmin = currentUser && currentUser.role === 'admin';
  const canEdit = ROLES[currentUser?.role || 'viewer']?.canEdit;
  const canScore = canEditScore(); // 当前用户是否可编辑分值
  const scoreHint = document.getElementById('score-hint');
  if (scoreHint) {
    if (!canScore) { scoreHint.style.display = ''; scoreHint.textContent = '（当前账号无「编辑分值」权限，分值列只读）'; }
    else scoreHint.style.display = 'none';
  }
  rows.filter(r => r.date === filterDate).forEach((r, rowIdx) => {
    const realIdx = rows.indexOf(r);
    const reviewed = r.status !== 'pending';
    const rowLocked = !canEditRow(r);                    // 行级权限锁：他人已保存本行 → 当前账号只读
    const locked = (reviewed && !isAdmin) || rowLocked;  // 已审核锁 或 行级锁 → 单元格只读
    const tr = document.createElement('tr'); tr.dataset.idx = realIdx;
    // 行背景色：已审核=浅绿，已保存未审核=浅蓝，未保存=默认白
    if (reviewed) tr.style.background = '#a5d6a7';        // 管理员审核后 → 柔和加深绿（非标准绿），深字清晰
    else if (r.savedBy) tr.style.background = '#e3f2fd';   // 用户保存后 → 蓝色
    // 序号+勾选
    const td0 = document.createElement('td'); td0.innerHTML = `<input type="checkbox" class="row-chk" data-idx="${realIdx}"> ${rowIdx+1}`; tr.appendChild(td0);
    allCols.filter(c => c.k !== 'date').forEach(col => {
      const td = document.createElement('td');
      const v = r[col.k] ?? '';
      if (col.t === 'calc') {
        const stC = splitTime(r, 'circu');
        const stS = splitTime(r, 'scrub');
        const hasCC = !!(r.circuHoName && r.circuHoTime);
        const hasSC = !!(r.scrubHoName && r.scrubHoTime);
        if (!hasCC && !hasSC) {
          const m = diffMin(r.inTime, r.outTime);
          td.textContent = fmtDur(m);
          td.className = 'dur' + (m?'':' zero');
        } else {
          const lines = [];
          if (hasCC) lines.push(`<div>${(r.circu||'').slice(-1)} ${fmtDur(stC.main)||'0'} → ${(r.circuHoName||'').slice(-1)} ${fmtDur(stC.hoDur)||'0'}</div>`);
          if (hasSC) lines.push(`<div>${(r.scrub||'').slice(-1)} ${fmtDur(stS.main)||'0'} → ${(r.scrubHoName||'').slice(-1)} ${fmtDur(stS.hoDur)||'0'}</div>`);
          td.innerHTML = lines.join('');
          td.className = 'dur';
        }
      } else if (col.t === 'select') {
        const sel = document.createElement('select');
        sel.dataset.f = col.k; sel.dataset.i = realIdx;
        sel.innerHTML = (col.nb?'':'<option value=""></option>') + (col.op||[]).map(o => `<option value="${o}"${v===o?' selected':''}>${o}</option>`).join('');
        sel.onchange = zi => { rows[realIdx][col.k] = zi.target.value; saveRows(); };
        if (locked) sel.disabled = true;
        td.appendChild(sel);
      } else if (col.t === 'dept') {
        const depts = getDepts();
        const sel = document.createElement('select');
        sel.dataset.f = col.k; sel.dataset.i = realIdx;
        sel.innerHTML = depts.map(o => `<option value="${o}"${v===o?' selected':''}>${o}</option>`).join('');
        // 兜底：如果当前值不在标准列表中，保留为历史选项（避免默认跳到第一个=眼科）
        if (v && !depts.includes(v)) { sel.innerHTML = '<option value="'+v+'" selected>'+v+'</option>' + sel.innerHTML; }
        sel.onchange = zi => { rows[realIdx][col.k] = zi.target.value; saveRows(); };
        if (locked) sel.disabled = true;
        td.appendChild(sel);
      } else if (col.t === 'nurse') {
        td.classList.add('nurse-cell'); // 拓宽单元格，让 [主岗▾][时间__][交接▾][✕] 横向铺开
        const hoNameKey = (col.k === 'circu') ? 'circuHoName' : 'scrubHoName';
        const hoTimeKey = (col.k === 'circu') ? 'circuHoTime' : 'scrubHoTime';
        if (locked) {
          // 锁定行：直接展示合成文本（初始护士 时分 交接巡回/器械 交接人员）
          td.textContent = nurseCellText(v, r[hoNameKey], r[hoTimeKey], col.k);
        } else {
          // 初始护士下拉（原有逻辑保持不变）；选项最底部追加「交接巡回/交接器械」触发项
          // 用 .nurse-wrap 包住主岗下拉 + 交接面板，单元格内横向排列：[主岗▾] [时间__] [交接▾] [✕]
          // 时间夹在两位护士之间，符合"用时间把两个人名字隔开"的需求
          const wrap = document.createElement('div');
          wrap.className = 'nurse-wrap';
          const sel = document.createElement('select');
          sel.className = 'nurse-primary';
          sel.dataset.f = col.k; sel.dataset.i = realIdx;
          const emptyOpt = (col.k === 'scrub') ? '<option value="无">无</option>' : '<option value=""></option>';
          const word = (col.k === 'circu') ? '交接巡回' : '交接器械';
          const orderedNurses = orderedNurseList(); // 金鑫固定第六位
          sel.innerHTML = emptyOpt
            + orderedNurses.map(n => `<option value="${n.name}"${v===n.name?' selected':''}>${n.name}</option>`).join('')
            + `<option value="${HO_TRIGGER}" class="ho-trigger-opt">→ ${word}</option>`;
          if (v && !orderedNurseList().find(n => n.name === v) && v !== '无') { sel.innerHTML = '<option value="'+v+'" selected>'+v+' (历史)</option>' + sel.innerHTML; }
          if (!v && col.k === 'circu') sel.value = '';
          sel.onchange = zi => {
            // 选中底部「交接」触发项：不写入主岗，也不触发全量重绘；直接在 wrap 内注入交接面板
            if (zi.target.value === HO_TRIGGER) {
              hoPanelOpen.add(realIdx + '_' + col.k);
              sel.value = v || (col.k === 'scrub' ? '无' : ''); // 还原为主岗护士
              const oldPanel = wrap.querySelector('.ho-row');
              if (oldPanel) oldPanel.remove();
              wrap.appendChild(createHandoverPanel(r, realIdx, col, v));
              return;
            }
            rows[realIdx][col.k] = zi.target.value; saveRows(); renderLedger();
          };
          wrap.appendChild(sel);
          // 已填写交接信息，或本次点开了「交接」触发项 → 渲染浮动交接面板（全量重绘时走此分支）
          if ((r[hoNameKey] || '') || hoPanelOpen.has(realIdx + '_' + col.k)) {
            wrap.appendChild(createHandoverPanel(r, realIdx, col, v));
          }
          td.appendChild(wrap);
        }
      } else if (col.t === 'time') {
        if (locked) { td.textContent = v; }
        else {
          const inp = document.createElement('input');
          inp.type = 'text'; inp.dataset.f = col.k; inp.dataset.i = realIdx;
          inp.value = v;
          inp.placeholder = '4位→时间';
          inp.onblur = function() { onTimeBlur(this, col.k, realIdx); };
          inp.oninput = function() {
            // 实时格式化：4位数字立刻转 HH:MM，避免 onblur 在手机端不触发导致未格式化
            let v = this.value;
            if (/^\d{4}$/.test(v)) { const p = parse4Digit(v); if (p !== v) { this.value = p; v = p; } }
            // 出室时间上限：不得晚于当前本地时间 +30 分钟，超限立即还原并提示，不写入/不保存
            if (col.k === 'outTime' && outTimeLimitExceeded(v, rows[realIdx].date)) {
              t('出室时间不能晚于当前时间 30 分钟以上（当前约 ' + nowHM() + '）。补登历史手术请把上方「登记日期」改为手术当天');
              this.value = rows[realIdx][col.k] || '';
              return;
            }
            rows[realIdx][col.k] = v; touchRow(rows[realIdx]); saveRows(); refreshRow(realIdx, col.k);
          };
          td.appendChild(inp);
        }
      } else {
        const isScore = (col.k === 'circuScore' || col.k === 'scrubScore');
        // 分值列独立权限：不跟随行级锁定。即使本行被他人保存锁定/已审核，
        // 只要当前账号拥有「编辑分值」权限，分值列仍可编辑；只读判断只看 canScore。
        if (locked && !isScore) {
          td.textContent = v;                          // 普通基础列锁定 → 只读文本
        } else if (col.k === 'surgery') {
          // 手术名称列：用 textarea 让长文本自动换行并撑高行
          const ta = document.createElement('textarea');
          ta.dataset.f = col.k; ta.dataset.i = realIdx;
          ta.value = v;
          ta.rows = 1;
          ta.style.cssText = 'width:100%;min-height:24px;height:auto;resize:none;border:none;background:transparent;text-align:center;font-size:12px;padding:4px 6px;font-family:inherit;line-height:1.5;overflow:hidden;';
          ta.oninput = function() {
            rows[realIdx][col.k] = this.value; touchRow(rows[realIdx]); saveRows();
            this.style.height = 'auto';
            this.style.height = (this.scrollHeight) + 'px';
          };
          ta.onfocus = function() { this.style.background = '#fffbe6'; };
          ta.onblur = function() { this.style.background = 'transparent'; };
          td.classList.add('surgery-cell');
          // 等内容渲染完再按内容撑高
          setTimeout(() => { ta.style.height = 'auto'; ta.style.height = (ta.scrollHeight) + 'px'; }, 0);
          td.appendChild(ta);
        } else {
          const inp = document.createElement('input');
          inp.type = col.t === 'number' ? 'number' : 'text';
          inp.dataset.f = col.k; inp.dataset.i = realIdx;
          inp.value = v;
          // 手术名称列：标记样式类，允许长文本换行
          if (col.k === 'surgery') td.classList.add('surgery-cell');
          let appended = false;
          if (isScore) {
            // 分值列：支持「留痕表达式」（如 3+4+6=13），输入 = 自动求和，统计取最终分值
            const linked = col.k === 'circuScore'
              ? (r.circu || '')
              : ((r.scrub && r.scrub !== '无') ? r.scrub : '');
            const wrap = document.createElement('div');
            wrap.className = 'score-wrap';
            if (linked) {
              const lbl = document.createElement('span');
              lbl.className = 'score-label';
              lbl.textContent = linked;
              wrap.appendChild(lbl);
            }
            const ta = document.createElement('textarea');
            ta.className = 'score-inp';
            ta.value = rows[realIdx][col.k] || '';
            ta.placeholder = '';
            ta.disabled = !canScore; // 分值只读仅由独立权限决定，与行锁定无关
            const autoH = () => { ta.style.height = 'auto'; ta.style.height = (ta.scrollHeight) + 'px'; };
            ta.oninput = function(e) {
              let v = this.value;
              // 只在用户主动输入等号时自动求和；删除/回退到等号时不重复计算，方便改错
              if (e.data === '=' && v.endsWith('=')) {
                const left = v.slice(0, -1).replace(/[^\d+\-*/().\s]/g, '').trim();
                if (left) { try { const res = Function('"use strict";return (' + left + ')')(); if (typeof res === 'number' && isFinite(res)) { v = v + res; this.value = v; } } catch (err) {} }
              }
              rows[realIdx][col.k] = v; touchRow(rows[realIdx]); saveRows();
              autoH();
            };
            setTimeout(autoH, 0); // 初始按内容撑高
            wrap.appendChild(ta);
            // 交接分支：若已选交接护士，在主岗分值下方自动追加一行（交接护士名+独立分值框）
            const hoNameKey = (col.k === 'circuScore') ? 'circuHoName' : 'scrubHoName';
            const hoScoreKey = (col.k === 'circuScore') ? 'circuHoScore' : 'scrubHoScore';
            const hoName = r[hoNameKey] || '';
            if (hoName) {
              const stack = document.createElement('div');
              stack.className = 'score-cell-stack';
              stack.appendChild(wrap);
              const branchWrap = document.createElement('div');
              branchWrap.className = 'score-wrap score-branch';
              const branchLbl = document.createElement('span');
              branchLbl.className = 'score-label';
              branchLbl.textContent = hoName; // 交接护士姓名自动带入
              branchWrap.appendChild(branchLbl);
              const bta = document.createElement('textarea');
              bta.className = 'score-inp';
              bta.value = r[hoScoreKey] || '';
              bta.placeholder = '';
              bta.disabled = !canScore;
              bta.oninput = function(e) {
                let v = this.value;
                // 只在用户主动输入等号时自动求和；删除/回退到等号时不重复计算
                if (e.data === '=' && v.endsWith('=')) {
                  const left = v.slice(0, -1).replace(/[^\d+\-*/().\s]/g, '').trim();
                  if (left) { try { const res = Function('"use strict";return (' + left + ')')(); if (typeof res === 'number' && isFinite(res)) { v = v + res; this.value = v; } } catch (err) {} }
                }
                rows[realIdx][hoScoreKey] = v; touchRow(rows[realIdx]); saveRows();
                this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px';
              };
              setTimeout(() => { bta.style.height = 'auto'; bta.style.height = (bta.scrollHeight) + 'px'; }, 0);
              branchWrap.appendChild(bta);
              stack.appendChild(branchWrap);
              td.appendChild(stack);
            } else {
              td.appendChild(wrap);
            }
            appended = true;
          } else if (col.ph) {
            inp.placeholder = col.ph;
          }
          if (col.min !== undefined) inp.min = col.min;
          if (col.step !== undefined) inp.step = col.step;
          if (col.max !== undefined) inp.max = col.max;
          inp.oninput = function() {
            let val = this.value;
            if (col.t === 'number' && val !== '') {
              // 仅允许合规数字（正整数或小数）
              let cleaned = val.replace(/[^\d.]/g, '').replace(/(\..*)\./g, '$1');
              if (col.min !== undefined && +cleaned < +col.min) cleaned = String(col.min);
              if (cleaned !== val) this.value = cleaned;
              val = cleaned;
            }
            rows[realIdx][col.k] = val; touchRow(rows[realIdx]); saveRows();
          };
          // 姓名：失焦时脱敏（实时脱敏会吃掉输入过程中的中间字，必须失焦触发）
          if (col.k === 'name') {
            inp.onblur = function() { maskNameBlur(this, 'ledger', realIdx); };
          }
          // 住院号失焦检查重复：编辑后若系统中已存在相同住院号的其他记录，提醒是否继续
          if (col.k === 'admNo' && !locked) {
            let _admFocus = '';
            inp.onfocus = function() { _admFocus = (this.value || '').trim(); };
            inp.onblur = function() {
              const val = (this.value || '').trim();
              if (!val || val === _admFocus) return;   // 未改或清空则不提醒
              const idx = +this.dataset.i;
              const dups = rows.filter((x, i) => i !== idx && (x.admNo || '').trim() === val);
              if (dups.length) {
                const ok = confirm('系统中已有住院号「' + val + '」的病人记录（共 ' + dups.length + ' 条）。\n可能是重复登记。\n\n点「确定」继续编辑/新增；点「取消」清空本行住院号。');
                if (!ok) {
                  rows[idx].admNo = '';
                  this.value = '';
                  touchRow(rows[idx]); saveRows();
                }
              }
            };
          }
          if (!appended) td.appendChild(inp);
        }
      }
      tr.appendChild(td);
    });
    // 行操作列：可编辑 → 保存按钮；被他人锁定 → 锁定标识；已审核 → 已审核标识
    const actTd = document.createElement('td');
    actTd.style.textAlign = 'center';
    if (locked) {
      if (rowLocked) {
        actTd.innerHTML = '<span title="由 ' + (r.savedByName || '他人') + ' 保存并锁定" style="color:#c0392b;font-size:12px;white-space:nowrap;">🔒 已锁</span>'
          + (r.savedByName ? '<div style="font-size:11px;color:#999;margin-top:2px;white-space:nowrap;">' + r.savedByName + '</div>' : '');
      } else {
        actTd.innerHTML = '<span style="color:#8a6d3b;font-size:12px;white-space:nowrap;">🔒 已审核</span>';
      }
    } else if (canEdit !== false || isAdmin) {
      const btn = document.createElement('button');
      btn.className = 'btn';
      btn.style.cssText = 'padding:2px 10px;font-size:12px;white-space:nowrap;';
      if (r.savedBy) {
        // 已保存：本人看「⏸ 取消保存」，管理员看「🔓 解锁」（双按钮合一，按角色显示）
        if (isAdmin) {
          btn.style.background = '#fff3e0'; btn.style.color = '#e65100'; btn.style.border = '1px solid #ffcc80';
          btn.textContent = '🔓 解锁';
          btn.onclick = () => unlockRow(realIdx);
        } else {
          btn.style.background = '#e3f2fd'; btn.style.color = '#1565c0'; btn.style.border = '1px solid #90caf9';
          btn.textContent = '⏸ 取消保存';
          btn.onclick = () => unsaveRow(realIdx);
        }
      } else {
        btn.className += ' primary';
        btn.textContent = '保存';
        btn.onclick = () => saveRow(realIdx);
      }
      actTd.appendChild(btn);
      if (r.savedBy) {
        const tag = document.createElement('div');
        tag.style.cssText = 'font-size:11px;color:#1d6e3c;margin-top:2px;white-space:nowrap;';
        tag.textContent = '✔ ' + (r.savedByName || '已保存');
        actTd.appendChild(tag);
      }
    }
    tr.appendChild(actTd);
    // 「导入C臂」列 — 绿色调（当行有巡回护士且未锁定时可用）
    const cArmTd = document.createElement('td');
    cArmTd.style.textAlign = 'center';
    cArmTd.style.padding = '2px';
    if (r.circu && !rowLocked) {
      const cArmReviewedBlocked = r.status === 'reviewed' && !(currentUser && currentUser.role === 'admin');
      const cArmBtn = document.createElement('button');
      cArmBtn.className = 'btn';
      if (cArmReviewedBlocked) {
        cArmBtn.disabled = true;
        cArmBtn.style.cssText = 'padding:2px 6px;font-size:11px;white-space:nowrap;background:#f0f0f0;color:#9e9e9e;border:1px solid #d9d9d9;cursor:not-allowed;';
        cArmBtn.textContent = '➡ 导入C臂';
        cArmBtn.title = '本行已审核，普通护士不能导入；仅管理员可导入';
      } else {
        cArmBtn.style.cssText = 'padding:2px 6px;font-size:11px;white-space:nowrap;background:#e8f5e9;color:#2e7d32;border:1px solid #a5d6a7;';
        cArmBtn.textContent = '➡ 导入C臂';
        cArmBtn.title = '把这一行导入到C臂登记本（当天日期、运行情况默认良好）';
        cArmBtn.onclick = () => importRowToCArm(realIdx);
      }
      cArmTd.appendChild(cArmBtn);
    }
    tr.appendChild(cArmTd);
    // 「导入感染」列 — 紫色调（当行有巡回护士且未锁定时可用）
    const infTd = document.createElement('td');
    infTd.style.textAlign = 'center';
    infTd.style.padding = '2px';
    if (r.circu && !rowLocked) {
      const infReviewedBlocked = r.status === 'reviewed' && !(currentUser && currentUser.role === 'admin');
      const infBtn = document.createElement('button');
      infBtn.className = 'btn';
      if (infReviewedBlocked) {
        infBtn.disabled = true;
        infBtn.style.cssText = 'padding:2px 6px;font-size:11px;white-space:nowrap;background:#f0f0f0;color:#9e9e9e;border:1px solid #d9d9d9;cursor:not-allowed;';
        infBtn.textContent = '➡ 导入感染';
        infBtn.title = '本行已审核，普通护士不能导入；仅管理员可导入';
      } else {
        infBtn.style.cssText = 'padding:2px 6px;font-size:11px;white-space:nowrap;background:#f3e5f5;color:#6a1b9a;border:1px solid #ce93d8;';
        infBtn.textContent = '➡ 导入感染';
        infBtn.title = '把这一行导入到感染登记表（感染类型/手术间号手动填，其余自动）';
        infBtn.onclick = () => importRowToInfection(realIdx);
      }
      infTd.appendChild(infBtn);
    }
    tr.appendChild(infTd);
    tb.appendChild(tr);
  });
  updateLedgerStats();
  renderPrintLedger(); // 同步填充打印专用表格，避免打印时主表列宽冲突
}

// 打印专用表格渲染：仅含业务列，独立 colgroup 避免主表复杂样式干扰
// 支持区间打印：传入 opts.range = {start:'YYYY-MM-DD', end:'YYYY-MM-DD'} 时打印该区间内全部手术（按日期、入室时间排序，并额外加「日期」列）
function renderPrintLedger(opts) {
  const tb = document.getElementById('ledger-print-body');
  if (!tb) return;
  const range = (opts && opts.range) || window._printRange;
  const cg = document.getElementById('ledger-print-cg');
  const thead = document.getElementById('ledger-print-thead');
  const titleEl = document.getElementById('print-range-title');
  // 列定义：[表头, 宽度(px), 取值函数]
  const baseCols = [
    ['住院号',       70,  r => r.admNo],
    ['科室',         60,  r => r.dept],
    ['床号',         34,  r => r.bedNo],
    ['姓名',         54,  r => r.name],
    ['性别',         32,  r => r.sex],
    ['年龄',         34,  r => r.age],
    ['手术名称',     185, r => r.surgery],
    ['巡回护士',     58,  r => r.circu],
    ['器械护士',     58,  r => r.scrub],
    ['入室',         42,  r => r.inTime],
    ['出室',         42,  r => r.outTime],
    ['耗时',         46,  r => fmtDur(diffMin(r.inTime, r.outTime))],
    ['巡回护士分值', 58,  r => r.circuScore],
    ['器械护士分值', 58,  r => r.scrubScore],
  ];
  const cols = range ? [['日期', 56, r => r.date]].concat(baseCols) : baseCols;
  if (cg) cg.innerHTML = cols.map(c => '<col style="width:' + c[1] + 'px">').join('');
  if (thead) thead.innerHTML = '<tr>' + cols.map(c => '<th>' + c[0] + '</th>').join('') + '</tr>';
  // 数据行：区间打印取区间内全部（按日期+入室时间排序），否则只取当前登记日期
  const list = range
    ? rows.filter(r => r.date && r.date >= range.start && r.date <= range.end)
          .slice().sort((a, b) => (a.date || '').localeCompare(b.date || '') || (a.inTime || '').localeCompare(b.inTime || ''))
    : rows.filter(r => r.date === (document.getElementById('reg-date').value || today()));
  tb.innerHTML = list.map(r => '<tr>' + cols.map(c => {
    const cls = (c[0] === '手术名称') ? ' class="surgery-cell"' : '';
    return '<td' + cls + '>' + escapeHtml(String(c[2](r) || '')) + '</td>';
  }).join('') + '</tr>').join('');
  // 区间打印时在表格上方显示日期范围与总台数（单日打印留空，CSS 会自动隐藏空标题）
  if (titleEl) {
    titleEl.textContent = range
      ? ('手术病人登记表（' + range.start + ' 至 ' + range.end + '，共 ' + list.length + ' 台）')
      : '';
  }
}

// 打印当前登记日期（单日）
function printLedger() {
  window._printRange = null;
  renderPrintLedger();
  setTimeout(() => window.print(), 80);
}

// 区间打印：弹出日期区间选择（.modal 默认 display:none，需加 .show 才显示）
function openPrintRangeModal() {
  const d = document.getElementById('reg-date').value || today();
  const s = document.getElementById('print-start'), e = document.getElementById('print-end');
  if (s && !s.value) s.value = d;
  if (e && !e.value) e.value = d;
  const m = document.getElementById('print-range-modal');
  if (m) { m.style.display = ''; m.classList.add('show'); }
}
function closePrintRangeModal() {
  const m = document.getElementById('print-range-modal');
  if (m) { m.classList.remove('show'); m.style.display = 'none'; }
}
function doPrintRange() {
  const s = document.getElementById('print-start').value;
  const e = document.getElementById('print-end').value;
  if (!s || !e) { t('请选择开始日期和结束日期'); return; }
  if (s > e) { t('开始日期不能晚于结束日期'); return; }
  closePrintRangeModal();
  window._printRange = { start: s, end: e };
  renderPrintLedger({ range: { start: s, end: e } });
  setTimeout(() => window.print(), 80);
}
// 打印结束后恢复为单日打印表，避免影响日常查看
window.addEventListener('afterprint', function() {
  window._printRange = null;
  renderPrintLedger();
});

function onTimeBlur(inp, field, idx) {
  let p = parse4Digit(inp.value);
  if (field === 'outTime' && outTimeLimitExceeded(p, rows[idx].date)) {
    t('出室时间不能晚于当前时间 30 分钟以上（当前约 ' + nowHM() + '）。补登历史手术请把「登记日期」改为手术当天');
    inp.value = rows[idx][field] || '';
    return;
  }
  if (p !== inp.value) { inp.value = p; }
  if (p !== rows[idx][field]) { rows[idx][field] = p; saveRows(); }
  refreshRow(idx, field);
}
function refreshRow(idx, field) {
  const tr = document.querySelector(`tr[data-idx="${idx}"]`);
  if (!tr) return;
  // 刷新耗时（有交接时按角色分别显示分段）
  const durs = tr.querySelectorAll('.dur');
  if (durs.length) {
    const r = rows[idx];
    const stC = splitTime(r, 'circu');
    const stS = splitTime(r, 'scrub');
    const hasCC = !!(r.circuHoName && r.circuHoTime);
    const hasSC = !!(r.scrubHoName && r.scrubHoTime);
    durs.forEach(d => {
      if (!hasCC && !hasSC) {
        const m = diffMin(r.inTime, r.outTime);
        d.textContent = fmtDur(m);
        d.className = 'dur' + (m?'':' zero');
      } else {
        const lines = [];
        if (hasCC) lines.push(`<div>${(r.circu||'').slice(-1)} ${fmtDur(stC.main)||'0'} → ${(r.circuHoName||'').slice(-1)} ${fmtDur(stC.hoDur)||'0'}</div>`);
        if (hasSC) lines.push(`<div>${(r.scrub||'').slice(-1)} ${fmtDur(stS.main)||'0'} → ${(r.scrubHoName||'').slice(-1)} ${fmtDur(stS.hoDur)||'0'}</div>`);
        d.innerHTML = lines.join('');
        d.className = 'dur';
      }
    });
  }
  updateLedgerStats();
}

function updateLedgerStats() {
  const filterDate = document.getElementById('reg-date').value || today();
  const dateRows = rows.filter(r => r.date === filterDate);
  const tot = dateRows.length;
  const ot = dateRows.filter(r => isOver(r.outTime)).length;
  const reviewed = dateRows.filter(r => r.status !== 'pending').length;
  const totalMin = dateRows.reduce((s,r) => s + diffMin(r.inTime, r.outTime), 0);
  const statTotal = document.getElementById('stat-total');
  if (statTotal) statTotal.textContent = tot;
  const statOvertime = document.getElementById('stat-overtime');
  if (statOvertime) statOvertime.textContent = ot;
  const statDuration = document.getElementById('stat-duration');
  if (statDuration) statDuration.textContent = fmtDur(totalMin) || '0';
  // 审核统计
  const cntEl = document.getElementById('review-count');
  if (cntEl) cntEl.textContent = '已审核 ' + reviewed + ' / ' + tot + ' 条';
  // 非管理员隐藏审核按钮
  const isAdmin = currentUser && currentUser.role === 'admin';
  const btns = ['btn-review-all','btn-unreview-all'];
  btns.forEach(id => { const b = document.getElementById(id); if (b) b.style.display = isAdmin ? 'inline-block' : 'none'; });
}

function addRow(batch) {
  const n = (typeof batch === 'number') ? batch : (batch ? 5 : 1);
  for (let i=0;i<n;i++) rows.push(blankRow());
  saveRows(); renderLedger();
  setTimeout(() => {
    const tbody = document.getElementById('tbody');
    if (tbody) tbody.lastElementChild?.scrollIntoView({behavior:'smooth', block:'nearest'});
  }, 50);
  t('新增' + n + '行');
}
// 行级保存：弹窗确认 → 保存 → 立即同步到服务器
async function saveRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = rows[idx];
  if (!r) return;
  if (!canEditRow(r)) { t('该行已被「' + (r.savedByName || '他人') + '」保存锁定，无权修改'); return; }
  // 出室时间上限：不得晚于当前本地时间 +30 分钟（仅当天记录；历史补登记放行）
  if (r.outTime && outTimeLimitExceeded(r.outTime, r.date)) {
    t('出室时间不能晚于当前时间 30 分钟以上，请修正后再保存（当前约 ' + nowHM() + '）。补登历史手术请把「登记日期」改为手术当天');
    return;
  }
  const hasCircuHo = !!(r.circuHoName && r.circuHoTime);
  const hasScrubHo = !!(r.scrubHoName && r.scrubHoTime);
  const msg = '确认保存本行？保存后仅本人与管理员可再编辑。\n\n' +
    '巡回护士：' + (r.circu || '（空）') + (hasCircuHo ? '\n交接巡回：' + r.circuHoName : '') + '\n' +
    '器械护士：' + ((r.scrub && r.scrub !== '无') ? r.scrub : '（无）') + (hasScrubHo ? '\n交接器械：' + r.scrubHoName : '') + '\n' +
    '病人：' + (r.name || '（空）') + '  ' + (r.dept || '（空）') + '\n' +
    '手术：' + (r.surgery || '（空）').slice(0, 50);
  if (!confirm(msg)) return;
  r.savedBy = currentUser.loginKey || currentUser.name;
  r.savedByName = currentUser.name;
  r.savedAt = new Date().toLocaleString('zh-CN');
  touchRow(r);
  saveRows();
  // 立即同步到服务器并等待服务器确认，避免「保存后马上刷新」导致数据丢失
  updateSyncStatus('同步中…');
  const ok = await syncToServer(true);
  renderLedger();
  if (ok) t('已保存并锁定，服务器已同步');
  else t('已保存到本地，服务器同步失败将自动重试');
  reportAudit('保存手术行', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || ''));
}
/** 本人取消保存：清除行锁，立即同步服务器 */
async function unsaveRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = rows[idx];
  if (!r) return;
  if (!canEditRow(r)) { t('该行已被「' + (r.savedByName || '他人') + '」保存锁定，无权取消'); return; }
  if (!confirm('确定取消保存？该行将恢复为可编辑状态')) return;
  r.savedBy = ''; r.savedByName = ''; r.savedAt = '';
  touchRow(r);
  saveRows(); const ok = await syncToServer(true); renderLedger();
  t(ok ? '已取消保存，服务器已同步' : '已取消保存（本地），服务器同步稍后重试');
  reportAudit('取消保存手术行', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || ''));
}
/** 把登记表的一行导入到 C 臂登记本（仅管理员） */
function importRowToCArm(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = rows[idx];
  if (!r) return;
  if (r.status === 'reviewed' && (!currentUser || currentUser.role !== 'admin')) { t('本行已审核，普通护士不能导入；仅管理员可导入'); return; }
  if (!r.circu) { t('本行未填巡回护士，无法导入'); return; }
  const importDate = r.date || document.getElementById('reg-date')?.value || today();
  // 去重：同一天、同一患者不能重复导入 C 臂（避免同一病人因术式手写后缀不同被重复导入）
  const dupKey = (importDate + '|' + (r.admNo || '') + '|' + (r.name || '')).trim();
  const already = cArmRows.some(x => (x.date + '|' + (x.admNo || '') + '|' + (x.name || '')).trim() === dupKey);
  if (already) { t('该患者已导入到 C 臂登记本，不能重复导入'); return; }
  // 弹窗确认，显示本次要导入的关键信息
  const msg = '确认把这台手术导入到 C 臂登记本？\n\n' +
    '日期：' + importDate + '（登记表日期）\n' + +
    '科室：' + (r.dept || '（空）') + '\n' +
    '床号：' + (r.bedNo || '（空）') + '\n' +
    '住院号：' + (r.admNo || '（空）') + '\n' +
    '病人姓名：' + (r.name || '（空）') + '\n' +
    '手术名称：' + (r.surgery || '（空）') + '\n' +
    '运行情况：良好（默认）\n' +
    '巡回护士：' + r.circu + '\n' +
    '洗手护士：' + ((r.scrub && r.scrub !== '无') ? r.scrub : '（无）');
  if (!confirm(msg)) return;
  cArmRows.unshift({
    id: genId(),
    date: importDate,
    dept: r.dept || '',
    bedNo: r.bedNo || '',
    admNo: r.admNo || '',
    name: maskName(r.name || ''),   // 导入即脱敏：C 臂登记本一律带星
    surgery: r.surgery || '',
    runStatus: '良好',
    circu: r.circu || '',
    circuHoName: r.circuHoName || '',
    scrub: (r.scrub && r.scrub !== '无') ? r.scrub : '',
    updatedAt: Date.now()
  });
  saveCArmRows();
  refreshCArm();
  // 直接切换页面（不依赖 tab.click 事件），同时刷新数据
  document.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  const carmTab = document.querySelector('.nav-tab[data-page="carm"]');
  const carmPage = document.getElementById('page-carm');
  if (carmTab) carmTab.classList.add('active');
  if (carmPage) {
    carmPage.classList.add('active');
    _pageIdx.carm = 0; // 重置到第一页
    refreshCArm();
  }
  setTimeout(() => { const tb = document.getElementById('carm-tbody'); tb.firstElementChild?.scrollIntoView({behavior:'smooth', block:'nearest'}); }, 300);
  t('已导入到 C 臂登记本');
  reportAudit('导入C臂(未保存)', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || ''));
}

/** 把登记表的一行导入到感染登记表（仅管理员）
 *  感染类型、手术间号 留空让人工填；人员防护/术后处置两项默认 √；签名 = 巡回+器械护士 */
function importRowToInfection(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = rows[idx];
  if (!r) return;
  if (r.status === 'reviewed' && (!currentUser || currentUser.role !== 'admin')) { t('本行已审核，普通护士不能导入；仅管理员可导入'); return; }
  if (!r.circu) { t('本行未填巡回护士，无法导入'); return; }
  const scrubName = (r.scrub && r.scrub !== '无') ? r.scrub : '';
  const signoff = scrubName ? (r.circu + '/' + scrubName) : r.circu;
  const importDate = r.date || document.getElementById('reg-date')?.value || today();
  // 去重：同一天、同一患者不能重复导入感染登记（避免同一病人因术式手写后缀不同被重复导入）
  const dupKey = (importDate + '|' + (r.admNo || '') + '|' + (r.name || '')).trim();
  const already = infectionRows.some(x => (x.date + '|' + (x.admNo || '') + '|' + (x.name || '')).trim() === dupKey);
  if (already) { t('该患者已导入到感染登记表，不能重复导入'); return; }
  const msg = '确认把这台手术导入到感染登记表？\n\n' +
    '日期：' + importDate + '（登记表日期）\n' +
    '科室：' + (r.dept || '（空）') + '\n' +
    '床号：' + (r.bedNo || '（空）') + '\n' +
    '姓名：' + (r.name || '（空）') + '\n' +
    '住院号：' + (r.admNo || '（空）') + '\n' +
    '感染类型：（留空，手动填）\n' +
    '手术间号：（留空，手动填）\n' +
    '手术名称：' + (r.surgery || '（空）') + '\n' +
    '人员防护：√（默认）\n' +
    '术后处置·空置30分钟：√（默认）\n' +
    '术后处置·擦拭消毒：√（默认）\n' +
    '签名：' + signoff + '（巡回+器械）';
  if (!confirm(msg)) return;
  infectionRows.unshift({
    id: genId(),
    date: importDate,
    dept: r.dept || '',
    bedNo: r.bedNo || '',
    name: maskName(r.name || ''),   // 导入即脱敏：感染登记表一律带星
    admNo: r.admNo || '',
    infectType: '',        // 留空人工填
    roomNo: '',            // 留空人工填
    surgery: r.surgery || '',
    protection: '√',       // 默认打勾
    postop30min: '√',      // 默认打勾
    postopWipe: '√',       // 默认打勾
    signoff: signoff
  });
  saveInfectionRows(); refreshInfection();
  // 直接切换页面 + 刷新
  document.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
  document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
  const infTab = document.querySelector('.nav-tab[data-page="infection"]');
  const infPage = document.getElementById('page-infection');
  if (infTab) infTab.classList.add('active');
  if (infPage) {
    infPage.classList.add('active');
    _pageIdx.infection = 0; // 重置到第一页
    refreshInfection();
  }
  setTimeout(() => { const tb = document.getElementById('infection-tbody'); tb.firstElementChild?.scrollIntoView({behavior:'smooth', block:'nearest'}); }, 300);
  t('已导入到感染登记表');
  reportAudit('导入感染(未保存)', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || ''));
}
// 管理员解锁：清除本行保存锁，交回给操作人员重新编辑
function unlockRow(idx) {
  if (!currentUser || currentUser.role !== 'admin') { t('仅管理员可解锁'); return; }
  const r = rows[idx];
  if (!r) return;
  if (!confirm('解锁本行？解锁后其他操作人员可重新编辑并保存。')) return;
  r.savedBy = ''; r.savedByName = ''; r.savedAt = '';
  touchRow(r);
  saveRows();
  renderLedger();
  t('已解锁本行');
}
function deleteRow(idx) {
  if (!canDelete()) { t('当前权限不允许删除'); return; }
  if (!confirm('删除此条记录？')) return;
  rows.splice(idx,1); saveRows(); renderLedger();
}
function deleteRow(idx) {
  if (!canDelete()) { t('当前权限不允许删除'); return; }
  if (rows[idx].status === 'reviewed' && (!currentUser || currentUser.role !== 'admin')) { t('已审核记录不可删除'); return; }
    if (!canEditRow(rows[idx])) { t('该行已被「' + (rows[idx].savedByName || '他人') + '」保存锁定，无权删除'); return; }
    if (!confirm('删除此条记录？')) return;
    const _adm = rows[idx] ? (rows[idx].admNo || '') : ''; const _nm = rows[idx] ? (rows[idx].name || '') : ''; const _op = rows[idx] ? (rows[idx].surgery || '') : '';
    rows.splice(idx,1); saveRows(); renderLedger();
    reportAudit('删除手术', '' + _adm + ' ' + _nm + ' ' + _op);
  }
function deleteSelectedRows() {
  if (!canDelete()) { t('当前权限不允许删除'); return; }
  const chk = Array.from(document.querySelectorAll('.row-chk:checked'));
  if (!chk.length) { t('请先勾选'); return; }
  const isAdmin = currentUser && currentUser.role === 'admin';
  if (!isAdmin) {
    const reviewed = chk.filter(c => rows[c.dataset.idx]?.status === 'reviewed');
    if (reviewed.length > 0) { t('包含已审核记录，仅管理员可删除'); return; }
    const lockedOther = chk.filter(c => !canEditRow(rows[c.dataset.idx]));
    if (lockedOther.length > 0) { t('选中记录含他人已保存锁定的行，无权删除'); return; }
  }
  if (!confirm('确定删除选中的 ' + chk.length + ' 条记录？')) return;
  const idxs = chk.map(c=>+c.dataset.idx).sort((a,b)=>b-a);
  const delInfo = idxs.map(i=>{ const r=rows[i]; return ''+(r?r.admNo:'')+' '+(r?r.name:'')+' '+(r?r.surgery:''); });
  idxs.forEach(i=>rows.splice(i,1));
  saveRows(); renderLedger(); t('已删除'+idxs.length+'行');
  delInfo.forEach(d=>reportAudit('删除手术', d));
}

/** 审核勾选的记录（仅处理用户手动勾中的行，不再全量处理） */
function reviewSelected() {
  if (!currentUser || currentUser.role !== 'admin') { t('仅管理员可审核'); return; }
  const checked = document.querySelectorAll('.row-chk:checked');
  if (!checked.length) { t('请先勾选需要操作的数据'); return; }
  const now = new Date().toLocaleString('zh-CN', {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
  let cnt = 0;
  const revInfo = [];
  checked.forEach(c => {
    const idx = c.dataset.idx;
    const r = rows[idx];
    if (r && r.status !== 'reviewed') {
      r.status = 'reviewed'; r.reviewedBy = currentUser.name; r.reviewedAt = now; touchRow(r); cnt++;
      revInfo.push(''+(r.admNo||'')+' '+(r.name||'')+' '+(r.surgery||''));
    }
  });
  saveRows(); renderLedger();
  t('已审核 ' + cnt + ' 条');
  revInfo.forEach(d=>reportAudit('审核手术', d));
}

/** 取消审核勾选的记录 */
function unreviewSelected() {
  if (!currentUser || currentUser.role !== 'admin') { t('仅管理员可审核'); return; }
  const checked = document.querySelectorAll('.row-chk:checked');
  if (!checked.length) { t('请先勾选需要操作的数据'); return; }
  let cnt = 0;
  const unrevInfo = [];
  checked.forEach(c => {
    const idx = c.dataset.idx;
    const r = rows[idx];
    if (r && r.status === 'reviewed') {
      r.status = 'pending'; r.reviewedBy = ''; r.reviewedAt = ''; touchRow(r); cnt++;
      unrevInfo.push(''+(r.admNo||'')+' '+(r.name||'')+' '+(r.surgery||''));
    }
  });
  saveRows(); renderLedger();
  t('已取消审核 ' + cnt + ' 条');
  unrevInfo.forEach(d=>reportAudit('取消审核', d));
}

let excelRows = [];      // 解析后的原始行（对象数组，key=表头）
let excelHeaders = [];   // 表头数组
const SYS_FIELDS = [
  {k:'admNo', l:'住院号'},
  {k:'dept',  l:'科室'},      {k:'bedNo', l:'床号'},
  {k:'name',  l:'姓名'},      {k:'sex',   l:'性别'},
  {k:'age',   l:'年龄'},      {k:'surgery',l:'手术名称'}
];

// 手术上传：Excel / CSV 文件 → 浏览器端解析（SheetJS）
function handleExcelUpload(e) {
  // 权限：仅管理员可上传手术（Excel / CSV 批量导入）
  if (!currentUser || currentUser.role !== 'admin') {
    const status = document.getElementById('excel-status');
    if (status) { status.textContent = '❌ 仅管理员可执行手术上传'; status.style.color = '#d33'; }
    e.target.value = '';
    t('仅管理员可上传手术');
    return;
  }
  const file = e.target.files[0];
  if (!file) return;
  const status = document.getElementById('excel-status');
  status.style.color = '';
  if (typeof XLSX === 'undefined') { status.textContent = '❌ 表格解析库未加载，请刷新页面或检查网络'; return; }
  status.textContent = '正在读取「' + file.name + '」...';
  const reader = new FileReader();
  reader.onload = (ev) => {
    try {
      const data = new Uint8Array(ev.target.result);
      const wb = XLSX.read(data, { type: 'array' });
      const ws = wb.Sheets[wb.SheetNames[0]];
      const rowsObj = XLSX.utils.sheet_to_json(ws, { defval: '' });
      if (!rowsObj.length) { status.textContent = '❌ 未解析到数据行（请确认表格有表头和至少一行数据）'; return; }
      excelHeaders = Object.keys(rowsObj[0]);
      excelRows = rowsObj;
      status.textContent = '✅ 已解析 ' + rowsObj.length + ' 行，' + excelHeaders.length + ' 列';
      buildExcelMapping();
      renderExcelPreview();
      document.getElementById('excel-import-btn').style.display = 'inline-block';
      document.getElementById('excel-retry-btn').style.display = 'inline-block';
      document.getElementById('excel-saved-ok').style.display = 'none';
    } catch (err) {
      console.error(err);
      status.textContent = '❌ 解析失败：' + ((err && err.message) || '请确认文件为 .xlsx/.xls/.csv');
    }
  };
  reader.onerror = () => { status.textContent = '❌ 文件读取失败'; };
  reader.readAsArrayBuffer(file);
}

// 根据表头自动推荐 系统字段 ← Excel列 的映射
function buildExcelMapping() {
  const box = document.getElementById('excel-map');
  const list = document.getElementById('excel-map-list');
  list.innerHTML = '';
  SYS_FIELDS.forEach(f => {
    const sel = document.createElement('select');
    sel.id = 'map-' + f.k;
    sel.style.cssText = 'margin-left:6px;padding:4px 6px;border:1px solid var(--border);border-radius:4px;font-size:12px;max-width:200px;';
    const o0 = document.createElement('option'); o0.value = ''; o0.textContent = '（不导入）'; sel.appendChild(o0);
    excelHeaders.forEach(h => { const o = document.createElement('option'); o.value = h; o.textContent = h; sel.appendChild(o); });
    const matched = autoMatchHeader(f);
    if (matched) sel.value = matched;
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;gap:8px;align-items:center;margin:5px 0;font-size:12px;';
    const lbl = document.createElement('span'); lbl.style.cssText = 'width:84px;color:var(--muted);'; lbl.textContent = f.l;
    const arrow = document.createElement('span'); arrow.textContent = '←';
    row.appendChild(lbl); row.appendChild(arrow); row.appendChild(sel);
    list.appendChild(row);
  });
  box.style.display = 'block';
}

function autoMatchHeader(f) {
  const syn = {
    date:['日期','手术日期'], admNo:['病案号','住院号','病历号','登记号'],
    dept:['科室','病区'], bedNo:['床号','床位'],
    name:['姓名','患者','病人'], sex:['性别'], age:['年龄'],
    surgery:['手术名称','术式','手术','手术方式'],
    circu:['巡回','巡回护士'], scrub:['器械','器械护士','洗手','洗手护士'],
    inTime:['入室','入室时间','开始时间'], outTime:['出室','出室时间','结束时间']
  };
  const keys = syn[f.k] || [];
  // 第一轮：按同义词优先级精确匹配（病案号优先于登记号等）
  for (const k of keys) { const h = excelHeaders.find(x => x === k); if (h) return h; }
  // 第二轮：模糊匹配（包含关系）
  for (const k of keys) { const h = excelHeaders.find(x => x.includes(k) || k.includes(x)); if (h) return h; }
  return '';
}

function renderExcelPreview() {
  const box = document.getElementById('excel-preview');
  const head = excelHeaders.map(h => '<th style="padding:5px 8px;border:1px solid var(--border);white-space:nowrap;">' + escapeHtml(h) + '</th>').join('');
  // 不限行数：全部显示，容器自带滚动
  const maxRows = excelRows.length;
  // 找"状态"列识别"撤销"行
  const statusHeader = excelHeaders.find(h => /^(状态|手术状态)$/.test(String(h).trim()));
  const revokeKeywords = ['撤销', '已撤销', '作废', '已作废', '取消', '已取消'];
  let body = '';
  for (let i = 0; i < maxRows; i++) {
    const r = excelRows[i];
    const isRevoke = statusHeader && revokeKeywords.includes(String(r[statusHeader] || '').trim());
    const rowStyle = isRevoke ? ' style="background:#f5f5f5;color:#999;"' : '';
    const chk = isRevoke
      ? '<input type="checkbox" disabled title="已撤销，不会导入">'
      : '<input type="checkbox" class="excel-row-chk" data-idx="' + i + '" checked>';
    body += '<tr' + rowStyle + '><td style="padding:5px 8px;border:1px solid var(--border);text-align:center;">' + chk + '</td>';
    excelHeaders.forEach(h => { let v = r[h]; v = (v === null || v === undefined) ? '' : String(v); body += '<td style="padding:5px 8px;border:1px solid var(--border);white-space:nowrap;">' + escapeHtml(v) + '</td>'; });
    body += '</tr>';
  }
  box.innerHTML =
    '<div style="font-size:12px;margin-bottom:6px;">数据预览（共 ' + maxRows + ' 行，勾选要导入的行，<span style="color:#999;">灰色=已撤销，不会导入</span>）：</div>' +
    '<div style="overflow:auto;max-height:340px;">' +
    '<table style="border-collapse:collapse;width:100%;font-size:12px;"><thead><tr style="background:var(--surface);">' +
    '<th style="padding:5px 8px;border:1px solid var(--border);"><input type="checkbox" id="excel-check-all" checked onclick="excelToggleAll(this)"></th>' + head + '</tr></thead>' +
    '<tbody>' + body + '</tbody></table></div>';
}

function excelToggleAll(chk) {
  document.querySelectorAll('.excel-row-chk').forEach(c => c.checked = chk.checked);
}

function escapeHtml(s) {
  return String(s).replace(/[<>&]/g, c => ({'<':'&lt;','>':'&gt;','&':'&amp;'}[c]));
}

function saveExcelResult() {
  // 权限：仅管理员可上传手术（Excel / CSV 批量导入）
  if (!currentUser || currentUser.role !== 'admin') { t('仅管理员可上传手术'); return; }
  const syncDate = document.getElementById('excel-sync-date') ? (document.getElementById('excel-sync-date').value || today()) : today();
  const map = {};
  SYS_FIELDS.forEach(f => {
    const sel = document.getElementById('map-' + f.k);
    if (sel && sel.value) map[f.k] = sel.value;
  });
  if (!Object.keys(map).length) { t('请先在字段映射中选择至少一个 Excel 列'); return; }
  const checked = Array.from(document.querySelectorAll('.excel-row-chk:checked'));
  if (!checked.length) { t('请先勾选要导入的行'); return; }
  let count = 0, skip = 0, revoke = 0;
  // 找 Excel 里的"状态"列（如「状态」「手术状态」），用于识别"撤销/作废"行
  const statusHeader = excelHeaders.find(h => /^(状态|手术状态)$/.test(String(h).trim()));
  const revokeKeywords = ['撤销', '已撤销', '作废', '已作废', '取消', '已取消'];
  checked.forEach(chk => {
    const i = +chk.dataset.idx;
    const src = excelRows[i];
    if (!src) return;
    // 撤销状态行直接跳过（不入登记表，避免医生撤回的手术仍计分）
    if (statusHeader && revokeKeywords.includes(String(src[statusHeader] || '').trim())) {
      revoke++; return;
    }
    const r = blankRow();
    r.date = syncDate;  // 强制使用「导入日期」作为默认日期，不受登记表当前显示日期影响
    Object.keys(map).forEach(k => {
      let v = src[map[k]];
      v = (v === null || v === undefined) ? '' : String(v).trim();
      if (!v) return;
      if (k === 'admNo') {
        // Excel 数字列会带 ".0"（如 1634327.0），统一去掉转为纯数字字符串
        v = v.replace(/\s+/g, '');
        if (/^\d+\.0$/.test(v)) v = v.slice(0, -2);
        r.admNo = v;
      }
      else if (k === 'age') r.age = v.replace('岁','');
      else if (k === 'inTime') r.inTime = parse4Digit(v);
      else if (k === 'outTime') r.outTime = parse4Digit(v);
      else if (k === 'dept') {
        // 科室格式「科室/床号」拆分：斜杠前→科室，斜杠后→床号
        const slash = v.indexOf('/');
        if (slash > 0) {
          r.dept = v.slice(0, slash).trim();
          const bedPart = v.slice(slash + 1).trim();
          if (bedPart) { if (!r.bedNo) r.bedNo = bedPart; }
        } else {
          r.dept = v;
        }
      }
      else if (k === 'bedNo') { if (!r.bedNo) r.bedNo = v; }
      else if (k === 'name') r.name = maskName(v);   // 患者姓名：导入即脱敏
      else if (k === 'sex') r.sex = v;
      else if (k === 'surgery') r.surgery = v;
      else if (k === 'circu') r.circu = v;
      else if (k === 'scrub') r.scrub = v;
      else if (k === 'date') r.date = v || syncDate;
    });
    r.date = r.date || syncDate;
    r.duration = fmtDur(diffMin(r.inTime, r.outTime));
    // 去重：同日期 + 同住院号 → 跳过（同一患者同一天只保留一条，防 OCR/重复导入产生重复手术）
    const dup = rows.some(ex => ex.date === r.date && ex.admNo && r.admNo && ex.admNo === r.admNo);
    if (dup) { skip++; return; }
    rows.push(r); count++;
  });
  saveRows(); renderLedger();
  document.querySelector('[data-page=ledger]').click();
  t('已导入 ' + count + ' 条' + (skip ? '，跳过 ' + skip + ' 条重复' : '') + (revoke ? '，' + revoke + ' 条已撤销' : '') + '到登记表');
  if (count) reportAudit('导入手术', '从Excel导入 ' + count + ' 条' + (skip ? '，跳过 ' + skip + ' 条重复' : ''));
  document.getElementById('excel-saved-ok').style.display = 'inline-block';
  setTimeout(() => { document.getElementById('excel-saved-ok').style.display = 'none'; }, 2000);
}

/** 重新上传：清空 Excel 解析结果 */
function resetExcel() {
  excelRows = []; excelHeaders = [];
  document.getElementById('excel-preview').innerHTML = '<p style="color:var(--muted);">请先上传 Excel / CSV 文件</p>';
  document.getElementById('excel-map').style.display = 'none';
  document.getElementById('excel-map-list').innerHTML = '';
  document.getElementById('excel-status').textContent = '等待上传';
  document.getElementById('excel-import-btn').style.display = 'none';
  document.getElementById('excel-saved-ok').style.display = 'none';
  document.getElementById('excel-retry-btn').style.display = 'none';
  document.getElementById('excel-input').value = '';
  t('已重置，请重新上传');
}

/** 导出完整登记表（全部字段） */
function exportFullCSV() {
  const heads = ['住院号','科室','床号','姓名','性别','年龄','手术名称','巡回护士','器械护士','入室','出室','耗时','巡回护士分值','器械护士分值'];
  const lines = [heads.join(',')];
  rows.forEach(r => {
    lines.push([r.admNo,r.dept,r.bedNo,r.name,r.sex,r.age,r.surgery,r.circu,r.scrub,
      r.inTime,r.outTime,fmtDur(diffMin(r.inTime,r.outTime)),r.circuScore||'',r.scrubScore||''].join(','));
  });
  downloadCSV('\ufeff'+lines.join('\n'), '手术登记_' + today() + '.csv');
  t('已导出 ' + rows.length + ' 条记录');
}

/* ===========================================================
   模块 3：日统计
   =========================================================== */

// 绩效/时长统计中仍以「普通护士」身份计入的管理员名单：金鑫是管理员，但其时长与分值也要在绩效里显示；其余管理员仍排除（只显示金鑫）
const PERF_INCLUDE_ADMINS = ['金鑫'];
function getAdminNames() {
  return nurses.filter(n => n.systemRole === 'admin' && !PERF_INCLUDE_ADMINS.includes(n.name)).map(n => n.name);
}

// 姓名归一化：去首尾/中间空白、去全角空格，保证 OCR/手输与名单能匹配
function normName(s) {
  if (!s) return '';
  return String(s).replace(/[ 　\t\u00a0]+/g, ' ').trim();
}
// 归一化姓名 -> 标准护士姓名 的映射表
let nurseNameMap = {};
function buildNurseNameMap() {
  nurseNameMap = {};
  nurses.forEach(n => { if (n.name) nurseNameMap[normName(n.name)] = n.name; });
}
// 把数据里的 巡回/器械 字段匹配到标准护士姓名；不是名单内的人返回 null
function matchNurse(s) {
  if (!s) return null;
  return nurseNameMap[normName(s)] || null;
}
// 容错取统计 key：名单内→标准姓名；名单外→归一化姓名（照样统计，避免写法差异漏算）
function keyOfName(n) {
  if (!n) return null;
  const m = matchNurse(n);
  if (m) return m;
  const nm = normName(n);
  return nm || null;
}
// 统计登记表中某个护士姓名出现的次数。支持「巡回/器械」等组合文本。
function countPersonOccurrences(text, name) {
  const source = String(text || '').replace(/\s/g, '');
  const target = String(name || '').replace(/\s/g, '');
  if (!source || !target || target === '无') return 0;
  let count = 0, pos = 0;
  while ((pos = source.indexOf(target, pos)) !== -1) {
    count++;
    pos += target.length;
  }
  return count;
}
// 只把已保存的 C 臂/感染登记计入绩效；日期范围与绩效页筛选一致。
function collectExtraPerfScores(ms, adminNames) {
  const extra = {};
  const add = (name, field, hits) => {
    const k = keyOfName(name);
    if (!k || adminNames.includes(k) || !hits) return;
    if (!extra[k]) extra[k] = { carm: 0, infection: 0 };
    extra[k][field] += hits;
  };
  const savedCArm = (cArmRows || []).filter(r => r.savedBy && (!ms._sd || (r.date || '') >= ms._sd) && (!ms._ed || (r.date || '') <= ms._ed));
  const savedInfection = (infectionRows || []).filter(r => r.savedBy && (!ms._sd || (r.date || '') >= ms._sd) && (!ms._ed || (r.date || '') <= ms._ed));
  nurses.forEach(n => {
    const name = n.name;
    savedCArm.forEach(r => {
      add(name, 'carm', countPersonOccurrences(r.circu, name) + countPersonOccurrences(r.scrub, name));
    });
    savedInfection.forEach(r => {
      add(name, 'infection', countPersonOccurrences(r.signoff, name));
    });
  });
  return extra;
}

function refreshDaily() {
  const date = document.getElementById('daily-date').value || today();
  document.getElementById('daily-date').value = date;
  const ds = rows.filter(r => r.date === date && r.status !== 'pending');
  const adminNames = getAdminNames();
  const tot = ds.length;
  const totalMin = ds.reduce((s,r)=>s+diffMin(r.inTime,r.outTime),0);
  const avgMin = tot ? Math.round(totalMin/tot) : 0;

  // 统计卡片（含主岗 + 交接分支分值）
  const dayScore = ds.reduce((s,r)=>s+parseScore(r.circuScore)+parseScore(r.scrubScore)+parseScore(r.circuHoScore)+parseScore(r.scrubHoScore),0);
  document.getElementById('daily-cards').innerHTML = `
    <div class="stat-card accent"><div class="value">${tot}</div><div class="label">手术总数（台）</div></div>
    <div class="stat-card"><div class="value">${fmtDur(totalMin)||'0'}</div><div class="label">总耗时</div></div>
    <div class="stat-card"><div class="value">${fmtDur(avgMin)||'0'}</div><div class="label">平均耗时</div></div>
    <div class="stat-card"><div class="value">${dayScore}</div><div class="label">当日绩效总分</div></div>
  `;

  // 人员手术台数排名 - 可视化表格（所有人员，排除管理员）
  // 统计规则：姓名在「巡回护士」出现几次 = 巡回台数；在「器械护士」出现几次 = 器械台数；相加 = 总台数
  // keyOfName：名单内的归并到标准姓名；名单外的用归一化姓名照常统计（避免写法差异导致漏算）
  // 双岗分离聚合：巡回 / 器械 各自累计分值与场次（适配护士轮岗，同一人可互换岗位）
  // 含主岗 + 交接分支：交接护士的分值记到对应人员的"巡回/器械"账户上
  const cCount = {}, sCount = {}, cScore = {}, sScore = {};
  ds.forEach(r => {
    const c = keyOfName(r.circu);
    const s = keyOfName(r.scrub);
    if (c && !adminNames.includes(c)) { cCount[c] = (cCount[c]||0)+1; cScore[c] = (cScore[c]||0)+parseScore(r.circuScore); }            // 主岗巡回
    if (s && !adminNames.includes(s) && r.scrub !== '无') { sCount[s] = (sCount[s]||0)+1; sScore[s] = (sScore[s]||0)+parseScore(r.scrubScore); } // 主岗器械
    // 交接分支：交接护士的场次 + 分值独立计入其本人的巡回/器械账户（与主岗护士分账，台数统计与绩效分值保持一致）
    const ch = keyOfName(r.circuHoName);
    if (ch && !adminNames.includes(ch)) { cCount[ch] = (cCount[ch]||0)+1; cScore[ch] = (cScore[ch]||0)+parseScore(r.circuHoScore); }
    const sh = keyOfName(r.scrubHoName);
    if (sh && !adminNames.includes(sh) && r.scrubHoName !== '无') { sCount[sh] = (sCount[sh]||0)+1; sScore[sh] = (sScore[sh]||0)+parseScore(r.scrubHoScore); }
  });
  // 名单内所有人 + 数据中实际出现过的名字（排除管理员），按拼音排序
  const seenNames = new Set();
  Object.keys(cCount).concat(Object.keys(sCount)).forEach(n => { if (!adminNames.includes(n)) seenNames.add(n); });
  const allPerson = [...new Set([...nurses.filter(n => !adminNames.includes(n.name)).map(n => n.name), ...seenNames])].sort((a,b) => a.localeCompare(b, 'zh'));
  const personRows = allPerson.map(name => {
    const cc = cCount[name] || 0, sc = sCount[name] || 0;
    const cs = cScore[name] || 0, ss = sScore[name] || 0;
    return { name, cCount: cc, sCount: sc, cScore: cs, sScore: ss, totalScore: cs+ss, totalCount: cc+sc };
  });
  const maxScore = Math.max(1, ...personRows.map(p => p.totalScore));
  let phtml = '<thead><tr><th style="width:36px;">排名</th><th>姓名</th><th>巡回分值</th><th>巡回场次</th><th>器械分值</th><th>器械场次</th><th style="min-width:130px;">合计分值</th><th>合计场次</th></tr></thead><tbody>';
  personRows.sort((a,b)=>b.totalScore-a.totalScore).forEach((p, i) => {
    const isZero = p.totalScore === 0;
    const zclass = isZero ? ' class="zero-row"' : '';
    const rankCls = i < 4 ? ' r'+(i+1) : '';
    const w = isZero ? 100 : Math.max(8, Math.round(p.totalScore / maxScore * 100));
    const barCls = isZero ? 'zero-bar' : 'score-bar';
    phtml += `<tr${zclass}><td><span class="rank${rankCls}">${i+1}</span></td><td class="name">${p.name}</td><td class="num">${p.cScore}</td><td class="num">${p.cCount}</td><td class="num">${p.sScore}</td><td class="num">${p.sCount}</td><td class="bar-cell"><span class="bar-bg ${barCls}" style="width:${w}%;">${p.totalScore}</span></td><td class="num">${p.totalCount}</td></tr>`;
  });
  const sumCS = personRows.reduce((s,p)=>s+p.cScore,0), sumCC = personRows.reduce((s,p)=>s+p.cCount,0);
  const sumSS = personRows.reduce((s,p)=>s+p.sScore,0), sumSC = personRows.reduce((s,p)=>s+p.sCount,0);
  const sumTS = personRows.reduce((s,p)=>s+p.totalScore,0), sumTC = personRows.reduce((s,p)=>s+p.totalCount,0);
  phtml += `</tbody><tfoot><tr class="total-row"><td colspan="2" style="text-align:right;">合计</td><td class="num">${sumCS}</td><td class="num">${sumCC}</td><td class="num">${sumSS}</td><td class="num">${sumSC}</td><td class="num">${sumTS}</td><td class="num">${sumTC}</td></tr></tfoot>`;
  document.getElementById('daily-person-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${phtml}</table></div>`;


  // 科室分布 - 可视化表格（按归一化科室聚合，下辖床号合并）
  const dmap = {};
  ds.forEach(r => { const d = normDept(r.dept); dmap[d] = (dmap[d] || 0) + 1; });
  const dlist = Object.entries(dmap).sort((a,b) => b[1] - a[1]);
  const maxDept = Math.max(1, ...dlist.map(d => d[1]));
  if (dlist.length) {
    let dhtml = '<thead><tr><th style="width:36px;">排名</th><th>科室</th><th style="min-width:160px;">台数</th></tr></thead><tbody>';
    dlist.forEach((d, i) => {
      const w = Math.max(8, Math.round(d[1] / maxDept * 100));
      dhtml += `<tr><td><span class="rank r${i<4?i+1:0}">${i+1}</span></td><td class="name">${d[0]}</td><td class="bar-cell"><span class="bar-bg count-bar" style="width:${w}%;">${d[1]}</span></td></tr>`;
    });
    dhtml += '</tbody>';
    document.getElementById('daily-dept-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${dhtml}</table></div>`;
  } else {
    document.getElementById('daily-dept-table').innerHTML = '<div class="empty">暂无数据</div>';
  }

  // 人员单日总时长 - 可视化表格（容错匹配，有交接时自动分段）
  const tmap = {};
  ds.forEach(r => {
    const stC = splitTime(r, 'circu');
    const stS = splitTime(r, 'scrub');
    const c = keyOfName(r.circu), s = keyOfName(r.scrub);
    if (c && !adminNames.includes(c)) tmap[c] = (tmap[c]||0) + stC.main;
    if (stC.hoName) { const h = keyOfName(stC.hoName); if (h && !adminNames.includes(h)) tmap[h] = (tmap[h]||0) + stC.hoDur; }
    if (s && !adminNames.includes(s)) tmap[s] = (tmap[s]||0) + stS.main;
    if (stS.hoName) { const h = keyOfName(stS.hoName); if (h && !adminNames.includes(h)) tmap[h] = (tmap[h]||0) + stS.hoDur; }
  });
  const tRows = allPerson.map(name => ({ name, min: tmap[name] || 0 })).sort((a,b) => b.min - a.min);
  const maxT = Math.max(1, ...tRows.map(p => p.min));
  let thtml = '<thead><tr><th style="width:36px;">排名</th><th>姓名</th><th style="min-width:180px;">总时长</th></tr></thead><tbody>';
  tRows.forEach((t, i) => {
    const isZero = t.min === 0;
    const zclass = isZero ? ' class="zero-row"' : '';
    const rankCls = i < 4 ? ' r'+(i+1) : '';
    const w = isZero ? 100 : Math.max(8, Math.round(t.min / maxT * 100));
    const barCls = isZero ? 'zero-bar' : 'time-bar';
    thtml += `<tr${zclass}><td><span class="rank${rankCls}">${i+1}</span></td><td class="name">${t.name}</td><td class="bar-cell"><span class="bar-bg ${barCls}" style="width:${w}%;">${fmtDur(t.min)||'0'}</span></td></tr>`;
  });
  thtml += '</tbody>';
  document.getElementById('daily-duration-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${thtml}</table></div>`;
}



function renderDailyTable(date) {
  date = date || document.getElementById('daily-date').value || today();
  const search = (document.getElementById('daily-search')?.value || '').toLowerCase();
  let ds = rows.filter(r => r.date === date && r.status !== 'pending');
  if (search) ds = ds.filter(r => (r.name||'').toLowerCase().includes(search) || (r.admNo||'').toLowerCase().includes(search) || (r.surgery||'').toLowerCase().includes(search));
  ds.sort((a,b) => (a.inTime||'').localeCompare(b.inTime||''));

  const hd = ['序号','住院号','科室','床号','姓名','性别','年龄','手术名称','巡回','器械','入室','出室','耗时','巡回分','器械分'];
  let html = '<thead><tr>'+hd.map(h=>`<th>${h}</th>`).join('')+'</tr></thead><tbody>';
  if (!ds.length) html += '<tr><td colspan="'+hd.length+'" style="padding:24px; color:var(--muted);">当日无已审核手术记录</td></tr>';
  else {
    ds.forEach((r,i) => {
      const dur = fmtDur(diffMin(r.inTime, r.outTime));
      const style = isOver(r.outTime) ? ' style="color:#d32f2f;font-weight:500;"' : '';
      html += `<tr${isOver(r.outTime)?' style="background:#ffebee;"':''}>` +
        `<td>${i+1}</td><td>${r.admNo||''}</td><td>${r.dept||''}</td><td>${r.bedNo||''}</td>` +
        `<td>${r.name||''}</td><td>${r.sex||''}</td><td>${r.age||''}</td><td>${r.surgery||''}</td>` +
        `<td>${r.circu||''}</td><td>${r.scrub||''}</td><td>${r.inTime||''}</td><td${style}>${r.outTime||''}</td>` +
        `<td>${dur}</td><td>${r.circuScore||''}</td><td>${r.scrubScore||''}</td></tr>`;
    });
  }
  html += '</tbody>';
  document.getElementById('daily-table').innerHTML = html;
}
exportDaily = () => {
  const date = document.getElementById('daily-date').value || today();
  const ds = rows.filter(r => r.date === date && r.status !== 'pending').sort((a,b)=>(a.inTime||'').localeCompare(b.inTime||''));
  const hd = ['序号','住院号','科室','床号','姓名','性别','年龄','手术名称','巡回','器械','入室','出室','耗时','巡回分','器械分'];
  let csv = hd.join(',') + '\n';
  ds.forEach((r,i) => {
    csv += [i+1, r.admNo, r.dept, r.bedNo, r.name, r.sex, r.age, r.surgery, r.circu, r.scrub, r.inTime, r.outTime, fmtDur(diffMin(r.inTime,r.outTime)), r.circuScore||'', r.scrubScore||''].join(',') + '\n';
  });
  downloadCSV('\ufeff' + csv, '日统计_' + date + '.csv');
  t('已导出 ' + ds.length + ' 条');
};

/* ===========================================================
   模块 4：月统计
   =========================================================== */
function refreshMonthly() {
  const sd = document.getElementById('monthly-start').value;
  const ed = document.getElementById('monthly-end').value;
  const personFilter = document.getElementById('monthly-person').value;
  const ms = rows.filter(r => r.status !== 'pending'
    && (!sd || (r.date||'') >= sd) && (!ed || (r.date||'') <= ed));
  const tot = ms.length;
  const avg = tot ? Math.round(ms.reduce((s,r)=>s+diffMin(r.inTime,r.outTime),0)/tot) : 0;

  // 按人员聚合（排除管理员，按标准姓名匹配）
  const pmap = {}, adminNames = getAdminNames();
  ms.forEach(r => {
    const stC = splitTime(r, 'circu');
    const stS = splitTime(r, 'scrub');
    const c = keyOfName(r.circu);
    if (c && !adminNames.includes(c) && (!personFilter || c === personFilter)) {
      if (!pmap[c]) pmap[c] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
      pmap[c].cCount++; pmap[c].totalMin += stC.main; pmap[c].cScore += parseScore(r.circuScore);   // 主岗巡回
    }
    if (stC.hoName) { // 巡回交接人时长
      const h = keyOfName(stC.hoName);
      if (h && !adminNames.includes(h) && (!personFilter || h === personFilter)) {
        if (!pmap[h]) pmap[h] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
        pmap[h].totalMin += stC.hoDur;
      }
    }
    const sName = keyOfName(r.scrub);
    if (sName && !adminNames.includes(sName) && r.scrub !== '无' && (!personFilter || sName === personFilter)) {
      if (!pmap[sName]) pmap[sName] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
      pmap[sName].sCount++; pmap[sName].totalMin += stS.main; pmap[sName].sScore += parseScore(r.scrubScore); // 主岗器械
    }
    if (stS.hoName) { // 器械交接人时长
      const h = keyOfName(stS.hoName);
      if (h && !adminNames.includes(h) && r.scrubHoName !== '无' && (!personFilter || h === personFilter)) {
        if (!pmap[h]) pmap[h] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
        pmap[h].totalMin += stS.hoDur;
      }
    }
    // 交接分支分值：交接护士的场次 + 分值独立计入其本人的巡回/器械账户（台数统计与绩效分值保持一致）
    const ch = keyOfName(r.circuHoName);
    if (ch && !adminNames.includes(ch) && (!personFilter || ch === personFilter)) {
      if (!pmap[ch]) pmap[ch] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
      pmap[ch].cCount++; pmap[ch].cScore += parseScore(r.circuHoScore);
    }
    const sh = keyOfName(r.scrubHoName);
    if (sh && !adminNames.includes(sh) && r.scrubHoName !== '无' && (!personFilter || sh === personFilter)) {
      if (!pmap[sh]) pmap[sh] = { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
      pmap[sh].sCount++; pmap[sh].sScore += parseScore(r.scrubHoScore);
    }
  });

  // 全部录入人员（排除管理员）
  const allPerson = nurses.filter(n => !adminNames.includes(n.name)).map(n => n.name);
  const pRows = allPerson.map(name => {
    const v = pmap[name] || { cCount:0, sCount:0, totalMin:0, cScore:0, sScore:0 };
    return { name, cCount: v.cCount, sCount: v.sCount, totalMin: v.totalMin, cScore: v.cScore, sScore: v.sScore, totalScore: v.cScore+v.sScore, totalCount: v.cCount+v.sCount };
  }).sort((a,b) => b.totalScore - a.totalScore);


  const totalAllMin = ms.reduce((s,r)=>s+diffMin(r.inTime,r.outTime),0);

  document.getElementById('monthly-cards').innerHTML = `
    <div class="stat-card accent"><div class="value">${tot}</div><div class="label">区间手术总数（台）</div></div>
    <div class="stat-card"><div class="value">${pRows.filter(p=>p.totalCount>0).length}</div><div class="label">参与人数</div></div>
    <div class="stat-card"><div class="value">${pRows.reduce((s,p)=>s+p.totalScore,0)}</div><div class="label">区间绩效总分</div></div>
    <div class="stat-card"><div class="value">${fmtDur(avg)||'0'}</div><div class="label">平均耗时</div></div>
    <div class="stat-card"><div class="value">${fmtDur(totalAllMin)||'0'}</div><div class="label">区间总耗时</div></div>
  `;

  // 人员累计时长排名 - 可视化表格
  const maxScore = Math.max(1, ...pRows.map(p => p.totalScore));
  let phtml = '<thead><tr><th style="width:36px;">排名</th><th>姓名</th><th>巡回分值</th><th>巡回场次</th><th>器械分值</th><th>器械场次</th><th style="min-width:130px;">合计分值</th><th>累计时长</th><th>合计场次</th></tr></thead><tbody>';
  pRows.forEach((p, i) => {
    const isZero = p.totalScore === 0;
    const zclass = isZero ? ' class="zero-row"' : '';
    const rankCls = i < 4 ? ' r'+(i+1) : '';
    const w = isZero ? 100 : Math.max(8, Math.round(p.totalScore / maxScore * 100));
    const barCls = isZero ? 'zero-bar' : 'score-bar';
    phtml += `<tr${zclass}><td><span class="rank${rankCls}">${i+1}</span></td><td class="name">${p.name}</td><td class="num">${p.cScore}</td><td class="num">${p.cCount}</td><td class="num">${p.sScore}</td><td class="num">${p.sCount}</td><td class="bar-cell"><span class="bar-bg ${barCls}" style="width:${w}%;">${p.totalScore}</span></td><td class="num">${fmtDur(p.totalMin)||'0'}</td><td class="num">${p.totalCount}</td></tr>`;
  });
  const gCS = pRows.reduce((s,p)=>s+p.cScore,0), gCC = pRows.reduce((s,p)=>s+p.cCount,0);
  const gSS = pRows.reduce((s,p)=>s+p.sScore,0), gSC = pRows.reduce((s,p)=>s+p.sCount,0);
  const gTS = pRows.reduce((s,p)=>s+p.totalScore,0), gTC = pRows.reduce((s,p)=>s+p.totalCount,0), gMin = pRows.reduce((s,p)=>s+p.totalMin,0);
  phtml += `</tbody><tfoot><tr class="total-row"><td colspan="2" style="text-align:right;">合计</td><td class="num">${gCS}</td><td class="num">${gCC}</td><td class="num">${gSS}</td><td class="num">${gSC}</td><td class="num">${gTS}</td><td class="num">${fmtDur(gMin)||'0'}</td><td class="num">${gTC}</td></tr></tfoot>`;
  document.getElementById('monthly-person-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${phtml}</table></div>`;


  // 每日手术台数分布 - 可视化表格（按区间内实际日期聚合）
  const dayMap = {};
  ms.forEach(r => { const dt = r.date || ''; if (dt) dayMap[dt] = (dayMap[dt]||0) + 1; });
  const dayRows = Object.entries(dayMap).map(([day, count]) => ({ day, count })).sort((a,b) => b.count - a.count);
  const maxD = Math.max(1, ...dayRows.map(d => d.count));
  let dhtml = '<thead><tr><th style="width:36px;">排名</th><th>日期</th><th style="min-width:160px;">台数</th></tr></thead><tbody>';
  if (!dayRows.length) dhtml += '<tr><td colspan="3" style="padding:24px;color:var(--muted);">暂无数据</td></tr>';
  dayRows.forEach((d, i) => {
    const rankCls = i < 4 ? ' r'+(i+1) : '';
    const w = Math.max(8, Math.round(d.count / maxD * 100));
    dhtml += `<tr><td><span class="rank${rankCls}">${i+1}</span></td><td class="name">${d.day}</td><td class="bar-cell"><span class="bar-bg count-bar" style="width:${w}%;">${d.count}</span></td></tr>`;
  });
  dhtml += '</tbody>';
  document.getElementById('monthly-daily-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${dhtml}</table></div>`;

  // 科室手术台数分布 - 可视化表格（按归一化科室聚合，下辖床号合并）
  const dmap2 = {};
  ms.forEach(r => { const d = normDept(r.dept); dmap2[d] = (dmap2[d] || 0) + 1; });
  const dlist2 = Object.entries(dmap2).sort((a,b) => b[1] - a[1]);
  if (dlist2.length) {
    const maxD2 = Math.max(1, ...dlist2.map(d => d[1]));
    let d2html = '<thead><tr><th style="width:36px;">排名</th><th>科室</th><th style="min-width:160px;">台数</th></tr></thead><tbody>';
    dlist2.forEach((d, i) => {
      const w = Math.max(8, Math.round(d[1] / maxD2 * 100));
      d2html += `<tr><td><span class="rank r${i<4?i+1:0}">${i+1}</span></td><td class="name">${d[0]}</td><td class="bar-cell"><span class="bar-bg count-bar" style="width:${w}%;">${d[1]}</span></td></tr>`;
    });
    d2html += '</tbody>';
    document.getElementById('monthly-dept-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${d2html}</table></div>`;
  } else {
    document.getElementById('monthly-dept-table').innerHTML = '<div class="empty">暂无数据</div>';
  }

  // 明细表
  let thtml = '<thead><tr><th>排名</th><th>姓名</th><th>巡回分值</th><th>巡回场次</th><th>器械分值</th><th>器械场次</th><th>合计分值</th><th>累计时长</th><th>合计场次</th></tr></thead><tbody>';
  if (!pRows.filter(p=>p.totalCount>0).length) thtml += '<tr><td colspan="9" style="padding:24px;color:var(--muted);">暂无数据</td></tr>';
  else pRows.filter(p=>p.totalCount>0).forEach((p,i) => { thtml += `<tr><td>${i+1}</td><td>${p.name}</td><td>${p.cScore}</td><td>${p.cCount}</td><td>${p.sScore}</td><td>${p.sCount}</td><td style="font-weight:500;">${p.totalScore}</td><td>${fmtDur(p.totalMin)}</td><td>${p.totalCount}</td></tr>`; });
  thtml += '</tbody>';
  document.getElementById('monthly-table').innerHTML = thtml;

}

function exportMonthly() {
  const sd = document.getElementById('monthly-start').value;
  const ed = document.getElementById('monthly-end').value;
  const ms = rows.filter(r => r.status !== 'pending'
    && (!sd || (r.date||'') >= sd) && (!ed || (r.date||'') <= ed));
  const lines = ['序号,住院号,科室,床号,姓名,性别,年龄,手术名称,巡回,器械,入室,出室,耗时,巡回分,器械分'];
  ms.forEach((r,i) => {
    lines.push([i+1,r.admNo,r.dept,r.bedNo,r.name,r.sex,r.age,r.surgery,r.circu,r.scrub,r.inTime,r.outTime,fmtDur(diffMin(r.inTime,r.outTime)),r.circuScore||'',r.scrubScore||''].join(','));
  });
  downloadCSV('\ufeff'+lines.join('\n'), '统计_'+(sd||'起')+'至'+(ed||'今')+'.csv');
  t('已导出');
}

/* ===========================================================
   模块 5：绩效分值
   =========================================================== */
function refreshPerf() {
  const sd = document.getElementById('perf-start').value;
  const ed = document.getElementById('perf-end').value;
  const ms = rows.filter(r => r.status !== 'pending'
    && (!sd || (r.date||'') >= sd) && (!ed || (r.date||'') <= ed));
  ms._sd = sd; ms._ed = ed;

  // 按人员汇总巡回分值 / 器械分值 / 累计时长（排除管理员，含主岗 + 交接分支）
  const pmap = {}, adminNames = getAdminNames();
  ms.forEach(r => {
    const stC = splitTime(r, 'circu');
    const stS = splitTime(r, 'scrub');
    [{n:r.circu, s:r.circuScore, t:'巡回', dur: stC.main},
     {n:r.scrub, s:r.scrubScore, t:'器械', dur: stS.main},
     {n:r.circuHoName, s:r.circuHoScore, t:'巡回', dur: stC.hoDur},
     {n:r.scrubHoName, s:r.scrubHoScore, t:'器械', dur: stS.hoDur}].forEach(({n,s,t,dur}) => {
      if (!n || n === '无' || adminNames.includes(n)) return;
      const name = keyOfName(n);
      if (!name || adminNames.includes(name)) return;
      if (!pmap[name]) pmap[name] = { circu:0, scrub:0, count:0, carm:0, infection:0, totalMin:0 };
      if (t==='巡回') { pmap[name].circu += parseScore(s); pmap[name].count++; }
      if (t==='器械') { pmap[name].scrub += parseScore(s); pmap[name].count++; }
      pmap[name].totalMin += (+dur||0);
    });
  });
  // C 臂：已保存记录中，姓名每出现一次计 5 分；感染：已保存记录中，签名姓名每出现一次计 1 分。
  const extra = collectExtraPerfScores(ms, adminNames);
  Object.keys(extra).forEach(name => {
    if (!pmap[name]) pmap[name] = { circu:0, scrub:0, count:0, carm:0, infection:0 };
    pmap[name].carm = extra[name].carm * 5;
    pmap[name].infection = extra[name].infection;
  });

  const allPerson = nurses.filter(n => !adminNames.includes(n.name)).map(n => n.name);
  const plist = allPerson.map(name => {
    const v = pmap[name] || { circu:0, scrub:0, count:0, carm:0, infection:0, totalMin:0 };
    return { name, circu: v.circu, scrub: v.scrub, carm: v.carm || 0, infection: v.infection || 0, count: v.count, totalMin: v.totalMin || 0, total: v.circu + v.scrub + (v.carm || 0) + (v.infection || 0) };
  });
  plist.sort((a,b) => b.total - a.total);

  const totScore = plist.reduce((s,p)=>s+p.total,0);
  const activeCount = plist.filter(p=>p.total>0).length;

  document.getElementById('perf-cards').innerHTML = `
    <div class="stat-card accent"><div class="value">${activeCount}</div><div class="label">参与考核人数</div></div>
    <div class="stat-card"><div class="value">${totScore}</div><div class="label">区间绩效总分</div></div>
    <div class="stat-card"><div class="value">${activeCount?Math.round(totScore/activeCount):0}</div><div class="label">人均分值</div></div>
  `;

  // 绩效分值排名
  const maxTotal = Math.max(1, ...plist.map(p => p.total));
  let rhtml = '<thead><tr><th style="width:36px;">排名</th><th>姓名</th><th>巡回分值</th><th>器械分值</th><th>C臂分值</th><th>感染分值</th><th>台数</th><th>累计时长</th><th style="min-width:180px;">总分</th></tr></thead><tbody>';
  plist.forEach((p, i) => {
    const isZero = p.total === 0;
    const zclass = isZero ? ' class="zero-row"' : '';
    const rankCls = i < 4 ? ' r'+(i+1) : '';
    const w = Math.max(8, Math.round(p.total / maxTotal * 100));
    const barCls = isZero ? ' zero-bar' : ' score-bar';
    rhtml += `<tr${zclass}><td><span class="rank${rankCls}">${i+1}</span></td><td class="name">${p.name}</td><td class="num">${p.circu}</td><td class="num">${p.scrub}</td><td class="num">${p.carm}</td><td class="num">${p.infection}</td><td class="num">${p.count}</td><td class="num">${fmtDur(p.totalMin)||'0'}</td><td class="bar-cell"><span class="bar-bg ${barCls}" style="width:${w}%;">${p.total}</span></td></tr>`;
  });
  const gCS = plist.reduce((s,p)=>s+p.circu,0), gSS = plist.reduce((s,p)=>s+p.scrub,0);
  const gCA = plist.reduce((s,p)=>s+p.carm,0), gIF = plist.reduce((s,p)=>s+p.infection,0);
  const gC = plist.reduce((s,p)=>s+p.count,0), gMin = plist.reduce((s,p)=>s+p.totalMin,0), gT = plist.reduce((s,p)=>s+p.total,0);
  rhtml += `</tbody><tfoot><tr class="total-row"><td colspan="2" style="text-align:right;">合计</td><td class="num">${gCS}</td><td class="num">${gSS}</td><td class="num">${gCA}</td><td class="num">${gIF}</td><td class="num">${gC}</td><td class="num">${fmtDur(gMin)||'0'}</td><td class="num">${gT}</td></tr></tfoot>`;
  document.getElementById('perf-rank-table').innerHTML = `<div class="viz-table-scroll"><table class="viz-table">${rhtml}</table></div>`;


  let tb = document.getElementById('perf-tbody');
  if (!activeCount) tb.innerHTML = '<tr><td colspan="9" style="padding:24px;color:var(--muted);">暂无数据</td></tr>';
  else tb.innerHTML = plist.filter(p=>p.total>0).map((p,i)=>
    `<tr><td>${i+1}</td><td>${p.name}</td><td>${p.circu}</td><td>${p.scrub}</td><td>${p.carm}</td><td>${p.infection}</td><td>${p.count}</td><td>${fmtDur(p.totalMin)||'0'}</td><td style="font-weight:500;">${p.total}</td></tr>`
  ).join('');
}

function exportPerf() {
  const sd = document.getElementById('perf-start').value;
  const ed = document.getElementById('perf-end').value;
  const lines = ['排名,姓名,巡回分值,器械分值,C臂分值,感染分值,台数,累计时长,总分'];
  const tb = document.getElementById('perf-tbody');
  tb.querySelectorAll('tr').forEach(tr => {
    if (tr.children.length < 6) return;
    lines.push(Array.from(tr.children).map(td=>td.textContent).join(','));
  });
  downloadCSV('\ufeff'+lines.join('\n'), '绩效分值_'+(sd||'起')+'至'+(ed||'今')+'.csv');
  t('已导出');
}

/* ===========================================================
   模块 7：C 臂使用登记本
   =========================================================== */
/** 判断当前用户是否能编辑/保存/取消保存某条 C 臂记录：管理员、本行巡回护士、交接巡回护士 */
function canEditCArmRow(r) {
  if (!currentUser) return false;
  if (currentUser.role === 'admin') return true;
  const me = currentUser.name || currentUser.loginKey;
  if (!me) return false;
  // 兼容 circu 字段内多个人名（空格/斜杠/逗号分隔）的历史数据
  const circuNames = String(r.circu || '').split(/[\s\/\\,，]+/).filter(Boolean);
  if (circuNames.includes(me)) return true;
  if (r.circuHoName === me) return true;
  return false;
}
function blankCArmRow() {
  return { id: genId(), date: today(), dept: '', bedNo: '', admNo: '', name: '', surgery: '', runStatus: '', circu: '', circuHoName: '', scrub: '', savedBy: '', savedByName: '', savedAt: '' };
}
function refreshCArm() {
  const sd = document.getElementById('carm-start').value;
  const ed = document.getElementById('carm-end').value;
  if (!document.getElementById('carm-start').value) document.getElementById('carm-start').value = sdOfMonth();
  if (!document.getElementById('carm-end').value) document.getElementById('carm-end').value = today();
  const tb = document.getElementById('carm-tbody');
  // C 臂登记是全员可登记的纸面账本：登录用户都能增删改
  const canEdit = !!currentUser;
  document.getElementById('carm-act-col').style.display = canEdit ? '' : 'none';
  const start = document.getElementById('carm-start').value;
  const end = document.getElementById('carm-end').value;
  const list = (cArmRows || []).filter(r => (!start || r.date >= start) && (!end || r.date <= end));
  list.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
  tb.innerHTML = '';
  if (!list.length) {
    tb.innerHTML = '<tr><td colspan="10" style="padding:24px;color:var(--muted);text-align:center;">暂无数据，点上方「新增一行」开始登记</td></tr>';
    return;
  }
  const pageIdx = _pageIdx.carm;
  const totalPages = Math.max(1, Math.ceil(list.length / PAGE_SIZE));
  list.forEach((r, idx) => {
    const realIdx = cArmRows.indexOf(r);
    const tr = document.createElement('tr');
    if (r.savedBy) tr.style.background = '#e3f2fd'; // 已保存行 → 蓝色
    // 屏幕分页：超出当前页的行隐藏
    if (Math.floor(idx / PAGE_SIZE) !== pageIdx) tr.classList.add('pg-hidden');
    // 打印分页：每 15 行末尾强制换页（确保打印完整）
    if (idx > 0 && idx % PAGE_SIZE === 0) tr.classList.add('break-page');
    // 行编辑权限：未保存=都可编辑；已保存=仅管理员或本行巡回/交接巡回护士可编辑
    const canEditRow = !r.savedBy || canEditCArmRow(r);
    // 删除权限：仅管理员 或 本行的巡回/交接巡回护士 可删
    const canDelete = currentUser && (currentUser.role === 'admin' || canEditCArmRow(r));
    const actHTML = canEdit
      ? `<button class="btn" style="padding:1px 8px;font-size:11px;${r.savedBy ? 'background:#fff3e0;color:#e65100;border:1px solid #ffcc80;' : ''}" onclick="${r.savedBy ? `unsaveCArmRow(${realIdx})` : `saveCArmRow(${realIdx})`}">${r.savedBy ? '⏸ 取消保存' : '保存'}</button>`
        + (canDelete ? `<div class="btn danger" style="padding:1px 6px;font-size:11px;margin-top:2px;display:inline-block;cursor:pointer;border:1px solid #e57373;color:#c0392b;background:#fff;border-radius:3px;" onclick="deleteCArmRow(${realIdx})">删</div>` : '')
        + (r.savedBy ? `<div style="font-size:11px;color:#1d6e3c;margin-top:2px;">✔ ${r.savedByName || '已保存'}</div>` : '')
      : '';
    tr.innerHTML = `
      <td>${fieldCArmCell(r.date, 'date', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.dept, 'dept', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.bedNo, 'bedNo', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.admNo, 'admNo', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.name, 'name', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.surgery, 'surgery', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.runStatus, 'runStatus', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.circu, 'circu', realIdx, canEditRow)}</td>
      <td>${fieldCArmCell(r.scrub, 'scrub', realIdx, canEditRow)}</td>
      <td style="text-align:center;white-space:nowrap;">${actHTML}</td>
    `;
    tb.appendChild(tr);
  });
  renderPager(document.getElementById('carm-pager'), list.length, 'carm');
}
window.carm_goPrev = () => { _pageIdx.carm--; refreshCArm(); };
window.carm_goNext = () => { _pageIdx.carm++; refreshCArm(); };
function fieldCArmCell(val, key, realIdx, isAdmin) {
  const v = (val || '').replace(/</g, '&lt;');
  if (!isAdmin) return v || '<span style="color:#ccc;">—</span>';
  // 病人姓名：失焦时脱敏（oninput 实时脱敏会吃掉输入过程中的中间字）
  const _blur = (key === 'name')
    ? `maskNameBlur(this,'carm',${realIdx})`
    : `this.style.background='transparent'`;
  return `<input type="text" data-k="${key}" data-i="${realIdx}" value="${v}" style="width:100%;padding:2px 4px;border:none;background:transparent;font-size:12px;" oninput="updateCArmCell(${realIdx},'${key}',this.value)" onblur="${_blur}">`;
}
function updateCArmCell(idx, key, val) {
  if (!cArmRows[idx]) return;
  cArmRows[idx][key] = val;
  touchRow(cArmRows[idx]);
  saveCArmRows();
}
function deleteCArmRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = cArmRows[idx];
  if (!r) return;
  const _info = '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || '');
  if (!confirm('确定删除此行？')) return;
  cArmRows.splice(idx, 1);
  saveCArmRows(); refreshCArm();
  t('已删除');
  reportAudit('删除C臂记录', _info);
}
/** C 臂登记行保存：行变蓝并锁定编辑 */
function saveCArmRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = cArmRows[idx];
  if (!r) return;
  // 仅管理员 或 本行的巡回/交接巡回护士 可以保存
  if (!canEditCArmRow(r)) {
    t('仅管理员或本行巡回/交接巡回护士可保存'); return;
  }
  r.savedBy = currentUser.loginKey || currentUser.name;
  r.savedByName = currentUser.name;
  r.savedAt = new Date().toLocaleString('zh-CN');
  touchRow(r);
  saveCArmRows(); refreshCArm();
  t('已保存，行变蓝');
  reportAudit('保存C臂记录(已锁定)', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || '') + ' ' + (r.room || ''));
}
function unsaveCArmRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = cArmRows[idx];
  if (!r) return;
  // 仅管理员 或 本行的巡回/交接巡回护士 可以对已保存行取消保存
  if (!canEditCArmRow(r)) {
    t('仅管理员或本行巡回/交接巡回护士可取消保存'); return;
  }
  if (!confirm('取消保存？该行恢复为可编辑状态')) return;
  r.savedBy = ''; r.savedByName = ''; r.savedAt = '';
  touchRow(r);
  saveCArmRows(); refreshCArm();
  t('已取消保存');
}
/** 导出 Excel（HTML 表格格式，.xls 后缀，Excel 直接开） */
function execExport(dataArr, headers, fields, sd, ed, title) {
  const list = dataArr.filter(r => (!sd || r.date >= sd) && (!ed || r.date <= ed))
    .sort((a,b) => (b.date || '').localeCompare(a.date || ''));
  if (!list.length) { t('没有符合条件的数据'); return; }
  // 生成 HTML 表格（UTF-8 BOM + HTML，Excel 识别）
  const hrow = headers.map(h => '<th style="border:1px solid #000;padding:4px 8px;">' + h + '</th>').join('');
  const rows = list.map(r => '<tr>' + fields.map(f => '<td style="border:1px solid #000;padding:3px 6px;">' + (String(r[f] || '').replace(/&/g,'&amp;').replace(/</g,'&lt;')) + '</td>').join('') + '</tr>').join('');
  const html = '<html><head><meta charset="UTF-8"></head><body><h2>' + title + '</h2><table style="border-collapse:collapse;font-size:12px;">' +
    '<tr>' + hrow + '</tr>' + rows + '</table></body></html>';
  const blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel;charset=utf-8' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a'); a.href = url;
  a.download = title + '_' + (sd||'起') + '至' + (ed||'今') + '.xls';
  document.body.appendChild(a); a.click(); document.body.removeChild(a);
  URL.revokeObjectURL(url);
  t('已导出 ' + list.length + ' 条到 Excel');
}

function exportCArm() {
  const sd = document.getElementById('carm-start').value;
  const ed = document.getElementById('carm-end').value;
  execExport(cArmRows || [], ['日期','科室','床号','住院号','病人姓名','手术名称','运行情况','巡回护士','洗手护士'],
    ['date','dept','bedNo','admNo','name','surgery','runStatus','circu','scrub'],
    sd, ed, 'C型臂使用登记本');
}

/* ===========================================================
   模块 8：感染 / 急诊手术登记表
   =========================================================== */
function blankInfectionRow() {
  return { id: genId(), date: today(), dept: '', bedNo: '', name: '', admNo: '', infectType: '', roomNo: '', surgery: '', protection: '', postop30min: '', postopWipe: '', signoff: '', savedBy: '', savedByName: '', savedAt: '' };
}
function refreshInfection() {
  const sd = document.getElementById('inf-start').value;
  const ed = document.getElementById('inf-end').value;
  if (!document.getElementById('inf-start').value) document.getElementById('inf-start').value = sdOfMonth();
  if (!document.getElementById('inf-end').value) document.getElementById('inf-end').value = today();
  const tb = document.getElementById('infection-tbody');
  // 感染登记是全员可登记的纸面账本：登录用户都能增删改
  const canEdit = !!currentUser;
  document.getElementById('infection-act-col').style.display = canEdit ? '' : 'none';
  // 纸面表头显示当前年份
  const yr = (new Date()).getFullYear();
  const yrEl = document.getElementById('infection-paper-year');
  if (yrEl) yrEl.textContent = yr + ' 年';
  const start = document.getElementById('inf-start').value;
  const end = document.getElementById('inf-end').value;
  const list = (infectionRows || []).filter(r => (!start || r.date >= start) && (!end || r.date <= end));
  list.sort((a, b) => (b.date || '').localeCompare(a.date || ''));
  tb.innerHTML = '';
  if (!list.length) {
    tb.innerHTML = '<tr><td colspan="13" style="padding:24px;color:var(--muted);text-align:center;">暂无数据，点上方「新增一行」开始登记</td></tr>';
    return;
  }
  list.forEach((r) => {
    const realIdx = infectionRows.indexOf(r);
    const idx = list.indexOf(r);
    const tr = document.createElement('tr');
    if (r.savedBy) tr.style.background = '#e3f2fd'; // 已保存行 → 蓝色
    if (Math.floor(idx / PAGE_SIZE) !== _pageIdx.infection) tr.classList.add('pg-hidden');
    if (idx > 0 && idx % PAGE_SIZE === 0) tr.classList.add('break-page');
    // 从 signoff（签名栏：巡回/器械 或 单个人名）提取巡回护士
    const circuFromSignoff = (r.signoff || '').split('/')[0].trim();
    // 编辑权限：未保存=都可编辑；已保存=仅管理员或本行巡回护士
    const canEditRow = !r.savedBy || (currentUser && (currentUser.role === 'admin' || currentUser.name === circuFromSignoff));
    // 删除权限：管理员 或 本行的巡回护士
    const canDelete = currentUser && (currentUser.role === 'admin' || currentUser.name === circuFromSignoff);
    // 保存/取消保存仅允许 admin 或巡回护士操作
    const canSaveOp = currentUser && (currentUser.role === 'admin' || currentUser.name === circuFromSignoff);
    const actHTML = canEdit
      ? (canSaveOp
        ? `<button class="btn" style="padding:1px 8px;font-size:11px;${r.savedBy ? 'background:#fff3e0;color:#e65100;border:1px solid #ffcc80;' : ''}" onclick="${r.savedBy ? `unsaveInfectionRow(${realIdx})` : `saveInfectionRow(${realIdx})`}">${r.savedBy ? '⏸ 取消保存' : '保存'}</button>`
        : (r.savedBy ? '' : `<button class="btn" style="padding:1px 8px;font-size:11px;" onclick="saveInfectionRow(${realIdx})">保存</button>`))
        + (canDelete ? `<div class="btn danger" style="padding:1px 6px;font-size:11px;margin-top:2px;display:inline-block;cursor:pointer;border:1px solid #e57373;color:#c0392b;background:#fff;border-radius:3px;" onclick="deleteInfectionRow(${realIdx})">删</div>` : '')
        + (r.savedBy ? `<div style="font-size:11px;color:#1d6e3c;margin-top:2px;">✔ ${r.savedByName || '已保存'}</div>` : '')
      : '';
    tr.innerHTML = `
      <td>${fieldInfCell(r.date, 'date', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.dept, 'dept', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.bedNo, 'bedNo', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.name, 'name', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.admNo, 'admNo', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.infectType, 'infectType', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.roomNo, 'roomNo', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.surgery, 'surgery', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.protection, 'protection', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.postop30min, 'postop30min', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.postopWipe, 'postopWipe', realIdx, canEditRow)}</td>
      <td>${fieldInfCell(r.signoff, 'signoff', realIdx, canEditRow)}</td>
      <td style="text-align:center;white-space:nowrap;">${actHTML}</td>
    `;
    tb.appendChild(tr);
  });
  renderPager(document.getElementById('infection-pager'), list.length, 'infection');
}
window.infection_goPrev = () => { _pageIdx.infection--; refreshInfection(); };
window.infection_goNext = () => { _pageIdx.infection++; refreshInfection(); };
function fieldInfCell(val, key, realIdx, isAdmin) {
  const v = (val || '').replace(/</g, '&lt;');
  if (!isAdmin) return v || '<span style="color:#ccc;">—</span>';
  // 患者姓名：失焦时脱敏（oninput 实时脱敏会吃掉输入过程中的中间字）
  const _blur = (key === 'name') ? `maskNameBlur(this,'infection',${realIdx})` : '';
  return `<input type="text" data-k="${key}" data-i="${realIdx}" value="${v}" style="width:100%;padding:2px 4px;border:none;background:transparent;font-size:12px;" oninput="updateInfCell(${realIdx},'${key}',this.value)" onblur="${_blur}">`;
}
function updateInfCell(idx, key, val) {
  if (!infectionRows[idx]) return;
  infectionRows[idx][key] = val;
  touchRow(infectionRows[idx]);
  saveInfectionRows();
}
function deleteInfectionRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = infectionRows[idx];
  if (!r) return;
  const _info = '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.surgery || '');
  if (!confirm('确定删除此行？')) return;
  infectionRows.splice(idx, 1);
  saveInfectionRows(); refreshInfection();
  t('已删除');
  reportAudit('删除感染记录', _info);
}
/** 感染登记行保存：行变蓝并锁定编辑 */
function saveInfectionRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  const r = infectionRows[idx];
  if (!r) return;
  r.savedBy = currentUser.loginKey || currentUser.name;
  r.savedByName = currentUser.name;
  r.savedAt = new Date().toLocaleString('zh-CN');
  touchRow(r);
  saveInfectionRows(); refreshInfection();
  t('已保存，行变蓝');
  reportAudit('保存感染记录(已锁定)', '' + (r.admNo || '') + ' ' + (r.name || '') + ' ' + (r.type || '') + ' ' + (r.room || ''));
}
function unsaveInfectionRow(idx) {
  if (!currentUser) { t('请先登录'); return; }
  if (!confirm('取消保存？该行恢复为可编辑状态')) return;
  const r = infectionRows[idx];
  if (!r) return;
  r.savedBy = ''; r.savedByName = ''; r.savedAt = '';
  touchRow(r);
  saveInfectionRows(); refreshInfection();
  t('已取消保存');
}
function exportInfection() {
  const sd = document.getElementById('inf-start').value;
  const ed = document.getElementById('inf-end').value;
  execExport(infectionRows || [], ['日期','科室','床号','姓名','住院号','感染类型','手术间号','手术名称','人员防护','术后处置·空置30分钟','术后处置·擦拭消毒','签名'],
    ['date','dept','bedNo','name','admNo','infectType','roomNo','surgery','protection','postop30min','postopWipe','signoff'],
    sd, ed, '感染急诊手术登记表');
}

// 月份范围辅助：start = 月初，end = 今天
function sdOfMonth() {
  const d = new Date();
  return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-01';
}

// 翻页通用工具：每页 15 行（屏幕看 + 打印都能用）
const PAGE_SIZE = 15;
const _pageIdx = { carm: 0, infection: 0 };
function renderPager(pagerEl, totalRows, key, renderFn) {
  const totalPages = Math.max(1, Math.ceil(totalRows / PAGE_SIZE));
  if (_pageIdx[key] >= totalPages) _pageIdx[key] = totalPages - 1;
  if (_pageIdx[key] < 0) _pageIdx[key] = 0;
  pagerEl.style.display = totalRows > PAGE_SIZE ? 'flex' : 'none';
  pagerEl.innerHTML = `
    <button onclick="window['${key}_goPrev'] && window['${key}_goPrev']()" ${_pageIdx[key] === 0 ? 'disabled' : ''}>‹ 上一页</button>
    <span class="reg-pager-info">第 ${_pageIdx[key] + 1} / ${totalPages} 页（共 ${totalRows} 条）</span>
    <button onclick="window['${key}_goNext'] && window['${key}_goNext']()" ${_pageIdx[key] >= totalPages - 1 ? 'disabled' : ''}>下一页 ›</button>
  `;
}

/* ===========================================================
   模块 6：管理
   =========================================================== */
let editingNurseIdx = -1;
function toggleNurseForm(idx) {
  const modal = document.getElementById('nurse-modal');
  const title = document.getElementById('nurse-modal-title');
  if (!modal.classList.contains('show') || (idx !== undefined && editingNurseIdx !== idx)) {
    modal.classList.add('show');
    title.textContent = (idx !== undefined) ? '编辑人员' : '新增人员';
    if (idx !== undefined) {
      editingNurseIdx = idx;
      document.getElementById('nf-name').value = nurses[idx].name;
      document.getElementById('nf-no').value = nurses[idx].empNo || '';
      document.getElementById('nf-sysrole').value = nurses[idx].systemRole || 'nurse';
      document.getElementById('nf-score').checked = !!nurses[idx].canEditScore;
      document.getElementById('nf-title').value = nurses[idx].title || '';
      document.getElementById('nf-pass').value = '';
      document.getElementById('nf-pass').placeholder = '留空不改；填写则重置其密码（对方下次需改密）';
    } else {
      editingNurseIdx = -1;
      document.getElementById('nf-name').value = '';
      document.getElementById('nf-no').value = '';
      document.getElementById('nf-sysrole').value = 'nurse';
      document.getElementById('nf-score').checked = false;
      document.getElementById('nf-title').value = '';
      document.getElementById('nf-pass').value = '';
    }
  } else { modal.classList.remove('show'); }
}
function saveNurse() {
  const name = document.getElementById('nf-name').value.trim();
  if (!name) { t('请输入姓名'); return; }
  const empNo = document.getElementById('nf-no').value.trim() || '未设';
  const data = {
    name,
    empNo: empNo,
    systemRole: document.getElementById('nf-sysrole').value,
    canEditScore: document.getElementById('nf-score').checked,
    status: '在职'
  };
  const pwd = document.getElementById('nf-pass').value.trim();
  // 先收起弹窗：保证任何后续渲染异常都不会卡住弹窗
  const closeModal = () => { document.getElementById('nurse-modal').classList.remove('show'); editingNurseIdx = -1; };
  if (editingNurseIdx >= 0 && nurses[editingNurseIdx]) {
    nurses[editingNurseIdx] = data;
  } else {
    if (nurses.some((n, i) => n.name === name && i !== editingNurseIdx)) { t('该护士已存在'); closeModal(); return; }
    nurses.push(data);
  }
  // 保存登录账号（护士key=工号，管理员key=姓名；新建默认密码123456，已存在仅当填新密码时更新）
  const acc = loadAccounts();
  const loginKey = (data.systemRole === 'admin') ? name : empNo;
  // 角色切换时，删除该人员可能残留的「另一把」登录账号（姓名键/工号键），
  // 否则旧角色账号残留会导致用旧登录名仍按旧角色登录（如管理员调回护士后仍能登管理员）
  const possibleKeys = [name, empNo].filter(k => k && k !== 'admin');
  possibleKeys.forEach(k => {
    if (k !== loginKey && acc[k] && acc[k].name === name) delete acc[k];
  });
  if (!acc[loginKey]) {
    acc[loginKey] = { password: pwd || '123456', role: data.systemRole, name: name };
    saveAccounts(acc);
  } else if (pwd) {
    acc[loginKey].password = pwd;
    saveAccounts(acc);
  }
  saveNurses();
  closeModal();
  renderNurses(); renderLedger();
  t('已保存' + (pwd ? '（含登录账号）' : ''));
  reportAudit('维护人员', name + (editingNurseIdx >= 0 ? '（修改）' : '（新增）') + ' ' + empNo);
}
function deleteNurse(i) {
  if (!confirm('删除 '+nurses[i].name+'？')) return;
  nurses.splice(i,1); saveNurses(); renderNurses(); renderLedger();
}
function getAccountStatus(name) {
  const nurse = nurses.find(n => n.name === name);
  const acc = loadAccounts();
  let entry = acc[name]; // 管理员直接查名字
  if (!entry && nurse && nurse.empNo) entry = acc[nurse.empNo]; // 护士查工号
  if (!entry) return '<span style="color:#aaa;">未开通</span>';
  return '已启用(' + (entry.role === 'admin' ? '管理员' : '护士') + ')';
}
function setNursePwd(name) {
  const nurse = nurses.find(n => n.name === name);
  const loginKey = (nurse && nurse.systemRole === 'admin') ? name : (nurse ? nurse.empNo : name);
  const pwd = prompt('为「'+name+'」设置登录密码（登录名：'+loginKey+'）（留空取消）：');
  if (pwd === null) return;
  const acc = loadAccounts();
  if (!pwd) {
    delete acc[loginKey];
    t('已取消账号');
  } else {
    acc[loginKey] = { password: pwd, role: (nurse && nurse.systemRole) || 'nurse', name: name };
    t('登录名：'+loginKey+'，密码已设置');
  }
  saveAccounts(acc);
  renderNurses();
}
// 本人自助修改登录密码（护士/管理员登录后均可）
function changeOwnPwd() {
  if (!currentUser) return;
  showChangePwd(false); // 非强制，可取消
}
// 一键重建登录账号：为人员基础库里所有护士按「工号」补齐登录账号（默认密码123456），
// 已存在的账号（含自定义密码）不覆盖。用于服务器账号被覆盖、本地登录账号丢失后的批量恢复。
function rebuildAccounts() {
  if (!canManage()) { t('仅管理员可操作'); return; }
  const acc = loadAccounts();
  let added = 0;
  nurses.forEach(n => {
    if (n.empNo && n.empNo !== '未设' && !acc[n.empNo]) {
      acc[n.empNo] = { password: '123456', role: n.systemRole || 'nurse', name: n.name };
      added++;
    }
  });
  saveAccounts(acc);
  renderNurses();
  t('已重建 ' + added + ' 个护士登录账号（工号+密码123456），原有账号不受影响');
  reportAudit('重建登录账号', '新增 ' + added + ' 个护士账号');
}
function renderNurses() {
  const search = (document.getElementById('nurse-search')?.value || '').toLowerCase();
  const tb = document.getElementById('nurse-tbody');
  // 用真实数组下标，避免搜索过滤后编辑定位到错误护士
  const idxs = nurses.map((n,i)=>i).filter(i => {
    const n = nurses[i];
    return !search || n.name.toLowerCase().includes(search) || (n.empNo||'').toLowerCase().includes(search);
  });
  if (!idxs.length) { tb.innerHTML = '<tr><td colspan="7" style="padding:24px;color:var(--muted);">暂无人员，请添加</td></tr>'; return; }
  tb.innerHTML = idxs.map(i => {
    const n = nurses[i];
    return `<tr><td>${i+1}</td><td>${n.name}</td><td>${n.empNo||'-'}</td>
    <td>${n.systemRole==='admin'?'<span style="color:#8b1a1a;font-weight:500;">管理员</span>':'护士'}</td>
    <td>${n.canEditScore?'<span style="color:#1d6e3c;">✔ 可编辑</span>':'—'}</td>
    <td>${getAccountStatus(n.name)}</td>
    <td><button class="btn small" onclick="toggleNurseForm(${i})">编辑</button> <button class="btn small" onclick="setNursePwd('${n.name}')">设密码</button> <button class="btn small danger" onclick="deleteNurse(${i})">删除</button></td></tr>`;
  }).join('');
}
function importNursesExcel() {
  // 打开批量录入弹窗（粘贴「工号 姓名」）
  document.getElementById('batch-modal').classList.add('show');
  const ta = document.getElementById('batch-text'); if (ta) ta.focus();
}
function doBatchImport() {
  const ta = document.getElementById('batch-text');
  const lines = (ta.value||'').split(/\n/).map(l=>l.trim()).filter(Boolean);
  let added = 0, skipped = 0;
  lines.forEach(line => {
    const m = line.split(/\s+/);
    const empNo = (m[0]||'').trim();
    const name = m.slice(1).join(' ').trim();
    if (!name) return;
    // 去重：姓名相同，或工号相同（排除占位'未设'）即视为已存在
    const exists = nurses.find(n => n.name === name || (empNo && empNo !== '未设' && n.empNo && n.empNo === empNo));
    if (exists) { skipped++; return; }
    nurses.push({ name, empNo: empNo || '未设', systemRole:'nurse', canEditScore:false, status:'在职' });
    added++;
  });
  saveNurses();
  // 批量创建登录账号（工号为登录名、初始密码123456；已存在账号不覆盖）
  const acc = loadAccounts();
  nurses.forEach(n => {
    if (n.empNo && n.empNo !== '未设' && !acc[n.empNo]) {
      acc[n.empNo] = { password: '123456', role: n.systemRole || 'nurse', name: n.name };
    }
  });
  saveAccounts(acc);
  // 先关闭弹窗（无论后续渲染是否异常）
  document.getElementById('batch-modal').classList.remove('show');
  ta.value = '';
  try { renderNurses(); renderLedger(); } catch(e) { console.error('batch import render', e); }
  t('批量录入完成：新增 '+added+' 人，跳过重复 '+skipped+' 人');
}
function exportNursesExcel() {
  const lines = ['姓名,工号,职称,状态'];
  nurses.forEach(n => lines.push([n.name,n.empNo,n.title,n.status].join(',')));
  downloadCSV('\ufeff'+lines.join('\n'), '人员名单.csv');
  t('已导出');
}

/* ===========================================================
   模块 7：数据导入导出
   =========================================================== */
function exportJSON() {
  const data = { rows, nurses, settings, exportedAt: new Date().toISOString() };
  const blob = new Blob([JSON.stringify(data,null,2)],{type:'application/json'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'surgery_backup_'+today()+'.json';
  a.click(); t('已导出');
  reportAudit('导出备份', 'JSON 全量备份');
}
function importJSON(e) {
  const file = e.target.files[0]; if (!file) return;
  const reader = new FileReader();
  reader.onload = async ev => {
    try {
      const d = JSON.parse(ev.target.result);
      rows = d.rows || []; ensureRowIds(); nurses = d.nurses || []; settings = d.settings || { overtime:'16:00',defaultBaseScore:10 };
      saveAll();
      safeStorage.set(K_SETTINGS, JSON.stringify(settings));
      // 推送到服务器
      await apiFetch('/api/import', {
        method: 'POST',
        body: JSON.stringify(d)
      });
      location.reload();
    } catch { t('文件格式错误'); }
  };
  reader.readAsText(file);
  e.target.value = '';
}
function exportCSV() {
  const lines = ['日期,住院号,科室,床号,姓名,性别,年龄,手术名称,巡回,器械,入室,出室,耗时,巡回分值,器械分值'];
  rows.forEach(r => {
    lines.push([r.date,r.admNo,r.dept,r.bedNo,r.name,r.sex,r.age,r.surgery,r.circu,r.scrub,r.inTime,r.outTime,
      fmtDur(diffMin(r.inTime,r.outTime)),r.circuScore||'',r.scrubScore||''].join(','));
  });
  downloadCSV('\ufeff'+lines.join('\n'), '全部手术数据_'+today()+'.csv');
  t('已导出');
  reportAudit('导出手术数据', 'CSV ' + rows.length + ' 条');
}
function importStaff() {
  const allRealNurses = [
    '程海绿','张园园','庞博','刘杨','辛闪闪','尹娟','吕静茹','王鑫','柳伟豪','金鑫',
    '许明轩','郭兴华','李晓天','杨宁','李爽','王晨','吴梦阳','王瑞','徐相文','于璐',
    '时艳路','张淇','郭春梦','毕文敬','祝晓林','孟庆智','白金权','刘小川','史磊'
  ];
  nurses = allRealNurses.map((name, i) => ({
    name, empNo: 'GH'+String(i+1).padStart(3,'0'),
    role: i < 15 ? '巡回' : '器械',
    systemRole: i===0 ? 'admin' : 'nurse',
    canEditScore: i < 3,
    status: '在职'
  }));
  saveNurses();
  renderNurses();
  renderLedger();
  t('已导入29人，刷新后生效');
}
function downloadCSV(csv, filename) {
  const blob = new Blob([csv],{type:'text/csv;charset=utf-8'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob); a.download = filename; a.click();
}

// ---- 操作日志：上报 + 渲染 ----
function reportAudit(action, target) {
  try {
    apiFetch('/api/audit', { method: 'POST', body: JSON.stringify({ action, target }) }).catch(() => {});
  } catch (e) {}
}
let auditCache = [];
function loadAuditLog() {
  if (!currentUser) return;
  apiFetch('/api/audit?limit=1000').then(r => r.json()).then(j => {
    auditCache = (j.logs || []).filter(x => x.action !== '登录' && x.action !== '退出登录');
    renderAuditTable();
  }).catch(() => {
    const el = document.getElementById('audit-table');
    if (el) el.innerHTML = '<div style="padding:20px;color:var(--muted)">加载失败</div>';
  });
}
function fmtAuditLocal(ts) {
  if (!ts) return '';
  const d = new Date(ts);
  if (isNaN(d.getTime())) return ts;
  const p = n => (n < 10 ? '0' : '') + n;
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
}
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c])); }
function csvCell(v) { const s = String(v == null ? '' : v); return '"' + s.replace(/"/g, '""') + '"'; }
function renderAuditTable() {
  const el = document.getElementById('audit-table'); if (!el) return;
  const kw = (document.getElementById('audit-search').value || '').trim().toLowerCase();
  const dt = document.getElementById('audit-date').value || '';
  const list = (auditCache || []).filter(r => {
    if (dt && (r.ts || '').slice(0, 10) !== dt) return false;
    if (kw) {
      const hay = ((r.user || '') + ' ' + (r.name || '') + ' ' + (r.action || '') + ' ' + (r.target || '')).toLowerCase();
      if (!hay.includes(kw)) return false;
    }
    return true;
  });
  let html = '<table class="data-table"><thead><tr><th>时间</th><th>工号</th><th>姓名</th><th>操作</th><th>对象/说明</th></tr></thead><tbody>';
  if (!list.length) html += '<tr><td colspan="5" style="text-align:center;color:var(--muted);padding:18px;">暂无记录</td></tr>';
  for (const r of list) {
    html += '<tr><td>' + esc(fmtAuditLocal(r.ts)) + '</td><td>' + esc(r.user) + '</td><td>' + esc(r.name) + '</td><td>' + esc(r.action) + '</td><td>' + esc(r.target) + '</td></tr>';
  }
  html += '</tbody></table>';
  el.innerHTML = html;
}
function exportAuditCSV() {
  const kw = (document.getElementById('audit-search').value || '').trim().toLowerCase();
  const dt = document.getElementById('audit-date').value || '';
  const list = (auditCache || []).filter(r => {
    if (dt && (r.ts || '').slice(0, 10) !== dt) return false;
    if (kw) {
      const hay = ((r.user || '') + ' ' + (r.name || '') + ' ' + (r.action || '') + ' ' + (r.target || '')).toLowerCase();
      if (!hay.includes(kw)) return false;
    }
    return true;
  });
  const lines = ['时间,工号,姓名,操作,对象'];
  for (const r of list) lines.push([fmtAuditLocal(r.ts), r.user, r.name, r.action, r.target].map(csvCell).join(','));
  downloadCSV('\ufeff' + lines.join('\n'), '操作日志_' + today() + '.csv');
}

/* ===========================================================
   初始化
   =========================================================== */
function fmtYMD(d) {
  return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');
}
function initMonthSelects() {
  // 默认区间：本月1号 ~ 今天
  const now = new Date();
  const ed = fmtYMD(now);
  const sd = now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-01';
  ['monthly','perf'].forEach(p => {
    const s = document.getElementById(p+'-start'), e = document.getElementById(p+'-end');
    if (s) s.value = sd;
    if (e) e.value = ed;
  });
}
// 快捷区间：本月 / 近30天
function setRange(prefix, mode) {
  const now = new Date();
  const ed = fmtYMD(now);
  let sd;
  if (mode === 'month') {
    sd = now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-01';
  } else { // 近30天
    const d = new Date(now); d.setDate(d.getDate()-29); sd = fmtYMD(d);
  }
  document.getElementById(prefix+'-start').value = sd;
  document.getElementById(prefix+'-end').value = ed;
  if (prefix === 'monthly') refreshMonthly(); else refreshPerf();
}
async function init() {
  initAccounts();
  loadAll();
  refreshDeptCol();
  document.getElementById('reg-date').value = safeStorage.get('surgery_active_date') || today();
  document.getElementById('daily-date').value = today();
  document.getElementById('excel-sync-date').value = safeStorage.get('surgery_active_date') || today();

  // 登录态由持久化 token 在服务端校验恢复（见 init 末尾），此处不据本地缓存直接放行

  // 演示数据开关 ?seed=1（仅写入本地演示数据，不影响登录鉴权）
  try {
    const params = new URLSearchParams(location.search);
    if (params.get('seed') === '1') {
      const NAMES = ['程海绿','张园园','庞博','刘杨','辛闪闪','尹娟','吕静茹','王鑫','柳伟豪','金鑫','许明轩','郭兴华','李晓天','杨宁','李爽','王晨','吴梦阳','王瑞','徐相文','于璐','时艳路','张淇','郭春梦','毕文敬','祝晓林','孟庆智','白金权','刘小川','史磊'];
      const DEPTS = ['乳腺外科一区','关节(运医)骨科三区','创伤骨科二区','妇科C','手足外科一区','泌尿外科(医专)','甲状腺头颈外科一区','甲状腺头颈外科二区','眼科(医专)','神经外科','肝胆胰(微创)外科','胃肠外科','胸外科一区','脊柱骨科三区'];
      const SURGERIES = ['阑尾切除术','胆囊切除术','腹腔镜下胆囊切除术','经尿道前列腺电切术','腹腔镜阑尾切除术','腹腔镜下子宫肌瘤剔除术','全髋关节置换术','膝关节镜检','脊柱内固定取出术','腹腔镜下胃穿孔修补术','白内障超声乳化术','甲状腺次全切除术'];
      safeStorage.set('surgery_nurses', JSON.stringify(NAMES.map((n,i)=>({name:n,empNo:'E'+(1000+i),role:'均可',systemRole:i===0?'admin':'nurse',canEditScore:false,status:'在职'}))));
      safeStorage.set(K_ACCOUNTS, JSON.stringify({admin:{password:'admin123',role:'admin',name:'管理员'}}));
      const today = new Date();
      const ymd = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0') + '-' + String(today.getDate()).padStart(2,'0');
      const ym = today.getFullYear() + '-' + String(today.getMonth()+1).padStart(2,'0');
      const sample = [];
      let id = 0;
      for (let i = 0; i < 12; i++) {
        const circu = NAMES[1 + (i % 14)];
        const scrub = NAMES[1 + ((i*3) % 14)];
        const durMin = 30 + (i*17) % 200;
        const h1 = 8 + (i%7);
        const m1 = (i*7) % 60;
        const inMin = h1*60 + m1;
        const outH = Math.floor((inMin + durMin) / 60);
        const outM = (inMin + durMin) % 60;
        sample.push({id:'r'+(id++),date:ymd,room:String((i%6)+1),inTime:String(h1).padStart(2,'0')+':'+String(m1).padStart(2,'0'),outTime:String(outH).padStart(2,'0')+':'+String(outM).padStart(2,'0'),admNo:'20'+(250000+i),sex:i%2===0?'男':'女',name:'患者'+(i+1),age:30+(i*3)%50,dept:DEPTS[i%DEPTS.length],bedNo:String(20+i),surgery:SURGERIES[i%SURGERIES.length],surgeon:'张医'+(i%5+1),assistant:'李医'+(i%5+1),anesMethod:i%2===0?'全麻':'局麻',anesDoc:'王麻'+(i%3+1),circu,scrub,circuScore: 10 + (i%5), scrubScore: 8 + (i%4),remark:'',status:'reviewed',enteredBy:'护士A',reviewedBy:'管理员',reviewedAt:Date.now()-i*60000});
      }
      for (let d = 1; d <= today.getDate(); d++) {
        const dayStr = ym + '-' + String(d).padStart(2,'0');
        for (let i = 0; i < 2 + (d%3); i++) {
          const circu = NAMES[1 + ((d+i) % 14)];
          const scrub = NAMES[1 + ((d*2+i) % 14)];
          const durMin = 30 + ((d*10+i*7) % 200);
          const h1 = 8 + ((d+i)%7);
          const m1 = ((d*5+i*11) % 60);
          const inMin = h1*60 + m1;
          const outH = Math.floor((inMin + durMin) / 60);
          const outM = (inMin + durMin) % 60;
          sample.push({id:'r'+(id++),date:dayStr,room:String((i%6)+1),inTime:String(h1).padStart(2,'0')+':'+String(m1).padStart(2,'0'),outTime:String(outH).padStart(2,'0')+':'+String(outM).padStart(2,'0'),admNo:'20'+(240000+d*10+i),sex:'男',name:'患者'+d+'_'+i,age:30+i*5,dept:DEPTS[(d+i)%DEPTS.length],bedNo:String(20+d),surgery:SURGERIES[(d+i)%SURGERIES.length],surgeon:'医'+(i%5+1),circu,scrub,circuScore: 10 + ((d+i)%5), scrubScore: 8 + ((d*2+i)%4),status:'reviewed',enteredBy:'护士A',reviewedBy:'管理员'});
        }
      }
      safeStorage.set('surgery_rows', JSON.stringify(sample));
    }
  } catch(e) {}

  // 尝试用持久化的 token 恢复会话（不主动过期、重启不失效）
  if (authToken) {
    const me = await fetchMe();
    if (me) {
      currentUser = me;
      try { safeStorage.set('surgery_currentUser', JSON.stringify(currentUser)); } catch(e) {}
      await bootAppData();
      finalizeInit();
      if (currentUser.mustChangePwd) { showChangePwd(true); }
      else { enterApp(); }
      return;
    } else {
      setAuthToken(''); // token 已失效
    }
  }
  // 无有效会话：展示登录页（已加载的本地缓存可在登录前预览）
  document.getElementById('login-overlay').classList.remove('hidden');
  if (!rows.length) createDemoData();
  if (!nurses.length) loadDefaultNurses();
  initMonthSelects();
  renderLedger(); refreshDaily(); refreshMonthly(); refreshPerf(); refreshCArm(); refreshInfection(); renderNurses();
  try {
    const savedPage = safeStorage.get('surgery_currentPage') || 'ledger';
    const target = document.querySelector('.nav-tab[data-page="' + savedPage + '"]');
    if (target && target.style.display !== 'none') target.click();
  } catch(e) {}
}

function createDemoData() {
  const names = ['王小明','李志强','赵晓东','刘芳','陈静','周建国','吴敏','黄志远'];
  const surgerys = ['腹腔镜胆囊切除术','甲状腺次全切除术','剖宫产术','经皮肾镜碎石术','全髋关节置换术','白内障超声乳化术','鼻内镜下鼻息肉切除术','胃癌根治术'];
  const deeds = ['普外科','骨科','妇产科','泌尿外科','神经外科','眼科','耳鼻喉科','口腔科'];
  const survs = ['程海绿','张园园','庞博','刘杨','辛闪闪'];
  const scrubs = ['尹娟','吕静茹','王鑫','柳伟豪','金鑫'];
  for (let i=0;i<8;i++) {
    const h = 8 + Math.floor(Math.random()*4);
    const m = Math.floor(Math.random()*60);
    const inT = String(h).padStart(2,'0')+':'+String(m).padStart(2,'0');
    const dur = 60 + Math.floor(Math.random()*300);
    const outM = h*60+m+dur;
    const outH = Math.floor(outM/60);
    const outMin = outM%60;
    const outT = String(outH).padStart(2,'0')+':'+String(outMin).padStart(2,'0');
    rows.push({
      date: today(), admNo:'ZY'+today().replace(/-/g,'')+String(i+1).padStart(3,'0'),
      dept: deeds[i], bedNo: String(Math.floor(Math.random()*20)+1)+'床',
      name: names[i], sex: i%2===0?'男':'女', age: 25+Math.floor(Math.random()*50),
      surgery: surgerys[i], circu: survs[i%5], scrub: scrubs[i%5], circuScore: 10 + (i%5), scrubScore: 8 + (i%4),
      inTime: inT, outTime: outT, duration: fmtDur(diffMin(inT,outT)),

    });
  }
  saveAll();
}

function loadDefaultNurses() {
  const allRealNurses = [
    '程海绿','张园园','庞博','刘杨','辛闪闪','尹娟','吕静茹','王鑫','柳伟豪','金鑫',
    '许明轩','郭兴华','李晓天','杨宁','李爽','王晨','吴梦阳','王瑞','徐相文','于璐',
    '时艳路','张淇','郭春梦','毕文敬','祝晓林','孟庆智','白金权','刘小川','史磊'
  ];
  nurses = allRealNurses.map((name, i) => ({
    name, empNo: 'GH'+String(i+1).padStart(3,'0'),
    role: i < 15 ? '巡回' : '器械',
    systemRole: i===0 ? 'admin' : 'nurse',
    canEditScore: i < 3,
    status: '在职'
  }));
  saveAll();
}
init();
</script>

<!-- 区间打印弹窗（放在 body 末尾，不受任何 page 显隐影响） -->
<div class="modal" id="print-range-modal" onclick="if(event.target===this)closePrintRangeModal()">
  <div class="modal-content" style="max-width:420px;">
    <h3>选择打印日期区间</h3>
    <div class="form-grid">
      <div class="fg-item"><label>开始日期</label><input id="print-start" type="date"></div>
      <div class="fg-item"><label>结束日期</label><input id="print-end" type="date"></div>
    </div>
    <div style="margin-top:10px; font-size:12px; color:var(--muted); line-height:1.6;">
      将按日期顺序打印该区间内的全部手术登记记录，并额外显示「日期」列。<br>
      例：2026-08-01 至 2026-08-31 可打印整个 8 月的登记本。
    </div>
    <div style="text-align:right; margin-top:16px; display:flex; gap:8px; justify-content:flex-end;">
      <button class="btn" onclick="closePrintRangeModal()">取消</button>
      <button class="btn primary" onclick="doPrintRange()">开始打印</button>
    </div>
  </div>
</div>
</body>
</html>
