# QUY ĐỊNH BỐ CỤC, THEMES & STYLE - HR MANAGER
# File structure: index.php và các file trong thư mục pages/

================================================================================
1. CẤU TRÚC FILE
================================================================================

File chính:
- index.php: File chính gắn kết tất cả các phần

Thư mục pages/:
- control_header.php: Phần header (đầu trang)
- control_menu.php: Phần sidebar (thanh điều hướng bên trái - chỉ desktop)
- control_footer.php: Phần footer (chân trang - chỉ mobile)
- home.php: Phần nội dung chính (trang chủ)
- company.php: Trang quản lý công ty
- category.php: Trang quản lý hạng mục (4 cột: ID, Tên tiếng Việt, Tên tiếng Anh, Thao tác)
- documentout.php: Trang quản lý công văn đi (8 cột: ID, Số hiệu, Ngày hiệu lực, Cơ quan nhận, Từ Công ty, Tóm tắt nội dung, Thể loại, Thao tác)
- permission.php: Trang quản lý quyền truy cập (3-level menu tree, toggle permissions 0-5)
- error.php: Trang hiển thị lỗi (404, 403, default) — được include từ các page khác
- {page_name}.php: Trang mới (tạo theo pattern)

Lưu ý: Khi tạo trang mới content, tạo file mới trong pages/ và include vào index.php

Thư mục assets/css/:
- all.css: Font Awesome icons
- base.css: Styles nền tảng (reset, body)
- layout.css: Styles bố cục (header, footer, container, sidebar, responsive)
- components.css: Các component tái sử dụng (buttons, inputs, tables, pagination, nav items, popup)
- pages.css: Styles theo scope trang (để chỉnh sửa trang cụ thể)

Thư mục lang/:
- message_vn.php: File ngôn ngữ tiếng Việt
- message_eng.php: File ngôn ngữ tiếng Anh
- Quy tắc: Keys phải giống nhau ở cả 2 file, dùng <?php echo $lang['key']; ?> cho tất cả text

Thư mục modules/:
- BaseModule.php: Base class cho tất cả modules
- CompanyModule.php: Module xử lý dữ liệu công ty
- CategoryModule.php: Module xử lý dữ liệu hạng mục (table=Category, PK=ID_Category, fillable=[Name_Vn, Name_Eng])
- DocumentOutModule.php: Module xử lý dữ liệu công văn đi (table=DocumentOut, PK=Id, fillable=[ID_Symbol, TitleVi, EffectiveDate, Issuer, ID_Company, Summary, DocType, TypeDoc, Quantity, FileUrl, FileStorage, ID_by, Notes, ID_Export], JOIN Company+DocType)
QUAN TRỌNG DocumentOut:
- Cột "Mã số văn bản" load từ cột 'ID' (Id) → map sang field document_code, READONLY
- Cột "Tiêu đề" load từ cột 'TitleVi' → map sang field title
- Cột "Thể loại" dropdown load từ bảng dbo.DocType:
  * Option value = cột Id_Type
  * Display text = cột Name_Vn (nếu lang=vn) hoặc Name_Eng (nếu lang=eng)
  * Dữ liệu được chọn so sánh DocumentOut.DocType với value option (Id_Type)
- Cột "Loại giấy tờ" dropdown load từ bảng dbo.DocPageType:
  * Option value = cột Id_Type
  * Display text = cột Name_Vn (nếu lang=vn) hoặc Name_Eng (nếu lang=eng)
  * Dữ liệu được chọn so sánh DocumentOut.TypeDoc với value option (Id_Type)
- {PageName}Module.php: Module xử lý dữ liệu cho từng trang
- Quy tắc: Extend BaseModule, có $table, $primaryKey, $fillable, $searchable

Thư mục lib/:
- AuthModule.php: Xử lý authentication
- LoginModule.php: Xử lý login/logout
- DB.php: Database singleton
- MenuTree.php: Xử lý menu tree
- PaginationHelper.php: Helper cho pagination
- popup_notice.php: Hiển thị popup thông báo
- EmailSender.php: Gửi email

Thư mục config/:
- database.php: Cấu hình database
- access.json: Cấu hình quyền truy cập menu theo permission level

Lưu ý: CSS được tách theo nguyên tắc modular để dễ quản lý và bảo trì

================================================================================
2. CẤU TRÚC CSS
================================================================================

Thứ tự load CSS trong index.php:
1. assets/css/all.css (Font Awesome)
2. assets/css/base.css (Nền tảng)
3. assets/css/layout.css (Bố cục)
4. assets/css/components.css (Component)
5. assets/css/pages.css (Trang cụ thể)

================================================================================
3. BỐ CỤC TỔNG THỂ (LAYOUT)
================================================================================

Cấu trúc trang gồm 4 phần chính:
- Header (đầu trang)
- Sidebar (thanh điều hướng bên trái - chỉ desktop)
- Container (nội dung chính)
- Footer (chân trang - chỉ mobile)

================================================================================
4. RESPONSIVE DESIGN
================================================================================

DESKTOP (min-width: 769px):
- Header: left: 250px, width: calc(100% - 250px)
- Container: left: 250px, right: 0, bottom: 0
- Sidebar: display: flex, width: 250px, lấp đầy chiều cao
- Footer: display: none (ẩn)

MOBILE (max-width: 768px):
- Header: left: 0, width: 100%
- Container: left: 0, right: 0, bottom: 65px
- Sidebar: display: none (ẩn)
- Footer: display: flex, left: 0, width: 100%

================================================================================
5. SIDEBAR STRUCTURE (DESKTOP ONLY)
================================================================================

Cấu trúc flexbox column:
- Logo: Cố định ở trên cùng (flex-shrink: 0)
- sidebar-content: Phần menu ở giữa, có thể scroll (flex: 1, overflow-y: auto)
  + Menu items: menu01 (Trang chủ), menu02 (Quản lý hồ sơ, có submenu), menu03-menu07, menu08 (Hệ thống, có submenu)
  + Mỗi menu item bọc trong canAccess() check (xem section 62)
- Submenu divs: nằm ngoài sidebar-content, trực tiếp trong .sidebar
  + menu02-submenu (L2), menu02_01-submenu (L3), menu08-submenu (L2), menu08_01-submenu (L3)
- language-section (menu09): Cố định ở dưới (flex-shrink: 0)
- theme-section (menu10): Cố định ở dưới cùng (flex-shrink: 0)

CSS:
.sidebar {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 250px;
  background: #1a2f1c;
  border-right: 1px solid #2d4a30;
  overflow: hidden;
  display: none;
  z-index: 9998;
  flex-direction: column;
}

.sidebar-content {
  flex: 1;
  overflow-y: auto;
  padding: 20px 0;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: rgba(45, 74, 48, 0.9) transparent;
}

================================================================================
6. COLOR PALETTE / THEME
================================================================================

PRIMARY COLORS:
- Background chính: #0d1b0f (rất tối xanh đen)
- Background phụ: #1a2f1c (xanh lục đậm)
- Border: #2d4a30 (xanh lục trung bình)
- Text chính: #ffffff (trắng)
- Text phụ: #8b9d8d (xám xanh)
- Text sáng: #c8d4c8 (xám sáng)
- Accent/Gold: #ffd700 (vàng gold)

BUTTON COLORS:
- Edit: #4a7c4f (xanh lục)
- View: #2d4a30 (xanh lục đậm) với text vàng
- Download: #5a6d5a (xanh lục xám)
- Delete: #8b3a3a (đỏ)
- Add/Primary: #ffd700 (vàng gold) với text xanh đậm

================================================================================
7. TYPOGRAPHY
================================================================================

Font: Arial, sans-serif

Base font size: 12px

Hierarchy:
- Page title: 18px, font-weight: 600, color: #ffd700
- Table header (th): 10px, font-weight: 600, uppercase, letter-spacing: 0.5px
- Table cell (td): 12px
- Search input: 12px
- Nav item text: 13px
- Menu title: 11px, font-weight: 600, letter-spacing: 1px
- User name: 12px, font-weight: 600
- User role: 10px
- Footer nav: 10px

================================================================================
8. SPACING & DIMENSIONS
================================================================================

Header:
- Height: 60px
- Padding: 10px 15px

Footer (mobile):
- Height: 65px
- Padding: 8px 0

Sidebar:
- Width: 250px
- Logo padding: 20px
- Nav item padding: 12px 20px
- Language section padding: 20px

Content:
- Content padding: 15px
- Table cell padding: 15px
- Search bar gap: 15px
- Pagination padding: 20px

Buttons:
- Action button: 32px x 32px
- Add button: padding 12px 25px
- Page button: padding 8px 15px

================================================================================
9. Z-INDEX LAYERING
================================================================================

- Popup Notice / Confirm Dialog: 10001 (CAO NHẤT)
- Header Submenu: 10000
- Header/Footer: 9999
- Sidebar: 9998
- Container: auto (dưới header/footer)

Chi tiết xem section 39. Z-INDEX CẤP HẠCH

================================================================================
10. COMPONENT STYLES
================================================================================

TABLE:
- Background: #1a2f1c
- Border radius: 12px
- Border: 1px solid #2d4a30
- Header background: #2d4a30
- Row hover: #243d27

INPUT FIELDS:
- Background: #1a2f1c
- Border: 1px solid #2d4a30
- Border radius: 8px
- Focus border: #ffd700
- Placeholder color: #5a6d5a

NAV ITEMS:
- Default color: #8b9d8d
- Hover color: #c8d4c8
- Active color: #ffd700
- Active background: #2d4a30
- Active border-right: 3px solid #ffd700

FOOTER SUBMENU:
- Header text color: #ffffff (trắng)
- Submenu item active: background #2d4a30, color #ffd700, border 1px solid #9E9144

================================================================================
11. SCROLLBAR STYLING
================================================================================

Container và sidebar-content:
- overscroll-behavior: contain
- scrollbar-width: thin (Firefox)
- scrollbar-color: rgba(45, 74, 48, 0.9) transparent (Firefox)

================================================================================
12. HTML STRUCTURE
================================================================================

index.php:
<body>
  <?php include 'pages/control_header.php'; ?>

  <div class="container">
    <?php include 'pages/control_menu.php'; ?>
    <?php include 'pages/home.php'; ?>
  </div>

  <?php include 'pages/control_footer.php'; ?>
</body>

Lưu ý:
- Footer nằm NGOÀI container (quan trọng cho iOS)
- Không có header duplicate trong main-content
- Sidebar nằm TRONG container

================================================================================
13. ERROR PAGE ROUTING
================================================================================

ROUTING LOGIC (trong index.php):
- Xử lý $_GET['pages'] để routing đến các trang khác nhau
- Mặc định: 'home' nếu không có parameter
- Error page: 'error' với $_GET['type'] để xác định loại lỗi

ERROR PAGE TYPES:
- ?pages=error&type=404 → Trang không tồn tại
- ?pages=error&type=403 → Không đủ quyền truy cập
- ?pages=error → Lỗi mặc định (500)

VARIABLES SET TRONG INDEX.PHP:
- $page: trang hiện tại (từ $_GET['pages'] hoặc 'home')
- $error_page: boolean flag (true nếu $page === 'error')
- $error_type: loại lỗi (từ $_GET['type'] hoặc 'default')
- $error_code: mã lỗi (404, 403, 500)
- $error_icon: icon Font Awesome (fa-exclamation-circle, fa-ban, fa-exclamation-triangle)
- $error_title: tiêu đề lỗi (từ $lang key)
- $error_message: mô tả lỗi (từ $lang key)

LAYOUT CHO ERROR PAGE:
- Header: luôn hiển thị
- Sidebar: luôn hiển thị (giống trang thường)
- Container: hiển thị error.php nếu $error_page = true, hiển thị home.php nếu false
- Footer: luôn hiển thị

HTML STRUCTURE (index.php):
  <div class="container">
    <?php include 'pages/control_menu.php'; ?>
    <?php if ($error_page): ?>
      <?php include 'pages/error.php'; ?>
    <?php else: ?>
      <?php include 'pages/home.php'; ?>
    <?php endif; ?>
  </div>

LANGUAGE KEYS (message_vn.php & message_eng.php):
- error_default_title: 'Đã xảy ra lỗi' / 'An error occurred'
- error_default_message: 'Trang bạn yêu cầu không thể hiển thị. Vui lòng thử lại sau.' / 'The page you requested cannot be displayed. Please try again later.'
- error_not_found_title: 'Trang không tồn tại' / 'Page not found'
- error_not_found_message: 'Trang bạn tìm kiếm không tồn tại hoặc đã bị xóa. Vui lòng kiểm tra lại đường dẫn.' / 'The page you are looking for does not exist or has been removed. Please check the URL.'
- error_forbidden_title: 'Không đủ quyền truy cập' / 'Access denied'
- error_forbidden_message: 'Bạn không có quyền truy cập trang này. Vui lòng liên hệ quản trị viên nếu cần hỗ trợ.' / 'You do not have permission to access this page. Please contact your administrator for assistance.'
- error_back_home: 'Về trang chủ' / 'Back to Home'
- error_go_back: 'Quay lại' / 'Go Back'

CSS (pages.css):
- .error-page: flex container center content, min-height calc(100vh - 60px)
- .error-card: background var(--bg-secondary), border var(--border), border-radius 12px, padding 40px 30px, max-width 500px
- .error-card::before: radial-gradient vàng nhạt background effect
- .error-watermark: mã lỗi lớn mờ ở background (font-size 120px, opacity 0.1)
- .error-icon-ring: icon với animation pulse
- .error-ring: border 2px solid var(--accent), border-radius 50%, animation error-ring-pulse
- .error-title: 20px, font-weight 600, color var(--text-primary)
- .error-message: 13px, color var(--text-secondary), line-height 1.6
- .error-actions: flex, gap 15px, center
- .error-back-btn: background var(--accent), color var(--bg-secondary), padding 10px 20px, border-radius 6px
- .error-back-secondary: background var(--bg-primary), border 1px solid var(--border)
- Responsive: mobile padding giảm, buttons flex-direction column

QUY TẮC:
1. Error page luôn hiển thị sidebar (không ẩn như login page)
2. Mọi text error page dùng $lang key, KHÔNG hardcode
3. CSS dùng CSS variables để hỗ trợ theme switching
4. Icon error dùng Font Awesome (fa-exclamation-circle, fa-ban, fa-exclamation-triangle)
5. Nút "Về trang chủ" link về index.php, nút "Quay lại" dùng JavaScript history.back()
6. Error 403 có thể hiển thị TRONG page content (không cần redirect) bằng cách include error.php với biến $error_code, $error_icon, $error_title, $error_message set trước include
   Ví dụ (trong page.php):
     $user_perm_id = isset($_SESSION['user_permission_id']) ? (int)$_SESSION['user_permission_id'] : 0;
     $access_check_json = @file_get_contents(__DIR__ . '/../config/access.json');
     $access_check_data = $access_check_json ? json_decode($access_check_json, true) : null;
     $allowed_levels = [];
     if ($access_check_data && isset($access_check_data['permissions'][$menu_perm_key])) {
       $allowed_levels = array_map('intval', explode(',', $access_check_data['permissions'][$menu_perm_key]));
     }
     if (!in_array($user_perm_id, $allowed_levels)) {
       $error_code = '403'; $error_icon = 'fa-ban';
       $error_title = $lang['error_forbidden_title'];
       $error_message = $lang['error_forbidden_message'];
       include __DIR__ . '/error.php';
       return;
     }

================================================================================
15. TRANSITIONS & ANIMATIONS
================================================================================

- Nav items: transition: all 0.3s
- Buttons: transition: all 0.3s
- Action buttons hover: transform: scale(1.1)
- Add button hover: background-color: #ffaa00
- Page button hover: background-color: #3d5a40
- Sidebar: transition: width 0.3s ease

================================================================================
16. ICONS
================================================================================

Sử dụng Font Awesome (fas):
- Header: fa-bell, fa-ellipsis-v, fa-bars
- Sidebar: fa-home, fa-user-cog, fa-user-shield, fa-tasks, fa-palette, fa-table, fa-database, fa-cog, fa-caret-down, fa-shield-alt (menu08_01 Quyền truy cập)
- Footer: fa-home, fa-user-cog, fa-user-shield, fa-tasks, fa-palette, fa-shield-alt (system submenu)
- Permission page: fa-shield-alt (page title icon), fa-save, fa-undo
- Action buttons: fa-edit, fa-eye, fa-download, fa-trash, fa-chevron-left, fa-chevron-right

================================================================================
17. BEST PRACTICES
================================================================================

1. Luôn sử dụng box-sizing: border-box
2. Sử dụng flexbox cho layout
3. Fixed positioning cho header, sidebar, footer
4. Responsive với mobile-first approach
5. Giữ consistency trong màu sắc và spacing
6. Sử dụng z-index đúng thứ tự
7. Scroll chỉ áp dụng cho container và sidebar-content
8. Logo và language-section trong sidebar luôn cố định
9. Footer chỉ hiển thị trên mobile
10. Footer phải nằm NGOÀI container (quan trọng cho iOS)
11. Sử dụng include PHP để modular code
12. Tách CSS theo nguyên tắc modular (base, layout, components, pages)
13. collapse-btn chỉ hiển thị trên desktop (display: none trong mobile media query)
14. Test trên iOS để đảm bảo compatibility

================================================================================
18. iOS-SPECIFIC FIXES
================================================================================

Viewport meta tag:
- Thêm: viewport-fit=cover, maximum-scale=1.0, user-scalable=no

Footer CSS:
- Thêm: -webkit-overflow-scrolling: touch
- Thêm: -webkit-transform: translateZ(0) (hardware acceleration)
- Thêm: backface-visibility: hidden
- Thêm: padding-bottom: calc(8px + env(safe-area-inset-bottom)) (cho iPhone X+)

Body CSS:
- Thêm: -webkit-overflow-scrolling: touch
- Thêm: overflow-x: hidden

iOS media query:
- @supports (-webkit-touch-callout: none) cho iOS-specific styles
- display: flex !important cho footer trong mobile media query

================================================================================
19. MENU ID NAMING CONVENTION
================================================================================

Quy ước đặt tên ID cho menu/submenu 3 cấp:

CẤU TRÚC: menuXX_YY_ZZ
- menu: Tiền tố cố định
- XX: Cấp 1 (01, 02, 03...)
- YY: Cấp 2 (01, 02, 03...) - optional
- ZZ: Cấp 3 (01, 02, 03...) - optional

VÍ DỤ:
- menu01: Trang chủ (cấp 1)
- menu02: Quản lý hồ sơ (cấp 1)
- menu02_01: Khai báo trong Quản lý hồ sơ (cấp 2)
- menu02_02: Thao tác trong Quản lý hồ sơ (cấp 2, có submenu cấp 3)
- menu02_02_01: Công văn đi trong Thao tác (cấp 3)
- menu02_02_02: Công văn đến trong Thao tác (cấp 3)
- menu05_01: Giao diện trong menu Khác (cấp 2)
- menu05_06: Ngôn ngữ trong menu Khác (cấp 2)
- menu05_06_01: VN trong Ngôn ngữ (cấp 3)
- menu05_06_02: ENG trong Ngôn ngữ (cấp 3)

ÁP DỤNG:
- control_menu.php: menu01-08 cho menu sidebar, menu09 cho phần ngôn ngữ, menu10 cho phần giao diện
- control_footer.php: menu01-05 cho footer nav, menu02_XX cho submenu Quản lý hồ sơ, menu05_XX cho submenu Khác
- menu08: Hệ thống (cấp 1, có submenu)
- menu08_01: Quyền truy cập (cấp 2, link trực tiếp ?pages=permission)
- menu08_02: Mục hệ thống 2 (cấp 2)
- Footer: menu05_05 (Hệ thống) → system-submenu → menu08_01 (Quyền truy cập)

================================================================================
20. LANGUAGE SYNCHRONIZATION (ĐỒNG BỘ NGÔN NGỮ)
================================================================================

CẤU TRÚC FILE:
- lang/message_vn.php: File ngôn ngữ tiếng Việt
- lang/message_eng.php: File ngôn ngữ tiếng Anh

CẢ HAI FILE CÙNG KEY, chỉ khác giá trị. Ví dụ:
  message_vn.php:  'menu01' => 'Trang chủ',
  message_eng.php: 'menu01' => 'Home',

SESSION HANDLING (trong index.php):
- session_start() ở đầu file
- $_SESSION['lang'] lưu ngôn ngữ hiện tại ('vn' hoặc 'eng')
- $_GET['lang'] xử lý thay đổi ngôn ngữ → lưu session → redirect về trang gốc
- Mặc định: 'vn' nếu chưa set session

LOAD NGÔN NGỮ (trong index.php):
  $current_lang = isset($_SESSION['lang']) ? $_SESSION['lang'] : 'vn';
  if ($current_lang === 'eng') {
    include 'lang/message_eng.php';
  } else {
    include 'lang/message_vn.php';
  }

ÁP DỤNG TRONG CÁC FILE PHP:
- Dùng <?php echo $lang['key']; ?> thay vì text cố định
- Tất cả text hiển thị phải dùng key từ $lang
- KHÔNG hardcode text tiếng Việt/Anh trực tiếp trong HTML

NÚT CHUYỂN NGÔN NGỮ (AJAX - KHÔNG RELOAD TRANG):
- Sidebar: <a href="#" class="lang-btn" onclick="event.preventDefault(); changeLanguage('vn')">VN</a>
- Footer submenu: <a href="#" class="footer-submenu-item" onclick="event.preventDefault(); event.stopPropagation(); changeLanguage('eng')">ENG</a>
- Class 'active' theo $current_lang: <?php echo $current_lang === 'vn' ? ' active' : ''; ?>
- changeLanguage() lưu session qua AJAX POST, fetch lang JSON, cập nhật DOM, reload page content
- Fallback: $_GET['lang'] vẫn hoạt động (full page reload) nếu JS không chạy

DATA-LANG-KEY ATTRIBUTE:
- Tất cả element chứa text cần dịch PHẢI có attribute data-lang-key="key_name"
- Ví dụ: <span data-lang-key="menu01"><?php echo $lang['menu01']; ?></span>
- changeLanguage() querySelectorAll('[data-lang-key]') → cập nhật textContent từ lang JSON
- Cho attribute (title, placeholder): thêm data-lang-attr="title"
  Ví dụ: <div data-lang-key="collapse_btn_title" data-lang-attr="title" title="...">

AJAX HANDLERS (index.php - TRƯỚC HTML output):
// Đổi ngôn ngữ (AJAX)
if (isset($_POST['action']) && $_POST['action'] === 'change_lang') {
  $_SESSION['lang'] = $new_lang;
  LoginModule::refreshPermissionName();
  echo json_encode(['success' => true, 'permission_name' => $_SESSION['user_permission'] ?? '']);
  exit;
}
// Lấy ngôn ngữ JSON
if (isset($_GET['ajax_get_lang'])) {
  include lang file;
  echo json_encode($lang);
  exit;
}

HEADER PERMISSION NAME:
- <span id="header-permission-name"><?php echo $_SESSION['user_permission']; ?></span>
- changeLanguage() cập nhật #header-permission-name từ response.permission_name

QUY TẮC ĐẶT TÊN KEY:
- Theo nhóm: header_, menu_, footer_, col_, btn_, pagination_
- Theo menu ID: menu01, menu02, footer_menu05_01...
- Uppercase cho header/title: footer_submenu_header, footer_lang_header
- Ví dụ:
  + 'collapse_btn_title' => 'Thu gọn' / 'Collapse'
  + 'menu01' => 'Trang chủ' / 'Home'
  + 'menu02_01' => 'Khai báo' / 'Declaration'
  + 'col_number' => 'SỐ HIỆU' / 'NUMBER'
  + 'btn_edit' => 'Sửa' / 'Edit'
  + 'pagination_records' => 'bản ghi' / 'records'

KHI THÊM TEXT MỚI:
1. Thêm key vào CẢ HAI file message_vn.php và message_eng.php
2. Dùng <?php echo $lang['key']; ?> trong file PHP tương ứng
3. Giữ key nhất quán với quy tắc đặt tên

================================================================================
21. SIDEBAR SUBMENU
================================================================================

MENU CÓ SUBMENU:
- Menu có submenu dùng class "has-submenu" và attribute "data-submenu"
- Ví dụ: <a href="#" id="menu02" class="nav-item has-submenu" data-submenu="menu02-submenu">

CẤU TRÚC HTML:
- Submenu nằm ngoài sidebar-content, trực tiếp trong .sidebar
- Dùng class "sidebar-submenu" với id khớp với data-submenu
- Tất cả span chứa text phải có data-lang-key để đổi ngôn ngữ không reload
- Ví dụ:
  <div id="menu02-submenu" class="sidebar-submenu">
    <a href="#" id="menu02_01" class="submenu-item">
      <i class="fas fa-file-signature"></i>
      <span data-lang-key="menu02_01"><?php echo $lang['menu02_01']; ?></span>
    </a>
  </div>

SUBMENU CẤP 3:
- Submenu-item có submenu dùng class "has-submenu" và attribute "data-submenu"
- Thêm icon fa-caret-right bên phải
- Submenu cấp 3 dùng class "sidebar-submenu level3"
- Ví dụ:
  <a href="#" id="menu02_01" class="submenu-item has-submenu" data-submenu="menu02_01-submenu">
    <i class="fas fa-file-signature"></i>
    <span data-lang-key="menu02_01"><?php echo $lang['menu02_01']; ?></span>
    <i class="fas fa-caret-right"></i>
  </a>
  ...
  <div id="menu02_01-submenu" class="sidebar-submenu level3">
    <a href="#" id="menu02_01_01" class="submenu-item">
      <i class="fas fa-building"></i>
      <span data-lang-key="menu02_01_01"><?php echo $lang['menu02_01_01']; ?></span>
    </a>
  </div>

CSS SIDEBAR SUBMENU:
- Level 2: position: fixed, left: 255px (bên phải sidebar)
- Level 3: position: fixed, left: 460px (bên phải submenu cấp 2)
- Sidebar collapsed: level 2 left: 95px, level 3 left: 300px
- Class .show để hiển thị
- Submenu-item: display flex, gap 10px, icon + text
- Icon submenu-item: width 16px, font-size 14px
- Hover/Active: background #2d4a30, color #ffd700
- fa-caret-right: rotate 90deg khi open

JAVASCRIPT:
- Click .nav-item.has-submenu → đóng tất cả submenu khác → mở submenu cấp 2
- Click .submenu-item.has-submenu → đóng tất cả submenu cấp 3 → mở submenu cấp 3
- Tính vị trí submenu dựa trên getBoundingClientRect() của menu item
- Scroll sidebar-content → cập nhật lại vị trí submenu đang mở (cả cấp 2 và 3)
- Click outside → đóng tất cả sidebar-submenu (cả cấp 2 và 3)

================================================================================
22. FOOTER SUBMENU
================================================================================

MENU CÓ SUBMENU TRONG FOOTER:
- Menu có submenu dùng <div> thay vì <a>, thêm class riêng (more-menu, profile-menu)
- Ví dụ: <div id="menu02" class="footer-nav-item profile-menu">

CẤU TRÚC HTML:
- Footer-submenu nằm trực tiếp trong menu item (sibling, KHÔNG lồng nhau)
- Submenu ngôn ngữ tách riêng dùng class "language-submenu"
- Submenu giao diện tách riêng dùng class "theme-submenu"
- Submenu hệ thống tách riêng dùng class "system-submenu"
- Tất cả span chứa text phải có data-lang-key để đổi ngôn ngữ không reload
- Ví dụ:
  <div id="menu05" class="footer-nav-item more-menu<?php echo $active_l1 === 'menu08' ? ' active' : ''; ?>">
    <div class="footer-submenu">...MENU KHÁC...</div>
    <div class="footer-submenu system-submenu">...HỆ THỐNG (menu08_01 Quyền truy cập)...</div>
    <div class="footer-submenu language-submenu">...NGÔN NGỮ (onclick changeLanguage)...</div>
    <div class="footer-submenu theme-submenu">...GIAO DIỆN...</div>
  </div>

FOOTER ACTIVE STATE CHO TRANG QUYỀN TRUY CẬP:
- menu05 (Khác): thêm active class khi $active_l1 === 'menu08'
- menu05_05 (Hệ thống): thêm active class khi $active_l1 === 'menu08'
- menu08_01 (Quyền truy cập): thêm active class khi $active_l2 === 'menu08_01'
- JS updateSidebarActive: menu08 L1 → footer menu05 (L1), menu08_01 L2 → footer menu05_05 (L2)

THEME SUBMENU:
- Menu item: menu05_07 (Giao diện/Appearance) với class "theme-menu"
- Theme options: menu05_07_01 đến menu05_07_05 với class "theme-option"
- Mỗi theme option có attribute data-theme="green|blue|purple|dark|monochrome"
- Theme option có color preview: <span class="theme-preview"></span> với inline style --theme-preview
- Màu preview: green (#4a7c4f), blue (#3b6a8c), purple (#6b4a8c), dark (#2a2a2a), monochrome (gradient)
- Active state dựa trên $current_theme: <?php echo $current_theme === 'green' ? ' active' : ''; ?>

CSS FOOTER SUBMENU:
- position: fixed, bottom: 70px, left: 5px, right: 5px
- Class .show để hiển thị với animation slideUp (0.25s ease)
- Animation: opacity 0 → 1, translateY(20px) → 0
- Header: text-transform uppercase, color white, border-bottom #2d4a30
- Item active: background #2d4a30, color #ffd700, border 1px solid #9E9144
- Close button (.close-submenu): xóa class .show trên submenu cha
- Theme preview: width 20px, height 20px, border-radius 50%, background var(--theme-preview), flex-shrink 0

JAVASCRIPT:
- Click menu item → đóng tất cả footer-submenu → toggle submenu hiện tại
- Click language-menu → đóng MENU KHÁC → mở NGÔN NGỮ
- Click theme-menu → đóng MENU KHÁC → mở GIAO DIỆN
- Click system-menu (menu05_05) → đóng MENU KHÁC → mở HỆ THỐNG (system-submenu)
- toggleSystemMenu(): đóng MENU KHÁC + language/theme submenu → mở system-submenu
- Click theme-option → thay đổi body class (theme-{name}), update active, save session via AJAX
- Click outside (không thuộc more-menu, profile-menu, language-menu, theme-menu) → đóng tất cả
- Click close-submenu → đóng submenu tương ứng

NGÔN NGỮ:
- Thêm key: footer_menu05_07, footer_theme_header vào message_vn.php và message_eng.php
- footer_menu05_07: 'Giao diện' / 'Appearance'
- footer_theme_header: 'GIAO DIỆN' / 'THEME'

================================================================================
23. SESSION PERSISTENCE (LƯU TRẠNG THÁI)
================================================================================

SIDEBAR COLLAPSED:
- $_SESSION['sidebar_collapsed'] lưu trạng thái thu gọn sidebar (true/false)
- AJAX endpoint: POST action=toggle_sidebar&collapsed=true/false
- Khi load trang: $sidebar_collapsed từ session → thêm class sidebar-collapsed vào body
- Icon collapse-btn đổi theo trạng thái (fa-chevron-left/fa-chevron-right)

LANGUAGE:
- $_SESSION['lang'] lưu ngôn ngữ ('vn' hoặc 'eng')
- GET ?lang=vn hoặc ?lang=eng → lưu session → redirect

================================================================================
24. INPUT TEMPLATE TYPES (CÁC KIỂU INPUT MẪU)
================================================================================

SECTION WRAPPER:
- Class: input-template-section
- Header: input-template-header (click toggle ẩn/hiện)
- Content: input-template-content (id="inputTemplateContent")
- Toggle JS: toggleInputTemplate() → toggle class "collapsed" trên section
- Collapsed: .input-template-section.collapsed → ẩn content, xoay icon chevron -90deg

LAYOUT:
- Grid: input-template-grid
- Desktop (min-width: 769px): 2 cột (grid-template-columns: 1fr 1fr)
- Mobile (max-width: 768px): 1 cột (grid-template-columns: 1fr)
- Gap: 20px

CÁC TYPE INPUT:

TYPE 1 - Text Input (văn bản):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type1_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper">
      <i class="fas fa-font input-icon"></i>
      <input type="text" class="form-input" placeholder="<?php echo $lang['type1_placeholder']; ?>">
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .form-input padding 12px 12px 12px 40px (40px left cho icon)
- Icon: fa-font, position absolute left 12px

TYPE 2 - Custom Select Dropdown (không có search):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type2_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="custom-select-wrapper">
      <div class="input-wrapper">
        <i class="fas fa-list input-icon"></i>
        <input type="text" class="form-input custom-select-input" placeholder="<?php echo $lang['type2_placeholder']; ?>" readonly>
        <i class="fas fa-chevron-down dropdown-arrow"></i>
      </div>
      <div class="custom-select-dropdown">
        <div class="custom-select-options">
          <div class="custom-select-option" data-value=""><?php echo $lang['type2_placeholder']; ?></div>
          <div class="custom-select-option" data-value="1"><?php echo $lang['type2_option1']; ?></div>
        </div>
      </div>
    </div>
    <input type="hidden" name="xxx" id="xxx">
    <div class="field-error"></div>
  </div>
- CSS: .custom-select-wrapper position relative, .custom-select-dropdown absolute với animation slideDown
- Options: .custom-select-option với hover/selected states, sử dụng CSS variables
- JS: Toggle dropdown, select option, close when click outside
- Icon: fa-list (input-icon), fa-chevron-down (dropdown-arrow)

QUAN TRỌNG: Click-outside-to-close handler:
- Nếu dropdown nằm TRONG popup (.popup-container), PHẢI nghe trên .popup-container thay vì document
- Lý do: .popup-container có onclick="event.stopPropagation()" → chặn event bubble lên document → document.addEventListener('click',...) không bao giờ chạy
- Giải pháp: var closeTarget = wrapper.closest('.popup-container') || document;
  closeTarget.addEventListener('click', function(e) { if (!wrapper.contains(e.target)) wrapper.classList.remove('open'); });
- Nếu dropdown nằm NGOÀI popup (như rows-per-page), dùng document như bình thường

QUAN TRỌNG: Hidden input là SIBLING của .custom-select-wrapper (ngang cấp), KHÔNG phải con của parent.
- Selector SAI: wrapper.parentElement.querySelector('input[type="hidden"]')
- Selector ĐÚNG: wrapper.nextElementSibling
- Nếu dùng generic handler, phải check: if (hiddenInput && hiddenInput.type !== 'hidden') hiddenInput = null;

TYPE 3 - Textarea (ghi chú):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type3_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper">
      <i class="fas fa-sticky-note input-icon"></i>
      <textarea class="form-textarea" placeholder="<?php echo $lang['type3_placeholder']; ?>"></textarea>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .form-textarea
  + padding: 12px 12px 12px 40px
  + height: 42px (BẮT BUỘC dùng height, KHÔNG dùng min-height để bằng input text)
  + box-sizing: border-box (BẮT BUỘC để height bao gồm padding)
  + line-height: 18px
  + resize: vertical
- Icon: fa-sticky-note, position absolute left 12px

TYPE 4 - Custom Date Picker:
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type4_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="custom-date-wrapper">
      <div class="input-wrapper">
        <i class="fas fa-calendar input-icon"></i>
        <input type="text" class="form-input custom-date-input" placeholder="<?php echo $lang['type4_placeholder']; ?>" readonly>
        <i class="fas fa-chevron-down dropdown-arrow"></i>
      </div>
      <div class="custom-date-dropdown">
        <div class="date-picker-header">
          <div class="date-nav-year">
            <button class="date-nav-btn date-prev-year" title="Năm trước">
              <i class="fas fa-minus"></i>
            </button>
            <button class="date-nav-btn date-next-year" title="Năm sau">
              <i class="fas fa-plus"></i>
            </button>
          </div>
          <span class="date-current-month"></span>
          <div class="date-nav-month">
            <button class="date-nav-btn date-prev-month" title="Tháng trước">
              <i class="fas fa-chevron-left"></i>
            </button>
            <button class="date-nav-btn date-next-month" title="Tháng sau">
              <i class="fas fa-chevron-right"></i>
            </button>
          </div>
        </div>
        <div class="date-picker-weekdays">
          <span>CN</span>
          <span>T2</span>
          <span>T3</span>
          <span>T4</span>
          <span>T5</span>
          <span>T6</span>
          <span>T7</span>
        </div>
        <div class="date-picker-days"></div>
      </div>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .custom-date-wrapper position relative, .custom-date-dropdown absolute với animation slideDown
- Calendar header: flex space-between với date-nav-year (bên trái), date-current-month (giữa), date-nav-month (bên phải)
- date-nav-year: flex với gap 4px, buttons +/- để điều chỉnh năm
- date-nav-month: flex với gap 4px, buttons chevron-left/right để điều chỉnh tháng
- Weekdays: grid 7 cột (CN-T7)
- Days: grid 7 cột, click để chọn ngày
- States: other-month (mờ), today (highlight), selected (màu accent)
- Footer: date-picker-footer với 2 buttons (Hôm nay / Clear), border-top, flex space-between
- date-today-btn: hover background var(--btn-view), color var(--accent)
- date-clear-btn: hover background #8b3a3a (đỏ), color white
- JS: Toggle dropdown, render calendar, navigation năm (+/-), navigation tháng (chevron), select ngày, format dd/mm/yyyy
- Click-outside-to-close: áp dụng cùng quy tắc như TYPE 2 (nghe trên .popup-container nếu trong popup)
- Nút Hôm nay: chọn ngày hiện tại, đóng dropdown
- Nút Clear: xóa ngày đã chọn, xóa data-selected-date, đóng dropdown
- Giá trị thực lưu trong data-selected-date (ISO string)
- Icon: fa-calendar (input-icon), fa-chevron-down (dropdown-arrow), fa-minus/plus (year nav), fa-chevron-left/right (month nav)

TYPE 5 - Searchable Select Dropdown (có search input):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type5_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="searchable-select-wrapper">
      <div class="input-wrapper">
        <i class="fas fa-search input-icon"></i>
        <input type="text" class="form-input searchable-select-input" placeholder="<?php echo $lang['type5_placeholder']; ?>" readonly>
        <i class="fas fa-chevron-down dropdown-arrow"></i>
      </div>
      <div class="searchable-select-dropdown">
        <div class="searchable-select-search">
          <input type="text" class="searchable-select-search-input" placeholder="<?php echo $lang['type5_search_placeholder']; ?>">
        </div>
        <div class="searchable-select-options">
          <div class="searchable-select-option" data-value="1"><?php echo $lang['type5_option1']; ?></div>
        </div>
      </div>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .searchable-select-wrapper position relative, .searchable-select-dropdown absolute với animation slideDown
- Search input: .searchable-select-search-input với styling riêng
- Options: .searchable-select-option với hover/selected/hidden states, sử dụng CSS variables
- JS: Toggle dropdown, search filter (hỗ trợ tiếng Việt không dấu), select option, close when click outside
- Click-outside-to-close: áp dụng cùng quy tắc như TYPE 2 (nghe trên .popup-container nếu trong popup)
- Hàm removeVietnameseDiacritics(): map đầy đủ ký tự tiếng Việt, chuyển có dấu → không dấu
- Icon: fa-search (input-icon), fa-chevron-down (dropdown-arrow)

TYPE 6 - Text Input with Action Buttons (readonly):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type6_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper input-with-actions">
      <i class="fas fa-image input-icon"></i>
      <input type="text" class="form-input input-with-buttons" placeholder="<?php echo $lang['type6_placeholder']; ?>" readonly>
      <div class="input-action-buttons">
        <button class="input-action-btn btn-view" title="<?php echo $lang['type6_btn_view']; ?>">
          <i class="fas fa-eye"></i>
        </button>
        <button class="input-action-btn btn-update" title="<?php echo $lang['type6_btn_update']; ?>">
          <i class="fas fa-sync-alt"></i>
        </button>
      </div>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .input-with-actions position relative, .input-with-buttons padding-right 70px
- Action buttons: .input-action-buttons absolute right 8px, center vertical, gap 5px
- Button: 28x28px, border-radius 6px, transition 0.3s, hover scale(1.1)
- btn-view: background var(--btn-view), color var(--accent)
- btn-update: background var(--btn-edit), color white
- Input readonly: cursor pointer, opacity 0.85, focus border var(--border) (không highlight vàng)
- Icon: fa-image (input-icon), fa-eye (btn-view), fa-sync-alt (btn-update)

TYPE 7 - Numeric Input with Formatting (XXX.XXX.XXX):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type7_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper">
      <i class="fas fa-hashtag input-icon"></i>
      <input type="text" class="form-input numeric-formatted" placeholder="<?php echo $lang['type7_placeholder']; ?>" maxlength="11">
    </div>
    <div class="field-error"></div>
  </div>
- CSS: dùng chung .form-input
- JS: Chỉ cho phép nhập số, giới hạn tối đa 9 chữ số
- Format hiển thị: XXX.XXX.XXX (ví dụ: 123.456.789)
- Giá trị thực tế lưu trong attribute data-raw-value (không có dấu chấm: 123456789)
- maxlength="11" để chừa chỗ cho 2 dấu chấm (9 số + 2 dấu chấm = 11 ký tự)
- Paste: tự động loại bỏ ký tự không phải số và format lại
- Icon: fa-hashtag (input-icon)

TYPE 8 - Numeric Input with Stepper Buttons (+/-):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type8_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper input-with-stepper">
      <i class="fas fa-sort-numeric-up input-icon"></i>
      <input type="number" class="form-input input-with-stepper-input" placeholder="<?php echo $lang['type8_placeholder']; ?>" min="0" step="1">
      <div class="stepper-buttons">
        <button class="stepper-btn stepper-up" title="Tăng">
          <i class="fas fa-plus"></i>
        </button>
        <button class="stepper-btn stepper-down" title="Giảm">
          <i class="fas fa-minus"></i>
        </button>
      </div>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .input-with-stepper position relative, display flex, align-items center
- .input-with-stepper-input: padding-right 80px
- Ẩn mũi tên mặc định: appearance: none, -webkit-appearance: none, -moz-appearance: textfield
- Stepper buttons: .stepper-buttons absolute right 8px, center vertical, flex row (không phải column), gap 4px
- Button: 28x28px, border-radius 6px, transition 0.3s, hover background var(--btn-edit)
- Active: transform scale(0.95)
- JS: Click up button tăng giá trị theo step, click down button giảm giá trị theo step
- Giới hạn min theo attribute min (mặc định 0)
- Input validation: chỉ cho phép số, dấu thập phân, dấu âm ở đầu
- Icon: fa-sort-numeric-up (input-icon), fa-plus (stepper-up), fa-minus (stepper-down)

TYPE 9 - Text Input with AUTO Button:
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type9_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="input-wrapper input-with-auto">
      <i class="fas fa-font input-icon"></i>
      <input type="text" class="form-input input-with-auto-input" placeholder="<?php echo $lang['type9_placeholder']; ?>">
      <button class="auto-button" title="Tự động điền"><?php echo $lang['type9_btn_auto']; ?></button>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .input-with-auto position relative
- .input-with-auto-input: padding-right 80px
- .auto-button: absolute right 8px, top 50%, transform translateY(-50%), padding 6px 12px
- Button: background var(--btn-edit), color white, border-radius 6px, font-size 11px, font-weight 600
- Hover: background #5a9a60, scale 1.05
- Active: scale 0.95
- Icon: fa-font (input-icon)

TYPE 10 - Custom Radio Buttons (2 options):
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type10_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="custom-radio-group">
      <div class="custom-radio-option">
        <input type="radio" id="type10_option1" name="type10_radio" value="option1">
        <label for="type10_option1" class="custom-radio-label"><?php echo $lang['type10_option1']; ?></label>
      </div>
      <div class="custom-radio-option">
        <input type="radio" id="type10_option2" name="type10_radio" value="option2">
        <label for="type10_option2" class="custom-radio-label"><?php echo $lang['type10_option2']; ?></label>
      </div>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .custom-radio-group display flex, gap 15px
- .custom-radio-option: flex 1, position relative
- Radio input: opacity 0, width 0, height 0 (ẩn mặc định)
- .custom-radio-label: display flex, align-items center, justify-content center, padding 12px 20px
- Label style: background var(--bg-primary), border 1px solid var(--border), border-radius 8px
- Hover: background var(--bg-secondary), border-color var(--text-light), color var(--text-light)
- Checked: background var(--btn-edit), border-color var(--btn-edit), color white
- Focus: border-color var(--accent), box-shadow 0 0 0 2px rgba(255, 215, 0, 0.2)
- Không có icon (radio button được style thành dạng button)

TYPE 11 - Toggle Switch:
- HTML:
  <div class="input-field">
    <div class="input-label-row">
      <label><?php echo $lang['type11_label']; ?></label>
      <i class="fas fa-check-circle validate-success"></i>
      <i class="fas fa-exclamation-circle validate-error"></i>
    </div>
    <div class="toggle-switch-wrapper">
      <label class="toggle-switch">
        <input type="checkbox" id="type11_toggle">
        <span class="toggle-slider"></span>
      </label>
      <span class="toggle-label" id="type11_label_text"><?php echo $lang['type11_option_off']; ?></span>
    </div>
    <div class="field-error"></div>
  </div>
- CSS: .toggle-switch-wrapper display flex, align-items center, gap 15px
- .toggle-switch: position relative, width 50px, height 26px
- Checkbox input: opacity 0, width 0, height 0 (ẩn mặc định)
- .toggle-slider: position absolute, cursor pointer, top 0, left 0, right 0, bottom 0
- Slider style: background var(--bg-primary), border 1px solid var(--border), border-radius 26px
- .toggle-slider:before: position absolute, content "", height 20px, width 20px, left 2px, bottom 2px
- Circle style: background var(--text-secondary), border-radius 50%, transition all 0.3s
- Checked: background var(--btn-edit), border-color var(--btn-edit)
- Checked circle: transform translateX(24px), background white
- Focus: border-color var(--accent), box-shadow 0 0 0 2px rgba(255, 215, 0, 0.2)
- .toggle-label: font-size 12px, color var(--text-light), font-weight 500
- JS: Event listener change để cập nhật label text (Bật/Tắt hoặc On/Off)
- Không có icon (toggle switch được style iOS-style)

CSS CHUNG CHO INPUT:
- .input-field: flex column, gap 8px
- .input-label-row: display flex, justify-content space-between, align-items center, margin-bottom 6px, position relative
- .input-field label: 12px, font-weight 600, uppercase, letter-spacing 0.5px, color var(--text-light)
- .validate-success: font-size 14px, color var(--border), opacity 0, transition 0.3s, position absolute, right 0
- .validate-success.show: color #4caf50 (xanh lá), opacity 1
- .validate-error: font-size 14px, color var(--border), opacity 0, transition 0.3s, position absolute, right 0
- .validate-error.show: color #ef5350 (đỏ), opacity 1
- .field-error: font-size 11px, color #ef5350 (đỏ), min-height 16px, padding-top 4px, display none
- .field-error.show: display block
- .input-wrapper: position relative, flex, align-items center
- .input-icon: absolute left 12px, color var(--text-secondary), font-size 14px, z-index 1
- .form-input: width 100%, padding 12px 12px 12px 40px, border-radius 8px, color var(--text-primary)
  + background: var(--bg-primary), border: 1px solid var(--border)
  + focus: border-color var(--accent)
  + placeholder: color var(--text-secondary)
  + readonly: cursor pointer, opacity 0.85, focus border var(--border)
- .dropdown-arrow: absolute right 12px, color var(--text-secondary), font-size 14px, pointer-events none, transition 0.3s
- .wrapper.open .dropdown-arrow: transform rotate(180deg)

QUY TẮC:
1. Mọi input có icon phải dùng .input-wrapper + .input-icon (absolute left 12px)
2. Padding-left 40px cho input/textarea/select để chừa chỗ icon
3. Textarea phải có height: 42px + box-sizing: border-box để bằng input text
4. Textarea KHÔNG dùng min-height (sẽ gây lỗi chiều cao khác input)
5. Tất cả input dùng CSS variables để hỗ trợ theme switching
6. Label luôn uppercase, dùng $lang key
7. Mọi input field phải có .input-label-row (label + validate-success + validate-error icons)
8. Mọi input field phải có .field-error div để hiển thị lỗi
9. Validate success icon: thêm class .show khi validate thành công (màu xanh lá #4caf50)
10. Validate error icon: thêm class .show khi validate thất bại (màu đỏ #ef5350)
11. Validate icons đều position absolute right 0, overlap nhau (chỉ 1 hiển thị tại 1 thời điểm)
12. Field error: thêm class .show và set textContent khi có lỗi
13. Custom dropdown (TYPE 2, 5) dùng animation slideDown (0.2s ease)
14. Searchable select (TYPE 5) hỗ trợ tiếng Việt không dấu
15. Input readonly (TYPE 6) dùng cursor pointer, opacity 0.85
16. Numeric formatted input (TYPE 7): chỉ cho phép số, format XXX.XXX.XXX, giá trị thực trong data-raw-value
17. Numeric stepper input (TYPE 8): buttons +/- horizontal layout, tăng/giảm theo step, giới hạn min, chỉ cho phép số, ẩn mũi tên mặc định
18. Custom date picker (TYPE 4): calendar với navigation, format dd/mm/yyyy, giá trị thực trong data-selected-date
19. Text input with AUTO button (TYPE 9): nút AUTO bên phải, padding-right 80px, background var(--btn-edit)
20. Custom radio buttons (TYPE 10): 2 options styled as buttons, flex layout, checked state màu xanh lá
21. Toggle switch (TYPE 11): iOS-style toggle 50x26px, checkbox ẩn, slider moves right khi checked, cập nhật label text

================================================================================
25. THEME SWITCHING
================================================================================

SESSION:
- $_SESSION['theme'] lưu theme hiện tại ('green', 'blue', 'purple', 'dark', 'monochrome')
- AJAX endpoint: POST action=change_theme&theme={name}
- Mặc định: 'green' nếu chưa set session
- Biến PHP: $current_theme = isset($_SESSION['theme']) ? $_SESSION['theme'] : 'green';
- Body class: theme-{name} (ví dụ: theme-green, theme-blue...)

CÁC THEME:
1. Green (mặc định): nền xanh lục tối, accent vàng gold
2. Blue: nền xanh dương tối, accent vàng gold
3. Purple: nền tím tối, accent vàng gold
4. Dark: nền đen, accent vàng gold
5. Monochrome: nền trắng, text đen, accent đen

CSS VARIABLES (định nghĩa trên body class):
- --bg-primary: màu nền chính (container, input background)
- --bg-secondary: màu nền phụ (header, sidebar, footer, table, card)
- --border: màu viền
- --text-primary: màu text chính (td, input text)
- --text-secondary: màu text phụ (placeholder, role, secondary info)
- --text-light: màu text sáng (th, label, nav item hover)
- --accent: màu nhấn (active, focus border, gold)
- --btn-edit: màu nút edit
- --btn-view: màu nút view / thead background
- --btn-delete: màu nút delete
- --hover: màu hover row

QUY TẮC:
1. TẤT CẢ màu sắc trong CSS phải dùng CSS variables (var(--name))
2. KHÔNG hardcode màu trực tiếp (trừ màu đặc thù không thuộc theme)
3. Mỗi theme mới chỉ cần thêm class .theme-{name} với các CSS variables
4. Thêm button trong control_menu.php với data-theme="{name}"
5. Thêm key ngôn ngữ theme_{name} vào cả message_vn.php và message_eng.php
6. JS theme switching: đổi body class, update active, save session via AJAX

SIDEBAR THEME SECTION (control_menu.php):
- Nằm phía dưới language-section (menu10)
- 4-5 theme buttons dạng tròn (32x32px) với preview màu
- Class: .theme-section, .theme-btn, .theme-preview
- Active: .theme-btn.active với border-color var(--accent) + box-shadow

SIDEBAR COLLAPSED:
- .sidebar.collapsed .theme-section: padding 10px 5px, text-align center
- .sidebar.collapsed .theme-title: display none
- .sidebar.collapsed .theme-btn: 24x24px
- .sidebar.collapsed .theme-preview: 14x14px

================================================================================
26. NOTES
================================================================================

- index.php là file chính gắn kết tất cả các phần
- ưu tiên giao diện mượt, nhẹ
- Khi tạo trang mới content, tạo file mới trong pages/ và include vào index.php
- File test.php giữ làm reference/backup
- Giữ nguyên color palette và typography
- Chỉ thay đổi nội dung trong file content trong pages/
- Không thay đổi layout structure (header, sidebar, container, footer)
- Luôn test trên iOS sau khi thay đổi footer-related CSS
- CSS được tách thành 4 file: base.css, layout.css, components.css, pages.css
- collapse-btn trong components.css không có display: none mặc định, được điều khiển bởi media query trong layout.css

================================================================================
27. LOGIN PAGE
================================================================================

FILE: login.php (standalone, không dùng layout index.php)

CẤU TRÚC HTML:
<body class="login-page theme-{name}">
  <div class="login-wrapper">
    <div class="login-card">
      <div class="card-content">
        <!-- Logo Area -->
        <div class="logo-area">
          <div class="logo-only" aria-hidden="true">
            <div id="login">
              <div class="canvas">
                <div class="icon-wrapper">
                  <div class="icon">
                    <div class="curve1">
                      <div class="curve2">
                        <div class="curve3">
                          <div class="curve4"></div>
                        </div>
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <div class="brand-name">
            <div class="brand-main">$lang['brand_main']</div>
            <div class="brand-subtitle">$lang['brand_subtitle']</div>
          </div>
        </div>

        <!-- Card Header -->
        <div class="login-card-header">
          <i class="fas fa-lock login-header-icon"></i>
          <h1 class="login-card-title">$lang['login_title']</h1>
        </div>

        <!-- Form -->
        <form class="login-form" method="POST">
          <!-- Mã nhân viên (numeric only) -->
          <div class="input-field">...</div>
          <!-- Mật khẩu (with toggle) -->
          <div class="input-field">...</div>
          <!-- Options: Remember me + Forgot password -->
          <div class="login-options">...</div>
          <!-- Login button -->
          <button class="login-btn">...</button>
        </form>

        <!-- Theme & Language switch -->
        <div class="login-theme-switch">
          <div class="theme-title">$lang['login_theme_lang_title']</div>
          <div class="theme-buttons">5 theme buttons</div>
          <div class="login-lang-switch">VN / ENG</div>
        </div>
      </div>
    </div>
  </div>
</body>

SESSION HANDLING (trong login.php):
- session_start() ở đầu file
- $_SESSION['lang'] lưu ngôn ngữ ('vn' hoặc 'eng')
- $_SESSION['theme'] lưu theme ('green', 'blue', 'purple', 'dark', 'monochrome')
- AJAX endpoint: POST action=change_theme&theme={name} → lưu session, trả JSON
- GET ?lang=vn hoặc ?lang=eng → lưu session → redirect về trang gốc
- Mặc định: lang='vn', theme='green'

CSS LOAD (trong login.php):
1. assets/css/all.css (Font Awesome)
2. assets/css/base.css
3. assets/css/layout.css
4. assets/css/components.css
5. assets/css/pages.css

LOGO AREA (từ hr project):
- Cấu trúc nested curves: curve1 > curve2 > curve3 > curve4 (KHÔNG phải sibling)
- ID #login (KHÔNG phải #fpldbp hay #fpldbp-login)
- .canvas: 88x88px, border-radius 50%, radial-gradient + inset box-shadow
- .icon-wrapper: scale(0.34), center positioning
- .icon: drop-shadow filter, animation logo-rotate 6s
- .curve1/.curve3: box-shadow dùng var(--gold-1, #ebe717)
- .curve2/.curve4: box-shadow dùng var(--gold-deep, #c7a900), animation curve-color 3s
- .brand-main: gradient text (linear-gradient #B5A800 → #EBE717 → #C4BD00), animation gradientShift 1s
- .brand-subtitle: gradient text (linear-gradient #B5A800 → #F0EA30 → #C9C200), animation gradientShift 1s

CARD EFFECTS:
- .login-card: position relative, overflow hidden
- .login-card::before: radial-gradient vàng nhạt góc trên-trái (z-index: 0)
- .login-card::after: radial-gradient vàng đậm góc dưới-phải (z-index: 0)
- .card-content: position relative, z-index: 1 (nội dung nằm trên pseudo-elements)

INPUT MÃ NHÂN VIÊN:
- Label: $lang['login_username_label'] = 'MÃ NHÂN VIÊN' / 'EMPLOYEE ID'
- Icon: fa-user
- CHỈ CHO PHÉP NHẬP SỐ: oninput="this.value=this.value.replace(/[^0-9]/g,'')"
- JS validate: kiểm tra /^\d+$/ nếu không phải số → hiện lỗi $lang['login_error_numeric']
- Placeholder: 'Nhập mã nhân viên...' / 'Enter employee ID...'

INPUT MẬT KHẨU:
- Label: $lang['login_password_label'] = 'MẬT KHẨU' / 'PASSWORD'
- Icon: fa-lock
- Toggle password: nút .login-toggle-password với icon fa-eye/fa-eye-slash
- Class .login-password-input: padding-right: 40px

OPTIONS ROW:
- .login-options: display grid, align-items center, justify-content center
- .toggle-switch-wrapper: display flex, align-items center, gap 8px
- .login-forgot-link: font-size 11px, text-align center, white-space nowrap

LOGIN BUTTON:
- .login-btn: background var(--accent), color var(--bg-secondary)
- Hover: background #ffaa00, translateY(-1px), box-shadow gold
- Active: translateY(0), box-shadow none

THEME & LANGUAGE SWITCH:
- .login-theme-switch: border-top 1px solid var(--border), text-align center
- .theme-title: 11px, uppercase, $lang['login_theme_lang_title'] = 'GIAO DIỆN & NGÔN NGỮ' / 'THEME & LANGUAGE'
- .theme-btn: 28x28px, border-radius 50%, border 2px solid var(--border)
- .theme-btn.active: border-color var(--accent)
- .theme-preview: 16x16px, border-radius 50%, background var(--theme-preview)
- .login-lang-switch: display flex, justify-content center, gap 10px, margin-top 10px

ERROR HANDLING:
- KHÔNG dùng .login-error div riêng
- Lỗi hiển thị trực tiếp trong .field-error của từng input
- PHP: lỗi server-side ghi vào field-error tương ứng
- JS: validate empty + numeric-only, ghi textContent vào .field-error + thêm class .show

LANGUAGE KEYS (login):
- login_title: 'ĐĂNG NHẬP' / 'LOGIN'
- login_username_label: 'MÃ NHÂN VIÊN' / 'EMPLOYEE ID'
- login_username_placeholder: 'Nhập mã nhân viên...' / 'Enter employee ID...'
- login_password_label: 'MẬT KHẨU' / 'PASSWORD'
- login_password_placeholder: 'Nhập mật khẩu...' / 'Enter password...'
- login_remember: 'Ghi nhớ đăng nhập' / 'Remember me'
- login_btn: 'ĐĂNG NHẬP' / 'LOGIN'
- login_forgot: 'Quên mật khẩu?' / 'Forgot password?'
- login_error_empty: 'Vui lòng nhập mã nhân viên và mật khẩu' / 'Please enter employee ID and password'
- login_error_invalid: 'Mã nhân viên hoặc mật khẩu không đúng' / 'Invalid employee ID or password'
- login_error_numeric: 'Mã nhân viên chỉ được nhập số' / 'Employee ID must contain only numbers'
- login_toggle_password: 'Hiện/Ẩn mật khẩu' / 'Show/Hide password'
- login_theme_lang_title: 'GIAO DIỆN & NGÔN NGỮ' / 'THEME & LANGUAGE'

RESPONSIVE:
- Mobile (max-width: 768px):
  + .login-wrapper: max-width 100%, padding 15px
  + .login-card: padding 25px 20px
  + .brand-main: font-size 22px
  + #login .canvas: 68x68px
  + #login .icon-wrapper: scale(0.26)
  + .logo-area: gap 8px
- Small (max-width: 360px):
  + .login-card: padding 20px 15px
  + .login-options: flex-direction column, align-items flex-start

QUY TẮC:
1. login.php là standalone page, KHÔNG dùng layout index.php (header, sidebar, footer)
2. Login page ẩn header/sidebar/footer: .login-page .header/.sidebar/.footer/.container { display: none !important }
3. Tất cả text dùng $lang key, KHÔNG hardcode
4. Mã nhân viên CHỈ cho nhập số (oninput filter + JS validate)
5. Lỗi hiển thị trong .field-error, KHÔNG dùng .login-error div riêng
6. Theme switching dùng AJAX save session, giống index.php
7. Logo dùng cấu trúc nested curves từ hr project, ID #login
8. Card có pseudo-elements (::before, ::after) cho hiệu ứng ánh sáng, .card-content z-index: 1

================================================================================
28. SQL SERVER & DATA LAYER
================================================================================

CẤU TRÚC THƯ MỤC:
hr-manager/
├── config/
│   └── database.php            ← DB class (singleton, lazy connection, helpers, transaction)
├── modules/                    ← Thay thế models/ - module theo tên page
│   ├── BaseModule.php          ← Generic CRUD (tất cả module kế thừa)
│   ├── LoginModule.php         ← Đăng nhập, session, permission (cho login.php)
│   └── ...                     ← 1 page = 1 module
├── pages/
│   ├── home.php                ← Gọi module → render HTML
│   └── ...                     ← Mỗi page chỉ có 1 file duy nhất
├── lang/						// (Lưu trữ các text ngôn ngữ để đồng bộ cả project)
│   ├── message_vn.php			//Tất cả các text tiếng việt
│   ├── message_eng.php			//Tất cả các text tiếng anh
├── lib/						//Chứa các modules ngoài mục modules, tái sử dụng lại
│   ├── AuthModule.php				//Xác thực & session (isLoggedIn, logout giữ lang/theme, requireLogin)
│   ├── LoginModule.php			//Logic đăng nhập (authenticate, remember me, rate limiting, refreshPermissionName)
│   ├── Db.php					//lớp mở kết nối, execute query, transaction
│   ├── MenuTree.php				//Kiểm soát quyền truy cập liên quan đến control_menu.php, canAccess() helper
│   ├── PaginationHelper.php		//module liên quan đến phân trang của các pages.php
│   ├── popup_notice.php			//showPopupNotice/showConfirmPopup cho toàn project với 3 trạng thái 'success', 'warning', 'error'
│   ├── EmailSender.php			//Hỗ trợ cả HTML và plain text email, nội dung email gửi
├── config/
│   ├── database.php				//DB class (singleton, lazy connection, helpers, transaction)
│   ├── access.json				//Cấu hình quyền truy cập menu: { "permissions": { "menuXX": "0,1,2,3,4,5" } }
QUY TẮC:
- 1 page = 1 file duy nhất trong pages/ (ví dụ: employee.php chứ KHÔNG phải employee_list.php, employee_form.php)
- 1 page = 1 module file trong modules/ (ví dụ: employee.php cho employee.php)
- Module chỉ chứa logic dữ liệu, KHÔNG chứa HTML
- Page chỉ chứa HTML + gọi module, KHÔNG viết SQL trực tiếp
- Page có thể chứa cả list và form trong cùng 1 file, dùng JS hoặc PHP để toggle giữa các view
- config/database.php là DUY NHẤT nơi mở kết nối
- Cấu trúc trang bắt buộc phải nhúng popup_notice thông báo (showPopupNotice/showConfirmPopup). Quy trình success theo project: set sessionStorage message -> closeModal (nếu có) -> reload trang -> checkSuccessMessage() -> showPopupNotice, và yêu cầu phải check quyền truy cập trước khi load trang

CẤU TRÚC CHUẨN CHO PAGE:
- Khi tạo page mới, PHẢI tham khảo cấu trúc từ pages/home.php để đảm bảo consistency
- Bắt đầu với kiểm tra quyền truy cập (permission check) — xem section 60
- Bắt đầu với <div class="content"> (KHÔNG dùng .page-content)
- Page title: <h1 class="page-title"><i class="fas fa-{icon} page-title-icon"></i>{TITLE}</h1>
- Search bar (nếu cần): <div class="search-bar"><input type="text" class="search-input" placeholder="..."><button class="add-btn"><i class="fas fa-plus"></i></button></div>
- Table: <div class="table-container"><table>...</table></div>
- Pagination: <div class="pagination"><div class="pagination-info">...</div><div class="pagination-controls">...</div></div>
- Action buttons: <div class="action-buttons"><button class="action-btn btn-edit"><i class="fas fa-edit"></i></button>...</div>

SỬ DỤNG CSS CÓ SẴN:
- PHẢI sử dụng lại các CSS class có sẵn từ components.css, layout.css, pages.css
- Các class sẵn có: .content, .page-title, .page-title-icon, .search-bar, .search-input, .add-btn, .table-container, .pagination, .pagination-info, .pagination-controls, .rows-per-page, .page-buttons, .page-btn, .action-buttons, .action-btn, .btn-edit, .btn-view, .btn-download, .btn-delete, .input-field, .input-wrapper, .input-icon, .form-input, .custom-select-wrapper, .custom-select-dropdown, .custom-select-option, .custom-date-wrapper, .custom-date-dropdown, .searchable-select-wrapper, .searchable-select-dropdown, .input-with-actions, .input-action-buttons, .input-action-btn, .numeric-formatted, .input-with-stepper, .stepper-buttons, .stepper-btn, .input-with-auto, .auto-button, .custom-radio-group, .custom-radio-option, .custom-radio-label, .toggle-switch-wrapper, .toggle-switch, .toggle-slider, .toggle-label, .permission-container, .permission-header, .permission-menu-col, .permission-toggles, .permission-toggle-wrapper, .permission-level-badge, .permission-tree, .permission-row, .permission-menu-name, .permission-toggle, .permission-toggle-slider, .permission-actions
- NẾU cần CSS mới chưa có trong các file hiện tại, PHẢI hỏi lại user trước khi tạo mới
- KHÔNG tự ý tạo CSS mới trừ khi được user yêu cầu

DB CLASS — QUY TẮC SỬ DỤNG:
- DB::getConnection()              // Lấy connection (singleton, lazy)
- DB::fetchAll($sql, $params)      // → array rows
- DB::fetchOne($sql, $params)      // → row | null
- DB::execute($sql, $params)       // → rowsAffected (int)
- DB::query($sql, $params)         // → statement resource
- DB::beginTransaction()
- DB::commit()
- DB::rollback()

QUY TẮC DB:
- KHÔNG bao giờ gọi sqlsrv_*() trực tiếp ngoài database.php và BaseModule.php
- Luôn dùng DB:: helpers
- Mọi query PHẢI truyền $params, KHÔNG nối chuỗi SQL
- Connection tự tạo khi cần (lazy), tự đóng khi script kết thúc
- Schema: Luôn dùng dbo.{TableName} để tránh lỗi phân giải schema
- Transaction: Dùng cho multi-table operations
- ConnectionPooling = true, MultipleActiveResultSets = false
- TransactionIsolation = SQLSRV_TXN_READ_COMMITTED

================================================================================
29. QUY TẮC VIẾT SQL
================================================================================

27.1 PARAMETERIZED QUERIES — BẮT BUỘC:
// ✅ ĐÚNG
DB::fetchAll("SELECT * FROM dbo.Employees WHERE department_id = ? AND status = ?", [$deptId, 1]);
// ❌ SAI — SQL injection
DB::fetchAll("SELECT * FROM dbo.Employees WHERE department_id = $deptId");

27.2 ĐẶT TÊN THAM SỐ — dùng ? (positional):
// ✅ Dùng ? theo thứ tự
DB::fetchAll("SELECT * FROM dbo.Employees WHERE department_id = ? AND status = ?", [$deptId, $status]);
// ❌ KHÔNG dùng named params (sqlsrv không hỗ trợ tốt)

27.3 LIKE:
// ✅ Tìm chứa từ khóa
$keyword = '%' . $input . '%';
DB::fetchAll("SELECT * FROM dbo.Employees WHERE full_name LIKE ?", [$keyword]);
// ✅ Tìm bắt đầu bằng
$prefix = $input . '%';
DB::fetchAll("SELECT * FROM dbo.Employees WHERE employee_id LIKE ?", [$prefix]);

27.4 IN — dùng dynamic placeholders:
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
DB::fetchAll("SELECT * FROM dbo.Employees WHERE employee_id IN ($placeholders)", $ids);

27.5 INSERT — LIỆT KÊ CỘT RÕ RÀNG:
// ✅ LUÔN liệt kê cột
DB::execute("INSERT INTO dbo.Employees (employee_id, full_name, department_id, created_at) VALUES (?, ?, ?, GETDATE())", [$id, $name, $deptId]);
// ❌ KHÔNG dùng INSERT không có cột (dễ lỗi khi thay đổi schema)

27.6 UPDATE — LUÔN CÓ WHERE:
DB::execute("UPDATE dbo.Employees SET full_name = ?, updated_at = GETDATE() WHERE employee_id = ?", [$name, $id]);

27.7 DELETE — ƯU TIÊN SOFT DELETE:
// ✅ Soft delete (khuyến khích)
DB::execute("UPDATE dbo.Employees SET deleted_at = GETDATE(), status = 0 WHERE employee_id = ?", [$id]);
// ✅ Hard delete (chỉ khi thực sự cần)
DB::execute("DELETE FROM dbo.Employees WHERE employee_id = ?", [$id]);

27.8 NGÀY GIỜ:
- Dùng GETDATE() trong SQL cho timestamp server
- ReturnDatesAsStrings = true → PHP nhận string, KHÔNG phải DateTime object
- Filter ngày từ PHP: truyền string format 'YYYY-MM-DD'
DB::fetchAll("SELECT * FROM dbo.Employees WHERE created_at >= ? AND created_at < ?", [$startDate, $endDate]);

27.9 SELECT — CHỈ CỘT CẦN, KHÔNG SELECT *:
// ✅ Chỉ cột cần
DB::fetchAll("SELECT employee_id, full_name, department_id FROM dbo.Employees WHERE ...");
// ❌
SELECT * FROM dbo.Employees WHERE ...

27.10 PHÂN TRANG SQL SERVER — ROW_NUMBER() (2008+):
// ✅ Dùng ROW_NUMBER() cho tương thích mọi phiên bản
$sql = "SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY employee_id DESC) AS [__RowNum]
    FROM dbo.Employees
    WHERE deleted_at IS NULL
) AS [__T] WHERE [__RowNum] > 20 AND [__RowNum] <= 40";

// ❌ KHÔNG fetch all rồi cắt bằng PHP
// ❌ COUNT() riêng, KHÔNG gộp với query data
// ❌ Chỉ kiểm tra tồn tại → EXISTS, không COUNT(*)
================================================================================
30. BASEMODULE & CRUD PATTERN
================================================================================

28.1 KẾ THỪA BASEMODULE — 4 PROPERTY BẮT BUỘC:
class EmployeeModule extends BaseModule {
    protected string $table      = 'Employees';        // Tên bảng SQL Server (KHÔNG có dbo.)
    protected string $primaryKey = 'employee_id';       // Khóa chính
    protected array  $fillable   = [                    // Cột cho phép insert/update
        'employee_id', 'full_name', 'department_id', 'position_id',
        'email', 'phone', 'address', 'status'
    ];
    protected array  $searchable = [                    // Cột được phép tìm kiếm
        'full_name', 'email', 'phone', 'address'
    ];
}

QUY TẮC:
- $table = tên bảng chính xác trong SQL Server (KHÔNG có dbo. prefix)
- $primaryKey = cột khóa chính, dùng cho getById(), update(), delete()
- $fillable = whitelist, insert() và update() chỉ ghi cột trong danh sách này
- $searchable = cột được phép tìm kiếm, search() chỉ tìm trên các cột này
- KHÔNG đưa cột nhạy cảm (password hash, token) vào $fillable trừ khi có ý đồ rõ ràng

28.2 CRUD — CÁCH GỌI TỪ PAGE:

// Lấy danh sách + phân trang
$module = new EmployeeModule();
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = (int)($_GET['per_page'] ?? 20);
if ($perPage <= 0 || $perPage > 100) $perPage = 20;
$offset = ($page - 1) * $perPage;

try {
    if (!empty($keyword)) {
        $rows = $module->search($keyword, [], 'employee_id DESC', $perPage, $offset);
        $total = $module->countSearch($keyword);
    } else {
        $rows = $module->getAll([], 'employee_id DESC', $perPage, $offset);
        $total = $module->count();
    }
} catch (Throwable $e) {
    error_log('Load error: ' . $e->getMessage());
    $_SESSION['popup_message'] = ['text' => $lang['error_load_failed'], 'type' => 'error'];
    $rows = [];
    $total = 0;
}

$totalPages = $total > 0 ? (int)ceil($total / $perPage) : 1;
if ($page > $totalPages) $page = $totalPages;

// Tìm kiếm
$rows = $module->search($keyword, [], 'employee_id DESC', $perPage, $offset);

// Lấy 1 record
$employee = $module->getById($id);

// Thêm mới (POST + redirect pattern)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        $result = $module->insert(['employee_id' => $id, 'full_name' => $name]);
        $_SESSION['popup_message'] = ['text' => $lang['employee_add_success'], 'type' => 'success'];
        header('Location: ?pages=employee');
        exit;
    } catch (Throwable $e) {
        error_log('Insert error: ' . $e->getMessage());
        $error = $lang['employee_save_error'];
    }
}

// Cập nhật (POST + redirect pattern)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    try {
        $affected = $module->update($id, ['full_name' => $newName]);
        $_SESSION['popup_message'] = ['text' => $lang['employee_update_success'], 'type' => 'success'];
        header('Location: ?pages=employee');
        exit;
    } catch (Throwable $e) {
        error_log('Update error: ' . $e->getMessage());
        $error = $lang['employee_save_error'];
    }
}

// Xóa (GET + confirm + redirect pattern)
if ($_GET['action'] === 'delete' && $id) {
    try {
        $affected = $module->delete($id);
        $_SESSION['popup_message'] = ['text' => $lang['employee_delete_success'], 'type' => 'success'];
    } catch (Throwable $e) {
        error_log('Delete error: ' . $e->getMessage());
        $_SESSION['popup_message'] = ['text' => $lang['employee_delete_error'], 'type' => 'error'];
    }
    header('Location: ?pages=employee');
    exit;
}

28.3 TRANSACTION PATTERN:
try {
    DB::beginTransaction();
    $employeeModule->insert($data);
    $logModule->insert(['action' => 'CREATE', 'employee_id' => $data['employee_id']]);
    DB::commit();
} catch (RuntimeException $e) {
    DB::rollback();
    $error = $e->getMessage();
}

QUY TẮC:
- Mọi thao tác multi-table PHẢI dùng transaction
- Luôn try/catch với rollback() trong catch
- KHÔNG để transaction mở quá lâu

28.4 SEARCH & FILTER:
// Tìm kiếm tự do (LIKE trên searchable columns)
$results = $module->search($keyword);
// Filter theo cột cụ thể (WHERE chính xác)
$results = $module->getAll(['department_id' => $deptId, 'status' => 1]);
// Kết hợp: viết thêm method trong module cụ thể

28.5 PAGINATION:
- $perPage mặc định 20, KHÔNG vượt quá 100
- Luôn đếm count() riêng
- Offset phải là số nguyên dương, ép kiểu (int)
- Dùng ROW_NUMBER() cho tương thích SQL Server 2008+
- Validate $perPage từ $_GET, fallback về 20

28.6 ERROR HANDLING:
try {
    $result = $module->insert($data);
} catch (Throwable $e) {
    error_log($e->getMessage());                           // log chi tiết
    $_SESSION['popup_message'] = [
        'text' => $lang['error_save_failed'],               // user thấy message chung
        'type' => 'error'
    ];
}

QUY TẮC:
- KHÔNG hiển thị SQL error trực tiếp cho user
- Log chi tiết, hiển thị message thân thiện từ $lang
- Dùng $_SESSION['popup_message'] cho redirect flow
- Lỗi kết nối DB → trang error riêng hoặc redirect login

28.7 NAMING CONVENTION:
- Module file: {tên_page}.php (VD: employee.php cho employee.php)
- Module class: PascalCase + Module (VD: EmployeeModule)
- Bảng SQL: PascalCase (VD: Employees, Departments, Company)
- Cột SQL: PascalCase (VD: ID_Company, Name_Vn, Name_Eng, Director)
- Khóa chính: ID_{Table} (VD: ID_Company, ID_Employee)
- Khóa ngoại: {ReferencedTable}ID (VD: DepartmentID)
- Method module: camelCase (VD: getById(), searchByDepartment())
- Page file: {tên_page}.php (1 file duy nhất cho cả list + form)

28.8 CHECKLIST TẠO MODULE MỚI:
- [ ] Tạo file modules/{tên_page}.php kế thừa BaseModule
- [ ] Khai báo $table, $primaryKey, $fillable, $searchable
- [ ] Nghiệp vụ đặc thù → viết thêm method trong module
- [ ] Page gọi module, KHÔNG viết SQL trong page
- [ ] Mọi query dùng $params, KHÔNG nối chuỗi
- [ ] Multi-table → dùng transaction
- [ ] Validate input trước khi truyền vào module
- [ ] Thêm language key cho success/error message
- [ ] Dùng $_SESSION['popup_message'] cho redirect flow
- [ ] Test: insert → fetch → update → delete → verify

================================================================================
31. MAPPING DỮ LIỆU GIỮA CÁC BẢNG
================================================================================

29.1 QUY TẮC THIẾT KẾ QUAN HỆ:
- Khóa ngoại luôn đặt tên {referenced_table_singular}_id
- KHÔNG dùng ID tự tăng làm khóa chính cho bảng nghiệp vụ, dùng mã nghiệp vụ
- Bảng trung gian M:M đặt tên {TableA}{TableB} (VD: EmployeeRoles)

29.2 PATTERN JOIN — LUÔN DÙNG ALIAS NGẮN:
// ✅ INNER JOIN — chỉ lấy record có quan hệ
SELECT e.employee_id, e.full_name, d.department_name, p.position_name
FROM Employees e
INNER JOIN Departments d ON e.department_id = d.department_id
INNER JOIN Positions p ON e.position_id = p.position_id
WHERE e.deleted_at IS NULL
ORDER BY e.employee_id DESC

// ✅ LEFT JOIN — lấy cả record không có quan hệ
SELECT e.employee_id, e.full_name, d.department_name
FROM Employees e
LEFT JOIN Departments d ON e.department_id = d.department_id
WHERE e.status = 1

QUY TẮC:
- Alias: bảng chính = 1 ký tự (e=Employees, d=Departments, p=Positions, r=Roles, er=EmployeeRoles)
- Luôn dùng INNER JOIN / LEFT JOIN, KHÔNG dùng implicit join (dấu phẩy WHERE)
- LEFT JOIN khi cần giữ record cha kể cả khi con NULL
- INNER JOIN khi chỉ cần record có đủ quan hệ
- Mỗi cột SELECT PHẢI có alias bảng (e.full_name, KHÔNG full_name)

29.3 JOIN VỚI FILTER:
// Filter trên bảng phụ → WHERE sau JOIN
SELECT e.employee_id, e.full_name
FROM Employees e
INNER JOIN Departments d ON e.department_id = d.department_id
WHERE d.branch_id = ? AND e.status = ?

29.4 BẢNG TRUNG GIAN M:M:
// Lấy tất cả role của 1 nhân viên
SELECT r.role_id, r.role_name
FROM EmployeeRoles er
INNER JOIN Roles r ON er.role_id = r.role_id
WHERE er.employee_id = ?

// Gán role (trong transaction)
DELETE FROM EmployeeRoles WHERE employee_id = ?
INSERT INTO EmployeeRoles (employee_id, role_id) VALUES (?, ?)

29.5 AGGREGATE + GROUP BY:
SELECT d.department_id, d.department_name, COUNT(e.employee_id) AS employee_count
FROM Departments d
LEFT JOIN Employees e ON d.department_id = e.department_id AND e.deleted_at IS NULL
GROUP BY d.department_id, d.department_name
ORDER BY employee_count DESC

QUY TẮC:
- COUNT() + LEFT JOIN → phòng không có nhân viên vẫn hiện, count = 0
- GROUP BY phải bao gồm tất cả cột non-aggregate trong SELECT
- Alias aggregate rõ ràng: AS employee_count, AS total_salary

29.6 SUBQUERY:
// Subquery trong WHERE
SELECT e.* FROM Employees e
WHERE e.salary > (SELECT AVG(e2.salary) FROM Employees e2 WHERE e2.department_id = e.department_id)
// EXISTS — kiểm tra quan hệ tồn tại
SELECT d.* FROM Departments d
WHERE EXISTS (SELECT 1 FROM Employees e WHERE e.department_id = d.department_id AND e.status = 1)

29.7 MAPPING TRONG MODULE:
// Method getListWith* = join phẳng, cho danh sách bảng
public function getListWithRelations(string $orderBy, int $limit, int $offset): array
// Method getDetailWith* = nested data, cho trang chi tiết
public function getDetailWithRelations($id): ?array
// Method countBy* = GROUP BY aggregate
public function countByDepartment(): array

QUY TẮC:
- Luôn thêm điều kiện deleted_at IS NULL cho soft-delete
- Alias bảng nhất quán: e=Employees, d=Departments, p=Positions, r=Roles, er=EmployeeRoles

================================================================================
32. BẢO MẬT OWASP TOP 10
================================================================================

A01 — BROKEN ACCESS CONTROL:
- Mọi page PHẢI kiểm tra session trước khi xử lý
- Mọi AJAX endpoint PHẢI kiểm tra session + quyền
- KHÔNG tin client-side ẩn/hiện nút — phải kiểm tra server-side
- IDOR: kiểm tra user có quyền truy cập record đó không
if (!isset($_SESSION['user_id'])) { header('Location: login.php'); exit; }
if (!in_array('employee_edit', $_SESSION['permissions'])) { http_response_code(403); exit; }

A02 — CRYPTOGRAPHIC FAILURES:
- Password: LUÔN dùng password_hash() + password_verify(), KHÔNG dùng MD5/SHA1
- password_hash($password, PASSWORD_BCRYPT, ['cost' => 12])
- Căn cước/CCCD: mã hóa AES-256 nếu lưu
- KHÔNG lưu dữ liệu nhạy cảm trong session/cookie
- HTTPS bắt buộc trên production

A03 — INJECTION:
- SQL: parameterized queries ONLY (đã quy định ở section 27)
- XSS: htmlspecialchars() MỌI output từ DB/user, ENT_QUOTES, UTF-8
- JS context: json_encode() khi đưa data vào JavaScript
- URL: urlencode() khi đưa data vào URL parameter
- KHÔNG bao giờ echo trực tiếp dữ liệu từ user/DB

A04 — INSECURE DESIGN:
- Mọi form POST PHẢI có CSRF token
- Login: giới hạn số lần thử (5 lần → khóa 15 phút)
- Logout: hủy session hoàn toàn (session_destroy() + xóa cookie)
- Khôi phục mật khẩu: token 1 lần dùng, hết hạn sau 30 phút
// CSRF token
$token = bin2hex(random_bytes(32));
$_SESSION['csrf_token'] = $token;
// Verify: $_POST['csrf_token'] !== $_SESSION['csrf_token'] → 403

A05 — SECURITY MISCONFIGURATION:
- Production: display_errors = Off, log_errors = On
- KHÔNG hiển thị stack trace, SQL query, connection info cho user
- Thêm security headers trong mọi response:
  header('X-Content-Type-Options: nosniff');
  header('X-Frame-Options: DENY');
  header('X-XSS-Protection: 1; mode=block');
  header('Referrer-Policy: strict-origin-when-cross-origin');
- .env hoặc environment variables cho credential, KHÔNG hardcode

A06 — VULNERABLE & OUTDATED COMPONENTS:
- Kiểm tra phiên bản PHP, sqlsrv extension định kỳ
- KHÔNG dùng thư viện đã depreciated (mysql_* → dùng sqlsrv only)
- Xóa file không dùng: test.php, backup file, .bak
- .gitignore: loại trừ config/database.php, .env, backup files

A07 — IDENTIFICATION & AUTHENTICATION FAILURES:
- Cookie: httponly, secure, samesite=Strict
  ini_set('session.cookie_httponly', 1);
  ini_set('session.cookie_secure', 1);
  ini_set('session.cookie_samesite', 'Strict');
- session_regenerate_id(true) sau login thành công
- Logout: xóa toàn bộ session data + cookie + destroy
- Session timeout: 30 phút không hoạt động → tự logout
- KHÔNG truyền session ID trong URL

A08 — SOFTWARE & DATA INTEGRITY FAILURES:
- Validate type + range của mọi input từ client
- Kiểm tra null/empty của data trả về từ DB trước khi dùng
- File upload: kiểm tra MIME type (KHÔNG tin extension), giới hạn size
- KHÔNG deserialize data không tin cậy

A09 — SECURITY LOGGING & MONITORING FAILURES:
- Log: login thành công/thất bại, thay đổi dữ liệu, truy cập bị từ chối
- Lưu: action, detail, user_id, IP, user_agent, timestamp
- KHÔNG log password, token, dữ liệu nhạy cảm
- Kiểm tra log định kỳ để phát hiện bất thường
class AuditLog {
    public static function write(string $action, string $detail = '', ?int $userId = null): void
    DB::execute("INSERT INTO AuditLog (action, detail, employee_id, ip_address, user_agent, created_at) VALUES (?, ?, ?, ?, ?, GETDATE())", [...]);
}

A10 — SERVER-SIDE REQUEST FORGERY (SSRF):
- KHÔNG cho user chỉ định URL để server fetch
- Nếu cần, whitelist các URL nội bộ
- KHÔNG expose internal network info qua error message

CHECKLIST BẢO MẬT CHO MỖI PAGE/AJAX:
- [ ] Kiểm tra session tồn tại?
- [ ] Kiểm tra quyền truy cập cho thao tác này?
- [ ] Kiểm tra ownership (user chỉ truy cập data của mình)?
- [ ] CSRF token cho mọi POST form?
- [ ] Parameterized SQL — KHÔNG nối chuỗi?
- [ ] htmlspecialchars() cho mọi output HTML?
- [ ] json_encode() cho data vào JS?
- [ ] Validate type + range input từ client?
- [ ] Rate limiting cho thao tác nhạy cảm?
- [ ] Log sự kiện bảo mật vào AuditLog?
- [ ] KHÔNG hiển thị SQL error cho user?
- [ ] KHÔNG lưu dữ liệu nhạy cảm trong session/cookie?

================================================================================
33. PERFORMANCE — GIỮ HỆ THỐNG NHẸ, MƯỢT
================================================================================

31.1 PHP LAYER:
- require_once chỉ file cần dùng trong page đó, KHÔNG load tất cả module
- Lazy connection — DB::getConnection() chỉ kết nối khi gọi lần đầu
- KHÔNG tạo object module nếu page không dùng DB → 0 overhead
- sqlsrv_free_stmt() ngay sau khi fetch xong
- KHÔNG dùng Composer/autoload cho project này — quá nặng

31.2 SQL QUERY:
-- LUÔN liệt kê cột cần SELECT, KHÔNG SELECT *
-- Phân trang bằng ROW_NUMBER(), KHÔNG fetch all rồi slice PHP
-- COUNT() riêng, KHÔNG gộp với query data
-- Schema: Luôn dùng dbo.{TableName} để tránh lỗi phân giải
- Chỉ kiểm tra tồn tại → EXISTS, không COUNT(*)

31.3 INDEX:
- Mỗi FK PHẢI có index riêng
- WHERE phổ biến → tạo index
- INCLUDE cho covering index, tránh key lookup
- KHÔNG index cột ít dùng → phí bộ nhớ
- Review EXECUTION PLAN cho query chậm
CREATE INDEX IX_Employees_DepartmentId ON Employees(department_id);
CREATE INDEX IX_Employees_DeptStatus ON Employees(department_id, status);
CREATE INDEX IX_Employees_ListCover ON Employees(department_id, status)
INCLUDE (employee_id, full_name, email);

31.4 CONNECTION POOLING:
- ConnectionPooling = true — KHÔNG tắt
- MultipleActiveResultSets = false — giảm memory server
- Singleton DB::getConnection() — 1 connection/request, KHÔNG mở nhiều

31.5 N+1 PROBLEM:
- LUÔN dùng JOIN thay N+1 query
- Nếu không JOIN được → dùng IN batch (2 query thay N+1 query)
- array_column() để map nhanh, KHÔNG loop query
// ❌ N+1 — 1 query list + N query detail
// ✅ JOIN 1 query lấy tất cả
// ✅ IN batch: lấy IDs → WHERE IN → array_column map

31.6 SESSION:
- Session chỉ lưu ID + tên + role IDs, KHÔNG lưu cả row
- Cần detail → query + static cache (1 lần/request)
- KHÔNG lưu blob/large data trong session
function currentUser(): ?array {
    static $cache = null;
    if ($cache !== null) return $cache;
    $cache = DB::fetchOne("SELECT employee_id, full_name, email FROM Employees WHERE employee_id = ?", [$_SESSION['user_id']]);
    return $cache;
}

31.7 AJAX RESPONSE:
- Trả JSON tối thiểu, chỉ cột cần
- KHÔNG trả nguyên object (gồm cả password_hash, created_at...)
- Theme switch AJAX — fire and forget, KHÔNG await

31.8 FRONTEND:
- Validate client-side trước, chỉ submit khi hợp lệ → giảm request không cần thiết
- Theme switch: đổi UI tức thì, AJAX lưu session ngầm

31.9 TỔNG HỢP — NGUYÊN TẮC NHẸ, MƯỢT:
- Lazy connection: Không query → không mở kết nối → 0ms
- Singleton + Pooling: 1 connection/request, tái sử dụng giữa request
- SELECT cột cần: Không SELECT *, giảm network + memory
- OFFSET FETCH: Phân trang SQL, không fetch all
- JOIN thay N+1: 1 query thay N+1 query
- IN batch: 2 query thay N query khi không JOIN được
- Index FK: Mỗi khóa ngoại có index, JOIN nhanh
- Session nhẹ: Chỉ lưu ID, detail query + static cache
- require_once chọn: Chỉ load module cần, không load tất cả
- free_stmt: Giải phóng statement ngay sau fetch
- AJAX tối thiểu: JSON nhẹ, fire-and-forget khi không cần response
- Client validate: Chặn sai trước khi gửi server, giảm request

MỤC TIÊU: Mỗi page load < 200ms, query đơn < 10ms, list 20 rows < 50ms

================================================================================
34. POPUP NOTICE & CONFIRM DIALOG
================================================================================

FILE: lib/popup_notice.php

HÀM PHP:
- showPopupNotice($message, $type, $duration): Hiển thị toast notification
  + $type: 'success', 'warning', 'error', 'info'
  + $duration: thời gian tự ẩn (ms), mặc định 3000
  + Icon map: success=fa-check-circle, warning=fa-exclamation-triangle, error=fa-times-circle, info=fa-info-circle
  + Màu: success=#4caf50, warning=#ff9800, error=#ef5350, info=#2196f3
- showConfirmPopup($message, $type, $confirmText, $cancelText, $confirmUrl, $cancelUrl): Hiển thị confirm dialog
  + $type: 'success', 'warning', 'error'
  + $confirmUrl: URL xử lý khi xác nhận
  + $cancelUrl: URL khi hủy (mặc định đóng popup)
- setPopupMessage($message, $type): Lưu message vào $_SESSION['popup_message'] để hiển thị sau redirect
- checkPopupMessage(): Kiểm tra và hiển thị popup từ session, xóa sau khi hiển thị

HÀM JAVASCRIPT (client-side):
- showPopupNoticeJS(message, type, duration): Gọi từ JS để hiện popup động
- showConfirmPopupJS(message, type, confirmText, cancelText, onConfirm, onCancel): Gọi từ JS, nhận callback function

CSS (components.css):
- .popup-notice: position fixed, top 15px, right 20px, z-index 10001 (CAO NHẤT)
- .popup-notice-icon: icon trạng thái
- .popup-notice-message: nội dung thông báo
- .popup-notice-close: nút đóng
- .popup-notice-progress: thanh tiến trình tự ẩn
- Animation: slideInRight + fadeIn, auto-dismiss sau $duration
- Responsive (max-width: 768px): top 65px, right 10px, left 10px, min-width auto, max-width none

- .popup-confirm-overlay: position fixed, z-index 10001, background rgba(0,0,0,0.6)
- .popup-confirm-content: modal dialog, min-width 350px, max-width 450px
- .popup-confirm-icon: icon trạng thái lớn
- .popup-confirm-message: nội dung xác nhận
- .popup-confirm-buttons: flex, gap 12px
- .popup-confirm-btn: nút xác nhận/hủy
- .popup-confirm-success/warning/error: màu icon theo type
- Responsive: min-width 280px, max-width 90%, buttons flex-direction column

QUY TẮC:
1. Mọi thông báo PHẢI dùng showPopupNotice, KHÔNG dùng alert()
2. Mọi xác nhận PHẢI dùng showConfirmPopup, KHÔNG dùng confirm()
3. Z-index 10001 là CAO NHẤT trong project, luôn trên header (9999)
4. Popup message persist qua redirect: dùng setPopupMessage() + checkPopupMessage()
5. Client-side callback: dùng showConfirmPopupJS() với onConfirm/onCancel
6. Mọi text trong popup PHẢI dùng $lang key, KHÔNG hardcode

INTEGRATION:
- index.php: require_once 'lib/popup_notice.php'; + checkPopupMessage()
- login.php: require_once 'lib/popup_notice.php'; + showPopupNotice cho lỗi đăng nhập

================================================================================
35. HEADER SUBMENU (MORE OPTIONS)
================================================================================

HTML (control_header.php):
- Nằm trong .header-right > .more-options
- .header-submenu: dropdown menu ẩn/hiện
- 3 item: Hồ sơ (header-profile), Đổi mật khẩu (header-change-password), Đăng xuất (header-logout)
- .header-submenu-divider: đường phân cách trước Đăng xuất
- .header-submenu-logout: class riêng cho item đăng xuất (màu đỏ hover)
- Tất cả span chứa text phải có data-lang-key để đổi ngôn ngữ không reload
- Permission name span phải có id="header-permission-name" để changeLanguage() cập nhật
- Collapse button dùng data-lang-key + data-lang-attr="title" cho title attribute

CSS (layout.css):
- .header-submenu: position absolute, top 100%, right 0, z-index 10000
- .header-submenu.show: display block, animation slideDown 0.2s ease
- .header-submenu-item: flex, align-items center, gap 12px, padding 12px 15px
- .header-submenu-item:hover: background var(--btn-view), color var(--accent)
- .header-submenu-divider: border-top 1px solid var(--border)
- Responsive (max-width: 768px): .header-submenu margin-top 17px

JAVASCRIPT (index.php inline):
- Click .more-options → toggle .show trên .header-submenu
- Click outside → đóng submenu
- Click Đăng xuất → gọi showConfirmPopupJS() với $lang key
- Confirm → window.location.href = 'logout.php'

LANGUAGE KEYS:
- header_profile: 'Hồ sơ' / 'Profile'
- header_change_password: 'Đổi mật khẩu' / 'Change Password'
- header_logout: 'Đăng xuất' / 'Logout'
- header_logout_confirm: 'Bạn có chắc muốn đăng xuất?' / 'Are you sure you want to logout?'
- header_logout_btn: 'Đăng xuất' / 'Logout'
- header_cancel_btn: 'Hủy' / 'Cancel'

Z-INDEX:
- Header submenu: 10000 (dưới popup notice 10001, trên header 9999)

================================================================================
36. AUTH MODULE & LOGIN MODULE
================================================================================

FILE: lib/AuthModule.php — Xác thực & session

METHODS:
- isLoggedIn(): bool — kiểm tra $_SESSION['user_id']
- getUserId(): ?string — lấy user ID
- logout(): void — hủy session nhưng GIỮ LẠI ngôn ngữ và giao diện
  + Lưu $lang, $theme trước khi $_SESSION = []
  + Khôi phục $_SESSION['lang'], $_SESSION['theme']
  + session_regenerate_id(true) chống session fixation
  + KHÔNG session_destroy() để giữ preferences
- requireLogin(): void — redirect login.php nếu chưa đăng nhập
- hasPermission(array $permissions): bool
- requirePermission(array $permissions): void — 403 nếu không có quyền

FILE: lib/LoginModule.php — Logic đăng nhập

CONSTANTS:
- MAX_ATTEMPTS: 500 (số lần đăng nhập tối đa trước khi khóa)
- LOCK_TIME: 900 (15 phút khóa)
- REMEMBER_SECRET: key dùng HMAC cho remember token
- REMEMBER_DURATION: 2592000 (30 ngày)

METHODS:
- authenticate(string $username, string $password): array
  + SQL: SELECT dw.Maso_NV, dw.Password, dw.Hoten, dw.Permisson, pp.Name_VN/Name_ENG AS PermissionName
  + LEFT JOIN dbo.Properti_Permisson pp ON dw.Permisson = pp.ID
  + Chọn cột tên quyền theo ngôn ngữ: Name_VN (vn) hoặc Name_ENG (eng)
  + Lưu session: user_id, user_name (Hoten), user_permission (PermissionName), user_permission_id (Permisson)
  + Ưu tiên bcrypt hash, fallback plain text → tự nâng cấp lên bcrypt
  + session_regenerate_id(true) sau login
- isLocked(): bool — kiểm tra khóa tạm thời
- getRemainingLockTime(): int — thời gian khóa còn lại (giây)
- resetAttempts(): void — reset số lần thử
- incrementAttempts(): void — tăng số lần thử
- validateInput(string $username, string $password): array — validate đầu vào
- handleRememberMe(string $username): void — tạo remember cookie (HMAC-signed)
- checkRememberCookie(): bool — kiểm tra remember cookie, tự động login
- generateRememberToken(string $username): string — tạo token HMAC
- clearRememberCookie(): void — xóa remember cookie
- refreshPermissionName(): void — cập nhật tên quyền khi đổi ngôn ngữ
  + Query lại PermissionName + Permisson ID theo ngôn ngữ mới
  + Cập nhật $_SESSION['user_permission'] và $_SESSION['user_permission_id']
- handleThemeChange(): void — AJAX lưu theme
- handleLanguageChange(): void — GET lưu lang + gọi refreshPermissionName()
- getCurrentLang(): string — trả về ngôn ngữ hiện tại
- getCurrentTheme(): string — trả về theme hiện tại
- loadLanguage(): void — include file ngôn ngữ

SESSION KEYS:
- user_id: Maso_NV (mã nhân viên)
- user_name: Hoten (họ tên)
- user_permission: PermissionName (tên quyền theo ngôn ngữ)
- user_permission_id: Permisson (ID quyền 0-5)
- lang: ngôn ngữ hiện tại ('vn'/'eng')
- theme: giao diện hiện tại ('green'/'blue'/...)
- login_attempts: số lần đăng nhập sai
- login_lock_time: thời điểm khóa
- welcome_message: thông báo chào mừng (xóa sau khi hiển thị)

REMEMBER ME:
- Cookie: remember_{username} = {selector}:{token HMAC}
- HMAC key: REMEMBER_SECRET, hash SHA-256
- Duration: 30 ngày (REMEMBER_DURATION)
- Cookie options: httponly, secure, samesite=Strict, path=/
- Check: verify HMAC signature, nếu hợp lệ → set session + regenerate ID
- Logout: clearRememberCookie() xóa cookie

DB COLUMNS (dbo.Datawork):
- Maso_NV: mã nhân viên (khóa chính, login username)
- Password: mật khẩu (bcrypt hash hoặc plain text)
- Hoten: họ tên nhân viên
- Permisson: ID quyền (0-5), LƯU Ý: 2 chữ s (không phải Permission)

DB COLUMNS (dbo.Properti_Permisson):
- ID: khóa chính, map với Datawork.Permisson
- Name_VN: tên quyền tiếng Việt
- Name_ENG: tên quyền tiếng Anh

QUY TẮC:
1. Cột Permisson (2 chữ s) là tên chính xác trong DB, KHÔNG dùng Permission
2. Tên quyền lấy từ Properti_Permisson theo ngôn ngữ hiện tại
3. Khi đổi ngôn ngữ → gọi refreshPermissionName() để cập nhật tên quyền
4. Remember Me dùng HMAC-signed cookie, KHÔNG lưu password
5. Logout GIỮ LẠI lang + theme để login page load đúng preferences
6. Login thành công → lưu welcome_message vào session, hiển thị trên index.php

================================================================================
37. PERMISSION BADGE (5 CẤP QUYỀN)
================================================================================

CSS (components.css):
- .permission-0 đến .permission-5: badge hiển thị quyền user
- Style chung: font-size 12px, font-weight 700, uppercase, border-radius 16px
  border 1px solid rgba(122, 220, 201, 0.5), letter-spacing 1.6px
  padding 0px 10px, display inline-block, animation textclip
- .permission-X span: background-clip text, animation gradient chạy vô hạn

GRADIENT MÀU (theo cấp):
- permission-0: #8b9d8d → #5a6b5e → #8b9d8d (xám nhạt - chưa gán quyền)
- permission-1: #59A397 → #4A90E2 → #64A7A6 (xanh ngọc - xanh dương - xanh ngọc)
- permission-2: #336E6A → #579099 → #436E70 (xanh lục đậm)
- permission-3: #095fab → #25abe8 → #BF00FF (xanh dương - tím)
- permission-4: #095fab → #25abe8 → #57d75b (xanh dương - xanh lá)
- permission-5: #57d75b → #25abe8 → #FF4757 (xanh lá - xanh dương - đỏ)

FALLBACK (không hỗ trợ -webkit-background-clip: text):
- Tất cả cấp: color #7adcc9

HTML (control_header.php):
<div class="permission-{id}"><span id="header-permission-name">{tên quyền}</span></div>
- {id}: $_SESSION['user_permission_id'] (0-5)
- {tên quyền}: $_SESSION['user_permission'] (từ Properti_Permisson)
- id="header-permission-name" cần thiết để changeLanguage() cập nhật tên quyền khi đổi ngôn ngữ

MAPPING:
- Datawork.Permisson (0-5) → Properti_Permisson.ID → Name_VN/Name_ENG
- Cấp 0: chưa gán quyền
- Cấp 1-5: các cấp quyền tăng dần

================================================================================
38. USER AVATAR
================================================================================

LOGIC (control_header.php):
- Kiểm tra file: data/avatar/{Maso_NV}_{Hoten}.jpg
- Đường dẫn tuyệt đối: dirname(__DIR__) . '/data/avatar/' (vì control_header.php nằm trong pages/)
- Nếu file tồn tại → hiển thị ảnh: <div class="user-avatar user-avatar-img" style="background-image: url(...)">
- Nếu không tồn tại → hiển thị chữ cái đầu: <div class="user-avatar">{chữ cái đầu}</div>
- URL dùng rawurlencode() để xử lý tên tiếng Việt có dấu
- Chữ cái đầu: strtoupper(mb_substr($userName, 0, 1, 'UTF-8'))

CSS (components.css):
- .user-avatar: width 35px, height 35px, border-radius 50%, flex center
  background gradient (btn-edit → btn-view), color white, font-weight bold, flex-shrink 0
- .user-avatar-img: background-size cover, background-position center, background-repeat no-repeat

THƯ MỤC AVATAR:
- data/avatar/: chứa file ảnh nhân viên
- Tên file: {Maso_NV}_{Hoten}.jpg (ví dụ: 001234_NguyenVanA.jpg)

================================================================================
39. LOGOUT FLOW
================================================================================

FILE: logout.php
1. Require AuthModule.php + LoginModule.php
2. LoginModule::clearRememberCookie() — xóa remember cookie
3. AuthModule::logout() — hủy session nhưng GIỮ lang + theme
4. Redirect → login.php

AUTHMODULE::LOGOUT():
- Lưu $_SESSION['lang'] + $_SESSION['theme'] vào biến tạm
- $_SESSION = [] — xóa toàn bộ session data
- Khôi phục $_SESSION['lang'] + $_SESSION['theme']
- session_regenerate_id(true) — chống session fixation
- KHÔNG session_destroy() — giữ session sống để lưu preferences

KẾT QUẢ:
- Sau logout → login.php load đúng ngôn ngữ + giao diện đã chọn
- Confirm popup dùng $lang key (header_logout_confirm, header_logout_btn, header_cancel_btn)

================================================================================
40. LOGIN VALIDATION & ERROR HANDLING
================================================================================

CLIENT-SIDE VALIDATION (login.php):
- Kiểm tra empty → thêm class .show cho validate-error + field-error
- Kiểm tra numeric-only (Mã NV) → thêm class .show cho validate-error + field-error
- QUY TẮC: Nếu field-error.show thì validate-error cũng phải .show (đồng bộ visual)
- Validate thành công → thêm class .show cho validate-success

SERVER-SIDE ERROR HANDLING (login.php):
- Lỗi đăng nhập → showPopupNotice('error', message, 5000)
- Thông báo số lần còn lại: sprintf($lang['login_attempts_remaining'], $remaining)
- Tài khoản khóa: $lang['login_error_locked']
- Lỗi hiển thị TRONG .field-error của input tương ứng (KHÔNG dùng div riêng)

WELCOME POPUP:
- Login thành công → $_SESSION['welcome_message'] = sprintf($lang['login_welcome'], $user_id)
- index.php kiểm tra welcome_message → showPopupNotice('success', $welcome_message, 4000)
- unset($_SESSION['welcome_message']) sau khi hiển thị

LANGUAGE KEYS (login):
- login_attempts_remaining: 'Còn %d lần thử' / '%d attempts remaining'
- login_welcome: 'Chào mừng %s!' / 'Welcome %s!'
- login_error_locked: thông báo khóa tài khoản

================================================================================
41. Z-INDEX CẤP HẠCH (CẬP NHẬT)
================================================================================

THỨ TỰ TỪ CAO XUỐNG THẤP:
- Popup Notice / Confirm Dialog: 10001 (CAO NHẤT)
- Header Submenu: 10000
- Header / Footer: 9999
- Sidebar: 9998
- Container: auto (dưới header/footer)

QUY TẮC:
1. Popup luôn phải nằm trên MỌI thành phần UI khác
2. Header submenu nằm trên header nhưng dưới popup
3. KHÔNG dùng z-index > 10001 cho bất kỳ thành phần nào
4. Khi thêm overlay/popup mới, z-index tối đa = 10001

================================================================================
42. RESPONSIVE TABLE - CARD VIEW ON MOBILE
================================================================================

TABLE RESPONSIVE (@media max-width: 768px):
- Table hiển thị dạng card thay vì bảng truyền thống
- Mỗi row (<tr>) trở thành một card riêng biệt

CẤU TRÚC HTML:
- Thêm data-label cho mỗi <td> để hiển thị label trong card
- Ví dụ: <td data-label="<?php echo $lang['col_id']; ?>">...</td>
- Label lấy từ language key để hỗ trợ đa ngôn ngữ

CSS (components.css):
- .table-container: border-radius 0, border none, background transparent
- .table-container table: display block
- .table-container thead: display none (ẩn header)
- .table-container tbody: display flex, flex-direction column, gap 10px, padding 0
- .table-container tbody tr: display flex, flex-direction column, background var(--bg-secondary), border-radius 12px, border 1px solid var(--border), padding 15px, gap 10px
- .table-container tbody tr:hover: background var(--bg-secondary) (không đổi màu)
- .table-container tbody tr td: display flex, justify-content space-between, align-items center, padding 8px 0, border-bottom 1px solid var(--border)
- .table-container tbody tr td:last-child: border-bottom none
- .table-container tbody tr td::before: content attr(data-label), font-weight 600, font-size 10px, text-transform uppercase, color var(--text-secondary), flex-shrink 0, margin-right 10px
- .table-container tbody tr td[colspan]: text-align center, justify-content center, border-bottom none
- .table-container tbody tr td[colspan]::before: display none

JAVASCRIPT UPDATE:
- Khi render rows mới qua AJAX, thêm data-label cho mỗi td
- Thêm language keys vào langKeys object trong JS
- Ví dụ: col_id, col_name_vn, col_name_en, col_abbreviation, col_director, col_business_code, col_actions

================================================================================
43. PAGINATION RESPONSIVE
================================================================================

PAGINATION LAYOUT MOBILE (@media max-width: 768px):
- Hàng 1: pagination-info (trái) + rows-per-page (phải) - cùng một hàng
- Hàng 2: page-buttons (full width, căn giữa)

CSS (components.css):
- .pagination: display flex, flex-direction column, gap 10px, padding 10px, margin-top 10px
- .pagination-info: font-size 11px, text-align center, width 100%, order 1
- .pagination-controls: display flex, flex-direction column, gap 10px, order 2
- .rows-per-page: font-size 11px, justify-content center, order 1
- .rows-per-page span: font-size 11px
- .rows-per-page select: padding 6px 10px, font-size 11px
- .page-buttons: width 100%, justify-content center, flex-wrap wrap, order 2
- .page-btn: min-width 32px, height 32px, font-size 11px

LƯU Ý:
- Xóa border-top của .pagination (không có border phân cách)
- pagination-controls chứa cả rows-per-page và page-buttons

================================================================================
44. POPUP TEMPLATE
================================================================================

CẤU TRÚC POPUP:
- Header (cố định): Tiêu đề + nút đóng
- Content (scroll được): Nội dung form
- Footer (cố định): Các nút hành động

HTML (home.php hoặc trang cần popup):
<div class="popup-overlay" id="addPopupOverlay" onclick="closeAddPopup(event)">
  <div class="popup-container" onclick="event.stopPropagation()">
    <div class="popup-header">
      <h3 class="popup-title">
        <i class="fas fa-plus-circle"></i>
        TIÊU ĐỀ POPUP
      </h3>
      <button class="popup-close" onclick="closeAddPopup()">
        <i class="fas fa-times"></i>
      </button>
    </div>
    <div class="popup-content">
      <!-- Content goes here -->
    </div>
    <div class="popup-footer">
      <button class="popup-btn popup-btn-cancel" onclick="closeAddPopup()">
        <i class="fas fa-times"></i>
        Hủy
      </button>
      <button class="popup-btn popup-btn-save" onclick="saveAddPopup()">
        <i class="fas fa-save"></i>
        Lưu
      </button>
    </div>
  </div>
</div>

CSS (components.css):
- .popup-overlay: position fixed, full screen, background rgba(0,0,0,0.7), z-index 10001, opacity 0, visibility hidden, transition 0.3s
- .popup-overlay.active: opacity 1, visibility visible
- .popup-container: background var(--bg-secondary), border-radius 12px, border 1px solid var(--border), width 90%, max-width 900px, max-height 90vh, display flex, flex-direction column, transform scale(0.9), transition 0.3s
- .popup-overlay.active .popup-container: transform scale(1)
- .popup-header: display flex, justify-content space-between, padding 5px 10px, background var(--btn-view), border-bottom 1px solid var(--border), border-radius 12px 12px 0 0, flex-shrink 0
- .popup-title: font-size 14px, font-weight 600, color var(--accent), display flex, align-items center, gap 10px, text-transform uppercase, letter-spacing 1.6px
- .popup-close: background none, border none, color var(--text-secondary), font-size 18px, cursor pointer, transition 0.3s
- .popup-close:hover: color var(--text-primary)
- .popup-content: padding 20px, overflow-y auto, flex 1
- .popup-footer: display flex, justify-content flex-end, gap 10px, padding 10px, background var(--bg-primary), border-top 1px solid var(--border), border-radius 0 0 12px 12px, flex-shrink 0
- .popup-btn: display flex, align-items center, gap 8px, padding 10px 20px, border-radius 8px, font-size 12px, font-weight 600, cursor pointer, transition 0.3s, border none
- .popup-btn-cancel: background var(--btn-delete), color var(--text-primary)
- .popup-btn-cancel:hover: background #a04545
- .popup-btn-save: background var(--accent), color var(--bg-primary)
- .popup-btn-save:hover: background #e6c200

FORM TRONG POPUP:
- Sử dụng .input-template-grid (2 cột desktop, 1 cột mobile)
- Sử dụng các TYPE input từ Input Template (Type 1-11)
- Ví dụ: Type 1 (Text Input), Type 4 (Date Picker), Type 5 (Searchable Select), v.v.
- Không tạo CSS riêng cho form trong popup, dùng CSS có sẵn từ components.css
- Thêm <span class="required">*</span> sau label cho trường bắt buộc

QUAN TRỌNG: popup-container có onclick="event.stopPropagation()" để ngăn click đóng overlay.
Điều này chặn event bubble lên document → TẤT CẢ handler document.addEventListener('click',...) sẽ KHÔNG chạy.
Do đó, click-outside-to-close cho dropdown/date picker TRONG popup PHẢI nghe trên .popup-container thay vì document.
Chi tiết xem ở TYPE 2 (section Click-outside-to-close handler).

REQUIRED FIELD CSS:
- .input-field label .required: color #ff6b6b, font-size 14px, font-weight 700

VALIDATION STATES:
- .validate-success: icon check-circle, color #4caf50, opacity 0, position absolute right 0
- .validate-success.show: opacity 1
- .validate-error: icon exclamation-circle, color #ef5350, opacity 0, position absolute right 0
- .validate-error.show: opacity 1
- .field-error: font-size 11px, color #ef5350, margin-top 4px

RESPONSIVE (@media max-width: 768px):
- .popup-container: width 95%, max-height 70vh
- .popup-footer: flex-direction column, gap 10px
- .popup-btn: width 100%, justify-content center

JAVASCRIPT:
function openAddPopup() {
  document.getElementById('addPopupOverlay').classList.add('active');
  document.body.style.overflow = 'hidden';
  // Reset form và clear validation states
  clearValidationStates();
}

function closeAddPopup(event) {
  if (event && event.target !== event.currentTarget) return;
  document.getElementById('addPopupOverlay').classList.remove('active');
  document.body.style.overflow = '';
}

function clearValidationStates() {
  // Xóa tất cả validation states (success/error icons, error messages)
  const fields = ['field1', 'field2', ...];
  fields.forEach(function(fieldId) {
    const field = document.getElementById(fieldId);
    const inputField = field.closest('.input-field');
    inputField.querySelector('.validate-success').classList.remove('show');
    inputField.querySelector('.validate-error').classList.remove('show');
    inputField.querySelector('.field-error').textContent = '';
  });
}

function showFieldError(fieldId, message) {
  const field = document.getElementById(fieldId);
  const inputField = field.closest('.input-field');
  inputField.querySelector('.validate-error').classList.add('show');
  inputField.querySelector('.field-error').textContent = message;
}

function showFieldSuccess(fieldId) {
  const field = document.getElementById(fieldId);
  const inputField = field.closest('.input-field');
  inputField.querySelector('.validate-success').classList.add('show');
  inputField.querySelector('.validate-error').classList.remove('show');
  inputField.querySelector('.field-error').textContent = '';
}

// Close on Escape key
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape') {
    closeAddPopup();
  }
});

================================================================================
45. POPUP FORM VALIDATION
================================================================================

VALIDATION FLOW:
1. Nút Lưu → validateRequiredFields() → kiểm tra tất cả trường không rỗng
2. Nếu pass → checkDuplicates() → AJAX check trùng các trường cần thiết
3. Nếu pass → submit form

REQUIRED FIELD VALIDATION:
function validateRequiredFields() {
  const fields = [
    { id: 'field1', name: 'Tên trường 1' },
    { id: 'field2', name: 'Tên trường 2' }
  ];
  
  let allValid = true;
  fields.forEach(function(field) {
    const value = document.getElementById(field.id).value.trim();
    if (value === '') {
      showFieldError(field.id, langKeys.error_required);
      allValid = false;
    } else {
      showFieldSuccess(field.id);
    }
  });
  return allValid;
}

DUPLICATE CHECK (AJAX):
- Gọi AJAX đến index.php với param ajax_check_duplicate=1
- Handler phải đặt trong index.php TRƯỚC khi output HTML
- Response: { success: true, duplicates: { Field1: true, Field2: false } }

async function checkDuplicates() {
  const params = new URLSearchParams({
    ajax_check_duplicate: '1',
    field1: document.getElementById('field1').value.trim(),
    field2: document.getElementById('field2').value.trim()
  });
  
  const response = await fetch('index.php?pages=page_name&' + params.toString());
  const data = await response.json();
  
  if (data.success && data.duplicates) {
    // Show error for duplicate fields
    if (data.duplicates.Field1) showFieldError('field1', langKeys.error_duplicate_field1);
  }
  return !hasDuplicates;
}

REAL-TIME VALIDATION:
// Setup validation on input và blur events
function setupFieldValidation(fieldId, checkDuplicate, dbField) {
  const field = document.getElementById(fieldId);
  
  field.addEventListener('input', function() {
    const value = this.value.trim();
    if (value === '') {
      showFieldError(fieldId, langKeys.error_required);
      return;
    }
    showFieldSuccess(fieldId);
    
    // Check duplicate với debounce 500ms
    if (checkDuplicate) {
      clearTimeout(duplicateCheckTimeout);
      duplicateCheckTimeout = setTimeout(async function() {
        await validateFieldDuplicate(fieldId, dbField, value);
      }, 500);
    }
  });
  
  field.addEventListener('blur', function() {
    if (this.value.trim() === '') {
      showFieldError(fieldId, langKeys.error_required);
    }
  });
}

// Initialize validation khi DOM ready
document.addEventListener('DOMContentLoaded', function() {
  setupFieldValidation('field1', true, 'Field1');  // check duplicate
  setupFieldValidation('field2', false, null);      // no duplicate check
});

NUMERIC INPUT FILTER:
- Thêm oninput để chỉ cho phép nhập số: oninput="this.value = this.value.replace(/[^0-9]/g, '')"
- Ví dụ: <input type="text" oninput="this.value = this.value.replace(/[^0-9]/g, '')">

================================================================================
46. AJAX HANDLER PLACEMENT
================================================================================

QUY TẮC QUAN TRỌNG:
- TẤT CẢ AJAX handlers và POST/GET handlers phải đặt trong index.php TRƯỚC khi output HTML
- Không đặt handlers trong pages/*.php vì sẽ bị lỗi "headers already sent"

VỊ TRÍ ĐÚNG TRONG index.php:
1. Session start
2. Auth check (AuthModule::requireLogin)
3. Load Database class: require_once __DIR__ . '/config/database.php'
4. AJAX handlers (json response + exit)

QUAN TRỌNG: PHẢI require database.php TRƯỚC khi gọi AJAX endpoints sử dụng DB class.
- Nếu không require → lỗi "Class DB not found"
- Ví dụ: ajax_get_doctypes, ajax_get_companies, ajax_search, v.v. đều cần DB class
5. Load language file
6. POST/GET handlers (redirect + exit) - cần $lang cho popup message
7. Routing logic
8. Output HTML

CÁC AJAX HANDLER CẦN THIẾT CHO MỖI PAGE:
1. ajax_search: Tìm kiếm/filter danh sách
2. ajax_check_duplicate: Kiểm tra trùng lặp dữ liệu
3. ajax_get_{entity}: Lấy dữ liệu bản ghi theo ID (dùng cho edit)

VÍ DỤ:
// AJAX duplicate check - trước HTML output
if (isset($_GET['ajax_check_duplicate']) && $_GET['pages'] === 'company') {
  header('Content-Type: application/json');
  require_once __DIR__ . '/modules/CompanyModule.php';
  $module = new CompanyModule();
  $duplicates = $module->checkDuplicate($data, $excludeId);
  echo json_encode(['success' => true, 'duplicates' => $duplicates]);
  exit;
}

// AJAX get company by ID - dùng cho edit popup
if (isset($_GET['ajax_get_company']) && $_GET['pages'] === 'company' && isset($_GET['id'])) {
  header('Content-Type: application/json');
  require_once __DIR__ . '/modules/CompanyModule.php';
  $module = new CompanyModule();
  $company = $module->getById((int)$_GET['id']);
  echo json_encode(['success' => true, 'company' => $company]);
  exit;
}

// POST handler - sau khi load language
if (isset($_GET['pages']) && $_GET['pages'] === 'company' && $_SERVER['REQUEST_METHOD'] === 'POST') {
  require_once __DIR__ . '/modules/CompanyModule.php';
  $module = new CompanyModule();
  $module->insert($data);
  $_SESSION['popup_message'] = ['text' => $lang['success'], 'type' => 'success'];
  header('Location: ?pages=company');
  exit;
}

================================================================================
47. EDIT BUTTON - AJAX FETCH PATTERN
================================================================================

QUY TẮC QUAN TRỌNG:
- KHÔNG truyền dữ liệu trực tiếp qua onclick parameter (gây lỗi JS với ký tự đặc biệt)
- Chỉ truyền ID: onclick="editEntity(<?php echo (int)$row['ID']; ?>)"
- Dùng AJAX để fetch dữ liệu từ server sau khi mở popup

LÝ DO:
- Dữ liệu có thể chứa dấu nháy đơn ('), dấu ngoặc kép ("), backslash, newline
- escapeHtml KHÔNG escape dấu nháy đơn cho JS string
- json_encode với flags vẫn có thể gây lỗi trong HTML attribute
- Cách an toàn nhất: chỉ pass ID, fetch data qua AJAX

PATTERN CHO NÚT EDIT (PHP):
<button class="action-btn btn-edit" onclick="editCompany(<?php echo (int)$row['ID_Company']; ?>)">
  <i class="fas fa-edit"></i>
</button>

PATTERN CHO HÀM EDIT (JS):
function editCompany(id) {
  // Mở popup trước
  document.getElementById('companyPopupOverlay').classList.add('active');
  document.body.style.overflow = 'hidden';
  
  // Clear validation
  clearValidationStates();
  
  // Update title
  document.getElementById('company-popup-title').innerHTML = '...';
  
  // Fetch data từ server
  fetch('index.php?pages=company&ajax_get_company=1&id=' + id)
    .then(response => response.json())
    .then(data => {
      if (data.success && data.company) {
        document.getElementById('company-edit-id').value = data.company.ID_Company;
        document.getElementById('company-name-vn').value = data.company.Name_Vn || '';
        // ... fill các trường khác
      }
    })
    .catch(error => console.error('Error:', error));
}

PATTERN CHO AJAX RENDER (JS):
// Trong updateTable(), dùng escapeJs cho onclick
function escapeJs(text) {
  return String(text || '')
    .replace(/\\/g, '\\\\')
    .replace(/'/g, "\\'")
    .replace(/"/g, '\\"')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '\\r');
}
// Nhưng KHÔNG dùng escapeJs cho PHP render - chỉ dùng AJAX fetch pattern

================================================================================
48. LANGUAGE KEYS FOR VALIDATION
================================================================================

KEYS CẦN THIẾT CHO VALIDATION:
// message_vn.php
'error_required' => 'Vui lòng nhập đầy đủ thông tin bắt buộc',
'error_duplicate_field1' => 'Trường 1 đã tồn tại',
'error_duplicate_field2' => 'Trường 2 đã tồn tại',

// message_eng.php
'error_required' => 'Please fill in all required fields',
'error_duplicate_field1' => 'Field 1 already exists',
'error_duplicate_field2' => 'Field 2 already exists',

SỬ DỤNG TRONG JS:
const langKeys = {
  error_required: '<?php echo $lang["error_required"]; ?>',
  error_duplicate_field1: '<?php echo $lang["error_duplicate_field1"]; ?>'
};

================================================================================
49. FOOTER & SIDEBAR LINK UPDATES
================================================================================

TRANG CHỦ (menu01):
- Sidebar (control_menu.php): <a href="index.php" id="menu01" class="nav-item active">
- Footer (control_footer.php): <a href="index.php" id="menu01" class="footer-nav-item active">
- Link về index.php (không dùng href="#")
- Tất cả span chứa text phải có data-lang-key

CÔNG TY (menu02_01_01):
- Footer submenu (control_footer.php): <a href="?pages=company" id="menu02_01_01" class="footer-submenu-item">
- Link về trang company khi click Khai báo → Công ty

QUYỀN TRUY CẬP (menu08_01):
- Sidebar submenu: <a href="?pages=permission" id="menu08_01" class="submenu-item">
- Footer system-submenu: <a href="?pages=permission" id="menu08_01" class="footer-submenu-item">
- Footer active: menu05 (Khác) + menu05_05 (Hệ thống) active khi $active_l1 === 'menu08'

QUY TẮC:
- Menu item dẫn đến trang cụ thể phải có href đúng (không dùng "#")
- Menu có submenu dùng href="#" hoặc data-submenu để toggle submenu
- Link trong submenu phải dẫn đến trang thực sự

================================================================================
50. INPUT TEMPLATE SECTION STYLING
================================================================================

INPUT TEMPLATE SECTION:
- .input-template-section: background var(--bg-secondary), border 1px solid var(--border), border-radius 12px, margin-bottom 10px, overflow hidden
- .input-template-header: display flex, justify-content space-between, padding 5px 10px, background var(--btn-view), border-bottom 1px solid var(--border), cursor pointer, transition 0.3s
- .input-template-header:hover: background var(--hover)
- .input-template-title: font-size 14px, font-weight 600, color var(--text-light), display flex, align-items center, gap 10px, margin 0, text-transform uppercase, letter-spacing 0.5px

TOGGLE BEHAVIOR:
- Click header → toggle class "collapsed" trên section
- Collapsed: ẩn content, xoay icon chevron -90deg
- JS: toggleInputTemplate()

================================================================================
51. AJAX SEARCH PATTERN — KHÔNG RELOAD TRANG
================================================================================

QUY TẮC QUAN TRỌNG:
- TẤT CẢ trang dữ liệu (company, category, v.v.) PHẢI dùng AJAX search
- KHÔNG dùng window.location.href để tìm kiếm/phân trang → gây reload trang → mất focus input/select
- Chỉ reload trang khi: thêm/sửa/xóa thành công (POST/GET handler redirect)

PATTERN CHUẨN (giống company.php):
1. PHP đầu trang: load dữ liệu từ DB qua Module (getAll/searchAccentInsensitive)
2. PHP render bảng ban đầu (foreach $rows)
3. JS performSearch(): AJAX fetch → updateTable() → updatePagination() → updateUrl()
4. JS updateUrl(): history.pushState() (đổi URL không reload)
5. JS popstate handler: xử lý browser back/forward

CÁC HÀM JS BẮT BUỘC:
- performSearch(): Gọi AJAX fetch đến index.php?ajax_search={entity}
- updateTable(rows): Cập nhật tbody.innerHTML với dữ liệu mới
- updatePagination(total, page, perPage, pages): Cập nhật pagination-info + page-buttons
- updatePageButtons(): Tạo lại HTML cho nút phân trang (dùng javascript:goToPage())
- goToPage(page): Đặt currentPage → performSearch()
- updateUrl(): pushState với state {page, keyword}
- changePerPage(value): Đặt currentPerPage, currentPage=1 → performSearch()
- popstate handler: Khôi phục state → performSearch()

BIẾN JS BẮT BUỘC:
let currentPage = <?php echo $page; ?>;
let currentPerPage = <?php echo $perPage; ?>;
let currentKeyword = '<?php echo addslashes($keyword); ?>';
let totalPages = <?php echo $totalPages; ?>;
let totalRecords = <?php echo $total; ?>;

SEARCH INPUT:
- Debounce 300ms trên input event
- So sánh keyword !== currentKeyword trước khi search (tránh search thừa)
- Reset currentPage = 1 khi đổi keyword

PAGINATION BUTTONS:
- Dùng <a href="javascript:goToPage(X)"> thay vì <a href="?pages=X&page=Y">
- Previous/Next: href="javascript:goToPage(currentPage-1/+1)"
- Disabled: <button disabled> thay vì <a>

AJAX HANDLER TRONG INDEX.PHP:
if (isset($_GET['ajax_search']) && $_GET['ajax_search'] === 'category') {
  header('Content-Type: application/json');
  // ... fetch data via Module
  echo json_encode(['success' => true, 'data' => $rows, 'total' => $total, ...]);
  exit;
}

QUY TẮC:
1. KHÔNG dùng window.location.href cho search/pagination (gây mất focus)
2. LUÔN dùng AJAX + pushState cho search/pagination
3. LUÔN có popstate handler cho browser back/forward
4. Page buttons dùng javascript:goToPage() thay vì URL trực tiếp
5. Chỉ reload trang khi CRUD thành công (POST/GET handler redirect)

================================================================================
52. JS/PHP QUOTE CONFLICT — TRÁNH SYNTAX ERROR
================================================================================

VẤN ĐỀ:
- PHP echo $lang['key'] dùng dấu nháy đơn cho array key
- Nếu JS string cũng dùng dấu nháy đơn → xung đột → SyntaxError

VÍ DỤ LỖI:
// ❌ SAI — PHP 'key' xung đột JS 'string'
const value = '<?php echo $lang['key']; ?>';
// Render thành: const value = 'Giá trị', → LỖI cú pháp

GIẢI PHÁP:
// ✅ DÙNG dấu nháy kép cho JS string
const value = "<?php echo $lang['key']; ?>";
// Render thành: const value = "Giá trị" → ĐÚNG

// ✅ HOẶC dùng dấu nháy kép cho PHP array key
const value = '<?php echo $lang["key"]; ?>';
// Render thành: const value = 'Giá trị' → ĐÚNG

ÁP DỤNG CHO TẤT CẢ:
1. langKeys object: Dùng "..." cho JS string chứa PHP echo
2. innerHTML gán: Dùng "..." cho JS string chứa PHP echo
3. showConfirmPopupJS(): Dùng "..." cho JS string chứa PHP echo
4. TẤT CẢ nơi có PHP echo $lang['key'] trong JS → PHẢI dùng nháy kép cho outer string

QUY TẮC:
- PHP echo trong JS: LUÔN dùng dấu nháy kép cho JS outer string
- HOẶC dùng dấu nháy kép cho PHP array key: $lang["key"]
- Ưu tiên cách 1 (nháy kép JS) vì rõ ràng hơn

================================================================================
53. SCRIPT TAG — LUÔN ĐÓNG ĐÚNG
================================================================================

VẤN ĐỀ:
- Thiếu </script> ở cuối file page → browser parse HTML/PHP tiếp theo thành JS → SyntaxError
- Lỗi biểu hiện: "Uncaught SyntaxError: expected expression, got '<'" tại index.php

QUY TẮC:
1. MỌI <script> PHẢI có </script> đóng tương ứng
2. Kiểm tra cuối mỗi file page.php có </script> đóng
3. Không có script tag nào không đóng
4. Khi thêm script block mới, LUÔN kiểm tra tag đóng

KIỂM TRA:
- Đếm số <script> và </script> trong file → phải bằng nhau
- Đặc biệt cuối file page.php — đảm bảo </script> cuối cùng tồn tại

================================================================================
54. SIDEBAR SUBMENU ACTIVE STATE
================================================================================

QUY TẮC ACTIVE STATE CHO MENU:
1. Menu cấp 1 (nav-item): Thêm class 'active' khi submenu con đang mở HOẶC trang hiện tại thuộc menu đó
2. Menu cấp 2 (submenu-item): Thêm class 'active' khi submenu cấp 3 đang mở HOẶC trang hiện tại thuộc menu đó
3. Khi click menu cấp 3 (load trang mới) → ĐÓNG tất cả submenu
4. Sau khi trang tải xong → submenu KHÔNG tự mở (chỉ highlight menu item active)
5. Active state chỉ dựa trên trang hiện tại ($page variable), KHÔNG dựa trên submenu đang mở

JAVASCRIPT LOGIC:
- Click nav-item.has-submenu → toggle submenu cấp 2, đóng các submenu khác
- Click submenu-item.has-submenu → toggle submenu cấp 3, đóng các submenu cấp 3 khác
- Click submenu-item KHÔNG có submenu (trang cụ thể) → đóng tất cả submenu + load trang
- Click outside sidebar → đóng tất cả submenu

PHP ACTIVE CLASS:
- Dựa trên $page variable để thêm class 'active' cho menu item tương ứng
- Ví dụ: <?php echo ($page === 'company') ? 'active' : ''; ?>
- Menu cấp 1 active khi BẤT KỲ menu con nào active

QUY TẮC:
1. KHÔNG tự động mở submenu khi trang tải xong
2. Active class chỉ thêm cho menu item của trang hiện tại
3. Click trang cụ thể (cấp 3) → đóng submenu, không giữ mở
4. Submenu chỉ mở khi user chủ động click vào menu có submenu

================================================================================
55. CATEGORY MODULE & PAGE
================================================================================

MODULE: modules/CategoryModule.php
- Kế thừa BaseModule
- $table = 'Category'
- $primaryKey = 'ID_Category'
- $fillable = ['Name_Vn', 'Name_Eng']
- $searchable = ['ID_Category', 'Name_Vn', 'Name_Eng']
- Methods bổ sung:
  + searchAccentInsensitive($keyword, $orderBy, $limit, $offset): Tìm kiếm không phân biệt dấu tiếng Việt (COLLATE SQL_Latin1_General_CP1_CI_AI)
  + countSearchAccentInsensitive($keyword): Đếm kết quả tìm kiếm không phân biệt dấu
  + checkDuplicate($data, $excludeId): Kiểm tra trùng Name_Vn, Name_Eng
  + removeVietnameseAccents($str): Helper chuyển có dấu → không dấu

PAGE: pages/documentout.php
- Bảng 8 cột: ID (Id int), Số hiệu (ID_Symbol nvarchar), Ngày hiệu lực (EffectiveDate date), Cơ quan nhận (Issuer nvarchar), Từ Công ty (ID_Company smallint → JOIN Company lấy Name_Vn/Name_Eng), Tóm tắt nội dung (Summary nvarchar), Thể loại (DocType smallint → JOIN DocType lấy Name_Vn/Name_Eng theo ngôn ngữ), Thao tác
- Popup form: 7 trường (ID_Symbol Type1, EffectiveDate Type4, Issuer Type1, ID_Company Type2 select, DocType Type2 select, Summary Type3 textarea full-width, Notes Type3 textarea full-width)
- Required fields: ID_Symbol, EffectiveDate, Issuer
- Dropdown Company/DocType load qua AJAX khi mở popup (ajax_get_companies, ajax_get_doctypes)
- Action buttons: btn-edit (fa-edit), btn-view (fa-eye), btn-import (fa-upload), btn-export (fa-download), btn-delete (fa-trash)
- Edit: AJAX fetch data (ajax_get_documentout) → đổ vào form, load dropdowns với selected value
- Delete: showConfirmPopupJS → AJAX GET action=delete
- AJAX search pattern (giống company.php — không reload trang)

AJAX HANDLERS (index.php):
- ajax_search=documentout: Tìm kiếm/filter danh sách công văn đi (accent insensitive)
- ajax_get_documentout=1 + pages=documentout + id: Lấy dữ liệu công văn theo ID (với JOINs)
- ajax_get_companies=1: Lấy danh sách Company cho dropdown (không cần pages param)
- ajax_get_doctypes=1: Lấy danh sách DocType cho dropdown (không cần pages param)
- ajax_get_docpagetypes=1: Lấy danh sách DocPageType cho dropdown Loại giấy tờ (không cần pages param)

POST/GET HANDLERS (index.php):
- POST docout_submit: Thêm/sửa công văn đi (edit_id rỗng = thêm, có giá trị = sửa)
- GET action=delete + id: Xóa công văn đi

DB TABLE: dbo.DocumentOut
- Id int (PK, identity)
- ID_Symbol nvarchar — Số hiệu công văn
- EffectiveDate date — Ngày hiệu lực
- Issuer nvarchar — Cơ quan nhận
- ID_Company smallint — FK → dbo.Company.ID_Company
- Summary nvarchar — Tóm tắt nội dung
- DocType smallint — FK → dbo.DocType.Id_Type (LƯU Ý: cột FK là DocType, KHÔNG phải Id_Type)
- TypeDoc smallint — FK → dbo.DocPageType.Id_Type (Loại giấy tờ)
- Quantity int — Số lượng
- FileUrl nvarchar — Đường dẫn file scan
- FileStorage nvarchar — Đường dẫn file lưu trữ
- ID_by int — Người tạo
- Notes nvarchar — Ghi chú
- CreatedAt datetime — Ngày tạo
- ID_Export int — ID xuất khẩu

DB TABLE: dbo.DocType
- Id_Type smallint (PK)
- Name_Vn nvarchar
- Name_Eng nvarchar

DB TABLE: dbo.DocPageType
- Id_Type smallint (PK)
- Name_Vn nvarchar
- Name_Eng nvarchar

JOIN QUERIES (DocumentOutModule):
- LEFT JOIN dbo.Company c ON do.ID_Company = c.ID_Company
- LEFT JOIN dbo.DocType dt ON do.DocType = dt.Id_Type
- Select Company name theo ngôn ngữ: c.Name_Vn/c.Name_Eng AS CompanyName
- Select DocType name theo ngôn ngữ: dt.Name_Vn/dt.Name_Eng AS DocTypeName

LANGUAGE KEYS (message_vn.php & message_eng.php):
- docout_main_title: 'Công văn đi' / 'Outgoing Document'
- docout_search_placeholder: 'Tìm theo số hiệu, cơ quan, nội dung...' / 'Search by number, agency, content...'
- docout_no_records: 'Không tìm thấy công văn phù hợp' / 'No matching documents found'
- col_document_number: 'SỐ HIỆU' / 'NUMBER'
- col_effective_date: 'NGÀY HIỆU LỰC' / 'EFFECTIVE DATE'
- col_receiving_agency: 'CƠ QUAN NHẬN' / 'RECEIVING AGENCY'
- col_from_company: 'TỪ CÔNG TY' / 'FROM COMPANY'
- col_content_summary: 'TÓM TẮT NỘI DUNG' / 'CONTENT SUMMARY'
- col_document_type: 'THỂ LOẠI' / 'DOCUMENT TYPE'
- btn_view_file: 'Xem file' / 'View file'
- btn_import_file: 'Nhập file' / 'Import file'
- btn_export_file: 'Xuất file' / 'Export file'
- docout_form_title: 'Thêm mới công văn đi' / 'Add Outgoing Document'
- docout_form_edit_title: 'Sửa công văn đi' / 'Edit Outgoing Document'
- docout_id_symbol_label/placeholder, docout_effective_date_label/placeholder, docout_issuer_label/placeholder
- docout_company_label/placeholder, docout_summary_label/placeholder, docout_doctype_label/placeholder
- docout_notes_label/placeholder, docout_error_required
- docout_add_success, docout_update_success, docout_save_error

PAGE: pages/category.php
- Bảng 4 cột: ID (ID_Category smallint), Tên tiếng Việt (Name_Vn nvarchar), Tên tiếng Anh (Name_Eng nvarchar), Thao tác
- Popup form: 2 trường (Name_Vn, Name_Eng), có validation + duplicate check
- AJAX search pattern (giống company.php — không reload trang)
- Action buttons: btn-edit (fa-edit) + btn-delete (fa-trash)
- Edit: AJAX fetch data → đổ vào form (giống company pattern)
- Delete: showConfirmPopupJS → redirect ?action=delete&id=

AJAX HANDLERS (index.php):
- ajax_search=category: Tìm kiếm/filter danh sách hạng mục
- ajax_check_duplicate + pages=category: Kiểm tra trùng Name_Vn, Name_Eng
- ajax_get_category=1 + pages=category + id: Lấy dữ liệu hạng mục theo ID

POST/GET HANDLERS (index.php):
- POST category_submit: Thêm/sửa hạng mục (edit_id rỗng = thêm, có giá trị = sửa)
- GET action=delete + id: Xóa hạng mục

DB TABLE: dbo.Category
- ID_Category smallint (PK)
- Name_Vn nvarchar
- Name_Eng nvarchar

================================================================================
56. CUSTOM SELECT DROPDOWN CHO ROWS-PER-PAGE
================================================================================

QUY TẮC:
- TẤT CẢ trang có phân trang (home, company, category, v.v.) PHẢI dùng custom select dropdown cho rows-per-page
- KHÔNG dùng native <select> — giao diện không đồng bộ với theme
- Dropdown mở LÊN TRÊN (bottom: calc(100% + 5px)) thay vì xuống dưới

HTML PATTERN:
<div class="rows-per-page">
  <span><?php echo $lang['pagination_rows_per_page']; ?></span>
  <div class="rows-per-page-select custom-select-wrapper" id="per-page-select">
    <div class="input-wrapper">
      <input type="text" class="form-input custom-select-input" value="<?php echo $perPage; ?>" data-selected-value="<?php echo $perPage; ?>" readonly>
      <i class="fas fa-chevron-down dropdown-arrow"></i>
    </div>
    <div class="custom-select-dropdown">
      <div class="custom-select-options">
        <div class="custom-select-option<?php echo $perPage === 5 ? ' selected' : ''; ?>" data-value="5">5</div>
        <div class="custom-select-option<?php echo $perPage === 10 ? ' selected' : ''; ?>" data-value="10">10</div>
        <div class="custom-select-option<?php echo $perPage === 20 ? ' selected' : ''; ?>" data-value="20">20</div>
        <div class="custom-select-option<?php echo $perPage === 50 ? ' selected' : ''; ?>" data-value="50">50</div>
        <div class="custom-select-option<?php echo $perPage === 100 ? ' selected' : ''; ?>" data-value="100">100</div>
      </div>
    </div>
  </div>
</div>

JS PATTERN (IIFE tự thực thi):
(function() {
  const wrapper = document.getElementById('per-page-select');
  if (!wrapper) return;
  const input = wrapper.querySelector('.custom-select-input');
  const options = wrapper.querySelectorAll('.custom-select-option');

  input.addEventListener('click', function(e) {
    e.stopPropagation();
    wrapper.classList.toggle('open');
  });

  options.forEach(function(option) {
    option.addEventListener('click', function(e) {
      e.stopPropagation();
      const value = this.getAttribute('data-value');
      const text = this.textContent;
      input.value = text;
      input.setAttribute('data-selected-value', value);
      options.forEach(function(opt) { opt.classList.remove('selected'); });
      this.classList.add('selected');
      wrapper.classList.remove('open');
      changePerPage(value);  // Gọi hàm changePerPage có sẵn
    });
  });

  document.addEventListener('click', function(e) {
    if (!wrapper.contains(e.target)) {
      wrapper.classList.remove('open');
    }
  });
})();

CSS (components.css):
- .rows-per-page-select.custom-select-wrapper: width 60px
- .rows-per-page-select .custom-select-input: padding 8px 30px 8px 12px, background var(--btn-view), border 1px solid var(--border), color var(--text-primary), border-radius 5px, cursor pointer, font-size 12px, width 100%, text-align center
- .rows-per-page-select .dropdown-arrow: position absolute, right 10px, color var(--text-secondary), font-size 12px, pointer-events none, transition 0.3s
- .rows-per-page-select.custom-select-wrapper.open .dropdown-arrow: transform rotate(180deg)
- .rows-per-page-select .custom-select-dropdown: top auto, bottom calc(100% + 5px), z-index 10000
- .rows-per-page-select .custom-select-option: text-align center
- Mobile: .custom-select-input padding 6px 25px 6px 10px, font-size 11px; .dropdown-arrow font-size 10px, right 8px; .custom-select-option font-size 11px, padding 8px 12px

QUY TẮC:
1. KHÔNG dùng native <select> cho rows-per-page — dùng custom select dropdown
2. Dropdown mở LÊN TRÊN (bottom thay vì top)
3. z-index 10000 cho dropdown (trên table-container)
4. Dùng IIFE để tránh conflict với TYPE 2 generic handler
5. TYPE 2 generic handler PHẢI exclude .rows-per-page-select: querySelectorAll('.custom-select-wrapper:not(.rows-per-page-select)')

================================================================================
57. PAGE BUTTON STYLING
================================================================================

CSS (.page-btn):
- text-decoration: none — KHÔNG có gạch dưới (vì dùng <a> tag)
- display: inline-flex — căn icon và text đồng nhất
- align-items: center, justify-content: center
- padding: 8px 15px
- background-color: var(--btn-view)
- border: 1px solid var(--border)
- color: var(--text-light)
- border-radius: 5px
- cursor: pointer
- transition: all 0.3s

QUY TẮC:
1. .page-btn dùng cho CẢ <button> và <a> — PHẢI có text-decoration: none
2. Dùng inline-flex + align-items/justify-content center cho icon căn giữa
3. <a> tag dùng href="javascript:goToPage(X)" thay vì URL trực tiếp
4. Disabled: <button disabled> thay vì <a>

================================================================================
58. TABLE CONTAINER — OVERFLOW VISIBLE
================================================================================

VẤN ĐỀ:
- .table-container có overflow: hidden → che custom select dropdown mở lên trên
- overflow: hidden cắt bỏ mọi nội dung tràn ra ngoài, kể cả z-index cao

GIẢI PHÁP:
- .table-container: overflow: visible (KHÔNG dùng overflow: hidden)
- Nếu cần bo góc table → dùng border-radius trên table trực tiếp, KHÔNG dựa vào overflow: hidden của container

CSS:
.table-container {
  background-color: var(--bg-secondary);
  border-radius: 12px;
  overflow: visible;  /* KHÔNG dùng hidden — sẽ che dropdown */
  border: 1px solid var(--border);
}

QUY TẮC:
1. .table-container KHÔNG dùng overflow: hidden
2. Dropdown pagination cần thoát ra ngoài container
3. Nếu cần ẩn nội dung tràn → xử lý ở level thấp hơn (table, thead, tbody), KHÔNG ở container

============================================================
59. AJAX PAGE LOADING - TẢI TRANG KHÔNG RELOAD HEADER/FOOTER/SIDEBAR
============================================================

MỤC ĐÍCH:
Khi click menu navigation → chỉ reload nội dung trang (page content), KHÔNG reload control_header, control_menu, control_footer.

CẤU TRÚC HTML (index.php):
<body>
  include control_header.php
  <div class="container">
    include control_menu.php
    <div id="page-content">  <!-- WRAPPER cho AJAX replace -->
      include page.php (home/company/category/...)
    </div>
  </div>
  include control_footer.php (OUTSIDE container)
</body>

AJAX HANDLER (index.php - TRƯỚC HTML output):
// Xử lý AJAX load page
if (isset($_GET['ajax_load_page']) && isset($_GET['pages'])) {
  // Load language file trước (pages dùng $lang)
  $current_lang = isset($_SESSION['lang']) ? $_SESSION['lang'] : 'vn';
  if ($current_lang === 'eng') include 'lang/message_eng.php';
  else include 'lang/message_vn.php';

  // Include page file
  if (file_exists($ajax_page_file)) {
    header('Content-Type: text/html; charset=UTF-8');
    include $ajax_page_file;
  }
  exit;
}

QUAN TRỌNG: ajax_load_page handler PHẢI load language file trước khi include page,
nếu không $lang sẽ undefined → Warning: Undefined variable $lang

QUAN TRỌNG: Khi page load qua AJAX, DOMContentLoaded ĐÃ KÍCH HOẠT từ trước (khi load trang chính lần đầu).
- Event listener trong document.addEventListener('DOMContentLoaded', ...) trong page script sẽ KHÔNG BAO GIỜ chạy.
- KHÔNG BAO GIỜ dùng document.addEventListener('DOMContentLoaded', ...) trong page script (pages/*.php).
- GIẢI PHÁP: Dùng IIFE chạy ngay lập tức thay vì đợi DOMContentLoaded:
  (function initPageControls() { ... })();
- Vì DOM đã sẵn sàng khi page load qua AJAX, code có thể chạy ngay mà không cần đợi event.
- Áp dụng cho TẤT CẢ các loại handler: custom select, date picker, toggle switch, numeric input, stepper, pagination, v.v.

CRUD AJAX HANDLER (index.php - POST/GET handlers):
Khi form submit hoặc delete qua AJAX, thêm param ajax_submit=1:
- POST: kiểm tra $_POST['ajax_submit'] → trả JSON thay vì redirect
- GET delete: kiểm tra $_GET['ajax_submit'] → trả JSON thay vì redirect

Format JSON response:
{ "success": true/false, "message": "thông báo", "type": "success/error" }

Ví dụ POST handler:
$is_ajax = !empty($_POST['ajax_submit']);
try {
  // ... insert/update logic ...
  $message = $lang['xxx_success'];
  $msg_type = 'success';
} catch (Throwable $e) {
  $message = $lang['xxx_error'];
  $msg_type = 'error';
}
if ($is_ajax) {
  header('Content-Type: application/json');
  echo json_encode(['success' => $msg_type === 'success', 'message' => $message, 'type' => $msg_type]);
  exit;
}
// Fallback: redirect (khi không phải AJAX)
$_SESSION['popup_message'] = ['text' => $message, 'type' => $msg_type];
header('Location: ?pages=xxx');
exit;

GLOBAL JS FUNCTIONS (index.php - script block riêng, NGOÀI DOMContentLoaded):

0. var ajaxPages = ['home', 'company', 'category', 'permission', 'documentout']
   - Biến toàn cục, dùng bởi cả menu click interceptor (trong DOMContentLoaded) và changeLanguage()
   - PHẢI nằm ngoài DOMContentLoaded closure để accessible globally

1. loadPageAjax(pageName, pushState)
   - Fetch: index.php?pages={pageName}&ajax_load_page=1
   - Replace: #page-content.innerHTML = html
   - Execute scripts: querySelectorAll('script') → tạo mới, wrap inline script trong IIFE
   - IIFE wrap: '(function() { ... })();' → tránh let/const redeclaration giữa các trang
   - pushState: cập nhật URL nếu pushState !== false
   - Gọi updateSidebarActive(pageName)
   - Gọi updateLanguageLinks(pageName) (fallback cho href-based language links)

2. updateSidebarActive(pageName)
   - Map page → menu IDs (L1, L2, L3)
   - Sidebar: scoped querySelectorAll trong .sidebar, set active/open cho nav-item + submenu-item
   - Footer: scoped querySelectorAll trong .footer, set active cho footer-nav-item + footer-submenu-item
   - L1 open khi có L2, L2 open khi có L3
   - Dùng selector kết hợp ID+class: sidebar.querySelector('#menu02.nav-item') để tránh duplicate ID
   - Permission page map: menu08 (L1), menu08_01 (L2)
   - Sidebar menu08 map sang footer menu05 (More) cho L1 active
   - Sidebar menu08_01 map sang footer menu05_05 (Hệ thống) cho L2 active
   - Footer cũng set active cho menu08_01 trong system-submenu (nếu khác với menu05_05)

3. changeLanguage(lang)
   - POST action=change_lang&lang={lang} → lưu session, trả permission_name
   - GET ajax_get_lang=1&lang={lang} → trả toàn bộ $lang array dạng JSON
   - Cập nhật tất cả [data-lang-key] elements: textContent hoặc attribute (nếu có data-lang-attr)
   - Cập nhật #header-permission-name từ response.permission_name
   - Cập nhật active state cho lang-btn (sidebar + footer)
   - Đóng footer submenus
   - Reload page content qua loadPageAjax(currentPage, false)

4. showPopupNoticeJS(message, type, duration)
   - Tạo popup notice động (không cần reload trang)
   - HTML structure: .popup-notice > .popup-notice-content > (.popup-notice-icon + .popup-notice-message + .popup-notice-close)
   - PHẢI có .popup-notice-content wrapper (CSS dùng flex layout)
   - Dùng requestAnimationFrame x2 trước addClass 'show' → đảm bảo CSS transition hoạt động
   - Tự động ẩn sau duration ms

MENU CLICK INTERCEPTOR (trong DOMContentLoaded):
- Bắt click trên a[href*="pages="] và a[href="index.php"] (home link)
- Skip language/theme links: nếu click target nằm trong .language-section, .language-submenu, .theme-section, .theme-submenu → return (không intercept)
- Chỉ intercept trang trong ajaxPages (biến toàn cục)
- Ctrl+Click / Shift+Click → KHÔNG intercept (mở tab mới)
- Close sidebar/footer submenus trước khi load
- Gọi loadPageAjax(pageName)

POPSTATE HANDLER:
- window.addEventListener('popstate') → loadPageAjax(e.state.ajaxPage, false)
- Browser back/forward hoạt động đúng

PAGE SCRIPT RULES (company.php, category.php, home.php):

1. WINDOW EXPOSE: Tất cả hàm gọi từ HTML onclick PHẢI gán lên window:
   window.openCompanyPopup = openCompanyPopup;
   window.closeCompanyPopup = closeCompanyPopup;
   window.editCompany = editCompany;
   window.submitCompanyForm = submitCompanyForm;
   window.confirmDeleteCompany = confirmDeleteCompany;

   Lý do: IIFE wrap cách ly scope → hàm không global nếu không gán window

2. SUBMIT FORM QUA AJAX:
   async function submitXxxForm() {
     // Validate + check duplicate...
     var form = document.getElementById('xxx-form');
     var formData = new FormData(form);
     formData.append('ajax_submit', '1');
     var response = await fetch(form.action, { method: 'POST', body: formData });
     var result = await response.json();
     closeXxxPopup();
     if (typeof showPopupNoticeJS === 'function') showPopupNoticeJS(result.message, result.type, 4000);
     if (typeof loadPageAjax === 'function') loadPageAjax('xxx', false);
     else window.location.reload();
   }

3. DELETE QUA AJAX:
   function confirmDeleteXxx(id) {
     showConfirmPopupJS(message, 'warning', confirmText, cancelText, function() {
       fetch('index.php?pages=xxx&action=delete&id=' + id + '&ajax_submit=1')
         .then(r => r.json())
         .then(result => {
           showPopupNoticeJS(result.message, result.type, 4000);
           loadPageAjax('xxx', false);
         });
     }, null);
   }

4. KHÔNG dùng form.submit() hay window.location.href cho CRUD → dùng fetch AJAX

POPUP NOTICE CSS:
.popup-notice {
  position: fixed; top: 15px; right: 20px; z-index: 10001;
  opacity: 0; transform: translateX(120%); transition: all 0.3s ease;
}
.popup-notice.show {
  opacity: 1; transform: translateX(0);
}
- Xuất hiện: phải sang trái (translateX 120% → 0)
- Ẩn: trái sang phải (translateX 0 → 120%)
- PHẢI dùng requestAnimationFrame x2 trước addClass 'show' để transition hoạt động

QUY TẮC TỔNG QUAN:
1. ajax_load_page handler PHẢI load $lang trước include page
2. CRUD handler trả JSON khi có ajax_submit param, redirect khi không có (fallback)
3. Page scripts PHẢI expose functions lên window (IIFE wrap cách ly scope)
4. Inline scripts được wrap IIFE khi AJAX load → tránh let/const redeclaration
5. showPopupNoticeJS PHẢI có .popup-notice-content wrapper + requestAnimationFrame delay
6. updateSidebarActive dùng scoped querySelector (trong .sidebar/.footer) + ID+class selector
7. Menu click interceptor xử lý cả home link (href="index.php" không có ?pages=)
8. popstate handler dùng pushState=false để không thêm history entry
9. loadPageAjax, updateSidebarActive, changeLanguage PHẢI nằm trong script block riêng (global scope), KHÔNG trong DOMContentLoaded closure
10. ajaxPages PHẢI là biến toàn cục (var), KHÔNG const trong closure
11. Tất cả text hiển thị trong sidebar/header/footer PHẢI có data-lang-key để changeLanguage() cập nhật
12. changeLanguage() cập nhật cả #header-permission-name (tên quyền theo ngôn ngữ mới)

================================================================================
60. PERMISSION MANAGEMENT PAGE (QUẢN LÝ QUYỀN TRUY CẬP)
================================================================================

FILE: pages/permission.php
- Trang quản lý quyền truy cập cho từng menu item theo permission level (0-5)
- Chỉ hiển thị cho user có quyền truy cập menu08_01 (mặc định: level 5)
- Permission check ở đầu file (xem section 61)

CẤU TRÚC HTML:
<div class="content">
  <h1 class="page-title"><i class="fas fa-shield-alt page-title-icon"></i>{TITLE}</h1>
  <div class="permission-container">
    <div class="permission-header">
      <div class="permission-menu-col">{LABEL}</div>
      <div class="permission-toggles">
        <!-- 6 toggle wrappers cho level 0-5 -->
        <div class="permission-toggle-wrapper" title="{tên quyền từ DB}">
          <span class="permission-level-badge permission-{level}">{level}</span>
        </div>
      </div>
    </div>
    <div class="permission-tree">
      <!-- PHP render đệ quy menu tree 3 cấp -->
    </div>
    <div class="permission-actions">
      <button class="btn btn-reset" onclick="resetPermissions()">...</button>
      <button class="btn btn-save" onclick="savePermissions()">...</button>
    </div>
  </div>
</div>

MENU TREE (3 cấp):
- Đệ quy từ $menu_tree array, mỗi item có: id, label, children (optional)
- Level 1: .permission-row.permission-level-1 (padding-left: 15px, font-weight: 600)
- Level 2: .permission-row.permission-level-2 (padding-left: 35px, font-weight: 500)
- Level 3: .permission-row.permission-level-3 (padding-left: 55px, font-weight: 400)
- Mỗi row: .permission-menu-name (tên menu) + .permission-toggles (6 toggle wrappers)

PERMISSION TOGGLE:
- .permission-toggle-wrapper: chứa badge + toggle switch
- .permission-level-badge: hiển thị số level (0-5), màu theo cấp
- .permission-toggle: iOS-style toggle switch (width 36px, height 18px)
- .permission-toggle-slider: slider tròn, di chuyển khi checked
- Checked state: thêm class 'checked' trên .permission-toggle
- Title attribute: hiển thị tên quyền từ dbo.Properti_Permisson

PERMISSION LEVELS (từ DB):
- Load từ dbo.Properti_Permisson: SELECT ID, Name_VN/Name_ENG AS PermName ORDER BY ID
- Name_VN/Name_ENG tùy theo $_SESSION['lang']
- Badge hiển thị số (0-5), title hiển thị tên quyền (Cấp 0, Cấp 1...)
- Fallback nếu DB thiếu: 'Cấp ' . $i

AJAX SAVE (index.php handler):
- POST action=save_permission + permissions data
- permissions: object { menu_id: "0,1,2,3,4,5" }
- Lưu vào config/access.json
- Response JSON: { success: true, message: "..." }

AJAX RESET (index.php handler):
- POST action=reset_permission
- Reset access.json về giá trị mặc định (tất cả levels "0,1,2,3,4,5")
- Response JSON: { success: true, message: "..." }

JS FUNCTIONS (trong permission.php):
- savePermissions(): thu thập toggle states → AJAX POST save_permission → showPopupNoticeJS
- resetPermissions(): showConfirmPopupJS → AJAX POST reset_permission → showPopupNoticeJS → reload
- Toggle click handler: thêm/xóa class 'checked', cập nhật visual state

MENU TREE STRUCTURE (phải đồng bộ với control_menu.php):
$menu_tree = [
  ['id' => 'menu01', 'label' => $lang['menu01']],
  ['id' => 'menu02', 'label' => $lang['menu02'], 'children' => [
    ['id' => 'menu02_01', 'label' => $lang['menu02_01'], 'children' => [
      ['id' => 'menu02_01_01', 'label' => $lang['menu02_01_01']],
      ['id' => 'menu02_01_02', 'label' => $lang['menu02_01_02']],
    ]],
    ['id' => 'menu02_02', 'label' => $lang['menu02_02'], 'children' => [
      ['id' => 'menu02_02_01', 'label' => $lang['menu02_02_01']],
      ['id' => 'menu02_02_02', 'label' => $lang['menu02_02_02']],
    ]],
  ]],
  ['id' => 'menu03', 'label' => $lang['menu03']],
  ['id' => 'menu04', 'label' => $lang['menu04']],
  ['id' => 'menu05', 'label' => $lang['menu05']],
  ['id' => 'menu06', 'label' => $lang['menu06']],
  ['id' => 'menu07', 'label' => $lang['menu07']],
  ['id' => 'menu08', 'label' => $lang['menu08'], 'children' => [
    ['id' => 'menu08_01', 'label' => $lang['permission_main_title']],
    ['id' => 'menu08_02', 'label' => $lang['menu08'] . ' 2'],
  ]],
];

QUY TẮC:
1. Menu tree PHẢI đồng bộ với sidebar structure trong control_menu.php
2. Permission levels load từ DB (Properti_Permisson), KHÔNG hardcode tên
3. Badge hiển thị SỐ level, title hiển thị TÊN quyền
4. CSS permission nằm trong components.css (KHÔNG inline CSS trong PHP)
5. Toggle checked state lưu dưới dạng comma-separated string "0,1,2,3,4,5"
6. Save/Reset qua AJAX, KHÔNG reload toàn bộ trang
7. Chỉ user có quyền menu08_01 mới truy cập được trang này

================================================================================
61. PAGE ACCESS CONTROL (KIỂM SOÁT QUYỀN TRUY CẬP TRANG)
================================================================================

QUY TẮC:
- MỌI trang trong pages/ PHẢI kiểm tra quyền trước khi load nội dung
- Nếu user không có quyền → hiển thị error 403 (include error.php + return)
- KHÔNG redirect sang trang error riêng, hiển thị lỗi TRONG page content

PATTERN (thêm ở đầu mỗi page.php):
<?php
// Check permission before loading page
$user_perm_id = isset($_SESSION['user_permission_id']) ? (int)$_SESSION['user_permission_id'] : 0;
$access_check_file = __DIR__ . '/../config/access.json';
$access_check_json = @file_get_contents($access_check_file);
$access_check_data = $access_check_json ? json_decode($access_check_json, true) : null;
$menu_perm_key = 'menuXX_YY_ZZ'; // Menu ID của trang này
$allowed_levels = [];
if ($access_check_data && isset($access_check_data['permissions'][$menu_perm_key])) {
  $allowed_levels = array_map('intval', explode(',', $access_check_data['permissions'][$menu_perm_key]));
}
if (!in_array($user_perm_id, $allowed_levels)) {
  $error_code = '403';
  $error_icon = 'fa-ban';
  $error_title = $lang['error_forbidden_title'];
  $error_message = $lang['error_forbidden_message'];
  include __DIR__ . '/error.php';
  return;
}
?>

MAPPING PAGE → MENU ID:
- permission.php → menu08_01
- company.php → menu02_01_01
- category.php → menu02_01_02
- documentout.php → menu02_02_01
- document_in.php → menu02_02_02
- home.php → menu01 (mặc định ai cũng truy cập được)
- {new_page}.php → menuXX_YY_ZZ (tương ứng với menu ID)

ACCESS.JSON STRUCTURE:
{
  "permissions": {
    "menu01": "0,1,2,3,4,5",
    "menu02": "0,1,2,3,4,5",
    "menu02_01": "0,1,2,3,4,5",
    "menu02_01_01": "2,3,4,5",
    "menu02_01_02": "2,3,4,5",
    "menu02_02": "0,2,5",
    "menu02_02_01": "2,3,4,5",
    "menu02_02_02": "2,3,4,5",
    "menu03": "0,1,2,3,4,5",
    "menu04": "0,1,2,3,4,5",
    "menu05": "0,1,2,3,4,5",
    "menu06": "0,1,2,3,4,5",
    "menu07": "0,1,2,3,4,5",
    "menu08": "0,1,2,3,4,5",
    "menu08_01": "5",
    "menu08_02": "0,1,2,3,4,5"
  }
}
- Giá trị: comma-separated string các permission level được phép truy cập
- Mặc định: "0,1,2,3,4,5" (tất cả level đều truy cập được)
- menu08_01 (trang quyền): chỉ level 5 (admin)
- menu02_01_01, menu02_01_02 (company, category): level 2,3,4,5

QUY TẮC:
1. Mọi trang mới PHẢI thêm permission check ở đầu file
2. Menu ID phải khớp với ID trong access.json
3. Nếu menu ID không có trong access.json → mặc định cho phép truy cập
4. Error 403 hiển thị inline (include error.php + return), KHÔNG redirect
5. Khi thêm trang mới → thêm entry vào access.json với default "0,1,2,3,4,5"

================================================================================
62. MENU VISIBILITY BASED ON PERMISSIONS (ẨN MENU THEO QUYỀN)
================================================================================

QUY TẮC:
- Menu items trong sidebar (control_menu.php) và footer (control_footer.php) PHẢI ẩn nếu user không có quyền
- Áp dụng cho TẤT CẢ 3 cấp menu (L1, L2, L3)
- Menu cha có submenu: ẩn nếu TẤT CẢ menu con đều bị ẩn
- Language/Theme section (menu09, menu10) luôn hiển thị (không kiểm tra quyền)

HELPER FUNCTION (định nghĩa trong control_menu.php / control_footer.php):
function canAccess($menu_id, $perm_id, $perms) {
  if (!isset($perms[$menu_id])) return true; // chưa định nghĩa → cho phép
  $allowed = array_map('intval', explode(',', $perms[$menu_id]));
  return in_array($perm_id, $allowed);
}

LOAD PERMISSION DATA (đầu control_menu.php / control_footer.php):
$user_perm_id = isset($_SESSION['user_permission_id']) ? (int)$_SESSION['user_permission_id'] : 0;
$access_json_raw = @file_get_contents(__DIR__ . '/../config/access.json');
$access_data = $access_json_raw ? json_decode($access_json_raw, true) : null;
$access_perms = $access_data['permissions'] ?? [];

// Kiểm tra submenu có con hiển thị không
$menu02_has_visible = canAccess('menu02_01', $user_perm_id, $access_perms) || canAccess('menu02_02', $user_perm_id, $access_perms);
$menu02_01_has_visible = canAccess('menu02_01_01', $user_perm_id, $access_perms) || canAccess('menu02_01_02', $user_perm_id, $access_perms);
$menu08_has_visible = canAccess('menu08_01', $user_perm_id, $access_perms) || canAccess('menu08_02', $user_perm_id, $access_perms);
$menu08_01_has_visible = canAccess('menu08_01', $user_perm_id, $access_perms);

PATTERN CHO SIDEBAR (control_menu.php):
// Menu L1 không có submenu
<?php if (canAccess('menu01', $user_perm_id, $access_perms)): ?>
<a href="index.php" id="menu01" class="nav-item active">...</a>
<?php endif; ?>

// Menu L1 có submenu — ẩn nếu không có quyền HOẶC tất cả con bị ẩn
<?php if (canAccess('menu02', $user_perm_id, $access_perms) && $menu02_has_visible): ?>
<a href="#" id="menu02" class="nav-item has-submenu" data-submenu="menu02-submenu">...</a>
<?php endif; ?>

// Submenu L2 có submenu L3 — ẩn nếu không có quyền HOẶC tất cả L3 bị ẩn
<?php if (canAccess('menu02_01', $user_perm_id, $access_perms) && $menu02_01_has_visible): ?>
<a href="#" id="menu02_01" class="submenu-item has-submenu" data-submenu="menu02_01-submenu">...</a>
<?php endif; ?>

// Submenu L3
<?php if (canAccess('menu02_01_01', $user_perm_id, $access_perms)): ?>
<a href="?pages=company" id="menu02_01_01" class="submenu-item">...</a>
<?php endif; ?>

// Toàn bộ submenu div cũng bọc trong điều kiện cha
<?php if (canAccess('menu02', $user_perm_id, $access_perms) && $menu02_has_visible): ?>
<div id="menu02-submenu" class="sidebar-submenu">...</div>
<div id="menu02_01-submenu" class="sidebar-submenu level3">...</div>
<?php endif; ?>

PATTERN CHO FOOTER (control_footer.php):
// Tương tự sidebar, dùng canAccess() cho từng menu item
// Footer dùng function_exists('canAccess') để tránh redefine (vì sidebar đã định nghĩa)
// Menu05 (More/Khác) luôn hiển thị (chứa language/theme)
// System submenu (menu05_05 + menu08_01) ẩn nếu không có quyền

if (!function_exists('canAccess')) {
  function canAccess($menu_id, $perm_id, $perms) {
    if (!isset($perms[$menu_id])) return true;
    $allowed = array_map('intval', explode(',', $perms[$menu_id]));
    return in_array($perm_id, $allowed);
  }
}

QUY TẮC:
1. Mọi menu item (L1, L2, L3) PHẢI bọc trong canAccess() check
2. Menu cha có submenu: ẩn thêm điều kiện _has_visible (ít nhất 1 con hiển thị)
3. Toàn bộ submenu div cũng bọc trong điều kiện của menu cha
4. Language/Theme luôn hiển thị (không kiểm tra quyền)
5. Footer menu05 (More) luôn hiển thị
6. canAccess() trả về true nếu menu_id không có trong access.json (chưa cấu hình → cho phép)
7. Khi thêm menu mới → thêm entry vào access.json

================================================================================
63. PERMISSION PAGE CSS (COMPONENTS.CSS)
================================================================================

CSS TẤT CẢ NẰM TRONG components.css (KHÔNG inline CSS trong permission.php):

CONTAINER:
- .permission-container: background var(--bg-secondary), border-radius 12px, border 1px solid var(--border), overflow hidden

HEADER:
- .permission-header: display flex, align-items center, padding 12px 15px, background var(--btn-view), border-bottom 1px solid var(--border), gap 2px
- .permission-menu-col: flex 1, font-size 11px, font-weight 600, uppercase, color var(--text-light), letter-spacing 0.5px
- .permission-toggles: display flex, gap 5px, justify-content flex-end
- .permission-toggle-wrapper: display flex, flex-direction column, align-items center, gap 2px, min-width 14px, margin 0px 5px
- .permission-level-badge: padding 2px 14px, border-radius 4px, font-size 10px, font-weight 600, color #fff
  + Mobile: padding 2px 8px

BADGE COLORS:
- .permission-level-badge.permission-0: background #666
- .permission-level-badge.permission-1: background #5a6d5a
- .permission-level-badge.permission-2: background #4a7c4f
- .permission-level-badge.permission-3: background #095fab
- .permission-level-badge.permission-4: background #57d75b
- .permission-level-badge.permission-5: background #8b3a3a

TREE:
- .permission-tree: max-height calc(100vh - 125px), overflow-y auto

ROW:
- .permission-row: display flex, align-items center, padding 8px 15px, border-bottom 1px solid var(--border), transition background 0.2s
- .permission-row:hover: background var(--hover)
- .permission-row.permission-level-1: padding-left 15px, font-weight 600
- .permission-row.permission-level-2: padding-left 35px, font-weight 500
- .permission-row.permission-level-3: padding-left 55px, font-weight 400
- .permission-menu-name: flex 1, font-size 12px, color var(--text-primary)
- .permission-row .permission-toggles: display flex, gap 5px

TOGGLE SWITCH:
- .permission-toggle: position relative, width 36px, height 18px, background var(--bg-primary), border-radius 18px, cursor pointer, border 1px solid var(--border), transition all 0.3s
- .permission-toggle.checked: background var(--btn-edit), border-color var(--btn-edit)
- .permission-toggle-slider: position absolute, top 1px, left 1px, width 14px, height 14px, background var(--text-secondary), border-radius 50%, transition all 0.3s
- .permission-toggle.checked .permission-toggle-slider: left 19px, background #fff

ACTIONS:
- .permission-actions: display flex, gap 10px, padding 16px, border-top 1px solid var(--border), background var(--hover), justify-content flex-end
- .permission-actions .btn: display flex, align-items center, gap 8px, padding 10px 20px, border-radius 8px, font-size 12px, font-weight 600, cursor pointer, transition all 0.3s, border none
- .permission-actions .btn-save: background var(--accent), color var(--bg-primary)
- .permission-actions .btn-save:hover: opacity 0.9, transform translateY(-1px)
- .permission-actions .btn-reset: background var(--border), color var(--text-primary)
- .permission-actions .btn-reset:hover: background var(--btn-delete), color #fff

RESPONSIVE (@media max-width: 768px):
- .permission-header .permission-toggles: gap 4px, justify-content flex-end
- .permission-header .permission-toggle-wrapper: min-width 12px
- .permission-row .permission-toggles: gap 4px
- .permission-toggle-wrapper: min-width 36px
- .permission-toggle: width 30px, height 15px
- .permission-toggle-slider::before: width 11px (trong slider)
- .permission-level-badge: padding 2px 8px

QUY TẮC:
1. TẤT CẢ permission CSS nằm trong components.css, KHÔNG inline
2. Dùng CSS variables cho màu (hỗ trợ theme switching)
3. Badge màu cố định (không dùng CSS variable) vì là màu định danh cấp quyền
4. Toggle dùng iOS-style pattern giống Type 11 nhưng nhỏ hơn
5. Responsive thu nhỏ toggle và badge trên mobile

================================================================================
64. PERMISSION LANGUAGE KEYS
================================================================================

KEYS THÊM VÀO CẢ message_vn.php VÀ message_eng.php:

PERMISSION PAGE:
- permission_main_title: 'Quyền truy cập' / 'Access Permission'
- permission_menu_label: 'MENU' / 'MENU'
- permission_btn_save: 'Lưu' / 'Save'
- permission_btn_reset: 'Đặt lại' / 'Reset'
- permission_save_success: 'Đã lưu quyền truy cập thành công!' / 'Access permissions saved successfully!'
- permission_save_error: 'Lỗi khi lưu quyền truy cập!' / 'Error saving access permissions!'
- permission_reset_confirm: 'Bạn có chắc muốn đặt lại tất cả quyền truy cập về mặc định?' / 'Are you sure you want to reset all permissions to default?'
- permission_reset_success: 'Đã đặt lại quyền truy cập về mặc định!' / 'Permissions reset to default!'
- permission_reset_error: 'Lỗi khi đặt lại quyền truy cập!' / 'Error resetting permissions!'
- permission_level_0: 'Cấp 0' / 'Level 0'

ERROR PAGE (403):
- error_forbidden_title: 'Không đủ quyền truy cập' / 'Access denied'
- error_forbidden_message: 'Bạn không có quyền truy cập trang này. Vui lòng liên hệ quản trị viên nếu cần hỗ trợ.' / 'You do not have permission to access this page. Please contact your administrator for assistance.'

FOOTER SYSTEM MENU:
- footer_menu05_05: 'Hệ thống' / 'System'

QUY TẮC:
1. Tất cả text permission page PHẢI dùng $lang key
2. permission_level_0 dùng 'Cấp 0' / 'Level 0' (KHÔNG dùng 'Chưa gán quyền' / 'Unassigned')
3. Tên quyền chi tiết lấy từ DB (Properti_Permisson), KHÔNG hardcode trong lang file
4. Khi thêm permission key mới → thêm vào CẢ HAI file vn và eng

================================================================================
65. DOCUMENTOUT FILE UPLOAD & PDF MERGE
================================================================================

QUY TẮC TỔNG QUAN:
- DocumentOut popup có trường "Hồ sơ scan" (TYPE 6: Text Input with Action Buttons) + toggle "Gộp file"
- Upload file PDF thực hiện theo luồng 2 bước: lưu text data trước → upload PDF riêng sau
- Merge file PDF dùng thư viện FPDI/FPDF (Composer: setasign/fpdi)

FRONTEND (pages/documentout.php):
- Trường scan file: input readonly (id="docout-scan-file") + file input ẩn (id="docout-file-input", accept="application/pdf")
- Toggle merge: checkbox id="docout-merge-files", label "Gộp file"
- Nút "Chọn file" trigger click file input ẩn, nút "Xem file" hiển thị khi file tồn tại
- Khi chọn file PDF mới: validate ngay (chỉ chấp nhận PDF), hiển thị success icon, lưu file vào biến tạm selectedDocOutFile
- Khi submit form: validate required fields → nếu scan file hiển thị tên file nhưng KHÔNG tồn tại trên server → showConfirmPopupJS hỏi "Hồ sơ scan không tồn tại. Bạn có muốn khởi tạo lại Hồ sơ scan rỗng không?"
  + Nếu đồng ý → submit với scan_file='' (reset FileUrl trong DB)
  + Nếu không → submit bình thường (KHÔNG gửi scan_file để giữ FileUrl cũ)

TWO-STEP SUBMIT:
1. Gửi FormData (KHÔNG bao gồm file input): formData.delete('docout-file-input'), xóa scan_file nếu có file mới hoặc reset
2. Nếu lưu thành công và có selectedDocOutFile → gọi uploadDocOutPDF(docId, file, successMsg)
3. uploadDocOutPDF: tạo FormData riêng với ajax_upload_pdf=1, doc_id, profile_file=file, merge_file='1'/'0'
4. Sau upload: nếu success → hiển thị message lưu công văn đi thành công (KHÔNG hiển thị message gộp file riêng)
   Nếu lỗi upload/gộp → hiển thị popup lỗi từ backend, vẫn reload trang

BACKEND (index.php - AJAX handler):
- Handler: pages=documentout + POST + ajax_upload_pdf
- Đọc doc_id, file từ $_FILES['profile_file'], merge_file ('1' hoặc 'true' = merge)
- Resolve existing file: SELECT FileUrl FROM dbo.DocumentOut WHERE Id = ?
  + FileUrl tương đối → resolve thành absolute path __DIR__ . '/' . FileUrl
  + Nếu file tồn tại và merge_file=true → gộp bằng FPDI
  + Nếu không có file cũ hoặc merge=false → ghi đè (move_uploaded_file)
- FPDI merge: new \setasign\Fpdi\Fpdi(), importPage từng trang của cả 2 file, Output('F', targetPath)
- Sau merge/upload: UPDATE dbo.DocumentOut SET FileUrl = ? WHERE Id = ?
- Response JSON: { success, message, file_url, mode ('merge'/'replace'/'error') }
- Message từ $lang key (KHÔNG hardcode tiếng Việt trong PHP handler)

FILE STORAGE:
- Thư mục: data/File/GoDocument/
- Tên file: {docId}.pdf
- FileUrl trong DB: 'data/File/GoDocument/{docId}.pdf' (path tương đối)

LANGUAGE KEYS (message_vn.php & message_eng.php):
// Popup labels
- docout_label_symbol: 'Số hiệu' / 'Document Number'
- docout_label_document_code: 'Mã số văn bản' / 'Document Code'
- docout_label_title: 'Tiêu đề' / 'Title'
- docout_label_doc_type: 'Thể loại' / 'Document Type'
- docout_label_summary: 'Tóm tắt nội dung' / 'Content Summary'
- docout_label_paper_type: 'Loại giấy tờ' / 'Paper Type'
- docout_label_effective_date: 'Ngày hiệu lực' / 'Effective Date'
- docout_label_from_company: 'Từ công ty' / 'From Company'
- docout_label_quantity: 'Số lượng' / 'Quantity'
- docout_label_receiving_agency: 'Cơ quan nhận' / 'Receiving Agency'
- docout_label_scan_file: 'Hồ sơ scan' / 'Scan File'
- docout_label_merge_files: 'Gộp file' / 'Merge Files'
- docout_label_storage_location: 'Lưu trữ tại' / 'Storage Location'
- docout_label_notes: 'Ghi chú' / 'Notes'
- docout_label_created_by: 'Nhập bởi' / 'Created By'
- docout_label_created_at: 'Nhập lúc' / 'Created At'

// Placeholders
- docout_placeholder_symbol, _document_code, _title, _doc_type, _summary, _paper_type
- docout_placeholder_from_company, _quantity, _receiving_agency, _scan_file, _storage_location, _notes

// Tooltips
- docout_title_view_file: 'Xem file' / 'View file'
- docout_title_select_file: 'Chọn file' / 'Select file'
- docout_title_increase: 'Tăng' / 'Increase'
- docout_title_decrease: 'Giảm' / 'Decrease'

// Date picker
- docout_date_prev_year: 'Năm trước' / 'Previous year'
- docout_date_next_year: 'Năm sau' / 'Next year'
- docout_date_prev_month: 'Tháng trước' / 'Previous month'
- docout_date_next_month: 'Tháng sau' / 'Next month'
- docout_date_today: 'Hôm nay' / 'Today'
- docout_date_clear: 'Xóa' / 'Clear'

// Errors
- docout_error_pdf_only: 'Chỉ chấp nhận file PDF' / 'Only PDF files are accepted'
- docout_error_upload_pdf: 'Lỗi upload file PDF' / 'Error uploading PDF file'
- error_default: 'Đã xảy ra lỗi' / 'An error occurred'

// Backend PDF upload messages
- docout_error_upload_invalid: 'Dữ liệu upload không hợp lệ' / 'Invalid upload data'
- docout_error_save_pdf: 'Không thể lưu file PDF' / 'Cannot save PDF file'
- docout_error_merge_pdf: 'Lỗi gộp PDF' / 'PDF merge error'
- docout_error_missing_fpdi: 'Thiếu thư viện FPDI để gộp file' / 'Missing FPDI library for merging'
- docout_error_db_update: 'Lỗi cập nhật database' / 'Database update error'
- docout_success_merge: 'Gộp file PDF thành công' / 'PDF files merged successfully'
- docout_success_upload: 'Upload PDF thành công' / 'PDF uploaded successfully'

QUY TẮC:
1. KHÔNG gửi file input trong form submit chính → dùng biến tạm selectedDocOutFile
2. KHÔNG dùng alert()/confirm() → dùng showPopupNoticeJS/showConfirmPopupJS
3. Backend merge_file chấp nhận cả '1' và 'true'
4. Existing file path resolve từ DB FileUrl (KHÔNG hardcode path)
5. FPDI/FPDF phải được cài qua Composer và autoload
6. Tất cả message trong handler trả về từ $lang key
7. Success message của upload vẫn hiển thị message lưu công văn đi gốc (KHÔNG hiển thị "Gộp file thành công")
8. FileUrl lưu path tương đối trong DB, resolve thành absolute khi check tồn tại
