Revert departure and arrival datetime of stops

This commit is contained in:
Roba Boru
2026-07-21 00:24:32 +03:00
parent 5adb145aca
commit be933c2c0d
8 changed files with 338 additions and 1402 deletions

View File

@@ -7,8 +7,6 @@ export class RouteStopInputDto {
@ApiProperty({ example: 1, description: 'Stop order (1 = origin, ascending)' }) @IsInt() @Min(1) sequence: number;
@ApiPropertyOptional({ example: 120.5, description: 'Distance in km from previous stop' }) @IsOptional() @IsNumber() distanceKm?: number;
@ApiPropertyOptional({ example: 45, description: 'Override check-in cutoff (minutes) for this stop. Falls back to route-level checkinMinutesBefore if omitted.' }) @IsOptional() @IsInt() @Min(1) checkinMinutesBefore?: number;
@ApiPropertyOptional({ example: '2026-06-15T06:30:00Z', description: 'Template planned arrival time at this stop. Only the time-of-day (EAT) is used when auto-populating new schedules. Omit for first stop.' }) @IsOptional() @IsDateString() plannedArrivalTime?: string;
@ApiPropertyOptional({ example: '2026-06-15T06:45:00Z', description: 'Template planned departure time from this stop. Only the time-of-day (EAT) is used when auto-populating new schedules. Omit for last stop.' }) @IsOptional() @IsDateString() plannedDepartureTime?: string;
}
export class CreateRouteDto {
@@ -39,8 +37,6 @@ export class AddRouteStopDto {
@ApiProperty({ example: 3 }) @IsInt() @Min(1) sequence: number;
@ApiPropertyOptional({ example: 75.5 }) @IsOptional() @IsNumber() distanceKm?: number;
@ApiPropertyOptional({ example: 45, description: 'Override check-in cutoff (minutes) for this stop. Falls back to route-level checkinMinutesBefore if omitted.' }) @IsOptional() @IsInt() @Min(1) checkinMinutesBefore?: number;
@ApiPropertyOptional({ example: '2026-06-15T06:30:00Z', description: 'Template planned arrival time at this stop (only time-of-day is used)' }) @IsOptional() @IsDateString() plannedArrivalTime?: string;
@ApiPropertyOptional({ example: '2026-06-15T06:45:00Z', description: 'Template planned departure time from this stop (only time-of-day is used)' }) @IsOptional() @IsDateString() plannedDepartureTime?: string;
}
export class UpdateRouteDto {

View File

@@ -37,8 +37,6 @@ export class RoutesService {
sequence: s.sequence,
distanceKm: s.distanceKm != null ? parseFloat(String(s.distanceKm)) : null,
checkinMinutesBefore: s.checkinMinutesBefore ?? null,
plannedArrivalTime: s.plannedArrivalTime ? new Date(s.plannedArrivalTime) : null,
plannedDepartureTime: s.plannedDepartureTime ? new Date(s.plannedDepartureTime) : null,
})),
},
},
@@ -108,8 +106,6 @@ export class RoutesService {
sequence: s.sequence,
distanceKm: s.distanceKm != null ? parseFloat(String(s.distanceKm)) : null,
checkinMinutesBefore: s.checkinMinutesBefore ?? null,
plannedArrivalTime: s.plannedArrivalTime ?? null,
plannedDepartureTime: s.plannedDepartureTime ?? null,
})),
});
}
@@ -229,8 +225,6 @@ export class RoutesService {
sequence: dto.sequence,
distanceKm: dto.distanceKm != null ? parseFloat(String(dto.distanceKm)) : null,
checkinMinutesBefore: dto.checkinMinutesBefore ?? null,
plannedArrivalTime: dto.plannedArrivalTime ?? null,
plannedDepartureTime: dto.plannedDepartureTime ?? null,
},
});
}

View File

@@ -54,20 +54,12 @@ export class CreateScheduleDto {
plannedTimes?: PlannedStopTimeDto[];
}
export class CoachAssignmentDto {
@ApiProperty({ example: 'coach-uuid' }) @IsString() coachId: string;
@ApiProperty({ example: 1 }) @IsInt() @Min(1) positionNumber: number;
}
export class UpdateScheduleDto {
@ApiPropertyOptional({ example: '2026-06-15T08:00:00Z', description: 'Scheduled departure from the first stop (origin)' }) @IsOptional() @IsDateString() departureAt?: string;
@ApiPropertyOptional({ example: '2026-06-15T20:00:00Z', description: 'Scheduled arrival at the last stop (destination)' }) @IsOptional() @IsDateString() arrivalAt?: string;
@ApiPropertyOptional({ enum: TripStatus, example: TripStatus.SCHEDULED }) @IsOptional() @IsEnum(TripStatus) status?: TripStatus;
@ApiPropertyOptional({ type: [CoachAssignmentDto], description: 'List of coaches to assign' }) @IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => CoachAssignmentDto) coaches?: CoachAssignmentDto[];
@ApiPropertyOptional({ type: Array, description: 'List of coaches to assign' }) @IsOptional() @IsArray() coaches?: Array<{ coachId: string; positionNumber: number }>;
@ApiPropertyOptional({ example: false, description: 'Exclude from public search (reserved for packages)' }) @IsOptional() isPackageOnly?: boolean;
@ApiPropertyOptional({ type: [PlannedStopTimeDto], description: 'Planned times per stop — when provided, replaces all existing stop times for the schedule' })
@IsOptional() @IsArray() @ValidateNested({ each: true }) @Type(() => PlannedStopTimeDto)
plannedTimes?: PlannedStopTimeDto[];
}
export class UpdateStopTimeDto {

View File

@@ -133,62 +133,26 @@ export class SchedulesService {
let plannedTimes = dto.plannedTimes;
if (!plannedTimes || plannedTimes.length === 0) {
const hasRouteTimes = route.stops.some(
s => (s as any).plannedArrivalTime != null || (s as any).plannedDepartureTime != null,
);
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
if (hasRouteTimes) {
// Extract EAT time-of-day from a template DateTime and anchor to the schedule's EAT date.
const EAT_MS = 3 * 60 * 60 * 1000;
const depEATMs = dep.getTime() + EAT_MS;
const depMsIntoDay = depEATMs % (24 * 60 * 60 * 1000);
const eatMidnightUTC = dep.getTime() - depMsIntoDay;
const templateToScheduleUTC = (templateDt: Date): Date => {
// Pull the time-of-day in EAT from the template DateTime
const templateEATMs = templateDt.getTime() + EAT_MS;
const timeOfDayMs = templateEATMs % (24 * 60 * 60 * 1000);
const candidate = new Date(eatMidnightUTC + timeOfDayMs);
// Overnight: if the stop time lands before departure, move to next day
if (candidate < dep) return new Date(candidate.getTime() + 24 * 60 * 60 * 1000);
return candidate;
plannedTimes = route.stops.map((stop, index) => {
let stopTime: Date;
if (index === 0) {
stopTime = dep;
} else if (index === route.stops.length - 1) {
stopTime = arr;
} else {
const stopDistance = stop.distanceKm || 0;
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
stopTime = new Date(dep.getTime() + totalDuration * progress);
}
return {
sequence: stop.sequence,
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
};
plannedTimes = route.stops.map((stop, index) => {
const arrDt: Date | null = (stop as any).plannedArrivalTime ?? null;
const depDt: Date | null = (stop as any).plannedDepartureTime ?? null;
return {
sequence: stop.sequence,
plannedArrivalAt: index > 0 && arrDt != null
? templateToScheduleUTC(arrDt).toISOString()
: undefined,
plannedDepartureAt: index < route.stops.length - 1 && depDt != null
? templateToScheduleUTC(depDt).toISOString()
: undefined,
};
});
} else {
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
plannedTimes = route.stops.map((stop, index) => {
let stopTime: Date;
if (index === 0) {
stopTime = dep;
} else if (index === route.stops.length - 1) {
stopTime = arr;
} else {
const stopDistance = stop.distanceKm || 0;
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
stopTime = new Date(dep.getTime() + totalDuration * progress);
}
return {
sequence: stop.sequence,
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
};
});
}
});
}
const providedSeqs = new Set((plannedTimes ?? []).map(t => t.sequence));
@@ -340,59 +304,26 @@ export class SchedulesService {
let plannedTimes = dto.plannedTimes;
if (!plannedTimes || plannedTimes.length === 0) {
const hasRouteTimes = route.stops.some(
s => (s as any).plannedArrivalTime != null || (s as any).plannedDepartureTime != null,
);
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
if (hasRouteTimes) {
const EAT_MS = 3 * 60 * 60 * 1000;
const depEATMs = dep.getTime() + EAT_MS;
const depMsIntoDay = depEATMs % (24 * 60 * 60 * 1000);
const eatMidnightUTC = dep.getTime() - depMsIntoDay;
const templateToScheduleUTC = (templateDt: Date): Date => {
const templateEATMs = templateDt.getTime() + EAT_MS;
const timeOfDayMs = templateEATMs % (24 * 60 * 60 * 1000);
const candidate = new Date(eatMidnightUTC + timeOfDayMs);
if (candidate < dep) return new Date(candidate.getTime() + 24 * 60 * 60 * 1000);
return candidate;
plannedTimes = route.stops.map((stop, index) => {
let stopTime: Date;
if (index === 0) {
stopTime = dep;
} else if (index === route.stops.length - 1) {
stopTime = arr;
} else {
const stopDistance = stop.distanceKm || 0;
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
stopTime = new Date(dep.getTime() + totalDuration * progress);
}
return {
sequence: stop.sequence,
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
};
plannedTimes = route.stops.map((stop, index) => {
const arrDt: Date | null = (stop as any).plannedArrivalTime ?? null;
const depDt: Date | null = (stop as any).plannedDepartureTime ?? null;
return {
sequence: stop.sequence,
plannedArrivalAt: index > 0 && arrDt != null
? templateToScheduleUTC(arrDt).toISOString()
: undefined,
plannedDepartureAt: index < route.stops.length - 1 && depDt != null
? templateToScheduleUTC(depDt).toISOString()
: undefined,
};
});
} else {
const totalDuration = arr.getTime() - dep.getTime();
const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
plannedTimes = route.stops.map((stop, index) => {
let stopTime: Date;
if (index === 0) {
stopTime = dep;
} else if (index === route.stops.length - 1) {
stopTime = arr;
} else {
const stopDistance = stop.distanceKm || 0;
const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
stopTime = new Date(dep.getTime() + totalDuration * progress);
}
return {
sequence: stop.sequence,
plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
};
});
}
});
}
const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
@@ -747,12 +678,6 @@ export class SchedulesService {
}
}
if (dto.plannedTimes && dto.plannedTimes.length > 0 && schedule.routeId) {
await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
const plannedTimesMap = Object.fromEntries(dto.plannedTimes.map(t => [t.sequence, t]));
await this.routesService.applyRouteToSchedule(schedule.routeId, id, plannedTimesMap);
}
return this.getSchedule(id);
}