Agregar campo username a User entity y DTO
- Columna username (unique, nullable) en auth.users - Campo username en CreateUserDto y UpdateUserDto Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
147
src/modules/trips/dto/create-trip.dto.ts
Normal file
147
src/modules/trips/dto/create-trip.dto.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsBoolean, IsNumber, IsDateString, IsArray, IsEnum, MaxLength } from 'class-validator';
|
||||
import { TripStatus } from '../../../entities/user-trip.entity';
|
||||
|
||||
export class CreateTripDto {
|
||||
@ApiProperty({ description: 'Trip name', example: 'Summer Vacation in DR' })
|
||||
@IsString()
|
||||
@MaxLength(150)
|
||||
name: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Trip description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Cover image URL' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
coverImageUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Start date', example: '2026-06-01' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
startDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'End date', example: '2026-06-07' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
endDate?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Destination' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
destination?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Number of travelers', default: 1 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
travelersCount?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Estimated budget' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
estimatedBudget?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Budget currency', default: 'USD' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
budgetCurrency?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Trip tags' })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
tags?: string[];
|
||||
|
||||
@ApiPropertyOptional({ description: 'Is public trip', default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isPublic?: boolean;
|
||||
}
|
||||
|
||||
export class CreateTripDayDto {
|
||||
@ApiProperty({ description: 'Day number', example: 1 })
|
||||
@IsNumber()
|
||||
dayNumber: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Date', example: '2026-06-01' })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
date?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Day title' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
title?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Notes' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateTripActivityDto {
|
||||
@ApiPropertyOptional({ description: 'Reference item ID' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
itemId?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Item type' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
itemType?: string;
|
||||
|
||||
@ApiProperty({ description: 'Activity title' })
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
title: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Description' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Start time', example: '09:00' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
startTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'End time', example: '12:00' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
endTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Duration in minutes' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
durationMinutes?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Location name' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locationName?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Location address' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locationAddress?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Location coordinates' })
|
||||
@IsOptional()
|
||||
locationCoords?: { lat: number; lng: number };
|
||||
|
||||
@ApiPropertyOptional({ description: 'Estimated cost' })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
estimatedCost?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Image URL' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
imageUrl?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Notes' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
2
src/modules/trips/dto/index.ts
Normal file
2
src/modules/trips/dto/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './create-trip.dto';
|
||||
export * from './update-trip.dto';
|
||||
16
src/modules/trips/dto/update-trip.dto.ts
Normal file
16
src/modules/trips/dto/update-trip.dto.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './create-trip.dto';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsEnum, IsOptional } from 'class-validator';
|
||||
import { TripStatus } from '../../../entities/user-trip.entity';
|
||||
|
||||
export class UpdateTripDto extends PartialType(CreateTripDto) {
|
||||
@ApiPropertyOptional({ description: 'Trip status', enum: TripStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(TripStatus)
|
||||
status?: TripStatus;
|
||||
}
|
||||
|
||||
export class UpdateTripDayDto extends PartialType(CreateTripDayDto) {}
|
||||
|
||||
export class UpdateTripActivityDto extends PartialType(CreateTripActivityDto) {}
|
||||
145
src/modules/trips/trips.controller.ts
Normal file
145
src/modules/trips/trips.controller.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Controller, Get, Post, Body, Patch, Param, Delete, Query, UseGuards, Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam,
|
||||
} from '@nestjs/swagger';
|
||||
import { TripsService } from './trips.service';
|
||||
import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './dto/create-trip.dto';
|
||||
import { UpdateTripDto, UpdateTripDayDto, UpdateTripActivityDto } from './dto/update-trip.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { UserTrip, TripDay, TripActivity, TripStatus } from '../../entities/user-trip.entity';
|
||||
|
||||
@ApiTags('Trips')
|
||||
@Controller('trips')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class TripsController {
|
||||
constructor(private readonly tripsService: TripsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Create a new trip' })
|
||||
@ApiResponse({ status: 201, type: UserTrip })
|
||||
create(@Body() createDto: CreateTripDto, @Request() req) {
|
||||
return this.tripsService.create(req.user.id, createDto);
|
||||
}
|
||||
|
||||
@Get('my')
|
||||
@ApiOperation({ summary: 'Get my trips' })
|
||||
@ApiQuery({ name: 'status', required: false, enum: TripStatus })
|
||||
@ApiQuery({ name: 'page', required: false, type: Number })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
findMyTrips(
|
||||
@Request() req,
|
||||
@Query('status') status?: TripStatus,
|
||||
@Query('page') page?: number,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.tripsService.findAllByUser(req.user.id, status, page, limit);
|
||||
}
|
||||
|
||||
@Get('my/stats')
|
||||
@ApiOperation({ summary: 'Get trip statistics' })
|
||||
getStats(@Request() req) {
|
||||
return this.tripsService.getTripStats(req.user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get trip by ID' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
findOne(@Param('id') id: string, @Request() req) {
|
||||
return this.tripsService.findOne(req.user.id, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: 'Update trip' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
update(@Param('id') id: string, @Body() updateDto: UpdateTripDto, @Request() req) {
|
||||
return this.tripsService.update(req.user.id, id, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: 'Delete trip' })
|
||||
@ApiParam({ name: 'id', type: 'string' })
|
||||
remove(@Param('id') id: string, @Request() req) {
|
||||
return this.tripsService.remove(req.user.id, id);
|
||||
}
|
||||
|
||||
// Days
|
||||
@Post(':tripId/days')
|
||||
@ApiOperation({ summary: 'Add day to trip' })
|
||||
addDay(
|
||||
@Param('tripId') tripId: string,
|
||||
@Body() createDto: CreateTripDayDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.addDay(req.user.id, tripId, createDto);
|
||||
}
|
||||
|
||||
@Patch(':tripId/days/:dayId')
|
||||
@ApiOperation({ summary: 'Update day' })
|
||||
updateDay(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Body() updateDto: UpdateTripDayDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.updateDay(req.user.id, tripId, dayId, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':tripId/days/:dayId')
|
||||
@ApiOperation({ summary: 'Remove day' })
|
||||
removeDay(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.removeDay(req.user.id, tripId, dayId);
|
||||
}
|
||||
|
||||
// Activities
|
||||
@Post(':tripId/days/:dayId/activities')
|
||||
@ApiOperation({ summary: 'Add activity to day' })
|
||||
addActivity(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Body() createDto: CreateTripActivityDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.addActivity(req.user.id, tripId, dayId, createDto);
|
||||
}
|
||||
|
||||
@Patch(':tripId/days/:dayId/activities/:activityId')
|
||||
@ApiOperation({ summary: 'Update activity' })
|
||||
updateActivity(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Param('activityId') activityId: string,
|
||||
@Body() updateDto: UpdateTripActivityDto,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.updateActivity(req.user.id, tripId, dayId, activityId, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':tripId/days/:dayId/activities/:activityId')
|
||||
@ApiOperation({ summary: 'Remove activity' })
|
||||
removeActivity(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Param('activityId') activityId: string,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.removeActivity(req.user.id, tripId, dayId, activityId);
|
||||
}
|
||||
|
||||
@Patch(':tripId/days/:dayId/activities/order')
|
||||
@ApiOperation({ summary: 'Reorder activities in day' })
|
||||
reorderActivities(
|
||||
@Param('tripId') tripId: string,
|
||||
@Param('dayId') dayId: string,
|
||||
@Body() body: { activityIds: string[] },
|
||||
@Request() req,
|
||||
) {
|
||||
return this.tripsService.reorderActivities(req.user.id, tripId, dayId, body.activityIds);
|
||||
}
|
||||
}
|
||||
13
src/modules/trips/trips.module.ts
Normal file
13
src/modules/trips/trips.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TripsService } from './trips.service';
|
||||
import { TripsController } from './trips.controller';
|
||||
import { UserTrip, TripDay, TripActivity } from '../../entities/user-trip.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([UserTrip, TripDay, TripActivity])],
|
||||
controllers: [TripsController],
|
||||
providers: [TripsService],
|
||||
exports: [TripsService],
|
||||
})
|
||||
export class TripsModule {}
|
||||
181
src/modules/trips/trips.service.ts
Normal file
181
src/modules/trips/trips.service.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UserTrip, TripDay, TripActivity, TripStatus } from '../../entities/user-trip.entity';
|
||||
import { CreateTripDto, CreateTripDayDto, CreateTripActivityDto } from './dto/create-trip.dto';
|
||||
import { UpdateTripDto, UpdateTripDayDto, UpdateTripActivityDto } from './dto/update-trip.dto';
|
||||
|
||||
@Injectable()
|
||||
export class TripsService {
|
||||
constructor(
|
||||
@InjectRepository(UserTrip)
|
||||
private readonly tripRepository: Repository<UserTrip>,
|
||||
@InjectRepository(TripDay)
|
||||
private readonly dayRepository: Repository<TripDay>,
|
||||
@InjectRepository(TripActivity)
|
||||
private readonly activityRepository: Repository<TripActivity>,
|
||||
) {}
|
||||
|
||||
async create(userId: string, createDto: CreateTripDto): Promise<UserTrip> {
|
||||
const trip = this.tripRepository.create({
|
||||
...createDto,
|
||||
userId,
|
||||
status: TripStatus.PLANNING,
|
||||
});
|
||||
return this.tripRepository.save(trip);
|
||||
}
|
||||
|
||||
async findAllByUser(
|
||||
userId: string,
|
||||
status?: TripStatus,
|
||||
page = 1,
|
||||
limit = 20,
|
||||
): Promise<{ data: UserTrip[]; total: number }> {
|
||||
const where: any = { userId };
|
||||
if (status) where.status = status;
|
||||
|
||||
const [data, total] = await this.tripRepository.findAndCount({
|
||||
where,
|
||||
relations: ['days', 'days.activities'],
|
||||
order: { updatedAt: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return { data, total };
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string): Promise<UserTrip> {
|
||||
const trip = await this.tripRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['days', 'days.activities'],
|
||||
});
|
||||
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
if (trip.userId !== userId && !trip.isPublic) {
|
||||
throw new ForbiddenException('Access denied');
|
||||
}
|
||||
|
||||
if (trip.days) {
|
||||
trip.days.sort((a, b) => a.dayNumber - b.dayNumber);
|
||||
trip.days.forEach(day => {
|
||||
if (day.activities) {
|
||||
day.activities.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return trip;
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, updateDto: UpdateTripDto): Promise<UserTrip> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
Object.assign(trip, updateDto);
|
||||
return this.tripRepository.save(trip);
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string): Promise<void> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
await this.tripRepository.remove(trip);
|
||||
}
|
||||
|
||||
async addDay(userId: string, tripId: string, createDto: CreateTripDayDto): Promise<TripDay> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const day = this.dayRepository.create({ ...createDto, tripId });
|
||||
return this.dayRepository.save(day);
|
||||
}
|
||||
|
||||
async updateDay(userId: string, tripId: string, dayId: string, updateDto: UpdateTripDayDto): Promise<TripDay> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } });
|
||||
if (!day) throw new NotFoundException('Day not found');
|
||||
|
||||
Object.assign(day, updateDto);
|
||||
return this.dayRepository.save(day);
|
||||
}
|
||||
|
||||
async removeDay(userId: string, tripId: string, dayId: string): Promise<void> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } });
|
||||
if (!day) throw new NotFoundException('Day not found');
|
||||
|
||||
await this.dayRepository.remove(day);
|
||||
}
|
||||
|
||||
async addActivity(userId: string, tripId: string, dayId: string, createDto: CreateTripActivityDto): Promise<TripActivity> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const day = await this.dayRepository.findOne({ where: { id: dayId, tripId } });
|
||||
if (!day) throw new NotFoundException('Day not found');
|
||||
|
||||
const maxOrder = await this.activityRepository
|
||||
.createQueryBuilder('a')
|
||||
.select('MAX(a.sort_order)', 'max')
|
||||
.where('a.day_id = :dayId', { dayId })
|
||||
.getRawOne();
|
||||
|
||||
const activity = this.activityRepository.create({
|
||||
...createDto,
|
||||
dayId,
|
||||
sortOrder: (maxOrder?.max || 0) + 1,
|
||||
});
|
||||
return this.activityRepository.save(activity);
|
||||
}
|
||||
|
||||
async updateActivity(userId: string, tripId: string, dayId: string, activityId: string, updateDto: UpdateTripActivityDto): Promise<TripActivity> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const activity = await this.activityRepository.findOne({ where: { id: activityId, dayId } });
|
||||
if (!activity) throw new NotFoundException('Activity not found');
|
||||
|
||||
Object.assign(activity, updateDto);
|
||||
return this.activityRepository.save(activity);
|
||||
}
|
||||
|
||||
async removeActivity(userId: string, tripId: string, dayId: string, activityId: string): Promise<void> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
const activity = await this.activityRepository.findOne({ where: { id: activityId, dayId } });
|
||||
if (!activity) throw new NotFoundException('Activity not found');
|
||||
|
||||
await this.activityRepository.remove(activity);
|
||||
}
|
||||
|
||||
async reorderActivities(userId: string, tripId: string, dayId: string, activityIds: string[]): Promise<void> {
|
||||
const trip = await this.tripRepository.findOne({ where: { id: tripId, userId } });
|
||||
if (!trip) throw new NotFoundException('Trip not found');
|
||||
|
||||
await Promise.all(
|
||||
activityIds.map((id, index) =>
|
||||
this.activityRepository.update({ id, dayId }, { sortOrder: index })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async getTripStats(userId: string): Promise<Record<TripStatus, number>> {
|
||||
const counts = await this.tripRepository
|
||||
.createQueryBuilder('t')
|
||||
.select('t.status', 'status')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('t.user_id = :userId', { userId })
|
||||
.groupBy('t.status')
|
||||
.getRawMany();
|
||||
|
||||
const result: Record<string, number> = {};
|
||||
Object.values(TripStatus).forEach(s => (result[s] = 0));
|
||||
counts.forEach(c => (result[c.status] = parseInt(c.count, 10)));
|
||||
return result as Record<TripStatus, number>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user