Files
TREK/shared/src/assignment/assignment.schema.ts
T
Maurice 8fa75b3c4e feat(tours): show a multi-day tour's stages in the list and put each on a day
A multi-day tour in the planner's list of the trip's tours now unfolds into
its stages (MDT-02), so the stages can be seen together and fitted into the
trip without leaving the one route they are cut from.

- The fold beside a multi-day tour lists its stages in route order, each
  from stop to stop with its distance, climb, descent and moving time, read
  off the saved tour the way the planner reads them, and below them what the
  whole tour adds up to. A value no stage tells is left out, a total some
  stages do not tell is marked as at least that much and says for how many,
  and a stage whose route is not planned yet says so.
- Each stage has its own day: a pill opens the trip's days, moves the stage
  to another one or takes it off again. A stage without a day shows a dashed
  "No day" pill. A day holds a tour, or one of its stages, once, and a stage
  is on one day at most; the plan's own offline path writes it.
- A stage opens in the planner: the tour opens as any tour does and the
  stage is shown once its points are in.
- The day plan names the stage a day holds with its own figures, and the
  day's route runs to that stage's start and on from its end; the trip map
  draws that stretch only. The tour list still counts a tour once, however
  many days hold it or its stages.
- A saved change that takes a stage away (its stage end gone, the tour a day
  tour again, back to the recorded track) takes it off its day in the same
  write and tells every client; the planner says so before the save.
- REST and the assign_place_to_day tool take the stage by the key of the
  stop it starts at, with the same checks; list_tours names the stops
  between the stages and the stage each day holds.

A day assignment remembers its stage by that key (migration 046010); every
assignment made before holds the whole tour, as it did.
2026-10-20 15:28:22 +02:00

142 lines
6.4 KiB
TypeScript

import { assignmentPlaceSchema } from '../place/place.schema';
import { stationKeySchema } from '../tours/tours.schema';
import { z } from 'zod';
/**
* Assignment API contract — single source of truth for the place↔day itinerary
* endpoints under /api/trips/:tripId/days/:dayId/assignments and
* /api/trips/:tripId/assignments/:id/*.
*
* Trip-scoped; mutations use the 'day_edit' permission. The server side is the
* DI-native AssignmentsService (server/src/nest/assignments/), validated via
* the createZodDto wrappers in assignments.dto.ts. Assignment rows carry
* joined place data and are kept open in responses; the request schemas + the
* bespoke 404 controller messages pin the rest.
*/
/**
* Assignment participant embedded on an assignment
* (server/src/services/queryHelpers.ts -> loadParticipantsByAssignmentIds).
*/
export const assignmentParticipantSchema = z.object({
user_id: z.number(),
username: z.string(),
avatar: z.string().nullable().optional(),
});
export type AssignmentParticipant = z.infer<typeof assignmentParticipantSchema>;
/**
* Assignment entity as returned by the day/assignment endpoints
* (server/src/services/queryHelpers.ts -> formatAssignmentWithPlace, and
* assignmentService.getAssignmentWithPlace). The embedded `place` is the trimmed
* assignment-place projection, NOT the full place pool entity. `assignment_time`
* /`assignment_end_time` carry the per-assignment override times.
*/
export const assignmentSchema = z.object({
id: z.number(),
day_id: z.number(),
place_id: z.number(),
order_index: z.number(),
notes: z.string().nullable().optional(),
assignment_time: z.string().nullable().optional(),
assignment_end_time: z.string().nullable().optional(),
end_day: z.boolean().optional(),
// Per-segment travel mode (#1281): the transport mode of the leg LEAVING this
// stop for the next one. null = inherit the day's default_transport_mode.
leg_transport_mode: z.string().nullable().optional(),
// Per-segment travel mode of the leg ENTERING this stop when its origin is
// not a place (booking arrival / morning hotel). null = inherit the day default.
// Inert when the previous timeline element is a place.
incoming_leg_transport_mode: z.string().nullable().optional(),
// Kept on the day and on the map, but not part of the day's route (#2532).
route_excluded: z.boolean().optional(),
// The lodging booking that put this stop on the day, when one did. The day
// planner already shows that booking as its own overnight block, so it hides
// this row and leaves it to road trip mode, which is the view that needs the
// hotel in the driving chain. Null for every stop a traveller placed.
accommodation_id: z.number().nullable().optional(),
tour_place_id: z.number().nullable().optional(),
tour_route_geometry: z.string().nullable().optional(),
/**
* MDT-02: the stage of a multi-day tour this day holds, by the key of the
* stop the stage starts at; null where the day holds the whole tour or a place.
*/
tour_stage_key: z.string().nullable().optional(),
participants: z.array(assignmentParticipantSchema).optional(),
created_at: z.string().optional(),
place: assignmentPlaceSchema,
});
export type Assignment = z.infer<typeof assignmentSchema>;
export const assignmentCreateRequestSchema = z.object({
place_id: z.union([z.number(), z.string()]),
notes: z.string().nullable().optional(),
/**
* MDT-02: puts one stage of a multi-day tour on the day instead of the whole
* tour, by the key of the stop the stage starts at (the tour list's
* `stage_stops`). A stage is on one day at most.
*/
tour_stage_key: stationKeySchema.optional(),
});
export type AssignmentCreateRequest = z.infer<typeof assignmentCreateRequestSchema>;
export const assignmentReorderRequestSchema = z.object({
orderedIds: z.array(z.number()),
});
export type AssignmentReorderRequest = z.infer<typeof assignmentReorderRequestSchema>;
export const assignmentMoveRequestSchema = z.object({
new_day_id: z.union([z.number(), z.string()]),
// The client api types this `number | null` (assignmentsApi.move) and the
// legacy route accepted null (`orderIndex || 0`) — keep null on the wire.
order_index: z.number().nullable().optional(),
});
export type AssignmentMoveRequest = z.infer<typeof assignmentMoveRequestSchema>;
export const assignmentTimeRequestSchema = z.object({
place_time: z.string().nullable().optional(),
end_time: z.string().nullable().optional(),
});
export type AssignmentTimeRequest = z.infer<typeof assignmentTimeRequestSchema>;
export const assignmentEndDayRequestSchema = z.object({
end_day: z.boolean(),
});
export type AssignmentEndDayRequest = z.infer<typeof assignmentEndDayRequestSchema>;
/**
* PUT /:id/notes (#2163) — edit the per-assignment note after creation.
* Deliberately uncapped like the create contract above: the REST create body
* and the plugin RPC accept notes of any length, so a length cap here would
* strand an existing longer note as uneditable (the unchanged textarea would
* be rejected on save). Falsy values ('' or null) clear the column, the same
* `notes || null` normalisation createAssignment applies on the way in.
*/
export const assignmentNotesRequestSchema = z.object({
notes: z.string().nullable(),
});
export type AssignmentNotesRequest = z.infer<typeof assignmentNotesRequestSchema>;
/** Set the leg's travel mode (a RouteProfileKey, or null to inherit the day default). */
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<typeof assignmentTransportRequestSchema>;
/** Take a stop out of the day's route, or put it back (#2532). */
export const assignmentRouteRequestSchema = z.object({
excluded: z.boolean(),
});
export type AssignmentRouteRequest = z.infer<typeof assignmentRouteRequestSchema>;
export const assignmentParticipantsRequestSchema = z.object({
user_ids: z.array(z.number()),
});
export type AssignmentParticipantsRequest = z.infer<typeof assignmentParticipantsRequestSchema>;