// hdata.jsx — data + helpers cho module Nhân sự & Chấm công (HRM).
// Nạp sau cdata.jsx (dùng TODAY cho seed tĩnh; chấm công seed theo ngày THẬT để thẻ chấm công sống).
// Helpers pháp lý bám BLLĐ 2019 + NĐ 145/2020 + Luật BHXH 2024 (chi tiết: docs/modules/module-hrm-spec.md).

/* ---------------- Catalogs ---------------- */
const HRM_STATUS = {
  probation:  { vi:'Thử việc',  en:'Probation',  cls:'badge-info' },
  active:     { vi:'Đang làm',  en:'Active',     cls:'badge-success' },
  suspended:  { vi:'Tạm nghỉ',  en:'Suspended',  cls:'badge-warning' },
  terminated: { vi:'Đã nghỉ',   en:'Terminated', cls:'badge-neutral' },
};
// Phân loại lý do chấm dứt HĐLĐ (0053) — báo cáo biến động cần tách tự nguyện vs cho thôi việc.
// `end_reason` (text tự do) vẫn giữ để ghi chi tiết; cột này chỉ để THỐNG KÊ.
const HRM_END_TYPES = {
  voluntary: { vi:'NV tự nguyện nghỉ',        en:'Voluntary',   cls:'badge-neutral' },
  dismissal: { vi:'Công ty cho thôi việc',    en:'Dismissal',   cls:'badge-error' },
  expiry:    { vi:'Hết hạn hợp đồng',         en:'Contract end', cls:'badge-neutral' },
  other:     { vi:'Khác',                     en:'Other',       cls:'badge-neutral' },
};
// Đánh giá hiệu suất (0053)
const HRM_REVIEW_KINDS = { kpi:{ vi:'KPI', en:'KPI' }, okr:{ vi:'OKR', en:'OKR' } };
const HRM_REVIEW_RATINGS = {
  exceed: { vi:'Vượt chỉ tiêu',  en:'Exceeds',   cls:'badge-success', color:'var(--success)' },
  meet:   { vi:'Đạt chỉ tiêu',   en:'Meets',     cls:'badge-info',    color:'var(--primary)' },
  below:  { vi:'Chưa đạt',       en:'Below',     cls:'badge-error',   color:'var(--error)' },
};
const HRM_CONTRACT_KINDS = {
  ctv:        { vi:'Cộng tác viên',  en:'Collaborator', cls:'badge-info' },
  seasonal:   { vi:'Thời vụ',        en:'Seasonal',     cls:'badge-warning' },
  fixed:      { vi:'Có thời hạn',    en:'Fixed-term',   cls:'badge-neutral' },
  indefinite: { vi:'Không thời hạn', en:'Indefinite',   cls:'badge-success' },
  appendix:   { vi:'Phụ lục',        en:'Appendix',     cls:'badge-neutral' },
};
const HRM_CONTRACT_STATUS = {
  active:     { vi:'Hiệu lực',    en:'Active',     cls:'badge-success' },
  expired:    { vi:'Hết hạn',     en:'Expired',    cls:'badge-neutral' },
  terminated: { vi:'Đã chấm dứt', en:'Terminated', cls:'badge-error' },
};
const HRM_DECISION_KINDS = {
  reward:     { vi:'Khen thưởng', en:'Reward',     cls:'badge-success', icon:'bi-trophy' },
  discipline: { vi:'Kỷ luật',     en:'Discipline', cls:'badge-error',   icon:'bi-exclamation-octagon' },
};
// Quan hệ nhân thân — mặc định của catalog chuỗi 'hrmRelations' (sửa được ở Thiết lập → app_catalogs).
const HRM_RELATIONS = ['Vợ', 'Chồng', 'Con', 'Bố', 'Mẹ', 'Anh ruột', 'Chị ruột', 'Em ruột'];
// Danh sách quan hệ ĐANG hiệu lực; `keep` = giá trị đang lưu của bản ghi cũ (có thể đã bị xóa khỏi
// danh mục, hoặc nhập tay từ trước) → luôn giữ trong danh sách để sửa hồ sơ không âm thầm đổi mất.
function hrmRelations(p, keep){
  const c = p && p.catalogs && p.catalogs.hrmRelations;
  const list = (c && c.length) ? c.slice() : HRM_RELATIONS.slice();
  if (keep && !list.includes(keep)) list.unshift(keep);
  return list;
}
// Loại hồ sơ scan — mặc định của catalog 'hrmDocTypes' (sửa được ở Thiết lập → app_catalogs).
// required = giấy tờ hồ sơ nào cũng phải có → thiếu thì cảnh báo ở trang hồ sơ.
const HRM_DOC_TYPES = [
  { id:'cccd',      vi:'CCCD / Hộ chiếu',      en:'ID card / Passport',  required:true },
  { id:'syll',      vi:'Sơ yếu lý lịch',       en:'CV / Personal record', required:true },
  { id:'bang-cap',  vi:'Bằng cấp · Chứng chỉ', en:'Diploma · Certificate' },
  { id:'hdld',      vi:'Hợp đồng đã ký',       en:'Signed contract',     required:true },
  { id:'bhxh',      vi:'Sổ BHXH',              en:'Social insurance book' },
  { id:'suc-khoe',  vi:'Khám sức khỏe',        en:'Health check' },
  { id:'khac',      vi:'Khác',                 en:'Other' },
];
const HRM_DOC_BUCKET = 'hrm-docs';
// Danh mục loại hồ sơ ĐANG hiệu lực: ưu tiên bản user sửa ở Thiết lập, chưa có thì dùng mặc định.
// `required` là cờ mức CODE (bảng app_catalogs chỉ lưu id + nhãn vi/en) → gắn lại theo id mặc định,
// nên loại do user tự thêm không thể đặt "bắt buộc" (đổi được thì phải thêm cột cho app_catalogs).
function hrmDocTypes(p){
  const c = p && p.catalogs && p.catalogs.hrmDocTypes;
  const list = (c && c.length) ? c : HRM_DOC_TYPES;
  return list.map(t => { const d = HRM_DOC_TYPES.find(x => x.id === t.id); return (d && d.required) ? { ...t, required:true } : t; });
}
function hrmDocTypeName(p, id, lang){ const t = hrmDocTypes(p).find(x => x.id === id); return t ? (t[lang] || t.vi) : id; }
// TK nhận lương của 1 NV (is_primary), không có thì lấy TK đầu tiên.
function hrmPrimaryBank(banks, empId){
  const mine = (banks || []).filter(b => b.employeeId === empId);
  return mine.find(b => b.isPrimary) || mine[0] || null;
}
// Che số TK khi hiện danh sách: chỉ lộ 4 số cuối (giảm rủi ro nhìn trộm màn hình).
function hrmMaskAccount(no){ const s = String(no || '').replace(/\s+/g, ''); return s.length <= 4 ? s : '••••' + s.slice(-4); }
// Loại giấy tờ BẮT BUỘC mà NV còn thiếu → nhắc HR bổ sung.
function hrmMissingDocs(p, docs, empId){
  const has = new Set((docs || []).filter(d => d.employeeId === empId).map(d => d.docType));
  return hrmDocTypes(p).filter(t => t.required && !has.has(t.id));
}
// deducts = trừ quỹ phép năm; half = cho phép nửa ngày (Luật BHXH 2024 — ốm đau); maxDays = trần luật (Đ115 BLLĐ)
const HRM_LEAVE_TYPES = {
  annual:         { vi:'Phép năm',              en:'Annual leave',        deducts:true },
  sick_si:        { vi:'Ốm đau (BHXH)',         en:'Sick (SI-paid)',      deducts:false, half:true },
  marriage:       { vi:'Kết hôn',               en:'Marriage',            deducts:false, maxDays:3 },
  child_marriage: { vi:'Con kết hôn',           en:"Child's marriage",    deducts:false, maxDays:1 },
  bereavement:    { vi:'Tang thân nhân',        en:'Bereavement',         deducts:false, maxDays:3 },
  maternity:      { vi:'Thai sản (BHXH)',       en:'Maternity (SI-paid)', deducts:false },
  unpaid:         { vi:'Nghỉ không lương',      en:'Unpaid leave',        deducts:false },
};
const HRM_REQ_STATUS = {
  pending:   { vi:'Chờ duyệt', en:'Pending',   cls:'badge-warning' },
  approved:  { vi:'Đã duyệt',  en:'Approved',  cls:'badge-success' },
  rejected:  { vi:'Từ chối',   en:'Rejected',  cls:'badge-error' },
  cancelled: { vi:'Đã hủy',    en:'Cancelled', cls:'badge-neutral' },
};

/* Kiểu tính lương gốc (chọn theo từng NV — lưu ở salary.payType) */
const HRM_PAY_TYPES = {
  fixed:     { vi:'Lương tháng cố định', en:'Fixed monthly' },
  byworkday: { vi:'Theo ngày công',      en:'By work-day' },
};
/* Lương tối thiểu vùng — NĐ 293/2025 (hiệu lực 1/1/2026) */
const HRM_REGION_MIN_WAGE = {
  1:{ month:5310000, hour:25500 }, 2:{ month:4730000, hour:22700 },
  3:{ month:4140000, hour:20000 }, 4:{ month:3700000, hour:17800 },
};
/* BHXH (NLĐ đóng): BHXH 8% + BHYT 1.5% + BHTN 1% = 10.5% */
const HRM_INSURANCE_RATES = { bhxh:0.08, bhyt:0.015, bhtn:0.01 };
/* Ma trận hệ số làm thêm giờ (Đ98 BLLĐ + Đ57 NĐ145): ngày thường 150% · nghỉ tuần 200% · lễ/Tết 300%.
   OT ban đêm = hệ_số_ngày × 1.2 + 0.3 → thường 210% · nghỉ tuần 270% · lễ 390%. */
const HRM_OT_MULT = { weekday:1.5, weekend:2.0, holiday:3.0 };
const HRM_OT_TYPE_LABEL = {
  weekday:{ vi:'Ngày thường', en:'Weekday' }, weekend:{ vi:'Ngày nghỉ tuần', en:'Weekly rest' }, holiday:{ vi:'Lễ/Tết', en:'Holiday' },
};
/* Biểu thuế TNCN lũy tiến từng phần (tháng) — [trần bậc, thuế suất] */
const HRM_PIT_BRACKETS = [[5000000,0.05],[10000000,0.10],[18000000,0.15],[32000000,0.20],[52000000,0.25],[80000000,0.30],[Infinity,0.35]];

/* Cấu hình mặc định (giờ chuẩn để CONFIG — đề xuất 44h/tuần chưa thành luật). Lưu app_settings key 'hrm'.
   Khối payroll (region + giảm trừ) sửa được ở tab Thiết lập; hệ số BHXH/biểu thuế/lương tối thiểu là hằng số theo luật. */
// shiftStart/shiftEnd: giờ ca chuẩn — CHỈ dùng để báo cáo đi trễ / về sớm (không ràng buộc chấm công).
const HRM_DEFAULTS = { hoursPerDay:8, hoursPerWeek:48, nightFrom:22, nightTo:6, otCapMonth:40, otCapYear:200, leaveBase:12,
  region:1, personalRelief:11000000, dependentRelief:4400000, shiftStart:'08:00', shiftEnd:'17:30', payPeriodStart:1 };
function hrmSettings(appSettings){
  try { const s = JSON.parse((appSettings || {}).hrm || '{}'); return { ...HRM_DEFAULTS, ...s }; }
  catch (e) { return { ...HRM_DEFAULTS }; }
}

/* ---------------- Cơ cấu tổ chức (Thiết lập → Tổ chức) ----------------
   Lưu trong settings blob 'hrm' key `depts`: [{ id, name, positions:[{ name, mgr }] }].
   `mgr` = chức danh CẤP QUẢN LÝ (giám đốc/phó giám đốc/trưởng phòng…) do HR tự tick trong danh mục —
   ai đang giữ chức danh đó trong phòng thì LÀ quản lý phòng (không gán tay từng người).
   Đây là DANH MỤC tổ chức (đổ select ở form) — hồ sơ nhân viên vẫn lưu dept/position dạng chuỗi,
   nên đổi tên/xóa phòng ban KHÔNG đụng hồ sơ đã lưu (giá trị cũ vẫn hiện, giống hrmRelations). */
// Danh sách phòng ban: đã khai báo ở Thiết lập thì dùng; CHƯA khai báo → gợi ý từ hồ sơ hiện có
// (để form vẫn có select trước khi admin cấu hình, và tab Tổ chức có sẵn dữ liệu nhập từ hồ sơ).
function hrmDepts(st, employees){
  if (st && Array.isArray(st.depts) && st.depts.length) return st.depts;
  const names = [...new Set((employees || []).map(e => e.dept).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'vi'));
  return names.map(n => ({ id:'d-' + n, name:n, positions:[] }));
}
function hrmDeptNames(st, employees, keep){
  const names = hrmDepts(st, employees).map(d => d.name);
  if (keep && !names.includes(keep)) names.push(keep);   // giá trị cũ không còn trong danh mục vẫn giữ
  return names;
}
// positions từng lưu dạng chuỗi → chuẩn hóa về {name, mgr} để tương thích dữ liệu cũ.
function hrmPosItems(d){ return ((d && d.positions) || []).map(x => typeof x === 'string' ? { name:x, mgr:false } : x); }
// Chức danh của 1 phòng ban: đã khai báo → dùng; chưa → gợi ý từ chức danh đang dùng trong phòng đó.
function hrmPositions(st, employees, dept, keep){
  const d = hrmDepts(st, employees).find(x => x.name === dept);
  const items = hrmPosItems(d);
  const list = items.length ? items.map(x => x.name)
    : [...new Set((employees || []).filter(e => !dept || e.dept === dept).map(e => e.position).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'vi'));
  if (keep && !list.includes(keep)) list.push(keep);
  return list;
}
function hrmIsMgrPosition(st, employees, dept, position){
  const d = hrmDepts(st, employees).find(x => x.name === dept);
  return hrmPosItems(d).some(x => x.name === position && x.mgr);
}
// Quản lý của 1 phòng = NV trong phòng đang giữ chức danh cấp quản lý (dùng được cả employees lẫn hrmDir).
function hrmDeptManagers(st, employees, dept){
  return (employees || []).filter(e => e.dept === dept && e.status !== 'terminated' && hrmIsMgrPosition(st, employees, dept, e.position));
}
// NV này có phải quản lý phòng không → [{dept, position}] (badge hồ sơ + nền cho quyền theo dõi/duyệt sau này)
function hrmManagerOf(st, employees, empId){
  const e = (employees || []).find(x => x.id === empId);
  return (e && e.dept && hrmIsMgrPosition(st, employees, e.dept, e.position)) ? [{ dept:e.dept, position:e.position }] : [];
}

/* ---------------- Helpers chung ---------------- */
// Ngày hiện tại theo giờ VN (chấm công dùng ngày THẬT, không dùng TODAY mock)
function todayVN(){ return new Date().toLocaleDateString('en-CA', { timeZone:'Asia/Ho_Chi_Minh' }); }
function hrmUid(pre){ return pre + '-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
function genEmpCode(list){
  const max = (list || []).reduce((m, e) => { const mm = (e.id || '').match(/NV-(\d+)/); return mm ? Math.max(m, +mm[1]) : m; }, 0);
  return 'NV-' + String(max + 1).padStart(3, '0');
}
function genHrmReqCode(list, kind){
  const pre = kind === 'ot' ? 'OT' : 'DP';
  const max = (list || []).reduce((m, r) => { const mm = (r.id || '').match(new RegExp('^' + pre + '-(\\d+)')); return mm ? Math.max(m, +mm[1]) : m; }, 0);
  return pre + '-' + String(max + 1).padStart(3, '0');
}
function genHrmContractCode(list){
  const max = (list || []).reduce((m, c) => { const mm = (c.id || '').match(/HDNS-(\d+)/); return mm ? Math.max(m, +mm[1]) : m; }, 0);
  return 'HDNS-' + String(max + 1).padStart(3, '0');
}
// Hồ sơ của CHÍNH user đăng nhập: ưu tiên link tài khoản (userId), fallback khớp email.
function hrmMyEmployee(employees, me){
  if (!me) return null;
  return (employees || []).find(e => e.userId && e.userId === me.id)
    || (employees || []).find(e => e.email && me.email && e.email.toLowerCase() === me.email.toLowerCase())
    || null;
}
function hrmEmpName(employees, id){ const e = (employees || []).find(x => x.id === id); return e ? e.fullName : id; }

/* ---------------- Helpers pháp lý ---------------- */
// Thâm niên (năm tròn) tính đến ngày `at`
function hrmYearsOfService(emp, at){
  if (!emp || !emp.startDate) return 0;
  const s = new Date(emp.startDate + 'T00:00:00'), d = new Date((at || todayVN()) + 'T00:00:00');
  let y = d.getFullYear() - s.getFullYear();
  if (d.getMonth() < s.getMonth() || (d.getMonth() === s.getMonth() && d.getDate() < s.getDate())) y--;
  return Math.max(0, y);
}
// Số tháng làm việc trong năm (tính tháng có làm việc bất kỳ ngày nào) — pro-rata Đ66 NĐ145
function hrmMonthsWorkedInYear(emp, year){
  if (!emp || !emp.startDate) return 0;
  const yStart = year + '-01', yEnd = year + '-12';
  const from = emp.startDate.slice(0, 7) > yStart ? emp.startDate.slice(0, 7) : yStart;
  const toRaw = emp.endDate ? emp.endDate.slice(0, 7) : yEnd;
  const to = toRaw < yEnd ? toRaw : yEnd;
  if (from > to) return 0;
  return (+to.slice(0, 4) - +from.slice(0, 4)) * 12 + (+to.slice(5) - +from.slice(5)) + 1;
}
// Quỹ phép năm = pro-rata theo tháng (Đ113) + thâm niên 1 ngày/5 năm (Đ114) + điều chỉnh tay
function hrmLeaveQuota(emp, year, st){
  const base = (st && st.leaveBase) || 12;
  const pro = Math.round(base * hrmMonthsWorkedInYear(emp, year) / 12 * 2) / 2;   // làm tròn nửa ngày
  const seniority = Math.floor(hrmYearsOfService(emp, year + '-12-31') / 5);
  return pro + seniority + (+emp.leaveAdjust || 0);
}
function hrmLeaveUsed(requests, empId, year){
  return (requests || []).filter(r => r.kind === 'leave' && r.employeeId === empId && r.status === 'approved'
    && (HRM_LEAVE_TYPES[r.leaveType] || {}).deducts && (r.startDate || '').slice(0, 4) === String(year))
    .reduce((s, r) => s + (+r.days || 0), 0);
}
function hrmLeaveBalance(emp, requests, year, st){
  return hrmLeaveQuota(emp, year, st) - hrmLeaveUsed(requests, emp.id, year);
}
// Tổng OT đã duyệt so trần 40h/tháng · 200h/năm (Đ107 BLLĐ; cap năm theo NV — 300h ngành đặc thù)
function hrmOtTotals(requests, emp, refDate, st){
  const ym = (refDate || todayVN()).slice(0, 7), y = (refDate || todayVN()).slice(0, 4);
  const rows = (requests || []).filter(r => r.kind === 'ot' && r.employeeId === emp.id && r.status === 'approved');
  const mUsed = rows.filter(r => (r.startDate || '').slice(0, 7) === ym).reduce((s, r) => s + (+r.hours || 0), 0);
  const yUsed = rows.filter(r => (r.startDate || '').slice(0, 4) === y).reduce((s, r) => s + (+r.hours || 0), 0);
  const mCap = (st && st.otCapMonth) || 40, yCap = +emp.otCapYear || (st && st.otCapYear) || 200;
  const lvl = (u, c) => u > c ? 'over' : (u >= c * 0.8 ? 'warn' : 'ok');
  return { month:{ used:mUsed, cap:mCap, level:lvl(mUsed, mCap) }, year:{ used:yUsed, cap:yCap, level:lvl(yUsed, yCap) } };
}
// Số giờ giữa 2 mốc HH:MM (qua đêm → +24h)
function hrmHoursBetween(t1, t2){
  if (!t1 || !t2) return 0;
  const m1 = +t1.slice(0, 2) * 60 + +t1.slice(3, 5); let m2 = +t2.slice(0, 2) * 60 + +t2.slice(3, 5);
  if (m2 <= m1) m2 += 1440;
  return Math.round((m2 - m1) / 6) / 10;
}
// Số giờ rơi vào khung ĐÊM 22h–6h (Đ106 BLLĐ) — nền phụ cấp 30% (giai đoạn 2)
function hrmNightHours(t1, t2, st){
  if (!t1 || !t2) return 0;
  const nf = ((st && st.nightFrom) || 22) * 60, nt = ((st && st.nightTo) || 6) * 60;
  const m1 = +t1.slice(0, 2) * 60 + +t1.slice(3, 5); let m2 = +t2.slice(0, 2) * 60 + +t2.slice(3, 5);
  if (m2 <= m1) m2 += 1440;
  // các cửa sổ đêm trong trục [0, 2880'): [0,nt) · [nf,1440+nt) · [1440+nf, 2880+nt)
  const wins = [[0, nt], [nf, 1440 + nt], [1440 + nf, 2880 + nt]];
  let mins = 0;
  wins.forEach(([a, b]) => { const lo = Math.max(m1, a), hi = Math.min(m2, b); if (hi > lo) mins += hi - lo; });
  return Math.round(mins / 6) / 10;
}
// Giờ công của 1 dòng chấm công
function hrmWorkedHours(row){
  if (!row || !row.checkIn || !row.checkOut) return 0;
  return Math.max(0, Math.round((new Date(row.checkOut) - new Date(row.checkIn)) / 360000) / 10);
}
// Cảnh báo pháp lý về HĐ của 1 NV (Đ20 + Đ25 BLLĐ): fixed >36 tháng · số HĐ fixed liên tiếp
// Số HĐ lao động / phụ lục — mẫu cấu hình ở Thiết lập nhân sự (app_settings).
// KHÔNG dùng lại formatContractNo() của HĐ bán: token khác hẳn ({KH} khách hàng vs {NV} nhân viên,
// {HD} số HĐ gốc cho phụ lục) và phụ lục cần đệm 2 chữ số (PL01) chứ không phải 3.
const HRM_CONTRACT_NO_FMT = '{SEQ}/YYYY/HĐLĐ-' + (((window.SWO_CONFIG && window.SWO_CONFIG.BRANDING && window.SWO_CONFIG.BRANDING.logoText) || 'SWO'));
const HRM_APPENDIX_NO_FMT = 'PL{SEQ}-{HD}';
function hrmDocNo(template, opts){
  opts = opts || {};
  const d = new Date(opts.date || todayVN());
  const YYYY = String(d.getFullYear());
  return String(template || '')
    .replace(/YYYY/g, YYYY).replace(/YY/g, YYYY.slice(2))
    .replace(/MM/g, String(d.getMonth() + 1).padStart(2, '0'))
    .replace(/DD/g, String(d.getDate()).padStart(2, '0'))
    .replace(/\{NV\}/gi, opts.employeeId || '')
    .replace(/\{HD\}/gi, opts.parentNo || '')
    .replace(/\{SEQ\}/gi, opts.seq != null ? String(opts.seq).padStart(opts.pad || 2, '0') : '');
}
// Mã HĐ này đã có ai dùng chưa? So trên TOÀN BỘ hợp đồng (mã chứng từ phải duy nhất cả công ty,
// không chỉ trong 1 hồ sơ). `exceptId` = id nội bộ của bản ghi đang sửa (không tự tính là trùng).
function hrmContractNoTaken(contracts, no, exceptId){
  const v = String(no || '').trim().toLowerCase();
  if (!v) return null;
  return (contracts || []).find(c => c.id !== exceptId && String(c.contractNo || '').trim().toLowerCase() === v) || null;
}
// Số kế tiếp: HĐ thường đếm theo NĂM ký (toàn công ty); phụ lục đếm theo HĐ gốc (PL01, PL02…).
// Đếm count+1 sẽ TRÙNG khi có bản ghi bị xóa hoặc số nhập tay → dò tiếp tới khi mã còn trống.
function hrmNextContractNo(contracts, settings, opts){
  const o = opts || {};
  const st = settings || {};
  // Mẫu nằm TRONG blob cấu hình 'hrm' (app_settings) — không đưa vào HRM_DEFAULTS vì HRM_DEFAULTS
  // khai TRƯỚC 2 hằng này trong file (const ở scope module → sẽ vướng TDZ).
  const year = String(o.date || todayVN()).slice(0, 4);
  const tpl = o.parent ? (st.appendixNoFormat || HRM_APPENDIX_NO_FMT) : (st.contractNoFormat || HRM_CONTRACT_NO_FMT);
  let seq = o.parent
    ? (contracts || []).filter(c => c.parentId === o.parent.id).length + 1
    : (contracts || []).filter(c => c.kind !== 'appendix' && String(c.signDate || c.startDate || '').slice(0, 4) === year).length + 1;
  const build = (n) => hrmDocNo(tpl, { date:o.date, seq:n, pad:2, employeeId:o.employeeId,
    parentNo:o.parent ? (o.parent.contractNo || o.parent.id) : '' });
  let no = build(seq);
  for (let i = 0; i < 999 && hrmContractNoTaken(contracts, no); i++) no = build(++seq);
  return no;
}
// Số ngày từ hôm nay tới `dateISO` (âm = đã qua).
function hrmDaysUntil(dateISO, today){
  if (!dateISO) return null;
  return Math.round((Date.parse(dateISO + 'T00:00:00Z') - Date.parse((today || todayVN()) + 'T00:00:00Z')) / 86400000);
}
// HĐ xác định thời hạn TỚI HẠN TÁI KÝ — Điều 20 BLLĐ 2019 (khoản 2):
//   • Hết hạn mà NLĐ vẫn làm việc → trong 30 NGÀY hai bên phải ký HĐ mới; quá 30 ngày chưa ký thì
//     HĐ đã ký NHIÊN trở thành HĐ KHÔNG xác định thời hạn (điểm b) → đây là mốc pháp lý, không phải nhắc nhở.
//   • Chỉ được ký THÊM 1 lần HĐ xác định thời hạn; sau đó còn làm việc thì phải ký HĐ không thời hạn (điểm c).
// Chỉ xét HĐ đang 'active' → HĐ đã đánh dấu hết hạn/chấm dứt không làm nhiễu danh sách.
function hrmContractRenewals(employees, contracts, withinDays, today){
  const t = today || todayVN();
  const win = withinDays == null ? 60 : withinDays;
  const out = [];
  (employees || []).forEach(e => {
    if (e.status === 'terminated') return;                    // đã nghỉ việc thì không phải tái ký
    const mine = (contracts || []).filter(c => c.employeeId === e.id && c.kind !== 'appendix');
    const fixedCount = mine.filter(c => c.kind === 'fixed').length;
    const hasIndef = mine.some(c => c.kind === 'indefinite');
    mine.forEach(c => {
      if (c.kind !== 'fixed' || c.status !== 'active' || !c.endDate) return;
      const left = hrmDaysUntil(c.endDate, t);
      if (left > win) return;
      out.push({ employee:e, contract:c, daysLeft:left,
        level:left < 0 ? 'over' : left <= 15 ? 'urgent' : 'warn',
        autoIndefinite:left < -30,                            // quá 30 ngày → luật coi như HĐ không thời hạn
        mustIndefinite:fixedCount >= 2 && !hasIndef });        // đã 2 HĐ xác định → lần ký tới phải không thời hạn
    });
  });
  return out.sort((a, b) => a.daysLeft - b.daysLeft);
}
// Lọc danh sách nhân sự theo phòng ban + trạng thái ('all' = không lọc).
function hrmFilterEmployees(rows, dept, status){
  return (rows || []).filter(e => (dept === 'all' || e.dept === dept) && (status === 'all' || e.status === status));
}
function hrmContractWarnings(contracts, empId){
  const list = (contracts || []).filter(c => c.employeeId === empId && c.kind !== 'appendix');
  const warns = [];
  list.forEach(c => {
    if (c.kind === 'fixed' && c.startDate && c.endDate) {
      const months = (+c.endDate.slice(0, 4) - +c.startDate.slice(0, 4)) * 12 + (+c.endDate.slice(5, 7) - +c.startDate.slice(5, 7));
      if (months > 36) warns.push({ cls:'error', vi:'HĐ ' + c.id + ' xác định thời hạn quá 36 tháng (Điều 20 BLLĐ cho phép tối đa 36 tháng).', en:'Contract ' + c.id + ' exceeds the 36-month fixed-term limit (Art. 20 Labor Code).' });
    }
  });
  const fixedCount = list.filter(c => c.kind === 'fixed').length;
  const hasIndef = list.some(c => c.kind === 'indefinite');
  if (fixedCount >= 3 && !hasIndef) warns.push({ cls:'error', vi:'Đã ký ' + fixedCount + ' HĐ xác định thời hạn — Điều 20 BLLĐ chỉ cho gia hạn 1 lần, HĐ này phải chuyển KHÔNG thời hạn.', en:fixedCount + ' consecutive fixed-term contracts — Art. 20 allows only 1 renewal; must convert to indefinite.' });
  else if (fixedCount === 2 && !hasIndef) warns.push({ cls:'warn', vi:'Đã ký 2 HĐ xác định thời hạn — lần ký tiếp theo phải là HĐ KHÔNG thời hạn (Điều 20 BLLĐ).', en:'2 fixed-term contracts signed — the next one must be indefinite (Art. 20 Labor Code).' });
  return warns;
}

/* ---------------- Helpers báo cáo nhân sự (0053) ----------------
   Mọi số liệu tính CLIENT-SIDE từ state, chỉ đọc. Nguyên tắc: KHÔNG bịa số —
   thiếu dữ liệu thì loại khỏi mẫu (vd đánh giá sau đào tạo cần đủ điểm 2 phía). */
// Dải tuổi / thâm niên cho biểu đồ cơ cấu
const HRM_AGE_BANDS = [
  { id:'lt25',  vi:'Dưới 25', en:'Under 25', min:0,  max:24 },
  { id:'25_34', vi:'25–34',   en:'25–34',    min:25, max:34 },
  { id:'35_44', vi:'35–44',   en:'35–44',    min:35, max:44 },
  { id:'45_54', vi:'45–54',   en:'45–54',    min:45, max:54 },
  { id:'gte55', vi:'Từ 55',   en:'55+',      min:55, max:200 },
];
const HRM_SENIORITY_BANDS = [
  { id:'lt1',   vi:'Dưới 1 năm', en:'< 1 year',  min:0,  max:0 },
  { id:'1_2',   vi:'1–2 năm',    en:'1–2 years', min:1,  max:2 },
  { id:'3_4',   vi:'3–4 năm',    en:'3–4 years', min:3,  max:4 },
  { id:'5_9',   vi:'5–9 năm',    en:'5–9 years', min:5,  max:9 },
  { id:'gte10', vi:'Từ 10 năm',  en:'10+ years', min:10, max:100 },
];
const HRM_GENDERS = { male:{ vi:'Nam', en:'Male' }, female:{ vi:'Nữ', en:'Female' }, other:{ vi:'Khác', en:'Other' } };
function hrmBandOf(bands, v){ return bands.find(b => v >= b.min && v <= b.max) || bands[bands.length - 1]; }
// Tuổi (năm tròn) tại ngày `at`
function hrmAge(emp, at){
  if (!emp || !emp.birthDate) return null;
  const s = new Date(emp.birthDate + 'T00:00:00'), d = new Date((at || todayVN()) + 'T00:00:00');
  let y = d.getFullYear() - s.getFullYear();
  if (d.getMonth() < s.getMonth() || (d.getMonth() === s.getMonth() && d.getDate() < s.getDate())) y--;
  return Math.max(0, y);
}
// Còn là người của công ty tại ngày `date`? (đã vào làm & chưa nghỉ tính đến ngày đó)
function hrmActiveAt(e, date){
  if (!e || !e.startDate || e.startDate > date) return false;
  return e.endDate ? e.endDate >= date : e.status !== 'terminated';
}
function hrmPrevDay(iso){ const d = new Date(iso + 'T00:00:00'); d.setDate(d.getDate() - 1); return d.toLocaleDateString('en-CA'); }
// Biến động nhân sự trong kỳ. `retentionPct` = (đầu kỳ − số nghỉ)/đầu kỳ — người vào-rồi-nghỉ NGAY trong
// kỳ vẫn bị trừ, nên tỷ lệ hơi thấp hơn công thức chuẩn; chấp nhận để công thức đọc được, số nhỏ nên lệch không đáng kể.
function hrmTurnover(employees, period){
  const list = employees || [];
  const head0 = list.filter(e => hrmActiveAt(e, hrmPrevDay(period.from))).length;
  const head1 = list.filter(e => hrmActiveAt(e, period.to)).length;
  const hires = list.filter(e => e.startDate && e.startDate >= period.from && e.startDate <= period.to);
  const leavers = list.filter(e => e.endDate && e.endDate >= period.from && e.endDate <= period.to);
  const avg = (head0 + head1) / 2;
  const byType = {};
  Object.keys(HRM_END_TYPES).forEach(k => { byType[k] = 0; });
  leavers.forEach(e => { byType[HRM_END_TYPES[e.endType] ? e.endType : 'other']++; });
  return { head0, head1, hires, leavers, avg, byType,
    turnoverPct:avg ? Math.round(leavers.length / avg * 1000) / 10 : 0,
    retentionPct:head0 ? Math.round((head0 - leavers.length) / head0 * 1000) / 10 : 0 };
}
function hrmMinOfHM(hm){ return +String(hm).slice(0, 2) * 60 + +String(hm).slice(3, 5); }
function hrmRowMinutes(iso){ const d = new Date(iso); return d.getHours() * 60 + d.getMinutes(); }
// Thống kê công 1 NV trong kỳ. Cửa sổ đo CẮT theo 3 mốc để "vắng không phép" không bị thổi lên:
//   • NV vào/nghỉ giữa kỳ → chỉ tính khoảng thực sự làm việc;
//   • không tính ngày TƯƠNG LAI (kỳ đang diễn ra);
//   • KHÔNG tính trước ngày có DỮ LIỆU CHẤM CÔNG đầu tiên của NV đó — vắng mặt suy ra từ "không có
//     dòng chấm công", nên quãng công ty chưa dùng hệ thống (hoặc NV chưa từng chấm) sẽ bị đếm là
//     vắng hàng loạt. Chưa có dòng chấm công nào → không đo được, trả 0 thay vì bịa ra số ngày vắng.
function hrmAttendanceStats(emp, period, hrm, st){
  const holidays = hrm.holidays || [];
  const mine = (hrm.attendance || []).filter(a => a.employeeId === emp.id);
  const firstTracked = mine.reduce((m, a) => (!m || a.workDate < m ? a.workDate : m), null);
  const today = todayVN();
  let from = emp.startDate && emp.startDate > period.from ? emp.startDate : period.from;
  if (firstTracked && firstTracked > from) from = firstTracked;
  const toRaw = emp.endDate && emp.endDate < period.to ? emp.endDate : period.to;
  const to = toRaw > today ? today : toRaw;
  if (!firstTracked || from > to) return { standard:0, present:0, leaveDays:0, absent:0, absentPct:0,
    late:0, lateMin:0, early:0, earlyMin:0, hours:0, from:null, to:null };
  const standard = hrmWorkingDaysBetween(from, to, holidays);
  const rows = mine.filter(a => a.workDate >= from && a.workDate <= to);
  const present = rows.filter(a => a.checkIn).length;
  const leaveDays = (hrm.requests || []).filter(r => r.kind === 'leave' && r.employeeId === emp.id && r.status === 'approved'
    && (r.startDate || '') >= from && (r.startDate || '') <= to).reduce((s, r) => s + (+r.days || 0), 0);
  const shiftIn = hrmMinOfHM((st && st.shiftStart) || '08:00'), shiftOut = hrmMinOfHM((st && st.shiftEnd) || '17:30');
  let late = 0, lateMin = 0, early = 0, earlyMin = 0;
  rows.forEach(a => {
    if (a.checkIn) { const d = hrmRowMinutes(a.checkIn) - shiftIn; if (d > 0) { late++; lateMin += d; } }
    if (a.checkOut) { const d = shiftOut - hrmRowMinutes(a.checkOut); if (d > 0) { early++; earlyMin += d; } }
  });
  const absent = Math.max(0, Math.round((standard - present - leaveDays) * 10) / 10);
  return { standard, present, leaveDays, absent, late, lateMin, early, earlyMin, from, to,
    absentPct:standard ? Math.round(absent / standard * 1000) / 10 : 0,
    hours:Math.round(rows.reduce((s, r) => s + hrmWorkedHours(r), 0) * 10) / 10 };
}
// Đánh giá SAU đào tạo: so điểm TB các kỳ đánh giá trước vs sau ngày kết thúc khóa.
// NV thiếu điểm ở một trong hai phía → BỎ QUA (không suy diễn), nên mẫu có thể nhỏ hơn số khóa.
function hrmTrainingEffect(trainings, reviews, period){
  const out = [];
  (trainings || []).filter(t => t.toDate && t.toDate >= period.from && t.toDate <= period.to).forEach(t => {
    const mine = (reviews || []).filter(r => r.employeeId === t.employeeId && r.score != null);
    const before = mine.filter(r => r.reviewDate < t.toDate), after = mine.filter(r => r.reviewDate > t.toDate);
    if (!before.length || !after.length) return;
    const avg = (xs) => xs.reduce((s, r) => s + (+r.score || 0), 0) / xs.length;
    const b = avg(before), a = avg(after);
    out.push({ training:t, before:Math.round(b * 10) / 10, after:Math.round(a * 10) / 10, delta:Math.round((a - b) * 10) / 10 });
  });
  return out.sort((x, y) => y.delta - x.delta);
}

/* ---------------- Helpers bảng lương (giai đoạn 2) ---------------- */
function genPayrunCode(ym){ return 'PR-' + ym; }
function genPayslipCode(ym, empId){ return 'PS-' + ym + '-' + empId; }
// Loại ngày để áp hệ số OT
function hrmDayType(dateIso, holidays){
  if ((holidays || []).some(h => h.date === dateIso)) return 'holiday';
  const dow = new Date(dateIso + 'T00:00:00').getDay();
  return (dow === 0 || dow === 6) ? 'weekend' : 'weekday';
}
// Số ngày công chuẩn của tháng 'YYYY-MM' (T2–T6 trừ lễ)
function hrmWorkingDaysBetween(start, end, holidays){
  if (!start || !end || end < start) return 0;
  const hs = new Set((holidays || []).map(h => h.date));
  let n = 0; const d = new Date(start + 'T00:00:00'), e = new Date(end + 'T00:00:00');
  while (d <= e) { const dow = d.getDay(); const iso = d.toLocaleDateString('en-CA'); if (dow !== 0 && dow !== 6 && !hs.has(iso)) n++; d.setDate(d.getDate() + 1); }
  return n;
}
// Thuế TNCN lũy tiến từng phần trên "thu nhập tính thuế" (đã trừ giảm trừ)
function hrmPit(taxed){
  if (taxed <= 0) return 0;
  let tax = 0, prev = 0;
  for (const [cap, rate] of HRM_PIT_BRACKETS) {
    const band = Math.min(taxed, cap) - prev;
    if (band > 0) tax += band * rate;
    prev = cap;
    if (taxed <= cap) break;
  }
  return Math.round(tax);
}
// Khoảng ngày của kỳ lương `ym` theo NGÀY BẮT ĐẦU KỲ D (payPeriodStart, 1–28; Thiết lập → Lương & thuế).
// D=1 → đúng tháng dương lịch (mặc định). D>1 → kỳ "tháng M" chạy từ ngày D tháng TRƯỚC đến ngày D−1
// tháng M (vd D=15: kỳ 2026-07 = 15/06 → 14/07). Trần 28 để không vỡ ở tháng 2/tháng 30 ngày.
function hrmPayPeriodRange(ym, cfg){
  const D = Math.min(28, Math.max(1, +((cfg || {}).payPeriodStart) || 1));
  const y = +ym.slice(0, 4), m = +ym.slice(5, 7);
  if (D === 1) return { from:ym + '-01', to:ym + '-' + String(new Date(y, m, 0).getDate()).padStart(2, '0'), start:1 };
  const py = m === 1 ? y - 1 : y, pm = m === 1 ? 12 : m - 1;
  return { from:py + '-' + String(pm).padStart(2, '0') + '-' + String(D).padStart(2, '0'),
    to:ym + '-' + String(D - 1).padStart(2, '0'), start:D };
}
// Bộ cấu hình chốt cho 1 kỳ lương (chụp để phiếu tái tính ổn định)
function hrmPayConfig(st, region){
  const r = region || st.region || 1;
  return { region:r, minWage:HRM_REGION_MIN_WAGE[r] || HRM_REGION_MIN_WAGE[1], insurance:HRM_INSURANCE_RATES,
    personalRelief:+st.personalRelief || 11000000, dependentRelief:+st.dependentRelief || 4400000,
    hoursPerDay:+st.hoursPerDay || 8, payPeriodStart:Math.min(28, Math.max(1, +st.payPeriodStart || 1)) };
}
// TÍNH PHIẾU LƯƠNG 1 NV cho kỳ ym — trả object detail (snapshot). Chạy client-side, tức thì.
// Kỳ = tháng dương lịch HOẶC kỳ tùy chỉnh theo cfg.payPeriodStart (vd 15/06→14/07) — xem hrmPayPeriodRange.
function hrmComputePayslip(emp, ym, hrm, cfg){
  const holidays = hrm.holidays || [];
  const { from, to } = hrmPayPeriodRange(ym, cfg);
  const standardDays = hrmWorkingDaysBetween(from, to, holidays) || 1;
  const hoursPerDay = cfg.hoursPerDay || 8;
  const base = (emp.salary && +emp.salary.base) || 0;
  const dailyRate = base / standardDays;
  const hourlyRate = dailyRate / hoursPerDay;
  const payType = (emp.salary && emp.salary.payType) || 'fixed';

  const att = (hrm.attendance || []).filter(a => a.employeeId === emp.id && a.workDate >= from && a.workDate <= to && a.checkOut);
  const presentDays = att.length;
  const leaves = (hrm.requests || []).filter(r => r.kind === 'leave' && r.employeeId === emp.id && r.status === 'approved' && (r.startDate || '') >= from && (r.startDate || '') <= to);
  const paidLeaveDays = leaves.filter(r => ['annual', 'marriage', 'child_marriage', 'bereavement'].includes(r.leaveType)).reduce((s, r) => s + (+r.days || 0), 0);
  const unpaidDays = leaves.filter(r => r.leaveType === 'unpaid').reduce((s, r) => s + (+r.days || 0), 0);

  const baseAmount = payType === 'byworkday'
    ? Math.round(dailyRate * Math.min(presentDays + paidLeaveDays, standardDays))
    : Math.max(0, Math.round(base - dailyRate * unpaidDays));

  const allowances = (emp.salary && emp.salary.allowances) || [];
  const allowTotal = allowances.reduce((s, a) => s + (+a.amount || 0), 0);
  // Phụ cấp MIỄN thuế TNCN theo TỪNG khoản: taxFree = miễn; taxFreeCap = trần miễn (vd ăn trưa 730k — vượt trần thì phần vượt chịu thuế).
  const allowExempt = allowances.reduce((s, a) => {
    if (!a.taxFree) return s;
    const amt = +a.amount || 0, cap = +a.taxFreeCap || 0;
    return s + (cap ? Math.min(amt, cap) : amt);
  }, 0);

  const ots = (hrm.requests || []).filter(r => r.kind === 'ot' && r.employeeId === emp.id && r.status === 'approved' && (r.startDate || '') >= from && (r.startDate || '') <= to);
  let otTotal = 0, totalOtHours = 0;
  const otLines = ots.map(r => {
    const type = hrmDayType(r.startDate, holidays);
    const nightH = r.isNight ? hrmNightHours(r.startTime, r.endTime, cfg) : 0;
    const dayH = Math.max(0, (+r.hours || 0) - nightH);
    const dayMult = HRM_OT_MULT[type], nightMult = Math.round((dayMult * 1.2 + 0.3) * 100) / 100;
    const amount = Math.round(hourlyRate * (dayH * dayMult + nightH * nightMult));
    otTotal += amount; totalOtHours += (+r.hours || 0);
    return { id:r.id, date:r.startDate, type, hours:+r.hours || 0, dayHours:Math.round(dayH * 10) / 10, nightHours:Math.round(nightH * 10) / 10, dayMult, nightMult, amount };
  });

  const gross = baseAmount + allowTotal + otTotal;

  const cap = 20 * (cfg.minWage ? cfg.minWage.month : 0);
  const siBaseRaw = (emp.salary && +emp.salary.siBase) || base;
  const siBase = cap ? Math.min(siBaseRaw, cap) : siBaseRaw;
  const ins = cfg.insurance || HRM_INSURANCE_RATES;
  const bhxh = Math.round(siBase * ins.bhxh), bhyt = Math.round(siBase * ins.bhyt), bhtn = Math.round(siBase * ins.bhtn);
  const insurance = bhxh + bhyt + bhtn;

  const taxDependents = (emp.salary && +emp.salary.taxDependents) || 0;
  // Chỉ phần OT bằng đơn giá GIỜ THƯỜNG chịu thuế; phần chênh do hệ số OT được miễn (Luật thuế TNCN).
  const taxableOt = Math.round(totalOtHours * hourlyRate);
  const taxableAllow = Math.max(0, allowTotal - allowExempt);
  const assessable = baseAmount + taxableAllow + taxableOt;
  const taxedIncome = Math.max(0, assessable - insurance - (cfg.personalRelief || 0) - taxDependents * (cfg.dependentRelief || 0));
  const pit = hrmPit(taxedIncome);

  const otherDeductions = [];
  const net = gross - insurance - pit;
  return { payType, periodFrom:from, periodTo:to, standardDays, presentDays, paidLeaveDays, unpaidDays, base, dailyRate:Math.round(dailyRate), hourlyRate:Math.round(hourlyRate),
    baseAmount, allowances, allowTotal, allowExempt, otLines, totalOtHours:Math.round(totalOtHours * 10) / 10, otTotal, gross,
    siBase, insurance:{ bhxh, bhyt, bhtn, total:insurance }, taxDependents, taxableOt, taxableAllow, assessable, taxedIncome, pit, otherDeductions, net, note:'' };
}
// Tổng thực lĩnh sau khấu trừ tay (otherDeductions có thể sửa trên phiếu)
function hrmPayslipNet(d){
  const other = (d.otherDeductions || []).reduce((s, x) => s + (+x.amount || 0), 0);
  return (d.gross || 0) - (d.insurance ? d.insurance.total : 0) - (d.pit || 0) - other;
}
// Phần DN đóng BHXH: BHXH 17.5% + BHYT 3% + BHTN 1% = 21.5% lương đóng BHXH (chi phí công ty ngoài gross)
const HRM_EMPLOYER_INS_RATE = 0.215;
// Chi phí lương công ty theo kỳ ĐÃ CHỐT → dẫn xuất khoản chi cho module Tài chính (cat 'luong').
// = Σ tổng thu nhập (gross — DN trả) + Σ phần DN đóng bảo hiểm. NV chưa xem được payslip → mảng rỗng (ẩn tự nhiên).
function hrmPayrollExpenses(hrm){
  const runs = ((hrm && hrm.payruns) || []).filter(r => r.status === 'finalized');
  return runs.map(run => {
    const slips = ((hrm && hrm.payslips) || []).filter(s => s.payrunId === run.id);
    let gross = 0, employerIns = 0;
    slips.forEach(s => { const d = s.detail || {}; gross += (+d.gross || 0); employerIns += Math.round((+d.siBase || 0) * HRM_EMPLOYER_INS_RATE); });
    const period = run.period;
    const endDate = hrmPayPeriodRange(period, run.config).to;   // kỳ tùy chỉnh → ngày cuối kỳ thật (snapshot trong config)
    return { id:'LUONG-' + period, date:endDate, cat:'luong', desc:'Chi phí lương tháng ' + period.slice(5) + '/' + period.slice(0, 4) + ' (' + slips.length + ' NV)',
      amount:gross + employerIns, status:'paid', source:'payroll', period, gross, employerIns };
  });
}

/* ---------------- Seed mock ---------------- */
// TODAY (mock tĩnh) = 2026-06-07; chấm công sinh động theo ngày thật bên dưới.
function hEmp(id, fullName, gender, birthDate, position, dept, startDate, x){
  return Object.assign({ id, fullName, gender, birthDate, position, dept, startDate,
    nationality:'Việt Nam', address:'', idNumber:'', education:'', skillLevel:'', userId:null,
    endDate:null, endReason:'', endType:null, siCode:'', siJoinDate:startDate, salary:{ base:0, allowances:[] },
    events:[], leaveAdjust:0, otCapYear:200, status:'active', email:'', phone:'', note:'' }, x || {});
}
const HRM_EMPLOYEES = [
  hEmp('NV-001', 'Trần Minh Quân', 'male', '1994-04-12', 'Nhân viên Kinh doanh cấp cao', 'Kinh doanh', '2021-03-01',
    { userId:'u1', email:'quan.tran@swo.vn', phone:'0903 111 000', address:'Q. Bình Thạnh, TP.HCM', idNumber:'079094001234',
      education:'Cử nhân QTKD', siCode:'7901234567', salary:{ base:18000000, payType:'fixed', siBase:18000000, taxDependents:2,
        allowances:[{ name:'Ăn trưa', amount:730000, taxFree:true, taxFreeCap:730000 }, { name:'Xăng xe', amount:500000 }] } }),
  hEmp('NV-002', 'Lê Thu Hà', 'female', '1990-09-25', 'Trưởng phòng Vận hành', 'Vận hành', '2019-08-15',
    { userId:'u2', email:'ha.le@swo.vn', phone:'0913 222 000', address:'Q.7, TP.HCM', idNumber:'079190002345',
      education:'Thạc sĩ QTKD', siCode:'7902345678', salary:{ base:28000000, payType:'fixed', siBase:28000000, taxDependents:1,
        allowances:[{ name:'Trách nhiệm', amount:2000000 }, { name:'Ăn trưa', amount:730000, taxFree:true, taxFreeCap:730000 }] } }),
  hEmp('NV-003', 'Phạm Quốc Bảo', 'male', '1988-01-30', 'Quản trị hệ thống', 'CNTT', '2020-01-02',
    { userId:'u3', email:'bao.pham@swo.vn', phone:'0934 333 000', address:'TP. Thủ Đức, TP.HCM', idNumber:'079088003456',
      education:'Kỹ sư CNTT', skillLevel:'Bậc 5/7', siCode:'7903456789', otCapYear:300,
      salary:{ base:30000000, allowances:[{ name:'Trực hệ thống', amount:1500000 }] } }),
  hEmp('NV-004', 'Nguyễn Thị Lan', 'female', '1996-12-05', 'Kế toán tổng hợp', 'Tài chính', '2026-03-16',
    { email:'lan.nguyen@swo.vn', phone:'0987 444 000', address:'Q. Gò Vấp, TP.HCM', idNumber:'079196004567',
      education:'Cử nhân Kế toán', siCode:'7904567890', salary:{ base:15000000, allowances:[] } }),
  hEmp('NV-005', 'Vũ Đức Anh', 'male', '2000-06-18', 'Lập trình viên', 'CNTT', '2026-05-04',
    { email:'anh.vu@swo.vn', phone:'0901 555 000', status:'probation', education:'Kỹ sư Phần mềm',
      salary:{ base:13600000, payType:'byworkday', siBase:13600000, taxDependents:0, allowances:[] }, note:'Thử việc 60 ngày (trình độ đại học) · lương 85%' }),
  hEmp('NV-006', 'Hoàng Mai Phương', 'female', '1993-03-08', 'Nhân viên Marketing', 'Kinh doanh', '2022-11-01',
    { email:'phuong.hoang@swo.vn', status:'terminated', endDate:'2026-04-30', endReason:'Thỏa thuận chấm dứt HĐLĐ',
      endType:'voluntary', salary:{ base:14000000, allowances:[] } }),
];
const HRM_CONTRACTS = [
  { id:'HDNS-001', employeeId:'NV-001', parentId:null, contractNo:'01/2021/HĐLĐ-SWO', kind:'fixed', signDate:'2021-02-25', startDate:'2021-03-01', endDate:'2022-02-28', position:'Nhân viên Kinh doanh', dept:'Kinh doanh', salary:{ base:12000000 }, status:'expired', note:'' },
  { id:'HDNS-002', employeeId:'NV-001', parentId:null, contractNo:'07/2022/HĐLĐ-SWO', kind:'fixed', signDate:'2022-02-20', startDate:'2022-03-01', endDate:'2023-02-28', position:'Nhân viên Kinh doanh', dept:'Kinh doanh', salary:{ base:14000000 }, status:'expired', note:'Gia hạn lần 1' },
  { id:'HDNS-003', employeeId:'NV-001', parentId:null, contractNo:'03/2023/HĐLĐ-SWO', kind:'indefinite', signDate:'2023-02-25', startDate:'2023-03-01', endDate:null, position:'Nhân viên Kinh doanh cấp cao', dept:'Kinh doanh', salary:{ base:16000000 }, status:'active', note:'Chuyển KHÔNG thời hạn sau 2 HĐ xác định thời hạn (Đ20 BLLĐ)' },
  { id:'HDNS-004', employeeId:'NV-001', parentId:'HDNS-003', contractNo:'PL01-03/2023', kind:'appendix', signDate:'2025-01-02', startDate:'2025-01-01', endDate:null, position:'Nhân viên Kinh doanh cấp cao', dept:'Kinh doanh', salary:{ base:18000000 }, status:'active', note:'Phụ lục điều chỉnh lương 2025' },
  { id:'HDNS-005', employeeId:'NV-002', parentId:null, contractNo:'05/2019/HĐLĐ-SWO', kind:'indefinite', signDate:'2019-08-10', startDate:'2019-08-15', endDate:null, position:'Chuyên viên Vận hành', dept:'Vận hành', salary:{ base:20000000 }, status:'active', note:'' },
  { id:'HDNS-006', employeeId:'NV-002', parentId:'HDNS-005', contractNo:'PL02-05/2019', kind:'appendix', signDate:'2023-06-28', startDate:'2023-07-01', endDate:null, position:'Trưởng phòng Vận hành', dept:'Vận hành', salary:{ base:28000000 }, status:'active', note:'Phụ lục bổ nhiệm Trưởng phòng + điều chỉnh lương' },
  { id:'HDNS-007', employeeId:'NV-003', parentId:null, contractNo:'01/2020/HĐLĐ-SWO', kind:'indefinite', signDate:'2019-12-28', startDate:'2020-01-02', endDate:null, position:'Quản trị hệ thống', dept:'CNTT', salary:{ base:30000000 }, status:'active', note:'' },
  { id:'HDNS-008', employeeId:'NV-004', parentId:null, contractNo:'02/2026/HĐLĐ-SWO', kind:'fixed', signDate:'2026-03-10', startDate:'2026-03-16', endDate:'2027-03-15', position:'Kế toán tổng hợp', dept:'Tài chính', salary:{ base:15000000 }, status:'active', note:'' },
  { id:'HDNS-009', employeeId:'NV-005', parentId:null, contractNo:'04/2026/HĐTV-SWO', kind:'fixed', signDate:'2026-04-28', startDate:'2026-05-04', endDate:'2026-07-02', position:'Lập trình viên', dept:'CNTT', salary:{ base:13600000 }, status:'active', note:'HĐ thử việc 60 ngày · lương ≥85% (Đ26 BLLĐ)' },
  { id:'HDNS-010', employeeId:'NV-006', parentId:null, contractNo:'09/2022/HĐLĐ-SWO', kind:'ctv', signDate:'2022-10-25', startDate:'2022-11-01', endDate:'2026-04-30', position:'Nhân viên Marketing', dept:'Kinh doanh', salary:{ base:14000000 }, status:'terminated', note:'' },
];
const HRM_CAREERS = [
  { id:'qt-001', employeeId:'NV-001', fromDate:'2021-03-01', toDate:'2022-12-31', position:'Nhân viên Kinh doanh', dept:'Kinh doanh', contractId:'HDNS-001', note:'' },
  { id:'qt-002', employeeId:'NV-001', fromDate:'2023-01-01', toDate:null, position:'Nhân viên Kinh doanh cấp cao', dept:'Kinh doanh', contractId:'HDNS-003', note:'' },
  { id:'qt-003', employeeId:'NV-002', fromDate:'2019-08-15', toDate:'2023-06-30', position:'Chuyên viên Vận hành', dept:'Vận hành', contractId:'HDNS-005', note:'' },
  { id:'qt-004', employeeId:'NV-002', fromDate:'2023-07-01', toDate:null, position:'Trưởng phòng Vận hành', dept:'Vận hành', contractId:'HDNS-006', note:'Bổ nhiệm theo phụ lục PL02' },
  { id:'qt-005', employeeId:'NV-003', fromDate:'2020-01-02', toDate:null, position:'Quản trị hệ thống', dept:'CNTT', contractId:'HDNS-007', note:'' },
  { id:'qt-006', employeeId:'NV-004', fromDate:'2026-03-16', toDate:null, position:'Kế toán tổng hợp', dept:'Tài chính', contractId:'HDNS-008', note:'' },
  { id:'qt-007', employeeId:'NV-005', fromDate:'2026-05-04', toDate:null, position:'Lập trình viên', dept:'CNTT', contractId:'HDNS-009', note:'Thử việc' },
  { id:'qt-008', employeeId:'NV-006', fromDate:'2022-11-01', toDate:'2026-04-30', position:'Nhân viên Marketing', dept:'Kinh doanh', contractId:'HDNS-010', note:'' },
];
// cost = chi phí DN chi cho khóa (VNĐ) · hours = số giờ học thực tế (0053 — báo cáo đào tạo).
const HRM_TRAININGS = [
  { id:'dt-001', employeeId:'NV-001', name:'Kỹ năng đàm phán B2B', org:'PACE Institute', fromDate:'2024-05-10', toDate:'2024-05-12', cost:4500000, hours:24, result:'Hoàn thành — Chứng nhận', note:'' },
  { id:'dt-002', employeeId:'NV-002', name:'Quản trị vận hành tinh gọn (Lean Ops)', org:'FMIT', fromDate:'2023-03-01', toDate:'2023-04-15', cost:12000000, hours:60, result:'Xuất sắc — Chứng chỉ', note:'' },
  { id:'dt-003', employeeId:'NV-003', name:'AWS Solutions Architect – Associate', org:'AWS Training', fromDate:'2025-02-01', toDate:'2025-03-30', cost:9800000, hours:80, result:'Đạt chứng chỉ SAA-C03', note:'' },
  { id:'dt-004', employeeId:'NV-005', name:'Onboarding & quy trình nội bộ S.W.O', org:'Nội bộ', fromDate:'2026-05-04', toDate:'2026-05-08', cost:0, hours:16, result:'Hoàn thành', note:'Đào tạo nội bộ — không phát sinh chi phí ngoài' },
  { id:'dt-005', employeeId:'NV-001', name:'Quản trị dự án (PMP cơ bản)', org:'PMI Vietnam', fromDate:'2026-02-10', toDate:'2026-02-14', cost:8500000, hours:32, result:'Hoàn thành', note:'' },
  { id:'dt-006', employeeId:'NV-004', name:'Cập nhật chính sách thuế TNCN 2026', org:'Deloitte Việt Nam', fromDate:'2026-03-20', toDate:'2026-03-21', cost:2400000, hours:12, result:'Hoàn thành', note:'' },
];
// Đánh giá hiệu suất định kỳ (0053). reviewDate = ngày CHỐT đánh giá → mốc lọc kỳ báo cáo
// (không dùng `period` vì đó là nhãn kỳ đánh giá dạng chuỗi, không so sánh được theo ngày).
const HRM_REVIEWS = [
  { id:'DG-001', employeeId:'NV-001', period:'2025-Q4', reviewDate:'2026-01-08', kind:'kpi', score:78, rating:'meet', reviewerId:'u2', note:'',
    goals:[{ name:'Doanh số ký mới', weight:60, target:'2.0 tỷ', actual:'1.8 tỷ', score:72 }, { name:'Số khách hàng mới', weight:40, target:'8', actual:'8', score:87 }] },
  { id:'DG-002', employeeId:'NV-002', period:'2025-Q4', reviewDate:'2026-01-08', kind:'kpi', score:92, rating:'exceed', reviewerId:'u1', note:'Dẫn dắt dự án chuyển đổi số nội bộ về đích sớm',
    goals:[{ name:'Tiến độ dự án trọng điểm', weight:50, target:'100%', actual:'100%', score:95 }, { name:'Giảm chi phí vận hành', weight:50, target:'-8%', actual:'-11%', score:90 }] },
  { id:'DG-003', employeeId:'NV-003', period:'2025-Q4', reviewDate:'2026-01-08', kind:'kpi', score:71, rating:'meet', reviewerId:'u2', note:'',
    goals:[{ name:'Uptime hệ thống', weight:70, target:'99.5%', actual:'99.4%', score:68 }, { name:'Thời gian xử lý sự cố', weight:30, target:'< 2h', actual:'1.8h', score:78 }] },
  { id:'DG-004', employeeId:'NV-006', period:'2025-Q4', reviewDate:'2026-01-08', kind:'kpi', score:55, rating:'below', reviewerId:'u2', note:'Không đạt chỉ tiêu 2 quý liên tiếp',
    goals:[{ name:'Lượt tiếp cận chiến dịch', weight:100, target:'50.000', actual:'27.500', score:55 }] },
  { id:'DG-005', employeeId:'NV-001', period:'2026-Q1', reviewDate:'2026-04-06', kind:'kpi', score:88, rating:'exceed', reviewerId:'u2', note:'Cải thiện rõ sau khóa quản trị dự án',
    goals:[{ name:'Doanh số ký mới', weight:60, target:'2.0 tỷ', actual:'2.4 tỷ', score:90 }, { name:'Số khách hàng mới', weight:40, target:'8', actual:'9', score:85 }] },
  { id:'DG-006', employeeId:'NV-002', period:'2026-Q1', reviewDate:'2026-04-06', kind:'okr', score:90, rating:'exceed', reviewerId:'u1', note:'',
    goals:[{ name:'Chuẩn hóa quy trình vận hành', weight:50, target:'6 quy trình', actual:'6', score:90 }, { name:'Đào tạo đội ngũ kế cận', weight:50, target:'3 người', actual:'3', score:90 }] },
  { id:'DG-007', employeeId:'NV-003', period:'2026-Q1', reviewDate:'2026-04-06', kind:'kpi', score:75, rating:'meet', reviewerId:'u2', note:'',
    goals:[{ name:'Uptime hệ thống', weight:70, target:'99.5%', actual:'99.6%', score:76 }, { name:'Hoàn thành nâng cấp hạ tầng', weight:30, target:'100%', actual:'90%', score:72 }] },
  { id:'DG-008', employeeId:'NV-004', period:'2026-Q1', reviewDate:'2026-04-06', kind:'kpi', score:62, rating:'below', reviewerId:'u2', note:'Mới vào 3/2026 — đang trong giai đoạn làm quen',
    goals:[{ name:'Đóng sổ đúng hạn', weight:100, target:'100%', actual:'80%', score:62 }] },
];
const HRM_DECISIONS = [
  { id:'qd-001', employeeId:'NV-002', kind:'reward', decisionNo:'QĐ-2025-08', decisionDate:'2025-12-30', content:'Thưởng hoàn thành xuất sắc dự án chuyển đổi số nội bộ', amount:5000000, note:'' },
  { id:'qd-002', employeeId:'NV-001', kind:'reward', decisionNo:'QĐ-2026-02', decisionDate:'2026-03-31', content:'Nhân viên kinh doanh xuất sắc Quý 1/2026', amount:3000000, note:'' },
  { id:'qd-003', employeeId:'NV-006', kind:'discipline', decisionNo:'QĐ-2025-11', decisionDate:'2025-10-15', content:'Khiển trách — vi phạm quy định giờ làm việc (đi trễ nhiều lần)', amount:null, note:'Hình thức: khiển trách (Đ124 BLLĐ)' },
];
const HRM_DEPENDENTS = [
  { id:'nt-001', employeeId:'NV-001', fullName:'Nguyễn Thu Trang', relation:'Vợ', birthDate:'1995-08-20', phone:'0902 888 111', note:'' },
  { id:'nt-002', employeeId:'NV-001', fullName:'Trần Minh Khang', relation:'Con', birthDate:'2022-02-14', phone:'', note:'' },
  { id:'nt-003', employeeId:'NV-002', fullName:'Đặng Quốc Huy', relation:'Chồng', birthDate:'1988-11-02', phone:'0918 999 222', note:'' },
  { id:'nt-004', employeeId:'NV-003', fullName:'Phạm Thị Hồng', relation:'Mẹ', birthDate:'1960-05-15', phone:'', note:'' },
];
const HRM_REQUESTS = [
  { id:'DP-003', employeeId:'NV-002', kind:'leave', leaveType:'annual', startDate:'2026-06-18', endDate:'2026-06-19', days:2, hours:null, startTime:null, endTime:null, isNight:false, consent:true, reason:'Việc gia đình', status:'pending', decidedBy:null, decidedAt:null, decidedNote:'' },
  { id:'DP-002', employeeId:'NV-004', kind:'leave', leaveType:'sick_si', startDate:'2026-06-03', endDate:'2026-06-03', days:0.5, hours:null, startTime:null, endTime:null, isNight:false, consent:true, reason:'Khám bệnh buổi sáng (có giấy BV)', status:'approved', decidedBy:'u2', decidedAt:'2026-06-03T02:00:00Z', decidedNote:'BHXH chi trả — không trừ phép' },
  { id:'DP-001', employeeId:'NV-001', kind:'leave', leaveType:'annual', startDate:'2026-05-19', endDate:'2026-05-20', days:2, hours:null, startTime:null, endTime:null, isNight:false, consent:true, reason:'Nghỉ phép về quê', status:'approved', decidedBy:'u2', decidedAt:'2026-05-15T03:00:00Z', decidedNote:'' },
  { id:'OT-002', employeeId:'NV-003', kind:'ot', leaveType:null, startDate:'2026-06-12', endDate:'2026-06-12', days:null, hours:3.5, startTime:'20:00', endTime:'23:30', isNight:true, consent:true, reason:'Bảo trì hệ thống ngoài giờ (cửa sổ downtime đêm)', status:'pending', decidedBy:null, decidedAt:null, decidedNote:'' },
  { id:'OT-001', employeeId:'NV-003', kind:'ot', leaveType:null, startDate:'2026-06-03', endDate:'2026-06-03', days:null, hours:3, startTime:'18:00', endTime:'21:00', isNight:false, consent:true, reason:'Nâng cấp máy chủ email', status:'approved', decidedBy:'u2', decidedAt:'2026-06-02T09:00:00Z', decidedNote:'' },
  { id:'OT-003', employeeId:'NV-001', kind:'ot', leaveType:null, startDate:'2026-05-28', endDate:'2026-05-28', days:null, hours:2, startTime:'17:30', endTime:'19:30', isNight:false, consent:true, reason:'Chuẩn bị hồ sơ thầu gấp', status:'approved', decidedBy:'u2', decidedAt:'2026-05-28T04:00:00Z', decidedNote:'' },
];
// Lịch nghỉ lễ 2026 (khớp seed DB 0048 — TB 9441/TB-BNV)
const HRM_HOLIDAYS = [
  { id:'le-2026-tet-dl', date:'2026-01-01', name:{ vi:'Tết Dương lịch', en:"New Year's Day" }, paid:true },
  { id:'le-2026-tet-1',  date:'2026-02-16', name:{ vi:'Tết Nguyên đán (29 Tết)', en:'Lunar New Year' }, paid:true },
  { id:'le-2026-tet-2',  date:'2026-02-17', name:{ vi:'Tết Nguyên đán (Mùng 1)', en:'Lunar New Year' }, paid:true },
  { id:'le-2026-tet-3',  date:'2026-02-18', name:{ vi:'Tết Nguyên đán (Mùng 2)', en:'Lunar New Year' }, paid:true },
  { id:'le-2026-tet-4',  date:'2026-02-19', name:{ vi:'Tết Nguyên đán (Mùng 3)', en:'Lunar New Year' }, paid:true },
  { id:'le-2026-tet-5',  date:'2026-02-20', name:{ vi:'Tết Nguyên đán (Mùng 4)', en:'Lunar New Year' }, paid:true },
  { id:'le-2026-gio-to', date:'2026-04-27', name:{ vi:'Giỗ Tổ Hùng Vương (nghỉ bù)', en:"Hung Kings' Day (in lieu)" }, paid:true },
  { id:'le-2026-30-4',   date:'2026-04-30', name:{ vi:'Ngày Giải phóng miền Nam', en:'Reunification Day' }, paid:true },
  { id:'le-2026-1-5',    date:'2026-05-01', name:{ vi:'Quốc tế Lao động', en:'International Labor Day' }, paid:true },
  { id:'le-2026-2-9-a',  date:'2026-09-01', name:{ vi:'Quốc khánh (ngày liền kề)', en:'National Day (adjacent)' }, paid:true },
  { id:'le-2026-2-9',    date:'2026-09-02', name:{ vi:'Quốc khánh', en:'National Day' }, paid:true },
];
// Chấm công seed: 10 ngày làm việc gần nhất theo ngày THẬT (hôm nay: đã vào ca, chưa ra — để thẻ chấm công sống)
function hrmSeedAttendance(){
  const rows = [], now = new Date();
  for (let d = 13; d >= 0; d--) {
    const dt = new Date(now); dt.setDate(dt.getDate() - d);
    const dow = dt.getDay(); if (dow === 0 || dow === 6) continue;
    const iso = dt.toLocaleDateString('en-CA');
    ['NV-001', 'NV-002', 'NV-003', 'NV-004', 'NV-005'].forEach((id, i) => {
      if (d === 0 && id === 'NV-004') return;   // hôm nay Lan chưa chấm công
      // Giờ vào/ra so với ca chuẩn 08:00–17:30 (HRM_DEFAULTS): ĐA SỐ đúng giờ, rải vài lượt đi trễ/
      // về sớm theo công thức cố định → báo cáo chấm công có dữ liệu thật để hiện. (Mốc cũ 08:02–08:30
      // khiến 100% lượt bị tính đi trễ, nhìn như lỗi.) Không dùng random: seed phải ổn định giữa các lần tải.
      const lateMin = (d + i) % 5 === 0 ? 14 : 0;
      const earlyMin = (d + i) % 7 === 0 ? 35 : 0;
      const ci = new Date(dt); ci.setHours(7, 50 + i * 2 + lateMin, 0, 0);
      const co = new Date(dt); co.setHours(17, 32 + i * 5 - earlyMin, 0, 0);
      rows.push({ id:'cc-' + iso + '-' + id, employeeId:id, workDate:iso, checkIn:ci.toISOString(),
        checkOut:d === 0 ? null : co.toISOString(), source:'self', editedBy:null, note:'' });
    });
  }
  return rows;
}
const HRM_BANK_ACCOUNTS = [
  { id:'tk-001', employeeId:'NV-001', bankName:'Vietcombank', branch:'CN Bình Thạnh', accountNo:'0071000123456', accountHolder:'TRAN MINH QUAN', isPrimary:true, note:'' },
  { id:'tk-002', employeeId:'NV-001', bankName:'Techcombank', branch:'CN Sài Gòn', accountNo:'19035678901234', accountHolder:'TRAN MINH QUAN', isPrimary:false, note:'Tài khoản phụ' },
  { id:'tk-003', employeeId:'NV-002', bankName:'ACB', branch:'CN Phú Mỹ Hưng', accountNo:'2450987654321', accountHolder:'LE THU HA', isPrimary:true, note:'' },
  { id:'tk-004', employeeId:'NV-003', bankName:'BIDV', branch:'CN Thủ Đức', accountNo:'31410001234567', accountHolder:'PHAM QUOC BAO', isPrimary:true, note:'' },
];
// Seed hồ sơ scan: chỉ có METADATA (mock không có file thật trên Storage) → nút Tải về sẽ báo
// "chưa kết nối máy chủ". Đủ để thấy bố cục + cảnh báo thiếu giấy tờ (NV-003 thiếu HĐ đã ký).
const HRM_DOCUMENTS = [
  { id:'hs-001', employeeId:'NV-001', docType:'cccd', name:'CCCD_TranMinhQuan.pdf', path:'NV-001/seed_cccd.pdf', size:842000, mime:'application/pdf', issuedDate:'2021-06-10', note:'' },
  { id:'hs-002', employeeId:'NV-001', docType:'syll', name:'SoYeuLyLich_Quan.pdf', path:'NV-001/seed_syll.pdf', size:1260000, mime:'application/pdf', issuedDate:'2021-02-20', note:'' },
  { id:'hs-003', employeeId:'NV-001', docType:'hdld', name:'HDLD-2021-014_daky.pdf', path:'NV-001/seed_hdld.pdf', size:990000, mime:'application/pdf', issuedDate:'2021-03-01', note:'' },
  { id:'hs-004', employeeId:'NV-002', docType:'cccd', name:'CCCD_LeThuHa.jpg', path:'NV-002/seed_cccd.jpg', size:520000, mime:'image/jpeg', issuedDate:'2019-05-04', note:'' },
  { id:'hs-005', employeeId:'NV-002', docType:'bang-cap', name:'ThacSi_QTKD.pdf', path:'NV-002/seed_bang.pdf', size:1450000, mime:'application/pdf', issuedDate:'2016-11-12', note:'' },
  { id:'hs-006', employeeId:'NV-003', docType:'cccd', name:'CCCD_PhamQuocBao.pdf', path:'NV-003/seed_cccd.pdf', size:760000, mime:'application/pdf', issuedDate:'2020-01-02', note:'' },
];
const HRM_SEED = {
  employees:HRM_EMPLOYEES, contracts:HRM_CONTRACTS, careers:HRM_CAREERS, trainings:HRM_TRAININGS,
  decisions:HRM_DECISIONS, dependents:HRM_DEPENDENTS, attendance:hrmSeedAttendance(),
  requests:HRM_REQUESTS, holidays:HRM_HOLIDAYS, payruns:[], payslips:[],
  bankAccounts:HRM_BANK_ACCOUNTS, documents:HRM_DOCUMENTS, reviews:HRM_REVIEWS,
};

Object.assign(window, {
  HRM_END_TYPES, HRM_REVIEW_KINDS, HRM_REVIEW_RATINGS, HRM_AGE_BANDS, HRM_SENIORITY_BANDS, HRM_GENDERS,
  hrmBandOf, hrmAge, hrmActiveAt, hrmPrevDay, hrmTurnover, hrmMinOfHM, hrmRowMinutes,
  hrmAttendanceStats, hrmTrainingEffect,
  HRM_STATUS, HRM_CONTRACT_KINDS, HRM_CONTRACT_STATUS, HRM_DECISION_KINDS, HRM_RELATIONS,
  HRM_DOC_TYPES, HRM_DOC_BUCKET, hrmDocTypes, hrmDocTypeName, hrmRelations, hrmPrimaryBank, hrmMaskAccount, hrmMissingDocs,
  HRM_LEAVE_TYPES, HRM_REQ_STATUS, HRM_DEFAULTS, hrmSettings,
  hrmDepts, hrmDeptNames, hrmPosItems, hrmPositions, hrmIsMgrPosition, hrmDeptManagers, hrmManagerOf,
  todayVN, hrmUid, genEmpCode, genHrmReqCode, genHrmContractCode, hrmMyEmployee, hrmEmpName,
  hrmYearsOfService, hrmMonthsWorkedInYear, hrmLeaveQuota, hrmLeaveUsed, hrmLeaveBalance,
  hrmOtTotals, hrmHoursBetween, hrmNightHours, hrmWorkedHours, hrmContractWarnings,
  hrmDaysUntil, hrmContractRenewals, hrmFilterEmployees,
  HRM_CONTRACT_NO_FMT, HRM_APPENDIX_NO_FMT, hrmDocNo, hrmNextContractNo, hrmContractNoTaken,
  HRM_PAY_TYPES, HRM_REGION_MIN_WAGE, HRM_INSURANCE_RATES, HRM_OT_MULT, HRM_OT_TYPE_LABEL, HRM_PIT_BRACKETS,
  genPayrunCode, genPayslipCode, hrmDayType, hrmWorkingDaysBetween, hrmPit,
  hrmPayConfig, hrmPayPeriodRange, hrmComputePayslip, hrmPayslipNet, HRM_EMPLOYER_INS_RATE, hrmPayrollExpenses,
  HRM_SEED,
});
