From c2954a13786b1c741a0dde14da5c190a0686bd23 Mon Sep 17 00:00:00 2001 From: Stefan Niclas Heun Date: Sat, 8 Feb 2025 22:16:03 +0100 Subject: [PATCH] adding input validation to application dates --- .../components/ApplicationConfigDialog.tsx | 142 +++++++++++------- .../ApplicationConfigDialogError.tsx | 12 ++ 2 files changed, 99 insertions(+), 55 deletions(-) create mode 100644 clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialogError.tsx diff --git a/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialog.tsx b/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialog.tsx index 4a6fa16e..2b6fc410 100644 --- a/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialog.tsx +++ b/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialog.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { Dialog, DialogContent, @@ -10,17 +10,18 @@ import { import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' import { Switch } from '@/components/ui/switch' -import { ApplicationMetaData } from '../../../interfaces/applicationMetaData' +import type { ApplicationMetaData } from '../../../interfaces/applicationMetaData' import { DatePicker } from '@/components/DatePicker' import { format, set, parse, formatISO } from 'date-fns' import { toZonedTime } from 'date-fns-tz' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { UpdateCoursePhase } from '@tumaet/prompt-shared-state' +import type { UpdateCoursePhase } from '@tumaet/prompt-shared-state' import { updateCoursePhase } from '@core/network/mutations/updateCoursePhase' import { useParams } from 'react-router-dom' import { DialogLoadingDisplay } from '@/components/dialog/DialogLoadingDisplay' -import { DialogErrorDisplay } from '@/components/dialog/DialogErrorDisplay' +import { AlertCircle } from 'lucide-react' import { Input } from '@/components/ui/input' +import { ApplicationConfigDialogError } from './ApplicationConfigDialogError' interface ApplicationConfigDialogProps { isOpen: boolean @@ -40,38 +41,41 @@ export function ApplicationConfigDialog({ const queryClient = useQueryClient() const { phaseId } = useParams<{ phaseId: string }>() - const [startDate, setStartDate] = useState( - initialData.applicationStartDate ? new Date(initialData.applicationStartDate) : undefined, - ) - const [endDate, setEndDate] = useState( - initialData.applicationEndDate ? new Date(initialData.applicationEndDate) : undefined, - ) - - const [startTime, setStartTime] = useState(() => { - if (initialData.applicationStartDate) { - const date = new Date(initialData.applicationStartDate) - return getTimeString(date) - } - return '00:00' - }) - - const [endTime, setEndTime] = useState(() => { - if (initialData.applicationEndDate) { - const date = new Date(initialData.applicationEndDate) - return getTimeString(date) - } - return '23:59' - }) + // States for form fields + const [startDate, setStartDate] = useState() + const [endDate, setEndDate] = useState() + const [startTime, setStartTime] = useState('00:00') + const [endTime, setEndTime] = useState('23:59') + const [externalStudentsAllowed, setExternalStudentsAllowed] = useState(false) + const [universityLoginAvailable, setUniversityLoginAvailable] = useState(false) + const [dateError, setDateError] = useState(null) const timeZone = 'Europe/Berlin' - const [externalStudentsAllowed, setExternalStudentsAllowed] = useState( - initialData.externalStudentsAllowed, - ) - - const [universityLoginAvailable, setUniversityLoginAvailable] = useState( - initialData.universityLoginAvailable, - ) + // Effect to reinitialize form values on dialog open (or when initialData changes) + useEffect(() => { + if (isOpen) { + setStartDate( + initialData.applicationStartDate ? new Date(initialData.applicationStartDate) : undefined, + ) + setEndDate( + initialData.applicationEndDate ? new Date(initialData.applicationEndDate) : undefined, + ) + setStartTime( + initialData.applicationStartDate + ? getTimeString(new Date(initialData.applicationStartDate)) + : '00:00', + ) + setEndTime( + initialData.applicationEndDate + ? getTimeString(new Date(initialData.applicationEndDate)) + : '23:59', + ) + setExternalStudentsAllowed(initialData?.externalStudentsAllowed ?? false) + setUniversityLoginAvailable(initialData?.universityLoginAvailable ?? false) + setDateError(null) + } + }, [isOpen, initialData]) const { mutate: mutatePhase, @@ -90,35 +94,45 @@ export function ApplicationConfigDialog({ const handleSubmit = (e: React.FormEvent) => { e.preventDefault() + // Build full Date objects by combining the date and time values + let startDateTime: Date | null = null + let endDateTime: Date | null = null + + if (startDate) { + const parsedStartTime = parse(startTime, 'HH:mm', new Date()) + startDateTime = set(startDate, { + hours: parsedStartTime.getHours(), + minutes: parsedStartTime.getMinutes(), + }) + } + if (endDate) { + const parsedEndTime = parse(endTime, 'HH:mm', new Date()) + endDateTime = set(endDate, { + hours: parsedEndTime.getHours(), + minutes: parsedEndTime.getMinutes(), + }) + } + + // Validate that the start date/time comes before the end date/time + if (startDateTime && endDateTime && startDateTime.getTime() >= endDateTime.getTime()) { + setDateError('Start date and time must be before end date and time.') + return + } + // Clear any previous error + setDateError(null) + const updatedPhase: UpdateCoursePhase = { id: phaseId ?? '', restrictedData: { - applicationStartDate: startDate - ? formatISO( - toZonedTime( - set(startDate, { - hours: parse(startTime, 'HH:mm', new Date()).getHours(), - minutes: parse(startTime, 'HH:mm', new Date()).getMinutes(), - }), - timeZone, - ), - ) - : undefined, - applicationEndDate: endDate - ? formatISO( - toZonedTime( - set(endDate, { - hours: parse(endTime, 'HH:mm', new Date()).getHours(), - minutes: parse(endTime, 'HH:mm', new Date()).getMinutes(), - }), - timeZone, - ), - ) + applicationStartDate: startDateTime + ? formatISO(toZonedTime(startDateTime, timeZone)) : undefined, + applicationEndDate: endDateTime ? formatISO(toZonedTime(endDateTime, timeZone)) : undefined, externalStudentsAllowed, universityLoginAvailable, }, } + mutatePhase(updatedPhase) } @@ -128,7 +142,10 @@ export function ApplicationConfigDialog({ {isPending ? ( ) : isMutateError ? ( - +
+ + +
) : ( <> @@ -138,6 +155,21 @@ export function ApplicationConfigDialog({ Note: All times are in German time (Europe/Berlin). + {/* Display validation error if present */} + {dateError && ( +
+ + Error +
+ Validation error: {dateError} +
+
+ )} +
{/* Start Date/Time */} diff --git a/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialogError.tsx b/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialogError.tsx new file mode 100644 index 00000000..f28316d0 --- /dev/null +++ b/clients/core/src/managementConsole/applicationAdministration/pages/ApplicationConfiguration/components/ApplicationConfigDialogError.tsx @@ -0,0 +1,12 @@ +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { AlertCircle } from 'lucide-react' + +export const ApplicationConfigDialogError = ({ error }: { error: Error }) => ( + + + Error + + {error.message || 'An unexpected error occurred. Please try again.'} + + +)