# HQLAND - WORKFLOW TOÀN HỆ THỐNG

> Tài liệu này mô tả chi tiết luồng dữ liệu, nghiệp vụ và tương tác giữa các module trong HQLand.
> **Cập nhật:** 29/04/2026

---

## I. TỔNG QUAN KIẾN TRÚC

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                            FILAMENT ADMIN PANEL v5.5                         │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐ │
│  │  Warehouse   │  │     CRM      │  │  Contracts   │  │     Finance      │ │
│  │  (Kho hàng)  │  │  (Khách hàng)│  │  (Hợp đồng)  │  │  (Thu tiền/Báo   │ │
│  │              │  │              │  │              │  │      cáo)        │ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘  └────────┬─────────┘ │
│         │                 │                 │                   │           │
│  ┌──────┴───────┐  ┌──────┴───────┐  ┌──────┴───────┐  ┌───────┴─────────┐ │
│  │   Project    │  │   Customer   │  │   Contract   │  │     Payment     │ │
│  │   Product    │  │   (INDIVIDUAL│  │   Appendix   │  │   PaymentFine   │ │
│  │              │  │    /COMPANY) │  │   Settlement │  │   Settlement    │ │
│  └──────────────┘  └──────────────┘  └──────────────┘  └─────────────────┘ │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                    SUPPORT MODULES                                       ││
│  │  PaymentTemplate → PaymentSchedule → PaymentScheduleItem                ││
│  │  SalesPhase → SalesPhaseProduct (Pivot)                                 ││
│  │  FormTemplate → FormField → FormPrintLog                                ││
│  │  MailMergeService | DiscountEngine | PriceCalculationService            ││
│  └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           POSTGRESQL (UUID Primary Keys)                     │
│  120 Customers | 45 Products | 139 Contracts | Notifications Table          │
└─────────────────────────────────────────────────────────────────────────────┘
```

### Tech Stack
- **Framework:** Laravel 13.x + PHP 8.3
- **Admin Panel:** Filament v5.5 (Schemas Architecture)
- **Database:** PostgreSQL + UUID (100% tables)
- **Excel:** PhpSpreadsheet 5.7
- **Testing:** Pest PHP 4.6

---

## II. LUỒNG DỮ LIỆU TỔNG THỂ (END-TO-END)

### 1. Khởi tạo Dự án & Kho hàng
```
Project (Hà Quang 1)
    ├── PaymentTemplate (Mẫu lịch thanh toán mặc định)
    │       └── PaymentScheduleItem x N đợt (% + ngày đến hạn)
    └── Product x N (Lô đất/Căn hộ)
            ├── Giá gốc (qsdd_value, foundation_temp_value, total_price)
            ├── Hạ tầng (infrastructure_status - JSONB)
            └── Custom data (block, building_density, legal_status)
```

### 2. Khởi tạo Khách hàng
```
Customer (INDIVIDUAL hoặc COMPANY)
    ├── INDIVIDUAL: full_name, cmnd_cccd, phone, addresses...
    └── COMPANY: full_name (tên cty), tax_code, representative_id
            └── representative → Customer (INDIVIDUAL)
```

### 3. Luồng Bán hàng chuẩn
```
Bước 1: Tạo Đợt mở bán (SalesPhase)
    ├── Chọn Project
    ├── Chọn PaymentTemplate (mẫu lịch thanh toán)
    ├── Định chính sách chiết khấu mặc định (discount_policy JSONB)
    └── Thêm sản phẩm vào đợt (SalesPhaseProduct pivot)
            ├── Giá riêng cho đợt (sale_price/land_value/foundation_value)
            ├── Chiết khấu riêng (discount_details JSONB)
            └── Status: Còn hàng | Đã giữ | Đã bán | Khóa

Bước 2: Tạo Hợp đồng (Contract)
    ├── Chọn Product (Lô đất)
    ├── [Optional] Chọn SalesPhase → Auto populate giá từ SalesPhaseProduct
    ├── Nhập giá trị tài chính:
    │       land_value + foundation_value = total_value (auto)
    ├── Chiết khấu (discount_details JSONB)
    ├── Chọn Khách hàng (belongsToMany qua contract_customers)
    ├── Chọn PaymentTemplate (để tạo lịch thanh toán sau khi lưu)
    └── [Auto] Khi save:
            ├── Booted: total_value = land_value + foundation_value
            ├── Saved: PriceCalculationService tạo calculation_log
            └── AfterCreate: ContractScheduleService tạo PaymentSchedule

Bước 3: PaymentSchedule tự động tạo
    ├── Contract → PaymentSchedule (1-1)
    └── PaymentSchedule → PaymentScheduleItem x N
            ├── Mỗi item = 1 đợt thanh toán
            ├── amount = total_value × (percentage / 100)
            ├── due_date tính từ signing_date + days_after_signing
            └── Sắp xếp theo installment_no

Bước 4: Thu tiền (Payment)
    ├── Chọn Contract
    ├── [Optional] Chọn PaymentScheduleItem (đợt thanh toán)
    │       Nếu không chọn = Tạm ứng / không đối soát đợt
    ├── Nhập amount, paid_date, method, receipt_number
    ├── [Validation] amount không vượt quá công nợ đợt / công nợ HĐ
    └── [Auto - PaymentObserver] Khi Payment created/updated/deleted:
            ├── recalculateContract: SUM payments → paid_amount
            │       remaining_amount = total_value - paid_amount
            │       excess_amount = paid_amount - total_value (nếu > 0)
            └── applySurplusToNextInstallment:
                    Nếu excess_amount > 0 → Auto tạo Payment mới cho đợt tiếp theo
```

### 4. In ấn (Form Templates)
```
FormTemplate (Admin tạo mẫu)
    ├── HTML content với placeholder {{ma_truong}}
    └── FormField x N:
            ├── db_column: Lấy trực tiếp cột từ Model
            ├── db_relation: Lấy qua relation (customers.0.full_name)
            ├── formula: Tính toán (dùng Shunting Yard + bcmath)
            ├── input: Nhập tay khi in
            └── static: Giá trị cố định

Khi in:
    MailMergeService::render(template, record)
        ├── evaluateFields() → Tính giá trị tất cả fields
        ├── Thay placeholder → rendered HTML
        └── savePrintLog() → Lưu snapshot + rendered HTML + user + timestamp
```

---

## III. CHI TIẾT MODULE

### 3.1. WAREHOUSE (Kho hàng)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **Project** | code, name, type, address, payment_template_id | hasMany Products, hasMany SalesPhases, hasMany PaymentTemplates |
| **Product** | code, project_id, product_type (LAND/APARTMENT), area, price_per_unit, total_price, qsdd_value, foundation_temp_value, contract_temp_value, infrastructure_status (JSONB), custom_data (JSONB), status | belongsTo Project, hasMany Contracts, belongsToMany SalesPhases |

#### Nghiệp vụ
1. **Import từ Excel** (`import:products-excel`):
   - File: `sanpham.xlsx`
   - Parse hạ tầng từ chuỗi "Key: Value - Key2: Value2" → `infrastructure_status` JSONB
   - Parse custom_data: block, building_density, legal_status_raw
   - `updateOrCreate` theo `code + project_id`

2. **Tìm đợt mở bán đang active** (`Product::activeSalesPhase()`):
   - SalesPhase.status = "Đang mở bán"
   - SalesPhaseProduct.status = "Còn hàng"
   - Ngày hiện tại nằm trong [start_date, end_date]

---

### 3.2. CRM (Khách hàng)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **Customer** | type (INDIVIDUAL/COMPANY), full_name, cmnd_cccd, tax_code, title, phone, secondary_phones (JSONB), email, dob, permanent_address, contact_address, id_issue_date, id_issue_place, representative_id | belongsToMany Contracts (via contract_customers), representative → Customer, representedCompanies → Customer[] |

#### Nghiệp vụ
1. **Import từ Excel** (`import:customers-excel`):
   - File: `khachhang.xlsx`
   - Tách nhiều số điện thoại (dấu phẩy, gạch chéo, xuống dòng) → `phone` + `secondary_phones`
   - Parse ngày Excel serial number → Carbon
   - Tạo mẫu: Công ty TNHH BĐS Thịnh Vượng + Ngườ đại diện

2. **Form tạo/sửa** (CustomerForm):
   - Chuyển đổi động INDIVIDUAL/COMPANY (live)
   - INDIVIDUAL: Hiện dob, id_issue_date, id_issue_place
   - COMPANY: Hiện tax_code, representative_id (chọn từ danh sách cá nhân)
   - Copy địa chỉ thường trú → liên hệ (suffixAction)

---

### 3.3. CONTRACTS (Hợp đồng)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **Contract** | contract_number, contract_type (HĐMB/HĐGV/HĐDC), product_id, status, signing_date, sale_date, hql_confirmation_date, land_value, foundation_value, total_value, total_value_with_foundation, paid_amount, remaining_amount, excess_amount, discount_details (JSONB), calculation_log (JSONB), brokerage_name, stored_contract_count, filing_note, transfer_order, payment_template_id, sales_phase_id | belongsTo Product, belongsToMany Customers (via contract_customers + pivot role/transfer_order), belongsTo PaymentTemplate, belongsTo SalesPhase, hasOne PaymentSchedule, hasManyThrough PaymentScheduleItem, hasMany Payments, hasMany PaymentFines, hasMany Appendices |

#### Nghiệp vụ

**A. Tạo hợp đồng mới (CreateContract)**
```
1. Chọn Product → Auto điền:
   - total_value = product.total_price
   - land_value = product.qsdd_value
   - foundation_value = product.foundation_temp_value

2. [Optional] Chọn SalesPhase → Auto điền từ SalesPhaseProduct:
   - land_value, foundation_value, sale_price
   - Nếu có sale_price → total_value = sale_price
   - discount_details từ pivot

3. Nhập tay land_value, foundation_value → total_value auto cập nhật

4. Nhập discount_details (Key-Value) + xem tổng quan chiết khấu

5. Chọn Customers (multiple), nhập các thông tin quản lý

6. Chọn payment_template_id → [Auto] Sau khi lưu tạo PaymentSchedule
```

**B. Booted Logic (Model Contract)**
```
static::saving:
    land_value + foundation_value → total_value
    Nếu tạo mới và chưa có giá → fallback lấy product.total_price
    remaining_amount = total_value - paid_amount

static::saved:
    Nếu có land_value/foundation_value:
        PriceCalculationService::calculateForContract() → calculation_log
        Cập nhật calculation_log (steps, final_values, price_sheet)
```

**C. Calculation Pipeline**
```
Input: land_value, foundation_value, discount_details, vat_rate
Step 1: Giá trị QSDĐ → land_value
Step 2: Giá trị Móng → foundation_value
Step 3: Subtotal = land_value + foundation_value
Step 4: Chiết khấu = total_amount HOẶC total_percentage × subtotal
Step 5: Net value = subtotal - discount
Step 6: VAT = net_value × vat_rate%
Step 7: Total payment = net_value + VAT
Output: calculation_log lưu snapshot tất cả bước
```

**D. Lịch thanh toán (ContractScheduleService)**
```
generateFromTemplate(contract, template):
    DB::transaction:
        Xóa lịch cũ (nếu có)
        Tạo PaymentSchedule
        Duyệt template.items theo installment_no:
            due_date = signing_date + days_after_signing
            amount = contract.total_value × (percentage / 100)
            Tạo PaymentScheduleItem
```

**E. Các module phụ**
- **Appendix (Phụ lục):** belongsTo Contract + Product, custom_data JSONB
- **Settlement (Quyết toán):** belongsTo Product, temp_value, final_value, difference
- **PaymentFine (Tiền phạt):** belongsTo Contract, amount, due_date, paid_date

---

### 3.4. FINANCE (Tài chính & Thu tiền)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **Payment** | contract_id, schedule_item_id, amount, paid_date, method, receipt_number, metadata (JSONB) | belongsTo Contract, belongsTo PaymentScheduleItem |
| **PaymentSchedule** | contract_id, template_id | belongsTo Contract, belongsTo PaymentTemplate, hasMany Items |
| **PaymentScheduleItem** | schedule_id, installment_no, type (PaymentType enum), percentage, amount, due_date, days_after_signing, days_after_previous | belongsTo PaymentSchedule, hasMany Payments |

#### PaymentType Enum
| Value | Label |
|-------|-------|
| QSDD | Tiền QSDĐ |
| MONG | Tiền Móng |
| THAN | Tiền Thân |
| CHI_PHI_TC | Chi phí thi công |
| CK | Chiết khấu |
| PHAT | Tiền phạt |
| OTHER | Khác |

#### Nghiệp vụ

**A. Thu tiền (PaymentForm)**
```
1. Chọn Contract → Cascade: Đợt thanh toán lọc theo contract
2. Chọn Đợt TT (optional)
   - Để trống = Tạm ứng / Không đối soát đợt
3. Nhập amount
   - Helper text hiển thị công nợ đợt / công nợ HĐ
   - Validation: Không cho phép thu quá công nợ
4. Nhập paid_date, method, receipt_number
5. Lưu
```

**B. PaymentObserver (Tự động)**
```
created/updated/deleted:
    ├── recalculateContract(contract):
    │       totalPaid = SUM(payments.amount)
    │       paid_amount = totalPaid
    │       Nếu totalPaid > total_value:
    │           remaining_amount = 0
    │           excess_amount = totalPaid - total_value
    │       Ngược lại:
    │           remaining_amount = total_value - totalPaid
    │           excess_amount = 0
    │       saveQuietly()
    └── applySurplusToNextInstallment(contract):
            Nếu excess_amount > 0:
                Tìm đợt tiếp theo chưa thanh toán đủ (theo installment_no)
                applyAmount = min(excess, remaining_for_item)
                Tạo Payment auto:
                    method = "Tự động khấu trừ"
                    receipt_number = "AUTO-SURPLUS-..."
                    metadata = {auto_surplus: true}
```

**C. Bảng thu tiền (PaymentsTable)**
- Cột: Hợp đồng, Số tiền, Ngày thu, Phương thức, Số phiếu thu
- Cột mở rộng: Loại đợt, Đợt TT, Trạng thái đối soát (Đủ/Thiếu/Thừa/Tạm ứng), Còn thiếu
- Filter: Phương thức, Ngày thu (range)
- Eager load: `scheduleItem.payments`

---

### 3.5. SALES PHASES (Đợt mở bán)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **SalesPhase** | project_id, name, code, status (Chuẩn bị/Đang mở bán/Tạm dừng/Đã đóng), start_date, end_date, description, payment_template_id, discount_policy (JSONB) | belongsTo Project, belongsTo PaymentTemplate, hasMany phaseProducts, belongsToMany Products |
| **SalesPhaseProduct** (Pivot) | sales_phase_id, product_id, sale_price, land_value, foundation_value, discount_details (JSONB), status (Còn hàng/Đã giữ/Đã bán/Khóa) | belongsTo SalesPhase, belongsTo Product |

#### Nghiệp vụ
1. **Tạo đợt mở bán:**
   - Thông tin cơ bản (name, code, status, dates)
   - Chính sách & Mẫu thanh toán
   - Danh sách sản phẩm (Repeater) với giá riêng và chiết khấu riêng

2. **Tích hợp vào ContractForm:**
   - Khi chọn `sales_phase_id` + `product_id` → Auto populate giá từ pivot
   - CreateContract: Nếu không chọn payment_template_id trực tiếp → fallback lấy từ `salesPhase->paymentTemplate`

---

### 3.6. FORM TEMPLATES (Biểu mẫu in ấn)

#### Models
| Model | Key Fields | Quan hệ |
|-------|-----------|---------|
| **FormTemplate** | name, code, target_model (Contract/Product/Customer), html_template, paper_size (A4/A5/Letter), is_active | hasMany fields, hasMany printLogs |
| **FormField** | template_id, code, label, source_type (db_column/db_relation/formula/input/static), source_config (JSONB), format (text/number/currency/date/percent), decimal_places, display_order | belongsTo template |
| **FormPrintLog** | template_id, target_model, target_id, target_number, snapshot_data (JSONB), rendered_html, printed_by, printed_at | belongsTo template, belongsTo printedBy (User) |

#### MailMergeService Workflow
```
evaluateFields(template, record):
    foreach field:
        db_column → record->{column}
        db_relation → record->{relation}[index]->{column}
        formula → Shunting Yard evaluate (bcmath)
        input → default value
        static → fixed value

render(template, record):
    evaluateFields() → raw_values
    formatValue() theo format/currency/date/percent
    str_replace placeholder {{code}} → value trong html_template

savePrintLog():
    Lưu snapshot_data + rendered_html + user + timestamp
```

---

## IV. DASHBOARD & BÁO CÁO

### Widgets
| Widget | Dữ liệu |
|--------|---------|
| **RecentNotifications** | Thông báo chưa đọc của user hiện tại (database notifications) |
| **ContractStatsOverview** | Tổng doanh thu, Đã thu, Công nợ phải thu, HĐ hiệu lực, Đợt TT sắp đến hạn |
| **UpcomingPaymentsTable** | PaymentScheduleItem trong 30 ngày tới (có đủ công nợ) |

### Pages
| Page | Chức năng |
|------|-----------|
| **ProjectReport** | Bảng thống kê theo dự án: Tổng SP, Đã bán, Số HĐ, Tổng giá trị, Đã thu, Công nợ |

### Commands
| Command | Chức năng |
|---------|-----------|
| `export:debt-report` | Xuất Excel 2 sheet: Tổng hợp công nợ + Chi tiết đợt TT |
| `notifications:send-due-payments` | Gửi cảnh báo đợt TT sắp đến hạn cho tất cả users (database notification) |
| `contracts:generate-schedules` | Tạo lịch thanh toán hàng loạt cho HĐ chưa có lịch |

---

## V. CÁC COMMAND IMPORT DỮ LIỆU

### `import:products-excel {file=sanpham.xlsx}`
- Input: File Excel sản phẩm
- Output: Product records trong Project "Hà Quang 1"
- Logic: `updateOrCreate` theo code, parse infrastructure_status, parse custom_data

### `import:customers-excel {file=khachhang.xlsx}`
- Input: File Excel khách hàng
- Output: Customer records (INDIVIDUAL) + Mẫu Công ty
- Logic: Tách nhiều phone, parse Excel date serial

### `import:contracts-complex {hopdong=hopdong.xlsx} {hdkh=Hd_kh.xlsx}`
- Input: 2 file (tài chính + liên kết KH)
- Logic "Bắc cầu":
  1. Đọc hopdong.xlsx → Map `contract_number → finance_data`
  2. Đọc Hd_kh.xlsx → Tìm product theo plotCode, customer theo cmnd_cccd
  3. Tìm mapping: `str_contains(contract_number, plotCode)`
  4. `updateOrCreate` Contract + `syncWithoutDetaching` customers qua pivot
- Transaction bọc toàn bộ

### `contracts:generate-schedules {--force}`
- Input: HĐ chưa có PaymentSchedule (hoặc --force để tạo lại)
- Logic: Ưu tiên `contract.paymentTemplate`, fallback `product.project.paymentTemplate`

---

## VI. MÔ HÌNH QUAN HỆ DATABASE (ER Tóm tắt)

```
Project 1───* Product 1───* Contract *───* Customer
    │               │            │
    │               │            1───1 PaymentSchedule 1───* PaymentScheduleItem
    │               │            │                        │
    │               │            *───* Payment *───────────┘
    │               │            │
    │               │            *───* Appendix
    │               │            *───* PaymentFine
    │               │            *───* Settlement (qua Product)
    │               │
    │               *───* SalesPhase *───* SalesPhaseProduct *───1 Product
    │                        │
    │                        1───1 PaymentTemplate 1───* PaymentScheduleItem (template)
    │
    *───* PaymentTemplate

FormTemplate 1───* FormField
            1───* FormPrintLog

User 1───* Notification (MorphMany)
```

---

## VII. ĐIỂM CẦN XEM XÉT (TỪ ASSESSMENT)

| # | Vấn đề | Mức độ | Ghi chú |
|---|--------|--------|---------|
| 1 | **~~Soft Delete~~ ✅** | 🟢 Đã xong | Contract, Payment, Customer có SoftDeletes + Restore/ForceDelete UI |
| 2 | **Phân quyền** | 🟡 Đang thiết kế | Kiến trúc Hướng B (Tự viết, không Spatie). Xem chi tiết Phần VIII |
| 3 | **~~Payment.collected_by~~ ✅** | 🟢 Đã xong | Đã thêm collected_by (FK → users) + hiển thị Form/Table |
| 4 | **Sổ quỹ** | 🟡 Trung bình | Thu tiền nhưng không ghi vào quỹ TM/NH. Không đối soát được |
| 5 | **CRM Pipeline** | 🟢 Thấp | Chưa quản lý Lead/Khách hàng tiềm năng |
| 6 | **Báo cáo BCTC** | 🟢 Thấp | Chưa có báo cáo theo chuẩn kế toán VN |

---

---

## VIII. PERMISSION SYSTEM DESIGN (Hướng B - Tự viết, không Spatie)

> **Quyết định kiến trúc:** Không dùng Spatie Permission. Tự viết module phân quyền đơn giản, đủ dùng, kiểm soát 100%.
> **Lý do:** Tránh config phức tạp, tránh xung đột UUID/BIGINT, phù hợp 5-10 role, không cần advanced features.

### Kiến trúc 3 lớp

```
LAYER 1: ROLE TEMPLATE (Mẫu nhóm)
  role_templates.id(UUID) | name | description | permissions(JSONB) | is_active
  
  permissions lưu dạng:
  {
    "contracts": ["view","create","update","delete","restore","forceDelete","export"],
    "payments": ["view","create","update","delete"],
    "customers": ["view","create","update","delete"]
  }

LAYER 2: USER (Kế thừa + Override)
  users.role_template_id(UUID) → nullable
  users.extra_permissions(JSONB) → thêm quyền vượt cấp
  users.excluded_permissions(JSONB) → bớt quyền so với mẫu

LAYER 3: EFFECTIVE PERMISSIONS (Tính toán động, cache trong Session)
  effective = template_permissions + extra_permissions - excluded_permissions
  
  Tính 1 lần khi user login → lưu vào session()->get('user.{id}.permissions')
  Tự động xóa khi logout
  Có thể xóa thủ công: auth()->user()->clearPermissionCache()
```

### Quy tắc khai báo Permission trong Resource

Mỗi Resource khai báo:
```php
class ContractResource extends Resource
{
    protected static array $permissionActions = [
        'view', 'create', 'update', 'delete', 
        'restore', 'forceDelete', 'export'
    ];
    protected static string $permissionLabel = 'Hợp đồng';
}
```

Naming: `{snake_case_resource_name}.{action}` (ví dụ: `contracts.create`, `payments.delete`)

### Command đồng bộ (THỦ CÔNG - KHÔNG TỰ ĐỘNG)

```bash
php artisan permissions:sync
```

**Khi nào chạy:**
- Khi thêm Resource mới
- Khi thêm/bớt `$permissionActions` trong Resource
- KHÔNG chạy tự động trong composer post-autoload-dump

**Command làm gì:**
1. Quét tất cả Filament Resources, lấy `$permissionActions` + `$permissionLabel`
2. Lưu vào bảng `permission_modules` (module, label, actions)
3. Action mới mặc định **TẮT** cho tất cả role hiện tại (an toàn)
4. Admin phải vào bật thủ công sau

### Luồng thêm module/action mới

**Step 1: Dev code**
- Tạo Resource mới / thêm action vào Resource
- Thêm `$permissionActions` vào Resource class

**Step 2: Dev chạy command**
- `php artisan permissions:sync`

**Step 3: Admin cấu hình**
- Vào Role Template UI → thấy module/action mới (badge "Mới")
- Tick bật quyền cho các nhóm cần dùng

### Các hàm kiểm tra quyền

```php
// User Model
public function getEffectivePermissions(): array;
public function hasEffectivePermission(string $permission): bool;
public function clearPermissionCache(): void;

// Trong Resource
public static function canCreate(): bool 
{
    return auth()->user()->can('contracts.create');
}
```

### UI quản lý (Filament Resources)

| Resource | Chức năng |
|----------|-----------|
| **RoleTemplateResource** | CRUD mẫu nhóm. Form chọn module → tick actions. CheckboxList per module |
| **UserPermissionPage** | Chọn user → hiện role template đang dùng → thêm/bớt quyền chi tiết → preview effective permissions |

---

*File này cần được cập nhật mỗi khi có thay đổi lớn trong kiến trúc hoặc nghiệp vụ.*
