diff --git a/client/src/Components/v2/inputs/DatePicker.tsx b/client/src/Components/v2/inputs/DatePicker.tsx index 4a852f2f1..c63c5ce08 100644 --- a/client/src/Components/v2/inputs/DatePicker.tsx +++ b/client/src/Components/v2/inputs/DatePicker.tsx @@ -3,6 +3,7 @@ import type { DatePickerProps } from "@mui/x-date-pickers/DatePicker"; import type { Dayjs } from "dayjs"; import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; import { FieldLabel } from "./FieldLabel"; import { Calendar } from "lucide-react"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; @@ -11,11 +12,15 @@ import { LocalizationProvider } from "@mui/x-date-pickers"; interface DatePickerComponentProps extends Omit, "label"> { fieldLabel?: string; required?: boolean; + error?: boolean; + helperText?: string; } export const DatePickerComponent = ({ fieldLabel, required, + error, + helperText, ...props }: DatePickerComponentProps) => { const theme = useTheme(); @@ -56,12 +61,12 @@ export const DatePickerComponent = ({ pr: theme.spacing(5), }, "& fieldset": { - borderColor: theme.palette.divider, + borderColor: error ? theme.palette.error.main : theme.palette.divider, borderRadius: theme.shape.borderRadius, }, "&:not(:has(.Mui-disabled)):not(:has(.Mui-error)) .MuiOutlinedInput-root:not(:has(input:focus)):hover fieldset": { - borderColor: theme.palette.divider, + borderColor: error ? theme.palette.error.main : theme.palette.divider, }, }, }, @@ -78,14 +83,18 @@ export const DatePickerComponent = ({ ); - if (fieldLabel) { - return ( - - {fieldLabel} - {picker} - - ); - } - - return picker; + return ( + + {fieldLabel && {fieldLabel}} + {picker} + {helperText && ( + + {helperText} + + )} + + ); }; diff --git a/client/src/Hooks/useMaintenanceWindowForm.ts b/client/src/Hooks/useMaintenanceWindowForm.ts index b1a6f86ad..1f10d55ef 100644 --- a/client/src/Hooks/useMaintenanceWindowForm.ts +++ b/client/src/Hooks/useMaintenanceWindowForm.ts @@ -3,7 +3,6 @@ import dayjs from "dayjs"; import { maintenanceWindowSchema, repeatOptions, - durationUnitOptions, type MaintenanceWindowFormData, } from "@/Validation/maintenanceWindow"; import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; @@ -17,27 +16,6 @@ const getRepeatId = (repeatMs: number): string => { return option?.id ?? "none"; }; -const getDurationAndUnit = ( - startMs: number, - endMs: number -): { duration: number; durationUnit: string } => { - const diffMs = endMs - startMs; - - for (const unit of [...durationUnitOptions].reverse()) { - if (diffMs >= unit.multiplier && diffMs % unit.multiplier === 0) { - return { - duration: diffMs / unit.multiplier, - durationUnit: unit.id, - }; - } - } - - return { - duration: diffMs / 1000, - durationUnit: "seconds", - }; -}; - export const useMaintenanceWindowForm = ({ data = null, }: UseMaintenanceWindowFormOptions = {}) => { @@ -46,19 +24,14 @@ export const useMaintenanceWindowForm = ({ if (data) { const startDate = dayjs(data.start); - const endDate = dayjs(data.end); - const { duration, durationUnit } = getDurationAndUnit( - startDate.valueOf(), - endDate.valueOf() - ); defaults = { name: data.name, repeat: getRepeatId(data.repeat), startDate: startDate.format("YYYY-MM-DD"), startTime: startDate.format("HH:mm"), - duration, - durationUnit, + duration: data.duration ?? 0, + durationUnit: data.durationUnit ?? "minutes", monitors: [data.monitorId], }; } else { diff --git a/client/src/Pages/Maintenance/create/index.tsx b/client/src/Pages/Maintenance/create/index.tsx index 7190adf32..0c247b017 100644 --- a/client/src/Pages/Maintenance/create/index.tsx +++ b/client/src/Pages/Maintenance/create/index.tsx @@ -19,25 +19,32 @@ import type { MaintenanceWindow } from "@/Types/MaintenanceWindow"; import type { MaintenanceWindowFormData } from "@/Validation/maintenanceWindow"; import { repeatOptions, durationUnitOptions } from "@/Validation/maintenanceWindow"; import { useMaintenanceWindowForm } from "@/Hooks/useMaintenanceWindowForm"; -import { useGet } from "@/Hooks/UseApi"; -import { useParams } from "react-router-dom"; +import { useGet, usePost, usePatch } from "@/Hooks/UseApi"; +import { useParams, useNavigate } from "react-router-dom"; import type { Monitor } from "@/Types/Monitor"; import { useForm, Controller } from "react-hook-form"; +import { useEffect } from "react"; import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; import { Trash2 } from "lucide-react"; const CreateMaintenanceWindowPage = () => { const theme = useTheme(); const { t } = useTranslation(); + const navigate = useNavigate(); const { maintenanceWindowId } = useParams<{ maintenanceWindowId: string }>(); const isEditMode = Boolean(maintenanceWindowId); const { data: existingMaintenanceWindow } = useGet( - isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null + isEditMode ? `/maintenance-window/${maintenanceWindowId}` : null, + {}, + { keepPreviousData: false } ); const { data: monitors } = useGet("/monitors/team"); + const { post, loading: isPosting } = usePost(); + const { patch, loading: isPatching } = usePatch(); + const { schema, defaults } = useMaintenanceWindowForm({ data: existingMaintenanceWindow, }); @@ -47,14 +54,61 @@ const CreateMaintenanceWindowPage = () => { defaultValues: defaults, }); - const { control, handleSubmit } = form; + const { control, handleSubmit, trigger } = form; + useEffect(() => { + if (existingMaintenanceWindow) { + form.reset(defaults); + } + }, [existingMaintenanceWindow, defaults, form]); - const onSubmit = (data: MaintenanceWindowFormData) => { - console.log("Form submitted with data:", data); + const buildPayload = (data: MaintenanceWindowFormData) => { + const startDateTime = dayjs(data.startDate) + .set("hour", parseInt(data.startTime.split(":")[0], 10)) + .set("minute", parseInt(data.startTime.split(":")[1], 10)); + + const durationUnit = durationUnitOptions.find((o) => o.id === data.durationUnit); + const durationInMs = data.duration * (durationUnit?.multiplier ?? 1000); + const endDateTime = startDateTime.add(durationInMs, "milliseconds"); + + const repeatOption = repeatOptions.find((o) => o.id === data.repeat); + const repeat = repeatOption?.value ?? 0; + + const payload: Record = { + name: data.name, + duration: data.duration, + durationUnit: data.durationUnit, + monitors: data.monitors, + start: startDateTime.toISOString(), + end: endDateTime.toISOString(), + repeat, + }; + + if (repeat === 0) { + payload.expiry = endDateTime.toISOString(); + } + + return payload; }; + const onSubmit = async (data: MaintenanceWindowFormData) => { + const payload = buildPayload(data); + + let result; + if (isEditMode && maintenanceWindowId) { + result = await patch(`/maintenance-window/${maintenanceWindowId}`, payload); + } else { + result = await post("/maintenance-window", payload); + } + + if (result?.success) { + navigate("/maintenance"); + } + }; + + const isLoading = isPosting || isPatching; + const onError = (errors: any) => { - console.log("Form errors:", errors); + console.error("Form submission errors:", errors); }; return ( @@ -121,7 +175,7 @@ const CreateMaintenanceWindowPage = () => { name="startDate" control={control} defaultValue={defaults.startDate} - render={({ field }) => ( + render={({ field, fieldState }) => ( { onChange={(date) => { field.onChange(date ? date.format("YYYY-MM-DD") : ""); }} + error={!!fieldState.error} + helperText={fieldState.error?.message} /> )} /> @@ -152,7 +208,7 @@ const CreateMaintenanceWindowPage = () => { )} value={field.value ? dayjs(field.value, "HH:mm") : null} onChange={(time) => { - field.onChange(time ? time.format("HH:mm") : ""); + field.onChange(time ? time.format("HH:mm") : ""); trigger("startDate"); }} /> )} @@ -241,7 +297,9 @@ const CreateMaintenanceWindowPage = () => { {...params} placeholder={ selectedMonitors.length === 0 - ? t("pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label") + ? t( + "pages.maintenanceWindow.form.startTime.monitors.option.addMonitors.label" + ) : "" } error={!!fieldState.error} @@ -269,9 +327,7 @@ const CreateMaintenanceWindowPage = () => { size="small" onClick={() => { field.onChange( - field.value.filter( - (id: string) => id !== monitor.id - ) + field.value.filter((id: string) => id !== monitor.id) ); }} aria-label="Remove monitor" @@ -294,7 +350,7 @@ const CreateMaintenanceWindowPage = () => { spacing={theme.spacing(2)} >