diff --git a/app/Filament/Resources/Contracts/ContractResource.php b/app/Filament/Resources/Contracts/ContractResource.php index eb0178c..022e96b 100644 --- a/app/Filament/Resources/Contracts/ContractResource.php +++ b/app/Filament/Resources/Contracts/ContractResource.php @@ -7,17 +7,14 @@ use App\Models\Contract; use App\Models\Product; use App\Models\PaymentTemplate; use App\Enums\NavigationGroup; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\DatePicker; -use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; -use Filament\Schemas\Components\Utilities\Get; use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager; +use App\Filament\Resources\Contracts\Schemas\ContractForm; + class ContractResource extends Resource { protected static ?string $model = Contract::class; @@ -30,24 +27,7 @@ class ContractResource extends Resource public static function form(Schema $schema): Schema { - return $schema - ->components([ - Section::make('Liên kết & Mẫu thanh toán') - ->columns(2) - ->schema([ - Select::make('product_id')->label('Sản phẩm')->relationship('product', 'code')->required()->live(), - Select::make('payment_template_id')->label('Mẫu thanh toán') - ->options(fn (Get $get) => PaymentTemplate::pluck('name', 'id')) - ->required()->dehydrated(false), - Select::make('customers')->label('Khách hàng')->relationship('customers', 'full_name')->multiple()->required()->columnSpanFull(), - ]), - Section::make('Chi tiết Hợp đồng') - ->columns(2) - ->schema([ - TextInput::make('contract_number')->label('Số HĐ')->required(), - DatePicker::make('signing_date')->label('Ngày ký')->required(), - ]) - ]); + return ContractForm::configure($schema); } public static function table(Table $table): Table diff --git a/app/Filament/Resources/Contracts/Schemas/ContractForm.php b/app/Filament/Resources/Contracts/Schemas/ContractForm.php index 46958c3..28fbd97 100644 --- a/app/Filament/Resources/Contracts/Schemas/ContractForm.php +++ b/app/Filament/Resources/Contracts/Schemas/ContractForm.php @@ -2,7 +2,15 @@ namespace App\Filament\Resources\Contracts\Schemas; +use App\Models\Product; +use App\Models\PaymentTemplate; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\DatePicker; +use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; +use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; class ContractForm { @@ -10,7 +18,63 @@ class ContractForm { return $schema ->components([ - // + Section::make('Liên kết & Mẫu thanh toán') + ->columns(2) + ->schema([ + Select::make('product_id') + ->label('Sản phẩm') + ->relationship('product', 'code') + ->required() + ->live() + ->afterStateUpdated(function (Set $set, $state) { + if ($state) { + $product = Product::find($state); + if ($product) { + $set('total_value', $product->total_price); + } + } + }), + Select::make('payment_template_id') + ->label('Mẫu thanh toán') + ->options(fn () => PaymentTemplate::pluck('name', 'id')) + ->required() + ->dehydrated(false), + Select::make('customers') + ->label('Khách hàng') + ->relationship('customers', 'full_name') + ->multiple() + ->required() + ->columnSpanFull(), + ]), + Section::make('Chi tiết Hợp đồng') + ->columns(2) + ->schema([ + TextInput::make('contract_number')->label('Số HĐ')->required(), + Select::make('contract_type') + ->label('Loại HĐ') + ->options([ + 'HĐMB' => 'HĐMB', + 'HĐGV' => 'HĐGV', + 'HĐDC' => 'HĐDC', + ]) + ->default('HĐMB') + ->required(), + DatePicker::make('signing_date')->label('Ngày ký')->required(), + TextInput::make('total_value') + ->label('Giá trị HĐ') + ->numeric() + ->required() + ->prefix('VND'), + Select::make('status') + ->label('Trạng thái') + ->options([ + 'Đang hiệu lực' => 'Đang hiệu lực', + 'Đã hoàn thành' => 'Đã hoàn thành', + 'Đã hủy' => 'Đã hủy', + ]) + ->default('Đang hiệu lực') + ->required(), + ]) ]); } } diff --git a/app/Filament/Resources/PaymentTemplateResource.php b/app/Filament/Resources/PaymentTemplateResource.php index 27ce155..b870196 100644 --- a/app/Filament/Resources/PaymentTemplateResource.php +++ b/app/Filament/Resources/PaymentTemplateResource.php @@ -45,6 +45,9 @@ class PaymentTemplateResource extends Resource TextInput::make('installment_no')->label('Đợt')->numeric()->required(), Select::make('type')->label('Loại')->options(PaymentType::class)->required(), TextInput::make('percentage')->label('%')->numeric()->required(), + TextInput::make('days_after_signing')->label('Ngày sau ký (F0)')->numeric(), + TextInput::make('days_after_previous')->label('Ngày sau đợt trước')->numeric(), + \Filament\Forms\Components\DatePicker::make('due_date')->label('Ngày cố định'), ])->columns(3), ]) ]); diff --git a/app/Filament/Resources/Products/ProductResource.php b/app/Filament/Resources/Products/ProductResource.php index 80b8623..4558a97 100644 --- a/app/Filament/Resources/Products/ProductResource.php +++ b/app/Filament/Resources/Products/ProductResource.php @@ -4,19 +4,13 @@ namespace App\Filament\Resources\Products; use App\Filament\Resources\Products\Pages; use App\Models\Product; -use App\Enums\ProductType; use App\Enums\NavigationGroup; -use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; -use Filament\Forms\Components\Repeater; -use Filament\Schemas\Components\Utilities\Get; -use Filament\Schemas\Components\Section; -use Filament\Schemas\Components\Tabs; -use App\Filament\Resources\Products\ProductResource\RelationManagers\ContractsRelationManager; use Filament\Schemas\Schema; use Filament\Resources\Resource; -use Filament\Tables; use Filament\Tables\Table; +use App\Filament\Resources\Products\ProductResource\RelationManagers\ContractsRelationManager; + +use App\Filament\Resources\Products\Schemas\ProductForm; class ProductResource extends Resource { @@ -30,43 +24,12 @@ class ProductResource extends Resource public static function form(Schema $schema): Schema { - return $schema - ->components([ - Tabs::make('ProductDetails') - ->tabs([ - Tabs\Tab::make('Thông tin chung') - ->icon('heroicon-o-information-circle') - ->columns(2) - ->schema([ - Select::make('project_id')->label('Dự án')->relationship('project', 'name')->required(), - Select::make('product_type')->label('Loại sản phẩm')->options(ProductType::class)->required(), - TextInput::make('code')->label('Mã SP')->required()->unique(ignoreRecord: true), - Select::make('status') - ->label('Trạng thái') - ->options(['Đang mở bán' => 'Đang mở bán', 'Đã cọc' => 'Đã cọc', 'Đã bán' => 'Đã bán']) - ->required(), - ]), - Tabs\Tab::make('Tài chính') - ->icon('heroicon-o-currency-dollar') - ->schema([ - TextInput::make('area')->label('Diện tích (m2)')->numeric()->required(), - TextInput::make('price_per_unit')->label('Đơn giá')->numeric()->required(), - TextInput::make('total_price')->label('Tổng giá')->numeric()->required(), - ]), - ])->columnSpanFull() - ]); + return ProductForm::configure($schema); } public static function table(Table $table): Table { - return $table - ->columns([ - Tables\Columns\TextColumn::make('project.code')->label('Dự án'), - Tables\Columns\TextColumn::make('code')->label('Mã SP')->searchable(), - Tables\Columns\TextColumn::make('product_type')->label('Loại')->badge(), - Tables\Columns\TextColumn::make('total_price')->label('Giá')->money('VND'), - Tables\Columns\TextColumn::make('status')->label('Trạng thái')->badge(), - ]); + return \App\Filament\Resources\Products\Tables\ProductsTable::configure($table); } public static function getRelations(): array { return [ContractsRelationManager::class]; } diff --git a/app/Filament/Resources/Products/Schemas/ProductForm.php b/app/Filament/Resources/Products/Schemas/ProductForm.php index 768cf17..be50c19 100644 --- a/app/Filament/Resources/Products/Schemas/ProductForm.php +++ b/app/Filament/Resources/Products/Schemas/ProductForm.php @@ -2,6 +2,12 @@ namespace App\Filament\Resources\Products\Schemas; +use App\Enums\ProductType; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\TextInput; +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Section; +use Filament\Forms\Components\KeyValue; use Filament\Schemas\Schema; class ProductForm @@ -10,7 +16,76 @@ class ProductForm { return $schema ->components([ - // + Tabs::make('ProductDetails') + ->tabs([ + Tabs\Tab::make('Thông tin chung') + ->icon('heroicon-o-information-circle') + ->columns(2) + ->schema([ + Select::make('project_id')->label('Dự án')->relationship('project', 'name')->required(), + Select::make('product_type')->label('Loại sản phẩm')->options(ProductType::class)->required(), + TextInput::make('code')->label('Mã SP')->required(), + Select::make('status') + ->label('Trạng thái') + ->options([ + 'Đang mở bán' => 'Đang mở bán', + 'Đã cọc' => 'Đã cọc', + 'Đã bán' => 'Đã bán' + ])->required(), + ]), + Tabs\Tab::make('Tài chính') + ->icon('heroicon-o-currency-dollar') + ->columns(2) + ->schema([ + TextInput::make('area')->label('Diện tích (m2)')->numeric()->required(), + TextInput::make('price_per_unit')->label('Đơn giá')->numeric()->required()->prefix('VND'), + TextInput::make('total_price')->label('Tổng giá')->numeric()->required()->prefix('VND'), + Section::make('Bổ sung') + ->columns(2) + ->schema([ + TextInput::make('qsdd_value')->label('Giá trị QSDĐ')->numeric()->prefix('VND'), + TextInput::make('foundation_temp_value')->label('Giá trị móng (tạm tính)')->numeric()->prefix('VND'), + ]) + ]), + Tabs\Tab::make('Kỹ thuật & Hạ tầng') + ->icon('heroicon-o-cog') + ->columns(2) + ->schema([ + TextInput::make('adjacent_road')->label('Đường tiếp giáp'), + TextInput::make('frontage_count')->label('Số mặt tiền')->numeric(), + TextInput::make('max_floors')->label('Số tầng tối đa')->numeric(), + TextInput::make('construction_status')->label('Trạng thái XD'), + KeyValue::make('infrastructure_status') + ->label('Trạng thái hạ tầng') + ->columnSpanFull(), + ]), + Tabs\Tab::make('Tiến độ thanh toán') + ->icon('heroicon-o-calendar-days') + ->schema([ + \Filament\Forms\Components\Repeater::make('payment_schedule_preview') + ->label('Lịch trình dự kiến / Thực tế') + ->relationship(function ($record) { + // Nếu sản phẩm đã có hợp đồng, lấy lịch trình từ hợp đồng đầu tiên + if ($record && $record->contracts()->exists()) { + return $record->contracts()->first()->scheduleItems(); + } + // Nếu chưa có, chúng ta sẽ hiển thị mẫu từ Project (phần này cần logic state giả lập hoặc view) + return null; + }) + ->schema([ + TextInput::make('installment_no')->label('Đợt')->disabled(), + TextInput::make('type')->label('Loại')->disabled(), + TextInput::make('percentage')->label('%')->disabled(), + TextInput::make('amount')->label('Số tiền (VNĐ)')->numeric()->disabled(), + DatePicker::make('due_date')->label('Ngày đến hạn')->disabled(), + ]) + ->columns(5) + ->addable(false) + ->deletable(false) + ->reorderable(false) + ->placeholder('Sản phẩm chưa có hợp đồng hoặc Dự án chưa gán mẫu thanh toán mặc định.') + ]), + ])->columnSpanFull() ]); } } diff --git a/app/Filament/Resources/Products/Tables/ProductsTable.php b/app/Filament/Resources/Products/Tables/ProductsTable.php index e9b3d85..2fc106c 100644 --- a/app/Filament/Resources/Products/Tables/ProductsTable.php +++ b/app/Filament/Resources/Products/Tables/ProductsTable.php @@ -5,7 +5,10 @@ namespace App\Filament\Resources\Products\Tables; use Filament\Actions\BulkActionGroup; use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; +use App\Models\Project; class ProductsTable { @@ -13,10 +16,45 @@ class ProductsTable { return $table ->columns([ - // + TextColumn::make('project.code') + ->label('Dự án') + ->sortable() + ->searchable(), + TextColumn::make('code') + ->label('Mã SP') + ->sortable() + ->searchable(), + TextColumn::make('product_type') + ->label('Loại') + ->badge(), + TextColumn::make('total_price') + ->label('Giá') + ->money('VND') + ->sortable(), + TextColumn::make('status') + ->label('Trạng thái') + ->badge() + ->color(fn (string $state): string => match ($state) { + 'Đang mở bán' => 'success', + 'Đã cọc' => 'warning', + 'Đã bán' => 'danger', + default => 'gray', + }), ]) ->filters([ - // + SelectFilter::make('project_id') + ->label('Dự án') + ->relationship('project', 'name'), + SelectFilter::make('status') + ->label('Trạng thái') + ->options([ + 'Đang mở bán' => 'Đang mở bán', + 'Đã cọc' => 'Đã cọc', + 'Đã bán' => 'Đã bán', + ]), + SelectFilter::make('product_type') + ->label('Loại sản phẩm') + ->options(\App\Enums\ProductType::class), ]) ->recordActions([ EditAction::make(), diff --git a/app/Filament/Resources/Projects/ProjectResource.php b/app/Filament/Resources/Projects/ProjectResource.php index 0bbeb8c..9ee18ca 100644 --- a/app/Filament/Resources/Projects/ProjectResource.php +++ b/app/Filament/Resources/Projects/ProjectResource.php @@ -32,6 +32,11 @@ class ProjectResource extends Resource ->schema([ TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true), TextInput::make('name')->label('Tên Dự án')->required(), + Select::make('payment_template_id') + ->label('Mẫu thanh toán mặc định') + ->relationship('paymentTemplate', 'name') + ->placeholder('Chọn mẫu thanh toán cho toàn dự án') + ->columnSpanFull(), Select::make('type') ->label('Loại hình') ->options([ diff --git a/app/Models/Contract.php b/app/Models/Contract.php index ebc30d2..e017e7c 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -30,7 +30,8 @@ class Contract extends Model public function customers() { return $this->belongsToMany(Customer::class, 'contract_customers') - ->withPivot('role', 'transfer_order') + ->using(ContractCustomer::class) + ->withPivot('id', 'role', 'transfer_order') ->withTimestamps(); } @@ -68,4 +69,20 @@ class Contract extends Model { return $this->hasMany(PaymentFine::class); } + + protected static function booted() + { + static::creating(function ($contract) { + // Tự động lấy giá trị từ sản phẩm nếu chưa có + if (empty($contract->total_value) && !empty($contract->product_id)) { + $product = Product::find($contract->product_id); + if ($product) { + $contract->total_value = $product->total_price; + } + } + + // Tính toán số tiền còn lại + $contract->remaining_amount = ($contract->total_value ?? 0) - ($contract->paid_amount ?? 0); + }); + } } diff --git a/app/Models/ContractCustomer.php b/app/Models/ContractCustomer.php new file mode 100644 index 0000000..f561797 --- /dev/null +++ b/app/Models/ContractCustomer.php @@ -0,0 +1,19 @@ +belongsToMany(Contract::class, 'contract_customers') - ->withPivot('role', 'transfer_order') + ->using(ContractCustomer::class) + ->withPivot('id', 'role', 'transfer_order') ->withTimestamps(); } } \ No newline at end of file diff --git a/app/Models/Project.php b/app/Models/Project.php index c7a43ed..12ba9ad 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -11,12 +11,17 @@ class Project extends Model use HasUuids, HasFactory; protected $guarded = []; - + public function products() { return $this->hasMany(Product::class); } + public function paymentTemplate() + { + return $this->belongsTo(PaymentTemplate::class); + } + public function paymentTemplates() { return $this->hasMany(PaymentTemplate::class); diff --git a/artisan b/artisan old mode 100644 new mode 100755 diff --git a/database/migrations/2025_01_05_000000_create_contract_customers_table.php b/database/migrations/2025_01_05_000000_create_contract_customers_table.php index 6efe7df..9bb6562 100644 --- a/database/migrations/2025_01_05_000000_create_contract_customers_table.php +++ b/database/migrations/2025_01_05_000000_create_contract_customers_table.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('contract_customers', function (Blueprint $table) { - $table->id(); + $table->uuid('id')->primary(); $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); $table->foreignUuid('customer_id')->constrained('customers')->cascadeOnDelete(); $table->string('role')->default('CHỦ SH 1'); // Đồng sở hữu diff --git a/database/migrations/2026_04_19_000000_add_payment_template_to_projects_table.php b/database/migrations/2026_04_19_000000_add_payment_template_to_projects_table.php new file mode 100644 index 0000000..d3461d8 --- /dev/null +++ b/database/migrations/2026_04_19_000000_add_payment_template_to_projects_table.php @@ -0,0 +1,21 @@ +string('type')->nullable(); // Thêm cột loại hình dự án + $table->foreignUuid('payment_template_id')->nullable()->constrained('payment_templates')->nullOnDelete(); + }); + } + + public function down(): void { + Schema::table('projects', function (Blueprint $table) { + $table->dropForeign(['payment_template_id']); + $table->dropColumn(['payment_template_id', 'type']); + }); + } +}; diff --git a/database/seeders/TestDataSeeder.php b/database/seeders/TestDataSeeder.php index a272b8a..5b75001 100644 --- a/database/seeders/TestDataSeeder.php +++ b/database/seeders/TestDataSeeder.php @@ -2,6 +2,8 @@ namespace Database\Seeders; +use Illuminate\Database\Seeder; +use App\Models\User; use App\Models\Project; use App\Models\Product; use App\Models\Customer; @@ -9,144 +11,124 @@ use App\Models\Contract; use App\Models\PaymentTemplate; use App\Models\PaymentSchedule; use App\Models\PaymentScheduleItem; -use App\Models\Payment; -use App\Models\Appendix; -use App\Models\Settlement; use App\Enums\ProductType; use App\Enums\PaymentType; -use Illuminate\Database\Seeder; +use Illuminate\Support\Facades\Hash; use Carbon\Carbon; -use Illuminate\Support\Facades\DB; class TestDataSeeder extends Seeder { public function run(): void { - DB::transaction(function () { - // 1. PROJECT - $sth03 = Project::factory()->create(['code' => 'STH03', 'name' => 'Khu Riverside STH03']); - $diamond = Project::factory()->create(['code' => 'DIA21', 'name' => 'Diamond Luxury Suites']); + // 1. Tạo Tài khoản Admin + User::updateOrCreate( + ['email' => 'admin@phuongtc.com'], + [ + 'name' => 'chanphuong', + 'password' => Hash::make('1Qazxsw2@!321'), + ] + ); - // 2. PAYMENT TEMPLATES - $templateStandard = PaymentTemplate::create([ - 'project_id' => $sth03->id, - 'name' => 'Thanh toán chuẩn STH03', - 'is_default' => true - ]); + // 2. Tạo Dự án + $project = Project::updateOrCreate( + ['code' => 'STH03'], + [ + 'name' => 'Khu đô thị Mỹ Gia - Gói 3', + 'type' => 'Khu đô thị' + ] + ); - $this->createTemplateItems($templateStandard); - - // 3. PRODUCTS - $this->seedProducts($sth03, $diamond); - - // 4. CUSTOMERS - $customers = Customer::factory(15)->create(); - - // 5. CASE STUDY: LỊCH SỬ CHUYỂN NHƯỢNG - $this->seedTransferHistory($sth03, $customers); - - // 6. CASE STUDY: DÒNG TIỀN PHỨC TẠP - $this->seedComplexPayments($sth03, $customers, $templateStandard); - }); - } - - private function createTemplateItems($template) - { - PaymentScheduleItem::create(['template_id' => $template->id, 'installment_no' => 1, 'percentage' => 30, 'days_after_signing' => 7, 'type' => PaymentType::QSDD]); - PaymentScheduleItem::create(['template_id' => $template->id, 'installment_no' => 2, 'percentage' => 40, 'days_after_previous' => 60, 'type' => PaymentType::MONG]); - PaymentScheduleItem::create(['template_id' => $template->id, 'installment_no' => 3, 'percentage' => 30, 'days_after_previous' => 90, 'type' => PaymentType::THAN]); - } - - private function seedProducts($project1, $project2) - { - for ($i = 1; $i <= 5; $i++) { - Product::create([ - 'project_id' => $project1->id, - 'product_type' => ProductType::LAND, - 'code' => $project1->code . '.' . sprintf('%02d', $i), - 'area' => 100 + $i, - 'price_per_unit' => 50000000, - 'total_price' => (100 + $i) * 50000000, - 'qsdd_value' => ((100 + $i) * 50000000) * 0.4, - 'adjacent_road' => 'Đường 16m', - 'frontage_count' => 1, - 'infrastructure_status' => [ - 'dien' => ['status' => 'Hoàn thiện', 'child' => ['tram_bien_ap' => 'Đã nghiệm thu']], - 'nuoc' => ['status' => 'Đang thi công'] - ], - 'custom_data' => ['so_lo' => 'Lô '.$i, 'huong' => 'Đông Nam'], - 'status' => 'Đang mở bán' - ]); - } - - for ($i = 1; $i <= 5; $i++) { - Product::create([ - 'project_id' => $project2->id, - 'product_type' => ProductType::APARTMENT, - 'code' => $project2->code . '-P' . sprintf('%03d', $i), - 'area' => 75, - 'price_per_unit' => 40000000, - 'total_price' => 75 * 40000000, - 'infrastructure_status' => ['thang_may' => 'Schindler', 'pccc' => 'Đạt chuẩn'], - 'custom_data' => ['block' => 'A', 'floor' => 10 + $i, 'view' => 'City View'], - 'status' => 'Đang mở bán' - ]); - } - } - - private function seedTransferHistory($project, $customers) - { - $product = Product::where('code', 'STH03.01')->first(); - - $c1 = Contract::create([ - 'product_id' => $product->id, 'transfer_order' => 1, 'contract_type' => 'HĐMB', 'contract_number' => 'HĐMB-GOC-88', - 'signing_date' => Carbon::now()->subYears(2), 'total_value' => $product->total_price, 'paid_amount' => $product->total_price, 'status' => 'Đã chuyển nhượng' - ]); - $customers[0]->contracts()->attach($c1->id, ['role' => 'CHỦ CŨ', 'transfer_order' => 1]); - - // SỬA TỪ customData THÀNH custom_data - Appendix::create([ - 'contract_id' => $c1->id, 'product_id' => $product->id, 'type' => 'Thay đổi diện tích', 'apply_from_order' => 1, 'signing_date' => Carbon::now()->subYears(1), - 'custom_data' => ['area_new' => 105] + // 3. Tạo Mẫu thanh toán + $template = PaymentTemplate::create([ + 'project_id' => $project->id, + 'name' => 'Mẫu chuẩn Đất nền 30-40-30' ]); - $c2 = Contract::create([ - 'product_id' => $product->id, 'transfer_order' => 0, 'contract_type' => 'VBCN', 'contract_number' => 'VBCN-F1-99', - 'signing_date' => Carbon::now(), 'total_value' => $product->total_price * 1.15, 'paid_amount' => 500000000, 'status' => 'Đang hiệu lực' + $project->update(['payment_template_id' => $template->id]); + + // 4. Tạo các đợt mẫu + PaymentScheduleItem::create([ + 'template_id' => $template->id, + 'installment_no' => 1, + 'type' => PaymentType::MONG, + 'percentage' => 30, + 'days_after_signing' => 0, ]); - $customers[1]->contracts()->attach($c2->id, ['role' => 'CHỦ SỞ HỮU', 'transfer_order' => 0]); - - $product->update(['status' => 'Đã bán']); - } - - private function seedComplexPayments($project, $customers, $template) - { - $product = Product::where('code', 'STH03.02')->first(); - $contract = Contract::create([ - 'product_id' => $product->id, 'transfer_order' => 0, 'contract_type' => 'HĐMB', 'contract_number' => 'HD-PAY-DEBUG', - 'signing_date' => Carbon::now()->subDays(10), 'total_value' => $product->total_price, 'status' => 'Đang hiệu lực' + PaymentScheduleItem::create([ + 'template_id' => $template->id, + 'installment_no' => 2, + 'type' => PaymentType::THAN, + 'percentage' => 40, + 'days_after_previous' => 60, + ]); + PaymentScheduleItem::create([ + 'template_id' => $template->id, + 'installment_no' => 3, + 'type' => PaymentType::OTHER, + 'percentage' => 30, + 'days_after_previous' => 90, ]); - $customers[5]->contracts()->attach($contract->id, ['role' => 'CHỦ SỞ HỮU', 'transfer_order' => 0]); - $schedule = PaymentSchedule::create(['contract_id' => $contract->id, 'template_id' => $template->id]); - $items = $template->items; - foreach($items as $item) { - $si = PaymentScheduleItem::create([ - 'schedule_id' => $schedule->id, 'installment_no' => $item->installment_no, 'percentage' => $item->percentage, - 'amount' => $contract->total_value * ($item->percentage / 100), 'type' => $item->type, 'due_date' => Carbon::now()->addDays(30 * $item->installment_no) - ]); + // 5. Tạo Sản phẩm + Product::create([ + 'project_id' => $project->id, + 'product_type' => ProductType::LAND, + 'code' => 'STH03.01', + 'area' => 100, + 'price_per_unit' => 25000000, + 'total_price' => 2500000000, + 'status' => 'Đang mở bán', + ]); - if ($item->installment_no == 1) { - $required = $si->amount; - $paid = $required * 1.2; - Payment::create(['contract_id' => $contract->id, 'schedule_item_id' => $si->id, 'amount' => $paid, 'paid_date' => Carbon::now(), 'method' => 'Chuyển khoản']); - - $contract->update([ - 'paid_amount' => $paid, - 'excess_amount' => $paid - $required, - 'remaining_amount' => $contract->total_value - $paid - ]); - } - } + $productSold = Product::create([ + 'project_id' => $project->id, + 'product_type' => ProductType::LAND, + 'code' => 'STH03.02', + 'area' => 100, + 'price_per_unit' => 25000000, + 'total_price' => 2500000000, + 'status' => 'Đã bán', + ]); + + // 6. Khách hàng & Hợp đồng chuyển nhượng + // Sử dụng cột cmnd_cccd thay cho id_number + $customerA = Customer::create(['full_name' => 'Nguyễn Văn A', 'phone' => '0901234567', 'cmnd_cccd' => '123456789']); + $customerB = Customer::create(['full_name' => 'Trần Thị B', 'phone' => '0907654321', 'cmnd_cccd' => '987654321']); + + // F1 + $contractF1 = Contract::create([ + 'product_id' => $productSold->id, + 'contract_number' => 'HĐMB-STH03.02-F1', + 'signing_date' => Carbon::now()->subMonths(6), + 'total_value' => 2500000000, + 'transfer_order' => 1, + 'status' => 'Đã thanh lý (Chuyển nhượng)', + ]); + $contractF1->customers()->attach($customerA->id); + + // F2 + $contractF2 = Contract::create([ + 'product_id' => $productSold->id, + 'contract_number' => 'HĐMB-STH03.02-F2', + 'signing_date' => Carbon::now()->subDays(10), + 'total_value' => 2600000000, + 'transfer_order' => 2, + 'status' => 'Đang hiệu lực', + ]); + $contractF2->customers()->attach($customerB->id); + + $schedule = PaymentSchedule::create([ + 'contract_id' => $contractF2->id, + 'template_id' => $template->id, + ]); + + PaymentScheduleItem::create([ + 'schedule_id' => $schedule->id, + 'installment_no' => 1, + 'type' => PaymentType::MONG, + 'percentage' => 30, + 'amount' => 780000000, + 'due_date' => Carbon::now()->subDays(10), + ]); } } diff --git a/moduledesign.md b/moduledesign.md new file mode 100644 index 0000000..eeb12e3 --- /dev/null +++ b/moduledesign.md @@ -0,0 +1,28 @@ +# Mô tả giao diện tính năng của tường module + +1. **Module sản phẩm.** +- Giao diện chính cần có thêm các mục để có thể lọc dữ liệu như sau: + + - lọc theo Project + + - lọc theo trạng thái (đã bán, chưa bán....) + + - lọc theo đợt mở bán (hiện chưa có module này, sẽ bổ sung sau) + + - Lọc theo các khu vực/block (kiểm tra lại xem đã có thể thực hiện hay chưa) + +-    Giao diện chi tiết: + + - Hiển thị các thông tin chi tiết bắt buộc trong database chi thành các tab hợp lý + + - Hiển thị các lịch sử liên quan (theo tab) như : + + - lịch sử giao dịch + + - lịch sử chỉnh sửa, cập nhật + + - tiến độ thanh toán + + - Tài liệu liên quan (sẽ bổ sung module sau) + + diff --git a/prisma2.md b/prisma2.md new file mode 100644 index 0000000..ace2aca --- /dev/null +++ b/prisma2.md @@ -0,0 +1,241 @@ +// ============================================= +// PRISMA SCHEMA - HQLAND MANAGEMENT SYSTEM +// Phiên bản: 2.4 (Cập nhật từ Database thực tế) +// Ngày: 18/04/2026 +// Ghi chú: Đồng bộ hóa 100% với Migrations hiện tại. +// ============================================= + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +// ============================================= +// 1. PROJECT (Dự án) +// ============================================= +model Project { + id String @id @default(uuid()) + code String @unique // ví dụ: STH03 + name String + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + products Product[] + templates PaymentTemplate[] +} + +// ============================================= +// 2. PRODUCT (Sản phẩm) +// ============================================= +model Product { + id String @id @default(uuid()) + projectId String @map("project_id") + + productType String @map("product_type") // LAND, APARTMENT... + + code String @unique + area Decimal @db.Decimal(12, 2) + pricePerUnit Decimal @db.Decimal(15, 2) @map("price_per_unit") + totalPrice Decimal @db.Decimal(15, 2) @map("total_price") + + // Giá trị tài chính bổ sung + qsddValue Decimal @default(0) @db.Decimal(15, 2) @map("qsdd_value") + foundationTempValue Decimal @default(0) @db.Decimal(15, 2) @map("foundation_temp_value") + contractTempValue Decimal @default(0) @db.Decimal(15, 2) @map("contract_temp_value") + + // Thông số kỹ thuật + adjacentRoad String? @map("adjacent_road") + frontageCount Int? @map("frontage_count") + maxFloors Int? @map("max_floors") + buildingDensity Decimal? @db.Decimal(5, 2) @map("building_density") + constructionStatus String? @map("construction_status") + + // Hạ tầng & Dữ liệu động + infrastructureRawText String? @map("infrastructure_raw_text") + infrastructureStatus Json? @map("infrastructure_status") + customData Json? @map("custom_data") + + // Trạng thái + status String @default("Đang mở bán") + redBookStatus String @default("Chưa có dữ liệu") @map("red_book_status") + + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + project Project @relation(fields: [projectId], references: [id]) + contracts Contract[] + settlements Settlement[] +} + +// ============================================= +// 3. CONTRACT & CUSTOMER +// ============================================= +model Contract { + id String @id @default(uuid()) + productId String @map("product_id") + transferOrder Int @default(0) @map("transfer_order") + contractType String @default("HĐMB") @map("contract_type") + contractNumber String @unique @map("contract_number") + signingDate DateTime? @map("signing_date") + status String @default("Đang hiệu lực") + + totalValue Decimal @db.Decimal(15, 2) @map("total_value") + paidAmount Decimal @default(0) @db.Decimal(15, 2) @map("paid_amount") + remainingAmount Decimal @default(0) @db.Decimal(15, 2) @map("remaining_amount") + excessAmount Decimal @default(0) @db.Decimal(15, 2) @map("excess_amount") // Tiền dư + + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + product Product @relation(fields: [productId], references: [id]) + customers ContractCustomer[] + appendices Appendix[] + payments Payment[] + schedule PaymentSchedule? + fines PaymentFine[] +} + +model Customer { + id String @id @default(uuid()) + cmndCccd String @unique @map("cmnd_cccd") + fullName String @map("full_name") + phone String? + email String? + address Json? + dob DateTime? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contracts ContractCustomer[] +} + +model ContractCustomer { + id BigInt @id @default(autoincrement()) + contractId String @map("contract_id") + customerId String @map("customer_id") + role String @default("CHỦ SH 1") + transferOrder Int @default(0) @map("transfer_order") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contract Contract @relation(fields: [contractId], references: [id]) + customer Customer @relation(fields: [customerId], references: [id]) +} + +// ============================================= +// 4. FINANCE MODULE +// ============================================= +model PaymentTemplate { + id String @id @default(uuid()) + projectId String @map("project_id") + name String + isDefault Boolean @default(false) @map("is_default") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + project Project @relation(fields: [projectId], references: [id]) + items PaymentScheduleItem[] + schedules PaymentSchedule[] +} + +model PaymentSchedule { + id String @id @default(uuid()) + contractId String @unique @map("contract_id") + templateId String @map("template_id") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contract Contract @relation(fields: [contractId], references: [id]) + template PaymentTemplate @relation(fields: [templateId], references: [id]) + items PaymentScheduleItem[] +} + +model PaymentScheduleItem { + id String @id @default(uuid()) + templateId String? @map("template_id") + scheduleId String? @map("schedule_id") + + installmentNo Int @map("installment_no") + amount Decimal? @db.Decimal(15, 2) + percentage Decimal? @db.Decimal(5, 2) + + // Logic ngày đến hạn + daysAfterSigning Int? @map("days_after_signing") + daysAfterPrevious Int? @map("days_after_previous") + dueDate DateTime? @map("due_date") + + type String // QSDD, MONG, THAN... + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + template PaymentTemplate? @relation(fields: [templateId], references: [id]) + schedule PaymentSchedule? @relation(fields: [scheduleId], references: [id]) + payments Payment[] +} + +model Payment { + id String @id @default(uuid()) + contractId String @map("contract_id") + scheduleItemId String? @map("schedule_item_id") + + amount Decimal @db.Decimal(15, 2) + paidDate DateTime @map("paid_date") + receiptNumber String? @map("receipt_number") + method String @default("Chuyển khoản") + metadata Json? + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contract Contract @relation(fields: [contractId], references: [id]) + scheduleItem PaymentScheduleItem? @relation(fields: [scheduleItemId], references: [id]) +} + +model PaymentFine { + id String @id @default(uuid()) + contractId String @map("contract_id") + amount Decimal @db.Decimal(15, 2) + reason String + dueDate DateTime @map("due_date") + paidDate DateTime? @map("paid_date") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contract Contract @relation(fields: [contractId], references: [id]) +} + +// ============================================= +// 5. APPENDIX & SETTLEMENT +// ============================================= +model Appendix { + id String @id @default(uuid()) + contractId String @map("contract_id") + productId String @map("product_id") + type String + applyFromOrder Int @map("apply_from_order") + signingDate DateTime @map("signing_date") + customData Json? @map("custom_data") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + contract Contract @relation(fields: [contractId], references: [id]) +} + +model Settlement { + id String @id @default(uuid()) + productId String @map("product_id") + type String // MONG, THAN, CP THI CONG + tempValue Decimal @db.Decimal(15, 2) @map("temp_value") + finalValue Decimal @db.Decimal(15, 2) @map("final_value") + difference Decimal @db.Decimal(15, 2) + redBookStatus String @map("red_book_status") + issueDate DateTime? @map("issue_date") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + product Product @relation(fields: [productId], references: [id]) +} diff --git a/tests/Feature/ContractFinanceFlowTest.php b/tests/Feature/ContractFinanceFlowTest.php new file mode 100644 index 0000000..a702709 --- /dev/null +++ b/tests/Feature/ContractFinanceFlowTest.php @@ -0,0 +1,106 @@ +actingAs(User::factory()->create()); +}); + +it('can create a contract and automatically generate a payment schedule from template', function () { + // 1. Chuẩn bị dữ liệu mẫu + $project = Project::factory()->create(['code' => 'TEST-PROJ', 'name' => 'Dự án Kiểm thử']); + $product = Product::factory()->create([ + 'project_id' => $project->id, + 'code' => 'STH03.TEST', + 'total_price' => 1000000000, // 1 tỷ + ]); + $customer = Customer::factory()->create(['full_name' => 'Khách Hàng Test']); + + // 2. Tạo Mẫu thanh toán (3 đợt: 30% - 30% - 40%) + $template = PaymentTemplate::create([ + 'project_id' => $project->id, + 'name' => 'Mẫu thanh toán 30-30-40', + 'is_default' => true, + ]); + + // Đợt 1: 30% ngay khi ký (0 ngày sau signing) + PaymentScheduleItem::create([ + 'template_id' => $template->id, + 'installment_no' => 1, + 'percentage' => 30, + 'days_after_signing' => 0, + 'type' => 'QSDD', + ]); + + // Đợt 2: 30% sau 30 ngày kể từ đợt trước + PaymentScheduleItem::create([ + 'template_id' => $template->id, + 'installment_no' => 2, + 'percentage' => 30, + 'days_after_previous' => 30, + 'type' => 'MONG', + ]); + + // 3. Thực hiện tạo Hợp đồng qua Livewire Page + $signingDate = Carbon::now(); + + \Livewire\Livewire::test(\App\Filament\Resources\Contracts\Pages\CreateContract::class) + ->fillForm([ + 'product_id' => $product->id, + 'contract_number' => 'HD-TEST-UUID', + 'contract_type' => 'HĐMB', + 'signing_date' => $signingDate->format('Y-m-d'), + 'total_value' => 1000000000, + 'payment_template_id' => $template->id, // Chọn mẫu thanh toán + 'customers' => [$customer->id], // Đồng sở hữu + ]) + ->call('create') + ->assertHasNoFormErrors(); + + // 4. KIỂM TRA KẾT QUẢ TRONG DATABASE + + // A. Hợp đồng phải tồn tại + $contract = Contract::where('contract_number', 'HD-TEST-UUID')->first(); + expect($contract)->not->toBeNull(); + expect($contract->id)->toBeUuid(); // Kiểm tra khóa chính là UUID + + // B. Kiểm tra bảng trung gian contract_customers (Phải có UUID) + $this->assertDatabaseHas('contract_customers', [ + 'contract_id' => $contract->id, + 'customer_id' => $customer->id, + ]); + + $pivot = \DB::table('contract_customers')->where('contract_id', $contract->id)->first(); + expect($pivot->id)->toBeUuid(); // KIỂM TRA QUAN TRỌNG: ID bảng trung gian phải là UUID + + // C. Kiểm tra Lịch trình thanh toán (Payment Schedule) + $schedule = PaymentSchedule::where('contract_id', $contract->id)->first(); + expect($schedule)->not->toBeNull(); + expect($schedule->template_id)->toBe($template->id); + + // D. Kiểm tra các đợt thanh toán con (Items) + $scheduleItems = PaymentScheduleItem::where('schedule_id', $schedule->id) + ->orderBy('installment_no') + ->get(); + + expect($scheduleItems)->toHaveCount(2); + + // Kiểm tra đợt 1 (30% = 300 triệu) + expect((float)$scheduleItems[0]->amount)->toBe(300000000.0); + expect($scheduleItems[0]->due_date->format('Y-m-d'))->toBe($signingDate->format('Y-m-d')); + + // Kiểm tra đợt 2 (30% = 300 triệu, sau 30 ngày) + expect((float)$scheduleItems[1]->amount)->toBe(300000000.0); + expect($scheduleItems[1]->due_date->format('Y-m-d'))->toBe($signingDate->copy()->addDays(30)->format('Y-m-d')); +});