167 lines
4.8 KiB
JavaScript
167 lines
4.8 KiB
JavaScript
function parseTimeToMinutes(time) {
|
|
if (typeof time !== "string" || !/^\d{2}:\d{2}$/.test(time)) {
|
|
return null;
|
|
}
|
|
const [hourStr, minuteStr] = time.split(":");
|
|
const hour = Number(hourStr);
|
|
const minute = Number(minuteStr);
|
|
if (
|
|
Number.isNaN(hour) ||
|
|
Number.isNaN(minute) ||
|
|
hour < 0 ||
|
|
hour > 23 ||
|
|
minute < 0 ||
|
|
minute > 59
|
|
) {
|
|
return null;
|
|
}
|
|
return hour * 60 + minute;
|
|
}
|
|
|
|
const DAYS = [
|
|
"monday",
|
|
"tuesday",
|
|
"wednesday",
|
|
"thursday",
|
|
"friday",
|
|
"saturday",
|
|
"sunday"
|
|
];
|
|
|
|
function dayIndex(day) {
|
|
return DAYS.indexOf(String(day || "").toLowerCase());
|
|
}
|
|
|
|
function buildWeeklyIntervals(menuPayload) {
|
|
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
|
|
const intervals = [];
|
|
|
|
menus.forEach((menu, menuIndex) => {
|
|
const availability = Array.isArray(menu?.service_availability) ? menu.service_availability : [];
|
|
availability.forEach((dayConfig, dayConfigIndex) => {
|
|
const dIdx = dayIndex(dayConfig?.day_of_week);
|
|
if (dIdx < 0) {
|
|
return;
|
|
}
|
|
const periods = Array.isArray(dayConfig?.time_periods) ? dayConfig.time_periods : [];
|
|
periods.forEach((period, periodIndex) => {
|
|
const start = parseTimeToMinutes(period?.start_time);
|
|
const end = parseTimeToMinutes(period?.end_time);
|
|
if (start === null || end === null || end < start) {
|
|
return;
|
|
}
|
|
const base = dIdx * 24 * 60;
|
|
intervals.push({
|
|
start: base + start,
|
|
end: base + end,
|
|
menuIndex,
|
|
dayConfigIndex,
|
|
periodIndex,
|
|
day: dayConfig?.day_of_week
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
return intervals.sort((a, b) => a.start - b.start);
|
|
}
|
|
|
|
function mergeContiguousIntervals(intervals) {
|
|
if (!intervals.length) {
|
|
return [];
|
|
}
|
|
|
|
const merged = [Object.assign({}, intervals[0])];
|
|
for (let i = 1; i < intervals.length; i += 1) {
|
|
const curr = intervals[i];
|
|
const last = merged[merged.length - 1];
|
|
if (curr.start <= last.end + 1) {
|
|
last.end = Math.max(last.end, curr.end);
|
|
} else {
|
|
merged.push(Object.assign({}, curr));
|
|
}
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
function collectVisibilityOverlapErrors(menuPayload) {
|
|
const items = Array.isArray(menuPayload?.items) ? menuPayload.items : [];
|
|
const errors = [];
|
|
|
|
items.forEach((item) => {
|
|
const visibilityHours = Array.isArray(item?.visibility_info?.hours)
|
|
? item.visibility_info.hours
|
|
: [];
|
|
visibilityHours.forEach((hourRule) => {
|
|
const hoursOfWeek = Array.isArray(hourRule?.hours_of_week) ? hourRule.hours_of_week : [];
|
|
const perDay = new Map();
|
|
hoursOfWeek.forEach((dayHours) => {
|
|
const d = String(dayHours?.day_of_week || "").toLowerCase();
|
|
if (!DAYS.includes(d)) {
|
|
return;
|
|
}
|
|
const list = perDay.get(d) || [];
|
|
const periods = Array.isArray(dayHours?.time_periods) ? dayHours.time_periods : [];
|
|
periods.forEach((p) => {
|
|
const s = parseTimeToMinutes(p?.start_time);
|
|
const e = parseTimeToMinutes(p?.end_time);
|
|
if (s === null || e === null || e < s) {
|
|
return;
|
|
}
|
|
list.push({ start: s, end: e, raw: p });
|
|
});
|
|
perDay.set(d, list);
|
|
});
|
|
|
|
perDay.forEach((periods, d) => {
|
|
const sorted = periods.sort((a, b) => a.start - b.start);
|
|
for (let i = 1; i < sorted.length; i += 1) {
|
|
const prev = sorted[i - 1];
|
|
const curr = sorted[i];
|
|
if (curr.start <= prev.end) {
|
|
errors.push(
|
|
`Invalid visibility overlap for item ${item?.id || "<unknown>"} on ${d}: ` +
|
|
`${String(prev.raw?.start_time)}-${String(prev.raw?.end_time)} overlaps ` +
|
|
`${String(curr.raw?.start_time)}-${String(curr.raw?.end_time)}`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
return errors;
|
|
}
|
|
|
|
function validateUploadMenuPayload(menuPayload) {
|
|
const errors = [];
|
|
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
|
|
if (menus.length === 0) {
|
|
errors.push("No Menus Errors: menu.menus must contain at least one menu.");
|
|
return errors;
|
|
}
|
|
|
|
const intervals = buildWeeklyIntervals(menuPayload);
|
|
if (intervals.length === 0) {
|
|
errors.push(
|
|
"No Hours Errors: at least one service_availability interval is required across the week."
|
|
);
|
|
} else {
|
|
const merged = mergeContiguousIntervals(intervals);
|
|
const shortBlocks = merged.filter((block) => block.end - block.start + 1 < 60);
|
|
if (shortBlocks.length > 0) {
|
|
errors.push(
|
|
"Short Hours Errors: each effective contiguous service_availability interval must be at least 60 minutes."
|
|
);
|
|
}
|
|
}
|
|
|
|
errors.push(...collectVisibilityOverlapErrors(menuPayload));
|
|
return errors;
|
|
}
|
|
|
|
module.exports = {
|
|
validateUploadMenuPayload
|
|
};
|