Untitled
Anonymous
plain_text
03/04/2026 8:24 AM
7.9 KB
12
Indexable
import type { ProfileV3WorkingHour } from '../types';
const WEEK_DAYS = [
'sunday',
'monday',
'tuesday',
'wednesday',
'thursday',
'friday',
'saturday',
] as const;
type ProviderNow = {
weekday: string;
year: number;
month: number;
day: number;
minutesOfDay: number;
};
type InternalOpenStatus = {
isOpen: boolean;
closesAtMinutes?: number;
closeDayOffset?: number;
};
export type WorkingHoursNowStatus = {
isOpen: boolean;
closesAt: string | null;
};
export const formatWorkingHoursDay = (
value: string,
weekdaysLabel: string,
nationalHolidaysLabel: string,
) => {
const normalized = value.replace(/[_-]+/g, ' ').trim();
const lower = normalized.toLowerCase();
if (
lower.includes('weekday') ||
lower === 'mon fri' ||
lower === 'monday friday'
) {
return weekdaysLabel;
}
if (lower === 'national holiday' || lower === 'national holidays') {
return nationalHolidaysLabel;
}
return normalized.replace(/\b\w/g, char => char.toUpperCase());
};
const getTimeZoneOffsetMinutes = (
date: Date,
timeZone: string,
): number | null => {
try {
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone,
timeZoneName: 'shortOffset',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
const part = formatter
.formatToParts(date)
.find(item => item.type === 'timeZoneName')?.value;
if (!part) return null;
const match = part.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2] ?? '0');
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
return hours * 60 + Math.sign(hours || 1) * minutes;
} catch {
return null;
}
};
const getProviderNow = (timeZone: string): ProviderNow | null => {
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone,
weekday: 'long',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hourCycle: 'h23',
});
const parts = formatter.formatToParts(now);
const get = (type: Intl.DateTimeFormatPartTypes) =>
parts.find(item => item.type === type)?.value || '';
const weekday = get('weekday').toLowerCase();
const year = Number(get('year'));
const month = Number(get('month'));
const day = Number(get('day'));
const hour = Number(get('hour'));
const minute = Number(get('minute'));
const minutesOfDay = hour * 60 + minute;
if (
!Number.isFinite(year) ||
!Number.isFinite(month) ||
!Number.isFinite(day) ||
!Number.isFinite(hour) ||
!Number.isFinite(minute)
) {
return null;
}
return { weekday, year, month, day, minutesOfDay };
};
const buildAbsoluteDateFromProviderLocal = (
providerNow: ProviderNow | null,
timeZone: string,
dayOffset: number,
minutesOfDay: number,
): Date | null => {
if (!providerNow) return null;
const hour = Math.floor(minutesOfDay / 60);
const minute = minutesOfDay % 60;
const approxUtc = Date.UTC(
providerNow.year,
providerNow.month - 1,
providerNow.day + dayOffset,
hour,
minute,
0,
);
const offsetMinutes = getTimeZoneOffsetMinutes(new Date(approxUtc), timeZone);
if (typeof offsetMinutes !== 'number') return null;
return new Date(approxUtc - offsetMinutes * 60 * 1000);
};
const getOpenStatusForNow = (
workingHours: ProfileV3WorkingHour[],
timeZone: string,
): InternalOpenStatus => {
const providerNow = getProviderNow(timeZone);
if (!providerNow) return { isOpen: false };
const currentDayIndex = WEEK_DAYS.findIndex(day => day === providerNow.weekday);
if (currentDayIndex < 0) return { isOpen: false };
const today = workingHours.find(
item => item.day.toLowerCase() === WEEK_DAYS[currentDayIndex],
);
const yesterdayIndex = (currentDayIndex + 6) % 7;
const yesterday = workingHours.find(
item => item.day.toLowerCase() === WEEK_DAYS[yesterdayIndex],
);
const nowMinutes = providerNow.minutesOfDay;
if (today?.isOpen24Hours) {
return { isOpen: true };
}
if (
today?.isOpen &&
Number.isFinite(today.opensAtMinutes) &&
Number.isFinite(today.closesAtMinutes)
) {
const open = Number(today.opensAtMinutes);
const close = Number(today.closesAtMinutes);
if (close > open && nowMinutes >= open && nowMinutes < close) {
return { isOpen: true, closesAtMinutes: close, closeDayOffset: 0 };
}
if (close < open && nowMinutes >= open) {
return { isOpen: true, closesAtMinutes: close, closeDayOffset: 1 };
}
}
if (
yesterday?.isOpen &&
Number.isFinite(yesterday.opensAtMinutes) &&
Number.isFinite(yesterday.closesAtMinutes)
) {
const yesterdayOpen = Number(yesterday.opensAtMinutes);
const yesterdayClose = Number(yesterday.closesAtMinutes);
const isOvernight = yesterdayClose < yesterdayOpen;
if (isOvernight && nowMinutes < yesterdayClose) {
return {
isOpen: true,
closesAtMinutes: yesterdayClose,
closeDayOffset: 0,
};
}
}
return { isOpen: false };
};
export const resolveWorkingHoursNowStatus = (
workingHours: ProfileV3WorkingHour[],
providerTimeZone: string,
locale?: string,
): WorkingHoursNowStatus => {
const openStatus = getOpenStatusForNow(workingHours, providerTimeZone);
if (!openStatus.isOpen || !Number.isFinite(openStatus.closesAtMinutes)) {
return { isOpen: openStatus.isOpen, closesAt: null };
}
const providerNow = getProviderNow(providerTimeZone);
const absoluteCloseDate = buildAbsoluteDateFromProviderLocal(
providerNow,
providerTimeZone,
openStatus.closeDayOffset ?? 0,
Number(openStatus.closesAtMinutes),
);
if (!absoluteCloseDate) {
return { isOpen: openStatus.isOpen, closesAt: null };
}
const closesAt = new Intl.DateTimeFormat(locale, {
hour: 'numeric',
minute: '2-digit',
}).format(absoluteCloseDate);
return { isOpen: openStatus.isOpen, closesAt };
};
Editor is loading...
Leave a Comment