From bb632d563c5214c52fe5c4e8107255972db9ed03 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 12 Jan 2025 02:45:40 -0800 Subject: [PATCH] Add WorkingHoursHelper --- .../widget/helpers/WorkingHoursHelper.js | 94 ++++++++++++++++++ .../helpers/specs/WorkingHourFixtures.js | 57 +++++++++++ .../helpers/specs/WorkingHoursHelper.spec.js | 95 +++++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 app/javascript/widget/helpers/WorkingHoursHelper.js create mode 100644 app/javascript/widget/helpers/specs/WorkingHourFixtures.js create mode 100644 app/javascript/widget/helpers/specs/WorkingHoursHelper.spec.js diff --git a/app/javascript/widget/helpers/WorkingHoursHelper.js b/app/javascript/widget/helpers/WorkingHoursHelper.js new file mode 100644 index 000000000..82dedafe9 --- /dev/null +++ b/app/javascript/widget/helpers/WorkingHoursHelper.js @@ -0,0 +1,94 @@ +const organizeByDay = workingHoursData => { + return workingHoursData.reduce((acc, schedule) => { + acc[schedule.dayOfWeek] = schedule; + return acc; + }, {}); +}; + +const toMinutes = (hours, minutes = 0) => { + return hours * 60 + (minutes || 0); +}; + +const calculateMinutesUntilOpen = ( + schedule, + currentTimeInMinutes, + dayOffset +) => { + if (!schedule || schedule.closedAllDay) { + return null; + } + + let openHour = schedule.openHour; + let openMinutes = schedule.openMinutes; + + if (schedule.openAllDay) { + openHour = 0; + openMinutes = 0; + } + + const openTimeInMinutes = toMinutes(openHour, openMinutes); + + if (dayOffset === 0) { + if (currentTimeInMinutes < openTimeInMinutes) { + return openTimeInMinutes - currentTimeInMinutes; + } + + return null; + } + + return ( + toMinutes(24, 0) * dayOffset + openTimeInMinutes - currentTimeInMinutes + ); +}; + +class WorkingHours { + constructor(workingHoursData) { + this.workingHoursByDay = organizeByDay(workingHoursData); + } + + isOpen(date) { + const schedule = this.workingHoursByDay[date.getDay()]; + + if (!schedule || schedule.closedAllDay) { + return false; + } + + if (schedule.openAllDay) { + return true; + } + + const currentMinutes = toMinutes(date.getHours(), date.getMinutes()); + const openMinutes = toMinutes(schedule.openHour, schedule.openMinutes); + const closeMinutes = toMinutes(schedule.closeHour, schedule.closeMinutes); + + return currentMinutes >= openMinutes && currentMinutes <= closeMinutes; + } + + openInMinutes(date) { + if (this.isOpen(date)) { + return 0; + } + + const currentMinutes = toMinutes(date.getHours(), date.getMinutes()); + + for (let i = 0; i < 7; i += 1) { + const checkDate = new Date(date); + checkDate.setDate(date.getDate() + i); + + const schedule = this.workingHoursByDay[checkDate.getDay()]; + const minutesUntilOpen = calculateMinutesUntilOpen( + schedule, + currentMinutes, + i + ); + + if (minutesUntilOpen !== null) { + return minutesUntilOpen; + } + } + + return -1; + } +} + +export default WorkingHours; diff --git a/app/javascript/widget/helpers/specs/WorkingHourFixtures.js b/app/javascript/widget/helpers/specs/WorkingHourFixtures.js new file mode 100644 index 000000000..9f1d8d037 --- /dev/null +++ b/app/javascript/widget/helpers/specs/WorkingHourFixtures.js @@ -0,0 +1,57 @@ +export default [ + { + dayOfWeek: 0, + openHour: 10, + openMinutes: 0, + closeHour: 16, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Sunday + { + dayOfWeek: 1, + openHour: 9, + openMinutes: 0, + closeHour: 17, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Monday + { + dayOfWeek: 2, + openHour: 9, + openMinutes: 0, + closeHour: 17, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Tuesday + { + dayOfWeek: 3, + openHour: 9, + openMinutes: 0, + closeHour: 17, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Wednesday + { + dayOfWeek: 4, + openHour: 9, + openMinutes: 0, + closeHour: 17, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Thursday + { + dayOfWeek: 5, + openHour: 9, + openMinutes: 0, + closeHour: 17, + closeMinutes: 0, + closedAllDay: false, + openAllDay: false, + }, // Friday + { dayOfWeek: 6, closedAllDay: true, openAllDay: false }, // Saturday +]; diff --git a/app/javascript/widget/helpers/specs/WorkingHoursHelper.spec.js b/app/javascript/widget/helpers/specs/WorkingHoursHelper.spec.js new file mode 100644 index 000000000..f6eda4438 --- /dev/null +++ b/app/javascript/widget/helpers/specs/WorkingHoursHelper.spec.js @@ -0,0 +1,95 @@ +import { describe, it, expect } from 'vitest'; +import WorkingHours from '../WorkingHoursHelper'; +import workingHoursData from './WorkingHourFixtures'; + +describe('WorkingHours', () => { + describe('isOpen', () => { + it('returns true during business hours', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(12, 0, 0); // 12:00 + expect(workingHours.isOpen(date)).toBe(true); + }); + + it('returns false before opening hours', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(8, 0, 0); // 8:00 + expect(workingHours.isOpen(date)).toBe(false); + }); + + it('returns false after closing hours', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(18, 0, 0); // 18:00 + expect(workingHours.isOpen(date)).toBe(false); + }); + + it('returns false on closed days', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 4); // Saturday + date.setHours(12, 0, 0); // 12:00 + expect(workingHours.isOpen(date)).toBe(false); + }); + + it('returns true for openAllDay', () => { + const customData = [...workingHoursData]; + customData[1] = { ...customData[1], openAllDay: true }; + const workingHours = new WorkingHours(customData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(22, 0, 0); + expect(workingHours.isOpen(date)).toBe(true); + }); + }); + + describe('openInMinutes', () => { + it('returns 0 when currently open', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(12, 0, 0); // 12:00 + expect(workingHours.openInMinutes(date)).toBe(0); + }); + + it('calculates minutes until opening same day', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(8, 0, 0); // 8:00 + expect(workingHours.openInMinutes(date)).toBe(60); // 60 minutes until 9:00 + }); + + it('calculates minutes until opening next day', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 6); // Monday + date.setHours(23, 0, 0); // 23:00 + // Tuesday opens at 9:00, so it's 10 hours (600 minutes) away + expect(workingHours.openInMinutes(date)).toBe(600); + }); + + it('handles closed days correctly', () => { + const workingHours = new WorkingHours(workingHoursData); + const date = new Date(2025, 0, 4); // Saturday + date.setHours(12, 0, 0); // 12:00 + // Should find Sunday's opening at 10:00 + const expectedMinutes = (24 - 12) * 60 + 10 * 60; // Remaining hours today + hours until opening tomorrow + expect(workingHours.openInMinutes(date)).toBe(expectedMinutes); + }); + + it('returns -1 if no opening time found within a week', () => { + const closedWeek = workingHoursData.map(day => ({ + ...day, + closedAllDay: true, + })); + const workingHours = new WorkingHours(closedWeek); + const date = new Date(2025, 0, 6); // Monday + date.setHours(12, 0, 0); // 12:00 + expect(workingHours.openInMinutes(date)).toBe(-1); + }); + + it('returns -1 if no working hours are provided', () => { + const workingHours = new WorkingHours([]); + const date = new Date(2025, 0, 6); // Monday + date.setHours(12, 0, 0); // 12:00 + expect(workingHours.openInMinutes(date)).toBe(-1); + }); + }); +});