@open-tender/utils
Version:
A library of utils for use with Open Tender applications that utilize our cloud-based Order API.
534 lines (533 loc) • 22.2 kB
JavaScript
import { add, differenceInMinutes, differenceInSeconds, parseISO, sub } from 'date-fns';
import { format, toDate, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz';
/* CONSTANTS */
export const DATE = 'yyyy-MM-dd';
export const TIME = 'h:mma';
export const DATETIME = 'yyyy-MM-dd h:mma';
export const HUMAN_DATE = 'MMM d, yyyy';
export const HUMAN_TIME = 'h:mma';
export const HUMAN_DATETIME = 'MMM d, h:mma';
export const timezoneMap = {
'US/Eastern': 'America/New_York',
'US/Central': 'America/Chicago',
'US/Mountain': 'America/Denver',
'US/Pacific': 'America/Los_Angeles'
};
export const weekdays = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday'
];
export const weekdaysUpper = weekdays.map(weekday => weekday.toUpperCase());
export const weekdaysLower = weekdays.map(weekday => weekday.toLowerCase());
export const weekdayOptions = weekdays.map(weekday => ({
value: weekday.toUpperCase(),
name: weekday
}));
export const makeWeekday = (date = new Date()) => {
return format(date, 'EEEE').toUpperCase();
};
export const minutesLeft = (start, end) => {
return Math.max(differenceInMinutes(start, end), 0);
};
export const secondsLeft = (start, end) => {
return Math.max(differenceInSeconds(start, end), 0);
};
export const secondsToTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = `${seconds % 60}`.padStart(2, '0');
return `${mins}:${secs}`;
};
export const timeLeft = (start, end) => {
const seconds = secondsLeft(start, end);
return secondsToTime(seconds);
};
export const dateForWeekday = (weekday) => {
const currentWeekday = makeWeekday();
const currentIndex = weekdaysUpper.findIndex(i => i === currentWeekday);
const index = weekdaysUpper.findIndex(i => i === weekday.toUpperCase());
const offset = index - currentIndex + (index >= currentIndex ? 0 : 7);
return add(new Date(), { days: offset });
};
export const weekdayAndTimeToDate = (weekday, timeStr) => {
const date = dateForWeekday(weekday);
const [hours, minutes] = timeStr.split(':').map(i => parseInt(i));
return setTimeForDate(date, hours, minutes);
};
/* HELPERS */
export const parseIsoToDate = (iso) => parseISO(iso);
export const fmtDate = (date, fmt) => {
return format(date, fmt);
};
// https://stackoverflow.com/questions/54555491/how-to-guess-users-timezone-using-date-fns-in-a-vuejs-app
export const getUserTimezone = () => {
try {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
catch (err) {
return 'America/New_York';
}
};
export const makeLocalDate = (dateStr) => {
const tz = getUserTimezone();
return toDate(dateStr, { timeZone: tz });
};
export const zonedTimeToDate = (str, timezone) => {
const tz = timezoneMap[timezone];
return toDate(str.split('.')[0], { timeZone: tz });
};
export const zonedTimeToDateStr = (str, timezone, fmt = DATE) => {
const date = zonedTimeToDate(str, timezone);
return format(date, fmt);
};
// returns a date string in a user's local time
export const makeLocalDateStr = (date, days = 0, fmt = DATE) => {
return format(add(date || new Date(), { days: days }), fmt);
};
export const todayDate = () => makeLocalDateStr();
export const tomorrowDate = () => makeLocalDateStr(null, 1);
export const isoToDate = (iso, tz) => {
return tz ? utcToZonedTime(parseISO(iso), tz) : parseISO(iso);
};
export const isoToDateStr = (iso, tz, fmt = DATETIME) => {
return format(isoToDate(iso, tz), fmt);
};
export const cleanISOString = (date) => {
return (date.toISOString().split('.')[0] + 'Z');
};
export const dateToIso = (date, tz) => {
return cleanISOString(zonedTimeToUtc(date, tz));
};
export const adjustIso = (iso, tz, adjustment) => {
const date = isoToDate(iso, tz);
return dateToIso(add(date, adjustment), tz);
};
export const adjustZonedIso = (zonedIso, tz, adjustment) => {
const adjusted = add(toDate(zonedIso), adjustment);
const zoned = utcToZonedTime(adjusted, tz);
const dateStr = format(zoned, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone: tz });
return dateStr;
};
export const dateToZonedDateStr = (date, tz, fmt = "yyyy-MM-dd'T'HH:mm:ssXXX", days = 0) => {
const adjustedDate = add(date, { days: days });
const zoned = utcToZonedTime(adjustedDate, tz);
return format(zoned, fmt, { timeZone: tz });
};
export const dateToZonedIso = (date, tz) => {
const zoned = utcToZonedTime(date, tz);
return format(zoned, "yyyy-MM-dd'T'HH:mm:ssXXX", { timeZone: tz });
};
export const currentLocalDate = (tz) => {
const date = new Date();
return isoToDate(date.toISOString(), tz);
};
export const currentLocalDateStr = (tz, fmt = DATE) => {
const date = new Date();
return format(isoToDate(date.toISOString(), tz), fmt);
};
export const dateStrToDate = (str) => toDate(str);
export const dateStrToZonedDate = (str, tz) => {
return toDate(str, { timeZone: tz });
};
export const dateStrToZonedWeekday = (str) => {
return makeWeekday(toDate(str));
};
export const replaceAmPm = (str) => str.replace('AM', 'am').replace('PM', 'pm');
export const formatDate = (date, fmt = HUMAN_DATETIME, amPm = true) => {
const str = format(date, fmt);
return amPm ? replaceAmPm(str) : str;
};
export const formatDateStr = (str, fmt = HUMAN_DATE) => {
return str ? format(toDate(str), fmt) : null;
};
export const formatTimeStr = (str) => {
const clean = str.replace(/\s/g, '').toLowerCase();
const parts = clean.split('-');
if (parts.length === 1)
return clean;
const [part1, part2] = parts;
const end1 = part1.substr(part1.length - 2);
const end2 = part2.substr(part1.length - 2);
const newPart1 = ['am', 'pm'].includes(end1) && end1 === end2
? part1.slice(0, part1.length - 2)
: part1;
return [newPart1, part2].join('-');
};
export const dateStrMinutesToIso = (dateStr, minutes, tz) => {
const hours = Math.floor(minutes / 60)
.toString()
.padStart(2, '0');
const mins = (minutes % 60).toString().padStart(2, '0');
const timeStr = `${hours}:${mins}:00`;
const dateTimeStr = `${dateStr} ${timeStr}`;
const dateUtc = zonedTimeToUtc(dateTimeStr, tz);
return cleanISOString(dateUtc);
};
export const isoToDateStrMinutes = (iso, tz) => {
const dateObj = isoToDate(iso, tz);
const minutes = getMinutesfromDate(dateObj);
const date = format(dateObj, DATE);
return { date, minutes };
};
export const makeReadableDateStrFromIso = (iso, tz, verbose = false, withTime = true) => {
if (!iso || iso.toLowerCase() === 'asap')
return 'ASAP';
const date = utcToZonedTime(parseISO(iso), tz);
const timeString = format(date, TIME).toLowerCase();
const dateString = makeLocalDateStr(date);
if (dateString === todayDate()) {
if (!withTime)
return 'Today';
return verbose ? `Today @ ${timeString}` : timeString;
}
else if (dateString === tomorrowDate()) {
return `${verbose ? 'Tomorrow' : 'Tmrw'} ${withTime ? `@ ${timeString}` : ''}`;
}
else {
return `${verbose ? format(date, 'EEE, MMM d') : format(date, 'M/d')} ${withTime ? `@ ${timeString}` : ''}`;
}
};
export const makeRequestedIso = (requestedAt) => {
return !requestedAt || requestedAt === 'asap'
? cleanISOString(new Date())
: requestedAt;
};
export const makeRequestedAtStr = (requestedAt, tz, verbose = false) => {
if (!requestedAt || requestedAt.toLowerCase() === 'asap')
return 'ASAP';
return makeReadableDateStrFromIso(requestedAt, tz, verbose);
};
export const makeEstimatedTime = (requestedAt, revenueCenter, serviceType, verbose = false) => {
var _a, _b;
if (requestedAt !== 'asap' || !serviceType || !revenueCenter)
return null;
const settings = revenueCenter;
const { first_times } = settings;
if (!first_times)
return null;
const st = serviceType === 'WALKIN' ? 'PICKUP' : serviceType;
const firstTime = first_times[st];
if ((firstTime === null || firstTime === void 0 ? void 0 : firstTime.date) === todayDate()) {
return `around ${firstTime.time}`;
}
else if ((firstTime === null || firstTime === void 0 ? void 0 : firstTime.date) === tomorrowDate()) {
return `tomorrow @ ${formatTimeStr(firstTime === null || firstTime === void 0 ? void 0 : firstTime.time)}`;
}
else {
const tz = timezoneMap[(_a = revenueCenter.timezone) !== null && _a !== void 0 ? _a : 'US/Eastern'];
const date = toDate((_b = firstTime === null || firstTime === void 0 ? void 0 : firstTime.date) !== null && _b !== void 0 ? _b : new Date(), { timeZone: tz });
return `${verbose ? format(date, 'EEEE, MMMM d') : format(date, 'M/d')} @ ${firstTime === null || firstTime === void 0 ? void 0 : firstTime.time}`;
}
};
function* range(start, end, step) {
for (let i = start; i <= end; i += step) {
yield i;
}
}
export const makeOppositeTimes = (times, interval, min = 0, max = 1440) => {
return [...range(min, max - interval, interval)].filter(i => !times.includes(i));
};
export const time24ToMinutes = (str) => {
const [hours, minutes] = str.split(':');
return parseInt(hours) * 60 + parseInt(minutes);
};
export const setTimeForDate = (date, hours, minutes, seconds = 0) => {
date.setHours(hours);
date.setMinutes(minutes);
date.setSeconds(seconds);
date.setMilliseconds(0);
return date;
};
export const minutesToDate = (minutes, date) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const dateCloned = date ? new Date(date.getTime()) : new Date();
return setTimeForDate(dateCloned, hours, mins);
};
export const time24ToDate = (str) => {
const minutes = time24ToMinutes(str);
return minutesToDate(minutes);
};
export const time24ToDateStr = (str, fmt = TIME) => {
const minutes = time24ToMinutes(str);
return format(minutesToDate(minutes), fmt).replace(':00', '').toLowerCase();
};
export const minutesToDates = (minutes, date) => {
return minutes.map(minute => {
return minutesToDate(minute, date);
});
};
export const getMinutesfromDate = (date) => {
return date.getHours() * 60 + date.getMinutes();
};
export const getNextIntervalMinutes = (minutes, interval) => {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const intervals = interval === 15 ? [0, 15, 30, 45, 60] : [0, 30, 60];
const nextMinute = intervals.filter(i => i >= mins)[0];
return hours * 60 + nextMinute;
};
export const makeDates = (startDateStr, days, fmt = HUMAN_DATE) => {
const startDate = dateStrToDate(startDateStr);
const dateArray = [];
for (let step = 0; step < days + 1; step++) {
const nextDate = add(startDate, { days: step });
const value = format(nextDate, DATE);
const label = value === todayDate()
? 'Today'
: value === tomorrowDate()
? 'Tomorrow'
: format(nextDate, fmt);
dateArray.push({ label, value });
}
return dateArray;
};
export const makeTimes = (date, firstTime, validTimes, holidays, serviceType, leadTime = 0) => {
var _a;
const weekday = dateStrToZonedWeekday(date);
let times = null;
const holiday = holidays ? holidays[date] : null;
if (holiday) {
times = holiday[serviceType] ? (_a = holiday[serviceType]) === null || _a === void 0 ? void 0 : _a.valid_times : null;
}
else {
const validTime = validTimes[serviceType];
times = validTime ? validTime[weekday] : null;
}
if (!times)
return null;
// if first available date, remove times before first available time
if (date === firstTime.date) {
times = times.filter(t => t.minutes >= firstTime.minutes + leadTime);
}
return times.map(t => ({
name: t.time,
value: t.minutes,
disabled: !t.is_orderable
}));
};
export const makeOrderTimes = (orderTimes, tz) => {
var _a;
const currentDate = new Date();
const withDates = orderTimes === null || orderTimes === void 0 ? void 0 : orderTimes.map(i => {
const { weekday, time } = i.order_by;
let orderByDate = weekdayAndTimeToDate(weekday, time);
let startDate = weekdayAndTimeToDate(i.weekday, i.start_time);
// if (orderByDate < currentDate || startDate < currentDate) {
// startDate = add(startDate, { days: 7 })
// }
if (orderByDate < currentDate) {
orderByDate = add(orderByDate, { days: 7 });
}
if (startDate < orderByDate) {
startDate = add(startDate, { days: 7 });
}
const orderBy = Object.assign(Object.assign({}, i.order_by), { date: orderByDate });
return Object.assign(Object.assign({}, i), { date: startDate, iso: dateToIso(startDate, tz), order_by: orderBy });
});
return (_a = withDates === null || withDates === void 0 ? void 0 : withDates.sort((a, b) => a.date.getTime() - b.date.getTime())) !== null && _a !== void 0 ? _a : [];
};
export const findOrderTime = (orderTimes, tz, requestedAt) => {
const sortedTimes = makeOrderTimes(orderTimes, tz);
const selected = requestedAt
? sortedTimes.find(i => i.iso === requestedAt)
: null;
return selected || sortedTimes[0];
};
export const makeFirstTime = (revenueCenter, tz, serviceType, requestedAt) => {
var _a;
const { first_times, order_times } = revenueCenter;
const st = serviceType === 'WALKIN' ? 'PICKUP' : serviceType;
if (!first_times || !first_times[st]) {
if (!order_times || !order_times[st] || !((_a = order_times[st]) === null || _a === void 0 ? void 0 : _a.length))
return null;
const selected = findOrderTime(order_times[st], tz, requestedAt);
return selected.iso;
}
const firstTime = first_times[st];
const firstDate = firstTime ? isoToDate(firstTime === null || firstTime === void 0 ? void 0 : firstTime.utc, tz) : new Date();
const hasAsap = firstTime === null || firstTime === void 0 ? void 0 : firstTime.has_asap;
if (requestedAt === 'asap' && hasAsap) {
return 'asap';
}
const requestedDate = requestedAt && requestedAt !== 'asap'
? isoToDate(requestedAt, tz)
: null;
if (requestedDate && requestedDate > firstDate) {
return requestedAt;
}
if (hasAsap && firstTime.minutes % 15 !== 0) {
return 'asap';
}
return firstTime.utc;
};
export const makeFirstRequestedAt = (revenueCenter, serviceType, requestedAt) => {
const { timezone, revenue_center_type } = revenueCenter;
const tz = timezoneMap[timezone];
const newRequestedAt = requestedAt || (revenue_center_type === 'OLO' ? 'asap' : null);
return makeFirstTime(revenueCenter, tz, serviceType, newRequestedAt);
};
export const makeFirstTimes = (revenueCenter, serviceType, requestedAt) => {
const { timezone } = revenueCenter;
const settings = revenueCenter;
const tz = timezoneMap[timezone];
const otherServiceType = serviceType === 'DELIVERY' ? 'PICKUP' : 'DELIVERY';
const current = makeFirstTime(settings, tz, serviceType, requestedAt);
const other = makeFirstTime(settings, tz, otherServiceType, requestedAt);
if (!current && !other)
return null;
return [
current ? { serviceType: serviceType, requestedAt: current } : null,
other ? { serviceType: otherServiceType, requestedAt: other } : null
];
// return { [serviceType]: current, [otherServiceType]: other }
};
export const getNextInterval = (requestedIso, tz, interval) => {
const date = isoToDate(requestedIso, tz);
const intervals = interval === 15 ? [0, 15, 30, 45, 60] : [0, 30, 60];
const nextInterval = intervals.filter(i => i >= date.getMinutes())[0];
const hours = nextInterval === 60 ? date.getHours() + 1 : date.getHours();
const minutes = nextInterval === 60 ? 0 : nextInterval;
date.setHours(hours);
date.setMinutes(minutes);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
};
export const adjustRequestedAt = (requestedIso, tz, interval, leadTime) => {
const nextDate = getNextInterval(requestedIso, tz, interval);
const adjusted = add(nextDate, { minutes: leadTime });
return dateToIso(adjusted, tz);
};
export const getFirstTime = (revenueCenter, serviceType, tz) => {
var _a;
const { first_times, order_times } = revenueCenter;
const st = serviceType === 'WALKIN' ? 'PICKUP' : serviceType;
if (!first_times || !first_times[st]) {
if (!order_times || !order_times[st])
return null;
const orderTimes = makeOrderTimes((_a = order_times[st]) !== null && _a !== void 0 ? _a : [], tz);
return orderTimes[0];
}
return first_times[st];
};
export const makeGroupOrderTime = (revenueCenter, serviceType, requestedAt) => {
const { timezone } = revenueCenter || {};
const settings = revenueCenter;
const tz = timezoneMap[timezone !== null && timezone !== void 0 ? timezone : 'US/Eastern'];
const { first_times, order_times, wait_times, group_ordering } = settings;
const { prep_time, lead_time } = group_ordering || {};
const st = serviceType === 'WALKIN' ? 'PICKUP' : serviceType;
const waitTime = wait_times && wait_times[st] ? wait_times[st] : 0;
const prepTime = `${(waitTime !== null && waitTime !== void 0 ? waitTime : 0) + (prep_time !== null && prep_time !== void 0 ? prep_time : 0)}`;
if (requestedAt === 'asap')
return { prepTime };
const firstTime = first_times && first_times[st] ? first_times[st] : null;
const orderTimes = order_times && order_times[st] ? order_times[st] : null;
if (!firstTime && !orderTimes)
return null;
let adjustedIso, adjustedDate, cutoffDate, firstIso;
if (orderTimes) {
const orderTime = findOrderTime(orderTimes, tz, requestedAt);
adjustedIso = orderTime.iso;
adjustedDate = isoToDate(adjustedIso, tz);
cutoffDate = orderTime.order_by.date;
const allOrderTimes = makeOrderTimes(orderTimes, tz);
firstIso = allOrderTimes[0].iso;
}
else {
// first available time depends on both suggested lead time and extra prep time
// lead_time = how much time cart guests have to place their orders
// prep_time = how much extra prep time is required for group orders
// over and above the pickup or delivery wait time
if (!firstTime)
return null;
const leadTime = (prep_time !== null && prep_time !== void 0 ? prep_time : 0) + (lead_time !== null && lead_time !== void 0 ? lead_time : 0);
const interval = 15;
firstIso = adjustRequestedAt(firstTime.utc, tz, interval, leadTime);
const firstDate = isoToDate(firstIso, tz);
const requestedDate = isoToDate(requestedAt, tz);
adjustedIso =
requestedDate && requestedDate > firstDate ? requestedAt : firstIso;
adjustedDate = isoToDate(adjustedIso, tz);
cutoffDate = sub(adjustedDate, { minutes: parseFloat(prepTime) });
}
const cutoffIso = dateToIso(cutoffDate, tz);
return {
isAdjusted: requestedAt !== adjustedIso,
firstIso,
iso: adjustedIso,
date: adjustedDate,
dateStr: makeReadableDateStrFromIso(adjustedIso, tz, true),
cutoffIso,
cutoffDate,
cutoffDateStr: makeReadableDateStrFromIso(cutoffIso, tz, true)
};
};
export const formatTime = (time) => {
return time
? time.replace('Today', 'today').replace('Tomorrow', 'tomorrow')
: '';
};
export const makeGroupOrderTimeStr = (requestedAt, tz) => {
if (!requestedAt || requestedAt.toLowerCase() === 'asap')
return 'ASAP';
const orderTime = makeReadableDateStrFromIso(requestedAt, tz, true);
return orderTime ? formatTime(orderTime) : null;
};
export const getLastInterval = (tz) => {
const date = isoToDate(new Date().toISOString(), tz);
date.setHours(23);
date.setMinutes(45);
date.setSeconds(0);
date.setMilliseconds(0);
return date;
};
export const makeIntervals = (tz) => {
const nextInterval = getNextInterval(new Date().toISOString(), tz, 15);
const lastInterval = getLastInterval(tz);
const diff = Math.max(differenceInMinutes(lastInterval, nextInterval), 0);
const steps = 24 * 4 + (diff ? Math.ceil(diff / 15) : 0);
let start = sub(nextInterval, { minutes: 24 * 60 });
const intervals = [];
for (let step = 0; step < steps; step++) {
const end = add(start, { minutes: 15 });
intervals.push({ start, end, orders: [] });
start = end;
}
return intervals;
};
export const formatTimeList = (dateStr, tz, includeDate) => {
const date = isoToDate(dateStr, tz);
const fmt = includeDate ? 'MMM d, h:mma' : 'h:mma';
return format(date, fmt).replace('AM', 'am').replace('PM', 'pm');
};
export const makeCartDateStr = (requestedAt, tz, waitTime) => {
if (requestedAt === 'asap')
return `Ready in about ${waitTime} minutes`;
const requestedDateStr = isoToDateStr(requestedAt, tz, 'yyyy-MM-dd');
const now = new Date();
const today = dateToZonedDateStr(now, tz, 'yyyy-MM-dd');
const tomorrow = dateToZonedDateStr(now, tz, 'yyyy-MM-dd', 1);
const isToday = requestedDateStr === today;
const isTomorrow = requestedDateStr === tomorrow;
const timeStr = isoToDateStr(requestedAt, tz, 'h:mma').toLowerCase();
const dateStr = isoToDateStr(requestedAt, tz, 'EEE, MMM dd');
if (isToday)
return `Ready by ${timeStr}`;
if (isTomorrow)
return `Ready by ${timeStr} tomorrow`;
return `Ready by ${timeStr} on ${dateStr}`;
};
export const makeOrderWindow = (orderTime) => {
if (orderTime.start_time === orderTime.end_time) {
return `@ ${time24ToDateStr(orderTime.start_time)}`;
}
else {
return `from ${time24ToDateStr(orderTime.start_time)} to ${time24ToDateStr(orderTime.end_time)}`;
}
};