UNPKG

@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
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)}`; } };