diff --git a/AGENTS.md b/AGENTS.md index a6edef6..ec67ded 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,9 +120,13 @@ **Quan hệ:** - `product()`, `customers()` (belongsToMany qua contract_customers) +- `paymentTemplate()` (belongsTo PaymentTemplate) - `appendices()`, `paymentSchedule()`, `scheduleItems()` (HasManyThrough) - `payments()`, `paymentFines()` +**Accessor:** +- `final_value`: Giá trị sau chiết khấu (tính từ `DiscountEngine`) + **Filament Resources:** - `ContractResource` → `ContractForm` + `ContractsTable` - Action "Tạo lịch TT" trong Table (gọi `ContractScheduleService`) @@ -159,8 +163,14 @@ **Filament Resources:** - `PaymentResource` → `PaymentForm` + `PaymentsTable` -- Form chọn Contract → chọn Đợt thanh toán (cascade) -- Table có filter theo phương thức và ngày thu + - 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 + - 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) - 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}` - Logic "Bắc cầu" 2 file: 1. `hopdong.xlsx`: Dữ liệu tài chính (theo Số HĐMB) @@ -204,21 +219,25 @@ - [x] PaymentResource (Form + Table) - [x] Test: ContractFinanceFlowTest (PASS) - [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 -- [ ] **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) -- [ ] **PaymentsTable:** Chưa có cột trạng thái đối soát (so sánh với schedule_item amount) -- [ ] **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 +- [x] **Dashboard thống kê:** Đã tạo `ContractStatsOverview` + `UpcomingPaymentsTable` +- [ ] **Notification:** Cảnh báo đợt thanh toán sắp đến hạn (chưa có hệ thống notification) -### 5.3. Vấn đề kỹ thuật cần xử lý -- [ ] `payment_template_id` trong ContractForm cần hook `afterCreate` hoặc đổi thành dehydrated + xử lý trong CreateContract -- [ ] PaymentsTable nên hiển thị `scheduleItem.type` và trạng thái đối soát -- [ ] ContractTable có thể thêm cột `paid_amount` / `remaining_amount` (đã có trong Resource nhưng chưa commit staged) -- [ ] Cần kiểm tra logic `updateOrCreate` trong ImportContractsComplex với nhiều khách hàng cùng 1 hợp đồng +### 5.3. Vấn đề kỹ thuật ĐÃ XỬ LÝ +- [x] ContractTable đã thêm cột `paid_amount` / `remaining_amount`, chuyển sang dùng `ContractsTable` Schemas +- [x] Logic `syncWithoutDetaching` trong ImportContractsComplex đảm bảo nhiều KH cùng 1 HĐ không bị ghi đè +- [x] Fix N+1 query ở `PaymentScheduleItem::getPaidAmountAttribute()` (dùng `relationLoaded`) +- [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) -- `HQLAND_PROJECT_BLUEPRINT.md` -- `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` +### Migrations mới +- `database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php` -### Unstaged (Đang chỉnh sửa, chưa xong) -- `.gitignore` -- `analyze_contracts.php` -- `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) +### Services mới +- `app/Services/DiscountEngine.php` - Tính toán chiết khấu +- `app/Console/Commands/GenerateContractSchedules.php` - Command tạo lịch hàng loạt -### Untracked (File mới chưa add) -- `app/Filament/Resources/Payments/` (PaymentResource, Form, Table, Pages) -- `app/Observers/PaymentObserver.php` -- `app/Services/ContractScheduleService.php` +### Filament Resources mới +- `app/Filament/Resources/PaymentFines/` (Resource + Form + Table + Pages) +- `app/Filament/Resources/Appendices/` (Resource + Form + Table + Pages) +- `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 --- diff --git a/COMMIT_GUIDE.md b/COMMIT_GUIDE.md new file mode 100644 index 0000000..0c3baee --- /dev/null +++ b/COMMIT_GUIDE.md @@ -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!* diff --git a/NEXT_SESSION.md b/NEXT_SESSION.md new file mode 100644 index 0000000..4d01055 --- /dev/null +++ b/NEXT_SESSION.md @@ -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!* diff --git a/app/Console/Commands/GenerateContractSchedules.php b/app/Console/Commands/GenerateContractSchedules.php new file mode 100644 index 0000000..9f49134 --- /dev/null +++ b/app/Console/Commands/GenerateContractSchedules.php @@ -0,0 +1,75 @@ +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; + } +} diff --git a/app/Console/Commands/ImportContractsComplex.php b/app/Console/Commands/ImportContractsComplex.php index b4d5e9f..30e723e 100644 --- a/app/Console/Commands/ImportContractsComplex.php +++ b/app/Console/Commands/ImportContractsComplex.php @@ -127,6 +127,7 @@ class ImportContractsComplex extends Command ); // 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([ $customer->id => [ 'role' => $row[7] ?? 'Chủ SH', diff --git a/app/Filament/Resources/Appendices/AppendixResource.php b/app/Filament/Resources/Appendices/AppendixResource.php new file mode 100644 index 0000000..1a09356 --- /dev/null +++ b/app/Filament/Resources/Appendices/AppendixResource.php @@ -0,0 +1,42 @@ +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'), + ]; + } +} diff --git a/app/Filament/Resources/Appendices/Pages/CreateAppendix.php b/app/Filament/Resources/Appendices/Pages/CreateAppendix.php new file mode 100644 index 0000000..fd7e2ec --- /dev/null +++ b/app/Filament/Resources/Appendices/Pages/CreateAppendix.php @@ -0,0 +1,11 @@ +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ị'), + ]), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Appendices/Tables/AppendicesTable.php b/app/Filament/Resources/Appendices/Tables/AppendicesTable.php new file mode 100644 index 0000000..06b3f73 --- /dev/null +++ b/app/Filament/Resources/Appendices/Tables/AppendicesTable.php @@ -0,0 +1,38 @@ +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'); + } +} diff --git a/app/Filament/Resources/Contracts/ContractResource.php b/app/Filament/Resources/Contracts/ContractResource.php index 636bf3a..f8773d9 100644 --- a/app/Filament/Resources/Contracts/ContractResource.php +++ b/app/Filament/Resources/Contracts/ContractResource.php @@ -5,13 +5,12 @@ namespace App\Filament\Resources\Contracts; use App\Filament\Resources\Contracts\Pages; use App\Models\Contract; use App\Enums\NavigationGroup; -use App\Services\ContractScheduleService; use Filament\Schemas\Schema; use Filament\Resources\Resource; -use Filament\Tables; use Filament\Tables\Table; use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager; use App\Filament\Resources\Contracts\Schemas\ContractForm; +use App\Filament\Resources\Contracts\Tables\ContractsTable; class ContractResource extends Resource { @@ -30,33 +29,7 @@ class ContractResource extends Resource public static function table(Table $table): Table { - return $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), - ]); + return ContractsTable::configure($table); } public static function getRelations(): array { return [ScheduleItemsRelationManager::class]; } diff --git a/app/Filament/Resources/Contracts/Pages/CreateContract.php b/app/Filament/Resources/Contracts/Pages/CreateContract.php index 9b31fec..dcca926 100644 --- a/app/Filament/Resources/Contracts/Pages/CreateContract.php +++ b/app/Filament/Resources/Contracts/Pages/CreateContract.php @@ -13,10 +13,9 @@ class CreateContract extends CreateRecord protected function afterCreate(): void { $contract = $this->record; - $templateId = $this->data['payment_template_id'] ?? null; - if ($templateId) { - $template = \App\Models\PaymentTemplate::find($templateId); + if ($contract->payment_template_id) { + $template = $contract->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 5ea849b..4d8e837 100644 --- a/app/Filament/Resources/Contracts/Schemas/ContractForm.php +++ b/app/Filament/Resources/Contracts/Schemas/ContractForm.php @@ -137,6 +137,24 @@ class ContractForm KeyValue::make('discount_details') ->label('Bảng chi tiết chiết khấu (Dạng Key-Value)') ->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("
Chưa có giá trị hợp đồng để tính chiết khấu.
"); + } + + $result = \App\Services\DiscountEngine::calculate($totalValue, $discountDetails); + $final = number_format($result['final_value']); + $discount = number_format($result['discount_amount']); + + return new HtmlString("
{$final} VNĐ
Đã chiết khấu: {$discount} VNĐ
"); + }), ]), 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...') ->options(PaymentTemplate::pluck('name', 'id')) ->searchable() - ->dehydrated(false) - ->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.'), + ->hiddenOn('edit') + ->helperText('Hệ thống sẽ tự động tạo lịch thanh toán sau khi lưu hợp đồng.'), ]), ]); } diff --git a/app/Filament/Resources/Contracts/Tables/ContractsTable.php b/app/Filament/Resources/Contracts/Tables/ContractsTable.php index ec59644..f98590c 100644 --- a/app/Filament/Resources/Contracts/Tables/ContractsTable.php +++ b/app/Filament/Resources/Contracts/Tables/ContractsTable.php @@ -2,11 +2,13 @@ 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\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Columns\BadgeColumn; use Filament\Tables\Table; class ContractsTable @@ -55,6 +57,19 @@ class ContractsTable 'Đã hủy' => 'danger', 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([ \Filament\Tables\Filters\SelectFilter::make('status') @@ -73,6 +88,17 @@ class ContractsTable ]) ->recordActions([ 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([ BulkActionGroup::make([ diff --git a/app/Filament/Resources/PaymentFines/Pages/CreatePaymentFine.php b/app/Filament/Resources/PaymentFines/Pages/CreatePaymentFine.php new file mode 100644 index 0000000..d5edb67 --- /dev/null +++ b/app/Filament/Resources/PaymentFines/Pages/CreatePaymentFine.php @@ -0,0 +1,11 @@ +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'), + ]; + } +} diff --git a/app/Filament/Resources/PaymentFines/Schemas/PaymentFineForm.php b/app/Filament/Resources/PaymentFines/Schemas/PaymentFineForm.php new file mode 100644 index 0000000..3084033 --- /dev/null +++ b/app/Filament/Resources/PaymentFines/Schemas/PaymentFineForm.php @@ -0,0 +1,52 @@ +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(), + ]), + ]), + ]); + } +} diff --git a/app/Filament/Resources/PaymentFines/Tables/PaymentFinesTable.php b/app/Filament/Resources/PaymentFines/Tables/PaymentFinesTable.php new file mode 100644 index 0000000..db3e5ef --- /dev/null +++ b/app/Filament/Resources/PaymentFines/Tables/PaymentFinesTable.php @@ -0,0 +1,46 @@ +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'); + } +} diff --git a/app/Filament/Resources/Payments/PaymentResource.php b/app/Filament/Resources/Payments/PaymentResource.php index 2d993da..05174ed 100644 --- a/app/Filament/Resources/Payments/PaymentResource.php +++ b/app/Filament/Resources/Payments/PaymentResource.php @@ -31,6 +31,12 @@ class PaymentResource extends Resource return PaymentsTable::configure($table); } + public static function getEloquentQuery(): \Illuminate\Database\Eloquent\Builder + { + return parent::getEloquentQuery() + ->with(['scheduleItem.payments']); + } + public static function getPages(): array { return [ diff --git a/app/Filament/Resources/Payments/Schemas/PaymentForm.php b/app/Filament/Resources/Payments/Schemas/PaymentForm.php index d91deb7..450a3fa 100644 --- a/app/Filament/Resources/Payments/Schemas/PaymentForm.php +++ b/app/Filament/Resources/Payments/Schemas/PaymentForm.php @@ -62,7 +62,74 @@ class PaymentForm ->label('Số tiền thu') ->numeric() ->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') ->label('Ngày thu') diff --git a/app/Filament/Resources/Payments/Tables/PaymentsTable.php b/app/Filament/Resources/Payments/Tables/PaymentsTable.php index e371a7b..0b0387d 100644 --- a/app/Filament/Resources/Payments/Tables/PaymentsTable.php +++ b/app/Filament/Resources/Payments/Tables/PaymentsTable.php @@ -29,9 +29,50 @@ class PaymentsTable Tables\Columns\TextColumn::make('receipt_number') ->label('Số phiếu thu') ->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') ->label('Đợt TT') ->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([ Tables\Filters\SelectFilter::make('method') diff --git a/app/Filament/Resources/Settlements/Pages/CreateSettlement.php b/app/Filament/Resources/Settlements/Pages/CreateSettlement.php new file mode 100644 index 0000000..c81e3d1 --- /dev/null +++ b/app/Filament/Resources/Settlements/Pages/CreateSettlement.php @@ -0,0 +1,11 @@ +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(), + ]), + ]), + ]); + } +} diff --git a/app/Filament/Resources/Settlements/SettlementResource.php b/app/Filament/Resources/Settlements/SettlementResource.php new file mode 100644 index 0000000..1feb8dc --- /dev/null +++ b/app/Filament/Resources/Settlements/SettlementResource.php @@ -0,0 +1,42 @@ +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'), + ]; + } +} diff --git a/app/Filament/Resources/Settlements/Tables/SettlementsTable.php b/app/Filament/Resources/Settlements/Tables/SettlementsTable.php new file mode 100644 index 0000000..898d969 --- /dev/null +++ b/app/Filament/Resources/Settlements/Tables/SettlementsTable.php @@ -0,0 +1,49 @@ +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'); + } +} diff --git a/app/Filament/Widgets/ContractStatsOverview.php b/app/Filament/Widgets/ContractStatsOverview.php new file mode 100644 index 0000000..ebcc9d1 --- /dev/null +++ b/app/Filament/Widgets/ContractStatsOverview.php @@ -0,0 +1,46 @@ +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'), + ]; + } +} diff --git a/app/Filament/Widgets/UpcomingPaymentsTable.php b/app/Filament/Widgets/UpcomingPaymentsTable.php new file mode 100644 index 0000000..189edfc --- /dev/null +++ b/app/Filament/Widgets/UpcomingPaymentsTable.php @@ -0,0 +1,47 @@ +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]); + } +} diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 0508e3b..69dca84 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -33,6 +33,11 @@ class Contract extends Model return $this->belongsTo(Product::class); } + public function paymentTemplate() + { + return $this->belongsTo(PaymentTemplate::class); + } + public function customers() { return $this->belongsToMany(Customer::class, 'contract_customers') @@ -73,6 +78,19 @@ class Contract extends Model 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() { static::saving(function ($contract) { diff --git a/app/Models/PaymentScheduleItem.php b/app/Models/PaymentScheduleItem.php index b690d45..21b3849 100644 --- a/app/Models/PaymentScheduleItem.php +++ b/app/Models/PaymentScheduleItem.php @@ -20,6 +20,21 @@ class PaymentScheduleItem extends Model '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() { return $this->belongsTo(PaymentTemplate::class); diff --git a/app/Models/User.php b/app/Models/User.php index f6ba1d2..c26ac12 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,12 +7,14 @@ use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Filament\Models\Contracts\FilamentUser; +use Filament\Panel; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; #[Fillable(['name', 'email', 'password'])] #[Hidden(['password', 'remember_token'])] -class User extends Authenticatable +class User extends Authenticatable implements FilamentUser { /** @use HasFactory */ use HasFactory, Notifiable; @@ -29,4 +31,9 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + public function canAccessPanel(Panel $panel): bool + { + return true; + } } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 1a23120..1596ea1 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -10,6 +10,8 @@ use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use App\Filament\Widgets\ContractStatsOverview; +use App\Filament\Widgets\UpcomingPaymentsTable; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -39,6 +41,8 @@ class AdminPanelProvider extends PanelProvider ]) ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\Filament\Widgets') ->widgets([ + ContractStatsOverview::class, + UpcomingPaymentsTable::class, AccountWidget::class, FilamentInfoWidget::class, ]) diff --git a/app/Services/DiscountEngine.php b/app/Services/DiscountEngine.php new file mode 100644 index 0000000..82558e0 --- /dev/null +++ b/app/Services/DiscountEngine.php @@ -0,0 +1,40 @@ + 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, + ]; + } +} diff --git a/database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php b/database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php new file mode 100644 index 0000000..dcb924b --- /dev/null +++ b/database/migrations/2026_04_24_083000_add_payment_template_id_to_contracts.php @@ -0,0 +1,19 @@ +foreignUuid('payment_template_id')->nullable()->constrained('payment_templates')->nullOnDelete(); + }); + } + + public function down(): void { + Schema::table('contracts', function (Blueprint $table) { + $table->dropConstrainedForeignId('payment_template_id'); + }); + } +}; diff --git a/tests/Feature/ContractResourceRenderTest.php b/tests/Feature/ContractResourceRenderTest.php new file mode 100644 index 0000000..85e1db7 --- /dev/null +++ b/tests/Feature/ContractResourceRenderTest.php @@ -0,0 +1,13 @@ +actingAs(User::factory()->create()); + + $response = $this->get(ContractResource::getUrl('index')); + + // Chỉ kiểm tra không bị 500 error + $response->assertStatus(200); +});