feat: implement project development module, transfer management features, and industrial AVM model integration

This commit is contained in:
Ho Ngoc Hai
2026-04-18 20:34:35 +07:00
parent 0f3b4d7b0d
commit 38b9def99a
66 changed files with 9051 additions and 17 deletions

View File

@@ -0,0 +1,146 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ProjectDevelopmentStatus } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsString,
IsNumber,
IsEnum,
IsOptional,
IsArray,
IsObject,
Min,
Max,
MaxLength,
IsDateString,
} from 'class-validator';
export class CreateProjectDto {
@ApiProperty({ example: 'Vinhomes Grand Park', description: 'Tên dự án' })
@IsString()
@MaxLength(200)
name!: string;
@ApiProperty({ example: 'vinhomes-grand-park', description: 'URL slug (unique)' })
@IsString()
@MaxLength(100)
slug!: string;
@ApiProperty({ example: 'Vingroup' })
@IsString()
developer!: string;
@ApiPropertyOptional({ example: 'https://example.com/logo.png' })
@IsOptional()
@IsString()
developerLogo?: string;
@ApiProperty({ example: 10000, description: 'Tổng số căn hộ/đơn vị' })
@IsNumber()
@Type(() => Number)
@Min(1)
totalUnits!: number;
@ApiProperty({ enum: ProjectDevelopmentStatus, example: 'UNDER_CONSTRUCTION' })
@IsEnum(ProjectDevelopmentStatus)
status!: ProjectDevelopmentStatus;
@ApiProperty({ example: 10.8231, description: 'Latitude' })
@IsNumber()
@Type(() => Number)
@Min(-90)
@Max(90)
latitude!: number;
@ApiProperty({ example: 106.8368, description: 'Longitude' })
@IsNumber()
@Type(() => Number)
@Min(-180)
@Max(180)
longitude!: number;
@ApiProperty({ example: 'Phường Long Thạnh Mỹ, TP. Thủ Đức' })
@IsString()
address!: string;
@ApiProperty({ example: 'Long Thạnh Mỹ' })
@IsString()
ward!: string;
@ApiProperty({ example: 'Thủ Đức' })
@IsString()
district!: string;
@ApiProperty({ example: 'Hồ Chí Minh' })
@IsString()
city!: string;
@ApiPropertyOptional({ description: 'Mô tả dự án' })
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ description: 'Tiện ích dự án (JSON)' })
@IsOptional()
@IsObject()
amenities?: Record<string, unknown>;
@ApiPropertyOptional({ example: 'https://example.com/masterplan.jpg' })
@IsOptional()
@IsString()
masterPlanUrl?: string;
@ApiPropertyOptional({ example: '3000000000', description: 'Giá thấp nhất (VND)' })
@IsOptional()
@IsString()
minPrice?: string;
@ApiPropertyOptional({ example: '15000000000', description: 'Giá cao nhất (VND)' })
@IsOptional()
@IsString()
maxPrice?: string;
@ApiPropertyOptional({ description: 'Giá/m² range (JSON)' })
@IsOptional()
@IsObject()
pricePerM2Range?: Record<string, unknown>;
@ApiPropertyOptional({ example: 271, description: 'Tổng diện tích (ha)' })
@IsOptional()
@IsNumber()
@Type(() => Number)
@Min(0)
totalArea?: number;
@ApiPropertyOptional({ example: 14 })
@IsOptional()
@IsNumber()
@Type(() => Number)
buildingCount?: number;
@ApiPropertyOptional({ example: 35 })
@IsOptional()
@IsNumber()
@Type(() => Number)
floorCount?: number;
@ApiPropertyOptional({ description: 'Loại căn hộ (JSON)' })
@IsOptional()
@IsObject()
unitTypes?: Record<string, unknown>;
@ApiPropertyOptional({ example: ['cao-cap', 'can-ho'], description: 'Tags' })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiPropertyOptional({ example: '2020-06-01', description: 'Ngày khởi công' })
@IsOptional()
@IsDateString()
startDate?: string;
@ApiPropertyOptional({ example: '2025-12-31', description: 'Ngày dự kiến hoàn thành' })
@IsOptional()
@IsDateString()
completionDate?: string;
}

View File

@@ -0,0 +1,52 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ProjectDevelopmentStatus } from '@prisma/client';
import { Type } from 'class-transformer';
import { IsString, IsEnum, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator';
export class SearchProjectsDto {
@ApiPropertyOptional({ description: 'Tìm kiếm theo tên, chủ đầu tư, quận, thành phố' })
@IsOptional()
@IsString()
q?: string;
@ApiPropertyOptional({ enum: ProjectDevelopmentStatus })
@IsOptional()
@IsEnum(ProjectDevelopmentStatus)
status?: ProjectDevelopmentStatus;
@ApiPropertyOptional({ example: 'Hồ Chí Minh' })
@IsOptional()
@IsString()
city?: string;
@ApiPropertyOptional({ example: 'Thủ Đức' })
@IsOptional()
@IsString()
district?: string;
@ApiPropertyOptional({ example: 'Vingroup' })
@IsOptional()
@IsString()
developer?: string;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
@Type(() => Boolean)
isVerified?: boolean;
@ApiPropertyOptional({ default: 1 })
@IsOptional()
@IsNumber()
@Type(() => Number)
@Min(1)
page?: number;
@ApiPropertyOptional({ default: 20 })
@IsOptional()
@IsNumber()
@Type(() => Number)
@Min(1)
@Max(100)
limit?: number;
}

View File

@@ -0,0 +1,50 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { ProjectDevelopmentStatus } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsString,
IsNumber,
IsEnum,
IsOptional,
IsArray,
IsObject,
IsBoolean,
Min,
IsDateString,
} from 'class-validator';
export class UpdateProjectDto {
@ApiPropertyOptional() @IsOptional() @IsString() name?: string;
@ApiPropertyOptional() @IsOptional() @IsString() developer?: string;
@ApiPropertyOptional() @IsOptional() @IsString() developerLogo?: string | null;
@ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) @Min(1)
totalUnits?: number;
@ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) @Min(0)
completedUnits?: number;
@ApiPropertyOptional({ enum: ProjectDevelopmentStatus })
@IsOptional() @IsEnum(ProjectDevelopmentStatus)
status?: ProjectDevelopmentStatus;
@ApiPropertyOptional() @IsOptional() @IsString() description?: string | null;
@ApiPropertyOptional() @IsOptional() @IsObject() amenities?: Record<string, unknown> | null;
@ApiPropertyOptional() @IsOptional() @IsString() masterPlanUrl?: string | null;
@ApiPropertyOptional() @IsOptional() @IsString() minPrice?: string | null;
@ApiPropertyOptional() @IsOptional() @IsString() maxPrice?: string | null;
@ApiPropertyOptional() @IsOptional() @IsObject() pricePerM2Range?: Record<string, unknown> | null;
@ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) @Min(0)
totalArea?: number | null;
@ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) buildingCount?: number | null;
@ApiPropertyOptional() @IsOptional() @IsNumber() @Type(() => Number) floorCount?: number | null;
@ApiPropertyOptional() @IsOptional() @IsObject() unitTypes?: Record<string, unknown> | null;
@ApiPropertyOptional() @IsOptional() @IsArray() media?: Record<string, unknown>[] | null;
@ApiPropertyOptional() @IsOptional() @IsArray() documents?: Record<string, unknown>[] | null;
@ApiPropertyOptional() @IsOptional() @IsArray() @IsString({ each: true }) tags?: string[];
@ApiPropertyOptional() @IsOptional() @IsBoolean() isVerified?: boolean;
@ApiPropertyOptional() @IsOptional() @IsDateString() startDate?: string | null;
@ApiPropertyOptional() @IsOptional() @IsDateString() completionDate?: string | null;
}