Hoan thien core finance v2
This commit is contained in:
120
AGENTS.md
120
AGENTS.md
@@ -120,9 +120,13 @@
|
|||||||
|
|
||||||
**Quan hệ:**
|
**Quan hệ:**
|
||||||
- `product()`, `customers()` (belongsToMany qua contract_customers)
|
- `product()`, `customers()` (belongsToMany qua contract_customers)
|
||||||
|
- `paymentTemplate()` (belongsTo PaymentTemplate)
|
||||||
- `appendices()`, `paymentSchedule()`, `scheduleItems()` (HasManyThrough)
|
- `appendices()`, `paymentSchedule()`, `scheduleItems()` (HasManyThrough)
|
||||||
- `payments()`, `paymentFines()`
|
- `payments()`, `paymentFines()`
|
||||||
|
|
||||||
|
**Accessor:**
|
||||||
|
- `final_value`: Giá trị sau chiết khấu (tính từ `DiscountEngine`)
|
||||||
|
|
||||||
**Filament Resources:**
|
**Filament Resources:**
|
||||||
- `ContractResource` → `ContractForm` + `ContractsTable`
|
- `ContractResource` → `ContractForm` + `ContractsTable`
|
||||||
- Action "Tạo lịch TT" trong Table (gọi `ContractScheduleService`)
|
- Action "Tạo lịch TT" trong Table (gọi `ContractScheduleService`)
|
||||||
@@ -160,7 +164,13 @@
|
|||||||
**Filament Resources:**
|
**Filament Resources:**
|
||||||
- `PaymentResource` → `PaymentForm` + `PaymentsTable`
|
- `PaymentResource` → `PaymentForm` + `PaymentsTable`
|
||||||
- Form chọn Contract → chọn Đợt thanh toán (cascade)
|
- Form chọn Contract → chọn Đợt thanh toán (cascade)
|
||||||
|
- Validation số tiền không vượt quá công nợ đợt / công nợ HĐ
|
||||||
- Table có filter theo phương thức và ngày thu
|
- Table có filter theo phương thức và ngày thu
|
||||||
|
- Cột đối soát: Đủ / Thiếu / Thừa (tính tổng payments của đợt)
|
||||||
|
- Cột còn thiếu tiền theo đợt
|
||||||
|
- `PaymentFineResource` → Quản lý tiền phạt chậm thanh toán
|
||||||
|
- `AppendixResource` → Quản lý phụ lục hợp đồng
|
||||||
|
- `SettlementResource` → Quản lý quyết toán & sổ đỏ
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -177,6 +187,11 @@
|
|||||||
- Parse ngày tháng Excel (số serial hoặc chuỗi)
|
- Parse ngày tháng Excel (số serial hoặc chuỗi)
|
||||||
- Tự động tạo mẫu Công ty + Ngườ đại diện (Công ty TNHH BĐS Thịnh Vượng)
|
- Tự động tạo mẫu Công ty + Ngườ đại diện (Công ty TNHH BĐS Thịnh Vượng)
|
||||||
|
|
||||||
|
### `contracts:generate-schedules {--force}`
|
||||||
|
- Tự động tạo lịch thanh toán cho các hợp đồng chưa có lịch
|
||||||
|
- Ưu tiên `contract.payment_template_id`, fallback lấy từ `product.project.paymentTemplate`
|
||||||
|
- Option `--force` để tạo lại lịch cho HĐ đã có schedule
|
||||||
|
|
||||||
### `import:contracts-complex {hopdong=hopdong.xlsx} {hdkh=Hd_kh.xlsx}`
|
### `import:contracts-complex {hopdong=hopdong.xlsx} {hdkh=Hd_kh.xlsx}`
|
||||||
- Logic "Bắc cầu" 2 file:
|
- Logic "Bắc cầu" 2 file:
|
||||||
1. `hopdong.xlsx`: Dữ liệu tài chính (theo Số HĐMB)
|
1. `hopdong.xlsx`: Dữ liệu tài chính (theo Số HĐMB)
|
||||||
@@ -204,21 +219,25 @@
|
|||||||
- [x] PaymentResource (Form + Table)
|
- [x] PaymentResource (Form + Table)
|
||||||
- [x] Test: ContractFinanceFlowTest (PASS)
|
- [x] Test: ContractFinanceFlowTest (PASS)
|
||||||
- [x] Cấu hình PHPUnit dùng PostgreSQL testing database
|
- [x] Cấu hình PHPUnit dùng PostgreSQL testing database
|
||||||
|
- [x] **Fix ContractForm:** `payment_template_id` đã lưu vào DB, tự động tạo lịch khi tạo HĐ mới
|
||||||
|
- [x] **PaymentForm validation:** Không cho phép thu quá công nợ đợt / HĐ
|
||||||
|
- [x] **PaymentsTable:** Thêm cột Loại đợt, Trạng thái đối soát, Còn thiếu
|
||||||
|
- [x] **Command generate schedule hàng loạt:** `php artisan contracts:generate-schedules`
|
||||||
|
- [x] **PaymentFine Resource:** Form + Table đầy đủ
|
||||||
|
- [x] **Appendix Resource:** Form + Table đầy đủ
|
||||||
|
- [x] **Settlement Resource:** Form + Table đầy đủ
|
||||||
|
- [x] **Discount Engine:** Tính toán tự động chiết khấu + hiển thị `final_value` trong ContractForm
|
||||||
|
|
||||||
### 5.2. Đang dở / Cần tiếp tục
|
### 5.2. Đang dở / Cần tiếp tục
|
||||||
- [ ] **ContractForm:** `payment_template_id` đang `dehydrated(false)` - chưa tự động tạo lịch khi tạo hợp đồng mới từ form (hiện chỉ có trong CreateContract page sau khi submit)
|
- [x] **Dashboard thống kê:** Đã tạo `ContractStatsOverview` + `UpcomingPaymentsTable`
|
||||||
- [ ] **PaymentsTable:** Chưa có cột trạng thái đối soát (so sánh với schedule_item amount)
|
- [ ] **Notification:** Cảnh báo đợt thanh toán sắp đến hạn (chưa có hệ thống notification)
|
||||||
- [ ] **Module Chiết khấu (Discounts):** Chưa có engine tính toán tự động dựa trên `discount_details`
|
|
||||||
- [ ] **PaymentFine:** Model đã có nhưng chưa có Resource/Form
|
|
||||||
- [ ] **Appendix & Settlement:** Chưa có Filament Resources
|
|
||||||
- [ ] **Báo cáo:** Chưa có Dashboard thống kê
|
|
||||||
- [ ] **Tự động hóa lịch trình cho 139 HĐ:** Cần command hoặc action để generate schedule hàng loạt
|
|
||||||
|
|
||||||
### 5.3. Vấn đề kỹ thuật cần xử lý
|
### 5.3. Vấn đề kỹ thuật ĐÃ XỬ LÝ
|
||||||
- [ ] `payment_template_id` trong ContractForm cần hook `afterCreate` hoặc đổi thành dehydrated + xử lý trong CreateContract
|
- [x] ContractTable đã thêm cột `paid_amount` / `remaining_amount`, chuyển sang dùng `ContractsTable` Schemas
|
||||||
- [ ] PaymentsTable nên hiển thị `scheduleItem.type` và trạng thái đối soát
|
- [x] Logic `syncWithoutDetaching` trong ImportContractsComplex đảm bảo nhiều KH cùng 1 HĐ không bị ghi đè
|
||||||
- [ ] ContractTable có thể thêm cột `paid_amount` / `remaining_amount` (đã có trong Resource nhưng chưa commit staged)
|
- [x] Fix N+1 query ở `PaymentScheduleItem::getPaidAmountAttribute()` (dùng `relationLoaded`)
|
||||||
- [ ] Cần kiểm tra logic `updateOrCreate` trong ImportContractsComplex với nhiều khách hàng cùng 1 hợp đồng
|
- [x] Fix PaymentForm validation khi edit (`instanceof Payment` thay vì truthy check)
|
||||||
|
- [x] Fix ContractForm `final_value_display` hiển thị được cả khi create (dùng `$get` state)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -267,47 +286,48 @@ DB_HOST=127.0.0.1 php artisan migrate
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. DANH SÁCH FILE ĐÃ THAY ĐỔI (Git Status)
|
## 8. DANH SÁCH FILE MỚI / THAY ĐỔI TRONG PHIÊN NÀY
|
||||||
|
|
||||||
### Staged (Sẵn sàng commit)
|
### Migrations mới
|
||||||
- `HQLAND_PROJECT_BLUEPRINT.md`
|
- `database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php`
|
||||||
- `analyze_contracts.php`, `analyze_excel.php`, `analyze_khachhang.php`
|
|
||||||
- `app/Console/Commands/ImportContractsComplex.php`
|
|
||||||
- `app/Console/Commands/ImportCustomersExcel.php`
|
|
||||||
- `app/Console/Commands/ImportProductsExcel.php`
|
|
||||||
- `app/Filament/Resources/Contracts/Schemas/ContractForm.php`
|
|
||||||
- `app/Filament/Resources/Contracts/Tables/ContractsTable.php`
|
|
||||||
- `app/Filament/Resources/Customers/CustomerResource.php`
|
|
||||||
- `app/Filament/Resources/Customers/Schemas/CustomerForm.php`
|
|
||||||
- `app/Filament/Resources/Customers/Tables/CustomersTable.php`
|
|
||||||
- `app/Filament/Resources/Products/Schemas/ProductForm.php`
|
|
||||||
- `app/Models/Contract.php`
|
|
||||||
- `app/Models/Customer.php`
|
|
||||||
- `composer.json`, `composer.lock`
|
|
||||||
- `database/migrations/2026_04_23_081206_update_customers_table_for_real_estate.php`
|
|
||||||
- `database/migrations/2026_04_23_094837_expand_contracts_table_for_finance.php`
|
|
||||||
- `tests/Feature/ContractFinanceFlowTest.php`
|
|
||||||
- `tests/Feature/ProductResourceTest.php`
|
|
||||||
|
|
||||||
### Unstaged (Đang chỉnh sửa, chưa xong)
|
### Services mới
|
||||||
- `.gitignore`
|
- `app/Services/DiscountEngine.php` - Tính toán chiết khấu
|
||||||
- `analyze_contracts.php`
|
- `app/Console/Commands/GenerateContractSchedules.php` - Command tạo lịch hàng loạt
|
||||||
- `app/Filament/Resources/Contracts/ContractResource.php` (thêm action Tạo lịch TT)
|
|
||||||
- `app/Filament/Resources/Contracts/Pages/CreateContract.php` (refactor dùng Service)
|
|
||||||
- `app/Filament/Resources/Contracts/Schemas/ContractForm.php`
|
|
||||||
- `app/Filament/Resources/Projects/ProjectResource.php` (refactor sang Schemas)
|
|
||||||
- `app/Filament/Resources/Projects/Schemas/ProjectForm.php`
|
|
||||||
- `app/Models/Contract.php` (booted logic tài chính)
|
|
||||||
- `app/Providers/AppServiceProvider.php` (đăng ký PaymentObserver)
|
|
||||||
- `composer.json` (xóa script tạo SQLite)
|
|
||||||
- `config/database.php` (default về pgsql)
|
|
||||||
- `database/factories/CustomerFactory.php`
|
|
||||||
- `phpunit.xml` (cấu hình PostgreSQL testing)
|
|
||||||
|
|
||||||
### Untracked (File mới chưa add)
|
### Filament Resources mới
|
||||||
- `app/Filament/Resources/Payments/` (PaymentResource, Form, Table, Pages)
|
- `app/Filament/Resources/PaymentFines/` (Resource + Form + Table + Pages)
|
||||||
- `app/Observers/PaymentObserver.php`
|
- `app/Filament/Resources/Appendices/` (Resource + Form + Table + Pages)
|
||||||
- `app/Services/ContractScheduleService.php`
|
- `app/Filament/Resources/Settlements/` (Resource + Form + Table + Pages)
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
### Models sửa đổi
|
||||||
|
- `app/Models/Contract.php` - Thêm `paymentTemplate()`, accessor `final_value`
|
||||||
|
- `app/Models/PaymentScheduleItem.php` - Thêm accessor `paid_amount`, `remaining_amount`
|
||||||
|
- `app/Models/User.php` - Thêm `FilamentUser` interface để user có quyền truy cập panel
|
||||||
|
|
||||||
|
### Forms/Tables sửa đổi
|
||||||
|
- `app/Filament/Resources/Contracts/ContractResource.php` - Fix action `EditAction` namespace (`Filament\Actions\EditAction`)
|
||||||
|
- `app/Filament/Resources/Contracts/Schemas/ContractForm.php` - Fix `payment_template_id`, thêm `final_value_display`
|
||||||
|
- `app/Filament/Resources/Contracts/Pages/CreateContract.php` - Refactor dùng `$contract->payment_template_id`
|
||||||
|
- `app/Filament/Resources/Payments/Schemas/PaymentForm.php` - Thêm validation amount + helper text công nợ
|
||||||
|
- `app/Filament/Resources/Payments/Tables/PaymentsTable.php` - Thêm cột Loại đợt, Đối soát, Còn thiếu
|
||||||
|
- `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
|
||||||
|
- `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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. FILE HỖ TRỢ CHUYỂN MÁY
|
||||||
|
- `NEXT_SESSION.md` - Checklist và hướng dẫn nhanh cho phiên làm việc tiếp theo
|
||||||
|
- `COMMIT_GUIDE.md` - Hướng dẫn commit toàn bộ thay đổi chưa commit
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
87
COMMIT_GUIDE.md
Normal file
87
COMMIT_GUIDE.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# HƯỚNG DẪN COMMIT NHANH
|
||||||
|
|
||||||
|
> Commit toàn bộ thay đổi trước khi chuyển máy
|
||||||
|
> **Ngày:** 24/04/2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bước 1: Kiểm tra trạng thái
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status
|
||||||
|
```
|
||||||
|
|
||||||
|
Hiện tại có **26 file thay đổi** chưa commit:
|
||||||
|
- 17 file modified
|
||||||
|
- 9 file untracked (mới)
|
||||||
|
|
||||||
|
## Bước 2: Commit ngay
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "Hoan thien core finance v2 - Fix EditAction, Payment validation, Discount Engine, Dashboard"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bước 3: Push (nếu cần)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
Nếu cần nhập credentials, bạn sẽ tự nhập.
|
||||||
|
|
||||||
|
## Bước 4: Kiểm tra
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline -3
|
||||||
|
```
|
||||||
|
|
||||||
|
Đảm bảo commit mới xuất hiện ở đầu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nếu quên commit trước khi chuyển máy
|
||||||
|
|
||||||
|
Các file thay đổi vẫn nằm trong thư mục git nhưng chưa commit. Khi mở máy mới:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd hqland-app
|
||||||
|
git status # Xem danh sách file chưa commit
|
||||||
|
git diff # Xem chi tiết thay đổi
|
||||||
|
```
|
||||||
|
|
||||||
|
Sau đó commit như bình thường.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Danh sách file cần commit
|
||||||
|
|
||||||
|
### Sửa đổi:
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `app/Console/Commands/ImportContractsComplex.php`
|
||||||
|
- `app/Filament/Resources/Contracts/ContractResource.php`
|
||||||
|
- `app/Filament/Resources/Contracts/Pages/CreateContract.php`
|
||||||
|
- `app/Filament/Resources/Contracts/Schemas/ContractForm.php`
|
||||||
|
- `app/Filament/Resources/Contracts/Tables/ContractsTable.php`
|
||||||
|
- `app/Filament/Resources/Payments/PaymentResource.php`
|
||||||
|
- `app/Filament/Resources/Payments/Schemas/PaymentForm.php`
|
||||||
|
- `app/Filament/Resources/Payments/Tables/PaymentsTable.php`
|
||||||
|
- `app/Models/Contract.php`
|
||||||
|
- `app/Models/PaymentScheduleItem.php`
|
||||||
|
- `app/Models/User.php`
|
||||||
|
- `app/Providers/Filament/AdminPanelProvider.php`
|
||||||
|
|
||||||
|
### File mới:
|
||||||
|
- `app/Console/Commands/GenerateContractSchedules.php`
|
||||||
|
- `app/Filament/Resources/Appendices/` (folder)
|
||||||
|
- `app/Filament/Resources/PaymentFines/` (folder)
|
||||||
|
- `app/Filament/Resources/Settlements/` (folder)
|
||||||
|
- `app/Filament/Widgets/` (folder)
|
||||||
|
- `app/Services/DiscountEngine.php`
|
||||||
|
- `database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php`
|
||||||
|
- `tests/Feature/ContractResourceRenderTest.php`
|
||||||
|
- `NEXT_SESSION.md` (file này)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Hãy commit NGAY BÂY GIỜ để đảm bảo không mất công việc!*
|
||||||
158
NEXT_SESSION.md
Normal file
158
NEXT_SESSION.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# HQLAND - HƯỚNG DẪN PHIÊN LÀM VIỆC TIẾP THEO
|
||||||
|
|
||||||
|
> File này giúp AI Agent nhanh chóng bắt nhịp khi bạn chuyển sang máy tính khác.
|
||||||
|
> **Cập nhật:** 24/04/2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ THÔNG BÁO QUAN TRỌNG
|
||||||
|
|
||||||
|
Có **26 file thay đổi CHƯA COMMIT**. Bạn cần commit hoặc stash trước khi chuyển máy, nếu không sẽ mất toàn bộ công việc vừa làm!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Cách 1: Commit ngay (KHUYẾN NGHỊ)
|
||||||
|
git add -A
|
||||||
|
git commit -m "Hoan thien core finance v2"
|
||||||
|
|
||||||
|
# Cách 2: Hoặc stash để commit sau
|
||||||
|
git stash -u
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. NHỮNG GÌ VỪA HOÀN THÀNH (Phiên hiện tại)
|
||||||
|
|
||||||
|
### ✅ Fix lỗi quan trọng
|
||||||
|
- **EditAction not found:** Đã sửa namespace `Filament\Actions\EditAction` (không phải `Filament\Tables\Actions\EditAction`)
|
||||||
|
- **User quyền truy cập:** Thêm `FilamentUser` interface vào `User.php`
|
||||||
|
|
||||||
|
### ✅ ContractForm + Tự động tạo lịch
|
||||||
|
- Migration mới: `payment_template_id` trong bảng `contracts`
|
||||||
|
- `payment_template_id` đã lưu vào DB, không còn `dehydrated(false)`
|
||||||
|
- Tự động tạo lịch thanh toán khi tạo HĐ mới
|
||||||
|
|
||||||
|
### ✅ PaymentForm Validation
|
||||||
|
- Số tiền thu không vượt quá công nợ đợt TT / công nợ HĐ
|
||||||
|
- Helper text hiển thị công nợ còn lại
|
||||||
|
- Fix lỗi khi edit payment (kiểm tra `instanceof Payment`)
|
||||||
|
|
||||||
|
### ✅ PaymentsTable
|
||||||
|
- Thêm cột: Loại đợt, Trạng thái đối soát (Đủ/Thiếu/Thừa), Còn thiếu
|
||||||
|
|
||||||
|
### ✅ ContractsTable
|
||||||
|
- Thêm cột: `paid_amount`, `remaining_amount`
|
||||||
|
- `ContractResource` giờ delegate về `ContractsTable` Schemas
|
||||||
|
|
||||||
|
### ✅ Hiệu năng
|
||||||
|
- Fix N+1 query ở `PaymentScheduleItem::getPaidAmountAttribute()` (kiểm tra `relationLoaded`)
|
||||||
|
|
||||||
|
### ✅ Command mới
|
||||||
|
- `php artisan contracts:generate-schedules {--force}` - Tạo lịch hàng loạt cho 139 HĐ
|
||||||
|
|
||||||
|
### ✅ Resources mới
|
||||||
|
- `PaymentFineResource` - Quản lý tiền phạt
|
||||||
|
- `AppendixResource` - Quản lý phụ lục HĐ
|
||||||
|
- `SettlementResource` - Quản lý quyết toán & sổ đỏ
|
||||||
|
|
||||||
|
### ✅ Discount Engine
|
||||||
|
- `DiscountEngine::calculate()` - Tính chiết khấu tự động
|
||||||
|
- Accessor `final_value` trong Contract model
|
||||||
|
- Hiển thị giá trị sau chiết khấu cả khi create và edit
|
||||||
|
|
||||||
|
### ✅ Dashboard
|
||||||
|
- `ContractStatsOverview` - 5 chỉ số tài chính tổng quan
|
||||||
|
- `UpcomingPaymentsTable` - Danh sách đợt TT sắp đến hạn (30 ngày)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. CẤU HÌNH DATABASE (QUAN TRỌNG)
|
||||||
|
|
||||||
|
### Database chính (Production)
|
||||||
|
- **Connection:** pgsql
|
||||||
|
- **Host:** 127.0.0.1 (từ host machine)
|
||||||
|
- **Database:** laravel
|
||||||
|
- **Username/Password:** sail / password
|
||||||
|
|
||||||
|
### Database test
|
||||||
|
- **Database:** laravel_testing (đã tạo, migrations đã chạy)
|
||||||
|
- **Chạy test:** `DB_HOST=127.0.0.1 ./vendor/bin/pest`
|
||||||
|
|
||||||
|
### Lệnh chạy Artisan
|
||||||
|
- `DB_HOST=127.0.0.1 php artisan tinker`
|
||||||
|
- `DB_HOST=127.0.0.1 php artisan migrate` (KHÔNG dùng `migrate:fresh`!)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. CÁC FILE CHƯA COMMIT
|
||||||
|
|
||||||
|
### Modified (17 file)
|
||||||
|
```
|
||||||
|
AGENTS.md
|
||||||
|
app/Console/Commands/ImportContractsComplex.php
|
||||||
|
app/Filament/Resources/Contracts/ContractResource.php
|
||||||
|
app/Filament/Resources/Contracts/Pages/CreateContract.php
|
||||||
|
app/Filament/Resources/Contracts/Schemas/ContractForm.php
|
||||||
|
app/Filament/Resources/Contracts/Tables/ContractsTable.php
|
||||||
|
app/Filament/Resources/Payments/PaymentResource.php
|
||||||
|
app/Filament/Resources/Payments/Schemas/PaymentForm.php
|
||||||
|
app/Filament/Resources/Payments/Tables/PaymentsTable.php
|
||||||
|
app/Models/Contract.php
|
||||||
|
app/Models/PaymentScheduleItem.php
|
||||||
|
app/Models/User.php
|
||||||
|
app/Providers/Filament/AdminPanelProvider.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Untracked mới (9 file/folder)
|
||||||
|
```
|
||||||
|
app/Console/Commands/GenerateContractSchedules.php
|
||||||
|
app/Filament/Resources/Appendices/
|
||||||
|
app/Filament/Resources/PaymentFines/
|
||||||
|
app/Filament/Resources/Settlements/
|
||||||
|
app/Filament/Widgets/
|
||||||
|
app/Services/DiscountEngine.php
|
||||||
|
database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php
|
||||||
|
tests/Feature/ContractResourceRenderTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. VIỆC CẦN LÀM TIẾP THEO (Checklist)
|
||||||
|
|
||||||
|
### 🟡 Trung bình ưu tiên
|
||||||
|
- [ ] **Notification:** Cảnh báo đợt thanh toán sắp đến hạn (30/7/3 ngày)
|
||||||
|
- [ ] **Export Excel:** Xuất báo cáo công nợ khách hàng
|
||||||
|
- [ ] **Báo cáo theo Dự án:** Thống kê bán hàng, thanh toán theo dự án
|
||||||
|
|
||||||
|
### 🟢 Thấp ưu tiên
|
||||||
|
- [ ] **Audit Log:** Lưu lịch sử sửa HĐ, thu tiền
|
||||||
|
- [ ] **Queue:** Generate schedules qua queue nếu >1000 HĐ
|
||||||
|
- [ ] **Email/SMS:** Tự động nhắc thanh toán
|
||||||
|
- [ ] **Advanced Filter:** Tìm HĐ theo khoảng giá trị, ngày ký
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. CÂU LỆNH TEST QUAN TRỌNG
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test toàn bộ
|
||||||
|
DB_HOST=127.0.0.1 ./vendor/bin/pest
|
||||||
|
|
||||||
|
# Test cụ thể
|
||||||
|
DB_HOST=127.0.0.1 ./vendor/bin/pest --filter="ContractFinanceFlowTest"
|
||||||
|
|
||||||
|
# Test render (kiểm tra không bị lỗi class not found)
|
||||||
|
DB_HOST=127.0.0.1 ./vendor/bin/pest --filter="ContractResourceRenderTest"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Kết quả hiện tại:** 9 tests passed, 0 failed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. TÀI KHOẢN ĐĂNG NHẬP
|
||||||
|
|
||||||
|
- **Email:** admin@phuongtc.com
|
||||||
|
- **Password:** 1Qazxsw2@!321
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Lưu ý: Commit ngay lập tức trước khi tắt máy hoặc chuyển sang máy khác!*
|
||||||
75
app/Console/Commands/GenerateContractSchedules.php
Normal file
75
app/Console/Commands/GenerateContractSchedules.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Models\Contract;
|
||||||
|
use App\Services\ContractScheduleService;
|
||||||
|
|
||||||
|
class GenerateContractSchedules extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'contracts:generate-schedules {--force : Tạo lại lịch cho các hợp đồng đã có lịch}';
|
||||||
|
protected $description = 'Tự động tạo lịch thanh toán cho các hợp đồng chưa có lịch';
|
||||||
|
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$force = $this->option('force');
|
||||||
|
|
||||||
|
$query = Contract::query()
|
||||||
|
->whereNotNull('signing_date')
|
||||||
|
->when(! $force, fn ($q) => $q->whereDoesntHave('paymentSchedule'));
|
||||||
|
|
||||||
|
$total = $query->count();
|
||||||
|
|
||||||
|
if ($total === 0) {
|
||||||
|
$this->info('Không có hợp đồng nào cần tạo lịch thanh toán.');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Tìm thấy {$total} hợp đồng cần tạo lịch thanh toán...");
|
||||||
|
|
||||||
|
$success = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
$query->chunk(50, function ($contracts) use (&$success, &$skipped, &$errors) {
|
||||||
|
foreach ($contracts as $contract) {
|
||||||
|
try {
|
||||||
|
// Xác định template
|
||||||
|
$template = $contract->paymentTemplate;
|
||||||
|
|
||||||
|
if (! $template) {
|
||||||
|
$template = $contract->product?->project?->paymentTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $template) {
|
||||||
|
$skipped++;
|
||||||
|
$this->warn("Bỏ qua HĐ {$contract->contract_number}: Không tìm thấy mẫu thanh toán.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ContractScheduleService::generateFromTemplate($contract, $template);
|
||||||
|
$success++;
|
||||||
|
$this->info("[OK] HĐ {$contract->contract_number} - Đã tạo lịch từ mẫu '{$template->name}'.");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$errors[] = "HĐ {$contract->contract_number}: " . $e->getMessage();
|
||||||
|
$this->error("[LỖI] HĐ {$contract->contract_number}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->info("===== KẾT QUẢ =====");
|
||||||
|
$this->info("Thành công: {$success}");
|
||||||
|
$this->info("Bỏ qua: {$skipped}");
|
||||||
|
|
||||||
|
if (count($errors) > 0) {
|
||||||
|
$this->error("Lỗi: " . count($errors));
|
||||||
|
foreach ($errors as $err) {
|
||||||
|
$this->error(" - {$err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,6 +127,7 @@ class ImportContractsComplex extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Liên kết khách hàng (Pivot)
|
// Liên kết khách hàng (Pivot)
|
||||||
|
// syncWithoutDetaching đảm bảo nhiều KH cùng 1 HĐ không bị ghi đè lẫn nhau
|
||||||
$contract->customers()->syncWithoutDetaching([
|
$contract->customers()->syncWithoutDetaching([
|
||||||
$customer->id => [
|
$customer->id => [
|
||||||
'role' => $row[7] ?? 'Chủ SH',
|
'role' => $row[7] ?? 'Chủ SH',
|
||||||
|
|||||||
42
app/Filament/Resources/Appendices/AppendixResource.php
Normal file
42
app/Filament/Resources/Appendices/AppendixResource.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Appendices\Pages;
|
||||||
|
use App\Models\Appendix;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use App\Filament\Resources\Appendices\Schemas\AppendixForm;
|
||||||
|
use App\Filament\Resources\Appendices\Tables\AppendicesTable;
|
||||||
|
|
||||||
|
class AppendixResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Appendix::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
||||||
|
protected static ?int $navigationSort = 4;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Phụ lục';
|
||||||
|
protected static ?string $pluralModelLabel = 'Phụ lục HĐ';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return AppendixForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return AppendicesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListAppendices::route('/'),
|
||||||
|
'create' => Pages\CreateAppendix::route('/create'),
|
||||||
|
'edit' => Pages\EditAppendix::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Appendices/Pages/CreateAppendix.php
Normal file
11
app/Filament/Resources/Appendices/Pages/CreateAppendix.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Appendices\AppendixResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateAppendix extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AppendixResource::class;
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Appendices/Pages/EditAppendix.php
Normal file
11
app/Filament/Resources/Appendices/Pages/EditAppendix.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Appendices\AppendixResource;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditAppendix extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = AppendixResource::class;
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Appendices/Pages/ListAppendices.php
Normal file
11
app/Filament/Resources/Appendices/Pages/ListAppendices.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Appendices\AppendixResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListAppendices extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = AppendixResource::class;
|
||||||
|
}
|
||||||
65
app/Filament/Resources/Appendices/Schemas/AppendixForm.php
Normal file
65
app/Filament/Resources/Appendices/Schemas/AppendixForm.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\KeyValue;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class AppendixForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
Section::make('Thông tin phụ lục')
|
||||||
|
->columnSpan(2)
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
Select::make('contract_id')
|
||||||
|
->label('Hợp đồng gốc')
|
||||||
|
->relationship('contract', 'contract_number')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
Select::make('product_id')
|
||||||
|
->label('Sản phẩm')
|
||||||
|
->relationship('product', 'code')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('type')
|
||||||
|
->label('Loại phụ lục')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('apply_from_order')
|
||||||
|
->label('Áp dụng từ CN thứ')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
DatePicker::make('signing_date')
|
||||||
|
->label('Ngày ký phụ lục')
|
||||||
|
->required(),
|
||||||
|
]),
|
||||||
|
|
||||||
|
Section::make('Dữ liệu bổ sung')
|
||||||
|
->columnSpan(1)
|
||||||
|
->schema([
|
||||||
|
KeyValue::make('custom_data')
|
||||||
|
->label('Thông tin bổ sung')
|
||||||
|
->keyLabel('Thông tin')
|
||||||
|
->valueLabel('Giá trị'),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Filament/Resources/Appendices/Tables/AppendicesTable.php
Normal file
38
app/Filament/Resources/Appendices/Tables/AppendicesTable.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Appendices\Tables;
|
||||||
|
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class AppendicesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('contract.contract_number')
|
||||||
|
->label('Hợp đồng')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('product.code')
|
||||||
|
->label('Sản phẩm')
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('type')
|
||||||
|
->label('Loại phụ lục')
|
||||||
|
->badge(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('apply_from_order')
|
||||||
|
->label('Từ CN')
|
||||||
|
->alignCenter(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('signing_date')
|
||||||
|
->label('Ngày ký')
|
||||||
|
->date('d/m/Y')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->defaultSort('signing_date', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,12 @@ namespace App\Filament\Resources\Contracts;
|
|||||||
use App\Filament\Resources\Contracts\Pages;
|
use App\Filament\Resources\Contracts\Pages;
|
||||||
use App\Models\Contract;
|
use App\Models\Contract;
|
||||||
use App\Enums\NavigationGroup;
|
use App\Enums\NavigationGroup;
|
||||||
use App\Services\ContractScheduleService;
|
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager;
|
use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager;
|
||||||
use App\Filament\Resources\Contracts\Schemas\ContractForm;
|
use App\Filament\Resources\Contracts\Schemas\ContractForm;
|
||||||
|
use App\Filament\Resources\Contracts\Tables\ContractsTable;
|
||||||
|
|
||||||
class ContractResource extends Resource
|
class ContractResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -30,33 +29,7 @@ class ContractResource extends Resource
|
|||||||
|
|
||||||
public static function table(Table $table): Table
|
public static function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return ContractsTable::configure($table);
|
||||||
->columns([
|
|
||||||
Tables\Columns\TextColumn::make('contract_number')->label('Số HĐ')->searchable(),
|
|
||||||
Tables\Columns\TextColumn::make('product.code')->label('Sản phẩm'),
|
|
||||||
Tables\Columns\TextColumn::make('total_value')->label('Giá trị')->money('VND'),
|
|
||||||
Tables\Columns\TextColumn::make('paid_amount')->label('Đã thu')->money('VND'),
|
|
||||||
Tables\Columns\TextColumn::make('remaining_amount')->label('Còn lại')->money('VND'),
|
|
||||||
])
|
|
||||||
->actions([
|
|
||||||
Tables\Actions\EditAction::make(),
|
|
||||||
Tables\Actions\Action::make('generateSchedule')
|
|
||||||
->label('Tạo lịch TT')
|
|
||||||
->icon('heroicon-o-calendar-days')
|
|
||||||
->color('warning')
|
|
||||||
->requiresConfirmation()
|
|
||||||
->modalHeading('Tạo lịch thanh toán')
|
|
||||||
->modalDescription('Hành động này sẽ xóa lịch thanh toán cũ (nếu có) và tạo lại từ mẫu của dự án.')
|
|
||||||
->action(function (Contract $record) {
|
|
||||||
try {
|
|
||||||
ContractScheduleService::generateFromTemplate($record);
|
|
||||||
} catch (\InvalidArgumentException $e) {
|
|
||||||
// Filament sẽ tự động hiển thị lỗi nếu throw ra trong action
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
->visible(fn (Contract $record) => $record->signing_date !== null),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getRelations(): array { return [ScheduleItemsRelationManager::class]; }
|
public static function getRelations(): array { return [ScheduleItemsRelationManager::class]; }
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ class CreateContract extends CreateRecord
|
|||||||
protected function afterCreate(): void
|
protected function afterCreate(): void
|
||||||
{
|
{
|
||||||
$contract = $this->record;
|
$contract = $this->record;
|
||||||
$templateId = $this->data['payment_template_id'] ?? null;
|
|
||||||
|
|
||||||
if ($templateId) {
|
if ($contract->payment_template_id) {
|
||||||
$template = \App\Models\PaymentTemplate::find($templateId);
|
$template = $contract->paymentTemplate;
|
||||||
if ($template) {
|
if ($template) {
|
||||||
ContractScheduleService::generateFromTemplate($contract, $template);
|
ContractScheduleService::generateFromTemplate($contract, $template);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,24 @@ class ContractForm
|
|||||||
KeyValue::make('discount_details')
|
KeyValue::make('discount_details')
|
||||||
->label('Bảng chi tiết chiết khấu (Dạng Key-Value)')
|
->label('Bảng chi tiết chiết khấu (Dạng Key-Value)')
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
|
Placeholder::make('final_value_display')
|
||||||
|
->label('Giá trị sau chiết khấu')
|
||||||
|
->columnSpanFull()
|
||||||
|
->content(function ($record, $get) {
|
||||||
|
$totalValue = $record ? (float) $record->total_value : (float) ($get('total_value') ?? 0);
|
||||||
|
$discountDetails = $record ? $record->discount_details : ($get('discount_details') ?? []);
|
||||||
|
|
||||||
|
if ($totalValue <= 0) {
|
||||||
|
return new HtmlString("<div style='font-size: 0.9rem; color: #9ca3af;'>Chưa có giá trị hợp đồng để tính chiết khấu.</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = \App\Services\DiscountEngine::calculate($totalValue, $discountDetails);
|
||||||
|
$final = number_format($result['final_value']);
|
||||||
|
$discount = number_format($result['discount_amount']);
|
||||||
|
|
||||||
|
return new HtmlString("<div style='font-size: 1.1rem; font-weight: bold; color: #16a34a;'>{$final} VNĐ</div><div style='font-size: 0.8rem; color: #9ca3af;'>Đã chiết khấu: {$discount} VNĐ</div>");
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
Section::make('Thông tin quản lý & Khách hàng')
|
Section::make('Thông tin quản lý & Khách hàng')
|
||||||
@@ -166,8 +184,8 @@ class ContractForm
|
|||||||
->placeholder('Chọn mẫu để tự động tạo lịch trình...')
|
->placeholder('Chọn mẫu để tự động tạo lịch trình...')
|
||||||
->options(PaymentTemplate::pluck('name', 'id'))
|
->options(PaymentTemplate::pluck('name', 'id'))
|
||||||
->searchable()
|
->searchable()
|
||||||
->dehydrated(false)
|
->hiddenOn('edit')
|
||||||
->helperText('Lưu ý: Chỉ chọn nếu bạn muốn khởi tạo lại lịch trình thanh toán.'),
|
->helperText('Hệ thống sẽ tự động tạo lịch thanh toán sau khi lưu hợp đồng.'),
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\Contracts\Tables;
|
namespace App\Filament\Resources\Contracts\Tables;
|
||||||
|
|
||||||
|
use App\Models\Contract;
|
||||||
|
use App\Services\ContractScheduleService;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\BulkActionGroup;
|
use Filament\Actions\BulkActionGroup;
|
||||||
use Filament\Actions\DeleteBulkAction;
|
use Filament\Actions\DeleteBulkAction;
|
||||||
use Filament\Actions\EditAction;
|
use Filament\Actions\EditAction;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
use Filament\Tables\Columns\TextColumn;
|
||||||
use Filament\Tables\Columns\BadgeColumn;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
class ContractsTable
|
class ContractsTable
|
||||||
@@ -55,6 +57,19 @@ class ContractsTable
|
|||||||
'Đã hủy' => 'danger',
|
'Đã hủy' => 'danger',
|
||||||
default => 'gray',
|
default => 'gray',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
TextColumn::make('paid_amount')
|
||||||
|
->label('Đã thu')
|
||||||
|
->money('VND')
|
||||||
|
->sortable()
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('remaining_amount')
|
||||||
|
->label('Còn lại')
|
||||||
|
->money('VND')
|
||||||
|
->sortable()
|
||||||
|
->color('danger')
|
||||||
|
->toggleable(),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
\Filament\Tables\Filters\SelectFilter::make('status')
|
\Filament\Tables\Filters\SelectFilter::make('status')
|
||||||
@@ -73,6 +88,17 @@ class ContractsTable
|
|||||||
])
|
])
|
||||||
->recordActions([
|
->recordActions([
|
||||||
EditAction::make(),
|
EditAction::make(),
|
||||||
|
Action::make('generateSchedule')
|
||||||
|
->label('Tạo lịch TT')
|
||||||
|
->icon('heroicon-o-calendar-days')
|
||||||
|
->color('warning')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading('Tạo lịch thanh toán')
|
||||||
|
->modalDescription('Hành động này sẽ xóa lịch thanh toán cũ (nếu có) và tạo lại từ mẫu của dự án.')
|
||||||
|
->action(function (Contract $record) {
|
||||||
|
ContractScheduleService::generateFromTemplate($record);
|
||||||
|
})
|
||||||
|
->visible(fn (Contract $record) => $record->signing_date !== null),
|
||||||
])
|
])
|
||||||
->bulkActions([
|
->bulkActions([
|
||||||
BulkActionGroup::make([
|
BulkActionGroup::make([
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentFines\PaymentFineResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreatePaymentFine extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentFineResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentFines\PaymentFineResource;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditPaymentFine extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentFineResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentFines\PaymentFineResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListPaymentFines extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentFineResource::class;
|
||||||
|
}
|
||||||
42
app/Filament/Resources/PaymentFines/PaymentFineResource.php
Normal file
42
app/Filament/Resources/PaymentFines/PaymentFineResource.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentFines\Pages;
|
||||||
|
use App\Models\PaymentFine;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use App\Filament\Resources\PaymentFines\Schemas\PaymentFineForm;
|
||||||
|
use App\Filament\Resources\PaymentFines\Tables\PaymentFinesTable;
|
||||||
|
|
||||||
|
class PaymentFineResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = PaymentFine::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-exclamation-triangle';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->value;
|
||||||
|
protected static ?int $navigationSort = 6;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Tiền phạt';
|
||||||
|
protected static ?string $pluralModelLabel = 'Tiền phạt';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return PaymentFineForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return PaymentFinesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListPaymentFines::route('/'),
|
||||||
|
'create' => Pages\CreatePaymentFine::route('/create'),
|
||||||
|
'edit' => Pages\EditPaymentFine::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class PaymentFineForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
Section::make('Thông tin tiền phạt')
|
||||||
|
->columnSpan(2)
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
Select::make('contract_id')
|
||||||
|
->label('Hợp đồng')
|
||||||
|
->relationship('contract', 'contract_number')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('amount')
|
||||||
|
->label('Số tiền phạt')
|
||||||
|
->numeric()
|
||||||
|
->prefix('VND')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('reason')
|
||||||
|
->label('Lý do phạt')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
DatePicker::make('due_date')
|
||||||
|
->label('Ngày đến hạn nộp phạt')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
DatePicker::make('paid_date')
|
||||||
|
->label('Ngày thực nộp')
|
||||||
|
->nullable(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentFines\Tables;
|
||||||
|
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class PaymentFinesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('contract.contract_number')
|
||||||
|
->label('Hợp đồng')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('amount')
|
||||||
|
->label('Số tiền phạt')
|
||||||
|
->money('VND')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('reason')
|
||||||
|
->label('Lý do')
|
||||||
|
->searchable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('due_date')
|
||||||
|
->label('Hạn nộp')
|
||||||
|
->date('d/m/Y')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('paid_date')
|
||||||
|
->label('Ngày nộp')
|
||||||
|
->date('d/m/Y')
|
||||||
|
->placeholder('Chưa nộp')
|
||||||
|
->color(fn ($state) => $state ? 'success' : 'danger'),
|
||||||
|
])
|
||||||
|
->filters([
|
||||||
|
Tables\Filters\Filter::make('unpaid')
|
||||||
|
->label('Chưa nộp')
|
||||||
|
->query(fn ($query) => $query->whereNull('paid_date')),
|
||||||
|
])
|
||||||
|
->defaultSort('due_date', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,12 @@ class PaymentResource extends Resource
|
|||||||
return PaymentsTable::configure($table);
|
return PaymentsTable::configure($table);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getEloquentQuery(): \Illuminate\Database\Eloquent\Builder
|
||||||
|
{
|
||||||
|
return parent::getEloquentQuery()
|
||||||
|
->with(['scheduleItem.payments']);
|
||||||
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -62,7 +62,74 @@ class PaymentForm
|
|||||||
->label('Số tiền thu')
|
->label('Số tiền thu')
|
||||||
->numeric()
|
->numeric()
|
||||||
->prefix('VND')
|
->prefix('VND')
|
||||||
->required(),
|
->required()
|
||||||
|
->live(onBlur: true)
|
||||||
|
->helperText(function ($component) {
|
||||||
|
$data = $component->getContainer()->getRawState();
|
||||||
|
$contractId = $data['contract_id'] ?? null;
|
||||||
|
$scheduleItemId = $data['schedule_item_id'] ?? null;
|
||||||
|
|
||||||
|
if (! $contractId) {
|
||||||
|
return 'Vui lòng chọn hợp đồng trước.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$contract = \App\Models\Contract::find($contractId);
|
||||||
|
if (! $contract) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scheduleItemId) {
|
||||||
|
$item = PaymentScheduleItem::find($scheduleItemId);
|
||||||
|
if ($item) {
|
||||||
|
$paid = $contract->payments()
|
||||||
|
->where('schedule_item_id', $scheduleItemId)
|
||||||
|
->when($component->getRecord() instanceof \App\Models\Payment, fn ($q, $r) => $q->where('id', '!=', $r->id))
|
||||||
|
->sum('amount');
|
||||||
|
$remaining = (float) $item->amount - (float) $paid;
|
||||||
|
|
||||||
|
return 'Công nợ đợt này: '.number_format($remaining).' VNĐ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Công nợ HĐ còn lại: '.number_format($contract->remaining_amount).' VNĐ';
|
||||||
|
})
|
||||||
|
->rules([
|
||||||
|
function ($component) {
|
||||||
|
return function (string $attribute, $value, \Closure $fail) use ($component) {
|
||||||
|
$data = $component->getContainer()->getRawState();
|
||||||
|
$contractId = $data['contract_id'] ?? null;
|
||||||
|
$scheduleItemId = $data['schedule_item_id'] ?? null;
|
||||||
|
|
||||||
|
if (! $contractId || ! is_numeric($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contract = \App\Models\Contract::find($contractId);
|
||||||
|
if (! $contract) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$maxAmount = null;
|
||||||
|
|
||||||
|
if ($scheduleItemId) {
|
||||||
|
$item = PaymentScheduleItem::find($scheduleItemId);
|
||||||
|
if ($item) {
|
||||||
|
$paid = $contract->payments()
|
||||||
|
->where('schedule_item_id', $scheduleItemId)
|
||||||
|
->when($component->getRecord() instanceof \App\Models\Payment, fn ($q, $r) => $q->where('id', '!=', $r->id))
|
||||||
|
->sum('amount');
|
||||||
|
$maxAmount = (float) $item->amount - (float) $paid;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$maxAmount = (float) $contract->remaining_amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($maxAmount !== null && (float) $value > $maxAmount) {
|
||||||
|
$fail('Số tiền thu không được vượt quá '.number_format($maxAmount).' VNĐ.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
|
||||||
DatePicker::make('paid_date')
|
DatePicker::make('paid_date')
|
||||||
->label('Ngày thu')
|
->label('Ngày thu')
|
||||||
|
|||||||
@@ -29,9 +29,50 @@ class PaymentsTable
|
|||||||
Tables\Columns\TextColumn::make('receipt_number')
|
Tables\Columns\TextColumn::make('receipt_number')
|
||||||
->label('Số phiếu thu')
|
->label('Số phiếu thu')
|
||||||
->searchable(),
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('scheduleItem.type')
|
||||||
|
->label('Loại đợt')
|
||||||
|
->placeholder('Tạm ứng')
|
||||||
|
->formatStateUsing(fn ($state) => $state?->getLabel()),
|
||||||
|
|
||||||
Tables\Columns\TextColumn::make('scheduleItem.installment_no')
|
Tables\Columns\TextColumn::make('scheduleItem.installment_no')
|
||||||
->label('Đợt TT')
|
->label('Đợt TT')
|
||||||
->placeholder('Tạm ứng'),
|
->placeholder('Tạm ứng'),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('reconciliation_status')
|
||||||
|
->label('Đối soát')
|
||||||
|
->badge()
|
||||||
|
->color(function ($record) {
|
||||||
|
if (! $record->scheduleItem) {
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
$remaining = (float) $record->scheduleItem->remaining_amount;
|
||||||
|
if ($remaining == 0) {
|
||||||
|
return 'success';
|
||||||
|
}
|
||||||
|
if ($remaining > 0) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
return 'danger';
|
||||||
|
})
|
||||||
|
->state(function ($record) {
|
||||||
|
if (! $record->scheduleItem) {
|
||||||
|
return 'Tạm ứng';
|
||||||
|
}
|
||||||
|
$remaining = (float) $record->scheduleItem->remaining_amount;
|
||||||
|
if ($remaining == 0) {
|
||||||
|
return 'Đủ';
|
||||||
|
}
|
||||||
|
if ($remaining > 0) {
|
||||||
|
return 'Thiếu';
|
||||||
|
}
|
||||||
|
return 'Thừa';
|
||||||
|
}),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('scheduleItem.remaining_amount')
|
||||||
|
->label('Còn thiếu')
|
||||||
|
->money('VND')
|
||||||
|
->placeholder('-')
|
||||||
|
->color('danger'),
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
Tables\Filters\SelectFilter::make('method')
|
Tables\Filters\SelectFilter::make('method')
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Settlements\SettlementResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateSettlement extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = SettlementResource::class;
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Settlements/Pages/EditSettlement.php
Normal file
11
app/Filament/Resources/Settlements/Pages/EditSettlement.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Settlements\SettlementResource;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditSettlement extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = SettlementResource::class;
|
||||||
|
}
|
||||||
11
app/Filament/Resources/Settlements/Pages/ListSettlements.php
Normal file
11
app/Filament/Resources/Settlements/Pages/ListSettlements.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Settlements\SettlementResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListSettlements extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = SettlementResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class SettlementForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
Grid::make(3)
|
||||||
|
->schema([
|
||||||
|
Section::make('Thông tin quyết toán')
|
||||||
|
->columnSpan(2)
|
||||||
|
->columns(2)
|
||||||
|
->schema([
|
||||||
|
Select::make('product_id')
|
||||||
|
->label('Sản phẩm')
|
||||||
|
->relationship('product', 'code')
|
||||||
|
->searchable()
|
||||||
|
->preload()
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('type')
|
||||||
|
->label('Loại quyết toán')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('temp_value')
|
||||||
|
->label('Giá trị tạm tính')
|
||||||
|
->numeric()
|
||||||
|
->prefix('VND')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('final_value')
|
||||||
|
->label('Giá trị chốt')
|
||||||
|
->numeric()
|
||||||
|
->prefix('VND')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('difference')
|
||||||
|
->label('Chênh lệch')
|
||||||
|
->numeric()
|
||||||
|
->prefix('VND')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
TextInput::make('red_book_status')
|
||||||
|
->label('Trạng thái sổ đỏ')
|
||||||
|
->required(),
|
||||||
|
|
||||||
|
DatePicker::make('issue_date')
|
||||||
|
->label('Ngày cấp sổ')
|
||||||
|
->nullable(),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Filament/Resources/Settlements/SettlementResource.php
Normal file
42
app/Filament/Resources/Settlements/SettlementResource.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Settlements\Pages;
|
||||||
|
use App\Models\Settlement;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use App\Filament\Resources\Settlements\Schemas\SettlementForm;
|
||||||
|
use App\Filament\Resources\Settlements\Tables\SettlementsTable;
|
||||||
|
|
||||||
|
class SettlementResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = Settlement::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-clipboard-document-check';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value;
|
||||||
|
protected static ?int $navigationSort = 5;
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Quyết toán';
|
||||||
|
protected static ?string $pluralModelLabel = 'Quyết toán & Sổ đỏ';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return SettlementForm::configure($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return SettlementsTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListSettlements::route('/'),
|
||||||
|
'create' => Pages\CreateSettlement::route('/create'),
|
||||||
|
'edit' => Pages\EditSettlement::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Settlements\Tables;
|
||||||
|
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class SettlementsTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('product.code')
|
||||||
|
->label('Sản phẩm')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('type')
|
||||||
|
->label('Loại QT')
|
||||||
|
->badge(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('temp_value')
|
||||||
|
->label('Tạm tính')
|
||||||
|
->money('VND')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('final_value')
|
||||||
|
->label('Chốt')
|
||||||
|
->money('VND')
|
||||||
|
->sortable(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('difference')
|
||||||
|
->label('Chênh lệch')
|
||||||
|
->money('VND')
|
||||||
|
->color(fn ($state) => (float) $state > 0 ? 'danger' : 'success'),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('red_book_status')
|
||||||
|
->label('Trạng thái sổ')
|
||||||
|
->badge(),
|
||||||
|
|
||||||
|
Tables\Columns\TextColumn::make('issue_date')
|
||||||
|
->label('Ngày cấp')
|
||||||
|
->date('d/m/Y')
|
||||||
|
->placeholder('Chưa cấp'),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Filament/Widgets/ContractStatsOverview.php
Normal file
46
app/Filament/Widgets/ContractStatsOverview.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Widgets;
|
||||||
|
|
||||||
|
use App\Models\Contract;
|
||||||
|
use App\Models\PaymentScheduleItem;
|
||||||
|
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
|
||||||
|
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||||
|
|
||||||
|
class ContractStatsOverview extends BaseWidget
|
||||||
|
{
|
||||||
|
protected function getStats(): array
|
||||||
|
{
|
||||||
|
$totalRevenue = (float) Contract::sum('total_value');
|
||||||
|
$totalPaid = (float) Contract::sum('paid_amount');
|
||||||
|
$totalRemaining = (float) Contract::sum('remaining_amount');
|
||||||
|
$activeContracts = Contract::where('status', 'Đang hiệu lực')->count();
|
||||||
|
$upcomingPayments = PaymentScheduleItem::whereNull('schedule_id')
|
||||||
|
->orWhereHas('schedule', fn ($q) => $q->whereHas('contract'))
|
||||||
|
->whereDate('due_date', '<=', now()->addDays(30))
|
||||||
|
->whereDate('due_date', '>=', now())
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return [
|
||||||
|
Stat::make('Tổng doanh thu', number_format($totalRevenue) . ' VNĐ')
|
||||||
|
->description('Tổng giá trị tất cả HĐ')
|
||||||
|
->color('primary'),
|
||||||
|
|
||||||
|
Stat::make('Đã thu', number_format($totalPaid) . ' VNĐ')
|
||||||
|
->description('Tổng tiền đã thanh toán')
|
||||||
|
->color('success'),
|
||||||
|
|
||||||
|
Stat::make('Công nợ phải thu', number_format($totalRemaining) . ' VNĐ')
|
||||||
|
->description('Tổng tiền chưa thu')
|
||||||
|
->color('danger'),
|
||||||
|
|
||||||
|
Stat::make('HĐ hiệu lực', $activeContracts)
|
||||||
|
->description('Số hợp đồng đang hiệu lực')
|
||||||
|
->color('warning'),
|
||||||
|
|
||||||
|
Stat::make('Đợt TT sắp đến hạn', $upcomingPayments)
|
||||||
|
->description('Trong 30 ngày tới')
|
||||||
|
->color('info'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Filament/Widgets/UpcomingPaymentsTable.php
Normal file
47
app/Filament/Widgets/UpcomingPaymentsTable.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Widgets;
|
||||||
|
|
||||||
|
use App\Models\PaymentScheduleItem;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Widgets\TableWidget as BaseWidget;
|
||||||
|
|
||||||
|
class UpcomingPaymentsTable extends BaseWidget
|
||||||
|
{
|
||||||
|
protected int | string | array $columnSpan = 'full';
|
||||||
|
|
||||||
|
public function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->query(
|
||||||
|
PaymentScheduleItem::query()
|
||||||
|
->whereHas('schedule.contract')
|
||||||
|
->whereDate('due_date', '>=', now())
|
||||||
|
->whereDate('due_date', '<=', now()->addDays(30))
|
||||||
|
->orderBy('due_date')
|
||||||
|
)
|
||||||
|
->columns([
|
||||||
|
Tables\Columns\TextColumn::make('schedule.contract.contract_number')
|
||||||
|
->label('Số HĐ')
|
||||||
|
->searchable(),
|
||||||
|
Tables\Columns\TextColumn::make('installment_no')
|
||||||
|
->label('Đợt')
|
||||||
|
->alignCenter(),
|
||||||
|
Tables\Columns\TextColumn::make('type')
|
||||||
|
->label('Loại')
|
||||||
|
->badge(),
|
||||||
|
Tables\Columns\TextColumn::make('amount')
|
||||||
|
->label('Số tiền')
|
||||||
|
->money('VND'),
|
||||||
|
Tables\Columns\TextColumn::make('due_date')
|
||||||
|
->label('Ngày đến hạn')
|
||||||
|
->date('d/m/Y')
|
||||||
|
->color('danger'),
|
||||||
|
Tables\Columns\TextColumn::make('remaining_amount')
|
||||||
|
->label('Còn thiếu')
|
||||||
|
->money('VND'),
|
||||||
|
])
|
||||||
|
->paginated([10, 25, 50]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,11 @@ class Contract extends Model
|
|||||||
return $this->belongsTo(Product::class);
|
return $this->belongsTo(Product::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function paymentTemplate()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PaymentTemplate::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function customers()
|
public function customers()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(Customer::class, 'contract_customers')
|
return $this->belongsToMany(Customer::class, 'contract_customers')
|
||||||
@@ -73,6 +78,19 @@ class Contract extends Model
|
|||||||
return $this->hasMany(PaymentFine::class);
|
return $this->hasMany(PaymentFine::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Giá trị sau chiết khấu.
|
||||||
|
*/
|
||||||
|
public function getFinalValueAttribute(): float
|
||||||
|
{
|
||||||
|
$result = \App\Services\DiscountEngine::calculate(
|
||||||
|
(float) $this->total_value,
|
||||||
|
$this->discount_details
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result['final_value'];
|
||||||
|
}
|
||||||
|
|
||||||
protected static function booted()
|
protected static function booted()
|
||||||
{
|
{
|
||||||
static::saving(function ($contract) {
|
static::saving(function ($contract) {
|
||||||
|
|||||||
@@ -20,6 +20,21 @@ class PaymentScheduleItem extends Model
|
|||||||
'due_date' => 'date',
|
'due_date' => 'date',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function getPaidAmountAttribute(): float
|
||||||
|
{
|
||||||
|
// Nếu đã eager load payments, dùng collection sum để tránh query thêm
|
||||||
|
if ($this->relationLoaded('payments')) {
|
||||||
|
return (float) $this->payments->sum('amount');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) $this->payments()->sum('amount');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRemainingAmountAttribute(): float
|
||||||
|
{
|
||||||
|
return (float) $this->amount - $this->paid_amount;
|
||||||
|
}
|
||||||
|
|
||||||
public function template()
|
public function template()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(PaymentTemplate::class);
|
return $this->belongsTo(PaymentTemplate::class);
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ use Database\Factories\UserFactory;
|
|||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Filament\Models\Contracts\FilamentUser;
|
||||||
|
use Filament\Panel;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
#[Fillable(['name', 'email', 'password'])]
|
#[Fillable(['name', 'email', 'password'])]
|
||||||
#[Hidden(['password', 'remember_token'])]
|
#[Hidden(['password', 'remember_token'])]
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable implements FilamentUser
|
||||||
{
|
{
|
||||||
/** @use HasFactory<UserFactory> */
|
/** @use HasFactory<UserFactory> */
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable;
|
||||||
@@ -29,4 +31,9 @@ class User extends Authenticatable
|
|||||||
'password' => 'hashed',
|
'password' => 'hashed',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function canAccessPanel(Panel $panel): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use Filament\Pages\Dashboard;
|
|||||||
use Filament\Panel;
|
use Filament\Panel;
|
||||||
use Filament\PanelProvider;
|
use Filament\PanelProvider;
|
||||||
use Filament\Support\Colors\Color;
|
use Filament\Support\Colors\Color;
|
||||||
|
use App\Filament\Widgets\ContractStatsOverview;
|
||||||
|
use App\Filament\Widgets\UpcomingPaymentsTable;
|
||||||
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;
|
||||||
@@ -39,6 +41,8 @@ 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([
|
||||||
|
ContractStatsOverview::class,
|
||||||
|
UpcomingPaymentsTable::class,
|
||||||
AccountWidget::class,
|
AccountWidget::class,
|
||||||
FilamentInfoWidget::class,
|
FilamentInfoWidget::class,
|
||||||
])
|
])
|
||||||
|
|||||||
40
app/Services/DiscountEngine.php
Normal file
40
app/Services/DiscountEngine.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
class DiscountEngine
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tính tổng chiết khấu và giá trị sau chiết khấu.
|
||||||
|
*
|
||||||
|
* @param float $totalValue Giá trị gốc
|
||||||
|
* @param array|null $discountDetails Dữ liệu chiết khấu từ contract
|
||||||
|
* @return array ['discount_amount' => float, 'final_value' => float]
|
||||||
|
*/
|
||||||
|
public static function calculate(float $totalValue, ?array $discountDetails): array
|
||||||
|
{
|
||||||
|
if (empty($discountDetails)) {
|
||||||
|
return [
|
||||||
|
'discount_amount' => 0,
|
||||||
|
'final_value' => $totalValue,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$discountAmount = 0;
|
||||||
|
|
||||||
|
// Ưu tiên total_amount nếu có
|
||||||
|
if (! empty($discountDetails['total_amount'])) {
|
||||||
|
$discountAmount = (float) $discountDetails['total_amount'];
|
||||||
|
} elseif (! empty($discountDetails['total_percentage'])) {
|
||||||
|
$discountAmount = $totalValue * ((float) $discountDetails['total_percentage'] / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Đảm bảo chiết khấu không vượt quá giá trị hợp đồng
|
||||||
|
$discountAmount = min($discountAmount, $totalValue);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'discount_amount' => $discountAmount,
|
||||||
|
'final_value' => $totalValue - $discountAmount,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?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->foreignUuid('payment_template_id')->nullable()->constrained('payment_templates')->nullOnDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void {
|
||||||
|
Schema::table('contracts', function (Blueprint $table) {
|
||||||
|
$table->dropConstrainedForeignId('payment_template_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
13
tests/Feature/ContractResourceRenderTest.php
Normal file
13
tests/Feature/ContractResourceRenderTest.php
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Resources\Contracts\ContractResource;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
it('can render contracts index without error', function () {
|
||||||
|
$this->actingAs(User::factory()->create());
|
||||||
|
|
||||||
|
$response = $this->get(ContractResource::getUrl('index'));
|
||||||
|
|
||||||
|
// Chỉ kiểm tra không bị 500 error
|
||||||
|
$response->assertStatus(200);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user