@perdieminc/time-slots 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/package.json +63 -0
- package/src/constants.ts +45 -0
- package/src/index.ts +23 -0
- package/src/schedule/available-dates.ts +129 -0
- package/src/schedule/generate.ts +276 -0
- package/src/schedule/get-schedules.ts +264 -0
- package/src/schedule/location.ts +58 -0
- package/src/types/business-hours.d.ts +32 -0
- package/src/types/common.d.ts +5 -0
- package/src/types/get-schedules.d.ts +122 -0
- package/src/types/index.ts +34 -0
- package/src/types/location.d.ts +25 -0
- package/src/types/schedule-filter.d.ts +27 -0
- package/src/types/schedule.d.ts +83 -0
- package/src/types/timezone-support.d.ts +31 -0
- package/src/utils/business-hours.ts +120 -0
- package/src/utils/catering.ts +85 -0
- package/src/utils/date.ts +163 -0
- package/src/utils/schedule-filter.ts +140 -0
- package/src/utils/store-hours.ts +223 -0
- package/src/utils/time.ts +38 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { compareAsc, isBefore } from "date-fns";
|
|
2
|
+
import { findTimeZone, getZonedTime } from "timezone-support";
|
|
3
|
+
|
|
4
|
+
import { getNextAvailableDates } from "../schedule/available-dates";
|
|
5
|
+
import type {
|
|
6
|
+
BusinessHour,
|
|
7
|
+
BusinessHoursOverrideOutput,
|
|
8
|
+
FulfillmentPreference,
|
|
9
|
+
GetOpeningClosingTimeOnDateParams,
|
|
10
|
+
LocationLike,
|
|
11
|
+
} from "../types";
|
|
12
|
+
import { getLocationBusinessHoursForFulfillment } from "./business-hours";
|
|
13
|
+
import { isMidnightTransition, isTodayInTimeZone, setHmOnDate } from "./date";
|
|
14
|
+
|
|
15
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
interface GetAvailableBusinessHoursParams {
|
|
18
|
+
businessHours?: BusinessHour[];
|
|
19
|
+
businessHoursOverrides?: BusinessHoursOverrideOutput[];
|
|
20
|
+
timeZone: string;
|
|
21
|
+
nextAvailableDate: Date;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getAvailableBusinessHours({
|
|
25
|
+
businessHours = [],
|
|
26
|
+
businessHoursOverrides = [],
|
|
27
|
+
timeZone,
|
|
28
|
+
nextAvailableDate,
|
|
29
|
+
}: GetAvailableBusinessHoursParams): {
|
|
30
|
+
dayBusinessTimes: Array<{ startDate: Date; endDate: Date }>;
|
|
31
|
+
businessHoursOverride: BusinessHoursOverrideOutput | undefined;
|
|
32
|
+
} {
|
|
33
|
+
const zonedDate = getZonedTime(nextAvailableDate, findTimeZone(timeZone));
|
|
34
|
+
|
|
35
|
+
const dayBusinessHours = businessHours.filter(
|
|
36
|
+
(bh) =>
|
|
37
|
+
bh.day ===
|
|
38
|
+
getZonedTime(nextAvailableDate, findTimeZone(timeZone)).dayOfWeek,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const businessHoursOverride = businessHoursOverrides.find(
|
|
42
|
+
(override) =>
|
|
43
|
+
override.day === zonedDate.day && override.month === zonedDate.month,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const dayBusinessTimes = dayBusinessHours
|
|
47
|
+
.map((businessHour) => {
|
|
48
|
+
const effectiveHour = businessHoursOverride
|
|
49
|
+
? {
|
|
50
|
+
day: businessHour.day,
|
|
51
|
+
startTime: businessHoursOverride.startTime ?? "00:00",
|
|
52
|
+
endTime: businessHoursOverride.endTime ?? "23:59",
|
|
53
|
+
}
|
|
54
|
+
: businessHour;
|
|
55
|
+
|
|
56
|
+
const startDate = setHmOnDate(
|
|
57
|
+
nextAvailableDate,
|
|
58
|
+
effectiveHour.startTime,
|
|
59
|
+
timeZone,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const endDate = setHmOnDate(
|
|
63
|
+
nextAvailableDate,
|
|
64
|
+
effectiveHour.endTime,
|
|
65
|
+
timeZone,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!isBefore(startDate, endDate)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { startDate, endDate };
|
|
73
|
+
})
|
|
74
|
+
.filter((time): time is { startDate: Date; endDate: Date } => time !== null)
|
|
75
|
+
.sort((a, b) => compareAsc(a.startDate, b.startDate));
|
|
76
|
+
|
|
77
|
+
return { dayBusinessTimes, businessHoursOverride };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export function getOpeningClosingTimeOnDate({
|
|
83
|
+
date = new Date(),
|
|
84
|
+
businessHours = [],
|
|
85
|
+
businessHoursOverrides = [],
|
|
86
|
+
timeZone,
|
|
87
|
+
}: GetOpeningClosingTimeOnDateParams): {
|
|
88
|
+
openingTime: Date;
|
|
89
|
+
closingTime: Date;
|
|
90
|
+
} | null {
|
|
91
|
+
try {
|
|
92
|
+
const nextAvailableDates = getNextAvailableDates({
|
|
93
|
+
startDate: date,
|
|
94
|
+
businessHours,
|
|
95
|
+
businessHoursOverrides,
|
|
96
|
+
timeZone,
|
|
97
|
+
datesCount: 7,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!Array.isArray(nextAvailableDates) || !nextAvailableDates.length) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (
|
|
105
|
+
let nextDateIndex = 0;
|
|
106
|
+
nextDateIndex < nextAvailableDates.length;
|
|
107
|
+
++nextDateIndex
|
|
108
|
+
) {
|
|
109
|
+
const nextAvailableDate = nextAvailableDates[nextDateIndex];
|
|
110
|
+
|
|
111
|
+
const { dayBusinessTimes, businessHoursOverride } =
|
|
112
|
+
getAvailableBusinessHours({
|
|
113
|
+
businessHours,
|
|
114
|
+
businessHoursOverrides,
|
|
115
|
+
timeZone,
|
|
116
|
+
nextAvailableDate,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (!Array.isArray(dayBusinessTimes) || dayBusinessTimes.length === 0) {
|
|
120
|
+
if (
|
|
121
|
+
businessHoursOverride?.startTime &&
|
|
122
|
+
businessHoursOverride?.endTime
|
|
123
|
+
) {
|
|
124
|
+
const openingTime = setHmOnDate(
|
|
125
|
+
nextAvailableDate,
|
|
126
|
+
businessHoursOverride.startTime,
|
|
127
|
+
timeZone,
|
|
128
|
+
);
|
|
129
|
+
const closingTime = setHmOnDate(
|
|
130
|
+
nextAvailableDate,
|
|
131
|
+
businessHoursOverride.endTime,
|
|
132
|
+
timeZone,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (isBefore(closingTime, date)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { openingTime, closingTime };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const currentTime = date;
|
|
146
|
+
let currentSlot: { startDate: Date; endDate: Date } | null = null;
|
|
147
|
+
|
|
148
|
+
for (const slot of dayBusinessTimes) {
|
|
149
|
+
if (isBefore(currentTime, slot.endDate)) {
|
|
150
|
+
currentSlot = slot;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!currentSlot) {
|
|
156
|
+
currentSlot = dayBusinessTimes[0];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (isBefore(currentSlot.endDate, date)) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
isTodayInTimeZone(nextAvailableDate, timeZone) &&
|
|
165
|
+
nextDateIndex + 1 < nextAvailableDates.length
|
|
166
|
+
) {
|
|
167
|
+
const { dayBusinessTimes: nextDayTimes } = getAvailableBusinessHours({
|
|
168
|
+
businessHours,
|
|
169
|
+
businessHoursOverrides,
|
|
170
|
+
timeZone,
|
|
171
|
+
nextAvailableDate: nextAvailableDates[nextDateIndex + 1],
|
|
172
|
+
});
|
|
173
|
+
if (nextDayTimes.length) {
|
|
174
|
+
const firstNextDaySlot = nextDayTimes?.[0];
|
|
175
|
+
if (
|
|
176
|
+
firstNextDaySlot &&
|
|
177
|
+
isMidnightTransition(
|
|
178
|
+
currentSlot.endDate,
|
|
179
|
+
firstNextDaySlot.startDate,
|
|
180
|
+
timeZone,
|
|
181
|
+
)
|
|
182
|
+
) {
|
|
183
|
+
currentSlot = {
|
|
184
|
+
...currentSlot,
|
|
185
|
+
endDate: firstNextDaySlot.endDate,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
openingTime: currentSlot.startDate,
|
|
193
|
+
closingTime: currentSlot.endDate,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getOpeningClosingTime({
|
|
204
|
+
location,
|
|
205
|
+
fulfillmentPreference,
|
|
206
|
+
businessHoursOverrides,
|
|
207
|
+
}: {
|
|
208
|
+
location: LocationLike;
|
|
209
|
+
fulfillmentPreference: FulfillmentPreference;
|
|
210
|
+
businessHoursOverrides?: Record<string, BusinessHoursOverrideOutput[]>;
|
|
211
|
+
}): { openingTime: Date; closingTime: Date } | null {
|
|
212
|
+
const businessHours = getLocationBusinessHoursForFulfillment(
|
|
213
|
+
location,
|
|
214
|
+
fulfillmentPreference,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return getOpeningClosingTimeOnDate({
|
|
218
|
+
businessHours,
|
|
219
|
+
businessHoursOverrides:
|
|
220
|
+
businessHoursOverrides?.[location.location_id] ?? [],
|
|
221
|
+
timeZone: location.timezone,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export function parseTimeString(timeString: string | null | undefined): {
|
|
2
|
+
hours: number;
|
|
3
|
+
minutes: number;
|
|
4
|
+
} {
|
|
5
|
+
if (!timeString) {
|
|
6
|
+
return { hours: 0, minutes: 0 };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const [hours = 0, minutes = 0] = String(timeString).split(":");
|
|
10
|
+
|
|
11
|
+
return { hours: Number(hours), minutes: Number(minutes) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTimeInRange(
|
|
15
|
+
schedule: { start_time?: string; end_time?: string },
|
|
16
|
+
time: { hours: number; minutes: number },
|
|
17
|
+
): boolean {
|
|
18
|
+
const startTime = parseTimeString(schedule?.start_time);
|
|
19
|
+
const endTime = parseTimeString(schedule?.end_time);
|
|
20
|
+
|
|
21
|
+
if (time.hours < startTime.hours || time.hours > endTime.hours) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (time.hours === startTime.hours) {
|
|
26
|
+
if (time.minutes < startTime.minutes) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (time.hours === endTime.hours) {
|
|
32
|
+
if (time.minutes > endTime.minutes) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return true;
|
|
38
|
+
}
|