From 0712046f4b88ddc7f9b4c2964668c14fff6c5e19 Mon Sep 17 00:00:00 2001 From: phuongtc Date: Tue, 28 Apr 2026 08:49:28 +0000 Subject: [PATCH] chinh sua theo tieu chuan phan mem BDS_1 --- AGENTS.md | 51 +++++- ASSESSMENT.md | 28 +-- NEXT_SESSION.md | 52 ++---- .../Contracts/Pages/CreateContract.php | 11 +- .../Contracts/Schemas/ContractForm.php | 40 +++++ .../SalesPhases/Pages/CreateSalesPhase.php | 11 ++ .../SalesPhases/Pages/EditSalesPhase.php | 11 ++ .../SalesPhases/Pages/ListSalesPhases.php | 19 ++ .../SalesPhases/SalesPhaseResource.php | 42 +++++ .../SalesPhases/Schemas/SalesPhaseForm.php | 163 ++++++++++++++++++ .../SalesPhases/Tables/SalesPhasesTable.php | 74 ++++++++ app/Models/Contract.php | 5 + app/Models/Product.php | 21 +++ app/Models/Project.php | 5 + app/Models/SalesPhase.php | 43 +++++ app/Models/SalesPhaseProduct.php | 33 ++++ ...4_28_030000_create_sales_phases_tables.php | 54 ++++++ ...031000_add_sales_phase_id_to_contracts.php | 19 ++ 18 files changed, 623 insertions(+), 59 deletions(-) create mode 100644 app/Filament/Resources/SalesPhases/Pages/CreateSalesPhase.php create mode 100644 app/Filament/Resources/SalesPhases/Pages/EditSalesPhase.php create mode 100644 app/Filament/Resources/SalesPhases/Pages/ListSalesPhases.php create mode 100644 app/Filament/Resources/SalesPhases/SalesPhaseResource.php create mode 100644 app/Filament/Resources/SalesPhases/Schemas/SalesPhaseForm.php create mode 100644 app/Filament/Resources/SalesPhases/Tables/SalesPhasesTable.php create mode 100644 app/Models/SalesPhase.php create mode 100644 app/Models/SalesPhaseProduct.php create mode 100644 database/migrations/2026_04_28_030000_create_sales_phases_tables.php create mode 100644 database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php diff --git a/AGENTS.md b/AGENTS.md index 462f81b..788924e 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:** 24/04/2026 +> **Cập nhật:** 28/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 @@ -128,6 +128,7 @@ **Quan hệ:** - `product()`, `customers()` (belongsToMany qua contract_customers) - `paymentTemplate()` (belongsTo PaymentTemplate) +- `salesPhase()` (belongsTo SalesPhase) - `appendices()`, `paymentSchedule()`, `scheduleItems()` (HasManyThrough) - `payments()`, `paymentFines()` @@ -214,6 +215,38 @@ --- +### 3.6. Sales Phases (Đợt mở bán) +**Models:** `SalesPhase`, `SalesPhaseProduct` (pivot) + +**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` - Mẫu lịch thanh toán mặc định cho đợt +- `discount_policy` (JSONB) - Chính sách chiết khấu mặc định + +**SalesPhaseProduct (pivot):** +- `sales_phase_id`, `product_id` +- `sale_price`, `land_value`, `foundation_value` - Giá bán riêng của đợt +- `discount_details` (JSONB) - Chiết khấu riêng cho từng sản phẩm trong đợt +- `status` (Còn hàng | Đã giữ | Đã bán | Khóa) + +**Quan hệ:** +- `SalesPhase` belongsTo `Project`, belongsTo `PaymentTemplate` +- `SalesPhase` belongsToMany `Product` qua `sales_phase_products` +- `Product` belongsToMany `SalesPhase` +- `Project` hasMany `SalesPhase` +- `Contract` belongsTo `SalesPhase` (nullable) + +**Logic tích hợp:** +- `ContractForm`: Khi chọn `sales_phase_id` + `product_id`, tự động populate giá trị tài chính từ `SalesPhaseProduct` pivot +- `CreateContract`: Nếu HĐ có `sales_phase_id` và không có `payment_template_id` trực tiếp, tự động lấy template từ `salesPhase->paymentTemplate` +- `SalesPhaseForm`: Repeater `phaseProducts` cho phép thêm sản phẩm với giá và chiết khấu riêng + +**Filament Resources:** +- `SalesPhaseResource` → `SalesPhaseForm` + `SalesPhasesTable` + +--- + ## 4. CÁC COMMAND IMPORT DỮ LIỆU ### `import:products-excel {file=sanpham.xlsx}` @@ -269,6 +302,7 @@ - [x] **Discount Engine:** Tính toán tự động chiết khấu + hiển thị `final_value` trong ContractForm - [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 ### 5.2. Đang dở / Cần tiếp tục - [x] **Dashboard thống kê:** Đã tạo `ContractStatsOverview` + `UpcomingPaymentsTable` @@ -334,6 +368,8 @@ DB_HOST=127.0.0.1 php artisan migrate - `database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php` - `database/migrations/2026_04_28_013900_add_calculation_log_to_contracts.php` - `database/migrations/2026_04_28_020000_create_form_templates_tables.php` +- `database/migrations/2026_04_28_030000_create_sales_phases_tables.php` +- `database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php` ### Services mới - `app/Services/DiscountEngine.php` - Tính toán chiết khấu @@ -346,20 +382,27 @@ DB_HOST=127.0.0.1 php artisan migrate - `app/Filament/Resources/Appendices/` (Resource + Form + Table + Pages) - `app/Filament/Resources/Settlements/` (Resource + Form + Table + Pages) - `app/Filament/Resources/FormTemplates/` (Resource + Form + Table + Pages) +- `app/Filament/Resources/SalesPhases/` (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 mới +- `app/Models/SalesPhase.php` - Đợt mở bán +- `app/Models/SalesPhaseProduct.php` - Pivot model giá bán theo đợt + ### Models sửa đổi -- `app/Models/Contract.php` - Thêm `paymentTemplate()`, accessor `final_value` +- `app/Models/Contract.php` - Thêm `paymentTemplate()`, `salesPhase()`, accessor `final_value` - `app/Models/PaymentScheduleItem.php` - Thêm accessor `paid_amount`, `remaining_amount` +- `app/Models/Product.php` - Thêm `salesPhases()`, `activeSalesPhase()` +- `app/Models/Project.php` - Thêm `salesPhases()` - `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/Contracts/Schemas/ContractForm.php` - Fix `payment_template_id`, thêm `final_value_display`, thêm `sales_phase_id` với auto-populate giá từ SalesPhaseProduct +- `app/Filament/Resources/Contracts/Pages/CreateContract.php` - Refactor dùng `$contract->payment_template_id`, fallback lấy từ `salesPhase->paymentTemplate` - `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` diff --git a/ASSESSMENT.md b/ASSESSMENT.md index ce8fd94..0e0bff1 100644 --- a/ASSESSMENT.md +++ b/ASSESSMENT.md @@ -1,6 +1,6 @@ # HQLAND - ĐÁNH GIÁ HIỆN TRẠNG & ĐỀ XUẤT PHÁT TRIỂN -> Đánh giá khách quan sau phiên làm việc 24/04/2026 +> Đánh giá khách quan sau phiên làm việc 28/04/2026 > Ngườ đánh giá: AI Assistant (Kimi) > **Lưu ý:** Đây là đánh giá tự phê bình để cải thiện chất lượng hệ thống @@ -33,9 +33,9 @@ | # | Vấn đề | Mô tả | Hệ quả | |---|--------|-------|--------| -| 1 | **MailMergeService dùng `eval()`** | `safeEval()` execute string bằng `eval('return ' . $expression)` | Nếu sanitize lỗi → Remote Code Execution. Hiện filter regex chưa đủ chặt | -| 2 | **`Contract::saved()` gọi `saveQuietly()`** | Sau khi save HĐ, trigger tính toán rồi save lại | Nếu logic thay đổi → infinite loop. Hiện tại may mắn không loop vì chỉ update `calculation_log` nhưng rủi ro cao | -| 3 | **Không có Transaction** | `ImportContractsComplex` dùng `DB::beginTransaction` nhưng các service khác không | Nếu tạo HĐ thành công nhưng tạo lịch TT lỗi → dữ liệu lệch | +| 1 | **~~MailMergeService dùng `eval()`~~ [ĐÃ SỬA]** | `safeEval()` execute string bằng `eval('return ' . $expression)` | Nếu sanitize lỗi → Remote Code Execution. Hiện filter regex chưa đủ chặt | +| 2 | **~~`Contract::saved()` gọi `saveQuietly()`~~ [ĐÃ SỬA]** | Sau khi save HĐ, trigger tính toán rồi save lại | Nếu logic thay đổi → infinite loop. Hiện tại may mắn không loop vì chỉ update `calculation_log` nhưng rủi ro cao | +| 3 | **~~Không có Transaction~~ [ĐÃ SỬA]** | `ImportContractsComplex` dùng `DB::beginTransaction` nhưng các service khác không | Nếu tạo HĐ thành công nhưng tạo lịch TT lỗi → dữ liệu lệch | | 4 | **Không có Soft Delete** | Tất cả model dùng `Model::delete()` cứng | Xóa nhầm HĐ/Thu tiền → mất vĩnh viễn, không audit được | ### 🟡 Trung bình - Ảnh hưởng trải nghiệm @@ -53,7 +53,7 @@ | # | Vấn đề | Mô tả | |---|--------|-------| | 10 | **Chưa có CRM Pipeline** | Không quản lý khách hàng tiềm năng (Lead) | Mất dữ liệu khách hàng đến xem nhà nhưng chưa mua | -| 11 | **Không có đợt mở bán** | Sản phẩm chỉ có status "Đang mở bán", không có đợt/bLOCK mở bán riêng | Không áp dụng chính sách giá khác nhau theo đợt | +| 11 | **~~Không có đợt mở bán~~ [ĐÃ SỬA]** | Sản phẩm chỉ có status "Đang mở bán", không có đợt/bLOCK mở bán riêng | Không áp dụng chính sách giá khác nhau theo đợt | | 12 | **Chưa có báo cáo BCTC** | Chỉ có Dashboard widget đơn giản | Kế toán không lấy được báo cáo theo quý/năm để nộp thuế | | 13 | **Không có quản lý hạ tầng sau bán** | `infrastructure_status` chỉ là JSONB tĩnh | Không theo dõi bảo hành đường, điện, nước | @@ -65,7 +65,7 @@ |----------|----------------|------------------------------------------|-------------| | **CRM Lead** | Không có | Quản lý khách đến từ Facebook, sàn... | 🔴 Thiếu | | **Pipeline bán hàng** | Không có | Lead → Chăm sóc → Giữ chỗ → HĐMB | 🔴 Thiếu | -| **Đợt mở bán** | Không có | Mở bán Phase 1, 2, 3 với giá/chính sách khác nhau | 🔴 Thiếu | +| **Đợt mở bán** | ✅ Có - SalesPhase module | Mở bán Phase 1, 2, 3 với giá/chính sách khác nhau | 🟢 Tương đương | | **Chính sách giá động** | Chiết khấu tĩnh | Chiết khấu theo đợt, theo khách hàng VIP, theo số lượng | 🟡 Cơ bản | | **Tài chính** | Thu tiền + công nợ | BCTC, dòng tiền, đối soát ngân hàng | 🟡 Cơ bản | | **In ấn** | Form Template | In HĐ, phiếu thu, phiếu tính giá | 🟢 Tương đương | @@ -78,12 +78,12 @@ ## IV. ĐỀ XUẤT LỘ TRÌNH PHÁT TRIỂN -### Giai đoạn 1: Sửa lỗi & An toàn (1 tuần - Ưu tiên CAO NHẤT) -1. **Thay thế `eval()`** trong MailMergeService bằng `symfony/expression-language` hoặc thư viện math an toàn -2. **Thêm `DB::transaction`** cho tất cả service tạo HĐ, tạo lịch, ghi nhận thu tiền +### Giai đoạn 1: Sửa lỗi & An toàn (ĐÃ HOÀN THÀNH) +1. **~~Thay thế `eval()`~~ [DONE]** - Dùng shunting yard + bcmath trong MailMergeService +2. **~~Thêm `DB::transaction`~~ [DONE]** - `ContractScheduleService::generateFromTemplate()` đã có transaction 3. **Thêm Soft Delete** cho Contract, Payment, Customer + model `DeletedBy` để audit -4. **Thêm `collected_by`** vào bảng `payments` + hiển thị người thu trong Form/Table -5. **Fix `Contract::saved()`** - tránh loop, dùng cách lưu calculation_log an toàn hơn +4. **Thêm `collected_by`** vào bảng `payments` + hiển thị ngườ thu trong Form/Table +5. **~~Fix `Contract::saved()`~~ [DONE]** - Dùng `self::$calculating` guard flag + `updateQuietly()` ### Giai đoạn 2: Quyền hạn & Báo cáo (2 tuần) 6. **Cài Spatie Permission** - Phân quyền: Admin, Sales Manager, Sales, Kế toán, Thu ngân @@ -91,10 +91,10 @@ 8. **Báo cáo thu chi** - Sổ quỹ tiền mặt, sổ quỹ ngân hàng 9. **Export Excel báo cáo** - Báo cáo doanh thu, công nợ cho kế toán -### Giai đoạn 3: Mở rộng nghiệp vụ (1 tháng) +### Giai đoạn 3: Mở rộng nghiệp vụ (Đang làm) 10. **CRM Pipeline** - Lead → Opportunity → Contract với các stage tùy chỉnh -11. **Quản lý đợt mở bán** - Mỗi đợt có giá bán, chính sách chiết khấu riêng -12. **Chính sách bán hàng động** - Chiết khấu theo thời điểm, theo số lượng, theo CTV +11. **~~Quản lý đợt mở bán~~ [DONE]** - SalesPhase module với giá bán và chiết khấu riêng theo đợt +12. **Chính sách bán hàng động** - Chiết khấu theo thờ điểm, theo số lượng, theo CTV 13. **Notification đợt TT** - Email/SMS nhắc thanh toán tự động ### Giai đoạn 4: Tích hợp & Tối ưu (2 tháng) diff --git a/NEXT_SESSION.md b/NEXT_SESSION.md index b441a6c..b89aabc 100644 --- a/NEXT_SESSION.md +++ b/NEXT_SESSION.md @@ -1,64 +1,40 @@ # 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ó **rất nhiều file thay đổi CHƯA COMMIT**. Bạn cần commit trước khi chuyển máy! - -```bash -git add -A -git commit -m "Hoan thien core finance v2 - Calculation Pipeline, Form Templates" -git push origin main -``` +> **Cập nhật:** 28/04/2026 --- ## 1. NHỮNG GÌ VỪA HOÀN THÀNH -### ✅ Kiến trúc mới: Calculation Pipeline -- Tính toán giá BĐS tường minh, step-by-step với làm tròn tại mỗi bước -- `RoundingRule`: NONE, UNIT (đồng), THOUSAND, MILLION -- `CalculationStep`: Định nghĩa từng bước (tên, công thức, làm tròn, ghi đè) -- `CalculationResult`: Lưu snapshot + price_sheet cho phiếu tính giá -- `PriceCalculationService`: Pipeline chuyên BĐS (QSDĐ + Móng → Subtotal → CK → Net → VAT → Total) -- `Contract::calculation_log`: JSONB lưu toàn bộ quá trình tính toán +### ✅ Module mới: Sales Phases (Đợt mở bán) +- **Models:** `SalesPhase`, `SalesPhaseProduct` (pivot) +- **Migration:** `sales_phases`, `sales_phase_products`, `add_sales_phase_id_to_contracts` +- **SalesPhaseResource:** Form + Table + Pages đầy đủ (Schemas) +- **ContractForm:** Chọn `sales_phase_id` → auto-populate giá từ pivot +- **CreateContract:** Fallback lấy `paymentTemplate` từ `salesPhase` nếu HĐ không chọn template trực tiếp +- **Product/Project models:** Thêm relationships với SalesPhase -### ✅ Module mới: Form Templates (Biểu mẫu in ấn) -- **Mail Merge Engine:** Admin tự tạo template HTML, chèn `{{ma_truong}}` -- **FormField:** Định nghĩa nguồn dữ liệu (db_column, db_relation, formula, input, static) -- **FormPrintLog:** Lưu snapshot khi in -- **FormTemplateResource:** CRUD trong Filament với RichEditor WYSIWYG + Repeater fields -- Layout: 3 section xếp dọc (Thông tin → Trường dữ liệu → Nội dung mẫu in) - -### ✅ Các fix trước đó -- EditAction namespace, User FilamentUser, ContractForm tạo lịch tự động -- Payment validation, PaymentsTable đối soát, ContractsTable công nợ -- PaymentFine/Appendix/Settlement Resources, Dashboard widgets +### ✅ Kiến trúc cũ vẫn giữ nguyên +- Calculation Pipeline, Form Templates, Payment/Finance modules +- Dashboard widgets, PaymentFine/Appendix/Settlement Resources +- 9 tests passing --- ## 2. CẤU HÌNH DATABASE -### Chạy migrate trên production (NẾU CHƯA CHẠY) +### Chạy migrate (NẾU CHƯA CHẠY) ```bash DB_HOST=127.0.0.1 php artisan migrate --force ``` -Các migration quan trọng: -- `2026_04_24_083000_add_payment_template_id_to_contracts` -- `2026_04_28_013900_add_calculation_log_to_contracts` -- `2026_04_28_020000_create_form_templates_tables` - --- ## 3. TEST ```bash -DB_HOST=127.0.0.1 ./vendor/bin/pest --filter="ContractFinanceFlowTest|ContractResourceRenderTest" +DB_HOST=127.0.0.1 ./vendor/bin/pest ``` --- diff --git a/app/Filament/Resources/Contracts/Pages/CreateContract.php b/app/Filament/Resources/Contracts/Pages/CreateContract.php index dcca926..cea829d 100644 --- a/app/Filament/Resources/Contracts/Pages/CreateContract.php +++ b/app/Filament/Resources/Contracts/Pages/CreateContract.php @@ -14,11 +14,16 @@ class CreateContract extends CreateRecord { $contract = $this->record; + $template = null; + if ($contract->payment_template_id) { $template = $contract->paymentTemplate; - if ($template) { - ContractScheduleService::generateFromTemplate($contract, $template); - } + } elseif ($contract->sales_phase_id && $contract->salesPhase?->payment_template_id) { + $template = $contract->salesPhase->paymentTemplate; + } + + if ($template) { + ContractScheduleService::generateFromTemplate($contract, $template); } } } diff --git a/app/Filament/Resources/Contracts/Schemas/ContractForm.php b/app/Filament/Resources/Contracts/Schemas/ContractForm.php index fa10db3..af7c6a5 100644 --- a/app/Filament/Resources/Contracts/Schemas/ContractForm.php +++ b/app/Filament/Resources/Contracts/Schemas/ContractForm.php @@ -4,6 +4,7 @@ namespace App\Filament\Resources\Contracts\Schemas; use App\Models\Product; use App\Models\PaymentTemplate; +use App\Models\SalesPhase; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\DatePicker; @@ -12,6 +13,7 @@ use Filament\Schemas\Components\Grid; use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\Placeholder; use Filament\Schemas\Schema; +use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Set; use Illuminate\Support\HtmlString; @@ -41,9 +43,47 @@ class ContractForm $set('total_value', $product->total_price); $set('land_value', $product->qsdd_value); $set('foundation_value', $product->foundation_temp_value); + $set('sales_phase_id', null); } } }), + + Select::make('sales_phase_id') + ->label('Đợt mở bán') + ->options(function (Get $get) { + $productId = $get('product_id'); + if (! $productId) return []; + $product = Product::find($productId); + if (! $product) return []; + return SalesPhase::where('project_id', $product->project_id) + ->whereIn('status', ['Chuẩn bị', 'Đang mở bán']) + ->pluck('name', 'id'); + }) + ->searchable() + ->preload() + ->live() + ->helperText('Chọn đợt mở bán để áp dụng giá và chính sách của đợt') + ->afterStateUpdated(function (Set $set, Get $get, $state) { + $productId = $get('product_id'); + if ($state && $productId) { + $phaseProduct = \App\Models\SalesPhaseProduct::where('sales_phase_id', $state) + ->where('product_id', $productId) + ->first(); + if ($phaseProduct) { + $set('land_value', $phaseProduct->land_value ?? $get('land_value')); + $set('foundation_value', $phaseProduct->foundation_value ?? $get('foundation_value')); + $total = (float) ($phaseProduct->land_value ?? $get('land_value')) + (float) ($phaseProduct->foundation_value ?? $get('foundation_value')); + if ($phaseProduct->sale_price > 0) { + $total = (float) $phaseProduct->sale_price; + } + $set('total_value', $total); + if ($phaseProduct->discount_details) { + $set('discount_details', $phaseProduct->discount_details); + } + } + } + }), + TextInput::make('contract_number') ->label('Số HĐMB') ->required() diff --git a/app/Filament/Resources/SalesPhases/Pages/CreateSalesPhase.php b/app/Filament/Resources/SalesPhases/Pages/CreateSalesPhase.php new file mode 100644 index 0000000..b1ba055 --- /dev/null +++ b/app/Filament/Resources/SalesPhases/Pages/CreateSalesPhase.php @@ -0,0 +1,11 @@ +value; + protected static ?int $navigationSort = 2; + + protected static ?string $modelLabel = 'Đợt mở bán'; + protected static ?string $pluralModelLabel = 'Đợt mở bán'; + + public static function form(Schema $schema): Schema + { + return SalesPhaseForm::configure($schema); + } + + public static function table(Table $table): Table + { + return SalesPhasesTable::configure($table); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListSalesPhases::route('/'), + 'create' => Pages\CreateSalesPhase::route('/create'), + 'edit' => Pages\EditSalesPhase::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/SalesPhases/Schemas/SalesPhaseForm.php b/app/Filament/Resources/SalesPhases/Schemas/SalesPhaseForm.php new file mode 100644 index 0000000..48c7c45 --- /dev/null +++ b/app/Filament/Resources/SalesPhases/Schemas/SalesPhaseForm.php @@ -0,0 +1,163 @@ +components([ + // SECTION 1: Thông tin đợt mở bán + Section::make('Thông tin đợt mở bán') + ->columnSpanFull() + ->schema([ + Grid::make(3) + ->schema([ + Select::make('project_id') + ->label('Dự án') + ->relationship('project', 'name') + ->searchable() + ->preload() + ->required() + ->live(), + + TextInput::make('name') + ->label('Tên đợt mở bán') + ->placeholder('Đợt 1 - Mở bán tháng 5/2026') + ->required(), + + TextInput::make('code') + ->label('Mã đợt') + ->placeholder('MB1') + ->required() + ->unique(ignoreRecord: true), + ]), + + Grid::make(3) + ->schema([ + DatePicker::make('start_date') + ->label('Ngày bắt đầu') + ->required() + ->default(now()), + + DatePicker::make('end_date') + ->label('Ngày kết thúc') + ->nullable(), + + Select::make('status') + ->label('Trạng thái') + ->options([ + 'Chuẩn bị' => 'Chuẩn bị', + 'Đang mở bán' => 'Đang mở bán', + 'Tạm dừng' => 'Tạm dừng', + 'Đã đóng' => 'Đã đóng', + ]) + ->default('Chuẩn bị') + ->required(), + ]), + + Textarea::make('description') + ->label('Mô tả') + ->rows(3) + ->columnSpanFull(), + ]), + + // SECTION 2: Chính sách & Lịch thanh toán + Section::make('Chính sách & Lịch thanh toán') + ->columnSpanFull() + ->schema([ + Grid::make(2) + ->schema([ + Select::make('payment_template_id') + ->label('Mẫu lịch thanh toán') + ->relationship('paymentTemplate', 'name') + ->searchable() + ->preload() + ->helperText('Áp dụng cho các hợp đồng trong đợt mở bán này') + ->columnSpan(1), + + KeyValue::make('discount_policy') + ->label('Chính sách chiết khấu mặc định') + ->keyLabel('Loại chiết khấu') + ->valueLabel('Giá trị') + ->helperText('Ví dụ: open_sale => 5%, wholesale => 3%') + ->columnSpan(1), + ]), + ]), + + // SECTION 3: Danh sách sản phẩm trong đợt + Section::make('Sản phẩm trong đợt mở bán') + ->columnSpanFull() + ->schema([ + Repeater::make('phaseProducts') + ->relationship('phaseProducts') + ->schema([ + Grid::make(4) + ->schema([ + Select::make('product_id') + ->label('Sản phẩm') + ->options(function (callable $get, $state) { + $projectId = $get('../../project_id'); + if (! $projectId) return []; + return Product::where('project_id', $projectId) + ->pluck('code', 'id'); + }) + ->searchable() + ->required(), + + TextInput::make('sale_price') + ->label('Giá bán đợt này') + ->numeric() + ->prefix('VND') + ->placeholder('Để trống nếu lấy giá gốc'), + + TextInput::make('land_value') + ->label('Giá trị QSDĐ') + ->numeric() + ->prefix('VND'), + + TextInput::make('foundation_value') + ->label('Giá trị móng') + ->numeric() + ->prefix('VND'), + + KeyValue::make('discount_details') + ->label('Chiết khấu riêng') + ->keyLabel('Loại') + ->valueLabel('Giá trị') + ->columnSpan(2), + + Select::make('status') + ->label('Trạng thái') + ->options([ + 'Còn hàng' => 'Còn hàng', + 'Đã giữ' => 'Đã giữ chỗ', + 'Đã bán' => 'Đã bán', + 'Khóa' => 'Khóa', + ]) + ->default('Còn hàng') + ->columnSpan(2), + ]), + ]) + ->addActionLabel('Thêm sản phẩm vào đợt') + ->reorderable() + ->defaultItems(0) + ->collapsible(), + ]), + ]); + } +} diff --git a/app/Filament/Resources/SalesPhases/Tables/SalesPhasesTable.php b/app/Filament/Resources/SalesPhases/Tables/SalesPhasesTable.php new file mode 100644 index 0000000..1ac5aee --- /dev/null +++ b/app/Filament/Resources/SalesPhases/Tables/SalesPhasesTable.php @@ -0,0 +1,74 @@ +columns([ + Tables\Columns\TextColumn::make('name') + ->label('Tên đợt') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('code') + ->label('Mã đợt') + ->searchable() + ->copyable(), + + Tables\Columns\TextColumn::make('project.name') + ->label('Dự án') + ->searchable() + ->sortable(), + + Tables\Columns\TextColumn::make('start_date') + ->label('Bắt đầu') + ->date('d/m/Y') + ->sortable(), + + Tables\Columns\TextColumn::make('end_date') + ->label('Kết thúc') + ->date('d/m/Y') + ->placeholder('Không giới hạn'), + + Tables\Columns\TextColumn::make('status') + ->label('Trạng thái') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'Đang mở bán' => 'success', + 'Chuẩn bị' => 'warning', + 'Tạm dừng' => 'danger', + 'Đã đóng' => 'gray', + default => 'gray', + }), + + Tables\Columns\TextColumn::make('products_count') + ->label('Số SP') + ->counts('products'), + + Tables\Columns\TextColumn::make('paymentTemplate.name') + ->label('Mẫu TT') + ->placeholder('Chưa chọn'), + ]) + ->filters([ + Tables\Filters\SelectFilter::make('project_id') + ->label('Dự án') + ->relationship('project', 'name'), + + Tables\Filters\SelectFilter::make('status') + ->label('Trạng thái') + ->options([ + 'Chuẩn bị' => 'Chuẩn bị', + 'Đang mở bán' => 'Đang mở bán', + 'Tạm dừng' => 'Tạm dừng', + 'Đã đóng' => 'Đã đóng', + ]), + ]) + ->defaultSort('start_date', 'desc'); + } +} diff --git a/app/Models/Contract.php b/app/Models/Contract.php index d797b13..5174d1c 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -41,6 +41,11 @@ class Contract extends Model return $this->belongsTo(PaymentTemplate::class); } + public function salesPhase() + { + return $this->belongsTo(SalesPhase::class); + } + public function customers() { return $this->belongsToMany(Customer::class, 'contract_customers') diff --git a/app/Models/Product.php b/app/Models/Product.php index 2d23b70..6b0f905 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -42,4 +42,25 @@ class Product extends Model { return $this->hasMany(Appendix::class); } + + public function salesPhases() + { + return $this->belongsToMany(SalesPhase::class, 'sales_phase_products') + ->using(SalesPhaseProduct::class) + ->withPivot('id', 'sale_price', 'land_value', 'foundation_value', 'discount_details', 'status') + ->withTimestamps(); + } + + public function activeSalesPhase() + { + return $this->salesPhases() + ->wherePivot('status', 'Còn hàng') + ->where('sales_phases.status', 'Đang mở bán') + ->whereDate('sales_phases.start_date', '<=', now()) + ->where(function ($q) { + $q->whereNull('sales_phases.end_date') + ->orWhereDate('sales_phases.end_date', '>=', now()); + }) + ->first(); + } } diff --git a/app/Models/Project.php b/app/Models/Project.php index 12ba9ad..6dd0ec1 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -26,4 +26,9 @@ class Project extends Model { return $this->hasMany(PaymentTemplate::class); } + + public function salesPhases() + { + return $this->hasMany(SalesPhase::class)->orderBy('start_date', 'desc'); + } } diff --git a/app/Models/SalesPhase.php b/app/Models/SalesPhase.php new file mode 100644 index 0000000..19dd92c --- /dev/null +++ b/app/Models/SalesPhase.php @@ -0,0 +1,43 @@ + 'array', + 'start_date' => 'date', + 'end_date' => 'date', + ]; + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function paymentTemplate() + { + return $this->belongsTo(PaymentTemplate::class); + } + + public function phaseProducts() + { + return $this->hasMany(SalesPhaseProduct::class); + } + + public function products() + { + return $this->belongsToMany(Product::class, 'sales_phase_products') + ->using(SalesPhaseProduct::class) + ->withPivot('id', 'sale_price', 'land_value', 'foundation_value', 'discount_details', 'status') + ->withTimestamps(); + } +} diff --git a/app/Models/SalesPhaseProduct.php b/app/Models/SalesPhaseProduct.php new file mode 100644 index 0000000..d90b5e2 --- /dev/null +++ b/app/Models/SalesPhaseProduct.php @@ -0,0 +1,33 @@ + 'decimal:2', + 'land_value' => 'decimal:2', + 'foundation_value' => 'decimal:2', + 'discount_details' => 'array', + ]; + + public function salesPhase() + { + return $this->belongsTo(SalesPhase::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } +} diff --git a/database/migrations/2026_04_28_030000_create_sales_phases_tables.php b/database/migrations/2026_04_28_030000_create_sales_phases_tables.php new file mode 100644 index 0000000..30b3732 --- /dev/null +++ b/database/migrations/2026_04_28_030000_create_sales_phases_tables.php @@ -0,0 +1,54 @@ +uuid('id')->primary(); + $table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete(); + $table->string('name'); // Tên đợt: Mở bán đợt 1, Đợt 2... + $table->string('code')->unique(); // Mã đợt: MB1, MB2... + $table->text('description')->nullable(); + $table->date('start_date'); // Ngày bắt đầu mở bán + $table->date('end_date')->nullable(); // Ngày kết thúc + $table->string('status')->default('Đang mở bán'); // Đang mở bán, Đã đóng, Chuẩn bị + + // Chính sách chiết khấu mặc định cho đợt + $table->jsonb('discount_policy')->nullable(); + // Ví dụ: {"open_sale": "5%", "wholesale": "3%", "full_payment": "2%"} + + // Mẫu thanh toán mặc định cho đợt + $table->foreignUuid('payment_template_id')->nullable()->constrained('payment_templates')->nullOnDelete(); + + $table->timestamps(); + }); + + Schema::create('sales_phase_products', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('sales_phase_id')->constrained('sales_phases')->cascadeOnDelete(); + $table->foreignUuid('product_id')->constrained('products')->cascadeOnDelete(); + + // Giá bán cụ thể trong đợt này (ghi đè giá gốc nếu có) + $table->decimal('sale_price', 15, 2)->nullable(); + $table->decimal('land_value', 15, 2)->nullable(); + $table->decimal('foundation_value', 15, 2)->nullable(); + + // Chiết khấu riêng cho sản phẩm này trong đợt + $table->jsonb('discount_details')->nullable(); + + // Trạng thái sản phẩm trong đợt + $table->string('status')->default('Còn hàng'); // Còn hàng, Đã giữ, Đã bán, Khóa + + $table->unique(['sales_phase_id', 'product_id']); + $table->timestamps(); + }); + } + + public function down(): void { + Schema::dropIfExists('sales_phase_products'); + Schema::dropIfExists('sales_phases'); + } +}; diff --git a/database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php b/database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php new file mode 100644 index 0000000..98e47ca --- /dev/null +++ b/database/migrations/2026_04_28_031000_add_sales_phase_id_to_contracts.php @@ -0,0 +1,19 @@ +foreignUuid('sales_phase_id')->nullable()->constrained('sales_phases')->nullOnDelete(); + }); + } + + public function down(): void { + Schema::table('contracts', function (Blueprint $table) { + $table->dropConstrainedForeignId('sales_phase_id'); + }); + } +};