WIP: SoftDelete Contract/Payment/Customer, collected_by, Notifications, ProjectReport, ExportDebtReport

This commit is contained in:
2026-04-29 04:46:58 +00:00
parent 0712046f4b
commit 78c22690eb
18 changed files with 1015 additions and 12 deletions

View File

@@ -1,7 +1,7 @@
# HQLAND - TRẠNG THÁI CODEBASE & LỘ TRÌNH PHÁT TRIỂN # HQLAND - TRẠNG THÁI CODEBASE & LỘ TRÌNH PHÁT TRIỂN
> File này được tạo để lưu trữ ngữ cảnh dự án cho các phiên làm việc sau. > File này được tạo để lưu trữ ngữ cảnh dự án cho các phiên làm việc sau.
> **Cập nhật:** 28/04/2026 > **Cập nhật:** 29/04/2026
> **Dự án:** HQLand - Hệ thống quản lý Bất động sản > **Dự án:** HQLand - Hệ thống quản lý Bất động sản
> **Stack:** Laravel 13 + Filament v5.5 (Schemas Architecture) + PostgreSQL + UUID > **Stack:** Laravel 13 + Filament v5.5 (Schemas Architecture) + PostgreSQL + UUID
@@ -303,10 +303,13 @@
- [x] **Calculation Pipeline:** Kiến trúc tính toán tường minh (Step-by-step) với làm tròn tại mỗi bước - [x] **Calculation Pipeline:** Kiến trúc tính toán tường minh (Step-by-step) với làm tròn tại mỗi bước
- [x] **Form Templates:** Mail Merge Engine cho phiếu tính giá, HĐ, phụ lục - Admin tự tạo template - [x] **Form Templates:** Mail Merge Engine cho phiếu tính giá, HĐ, phụ lục - Admin tự tạo template
- [x] **Sales Phase Module:** Quản lý đợt mở bán, giá bán riêng theo đợt, tích hợp vào ContractForm - [x] **Sales Phase Module:** Quản lý đợt mở bán, giá bán riêng theo đợt, tích hợp vào ContractForm
- [x] **ProjectReport Page:** Báo cáo thống kê bán hàng, thanh toán theo từng dự án
- [x] **ExportDebtReport Command:** Xuất Excel báo cáo công nợ khách hàng (2 sheet: Tổng hợp + Chi tiết đợt TT)
- [x] **Notification System:** `PaymentDueNotification` + `SendPaymentDueNotifications` command + `RecentNotifications` widget
### 5.2. Đang dở / Cần tiếp tục ### 5.2. Đang dở / Cần tiếp tục
- [x] **Dashboard thống kê:** Đã tạo `ContractStatsOverview` + `UpcomingPaymentsTable` - [x] **Dashboard thống kê:** Đã tạo `ContractStatsOverview` + `UpcomingPaymentsTable`
- [ ] **Notification:** Cảnh báo đợt thanh toán sắp đến hạn (chưa có hệ thống notification) - [x] **Notification:** Đã có hệ thống cảnh báo đợt thanh toán sắp đến hạn (database notifications)
### 5.3. Vấn đề kỹ thuật ĐÃ XỬ LÝ ### 5.3. Vấn đề kỹ thuật ĐÃ XỬ LÝ
- [x] ContractTable đã thêm cột `paid_amount` / `remaining_amount`, chuyển sang dùng `ContractsTable` Schemas - [x] ContractTable đã thêm cột `paid_amount` / `remaining_amount`, chuyển sang dùng `ContractsTable` Schemas
@@ -332,10 +335,10 @@
8. **Discount Engine:** Tính toán tự động chiết khấu từ `discount_details` vào giá trị hợp đồng 8. **Discount Engine:** Tính toán tự động chiết khấu từ `discount_details` vào giá trị hợp đồng
### Giai đoạn 3: Báo cáo & Tối ưu (Ưu tiên THẤP) ### Giai đoạn 3: Báo cáo & Tối ưu (Ưu tiên THẤP)
9. **Dashboard Tài chính:** Tổng doanh thu, dòng tiền dự kiến, công nợ phải thu 9. **Dashboard Tài chính:** Tổng doanh thu, dòng tiền dự kiến, công nợ phải thu
10. **Báo cáo theo Dự án:** Thống kê bán hàng, thanh toán theo từng dự án 10. **Báo cáo theo Dự án:** Thống kê bán hàng, thanh toán theo từng dự án
11. **Export Excel:** Xuất báo cáo công nợ khách hàng 11. **Export Excel:** Xuất báo cáo công nợ khách hàng
12. **Notification:** Cảnh báo đợt thanh toán sắp đến hạn 12. **Notification:** Cảnh báo đợt thanh toán sắp đến hạn
--- ---
@@ -370,12 +373,16 @@ DB_HOST=127.0.0.1 php artisan migrate
- `database/migrations/2026_04_28_020000_create_form_templates_tables.php` - `database/migrations/2026_04_28_020000_create_form_templates_tables.php`
- `database/migrations/2026_04_28_030000_create_sales_phases_tables.php` - `database/migrations/2026_04_28_030000_create_sales_phases_tables.php`
- `database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php` - `database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php`
- `database/migrations/2026_04_29_000000_create_notifications_table.php`
### Services mới ### Services mới
- `app/Services/DiscountEngine.php` - Tính toán chiết khấu - `app/Services/DiscountEngine.php` - Tính toán chiết khấu
- `app/Services/Calculation/` - Calculation Pipeline (RoundingRule, CalculationStep, CalculationResult, CalculationPipeline, PriceCalculationService) - `app/Services/Calculation/` - Calculation Pipeline (RoundingRule, CalculationStep, CalculationResult, CalculationPipeline, PriceCalculationService)
- `app/Services/Forms/MailMergeService.php` - Engine xử lý biểu mẫu in ấn - `app/Services/Forms/MailMergeService.php` - Engine xử lý biểu mẫu in ấn
- `app/Console/Commands/GenerateContractSchedules.php` - Command tạo lịch hàng loạt - `app/Console/Commands/GenerateContractSchedules.php` - Command tạo lịch hàng loạt
- `app/Console/Commands/ExportDebtReport.php` - Command xuất Excel báo cáo công nợ
- `app/Console/Commands/SendPaymentDueNotifications.php` - Command gửi cảnh báo đợt TT sắp đến hạn
- `app/Notifications/PaymentDueNotification.php` - Database notification cho đợt thanh toán
### Filament Resources mới ### Filament Resources mới
- `app/Filament/Resources/PaymentFines/` (Resource + Form + Table + Pages) - `app/Filament/Resources/PaymentFines/` (Resource + Form + Table + Pages)
@@ -387,6 +394,10 @@ DB_HOST=127.0.0.1 php artisan migrate
### Widgets mới ### Widgets mới
- `app/Filament/Widgets/ContractStatsOverview.php` - Dashboard tổng quan tài chính - `app/Filament/Widgets/ContractStatsOverview.php` - Dashboard tổng quan tài chính
- `app/Filament/Widgets/UpcomingPaymentsTable.php` - Danh sách đợt TT sắp đến hạn - `app/Filament/Widgets/UpcomingPaymentsTable.php` - Danh sách đợt TT sắp đến hạn
- `app/Filament/Widgets/RecentNotifications.php` - Danh sách thông báo chưa đọc
### Filament Pages mới
- `app/Filament/Pages/ProjectReport.php` - Báo cáo thống kê theo dự án
### Models mới ### Models mới
- `app/Models/SalesPhase.php` - Đợt mở bán - `app/Models/SalesPhase.php` - Đợt mở bán
@@ -408,11 +419,14 @@ DB_HOST=127.0.0.1 php artisan migrate
- `app/Filament/Resources/Payments/PaymentResource.php` - Thêm eager load `scheduleItem.payments` - `app/Filament/Resources/Payments/PaymentResource.php` - Thêm eager load `scheduleItem.payments`
### Config/Provider sửa đổi ### Config/Provider sửa đổi
- `app/Providers/Filament/AdminPanelProvider.php` - Đăng ký widgets mới - `app/Providers/Filament/AdminPanelProvider.php` - Đăng ký widgets mới (ContractStatsOverview, UpcomingPaymentsTable, RecentNotifications)
- `phpunit.xml` - Cấu hình PostgreSQL testing (DB_HOST, DB_DATABASE, etc.) - `phpunit.xml` - Cấu hình PostgreSQL testing (DB_HOST, DB_DATABASE, etc.)
- `config/database.php` - Default `pgsql` - `config/database.php` - Default `pgsql`
- `composer.json` - Xóa script tạo SQLite - `composer.json` - Xóa script tạo SQLite
### Views mới
- `resources/views/filament/pages/project-report.blade.php` - View cho ProjectReport Page
--- ---
## 9. FILE HỖ TRỢ CHUYỂN MÁY ## 9. FILE HỖ TRỢ CHUYỂN MÁY

463
WORKFLOW.md Normal file
View File

@@ -0,0 +1,463 @@
# 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** | 🔴 Nghiêm trọng | Tất cả model dùng delete cứng. Xóa nhầm → mất vĩnh viễn |
| 2 | **Phân quyền** | 🟡 Trung bình | Chỉ 1 loại user. Ai cũng xóa/sửa được mọi thứ |
| 3 | **Payment.collected_by** | 🟡 Trung bình | Không ghi nhận ai thu tiền. Khó truy trách nhiệm |
| 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 |
---
*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ụ.*

View File

@@ -0,0 +1,151 @@
<?php
namespace App\Console\Commands;
use App\Models\Contract;
use App\Models\PaymentScheduleItem;
use Illuminate\Console\Command;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
use PhpOffice\PhpSpreadsheet\Style\Fill;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
class ExportDebtReport extends Command
{
protected $signature = 'export:debt-report
{--output=storage/app/reports/bao-cao-cong-no.xlsx : Đường dẫn file xuất}
{--project= : Lọc theo UUID dự án}';
protected $description = 'Xuất báo cáo công nợ khách hàng ra file Excel';
public function handle(): int
{
$outputPath = $this->option('output');
$projectId = $this->option('project');
// Đảm bảo thư mục tồn tại
$dir = dirname($outputPath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
$this->info('Đang tải dữ liệu...');
// ===== Sheet 1: Tổng hợp công nợ =====
$contractsQuery = Contract::query()
->with(['customers', 'product.project', 'paymentSchedule.items'])
->when($projectId, fn ($q) => $q->whereHas('product', fn ($q2) => $q2->where('project_id', $projectId)))
->orderBy('contract_number');
$contracts = $contractsQuery->get();
$spreadsheet = new Spreadsheet();
$sheet1 = $spreadsheet->getActiveSheet();
$sheet1->setTitle('Tổng hợp công nợ');
// Header
$headers1 = ['STT', 'Số HĐMB', 'Khách hàng', 'Dự án', 'Lô đất', 'Giá trị HĐ (VNĐ)', 'Đã thu (VNĐ)', 'Còn lại (VNĐ)', 'Trạng thái', 'Ngày ký'];
$this->writeHeader($sheet1, $headers1);
$row = 2;
foreach ($contracts as $index => $contract) {
$sheet1->setCellValue('A' . $row, $index + 1);
$sheet1->setCellValue('B' . $row, $contract->contract_number);
$sheet1->setCellValue('C' . $row, $contract->customers->pluck('full_name')->implode(', '));
$sheet1->setCellValue('D' . $row, $contract->product->project->name ?? '');
$sheet1->setCellValue('E' . $row, $contract->product->code ?? '');
$sheet1->setCellValue('F' . $row, (float) $contract->total_value);
$sheet1->setCellValue('G' . $row, (float) $contract->paid_amount);
$sheet1->setCellValue('H' . $row, (float) $contract->remaining_amount);
$sheet1->setCellValue('I' . $row, $contract->status);
$sheet1->setCellValue('J' . $row, $contract->signing_date ? $contract->signing_date->format('d/m/Y') : '');
$row++;
}
$this->formatNumberColumns($sheet1, ['F', 'G', 'H'], $row - 1);
$this->autoSizeColumns($sheet1, $headers1);
// ===== Sheet 2: Chi tiết đợt thanh toán chưa đủ =====
$sheet2 = $spreadsheet->createSheet();
$sheet2->setTitle('Chi tiết đợt TT');
$headers2 = ['STT', 'Số HĐMB', 'Khách hàng', 'Đợt', 'Loại', 'Ngày đến hạn', 'Số tiền đợt (VNĐ)', 'Đã thu (VNĐ)', 'Còn thiếu (VNĐ)'];
$this->writeHeader($sheet2, $headers2);
$itemsQuery = PaymentScheduleItem::query()
->with(['schedule.contract.customers', 'payments'])
->whereHas('schedule.contract')
->when($projectId, fn ($q) => $q->whereHas('schedule.contract.product', fn ($q2) => $q2->where('project_id', $projectId)))
->orderBy('due_date');
$items = $itemsQuery->get();
$row = 2;
$stt = 1;
foreach ($items as $item) {
$contract = $item->schedule?->contract;
if (! $contract) continue;
$paid = (float) $item->paid_amount;
$remaining = (float) $item->remaining_amount;
$sheet2->setCellValue('A' . $row, $stt);
$sheet2->setCellValue('B' . $row, $contract->contract_number);
$sheet2->setCellValue('C' . $row, $contract->customers->pluck('full_name')->implode(', '));
$sheet2->setCellValue('D' . $row, $item->installment_no);
$sheet2->setCellValue('E' . $row, $item->type?->getLabel() ?? (string) $item->type);
$sheet2->setCellValue('F' . $row, $item->due_date ? $item->due_date->format('d/m/Y') : '');
$sheet2->setCellValue('G' . $row, (float) $item->amount);
$sheet2->setCellValue('H' . $row, $paid);
$sheet2->setCellValue('I' . $row, $remaining);
$row++;
$stt++;
}
$this->formatNumberColumns($sheet2, ['G', 'H', 'I'], $row - 1);
$this->autoSizeColumns($sheet2, $headers2);
// Lưu file
$writer = new Xlsx($spreadsheet);
$writer->save($outputPath);
$this->info("Xuất báo cáo thành công: {$outputPath}");
$this->info("- Tổng hợp: {$contracts->count()} hợp đồng");
$this->info("- Chi tiết đợt: {$items->count()} dòng");
return self::SUCCESS;
}
private function writeHeader($sheet, array $headers): void
{
foreach ($headers as $colIndex => $header) {
$coord = [$colIndex + 1, 1];
$sheet->setCellValue($coord, $header);
$cell = $sheet->getCell($coord);
$cell->getStyle()->getFont()->setBold(true);
$cell->getStyle()->getFill()->setFillType(Fill::FILL_SOLID)->getStartColor()->setRGB('E5E7EB');
$cell->getStyle()->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
$cell->getStyle()->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
}
}
private function formatNumberColumns($sheet, array $columns, int $lastRow): void
{
foreach ($columns as $col) {
$sheet->getStyle("{$col}2:{$col}{$lastRow}")
->getNumberFormat()
->setFormatCode(NumberFormat::FORMAT_NUMBER_COMMA_SEPARATED1);
}
}
private function autoSizeColumns($sheet, array $headers): void
{
foreach (range(1, count($headers)) as $colIndex) {
$colLetter = Coordinate::stringFromColumnIndex($colIndex);
$sheet->getColumnDimension($colLetter)->setAutoSize(true);
}
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Console\Commands;
use App\Models\PaymentScheduleItem;
use App\Models\User;
use App\Notifications\PaymentDueNotification;
use Illuminate\Console\Command;
class SendPaymentDueNotifications extends Command
{
protected $signature = 'notifications:send-due-payments
{--days=7 : Số ngày trước hạn để cảnh báo}
{--dry-run : Chỉ liệt , không gửi}';
protected $description = 'Gửi cảnh báo đợt thanh toán sắp đến hạn cho tất cả users';
public function handle(): int
{
$days = (int) $this->option('days');
$dryRun = $this->option('dry-run');
$from = now();
$to = now()->addDays($days);
$items = PaymentScheduleItem::query()
->with(['schedule.contract', 'payments'])
->whereHas('schedule.contract')
->whereDate('due_date', '>=', $from)
->whereDate('due_date', '<=', $to)
->whereRaw('amount > (SELECT COALESCE(SUM(amount), 0) FROM payments WHERE payments.schedule_item_id = payment_schedule_items.id)')
->orderBy('due_date')
->get();
if ($items->isEmpty()) {
$this->warn('Không có đợt thanh toán nào sắp đến hạn trong ' . $days . ' ngày tới.');
return self::SUCCESS;
}
$this->info("Tìm thấy {$items->count()} đợt thanh toán sắp đến hạn.");
$users = User::all();
if ($users->isEmpty()) {
$this->warn('Không có user nào trong hệ thống để nhận thông báo.');
return self::FAILURE;
}
foreach ($items as $item) {
$contract = $item->schedule?->contract;
$remaining = (float) $item->remaining_amount;
$this->line(sprintf(
'- HĐ %s | Đợt %d | Ngày %s | Còn thiếu: %s VNĐ',
$contract?->contract_number ?? 'N/A',
$item->installment_no,
$item->due_date?->format('d/m/Y'),
number_format($remaining)
));
if (! $dryRun) {
foreach ($users as $user) {
$user->notify(new PaymentDueNotification($item));
}
}
}
if ($dryRun) {
$this->info('Chế độ dry-run: Không có thông báo nào được gửi.');
} else {
$this->info("Đã gửi thông báo cho {$users->count()} user(s).");
}
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Filament\Pages;
use App\Models\Project;
use Filament\Pages\Page;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
class ProjectReport extends Page implements HasTable
{
use InteractsWithTable;
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-chart-bar';
protected static ?string $navigationLabel = 'Báo cáo theo Dự án';
protected static ?string $title = 'Báo cáo Thống kê theo Dự án';
protected static string | \UnitEnum | null $navigationGroup = 'Quản lý Dòng tiền';
protected static ?int $navigationSort = 50;
protected string $view = 'filament.pages.project-report';
public function table(Table $table): Table
{
return $table
->query(
Project::query()
->select('projects.id', 'projects.name', 'projects.code')
->selectRaw('COUNT(DISTINCT products.id) as product_count')
->selectRaw('COUNT(DISTINCT CASE WHEN contracts.id IS NOT NULL THEN products.id END) as sold_product_count')
->selectRaw('COUNT(DISTINCT contracts.id) as contract_count')
->selectRaw('COALESCE(SUM(contracts.total_value), 0) as total_revenue')
->selectRaw('COALESCE(SUM(contracts.paid_amount), 0) as total_paid')
->selectRaw('COALESCE(SUM(contracts.remaining_amount), 0) as total_remaining')
->leftJoin('products', 'products.project_id', '=', 'projects.id')
->leftJoin('contracts', 'contracts.product_id', '=', 'products.id')
->groupBy('projects.id', 'projects.name', 'projects.code')
)
->columns([
TextColumn::make('name')
->label('Dự án')
->searchable()
->sortable(),
TextColumn::make('product_count')
->label('Tổng SP')
->alignCenter()
->sortable(),
TextColumn::make('sold_product_count')
->label('Đã bán')
->alignCenter()
->sortable()
->color('success'),
TextColumn::make('contract_count')
->label('Số HĐ')
->alignCenter()
->sortable(),
TextColumn::make('total_revenue')
->label('Tổng giá trị HĐ')
->money('VND')
->sortable()
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
TextColumn::make('total_paid')
->label('Đã thu')
->money('VND')
->sortable()
->color('success')
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
TextColumn::make('total_remaining')
->label('Công nợ phải thu')
->money('VND')
->sortable()
->color('danger')
->summarize(\Filament\Tables\Columns\Summarizers\Sum::make()->label('Tổng')->money('VND')),
])
->defaultSort('total_revenue', 'desc')
->paginated([10, 25, 50]);
}
}

View File

@@ -6,8 +6,8 @@ use App\Models\Customer;
use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select; use Filament\Forms\Components\Select;
use Filament\Forms\Components\Section; use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TagsInput;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;

View File

@@ -149,6 +149,14 @@ class PaymentForm
]) ])
->default('Chuyển khoản') ->default('Chuyển khoản')
->required(), ->required(),
Select::make('collected_by')
->label('Ngườ thu')
->relationship('collector', 'name')
->searchable()
->preload()
->default(auth()->id())
->required(),
]), ]),
Section::make('Bổ sung') Section::make('Bổ sung')

View File

@@ -73,6 +73,11 @@ class PaymentsTable
->money('VND') ->money('VND')
->placeholder('-') ->placeholder('-')
->color('danger'), ->color('danger'),
Tables\Columns\TextColumn::make('collector.name')
->label('Ngườ thu')
->placeholder('-')
->sortable(),
]) ])
->filters([ ->filters([
Tables\Filters\SelectFilter::make('method') Tables\Filters\SelectFilter::make('method')

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Filament\Widgets;
use Filament\Actions\Action;
use Filament\Tables;
use Filament\Tables\Table;
use Filament\Widgets\TableWidget as BaseWidget;
class RecentNotifications extends BaseWidget
{
protected int | string | array $columnSpan = 'full';
protected static ?int $sort = 1;
public function table(Table $table): Table
{
return $table
->query(function () {
$user = auth()->user();
if (! $user) {
return \Illuminate\Notifications\DatabaseNotification::query()->whereRaw('1=0');
}
return $user->notifications()->whereNull('read_at')->latest()->getQuery();
})
->columns([
Tables\Columns\TextColumn::make('data.title')
->label('Tiêu đề')
->badge()
->color('warning'),
Tables\Columns\TextColumn::make('data.message')
->label('Nội dung')
->limit(100),
Tables\Columns\TextColumn::make('created_at')
->label('Thờ gian')
->dateTime('d/m/Y H:i')
->color('gray'),
])
->actions([
Action::make('markAsRead')
->label('Đánh dấu đã đọc')
->icon('heroicon-o-check-circle')
->color('success')
->action(function ($record) {
$record->markAsRead();
}),
])
->paginated([5, 10, 25])
->emptyStateHeading('Không có thông báo mới')
->emptyStateDescription('Bạn sẽ nhận được cảnh báo khi có đợt thanh toán sắp đến hạn.');
}
}

View File

@@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contract extends Model class Contract extends Model
{ {
use HasUuids, HasFactory; use HasUuids, HasFactory, SoftDeletes;
protected $guarded = []; protected $guarded = [];

View File

@@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Customer extends Model class Customer extends Model
{ {
use HasUuids, HasFactory; use HasUuids, HasFactory, SoftDeletes;
protected $guarded = []; protected $guarded = [];

View File

@@ -5,10 +5,11 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Payment extends Model class Payment extends Model
{ {
use HasUuids, HasFactory; use HasUuids, HasFactory, SoftDeletes;
protected $guarded = []; protected $guarded = [];
@@ -27,4 +28,9 @@ class Payment extends Model
{ {
return $this->belongsTo(PaymentScheduleItem::class, 'schedule_item_id'); return $this->belongsTo(PaymentScheduleItem::class, 'schedule_item_id');
} }
public function collector()
{
return $this->belongsTo(User::class, 'collected_by');
}
} }

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Notifications;
use App\Models\PaymentScheduleItem;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
class PaymentDueNotification extends Notification
{
use Queueable;
public function __construct(
public PaymentScheduleItem $scheduleItem
) {}
public function via(object $notifiable): array
{
return ['database'];
}
public function toDatabase(object $notifiable): array
{
$contract = $this->scheduleItem->schedule?->contract;
$remaining = (float) $this->scheduleItem->remaining_amount;
return [
'title' => 'Cảnh báo đợt thanh toán sắp đến hạn',
'message' => sprintf(
'HĐ %s - Đợt %d (%s) sẽ đến hạn vào %s. Còn thiếu: %s VNĐ.',
$contract?->contract_number ?? 'N/A',
$this->scheduleItem->installment_no,
$this->scheduleItem->type?->getLabel() ?? 'N/A',
$this->scheduleItem->due_date?->format('d/m/Y') ?? 'N/A',
number_format($remaining)
),
'contract_id' => $contract?->id,
'schedule_item_id' => $this->scheduleItem->id,
'due_date' => $this->scheduleItem->due_date?->toDateString(),
'remaining_amount' => $remaining,
];
}
}

View File

@@ -12,6 +12,7 @@ use Filament\PanelProvider;
use Filament\Support\Colors\Color; use Filament\Support\Colors\Color;
use App\Filament\Widgets\ContractStatsOverview; use App\Filament\Widgets\ContractStatsOverview;
use App\Filament\Widgets\UpcomingPaymentsTable; use App\Filament\Widgets\UpcomingPaymentsTable;
use App\Filament\Widgets\RecentNotifications;
use Filament\Widgets\AccountWidget; use Filament\Widgets\AccountWidget;
use Filament\Widgets\FilamentInfoWidget; use Filament\Widgets\FilamentInfoWidget;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
@@ -41,6 +42,7 @@ class AdminPanelProvider extends PanelProvider
]) ])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets') ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets')
->widgets([ ->widgets([
RecentNotifications::class,
ContractStatsOverview::class, ContractStatsOverview::class,
UpcomingPaymentsTable::class, UpcomingPaymentsTable::class,
AccountWidget::class, AccountWidget::class,

View File

@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('contracts', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('payments', function (Blueprint $table) {
$table->softDeletes();
});
Schema::table('customers', function (Blueprint $table) {
$table->softDeletes();
});
}
public function down(): void
{
Schema::table('contracts', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('payments', function (Blueprint $table) {
$table->dropSoftDeletes();
});
Schema::table('customers', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->foreignId('collected_by')->nullable()->constrained('users')->nullOnDelete();
});
}
public function down(): void
{
Schema::table('payments', function (Blueprint $table) {
$table->dropForeign(['collected_by']);
$table->dropColumn('collected_by');
});
}
};

View File

@@ -0,0 +1,10 @@
<x-filament-panels::page>
<div style="margin-bottom: 1rem;">
<p style="color: #6b7280; font-size: 0.875rem;">
Báo cáo tổng hợp giá trị hợp đồng, thanh toán công nợ theo từng dự án.
Dữ liệu được tính toán trực tiếp từ hệ thống.
</p>
</div>
{{ $this->table }}
</x-filament-panels::page>