diff --git a/server/src/nest/assignments/assignments.controller.ts b/server/src/nest/assignments/assignments.controller.ts index e942dd845..d2c0dbecd 100644 --- a/server/src/nest/assignments/assignments.controller.ts +++ b/server/src/nest/assignments/assignments.controller.ts @@ -175,7 +175,9 @@ export class AssignmentOpsController { if (!this.assignments.getAssignmentForTrip(id, tripId)) { throw new HttpException({ error: 'Assignment not found' }, 404); } - const assignment = this.assignments.setLegTransportMode(id, body.transport_mode ?? null); + const assignment = body.direction === 'incoming' + ? this.assignments.setIncomingLegTransportMode(id, body.transport_mode ?? null) + : this.assignments.setLegTransportMode(id, body.transport_mode ?? null); this.assignments.broadcast(tripId, 'assignment:updated', { assignment }, socketId); return { assignment }; } diff --git a/server/src/nest/assignments/assignments.service.ts b/server/src/nest/assignments/assignments.service.ts index 8e901a666..379943811 100644 --- a/server/src/nest/assignments/assignments.service.ts +++ b/server/src/nest/assignments/assignments.service.ts @@ -272,6 +272,17 @@ export class AssignmentsService { return this.getAssignmentWithPlace(Number(id)); } + /** + * Set the travel mode of the leg arriving at this stop (#1281 boundary legs). + * Mirrors setLegTransportMode but targets incoming_leg_transport_mode; inert + * when the previous timeline element is a place (the column is only read for + * non-place origins like a booking arrival or a morning hotel departure). + */ + setIncomingLegTransportMode(id: string | number, mode: string | null) { + this.dbs.run('UPDATE day_assignments SET incoming_leg_transport_mode = ? WHERE id = ?', mode ?? null, id); + return this.getAssignmentWithPlace(Number(id)); + } + setParticipants(assignmentId: string | number, userIds: number[]) { this.dbs.transaction(() => { this.dbs.run('DELETE FROM assignment_participants WHERE assignment_id = ?', assignmentId); diff --git a/server/tests/integration/leg-mode-incoming.test.ts b/server/tests/integration/leg-mode-incoming.test.ts index 2f3a34e4b..3b55884d2 100644 --- a/server/tests/integration/leg-mode-incoming.test.ts +++ b/server/tests/integration/leg-mode-incoming.test.ts @@ -124,4 +124,74 @@ describe('incoming_leg_transport_mode read-path parity', () => { const a = foundDay.assignments.find((x: any) => x.id === assignmentId); expect(a.incoming_leg_transport_mode).toBe('transit'); }); + + // ─────────────────────────────────────────────────────────────────────────── + // Write path — PUT .../transport gains `direction` (outgoing|incoming). + // ─────────────────────────────────────────────────────────────────────────── + + describe('PUT /assignments/:id/transport direction', () => { + it('defaults to outgoing (back-compat)', async () => { + const { user } = createUser(testDb2); + const trip = createTrip(testDb2, user.id); + const day = createDay(testDb2, trip.id, { date: '2025-06-01' }); + const place = createPlace(testDb2, trip.id, { name: 'Test Place' }); + + const create = await request(app) + .post(`/api/trips/${trip.id}/days/${day.id}/assignments`) + .set('Cookie', authCookie(user.id)) + .send({ place_id: place.id }); + const assignmentId = create.body.assignment.id; + + const res = await request(app) + .put(`/api/trips/${trip.id}/assignments/${assignmentId}/transport`) + .set('Cookie', authCookie(user.id)) + .send({ transport_mode: 'cycling' }); + expect(res.status).toBe(200); + + const row = testDb2.prepare('SELECT leg_transport_mode, incoming_leg_transport_mode FROM day_assignments WHERE id = ?').get(assignmentId); + expect(row.leg_transport_mode).toBe('cycling'); + expect(row.incoming_leg_transport_mode).toBeNull(); + }); + + it("direction: 'incoming' writes the incoming column", async () => { + const { user } = createUser(testDb2); + const trip = createTrip(testDb2, user.id); + const day = createDay(testDb2, trip.id, { date: '2025-06-01' }); + const place = createPlace(testDb2, trip.id, { name: 'Test Place' }); + + const create = await request(app) + .post(`/api/trips/${trip.id}/days/${day.id}/assignments`) + .set('Cookie', authCookie(user.id)) + .send({ place_id: place.id }); + const assignmentId = create.body.assignment.id; + + const res = await request(app) + .put(`/api/trips/${trip.id}/assignments/${assignmentId}/transport`) + .set('Cookie', authCookie(user.id)) + .send({ transport_mode: 'transit', direction: 'incoming' }); + expect(res.status).toBe(200); + + const row = testDb2.prepare('SELECT incoming_leg_transport_mode FROM day_assignments WHERE id = ?').get(assignmentId); + expect(row.incoming_leg_transport_mode).toBe('transit'); + }); + + it('rejects an invalid direction with 400', async () => { + const { user } = createUser(testDb2); + const trip = createTrip(testDb2, user.id); + const day = createDay(testDb2, trip.id, { date: '2025-06-01' }); + const place = createPlace(testDb2, trip.id, { name: 'Test Place' }); + + const create = await request(app) + .post(`/api/trips/${trip.id}/days/${day.id}/assignments`) + .set('Cookie', authCookie(user.id)) + .send({ place_id: place.id }); + const assignmentId = create.body.assignment.id; + + const res = await request(app) + .put(`/api/trips/${trip.id}/assignments/${assignmentId}/transport`) + .set('Cookie', authCookie(user.id)) + .send({ transport_mode: 'walking', direction: 'sideways' }); + expect(res.status).toBe(400); + }); + }); }); diff --git a/shared/src/assignment/assignment.schema.ts b/shared/src/assignment/assignment.schema.ts index 0a70fe069..f81d827c3 100644 --- a/shared/src/assignment/assignment.schema.ts +++ b/shared/src/assignment/assignment.schema.ts @@ -83,6 +83,9 @@ export const assignmentTransportRequestSchema = z.object({ // The legacy route read `body.transport_mode ?? null`, so an absent key // behaves like an explicit null — keep it optional on the wire. transport_mode: z.string().nullable().optional(), + // Which leg this write targets: the one leaving this stop (default, and the + // legacy behaviour) or the one arriving at it (#1281 boundary legs). + direction: z.enum(['outgoing', 'incoming']).default('outgoing'), }); export type AssignmentTransportRequest = z.infer;