diff --git a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx index e8f52ee31..e34ae14f7 100644 --- a/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx +++ b/client/src/components/form-date-time-picker/form-date-time-picker.component.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import React, { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import dayjs from "../../utils/day"; -import { dateFormats, dateTimeFormats } from "./formats.js"; +import { fuzzyMatchDate } from "./formats.js"; const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => { const [isManualInput, setIsManualInput] = useState(false); @@ -11,79 +11,34 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, is const handleChange = useCallback( (newDate) => { - if (newDate === null && onChange) { - onChange(null); - } else if (newDate && onChange) { - onChange(newDate); + if (onChange) { + onChange(newDate || null); } setIsManualInput(false); }, [onChange] ); - const normalizeDateTimeString = (input) => { - const upperV = input.toUpperCase().replaceAll(".", "/").replaceAll("-", "/"); - - const [datePart, ...timeParts] = upperV.split(" "); - - if (timeParts.length === 0) { - return datePart; // If there's no time part, just return the date part. - } - - const timePart = timeParts.join(" "); // In case there are multiple spaces, join them back - - // Normalize the time part by ensuring there's a space before AM/PM if not already present - const normalizedTime = timePart.replace(/(\d{1,2})(:\d{2})?\s?(AM|PM)/, "$1$2 $3"); - - // Combine the date part with the normalized time part - return `${datePart} ${normalizedTime}`.trim(); - }; - const handleBlur = useCallback( (e) => { + // Bail if this is not a manual input if (!isManualInput) { return; } - + // Reset manual input flag setIsManualInput(false); - const v = e.target.value; + const v = e?.target?.value; + if (!v) return; - const upperV = normalizeDateTimeString(v); - let parsedDate; + let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v); - for (const format of isDateOnly ? dateFormats : dateTimeFormats) { - parsedDate = dayjs(upperV, format); - if (parsedDate.isValid()) break; - } - - if (parsedDate && parsedDate.isValid()) { - if (isDateOnly) { - parsedDate = parsedDate.startOf("day"); - } - - if (value && value.isValid && value.isValid()) { - parsedDate = parsedDate.set({ - hours: value.hours(), - minutes: value.minutes(), - seconds: value.seconds(), - milliseconds: value.milliseconds() - }); - } - - if (onlyFuture) { - if (dayjs().subtract(1, "day").isBefore(parsedDate)) { - onChange(parsedDate); - } else { - onChange(dayjs().startOf("day")); - } - } else { - onChange(parsedDate); - } + if (parsedDate && onChange) { + onChange(parsedDate); } }, - [isManualInput, isDateOnly, onlyFuture, onChange, value] + [isManualInput, isDateOnly, onChange] ); const handleKeyDown = useCallback( diff --git a/client/src/components/form-date-time-picker/formats.js b/client/src/components/form-date-time-picker/formats.js index b68704a72..ffe9f68c2 100644 --- a/client/src/components/form-date-time-picker/formats.js +++ b/client/src/components/form-date-time-picker/formats.js @@ -1,96 +1,63 @@ -export const dateTimeFormats = [ - // Four-digit year with time - "M/D/YYYY h:mm A", // Example: 1/5/2023 9:00 AM - "M/D/YYYY h:mmA", // Example: 1/5/2023 9:00AM - "M/D/YYYY h A", // Example: 1/5/2023 9 AM - "M/D/YYYY hA", // Example: 1/5/2023 9AM - "M/D/YYYY hh:mm A", // Example: 1/5/2023 02:25 PM - "M/D/YYYY hh:mm:ss A", // Example: 1/5/2023 02:25:45 PM +import dayjs from "../../utils/day"; - "MM/D/YYYY h:mm A", // Example: 12/5/2023 9:00 AM - "MM/D/YYYY h:mmA", // Example: 12/5/2023 9:00AM - "MM/D/YYYY h A", // Example: 12/5/2023 9 AM - "MM/D/YYYY hA", // Example: 12/5/2023 9AM - "MM/D/YYYY hh:mm A", // Example: 12/5/2023 02:25 PM - "MM/D/YYYY hh:mm:ss A", // Example: 12/5/2023 02:25:45 PM - - "M/DD/YYYY h:mm A", // Example: 1/25/2023 9:00 AM - "M/DD/YYYY h:mmA", // Example: 1/25/2023 9:00AM - "M/DD/YYYY h A", // Example: 1/25/2023 9 AM - "M/DD/YYYY hA", // Example: 1/25/2023 9AM - "M/DD/YYYY hh:mm A", // Example: 1/25/2023 02:25 PM - "M/DD/YYYY hh:mm:ss A", // Example: 1/25/2023 02:25:45 PM - - "MM/DD/YYYY h:mm A", // Example: 12/25/2023 9:00 AM - "MM/DD/YYYY h:mmA", // Example: 12/25/2023 9:00AM - "MM/DD/YYYY h A", // Example: 12/25/2023 9 AM - "MM/DD/YYYY hA", // Example: 12/25/2023 9AM - "MM/DD/YYYY hh:mm A", // Example: 12/25/2023 02:25 PM - "MM/DD/YYYY hh:mm:ss A", // Example: 12/25/2023 02:25:45 PM - - // Two-digit year with time - "M/D/YY h:mm A", // Example: 1/5/23 9:00 AM - "M/D/YY h:mmA", // Example: 1/5/23 9:00AM - "M/D/YY h A", // Example: 1/5/23 9 AM - "M/D/YY hA", // Example: 1/5/23 9AM - "M/D/YY hh:mm A", // Example: 1/5/23 02:25 PM - "M/D/YY hh:mm:ss A", // Example: 1/5/23 02:25:45 PM - - "MM/D/YY h:mm A", // Example: 12/5/23 9:00 AM - "MM/D/YY h:mmA", // Example: 12/5/23 9:00AM - "MM/D/YY h A", // Example: 12/5/23 9 AM - "MM/D/YY hA", // Example: 12/5/23 9AM - "MM/D/YY hh:mm A", // Example: 12/5/23 02:25 PM - "MM/D/YY hh:mm:ss A", // Example: 12/5/23 02:25:45 PM - - "M/DD/YY h:mm A", // Example: 1/25/23 9:00 AM - "M/DD/YY h:mmA", // Example: 1/25/23 9:00AM - "M/DD/YY h A", // Example: 1/25/23 9 AM - "M/DD/YY hA", // Example: 1/25/23 9AM - "M/DD/YY hh:mm A", // Example: 1/25/23 02:25 PM - "M/DD/YY hh:mm:ss A", // Example: 1/25/23 02:25:45 PM - - "MM/DD/YY h:mm A", // Example: 12/25/23 9:00 AM - "MM/DD/YY h:mmA", // Example: 12/25/23 9:00AM - "MM/DD/YY h A", // Example: 12/25/23 9 AM - "MM/DD/YY hA", // Example: 12/25/23 9AM - "MM/DD/YY hh:mm A", // Example: 12/25/23 02:25 PM - "MM/DD/YY hh:mm:ss A", // Example: 12/25/23 02:25:45 PM - - // Four-digit year without time - "M/D/YYYY", // Example: 1/5/2023 - "MM/D/YYYY", // Example: 12/5/2023 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/DD/YYYY", // Example: 12/25/2023 - - // Two-digit year without time - "M/D/YY", // Example: 1/5/23 - "MM/D/YY", // Example: 12/5/23 - "M/DD/YY", // Example: 1/25/23 - "MM/DD/YY" // Example: 12/25/23 -]; - -// CONFIRMED -export const dateFormats = [ +const dateFormats = [ "MMDDYYYY", "MMDDYY", - // Four-digit year - "M/D/YYYY", // Example: 1/5/2023 - "MM/D/YYYY", // Example: 12/5/2023 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/DD/YYYY", // Example: 12/25/2023 - - // Two-digit year - "M/D/YY", // Example: 1/5/23 - "MM/D/YY", // Example: 12/5/23 - "M/DD/YY", // Example: 1/25/23 - "MM/DD/YY", // Example: 12/25/23 - - // Explicitly handle single-digit month and day - "M/D/YY", // Example: 1/5/23 - "M/D/YYYY", // Example: 1/5/2023 - "M/DD/YY", // Example: 1/25/23 - "M/DD/YYYY", // Example: 1/25/2023 - "MM/D/YY", // Example: 12/5/23 - "MM/D/YYYY" // Example: 12/5/2023 + "M/D/YYYY", + "MM/D/YYYY", + "M/DD/YYYY", + "MM/DD/YYYY", + "M/D/YY", + "MM/D/YY", + "M/DD/YY", + "MM/DD/YY" ]; + +const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"]; + +const dateTimeFormats = [ + ...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap( + (dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`) + ), + + ...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)), + + "M/D/YYYY", + "MM/D/YYYY", + "M/DD/YYYY", + "MM/DD/YYYY", + "M/D/YY", + "MM/D/YY", + "M/DD/YY", + "MM/DD/YY", + "MMDDYYYY", + "MMDDYY" +]; + +const sanitizeInput = (input) => + input + .trim() + .toUpperCase() + .replace(/\s*(am|pm)\s*/i, " $1") + .replaceAll(".", "/") + .replaceAll("-", "/"); + +export const fuzzyMatchDate = (dateString) => { + const sanitizedInput = sanitizeInput(dateString); + + for (const format of dateFormats) { + const parsedDate = dayjs(sanitizedInput, format, true); + if (parsedDate.isValid()) { + return parsedDate; + } + } + + for (const format of dateTimeFormats) { + const parsedDateTime = dayjs(sanitizedInput, format, true); + if (parsedDateTime.isValid()) { + return parsedDateTime; // Return the dayjs object + } + } + + return null; // If no matching format is found +};