From 78c22690eb27e7480782df1e2427e539304fff55 Mon Sep 17 00:00:00 2001 From: phuongtc Date: Wed, 29 Apr 2026 04:46:58 +0000 Subject: [PATCH] WIP: SoftDelete Contract/Payment/Customer, collected_by, Notifications, ProjectReport, ExportDebtReport --- AGENTS.md | 28 +- WORKFLOW.md | 463 ++++++++++++++++++ app/Console/Commands/ExportDebtReport.php | 151 ++++++ .../Commands/SendPaymentDueNotifications.php | 76 +++ app/Filament/Pages/ProjectReport.php | 84 ++++ .../Customers/Schemas/CustomerForm.php | 4 +- .../Payments/Schemas/PaymentForm.php | 8 + .../Payments/Tables/PaymentsTable.php | 5 + app/Filament/Widgets/RecentNotifications.php | 53 ++ app/Models/Contract.php | 3 +- app/Models/Customer.php | 3 +- app/Models/Payment.php | 8 +- app/Notifications/PaymentDueNotification.php | 43 ++ app/Providers/Filament/AdminPanelProvider.php | 2 + ...4_29_000000_create_notifications_table.php | 25 + ...eletes_to_contracts_payments_customers.php | 38 ++ ...29_020000_add_collected_by_to_payments.php | 23 + .../filament/pages/project-report.blade.php | 10 + 18 files changed, 1015 insertions(+), 12 deletions(-) create mode 100644 WORKFLOW.md create mode 100644 app/Console/Commands/ExportDebtReport.php create mode 100644 app/Console/Commands/SendPaymentDueNotifications.php create mode 100644 app/Filament/Pages/ProjectReport.php create mode 100644 app/Filament/Widgets/RecentNotifications.php create mode 100644 app/Notifications/PaymentDueNotification.php create mode 100644 database/migrations/2026_04_29_000000_create_notifications_table.php create mode 100644 database/migrations/2026_04_29_010000_add_soft_deletes_to_contracts_payments_customers.php create mode 100644 database/migrations/2026_04_29_020000_add_collected_by_to_payments.php create mode 100644 resources/views/filament/pages/project-report.blade.php diff --git a/AGENTS.md b/AGENTS.md index 788924e..78bbe72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # 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. -> **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 > **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] **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] **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 - [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Ý - [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 ### 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 -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 -12. **Notification:** Cảnh báo đợt thanh toán sắp đến hạn +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 ✅ +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 ✅ --- @@ -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_030000_create_sales_phases_tables.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 - `app/Services/DiscountEngine.php` - Tính toán chiết khấu - `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/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 - `app/Filament/Resources/PaymentFines/` (Resource + Form + Table + Pages) @@ -387,6 +394,10 @@ DB_HOST=127.0.0.1 php artisan migrate ### Widgets mới - `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/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 - `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` ### 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.) - `config/database.php` - Default `pgsql` - `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 diff --git a/WORKFLOW.md b/WORKFLOW.md new file mode 100644 index 0000000..8759e71 --- /dev/null +++ b/WORKFLOW.md @@ -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ụ.* diff --git a/app/Console/Commands/ExportDebtReport.php b/app/Console/Commands/ExportDebtReport.php new file mode 100644 index 0000000..da87fd5 --- /dev/null +++ b/app/Console/Commands/ExportDebtReport.php @@ -0,0 +1,151 @@ +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); + } + } +} diff --git a/app/Console/Commands/SendPaymentDueNotifications.php b/app/Console/Commands/SendPaymentDueNotifications.php new file mode 100644 index 0000000..75e577c --- /dev/null +++ b/app/Console/Commands/SendPaymentDueNotifications.php @@ -0,0 +1,76 @@ +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; + } +} diff --git a/app/Filament/Pages/ProjectReport.php b/app/Filament/Pages/ProjectReport.php new file mode 100644 index 0000000..b957dc1 --- /dev/null +++ b/app/Filament/Pages/ProjectReport.php @@ -0,0 +1,84 @@ +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]); + } +} diff --git a/app/Filament/Resources/Customers/Schemas/CustomerForm.php b/app/Filament/Resources/Customers/Schemas/CustomerForm.php index f2bd323..3d3f96b 100644 --- a/app/Filament/Resources/Customers/Schemas/CustomerForm.php +++ b/app/Filament/Resources/Customers/Schemas/CustomerForm.php @@ -6,8 +6,8 @@ use App\Models\Customer; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Select; -use Filament\Forms\Components\Section; -use Filament\Forms\Components\Actions\Action; +use Filament\Schemas\Components\Section; +use Filament\Actions\Action; use Filament\Forms\Components\TagsInput; use Filament\Schemas\Schema; use Filament\Schemas\Components\Utilities\Set; diff --git a/app/Filament/Resources/Payments/Schemas/PaymentForm.php b/app/Filament/Resources/Payments/Schemas/PaymentForm.php index 450a3fa..3f9d99b 100644 --- a/app/Filament/Resources/Payments/Schemas/PaymentForm.php +++ b/app/Filament/Resources/Payments/Schemas/PaymentForm.php @@ -149,6 +149,14 @@ class PaymentForm ]) ->default('Chuyển khoản') ->required(), + + Select::make('collected_by') + ->label('Ngườ thu') + ->relationship('collector', 'name') + ->searchable() + ->preload() + ->default(auth()->id()) + ->required(), ]), Section::make('Bổ sung') diff --git a/app/Filament/Resources/Payments/Tables/PaymentsTable.php b/app/Filament/Resources/Payments/Tables/PaymentsTable.php index 0b0387d..c9a9d55 100644 --- a/app/Filament/Resources/Payments/Tables/PaymentsTable.php +++ b/app/Filament/Resources/Payments/Tables/PaymentsTable.php @@ -73,6 +73,11 @@ class PaymentsTable ->money('VND') ->placeholder('-') ->color('danger'), + + Tables\Columns\TextColumn::make('collector.name') + ->label('Ngườ thu') + ->placeholder('-') + ->sortable(), ]) ->filters([ Tables\Filters\SelectFilter::make('method') diff --git a/app/Filament/Widgets/RecentNotifications.php b/app/Filament/Widgets/RecentNotifications.php new file mode 100644 index 0000000..a87646d --- /dev/null +++ b/app/Filament/Widgets/RecentNotifications.php @@ -0,0 +1,53 @@ +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.'); + } +} diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 5174d1c..35b4b87 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -6,10 +6,11 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\SoftDeletes; class Contract extends Model { - use HasUuids, HasFactory; + use HasUuids, HasFactory, SoftDeletes; protected $guarded = []; diff --git a/app/Models/Customer.php b/app/Models/Customer.php index dac6a57..e2c8418 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -7,10 +7,11 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\SoftDeletes; class Customer extends Model { - use HasUuids, HasFactory; + use HasUuids, HasFactory, SoftDeletes; protected $guarded = []; diff --git a/app/Models/Payment.php b/app/Models/Payment.php index cfdb1ab..c2299c2 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -5,10 +5,11 @@ namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class Payment extends Model { - use HasUuids, HasFactory; + use HasUuids, HasFactory, SoftDeletes; protected $guarded = []; @@ -27,4 +28,9 @@ class Payment extends Model { return $this->belongsTo(PaymentScheduleItem::class, 'schedule_item_id'); } + + public function collector() + { + return $this->belongsTo(User::class, 'collected_by'); + } } diff --git a/app/Notifications/PaymentDueNotification.php b/app/Notifications/PaymentDueNotification.php new file mode 100644 index 0000000..a3b54b6 --- /dev/null +++ b/app/Notifications/PaymentDueNotification.php @@ -0,0 +1,43 @@ +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, + ]; + } +} diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 1596ea1..92d3c2e 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -12,6 +12,7 @@ use Filament\PanelProvider; use Filament\Support\Colors\Color; use App\Filament\Widgets\ContractStatsOverview; use App\Filament\Widgets\UpcomingPaymentsTable; +use App\Filament\Widgets\RecentNotifications; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -41,6 +42,7 @@ class AdminPanelProvider extends PanelProvider ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets') ->widgets([ + RecentNotifications::class, ContractStatsOverview::class, UpcomingPaymentsTable::class, AccountWidget::class, diff --git a/database/migrations/2026_04_29_000000_create_notifications_table.php b/database/migrations/2026_04_29_000000_create_notifications_table.php new file mode 100644 index 0000000..52e3b00 --- /dev/null +++ b/database/migrations/2026_04_29_000000_create_notifications_table.php @@ -0,0 +1,25 @@ +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'); + } +}; diff --git a/database/migrations/2026_04_29_010000_add_soft_deletes_to_contracts_payments_customers.php b/database/migrations/2026_04_29_010000_add_soft_deletes_to_contracts_payments_customers.php new file mode 100644 index 0000000..7e3d525 --- /dev/null +++ b/database/migrations/2026_04_29_010000_add_soft_deletes_to_contracts_payments_customers.php @@ -0,0 +1,38 @@ +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(); + }); + } +}; diff --git a/database/migrations/2026_04_29_020000_add_collected_by_to_payments.php b/database/migrations/2026_04_29_020000_add_collected_by_to_payments.php new file mode 100644 index 0000000..7ba52b5 --- /dev/null +++ b/database/migrations/2026_04_29_020000_add_collected_by_to_payments.php @@ -0,0 +1,23 @@ +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'); + }); + } +}; diff --git a/resources/views/filament/pages/project-report.blade.php b/resources/views/filament/pages/project-report.blade.php new file mode 100644 index 0000000..d16b2a2 --- /dev/null +++ b/resources/views/filament/pages/project-report.blade.php @@ -0,0 +1,10 @@ + +
+

+ Báo cáo tổng hợp giá trị hợp đồng, thanh toán và công nợ theo từng dự án. + Dữ liệu được tính toán trực tiếp từ hệ thống. +

+
+ + {{ $this->table }} +