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 __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); } }; import 'reflect-metadata'; import { NestFactory } from '@nestjs/core'; import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query } from '@nestjs/common'; import { PrismaService } from './prisma.service.js'; import 'dotenv/config'; import * as bcrypt from 'bcrypt'; import { JwtModule, JwtService } from '@nestjs/jwt'; import { JwtAuthGuard } from './jwt-auth.guard.js'; import { JwtStrategy } from './jwt.strategy.js'; import { TourRoleGuard } from './rbac.middleware.js'; let AppController = class AppController { getHello() { return 'Travel Planning API is running!'; } }; __decorate([ Get(), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", String) ], AppController.prototype, "getHello", null); AppController = __decorate([ 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 UnauthorizedException('Email hoặc mật khẩu không chính xác'); } if (user.isBlocked) { throw new 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 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([ Get('status'), __metadata("design:type", Function), __metadata("design:paramtypes", []), __metadata("design:returntype", Promise) ], AuthController.prototype, "getStatus", null); __decorate([ Post('login'), __param(0, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], AuthController.prototype, "login", null); __decorate([ Post('signup'), __param(0, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], AuthController.prototype, "signup", null); AuthController = __decorate([ Controller('v1/auth'), __metadata("design:paramtypes", [PrismaService, JwtService]) ], AuthController); let TourController = class TourController { constructor(prisma) { this.prisma = prisma; } async createTour(body, req) { const { title, startDate, endDate } = body; return this.prisma.tour.create({ data: { title, startDate: startDate ? new Date(startDate) : null, endDate: endDate ? new Date(endDate) : null, createdById: req.user.id, participants: { create: { userId: req.user.id, role: 'OWNER' } }, legs: { create: { sequence: 1, note: 'Chặng khởi đầu' } } } }); } 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 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 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 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 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 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, startDate: body.startDate ? new Date(body.startDate) : undefined, endDate: body.endDate ? new Date(body.endDate) : undefined, }, }); } async deleteTour(id) { 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: { photos: { take: 1 }, legs: { orderBy: { sequence: 'asc' }, include: { locations: { orderBy: { plannedStart: 'asc' } } } } } }); } 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: { locations: { orderBy: { plannedStart: 'asc' } }, }, }, }, }); if (!tour) throw new 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'; 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 } } }, }); } return this.prisma.tourParticipant.create({ data: { tourId, userId: body.userId, role, }, include: { user: { select: { id: true, name: true, email: true } } }, }); } }; __decorate([ UseGuards(JwtAuthGuard), Post(), __param(0, Body()), __param(1, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "createTour", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/locations'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __param(2, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addLocation", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/start-point'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __param(2, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTourStartPoint", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/end-point'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __param(2, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTourEndPoint", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/legs/batch'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "initializeLegs", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/legs'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addLeg", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Patch(':id'), __param(0, Param('id', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "updateTour", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Delete(':id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TourController.prototype, "deleteTour", null); __decorate([ UseGuards(JwtAuthGuard), Get('explore'), __param(0, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "getPublicTours", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Get(':id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], TourController.prototype, "getTourDetails", null); __decorate([ UseGuards(JwtAuthGuard, TourRoleGuard), Post(':tourId/members'), __param(0, Param('tourId', ParseUUIDPipe)), __param(1, Body()), __param(2, Req()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object, Object]), __metadata("design:returntype", Promise) ], TourController.prototype, "addMember", null); TourController = __decorate([ Controller('v1/tours'), __metadata("design:paramtypes", [PrismaService]) ], 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([ Patch(':id'), __param(0, Param('id', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], LocationController.prototype, "updateLocation", null); __decorate([ Delete(':id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], LocationController.prototype, "deleteLocation", null); LocationController = __decorate([ Controller('v1/locations'), UseGuards(JwtAuthGuard), __metadata("design:paramtypes", [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 } }); } 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 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([ Patch(':id'), __param(0, Param('id', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], LegController.prototype, "updateLeg", null); __decorate([ Delete(':id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], LegController.prototype, "deleteLeg", null); LegController = __decorate([ Controller('v1/legs'), UseGuards(JwtAuthGuard), __metadata("design:paramtypes", [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 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([ Post('optimize/:legId'), __param(0, Param('legId', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], RoutingController.prototype, "optimize", null); RoutingController = __decorate([ Controller('v1/routing'), __metadata("design:paramtypes", [PrismaService]) ], RoutingController); 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 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 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 BadRequestException('Không thể xóa Quản trị viên cuối cùng'); } await this.prisma.tourParticipant.deleteMany({ where: { userId: id } }); await this.prisma.user.delete({ where: { id } }); return { message: 'Đã xóa người dùng' }; } async toggleBlock(id) { const user = await this.prisma.user.findUnique({ where: { id } }); if (!user) throw new 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([ Get(), __param(0, Req()), __param(1, Query('q')), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, String]), __metadata("design:returntype", Promise) ], UserController.prototype, "getAllUsers", null); __decorate([ Patch(':id'), __param(0, Param('id', ParseUUIDPipe)), __param(1, Body()), __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", Promise) ], UserController.prototype, "updateUser", null); __decorate([ Delete(':id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], UserController.prototype, "deleteUser", null); __decorate([ Post('block/:id'), __param(0, Param('id', ParseUUIDPipe)), __metadata("design:type", Function), __metadata("design:paramtypes", [String]), __metadata("design:returntype", Promise) ], UserController.prototype, "toggleBlock", null); UserController = __decorate([ Controller('v1/users'), __metadata("design:paramtypes", [PrismaService]) ], UserController); let AppModule = class AppModule { }; AppModule = __decorate([ Module({ imports: [ JwtModule.register({ secret: process.env.JWT_SECRET || 'super-secret', signOptions: { expiresIn: '1d' }, }), ], controllers: [AppController, AuthController, TourController, UserController, RoutingController, LegController, LocationController], providers: [PrismaService, JwtStrategy, TourRoleGuard], exports: [PrismaService] }) ], AppModule); async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors(); app.setGlobalPrefix('api'); const port = process.env.PORT || 3001; await app.listen(port); console.log(`🚀 Server is running on: http://localhost:${port}`); } bootstrap(); //# sourceMappingURL=main.js.map