From 12503409b6c8abf84237ebe7930adcb400b563a3 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sun, 5 May 2024 20:43:47 +0200 Subject: [PATCH] fix dates ant TZ, cleanup --- components/availability/AvailabilityForm.js | 79 +++------- components/calendar/avcalendar.tsx | 58 +++---- components/cartevent/CartEventForm.tsx | 4 +- components/publisher/PublisherSearchBox.js | 13 +- pages/api/email.ts | 4 +- pages/dash.tsx | 25 ++- src/helpers/common.js | 161 ++++++++++++++------ 7 files changed, 190 insertions(+), 154 deletions(-) diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index b911438..79b1345 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -11,6 +11,7 @@ import { ToastContainer } from 'react-toastify'; const common = require('src/helpers/common'); //todo import Availability type from prisma schema import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete +import { stat } from 'fs'; const { DateTime, FixedOffsetZone } = require('luxon'); @@ -21,7 +22,7 @@ const fetchConfig = async () => { return config.default; }; -export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, datePicker = false }) { +export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, cartEvent, datePicker = false }) { const router = useRouter(); const urls = { @@ -67,14 +68,13 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o }, []); - // Define the minimum and maximum times - const minTime = new Date(); - minTime.setHours(9, 0, 0, 0); // 8:00 AM - const maxTime = new Date(); - maxTime.setHours(19, 30, 0, 0); // 8:00 PM + // get cart event or set default time for Sofia timezone + const minTime = cartEvent?.startTime || DateTime.now().set({ hour: 8, minute: 0, second: 0, millisecond: 0, zone: 'Europe/Sofia' }).toJSDate(); + const maxTime = cartEvent?.endTime || DateTime.now().set({ hour: 20, minute: 0, second: 0, millisecond: 0, zone: 'Europe/Sofia' }).toJSDate(); useEffect(() => { - setTimeSlots(generateTimeSlots(minTime, maxTime, 90, availabilities)); + setTimeSlots(generateTimeSlots(new Date(minTime), new Date(maxTime), cartEvent.shiftDuration, availabilities)); + console.log("AvailabilityForm: minTime: " + common.getTimeFormatted(minTime) + ", maxTime: " + common.getTimeFormatted(maxTime), ", " + cartEvent.shiftDuration + " min. shifts"); }, []); @@ -189,34 +189,11 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o // Common function to set shared properties function setSharedAvailabilityProperties(availability, group, timeSlots) { - // Define a fixed offset for Sofia (+2 hours from UTC, ignoring DST) - const fixedZone = FixedOffsetZone.instance(120); // Offset in minutes - - // availability.startTime = group[0].startTime; - // availability.endTime = group[group.length - 1].endTime; - // Adjust start time - let startTime = DateTime.fromJSDate(group[0].startTime, { zone: 'utc' }) - .setZone(fixedZone, { keepLocalTime: true }); - startTime = startTime.set({ - hour: group[0].startTime.getUTCHours(), - minute: group[0].startTime.getUTCMinutes(), - second: group[0].startTime.getUTCSeconds() - }); - - // Adjust end time - let endTime = DateTime.fromJSDate(group[group.length - 1].endTime, { zone: 'utc' }) - .setZone(fixedZone, { keepLocalTime: true }); - endTime = endTime.set({ - hour: group[group.length - 1].endTime.getUTCHours(), - minute: group[group.length - 1].endTime.getUTCMinutes(), - second: group[group.length - 1].endTime.getUTCSeconds() - }); - - // Update the availability object with the new times - availability.startTime = startTime.toJSDate(); - availability.endTime = endTime.toJSDate(); - - availability.name = common.getTimeFomatted(group[0].startTime) + "-" + common.getTimeFomatted(group[group.length - 1].endTime); + let startTime = common.setTimeHHmm(new Date(availability.startTime || day), common.getTimeFormatted(group[0].startTime)); + let endTime = common.setTimeHHmm(new Date(availability.endTime || day), common.getTimeFormatted(group[group.length - 1].endTime)); + availability.startTime = startTime; + availability.endTime = endTime; + availability.name = common.getTimeFormatted(group[0].startTime) + "--" + common.getTimeFormatted(group[group.length - 1].endTime); availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport; availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport; @@ -308,27 +285,19 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o let currentTime = start; //const baseDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0)); - const baseDate = new Date(start); + //const baseDate = new Date(start); while (isBefore(currentTime, end)) { - let slotStart = normalizeTime(currentTime, baseDate); - let slotEnd = normalizeTime(addMinutes(currentTime, increment), baseDate); + let slotStart = currentTime; + let slotEnd = addMinutes(currentTime, increment); const isChecked = items.some(item => { - let itemStart = item.startTime ? normalizeTime(new Date(item.startTime), baseDate) : null; - let itemEnd = item.endTime ? normalizeTime(new Date(item.endTime), baseDate) : null; - - return itemStart && itemEnd && - (slotStart.getTime() < itemEnd.getTime()) && - (slotEnd.getTime() > itemStart.getTime()); - }); - - slots.push({ - startTime: slotStart, - endTime: slotEnd, - isChecked: isChecked, + return item.startTime && item.endTime && + (slotStart.getTime() < item.endTime.getTime()) && + (slotEnd.getTime() > item.startTime.getTime()); }); + slots.push({ startTime: slotStart, endTime: slotEnd, isChecked: isChecked, }); currentTime = addMinutes(currentTime, increment); } @@ -342,16 +311,6 @@ export default function AvailabilityForm({ publisherId, existingItems, inline, o return slots; }; - // Normalize the time part of a date by using a base date - function normalizeTime(date, baseDate) { - return set(baseDate, { - hours: getHours(date), - minutes: getMinutes(date), - seconds: getSeconds(date), - milliseconds: 0 - }); - } - const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked)); const handleAllDayChange = (e) => { diff --git a/components/calendar/avcalendar.tsx b/components/calendar/avcalendar.tsx index f0d47e0..662047f 100644 --- a/components/calendar/avcalendar.tsx +++ b/components/calendar/avcalendar.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, use } from 'react'; import { Calendar, momentLocalizer, dateFnsLocalizer } from 'react-big-calendar'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import AvailabilityForm from '../availability/AvailabilityForm'; @@ -18,11 +18,13 @@ import { MdToday } from 'react-icons/md'; import { useSwipeable } from 'react-swipeable'; import axiosInstance from '../../src/axiosSecure'; +import { set } from 'date-fns'; +import { get } from 'http'; // import { set, format, addDays } from 'date-fns'; // import { isEqual, isSameDay, getHours, getMinutes } from 'date-fns'; -import { filter } from 'jszip'; -import e from 'express'; +// import { filter } from 'jszip'; +// import e from 'express'; @@ -46,7 +48,7 @@ const messages = { // Any other labels you want to translate... }; -const AvCalendar = ({ publisherId, events, selectedDate }) => { +const AvCalendar = ({ publisherId, events, selectedDate, cartEvents }) => { const isAdmin = ProtectedRoute.IsInRole(UserRole.ADMIN); @@ -65,7 +67,15 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { return { start, end }; }); + const [cartEvent, setCartEvent] = useState(null); + function getCartEvent(date) { + const dayOfWeek = common.getDayOfWeekNameEnEnumForDate(date); + const ce = cartEvents?.find(e => e.dayofweek === dayOfWeek); + return ce; + } + useEffect(() => { setCartEvent(getCartEvent(date)); }, + [date, selectedEvents]); // Update internal state when `events` prop changes useEffect(() => { @@ -113,6 +123,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { //setDisplayedEvents(evts); }, [visibleRange, evts, currentView]); + // todo: review that const handlers = useSwipeable({ onSwipedLeft: () => navigate('NEXT'), onSwipedRight: () => navigate('PREV'), @@ -201,18 +212,13 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { return existingEvents; }; - // Define min and max times - const minHour = 8; // 8:00 AM - const maxHour = 20; // 8:00 PM - const minTime = new Date(); - minTime.setHours(minHour, 0, 0); - const maxTime = new Date(); - maxTime.setHours(maxHour, 0, 0); - const totalHours = maxHour - minHour; + + // const totalHours = maxHour - minHour; const handleSelect = ({ mode, start, end }) => { - const startdate = typeof start === 'string' ? new Date(start) : start; - const enddate = typeof end === 'string' ? new Date(end) : end; + //we set the time to proper timezone + const startdate = common.setTimezone(start); + const enddate = common.setTimezone(end); if (!start || !end) return; //readonly for past dates (ToDo: if not admin) @@ -224,27 +230,10 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { end = common.setTimeHHmm(startdate, "23:59"); } - const startMinutes = common.getTimeInMinutes(start); - const endMinutes = common.getTimeInMinutes(end); - - // Adjust start and end times to be within min and max hours - if (startMinutes < common.getTimeInMinutes(common.setTimeHHmm(start, minHour))) { - start = common.setTimeHHmm(start, minHour); - } - if (endMinutes > common.getTimeInMinutes(common.setTimeHHmm(end, maxHour))) { - end = common.setTimeHHmm(end, maxHour); - } - setDate(start); - - // get exising events for the selected date - //ToDo: properly fix this. filterEvents does not return the expcted results let existingEvents = filterEvents(evts, publisherId, startdate); - // if existingEvents is empty - create new with the selected range - // if (existingEvents.length === 0) { - // existingEvents = [{ startTime: start, endTime: end }]; - // } console.log("handleSelect: " + existingEvents); + setCartEvent(getCartEvent(date)); setSelectedEvents(existingEvents); setIsModalOpen(true); }; @@ -509,8 +498,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { onSelectSlot={handleSelect} onSelectEvent={handleEventClick} style={{ height: '100%', width: '100%' }} - min={minTime} // Set minimum time - max={maxTime} // Set maximum time + min={cartEvent?.startTime} // Set minimum time + max={cartEvent?.endTime} // Set maximum time messages={messages} view={currentView} views={['month', 'week', 'agenda']} @@ -541,6 +530,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { date={date} onDone={handleDialogClose} inline={true} + cartEvent={cartEvent} // Pass other props as needed /> diff --git a/components/cartevent/CartEventForm.tsx b/components/cartevent/CartEventForm.tsx index d6d6fdd..b8defce 100644 --- a/components/cartevent/CartEventForm.tsx +++ b/components/cartevent/CartEventForm.tsx @@ -69,8 +69,8 @@ export default function CartEventForm(props: IProps) { try { console.log("fetching cart event from component " + router.query.id); const { data } = await axiosInstance.get(urls.apiUrl + id); - data.startTime = common.formatTimeHHmm(data.startTime) - data.endTime = common.formatTimeHHmm(data.endTime) + data.startTime = common.getTimeFormatted(data.startTime) + data.endTime = common.getTimeFormatted(data.endTime) setEvt(data); console.log("id:" + evt.id); diff --git a/components/publisher/PublisherSearchBox.js b/components/publisher/PublisherSearchBox.js index 19f06ac..fe21a12 100644 --- a/components/publisher/PublisherSearchBox.js +++ b/components/publisher/PublisherSearchBox.js @@ -11,9 +11,15 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s const [searchResults, setSearchResults] = useState([]); const [selectedDate, setSelectedDate] = useState(filterDate); + + // useEffect(() => { + // fetchPublishers(); + // }, []); // Empty dependency array ensures this useEffect runs only once + + // Update publishers when filterDate or showList changes useEffect(() => { fetchPublishers(); - }, []); // Empty dependency array ensures this useEffect runs only once + }, [filterDate, showList]); const fetchPublishers = async () => { console.log("fetchPublishers called"); @@ -60,10 +66,7 @@ function PublisherSearchBox({ id, selectedId, onChange, isFocused, filterDate, s // console.log("filterDate changed = ", filterDate); // }, [filterDate]); - // Update publishers when filterDate or showList changes - useEffect(() => { - fetchPublishers(); - }, [filterDate, showList]); + // Update selectedItem when selectedId changes and also at the initial load useEffect(() => { diff --git a/pages/api/email.ts b/pages/api/email.ts index c9914d7..b8c2161 100644 --- a/pages/api/email.ts +++ b/pages/api/email.ts @@ -161,7 +161,7 @@ export default async function handler(req, res) { newPubs: newPubs, placeName: assignment.shift.cartEvent.location.name, dateStr: common.getDateFormated(assignment.shift.startTime), - time: common.formatTimeHHmm(assignment.shift.startTime), + time: common.getTimeFormatted(assignment.shift.startTime), sentDate: common.getDateFormated(new Date()) }; @@ -383,7 +383,7 @@ export default async function handler(req, res) { email: pubsToSend[i].email, placeName: assignment.shift.cartEvent.location.name, dateStr: common.getDateFormated(assignment.shift.startTime), - time: common.formatTimeHHmm(assignment.shift.startTime), + time: common.getTimeFormatted(assignment.shift.startTime), sentDate: common.getDateFormated(new Date()) }; let results = emailHelper.SendEmailHandlebars( diff --git a/pages/dash.tsx b/pages/dash.tsx index 568b1d5..fa8beb1 100644 --- a/pages/dash.tsx +++ b/pages/dash.tsx @@ -15,13 +15,14 @@ import { getServerSession } from "next-auth/next" import PublisherSearchBox from '../components/publisher/PublisherSearchBox'; import PublisherInlineForm from '../components/publisher/PublisherInlineForm'; +import CartEventForm from "components/cartevent/CartEventForm"; interface IProps { initialItems: Availability[]; initialUserId: string; } -export default function IndexPage({ initialItems, initialUserId }: IProps) { +export default function IndexPage({ initialItems, initialUserId, cartEvents }: IProps) { const { data: session } = useSession(); const [userName, setUserName] = useState(session?.user?.name); const [userId, setUserId] = useState(initialUserId); @@ -78,7 +79,7 @@ export default function IndexPage({ initialItems, initialUserId }: IProps) {
- + @@ -223,10 +224,30 @@ export const getServerSideProps = async (context) => { return updatedItem; }); + // log first availability startTime to verify timezone and UTC conversion + + console.log("First availability startTime: " + items[0].startTime); + console.log("First availability startTime: " + items[0].startTime.toLocaleString()); + + const prisma = common.getPrismaClient(); + let cartEvents = await prisma.cartEvent.findMany({ + where: { + isActive: true, + }, + select: { + id: true, + startTime: true, + endTime: true, + dayofweek: true, + shiftDuration: true, + } + }); + cartEvents = common.convertDatesToISOStrings(cartEvents); return { props: { initialItems: items, userId: session?.user.id, + cartEvents: cartEvents, // messages: (await import(`../content/i18n/${context.locale}.json`)).default }, }; diff --git a/src/helpers/common.js b/src/helpers/common.js index 7d677aa..39c352d 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -339,14 +339,8 @@ exports.compareTimes = function (time1, time2) { const time2String = `${getHours(time2)}:${getMinutes(time2)}`; return time1String.localeCompare(time2String); }; + exports.normalizeTime = function (date, baseDate) { - // return set(baseDate, { - // hours: getHours(date), - // minutes: getMinutes(date), - // seconds: getSeconds(date), - // milliseconds: 0 - // }); - //don't use date-fns let newDate = new Date(baseDate); newDate.setHours(date.getHours(), date.getMinutes(), date.getSeconds(), 0); return newDate; @@ -549,54 +543,96 @@ exports.getCurrentYearMonth = () => { const month = String(currentDate.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed return `${year}-${month}`; } -exports.getTimeFormatted = function (date) { - const dateTime = DateTime.fromJSDate(date, { zone: 'Europe/Sofia' }); - return dateTime.toFormat('HH:mm'); + + +// new date FNs +// Utility to handle date parsing consistently +const parseDate = (input) => { + return (typeof input === 'string' || input instanceof Date) + ? DateTime.fromJSDate(new Date(input), { zone: 'Europe/Sofia' }) + : DateTime.now({ zone: 'Europe/Sofia' }); +}; +exports.parseDate = parseDate; + +// Set timezone to 'Europe/Sofia' without translating time +exports.setTimezone = (input) => { + let dateTime = parseDate(input); + dateTime = dateTime.setZone('Europe/Sofia', { keepLocalTime: true }); + return dateTime.toJSDate(); }; -// format date to 'HH:mm' time string required by the time picker -exports.formatTimeHHmm = function (input) { - // Check if the input is a string or a Date object - const date = (typeof input === 'string') ? new Date(input) : input; +// Format date to a specified format, defaulting to 'HH:mm' +exports.getTimeFormatted = (input, format = 'HH:mm') => { + const dateTime = parseDate(input); + return dateTime.toFormat(format); +}; - return date.toLocaleTimeString('en-US', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - timeZone: 'Europe/Sofia' - }).substring(0, 5); -} +// Set time in 'HH:mm' format to a date and return as JS Date in Sofia timezone +exports.setTimeHHmm = (input, timeString) => { + let dateTime = parseDate(input); + const [hour, minute] = timeString.split(':').map(Number); + dateTime = dateTime.set({ hour, minute, second: 0, millisecond: 0 }); + return dateTime.toJSDate(); +}; - -//parse 'HH:mm' time string to date object +// Parse 'HH:mm' time string to a JS Date object in Sofia timezone for today exports.parseTimeHHmm = (timeString) => { - // If timeString is already a date, return it as is - if (timeString instanceof Date) { - return timeString; - } - - const [hours, minutes] = timeString.split(':'); - const date = new Date(); - date.setHours(hours); - date.setMinutes(minutes); - return date; -} - -exports.setTimeHHmm = (date, timeStringOrHours) => { - const newDate = new Date(date); - - if (typeof timeStringOrHours === 'string' && timeStringOrHours.includes(':')) { - // If hours is a string in "HH:mm" format - const [h, m] = timeStringOrHours.split(':'); - newDate.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0); - } else { - // If hours and minutes are provided separately - newDate.setHours(parseInt(timeStringOrHours, 10), 0, 0, 0); - } - - return newDate; + const dateTime = DateTime.now({ zone: 'Europe/Sofia' }); + const [hour, minute] = timeString.split(':').map(Number); + return dateTime.set({ hour, minute, second: 0, millisecond: 0 }).toJSDate(); }; +// ToDo: update all uses of this function to use the new one + +// exports.getTimeFormatted = function (date) { +// const dateTime = DateTime.fromJSDate(date, { zone: 'Europe/Sofia' }); +// return dateTime.toFormat('HH:mm'); +// }; + +// exports.setTimeHHmm = (date, timeStringOrHours) => { +// const newDate = new Date(date); + +// if (typeof timeStringOrHours === 'string' && timeStringOrHours.includes(':')) { +// // If hours is a string in "HH:mm" format +// const [h, m] = timeStringOrHours.split(':'); +// newDate.setHours(parseInt(h, 10), parseInt(m, 10), 0, 0); +// } else { +// // If hours and minutes are provided separately +// newDate.setHours(parseInt(timeStringOrHours, 10), 0, 0, 0); +// } + +// return newDate; +// }; +// // format date to 'HH:mm' time string required by the time picker +// exports.formatTimeHHmm = function (input) { +// // Check if the input is a string or a Date object +// const date = (typeof input === 'string') ? new Date(input) : input; + +// return date.toLocaleTimeString('en-US', { +// hour12: false, +// hour: '2-digit', +// minute: '2-digit', +// timeZone: 'Europe/Sofia' +// }).substring(0, 5); +// } + + +// //parse 'HH:mm' time string to date object +// exports.parseTimeHHmm = (timeString) => { +// // If timeString is already a date, return it as is +// if (timeString instanceof Date) { +// return timeString; +// } + +// const [hours, minutes] = timeString.split(':'); +// const date = new Date(); +// date.setHours(hours); +// date.setMinutes(minutes); +// return date; +// } + + + exports.getTimeInMinutes = (dateOrTimestamp) => { const date = new Date(dateOrTimestamp); logger.debug("getTimeInMinutes: date = ", date); @@ -782,8 +818,13 @@ exports.convertDatesToISOStrings = function (obj) { return obj; } + // if (obj instanceof Date) { + // return obj.toISOString(); + // } if (obj instanceof Date) { - return obj.toISOString(); + // Convert the Date object to a Luxon DateTime in UTC + const utcDate = DateTime.fromJSDate(obj, { zone: 'utc' }); + return utcDate.toISO(); // Output in UTC as ISO string } if (Array.isArray(obj)) { @@ -800,7 +841,29 @@ exports.convertDatesToISOStrings = function (obj) { return obj; } +function adjustDateForDST(date, timezone) { + // Convert the date to the specified timezone + let dateTime = DateTime.fromJSDate(date, { zone: timezone }); + // Check if the original date is in DST + const isOriginalDST = dateTime.isInDST; + + // Check if the current date in the same timezone is in DST + const isNowDST = DateTime.now().setZone(timezone).isInDST; + + // Compare and adjust if necessary + if (isOriginalDST && !isNowDST) { + // If original date was in DST but now is not, subtract one hour + dateTime = dateTime.minus({ hours: 1 }); + } else if (!isOriginalDST && isNowDST) { + // If original date was not in DST but now is, add one hour + dateTime = dateTime.plus({ hours: 1 }); + } + + // Return the adjusted date as a JavaScript Date + return dateTime.toJSDate(); +} +exports.adjustDateForDST = adjustDateForDST; // exports.getInitials = function (names) { // const parts = names.split(' '); // Split the full name into parts