Skip to content

Commit

Permalink
feat: schedule form closing date (#4433)
Browse files Browse the repository at this point in the history
* Initial commit - adds dialog

* add strings

* add link text

* use strings

* add padding

* styles

* Adds some style to the time input

* Adds the dates html

* Wires up inputs

* Adds start on date validation

* Updates comments with todos

* add data attr

* add use effect

* add scheduled

* use new callback for future dates

* fix comment

* Updates closingDate to use null as closed or date as open

* Adds date check to status update

* Fixes a few hydration errors

* Adds basic client validation to date dialog

* Adds server validation for closing date

* Adds start on handling past dates

* Fixes closeForm date check

* Fixes setting a closed form to open using the toggle

* Update i18n/translations/en/form-builder.json

Co-authored-by: Anik Brazeau <[email protected]>

* Update i18n/translations/en/form-builder.json

Co-authored-by: Anik Brazeau <[email protected]>

* Update i18n/translations/fr/form-builder.json

Co-authored-by: Anik Brazeau <[email protected]>

* Adds a workaround for the toggle bug

* Adds error message for dates in the past

* try am/pm formatting

* Updates close dialog to use a month dropdown

* Updates dialog to set the date order by language

* Updates to have feature behind a feature flag

* Updates dialog to pre-populate date info

* Fixes a bug related to months

* Removes some unnecessary types

* Fixes a unit test

* Updates a few comments

* Fixes a bug

* wrap flag

* cleanup

* add note

* cleanup

---------

Co-authored-by: Tim Arney <[email protected]>
Co-authored-by: Tim Arney <[email protected]>
Co-authored-by: Anik Brazeau <[email protected]>
  • Loading branch information
4 people authored Oct 24, 2024
1 parent 5c5ec15 commit cd724b6
Show file tree
Hide file tree
Showing 20 changed files with 517 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ export const ClosedDateBanner = ({ closingDate }: { closingDate?: string | null

const isPastClosingDate = dateHasPast(Date.parse(closingDate));

const { month, day, year, hour, minute } = formClosingDateEst(closingDate);
const { month, day, year, hour, minute, dayPeriod } = formClosingDateEst(closingDate);

const closedText = t("closingDate.banner.text", {
month,
day,
year,
hour,
minute,
dayPeriod,
});

if (!isPastClosingDate) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import { getMaxMonthDay } from "@clientComponents/forms/FormattedDate/utils";
import { Button } from "@clientComponents/globals";
import { Dialog, useDialogRef } from "@formBuilder/components/shared";
import { useTranslation } from "@i18n/client";
import { useEffect, useState } from "react";
import { toast } from "@formBuilder/components/shared/Toast";
import { WarningIcon } from "@serverComponents/icons";
import { formClosingDateEst } from "@lib/utils/date/utcToEst";
import { logMessage } from "@lib/logger";

export const ClosingDateDialog = ({
showDateTimeDialog,
setShowDateTimeDialog,
save,
closingDate,
}: {
showDateTimeDialog: boolean;
setShowDateTimeDialog: React.Dispatch<React.SetStateAction<boolean>>;
save: (futureDate?: number) => Promise<void>;
closingDate: string | null | undefined;
}) => {
const {
t,
i18n: { language },
} = useTranslation("form-builder");
const dialogRef = useDialogRef();
const [hasErrors, setHasErrors] = useState(false);

const [month, setMonth] = useState<string>("");
const [day, setDay] = useState<string>("");
const [year, setYear] = useState<string>("");
const [time, setTime] = useState<string>("");

// Pre-populate the form with the closing date if it exists
useEffect(() => {
if (!closingDate) {
return;
}
try {
const { day, year, hour, minute } = formClosingDateEst(closingDate, language);
const month = (new Date(closingDate).getMonth() + 1).toString();
if (month && day && year && hour && minute) {
setMonth(month);
setDay(day);
setYear(year);
setTime(`${hour}:${minute}`);
}
} catch (error) {
logMessage.debug(`Unable to parse closing date: ${closingDate}`);
}
}, [closingDate, language]);

const handleClose = () => {
setShowDateTimeDialog(false);
dialogRef.current?.close();
};

const handleSave = () => {
try {
// Client validation is done using native HTML Form validation, the below is unlikely
if (!month || !day || !year || !time) {
throw new Error("Missing required fields");
}

const hours = Number(time.split(":")[0]);
const minutes = Number(time.split(":")[1]);
const date = new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hours),
Number(minutes)
);
const timestamp = date.getTime();

if (timestamp < Date.now()) {
setHasErrors(true);
return;
}

save(timestamp);
handleClose();
} catch (error) {
handleClose();
toast.error(t("closingDate.savedErrorMessage"));
}
};

if (!showDateTimeDialog) {
return null;
}

return (
<Dialog
title={t("scheduleClosingPage.dialog.title")}
dialogRef={dialogRef}
handleClose={handleClose}
className="max-w-[800px]"
>
<form
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
<div className="p-4">
<p className="mb-2 font-bold">{t("scheduleClosingPage.dialog.text1")}</p>
<div>
<div className="mb-4">
<fieldset role="group" aria-label="Date picker">
<legend className="mb-4">{t("scheduleClosingPage.dialog.text2")}</legend>
<div role="alert">
{hasErrors && (
<div className="mb-4 text-red-700 flex align-middle">
<WarningIcon className="fill-red-800 mr-2" />
{t("scheduleClosingPage.dialog.error.notFutureDate")}
</div>
)}
</div>
<div className="inline-flex gap-2">
{language === "en" && (
<MonthDropdown
month={month}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setMonth(e.target.value)
}
/>
)}

<div className="gcds-input-wrapper !mr-2 flex flex-col">
<label className="!mr-2 mb-2" htmlFor="date-picker-day">
{t("scheduleClosingPage.dialog.datePicker.day")}
</label>
<input
className={"!w-16 !mr-2"}
name="date-picker-day"
id="date-picker-day"
type="number"
min={1}
max={month && year ? getMaxMonthDay(Number(month), Number(year)) : 31}
onChange={(e) => setDay(e.target.value)}
value={day}
required
data-testid="date-picker-day"
/>
</div>

{language === "fr" && (
<MonthDropdown
month={month}
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
setMonth(e.target.value)
}
/>
)}

<div className="gcds-input-wrapper !mr-2 !flex !flex-col">
<label className="mb-2" htmlFor="date-picker-year">
{t("scheduleClosingPage.dialog.datePicker.year")}
</label>
<input
className={"!w-28"}
name="date-picker-year"
id="date-picker-year"
type="number"
min={new Date().getFullYear()}
onChange={(e) => setYear(e.target.value)}
value={year}
required
data-testid="date-picker-year"
/>
</div>
</div>
</fieldset>
</div>

<div className="gcds-input-wrapper !mr-2 !flex !flex-col">
<label htmlFor="time-picker" className="mb-2 font-bold">
{t("scheduleClosingPage.dialog.timePicker.text1")}
</label>
<p id="time-picker-description" className="mb-4">
{t("scheduleClosingPage.dialog.timePicker.text2")}
</p>
<input
className="!w-20"
id="time-picker"
name="time-picker"
role="time"
aria-describedby="time-picker-description"
minLength={5}
maxLength={5}
onChange={(e) => setTime(e.target.value)}
value={time}
placeholder="00:00"
required
pattern="^(?:[01][0-9]|2[0-3]):[0-5][0-9]$"
/>
</div>
</div>

<div className="mt-8 flex gap-4">
<Button theme="secondary" onClick={handleClose}>
{t("scheduleClosingPage.dialog.cancel")}
</Button>
<Button theme="primary" type="submit">
{t("scheduleClosingPage.dialog.save")}
</Button>
</div>
</div>
</form>
</Dialog>
);
};

const MonthDropdown = ({
month,
onChange,
}: {
month?: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
}) => {
const { t } = useTranslation("form-builder");
return (
<div className="gcds-select-wrapper !mr-2 flex flex-col">
<label className="mb-2" htmlFor="date-picker-month">
{t("scheduleClosingPage.dialog.datePicker.month")}
</label>
<select
name="date-picker-month"
id="date-picker-month"
className={"gc-dropdown"}
onChange={onChange}
value={month ? month : ""}
required
data-testid="date-picker-month"
>
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
<option key={month} value={month}>
{t(`formattedDate.months.${month}`)}
</option>
))}
</select>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import { isFutureDate } from "lib/utils/date/isFutureDate";
import { useTranslation } from "@i18n/client";
import { formClosingDateEst } from "lib/utils/date/utcToEst";
import { logMessage } from "@lib/logger";
import { useRehydrate } from "@lib/store/useTemplateStore";

export const ScheduledClosingDate = ({
closingDate,
language,
}: {
closingDate?: string;
language: string;
}) => {
const { t } = useTranslation("common");

const hasHydrated = useRehydrate();

if (!hasHydrated) {
return null;
}

if (!closingDate) {
return null;
}

if (!isFutureDate(closingDate)) {
return null;
}

let month, day, year, hour, minute, dayPeriod;

try {
({ month, day, year, hour, minute, dayPeriod } = formClosingDateEst(closingDate, language));
} catch (error) {
logMessage.info("Unable to parse closing date", closingDate);
return null;
}

if (!month || !day || !year || !hour || !minute || !dayPeriod) {
return null;
}

return (
<div className="mb-4">
{t("closingNotice.text1")}{" "}
{t("closingNotice.text2", {
month,
day,
year,
hour,
minute,
dayPeriod,
})}
</div>
);
};
Loading

0 comments on commit cd724b6

Please sign in to comment.