From 0e152958d09af1e976741f6142792cc417c689e6 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Thu, 29 Aug 2024 22:56:58 +0200 Subject: [PATCH] fix: Fix schedule rule naming conflict Having same nested objects naming caused a naming conflict in swagger. --- .../AddSchedule/AddScheduleCommand.cs | 5 +- .../GetChildSchedules/ChildScheduleListVM.cs | 4 +- .../Application/Profiles/MappingProfile.cs | 4 +- src/web/orval.config.ts | 1 + src/web/output.openapi.json | 116 +++++---- src/web/package.json | 2 +- .../schedule-items/schedule-items.ts | 243 ------------------ .../src/api/endpoints/schedules/schedules.ts | 235 +++++++++++++++++ ...leItemCommand.ts => addScheduleCommand.ts} | 6 +- ...e.ts => addScheduleCommandScheduleRule.ts} | 3 +- ...leItemListVM.ts => childScheduleListVM.ts} | 5 +- .../models/childScheduleListVMScheduleRule.ts | 12 + ...msParams.ts => getChildSchedulesParams.ts} | 2 +- .../src/features/groups/GroupAutocomplete.tsx | 45 ++++ .../schedules/AddChildScheduleDialog.tsx | 229 +++++++++++++++++ .../src/features/schedules/ChildSchedule.tsx | 70 +++++ .../timeSlots/TimeSlotAutocomplete.tsx | 45 ++++ .../src/pages/children/UpdateChildPage.tsx | 10 +- 18 files changed, 731 insertions(+), 306 deletions(-) delete mode 100644 src/web/src/api/endpoints/schedule-items/schedule-items.ts create mode 100644 src/web/src/api/endpoints/schedules/schedules.ts rename src/web/src/api/models/{addScheduleItemCommand.ts => addScheduleCommand.ts} (57%) rename src/web/src/api/models/{schedule.ts => addScheduleCommandScheduleRule.ts} (72%) rename src/web/src/api/models/{scheduleItemListVM.ts => childScheduleListVM.ts} (52%) create mode 100644 src/web/src/api/models/childScheduleListVMScheduleRule.ts rename src/web/src/api/models/{listScheduleItemsParams.ts => getChildSchedulesParams.ts} (77%) create mode 100644 src/web/src/features/groups/GroupAutocomplete.tsx create mode 100644 src/web/src/features/schedules/AddChildScheduleDialog.tsx create mode 100644 src/web/src/features/schedules/ChildSchedule.tsx create mode 100644 src/web/src/features/timeSlots/TimeSlotAutocomplete.tsx diff --git a/src/Services/Scheduling/Application/Features/Schedules/Commands/AddSchedule/AddScheduleCommand.cs b/src/Services/Scheduling/Application/Features/Schedules/Commands/AddSchedule/AddScheduleCommand.cs index e120f062..c8e664fb 100644 --- a/src/Services/Scheduling/Application/Features/Schedules/Commands/AddSchedule/AddScheduleCommand.cs +++ b/src/Services/Scheduling/Application/Features/Schedules/Commands/AddSchedule/AddScheduleCommand.cs @@ -1,7 +1,6 @@ using System; using MediatR; using System.Collections.Generic; - namespace KDVManager.Services.Scheduling.Application.Features.Schedules.Commands.AddSchedule; public class AddScheduleCommand : IRequest @@ -15,9 +14,9 @@ public class AddScheduleCommand : IRequest public DateTime? EndDate { get; set; } // Collection of nested schedules - public ICollection ScheduleRules { get; set; } = new List(); + public ICollection ScheduleRules { get; set; } = new List(); - public class ScheduleRule + public class AddScheduleCommandScheduleRule { public DayOfWeek Day { get; set; } diff --git a/src/Services/Scheduling/Application/Features/Schedules/Queries/GetChildSchedules/ChildScheduleListVM.cs b/src/Services/Scheduling/Application/Features/Schedules/Queries/GetChildSchedules/ChildScheduleListVM.cs index 288c5f7a..a9da02a3 100644 --- a/src/Services/Scheduling/Application/Features/Schedules/Queries/GetChildSchedules/ChildScheduleListVM.cs +++ b/src/Services/Scheduling/Application/Features/Schedules/Queries/GetChildSchedules/ChildScheduleListVM.cs @@ -11,9 +11,9 @@ public class ChildScheduleListVM public DateTime? EndDate { get; set; } // Collection of nested schedules - public ICollection ScheduleRules { get; set; } = new List(); + public ICollection ScheduleRules { get; set; } = new List(); - public class ScheduleRule + public class ChildScheduleListVMScheduleRule { public DayOfWeek Day { get; set; } diff --git a/src/Services/Scheduling/Application/Profiles/MappingProfile.cs b/src/Services/Scheduling/Application/Profiles/MappingProfile.cs index 8af9ff6b..061caa14 100644 --- a/src/Services/Scheduling/Application/Profiles/MappingProfile.cs +++ b/src/Services/Scheduling/Application/Profiles/MappingProfile.cs @@ -27,11 +27,11 @@ public MappingProfile() // Schedule Mappings CreateMap() .ForMember(dest => dest.ScheduleRules, opt => opt.MapFrom(src => src.ScheduleRules)); - CreateMap(); + CreateMap(); CreateMap() .ForMember(dest => dest.ScheduleRules, opt => opt.MapFrom(src => src.ScheduleRules)); - CreateMap(); + CreateMap(); } } diff --git a/src/web/orval.config.ts b/src/web/orval.config.ts index 8fadcc97..137ba43f 100644 --- a/src/web/orval.config.ts +++ b/src/web/orval.config.ts @@ -14,6 +14,7 @@ const queryPaginated = { mutator: useMutatorFetchPaginated, query: { useQuery: true, + useInfiniteQueryParam: "PageNumber", }, }; diff --git a/src/web/output.openapi.json b/src/web/output.openapi.json index a6554dc3..1c3eb4ca 100644 --- a/src/web/output.openapi.json +++ b/src/web/output.openapi.json @@ -485,10 +485,10 @@ } } }, - "/scheduling/v1/scheduleitems": { + "/scheduling/v1/schedules": { "get": { - "tags": ["ScheduleItems"], - "operationId": "ListScheduleItems", + "tags": ["Schedules"], + "operationId": "GetChildSchedules", "parameters": [ { "name": "ChildId", @@ -507,7 +507,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleItemListVM" + "$ref": "#/components/schemas/ChildScheduleListVM" } } }, @@ -515,7 +515,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleItemListVM" + "$ref": "#/components/schemas/ChildScheduleListVM" } } }, @@ -523,7 +523,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ScheduleItemListVM" + "$ref": "#/components/schemas/ChildScheduleListVM" } } } @@ -532,23 +532,23 @@ } }, "post": { - "tags": ["ScheduleItems"], - "operationId": "AddScheduleItem", + "tags": ["Schedules"], + "operationId": "AddSchedule", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AddScheduleItemCommand" + "$ref": "#/components/schemas/AddScheduleCommand" } }, "text/json": { "schema": { - "$ref": "#/components/schemas/AddScheduleItemCommand" + "$ref": "#/components/schemas/AddScheduleCommand" } }, "application/*+json": { "schema": { - "$ref": "#/components/schemas/AddScheduleItemCommand" + "$ref": "#/components/schemas/AddScheduleCommand" } } } @@ -912,7 +912,7 @@ }, "additionalProperties": false }, - "AddScheduleItemCommand": { + "AddScheduleCommand": { "type": "object", "properties": { "childId": { @@ -932,16 +932,29 @@ "format": "date-time", "nullable": true }, - "schedules": { + "scheduleRules": { "type": "array", "items": { - "$ref": "#/components/schemas/Schedule" + "$ref": "#/components/schemas/AddScheduleCommandScheduleRule" }, "nullable": true } }, "additionalProperties": false }, + "AddScheduleCommandScheduleRule": { + "type": "object", + "properties": { + "day": { + "$ref": "#/components/schemas/DayOfWeek" + }, + "timeSlotId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "AddTimeSlotCommand": { "type": "object", "properties": { @@ -960,6 +973,49 @@ }, "additionalProperties": false }, + "ChildScheduleListVM": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "childId": { + "type": "string", + "format": "uuid" + }, + "startDate": { + "type": "string", + "format": "date-time" + }, + "endDate": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "scheduleRules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChildScheduleListVMScheduleRule" + }, + "nullable": true + } + }, + "additionalProperties": false + }, + "ChildScheduleListVMScheduleRule": { + "type": "object", + "properties": { + "day": { + "$ref": "#/components/schemas/DayOfWeek" + }, + "timeSlotId": { + "type": "string", + "format": "uuid" + } + }, + "additionalProperties": false + }, "DayOfWeek": { "enum": [0, 1, 2, 3, 4, 5, 6], "type": "integer", @@ -1006,38 +1062,6 @@ }, "additionalProperties": {} }, - "Schedule": { - "type": "object", - "properties": { - "day": { - "$ref": "#/components/schemas/DayOfWeek" - } - }, - "additionalProperties": false - }, - "ScheduleItemListVM": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "childId": { - "type": "string", - "format": "uuid" - }, - "startDate": { - "type": "string", - "format": "date-time" - }, - "endDate": { - "type": "string", - "format": "date-time", - "nullable": true - } - }, - "additionalProperties": false - }, "TimeSlotListVM": { "type": "object", "properties": { diff --git a/src/web/package.json b/src/web/package.json index 88c03906..0aef02a5 100644 --- a/src/web/package.json +++ b/src/web/package.json @@ -40,7 +40,7 @@ "api:generate": "orval --clean", "api:merge": "openapi-merge-cli --config ./openapi-merge.json", "start": "vite", - "build": "tsc -b && vite build", + "build": "vite build", "preview": "vite preview", "lint": "eslint", "lint:fix": "eslint --fix", diff --git a/src/web/src/api/endpoints/schedule-items/schedule-items.ts b/src/web/src/api/endpoints/schedule-items/schedule-items.ts deleted file mode 100644 index fdbce54e..00000000 --- a/src/web/src/api/endpoints/schedule-items/schedule-items.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Generated by orval v7.0.1 🍺 - * Do not edit manually. - * KDVManager CRM API - * OpenAPI spec version: v1 - */ -import { useMutation, useQuery } from "@tanstack/react-query"; -import type { - DefinedInitialDataOptions, - DefinedUseQueryResult, - MutationFunction, - QueryFunction, - QueryKey, - UndefinedInitialDataOptions, - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, -} from "@tanstack/react-query"; -import { useCallback } from "react"; -import type { AddScheduleItemCommand } from "../../models/addScheduleItemCommand"; -import type { ListScheduleItemsParams } from "../../models/listScheduleItemsParams"; -import type { ScheduleItemListVM } from "../../models/scheduleItemListVM"; -import type { UnprocessableEntityResponse } from "../../models/unprocessableEntityResponse"; -import { useExecuteFetch } from "../../mutator/useExecuteFetch"; - -export const useListScheduleItemsHook = () => { - const listScheduleItems = useExecuteFetch(); - - return useCallback( - (params?: ListScheduleItemsParams, signal?: AbortSignal) => { - return listScheduleItems({ - url: `/scheduling/v1/scheduleitems`, - method: "GET", - params, - signal, - }); - }, - [listScheduleItems], - ); -}; - -export const getListScheduleItemsQueryKey = (params?: ListScheduleItemsParams) => { - return [`/scheduling/v1/scheduleitems`, ...(params ? [params] : [])] as const; -}; - -export const useListScheduleItemsQueryOptions = < - TData = Awaited>>, - TError = unknown, ->( - params?: ListScheduleItemsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>>, - TError, - TData - > - >; - }, -) => { - const { query: queryOptions } = options ?? {}; - - const queryKey = queryOptions?.queryKey ?? getListScheduleItemsQueryKey(params); - - const listScheduleItems = useListScheduleItemsHook(); - - const queryFn: QueryFunction< - Awaited>> - > = ({ signal }) => listScheduleItems(params, signal); - - return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< - Awaited>>, - TError, - TData - > & { queryKey: QueryKey }; -}; - -export type ListScheduleItemsQueryResult = NonNullable< - Awaited>> ->; -export type ListScheduleItemsQueryError = unknown; - -export function useListScheduleItems< - TData = Awaited>>, - TError = unknown, ->( - params: undefined | ListScheduleItemsParams, - options: { - query: Partial< - UseQueryOptions< - Awaited>>, - TError, - TData - > - > & - Pick< - DefinedInitialDataOptions< - Awaited>>, - TError, - TData - >, - "initialData" - >; - }, -): DefinedUseQueryResult & { queryKey: QueryKey }; -export function useListScheduleItems< - TData = Awaited>>, - TError = unknown, ->( - params?: ListScheduleItemsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>>, - TError, - TData - > - > & - Pick< - UndefinedInitialDataOptions< - Awaited>>, - TError, - TData - >, - "initialData" - >; - }, -): UseQueryResult & { queryKey: QueryKey }; -export function useListScheduleItems< - TData = Awaited>>, - TError = unknown, ->( - params?: ListScheduleItemsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>>, - TError, - TData - > - >; - }, -): UseQueryResult & { queryKey: QueryKey }; - -export function useListScheduleItems< - TData = Awaited>>, - TError = unknown, ->( - params?: ListScheduleItemsParams, - options?: { - query?: Partial< - UseQueryOptions< - Awaited>>, - TError, - TData - > - >; - }, -): UseQueryResult & { queryKey: QueryKey } { - const queryOptions = useListScheduleItemsQueryOptions(params, options); - - const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; - - query.queryKey = queryOptions.queryKey; - - return query; -} - -export const useAddScheduleItemHook = () => { - const addScheduleItem = useExecuteFetch(); - - return useCallback( - (addScheduleItemCommand: AddScheduleItemCommand) => { - return addScheduleItem({ - url: `/scheduling/v1/scheduleitems`, - method: "POST", - headers: { "Content-Type": "application/json" }, - data: addScheduleItemCommand, - }); - }, - [addScheduleItem], - ); -}; - -export const useAddScheduleItemMutationOptions = < - TError = UnprocessableEntityResponse, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>>, - TError, - { data: AddScheduleItemCommand }, - TContext - >; -}): UseMutationOptions< - Awaited>>, - TError, - { data: AddScheduleItemCommand }, - TContext -> => { - const { mutation: mutationOptions } = options ?? {}; - - const addScheduleItem = useAddScheduleItemHook(); - - const mutationFn: MutationFunction< - Awaited>>, - { data: AddScheduleItemCommand } - > = (props) => { - const { data } = props ?? {}; - - return addScheduleItem(data); - }; - - return { mutationFn, ...mutationOptions }; -}; - -export type AddScheduleItemMutationResult = NonNullable< - Awaited>> ->; -export type AddScheduleItemMutationBody = AddScheduleItemCommand; -export type AddScheduleItemMutationError = UnprocessableEntityResponse; - -export const useAddScheduleItem = < - TError = UnprocessableEntityResponse, - TContext = unknown, ->(options?: { - mutation?: UseMutationOptions< - Awaited>>, - TError, - { data: AddScheduleItemCommand }, - TContext - >; -}): UseMutationResult< - Awaited>>, - TError, - { data: AddScheduleItemCommand }, - TContext -> => { - const mutationOptions = useAddScheduleItemMutationOptions(options); - - return useMutation(mutationOptions); -}; diff --git a/src/web/src/api/endpoints/schedules/schedules.ts b/src/web/src/api/endpoints/schedules/schedules.ts new file mode 100644 index 00000000..4d52d9ef --- /dev/null +++ b/src/web/src/api/endpoints/schedules/schedules.ts @@ -0,0 +1,235 @@ +/** + * Generated by orval v7.0.1 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ +import { useMutation, useQuery } from "@tanstack/react-query"; +import type { + DefinedInitialDataOptions, + DefinedUseQueryResult, + MutationFunction, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult, +} from "@tanstack/react-query"; +import { useCallback } from "react"; +import type { AddScheduleCommand } from "../../models/addScheduleCommand"; +import type { ChildScheduleListVM } from "../../models/childScheduleListVM"; +import type { GetChildSchedulesParams } from "../../models/getChildSchedulesParams"; +import type { UnprocessableEntityResponse } from "../../models/unprocessableEntityResponse"; +import { useExecuteFetch } from "../../mutator/useExecuteFetch"; + +export const useGetChildSchedulesHook = () => { + const getChildSchedules = useExecuteFetch(); + + return useCallback( + (params?: GetChildSchedulesParams, signal?: AbortSignal) => { + return getChildSchedules({ url: `/scheduling/v1/schedules`, method: "GET", params, signal }); + }, + [getChildSchedules], + ); +}; + +export const getGetChildSchedulesQueryKey = (params?: GetChildSchedulesParams) => { + return [`/scheduling/v1/schedules`, ...(params ? [params] : [])] as const; +}; + +export const useGetChildSchedulesQueryOptions = < + TData = Awaited>>, + TError = unknown, +>( + params?: GetChildSchedulesParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>>, + TError, + TData + > + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getGetChildSchedulesQueryKey(params); + + const getChildSchedules = useGetChildSchedulesHook(); + + const queryFn: QueryFunction< + Awaited>> + > = ({ signal }) => getChildSchedules(params, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type GetChildSchedulesQueryResult = NonNullable< + Awaited>> +>; +export type GetChildSchedulesQueryError = unknown; + +export function useGetChildSchedules< + TData = Awaited>>, + TError = unknown, +>( + params: undefined | GetChildSchedulesParams, + options: { + query: Partial< + UseQueryOptions< + Awaited>>, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited>>, + TError, + TData + >, + "initialData" + >; + }, +): DefinedUseQueryResult & { queryKey: QueryKey }; +export function useGetChildSchedules< + TData = Awaited>>, + TError = unknown, +>( + params?: GetChildSchedulesParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>>, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited>>, + TError, + TData + >, + "initialData" + >; + }, +): UseQueryResult & { queryKey: QueryKey }; +export function useGetChildSchedules< + TData = Awaited>>, + TError = unknown, +>( + params?: GetChildSchedulesParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>>, + TError, + TData + > + >; + }, +): UseQueryResult & { queryKey: QueryKey }; + +export function useGetChildSchedules< + TData = Awaited>>, + TError = unknown, +>( + params?: GetChildSchedulesParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>>, + TError, + TData + > + >; + }, +): UseQueryResult & { queryKey: QueryKey } { + const queryOptions = useGetChildSchedulesQueryOptions(params, options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +} + +export const useAddScheduleHook = () => { + const addSchedule = useExecuteFetch(); + + return useCallback( + (addScheduleCommand: AddScheduleCommand) => { + return addSchedule({ + url: `/scheduling/v1/schedules`, + method: "POST", + headers: { "Content-Type": "application/json" }, + data: addScheduleCommand, + }); + }, + [addSchedule], + ); +}; + +export const useAddScheduleMutationOptions = < + TError = UnprocessableEntityResponse, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>>, + TError, + { data: AddScheduleCommand }, + TContext + >; +}): UseMutationOptions< + Awaited>>, + TError, + { data: AddScheduleCommand }, + TContext +> => { + const { mutation: mutationOptions } = options ?? {}; + + const addSchedule = useAddScheduleHook(); + + const mutationFn: MutationFunction< + Awaited>>, + { data: AddScheduleCommand } + > = (props) => { + const { data } = props ?? {}; + + return addSchedule(data); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AddScheduleMutationResult = NonNullable< + Awaited>> +>; +export type AddScheduleMutationBody = AddScheduleCommand; +export type AddScheduleMutationError = UnprocessableEntityResponse; + +export const useAddSchedule = (options?: { + mutation?: UseMutationOptions< + Awaited>>, + TError, + { data: AddScheduleCommand }, + TContext + >; +}): UseMutationResult< + Awaited>>, + TError, + { data: AddScheduleCommand }, + TContext +> => { + const mutationOptions = useAddScheduleMutationOptions(options); + + return useMutation(mutationOptions); +}; diff --git a/src/web/src/api/models/addScheduleItemCommand.ts b/src/web/src/api/models/addScheduleCommand.ts similarity index 57% rename from src/web/src/api/models/addScheduleItemCommand.ts rename to src/web/src/api/models/addScheduleCommand.ts index 1fdd03f4..e5206edf 100644 --- a/src/web/src/api/models/addScheduleItemCommand.ts +++ b/src/web/src/api/models/addScheduleCommand.ts @@ -4,14 +4,14 @@ * KDVManager CRM API * OpenAPI spec version: v1 */ -import type { Schedule } from "./schedule"; +import type { AddScheduleCommandScheduleRule } from "./addScheduleCommandScheduleRule"; -export type AddScheduleItemCommand = { +export type AddScheduleCommand = { childId?: string; /** @nullable */ endDate?: string | null; groupId?: string; /** @nullable */ - schedules?: Schedule[] | null; + scheduleRules?: AddScheduleCommandScheduleRule[] | null; startDate?: string; }; diff --git a/src/web/src/api/models/schedule.ts b/src/web/src/api/models/addScheduleCommandScheduleRule.ts similarity index 72% rename from src/web/src/api/models/schedule.ts rename to src/web/src/api/models/addScheduleCommandScheduleRule.ts index d4b4bec3..f1529a3e 100644 --- a/src/web/src/api/models/schedule.ts +++ b/src/web/src/api/models/addScheduleCommandScheduleRule.ts @@ -6,6 +6,7 @@ */ import type { DayOfWeek } from "./dayOfWeek"; -export type Schedule = { +export type AddScheduleCommandScheduleRule = { day?: DayOfWeek; + timeSlotId?: string; }; diff --git a/src/web/src/api/models/scheduleItemListVM.ts b/src/web/src/api/models/childScheduleListVM.ts similarity index 52% rename from src/web/src/api/models/scheduleItemListVM.ts rename to src/web/src/api/models/childScheduleListVM.ts index 47159a7c..71f92ffe 100644 --- a/src/web/src/api/models/scheduleItemListVM.ts +++ b/src/web/src/api/models/childScheduleListVM.ts @@ -4,11 +4,14 @@ * KDVManager CRM API * OpenAPI spec version: v1 */ +import type { ChildScheduleListVMScheduleRule } from "./childScheduleListVMScheduleRule"; -export type ScheduleItemListVM = { +export type ChildScheduleListVM = { childId?: string; /** @nullable */ endDate?: string | null; id?: string; + /** @nullable */ + scheduleRules?: ChildScheduleListVMScheduleRule[] | null; startDate?: string; }; diff --git a/src/web/src/api/models/childScheduleListVMScheduleRule.ts b/src/web/src/api/models/childScheduleListVMScheduleRule.ts new file mode 100644 index 00000000..089ae47e --- /dev/null +++ b/src/web/src/api/models/childScheduleListVMScheduleRule.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v7.0.1 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ +import type { DayOfWeek } from "./dayOfWeek"; + +export type ChildScheduleListVMScheduleRule = { + day?: DayOfWeek; + timeSlotId?: string; +}; diff --git a/src/web/src/api/models/listScheduleItemsParams.ts b/src/web/src/api/models/getChildSchedulesParams.ts similarity index 77% rename from src/web/src/api/models/listScheduleItemsParams.ts rename to src/web/src/api/models/getChildSchedulesParams.ts index 23442c97..c8f327f3 100644 --- a/src/web/src/api/models/listScheduleItemsParams.ts +++ b/src/web/src/api/models/getChildSchedulesParams.ts @@ -5,6 +5,6 @@ * OpenAPI spec version: v1 */ -export type ListScheduleItemsParams = { +export type GetChildSchedulesParams = { ChildId?: string; }; diff --git a/src/web/src/features/groups/GroupAutocomplete.tsx b/src/web/src/features/groups/GroupAutocomplete.tsx new file mode 100644 index 00000000..a4dc9478 --- /dev/null +++ b/src/web/src/features/groups/GroupAutocomplete.tsx @@ -0,0 +1,45 @@ +import { useListGroups } from "@api/endpoints/groups/groups"; +import { type GroupListVM } from "@api/models/groupListVM"; +import { Autocomplete, type AutocompleteProps, CircularProgress, TextField } from "@mui/material"; +import React from "react"; + +type OmittedProps = "options" | "loading" | "getOptionLabel" | "renderInput"; +type GroupAutocompleteProps< + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, + ChipComponent extends React.ElementType = React.ElementType, +> = Omit< + AutocompleteProps, + OmittedProps +>; + +const GroupAutocomplete: React.FC = (props) => { + const { data, isLoading, isFetching } = useListGroups(); + + return ( + + {...props} + options={data?.value ?? []} + loading={isLoading || isFetching} + getOptionLabel={(option) => option.name!} + renderInput={(params) => ( + + {isLoading || isFetching ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + ); +}; + +export default GroupAutocomplete; diff --git a/src/web/src/features/schedules/AddChildScheduleDialog.tsx b/src/web/src/features/schedules/AddChildScheduleDialog.tsx new file mode 100644 index 00000000..552673b2 --- /dev/null +++ b/src/web/src/features/schedules/AddChildScheduleDialog.tsx @@ -0,0 +1,229 @@ +import { Controller, useForm, useFieldArray, type SubmitHandler } from "react-hook-form"; +import { FormContainer } from "react-hook-form-mui"; +import Button from "@mui/material/Button"; +import DialogContent from "@mui/material/DialogContent/DialogContent"; +import DialogActions from "@mui/material/DialogActions/DialogActions"; +import Dialog from "@mui/material/Dialog/Dialog"; +import DialogContentText from "@mui/material/DialogContentText/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle/DialogTitle"; +import NiceModal, { muiDialogV5, useModal } from "@ebay/nice-modal-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import LoadingButton from "@mui/lab/LoadingButton"; +import { useSnackbar } from "notistack"; +import { type UnprocessableEntityResponse } from "@api/models/unprocessableEntityResponse"; +import { type AddScheduleCommand } from "@api/models/addScheduleCommand"; +import Grid from "@mui/material/Grid"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import dayjs from "dayjs"; +import { MenuItem, Select, InputLabel, FormControl, IconButton } from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import DeleteIcon from "@mui/icons-material/Delete"; +import GroupAutocomplete from "../groups/GroupAutocomplete"; +import TimeSlotAutocomplete from "../timeSlots/TimeSlotAutocomplete"; +import { getGetChildSchedulesQueryKey, useAddSchedule } from "@api/endpoints/schedules/schedules"; + +type AddChildScheduleDialogProps = { + childId: string; +}; + +export const AddChildScheduleDialog = NiceModal.create( + ({ childId }) => { + const { t } = useTranslation(); + const modal = useModal(); + const mutate = useAddSchedule(); + const queryClient = useQueryClient(); + const formContext = useForm({ defaultValues: { scheduleRules: [] } }); + + const { + control, + handleSubmit, + reset, + setError, + formState: { isValid, isDirty, isSubmitting }, + } = formContext; + const { enqueueSnackbar } = useSnackbar(); + const { fields, append, remove } = useFieldArray({ + control, + name: "scheduleRules", + }); + + const handleOnCancelClick = () => { + modal.remove(); + reset(); + }; + + const onSubmit: SubmitHandler = async (data) => { + await mutate.mutateAsync( + { data: { childId: childId, ...data } }, + { onSuccess: onMutateSuccess, onError: onMutateError }, + ); + }; + + const onMutateSuccess = () => { + void queryClient.invalidateQueries({ + queryKey: getGetChildSchedulesQueryKey({ ChildId: childId }), + }); + modal.remove(); + enqueueSnackbar(t("Schedule added"), { variant: "success" }); + reset(); + }; + + const onMutateError = (error: UnprocessableEntityResponse) => { + error.errors.forEach((propertyError) => { + setError(propertyError.property as any, { + type: "server", + message: propertyError.title, + }); + }); + }; + + const renderScheduleFields = (item, index) => ( + + + + {t("Day")} + ( + + )} + /> + + + + + ( + field.onChange(newValue ? newValue.id : null)} + value={field.value} + error={!!fieldState.error} + helperText={fieldState.error ? fieldState.error.message : null} + /> + )} + /> + + + + remove(index)}> + + + + + ); + + return ( + + {t("Add schedule")} + + + {t("To add a schedule, please enter the details below.")} + + + + + ( + { + field.onChange(date); + }} + /> + )} + /> + + + ( + { + field.onChange(date); + }} + /> + )} + /> + + + + ( + + field.onChange(newValue ? newValue.id : null) + } + value={field.value} + error={!!fieldState.error} + helperText={fieldState.error ? fieldState.error.message : null} + /> + )} + /> + + + + {fields.map((item, index) => renderScheduleFields(item, index))} + + + + + + + + + + + {t("Add", { ns: "common" })} + + + + ); + }, +); diff --git a/src/web/src/features/schedules/ChildSchedule.tsx b/src/web/src/features/schedules/ChildSchedule.tsx new file mode 100644 index 00000000..1f0847fa --- /dev/null +++ b/src/web/src/features/schedules/ChildSchedule.tsx @@ -0,0 +1,70 @@ +import { DataGrid, type GridColDef } from "@mui/x-data-grid"; +import { type ChildListVM } from "@api/models/childListVM"; +import Toolbar from "@mui/material/Toolbar"; +import Button from "@mui/material/Button"; +import NiceModal from "@ebay/nice-modal-react"; +import AddIcon from "@mui/icons-material/Add"; +import { AddChildScheduleDialog } from "./AddChildScheduleDialog"; +import { useTranslation } from "react-i18next"; +import dayjs from "dayjs"; +import { useGetChildSchedules } from "@api/endpoints/schedules/schedules"; + +const columns: GridColDef[] = [ + { + field: "name", + headerName: "Name", + flex: 1, + disableColumnMenu: true, + disableReorder: true, + }, + { + field: "startDate", + headerName: "StartDate", + flex: 1, + disableColumnMenu: true, + disableReorder: true, + valueFormatter: (value) => value && dayjs(value).format("DD/MM/YYYY"), + }, + { + field: "endDate", + headerName: "EndDate", + flex: 1, + disableColumnMenu: true, + disableReorder: true, + valueFormatter: (value) => value && dayjs(value).format("DD/MM/YYYY"), + }, +]; + +type ChildScheduleProps = { + childId: string; +}; + +export const ChildSchedule: React.FC = ({ childId }) => { + const { t } = useTranslation(); + const { data, isLoading, isFetching } = useGetChildSchedules({ + ChildId: childId, + }); + + const onAddChildScheduleClickHandler = () => + void NiceModal.show(AddChildScheduleDialog, { childId: childId }); + + return ( + <> + + + + + autoHeight + loading={isLoading || isFetching} + columns={columns} + rows={data || []} + /> + + ); +}; diff --git a/src/web/src/features/timeSlots/TimeSlotAutocomplete.tsx b/src/web/src/features/timeSlots/TimeSlotAutocomplete.tsx new file mode 100644 index 00000000..b3c29733 --- /dev/null +++ b/src/web/src/features/timeSlots/TimeSlotAutocomplete.tsx @@ -0,0 +1,45 @@ +import { useListTimeSlots } from "@api/endpoints/time-slots/time-slots"; +import { type TimeSlotListVM } from "@api/models/timeSlotListVM"; +import { Autocomplete, type AutocompleteProps, CircularProgress, TextField } from "@mui/material"; +import React from "react"; + +type OmittedProps = "options" | "loading" | "getOptionLabel" | "renderInput"; +type TimeSlotAutocompleteProps< + Multiple extends boolean | undefined = false, + DisableClearable extends boolean | undefined = false, + FreeSolo extends boolean | undefined = false, + ChipComponent extends React.ElementType = React.ElementType, +> = Omit< + AutocompleteProps, + OmittedProps +>; + +const TimeSlotAutocomplete: React.FC = (props) => { + const { data, isLoading, isFetching } = useListTimeSlots(); + + return ( + + {...props} + options={data?.value ?? []} + loading={isLoading || isFetching} + getOptionLabel={(option) => option.name!} + renderInput={(params) => ( + + {isLoading || isFetching ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + ); +}; + +export default TimeSlotAutocomplete; diff --git a/src/web/src/pages/children/UpdateChildPage.tsx b/src/web/src/pages/children/UpdateChildPage.tsx index 7f3f9e50..ab1006a5 100644 --- a/src/web/src/pages/children/UpdateChildPage.tsx +++ b/src/web/src/pages/children/UpdateChildPage.tsx @@ -17,6 +17,7 @@ import { type UnprocessableEntityResponse } from "@api/models/unprocessableEntit import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; import LoadingButton from "@mui/lab/LoadingButton/LoadingButton"; +import { ChildSchedule } from "../../features/schedules/ChildSchedule"; const UpdateChildPage = () => { const { childId } = useParams() as { childId: string }; @@ -74,10 +75,10 @@ const UpdateChildPage = () => { - + - + { render={({ field }) => { return ( { }} > + + +