"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CommentGateway = exports.TourRoleGuard = exports.Roles = exports.ROLES_KEY = void 0; const dotenv = __importStar(require("dotenv")); const path = __importStar(require("path")); const envPath = path.resolve(process.cwd(), '..', '.env'); dotenv.config({ path: envPath }); require("reflect-metadata"); const fs = __importStar(require("fs")); const zlib = __importStar(require("zlib")); const util_1 = require("util"); const sharp_1 = __importDefault(require("sharp")); const core_1 = require("@nestjs/core"); const common_1 = require("@nestjs/common"); const platform_express_1 = require("@nestjs/platform-express"); const websockets_1 = require("@nestjs/websockets"); const socket_io_1 = require("socket.io"); const prisma_service_1 = require("../prisma/prisma.service"); const client_1 = require("@prisma/client"); const bcrypt = __importStar(require("bcrypt")); const admin_guard_1 = require("./auth/admin.guard"); const jwt_1 = require("@nestjs/jwt"); const jwt_auth_guard_1 = require("./auth/jwt-auth.guard"); const jwt_strategy_1 = require("./auth/jwt.strategy"); const core_2 = require("@nestjs/core"); const common_2 = require("@nestjs/common"); const cache_manager_1 = require("@nestjs/cache-manager"); const cache_manager_redis_yet_1 = require("cache-manager-redis-yet"); const compress_cache_interceptor_1 = require("./common/compress-cache.interceptor"); const gzip = (0, util_1.promisify)(zlib.gzip); const gunzip = (0, util_1.promisify)(zlib.gunzip); const UPLOAD_ROOT = path.join(process.cwd(), 'uploads'); const CACHE_TTL = { DEFAULT: 600000, RESOURCE_TO_TOUR: 3600000, USER_ROLE: 300000, }; async function bootstrap() { if (!process.env.DATABASE_URL) { throw new Error('❌ DATABASE_URL không tồn tại trong biến môi trường! Kiểm tra file .env tại: ' + envPath); } console.log('===================================='); console.log('DATABASE_URL:', process.env.DATABASE_URL); console.log('===================================='); const app = await core_1.NestFactory.create(AppModule); app.setGlobalPrefix('api/v1'); app.enableCors(); if (!fs.existsSync(UPLOAD_ROOT)) { fs.mkdirSync(UPLOAD_ROOT, { recursive: true }); } app.useStaticAssets(UPLOAD_ROOT, { prefix: '/uploads/', }); await app.listen(3001); console.log(`🚀 Server is running on: http://localhost:3001`); } exports.ROLES_KEY = 'roles'; const Roles = (...roles) => (0, common_2.SetMetadata)(exports.ROLES_KEY, roles); exports.Roles = Roles; let TourRoleGuard = class TourRoleGuard { constructor(reflector, prisma, cacheManager) { this.reflector = reflector; this.prisma = prisma; this.cacheManager = cacheManager; } async canActivate(context) { const requiredRoles = this.reflector.getAllAndOverride(exports.ROLES_KEY, [ context.getHandler(), context.getClass(), ]); const defaultRoles = [client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER]; const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles; const request = context.switchToHttp().getRequest(); const user = request.user; let tourId = request.params.tourId; const resourceId = request.params.id || request.params.legId || request.params.locationId; if (!tourId && resourceId) { const resCacheKey = `res-to-tour:${resourceId}`; const compressedData = await this.cacheManager.get(resCacheKey); if (compressedData) { try { const decompressed = await gunzip(compressedData); tourId = decompressed.toString(); } catch (e) { console.error('Lỗi giải nén cache:', e); } } else { const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } }); if (isTour) { tourId = resourceId; } else { const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } }); if (leg) { tourId = leg.tourId; } else { const loc = await this.prisma.location.findUnique({ where: { id: resourceId }, include: { leg: { select: { tourId: true } } } }); if (loc) { tourId = loc.leg.tourId; } else { const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true } }); if (photo) tourId = photo.tourId; } } } if (tourId) { try { const compressed = await gzip(Buffer.from(tourId)); await this.cacheManager.set(resCacheKey, compressed, CACHE_TTL.RESOURCE_TO_TOUR); } catch (e) { await this.cacheManager.set(resCacheKey, tourId, CACHE_TTL.RESOURCE_TO_TOUR); } } } } if (!user || !tourId) { if (!resourceId && !request.params.tourId) return true; return false; } const roleCacheKey = `user-role:${user.id}:${tourId}`; let role = await this.cacheManager.get(roleCacheKey); if (!role) { const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: user.id } }, }); if (!participation) return false; role = participation.role; await this.cacheManager.set(roleCacheKey, role, CACHE_TTL.USER_ROLE); } if (!rolesToCheck.some(r => role === r)) { throw new common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.'); } return true; } }; exports.TourRoleGuard = TourRoleGuard; exports.TourRoleGuard = TourRoleGuard = __decorate([ (0, common_1.Injectable)(), __param(2, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)), __metadata("design:paramtypes", [core_2.Reflector, prisma_service_1.PrismaService, Object]) ], TourRoleGuard); let AppController = class AppController { getHello() { return 'Travel Planning API is running!'; } }; __decorate([ (0, common_1.Get)(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", String) ], AppController.prototype, "getHello", null); AppController = __decorate([ (0, common_1.Controller)() ], AppController); let AuthController = class AuthController { constructor(prisma, jwtService) { this.prisma = prisma; this.jwtService = jwtService; } async getStatus() { const userCount = await this.prisma.user.count(); console.log(`[Status Check] Users found: ${userCount}`); return { isInitialSetup: userCount === 0 }; } async login(body) { const { email, password } = body; const user = await this.prisma.user.findUnique({ where: { email } }); if (!user || !(await bcrypt.compare(password, user.passwordHash))) { throw new common_1.UnauthorizedException('Email hoặc mật khẩu không chính xác'); } if (user.isBlocked) { throw new common_1.UnauthorizedException('Tài khoản của bạn đã bị khóa. Vui lòng liên hệ quản trị viên.'); } const payload = { email: user.email, sub: user.id }; return { access_token: this.jwtService.sign(payload), user: { id: user.id, email: user.email, name: user.name, isAdmin: user.isAdmin, }, }; } async signup(body) { const { email, password, name, phone, address } = body; const existingUser = await this.prisma.user.findUnique({ where: { email } }); if (existingUser) throw new common_1.BadRequestException('Email đã được sử dụng'); const userCount = await this.prisma.user.count(); const shouldBeAdmin = userCount === 0; const passwordHash = await bcrypt.hash(password, 10); return this.prisma.user.create({ data: { email, passwordHash, name, phone, address, isAdmin: shouldBeAdmin }, select: { id: true, email: true, name: true, phone: true, address: true, isAdmin: true } }); } }; __decorate([ (0, common_1.Get)('status'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], AuthController.prototype, "getStatus", null); __decorate([ (0, common_1.Post)('login'), __param(0, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], AuthController.prototype, "login", null); __decorate([ (0, common_1.Post)('signup'), __param(0, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], AuthController.prototype, "signup", null); AuthController = __decorate([ (0, common_1.Controller)('auth'), __metadata("design:paramtypes", [prisma_service_1.PrismaService, jwt_1.JwtService]) ], AuthController); let PublicTourController = class PublicTourController { constructor(prisma) { this.prisma = prisma; } async getPublicTourDetails(id) { console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`); const tour = await this.prisma.tour.findUnique({ where: { id }, include: { participants: { include: { user: { select: { id: true, name: true, email: true } } } }, photos: true, legs: { orderBy: { sequence: 'asc' }, include: { expenses: { include: { location: { select: { name: true, plannedStart: true } }, paidBy: { select: { name: true } } } }, locations: { orderBy: { plannedStart: 'asc' }, include: { _count: { select: { comments: true } } } }, }, }, }, }); if (!tour) { console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`); throw new common_1.NotFoundException(`Không tìm thấy Tour`); } return tour; } }; __decorate([ (0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor), (0, common_1.Get)(':id/public'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], PublicTourController.prototype, "getPublicTourDetails", null); PublicTourController = __decorate([ (0, common_1.Controller)('tours'), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], PublicTourController); let TourController = class TourController { constructor(prisma, cacheManager) { this.prisma = prisma; this.cacheManager = cacheManager; } async createTour(body, req) { const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body; return this.prisma.tour.create({ data: { title, description, startDate: startDate ? new Date(startDate) : null, endDate: endDate ? new Date(endDate) : null, tags: tags || [], adultCount: adultCount || 1, childCount: childCount || 0, childDiscount: childDiscount || 0, createdById: req.user.id, participants: { create: { userId: req.user.id, role: 'OWNER' } }, legs: { create: { sequence: 1, note: 'Chặng khởi đầu' } } }, include: { participants: { include: { user: { select: { id: true, name: true, email: true } } } } } }); } async addLocation(tourId, body, req) { const legId = body.legId; console.log(`[USER ACTION] User ${req.user.id} added a NEW VISIT point to Tour ${tourId}: "${body.name}"`); const leg = legId ? await this.prisma.leg.findUnique({ where: { id: legId } }) : await this.prisma.leg.findFirst({ where: { tourId } }); if (!leg) throw new common_1.NotFoundException('Không tìm thấy chặng phù hợp để thêm địa điểm'); return this.prisma.location.create({ data: { name: body.name, address: body.address, latitude: body.latitude, longitude: body.longitude, type: body.type, legId: leg.id, plannedStart: body.plannedStart ? new Date(body.plannedStart) : null, plannedEnd: body.plannedEnd ? new Date(body.plannedEnd) : null, } }).then(async (loc) => { if (body.expenseAmount && Number(body.expenseAmount) > 0) { await this.prisma.expense.create({ data: { amount: Number(body.expenseAmount), category: body.expenseCategory || 'OTHER', locationId: loc.id, legId: loc.legId, description: body.expenseDescription || `Chi phí tại ${loc.name}`, note: body.expenseNote || null, paidById: body.paidById || null, } }); } return loc; }); } async updateTourStartPoint(tourId, body, req) { const { latitude, longitude, name } = body; console.log(`[USER ACTION] User ${req.user.id} set START point for Tour ${tourId}: "${name}" at [${latitude}, ${longitude}]`); await this.prisma.location.deleteMany({ where: { leg: { tourId: tourId }, plannedStart: new Date(0) } }); const firstLeg = await this.prisma.leg.findFirst({ where: { tourId }, orderBy: { sequence: 'asc' } }); if (!firstLeg) throw new common_1.NotFoundException('Không tìm thấy chặng đầu tiên cho Tour này'); return this.prisma.location.create({ data: { name: name || 'Điểm xuất phát', latitude, longitude, type: 'MOVE', legId: firstLeg.id, plannedStart: new Date(0), } }); } async updateTourEndPoint(tourId, body, req) { const { latitude, longitude, name } = body; console.log(`[USER ACTION] User ${req.user.id} set END point for Tour ${tourId}: "${name}" at [${latitude}, ${longitude}]`); await this.prisma.location.deleteMany({ where: { leg: { tourId: tourId }, plannedEnd: new Date(0) } }); const lastLeg = await this.prisma.leg.findFirst({ where: { tourId }, orderBy: { sequence: 'desc' } }); if (!lastLeg) throw new common_1.NotFoundException('Không tìm thấy chặng cuối cùng cho Tour này'); return this.prisma.location.create({ data: { name: name || 'Điểm kết thúc', latitude, longitude, type: 'MOVE', legId: lastLeg.id, plannedEnd: new Date(0), } }); } async initializeLegs(tourId, body) { const { count } = body; if (count <= 0 || count > 20) throw new common_1.BadRequestException('Số lượng chặng không hợp lệ (1-20)'); const existingLegs = await this.prisma.leg.findMany({ where: { tourId }, orderBy: { sequence: 'asc' } }); const needed = count - existingLegs.length; if (needed > 0) { const createData = Array.from({ length: needed }).map((_, i) => ({ tourId, sequence: existingLegs.length + i + 1, note: `Chặng ${existingLegs.length + i + 1}` })); await this.prisma.leg.createMany({ data: createData }); } const allLegs = await this.prisma.leg.findMany({ where: { tourId }, orderBy: { sequence: 'asc' } }); const lastLeg = allLegs[allLegs.length - 1]; const endPoint = await this.prisma.location.findFirst({ where: { leg: { tourId }, plannedEnd: new Date(0) } }); if (endPoint && lastLeg && endPoint.legId !== lastLeg.id) { await this.prisma.location.update({ where: { id: endPoint.id }, data: { legId: lastLeg.id } }); } return allLegs; } async addLeg(tourId, body) { const tour = await this.prisma.tour.findUnique({ where: { id: tourId }, include: { legs: true } }); if (!tour) throw new common_1.NotFoundException('Không tìm thấy tour'); return this.prisma.leg.create({ data: { tourId, sequence: tour.legs.length + 1, note: body.note || `Chặng ${tour.legs.length + 1}` } }); } async updateTour(id, body) { return this.prisma.tour.update({ where: { id }, data: { title: body.title, description: body.description, startDate: body.startDate ? new Date(body.startDate) : undefined, tags: body.tags, endDate: body.endDate ? new Date(body.endDate) : undefined, adultCount: body.adultCount !== undefined ? Number(body.adultCount) : undefined, childCount: body.childCount !== undefined ? Number(body.childCount) : undefined, childDiscount: body.childDiscount !== undefined ? Number(body.childDiscount) : undefined, }, }); } async deleteTour(id) { const tour = await this.prisma.tour.findUnique({ where: { id }, include: { participants: true, photos: true, legs: { include: { locations: true } } } }); if (!tour) throw new common_1.NotFoundException('Không tìm thấy tour'); for (const participant of tour.participants) { await this.cacheManager.del(`user-role:${participant.userId}:${id}`); } await this.cacheManager.del(`res-to-tour:${id}`); for (const leg of tour.legs) { await this.cacheManager.del(`res-to-tour:${leg.id}`); for (const loc of leg.locations) { await this.cacheManager.del(`res-to-tour:${loc.id}`); } } for (const photo of tour.photos) { await this.cacheManager.del(`res-to-tour:${photo.id}`); } for (const photo of tour.photos) { if (photo.imageUrl) { const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); if (fs.existsSync(displayFilePath)) { fs.unlinkSync(displayFilePath); } } } await this.prisma.photo.updateMany({ where: { tourId: id }, data: { imageUrl: null } }); await this.prisma.tour.delete({ where: { id }, }); return { success: true }; } async getPublicTours(req) { return this.prisma.tour.findMany({ where: { participants: { some: { userId: req.user.id } } }, take: 20, include: { participants: { where: { userId: req.user.id }, select: { role: true } }, photos: { take: 1 }, legs: { orderBy: { sequence: 'asc' }, include: { locations: { orderBy: { plannedStart: 'asc' }, include: { _count: { select: { comments: true } } } } } } } }); } async getTourDetails(id) { const tour = await this.prisma.tour.findUnique({ where: { id }, include: { participants: { include: { user: { select: { id: true, name: true, email: true } } } }, photos: true, legs: { orderBy: { sequence: 'asc' }, include: { expenses: { include: { location: { select: { name: true, plannedStart: true } }, paidBy: { select: { name: true } } } }, locations: { orderBy: { plannedStart: 'asc' }, include: { _count: { select: { comments: true } } } }, }, }, }, }); if (!tour) throw new common_1.NotFoundException(`Không tìm thấy Tour với ID ${id}`); return tour; } async addMember(tourId, body, req) { const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY']; const role = validRoles.includes(body.role) ? body.role : 'MEMBER'; await this.cacheManager.del(`user-role:${body.userId}:${tourId}`); const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: body.userId } }, }); if (participation) { return this.prisma.tourParticipant.update({ where: { tourId_userId: { tourId, userId: body.userId } }, data: { role }, include: { user: { select: { id: true, name: true, email: true } } }, }); } let currentRole = req.user.tourParticipation?.role; if (!currentRole) { const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: req.user.id } }, }); currentRole = participation?.role; } if (!currentRole || !(currentRole === 'OWNER' || currentRole === 'MANAGER')) { const joinRequest = await this.prisma.joinRequest.create({ data: { tourId, userId: body.userId, requestedById: req.user.id, status: 'PENDING', }, include: { user: { select: { id: true, name: true, email: true } }, requestedBy: { select: { id: true, name: true, email: true } }, }, }); return { ...joinRequest, pendingApproval: true }; } return this.prisma.tourParticipant.create({ data: { tourId, userId: body.userId, role, }, include: { user: { select: { id: true, name: true, email: true } } }, }); } async getJoinRequests(tourId, req) { const requests = await this.prisma.joinRequest.findMany({ where: { tourId, status: 'PENDING' }, orderBy: { createdAt: 'desc' }, include: { user: { select: { id: true, name: true, email: true } }, requestedBy: { select: { id: true, name: true, email: true } }, }, }); return requests; } async createJoinRequest(tourId, body, req) { const requestingUserId = body.userId || req.user.id; const existingParticipation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: requestingUserId } }, }); if (existingParticipation) { throw new common_1.BadRequestException('Người dùng này đã là thành viên của tour.'); } const pendingRequest = await this.prisma.joinRequest.findFirst({ where: { tourId, userId: requestingUserId, status: 'PENDING' }, }); if (pendingRequest) { return pendingRequest; } const joinRequest = await this.prisma.joinRequest.create({ data: { tourId, userId: requestingUserId, requestedById: req.user.id, status: 'PENDING', }, include: { user: { select: { id: true, name: true, email: true } }, requestedBy: { select: { id: true, name: true, email: true } }, }, }); return joinRequest; } async acceptJoinRequest(tourId, requestId, req) { let role = req.user.tourParticipation?.role; if (!role) { const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: req.user.id } }, }); role = participation?.role; } if (!role || !(role === 'OWNER' || role === 'MANAGER')) { throw new common_1.ForbiddenException('Bạn không có quyền chấp nhận yêu cầu tham gia.'); } const joinRequest = await this.prisma.joinRequest.findUnique({ where: { id: requestId }, }); if (!joinRequest || joinRequest.tourId !== tourId) { throw new common_1.NotFoundException('Không tìm thấy yêu cầu tham gia.'); } if (joinRequest.status !== 'PENDING') { throw new common_1.BadRequestException('Yêu cầu này đã được xử lý trước đó.'); } await this.cacheManager.del(`user-role:${joinRequest.userId}:${tourId}`); const existing = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: joinRequest.userId } }, }); if (existing) { await this.prisma.joinRequest.update({ where: { id: requestId }, data: { status: 'REJECTED' }, }); return { success: true, message: 'Người dùng đã là thành viên, yêu cầu đã bị từ chối.' }; } await this.prisma.$transaction([ this.prisma.tourParticipant.create({ data: { tourId, userId: joinRequest.userId, role: 'MEMBER', }, }), this.prisma.joinRequest.update({ where: { id: requestId }, data: { status: 'ACCEPTED' }, }), ]); return { success: true, message: 'Đã chấp nhận yêu cầu tham gia.' }; } async rejectJoinRequest(tourId, requestId, req) { let role = req.user.tourParticipation?.role; if (!role) { const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId: req.user.id } }, }); role = participation?.role; } if (!role || role !== 'OWNER') { throw new common_1.ForbiddenException('Bạn không có quyền từ chối yêu cầu tham gia.'); } const joinRequest = await this.prisma.joinRequest.findUnique({ where: { id: requestId }, }); if (!joinRequest || joinRequest.tourId !== tourId) { throw new common_1.NotFoundException('Không tìm thấy yêu cầu tham gia.'); } if (joinRequest.status !== 'PENDING') { throw new common_1.BadRequestException('Yêu cầu này đã được xử lý trước đó.'); } await this.prisma.joinRequest.update({ where: { id: requestId }, data: { status: 'REJECTED' }, }); return { success: true, message: 'Đã từ chối yêu cầu tham gia.' }; } async removeMember(tourId, userId) { const participation = await this.prisma.tourParticipant.findUnique({ where: { tourId_userId: { tourId, userId } }, }); if (!participation) { throw new common_1.NotFoundException('Thành viên này không có trong tour'); } await this.cacheManager.del(`user-role:${userId}:${tourId}`); await this.prisma.tourParticipant.delete({ where: { tourId_userId: { tourId, userId } }, }); return { message: 'Đã xóa thành viên khỏi tour' }; } async uploadPhotos(tourId, files, req) { if (!files || files.length === 0) { throw new common_1.BadRequestException('Vui lòng chọn ít nhất một ảnh'); } const uploaderId = req.user.id; const memberOriginalDir = path.join(UPLOAD_ROOT, 'members', uploaderId, 'originals'); const tourDisplayPath = path.join(UPLOAD_ROOT, 'tours'); if (!fs.existsSync(memberOriginalDir)) fs.mkdirSync(memberOriginalDir, { recursive: true }); if (!fs.existsSync(tourDisplayPath)) fs.mkdirSync(tourDisplayPath, { recursive: true }); return Promise.all(files.map(async (file) => { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9); const extension = path.extname(file.originalname).toLowerCase() || '.jpg'; const filename = `${uniqueSuffix}${extension}`; const originalFilePath = path.join(memberOriginalDir, filename); const displayFilePath = path.join(tourDisplayPath, filename); await fs.promises.writeFile(originalFilePath, file.buffer); await (0, sharp_1.default)(file.buffer) .resize(2560, 2560, { fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 85 }) .toFile(displayFilePath); return this.prisma.photo.create({ data: { tourId: tourId, uploaderId: uploaderId, imageUrl: `/uploads/tours/${filename}`, originalUrl: `/uploads/members/${uploaderId}/originals/${filename}`, privacy: 'TOUR_ONLY', } }); })); } }; __decorate([ (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard), (0, common_1.Post)(), __param(0, (0, common_1.Body)()), __param(1, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "createTour", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/locations'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addLocation", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/start-point'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTourStartPoint", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/end-point'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTourEndPoint", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/legs/batch'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "initializeLegs", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/legs'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addLeg", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Patch)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTour", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Delete)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TourController.prototype, "deleteTour", null); __decorate([ (0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard), (0, common_1.Get)('explore'), __param(0, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "getPublicTours", null); __decorate([ (0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor), (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Get)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TourController.prototype, "getTourDetails", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/members'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addMember", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Get)(':tourId/join-requests'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "getJoinRequests", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/join-requests'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "createJoinRequest", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/join-requests/:requestId/accept'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Param)('requestId')), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "acceptJoinRequest", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/join-requests/:requestId/reject'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Param)('requestId')), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "rejectJoinRequest", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Delete)(':tourId/members/:userId'), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Param)('userId', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String, String]), __metadata("design:returntype", Promise) ], TourController.prototype, "removeMember", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':tourId/photos'), (0, common_1.UseInterceptors)((0, platform_express_1.FilesInterceptor)('images', 10)), __param(0, (0, common_1.Param)('tourId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.UploadedFiles)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Array, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "uploadPhotos", null); TourController = __decorate([ (0, common_1.Controller)('tours'), __param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)), __metadata("design:paramtypes", [prisma_service_1.PrismaService, Object]) ], TourController); let LocationController = class LocationController { constructor(prisma) { this.prisma = prisma; } async updateLocation(id, body) { const { expenseAmount, expenseCategory, ...data } = body; const location = await this.prisma.location.update({ where: { id }, data: { name: data.name, address: data.address, latitude: data.latitude, longitude: data.longitude, type: data.type, plannedStart: data.plannedStart ? new Date(data.plannedStart) : undefined, plannedEnd: data.plannedEnd ? new Date(data.plannedEnd) : undefined, status: data.status, } }); if (expenseAmount !== undefined) { const amount = Number(expenseAmount); const existingExpense = await this.prisma.expense.findFirst({ where: { locationId: id } }); if (existingExpense) { await this.prisma.expense.update({ where: { id: existingExpense.id }, data: { amount, category: expenseCategory || 'OTHER' } }); } else if (amount > 0) { await this.prisma.expense.create({ data: { amount, category: expenseCategory || 'OTHER', locationId: id, legId: location.legId, description: `Chi phí tại ${location.name}` } }); } } return location; } async deleteLocation(id) { await this.prisma.location.delete({ where: { id } }); return { success: true }; } }; __decorate([ (0, common_1.Patch)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], LocationController.prototype, "updateLocation", null); __decorate([ (0, common_1.Delete)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], LocationController.prototype, "deleteLocation", null); LocationController = __decorate([ (0, common_1.Controller)('locations'), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], LocationController); let LegController = class LegController { constructor(prisma) { this.prisma = prisma; } async updateLeg(id, body) { return this.prisma.leg.update({ where: { id }, data: { note: body.note, sequence: body.sequence, startDate: body.startDate ? new Date(body.startDate) : undefined, endDate: body.endDate ? new Date(body.endDate) : undefined, description: body.description, } }); } async deleteLeg(id) { const leg = await this.prisma.leg.findUnique({ where: { id }, include: { _count: { select: { locations: true } } } }); if (leg?._count.locations && leg._count.locations > 0) { throw new common_1.BadRequestException('Không thể xóa chặng đang có địa điểm. Hãy xóa địa điểm trước.'); } await this.prisma.leg.delete({ where: { id } }); return { success: true }; } }; __decorate([ (0, common_1.Patch)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], LegController.prototype, "updateLeg", null); __decorate([ (0, common_1.Delete)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], LegController.prototype, "deleteLeg", null); LegController = __decorate([ (0, common_1.Controller)('legs'), (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], LegController); function calculateDistance(lat1, lon1, lat2, lon2) { const p = 0.017453292519943295; const c = Math.cos; const a = 0.5 - c((lat2 - lat1) * p) / 2 + c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2; return 12742 * Math.asin(Math.sqrt(a)); } let RoutingController = class RoutingController { constructor(prisma) { this.prisma = prisma; } async optimize(legId) { const currentLeg = await this.prisma.leg.findUnique({ where: { id: legId }, }); if (!currentLeg) throw new common_1.NotFoundException('Không tìm thấy chặng'); const locations = await this.prisma.location.findMany({ where: { legId }, }); if (locations.length === 0) return { locations: [], totalDistance: 0 }; let startAnchor = null; const prevLeg = await this.prisma.leg.findFirst({ where: { tourId: currentLeg.tourId, sequence: currentLeg.sequence - 1 }, include: { locations: { orderBy: { plannedStart: 'asc' } } } }); if (prevLeg?.locations?.length) { startAnchor = prevLeg.locations[prevLeg.locations.length - 1]; } if (locations.length <= 2 && !startAnchor) return { locations, totalDistance: 0 }; const optimized = []; const unvisited = [...locations]; let current; if (startAnchor) { let nearestIdx = 0; let minDist = Infinity; for (let i = 0; i < unvisited.length; i++) { const d = calculateDistance(startAnchor.latitude, startAnchor.longitude, unvisited[i].latitude, unvisited[i].longitude); if (d < minDist) { minDist = d; nearestIdx = i; } } current = unvisited.splice(nearestIdx, 1)[0]; } else { current = unvisited.sort((a, b) => (a.plannedStart?.getTime() || 0) - (b.plannedStart?.getTime() || 0)).shift(); } optimized.push(current); while (unvisited.length > 0) { let nearestIdx = 0; let minDist = Infinity; for (let i = 0; i < unvisited.length; i++) { const d = calculateDistance(current.latitude, current.longitude, unvisited[i].latitude, unvisited[i].longitude); if (d < minDist) { minDist = d; nearestIdx = i; } } current = unvisited.splice(nearestIdx, 1)[0]; optimized.push(current); } let totalDistance = 0; if (startAnchor) { totalDistance += calculateDistance(startAnchor.latitude, startAnchor.longitude, optimized[0].latitude, optimized[0].longitude); } for (let i = 0; i < optimized.length - 1; i++) { totalDistance += calculateDistance(optimized[i].latitude, optimized[i].longitude, optimized[i + 1].latitude, optimized[i + 1].longitude); } const baseTime = optimized[0].plannedStart || new Date(); await Promise.all(optimized.map((loc, index) => this.prisma.location.update({ where: { id: loc.id }, data: { plannedStart: new Date(baseTime.getTime() + index * 3600000) }, }))); const updatedLocations = await this.prisma.location.findMany({ where: { legId }, orderBy: { plannedStart: 'asc' } }); return { locations: updatedLocations, totalDistance: parseFloat(totalDistance.toFixed(2)) }; } }; __decorate([ (0, common_1.Post)('optimize/:legId'), __param(0, (0, common_1.Param)('legId', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], RoutingController.prototype, "optimize", null); RoutingController = __decorate([ (0, common_1.Controller)('routing'), (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], RoutingController); let PhotoController = class PhotoController { constructor(prisma) { this.prisma = prisma; } async deletePhoto(id, req) { const photo = await this.prisma.photo.findUnique({ where: { id }, }); if (!photo) { throw new common_1.NotFoundException('Không tìm thấy ảnh.'); } if (photo.uploaderId !== req.user.id) { throw new common_1.ForbiddenException('Bạn không có quyền xóa ảnh này.'); } if (photo.imageUrl) { const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); if (fs.existsSync(displayFilePath)) { fs.unlinkSync(displayFilePath); } } if (photo.originalUrl) { const originalFilePath = path.join(process.cwd(), photo.originalUrl.replace(/^\//, '')); if (fs.existsSync(originalFilePath)) { fs.unlinkSync(originalFilePath); } } await this.prisma.photo.delete({ where: { id } }); return { message: 'Ảnh đã được xóa thành công.' }; } }; __decorate([ (0, common_1.Delete)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], PhotoController.prototype, "deletePhoto", null); PhotoController = __decorate([ (0, common_1.Controller)('photos'), (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], PhotoController); let UserController = class UserController { constructor(prisma) { this.prisma = prisma; } async getAllUsers(req, q) { const currentUserId = req.user?.sub; const users = await this.prisma.user.findMany({ where: q ? { OR: [ { name: { contains: q, mode: 'insensitive' } }, { email: { contains: q, mode: 'insensitive' } }, ], } : undefined, select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true, createdAt: true, phone: true, address: true } }); return users.filter((u) => u.id !== currentUserId); } async getMyPhotos(req) { return this.prisma.photo.findMany({ where: { uploaderId: req.user.id }, include: { tour: { select: { title: true } } }, orderBy: { capturedAt: 'desc' } }); } async updateUser(id, data) { if (data.password) { data.passwordHash = await bcrypt.hash(data.password, 10); delete data.password; } return this.prisma.user.update({ where: { id }, data, select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true } }); } async deleteUser(id) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new common_1.NotFoundException('Không tìm thấy người dùng'); if (user.isAdmin) { const adminCount = await this.prisma.user.count({ where: { isAdmin: true } }); if (adminCount <= 1) throw new common_1.BadRequestException('Không thể xóa Quản trị viên cuối cùng'); } const memberDir = path.join(UPLOAD_ROOT, 'members', id); const photos = await this.prisma.photo.findMany({ where: { uploaderId: id } }); for (const photo of photos) { if (photo.imageUrl) { const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, '')); if (fs.existsSync(displayFilePath)) fs.unlinkSync(displayFilePath); } } await this.prisma.photo.deleteMany({ where: { uploaderId: id } }); await this.prisma.tourParticipant.deleteMany({ where: { userId: id } }); await this.prisma.user.delete({ where: { id } }); if (fs.existsSync(memberDir)) { fs.rmSync(memberDir, { recursive: true, force: true }); } return { message: 'Đã xóa người dùng' }; } async toggleBlock(id) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new common_1.NotFoundException('Người dùng không tồn tại'); const updated = await this.prisma.user.update({ where: { id }, data: { isBlocked: !user.isBlocked }, select: { id: true, email: true, name: true, isBlocked: true } }); return updated; } }; __decorate([ (0, common_1.Get)(), __param(0, (0, common_1.Req)()), __param(1, (0, common_1.Query)('q')), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, String]), __metadata("design:returntype", Promise) ], UserController.prototype, "getAllUsers", null); __decorate([ (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard), (0, common_1.Get)('me/photos'), __param(0, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], UserController.prototype, "getMyPhotos", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.Patch)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], UserController.prototype, "updateUser", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER), (0, common_1.Delete)(':id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], UserController.prototype, "deleteUser", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), (0, common_1.Post)('block/:id'), __param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], UserController.prototype, "toggleBlock", null); UserController = __decorate([ (0, common_1.Controller)('users'), (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER), __metadata("design:paramtypes", [prisma_service_1.PrismaService]) ], UserController); let CommentGateway = class CommentGateway { handleConnection(client) { console.log(`[WS] Client connected: ${client.id}`); } handleJoinTour(client, tourId) { client.join(`tour_${tourId}`); console.log(`[WS] Client ${client.id} joined room: tour_${tourId}`); } notifyNewComment(tourId, data) { this.server.to(`tour_${tourId}`).emit('commentAdded', data); } }; exports.CommentGateway = CommentGateway; __decorate([ (0, websockets_1.WebSocketServer)(), __metadata("design:type", socket_io_1.Server) ], CommentGateway.prototype, "server", void 0); __decorate([ (0, websockets_1.SubscribeMessage)('joinTour'), __metadata("design:type", Function), __metadata("design:paramtypes", [socket_io_1.Socket, String]), __metadata("design:returntype", void 0) ], CommentGateway.prototype, "handleJoinTour", null); exports.CommentGateway = CommentGateway = __decorate([ (0, websockets_1.WebSocketGateway)({ cors: { origin: '*' } }) ], CommentGateway); let CommentController = class CommentController { constructor(prisma, commentGateway) { this.prisma = prisma; this.commentGateway = commentGateway; } async getComments(locationId) { return this.prisma.comment.findMany({ where: { locationId }, include: { user: { select: { name: true } } }, orderBy: { createdAt: 'asc' } }); } async addComment(locationId, body, req) { const comment = await this.prisma.comment.create({ data: { content: body.content, locationId, userId: req.user.id }, include: { user: { select: { name: true } } } }); const location = await this.prisma.location.findUnique({ where: { id: locationId }, include: { leg: { select: { tourId: true } } } }); if (location?.leg?.tourId) { this.commentGateway.notifyNewComment(location.leg.tourId, { ...comment, locationId }); } return comment; } }; __decorate([ (0, common_1.Get)(':locationId/comments'), __param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], CommentController.prototype, "getComments", null); __decorate([ (0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY), (0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard), (0, common_1.Post)(':locationId/comments'), __param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)), __param(1, (0, common_1.Body)()), __param(2, (0, common_1.Req)()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], CommentController.prototype, "addComment", null); CommentController = __decorate([ (0, common_1.Controller)('locations'), __metadata("design:paramtypes", [prisma_service_1.PrismaService, CommentGateway]) ], CommentController); let AppModule = class AppModule { }; AppModule = __decorate([ (0, common_1.Module)({ imports: [ cache_manager_1.CacheModule.registerAsync({ isGlobal: true, useFactory: async () => ({ store: await (0, cache_manager_redis_yet_1.redisStore)({ url: process.env.REDIS_URL || 'redis://localhost:6379', ttl: CACHE_TTL.DEFAULT, }), }), }), jwt_1.JwtModule.register({ secret: process.env.JWT_SECRET || 'super-secret', signOptions: { expiresIn: '1d' }, }), ], controllers: [AppController, AuthController, PublicTourController, TourController, UserController, RoutingController, LegController, LocationController, CommentController, PhotoController], providers: [prisma_service_1.PrismaService, jwt_strategy_1.JwtStrategy, TourRoleGuard, jwt_auth_guard_1.JwtAuthGuard, admin_guard_1.AdminGuard, CommentGateway, core_2.Reflector], exports: [prisma_service_1.PrismaService] }) ], AppModule); bootstrap().catch(err => { console.error('💥 Lỗi khởi động Server:'); console.error(err); process.exit(1); }); //# sourceMappingURL=main.js.map