Xu ly giao dien San Pham
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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]; }
|
||||||
|
|||||||
@@ -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()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
app/Models/ContractCustomer.php
Normal file
19
app/Models/ContractCustomer.php
Normal 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 = [];
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,11 @@ class Project extends Model
|
|||||||
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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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ị'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->createTemplateItems($templateStandard);
|
$project->update(['payment_template_id' => $template->id]);
|
||||||
|
|
||||||
// 3. PRODUCTS
|
// 4. Tạo các đợt mẫu
|
||||||
$this->seedProducts($sth03, $diamond);
|
PaymentScheduleItem::create([
|
||||||
|
'template_id' => $template->id,
|
||||||
|
'installment_no' => 1,
|
||||||
|
'type' => PaymentType::MONG,
|
||||||
|
'percentage' => 30,
|
||||||
|
'days_after_signing' => 0,
|
||||||
|
]);
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
|
||||||
// 4. CUSTOMERS
|
// 5. Tạo Sản phẩm
|
||||||
$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([
|
Product::create([
|
||||||
'project_id' => $project1->id,
|
'project_id' => $project->id,
|
||||||
'product_type' => ProductType::LAND,
|
'product_type' => ProductType::LAND,
|
||||||
'code' => $project1->code . '.' . sprintf('%02d', $i),
|
'code' => 'STH03.01',
|
||||||
'area' => 100 + $i,
|
'area' => 100,
|
||||||
'price_per_unit' => 50000000,
|
'price_per_unit' => 25000000,
|
||||||
'total_price' => (100 + $i) * 50000000,
|
'total_price' => 2500000000,
|
||||||
'qsdd_value' => ((100 + $i) * 50000000) * 0.4,
|
'status' => 'Đang mở bán',
|
||||||
'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([
|
$productSold = Product::create([
|
||||||
'product_id' => $product->id, 'transfer_order' => 0, 'contract_type' => 'VBCN', 'contract_number' => 'VBCN-F1-99',
|
'project_id' => $project->id,
|
||||||
'signing_date' => Carbon::now(), 'total_value' => $product->total_price * 1.15, 'paid_amount' => 500000000, 'status' => 'Đang hiệu lực'
|
'product_type' => ProductType::LAND,
|
||||||
]);
|
'code' => 'STH03.02',
|
||||||
$customers[1]->contracts()->attach($c2->id, ['role' => 'CHỦ SỞ HỮU', 'transfer_order' => 0]);
|
'area' => 100,
|
||||||
|
'price_per_unit' => 25000000,
|
||||||
$product->update(['status' => 'Đã bán']);
|
'total_price' => 2500000000,
|
||||||
}
|
'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'
|
|
||||||
]);
|
|
||||||
$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)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($item->installment_no == 1) {
|
// 6. Khách hàng & Hợp đồng chuyển nhượng
|
||||||
$required = $si->amount;
|
// Sử dụng cột cmnd_cccd thay cho id_number
|
||||||
$paid = $required * 1.2;
|
$customerA = Customer::create(['full_name' => 'Nguyễn Văn A', 'phone' => '0901234567', 'cmnd_cccd' => '123456789']);
|
||||||
Payment::create(['contract_id' => $contract->id, 'schedule_item_id' => $si->id, 'amount' => $paid, 'paid_date' => Carbon::now(), 'method' => 'Chuyển khoản']);
|
$customerB = Customer::create(['full_name' => 'Trần Thị B', 'phone' => '0907654321', 'cmnd_cccd' => '987654321']);
|
||||||
|
|
||||||
$contract->update([
|
// F1
|
||||||
'paid_amount' => $paid,
|
$contractF1 = Contract::create([
|
||||||
'excess_amount' => $paid - $required,
|
'product_id' => $productSold->id,
|
||||||
'remaining_amount' => $contract->total_value - $paid
|
'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
28
moduledesign.md
Normal 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
241
prisma2.md
Normal 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])
|
||||||
|
}
|
||||||
106
tests/Feature/ContractFinanceFlowTest.php
Normal file
106
tests/Feature/ContractFinanceFlowTest.php
Normal 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'));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user