Improve scheduler scrolling and date range overrides
Create PR containing updated CHANGELOG.md and release packages to NPM once PR is merged / Release (push) Has been cancelled
Run i18n AI automation / Run i18n (push) Has been cancelled
Next.js Bundle Analysis / analyze (push) Has been cancelled

This commit is contained in:
2026-06-15 09:43:41 -06:00
parent 2fa50b067b
commit 893def4d08
9 changed files with 475 additions and 119 deletions
+133 -10
View File
@@ -210,6 +210,7 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
.main-surface {
padding: 28px 26px;
min-width: 0;
min-height: 0;
animation: surface-in 0.28s ease-out;
}
@@ -296,6 +297,30 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
background: var(--accent-bg);
}
.icon-button {
display: inline-grid;
place-items: center;
width: 34px;
height: 34px;
border: 1px solid var(--accent-border);
border-radius: var(--radius);
background: var(--accent-bg);
color: var(--accent);
cursor: pointer;
font-size: 22px;
line-height: 1;
font-weight: 400;
transition: background 0.14s, border-color 0.14s, color 0.14s, transform 0.09s;
}
.icon-button:hover {
color: var(--accent-text);
border-color: rgba(82, 181, 131, 0.46);
background: var(--accent-bg-hover);
}
.icon-button:active { transform: scale(0.96); }
/* Notices */
.notice {
font-size: 12px;
@@ -457,7 +482,9 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
display: grid;
grid-template-columns: minmax(0, 1fr) 288px;
gap: 16px;
align-items: start;
align-items: stretch;
height: calc(100vh - 56px);
min-height: 620px;
}
.calendar-panel {
@@ -466,15 +493,33 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
border-radius: var(--radius-xl);
padding: 20px;
overflow: hidden;
min-height: 0;
display: flex;
flex-direction: column;
}
.week-scroller {
min-height: 0;
overflow: auto;
border: 1px solid var(--border-subtle);
border-radius: var(--radius);
background: rgba(255, 255, 255, 0.01);
}
.week-grid {
display: grid;
grid-template-columns: 46px repeat(5, minmax(0, 1fr));
gap: 0;
min-width: 720px;
}
.time-gutter { padding-top: 30px; }
.time-gutter {
padding-top: 30px;
position: sticky;
left: 0;
z-index: 4;
background: var(--surface-1);
}
.hour-label {
font-size: 9.5px;
@@ -491,6 +536,9 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
}
.day-heading {
position: sticky;
top: 0;
z-index: 3;
font-size: 10px;
font-weight: 600;
text-align: center;
@@ -498,6 +546,8 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
color: var(--text-3);
letter-spacing: 0.1em;
text-transform: uppercase;
background: var(--surface-1);
border-bottom: 1px solid var(--border-subtle);
}
.day-body {
@@ -585,6 +635,8 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
padding: 20px;
position: sticky;
top: 28px;
max-height: calc(100vh - 56px);
overflow: auto;
}
.composer h2 {
@@ -696,6 +748,13 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
color: var(--text);
}
.settings-card-heading {
display: flex;
align-items: start;
justify-content: space-between;
gap: 14px;
}
.weekly-rows {
display: grid;
gap: 0;
@@ -776,16 +835,47 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
font-size: 13px;
}
.override-row > span:first-child {
display: grid;
gap: 2px;
}
.override-row strong {
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.override-editor {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 10px;
padding-bottom: 16px;
display: grid;
gap: 12px;
margin-top: 16px;
padding: 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-2);
}
.override-date { margin: 0; }
.override-unavailable { margin: 0 4px 8px 0; }
.override-date-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.override-mode-row,
.override-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.override-actions {
padding-top: 2px;
}
.override-unavailable { margin: 0; }
.override-time-range { margin-bottom: 1px; }
.override-add { margin-bottom: 1px; }
@@ -849,11 +939,44 @@ input[type="number"]::-webkit-inner-spin-button { filter: invert(0.5); }
================================================================ */
@media (max-width: 960px) {
.calendar-layout { grid-template-columns: 1fr; }
.composer { position: static; }
.calendar-layout {
grid-template-columns: 1fr;
height: auto;
min-height: 0;
}
.calendar-panel { max-height: min(680px, calc(100vh - 220px)); }
.week-scroller { max-height: 560px; }
.composer {
position: static;
max-height: none;
}
}
@media (max-width: 860px) {
.app-frame { grid-template-columns: 1fr; }
.sidebar { min-height: auto; height: auto; position: static; border-right: 0; border-bottom: 1px solid var(--border-subtle); }
}
@media (max-width: 640px) {
.main-surface { padding: 18px 14px; }
.panel-header {
align-items: start;
flex-direction: column;
gap: 4px;
}
.weekly-row {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
}
.weekly-toggle { min-width: 0; }
.weekly-row .time-range {
padding-left: 24px;
min-width: 0;
}
.weekly-row .time-range input {
width: min(112px, calc((100vw - 112px) / 2));
}
.override-date-grid { grid-template-columns: 1fr; }
.settings-card-heading { align-items: center; }
}
+127 -50
View File
@@ -2,7 +2,7 @@
import { useMemo, useState } from "react";
import { upsertDateOverride } from "@scheduler/lib/scheduler/overrides";
import { overrideEndDate, upsertDateOverride } from "@scheduler/lib/scheduler/overrides";
import type { DateOverride, SchedulerSchedule, WeeklyRange } from "@scheduler/lib/scheduler/types";
type AvailabilityEditorProps = {
@@ -35,7 +35,9 @@ function buildRows(weekly: WeeklyRange[]): WeeklyRange[] {
export function AvailabilityEditor({ schedule }: AvailabilityEditorProps) {
const [rows, setRows] = useState<WeeklyRange[]>(() => buildRows(schedule.weekly));
const [overrides, setOverrides] = useState<DateOverride[]>(schedule.overrides);
const [isAddingOverride, setIsAddingOverride] = useState(false);
const [overrideDate, setOverrideDate] = useState("");
const [overrideEndDateValue, setOverrideEndDateValue] = useState("");
const [overrideUnavailable, setOverrideUnavailable] = useState(true);
const [overrideStart, setOverrideStart] = useState(DEFAULT_START);
const [overrideEnd, setOverrideEnd] = useState(DEFAULT_END);
@@ -55,7 +57,11 @@ export function AvailabilityEditor({ schedule }: AvailabilityEditorProps) {
const addOverride = () => {
if (!overrideDate) {
setOverrideError("Choose a date.");
setOverrideError("Choose a start date.");
return;
}
if (overrideEndDateValue && overrideEndDateValue < overrideDate) {
setOverrideError("End date must be after the start date.");
return;
}
if (!overrideUnavailable && overrideStart >= overrideEnd) {
@@ -64,11 +70,28 @@ export function AvailabilityEditor({ schedule }: AvailabilityEditorProps) {
}
const next: DateOverride = overrideUnavailable
? { date: overrideDate, unavailable: true }
: { date: overrideDate, unavailable: false, startTime: overrideStart, endTime: overrideEnd };
? { date: overrideDate, ...(overrideEndDateValue && { endDate: overrideEndDateValue }), unavailable: true }
: {
date: overrideDate,
...(overrideEndDateValue && { endDate: overrideEndDateValue }),
unavailable: false,
startTime: overrideStart,
endTime: overrideEnd,
};
setOverrides((current) => upsertDateOverride(current, next));
setOverrideDate("");
setOverrideEndDateValue("");
setOverrideError("");
setIsAddingOverride(false);
};
const overrideSummary = (override: DateOverride) => {
const endDate = overrideEndDate(override);
const dates = endDate === override.date ? override.date : `${override.date} to ${endDate}`;
const hours = override.unavailable
? "Unavailable all day"
: `${override.startTime ?? DEFAULT_START} - ${override.endTime ?? DEFAULT_END}`;
return { dates, hours };
};
const save = async () => {
@@ -141,59 +164,113 @@ export function AvailabilityEditor({ schedule }: AvailabilityEditorProps) {
</section>
<section className="settings-card">
<h2>Date overrides</h2>
<div className="override-editor">
<label className="field override-date">
Date
<input type="date" value={overrideDate} onChange={(event) => setOverrideDate(event.target.value)} />
</label>
<label className="field-inline override-unavailable">
<input
type="checkbox"
checked={overrideUnavailable}
onChange={(event) => setOverrideUnavailable(event.target.checked)}
/>
Unavailable all day
</label>
{!overrideUnavailable && (
<div className="time-range override-time-range">
<input
type="time"
value={overrideStart}
onChange={(event) => setOverrideStart(event.target.value)}
aria-label="Override start"
/>
<span className="dash"></span>
<input
type="time"
value={overrideEnd}
onChange={(event) => setOverrideEnd(event.target.value)}
aria-label="Override end"
/>
</div>
)}
<button type="button" className="secondary-button override-add" onClick={addOverride}>
Add override
<div className="settings-card-heading">
<div>
<h2>Date overrides</h2>
<p className="muted">Change one date or a short date range without touching weekly hours.</p>
</div>
<button
type="button"
className="icon-button"
aria-label="Add date override"
aria-expanded={isAddingOverride}
onClick={() => {
setIsAddingOverride((current) => !current);
setOverrideError("");
}}>
+
</button>
</div>
{isAddingOverride && (
<div className="override-editor" aria-label="New date override">
<div className="override-date-grid">
<label className="field override-date" htmlFor="override-start-date">
<span>Start date</span>
<input
id="override-start-date"
type="date"
value={overrideDate}
onChange={(event) => setOverrideDate(event.target.value)}
/>
</label>
<label className="field override-date" htmlFor="override-end-date">
<span>End date</span>
<input
id="override-end-date"
type="date"
value={overrideEndDateValue}
min={overrideDate || undefined}
onChange={(event) => setOverrideEndDateValue(event.target.value)}
/>
</label>
</div>
<div className="override-mode-row" role="group" aria-label="Override type">
<button
type="button"
className="secondary-button"
data-active={overrideUnavailable}
onClick={() => setOverrideUnavailable(true)}>
Unavailable
</button>
<button
type="button"
className="secondary-button"
data-active={!overrideUnavailable}
onClick={() => setOverrideUnavailable(false)}>
Custom hours
</button>
</div>
{!overrideUnavailable && (
<div className="time-range override-time-range">
<input
type="time"
value={overrideStart}
onChange={(event) => setOverrideStart(event.target.value)}
aria-label="Override start"
/>
<span className="dash"></span>
<input
type="time"
value={overrideEnd}
onChange={(event) => setOverrideEnd(event.target.value)}
aria-label="Override end"
/>
</div>
)}
<div className="override-actions">
<button type="button" className="primary-button" onClick={addOverride}>
Add override
</button>
<button type="button" className="secondary-button" onClick={() => setIsAddingOverride(false)}>
Cancel
</button>
</div>
</div>
)}
{overrideError && <p className="notice notice-error">{overrideError}</p>}
{overrides.length === 0 ? (
<p className="muted">No date overrides.</p>
) : (
<ul className="override-list">
{overrides.map((override) => (
<li key={override.date} className="override-row">
<span>{override.date}</span>
<span className="muted">
{override.unavailable
? "Unavailable all day"
: `${override.startTime ?? DEFAULT_START}${override.endTime ?? DEFAULT_END}`}
</span>
<button type="button" className="secondary-button" onClick={() => removeOverride(override.date)}>
Remove
</button>
</li>
))}
{overrides.map((override) => {
const summary = overrideSummary(override);
return (
<li key={`${override.date}-${override.endDate ?? override.date}`} className="override-row">
<span>
<strong>{summary.dates}</strong>
<span className="muted">{summary.hours}</span>
</span>
<button type="button" className="secondary-button" onClick={() => removeOverride(override.date)}>
Remove
</button>
</li>
);
})}
</ul>
)}
</section>
+50 -48
View File
@@ -197,57 +197,59 @@ export function CalendarWorkspace({
<h1>Calendar</h1>
<span className="muted">{loading ? "Updating availability…" : "Mutual free slots outlined"}</span>
</header>
<div className="week-grid">
<div className="time-gutter">
{hours.map((hour) => (
<div key={hour} className="hour-label" style={{ height: PX_PER_HOUR }}>
{hour}:00
<div className="week-scroller">
<div className="week-grid">
<div className="time-gutter">
{hours.map((hour) => (
<div key={hour} className="hour-label" style={{ height: PX_PER_HOUR }}>
{hour}:00
</div>
))}
</div>
{days.map((day, index) => (
<div key={day.toISOString()} className="day-column">
<div className="day-heading">
{WEEKDAYS[index]} {day.getMonth() + 1}/{day.getDate()}
</div>
<div className="day-body" style={{ height: (DAY_END_HOUR - DAY_START_HOUR) * PX_PER_HOUR }}>
{busy
.filter((block) => sameDay(block.start, day))
.map((block) => {
// Teammate blocks never reveal a title, even if one leaks
// past the server-side privacy filter.
const label = block.userId === viewerId ? block.title ?? "Busy" : "Busy";
return (
<div
key={block.id}
className="busy-block"
role="img"
aria-label={`${WEEKDAYS[index]} ${formatTime(block.start)} ${label}`}
data-own={block.userId === viewerId}
style={{ top: offsetTop(block.start), height: blockHeight(block.start, block.end) }}
title={label}>
{label}
</div>
);
})}
{slots
.filter((slot) => sameDay(slot.start, day))
.map((slot) => (
<button
key={slot.start}
type="button"
className="mutual-slot"
data-selected={slot.start === selectedSlotStart}
aria-label={`Schedule ${WEEKDAYS[index]} at ${formatTime(slot.start)}`}
aria-pressed={slot.start === selectedSlotStart}
style={{ top: offsetTop(slot.start), height: blockHeight(slot.start, slot.end) }}
onClick={() => setSelectedSlotStart(slot.start)}>
{formatTime(slot.start)}
</button>
))}
</div>
</div>
))}
</div>
{days.map((day, index) => (
<div key={day.toISOString()} className="day-column">
<div className="day-heading">
{WEEKDAYS[index]} {day.getMonth() + 1}/{day.getDate()}
</div>
<div className="day-body" style={{ height: (DAY_END_HOUR - DAY_START_HOUR) * PX_PER_HOUR }}>
{busy
.filter((block) => sameDay(block.start, day))
.map((block) => {
// Teammate blocks never reveal a title, even if one leaks
// past the server-side privacy filter.
const label = block.userId === viewerId ? block.title ?? "Busy" : "Busy";
return (
<div
key={block.id}
className="busy-block"
role="img"
aria-label={`${WEEKDAYS[index]} ${formatTime(block.start)} ${label}`}
data-own={block.userId === viewerId}
style={{ top: offsetTop(block.start), height: blockHeight(block.start, block.end) }}
title={label}>
{label}
</div>
);
})}
{slots
.filter((slot) => sameDay(slot.start, day))
.map((slot) => (
<button
key={slot.start}
type="button"
className="mutual-slot"
data-selected={slot.start === selectedSlotStart}
aria-label={`Schedule ${WEEKDAYS[index]} at ${formatTime(slot.start)}`}
aria-pressed={slot.start === selectedSlotStart}
style={{ top: offsetTop(slot.start), height: blockHeight(slot.start, slot.end) }}
onClick={() => setSelectedSlotStart(slot.start)}>
{formatTime(slot.start)}
</button>
))}
</div>
</div>
))}
</div>
</section>
@@ -119,4 +119,23 @@ describe("computeMutualSlots", () => {
expect(slots.map((slot) => slot.start)).toEqual(["2026-06-15T19:00:00.000Z", "2026-06-15T19:30:00.000Z"]);
});
it("applies date range overrides to each date in the range", () => {
const slots = computeMutualSlots({
schedules: schedules.map((schedule) => ({
...schedule,
weekly: [
{ day: 1, enabled: true, startTime: "09:00", endTime: "11:00" },
{ day: 2, enabled: true, startTime: "09:00", endTime: "11:00" },
],
overrides: [{ date: "2026-06-15", endDate: "2026-06-16", unavailable: true }],
})),
busy: [],
rangeStart: "2026-06-15T00:00:00.000Z",
rangeEnd: "2026-06-17T00:00:00.000Z",
durationMinutes: 30,
});
expect(slots).toEqual([]);
});
});
+3 -1
View File
@@ -1,5 +1,6 @@
import { addMinutes, overlaps, toIso } from "./time";
import type { BusyBlock, MutualSlot, SchedulerSchedule } from "./types";
import { overrideEndDate } from "./overrides";
type ComputeInput = {
schedules: SchedulerSchedule[];
@@ -25,7 +26,8 @@ function windowsForSchedule(schedule: SchedulerSchedule, rangeStart: Date, range
const windows: { start: Date; end: Date; userId: number }[] = [];
for (let cursor = new Date(rangeStart); cursor < rangeEnd; cursor = addMinutes(cursor, 24 * 60)) {
const day = cursor.getUTCDay();
const override = schedule.overrides.find((item) => item.date === cursor.toISOString().slice(0, 10));
const date = cursor.toISOString().slice(0, 10);
const override = schedule.overrides.find((item) => item.date <= date && overrideEndDate(item) >= date);
if (override?.unavailable) continue;
const ranges =
+44 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { upsertDateOverride } from "./overrides";
import { collapseDateOverrides, expandDateOverride, upsertDateOverride } from "./overrides";
describe("upsertDateOverride", () => {
it("adds a new override in date order", () => {
@@ -23,4 +23,47 @@ describe("upsertDateOverride", () => {
)
).toEqual([{ date: "2026-06-18", unavailable: true }]);
});
it("replaces overrides that overlap a new date range", () => {
expect(
upsertDateOverride(
[
{ date: "2026-06-17", unavailable: true },
{ date: "2026-06-18", unavailable: true },
{ date: "2026-06-23", unavailable: true },
],
{ date: "2026-06-18", endDate: "2026-06-20", unavailable: false, startTime: "10:00", endTime: "14:00" }
)
).toEqual([
{ date: "2026-06-17", unavailable: true },
{ date: "2026-06-18", endDate: "2026-06-20", unavailable: false, startTime: "10:00", endTime: "14:00" },
{ date: "2026-06-23", unavailable: true },
]);
});
});
describe("expandDateOverride", () => {
it("expands a date range to one override per day", () => {
expect(expandDateOverride({ date: "2026-06-18", endDate: "2026-06-20", unavailable: true })).toEqual([
{ date: "2026-06-18", unavailable: true },
{ date: "2026-06-19", unavailable: true },
{ date: "2026-06-20", unavailable: true },
]);
});
});
describe("collapseDateOverrides", () => {
it("collapses adjacent overrides with the same rule", () => {
expect(
collapseDateOverrides([
{ date: "2026-06-18", unavailable: true },
{ date: "2026-06-19", unavailable: true },
{ date: "2026-06-20", unavailable: true },
{ date: "2026-06-21", unavailable: false, startTime: "10:00", endTime: "14:00" },
])
).toEqual([
{ date: "2026-06-18", endDate: "2026-06-20", unavailable: true },
{ date: "2026-06-21", unavailable: false, startTime: "10:00", endTime: "14:00" },
]);
});
});
+83 -1
View File
@@ -1,7 +1,89 @@
import type { DateOverride } from "./types";
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
function parseDateOnly(value: string): Date {
return new Date(`${value}T00:00:00.000Z`);
}
function formatDateOnly(value: Date): string {
return value.toISOString().slice(0, 10);
}
function addDays(date: Date, days: number): Date {
const next = new Date(date);
next.setUTCDate(next.getUTCDate() + days);
return next;
}
export function overrideEndDate(override: DateOverride): string {
return override.endDate && override.endDate >= override.date ? override.endDate : override.date;
}
function rangesOverlap(a: DateOverride, b: DateOverride): boolean {
return a.date <= overrideEndDate(b) && b.date <= overrideEndDate(a);
}
export function upsertDateOverride(overrides: DateOverride[], next: DateOverride): DateOverride[] {
return [...overrides.filter((override) => override.date !== next.date), next].sort((a, b) =>
return [...overrides.filter((override) => !rangesOverlap(override, next)), next].sort((a, b) =>
a.date.localeCompare(b.date)
);
}
export function expandDateOverride(override: DateOverride): DateOverride[] {
const start = parseDateOnly(override.date);
const end = parseDateOnly(overrideEndDate(override));
const expanded: DateOverride[] = [];
for (let cursor = start; cursor <= end; cursor = addDays(cursor, 1)) {
const date = formatDateOnly(cursor);
expanded.push({
date,
unavailable: override.unavailable,
...(!override.unavailable && { startTime: override.startTime, endTime: override.endTime }),
});
}
return expanded;
}
function sameOverrideRule(left: DateOverride, right: DateOverride): boolean {
return (
left.unavailable === right.unavailable &&
left.startTime === right.startTime &&
left.endTime === right.endTime
);
}
export function collapseDateOverrides(overrides: DateOverride[]): DateOverride[] {
const sorted = overrides
.filter((override) => DATE_ONLY.test(override.date))
.map((override) => ({ ...override, endDate: undefined }))
.sort((a, b) => a.date.localeCompare(b.date));
const collapsed: DateOverride[] = [];
for (const override of sorted) {
const previous = collapsed[collapsed.length - 1];
if (!previous || !sameOverrideRule(previous, override)) {
collapsed.push(override);
continue;
}
const previousEnd = parseDateOnly(overrideEndDate(previous));
const nextDay = formatDateOnly(addDays(previousEnd, 1));
if (nextDay === override.date) {
previous.endDate = override.date;
continue;
}
collapsed.push(override);
}
return collapsed.map((override) => {
if (override.endDate === override.date) {
const { endDate: _endDate, ...singleDay } = override;
return singleDay;
}
return override;
});
}
+15 -8
View File
@@ -1,6 +1,7 @@
import prisma from "@calcom/prisma";
import { computeMutualSlots } from "./availability";
import { collapseDateOverrides, expandDateOverride } from "./overrides";
import {
demoBusyBlocks,
demoConnectionsByUser,
@@ -96,7 +97,7 @@ function buildSchedule(
userId,
timeZone: schedule?.timeZone ?? fallbackTimeZone,
weekly,
overrides,
overrides: collapseDateOverrides(overrides),
};
}
@@ -210,10 +211,14 @@ const weeklyRangeSchema = z.object({
const dateOverrideSchema = z
.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD date"),
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD date").optional(),
unavailable: z.boolean(),
startTime: hhMm.optional(),
endTime: hhMm.optional(),
})
.refine((override) => !override.endDate || override.endDate >= override.date, {
message: "End date must be on or after start date",
})
.refine((override) => override.unavailable || (override.startTime && override.endTime), {
message: "Available overrides require start and end times",
});
@@ -263,13 +268,15 @@ export async function updateSchedule(userId: number, body: unknown): Promise<Sch
date: null as Date | null,
userId,
})),
...parsed.overrides.map((override) => ({
days: [] as number[],
startTime: hhMmToTime(override.unavailable ? "00:00" : (override.startTime as string)),
endTime: hhMmToTime(override.unavailable ? "00:00" : (override.endTime as string)),
date: new Date(`${override.date}T00:00:00.000Z`),
userId,
})),
...parsed.overrides.flatMap((override) =>
expandDateOverride(override).map((dateOverride) => ({
days: [] as number[],
startTime: hhMmToTime(dateOverride.unavailable ? "00:00" : (dateOverride.startTime as string)),
endTime: hhMmToTime(dateOverride.unavailable ? "00:00" : (dateOverride.endTime as string)),
date: new Date(`${dateOverride.date}T00:00:00.000Z`),
userId,
}))
),
];
// Replace the schedule's availability atomically; immutable from the caller's view.
+1
View File
@@ -27,6 +27,7 @@ export type WeeklyRange = {
export type DateOverride = {
date: string;
endDate?: string;
unavailable: boolean;
startTime?: string;
endTime?: string;