import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { UserRole } from '@prisma/client'; import { JwtAuthGuard, Roles, RolesGuard } from '@modules/auth'; import { NotFoundException } from '@modules/shared'; import { CreateProjectCommand } from '../../application/commands/create-project/create-project.command'; import { UpdateProjectCommand } from '../../application/commands/update-project/update-project.command'; import { GetProjectQuery } from '../../application/queries/get-project/get-project.query'; import { ListProjectsQuery } from '../../application/queries/list-projects/list-projects.query'; import { CreateProjectDto } from '../dto/create-project.dto'; import { SearchProjectsDto } from '../dto/search-projects.dto'; import { UpdateProjectDto } from '../dto/update-project.dto'; interface RawProjectListItem { id: string; name: string; slug: string; developer: string; developerLogo: string | null; media?: { url: string; type?: string; order?: number }[] | null; [k: string]: unknown; } function shapeProject(row: T) { const { developer, developerLogo, media, ...rest } = row; const thumbnailUrl = Array.isArray(media) && media.length > 0 ? (media[0]?.url ?? null) : null; return { ...rest, developer: { id: developer, name: developer, logo: developerLogo ?? null }, thumbnailUrl, propertyTypes: [], completionDate: (rest as { completionDate?: Date | null }).completionDate ?? null, }; } @ApiTags('projects') @Controller('projects') export class ProjectsController { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} // ── Public endpoints ────────────────────────────────────────────── @ApiOperation({ summary: 'Danh sách dự án', description: 'Tìm kiếm và lọc dự án bất động sản' }) @ApiResponse({ status: 200, description: 'Danh sách dự án phân trang' }) @Get() async listProjects(@Query() dto: SearchProjectsDto) { const result = await this.queryBus.execute< ListProjectsQuery, { data: RawProjectListItem[]; total: number; page: number; limit: number; totalPages: number } >( new ListProjectsQuery( dto.q, dto.status, dto.city, dto.district, dto.developer, dto.isVerified, dto.page ?? 1, dto.limit ?? 20, ), ); return { ...result, data: result.data.map(shapeProject) }; } @ApiOperation({ summary: 'Chi tiết dự án', description: 'Xem chi tiết dự án theo slug hoặc ID' }) @ApiResponse({ status: 200, description: 'Thông tin chi tiết dự án' }) @ApiResponse({ status: 404, description: 'Không tìm thấy dự án' }) @Get(':slugOrId') async getProject(@Param('slugOrId') slugOrId: string) { const result = await this.queryBus.execute( new GetProjectQuery(slugOrId), ); if (!result) { throw new NotFoundException('Dự án', slugOrId); } return shapeProject(result); } // ── Admin endpoints ─────────────────────────────────────────────── @ApiOperation({ summary: 'Tạo dự án (admin)', description: 'Tạo mới dự án bất động sản' }) @ApiResponse({ status: 201, description: 'Dự án đã tạo' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @Post() async createProject(@Body() dto: CreateProjectDto) { return this.commandBus.execute( new CreateProjectCommand( dto.name, dto.slug, dto.developer, dto.developerLogo ?? null, dto.totalUnits, dto.status, dto.latitude, dto.longitude, dto.address, dto.ward, dto.district, dto.city, dto.description ?? null, dto.amenities ?? null, dto.masterPlanUrl ?? null, dto.minPrice ? BigInt(dto.minPrice) : null, dto.maxPrice ? BigInt(dto.maxPrice) : null, dto.pricePerM2Range ?? null, dto.totalArea ?? null, dto.buildingCount ?? null, dto.floorCount ?? null, dto.unitTypes ?? null, dto.tags ?? [], dto.startDate ? new Date(dto.startDate) : null, dto.completionDate ? new Date(dto.completionDate) : null, ), ); } @ApiOperation({ summary: 'Cập nhật dự án (admin)', description: 'Cập nhật thông tin dự án' }) @ApiResponse({ status: 200, description: 'Dự án đã cập nhật' }) @ApiBearerAuth('JWT') @UseGuards(JwtAuthGuard, RolesGuard) @Roles(UserRole.ADMIN) @Patch(':id') async updateProject(@Param('id') id: string, @Body() dto: UpdateProjectDto) { return this.commandBus.execute( new UpdateProjectCommand( id, dto.name, dto.developer, dto.developerLogo, dto.totalUnits, dto.completedUnits, dto.status, dto.description, dto.amenities, dto.masterPlanUrl, dto.minPrice !== undefined ? (dto.minPrice ? BigInt(dto.minPrice) : null) : undefined, dto.maxPrice !== undefined ? (dto.maxPrice ? BigInt(dto.maxPrice) : null) : undefined, dto.pricePerM2Range, dto.totalArea, dto.buildingCount, dto.floorCount, dto.unitTypes, dto.media, dto.documents, dto.tags, dto.isVerified, dto.startDate !== undefined ? (dto.startDate ? new Date(dto.startDate) : null) : undefined, dto.completionDate !== undefined ? (dto.completionDate ? new Date(dto.completionDate) : null) : undefined, ), ); } }