Xu ly giao dien San Pham

This commit is contained in:
2026-04-19 23:50:21 +00:00
parent 10eac55520
commit 91ff4a5e4d
18 changed files with 744 additions and 196 deletions

View File

@@ -7,17 +7,14 @@ use App\Models\Contract;
use App\Models\Product; use App\Models\Product;
use App\Models\PaymentTemplate; use App\Models\PaymentTemplate;
use App\Enums\NavigationGroup; 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\Schemas\Schema;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables; use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use Filament\Schemas\Components\Utilities\Get;
use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager; use App\Filament\Resources\Contracts\ContractResource\RelationManagers\ScheduleItemsRelationManager;
use App\Filament\Resources\Contracts\Schemas\ContractForm;
class ContractResource extends Resource class ContractResource extends Resource
{ {
protected static ?string $model = Contract::class; protected static ?string $model = Contract::class;
@@ -30,24 +27,7 @@ class ContractResource extends Resource
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
{ {
return $schema return ContractForm::configure($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(),
])
]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table

View File

@@ -2,7 +2,15 @@
namespace App\Filament\Resources\Contracts\Schemas; 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\Schema;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class ContractForm class ContractForm
{ {
@@ -10,7 +18,63 @@ class ContractForm
{ {
return $schema return $schema
->components([ ->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(),
])
]); ]);
} }
} }

View File

@@ -45,6 +45,9 @@ class PaymentTemplateResource extends Resource
TextInput::make('installment_no')->label('Đợt')->numeric()->required(), TextInput::make('installment_no')->label('Đợt')->numeric()->required(),
Select::make('type')->label('Loại')->options(PaymentType::class)->required(), Select::make('type')->label('Loại')->options(PaymentType::class)->required(),
TextInput::make('percentage')->label('%')->numeric()->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), ])->columns(3),
]) ])
]); ]);

View File

@@ -4,19 +4,13 @@ namespace App\Filament\Resources\Products;
use App\Filament\Resources\Products\Pages; use App\Filament\Resources\Products\Pages;
use App\Models\Product; use App\Models\Product;
use App\Enums\ProductType;
use App\Enums\NavigationGroup; 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\Schemas\Schema;
use Filament\Resources\Resource; use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table; use Filament\Tables\Table;
use App\Filament\Resources\Products\ProductResource\RelationManagers\ContractsRelationManager;
use App\Filament\Resources\Products\Schemas\ProductForm;
class ProductResource extends Resource class ProductResource extends Resource
{ {
@@ -30,43 +24,12 @@ class ProductResource extends Resource
public static function form(Schema $schema): Schema public static function form(Schema $schema): Schema
{ {
return $schema return ProductForm::configure($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()
]);
} }
public static function table(Table $table): Table public static function table(Table $table): Table
{ {
return $table return \App\Filament\Resources\Products\Tables\ProductsTable::configure($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(),
]);
} }
public static function getRelations(): array { return [ContractsRelationManager::class]; } public static function getRelations(): array { return [ContractsRelationManager::class]; }

View File

@@ -2,6 +2,12 @@
namespace App\Filament\Resources\Products\Schemas; 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; use Filament\Schemas\Schema;
class ProductForm class ProductForm
@@ -10,7 +16,76 @@ class ProductForm
{ {
return $schema return $schema
->components([ ->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()
]); ]);
} }
} }

View File

@@ -5,7 +5,10 @@ namespace App\Filament\Resources\Products\Tables;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction; use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction; use Filament\Actions\EditAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table; use Filament\Tables\Table;
use App\Models\Project;
class ProductsTable class ProductsTable
{ {
@@ -13,10 +16,45 @@ class ProductsTable
{ {
return $table return $table
->columns([ ->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([ ->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([ ->recordActions([
EditAction::make(), EditAction::make(),

View File

@@ -32,6 +32,11 @@ class ProjectResource extends Resource
->schema([ ->schema([
TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true), TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true),
TextInput::make('name')->label('Tên Dự án')->required(), 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') Select::make('type')
->label('Loại hình') ->label('Loại hình')
->options([ ->options([

View File

@@ -30,7 +30,8 @@ class Contract extends Model
public function customers() public function customers()
{ {
return $this->belongsToMany(Customer::class, 'contract_customers') return $this->belongsToMany(Customer::class, 'contract_customers')
->withPivot('role', 'transfer_order') ->using(ContractCustomer::class)
->withPivot('id', 'role', 'transfer_order')
->withTimestamps(); ->withTimestamps();
} }
@@ -68,4 +69,20 @@ class Contract extends Model
{ {
return $this->hasMany(PaymentFine::class); 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);
});
}
} }

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Relations\Pivot;
class ContractCustomer extends Pivot
{
use HasUuids;
protected $table = 'contract_customers';
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
}

View File

@@ -21,7 +21,8 @@ class Customer extends Model
public function contracts() public function contracts()
{ {
return $this->belongsToMany(Contract::class, 'contract_customers') return $this->belongsToMany(Contract::class, 'contract_customers')
->withPivot('role', 'transfer_order') ->using(ContractCustomer::class)
->withPivot('id', 'role', 'transfer_order')
->withTimestamps(); ->withTimestamps();
} }
} }

View File

@@ -11,12 +11,17 @@ class Project extends Model
use HasUuids, HasFactory; use HasUuids, HasFactory;
protected $guarded = []; protected $guarded = [];
public function products() public function products()
{ {
return $this->hasMany(Product::class); return $this->hasMany(Product::class);
} }
public function paymentTemplate()
{
return $this->belongsTo(PaymentTemplate::class);
}
public function paymentTemplates() public function paymentTemplates()
{ {
return $this->hasMany(PaymentTemplate::class); return $this->hasMany(PaymentTemplate::class);

0
artisan Normal file → Executable file
View File

View File

@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Schema;
return new class extends Migration { return new class extends Migration {
public function up(): void { public function up(): void {
Schema::create('contract_customers', function (Blueprint $table) { Schema::create('contract_customers', function (Blueprint $table) {
$table->id(); $table->uuid('id')->primary();
$table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete();
$table->foreignUuid('customer_id')->constrained('customers')->cascadeOnDelete(); $table->foreignUuid('customer_id')->constrained('customers')->cascadeOnDelete();
$table->string('role')->default('CHỦ SH 1'); // Đồng sở hữu $table->string('role')->default('CHỦ SH 1'); // Đồng sở hữu

View File

@@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::table('projects', function (Blueprint $table) {
$table->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']);
});
}
};

View File

@@ -2,6 +2,8 @@
namespace Database\Seeders; namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Project; use App\Models\Project;
use App\Models\Product; use App\Models\Product;
use App\Models\Customer; use App\Models\Customer;
@@ -9,144 +11,124 @@ use App\Models\Contract;
use App\Models\PaymentTemplate; use App\Models\PaymentTemplate;
use App\Models\PaymentSchedule; use App\Models\PaymentSchedule;
use App\Models\PaymentScheduleItem; use App\Models\PaymentScheduleItem;
use App\Models\Payment;
use App\Models\Appendix;
use App\Models\Settlement;
use App\Enums\ProductType; use App\Enums\ProductType;
use App\Enums\PaymentType; use App\Enums\PaymentType;
use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class TestDataSeeder extends Seeder class TestDataSeeder extends Seeder
{ {
public function run(): void public function run(): void
{ {
DB::transaction(function () { // 1. Tạo Tài khoản Admin
// 1. PROJECT User::updateOrCreate(
$sth03 = Project::factory()->create(['code' => 'STH03', 'name' => 'Khu Riverside STH03']); ['email' => 'admin@phuongtc.com'],
$diamond = Project::factory()->create(['code' => 'DIA21', 'name' => 'Diamond Luxury Suites']); [
'name' => 'chanphuong',
'password' => Hash::make('1Qazxsw2@!321'),
]
);
// 2. PAYMENT TEMPLATES // 2. Tạo Dự án
$templateStandard = PaymentTemplate::create([ $project = Project::updateOrCreate(
'project_id' => $sth03->id, ['code' => 'STH03'],
'name' => 'Thanh toán chuẩn STH03', [
'is_default' => true 'name' => 'Khu đô thị Mỹ Gia - Gói 3',
]); 'type' => 'Khu đô thị'
]
);
$this->createTemplateItems($templateStandard); // 3. Tạo Mẫu thanh toán
$template = PaymentTemplate::create([
// 3. PRODUCTS 'project_id' => $project->id,
$this->seedProducts($sth03, $diamond); 'name' => 'Mẫu chuẩn Đất nền 30-40-30'
// 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]
]); ]);
$c2 = Contract::create([ $project->update(['payment_template_id' => $template->id]);
'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' // 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]); PaymentScheduleItem::create([
'template_id' => $template->id,
$product->update(['status' => 'Đã bán']); 'installment_no' => 2,
} 'type' => PaymentType::THAN,
'percentage' => 40,
private function seedComplexPayments($project, $customers, $template) 'days_after_previous' => 60,
{ ]);
$product = Product::where('code', 'STH03.02')->first(); PaymentScheduleItem::create([
$contract = Contract::create([ 'template_id' => $template->id,
'product_id' => $product->id, 'transfer_order' => 0, 'contract_type' => 'HĐMB', 'contract_number' => 'HD-PAY-DEBUG', 'installment_no' => 3,
'signing_date' => Carbon::now()->subDays(10), 'total_value' => $product->total_price, 'status' => 'Đang hiệu lực' '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]); // 5. Tạo Sản phẩm
$items = $template->items; Product::create([
foreach($items as $item) { 'project_id' => $project->id,
$si = PaymentScheduleItem::create([ 'product_type' => ProductType::LAND,
'schedule_id' => $schedule->id, 'installment_no' => $item->installment_no, 'percentage' => $item->percentage, 'code' => 'STH03.01',
'amount' => $contract->total_value * ($item->percentage / 100), 'type' => $item->type, 'due_date' => Carbon::now()->addDays(30 * $item->installment_no) 'area' => 100,
]); 'price_per_unit' => 25000000,
'total_price' => 2500000000,
'status' => 'Đang mở bán',
]);
if ($item->installment_no == 1) { $productSold = Product::create([
$required = $si->amount; 'project_id' => $project->id,
$paid = $required * 1.2; 'product_type' => ProductType::LAND,
Payment::create(['contract_id' => $contract->id, 'schedule_item_id' => $si->id, 'amount' => $paid, 'paid_date' => Carbon::now(), 'method' => 'Chuyển khoản']); 'code' => 'STH03.02',
'area' => 100,
$contract->update([ 'price_per_unit' => 25000000,
'paid_amount' => $paid, 'total_price' => 2500000000,
'excess_amount' => $paid - $required, 'status' => 'Đã bán',
'remaining_amount' => $contract->total_value - $paid ]);
]);
} // 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),
]);
} }
} }

28
moduledesign.md Normal file
View File

@@ -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)

241
prisma2.md Normal file
View File

@@ -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])
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Tests\Feature;
use App\Filament\Resources\Contracts\ContractResource;
use App\Models\Contract;
use App\Models\Customer;
use App\Models\PaymentSchedule;
use App\Models\PaymentScheduleItem;
use App\Models\PaymentTemplate;
use App\Models\Product;
use App\Models\Project;
use App\Models\User;
use Carbon\Carbon;
beforeEach(function () {
$this->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'));
});