fix dates ant TZ, cleanup

This commit is contained in:
Dobromir Popov
2024-05-05 20:43:47 +02:00
parent 227c1f0ab3
commit 12503409b6
7 changed files with 190 additions and 154 deletions

View File

@ -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) => {

View File

@ -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
/>
</div>

View File

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

View File

@ -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(() => {

View File

@ -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(

View File

@ -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) {
<div className="text-center font-bold pb-3 xs:pb-1">
<PublisherInlineForm publisherId={userId} />
</div>
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} />
<AvCalendar publisherId={userId} events={events} selectedDate={new Date()} cartEvents={cartEvents} />
</div>
</div>
</ProtectedRoute>
@ -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
},
};

View File

@ -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