Đóng gói cuối tuần
This commit is contained in:
18
app/Enums/NavigationGroup.php
Normal file
18
app/Enums/NavigationGroup.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum NavigationGroup: string
|
||||||
|
{
|
||||||
|
case PROJECT = 'Quản lý Dự án';
|
||||||
|
case WAREHOUSE = 'Quản lý Kho';
|
||||||
|
case CUSTOMER = 'Quản lý Khách hàng';
|
||||||
|
case TRANSACTION = 'Quản lý Giao dịch';
|
||||||
|
case FINANCE = 'Quản lý Dòng tiền';
|
||||||
|
case SETTING = 'Cấu hình Hệ thống';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Enums/PaymentType.php
Normal file
27
app/Enums/PaymentType.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PaymentType: string
|
||||||
|
{
|
||||||
|
case QSDD = 'QSDD';
|
||||||
|
case MONG = 'MONG';
|
||||||
|
case THAN = 'THAN';
|
||||||
|
case CHI_PHI_TC = 'CHI_PHI_TC';
|
||||||
|
case CK = 'CK';
|
||||||
|
case PHAT = 'PHAT';
|
||||||
|
case OTHER = 'OTHER';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::QSDD => '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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Enums/ProductType.php
Normal file
25
app/Enums/ProductType.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ProductType: string
|
||||||
|
{
|
||||||
|
case LAND = 'LAND';
|
||||||
|
case APARTMENT = 'APARTMENT';
|
||||||
|
case SHOPHOUSE = 'SHOPHOUSE';
|
||||||
|
case OFFICE = 'OFFICE';
|
||||||
|
case CONDOTEL = 'CONDOTEL';
|
||||||
|
case VILLA = 'VILLA';
|
||||||
|
|
||||||
|
public function getLabel(): string
|
||||||
|
{
|
||||||
|
return match($this) {
|
||||||
|
self::LAND => 'Đấ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ự',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@ namespace App\Filament\Resources\Contracts;
|
|||||||
|
|
||||||
use App\Filament\Resources\Contracts\Pages;
|
use App\Filament\Resources\Contracts\Pages;
|
||||||
use App\Models\Contract;
|
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\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\DatePicker;
|
use Filament\Forms\Components\DatePicker;
|
||||||
@@ -12,40 +15,38 @@ 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;
|
||||||
|
|
||||||
class ContractResource extends Resource
|
class ContractResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Contract::class;
|
protected static ?string $model = Contract::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-document-text';
|
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
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Section::make('Thông tin Liên kết')
|
Section::make('Liên kết & Mẫu thanh toán')
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
Select::make('product_id')->label('Mã Sản phẩm')->relationship('product', 'code')->searchable()->preload()->required(),
|
Select::make('product_id')->label('Sản phẩm')->relationship('product', 'code')->required()->live(),
|
||||||
Select::make('customers')->label('Khách hàng')->relationship('customers', 'full_name')->multiple()->searchable()->preload()->required(),
|
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')
|
Section::make('Chi tiết Hợp đồng')
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('contract_number')->label('Số Hợp đồng')->required()->unique(ignoreRecord: true),
|
TextInput::make('contract_number')->label('Số HĐ')->required(),
|
||||||
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ý')->required(),
|
||||||
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),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,23 +55,13 @@ class ContractResource extends Resource
|
|||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('contract_number')->label('Số HĐ')->searchable(),
|
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('Sản phẩm'),
|
||||||
Tables\Columns\TextColumn::make('product.code')->label('Mã SP')->searchable(),
|
Tables\Columns\TextColumn::make('total_value')->label('Giá trị')->money('VND'),
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array { return [ScheduleItemsRelationManager::class]; }
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\Contracts\ContractResource\RelationManagers;
|
||||||
|
|
||||||
|
use Filament\Resources\RelationManagers\RelationManager;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Actions\ViewAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\DatePicker;
|
||||||
|
use Filament\Forms\Components\Placeholder;
|
||||||
|
|
||||||
|
class ScheduleItemsRelationManager extends RelationManager
|
||||||
|
{
|
||||||
|
protected static string $relationship = 'scheduleItems';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Lịch thanh toán thực tế';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'Đợt thanh toán';
|
||||||
|
protected static ?string $pluralModelLabel = 'Đợt thanh toán';
|
||||||
|
|
||||||
|
public function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->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(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,59 @@
|
|||||||
namespace App\Filament\Resources\Contracts\Pages;
|
namespace App\Filament\Resources\Contracts\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\Contracts\ContractResource;
|
use App\Filament\Resources\Contracts\ContractResource;
|
||||||
|
use App\Models\PaymentTemplate;
|
||||||
|
use App\Models\PaymentSchedule;
|
||||||
|
use App\Models\PaymentScheduleItem;
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
class CreateContract extends CreateRecord
|
class CreateContract extends CreateRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = ContractResource::class;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ use Filament\Resources\Resource;
|
|||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
|
||||||
class CustomerResource extends Resource
|
class CustomerResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Customer::class;
|
protected static ?string $model = Customer::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-users';
|
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
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
|
|||||||
70
app/Filament/Resources/PaymentTemplateResource.php
Normal file
70
app/Filament/Resources/PaymentTemplateResource.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentTemplateResource\Pages;
|
||||||
|
use App\Models\PaymentTemplate;
|
||||||
|
use App\Enums\NavigationGroup;
|
||||||
|
use App\Enums\PaymentType;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class PaymentTemplateResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = PaymentTemplate::class;
|
||||||
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-table-cells';
|
||||||
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::FINANCE->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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentTemplateResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentTemplateResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreatePaymentTemplate extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentTemplateResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentTemplateResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentTemplateResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditPaymentTemplate extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentTemplateResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\PaymentTemplateResource\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\PaymentTemplateResource;
|
||||||
|
use Filament\Actions;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListPaymentTemplates extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = PaymentTemplateResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Actions\CreateAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,14 @@ 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 Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Schemas\Components\Utilities\Get;
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Tabs;
|
||||||
use App\Filament\Resources\Products\ProductResource\RelationManagers\ContractsRelationManager;
|
use App\Filament\Resources\Products\ProductResource\RelationManagers\ContractsRelationManager;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
@@ -18,72 +22,38 @@ class ProductResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Product::class;
|
protected static ?string $model = Product::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-squares-2x2';
|
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-squares-2x2';
|
||||||
protected static string | \UnitEnum | null $navigationGroup = 'Quản lý kho';
|
protected static string | \UnitEnum | null $navigationGroup = NavigationGroup::WAREHOUSE->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
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
->components([
|
->components([
|
||||||
Section::make('Liên kết Dự án & Phân loại')
|
Tabs::make('ProductDetails')
|
||||||
->columns(2)
|
->tabs([
|
||||||
->schema([
|
Tabs\Tab::make('Thông tin chung')
|
||||||
Select::make('project_id')
|
->icon('heroicon-o-information-circle')
|
||||||
->label('Thuộc Dự án')
|
->columns(2)
|
||||||
->relationship('project', 'name')
|
->schema([
|
||||||
->searchable()->preload()->required(),
|
Select::make('project_id')->label('Dự án')->relationship('project', 'name')->required(),
|
||||||
Select::make('product_type')
|
Select::make('product_type')->label('Loại sản phẩm')->options(ProductType::class)->required(),
|
||||||
->label('Loại sản phẩm')
|
TextInput::make('code')->label('Mã SP')->required()->unique(ignoreRecord: true),
|
||||||
->options([
|
Select::make('status')
|
||||||
'LAND' => 'Đất nền',
|
->label('Trạng thái')
|
||||||
'APARTMENT' => 'Căn hộ chung cư',
|
->options(['Đang mở bán' => 'Đang mở bán', 'Đã cọc' => 'Đã cọc', 'Đã bán' => 'Đã bán'])
|
||||||
'SHOPHOUSE' => 'Shophouse',
|
->required(),
|
||||||
])->required()->live(),
|
]),
|
||||||
]),
|
Tabs\Tab::make('Tài chính')
|
||||||
|
->icon('heroicon-o-currency-dollar')
|
||||||
Section::make('Thông tin định danh & Diện tích')
|
->schema([
|
||||||
->columns(3)
|
TextInput::make('area')->label('Diện tích (m2)')->numeric()->required(),
|
||||||
->schema([
|
TextInput::make('price_per_unit')->label('Đơn giá')->numeric()->required(),
|
||||||
TextInput::make('code')->label('Mã Sản Phẩm')->required()->unique(ignoreRecord: true),
|
TextInput::make('total_price')->label('Tổng giá')->numeric()->required(),
|
||||||
TextInput::make('area')->label('Diện tích (m2)')->numeric()->required()->live(onBlur: true)
|
]),
|
||||||
->afterStateUpdated(function (Get $get, $state, $set) {
|
])->columnSpanFull()
|
||||||
$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
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,26 +61,15 @@ class ProductResource extends Resource
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('project.name')->label('Dự án')->sortable()->searchable(),
|
Tables\Columns\TextColumn::make('project.code')->label('Dự án'),
|
||||||
Tables\Columns\TextColumn::make('code')->label('Mã SP')->searchable()->sortable(),
|
Tables\Columns\TextColumn::make('code')->label('Mã SP')->searchable(),
|
||||||
Tables\Columns\TextColumn::make('contracts_count')->label('Số HĐ')->counts('contracts')->badge()->color('info'),
|
|
||||||
Tables\Columns\TextColumn::make('product_type')->label('Loại')->badge(),
|
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\TextColumn::make('total_price')->label('Giá')->money('VND'),
|
||||||
Tables\Columns\SelectColumn::make('status')->label('Trạng thái')
|
Tables\Columns\TextColumn::make('status')->label('Trạng thái')->badge(),
|
||||||
->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ộ']),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getRelations(): array
|
public static function getRelations(): array { return [ContractsRelationManager::class]; }
|
||||||
{
|
|
||||||
return [
|
|
||||||
ContractsRelationManager::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,12 +11,17 @@ 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 App\Enums\NavigationGroup;
|
||||||
|
|
||||||
class ProjectResource extends Resource
|
class ProjectResource extends Resource
|
||||||
{
|
{
|
||||||
protected static ?string $model = Project::class;
|
protected static ?string $model = Project::class;
|
||||||
protected static string | \BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2';
|
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
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
@@ -25,9 +30,16 @@ class ProjectResource extends Resource
|
|||||||
Section::make('Thông tin Dự án')
|
Section::make('Thông tin Dự án')
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('name')->label('Tên Dự án')->required()->maxLength(255),
|
TextInput::make('code')->label('Mã Dự án')->required()->unique(ignoreRecord: true),
|
||||||
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('name')->label('Tên Dự án')->required(),
|
||||||
TextInput::make('address')->label('Địa chỉ chi tiết')->columnSpanFull(),
|
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
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
Tables\Columns\TextColumn::make('name')->label('Tên Dự án')->searchable()->sortable(),
|
Tables\Columns\TextColumn::make('code')->label('Mã')->sortable(),
|
||||||
Tables\Columns\TextColumn::make('type')->label('Loại hình')->badge()->sortable(),
|
Tables\Columns\TextColumn::make('name')->label('Tên Dự án')->searchable(),
|
||||||
Tables\Columns\TextColumn::make('address')->label('Địa chỉ')->limit(50),
|
Tables\Columns\TextColumn::make('type')->label('Loại hình')->badge(),
|
||||||
Tables\Columns\TextColumn::make('created_at')->label('Ngày tạo')->dateTime('d/m/Y')->sortable()->toggleable(isToggledHiddenByDefault: true),
|
]);
|
||||||
])
|
|
||||||
->defaultSort('created_at', 'desc');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
|
|||||||
31
app/Models/Appendix.php
Normal file
31
app/Models/Appendix.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Appendix extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'appendices'; // Đảm bảo khớp với migration
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'custom_data' => 'array',
|
||||||
|
'signing_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function contract()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Contract::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
|||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||||
|
|
||||||
class Contract extends Model
|
class Contract extends Model
|
||||||
{
|
{
|
||||||
@@ -12,15 +13,59 @@ class Contract extends Model
|
|||||||
|
|
||||||
protected $guarded = [];
|
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()
|
public function product()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Product::class);
|
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()
|
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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
app/Models/Payment.php
Normal file
30
app/Models/Payment.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Payment extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'amount' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Models/PaymentFine.php
Normal file
25
app/Models/PaymentFine.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentFine extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
'due_date' => 'date',
|
||||||
|
'paid_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function contract()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Contract::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Models/PaymentSchedule.php
Normal file
29
app/Models/PaymentSchedule.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentSchedule extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public function contract()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Contract::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function template()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PaymentTemplate::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PaymentScheduleItem::class, 'schedule_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Models/PaymentScheduleItem.php
Normal file
37
app/Models/PaymentScheduleItem.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\PaymentType;
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentScheduleItem extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'type' => 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Models/PaymentTemplate.php
Normal file
28
app/Models/PaymentTemplate.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class PaymentTemplate extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_default' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function project()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Project::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PaymentScheduleItem::class, 'template_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\ProductType;
|
||||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -12,20 +13,33 @@ class Product extends Model
|
|||||||
|
|
||||||
protected $guarded = [];
|
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 = [
|
protected $casts = [
|
||||||
|
'product_type' => ProductType::class,
|
||||||
'custom_data' => 'array',
|
'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()
|
public function project()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Project::class);
|
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()
|
public function contracts()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Contract::class);
|
return $this->hasMany(Contract::class);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function settlements()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Settlement::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function appendices()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Appendix::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ class Project extends Model
|
|||||||
{
|
{
|
||||||
use HasUuids, HasFactory;
|
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()
|
public function products()
|
||||||
{
|
{
|
||||||
return $this->hasMany(Product::class);
|
return $this->hasMany(Product::class);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function paymentTemplates()
|
||||||
|
{
|
||||||
|
return $this->hasMany(PaymentTemplate::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
app/Models/Settlement.php
Normal file
26
app/Models/Settlement.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Settlement extends Model
|
||||||
|
{
|
||||||
|
use HasUuids, HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'temp_value' => 'decimal:2',
|
||||||
|
'final_value' => 'decimal:2',
|
||||||
|
'difference' => 'decimal:2',
|
||||||
|
'issue_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function product()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,9 +12,8 @@ class ProjectFactory extends Factory
|
|||||||
public function definition(): array
|
public function definition(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => 'Khu đô thị HQLand ' . $this->faker->unique()->city(),
|
'code' => $this->faker->unique()->regexify('[A-Z]{3}[0-9]{2}'),
|
||||||
'type' => $this->faker->randomElement(['Khu đô thị', 'Chung cư', 'Đất nền phân lô']),
|
'name' => 'Khu đô thị ' . $this->faker->city(),
|
||||||
'address' => $this->faker->address(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
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('projects', function (Blueprint $table) {
|
Schema::create('projects', function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->uuid('id')->primary();
|
||||||
|
$table->string('code')->unique(); // STH03
|
||||||
$table->string('name');
|
$table->string('name');
|
||||||
$table->string('type');
|
|
||||||
$table->string('address')->nullable();
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('projects'); }
|
public function down(): void { Schema::dropIfExists('projects'); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,47 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
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('products', function (Blueprint $table) {
|
Schema::create('products', function (Blueprint $table) {
|
||||||
$table->uuid('id')->primary();
|
$table->uuid('id')->primary();
|
||||||
$table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete();
|
$table->foreignUuid('project_id')->constrained('projects')->cascadeOnDelete();
|
||||||
$table->string('product_type');
|
$table->string('product_type'); // LAND, APARTMENT, SHOPHOUSE...
|
||||||
$table->string('code')->unique();
|
|
||||||
$table->decimal('area', 10, 2);
|
// === TRƯỜNG CHUNG ===
|
||||||
$table->decimal('price_per_unit', 15, 2)->nullable();
|
$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->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('status')->default('Đang mở bán');
|
||||||
$table->string('red_book_status')->default('Chưa có dữ liệu');
|
$table->string('red_book_status')->default('Chưa có dữ liệu');
|
||||||
|
|
||||||
|
// === LINH HOẠT ===
|
||||||
|
$table->jsonb('custom_data')->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('products'); }
|
public function down(): void { Schema::dropIfExists('products'); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
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('customers', function (Blueprint $table) {
|
Schema::create('customers', function (Blueprint $table) {
|
||||||
@@ -10,10 +12,10 @@ return new class extends Migration {
|
|||||||
$table->string('full_name');
|
$table->string('full_name');
|
||||||
$table->string('phone')->nullable();
|
$table->string('phone')->nullable();
|
||||||
$table->string('email')->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->date('dob')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('customers'); }
|
public function down(): void { Schema::dropIfExists('customers'); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
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('contracts', function (Blueprint $table) {
|
Schema::create('contracts', function (Blueprint $table) {
|
||||||
@@ -14,11 +16,11 @@ return new class extends Migration {
|
|||||||
$table->string('status')->default('Đang hiệu lực');
|
$table->string('status')->default('Đang hiệu lực');
|
||||||
$table->decimal('total_value', 15, 2);
|
$table->decimal('total_value', 15, 2);
|
||||||
$table->decimal('paid_amount', 15, 2)->default(0);
|
$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)->default(0);
|
||||||
$table->decimal('remaining_amount', 15, 2)->virtualAs('total_value - paid_amount');
|
$table->decimal('excess_amount', 15, 2)->default(0); // Tiền dư từ đợt trước
|
||||||
$table->jsonb('metadata')->nullable();
|
$table->jsonb('metadata')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public function down(): void { Schema::dropIfExists('contracts'); }
|
public function down(): void { Schema::dropIfExists('contracts'); }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?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::create('appendices', function (Blueprint $table) {
|
|
||||||
$table->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'); }
|
|
||||||
};
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<?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::create('settlements', function (Blueprint $table) {
|
|
||||||
$table->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'); }
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?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::create('payments', function (Blueprint $table) {
|
|
||||||
$table->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'); }
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?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 {
|
||||||
|
// Mẫu thanh toán
|
||||||
|
Schema::create('payment_templates', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?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 {
|
||||||
|
// Phụ lục hợp đồng
|
||||||
|
Schema::create('appendices', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,10 +3,6 @@
|
|||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\User;
|
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\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -15,95 +11,21 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// 1. Xóa sạch dữ liệu cũ
|
// 1. Dọn dẹp an toàn
|
||||||
Schema::disableForeignKeyConstraints();
|
Schema::disableForeignKeyConstraints();
|
||||||
Contract::query()->delete();
|
User::query()->delete();
|
||||||
Customer::query()->delete();
|
|
||||||
Product::query()->delete();
|
|
||||||
Project::query()->delete();
|
|
||||||
Schema::enableForeignKeyConstraints();
|
Schema::enableForeignKeyConstraints();
|
||||||
|
|
||||||
// 2. Tạo tài khoản Admin mặc định
|
// 2. Tạo tài khoản quản trị Admin
|
||||||
User::updateOrCreate(
|
User::create([
|
||||||
['email' => 'admin@phuongtc.com'],
|
'name' => 'chanphuong',
|
||||||
[
|
'email' => 'admin@phuongtc.com',
|
||||||
'name' => 'Administrator',
|
'password' => Hash::make('1Qazxsw2@!321'),
|
||||||
'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'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 4. Tạo thêm các Dự án và Sản phẩm ngẫu nhiên khác
|
// 3. Gọi bộ nạp dữ liệu Test chuyên sâu
|
||||||
Project::factory(2)
|
$this->call([
|
||||||
->has(Product::factory()->count(15), 'products')
|
TestDataSeeder::class,
|
||||||
->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']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
152
database/seeders/TestDataSeeder.php
Normal file
152
database/seeders/TestDataSeeder.php
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Project;
|
||||||
|
use App\Models\Product;
|
||||||
|
use App\Models\Customer;
|
||||||
|
use App\Models\Contract;
|
||||||
|
use App\Models\PaymentTemplate;
|
||||||
|
use App\Models\PaymentSchedule;
|
||||||
|
use App\Models\PaymentScheduleItem;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\Appendix;
|
||||||
|
use App\Models\Settlement;
|
||||||
|
use App\Enums\ProductType;
|
||||||
|
use App\Enums\PaymentType;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class TestDataSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () {
|
||||||
|
// 1. PROJECT
|
||||||
|
$sth03 = Project::factory()->create(['code' => 'STH03', 'name' => 'Khu Riverside STH03']);
|
||||||
|
$diamond = Project::factory()->create(['code' => 'DIA21', 'name' => 'Diamond Luxury Suites']);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
prisma.md
Normal file
276
prisma.md
Normal file
@@ -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.
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
name: chanphuong
|
name: chanphuong
|
||||||
email: admin@phuongt.com
|
email: admin@phuongtc.com
|
||||||
pass: 1Qazxsw2@!321
|
pass: 1Qazxsw2@!321
|
||||||
|
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m "Thêm tính năng X"
|
||||||
|
git push
|
||||||
Reference in New Issue
Block a user