Spaces:
Sleeping
Sleeping
| import { | |
| Body, | |
| Controller, | |
| Delete, | |
| Get, | |
| Headers, | |
| HttpException, | |
| Param, | |
| Post, | |
| Put, | |
| UseGuards, | |
| } from '@nestjs/common'; | |
| import type { User } from '../../types'; | |
| import { AssignmentsService } from './assignments.service'; | |
| import { JwtAuthGuard } from '../auth/jwt-auth.guard'; | |
| import { CurrentUser } from '../auth/current-user.decorator'; | |
| type Trip = NonNullable<ReturnType<AssignmentsService['verifyTripAccess']>>; | |
| /** Shared trip-access guard (mirrors requireTripAccess → 404 "Trip not found"). */ | |
| function requireTrip(svc: AssignmentsService, tripId: string, user: User): Trip { | |
| const trip = svc.verifyTripAccess(tripId, user.id); | |
| if (!trip) { | |
| throw new HttpException({ error: 'Trip not found' }, 404); | |
| } | |
| return trip; | |
| } | |
| function requireEdit(svc: AssignmentsService, trip: Trip, user: User): void { | |
| if (!svc.canEdit(trip, user)) { | |
| throw new HttpException({ error: 'No permission' }, 403); | |
| } | |
| } | |
| /** | |
| * /api/trips/:tripId/days/:dayId/assignments — the day's ordered itinerary items. | |
| * | |
| * Byte-identical to the legacy Express route (server/src/routes/assignments.ts): | |
| * trip access (404), 'day_edit' on mutations (403, GET is access-only), create | |
| * 201 / rest 200, the bespoke "Day not found" / "Place not found" / "Assignment | |
| * not found" bodies, the journey place-created hook, and WebSocket broadcasts. | |
| */ | |
| ('api/trips/:tripId/days/:dayId/assignments') | |
| (JwtAuthGuard) | |
| export class DayAssignmentsController { | |
| constructor(private readonly assignments: AssignmentsService) {} | |
| () | |
| list(() user: User, ('tripId') tripId: string, ('dayId') dayId: string) { | |
| requireTrip(this.assignments, tripId, user); | |
| if (!this.assignments.dayExists(dayId, tripId)) { | |
| throw new HttpException({ error: 'Day not found' }, 404); | |
| } | |
| return { assignments: this.assignments.listDayAssignments(dayId) }; | |
| } | |
| () | |
| create( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('dayId') dayId: string, | |
| () body: { place_id?: unknown; notes?: string | null }, | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| if (!this.assignments.dayExists(dayId, tripId)) { | |
| throw new HttpException({ error: 'Day not found' }, 404); | |
| } | |
| if (!this.assignments.placeExists(body.place_id, tripId)) { | |
| throw new HttpException({ error: 'Place not found' }, 404); | |
| } | |
| const assignment = this.assignments.createAssignment(dayId, body.place_id, body.notes); | |
| this.assignments.broadcast(tripId, 'assignment:created', { assignment }, socketId); | |
| this.assignments.notifyPlaceCreated(tripId, body.place_id); | |
| return { assignment }; | |
| } | |
| ('reorder') | |
| reorder( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('dayId') dayId: string, | |
| ('orderedIds') orderedIds: number[], | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| if (!this.assignments.dayExists(dayId, tripId)) { | |
| throw new HttpException({ error: 'Day not found' }, 404); | |
| } | |
| this.assignments.reorderAssignments(dayId, orderedIds); | |
| this.assignments.broadcast(tripId, 'assignment:reordered', { dayId: Number(dayId), orderedIds }, socketId); | |
| return { success: true }; | |
| } | |
| (':id') | |
| remove( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('dayId') dayId: string, | |
| ('id') id: string, | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| if (!this.assignments.assignmentExistsInDay(id, dayId, tripId)) { | |
| throw new HttpException({ error: 'Assignment not found' }, 404); | |
| } | |
| this.assignments.deleteAssignment(id); | |
| this.assignments.broadcast(tripId, 'assignment:deleted', { assignmentId: Number(id), dayId: Number(dayId) }, socketId); | |
| return { success: true }; | |
| } | |
| } | |
| /** | |
| * /api/trips/:tripId/assignments/:id/* — per-assignment ops (move, time, | |
| * participants), independent of the day path. Same parity rules as above. | |
| */ | |
| ('api/trips/:tripId/assignments') | |
| (JwtAuthGuard) | |
| export class AssignmentOpsController { | |
| constructor(private readonly assignments: AssignmentsService) {} | |
| (':id/move') | |
| move( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('id') id: string, | |
| () body: { new_day_id?: unknown; order_index?: number }, | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| const existing = this.assignments.getAssignmentForTrip(id, tripId); | |
| if (!existing) { | |
| throw new HttpException({ error: 'Assignment not found' }, 404); | |
| } | |
| if (!this.assignments.dayExists(String(body.new_day_id), tripId)) { | |
| throw new HttpException({ error: 'Target day not found' }, 404); | |
| } | |
| const oldDayId = (existing as { day_id: number }).day_id; | |
| const { assignment } = this.assignments.moveAssignment(id, body.new_day_id, body.order_index, oldDayId); | |
| this.assignments.broadcast(tripId, 'assignment:moved', { assignment, oldDayId: Number(oldDayId), newDayId: Number(body.new_day_id) }, socketId); | |
| return { assignment }; | |
| } | |
| (':id/participants') | |
| participants(() user: User, ('tripId') tripId: string, ('id') id: string) { | |
| requireTrip(this.assignments, tripId, user); | |
| return { participants: this.assignments.getParticipants(id) }; | |
| } | |
| (':id/time') | |
| time( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('id') id: string, | |
| () body: { place_time?: string | null; end_time?: string | null }, | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| if (!this.assignments.getAssignmentForTrip(id, tripId)) { | |
| throw new HttpException({ error: 'Assignment not found' }, 404); | |
| } | |
| const assignment = this.assignments.updateTime(id, body.place_time, body.end_time); | |
| this.assignments.broadcast(tripId, 'assignment:updated', { assignment }, socketId); | |
| return { assignment }; | |
| } | |
| (':id/participants') | |
| setParticipants( | |
| () user: User, | |
| ('tripId') tripId: string, | |
| ('id') id: string, | |
| ('user_ids') userIds: unknown, | |
| ('x-socket-id') socketId?: string, | |
| ) { | |
| const trip = requireTrip(this.assignments, tripId, user); | |
| requireEdit(this.assignments, trip, user); | |
| if (!Array.isArray(userIds)) { | |
| throw new HttpException({ error: 'user_ids must be an array' }, 400); | |
| } | |
| const participants = this.assignments.setParticipants(id, userIds); | |
| this.assignments.broadcast(tripId, 'assignment:participants', { assignmentId: Number(id), participants }, socketId); | |
| return { participants }; | |
| } | |
| } | |