diff --git a/app/Enums/NavigationGroup.php b/app/Enums/NavigationGroup.php new file mode 100644 index 0000000..1897c7b --- /dev/null +++ b/app/Enums/NavigationGroup.php @@ -0,0 +1,18 @@ +value; + } +} diff --git a/app/Enums/PaymentType.php b/app/Enums/PaymentType.php new file mode 100644 index 0000000..d1b1502 --- /dev/null +++ b/app/Enums/PaymentType.php @@ -0,0 +1,27 @@ + 'Tiền QSDĐ', + self::MONG => 'Tiền Móng', + self::THAN => 'Tiền Thân', + self::CHI_PHI_TC => 'Chi phí thi công', + self::CK => 'Chiết khấu', + self::PHAT => 'Tiền phạt', + self::OTHER => 'Khác', + }; + } +} diff --git a/app/Enums/ProductType.php b/app/Enums/ProductType.php new file mode 100644 index 0000000..2a57e4a --- /dev/null +++ b/app/Enums/ProductType.php @@ -0,0 +1,25 @@ + 'Đất nền', + self::APARTMENT => 'Căn hộ', + self::SHOPHOUSE => 'Shophouse', + self::OFFICE => 'Văn phòng', + self::CONDOTEL => 'Condotel', + self::VILLA => 'Biệt thự', + }; + } +} diff --git a/app/Filament/Resources/Contracts/ContractResource.php b/app/Filament/Resources/Contracts/ContractResource.php index da6fc47..eb0178c 100644 --- a/app/Filament/Resources/Contracts/ContractResource.php +++ b/app/Filament/Resources/Contracts/ContractResource.php @@ -4,6 +4,9 @@ namespace App\Filament\Resources\Contracts; use App\Filament\Resources\Contracts\Pages; 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; @@ -12,40 +15,38 @@ 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; class ContractResource extends Resource { protected static ?string $model = Contract::class; protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text'; - protected static string | \UnitEnum | null $navigationGroup = 'Quản lý Giao dịch'; + protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::TRANSACTION->value; + protected static ?int $navigationSort = 4; + + protected static ?string $modelLabel = 'Hợp đồng'; + protected static ?string $pluralModelLabel = 'Hợp đồng'; public static function form(Schema $schema): Schema { return $schema ->components([ - Section::make('Thông tin Liên kết') + Section::make('Liên kết & Mẫu thanh toán') ->columns(2) ->schema([ - Select::make('product_id')->label('Mã Sản phẩm')->relationship('product', 'code')->searchable()->preload()->required(), - Select::make('customers')->label('Khách hàng')->relationship('customers', 'full_name')->multiple()->searchable()->preload()->required(), + 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ợp đồng')->required()->unique(ignoreRecord: true), - Select::make('contract_type')->label('Loại Hợp đồng')->options(['HĐMB' => 'HĐ Mua bán', 'HĐĐC' => 'HĐ Đặt cọc', 'HĐGV' => 'HĐ Góp vốn', 'VBCN' => 'VBCN'])->default('HĐMB'), - DatePicker::make('signing_date')->label('Ngày ký')->displayFormat('d/m/Y'), - TextInput::make('transfer_order')->label('Thứ tự chuyển nhượng')->numeric()->default(0), - Select::make('status')->label('Trạng thái') - ->options(['Đang hiệu lực' => 'Đang hiệu lực', 'Đã thanh lý' => 'Đã thanh lý', 'Đã chuyển nhượng' => 'Đã chuyển nhượng']) - ->default('Đang hiệu lực'), - ]), - Section::make('Tài chính') - ->columns(2) - ->schema([ - TextInput::make('total_value')->label('Tổng giá trị (VND)')->numeric()->required(), - TextInput::make('paid_amount')->label('Đã thanh toán (VND)')->numeric()->default(0), - ]), + TextInput::make('contract_number')->label('Số HĐ')->required(), + DatePicker::make('signing_date')->label('Ngày ký')->required(), + ]) ]); } @@ -54,23 +55,13 @@ class ContractResource extends Resource return $table ->columns([ Tables\Columns\TextColumn::make('contract_number')->label('Số HĐ')->searchable(), - Tables\Columns\TextColumn::make('contract_type')->label('Loại HĐ')->badge(), - Tables\Columns\TextColumn::make('product.code')->label('Mã SP')->searchable(), - Tables\Columns\TextColumn::make('customers.full_name')->label('Khách hàng')->badge(), - Tables\Columns\TextColumn::make('status')->label('Trạng thái') - ->color(fn (string $state): string => match ($state) { - 'Đang hiệu lực' => 'success', - 'Đã thanh lý' => 'warning', - 'Đã chuyển nhượng' => 'gray', - default => 'gray', - })->badge(), - Tables\Columns\TextColumn::make('total_value')->label('Tổng giá trị')->money('VND'), - Tables\Columns\TextColumn::make('paid_amount')->label('Đã TT')->money('VND'), - Tables\Columns\TextColumn::make('remaining_amount')->label('Còn lại')->money('VND')->color('danger'), - ]) - ->defaultSort('created_at', 'desc'); + Tables\Columns\TextColumn::make('product.code')->label('Sản phẩm'), + Tables\Columns\TextColumn::make('total_value')->label('Giá trị')->money('VND'), + ]); } + public static function getRelations(): array { return [ScheduleItemsRelationManager::class]; } + public static function getPages(): array { return [ diff --git a/app/Filament/Resources/Contracts/ContractResource/RelationManagers/ScheduleItemsRelationManager.php b/app/Filament/Resources/Contracts/ContractResource/RelationManagers/ScheduleItemsRelationManager.php new file mode 100644 index 0000000..6990740 --- /dev/null +++ b/app/Filament/Resources/Contracts/ContractResource/RelationManagers/ScheduleItemsRelationManager.php @@ -0,0 +1,56 @@ +components([ + Section::make('Chi tiết đợt thanh toán') + ->columns(2) + ->schema([ + Placeholder::make('installment_no')->label('Đợt số')->content(fn ($record) => $record->installment_no), + Placeholder::make('type')->label('Loại đợt')->content(fn ($record) => $record->type?->getLabel()), + TextInput::make('amount')->label('Số tiền (VND)')->numeric()->required(), + DatePicker::make('due_date')->label('Ngày đến hạn')->required(), + ]), + ]); + } + + public function table(Table $table): Table + { + return $table + ->columns([ + TextColumn::make('installment_no')->label('Đợt')->badge(), + TextColumn::make('type')->label('Loại')->badge(), + TextColumn::make('amount')->label('Số tiền dự kiến')->money('VND'), + TextColumn::make('due_date')->label('Hạn thanh toán')->date('d/m/Y'), + ]) + ->defaultSort('installment_no', 'asc') + ->actions([ + ViewAction::make(), + EditAction::make(), + ]); + } +} diff --git a/app/Filament/Resources/Contracts/Pages/CreateContract.php b/app/Filament/Resources/Contracts/Pages/CreateContract.php index 9852204..f7a08fe 100644 --- a/app/Filament/Resources/Contracts/Pages/CreateContract.php +++ b/app/Filament/Resources/Contracts/Pages/CreateContract.php @@ -3,9 +3,59 @@ namespace App\Filament\Resources\Contracts\Pages; use App\Filament\Resources\Contracts\ContractResource; +use App\Models\PaymentTemplate; +use App\Models\PaymentSchedule; +use App\Models\PaymentScheduleItem; use Filament\Resources\Pages\CreateRecord; +use Carbon\Carbon; class CreateContract extends CreateRecord { protected static string $resource = ContractResource::class; + + protected function afterCreate(): void + { + $contract = $this->record; + $templateId = $this->data['payment_template_id'] ?? null; + + if ($templateId) { + $template = PaymentTemplate::find($templateId); + + // 1. Tạo Schedule cho Hợp đồng + $schedule = PaymentSchedule::create([ + 'contract_id' => $contract->id, + 'template_id' => $template->id, + ]); + + // 2. Clone từng Item từ Template sang Schedule thực tế + $items = $template->items()->orderBy('installment_no')->get(); + $lastDueDate = Carbon::parse($contract->signing_date); + + foreach ($items as $item) { + $dueDate = null; + + // Logic tính ngày đến hạn chuẩn v5.5 + if ($item->days_after_signing !== null) { + $dueDate = Carbon::parse($contract->signing_date)->addDays($item->days_after_signing); + } elseif ($item->days_after_previous !== null) { + $dueDate = $lastDueDate->copy()->addDays($item->days_after_previous); + } elseif ($item->due_date !== null) { + $dueDate = $item->due_date; + } + + PaymentScheduleItem::create([ + 'schedule_id' => $schedule->id, + 'installment_no' => $item->installment_no, + 'type' => $item->type, + 'percentage' => $item->percentage, + 'amount' => $contract->total_value * ($item->percentage / 100), + 'due_date' => $dueDate, + ]); + + if ($dueDate) { + $lastDueDate = $dueDate; + } + } + } + } } diff --git a/app/Filament/Resources/Customers/CustomerResource.php b/app/Filament/Resources/Customers/CustomerResource.php index 0a2ea3a..e0ed15e 100644 --- a/app/Filament/Resources/Customers/CustomerResource.php +++ b/app/Filament/Resources/Customers/CustomerResource.php @@ -13,11 +13,14 @@ use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use App\Enums\NavigationGroup; + class CustomerResource extends Resource { protected static ?string $model = Customer::class; protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-users'; - protected static string | \UnitEnum | null $navigationGroup = 'Quản lý Khách hàng'; + protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::CUSTOMER->value; + protected static ?int $navigationSort = 3; public static function form(Schema $schema): Schema { diff --git a/app/Filament/Resources/PaymentTemplateResource.php b/app/Filament/Resources/PaymentTemplateResource.php new file mode 100644 index 0000000..27ce155 --- /dev/null +++ b/app/Filament/Resources/PaymentTemplateResource.php @@ -0,0 +1,70 @@ +value; + protected static ?int $navigationSort = 1; + + protected static ?string $modelLabel = 'Mẫu thanh toán'; + protected static ?string $pluralModelLabel = 'Mẫu thanh toán'; + + public static function form(Schema $schema): Schema + { + return $schema + ->components([ + Section::make('Thông tin mẫu') + ->columns(2) + ->schema([ + Select::make('project_id')->label('Dự án')->relationship('project', 'name')->required(), + TextInput::make('name')->label('Tên mẫu')->required(), + ]), + Section::make('Chi tiết đợt') + ->schema([ + Repeater::make('items') + ->label('Danh sách đợt') + ->relationship('items') + ->schema([ + TextInput::make('installment_no')->label('Đợt')->numeric()->required(), + Select::make('type')->label('Loại')->options(PaymentType::class)->required(), + TextInput::make('percentage')->label('%')->numeric()->required(), + ])->columns(3), + ]) + ]); + } + + public static function table(Table $table): Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('project.name')->label('Dự án'), + Tables\Columns\TextColumn::make('name')->label('Tên mẫu'), + ]); + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPaymentTemplates::route('/'), + 'create' => Pages\CreatePaymentTemplate::route('/create'), + 'edit' => Pages\EditPaymentTemplate::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/PaymentTemplateResource/Pages/CreatePaymentTemplate.php b/app/Filament/Resources/PaymentTemplateResource/Pages/CreatePaymentTemplate.php new file mode 100644 index 0000000..b72a3e1 --- /dev/null +++ b/app/Filament/Resources/PaymentTemplateResource/Pages/CreatePaymentTemplate.php @@ -0,0 +1,11 @@ +value; + protected static ?int $navigationSort = 2; + + protected static ?string $modelLabel = 'Sản phẩm'; + protected static ?string $pluralModelLabel = 'Sản phẩm'; public static function form(Schema $schema): Schema { return $schema ->components([ - Section::make('Liên kết Dự án & Phân loại') - ->columns(2) - ->schema([ - Select::make('project_id') - ->label('Thuộc Dự án') - ->relationship('project', 'name') - ->searchable()->preload()->required(), - Select::make('product_type') - ->label('Loại sản phẩm') - ->options([ - 'LAND' => 'Đất nền', - 'APARTMENT' => 'Căn hộ chung cư', - 'SHOPHOUSE' => 'Shophouse', - ])->required()->live(), - ]), - - Section::make('Thông tin định danh & Diện tích') - ->columns(3) - ->schema([ - TextInput::make('code')->label('Mã Sản Phẩm')->required()->unique(ignoreRecord: true), - TextInput::make('area')->label('Diện tích (m2)')->numeric()->required()->live(onBlur: true) - ->afterStateUpdated(function (Get $get, $state, $set) { - $price = (float) $get('price_per_unit'); - if ($state && $price) $set('total_price', (float) $state * $price); - }), - Select::make('red_book_status')->label('Sổ đỏ') - ->options(['Chưa có dữ liệu' => 'Chưa có dữ liệu', 'Đã có sổ' => 'Đã có sổ', 'Đang chờ sổ' => 'Đang chờ sổ']) - ->default('Chưa có dữ liệu'), - ]), - - Section::make('Thông tin Tài chính') - ->columns(2) - ->schema([ - TextInput::make('price_per_unit')->label('Đơn giá (VND/m2)')->numeric()->live(onBlur: true) - ->afterStateUpdated(function (Get $get, $state, $set) { - $area = (float) $get('area'); - if ($state && $area) $set('total_price', (float) $state * $area); - }), - TextInput::make('total_price')->label('Tổng giá trị niêm yết')->numeric()->required(), - ]), - - Section::make('Thông tin chi tiết') - ->columns(2) - ->schema([ - TextInput::make('custom_data.block')->label('Block/Tòa')->visible(fn (Get $get) => $get('product_type') === 'APARTMENT'), - TextInput::make('custom_data.floor')->label('Tầng')->numeric()->visible(fn (Get $get) => $get('product_type') === 'APARTMENT'), - TextInput::make('custom_data.view')->label('Hướng View')->visible(fn (Get $get) => $get('product_type') === 'APARTMENT'), - TextInput::make('custom_data.road_width')->label('Đường (m)')->visible(fn (Get $get) => in_array($get('product_type'), ['LAND', 'SHOPHOUSE'])), - TextInput::make('custom_data.frontage')->label('Số mặt tiền')->numeric()->visible(fn (Get $get) => in_array($get('product_type'), ['LAND', 'SHOPHOUSE'])), - ]), - - Section::make('Trạng thái kinh doanh') - ->schema([ - Select::make('status') - ->label('Tình trạng rổ hàng') - ->options(['Đang mở bán' => 'Đang mở bán', 'Đã giữ chỗ' => 'Đã giữ chỗ', 'Đã cọc' => 'Đã cọc', 'Đã bán' => 'Đã bán', 'Tạm khóa' => 'Tạm khóa']) - ->default('Đang mở bán')->required(), - ]), - - // Đã loại bỏ phần Lịch sử Giao dịch nhúng thủ công tại đây để tránh trùng lặp + 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() ]); } @@ -91,26 +61,15 @@ class ProductResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('project.name')->label('Dự án')->sortable()->searchable(), - Tables\Columns\TextColumn::make('code')->label('Mã SP')->searchable()->sortable(), - Tables\Columns\TextColumn::make('contracts_count')->label('Số HĐ')->counts('contracts')->badge()->color('info'), + 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á niêm yết')->money('VND')->sortable(), - Tables\Columns\SelectColumn::make('status')->label('Trạng thái') - ->options(['Đang mở bán' => 'Đang mở bán', 'Đã giữ chỗ' => 'Đã giữ chỗ', 'Đã cọc' => 'Đã cọc', 'Đã bán' => 'Đã bán', 'Tạm khóa' => 'Tạm khóa']), - ]) - ->filters([ - Tables\Filters\SelectFilter::make('project_id')->label('Dự án')->relationship('project', 'name'), - Tables\Filters\SelectFilter::make('product_type')->options(['LAND' => 'Đất nền', 'APARTMENT' => 'Căn hộ']), + 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]; } public static function getPages(): array { diff --git a/app/Filament/Resources/Projects/ProjectResource.php b/app/Filament/Resources/Projects/ProjectResource.php index 08f0033..0bbeb8c 100644 --- a/app/Filament/Resources/Projects/ProjectResource.php +++ b/app/Filament/Resources/Projects/ProjectResource.php @@ -11,12 +11,17 @@ use Filament\Schemas\Schema; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use App\Enums\NavigationGroup; class ProjectResource extends Resource { protected static ?string $model = Project::class; protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2'; - protected static string | \UnitEnum | null $navigationGroup = 'Quản lý Dự án'; + protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::PROJECT->value; + protected static ?int $navigationSort = 1; + + protected static ?string $modelLabel = 'Dự án'; + protected static ?string $pluralModelLabel = 'Dự án'; public static function form(Schema $schema): Schema { @@ -25,9 +30,16 @@ class ProjectResource extends Resource Section::make('Thông tin Dự án') ->columns(2) ->schema([ - TextInput::make('name')->label('Tên Dự án')->required()->maxLength(255), - Select::make('type')->label('Loại hình')->options(['Khu đô thị' => 'Khu đô thị', 'Chung cư' => 'Chung cư', 'Đất nền phân lô' => 'Đất nền phân lô', 'Khu nghỉ dưỡng' => 'Khu nghỉ dưỡng'])->required(), - TextInput::make('address')->label('Địa chỉ chi tiết')->columnSpanFull(), + TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true), + TextInput::make('name')->label('Tên Dự án')->required(), + Select::make('type') + ->label('Loại hình') + ->options([ + 'Khu đô thị' => 'Khu đô thị', + 'Chung cư' => 'Chung cư', + 'Đất nền phân lô' => 'Đất nền phân lô', + ])->required(), + TextInput::make('address')->label('Địa chỉ')->columnSpanFull(), ]) ]); } @@ -36,12 +48,10 @@ class ProjectResource extends Resource { return $table ->columns([ - Tables\Columns\TextColumn::make('name')->label('Tên Dự án')->searchable()->sortable(), - Tables\Columns\TextColumn::make('type')->label('Loại hình')->badge()->sortable(), - Tables\Columns\TextColumn::make('address')->label('Địa chỉ')->limit(50), - Tables\Columns\TextColumn::make('created_at')->label('Ngày tạo')->dateTime('d/m/Y')->sortable()->toggleable(isToggledHiddenByDefault: true), - ]) - ->defaultSort('created_at', 'desc'); + Tables\Columns\TextColumn::make('code')->label('Mã')->sortable(), + Tables\Columns\TextColumn::make('name')->label('Tên Dự án')->searchable(), + Tables\Columns\TextColumn::make('type')->label('Loại hình')->badge(), + ]); } public static function getPages(): array diff --git a/app/Models/Appendix.php b/app/Models/Appendix.php new file mode 100644 index 0000000..5d5fb6c --- /dev/null +++ b/app/Models/Appendix.php @@ -0,0 +1,31 @@ + 'array', + 'signing_date' => 'date', + ]; + + public function contract() + { + return $this->belongsTo(Contract::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } +} diff --git a/app/Models/Contract.php b/app/Models/Contract.php index 322d0dc..ebc30d2 100644 --- a/app/Models/Contract.php +++ b/app/Models/Contract.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Contract extends Model { @@ -12,15 +13,59 @@ class Contract extends Model protected $guarded = []; - // 1 Hợp đồng thuộc về 1 Sản phẩm + protected $casts = [ + 'metadata' => 'array', + 'total_value' => 'decimal:2', + 'paid_amount' => 'decimal:2', + 'remaining_amount' => 'decimal:2', + 'excess_amount' => 'decimal:2', + 'signing_date' => 'date', + ]; + public function product() { return $this->belongsTo(Product::class); } - // 1 Hợp đồng có thể có nhiều Khách hàng (Đồng sở hữu) public function customers() { - return $this->belongsToMany(Customer::class, 'contract_customers')->withPivot('role', 'transfer_order')->withTimestamps(); + return $this->belongsToMany(Customer::class, 'contract_customers') + ->withPivot('role', 'transfer_order') + ->withTimestamps(); } -} \ No newline at end of file + + public function appendices() + { + return $this->hasMany(Appendix::class); + } + + public function paymentSchedule() + { + return $this->hasOne(PaymentSchedule::class); + } + + /** + * Lấy trực tiếp các đợt thanh toán của hợp đồng này + */ + public function scheduleItems(): HasManyThrough + { + return $this->hasManyThrough( + PaymentScheduleItem::class, + PaymentSchedule::class, + 'contract_id', // Khóa ngoại trên bảng PaymentSchedule + 'schedule_id', // Khóa ngoại trên bảng PaymentScheduleItem + 'id', // Khóa chính trên bảng Contract + 'id' // Khóa chính trên bảng PaymentSchedule + ); + } + + public function payments() + { + return $this->hasMany(Payment::class); + } + + public function paymentFines() + { + return $this->hasMany(PaymentFine::class); + } +} diff --git a/app/Models/Payment.php b/app/Models/Payment.php new file mode 100644 index 0000000..cfdb1ab --- /dev/null +++ b/app/Models/Payment.php @@ -0,0 +1,30 @@ + 'decimal:2', + 'paid_date' => 'date', + 'metadata' => 'array', + ]; + + public function contract() + { + return $this->belongsTo(Contract::class); + } + + public function scheduleItem() + { + return $this->belongsTo(PaymentScheduleItem::class, 'schedule_item_id'); + } +} diff --git a/app/Models/PaymentFine.php b/app/Models/PaymentFine.php new file mode 100644 index 0000000..39258c1 --- /dev/null +++ b/app/Models/PaymentFine.php @@ -0,0 +1,25 @@ + 'decimal:2', + 'due_date' => 'date', + 'paid_date' => 'date', + ]; + + public function contract() + { + return $this->belongsTo(Contract::class); + } +} diff --git a/app/Models/PaymentSchedule.php b/app/Models/PaymentSchedule.php new file mode 100644 index 0000000..f7f5c5e --- /dev/null +++ b/app/Models/PaymentSchedule.php @@ -0,0 +1,29 @@ +belongsTo(Contract::class); + } + + public function template() + { + return $this->belongsTo(PaymentTemplate::class); + } + + public function items() + { + return $this->hasMany(PaymentScheduleItem::class, 'schedule_id'); + } +} diff --git a/app/Models/PaymentScheduleItem.php b/app/Models/PaymentScheduleItem.php new file mode 100644 index 0000000..b690d45 --- /dev/null +++ b/app/Models/PaymentScheduleItem.php @@ -0,0 +1,37 @@ + PaymentType::class, + 'amount' => 'decimal:2', + 'percentage' => 'decimal:2', + 'due_date' => 'date', + ]; + + public function template() + { + return $this->belongsTo(PaymentTemplate::class); + } + + public function schedule() + { + return $this->belongsTo(PaymentSchedule::class); + } + + public function payments() + { + return $this->hasMany(Payment::class, 'schedule_item_id'); + } +} diff --git a/app/Models/PaymentTemplate.php b/app/Models/PaymentTemplate.php new file mode 100644 index 0000000..ff2e6b1 --- /dev/null +++ b/app/Models/PaymentTemplate.php @@ -0,0 +1,28 @@ + 'boolean', + ]; + + public function project() + { + return $this->belongsTo(Project::class); + } + + public function items() + { + return $this->hasMany(PaymentScheduleItem::class, 'template_id'); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php index f35f51e..2d23b70 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Enums\ProductType; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -12,20 +13,33 @@ class Product extends Model protected $guarded = []; - // Ép kiểu JSON để Filament có thể đọc/ghi dữ liệu linh hoạt (tầng, hướng, mặt tiền...) protected $casts = [ + 'product_type' => ProductType::class, 'custom_data' => 'array', + 'infrastructure_status' => 'array', + 'area' => 'decimal:2', + 'price_per_unit' => 'decimal:2', + 'total_price' => 'decimal:2', + 'building_density' => 'decimal:2', ]; - // Khai báo mối quan hệ V3: 1 Sản phẩm thuộc về 1 Dự án public function project() { return $this->belongsTo(Project::class); } - // Đón đầu cho các bước tiếp theo: 1 Sản phẩm có nhiều Hợp đồng public function contracts() { return $this->hasMany(Contract::class); } -} \ No newline at end of file + + public function settlements() + { + return $this->hasMany(Settlement::class); + } + + public function appendices() + { + return $this->hasMany(Appendix::class); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php index 9de8692..c7a43ed 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -10,11 +10,15 @@ class Project extends Model { use HasUuids, HasFactory; - protected $guarded = []; // Cho phép lưu tất cả các cột + protected $guarded = []; - // Khai báo mối quan hệ: 1 Dự án có nhiều Sản phẩm public function products() { return $this->hasMany(Product::class); } -} \ No newline at end of file + + public function paymentTemplates() + { + return $this->hasMany(PaymentTemplate::class); + } +} diff --git a/app/Models/Settlement.php b/app/Models/Settlement.php new file mode 100644 index 0000000..8295619 --- /dev/null +++ b/app/Models/Settlement.php @@ -0,0 +1,26 @@ + 'decimal:2', + 'final_value' => 'decimal:2', + 'difference' => 'decimal:2', + 'issue_date' => 'date', + ]; + + public function product() + { + return $this->belongsTo(Product::class); + } +} diff --git a/database/factories/ProjectFactory.php b/database/factories/ProjectFactory.php index 01c2f9b..e2d8ab3 100644 --- a/database/factories/ProjectFactory.php +++ b/database/factories/ProjectFactory.php @@ -12,9 +12,8 @@ class ProjectFactory extends Factory public function definition(): array { return [ - 'name' => 'Khu đô thị HQLand ' . $this->faker->unique()->city(), - 'type' => $this->faker->randomElement(['Khu đô thị', 'Chung cư', 'Đất nền phân lô']), - 'address' => $this->faker->address(), + 'code' => $this->faker->unique()->regexify('[A-Z]{3}[0-9]{2}'), + 'name' => 'Khu đô thị ' . $this->faker->city(), ]; } } diff --git a/database/migrations/2025_01_01_000000_create_projects_table.php b/database/migrations/2025_01_01_000000_create_projects_table.php index 7adfb2d..9b6539e 100644 --- a/database/migrations/2025_01_01_000000_create_projects_table.php +++ b/database/migrations/2025_01_01_000000_create_projects_table.php @@ -1,16 +1,17 @@ uuid('id')->primary(); + $table->string('code')->unique(); // STH03 $table->string('name'); - $table->string('type'); - $table->string('address')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('projects'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_01_02_000000_create_products_table.php b/database/migrations/2025_01_02_000000_create_products_table.php index 128bf12..0a883fa 100644 --- a/database/migrations/2025_01_02_000000_create_products_table.php +++ b/database/migrations/2025_01_02_000000_create_products_table.php @@ -1,22 +1,47 @@ uuid('id')->primary(); $table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete(); - $table->string('product_type'); - $table->string('code')->unique(); - $table->decimal('area', 10, 2); - $table->decimal('price_per_unit', 15, 2)->nullable(); + $table->string('product_type'); // LAND, APARTMENT, SHOPHOUSE... + + // === TRƯỜNG CHUNG === + $table->string('code')->unique(); // Khu + Lô (STH03.01) + $table->decimal('area', 12, 2); + $table->decimal('price_per_unit', 15, 2); $table->decimal('total_price', 15, 2); - $table->jsonb('custom_data')->nullable(); // Chứa thông tin tầng, view, hướng... + + // === TÀI CHÍNH BỔ SUNG === + $table->decimal('qsdd_value', 15, 2)->default(0); + $table->decimal('foundation_temp_value', 15, 2)->default(0); + $table->decimal('contract_temp_value', 15, 2)->default(0); + + // === KỸ THUẬT & XÂY DỰNG === + $table->string('adjacent_road')->nullable(); + $table->integer('frontage_count')->nullable(); + $table->integer('max_floors')->nullable(); + $table->decimal('building_density', 5, 2)->nullable(); + $table->string('construction_status')->nullable(); + + // === HẠ TẦNG (NESTED) === + $table->text('infrastructure_raw_text')->nullable(); + $table->jsonb('infrastructure_status')->nullable(); + + // === TRẠNG THÁI === $table->string('status')->default('Đang mở bán'); $table->string('red_book_status')->default('Chưa có dữ liệu'); + + // === LINH HOẠT === + $table->jsonb('custom_data')->nullable(); + $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('products'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_01_03_000000_create_customers_table.php b/database/migrations/2025_01_03_000000_create_customers_table.php index 49da4f1..129206d 100644 --- a/database/migrations/2025_01_03_000000_create_customers_table.php +++ b/database/migrations/2025_01_03_000000_create_customers_table.php @@ -1,7 +1,9 @@ string('full_name'); $table->string('phone')->nullable(); $table->string('email')->nullable(); - $table->jsonb('address')->nullable(); // Lưu cấu trúc: số nhà, phường, quận... + $table->jsonb('address')->nullable(); $table->date('dob')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('customers'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_01_04_000000_create_contracts_table.php b/database/migrations/2025_01_04_000000_create_contracts_table.php index 0a4be7f..e338cfd 100644 --- a/database/migrations/2025_01_04_000000_create_contracts_table.php +++ b/database/migrations/2025_01_04_000000_create_contracts_table.php @@ -1,7 +1,9 @@ string('status')->default('Đang hiệu lực'); $table->decimal('total_value', 15, 2); $table->decimal('paid_amount', 15, 2)->default(0); - // Cột ảo tự động tính số tiền còn lại, dev không cần query tính toán - $table->decimal('remaining_amount', 15, 2)->virtualAs('total_value - paid_amount'); + $table->decimal('remaining_amount', 15, 2)->default(0); + $table->decimal('excess_amount', 15, 2)->default(0); // Tiền dư từ đợt trước $table->jsonb('metadata')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('contracts'); } -}; \ No newline at end of file +}; diff --git a/database/migrations/2025_01_06_000000_create_appendices_table.php b/database/migrations/2025_01_06_000000_create_appendices_table.php deleted file mode 100644 index 7e1758c..0000000 --- a/database/migrations/2025_01_06_000000_create_appendices_table.php +++ /dev/null @@ -1,19 +0,0 @@ -uuid('id')->primary(); - $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); - $table->foreignUuid('product_id')->constrained('products')->cascadeOnDelete(); - $table->string('type'); - $table->integer('apply_from_order')->default(0); - $table->date('signing_date')->nullable(); - $table->jsonb('custom_data')->nullable(); - $table->timestamps(); - }); - } - public function down(): void { Schema::dropIfExists('appendices'); } -}; \ No newline at end of file diff --git a/database/migrations/2025_01_07_000000_create_settlements_table.php b/database/migrations/2025_01_07_000000_create_settlements_table.php deleted file mode 100644 index 951ad18..0000000 --- a/database/migrations/2025_01_07_000000_create_settlements_table.php +++ /dev/null @@ -1,20 +0,0 @@ -uuid('id')->primary(); - $table->foreignUuid('product_id')->constrained('products')->cascadeOnDelete(); - $table->string('type'); // "MÓNG", "THÂN", "CP THI CÔNG" - $table->decimal('temp_value', 15, 2)->default(0); - $table->decimal('final_value', 15, 2)->default(0); - $table->decimal('difference', 15, 2)->virtualAs('final_value - temp_value'); - $table->string('red_book_status')->nullable(); - $table->date('issue_date')->nullable(); - $table->timestamps(); - }); - } - public function down(): void { Schema::dropIfExists('settlements'); } -}; \ No newline at end of file diff --git a/database/migrations/2025_01_08_000000_create_payments_table.php b/database/migrations/2025_01_08_000000_create_payments_table.php deleted file mode 100644 index 316ca88..0000000 --- a/database/migrations/2025_01_08_000000_create_payments_table.php +++ /dev/null @@ -1,22 +0,0 @@ -uuid('id')->primary(); - $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); - $table->string('payment_type'); - $table->integer('installment_no')->default(1); - $table->decimal('amount', 15, 2); - $table->date('due_date')->nullable(); - $table->date('paid_date')->nullable(); - $table->string('status')->default('PENDING'); // PENDING, PAID, OVERDUE, CANCELLED - $table->string('receipt_number')->nullable(); - $table->jsonb('metadata')->nullable(); - $table->timestamps(); - }); - } - public function down(): void { Schema::dropIfExists('payments'); } -}; \ No newline at end of file diff --git a/database/migrations/2025_01_10_000000_create_finance_module_tables.php b/database/migrations/2025_01_10_000000_create_finance_module_tables.php new file mode 100644 index 0000000..dd37817 --- /dev/null +++ b/database/migrations/2025_01_10_000000_create_finance_module_tables.php @@ -0,0 +1,76 @@ +uuid('id')->primary(); + $table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete(); + $table->string('name'); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + + // Lịch trình thanh toán + Schema::create('payment_schedules', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); + $table->foreignUuid('template_id')->constrained('payment_templates')->cascadeOnDelete(); + $table->timestamps(); + }); + + // Chi tiết từng đợt thanh toán + Schema::create('payment_schedule_items', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('template_id')->nullable()->constrained('payment_templates')->cascadeOnDelete(); + $table->foreignUuid('schedule_id')->nullable()->constrained('payment_schedules')->cascadeOnDelete(); + $table->integer('installment_no'); + $table->decimal('amount', 15, 2)->nullable(); + $table->decimal('percentage', 5, 2)->nullable(); + + // --- LOGIC NGÀY ĐẾN HẠN --- + $table->integer('days_after_signing')->nullable(); // Cách 1: X ngày sau ngày ký + $table->integer('days_after_previous')->nullable(); // Cách 2: X ngày sau đợt trước + $table->date('due_date')->nullable(); // Cách 3: Ngày chính xác + + $table->string('type'); // QSDD, MONG, THAN... + $table->timestamps(); + }); + + // Phiếu thu thực tế + Schema::create('payments', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); + $table->foreignUuid('schedule_item_id')->nullable()->constrained('payment_schedule_items')->nullOnDelete(); + $table->decimal('amount', 15, 2); + $table->date('paid_date'); + $table->string('receipt_number')->nullable(); + $table->string('method')->default('Chuyển khoản'); + $table->jsonb('metadata')->nullable(); + $table->timestamps(); + }); + + // Tiền phạt chậm nộp + Schema::create('payment_fines', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); + $table->decimal('amount', 15, 2); + $table->string('reason'); + $table->date('due_date'); + $table->date('paid_date')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void { + Schema::dropIfExists('payment_fines'); + Schema::dropIfExists('payments'); + Schema::dropIfExists('payment_schedule_items'); + Schema::dropIfExists('payment_schedules'); + Schema::dropIfExists('payment_templates'); + } +}; diff --git a/database/migrations/2025_01_11_000000_create_appendix_and_settlement_tables.php b/database/migrations/2025_01_11_000000_create_appendix_and_settlement_tables.php new file mode 100644 index 0000000..acb7c40 --- /dev/null +++ b/database/migrations/2025_01_11_000000_create_appendix_and_settlement_tables.php @@ -0,0 +1,39 @@ +uuid('id')->primary(); + $table->foreignUuid('contract_id')->constrained('contracts')->cascadeOnDelete(); + $table->foreignUuid('product_id')->constrained('products')->cascadeOnDelete(); + $table->string('type'); + $table->integer('apply_from_order'); // Kế thừa từ CN số mấy + $table->date('signing_date'); + $table->jsonb('custom_data')->nullable(); + $table->timestamps(); + }); + + // Quyết toán & Sổ đỏ + Schema::create('settlements', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('product_id')->constrained('products')->cascadeOnDelete(); + $table->string('type'); // MONG, THAN, CP THI CONG + $table->decimal('temp_value', 15, 2); + $table->decimal('final_value', 15, 2); + $table->decimal('difference', 15, 2); + $table->string('red_book_status'); + $table->date('issue_date')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void { + Schema::dropIfExists('settlements'); + Schema::dropIfExists('appendices'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 29f7b2a..01914c1 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,10 +3,6 @@ namespace Database\Seeders; use App\Models\User; -use App\Models\Project; -use App\Models\Product; -use App\Models\Customer; -use App\Models\Contract; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Schema; @@ -15,95 +11,21 @@ class DatabaseSeeder extends Seeder { public function run(): void { - // 1. Xóa sạch dữ liệu cũ + // 1. Dọn dẹp an toàn Schema::disableForeignKeyConstraints(); - Contract::query()->delete(); - Customer::query()->delete(); - Product::query()->delete(); - Project::query()->delete(); + User::query()->delete(); Schema::enableForeignKeyConstraints(); - // 2. Tạo tài khoản Admin mặc định - User::updateOrCreate( - ['email' => 'admin@phuongtc.com'], - [ - 'name' => 'Administrator', - 'password' => Hash::make('1Qazxsw2@!321'), - ] - ); - - // 3. Tạo 1 Dự án cố định và 1 Sản phẩm cố định STH-6535 để test - $specialProject = Project::factory()->create(['name' => 'Dự án HQLand Center']); - $specialProduct = Product::factory()->create([ - 'project_id' => $specialProject->id, - 'code' => 'STH-6535', - 'status' => 'Đang mở bán' + // 2. Tạo tài khoản quản trị Admin + User::create([ + 'name' => 'chanphuong', + 'email' => 'admin@phuongtc.com', + 'password' => Hash::make('1Qazxsw2@!321'), ]); - // 4. Tạo thêm các Dự án và Sản phẩm ngẫu nhiên khác - Project::factory(2) - ->has(Product::factory()->count(15), 'products') - ->create(); - - // 5. Tạo 20 Khách hàng - Customer::factory(20)->create(); - - // 6. Tạo dữ liệu Lịch sử chuyển nhượng cho 10 sản phẩm (bao gồm cả STH-6535) - $transferProducts = Product::limit(10)->get(); - $allCustomers = Customer::all(); - - foreach ($transferProducts as $product) { - $baseValue = $product->total_price; - - // --- Lần 1: Hợp đồng gốc (Mua từ CĐT) --- - $contract1 = Contract::factory()->create([ - 'product_id' => $product->id, - 'contract_type' => 'HĐMB', - 'transfer_order' => 1, - 'total_value' => $baseValue, - 'status' => 'Đã chuyển nhượng', - 'signing_date' => now()->subYears(2), - ]); - $allCustomers->random()->contracts()->attach($contract1->id, ['role' => 'CHỦ CŨ', 'transfer_order' => 1]); - - // --- Lần 2: Chuyển nhượng F1 --- - $valueF1 = $baseValue * 1.1; - $contract2 = Contract::factory()->create([ - 'product_id' => $product->id, - 'contract_type' => 'VBCN', - 'transfer_order' => 2, - 'total_value' => $valueF1, - 'status' => 'Đã chuyển nhượng', - 'signing_date' => now()->subYear(), - ]); - $allCustomers->random()->contracts()->attach($contract2->id, ['role' => 'CHỦ CŨ', 'transfer_order' => 2]); - - // --- Lần 3: Chủ hiện tại (Sở hữu cuối cùng) --- - $valueFinal = $valueF1 * 1.1; - $contract3 = Contract::factory()->create([ - 'product_id' => $product->id, - 'contract_type' => 'VBCN', - 'transfer_order' => 0, - 'total_value' => $valueFinal, - 'status' => 'Đang hiệu lực', - 'signing_date' => now(), - ]); - $allCustomers->random()->contracts()->attach($contract3->id, ['role' => 'CHỦ SỞ HỮU', 'transfer_order' => 0]); - - // Cập nhật trạng thái sản phẩm cuối cùng - $product->update(['status' => 'Đã bán', 'total_price' => $valueFinal]); - } - - // 7. Tạo thêm 5 hợp đồng lẻ cho các sản phẩm còn lại để đa dạng hóa - $remainingProducts = Product::where('status', 'Đang mở bán')->inRandomOrder()->limit(5)->get(); - foreach ($remainingProducts as $product) { - $contract = Contract::factory()->create([ - 'product_id' => $product->id, - 'transfer_order' => 0, - 'total_value' => $product->total_price, - ]); - $allCustomers->random()->contracts()->attach($contract->id, ['role' => 'CHỦ SỞ HỮU', 'transfer_order' => 0]); - $product->update(['status' => 'Đã bán']); - } + // 3. Gọi bộ nạp dữ liệu Test chuyên sâu + $this->call([ + TestDataSeeder::class, + ]); } } diff --git a/database/seeders/TestDataSeeder.php b/database/seeders/TestDataSeeder.php new file mode 100644 index 0000000..a272b8a --- /dev/null +++ b/database/seeders/TestDataSeeder.php @@ -0,0 +1,152 @@ +create(['code' => 'STH03', 'name' => 'Khu Riverside STH03']); + $diamond = Project::factory()->create(['code' => 'DIA21', 'name' => 'Diamond Luxury Suites']); + + // 2. PAYMENT TEMPLATES + $templateStandard = PaymentTemplate::create([ + 'project_id' => $sth03->id, + 'name' => 'Thanh toán chuẩn STH03', + 'is_default' => true + ]); + + $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] + ]); + + $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' + ]); + $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' + ]); + $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) { + $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 + ]); + } + } + } +} diff --git a/prisma.md b/prisma.md new file mode 100644 index 0000000..45a7755 --- /dev/null +++ b/prisma.md @@ -0,0 +1,276 @@ +**Dành cho AI Agent & Lập trình** + +Dưới đây là **file Prisma Schema đầy đủ, thống nhất và chi tiết nhất** dựa trên tất cả các yêu cầu bạn đã nêu từ đầu đến giờ (bao gồm file Excel, logic chuyển nhượng, tình trạng hạ tầng nested, PaymentSchedule với template, dư/thiếu tiền, v.v.). + +prisma + +``` +// ============================================= +// PRISMA SCHEMA - HỆ THỐNG QUẢN LÝ BẤT ĐỘNG SẢN +// Phiên bản: 2.3 +// Ngày: 18/04/2026 +// Mục đích: Hoàn chỉnh, self-host, hỗ trợ nhiều loại sản phẩm, +// lịch sử chuyển nhượng, phụ lục kế thừa, +// tình trạng hạ tầng nested, và dòng tiền chi tiết. +// ============================================= + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" +} + +// ============================================= +// 1. PROJECT (Khu / Dự án) +// ============================================= +model Project { + id String @id @default(uuid()) + code String @unique // ví dụ: STH03 + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + products Product[] + templates PaymentTemplate[] +} + +// ============================================= +// 2. PRODUCT (Sản phẩm - Đất nền, Căn hộ...) +// ============================================= +model Product { + id String @id @default(uuid()) + projectId String + + productType ProductType + + // === TRƯỜNG CHUNG TỪ FILE sanpham.xlsx === + code String @unique // Khu + Lô (STH03.01) + area Decimal + pricePerUnit Decimal + totalPrice Decimal + qsddValue Decimal + foundationTempValue Decimal + contractTempValue Decimal + adjacentRoad String? + frontageCount Int? + maxFloors Int? + buildingDensity Decimal? + constructionStatus String? + + // === TÌNH TRẠNG HẠ TẦNG (NESTED JSONB) === + infrastructureRawText String? // Giữ nguyên text gốc để backup + infrastructureStatus Json // Cấu trúc nested (hỗ trợ child-of-child) + + // === TRẠNG THÁI SỔ ĐỎ === + redBookStatus String @default("Chưa có dữ liệu") + + // === TRƯỜNG LINH HOẠT CHO SẢN PHẨM MỚI === + customData Json // Block, tầng, hướng, sổ hồng riêng, giấy phép XD... + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project Project @relation(fields: [projectId], references: [id]) + contracts Contract[] + settlements Settlement[] +} + +// ============================================= +// ENUM LOẠI SẢN PHẨM +// ============================================= +enum ProductType { + LAND + APARTMENT + SHOPHOUSE + OFFICE + CONDOTEL + VILLA + // Thêm loại mới ở đây +} + +// ============================================= +// 3. CONTRACT & CHUYỂN NHƯỢNG (Giữ nguyên logic Excel) +// ============================================= +model Contract { + id String @id @default(uuid()) + productId String + transferOrder Int // 0 = KH HIỆN TẠI, 1 = HĐ GỐC, 2+ = VBCN + contractType String // HĐGV, HĐMB, VBCN + contractNumber String + signingDate DateTime + totalValue Decimal + paidAmount Decimal + remainingAmount Decimal + metadata Json? + + product Product @relation(fields: [productId], references: [id]) + customers ContractCustomer[] + appendices Appendix[] + payments Payment[] + schedule PaymentSchedule? + fines PaymentFine[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Customer { + id String @id @default(uuid()) + cmndCccd String @unique + fullName String + phone String + email String? + address Json? + dob DateTime? + + contracts ContractCustomer[] +} + +model ContractCustomer { + id String @id @default(uuid()) + contractId String + customerId String + role String // "CHỦ SH 1", "CHỦ SH 2", "CHỦ SH 3" + transferOrder Int + + contract Contract @relation(fields: [contractId], references: [id]) + customer Customer @relation(fields: [customerId], references: [id]) +} + +// ============================================= +// 4. APPENDIX (Phụ lục) +// ============================================= +model Appendix { + id String @id @default(uuid()) + contractId String + productId String + type String + applyFromOrder Int // Kế thừa từ CN # + signingDate DateTime + customData Json? + + contract Contract @relation(fields: [contractId], references: [id]) +} + +// ============================================= +// 5. SETTLEMENT (Quyết toán & Sổ đỏ) +// ============================================= +model Settlement { + id String @id @default(uuid()) + productId String + type String // MÓNG, THÂN, CP THI CÔNG + tempValue Decimal + finalValue Decimal + difference Decimal + redBookStatus String + issueDate DateTime? + + product Product @relation(fields: [productId], references: [id]) +} + +// ============================================= +// 6. PAYMENT MODULE - DÒNG TIỀN (THEO YÊU CẦU) +// ============================================= +model PaymentTemplate { + id String @id @default(uuid()) + projectId String + name String // "Thanh toán chuẩn 30-30-40", "Trả một lần" + isDefault Boolean @default(false) + + project Project @relation(fields: [projectId], references: [id]) + items PaymentScheduleItem[] + schedules PaymentSchedule[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model PaymentScheduleItem { + id String @id @default(uuid()) + templateId String? + scheduleId String? + + installmentNo Int + amount Decimal? + percentage Decimal? // Ưu tiên % nếu có + + // Hai cách tính ngày đến hạn + daysAfterSigning Int? // Số ngày sau ngày ký hợp đồng + dueDate DateTime? // Ngày chính xác + + type PaymentType + + template PaymentTemplate? @relation(fields: [templateId], references: [id]) + schedule PaymentSchedule? @relation(fields: [scheduleId], references: [id]) + payments Payment[] + + createdAt DateTime @default(now()) +} + +model PaymentSchedule { + id String @id @default(uuid()) + contractId String + templateId String // Template đã chọn khi tạo hợp đồng + + contract Contract @relation(fields: [contractId], references: [id]) + template PaymentTemplate @relation(fields: [templateId], references: [id]) + items PaymentScheduleItem[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Payment { + id String @id @default(uuid()) + contractId String + scheduleItemId String? + + amount Decimal + paidDate DateTime + receiptNumber String? + method String + metadata Json? + + contract Contract @relation(fields: [contractId], references: [id]) + scheduleItem PaymentScheduleItem? @relation(fields: [scheduleItemId], references: [id]) + + createdAt DateTime @default(now()) +} + +model PaymentFine { + id String @id @default(uuid()) + contractId String + amount Decimal + reason String + dueDate DateTime + paidDate DateTime? + + contract Contract @relation(fields: [contractId], references: [id]) +} + +// ============================================= +// ENUMS +// ============================================= +enum PaymentType { + QSDD + MONG + THAN + CHI_PHI_TC + CK + PHAT + OTHER +} +``` + +--- + +**Hướng dẫn sử dụng cho AI Agent / Lập trình:** + +- **Model chính**: Product, Contract, PaymentTemplate, PaymentScheduleItem, PaymentSchedule, Payment. +- **Logic tạo hợp đồng**: Khi tạo Contract → clone PaymentTemplate (mặc định hoặc do nhân viên chọn) thành PaymentSchedule + các PaymentScheduleItem. +- **Dư/thiếu tiền**: Xử lý ở tầng application logic (khi tạo Payment). +- **Tình trạng hạ tầng**: Sử dụng infrastructureStatus JSONB (nested). +- **Dynamic fields**: Sử dụng customData JSONB. diff --git a/taikhoan.txt b/taikhoan.txt index 0f69c67..2f4b58e 100644 --- a/taikhoan.txt +++ b/taikhoan.txt @@ -1,3 +1,8 @@ name: chanphuong -email: admin@phuongt.com -pass: 1Qazxsw2@!321 \ No newline at end of file +email: admin@phuongtc.com +pass: 1Qazxsw2@!321 + + +git add . +git commit -m "Thêm tính năng X" +git push \ No newline at end of file