From d467aeb23b529021afa378b6a806d2b986c826d0 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 7 Jul 2026 12:06:56 +0100 Subject: [PATCH 01/15] inital commit of date picker range --- .../QuestionaryComponentRegistry.tsx | 2 + .../DateRangeAnswerRenderer.tsx | 39 ++++ .../DateRangePickerDefinition.tsx | 33 +++ .../DateRangeSearchCriteriaInput.tsx | 108 ++++++++++ .../DateRangePicker/QuestionDateRangeForm.tsx | 188 ++++++++++++++++++ .../QuestionTemplateRelationDateRangeForm.tsx | 168 ++++++++++++++++ .../QuestionaryComponentDateRangePicker.tsx | 71 +++++++ .../preSubmitDateRangeTransform.ts | 17 ++ .../src/hooks/admin/useFormattedDateTime.ts | 2 +- 9 files changed, 627 insertions(+), 1 deletion(-) create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx create mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts diff --git a/apps/frontend/src/components/questionary/QuestionaryComponentRegistry.tsx b/apps/frontend/src/components/questionary/QuestionaryComponentRegistry.tsx index 96a9b809be..94f88bfa41 100644 --- a/apps/frontend/src/components/questionary/QuestionaryComponentRegistry.tsx +++ b/apps/frontend/src/components/questionary/QuestionaryComponentRegistry.tsx @@ -18,6 +18,7 @@ import { QuestionarySubmissionState } from 'models/questionary/QuestionarySubmis import { booleanDefinition } from './questionaryComponents/Boolean/BooleanDefinition'; import { dateDefinition } from './questionaryComponents/DatePicker/DatePickerDefinition'; +import { dateRangeDefinition } from './questionaryComponents/DateRangePicker/DateRangePickerDefinition'; import { dynamicMultipleChoiceDefinition } from './questionaryComponents/DynamicMultipleChoice/DynamicMultipleChoiceDefinition'; import { embellishmentDefinition } from './questionaryComponents/Embellishment/EmbellishmentDefinition'; import { experimentSafetyReviewBasisDefinition } from './questionaryComponents/ExperimentSafetyReviewBasis/experimentSafetyReviewBasisDefinition'; @@ -163,6 +164,7 @@ export interface QuestionaryComponentDefinition { const registry = [ booleanDefinition, dateDefinition, + dateRangeDefinition, embellishmentDefinition, feedbackBasisDefinition, fileUploadDefinition, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx new file mode 100644 index 0000000000..5836724aa0 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import { AnswerRenderer } from 'components/questionary/QuestionaryComponentRegistry'; +import { + DateConfig, + FieldConfig, + Maybe, + Scalars, + SettingsId, +} from 'generated/sdk'; +import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; + +// NOTE: This is additional component because of some react warning with hooks when we use the useFormattedDateTime inside default DateAnswerRenderer component. +const DateAnswerValueRenderer = ({ + config, + value, +}: { + config: FieldConfig; + value: Maybe; +}) => { + const settingsFormatToUse = (config as DateConfig).includeTime + ? SettingsId.DATE_TIME_FORMAT + : SettingsId.DATE_FORMAT; + const { toFormattedDateTime } = useFormattedDateTime({ + settingsFormatToUse, + }); + + return {toFormattedDateTime(value)}; +}; + +const DateAnswerRenderer: AnswerRenderer = ({ config, value }) => { + if (!value) { + return Left blank; + } + + return ; +}; + +export default DateAnswerRenderer; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx new file mode 100644 index 0000000000..857664e644 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx @@ -0,0 +1,33 @@ +import TodayIcon from '@mui/icons-material/Today'; +import { dateQuestionValidationSchema } from '@user-office-software/duo-validation'; +import React from 'react'; + +import defaultRenderer from 'components/questionary/DefaultQuestionRenderer'; +import { DataType } from 'generated/sdk'; + +import DateAnswerRenderer from './DateRangeAnswerRenderer'; +import DateSearchCriteriaInput from './DateRangeSearchCriteriaInput'; +import { preSubmitDateTransform } from './preSubmitDateRangeTransform'; +import { QuestionaryComponentDatePicker } from './QuestionaryComponentDateRangePicker'; +import { QuestionDateForm } from './QuestionDateRangeForm'; +import { QuestionTemplateRelationDateForm } from './QuestionTemplateRelationDateRangeForm'; +import { QuestionaryComponentDefinition } from '../../QuestionaryComponentRegistry'; + +export const dateRangeDefinition: QuestionaryComponentDefinition = { + dataType: DataType.DATE, + name: 'Date Range', + questionaryComponent: QuestionaryComponentDatePicker, + questionForm: () => QuestionDateForm, + questionTemplateRelationForm: () => QuestionTemplateRelationDateForm, + readonly: false, + creatable: true, + icon: , + renderers: { + questionRenderer: defaultRenderer.questionRenderer, + answerRenderer: DateAnswerRenderer, + }, + createYupValidationSchema: dateQuestionValidationSchema, + getYupInitialValue: ({ answer }) => answer.value, + searchCriteriaComponent: DateSearchCriteriaInput, + preSubmitTransform: preSubmitDateTransform, +}; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx new file mode 100644 index 0000000000..217dd82bb0 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx @@ -0,0 +1,108 @@ +import FormControl from '@mui/material/FormControl'; +import Grid from '@mui/material/Grid'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import { useTheme } from '@mui/material/styles'; +import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { DateTime } from 'luxon'; +import React, { useState, useEffect } from 'react'; + +import { SearchCriteriaInputProps } from 'components/proposal/SearchCriteriaInputProps'; +import { QuestionFilterCompareOperator, SettingsId } from 'generated/sdk'; +import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; + +function DateSearchCriteriaInput({ + onChange, + searchCriteria, +}: SearchCriteriaInputProps) { + const theme = useTheme(); + const { format } = useFormattedDateTime({ + settingsFormatToUse: SettingsId.DATE_FORMAT, + }); + const [value, setValue] = useState( + searchCriteria + ? DateTime.fromISO((searchCriteria.value as string) || '') + : null + ); + const [comparator, setComparator] = useState( + searchCriteria?.compareOperator ?? QuestionFilterCompareOperator.EQUALS + ); + + useEffect(() => { + if (!searchCriteria?.value) { + setValue(null); + } + }, [searchCriteria?.value]); + + return ( + + + + + Operator + + + + + + + { + const newDate = date?.startOf('day'); + + if (newDate && newDate.isValid) { + onChange(comparator, newDate.toJSDate().toISOString()); + } + setValue(newDate || null); + }} + desktopModeMediaQuery={theme.breakpoints.up('sm')} + /> + + + + ); +} + +export default DateSearchCriteriaInput; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx new file mode 100644 index 0000000000..b315ad77d3 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx @@ -0,0 +1,188 @@ +import useTheme from '@mui/material/styles/useTheme'; +import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { Field } from 'formik'; +import React, { useContext } from 'react'; +import * as Yup from 'yup'; + +import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; +import DatePicker from 'components/common/FormikUIDatePicker'; +import DateTimePicker from 'components/common/FormikUIDateTimePicker'; +import TextField from 'components/common/FormikUITextField'; +import TitledContainer from 'components/common/TitledContainer'; +import { QuestionFormProps } from 'components/questionary/QuestionaryComponentRegistry'; +import { SettingsContext } from 'context/SettingsContextProvider'; +import { DateConfig, SettingsId } from 'generated/sdk'; +import { minMaxDateTimeCalculations } from 'utils/Time'; +import { useNaturalKeySchema } from 'utils/userFieldValidationSchema'; + +import { QuestionFormShell } from '../QuestionFormShell'; + +export const QuestionDateForm = (props: QuestionFormProps) => { + const theme = useTheme(); + const { settingsMap } = useContext(SettingsContext); + + const dateTimeFormat = settingsMap.get( + SettingsId.DATE_TIME_FORMAT + )?.settingsValue; + const dateFormat = settingsMap.get(SettingsId.DATE_FORMAT)?.settingsValue; + const field = props.question; + + const naturalKeySchema = useNaturalKeySchema(field.naturalKey); + + return ( + + {(formikProps) => { + const { minDate, maxDate, defaultDate, includeTime } = formikProps + .values.config as DateConfig; + + const { + defaultFieldMaxDate, + defaultFieldMinDate, + isDefaultAfterMaxDate, + isDefaultBeforeMinDate, + isMinAfterMaxDate, + } = minMaxDateTimeCalculations({ + minDate, + maxDate, + defaultDate, + includeTime, + }); + + if (formikProps.isValid) { + if (isDefaultBeforeMinDate) { + formikProps.setFieldError( + 'config.defaultDate', + 'Default should be after "Min" date' + ); + } + if (isDefaultAfterMaxDate) { + formikProps.setFieldError( + 'config.defaultDate', + 'Default should be before "Max" date' + ); + } + if (isMinAfterMaxDate) { + formikProps.setFieldError( + 'config.minDate', + '"Min" date should be before "Max" date' + ); + } + } + + const component = includeTime ? DateTimePicker : DatePicker; + const inputFormat = includeTime ? dateTimeFormat : dateFormat; + + return ( + <> + + + + + + + + + + + + + + + ); + }} + + ); +}; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx new file mode 100644 index 0000000000..2d7f20e9da --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx @@ -0,0 +1,168 @@ +import useTheme from '@mui/material/styles/useTheme'; +import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { Field } from 'formik'; +import React, { useContext } from 'react'; +import * as Yup from 'yup'; + +import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; +import DatePicker from 'components/common/FormikUIDatePicker'; +import DateTimePicker from 'components/common/FormikUIDateTimePicker'; +import TextField from 'components/common/FormikUITextField'; +import TitledContainer from 'components/common/TitledContainer'; +import { QuestionTemplateRelationFormProps } from 'components/questionary/QuestionaryComponentRegistry'; +import { SettingsContext } from 'context/SettingsContextProvider'; +import { DateConfig, SettingsId } from 'generated/sdk'; +import { minMaxDateTimeCalculations } from 'utils/Time'; + +import QuestionDependencyList from '../QuestionDependencyList'; +import { QuestionExcerpt } from '../QuestionExcerpt'; +import { QuestionTemplateRelationFormShell } from '../QuestionTemplateRelationFormShell'; + +export const QuestionTemplateRelationDateForm = ( + props: QuestionTemplateRelationFormProps +) => { + const theme = useTheme(); + const { settingsMap } = useContext(SettingsContext); + + const dateTimeFormat = settingsMap.get( + SettingsId.DATE_TIME_FORMAT + )?.settingsValue; + const dateFormat = settingsMap.get(SettingsId.DATE_FORMAT)?.settingsValue; + + return ( + + {(formikProps) => { + const { minDate, maxDate, defaultDate, includeTime } = formikProps + .values.config as DateConfig; + + const { + defaultFieldMaxDate, + defaultFieldMinDate, + isDefaultAfterMaxDate, + isDefaultBeforeMinDate, + isMinAfterMaxDate, + } = minMaxDateTimeCalculations({ + minDate, + maxDate, + defaultDate, + includeTime, + }); + + if (formikProps.isValid) { + if (isDefaultBeforeMinDate) { + formikProps.setFieldError( + 'config.defaultDate', + 'Default should be after "Min" date' + ); + } + if (isDefaultAfterMaxDate) { + formikProps.setFieldError( + 'config.defaultDate', + 'Default should be before "Max" date' + ); + } + if (isMinAfterMaxDate) { + formikProps.setFieldError( + 'config.minDate', + '"Min" date should be before "Max" date' + ); + } + } + + const component = includeTime ? DateTimePicker : DatePicker; + const inputFormat = includeTime ? dateTimeFormat : dateFormat; + + return ( + <> + + + + + + + + + + + + + + + + + ); + }} + + ); +}; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx new file mode 100644 index 0000000000..1bf83eefb8 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -0,0 +1,71 @@ +import FormControl from '@mui/material/FormControl'; +import useTheme from '@mui/material/styles/useTheme'; +import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { Field } from 'formik'; +import { DateTime } from 'luxon'; +import React from 'react'; + +import DatePicker from 'components/common/FormikUIDatePicker'; +import DateTimePicker from 'components/common/FormikUIDateTimePicker'; +import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; +import { DateConfig, SettingsId } from 'generated/sdk'; +import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; + +import Hint from '../Hint'; + +export function QuestionaryComponentDatePicker(props: BasicComponentProps) { + const theme = useTheme(); + const { answer, onComplete } = props; + const { + question: { id, question }, + } = answer; + const { tooltip, required, minDate, maxDate, includeTime } = + answer.config as DateConfig; + const { format } = useFormattedDateTime({ + settingsFormatToUse: includeTime + ? SettingsId.DATE_TIME_FORMAT + : SettingsId.DATE_FORMAT, + }); + + const fieldMinDate = minDate + ? DateTime.fromISO(minDate).startOf(includeTime ? 'minute' : 'day') + : null; + const fieldMaxDate = maxDate + ? DateTime.fromISO(maxDate).startOf(includeTime ? 'minute' : 'day') + : null; + + const component = includeTime ? DateTimePicker : DatePicker; + + return ( + + + + date && + onComplete( + includeTime ? date.startOf('minute') : date.startOf('day') + ) + } + textField={{ + 'data-cy': `${id}.value`, + required: required, + margin: 'none', + }} + minDate={fieldMinDate} + maxDate={fieldMaxDate} + desktopModeMediaQuery={theme.breakpoints.up('sm')} + /> + + {tooltip} + + ); +} diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts new file mode 100644 index 0000000000..dd34422746 --- /dev/null +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts @@ -0,0 +1,17 @@ +import { DateTime } from 'luxon'; + +import { QuestionaryComponentDefinition } from 'components/questionary/QuestionaryComponentRegistry'; +import { DateConfig } from 'generated/sdk'; + +export const preSubmitDateTransform: QuestionaryComponentDefinition['preSubmitTransform'] = + (answer) => { + const ifNotRequiredDateCanBeNull = + !(answer.config as DateConfig).required && answer.value === null; + + return { + ...answer, + value: ifNotRequiredDateCanBeNull + ? null + : DateTime.fromISO(answer.value, { zone: 'utc' }).toISO(), // ISO time format with utc timezone + }; + }; diff --git a/apps/frontend/src/hooks/admin/useFormattedDateTime.ts b/apps/frontend/src/hooks/admin/useFormattedDateTime.ts index 3619206cd2..e98c6f5c5e 100644 --- a/apps/frontend/src/hooks/admin/useFormattedDateTime.ts +++ b/apps/frontend/src/hooks/admin/useFormattedDateTime.ts @@ -24,7 +24,7 @@ export function useFormattedDateTime(params?: { }); if (!format) { - // IF format is not provided with the settings return some default one from luxon + // If format is not provided with the settings return some default one from luxon return dateTime.toLocaleString(DateTime.DATETIME_SHORT); } From 056322a8fc6940990a9a336c676761ab4e8beea0 Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 9 Jul 2026 11:54:15 +0100 Subject: [PATCH 02/15] add config support for new question --- .../db_patches/0214_AddDateRangePicker.sql | 13 ++++ apps/backend/src/models/Template.ts | 1 + .../src/models/questionTypes/DateRange.ts | 63 +++++++++++++++++++ .../models/questionTypes/QuestionRegistry.ts | 8 ++- .../src/resolvers/types/FieldConfig.ts | 13 ++++ .../DateRangeAnswerRenderer.tsx | 4 +- .../DateRangePickerDefinition.tsx | 10 +-- .../DateRangePicker/QuestionDateRangeForm.tsx | 56 ++++------------- .../QuestionTemplateRelationDateRangeForm.tsx | 55 ++++------------ .../QuestionaryComponentDateRangePicker.tsx | 42 ++++++++++--- .../preSubmitDateRangeTransform.ts | 4 +- .../template/fragment.fieldConfig.graphql | 10 +++ 12 files changed, 172 insertions(+), 107 deletions(-) create mode 100644 apps/backend/db_patches/0214_AddDateRangePicker.sql create mode 100644 apps/backend/src/models/questionTypes/DateRange.ts diff --git a/apps/backend/db_patches/0214_AddDateRangePicker.sql b/apps/backend/db_patches/0214_AddDateRangePicker.sql new file mode 100644 index 0000000000..fc7cfa379f --- /dev/null +++ b/apps/backend/db_patches/0214_AddDateRangePicker.sql @@ -0,0 +1,13 @@ +DO +$$ +BEGIN + IF register_patch('AddDateRangePicker.sql', 'Zachary Hankin', 'Adding date range picker type', '2026-07-07') THEN + BEGIN + + INSERT INTO question_datatypes VALUES('DATE_RANGE_PICKER'); + + END; + END IF; +END; +$$ +LANGUAGE plpgsql; \ No newline at end of file diff --git a/apps/backend/src/models/Template.ts b/apps/backend/src/models/Template.ts index ead41414ba..32937fd48d 100644 --- a/apps/backend/src/models/Template.ts +++ b/apps/backend/src/models/Template.ts @@ -17,6 +17,7 @@ export class FieldDependency { export enum DataType { BOOLEAN = 'BOOLEAN', DATE = 'DATE', + DATE_RANGE_PICKER = 'DATE_RANGE_PICKER', EMBELLISHMENT = 'EMBELLISHMENT', FILE_UPLOAD = 'FILE_UPLOAD', GENERIC_TEMPLATE = 'GENERIC_TEMPLATE', diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts new file mode 100644 index 0000000000..238fb3aac1 --- /dev/null +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -0,0 +1,63 @@ +/* eslint-disable quotes */ +import { dateQuestionValidationSchema } from '@user-office-software/duo-validation'; +import { GraphQLError } from 'graphql'; +import { DateTime } from 'luxon'; + +import { DateConfig } from '../../resolvers/types/FieldConfig'; +import { QuestionFilterCompareOperator } from '../Questionary'; +import { DataType, QuestionTemplateRelation } from '../Template'; +import { Question } from './QuestionRegistry'; + +export const dateRangeDefinition: Question = { + dataType: DataType.DATE_RANGE_PICKER, + validate: (field: QuestionTemplateRelation, value: Date | null) => { + if (field.question.dataType !== DataType.DATE) { + throw new GraphQLError('DataType should be DATE'); + } + + return dateQuestionValidationSchema(field).isValid(value); + }, + createBlankConfig: (): DateConfig => { + const config = new DateConfig(); + config.small_label = ''; + config.required = false; + config.tooltip = ''; + config.includeTime = false; + config.readPermissions = []; + + return config; + }, + getDefaultAnswer: (relation: QuestionTemplateRelation) => + (relation.config as DateConfig).defaultDate || null, + filterQuery: (queryBuilder, filter) => { + const value = JSON.parse(filter.value).value; + // NOTE: For exact comparison we need to use like interval of whole day because for now filter works only with date without time. + const valuePlusOneDay = DateTime.fromISO(value, { zone: 'utc' }) + .plus({ day: 1 }) + .toISO(); + + switch (filter.compareOperator) { + case QuestionFilterCompareOperator.EQUALS: + return queryBuilder + .andWhereRaw("(answer->>'value')::timestamp >= (?)::timestamp", value) + .andWhereRaw( + "(answer->>'value')::timestamp < (?)::timestamp", + valuePlusOneDay + ); + case QuestionFilterCompareOperator.GREATER_THAN: + return queryBuilder.andWhereRaw( + "(answer->>'value')::timestamp > (?)::timestamp", + value + ); + case QuestionFilterCompareOperator.LESS_THAN: + return queryBuilder.andWhereRaw( + "(answer->>'value')::timestamp < (?)::timestamp", + value + ); + default: + throw new GraphQLError( + `Unsupported comparator for TextInput ${filter.compareOperator}` + ); + } + }, +}; diff --git a/apps/backend/src/models/questionTypes/QuestionRegistry.ts b/apps/backend/src/models/questionTypes/QuestionRegistry.ts index 52f5a80eb1..17f7f029fe 100644 --- a/apps/backend/src/models/questionTypes/QuestionRegistry.ts +++ b/apps/backend/src/models/questionTypes/QuestionRegistry.ts @@ -29,10 +29,12 @@ import { TextInputConfig, VisitBasisConfig, ExperimentSafetyReviewBasisConfig, + DateRangeConfig, } from '../../resolvers/types/FieldConfig'; import { DataType, QuestionTemplateRelation } from '../Template'; import { booleanDefinition } from './Boolean'; import { dateDefinition } from './Date'; +import { dateRangeDefinition } from './DateRange'; import { dynamicMultipleChoiceDefinition } from './DynamicMultipleChoice'; import { embellishmentDefinition } from './Embellishment'; import { experimentSafetyReviewBasisDefinition } from './ExperimentSafetyReviewBasis'; @@ -108,8 +110,9 @@ export type QuestionDataTypeConfigMapping = ? InstrumentPickerConfig : T extends DataType.TECHNIQUE_PICKER ? TechniquePickerConfig - : never; - + : T extends DataType.DATE_RANGE_PICKER + ? DateRangeConfig + : never; export interface Question { /** * The enum value from DataType @@ -172,6 +175,7 @@ export interface Question { const registry = [ booleanDefinition, dateDefinition, + dateRangeDefinition, embellishmentDefinition, feedbackBasisDefinition, fileUploadDefinition, diff --git a/apps/backend/src/resolvers/types/FieldConfig.ts b/apps/backend/src/resolvers/types/FieldConfig.ts index 044b609d38..ff7fab5013 100644 --- a/apps/backend/src/resolvers/types/FieldConfig.ts +++ b/apps/backend/src/resolvers/types/FieldConfig.ts @@ -94,6 +94,18 @@ export class DateConfig extends ConfigBase { includeTime: boolean; } +@ObjectType() +export class DateRangeConfig extends ConfigBase { + @Field(() => String, { nullable: true }) + minDate: string | null; + + @Field(() => String, { nullable: true }) + maxDate: string | null; + + @Field(() => Boolean) + includeTime: boolean; +} + @ObjectType() export class EmbellishmentConfig { @Field(() => Boolean) @@ -386,6 +398,7 @@ export const FieldConfigType = createUnionType({ types: () => [ BooleanConfig, DateConfig, + DateRangeConfig, EmbellishmentConfig, FileUploadConfig, SelectionFromOptionsConfig, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx index 5836724aa0..840bdab5c3 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { AnswerRenderer } from 'components/questionary/QuestionaryComponentRegistry'; import { - DateConfig, + DateRangeConfig, FieldConfig, Maybe, Scalars, @@ -18,7 +18,7 @@ const DateAnswerValueRenderer = ({ config: FieldConfig; value: Maybe; }) => { - const settingsFormatToUse = (config as DateConfig).includeTime + const settingsFormatToUse = (config as DateRangeConfig).includeTime ? SettingsId.DATE_TIME_FORMAT : SettingsId.DATE_FORMAT; const { toFormattedDateTime } = useFormattedDateTime({ diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx index 857664e644..20d68e5453 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx @@ -5,18 +5,18 @@ import React from 'react'; import defaultRenderer from 'components/questionary/DefaultQuestionRenderer'; import { DataType } from 'generated/sdk'; -import DateAnswerRenderer from './DateRangeAnswerRenderer'; +import DateRangeAnswerRenderer from './DateRangeAnswerRenderer'; import DateSearchCriteriaInput from './DateRangeSearchCriteriaInput'; import { preSubmitDateTransform } from './preSubmitDateRangeTransform'; -import { QuestionaryComponentDatePicker } from './QuestionaryComponentDateRangePicker'; +import { QuestionaryComponentDateRangePicker } from './QuestionaryComponentDateRangePicker'; import { QuestionDateForm } from './QuestionDateRangeForm'; import { QuestionTemplateRelationDateForm } from './QuestionTemplateRelationDateRangeForm'; import { QuestionaryComponentDefinition } from '../../QuestionaryComponentRegistry'; export const dateRangeDefinition: QuestionaryComponentDefinition = { - dataType: DataType.DATE, + dataType: DataType.DATE_RANGE_PICKER, name: 'Date Range', - questionaryComponent: QuestionaryComponentDatePicker, + questionaryComponent: QuestionaryComponentDateRangePicker, questionForm: () => QuestionDateForm, questionTemplateRelationForm: () => QuestionTemplateRelationDateForm, readonly: false, @@ -24,7 +24,7 @@ export const dateRangeDefinition: QuestionaryComponentDefinition = { icon: , renderers: { questionRenderer: defaultRenderer.questionRenderer, - answerRenderer: DateAnswerRenderer, + answerRenderer: DateRangeAnswerRenderer, }, createYupValidationSchema: dateQuestionValidationSchema, getYupInitialValue: ({ answer }) => answer.value, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx index b315ad77d3..a5ad919a2d 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx @@ -12,12 +12,13 @@ import TextField from 'components/common/FormikUITextField'; import TitledContainer from 'components/common/TitledContainer'; import { QuestionFormProps } from 'components/questionary/QuestionaryComponentRegistry'; import { SettingsContext } from 'context/SettingsContextProvider'; -import { DateConfig, SettingsId } from 'generated/sdk'; +import { DateRangeConfig, SettingsId } from 'generated/sdk'; import { minMaxDateTimeCalculations } from 'utils/Time'; import { useNaturalKeySchema } from 'utils/userFieldValidationSchema'; import { QuestionFormShell } from '../QuestionFormShell'; +//This defines what the UI shows when adding this question parent template export const QuestionDateForm = (props: QuestionFormProps) => { const theme = useTheme(); const { settingsMap } = useContext(SettingsContext); @@ -39,35 +40,18 @@ export const QuestionDateForm = (props: QuestionFormProps) => { })} > {(formikProps) => { - const { minDate, maxDate, defaultDate, includeTime } = formikProps - .values.config as DateConfig; - - const { - defaultFieldMaxDate, - defaultFieldMinDate, - isDefaultAfterMaxDate, - isDefaultBeforeMinDate, - isMinAfterMaxDate, - } = minMaxDateTimeCalculations({ - minDate, - maxDate, - defaultDate, - includeTime, - }); + const { minDate, maxDate, includeTime } = formikProps.values + .config as DateRangeConfig; + const defaultDate = null; + const { defaultFieldMaxDate, defaultFieldMinDate, isMinAfterMaxDate } = + minMaxDateTimeCalculations({ + minDate, + maxDate, + defaultDate, + includeTime, + }); if (formikProps.isValid) { - if (isDefaultBeforeMinDate) { - formikProps.setFieldError( - 'config.defaultDate', - 'Default should be after "Min" date' - ); - } - if (isDefaultAfterMaxDate) { - formikProps.setFieldError( - 'config.defaultDate', - 'Default should be before "Max" date' - ); - } if (isMinAfterMaxDate) { formikProps.setFieldError( 'config.minDate', @@ -162,22 +146,6 @@ export const QuestionDateForm = (props: QuestionFormProps) => { }} desktopModeMediaQuery={theme.breakpoints.up('sm')} /> - diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx index 2d7f20e9da..93ed83dd4c 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx @@ -12,13 +12,14 @@ import TextField from 'components/common/FormikUITextField'; import TitledContainer from 'components/common/TitledContainer'; import { QuestionTemplateRelationFormProps } from 'components/questionary/QuestionaryComponentRegistry'; import { SettingsContext } from 'context/SettingsContextProvider'; -import { DateConfig, SettingsId } from 'generated/sdk'; +import { DateRangeConfig, SettingsId } from 'generated/sdk'; import { minMaxDateTimeCalculations } from 'utils/Time'; import QuestionDependencyList from '../QuestionDependencyList'; import { QuestionExcerpt } from '../QuestionExcerpt'; import { QuestionTemplateRelationFormShell } from '../QuestionTemplateRelationFormShell'; +//This defines what the user looks at when they create this question. It is the instanciation (child) export const QuestionTemplateRelationDateForm = ( props: QuestionTemplateRelationFormProps ) => { @@ -36,35 +37,18 @@ export const QuestionTemplateRelationDateForm = ( validationSchema={Yup.object().shape({})} > {(formikProps) => { - const { minDate, maxDate, defaultDate, includeTime } = formikProps - .values.config as DateConfig; - - const { - defaultFieldMaxDate, - defaultFieldMinDate, - isDefaultAfterMaxDate, - isDefaultBeforeMinDate, - isMinAfterMaxDate, - } = minMaxDateTimeCalculations({ - minDate, - maxDate, - defaultDate, - includeTime, - }); + const { minDate, maxDate, includeTime } = formikProps.values + .config as DateRangeConfig; + const defaultDate = null; + const { defaultFieldMaxDate, defaultFieldMinDate, isMinAfterMaxDate } = + minMaxDateTimeCalculations({ + minDate, + maxDate, + defaultDate, + includeTime, + }); if (formikProps.isValid) { - if (isDefaultBeforeMinDate) { - formikProps.setFieldError( - 'config.defaultDate', - 'Default should be after "Min" date' - ); - } - if (isDefaultAfterMaxDate) { - formikProps.setFieldError( - 'config.defaultDate', - 'Default should be before "Max" date' - ); - } if (isMinAfterMaxDate) { formikProps.setFieldError( 'config.minDate', @@ -137,21 +121,6 @@ export const QuestionTemplateRelationDateForm = ( }} desktopModeMediaQuery={theme.breakpoints.up('sm')} /> - diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index 1bf83eefb8..a458ba9de1 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -9,19 +9,20 @@ import React from 'react'; import DatePicker from 'components/common/FormikUIDatePicker'; import DateTimePicker from 'components/common/FormikUIDateTimePicker'; import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; -import { DateConfig, SettingsId } from 'generated/sdk'; +import { DateRangeConfig, SettingsId } from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; -import Hint from '../Hint'; - -export function QuestionaryComponentDatePicker(props: BasicComponentProps) { +//This is what appears to the user when filling in this question type +export function QuestionaryComponentDateRangePicker( + props: BasicComponentProps +) { const theme = useTheme(); const { answer, onComplete } = props; const { question: { id, question }, } = answer; - const { tooltip, required, minDate, maxDate, includeTime } = - answer.config as DateConfig; + const { required, minDate, maxDate, includeTime } = + answer.config as DateRangeConfig; const { format } = useFormattedDateTime({ settingsFormatToUse: includeTime ? SettingsId.DATE_TIME_FORMAT @@ -42,8 +43,32 @@ export function QuestionaryComponentDatePicker(props: BasicComponentProps) { + date && + onComplete( + includeTime ? date.startOf('minute') : date.startOf('day') + ) + } + textField={{ + 'data-cy': `${id}.value`, + required: required, + margin: 'none', + }} + minDate={fieldMinDate} + maxDate={fieldMaxDate} + desktopModeMediaQuery={theme.breakpoints.up('sm')} + /> + - {tooltip} ); } diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts index dd34422746..2742331406 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts @@ -1,12 +1,12 @@ import { DateTime } from 'luxon'; import { QuestionaryComponentDefinition } from 'components/questionary/QuestionaryComponentRegistry'; -import { DateConfig } from 'generated/sdk'; +import { DateRangeConfig } from 'generated/sdk'; export const preSubmitDateTransform: QuestionaryComponentDefinition['preSubmitTransform'] = (answer) => { const ifNotRequiredDateCanBeNull = - !(answer.config as DateConfig).required && answer.value === null; + !(answer.config as DateRangeConfig).required && answer.value === null; return { ...answer, diff --git a/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql b/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql index 4138ab9be0..7870b5acca 100644 --- a/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql +++ b/apps/frontend/src/graphql/template/fragment.fieldConfig.graphql @@ -15,6 +15,16 @@ fragment fieldConfig on FieldConfig { defaultDate includeTime } + + ... on DateRangeConfig { + small_label + required + tooltip + readPermissions + minDate + maxDate + includeTime + } ... on EmbellishmentConfig { html plain From 09ffba8331314dd3b3355afc94cfa9df668b04f3 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 14 Jul 2026 12:59:44 +0100 Subject: [PATCH 03/15] working save --- apps/backend/src/resolvers/CustomScalars.ts | 1 + .../mutations/AnswerTopicMutation.ts | 2 + .../questionary/QuestionaryStepView.tsx | 3 + .../DateRangePickerDefinition.tsx | 43 +++++- .../QuestionaryComponentDateRangePicker.tsx | 133 ++++++++---------- validation/src/Questionary/date.ts | 2 +- validation/src/Questionary/dateRange.ts | 23 +++ 7 files changed, 130 insertions(+), 77 deletions(-) create mode 100644 validation/src/Questionary/dateRange.ts diff --git a/apps/backend/src/resolvers/CustomScalars.ts b/apps/backend/src/resolvers/CustomScalars.ts index 7c6e506273..74334f9805 100644 --- a/apps/backend/src/resolvers/CustomScalars.ts +++ b/apps/backend/src/resolvers/CustomScalars.ts @@ -3,6 +3,7 @@ import { GraphQLScalarType, Kind } from 'graphql'; export type AnswerType = number | string | Date | boolean | number[] | unknown; const coerce = (value: AnswerType) => { + //console.log(`Coercing: ${JSON.stringify(value)}`); if (typeof value === 'number' && Number.isInteger(value)) { return value; } diff --git a/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts b/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts index fb2d6ac0e3..c47842c498 100644 --- a/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts +++ b/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts @@ -44,6 +44,8 @@ export class UpdateQuestionaryMutation { args: AnswerTopicArgs, @Ctx() context: ResolverContext ) { + //console.log(`backend answerTopic called with ${JSON.stringify(args)}`); + return context.mutations.questionary.answerTopic(context.user, args); } } diff --git a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx index 42f443e705..a2e21a7f0f 100644 --- a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx +++ b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx @@ -340,6 +340,9 @@ function QuestionaryStepView(props: { answer: field, formikProps, onComplete: (newValue: Answer['value']) => { + console.log( + `Executing onComplete with ${JSON.stringify(newValue)}` + ); if (field.value !== newValue) { dispatch({ type: 'FIELD_CHANGED', diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx index 20d68e5453..0efbf8f203 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx @@ -1,5 +1,6 @@ import TodayIcon from '@mui/icons-material/Today'; -import { dateQuestionValidationSchema } from '@user-office-software/duo-validation'; +//import { dateRangeQuestionValidationSchema } from '@user-office-software/duo-validation'; +import { DateTime } from 'luxon'; import React from 'react'; import defaultRenderer from 'components/questionary/DefaultQuestionRenderer'; @@ -13,6 +14,44 @@ import { QuestionDateForm } from './QuestionDateRangeForm'; import { QuestionTemplateRelationDateForm } from './QuestionTemplateRelationDateRangeForm'; import { QuestionaryComponentDefinition } from '../../QuestionaryComponentRegistry'; +export function normalizeDate(date: string, includeTime: boolean) { + let normalizedDate = DateTime.fromISO(date); + + if (includeTime) { + normalizedDate = normalizedDate.startOf('minute'); + } else { + normalizedDate = normalizedDate.startOf('day'); + } + + return normalizedDate.toJSDate(); +} + +// const dateRangeQuestionValidationSchema = (field: any) => { +// console.log('running validation now on: '); +// const schema = Yup.array().of( +// Yup.object({ +// from: Yup.date() +// .required() +// .transform(function (value: Date) { +// return value && this.isType(value) +// ? normalizeDate(value.toISOString(), false) +// : value; +// }) +// .typeError('Invalid date'), +// to: Yup.date() +// .required() +// .transform(function (value: Date) { +// return value && this.isType(value) +// ? normalizeDate(value.toISOString(), false) +// : value; +// }) +// .typeError('Invalid date'), +// }) +// ); + +// return schema; +// }; + export const dateRangeDefinition: QuestionaryComponentDefinition = { dataType: DataType.DATE_RANGE_PICKER, name: 'Date Range', @@ -26,7 +65,7 @@ export const dateRangeDefinition: QuestionaryComponentDefinition = { questionRenderer: defaultRenderer.questionRenderer, answerRenderer: DateRangeAnswerRenderer, }, - createYupValidationSchema: dateQuestionValidationSchema, + createYupValidationSchema: null, getYupInitialValue: ({ answer }) => answer.value, searchCriteriaComponent: DateSearchCriteriaInput, preSubmitTransform: preSubmitDateTransform, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index a458ba9de1..98c0e02805 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -1,95 +1,80 @@ import FormControl from '@mui/material/FormControl'; -import useTheme from '@mui/material/styles/useTheme'; -import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { Field } from 'formik'; +import { styled } from '@mui/material/styles'; import { DateTime } from 'luxon'; -import React from 'react'; +import React, { useState } from 'react'; +import { DayPicker, DateRange } from 'react-day-picker'; -import DatePicker from 'components/common/FormikUIDatePicker'; -import DateTimePicker from 'components/common/FormikUIDateTimePicker'; import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; -import { DateRangeConfig, SettingsId } from 'generated/sdk'; -import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; + +const StyledPickerWrapper = styled('div')(({ theme }) => ({ + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + '.rdp-range_start': { + background: `linear-gradient(90deg, transparent 50%, ${theme.palette.primary.main} 50%)`, + border: 'none', + }, + '.rdp-range_end': { + background: `linear-gradient(90deg, ${theme.palette.primary.main} 50%, transparent 50%)`, + border: 'none', + }, + '.rdp-range_start .rdp-day_button, .rdp-range_end .rdp-day_button': { + backgroundColor: theme.palette.primary.main, + border: 'none', + }, + '.rdp-range_middle .rdp-day_button': { + backgroundColor: theme.palette.primary.main, + borderColor: theme.palette.primary.main, + color: 'white', + }, + '.rdp-day': { + padding: 0, + width: '100%', + height: '100%', + }, + '.rdp-nav svg': { + fill: theme.palette.primary.dark, + }, +})); //This is what appears to the user when filling in this question type export function QuestionaryComponentDateRangePicker( props: BasicComponentProps ) { - const theme = useTheme(); const { answer, onComplete } = props; const { - question: { id, question }, + question: { id }, } = answer; - const { required, minDate, maxDate, includeTime } = - answer.config as DateRangeConfig; - const { format } = useFormattedDateTime({ - settingsFormatToUse: includeTime - ? SettingsId.DATE_TIME_FORMAT - : SettingsId.DATE_FORMAT, - }); - - const fieldMinDate = minDate - ? DateTime.fromISO(minDate).startOf(includeTime ? 'minute' : 'day') - : null; - const fieldMaxDate = maxDate - ? DateTime.fromISO(maxDate).startOf(includeTime ? 'minute' : 'day') - : null; - const component = includeTime ? DateTimePicker : DatePicker; + const [range, setRange] = useState(); return ( - - - date && - onComplete( - includeTime ? date.startOf('minute') : date.startOf('day') - ) - } - textField={{ - 'data-cy': `${id}.value`, - required: required, - margin: 'none', - }} - minDate={fieldMinDate} - maxDate={fieldMaxDate} - desktopModeMediaQuery={theme.breakpoints.up('sm')} - /> - - date && - onComplete( - includeTime ? date.startOf('minute') : date.startOf('day') - ) - } - textField={{ - 'data-cy': `${id}.value`, - required: required, - margin: 'none', + + { + setRange(value); + if (value?.from && value?.to) { + //const newAnswer = `${DateTime.fromJSDate(new Date('2001-03-11'))}-${DateTime.fromJSDate(new Date('2001-03-11'))}` + + // const newValue1 = { + // from: DateTime.fromJSDate(new Date('2001-03-11')), + // to:DateTime.fromJSDate(new Date('2001-03-11')) + // } + // onComplete([newValue]); + + // const newValue = { + // from: new Date('2001-03-11'), + // to: new Date('2001-03-11'), + // }; + onComplete(DateTime.fromJSDate(new Date('2001-03-11'))); + } }} - minDate={fieldMinDate} - maxDate={fieldMaxDate} - desktopModeMediaQuery={theme.breakpoints.up('sm')} /> - + ); } diff --git a/validation/src/Questionary/date.ts b/validation/src/Questionary/date.ts index f5cc64a988..ef1fa94d92 100644 --- a/validation/src/Questionary/date.ts +++ b/validation/src/Questionary/date.ts @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import * as Yup from 'yup'; -function normalizeDate(date: string, includeTime: boolean) { +export function normalizeDate(date: string, includeTime: boolean) { let normalizedDate = DateTime.fromISO(date); if (includeTime) { diff --git a/validation/src/Questionary/dateRange.ts b/validation/src/Questionary/dateRange.ts new file mode 100644 index 0000000000..72bd1d20fa --- /dev/null +++ b/validation/src/Questionary/dateRange.ts @@ -0,0 +1,23 @@ +import * as Yup from 'yup'; +import { normalizeDate } from './date'; + +export const dateRangeQuestionValidationSchema = (field: any) => { + const schema = Yup.array().of( + Yup.object({ + from: Yup.date().required().transform(function (value: Date) { + return value && this.isType(value) + ? normalizeDate(value.toISOString(), false) + : value; + }) + .typeError('Invalid date'), + to: Yup.date().required().transform(function (value: Date) { + return value && this.isType(value) + ? normalizeDate(value.toISOString(), false) + : value; + }) + .typeError('Invalid date') + }) + ); + + return schema; +}; \ No newline at end of file From 0d00799492c3be11598cbe558860ac90d4f8c6c6 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 14 Jul 2026 13:26:43 +0100 Subject: [PATCH 04/15] run dateRange fix --- .../DateRangePickerDefinition.tsx | 2 -- .../QuestionaryComponentDateRangePicker.tsx | 15 +-------------- .../preSubmitDateRangeTransform.ts | 17 ----------------- 3 files changed, 1 insertion(+), 33 deletions(-) delete mode 100644 apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx index 0efbf8f203..a49b9c14c7 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx @@ -8,7 +8,6 @@ import { DataType } from 'generated/sdk'; import DateRangeAnswerRenderer from './DateRangeAnswerRenderer'; import DateSearchCriteriaInput from './DateRangeSearchCriteriaInput'; -import { preSubmitDateTransform } from './preSubmitDateRangeTransform'; import { QuestionaryComponentDateRangePicker } from './QuestionaryComponentDateRangePicker'; import { QuestionDateForm } from './QuestionDateRangeForm'; import { QuestionTemplateRelationDateForm } from './QuestionTemplateRelationDateRangeForm'; @@ -68,5 +67,4 @@ export const dateRangeDefinition: QuestionaryComponentDefinition = { createYupValidationSchema: null, getYupInitialValue: ({ answer }) => answer.value, searchCriteriaComponent: DateSearchCriteriaInput, - preSubmitTransform: preSubmitDateTransform, }; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index 98c0e02805..bb5461ca23 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -1,6 +1,5 @@ import FormControl from '@mui/material/FormControl'; import { styled } from '@mui/material/styles'; -import { DateTime } from 'luxon'; import React, { useState } from 'react'; import { DayPicker, DateRange } from 'react-day-picker'; @@ -58,19 +57,7 @@ export function QuestionaryComponentDateRangePicker( onSelect={(value) => { setRange(value); if (value?.from && value?.to) { - //const newAnswer = `${DateTime.fromJSDate(new Date('2001-03-11'))}-${DateTime.fromJSDate(new Date('2001-03-11'))}` - - // const newValue1 = { - // from: DateTime.fromJSDate(new Date('2001-03-11')), - // to:DateTime.fromJSDate(new Date('2001-03-11')) - // } - // onComplete([newValue]); - - // const newValue = { - // from: new Date('2001-03-11'), - // to: new Date('2001-03-11'), - // }; - onComplete(DateTime.fromJSDate(new Date('2001-03-11'))); + onComplete([range]); } }} /> diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts deleted file mode 100644 index 2742331406..0000000000 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/preSubmitDateRangeTransform.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DateTime } from 'luxon'; - -import { QuestionaryComponentDefinition } from 'components/questionary/QuestionaryComponentRegistry'; -import { DateRangeConfig } from 'generated/sdk'; - -export const preSubmitDateTransform: QuestionaryComponentDefinition['preSubmitTransform'] = - (answer) => { - const ifNotRequiredDateCanBeNull = - !(answer.config as DateRangeConfig).required && answer.value === null; - - return { - ...answer, - value: ifNotRequiredDateCanBeNull - ? null - : DateTime.fromISO(answer.value, { zone: 'utc' }).toISO(), // ISO time format with utc timezone - }; - }; From 78d0f6f8825efaf35e790b89f35dc0c10c277fab Mon Sep 17 00:00:00 2001 From: zachary Date: Mon, 20 Jul 2026 12:50:03 +0100 Subject: [PATCH 05/15] UI fix --- .../src/models/questionTypes/DateRange.ts | 38 +++----- .../src/resolvers/types/FieldConfig.ts | 13 ++- .../QuestionaryComponentDateRangePicker.tsx | 86 +++++++++++++++---- 3 files changed, 85 insertions(+), 52 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 238fb3aac1..08e5d77eb0 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -1,9 +1,8 @@ /* eslint-disable quotes */ import { dateQuestionValidationSchema } from '@user-office-software/duo-validation'; import { GraphQLError } from 'graphql'; -import { DateTime } from 'luxon'; -import { DateConfig } from '../../resolvers/types/FieldConfig'; +import { DateRangeConfig } from '../../resolvers/types/FieldConfig'; import { QuestionFilterCompareOperator } from '../Questionary'; import { DataType, QuestionTemplateRelation } from '../Template'; import { Question } from './QuestionRegistry'; @@ -11,49 +10,32 @@ import { Question } from './QuestionRegistry'; export const dateRangeDefinition: Question = { dataType: DataType.DATE_RANGE_PICKER, validate: (field: QuestionTemplateRelation, value: Date | null) => { - if (field.question.dataType !== DataType.DATE) { - throw new GraphQLError('DataType should be DATE'); + if (field.question.dataType !== DataType.DATE_RANGE_PICKER) { + throw new GraphQLError('DataType should be list of DATE RANGE'); } return dateQuestionValidationSchema(field).isValid(value); }, - createBlankConfig: (): DateConfig => { - const config = new DateConfig(); + createBlankConfig: (): DateRangeConfig => { + const config = new DateRangeConfig(); config.small_label = ''; config.required = false; config.tooltip = ''; - config.includeTime = false; config.readPermissions = []; + config.defaultDate = null; return config; }, getDefaultAnswer: (relation: QuestionTemplateRelation) => - (relation.config as DateConfig).defaultDate || null, + (relation.config as DateRangeConfig).defaultDate || null, filterQuery: (queryBuilder, filter) => { - const value = JSON.parse(filter.value).value; - // NOTE: For exact comparison we need to use like interval of whole day because for now filter works only with date without time. - const valuePlusOneDay = DateTime.fromISO(value, { zone: 'utc' }) - .plus({ day: 1 }) - .toISO(); - switch (filter.compareOperator) { case QuestionFilterCompareOperator.EQUALS: - return queryBuilder - .andWhereRaw("(answer->>'value')::timestamp >= (?)::timestamp", value) - .andWhereRaw( - "(answer->>'value')::timestamp < (?)::timestamp", - valuePlusOneDay - ); + return true; case QuestionFilterCompareOperator.GREATER_THAN: - return queryBuilder.andWhereRaw( - "(answer->>'value')::timestamp > (?)::timestamp", - value - ); + return true; case QuestionFilterCompareOperator.LESS_THAN: - return queryBuilder.andWhereRaw( - "(answer->>'value')::timestamp < (?)::timestamp", - value - ); + return true; default: throw new GraphQLError( `Unsupported comparator for TextInput ${filter.compareOperator}` diff --git a/apps/backend/src/resolvers/types/FieldConfig.ts b/apps/backend/src/resolvers/types/FieldConfig.ts index 7c35d4c38c..73e53d6069 100644 --- a/apps/backend/src/resolvers/types/FieldConfig.ts +++ b/apps/backend/src/resolvers/types/FieldConfig.ts @@ -97,13 +97,12 @@ export class DateConfig extends ConfigBase { @ObjectType() export class DateRangeConfig extends ConfigBase { @Field(() => String, { nullable: true }) - minDate: string | null; - - @Field(() => String, { nullable: true }) - maxDate: string | null; - - @Field(() => Boolean) - includeTime: boolean; + defaultDate: + | { + from: Date | undefined; + to?: Date | undefined; + }[] + | null; } @ObjectType() diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index bb5461ca23..8b9c25ba8f 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -7,31 +7,83 @@ import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; const StyledPickerWrapper = styled('div')(({ theme }) => ({ padding: theme.spacing(2), - borderRadius: theme.shape.borderRadius, - '.rdp-range_start': { - background: `linear-gradient(90deg, transparent 50%, ${theme.palette.primary.main} 50%)`, - border: 'none', + borderRadius: theme.shape.borderRadius * 2, + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[2], + + '.rdp-month_caption': { + fontSize: '1.5rem', + fontWeight: 600, + marginBottom: theme.spacing(1), }, - '.rdp-range_end': { - background: `linear-gradient(90deg, ${theme.palette.primary.main} 50%, transparent 50%)`, - border: 'none', + + '.rdp-weekday': { + fontWeight: 600, }, - '.rdp-range_start .rdp-day_button, .rdp-range_end .rdp-day_button': { - backgroundColor: theme.palette.primary.main, + + '.rdp-day': { + padding: 0, + }, + + '.rdp-day_button': { + width: 40, + height: 40, + borderRadius: '50%', border: 'none', + transition: 'all 0.15s ease', + }, + + '.rdp-day_button:hover': { + backgroundColor: theme.palette.action.hover, + }, + + // Range background + '.rdp-range_middle': { + backgroundColor: theme.palette.primary.light, }, + '.rdp-range_middle .rdp-day_button': { + backgroundColor: 'transparent', + color: theme.palette.primary.contrastText, + borderRadius: 0, + }, + + // Start of range + '.rdp-range_start': { + background: `linear-gradient( + 90deg, + transparent 50%, + ${theme.palette.primary.light} 50% + )`, + }, + + '.rdp-range_start .rdp-day_button': { backgroundColor: theme.palette.primary.main, - borderColor: theme.palette.primary.main, - color: 'white', + color: theme.palette.primary.contrastText, + borderRadius: '50%', }, - '.rdp-day': { - padding: 0, - width: '100%', - height: '100%', + + // End of range + '.rdp-range_end': { + background: `linear-gradient( + 90deg, + ${theme.palette.primary.light} 50%, + transparent 50% + )`, + }, + + '.rdp-range_end .rdp-day_button': { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + borderRadius: '50%', + }, + + '.rdp-nav_button': { + borderRadius: '50%', }, + '.rdp-nav svg': { - fill: theme.palette.primary.dark, + fill: theme.palette.primary.main, }, })); @@ -57,7 +109,7 @@ export function QuestionaryComponentDateRangePicker( onSelect={(value) => { setRange(value); if (value?.from && value?.to) { - onComplete([range]); + onComplete([value]); } }} /> From 9d9b04af26aea98fcad1793e9c5b5ee4125f9695 Mon Sep 17 00:00:00 2001 From: zachary Date: Mon, 20 Jul 2026 15:29:29 +0100 Subject: [PATCH 06/15] working v1 --- .../src/models/questionTypes/DateRange.ts | 2 +- .../src/resolvers/types/FieldConfig.ts | 3 + .../DateRangePicker/QuestionDateRangeForm.tsx | 84 +------------------ .../QuestionTemplateRelationDateRangeForm.tsx | 72 +--------------- .../template/fragment.fieldConfig.graphql | 2 - 5 files changed, 7 insertions(+), 156 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 08e5d77eb0..5767dc7a21 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -22,7 +22,7 @@ export const dateRangeDefinition: Question = { config.required = false; config.tooltip = ''; config.readPermissions = []; - config.defaultDate = null; + config.includeTime = false; return config; }, diff --git a/apps/backend/src/resolvers/types/FieldConfig.ts b/apps/backend/src/resolvers/types/FieldConfig.ts index 73e53d6069..74e4060496 100644 --- a/apps/backend/src/resolvers/types/FieldConfig.ts +++ b/apps/backend/src/resolvers/types/FieldConfig.ts @@ -103,6 +103,9 @@ export class DateRangeConfig extends ConfigBase { to?: Date | undefined; }[] | null; + + @Field(() => Boolean) + includeTime: boolean; } @ObjectType() diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx index a5ad919a2d..586924b63e 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx @@ -1,32 +1,17 @@ -import useTheme from '@mui/material/styles/useTheme'; -import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { Field } from 'formik'; -import React, { useContext } from 'react'; +import React from 'react'; import * as Yup from 'yup'; import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; -import DatePicker from 'components/common/FormikUIDatePicker'; -import DateTimePicker from 'components/common/FormikUIDateTimePicker'; import TextField from 'components/common/FormikUITextField'; import TitledContainer from 'components/common/TitledContainer'; import { QuestionFormProps } from 'components/questionary/QuestionaryComponentRegistry'; -import { SettingsContext } from 'context/SettingsContextProvider'; -import { DateRangeConfig, SettingsId } from 'generated/sdk'; -import { minMaxDateTimeCalculations } from 'utils/Time'; import { useNaturalKeySchema } from 'utils/userFieldValidationSchema'; import { QuestionFormShell } from '../QuestionFormShell'; //This defines what the UI shows when adding this question parent template export const QuestionDateForm = (props: QuestionFormProps) => { - const theme = useTheme(); - const { settingsMap } = useContext(SettingsContext); - - const dateTimeFormat = settingsMap.get( - SettingsId.DATE_TIME_FORMAT - )?.settingsValue; - const dateFormat = settingsMap.get(SettingsId.DATE_FORMAT)?.settingsValue; const field = props.question; const naturalKeySchema = useNaturalKeySchema(field.naturalKey); @@ -39,30 +24,7 @@ export const QuestionDateForm = (props: QuestionFormProps) => { question: Yup.string().required('Question is required'), })} > - {(formikProps) => { - const { minDate, maxDate, includeTime } = formikProps.values - .config as DateRangeConfig; - const defaultDate = null; - const { defaultFieldMaxDate, defaultFieldMinDate, isMinAfterMaxDate } = - minMaxDateTimeCalculations({ - minDate, - maxDate, - defaultDate, - includeTime, - }); - - if (formikProps.isValid) { - if (isMinAfterMaxDate) { - formikProps.setFieldError( - 'config.minDate', - '"Min" date should be before "Max" date' - ); - } - } - - const component = includeTime ? DateTimePicker : DatePicker; - const inputFormat = includeTime ? dateTimeFormat : dateFormat; - + {() => { return ( <> { }} inputProps={{ 'data-cy': 'includeTime' }} /> - { }} data-cy="required" /> - - - - ); diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx index 93ed83dd4c..38f868a564 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionTemplateRelationDateRangeForm.tsx @@ -1,19 +1,11 @@ -import useTheme from '@mui/material/styles/useTheme'; -import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { Field } from 'formik'; -import React, { useContext } from 'react'; +import React from 'react'; import * as Yup from 'yup'; import CheckboxWithLabel from 'components/common/FormikUICheckboxWithLabel'; -import DatePicker from 'components/common/FormikUIDatePicker'; -import DateTimePicker from 'components/common/FormikUIDateTimePicker'; import TextField from 'components/common/FormikUITextField'; import TitledContainer from 'components/common/TitledContainer'; import { QuestionTemplateRelationFormProps } from 'components/questionary/QuestionaryComponentRegistry'; -import { SettingsContext } from 'context/SettingsContextProvider'; -import { DateRangeConfig, SettingsId } from 'generated/sdk'; -import { minMaxDateTimeCalculations } from 'utils/Time'; import QuestionDependencyList from '../QuestionDependencyList'; import { QuestionExcerpt } from '../QuestionExcerpt'; @@ -23,43 +15,12 @@ import { QuestionTemplateRelationFormShell } from '../QuestionTemplateRelationFo export const QuestionTemplateRelationDateForm = ( props: QuestionTemplateRelationFormProps ) => { - const theme = useTheme(); - const { settingsMap } = useContext(SettingsContext); - - const dateTimeFormat = settingsMap.get( - SettingsId.DATE_TIME_FORMAT - )?.settingsValue; - const dateFormat = settingsMap.get(SettingsId.DATE_FORMAT)?.settingsValue; - return ( {(formikProps) => { - const { minDate, maxDate, includeTime } = formikProps.values - .config as DateRangeConfig; - const defaultDate = null; - const { defaultFieldMaxDate, defaultFieldMinDate, isMinAfterMaxDate } = - minMaxDateTimeCalculations({ - minDate, - maxDate, - defaultDate, - includeTime, - }); - - if (formikProps.isValid) { - if (isMinAfterMaxDate) { - formikProps.setFieldError( - 'config.minDate', - '"Min" date should be before "Max" date' - ); - } - } - - const component = includeTime ? DateTimePicker : DatePicker; - const inputFormat = includeTime ? dateTimeFormat : dateFormat; - return ( <> @@ -91,37 +52,6 @@ export const QuestionTemplateRelationDateForm = ( }} data-cy="required" /> - - - - - Date: Tue, 21 Jul 2026 13:59:15 +0100 Subject: [PATCH 07/15] fix saving --- .../src/models/questionTypes/DateRange.ts | 19 ++++++++++++++++--- .../QuestionaryComponentDateRangePicker.tsx | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 5767dc7a21..6a17662eb6 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -1,20 +1,33 @@ /* eslint-disable quotes */ -import { dateQuestionValidationSchema } from '@user-office-software/duo-validation'; import { GraphQLError } from 'graphql'; +import * as Yup from 'yup'; import { DateRangeConfig } from '../../resolvers/types/FieldConfig'; import { QuestionFilterCompareOperator } from '../Questionary'; import { DataType, QuestionTemplateRelation } from '../Template'; import { Question } from './QuestionRegistry'; +//TODO add validation script to duo-validation +const dateRangeQuestionValidationSchema = (field: QuestionTemplateRelation) => + Yup.object({ + dateRanges: Yup.array() + .of( + Yup.object({ + from: Yup.date().required(), + to: Yup.date().required(), + }) + ) + .required(), + }); + export const dateRangeDefinition: Question = { dataType: DataType.DATE_RANGE_PICKER, validate: (field: QuestionTemplateRelation, value: Date | null) => { if (field.question.dataType !== DataType.DATE_RANGE_PICKER) { - throw new GraphQLError('DataType should be list of DATE RANGE'); + throw new GraphQLError('DataType should be of DATE RANGE type'); } - return dateQuestionValidationSchema(field).isValid(value); + return dateRangeQuestionValidationSchema(field).isValid(value); }, createBlankConfig: (): DateRangeConfig => { const config = new DateRangeConfig(); diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index 8b9c25ba8f..3b897480df 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -109,7 +109,7 @@ export function QuestionaryComponentDateRangePicker( onSelect={(value) => { setRange(value); if (value?.from && value?.to) { - onComplete([value]); + onComplete({ dateRanges: [value] }); } }} /> From 7ac6406a5e0b3095aa9e5bea6050d7e5f6267940 Mon Sep 17 00:00:00 2001 From: zachary Date: Tue, 21 Jul 2026 14:24:52 +0100 Subject: [PATCH 08/15] add answer rendererer --- .../src/models/questionTypes/DateRange.ts | 3 +- .../DateRangeAnswerRenderer.tsx | 30 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 6a17662eb6..728997aae0 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -39,8 +39,7 @@ export const dateRangeDefinition: Question = { return config; }, - getDefaultAnswer: (relation: QuestionTemplateRelation) => - (relation.config as DateRangeConfig).defaultDate || null, + getDefaultAnswer: (relation: QuestionTemplateRelation) => null, filterQuery: (queryBuilder, filter) => { switch (filter.compareOperator) { case QuestionFilterCompareOperator.EQUALS: diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx index 840bdab5c3..bd4d089e96 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx @@ -1,39 +1,37 @@ import React from 'react'; import { AnswerRenderer } from 'components/questionary/QuestionaryComponentRegistry'; -import { - DateRangeConfig, - FieldConfig, - Maybe, - Scalars, - SettingsId, -} from 'generated/sdk'; +import { Maybe, Scalars, SettingsId } from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; // NOTE: This is additional component because of some react warning with hooks when we use the useFormattedDateTime inside default DateAnswerRenderer component. -const DateAnswerValueRenderer = ({ - config, +const DateRangeAnswerValueRenderer = ({ value, }: { - config: FieldConfig; value: Maybe; }) => { - const settingsFormatToUse = (config as DateRangeConfig).includeTime - ? SettingsId.DATE_TIME_FORMAT - : SettingsId.DATE_FORMAT; + const settingsFormatToUse = SettingsId.DATE_FORMAT; const { toFormattedDateTime } = useFormattedDateTime({ settingsFormatToUse, }); + if (value?.dateRanges?.[0]?.from && value?.dateRanges?.[0]?.to) { + return ( + + {toFormattedDateTime(value.dateRanges[0].from)} :{' '} + {toFormattedDateTime(value.dateRanges[0].to)} + + ); + } - return {toFormattedDateTime(value)}; + return Invalid date range; }; -const DateAnswerRenderer: AnswerRenderer = ({ config, value }) => { +const DateAnswerRenderer: AnswerRenderer = ({ value }) => { if (!value) { return Left blank; } - return ; + return ; }; export default DateAnswerRenderer; From dbb088f14a8b103800f557806e3ebc055ff455cf Mon Sep 17 00:00:00 2001 From: zachary Date: Wed, 22 Jul 2026 12:07:06 +0100 Subject: [PATCH 09/15] add: required logic --- .../src/models/questionTypes/DateRange.ts | 27 +++++---- .../DateRangePickerDefinition.tsx | 56 ++++++------------- .../QuestionaryComponentDateRangePicker.tsx | 15 +++-- validation/src/Questionary/dateRange.ts | 28 ++++------ 4 files changed, 56 insertions(+), 70 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 728997aae0..0a7830ebb3 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -8,17 +8,22 @@ import { DataType, QuestionTemplateRelation } from '../Template'; import { Question } from './QuestionRegistry'; //TODO add validation script to duo-validation -const dateRangeQuestionValidationSchema = (field: QuestionTemplateRelation) => - Yup.object({ - dateRanges: Yup.array() - .of( - Yup.object({ - from: Yup.date().required(), - to: Yup.date().required(), - }) - ) - .required(), - }); +export const dateRangeQuestionValidationSchema = (answer: any) => { + const config = answer.config; + let dateRangeSchema = Yup.array() + .of( + Yup.object({ + from: Yup.date().required(), + to: Yup.date().required(), + }) + ) + .required(); + if (config.required) { + dateRangeSchema = dateRangeSchema.min(1, 'A daterange is reqruired'); + } + + return Yup.object({ dateRanges: dateRangeSchema }); +}; export const dateRangeDefinition: Question = { dataType: DataType.DATE_RANGE_PICKER, diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx index a49b9c14c7..761fb24974 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangePickerDefinition.tsx @@ -1,10 +1,10 @@ import TodayIcon from '@mui/icons-material/Today'; //import { dateRangeQuestionValidationSchema } from '@user-office-software/duo-validation'; -import { DateTime } from 'luxon'; import React from 'react'; +import * as Yup from 'yup'; import defaultRenderer from 'components/questionary/DefaultQuestionRenderer'; -import { DataType } from 'generated/sdk'; +import { DataType, DateRangeConfig, Answer } from 'generated/sdk'; import DateRangeAnswerRenderer from './DateRangeAnswerRenderer'; import DateSearchCriteriaInput from './DateRangeSearchCriteriaInput'; @@ -13,43 +13,23 @@ import { QuestionDateForm } from './QuestionDateRangeForm'; import { QuestionTemplateRelationDateForm } from './QuestionTemplateRelationDateRangeForm'; import { QuestionaryComponentDefinition } from '../../QuestionaryComponentRegistry'; -export function normalizeDate(date: string, includeTime: boolean) { - let normalizedDate = DateTime.fromISO(date); - - if (includeTime) { - normalizedDate = normalizedDate.startOf('minute'); - } else { - normalizedDate = normalizedDate.startOf('day'); +//TODO add validation script to duo-validation +export const dateRangeQuestionValidationSchema = (answer: Answer) => { + const config = answer.config as DateRangeConfig; + let dateRangeSchema = Yup.array() + .of( + Yup.object({ + from: Yup.date().required(), + to: Yup.date().required(), + }) + ) + .required(); + if (config.required) { + dateRangeSchema = dateRangeSchema.min(1, 'A daterange is required.'); } - return normalizedDate.toJSDate(); -} - -// const dateRangeQuestionValidationSchema = (field: any) => { -// console.log('running validation now on: '); -// const schema = Yup.array().of( -// Yup.object({ -// from: Yup.date() -// .required() -// .transform(function (value: Date) { -// return value && this.isType(value) -// ? normalizeDate(value.toISOString(), false) -// : value; -// }) -// .typeError('Invalid date'), -// to: Yup.date() -// .required() -// .transform(function (value: Date) { -// return value && this.isType(value) -// ? normalizeDate(value.toISOString(), false) -// : value; -// }) -// .typeError('Invalid date'), -// }) -// ); - -// return schema; -// }; + return Yup.object({ dateRanges: dateRangeSchema }); +}; export const dateRangeDefinition: QuestionaryComponentDefinition = { dataType: DataType.DATE_RANGE_PICKER, @@ -64,7 +44,7 @@ export const dateRangeDefinition: QuestionaryComponentDefinition = { questionRenderer: defaultRenderer.questionRenderer, answerRenderer: DateRangeAnswerRenderer, }, - createYupValidationSchema: null, + createYupValidationSchema: dateRangeQuestionValidationSchema, getYupInitialValue: ({ answer }) => answer.value, searchCriteriaComponent: DateSearchCriteriaInput, }; diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index 3b897480df..3a5f751617 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -1,4 +1,5 @@ import FormControl from '@mui/material/FormControl'; +import FormHelperText from '@mui/material/FormHelperText'; import { styled } from '@mui/material/styles'; import React, { useState } from 'react'; import { DayPicker, DateRange } from 'react-day-picker'; @@ -91,22 +92,27 @@ const StyledPickerWrapper = styled('div')(({ theme }) => ({ export function QuestionaryComponentDateRangePicker( props: BasicComponentProps ) { - const { answer, onComplete } = props; + const { + answer, + onComplete, + formikProps: { errors }, + } = props; const { question: { id }, } = answer; - const [range, setRange] = useState(); + const isError = errors[id] ? true : false; return ( - + { + onSelect={(value: DateRange | undefined) => { setRange(value); if (value?.from && value?.to) { onComplete({ dateRanges: [value] }); @@ -114,6 +120,7 @@ export function QuestionaryComponentDateRangePicker( }} /> + {isError && {errors[id]?.toString()}} ); } diff --git a/validation/src/Questionary/dateRange.ts b/validation/src/Questionary/dateRange.ts index 72bd1d20fa..0cd66c351e 100644 --- a/validation/src/Questionary/dateRange.ts +++ b/validation/src/Questionary/dateRange.ts @@ -1,23 +1,17 @@ import * as Yup from 'yup'; import { normalizeDate } from './date'; -export const dateRangeQuestionValidationSchema = (field: any) => { - const schema = Yup.array().of( - Yup.object({ - from: Yup.date().required().transform(function (value: Date) { - return value && this.isType(value) - ? normalizeDate(value.toISOString(), false) - : value; - }) - .typeError('Invalid date'), - to: Yup.date().required().transform(function (value: Date) { - return value && this.isType(value) - ? normalizeDate(value.toISOString(), false) - : value; - }) - .typeError('Invalid date') - }) - ); +export const dateRangeQuestionValidationSchema = (answer: any) => { + const schema = Yup.object({ + dateRanges: Yup.array() + .of( + Yup.object({ + from: Yup.date().required(), + to: Yup.date().required(), + }) + ) + .required(), + }); return schema; }; \ No newline at end of file From d8601facb157273af66b0ba96dd3bc9923d4c070 Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 15:14:49 +0100 Subject: [PATCH 10/15] finish filterQuery logic --- apps/backend/src/models/questionTypes/DateRange.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/apps/backend/src/models/questionTypes/DateRange.ts b/apps/backend/src/models/questionTypes/DateRange.ts index 0a7830ebb3..a8b0be0861 100644 --- a/apps/backend/src/models/questionTypes/DateRange.ts +++ b/apps/backend/src/models/questionTypes/DateRange.ts @@ -3,7 +3,6 @@ import { GraphQLError } from 'graphql'; import * as Yup from 'yup'; import { DateRangeConfig } from '../../resolvers/types/FieldConfig'; -import { QuestionFilterCompareOperator } from '../Questionary'; import { DataType, QuestionTemplateRelation } from '../Template'; import { Question } from './QuestionRegistry'; @@ -46,17 +45,6 @@ export const dateRangeDefinition: Question = { }, getDefaultAnswer: (relation: QuestionTemplateRelation) => null, filterQuery: (queryBuilder, filter) => { - switch (filter.compareOperator) { - case QuestionFilterCompareOperator.EQUALS: - return true; - case QuestionFilterCompareOperator.GREATER_THAN: - return true; - case QuestionFilterCompareOperator.LESS_THAN: - return true; - default: - throw new GraphQLError( - `Unsupported comparator for TextInput ${filter.compareOperator}` - ); - } + throw new GraphQLError(`Unsupported comparator ${filter.compareOperator}`); }, }; From 88b0c0df3bdd69f6993292b0df562263348c60d2 Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 15:40:26 +0100 Subject: [PATCH 11/15] add string to make it clear what is going on --- .../DateRangeSearchCriteriaInput.tsx | 96 +------------------ 1 file changed, 5 insertions(+), 91 deletions(-) diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx index 217dd82bb0..6923dd4821 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeSearchCriteriaInput.tsx @@ -1,106 +1,20 @@ import FormControl from '@mui/material/FormControl'; import Grid from '@mui/material/Grid'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select from '@mui/material/Select'; import { useTheme } from '@mui/material/styles'; -import { AdapterLuxon as DateAdapter } from '@mui/x-date-pickers/AdapterLuxon'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { DateTime } from 'luxon'; -import React, { useState, useEffect } from 'react'; +import React from 'react'; -import { SearchCriteriaInputProps } from 'components/proposal/SearchCriteriaInputProps'; -import { QuestionFilterCompareOperator, SettingsId } from 'generated/sdk'; -import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; - -function DateSearchCriteriaInput({ - onChange, - searchCriteria, -}: SearchCriteriaInputProps) { +function DateSearchCriteriaInput() { const theme = useTheme(); - const { format } = useFormattedDateTime({ - settingsFormatToUse: SettingsId.DATE_FORMAT, - }); - const [value, setValue] = useState( - searchCriteria - ? DateTime.fromISO((searchCriteria.value as string) || '') - : null - ); - const [comparator, setComparator] = useState( - searchCriteria?.compareOperator ?? QuestionFilterCompareOperator.EQUALS - ); - - useEffect(() => { - if (!searchCriteria?.value) { - setValue(null); - } - }, [searchCriteria?.value]); return ( - - Operator - - + + Search not implemented for this question type. + - - - { - const newDate = date?.startOf('day'); - - if (newDate && newDate.isValid) { - onChange(comparator, newDate.toJSDate().toISOString()); - } - setValue(newDate || null); - }} - desktopModeMediaQuery={theme.breakpoints.up('sm')} - /> - - ); } From b0bbc8a22c473b21619c92b6392da62c704d84d0 Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 15:54:35 +0100 Subject: [PATCH 12/15] cleanup loading logic and usetime --- .../resolvers/mutations/AnswerTopicMutation.ts | 2 -- .../DateRangePicker/QuestionDateRangeForm.tsx | 10 ---------- .../QuestionTemplateRelationDateRangeForm.tsx | 9 --------- .../QuestionaryComponentDateRangePicker.tsx | 7 ++++++- validation/src/Questionary/dateRange.ts | 17 ----------------- 5 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 validation/src/Questionary/dateRange.ts diff --git a/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts b/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts index c47842c498..fb2d6ac0e3 100644 --- a/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts +++ b/apps/backend/src/resolvers/mutations/AnswerTopicMutation.ts @@ -44,8 +44,6 @@ export class UpdateQuestionaryMutation { args: AnswerTopicArgs, @Ctx() context: ResolverContext ) { - //console.log(`backend answerTopic called with ${JSON.stringify(args)}`); - return context.mutations.questionary.answerTopic(context.user, args); } } diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx index 586924b63e..62e545ea0a 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionDateRangeForm.tsx @@ -45,16 +45,6 @@ export const QuestionDateForm = (props: QuestionFormProps) => { fullWidth inputProps={{ 'data-cy': 'question' }} /> - - (); const isError = errors[id] ? true : false; + useEffect(() => { + if (answer?.value?.dateRanges?.[0]) { + setRange(answer.value.dateRanges[0]); + } + }, []); return ( diff --git a/validation/src/Questionary/dateRange.ts b/validation/src/Questionary/dateRange.ts deleted file mode 100644 index 0cd66c351e..0000000000 --- a/validation/src/Questionary/dateRange.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as Yup from 'yup'; -import { normalizeDate } from './date'; - -export const dateRangeQuestionValidationSchema = (answer: any) => { - const schema = Yup.object({ - dateRanges: Yup.array() - .of( - Yup.object({ - from: Yup.date().required(), - to: Yup.date().required(), - }) - ) - .required(), - }); - - return schema; -}; \ No newline at end of file From 7895c391877864193fe53d5a97a39a1c92e97d3e Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 15:57:49 +0100 Subject: [PATCH 13/15] remove dev comments --- apps/backend/src/resolvers/CustomScalars.ts | 1 - .../src/components/questionary/QuestionaryStepView.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/apps/backend/src/resolvers/CustomScalars.ts b/apps/backend/src/resolvers/CustomScalars.ts index 74334f9805..7c6e506273 100644 --- a/apps/backend/src/resolvers/CustomScalars.ts +++ b/apps/backend/src/resolvers/CustomScalars.ts @@ -3,7 +3,6 @@ import { GraphQLScalarType, Kind } from 'graphql'; export type AnswerType = number | string | Date | boolean | number[] | unknown; const coerce = (value: AnswerType) => { - //console.log(`Coercing: ${JSON.stringify(value)}`); if (typeof value === 'number' && Number.isInteger(value)) { return value; } diff --git a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx index a2e21a7f0f..42f443e705 100644 --- a/apps/frontend/src/components/questionary/QuestionaryStepView.tsx +++ b/apps/frontend/src/components/questionary/QuestionaryStepView.tsx @@ -340,9 +340,6 @@ function QuestionaryStepView(props: { answer: field, formikProps, onComplete: (newValue: Answer['value']) => { - console.log( - `Executing onComplete with ${JSON.stringify(newValue)}` - ); if (field.value !== newValue) { dispatch({ type: 'FIELD_CHANGED', From 8ede72930269db3a75930b060b27de23ff653b9b Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 15:59:03 +0100 Subject: [PATCH 14/15] remove dev comments2 --- .../DateRangePicker/DateRangeAnswerRenderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx index bd4d089e96..e049e5c9b5 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/DateRangeAnswerRenderer.tsx @@ -4,7 +4,6 @@ import { AnswerRenderer } from 'components/questionary/QuestionaryComponentRegis import { Maybe, Scalars, SettingsId } from 'generated/sdk'; import { useFormattedDateTime } from 'hooks/admin/useFormattedDateTime'; -// NOTE: This is additional component because of some react warning with hooks when we use the useFormattedDateTime inside default DateAnswerRenderer component. const DateRangeAnswerValueRenderer = ({ value, }: { From 5d13ca8b5b67f4ab65bd92d242f28f10322f4e6c Mon Sep 17 00:00:00 2001 From: zachary Date: Thu, 23 Jul 2026 16:13:33 +0100 Subject: [PATCH 15/15] add tooltip --- .../DateRangePicker/QuestionaryComponentDateRangePicker.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx index 800943f826..038181ef5c 100644 --- a/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx +++ b/apps/frontend/src/components/questionary/questionaryComponents/DateRangePicker/QuestionaryComponentDateRangePicker.tsx @@ -5,6 +5,9 @@ import React, { useEffect, useState } from 'react'; import { DayPicker, DateRange } from 'react-day-picker'; import { BasicComponentProps } from 'components/proposal/IBasicComponentProps'; +import { DateRangeConfig } from 'generated/sdk'; + +import Hint from '../Hint'; const StyledPickerWrapper = styled('div')(({ theme }) => ({ padding: theme.spacing(2), @@ -100,6 +103,7 @@ export function QuestionaryComponentDateRangePicker( const { question: { id }, } = answer; + const { tooltip } = answer.config as DateRangeConfig; const [range, setRange] = useState(); const isError = errors[id] ? true : false; useEffect(() => { @@ -125,6 +129,7 @@ export function QuestionaryComponentDateRangePicker( }} /> + {tooltip} {isError && {errors[id]?.toString()}} );