From 2f9f51bd5763a16e9dc16fa8e03679fa88ff15f7 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Thu, 18 Jul 2024 19:43:19 +0200 Subject: [PATCH 01/11] feat: Show login and logout button Show the login and logout button based on the isAuthenticated state. --- src/web/src/components/MainNavbar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/web/src/components/MainNavbar.tsx b/src/web/src/components/MainNavbar.tsx index adb2698a..de68b500 100644 --- a/src/web/src/components/MainNavbar.tsx +++ b/src/web/src/components/MainNavbar.tsx @@ -24,7 +24,7 @@ type MainNavbarProps = { const MainNavbar: React.FC = ({ children }) => { const navigate = useNavigate(); const { t } = useTranslation(); - const { loginWithRedirect, logout, user } = useAuth0(); + const { loginWithRedirect, logout, user, isAuthenticated } = useAuth0(); const [anchorElNav, setAnchorElNav] = React.useState(null); const [anchorElUser, setAnchorElUser] = React.useState(null); @@ -175,6 +175,7 @@ const MainNavbar: React.FC = ({ children }) => { {setting} ))} + {isAuthenticated ? ( { @@ -183,6 +184,7 @@ const MainNavbar: React.FC = ({ children }) => { > {t("Logout")} + ) : ( { @@ -191,6 +193,7 @@ const MainNavbar: React.FC = ({ children }) => { > {t("Login")} + )} From ba4d942da6cc2209e7fd8a199501c479981f824b Mon Sep 17 00:00:00 2001 From: LuukvH Date: Thu, 18 Jul 2024 20:34:33 +0200 Subject: [PATCH 02/11] refactor: Extract account menu component Extract account menu to separate component. --- src/web/src/components/AccountMenu.tsx | 111 +++++++++++++++++++++++++ src/web/src/components/MainNavbar.tsx | 67 +-------------- 2 files changed, 114 insertions(+), 64 deletions(-) create mode 100644 src/web/src/components/AccountMenu.tsx diff --git a/src/web/src/components/AccountMenu.tsx b/src/web/src/components/AccountMenu.tsx new file mode 100644 index 00000000..3903e55f --- /dev/null +++ b/src/web/src/components/AccountMenu.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import Box from "@mui/material/Box"; +import Avatar from "@mui/material/Avatar"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import Typography from "@mui/material/Typography"; +import Tooltip from "@mui/material/Tooltip"; +import Settings from "@mui/icons-material/Settings"; +import Logout from "@mui/icons-material/Logout"; +import { useAuth0 } from "@auth0/auth0-react"; +import { useTranslation } from "react-i18next"; +import Stack from "@mui/material/Stack"; + +export default function AccountMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const { logout, user, isAuthenticated } = useAuth0(); + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + {user?.name} + {user?.email} + + + + + + + + {t("Settings")} + + {isAuthenticated && ( + { + void handleClose(); + void logout(); + }} + > + + + + {t("Logout")} + + )} + + + ); +} diff --git a/src/web/src/components/MainNavbar.tsx b/src/web/src/components/MainNavbar.tsx index de68b500..21d86637 100644 --- a/src/web/src/components/MainNavbar.tsx +++ b/src/web/src/components/MainNavbar.tsx @@ -6,17 +6,14 @@ import Typography from "@mui/material/Typography"; import Menu from "@mui/material/Menu"; import MenuIcon from "@mui/icons-material/Menu"; import Container from "@mui/material/Container"; -import Avatar from "@mui/material/Avatar"; import Button from "@mui/material/Button"; -import Tooltip from "@mui/material/Tooltip"; -import MenuItem from "@mui/material/MenuItem"; import AdbIcon from "@mui/icons-material/Adb"; import { useNavigate } from "react-router-dom"; -import { useAuth0, withAuthenticationRequired } from "@auth0/auth0-react"; +import { withAuthenticationRequired } from "@auth0/auth0-react"; import { AppBar } from "@mui/material"; import { useTranslation } from "react-i18next"; +import AccountMenu from "./AccountMenu"; -const settings = ["Profile", "Account", "Dashboard"]; type MainNavbarProps = { children: React.ReactNode; }; @@ -24,24 +21,14 @@ type MainNavbarProps = { const MainNavbar: React.FC = ({ children }) => { const navigate = useNavigate(); const { t } = useTranslation(); - const { loginWithRedirect, logout, user, isAuthenticated } = useAuth0(); const [anchorElNav, setAnchorElNav] = React.useState(null); - const [anchorElUser, setAnchorElUser] = React.useState(null); - const handleOpenNavMenu = (event: React.MouseEvent) => { setAnchorElNav(event.currentTarget); }; - const handleOpenUserMenu = (event: React.MouseEvent) => { - setAnchorElUser(event.currentTarget); - }; const handleCloseNavMenu = () => { setAnchorElNav(null); }; - const handleCloseUserMenu = () => { - setAnchorElUser(null); - }; - return ( <> @@ -147,55 +134,7 @@ const MainNavbar: React.FC = ({ children }) => { {t("People")} - - - - - - - - - {settings.map((setting) => ( - - {setting} - - ))} - {isAuthenticated ? ( - { - void logout(); - }} - > - {t("Logout")} - - ) : ( - { - void loginWithRedirect(); - }} - > - {t("Login")} - - )} - - + From 100678b478e6d1a6a4ec3dfeacefc0ffb72e4c89 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Thu, 18 Jul 2024 21:24:00 +0200 Subject: [PATCH 03/11] feat: Add settings page Add empty settings page where the settings will be shown. --- src/web/src/App.tsx | 4 ++++ src/web/src/components/AccountMenu.tsx | 5 +++-- src/web/src/pages/settings/SettingsPage.tsx | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/web/src/pages/settings/SettingsPage.tsx diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index 29ed7ac7..201cc35a 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -28,6 +28,10 @@ const router = createBrowserRouter([ path: "groups", lazy: () => import("./pages/groups/ListGroupsPage"), }, + { + path: "settings", + lazy: () => import("./pages/settings/SettingsPage"), + }, ], }, ]); diff --git a/src/web/src/components/AccountMenu.tsx b/src/web/src/components/AccountMenu.tsx index 3903e55f..36ba2105 100644 --- a/src/web/src/components/AccountMenu.tsx +++ b/src/web/src/components/AccountMenu.tsx @@ -13,6 +13,7 @@ import Logout from "@mui/icons-material/Logout"; import { useAuth0 } from "@auth0/auth0-react"; import { useTranslation } from "react-i18next"; import Stack from "@mui/material/Stack"; +import { useNavigate } from "react-router-dom"; export default function AccountMenu() { const [anchorEl, setAnchorEl] = React.useState(null); @@ -23,6 +24,7 @@ export default function AccountMenu() { const handleClose = () => { setAnchorEl(null); }; + const navigate = useNavigate(); const { logout, user, isAuthenticated } = useAuth0(); const { t } = useTranslation(); @@ -85,7 +87,7 @@ export default function AccountMenu() { - + navigate("/settings")}> @@ -95,7 +97,6 @@ export default function AccountMenu() { { - void handleClose(); void logout(); }} > diff --git a/src/web/src/pages/settings/SettingsPage.tsx b/src/web/src/pages/settings/SettingsPage.tsx new file mode 100644 index 00000000..6e31bf5f --- /dev/null +++ b/src/web/src/pages/settings/SettingsPage.tsx @@ -0,0 +1,14 @@ +import { Typography } from "@mui/material"; +import { useTranslation } from "react-i18next"; + +const SettingsPage = () => { + const { t } = useTranslation(); + + return ( + <> + {t("Settings")} + + ); +}; + +export const Component = SettingsPage; From 4079365ce43c4861dae0598fff62ef04cccb41f5 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 20 Jul 2024 09:20:40 +0200 Subject: [PATCH 04/11] feat: Create pages for scheduling settings page Added the pages and cards for the scheduling settings page. --- src/web/src/App.tsx | 11 ++++- .../src/features/settings/SettingsCard.tsx | 49 +++++++++++++++++++ .../pages/settings/SchedulingSettingsPage.tsx | 17 +++++++ src/web/src/pages/settings/SettingsPage.tsx | 31 ++++++++++-- 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/web/src/features/settings/SettingsCard.tsx create mode 100644 src/web/src/pages/settings/SchedulingSettingsPage.tsx diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index 201cc35a..dbcd7c45 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -30,7 +30,16 @@ const router = createBrowserRouter([ }, { path: "settings", - lazy: () => import("./pages/settings/SettingsPage"), + children: [ + { + index: true, + lazy: () => import("./pages/settings/SettingsPage"), + }, + { + path: "scheduling", + lazy: () => import("./pages/settings/SchedulingSettingsPage"), + }, + ], }, ], }, diff --git a/src/web/src/features/settings/SettingsCard.tsx b/src/web/src/features/settings/SettingsCard.tsx new file mode 100644 index 00000000..664961ee --- /dev/null +++ b/src/web/src/features/settings/SettingsCard.tsx @@ -0,0 +1,49 @@ +import { useNavigate } from "react-router-dom"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import CardActionArea from "@mui/material/CardActionArea"; +import Box from "@mui/material/Box"; +import { type ReactNode } from "react"; + +type SettingsCardProps = { + title: string; + description: string; + navigateTo: string; + icon: ReactNode; +}; + +export const SettingsCard: React.FC = ({ + title, + description, + navigateTo, + icon, +}) => { + const navigate = useNavigate(); + + const handleClick = () => { + navigate(navigateTo); + }; + + return ( + + + + + {icon} + + + {title} + + + {description} + + + + + + + ); +}; + +export default SettingsCard; diff --git a/src/web/src/pages/settings/SchedulingSettingsPage.tsx b/src/web/src/pages/settings/SchedulingSettingsPage.tsx new file mode 100644 index 00000000..a4a9c210 --- /dev/null +++ b/src/web/src/pages/settings/SchedulingSettingsPage.tsx @@ -0,0 +1,17 @@ +import { useTranslation } from "react-i18next"; +import { Typography } from "@mui/material"; +import React from "react"; + +const SchedulingSettingsPage = () => { + const { t } = useTranslation(); + + return ( + + + {t("Scheduling settings page")} + + + ); +}; + +export const Component = SchedulingSettingsPage; diff --git a/src/web/src/pages/settings/SettingsPage.tsx b/src/web/src/pages/settings/SettingsPage.tsx index 6e31bf5f..1819a3d5 100644 --- a/src/web/src/pages/settings/SettingsPage.tsx +++ b/src/web/src/pages/settings/SettingsPage.tsx @@ -1,13 +1,36 @@ -import { Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; +import { SettingsCard } from "../../features/settings/SettingsCard"; +import Grid from "@mui/material/Grid"; +import CalendarMonth from "@mui/icons-material/CalendarMonth"; +import React from "react"; const SettingsPage = () => { const { t } = useTranslation(); + const settings = [ + { + title: t("Scheduling"), + description: t("Manage groups and timeslots."), + navigateTo: "/settings/scheduling", + icon: , + }, + ]; + return ( - <> - {t("Settings")} - + + + {settings.map((setting, index) => ( + + + + ))} + + ); }; From 211cd7b0f16650cef285a1535f7b02d8ec1e0fde Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 20 Jul 2024 21:17:21 +0200 Subject: [PATCH 05/11] feat: Add breadcrumbs Add breadcrumbs to show your location within the application. --- src/web/src/App.tsx | 18 +++++++ src/web/src/components/MainNavbar.tsx | 8 +++- src/web/src/components/RouterBreadcrumbs.tsx | 49 ++++++++++++++++++++ src/web/src/pages/settings/SettingsPage.tsx | 2 +- 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/web/src/components/RouterBreadcrumbs.tsx diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index dbcd7c45..0dcffa0b 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -7,6 +7,9 @@ const router = createBrowserRouter([ path: "/", element: , errorElement: , + handle: { + crumb: () => "Home", + }, children: [ { path: "children/:childId", @@ -15,10 +18,16 @@ const router = createBrowserRouter([ { path: "children", lazy: () => import("./pages/children/IndexChildPage"), + handle: { + crumb: () => "Children", + }, }, { path: "people", lazy: () => import("./pages/people/IndexPersonPage"), + handle: { + crumb: () => "People", + }, }, { path: "children/new", @@ -27,9 +36,15 @@ const router = createBrowserRouter([ { path: "groups", lazy: () => import("./pages/groups/ListGroupsPage"), + handle: { + crumb: () => "Groups", + }, }, { path: "settings", + handle: { + crumb: () => "Settings", + }, children: [ { index: true, @@ -38,6 +53,9 @@ const router = createBrowserRouter([ { path: "scheduling", lazy: () => import("./pages/settings/SchedulingSettingsPage"), + handle: { + crumb: () => "Scheduling", + }, }, ], }, diff --git a/src/web/src/components/MainNavbar.tsx b/src/web/src/components/MainNavbar.tsx index 21d86637..0ed73605 100644 --- a/src/web/src/components/MainNavbar.tsx +++ b/src/web/src/components/MainNavbar.tsx @@ -13,6 +13,7 @@ import { withAuthenticationRequired } from "@auth0/auth0-react"; import { AppBar } from "@mui/material"; import { useTranslation } from "react-i18next"; import AccountMenu from "./AccountMenu"; +import RouterBreadcrumbs from "./RouterBreadcrumbs"; type MainNavbarProps = { children: React.ReactNode; @@ -138,7 +139,12 @@ const MainNavbar: React.FC = ({ children }) => { - {children} + + + + + {children} + ); }; diff --git a/src/web/src/components/RouterBreadcrumbs.tsx b/src/web/src/components/RouterBreadcrumbs.tsx new file mode 100644 index 00000000..06522a5e --- /dev/null +++ b/src/web/src/components/RouterBreadcrumbs.tsx @@ -0,0 +1,49 @@ +import * as React from "react"; +import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Link from "@mui/material/Link"; +import { Link as RouterLink, type UIMatch } from "react-router-dom"; +import { useMatches } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Typography } from "@mui/material"; + +type Handle = { + crumb: (data: any) => string; +}; + +const RouterBreadcrumbs: React.FC = () => { + const matches = useMatches() as UIMatch[]; + const { t } = useTranslation(); + + const crumbs = matches + // Filter out matches that don't have a handle or crumb function + .filter((match) => Boolean(match.handle?.crumb)) + // Map to an array of elements + .map((match) => ({ + pathname: match.pathname, + crumbElement: match.handle!.crumb(match.data), + })); + + return ( + + {crumbs.map((crumb, index) => + index === crumbs.length - 1 ? ( + + {crumb.crumbElement} + + ) : ( + + {t(crumb.crumbElement)} + + ), + )} + + ); +}; + +export default RouterBreadcrumbs; diff --git a/src/web/src/pages/settings/SettingsPage.tsx b/src/web/src/pages/settings/SettingsPage.tsx index 1819a3d5..2da3b160 100644 --- a/src/web/src/pages/settings/SettingsPage.tsx +++ b/src/web/src/pages/settings/SettingsPage.tsx @@ -18,7 +18,7 @@ const SettingsPage = () => { return ( - + {settings.map((setting, index) => ( Date: Sat, 20 Jul 2024 22:18:08 +0200 Subject: [PATCH 06/11] chore: Change background color Change background color using the material theme. --- src/web/src/index.tsx | 12 +++++++++--- src/web/src/lib/theme.ts | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 src/web/src/lib/theme.ts diff --git a/src/web/src/index.tsx b/src/web/src/index.tsx index 09ef1939..eb7dd4f7 100644 --- a/src/web/src/index.tsx +++ b/src/web/src/index.tsx @@ -9,6 +9,9 @@ import i18n from "./i18n"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import "dayjs/locale/nl"; +import { ThemeProvider } from "@mui/material/styles"; +import { theme } from "./lib/theme"; +import { CssBaseline } from "@mui/material"; const container = document.getElementById("root") as Element; const root = createRoot(container); @@ -17,9 +20,12 @@ root.render( - - - + + + + + + , diff --git a/src/web/src/lib/theme.ts b/src/web/src/lib/theme.ts new file mode 100644 index 00000000..43e87803 --- /dev/null +++ b/src/web/src/lib/theme.ts @@ -0,0 +1,13 @@ +import { createTheme } from "@mui/material/styles"; + +export const theme = createTheme({ + components: { + MuiCssBaseline: { + styleOverrides: { + body: { + backgroundColor: "#efefef", + }, + }, + }, + }, +}); From a813fe04beb3077e1fa49a4c802a9279c8c6c21d Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 20 Jul 2024 22:20:14 +0200 Subject: [PATCH 07/11] feat: Improve crumbs translations Move the translation of the crumbs inside the router. This will make sure translations can be correctly extracted from the source. --- src/web/src/App.tsx | 123 +++++++++++-------- src/web/src/components/RouterBreadcrumbs.tsx | 4 +- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/src/web/src/App.tsx b/src/web/src/App.tsx index 0dcffa0b..8ef6b3e0 100644 --- a/src/web/src/App.tsx +++ b/src/web/src/App.tsx @@ -1,70 +1,87 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import ErrorPage from "./components/ErrorPage"; import MainLayout from "./components/AuthProviderLayout"; +import { type TFunction } from "i18next"; -const router = createBrowserRouter([ - { - path: "/", - element: , - errorElement: , - handle: { - crumb: () => "Home", - }, - children: [ - { - path: "children/:childId", - lazy: () => import("./pages/children/UpdateChildPage"), - }, - { - path: "children", - lazy: () => import("./pages/children/IndexChildPage"), - handle: { - crumb: () => "Children", +const router = (t: TFunction<"translation", undefined>) => + createBrowserRouter([ + { + path: "/", + element: , + errorElement: , + handle: { + crumb: () => { + return t("Home"); }, }, - { - path: "people", - lazy: () => import("./pages/people/IndexPersonPage"), - handle: { - crumb: () => "People", + children: [ + { + path: "children/:childId", + lazy: () => import("./pages/children/UpdateChildPage"), }, - }, - { - path: "children/new", - lazy: () => import("./pages/children/NewChildPage"), - }, - { - path: "groups", - lazy: () => import("./pages/groups/ListGroupsPage"), - handle: { - crumb: () => "Groups", + { + path: "children", + lazy: () => import("./pages/children/IndexChildPage"), + handle: { + crumb: () => { + return t("Children"); + }, + }, }, - }, - { - path: "settings", - handle: { - crumb: () => "Settings", + { + path: "people", + lazy: () => import("./pages/people/IndexPersonPage"), + handle: { + crumb: () => { + return t("People"); + }, + }, }, - children: [ - { - index: true, - lazy: () => import("./pages/settings/SettingsPage"), + { + path: "children/new", + lazy: () => import("./pages/children/NewChildPage"), + }, + { + path: "groups", + lazy: () => import("./pages/groups/ListGroupsPage"), + handle: { + crumb: () => { + return t("Groups"); + }, }, - { - path: "scheduling", - lazy: () => import("./pages/settings/SchedulingSettingsPage"), - handle: { - crumb: () => "Scheduling", + }, + { + path: "settings", + handle: { + crumb: () => { + return t("Settings"); }, }, - ], - }, - ], - }, -]); + children: [ + { + index: true, + lazy: () => import("./pages/settings/SettingsPage"), + }, + { + path: "scheduling", + lazy: () => import("./pages/settings/SchedulingSettingsPage"), + handle: { + crumb: () => { + return t("Scheduling"); + }, + }, + }, + ], + }, + ], + }, + ]); function App() { - return ; + const { t } = useTranslation(); + + return ; } export default App; diff --git a/src/web/src/components/RouterBreadcrumbs.tsx b/src/web/src/components/RouterBreadcrumbs.tsx index 06522a5e..ef1447e3 100644 --- a/src/web/src/components/RouterBreadcrumbs.tsx +++ b/src/web/src/components/RouterBreadcrumbs.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { Typography } from "@mui/material"; type Handle = { - crumb: (data: any) => string; + crumb: (data: any) => React.ReactElement; }; const RouterBreadcrumbs: React.FC = () => { @@ -38,7 +38,7 @@ const RouterBreadcrumbs: React.FC = () => { color="inherit" to={crumb.pathname} > - {t(crumb.crumbElement)} + {crumb.crumbElement} ), )} From 5573cf4f6abd8a52172344e27383eacc42724a87 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 20 Jul 2024 23:35:49 +0200 Subject: [PATCH 08/11] feat: Add timeslot api Add api for adding and listing time slots --- .../Middleware/ExceptionHandlerMiddleware.cs | 6 +- .../ExceptionHandlerMiddlewareExtension.cs | 5 +- .../Responses/UnprocessableEntityResponse.cs | 1 - .../Services/TenantService/TenantService.cs | 3 - .../CRM/Application/ConfigureServices.cs | 4 +- .../Contracts/Pagination/PageParameters.cs | 1 - .../Contracts/Persistence/IChildRepository.cs | 3 +- .../Persistence/IPersonRepository.cs | 3 +- .../Contracts/Validation/ValidationError.cs | 3 +- .../CreateChildCommandValidator.cs | 6 +- .../DeleteChild/DeleteChildCommandHandler.cs | 3 +- .../UpdateChild/UpdateChildCommandHandler.cs | 4 +- .../UpdateChildCommandValidator.cs | 6 +- .../GetChildDetailQueryHandler.cs | 3 +- .../Queries/GetChildList/GetChildListQuery.cs | 5 +- .../GetChildList/GetChildListQueryHandler.cs | 4 +- .../AddPerson/AddPersonCommandValidator.cs | 3 +- .../GetPersonList/GetPersonListQuery.cs | 5 +- .../GetPersonListQueryHandler.cs | 4 +- .../Application/Profiles/MappingProfile.cs | 3 +- .../ChildManagementDbContext.cs | 3 +- .../Configurations/ChildConfiguration.cs | 3 +- .../CRM/Infrastructure/ConfigureServices.cs | 4 +- .../CRM/Infrastructure/MigrationDbContext.cs | 3 +- .../Repositories/PersonRepository.cs | 3 +- .../Api/Controllers/GroupsController.cs | 10 +-- .../Api/Controllers/TimeSlotsController.cs | 39 ++++++++++ .../Middleware/ExceptionHandlerMiddleware.cs | 6 +- .../ExceptionHandlerMiddlewareExtension.cs | 5 +- src/Services/Scheduling/Api/Program.cs | 2 - .../Responses/UnprocessableEntityResponse.cs | 1 - .../Services/TenantService/TenantService.cs | 3 - .../Application/ConfigureServices.cs | 4 +- .../Contracts/Pagination/PageParameters.cs | 1 - .../Contracts/Persistence/IGroupRepository.cs | 4 +- .../Persistence/ITimeSlotRepository.cs | 16 +++++ .../Contracts/Validation/ValidationError.cs | 3 +- .../AddGroup/AddGroupCommandValidator.cs | 3 +- .../DeleteGroup/DeleteGroupCommandHandler.cs | 3 +- .../Queries/ListGroups/ListGroupsQuery.cs | 5 +- .../ListGroups/ListGroupsQueryHandler.cs | 4 +- .../AddTimeSlot/AddTimeSlotCommand.cs | 13 ++++ .../AddTimeSlot/AddTimeSlotCommandHandler.cs | 37 ++++++++++ .../AddTimeSlotCommandValidator.cs | 31 ++++++++ .../ListTimeSlots/ListTimeSlotsQuery.cs | 9 +++ .../ListTimeSlotsQueryHandler.cs | 32 +++++++++ .../Queries/ListTimeSlots/TimeSlotListVM.cs | 11 +++ .../Application/Profiles/MappingProfile.cs | 9 ++- .../Scheduling/Domain/Entities/TimeSlot.cs | 17 +++++ .../Configurations/GroupConfiguration.cs | 3 +- .../Infrastructure/ConfigureServices.cs | 5 +- .../Infrastructure/MigrationDbContext.cs | 5 +- .../20240726205309_AddTimeSlots.Designer.cs | 71 +++++++++++++++++++ .../Migrations/20240726205309_AddTimeSlots.cs | 37 ++++++++++ .../MigrationDbContextModelSnapshot.cs | 23 ++++++ .../Repositories/GroupRepository.cs | 3 +- .../Repositories/TimeSlotRepository.cs | 37 ++++++++++ .../Infrastructure/SchedulingDbContext.cs | 5 +- 58 files changed, 422 insertions(+), 126 deletions(-) create mode 100644 src/Services/Scheduling/Api/Controllers/TimeSlotsController.cs create mode 100644 src/Services/Scheduling/Application/Contracts/Persistence/ITimeSlotRepository.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommand.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandHandler.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQuery.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQueryHandler.cs create mode 100644 src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/TimeSlotListVM.cs create mode 100644 src/Services/Scheduling/Domain/Entities/TimeSlot.cs create mode 100644 src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.Designer.cs create mode 100644 src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.cs create mode 100644 src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs diff --git a/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddleware.cs b/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddleware.cs index 92ace6c0..758c2719 100644 --- a/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddleware.cs @@ -1,10 +1,6 @@ -using System; -using System.Net; -using System.Threading.Tasks; +using System.Net; using KDVManager.Services.CRM.Application.Exceptions; using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http; namespace KDVManager.Services.CRM.Api.Middleware; diff --git a/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs b/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs index 268fe013..6cf78a62 100644 --- a/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs +++ b/src/Services/CRM/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs @@ -1,7 +1,4 @@ -using System; -using Microsoft.AspNetCore.Builder; - -namespace KDVManager.Services.CRM.Api.Middleware +namespace KDVManager.Services.CRM.Api.Middleware { public static class ExceptionHandlerMiddlewareExtension { diff --git a/src/Services/CRM/Api/Responses/UnprocessableEntityResponse.cs b/src/Services/CRM/Api/Responses/UnprocessableEntityResponse.cs index 6010153e..1a542e6b 100644 --- a/src/Services/CRM/Api/Responses/UnprocessableEntityResponse.cs +++ b/src/Services/CRM/Api/Responses/UnprocessableEntityResponse.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using KDVManager.Services.CRM.Application.Exceptions; using ValidationException = KDVManager.Services.CRM.Application.Exceptions.ValidationException; public class ValidationError diff --git a/src/Services/CRM/Api/Services/TenantService/TenantService.cs b/src/Services/CRM/Api/Services/TenantService/TenantService.cs index b59a34d8..3dff8998 100644 --- a/src/Services/CRM/Api/Services/TenantService/TenantService.cs +++ b/src/Services/CRM/Api/Services/TenantService/TenantService.cs @@ -1,7 +1,4 @@ -using System; using KDVManager.Services.CRM.Application.Contracts.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; using KDVManager.Services.CRM.Application.Exceptions; namespace KDVManager.Services.CRM.Api.Services; diff --git a/src/Services/CRM/Application/ConfigureServices.cs b/src/Services/CRM/Application/ConfigureServices.cs index c53755ef..d8c82be1 100644 --- a/src/Services/CRM/Application/ConfigureServices.cs +++ b/src/Services/CRM/Application/ConfigureServices.cs @@ -1,7 +1,5 @@ -using System; -using System.Reflection; +using System.Reflection; using MediatR; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/Services/CRM/Application/Contracts/Pagination/PageParameters.cs b/src/Services/CRM/Application/Contracts/Pagination/PageParameters.cs index f6bbf90d..7fa8eda8 100644 --- a/src/Services/CRM/Application/Contracts/Pagination/PageParameters.cs +++ b/src/Services/CRM/Application/Contracts/Pagination/PageParameters.cs @@ -1,5 +1,4 @@ using KDVManager.Services.CRM.Domain.Interfaces; -using MediatR; namespace KDVManager.Services.CRM.Application.Contracts.Pagination { diff --git a/src/Services/CRM/Application/Contracts/Persistence/IChildRepository.cs b/src/Services/CRM/Application/Contracts/Persistence/IChildRepository.cs index be0240b2..397b35a9 100644 --- a/src/Services/CRM/Application/Contracts/Persistence/IChildRepository.cs +++ b/src/Services/CRM/Application/Contracts/Persistence/IChildRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using KDVManager.Services.CRM.Domain.Entities; using KDVManager.Services.CRM.Domain.Interfaces; diff --git a/src/Services/CRM/Application/Contracts/Persistence/IPersonRepository.cs b/src/Services/CRM/Application/Contracts/Persistence/IPersonRepository.cs index a137b6d6..db8be338 100644 --- a/src/Services/CRM/Application/Contracts/Persistence/IPersonRepository.cs +++ b/src/Services/CRM/Application/Contracts/Persistence/IPersonRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; using KDVManager.Services.CRM.Domain.Entities; using KDVManager.Services.CRM.Domain.Interfaces; diff --git a/src/Services/CRM/Application/Contracts/Validation/ValidationError.cs b/src/Services/CRM/Application/Contracts/Validation/ValidationError.cs index 1ec1e97f..1742f3bf 100644 --- a/src/Services/CRM/Application/Contracts/Validation/ValidationError.cs +++ b/src/Services/CRM/Application/Contracts/Validation/ValidationError.cs @@ -1,5 +1,4 @@ -using System; -namespace KDVManager.Services.CRM.Application.Contracts.Validation +namespace KDVManager.Services.CRM.Application.Contracts.Validation { public class ValidationError { diff --git a/src/Services/CRM/Application/Features/Children/Commands/CreateChild/CreateChildCommandValidator.cs b/src/Services/CRM/Application/Features/Children/Commands/CreateChild/CreateChildCommandValidator.cs index 4ab6ec7e..432079bf 100644 --- a/src/Services/CRM/Application/Features/Children/Commands/CreateChild/CreateChildCommandValidator.cs +++ b/src/Services/CRM/Application/Features/Children/Commands/CreateChild/CreateChildCommandValidator.cs @@ -1,8 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using FluentValidation; -using KDVManager.Services.CRM.Application.Contracts.Persistence; +using FluentValidation; namespace KDVManager.Services.CRM.Application.Features.Children.Commands.CreateChild; diff --git a/src/Services/CRM/Application/Features/Children/Commands/DeleteChild/DeleteChildCommandHandler.cs b/src/Services/CRM/Application/Features/Children/Commands/DeleteChild/DeleteChildCommandHandler.cs index b22fd1d1..6a7c857c 100644 --- a/src/Services/CRM/Application/Features/Children/Commands/DeleteChild/DeleteChildCommandHandler.cs +++ b/src/Services/CRM/Application/Features/Children/Commands/DeleteChild/DeleteChildCommandHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Persistence; diff --git a/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandHandler.cs b/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandHandler.cs index 189cc57a..9e58109f 100644 --- a/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandHandler.cs +++ b/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandHandler.cs @@ -1,9 +1,7 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Persistence; -using KDVManager.Services.CRM.Application.Features.Children.Commands.CreateChild; using KDVManager.Services.CRM.Domain.Entities; using MediatR; diff --git a/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandValidator.cs b/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandValidator.cs index fe0df7d0..9650f1c0 100644 --- a/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandValidator.cs +++ b/src/Services/CRM/Application/Features/Children/Commands/UpdateChild/UpdateChildCommandValidator.cs @@ -1,8 +1,4 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using FluentValidation; -using KDVManager.Services.CRM.Application.Contracts.Persistence; +using FluentValidation; namespace KDVManager.Services.CRM.Application.Features.Children.Commands.UpdateChild; diff --git a/src/Services/CRM/Application/Features/Children/Queries/GetChildDetail/GetChildDetailQueryHandler.cs b/src/Services/CRM/Application/Features/Children/Queries/GetChildDetail/GetChildDetailQueryHandler.cs index 074fc780..bfe306a8 100644 --- a/src/Services/CRM/Application/Features/Children/Queries/GetChildDetail/GetChildDetailQueryHandler.cs +++ b/src/Services/CRM/Application/Features/Children/Queries/GetChildDetail/GetChildDetailQueryHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Persistence; diff --git a/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQuery.cs b/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQuery.cs index 0403183e..5e163af9 100644 --- a/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQuery.cs +++ b/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQuery.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using KDVManager.Services.CRM.Domain; -using KDVManager.Services.CRM.Application.Contracts.Pagination; +using KDVManager.Services.CRM.Application.Contracts.Pagination; using MediatR; namespace KDVManager.Services.CRM.Application.Features.Children.Queries.GetChildList diff --git a/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQueryHandler.cs b/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQueryHandler.cs index 233f6214..18bac5ff 100644 --- a/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQueryHandler.cs +++ b/src/Services/CRM/Application/Features/Children/Queries/GetChildList/GetChildListQueryHandler.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Persistence; using KDVManager.Services.CRM.Application.Contracts.Pagination; -using KDVManager.Services.CRM.Domain.Entities; using MediatR; namespace KDVManager.Services.CRM.Application.Features.Children.Queries.GetChildList; diff --git a/src/Services/CRM/Application/Features/People/Commands/AddPerson/AddPersonCommandValidator.cs b/src/Services/CRM/Application/Features/People/Commands/AddPerson/AddPersonCommandValidator.cs index 3a4172f0..a46a16cd 100644 --- a/src/Services/CRM/Application/Features/People/Commands/AddPerson/AddPersonCommandValidator.cs +++ b/src/Services/CRM/Application/Features/People/Commands/AddPerson/AddPersonCommandValidator.cs @@ -1,5 +1,4 @@ -using System; -using FluentValidation; +using FluentValidation; namespace KDVManager.Services.CRM.Application.Features.People.Commands.AddPerson; diff --git a/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQuery.cs b/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQuery.cs index 79393356..54ae3eb0 100644 --- a/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQuery.cs +++ b/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQuery.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using KDVManager.Services.CRM.Domain; -using KDVManager.Services.CRM.Application.Contracts.Pagination; +using KDVManager.Services.CRM.Application.Contracts.Pagination; using MediatR; namespace KDVManager.Services.CRM.Application.Features.People.Queries.GetPersonList; diff --git a/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQueryHandler.cs b/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQueryHandler.cs index 3367091b..fc877394 100644 --- a/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQueryHandler.cs +++ b/src/Services/CRM/Application/Features/People/Queries/GetPersonList/GetPersonListQueryHandler.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Persistence; using KDVManager.Services.CRM.Application.Contracts.Pagination; -using KDVManager.Services.CRM.Domain.Entities; using MediatR; namespace KDVManager.Services.CRM.Application.Features.People.Queries.GetPersonList; diff --git a/src/Services/CRM/Application/Profiles/MappingProfile.cs b/src/Services/CRM/Application/Profiles/MappingProfile.cs index d0ac01ca..c2086896 100644 --- a/src/Services/CRM/Application/Profiles/MappingProfile.cs +++ b/src/Services/CRM/Application/Profiles/MappingProfile.cs @@ -1,5 +1,4 @@ -using System; -using AutoMapper; +using AutoMapper; using KDVManager.Services.CRM.Application.Contracts.Pagination; using KDVManager.Services.CRM.Application.Features.Children.Commands.CreateChild; using KDVManager.Services.CRM.Application.Features.Children.Commands.UpdateChild; diff --git a/src/Services/CRM/Infrastructure/ChildManagementDbContext.cs b/src/Services/CRM/Infrastructure/ChildManagementDbContext.cs index 42498bb2..51a6f6ef 100644 --- a/src/Services/CRM/Infrastructure/ChildManagementDbContext.cs +++ b/src/Services/CRM/Infrastructure/ChildManagementDbContext.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.CRM.Domain.Entities; +using KDVManager.Services.CRM.Domain.Entities; using KDVManager.Services.CRM.Application.Contracts.Services; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; diff --git a/src/Services/CRM/Infrastructure/Configurations/ChildConfiguration.cs b/src/Services/CRM/Infrastructure/Configurations/ChildConfiguration.cs index eace52d7..2d9b12c4 100644 --- a/src/Services/CRM/Infrastructure/Configurations/ChildConfiguration.cs +++ b/src/Services/CRM/Infrastructure/Configurations/ChildConfiguration.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.CRM.Domain.Entities; +using KDVManager.Services.CRM.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Services/CRM/Infrastructure/ConfigureServices.cs b/src/Services/CRM/Infrastructure/ConfigureServices.cs index bb719b2e..ac8f6caa 100644 --- a/src/Services/CRM/Infrastructure/ConfigureServices.cs +++ b/src/Services/CRM/Infrastructure/ConfigureServices.cs @@ -1,10 +1,8 @@ -using System; -using KDVManager.Services.CRM.Application.Contracts.Persistence; +using KDVManager.Services.CRM.Application.Contracts.Persistence; using KDVManager.Services.CRM.Infrastructure; using KDVManager.Services.CRM.Infrastructure.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/Services/CRM/Infrastructure/MigrationDbContext.cs b/src/Services/CRM/Infrastructure/MigrationDbContext.cs index d1a1984a..a023410e 100644 --- a/src/Services/CRM/Infrastructure/MigrationDbContext.cs +++ b/src/Services/CRM/Infrastructure/MigrationDbContext.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.CRM.Domain.Entities; +using KDVManager.Services.CRM.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace KDVManager.Services.CRM.Infrastructure diff --git a/src/Services/CRM/Infrastructure/Repositories/PersonRepository.cs b/src/Services/CRM/Infrastructure/Repositories/PersonRepository.cs index 6a5a9ee1..aac4276d 100644 --- a/src/Services/CRM/Infrastructure/Repositories/PersonRepository.cs +++ b/src/Services/CRM/Infrastructure/Repositories/PersonRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; using KDVManager.Services.CRM.Application.Contracts.Persistence; diff --git a/src/Services/Scheduling/Api/Controllers/GroupsController.cs b/src/Services/Scheduling/Api/Controllers/GroupsController.cs index 440e8c66..19ca2422 100644 --- a/src/Services/Scheduling/Api/Controllers/GroupsController.cs +++ b/src/Services/Scheduling/Api/Controllers/GroupsController.cs @@ -1,17 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using KDVManager.Services.Scheduling.Application.Features.Groups.Commands.AddGroup; +using KDVManager.Services.Scheduling.Application.Features.Groups.Commands.AddGroup; using KDVManager.Services.Scheduling.Application.Features.Groups.Queries.ListGroups; using MediatR; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Authorization; using KDVManager.Services.Scheduling.Application.Contracts.Pagination; using System.Net; -using KDVManager.Services.Scheduling.Application.Contracts.Persistence; -using System.Net.Mime; using KDVManager.Services.Scheduling.Application.Features.Groups.Commands.DeleteGroup; namespace KDVManager.Services.Scheduling.Api.Controllers; diff --git a/src/Services/Scheduling/Api/Controllers/TimeSlotsController.cs b/src/Services/Scheduling/Api/Controllers/TimeSlotsController.cs new file mode 100644 index 00000000..8f9db7e8 --- /dev/null +++ b/src/Services/Scheduling/Api/Controllers/TimeSlotsController.cs @@ -0,0 +1,39 @@ +using KDVManager.Services.Scheduling.Application.Features.TimeSlots.Commands.AddTimeSlot; +using KDVManager.Services.Scheduling.Application.Features.TimeSlots.Queries.ListTimeSlots; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using KDVManager.Services.Scheduling.Application.Contracts.Pagination; +using System.Net; + +namespace KDVManager.Services.Scheduling.Api.Controllers; + +[ApiController] +[Route("v1/[controller]")] +public class TimeSlotsController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly ILogger _logger; + + public TimeSlotsController(IMediator mediator, ILogger logger) + { + _logger = logger; + _mediator = mediator; + } + + [HttpGet("", Name = "ListTimeSlots")] + public async Task>> ListTimeSlots([FromQuery] ListTimeSlotsQuery listTimeSlotsQuery) + { + var dtos = await _mediator.Send(listTimeSlotsQuery); + Response.Headers.Add("x-Total", dtos.TotalCount.ToString()); + return Ok(dtos); + } + + [HttpPost(Name = "AddTimeSlot")] + [ProducesResponseType(typeof(Guid), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(UnprocessableEntityResponse), (int)HttpStatusCode.UnprocessableEntity)] + public async Task> AddTimeSlot([FromBody] AddTimeSlotCommand addTimeSlotCommand) + { + var id = await _mediator.Send(addTimeSlotCommand); + return Ok(id); + } +} diff --git a/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddleware.cs b/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddleware.cs index d4a12218..fc96bcb1 100644 --- a/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddleware.cs @@ -1,10 +1,6 @@ -using System; -using System.Net; -using System.Threading.Tasks; +using System.Net; using KDVManager.Services.Scheduling.Application.Exceptions; using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.AspNetCore.Http; namespace KDVManager.Services.Scheduling.Api.Middleware; diff --git a/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs b/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs index c0a3ec32..5f372dd1 100644 --- a/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs +++ b/src/Services/Scheduling/Api/Middleware/ExceptionHandlerMiddlewareExtension.cs @@ -1,7 +1,4 @@ -using System; -using Microsoft.AspNetCore.Builder; - -namespace KDVManager.Services.Scheduling.Api.Middleware; +namespace KDVManager.Services.Scheduling.Api.Middleware; public static class ExceptionHandlerMiddlewareExtension { diff --git a/src/Services/Scheduling/Api/Program.cs b/src/Services/Scheduling/Api/Program.cs index 15d969f8..f8ec5240 100644 --- a/src/Services/Scheduling/Api/Program.cs +++ b/src/Services/Scheduling/Api/Program.cs @@ -1,6 +1,4 @@ using KDVManager.Services.Scheduling.Api.Middleware; -using KDVManager.Services.Scheduling.Application; -using KDVManager.Services.Scheduling.Infrastructure; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Services/Scheduling/Api/Responses/UnprocessableEntityResponse.cs b/src/Services/Scheduling/Api/Responses/UnprocessableEntityResponse.cs index fdd90ce4..3c3edf82 100644 --- a/src/Services/Scheduling/Api/Responses/UnprocessableEntityResponse.cs +++ b/src/Services/Scheduling/Api/Responses/UnprocessableEntityResponse.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using KDVManager.Services.Scheduling.Application.Exceptions; using ValidationException = KDVManager.Services.Scheduling.Application.Exceptions.ValidationException; public class ValidationError diff --git a/src/Services/Scheduling/Api/Services/TenantService/TenantService.cs b/src/Services/Scheduling/Api/Services/TenantService/TenantService.cs index 699d94fc..8c2d25fc 100644 --- a/src/Services/Scheduling/Api/Services/TenantService/TenantService.cs +++ b/src/Services/Scheduling/Api/Services/TenantService/TenantService.cs @@ -1,7 +1,4 @@ -using System; using KDVManager.Services.Scheduling.Application.Contracts.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; using KDVManager.Services.Scheduling.Application.Exceptions; namespace KDVManager.Services.Scheduling.Api.Services; diff --git a/src/Services/Scheduling/Application/ConfigureServices.cs b/src/Services/Scheduling/Application/ConfigureServices.cs index c53755ef..d8c82be1 100644 --- a/src/Services/Scheduling/Application/ConfigureServices.cs +++ b/src/Services/Scheduling/Application/ConfigureServices.cs @@ -1,7 +1,5 @@ -using System; -using System.Reflection; +using System.Reflection; using MediatR; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection; diff --git a/src/Services/Scheduling/Application/Contracts/Pagination/PageParameters.cs b/src/Services/Scheduling/Application/Contracts/Pagination/PageParameters.cs index 8e280a2b..cb248600 100644 --- a/src/Services/Scheduling/Application/Contracts/Pagination/PageParameters.cs +++ b/src/Services/Scheduling/Application/Contracts/Pagination/PageParameters.cs @@ -1,5 +1,4 @@ using KDVManager.Services.Scheduling.Domain.Interfaces; -using MediatR; namespace KDVManager.Services.Scheduling.Application.Contracts.Pagination; diff --git a/src/Services/Scheduling/Application/Contracts/Persistence/IGroupRepository.cs b/src/Services/Scheduling/Application/Contracts/Persistence/IGroupRepository.cs index c773fab4..d1e26194 100644 --- a/src/Services/Scheduling/Application/Contracts/Persistence/IGroupRepository.cs +++ b/src/Services/Scheduling/Application/Contracts/Persistence/IGroupRepository.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading.Tasks; -using KDVManager.Services.Scheduling.Application.Contracts.Persistence; using KDVManager.Services.Scheduling.Domain.Entities; using KDVManager.Services.Scheduling.Domain.Interfaces; diff --git a/src/Services/Scheduling/Application/Contracts/Persistence/ITimeSlotRepository.cs b/src/Services/Scheduling/Application/Contracts/Persistence/ITimeSlotRepository.cs new file mode 100644 index 00000000..c6f7283a --- /dev/null +++ b/src/Services/Scheduling/Application/Contracts/Persistence/ITimeSlotRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using KDVManager.Services.Scheduling.Domain.Entities; +using KDVManager.Services.Scheduling.Domain.Interfaces; + +namespace KDVManager.Services.Scheduling.Application.Contracts.Persistence; + +public interface ITimeSlotRepository : IAsyncRepository +{ + Task> PagedAsync(IPaginationFilter paginationFilter); + + Task CountAsync(); + + Task IsTimeSlotNameUnique(string name); +} + diff --git a/src/Services/Scheduling/Application/Contracts/Validation/ValidationError.cs b/src/Services/Scheduling/Application/Contracts/Validation/ValidationError.cs index 1470d2bf..d8f676ea 100644 --- a/src/Services/Scheduling/Application/Contracts/Validation/ValidationError.cs +++ b/src/Services/Scheduling/Application/Contracts/Validation/ValidationError.cs @@ -1,5 +1,4 @@ -using System; -namespace KDVManager.Services.Scheduling.Application.Contracts.Validation; +namespace KDVManager.Services.Scheduling.Application.Contracts.Validation; public class ValidationError { diff --git a/src/Services/Scheduling/Application/Features/Groups/Commands/AddGroup/AddGroupCommandValidator.cs b/src/Services/Scheduling/Application/Features/Groups/Commands/AddGroup/AddGroupCommandValidator.cs index 0c152bd2..0e8bfd0f 100644 --- a/src/Services/Scheduling/Application/Features/Groups/Commands/AddGroup/AddGroupCommandValidator.cs +++ b/src/Services/Scheduling/Application/Features/Groups/Commands/AddGroup/AddGroupCommandValidator.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using FluentValidation; using KDVManager.Services.Scheduling.Application.Contracts.Persistence; diff --git a/src/Services/Scheduling/Application/Features/Groups/Commands/DeleteGroup/DeleteGroupCommandHandler.cs b/src/Services/Scheduling/Application/Features/Groups/Commands/DeleteGroup/DeleteGroupCommandHandler.cs index 0b62e3f8..d6ff0a18 100644 --- a/src/Services/Scheduling/Application/Features/Groups/Commands/DeleteGroup/DeleteGroupCommandHandler.cs +++ b/src/Services/Scheduling/Application/Features/Groups/Commands/DeleteGroup/DeleteGroupCommandHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.Scheduling.Application.Contracts.Persistence; diff --git a/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQuery.cs b/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQuery.cs index 8e332f60..442f662d 100644 --- a/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQuery.cs +++ b/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQuery.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using KDVManager.Services.Scheduling.Domain; -using KDVManager.Services.Scheduling.Application.Contracts.Pagination; +using KDVManager.Services.Scheduling.Application.Contracts.Pagination; using MediatR; namespace KDVManager.Services.Scheduling.Application.Features.Groups.Queries.ListGroups; diff --git a/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQueryHandler.cs b/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQueryHandler.cs index db61d729..51756a4d 100644 --- a/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQueryHandler.cs +++ b/src/Services/Scheduling/Application/Features/Groups/Queries/ListGroups/ListGroupsQueryHandler.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using AutoMapper; using KDVManager.Services.Scheduling.Application.Contracts.Persistence; using KDVManager.Services.Scheduling.Application.Contracts.Pagination; -using KDVManager.Services.Scheduling.Domain.Entities; using MediatR; namespace KDVManager.Services.Scheduling.Application.Features.Groups.Queries.ListGroups; diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommand.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommand.cs new file mode 100644 index 00000000..dadc49ae --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommand.cs @@ -0,0 +1,13 @@ +using System; +using MediatR; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Commands.AddTimeSlot; + +public class AddTimeSlotCommand : IRequest +{ + public string Name { get; set; } + + public TimeOnly StartTime { get; set; } + + public TimeOnly EndTime { get; set; } +} diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandHandler.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandHandler.cs new file mode 100644 index 00000000..fcd92cdc --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandHandler.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using KDVManager.Services.Scheduling.Application.Contracts.Persistence; +using KDVManager.Services.Scheduling.Domain.Entities; +using MediatR; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Commands.AddTimeSlot; + +public class AddTimeSlotCommandHandler : IRequestHandler +{ + private readonly ITimeSlotRepository _timeSlotRepository; + private readonly IMapper _mapper; + + public AddTimeSlotCommandHandler(ITimeSlotRepository timeSlotRepository, IMapper mapper) + { + _timeSlotRepository = timeSlotRepository; + _mapper = mapper; + } + + public async Task Handle(AddTimeSlotCommand request, CancellationToken cancellationToken) + { + var validator = new AddTimeSlotCommandValidator(_timeSlotRepository); + var validationResult = await validator.ValidateAsync(request); + + if (!validationResult.IsValid) + throw new Exceptions.ValidationException(validationResult); + + var timeSlot = _mapper.Map(request); + + timeSlot = await _timeSlotRepository.AddAsync(timeSlot); + + return timeSlot.Id; + } +} + diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs new file mode 100644 index 00000000..f4a2bec3 --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using FluentValidation; +using KDVManager.Services.Scheduling.Application.Contracts.Persistence; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Commands.AddTimeSlot; + +public class AddTimeSlotCommandValidator : AbstractValidator +{ + private readonly ITimeSlotRepository _timeSlotRepository; + + public AddTimeSlotCommandValidator(ITimeSlotRepository timeSlotRepository) + { + _timeSlotRepository = timeSlotRepository; + + RuleFor(addTimeSlotCommand => addTimeSlotCommand.Name) + .NotEmpty() + .NotNull() + .MaximumLength(25); + + RuleFor(addTimeSlotCommand => addTimeSlotCommand.Name) + .MustAsync(TimeSlotNameUnique) + .WithErrorCode("TSNU001") + .WithMessage("An group with the same name already exists."); + } + + private async Task TimeSlotNameUnique(string name, CancellationToken token) + { + return !(await _timeSlotRepository.IsTimeSlotNameUnique(name)); + } +} diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQuery.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQuery.cs new file mode 100644 index 00000000..edb1518a --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQuery.cs @@ -0,0 +1,9 @@ +using KDVManager.Services.Scheduling.Application.Contracts.Pagination; +using MediatR; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Queries.ListTimeSlots; + +public class ListTimeSlotsQuery : PageParameters, IRequest> +{ +} + diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQueryHandler.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQueryHandler.cs new file mode 100644 index 00000000..d439e900 --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/ListTimeSlotsQueryHandler.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AutoMapper; +using KDVManager.Services.Scheduling.Application.Contracts.Persistence; +using KDVManager.Services.Scheduling.Application.Contracts.Pagination; +using MediatR; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Queries.ListTimeSlots; + +public class ListTimeSlotsQueryHandler : IRequestHandler> +{ + private readonly ITimeSlotRepository _timeSlotRepository; + private readonly IMapper _mapper; + + public ListTimeSlotsQueryHandler(IMapper mapper, ITimeSlotRepository timeSlotRepository) + { + _timeSlotRepository = timeSlotRepository; + _mapper = mapper; + } + + public async Task> Handle(ListTimeSlotsQuery request, CancellationToken cancellationToken) + { + var groups = await _timeSlotRepository.PagedAsync(request); + var count = await _timeSlotRepository.CountAsync(); + + List timeSlotListVMs = _mapper.Map>(groups); + + return new PagedList(timeSlotListVMs, count); + } +} + diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/TimeSlotListVM.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/TimeSlotListVM.cs new file mode 100644 index 00000000..8d2e4f23 --- /dev/null +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Queries/ListTimeSlots/TimeSlotListVM.cs @@ -0,0 +1,11 @@ +using System; + +namespace KDVManager.Services.Scheduling.Application.Features.TimeSlots.Queries.ListTimeSlots; + +public class TimeSlotListVM +{ + public Guid Id { get; set; } + public string Name { get; set; } + public TimeOnly StartTime { get; set; } + public TimeOnly EndTime { get; set; } +} diff --git a/src/Services/Scheduling/Application/Profiles/MappingProfile.cs b/src/Services/Scheduling/Application/Profiles/MappingProfile.cs index a09ecef8..4c70a424 100644 --- a/src/Services/Scheduling/Application/Profiles/MappingProfile.cs +++ b/src/Services/Scheduling/Application/Profiles/MappingProfile.cs @@ -1,8 +1,9 @@ -using System; -using AutoMapper; +using AutoMapper; using KDVManager.Services.Scheduling.Application.Contracts.Pagination; using KDVManager.Services.Scheduling.Application.Features.Groups.Commands.AddGroup; using KDVManager.Services.Scheduling.Application.Features.Groups.Queries.ListGroups; +using KDVManager.Services.Scheduling.Application.Features.TimeSlots.Commands.AddTimeSlot; +using KDVManager.Services.Scheduling.Application.Features.TimeSlots.Queries.ListTimeSlots; using KDVManager.Services.Scheduling.Domain.Entities; namespace KDVManager.Services.Scheduling.Application.Profiles; @@ -14,6 +15,10 @@ public MappingProfile() CreateMap(); CreateMap().ReverseMap(); CreateMap(); + + CreateMap(); + CreateMap().ReverseMap(); + CreateMap(); } } diff --git a/src/Services/Scheduling/Domain/Entities/TimeSlot.cs b/src/Services/Scheduling/Domain/Entities/TimeSlot.cs new file mode 100644 index 00000000..57913455 --- /dev/null +++ b/src/Services/Scheduling/Domain/Entities/TimeSlot.cs @@ -0,0 +1,17 @@ +using System; +using KDVManager.Services.Scheduling.Domain.Interfaces; + +namespace KDVManager.Services.Scheduling.Domain.Entities; + +public class TimeSlot : IMustHaveTenant +{ + public Guid Id { get; set; } + + public Guid TenantId { get; set; } + + public string Name { get; set; } + + public TimeOnly StartTime { get; set; } + + public TimeOnly EndTime { get; set; } +} diff --git a/src/Services/Scheduling/Infrastructure/Configurations/GroupConfiguration.cs b/src/Services/Scheduling/Infrastructure/Configurations/GroupConfiguration.cs index af9fd201..cdf9bd9d 100644 --- a/src/Services/Scheduling/Infrastructure/Configurations/GroupConfiguration.cs +++ b/src/Services/Scheduling/Infrastructure/Configurations/GroupConfiguration.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.Scheduling.Domain.Entities; +using KDVManager.Services.Scheduling.Domain.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Services/Scheduling/Infrastructure/ConfigureServices.cs b/src/Services/Scheduling/Infrastructure/ConfigureServices.cs index 9be337af..a0ecb9a3 100644 --- a/src/Services/Scheduling/Infrastructure/ConfigureServices.cs +++ b/src/Services/Scheduling/Infrastructure/ConfigureServices.cs @@ -1,10 +1,8 @@ -using System; -using KDVManager.Services.Scheduling.Application.Contracts.Persistence; +using KDVManager.Services.Scheduling.Application.Contracts.Persistence; using KDVManager.Services.Scheduling.Infrastructure; using KDVManager.Services.Scheduling.Infrastructure.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.Extensions.DependencyInjection; @@ -19,6 +17,7 @@ public static IServiceCollection AddInfrastructureServices(this IServiceCollecti options.UseNpgsql(configuration.GetConnectionString("KDVManagerSchedulingConnectionString"))); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Services/Scheduling/Infrastructure/MigrationDbContext.cs b/src/Services/Scheduling/Infrastructure/MigrationDbContext.cs index ec703281..36e816dd 100644 --- a/src/Services/Scheduling/Infrastructure/MigrationDbContext.cs +++ b/src/Services/Scheduling/Infrastructure/MigrationDbContext.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.Scheduling.Domain.Entities; +using KDVManager.Services.Scheduling.Domain.Entities; using Microsoft.EntityFrameworkCore; using System.Reflection; @@ -13,6 +12,8 @@ public MigrationDbContext(DbContextOptions options) : base(o public DbSet Groups { get; set; } + public DbSet TimeSlots { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); diff --git a/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.Designer.cs b/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.Designer.cs new file mode 100644 index 00000000..056944df --- /dev/null +++ b/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.Designer.cs @@ -0,0 +1,71 @@ +// +using System; +using KDVManager.Services.Scheduling.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(MigrationDbContext))] + [Migration("20240726205309_AddTimeSlots")] + partial class AddTimeSlots + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("KDVManager.Services.Scheduling.Domain.Entities.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("KDVManager.Services.Scheduling.Domain.Entities.TimeSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TimeSlots"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.cs b/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.cs new file mode 100644 index 00000000..cd8142ef --- /dev/null +++ b/src/Services/Scheduling/Infrastructure/Migrations/20240726205309_AddTimeSlots.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class AddTimeSlots : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TimeSlots", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TenantId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: true), + StartTime = table.Column(type: "time without time zone", nullable: false), + EndTime = table.Column(type: "time without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TimeSlots", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TimeSlots"); + } + } +} diff --git a/src/Services/Scheduling/Infrastructure/Migrations/MigrationDbContextModelSnapshot.cs b/src/Services/Scheduling/Infrastructure/Migrations/MigrationDbContextModelSnapshot.cs index ffae097c..158714dd 100644 --- a/src/Services/Scheduling/Infrastructure/Migrations/MigrationDbContextModelSnapshot.cs +++ b/src/Services/Scheduling/Infrastructure/Migrations/MigrationDbContextModelSnapshot.cs @@ -39,6 +39,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Groups"); }); + + modelBuilder.Entity("KDVManager.Services.Scheduling.Domain.Entities.TimeSlot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("TenantId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TimeSlots"); + }); #pragma warning restore 612, 618 } } diff --git a/src/Services/Scheduling/Infrastructure/Repositories/GroupRepository.cs b/src/Services/Scheduling/Infrastructure/Repositories/GroupRepository.cs index 766c7def..aec52ba8 100644 --- a/src/Services/Scheduling/Infrastructure/Repositories/GroupRepository.cs +++ b/src/Services/Scheduling/Infrastructure/Repositories/GroupRepository.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using System.Collections.Generic; using System.Threading.Tasks; using KDVManager.Services.Scheduling.Application.Contracts.Persistence; diff --git a/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs b/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs new file mode 100644 index 00000000..77466644 --- /dev/null +++ b/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using KDVManager.Services.Scheduling.Application.Contracts.Persistence; +using KDVManager.Services.Scheduling.Domain.Entities; +using KDVManager.Services.Scheduling.Domain.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace KDVManager.Services.Scheduling.Infrastructure.Repositories; + +public class TimeSlotRepository : BaseRepository, ITimeSlotRepository +{ + public TimeSlotRepository(SchedulingDbContext dbContext) : base(dbContext) + { + } + + public async Task> PagedAsync(IPaginationFilter paginationFilter) + { + int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize; + + return await _dbContext.Set() + .OrderBy(timeSlot => timeSlot.Name) + .Skip((paginationFilter.PageNumber - 1) * paginationFilter.PageSize).Take(paginationFilter.PageSize) + .ToListAsync(); + } + + public async Task CountAsync() + { + return await _dbContext.Set().CountAsync(); + } + + public async Task IsTimeSlotNameUnique(string name) + { + var matches = _dbContext.TimeSlots.Any(e => e.Name.Equals(name)); + return await Task.FromResult(matches); + } +} diff --git a/src/Services/Scheduling/Infrastructure/SchedulingDbContext.cs b/src/Services/Scheduling/Infrastructure/SchedulingDbContext.cs index 32320c96..5216e2c6 100644 --- a/src/Services/Scheduling/Infrastructure/SchedulingDbContext.cs +++ b/src/Services/Scheduling/Infrastructure/SchedulingDbContext.cs @@ -1,5 +1,4 @@ -using System; -using KDVManager.Services.Scheduling.Domain.Entities; +using KDVManager.Services.Scheduling.Domain.Entities; using KDVManager.Services.Scheduling.Application.Contracts.Services; using Microsoft.EntityFrameworkCore; using System.Threading.Tasks; @@ -18,11 +17,13 @@ public SchedulingDbContext(DbContextOptions options, ITenan } public DbSet Groups { get; set; } + public DbSet TimeSlots { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity().HasQueryFilter(a => a.TenantId == _tenantService.Tenant); + modelBuilder.Entity().HasQueryFilter(a => a.TenantId == _tenantService.Tenant); modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } From 71fed69091ff5a181d85908f5673e21919c1ff15 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 27 Jul 2024 10:01:25 +0200 Subject: [PATCH 09/11] chore: Update to net8.0 Update the application to .net8. --- src/Services/CRM/Api/Api.csproj | 8 ++++---- src/Services/CRM/Api/Dockerfile | 4 ++-- src/Services/CRM/Application/Application.csproj | 10 +++++----- src/Services/CRM/Domain/Domain.csproj | 2 +- src/Services/CRM/Infrastructure/Dockerfile | 6 +++--- src/Services/CRM/Infrastructure/Infrastructure.csproj | 6 +++--- src/Services/Scheduling/Api/Api.csproj | 8 ++++---- src/Services/Scheduling/Api/Dockerfile | 4 ++-- src/Services/Scheduling/Application/Application.csproj | 10 +++++----- src/Services/Scheduling/Domain/Domain.csproj | 2 +- src/Services/Scheduling/Infrastructure/Dockerfile | 6 +++--- .../Scheduling/Infrastructure/Infrastructure.csproj | 6 +++--- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/src/Services/CRM/Api/Api.csproj b/src/Services/CRM/Api/Api.csproj index 5a60217e..701c08d6 100644 --- a/src/Services/CRM/Api/Api.csproj +++ b/src/Services/CRM/Api/Api.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable ../../../docker-compose.dcproj @@ -21,11 +21,11 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Services/CRM/Api/Dockerfile b/src/Services/CRM/Api/Dockerfile index eed39506..000d0658 100644 --- a/src/Services/CRM/Api/Dockerfile +++ b/src/Services/CRM/Api/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Services/CRM/Api/Api.csproj", "Services/CRM/Api/"] COPY ["Services/CRM/Application/Application.csproj", "Services/CRM/Application/"] diff --git a/src/Services/CRM/Application/Application.csproj b/src/Services/CRM/Application/Application.csproj index 808047ae..009f8e6d 100644 --- a/src/Services/CRM/Application/Application.csproj +++ b/src/Services/CRM/Application/Application.csproj @@ -1,6 +1,6 @@ - net7.0 + net8.0 @@ -12,9 +12,9 @@ - - - - + + + + \ No newline at end of file diff --git a/src/Services/CRM/Domain/Domain.csproj b/src/Services/CRM/Domain/Domain.csproj index 8cc99390..8dcf2d7d 100644 --- a/src/Services/CRM/Domain/Domain.csproj +++ b/src/Services/CRM/Domain/Domain.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Services/CRM/Infrastructure/Dockerfile b/src/Services/CRM/Infrastructure/Dockerfile index a1ad5d15..4ba2a5ae 100644 --- a/src/Services/CRM/Infrastructure/Dockerfile +++ b/src/Services/CRM/Infrastructure/Dockerfile @@ -1,5 +1,5 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Services/CRM/Api/Api.csproj", "Services/CRM/Api/"] COPY ["Services/CRM/Application/Application.csproj", "Services/CRM/Application/"] @@ -9,11 +9,11 @@ RUN dotnet restore "Services/CRM/Api/Api.csproj" COPY . . WORKDIR "/src/Services/CRM/Infrastructure" -RUN dotnet tool install -g dotnet-ef --version 7.0.0 +RUN dotnet tool install -g dotnet-ef --version 8.0.0 ENV PATH $PATH:/root/.dotnet/tools RUN dotnet ef migrations bundle --self-contained -r linux-x64 --startup-project "../Api" --context MigrationDbContext -FROM ubuntu:22.10 AS migrator +FROM ubuntu:24.04 AS migrator RUN apt-get update && apt-get install -qqq libicu-dev libssl-dev diff --git a/src/Services/CRM/Infrastructure/Infrastructure.csproj b/src/Services/CRM/Infrastructure/Infrastructure.csproj index d049d41b..93e3d38b 100644 --- a/src/Services/CRM/Infrastructure/Infrastructure.csproj +++ b/src/Services/CRM/Infrastructure/Infrastructure.csproj @@ -1,12 +1,12 @@ - net7.0 + net8.0 - - + + diff --git a/src/Services/Scheduling/Api/Api.csproj b/src/Services/Scheduling/Api/Api.csproj index 5a60217e..701c08d6 100644 --- a/src/Services/Scheduling/Api/Api.csproj +++ b/src/Services/Scheduling/Api/Api.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable ../../../docker-compose.dcproj @@ -21,11 +21,11 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Services/Scheduling/Api/Dockerfile b/src/Services/Scheduling/Api/Dockerfile index eb6a2a19..56bcb0af 100644 --- a/src/Services/Scheduling/Api/Dockerfile +++ b/src/Services/Scheduling/Api/Dockerfile @@ -1,11 +1,11 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Services/Scheduling/Api/Api.csproj", "Services/Scheduling/Api/"] COPY ["Services/Scheduling/Application/Application.csproj", "Services/Scheduling/Application/"] diff --git a/src/Services/Scheduling/Application/Application.csproj b/src/Services/Scheduling/Application/Application.csproj index 1bdf7235..50daf2be 100644 --- a/src/Services/Scheduling/Application/Application.csproj +++ b/src/Services/Scheduling/Application/Application.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 @@ -14,9 +14,9 @@ - - - - + + + + diff --git a/src/Services/Scheduling/Domain/Domain.csproj b/src/Services/Scheduling/Domain/Domain.csproj index 8cc99390..8dcf2d7d 100644 --- a/src/Services/Scheduling/Domain/Domain.csproj +++ b/src/Services/Scheduling/Domain/Domain.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 diff --git a/src/Services/Scheduling/Infrastructure/Dockerfile b/src/Services/Scheduling/Infrastructure/Dockerfile index a2ece11b..584ed7fd 100644 --- a/src/Services/Scheduling/Infrastructure/Dockerfile +++ b/src/Services/Scheduling/Infrastructure/Dockerfile @@ -1,5 +1,5 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Services/Scheduling/Api/Api.csproj", "Services/Scheduling/Api/"] COPY ["Services/Scheduling/Application/Application.csproj", "Services/Scheduling/Application/"] @@ -9,11 +9,11 @@ RUN dotnet restore "Services/Scheduling/Api/Api.csproj" COPY . . WORKDIR "/src/Services/Scheduling/Infrastructure" -RUN dotnet tool install -g dotnet-ef --version 7.0.0 +RUN dotnet tool install -g dotnet-ef --version 8.0.0 ENV PATH $PATH:/root/.dotnet/tools RUN dotnet ef migrations bundle --self-contained -r linux-x64 --startup-project "../Api" --context MigrationDbContext -FROM ubuntu:22.10 AS migrator +FROM ubuntu:24.04 AS migrator RUN apt-get update && apt-get install -qqq libicu-dev libssl-dev diff --git a/src/Services/Scheduling/Infrastructure/Infrastructure.csproj b/src/Services/Scheduling/Infrastructure/Infrastructure.csproj index d049d41b..93e3d38b 100644 --- a/src/Services/Scheduling/Infrastructure/Infrastructure.csproj +++ b/src/Services/Scheduling/Infrastructure/Infrastructure.csproj @@ -1,12 +1,12 @@ - net7.0 + net8.0 - - + + From 761d4bc45d1792665c66f5e4a5d928c13b2210c9 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 27 Jul 2024 11:03:28 +0200 Subject: [PATCH 10/11] chore: Add timeslots Add settings timeslots ui to manage timeslots. --- src/web/orval.config.ts | 1 + src/web/output.openapi.json | 190 ++++++++++++++++-- .../api/endpoints/time-slots/time-slots.ts | 163 +++++++++++++++ src/web/src/api/models/addTimeSlotCommand.ts | 13 ++ src/web/src/api/models/listTimeSlotsParams.ts | 11 + src/web/src/api/models/timeSlotListVM.ts | 14 ++ src/web/src/api/models/validationError.ts | 3 + .../features/timeSlots/AddTimeSlotDialog.tsx | 132 ++++++++++++ .../src/features/timeSlots/TimeSlotsTable.tsx | 59 ++++++ src/web/src/pages/children/IndexChildPage.tsx | 10 +- .../pages/settings/SchedulingSettingsPage.tsx | 26 ++- 11 files changed, 598 insertions(+), 24 deletions(-) create mode 100644 src/web/src/api/endpoints/time-slots/time-slots.ts create mode 100644 src/web/src/api/models/addTimeSlotCommand.ts create mode 100644 src/web/src/api/models/listTimeSlotsParams.ts create mode 100644 src/web/src/api/models/timeSlotListVM.ts create mode 100644 src/web/src/features/timeSlots/AddTimeSlotDialog.tsx create mode 100644 src/web/src/features/timeSlots/TimeSlotsTable.tsx diff --git a/src/web/orval.config.ts b/src/web/orval.config.ts index ef85bd05..8fadcc97 100644 --- a/src/web/orval.config.ts +++ b/src/web/orval.config.ts @@ -31,6 +31,7 @@ const config: ReturnType = { operations: { GetAllChildren: queryPaginated, ListGroups: queryPaginated, + ListTimeSlots: queryPaginated, GetAllPeople: queryPaginated, }, }, diff --git a/src/web/output.openapi.json b/src/web/output.openapi.json index af1c7084..dc6e68c8 100644 --- a/src/web/output.openapi.json +++ b/src/web/output.openapi.json @@ -40,7 +40,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -94,7 +94,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -136,7 +136,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "application/json": { "schema": { @@ -182,10 +182,10 @@ }, "responses": { "204": { - "description": "Success" + "description": "No Content" }, "422": { - "description": "Client Error", + "description": "Unprocessable Content", "content": { "text/plain": { "schema": { @@ -222,7 +222,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" } } } @@ -251,7 +251,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -305,7 +305,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -354,7 +354,7 @@ ], "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -408,7 +408,7 @@ }, "responses": { "200": { - "description": "Success", + "description": "OK", "content": { "text/plain": { "schema": { @@ -431,7 +431,7 @@ } }, "422": { - "description": "Client Error", + "description": "Unprocessable Content", "content": { "text/plain": { "schema": { @@ -470,7 +470,7 @@ ], "responses": { "204": { - "description": "Success" + "description": "No Content" }, "404": { "description": "Not Found", @@ -484,6 +484,129 @@ } } } + }, + "/scheduling/v1/timeslots": { + "get": { + "tags": ["TimeSlots"], + "operationId": "ListTimeSlots", + "parameters": [ + { + "name": "PageNumber", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "PageSize", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeSlotListVM" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeSlotListVM" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeSlotListVM" + } + } + } + } + } + } + }, + "post": { + "tags": ["TimeSlots"], + "operationId": "AddTimeSlot", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AddTimeSlotCommand" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/AddTimeSlotCommand" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/AddTimeSlotCommand" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string", + "format": "uuid" + } + }, + "application/json": { + "schema": { + "type": "string", + "format": "uuid" + } + }, + "text/json": { + "schema": { + "type": "string", + "format": "uuid" + } + } + } + }, + "422": { + "description": "Unprocessable Content", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityResponse" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityResponse" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/UnprocessableEntityResponse" + } + } + } + } + } + } } }, "components": { @@ -650,12 +773,15 @@ "type": "object", "properties": { "property": { + "minLength": 1, "type": "string" }, "code": { + "minLength": 1, "type": "string" }, "title": { + "minLength": 1, "type": "string" } }, @@ -671,6 +797,24 @@ }, "additionalProperties": false }, + "AddTimeSlotCommand": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true + }, + "startTime": { + "type": "string", + "format": "time" + }, + "endTime": { + "type": "string", + "format": "time" + } + }, + "additionalProperties": false + }, "GroupListVM": { "type": "object", "properties": { @@ -711,6 +855,28 @@ } }, "additionalProperties": {} + }, + "TimeSlotListVM": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "startTime": { + "type": "string", + "format": "time" + }, + "endTime": { + "type": "string", + "format": "time" + } + }, + "additionalProperties": false } } } diff --git a/src/web/src/api/endpoints/time-slots/time-slots.ts b/src/web/src/api/endpoints/time-slots/time-slots.ts new file mode 100644 index 00000000..b358f015 --- /dev/null +++ b/src/web/src/api/endpoints/time-slots/time-slots.ts @@ -0,0 +1,163 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ +import { useMutation, useQuery } from "@tanstack/react-query"; +import type { + MutationFunction, + QueryFunction, + QueryKey, + UseMutationOptions, + UseMutationResult, + UseQueryOptions, + UseQueryResult, +} from "@tanstack/react-query"; +import { useCallback } from "react"; +import type { AddTimeSlotCommand } from "../../models/addTimeSlotCommand"; +import type { ListTimeSlotsParams } from "../../models/listTimeSlotsParams"; +import type { TimeSlotListVM } from "../../models/timeSlotListVM"; +import type { UnprocessableEntityResponse } from "../../models/unprocessableEntityResponse"; +import { useExecuteFetchPaginated } from "../../mutator/useExecuteFetchPaginated"; +import { useExecuteFetch } from "../../mutator/useExecuteFetch"; + +export const useListTimeSlotsHook = () => { + const listTimeSlots = useExecuteFetchPaginated(); + + return useCallback( + (params?: ListTimeSlotsParams, signal?: AbortSignal) => { + return listTimeSlots({ url: `/scheduling/v1/timeslots`, method: "GET", params, signal }); + }, + [listTimeSlots], + ); +}; + +export const getListTimeSlotsQueryKey = (params?: ListTimeSlotsParams) => { + return [`/scheduling/v1/timeslots`, ...(params ? [params] : [])] as const; +}; + +export const useListTimeSlotsQueryOptions = < + TData = Awaited>>, + TError = unknown, +>( + params?: ListTimeSlotsParams, + options?: { + query?: Partial< + UseQueryOptions>>, TError, TData> + >; + }, +) => { + const { query: queryOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListTimeSlotsQueryKey(params); + + const listTimeSlots = useListTimeSlotsHook(); + + const queryFn: QueryFunction>>> = ({ + signal, + }) => listTimeSlots(params, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>>, + TError, + TData + > & { queryKey: QueryKey }; +}; + +export type ListTimeSlotsQueryResult = NonNullable< + Awaited>> +>; +export type ListTimeSlotsQueryError = unknown; + +export const useListTimeSlots = < + TData = Awaited>>, + TError = unknown, +>( + params?: ListTimeSlotsParams, + options?: { + query?: Partial< + UseQueryOptions>>, TError, TData> + >; + }, +): UseQueryResult & { queryKey: QueryKey } => { + const queryOptions = useListTimeSlotsQueryOptions(params, options); + + const query = useQuery(queryOptions) as UseQueryResult & { queryKey: QueryKey }; + + query.queryKey = queryOptions.queryKey; + + return query; +}; + +export const useAddTimeSlotHook = () => { + const addTimeSlot = useExecuteFetch(); + + return useCallback( + (addTimeSlotCommand: AddTimeSlotCommand) => { + return addTimeSlot({ + url: `/scheduling/v1/timeslots`, + method: "POST", + headers: { "Content-Type": "application/json" }, + data: addTimeSlotCommand, + }); + }, + [addTimeSlot], + ); +}; + +export const useAddTimeSlotMutationOptions = < + TError = UnprocessableEntityResponse, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>>, + TError, + { data: AddTimeSlotCommand }, + TContext + >; +}): UseMutationOptions< + Awaited>>, + TError, + { data: AddTimeSlotCommand }, + TContext +> => { + const { mutation: mutationOptions } = options ?? {}; + + const addTimeSlot = useAddTimeSlotHook(); + + const mutationFn: MutationFunction< + Awaited>>, + { data: AddTimeSlotCommand } + > = (props) => { + const { data } = props ?? {}; + + return addTimeSlot(data); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type AddTimeSlotMutationResult = NonNullable< + Awaited>> +>; +export type AddTimeSlotMutationBody = AddTimeSlotCommand; +export type AddTimeSlotMutationError = UnprocessableEntityResponse; + +export const useAddTimeSlot = (options?: { + mutation?: UseMutationOptions< + Awaited>>, + TError, + { data: AddTimeSlotCommand }, + TContext + >; +}): UseMutationResult< + Awaited>>, + TError, + { data: AddTimeSlotCommand }, + TContext +> => { + const mutationOptions = useAddTimeSlotMutationOptions(options); + + return useMutation(mutationOptions); +}; diff --git a/src/web/src/api/models/addTimeSlotCommand.ts b/src/web/src/api/models/addTimeSlotCommand.ts new file mode 100644 index 00000000..83f5d0ca --- /dev/null +++ b/src/web/src/api/models/addTimeSlotCommand.ts @@ -0,0 +1,13 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ + +export type AddTimeSlotCommand = { + endTime?: string; + /** @nullable */ + name?: string | null; + startTime?: string; +}; diff --git a/src/web/src/api/models/listTimeSlotsParams.ts b/src/web/src/api/models/listTimeSlotsParams.ts new file mode 100644 index 00000000..9117d4e2 --- /dev/null +++ b/src/web/src/api/models/listTimeSlotsParams.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ + +export type ListTimeSlotsParams = { + PageNumber?: number; + PageSize?: number; +}; diff --git a/src/web/src/api/models/timeSlotListVM.ts b/src/web/src/api/models/timeSlotListVM.ts new file mode 100644 index 00000000..27f64efc --- /dev/null +++ b/src/web/src/api/models/timeSlotListVM.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v6.31.0 🍺 + * Do not edit manually. + * KDVManager CRM API + * OpenAPI spec version: v1 + */ + +export type TimeSlotListVM = { + endTime?: string; + id?: string; + /** @nullable */ + name?: string | null; + startTime?: string; +}; diff --git a/src/web/src/api/models/validationError.ts b/src/web/src/api/models/validationError.ts index ead21e50..d2c116e7 100644 --- a/src/web/src/api/models/validationError.ts +++ b/src/web/src/api/models/validationError.ts @@ -6,7 +6,10 @@ */ export type ValidationError = { + /** @minLength 1 */ code: string; + /** @minLength 1 */ property: string; + /** @minLength 1 */ title: string; }; diff --git a/src/web/src/features/timeSlots/AddTimeSlotDialog.tsx b/src/web/src/features/timeSlots/AddTimeSlotDialog.tsx new file mode 100644 index 00000000..98fe73eb --- /dev/null +++ b/src/web/src/features/timeSlots/AddTimeSlotDialog.tsx @@ -0,0 +1,132 @@ +import { Controller, type SubmitHandler, useForm } from "react-hook-form"; +import { FormContainer, TextFieldElement } 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 { useSnackbar } from "notistack"; +import { type UnprocessableEntityResponse } from "@api/models/unprocessableEntityResponse"; +import { getListTimeSlotsQueryKey, useAddTimeSlot } from "@api/endpoints/time-slots/time-slots"; +import { type AddTimeSlotCommand } from "@api/models/addTimeSlotCommand"; +import { TimeField } from "@mui/x-date-pickers/TimeField"; +import LoadingButton from "@mui/lab/LoadingButton"; +import dayjs from "dayjs"; + +export const AddTimeSlotDialog = NiceModal.create(() => { + const { t } = useTranslation(); + const modal = useModal(); + const mutate = useAddTimeSlot(); + const queryClient = useQueryClient(); + const formContext = useForm(); + + const { + control, + handleSubmit, + reset, + setError, + formState: { isValid, isDirty, isSubmitting }, + } = formContext; + const { enqueueSnackbar } = useSnackbar(); + + const handleOnCancelClick = () => { + modal.remove(); + reset(); + }; + + const onSubmit: SubmitHandler = async (data) => { + await mutate.mutateAsync( + { data: data }, + { onSuccess: onMutateSuccess, onError: onMutateError }, + ); + }; + + const onMutateSuccess = () => { + void queryClient.invalidateQueries({ queryKey: getListTimeSlotsQueryKey() }); + modal.remove(); + enqueueSnackbar(t("Time slot added"), { variant: "success" }); + reset(); + }; + + const onMutateError = (error: UnprocessableEntityResponse) => { + error.errors.forEach((propertyError) => { + setError(propertyError.property as any, { + type: "server", + message: propertyError.title, + }); + }); + }; + + return ( + + {t("Add time slot")} + + + {t("To add a time slot, please specify the name here and start and end times.")} + + + + ( + { + field.onChange(date?.format("HH:mm:ss")); + }} + fullWidth + /> + )} + /> + ( + { + field.onChange(date?.format("HH:mm:ss")); + }} + fullWidth + /> + )} + /> + + + + + + {t("Add", { ns: "common" })} + + + + ); +}); diff --git a/src/web/src/features/timeSlots/TimeSlotsTable.tsx b/src/web/src/features/timeSlots/TimeSlotsTable.tsx new file mode 100644 index 00000000..60f23916 --- /dev/null +++ b/src/web/src/features/timeSlots/TimeSlotsTable.tsx @@ -0,0 +1,59 @@ +import { type GridColDef } from "@mui/x-data-grid/models/colDef"; +import { DataGrid } from "@mui/x-data-grid/DataGrid"; +import { keepPreviousData } from "@tanstack/react-query"; +import { usePagination } from "@hooks/usePagination"; +import { type GroupListVM } from "@api/models/groupListVM"; +import { useListTimeSlots } from "@api/endpoints/time-slots/time-slots"; +import dayjs from "dayjs"; + +const columns: GridColDef[] = [ + { + field: "name", + headerName: "Name", + flex: 1, + sortable: false, + disableColumnMenu: true, + disableReorder: true, + }, + { + field: "startTime", + headerName: "Start time", + flex: 1, + sortable: false, + disableColumnMenu: true, + disableReorder: true, + valueFormatter: (value) => value && dayjs(value, "HH:mm:ss").format("HH:mm"), + }, + { + field: "endTime", + headerName: "End time", + flex: 1, + sortable: false, + disableColumnMenu: true, + disableReorder: true, + valueFormatter: (value) => value && dayjs(value, "HH:mm:ss").format("HH:mm"), + }, +]; + +const TimeSlotsTable = () => { + const { apiPagination, muiPagination } = usePagination(); + + const { data, isLoading, isFetching } = useListTimeSlots(apiPagination, { + query: { placeholderData: keepPreviousData }, + }); + + return ( + + autoHeight + pageSizeOptions={[5, 10, 20]} + rowCount={data?.meta.total || 0} + loading={isLoading || isFetching} + columns={columns} + rows={data?.value || []} + disableRowSelectionOnClick + {...muiPagination} + /> + ); +}; + +export default TimeSlotsTable; diff --git a/src/web/src/pages/children/IndexChildPage.tsx b/src/web/src/pages/children/IndexChildPage.tsx index ded689bf..587a297b 100644 --- a/src/web/src/pages/children/IndexChildPage.tsx +++ b/src/web/src/pages/children/IndexChildPage.tsx @@ -6,6 +6,7 @@ import AddIcon from "@mui/icons-material/Add"; import { useNavigate } from "react-router-dom"; import { styled } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; +import Box from "@mui/material/Box"; const StyledToolbar = styled(Toolbar)(() => ({ marginLeft: "auto", @@ -20,16 +21,15 @@ const IndexChildPage = () => { }; return ( - <> + + - - - - + + ); }; diff --git a/src/web/src/pages/settings/SchedulingSettingsPage.tsx b/src/web/src/pages/settings/SchedulingSettingsPage.tsx index a4a9c210..4b893032 100644 --- a/src/web/src/pages/settings/SchedulingSettingsPage.tsx +++ b/src/web/src/pages/settings/SchedulingSettingsPage.tsx @@ -1,16 +1,28 @@ +import Box from "@mui/system/Box"; +import TimeSlotsTable from "../../features/timeSlots/TimeSlotsTable"; +import Paper from "@mui/material/Paper"; +import Toolbar from "@mui/material/Toolbar"; +import Button from "@mui/material/Button"; +import AddIcon from "@mui/icons-material/Add"; import { useTranslation } from "react-i18next"; -import { Typography } from "@mui/material"; -import React from "react"; +import NiceModal from "@ebay/nice-modal-react"; +import { AddTimeSlotDialog } from "../../features/timeSlots/AddTimeSlotDialog"; const SchedulingSettingsPage = () => { const { t } = useTranslation(); + const onAddTimeSlotClickHandler = () => void NiceModal.show(AddTimeSlotDialog); + return ( - - - {t("Scheduling settings page")} - - + + + + + + + ); }; From 820326666f137435dd867e942510a5d5fe1d08c0 Mon Sep 17 00:00:00 2001 From: LuukvH Date: Sat, 27 Jul 2024 13:06:32 +0200 Subject: [PATCH 11/11] chore: Sort add validations to timeslot Validate that the StartTime and EndTime are present. And that the Endtime is after the StartTime. --- .../AddTimeSlot/AddTimeSlotCommandValidator.cs | 13 +++++++++++++ .../Repositories/TimeSlotRepository.cs | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs index f4a2bec3..2060e74b 100644 --- a/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs +++ b/src/Services/Scheduling/Application/Features/TimeSlots/Commands/AddTimeSlot/AddTimeSlotCommandValidator.cs @@ -22,6 +22,19 @@ public AddTimeSlotCommandValidator(ITimeSlotRepository timeSlotRepository) .MustAsync(TimeSlotNameUnique) .WithErrorCode("TSNU001") .WithMessage("An group with the same name already exists."); + + RuleFor(addTimeSlotCommand => addTimeSlotCommand.StartTime) + .NotEmpty() + .NotNull(); + + RuleFor(addTimeSlotCommand => addTimeSlotCommand.EndTime) + .NotEmpty() + .NotNull(); + + RuleFor(addTimeSlotCommand => addTimeSlotCommand.EndTime) + .GreaterThan(addTimeSlotCommand => addTimeSlotCommand.StartTime) + .WithErrorCode("TSEV001") + .WithMessage("EndTime must be after StartTime."); } private async Task TimeSlotNameUnique(string name, CancellationToken token) diff --git a/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs b/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs index 77466644..8a0dee66 100644 --- a/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs +++ b/src/Services/Scheduling/Infrastructure/Repositories/TimeSlotRepository.cs @@ -19,7 +19,7 @@ public async Task> PagedAsync(IPaginationFilter paginati int skip = (paginationFilter.PageNumber - 1) * paginationFilter.PageSize; return await _dbContext.Set() - .OrderBy(timeSlot => timeSlot.Name) + .OrderBy(timeSlot => timeSlot.EndTime).ThenBy(timeSlot => timeSlot.StartTime) .Skip((paginationFilter.PageNumber - 1) * paginationFilter.PageSize).Take(paginationFilter.PageSize) .ToListAsync(); }