Reorder whole days and insert a day (#589) (#1148)

* 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
This commit is contained in:
Maurice
2026-06-12 00:17:49 +02:00
committed by GitHub
parent 1378c95078
commit f46cc8a98e
34 changed files with 872 additions and 9 deletions
+35 -3
View File
@@ -12,6 +12,7 @@ import {
} 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';
@@ -52,16 +53,47 @@ export class DaysController {
create(
@CurrentUser() user: User,
@Param('tripId') tripId: string,
@Body() body: { date?: string; notes?: 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);
const day = this.days.create(tripId, body.date, body.notes);
this.days.broadcast(tripId, 'day:created', { day }, socketId);
// 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,