mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 21:31:46 +00:00
f46cc8a98e
* feat(days): reorder whole days and insert a day at a position Adds reorderDays + insertDay to the day service and a PUT /days/reorder route (plus an optional position on create). Day rows stay stable so a day's assignments, notes, bookings and accommodations ride along by id; on a dated trip the calendar dates stay pinned to their slots while the content moves across them, and each booking's date is re-stamped onto its day's new date (time-of-day preserved) so day_id stays consistent. Renumbering uses the two-phase write to avoid the UNIQUE(trip_id, day_number) collision, and a move that would invert an accommodation's check-in/out span is rejected. * feat(planner): reorder days from a toolbar popup, and add days A new toolbar button opens a popup listing the days; drag a row by its grip or use the up/down arrows to reorder, and add a day from there. Reorders apply optimistically with rollback and sync over WebSocket; the day headers are left untouched, so the existing place drop-targets are unaffected. * i18n: add day-reorder strings across all languages
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
Headers,
|
|
HttpException,
|
|
Param,
|
|
Post,
|
|
Put,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import type { User } from '../../types';
|
|
import { DaysService } from './days.service';
|
|
import { DayReorderError } from '../../services/dayService';
|
|
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
|
import { CurrentUser } from '../auth/current-user.decorator';
|
|
|
|
/**
|
|
* /api/trips/:tripId/days — trip itinerary days.
|
|
*
|
|
* Byte-identical to the legacy Express route (server/src/routes/days.ts): trip
|
|
* access (404 "Trip not found"), the 'day_edit' permission on mutations (403),
|
|
* create 201 / rest 200, the bespoke 404 "Day not found", and WebSocket
|
|
* broadcasts with the forwarded X-Socket-Id.
|
|
*/
|
|
@Controller('api/trips/:tripId/days')
|
|
@UseGuards(JwtAuthGuard)
|
|
export class DaysController {
|
|
constructor(private readonly days: DaysService) {}
|
|
|
|
private requireTrip(tripId: string, user: User) {
|
|
const trip = this.days.verifyTripAccess(tripId, user.id);
|
|
if (!trip) {
|
|
throw new HttpException({ error: 'Trip not found' }, 404);
|
|
}
|
|
return trip;
|
|
}
|
|
|
|
private requireEdit(trip: NonNullable<ReturnType<DaysService['verifyTripAccess']>>, user: User): void {
|
|
if (!this.days.canEdit(trip, user)) {
|
|
throw new HttpException({ error: 'No permission' }, 403);
|
|
}
|
|
}
|
|
|
|
@Get()
|
|
list(@CurrentUser() user: User, @Param('tripId') tripId: string) {
|
|
this.requireTrip(tripId, user);
|
|
return this.days.list(tripId);
|
|
}
|
|
|
|
@Post()
|
|
create(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Body() body: { date?: string; notes?: string; position?: number },
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const trip = this.requireTrip(tripId, user);
|
|
this.requireEdit(trip, user);
|
|
// A `position` means "insert a new empty day here" (which on a dated trip
|
|
// extends the trip and re-pins dates); without it, the legacy append.
|
|
const day = body.position !== undefined
|
|
? this.days.insert(tripId, body.position)
|
|
: this.days.create(tripId, body.date, body.notes);
|
|
// An insert can shuffle dates/positions of other days, so collaborators
|
|
// refetch the whole list; a plain append only needs the new day.
|
|
const event = body.position !== undefined ? 'day:reordered' : 'day:created';
|
|
this.days.broadcast(tripId, event, { day }, socketId);
|
|
return { day };
|
|
}
|
|
|
|
@Put('reorder')
|
|
reorder(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Body() body: { orderedIds?: number[] },
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const trip = this.requireTrip(tripId, user);
|
|
this.requireEdit(trip, user);
|
|
if (!Array.isArray(body.orderedIds)) {
|
|
throw new HttpException({ error: 'orderedIds must be an array' }, 400);
|
|
}
|
|
try {
|
|
this.days.reorder(tripId, body.orderedIds);
|
|
} catch (err) {
|
|
if (err instanceof DayReorderError) {
|
|
throw new HttpException({ error: err.message }, 400);
|
|
}
|
|
throw err;
|
|
}
|
|
this.days.broadcast(tripId, 'day:reordered', { orderedIds: body.orderedIds }, socketId);
|
|
return { success: true };
|
|
}
|
|
|
|
@Put(':id')
|
|
update(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Param('id') id: string,
|
|
@Body() body: { notes?: string; title?: string | null },
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const trip = this.requireTrip(tripId, user);
|
|
this.requireEdit(trip, user);
|
|
const current = this.days.getDay(id, tripId);
|
|
if (!current) {
|
|
throw new HttpException({ error: 'Day not found' }, 404);
|
|
}
|
|
const day = this.days.update(id, current as never, { notes: body.notes, title: body.title });
|
|
this.days.broadcast(tripId, 'day:updated', { day }, socketId);
|
|
return { day };
|
|
}
|
|
|
|
@Delete(':id')
|
|
remove(
|
|
@CurrentUser() user: User,
|
|
@Param('tripId') tripId: string,
|
|
@Param('id') id: string,
|
|
@Headers('x-socket-id') socketId?: string,
|
|
) {
|
|
const trip = this.requireTrip(tripId, user);
|
|
this.requireEdit(trip, user);
|
|
if (!this.days.getDay(id, tripId)) {
|
|
throw new HttpException({ error: 'Day not found' }, 404);
|
|
}
|
|
this.days.remove(id);
|
|
this.days.broadcast(tripId, 'day:deleted', { dayId: Number(id) }, socketId);
|
|
return { success: true };
|
|
}
|
|
}
|