From bfe76a5e2b0ac2a90d9e0ca6084fe6a73811c0e0 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:10:43 +0300 Subject: [PATCH] Fix availability that goes from one day to another --- .../availability/core/Availability.ts | 26 ++++++++++ .../availability/core/MySchedule.server.ts | 7 ++- .../availability/core/ScheduleWeek.test.ts | 50 +++++++++++++++++++ .../availability/core/ScheduleWeek.ts | 14 ++++-- .../2026-09-02-schedule-overnight-days.md | 5 ++ 5 files changed, 96 insertions(+), 6 deletions(-) create mode 100644 app/features/availability/core/ScheduleWeek.test.ts create mode 100644 changelog/2026-09-02-schedule-overnight-days.md diff --git a/app/features/availability/core/Availability.ts b/app/features/availability/core/Availability.ts index 8bcd7566c..de8715b85 100644 --- a/app/features/availability/core/Availability.ts +++ b/app/features/availability/core/Availability.ts @@ -220,6 +220,32 @@ export function clip( }); } +/** The ranges cut up into the day tracks they render on, what runs past a track's end continuing on the next day's. */ +export function splitByDayTracks( + ranges: Array, + timezone: string, +): Array { + return ranges.flatMap((range) => { + const tracks: Array = []; + + let startsAt = range.startsAt; + while (startsAt < range.endsAt) { + const trackEndsAt = dayMinutesToTimestamp({ + date: dateInTimezone(startsAt, timezone), + minutes: AVAILABILITY.TRACK_LATER_END_MINUTES, + timezone, + }); + const endsAt = Math.min(range.endsAt, trackEndsAt); + if (endsAt <= startsAt) break; + + tracks.push({ startsAt, endsAt }); + startsAt = endsAt; + } + + return tracks; + }); +} + /** * How one person's schedule relates to an event's window. A busy block overlapping it * wins over anything reported (committed elsewhere, schedule known or not). Otherwise the diff --git a/app/features/availability/core/MySchedule.server.ts b/app/features/availability/core/MySchedule.server.ts index 7c06961fe..c4272e33e 100644 --- a/app/features/availability/core/MySchedule.server.ts +++ b/app/features/availability/core/MySchedule.server.ts @@ -84,10 +84,15 @@ function editorWeek({ Availability.isSameWeek(week.weekStartsAt, range.startsAt), ); + const slots = Availability.splitByDayTracks( + matchingWeek?.slots ?? [], + timezone, + ); + const days = ScheduleWeek.days(range, timezone).map(({ date }) => ({ date, ranges: Availability.mergedDayRanges( - (matchingWeek?.slots ?? []) + slots .filter( (slot) => Availability.dateInTimezone(slot.startsAt, timezone) === date, diff --git a/app/features/availability/core/ScheduleWeek.test.ts b/app/features/availability/core/ScheduleWeek.test.ts new file mode 100644 index 000000000..487b8d1af --- /dev/null +++ b/app/features/availability/core/ScheduleWeek.test.ts @@ -0,0 +1,50 @@ +import { addWeeks } from "date-fns"; +import { describe, expect, test } from "vitest"; +import * as Availability from "./Availability"; +import * as ScheduleWeek from "./ScheduleWeek"; + +const DAY_SECONDS = 24 * 60 * 60; +const TIMEZONE = "UTC"; +const FRIDAY = 4; +const SATURDAY = 5; + +describe("ScheduleWeek.memberRow", () => { + test("shows a member free on Saturday when their Friday night range runs into their Saturday morning range", () => { + const weekStartsAt = Availability.weekStartsAt( + addWeeks(new Date(), 1), + TIMEZONE, + ); + const range = { + startsAt: weekStartsAt, + endsAt: weekStartsAt + 7 * DAY_SECONDS, + }; + const days = ScheduleWeek.days(range, TIMEZONE); + const fridayAt = (minutes: number) => + Availability.dayMinutesToTimestamp({ + date: days[FRIDAY].date, + minutes, + timezone: TIMEZONE, + }); + + // what SAVE_WEEK stores for Fri 20:00-06:00 + Sat 06:00-10:00 + const slots = Availability.normalize([ + { startsAt: fridayAt(20 * 60), endsAt: fridayAt(30 * 60) }, + { startsAt: fridayAt(30 * 60), endsAt: fridayAt(34 * 60) }, + ]); + + const row = ScheduleWeek.memberRow({ + userId: 1, + days, + timezone: TIMEZONE, + reportedWeeks: [ + { userId: 1, weekStartsAt, timezone: TIMEZONE, slots, dayNotes: [] }, + ], + range, + busy: [], + }); + + expect(row.days[SATURDAY].ranges).toEqual([ + { startsAt: fridayAt(30 * 60), endsAt: fridayAt(34 * 60) }, + ]); + }); +}); diff --git a/app/features/availability/core/ScheduleWeek.ts b/app/features/availability/core/ScheduleWeek.ts index 7a40c3f84..52dcfe90e 100644 --- a/app/features/availability/core/ScheduleWeek.ts +++ b/app/features/availability/core/ScheduleWeek.ts @@ -48,8 +48,9 @@ export function weekNumber(range: TimeRange, timezone: string) { /** * One member's week bucketed into the viewer's days: effective free time (commitments cut out - * first), the commitments and their notes. Slots land on the viewer-local day they start on, - * wherever the author's week put them, adjacent weeks' spillover included. + * first), the commitments and their notes. Slots land on the viewer-local day track they start + * on, wherever the author's week put them, adjacent weeks' spillover included, and what runs past + * the end of that track continues on the next day. */ export function memberRow({ userId, @@ -89,9 +90,12 @@ export function memberRow({ }; } - const slots = Availability.subtract( - memberWeeks.flatMap((week) => week.slots), - busy, + const slots = Availability.splitByDayTracks( + Availability.subtract( + memberWeeks.flatMap((week) => week.slots), + busy, + ), + timezone, ); return { diff --git a/changelog/2026-09-02-schedule-overnight-days.md b/changelog/2026-09-02-schedule-overnight-days.md new file mode 100644 index 000000000..5ddbaa2fe --- /dev/null +++ b/changelog/2026-09-02-schedule-overnight-days.md @@ -0,0 +1,5 @@ +--- +navItem: calendar +type: bug +--- +Fixed availability that runs into the next morning making that next day look empty on your schedule and on your team's