From 179c5df83d9fd75e6ca52b031f66197d39f064aa Mon Sep 17 00:00:00 2001 From: Lucieo Date: Wed, 21 Feb 2024 10:34:01 +0000 Subject: [PATCH] List companies applications (#73) * List company applications * Company no applications * Fix application form place id --- .../config/policies/is-concerned.js | 17 +++ back/api/application/config/routes.json | 13 ++- .../application/controllers/application.js | 4 +- .../documentation/1.0.0/application.json | 57 +++++++++ .../1.0.0/overrides/application.json | 63 ++++++++++ .../1.0.0/full_documentation.json | 59 +++++++++- web/components/Account/AccountLayout.tsx | 19 ++- web/components/Account/AccountMenu.tsx | 40 ++++--- .../Company/ApplicationCompanyHelper.tsx | 56 +++++++++ .../Company/ApplicationCompanyList.tsx | 108 ++++++++++++++++++ .../Company/ApplicationCompanyListItem.tsx | 76 ++++++++++++ .../Account/Application/ConfirmButton.tsx | 56 +++++++++ .../Place/ApplicationPlaceList.tsx | 108 ++++++++++++++++++ .../Place/ApplicationPlaceListItem.tsx | 76 ++++++++++++ .../Account/Info/InfoCompanyApplications.tsx | 62 ++++++++++ .../Account/Info/InfoPlaceApplications.tsx | 35 ++++++ .../Application/ApplicationConfirmed.tsx | 4 +- .../Places/Application/ApplicationForm.tsx | 6 +- web/hooks/useCurrentUser.tsx | 2 + web/hooks/useMyApplications.ts | 5 +- web/pages/compte/candidatures/index.tsx | 11 +- web/pages/compte/mes-candidatures/index.tsx | 12 +- .../assets/img/companyApplicationsEmpty.svg | 8 ++ .../assets/img/companyApplicationsNext.svg | 20 ++++ web/public/locales/fr/account.json | 19 ++- web/public/locales/fr/application.json | 36 ++++++ web/typings/api.ts | 31 +++++ 27 files changed, 958 insertions(+), 45 deletions(-) create mode 100644 back/api/application/config/policies/is-concerned.js create mode 100644 back/api/application/documentation/1.0.0/overrides/application.json create mode 100644 web/components/Account/Application/Company/ApplicationCompanyHelper.tsx create mode 100644 web/components/Account/Application/Company/ApplicationCompanyList.tsx create mode 100644 web/components/Account/Application/Company/ApplicationCompanyListItem.tsx create mode 100644 web/components/Account/Application/ConfirmButton.tsx create mode 100644 web/components/Account/Application/Place/ApplicationPlaceList.tsx create mode 100644 web/components/Account/Application/Place/ApplicationPlaceListItem.tsx create mode 100644 web/components/Account/Info/InfoCompanyApplications.tsx create mode 100644 web/components/Account/Info/InfoPlaceApplications.tsx create mode 100644 web/public/assets/img/companyApplicationsEmpty.svg create mode 100644 web/public/assets/img/companyApplicationsNext.svg create mode 100644 web/public/locales/fr/application.json diff --git a/back/api/application/config/policies/is-concerned.js b/back/api/application/config/policies/is-concerned.js new file mode 100644 index 00000000..4d6598d9 --- /dev/null +++ b/back/api/application/config/policies/is-concerned.js @@ -0,0 +1,17 @@ +module.exports = async (ctx, next) => { + const { id } = ctx.params; + + if (id && ctx.state.user) { + const { user } = ctx.state; + const application = await strapi.query("application").findOne({ id }); + + if ( + application && + (application.company.id === user.id) + ) { + return await next(); + } + } + + ctx.unauthorized(`Not allowed`); +}; diff --git a/back/api/application/config/routes.json b/back/api/application/config/routes.json index a03e2d1d..7d690f32 100644 --- a/back/api/application/config/routes.json +++ b/back/api/application/config/routes.json @@ -1,5 +1,15 @@ { "routes": [ + { + "method": "GET", + "path": "/applications/me", + "handler": "application.myApplications", + "config": { + "policies": ["global::is-authenticated"], + "operationId": "myApplications", + "description": "Get applications related to current user" + } + }, { "method": "GET", "path": "/applications", @@ -45,8 +55,9 @@ "path": "/applications/:id", "handler": "application.delete", "config": { - "policies": [] + "policies": ["is-concerned"] } } ] } + diff --git a/back/api/application/controllers/application.js b/back/api/application/controllers/application.js index 524770e7..7a9664e9 100644 --- a/back/api/application/controllers/application.js +++ b/back/api/application/controllers/application.js @@ -15,9 +15,9 @@ module.exports = { .find( { ...query, - _sort: "disponibilities.start:desc", + _sort: "disponibility.start:desc", }, - populate + ['disponibility.espace', 'place'] ) .then((res) => { return Promise.all( diff --git a/back/api/application/documentation/1.0.0/application.json b/back/api/application/documentation/1.0.0/application.json index f4fc6642..7dbf2094 100644 --- a/back/api/application/documentation/1.0.0/application.json +++ b/back/api/application/documentation/1.0.0/application.json @@ -1,5 +1,62 @@ { "paths": { + "/applications/me": { + "get": { + "deprecated": false, + "description": "Get applications related to current user", + "responses": { + "200": { + "description": "response", + "content": { + "application/json": { + "schema": { + "properties": { + "foo": { + "type": "string" + } + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "summary": "", + "tags": [ + "Application" + ], + "parameters": [] + } + }, "/applications": { "get": { "deprecated": false, diff --git a/back/api/application/documentation/1.0.0/overrides/application.json b/back/api/application/documentation/1.0.0/overrides/application.json new file mode 100644 index 00000000..85184a52 --- /dev/null +++ b/back/api/application/documentation/1.0.0/overrides/application.json @@ -0,0 +1,63 @@ +{ + "paths": { + "/applications/me": { + "get": { + "deprecated": false, + "description": "Get applications related to current user", + "operationId": "getMyApplications", + "responses": { + "200": { + "description": "response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Application" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "summary": "", + "tags": [ + "Application" + ], + "parameters": [ + + ] + } + } + } +} diff --git a/back/extensions/documentation/documentation/1.0.0/full_documentation.json b/back/extensions/documentation/documentation/1.0.0/full_documentation.json index 112d36b4..2c29f6cb 100644 --- a/back/extensions/documentation/documentation/1.0.0/full_documentation.json +++ b/back/extensions/documentation/documentation/1.0.0/full_documentation.json @@ -14,7 +14,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "x-generation-date": "02/20/2024 10:02:44 AM" + "x-generation-date": "02/21/2024 10:53:19 AM" }, "x-strapi-config": { "path": "/documentation", @@ -556,6 +556,63 @@ ] } }, + "/applications/me": { + "get": { + "deprecated": false, + "description": "Get applications related to current user", + "responses": { + "200": { + "description": "response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Application" + } + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "summary": "", + "tags": [ + "Application" + ], + "parameters": [], + "operationId": "getMyApplications" + } + }, "/applications": { "get": { "deprecated": false, diff --git a/web/components/Account/AccountLayout.tsx b/web/components/Account/AccountLayout.tsx index 3eca81ac..bce1380e 100644 --- a/web/components/Account/AccountLayout.tsx +++ b/web/components/Account/AccountLayout.tsx @@ -1,14 +1,16 @@ import React, { useMemo } from 'react' -import { Container, Flex, useBreakpointValue } from '@chakra-ui/react' +import { Box, Container, Flex, useBreakpointValue } from '@chakra-ui/react' import AccountMenu from '~components/Account/AccountMenu' import AccountMobileMenu from '~components/Account/AccountMobileMenu' import Loading from '~components/Loading' import { ROUTE_ACCOUNT_MESSAGE } from '~constants' import { useCurrentUser } from '~hooks/useCurrentUser' import { useRouter } from 'next/router' +import useCampaignContext from '~components/Campaign/useCampaignContext' const AccountLayout = (props) => { const router = useRouter() + const { isLoading: isCampaignLoading } = useCampaignContext() const isMobile = useBreakpointValue({ base: true, md: false }) const { data: user, isLoading } = useCurrentUser() const isMessage = useMemo( @@ -18,11 +20,16 @@ const AccountLayout = (props) => { return ( - {isMobile ? ( - - ) : ( - - )} + } + > + {isMobile ? ( + + ) : ( + + )} + ({ - title: 'applications', + title: 'applications.menu_title', translationParams, items: [ { @@ -133,6 +133,25 @@ const AccountMenu = ({ user }: { user: UsersPermissionsUser }) => { const { data: notifs } = useMyNotifications() const { currentCampaign, isCampaignPlace } = useCampaignContext() + const applicationItems = useMemo( + () => + getApplicationsItems({ + isNext: + user?.type === 'company' && + currentCampaign?.mode === 'disponibilities', + translationParams: { title: currentCampaign?.title }, + isPlace: user?.type === 'place', + }), + [currentCampaign, user?.type], + ) + + const placeItems = useMemo(() => getPlaceItems(isCampaignPlace), [ + isCampaignPlace, + ]) + const companyItems = useMemo( + () => getCompanyItems(Boolean(currentCampaign)), + [currentCampaign], + ) const displayMenu = ({ title, items, translationParams = {} }) => { const isDisactivated = !isComplete && title === 'dashboard' @@ -192,6 +211,7 @@ const AccountMenu = ({ user }: { user: UsersPermissionsUser }) => { ) } + return ( { {user?.confirmed && user?.accepted && - displayMenu( - user?.type === 'company' - ? getCompanyItems(Boolean(currentCampaign)) - : getPlaceItems(isCampaignPlace), - )} + displayMenu(user?.type === 'company' ? companyItems : placeItems)} {((user?.type === 'place' && isCampaignPlace) || currentCampaign) && - displayMenu( - getApplicationsItems({ - isNext: - user?.type === 'company' && - currentCampaign?.mode === 'disponibilities', - translationParams: { title: currentCampaign?.title }, - isPlace: user?.type === 'place', - }), - )} + displayMenu(applicationItems)} {displayMenu(accountItems)} diff --git a/web/components/Account/Application/Company/ApplicationCompanyHelper.tsx b/web/components/Account/Application/Company/ApplicationCompanyHelper.tsx new file mode 100644 index 00000000..0793a0c0 --- /dev/null +++ b/web/components/Account/Application/Company/ApplicationCompanyHelper.tsx @@ -0,0 +1,56 @@ +import { Box, Button, Stack, Text } from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import { ROUTE_PLACES } from '~constants' +import { useCurrentUser } from '~hooks/useCurrentUser' +import { format } from '~utils/date' +import Link from '~components/Link' + +const ApplicationCompanyHelper = () => { + const { remainingApplications } = useCurrentUser() + const { currentCampaign } = useCampaignContext() + const { t } = useTranslation('application') + if (remainingApplications > 0 && currentCampaign.mode === 'applications') + return ( + + + + + {t( + `company.helper.start${remainingApplications > 1 ? 's' : ''}`, + { + num: remainingApplications, + }, + )} + + + {t('company.helper.end', { + date: format(currentCampaign.application_end), + })} + + + + + + ) + return null +} + +export default ApplicationCompanyHelper diff --git a/web/components/Account/Application/Company/ApplicationCompanyList.tsx b/web/components/Account/Application/Company/ApplicationCompanyList.tsx new file mode 100644 index 00000000..dfee32c1 --- /dev/null +++ b/web/components/Account/Application/Company/ApplicationCompanyList.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import { + Flex, + Text, + SimpleGrid, + Box, + Divider as ChakraDivider, + DividerProps, +} from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import { Application } from '~typings/api' +import Chevron from 'public/assets/img/chevron-down.svg' +import Cell from '~components/Account/Booking/Cell' +import ApplicationCompanyListItem from '~components/Account/Application/Company/ApplicationCompanyListItem' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import ApplicationCompanyHelper from '~components/Account/Application/Company/ApplicationCompanyHelper' + +interface Props { + applications: Application[] +} + +const Divider = (props: DividerProps) => ( + +) + +const ApplicationCompanyList = ({ applications = [] }: Props) => { + const { currentCampaign } = useCampaignContext() + const { t } = useTranslation('application') + const [list, setList] = useState([]) + const [isDesc, setDesc] = useState(true) + + useEffect(() => { + setList(applications) + setDesc(true) + }, [applications]) + + const sortByDate = () => { + setDesc(!isDesc) + setList(list.reverse()) + } + + return ( + + + + {t('company.title', { title: currentCampaign?.title })} + + + + + {t('company.table.head.number')} + + + + {t('company.table.head.place')} + + + + {t('company.table.head.space')} + + + + + {t('company.table.head.slot')} + + + + + + + + {t('company.table.head.creation')} + + + + + + {list.map((application) => ( + + ))} + + + + ) +} + +export default ApplicationCompanyList diff --git a/web/components/Account/Application/Company/ApplicationCompanyListItem.tsx b/web/components/Account/Application/Company/ApplicationCompanyListItem.tsx new file mode 100644 index 00000000..f10553d0 --- /dev/null +++ b/web/components/Account/Application/Company/ApplicationCompanyListItem.tsx @@ -0,0 +1,76 @@ +import React, { Fragment } from 'react' +import { format } from '~utils/date' +import { Application } from '~typings/api' +import { Text, Button } from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import Cell from '~components/Account/Booking/Cell' +import ConfirmButton from '~components/Account/Application/ConfirmButton' +import { client } from '~api/client-api' +import useToast from '~hooks/useToast' +import { useQueryClient } from 'react-query' + +interface Props { + application: Application +} + +const ApplicationCompanyListItem = ({ application }: Props) => { + const { errorToast, successToast } = useToast() + const { t } = useTranslation('application') + const queryClient = useQueryClient() + + const onDelete = async () => { + try { + await client.applications.applicationsDelete(application.id) + successToast(t('company.delete_success')) + queryClient.refetchQueries(['myApplications']) + } catch (e) { + errorToast(t('company.delete_error')) + } + } + + return ( + + + {application?.id} + + + {application?.place?.structureName} + + + {/* @ts-expect-error */} + {application?.disponibility?.espace?.name} + + + {`${format(application?.disponibility.start, 'dd/MM')} → ${format( + application?.disponibility.end, + 'dd/MM', + )}`} + + + {application?.creation_title} + + + + + + + + ) +} + +export default ApplicationCompanyListItem diff --git a/web/components/Account/Application/ConfirmButton.tsx b/web/components/Account/Application/ConfirmButton.tsx new file mode 100644 index 00000000..bed21b3d --- /dev/null +++ b/web/components/Account/Application/ConfirmButton.tsx @@ -0,0 +1,56 @@ +import { + Button, + Popover, + PopoverContent, + PopoverBody, + PopoverCloseButton, + ButtonGroup, + useDisclosure, + PopoverTrigger, + Box, +} from '@chakra-ui/react' +import { ReactNode } from 'react-markdown' +import { useTranslation } from 'next-i18next' + +const ConfirmButton = ({ + helper, + children, + handleConfirm, + confirmLabel, +}: { + helper: string + children: ReactNode + handleConfirm: () => void + confirmLabel: string +}) => { + const { t } = useTranslation('application') + const { isOpen, onClose, onToggle } = useDisclosure() + + return ( + + + {children} + + + + {helper} + + + + + + + ) +} + +export default ConfirmButton diff --git a/web/components/Account/Application/Place/ApplicationPlaceList.tsx b/web/components/Account/Application/Place/ApplicationPlaceList.tsx new file mode 100644 index 00000000..86b0654b --- /dev/null +++ b/web/components/Account/Application/Place/ApplicationPlaceList.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import { + Flex, + Text, + SimpleGrid, + Box, + Divider as ChakraDivider, + DividerProps, +} from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import { Application } from '~typings/api' +import Chevron from 'public/assets/img/chevron-down.svg' +import Cell from '~components/Account/Booking/Cell' +import ApplicationCompanyListItem from '~components/Account/Application/Company/ApplicationCompanyListItem' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import ApplicationCompanyHelper from '~components/Account/Application/Company/ApplicationCompanyHelper' + +interface Props { + applications: Application[] +} + +const Divider = (props: DividerProps) => ( + +) + +const ApplicationPlaceList = ({ applications = [] }: Props) => { + const { currentCampaign } = useCampaignContext() + const { t } = useTranslation('application') + const [list, setList] = useState([]) + const [isDesc, setDesc] = useState(true) + + useEffect(() => { + setList(applications) + setDesc(true) + }, [applications]) + + const sortByDate = () => { + setDesc(!isDesc) + setList(list.reverse()) + } + + return ( + + + + {t('place.title', { title: currentCampaign?.title })} + + + + + {t('place.table.head.number')} + + + + {t('place.table.head.place')} + + + + {t('place.table.head.space')} + + + + + {t('place.table.head.slot')} + + + + + + + + {t('place.table.head.creation')} + + + + + + {list.map((application) => ( + + ))} + + + + ) +} + +export default ApplicationPlaceList diff --git a/web/components/Account/Application/Place/ApplicationPlaceListItem.tsx b/web/components/Account/Application/Place/ApplicationPlaceListItem.tsx new file mode 100644 index 00000000..b9d9b3a6 --- /dev/null +++ b/web/components/Account/Application/Place/ApplicationPlaceListItem.tsx @@ -0,0 +1,76 @@ +import React, { Fragment } from 'react' +import { format } from '~utils/date' +import { Application } from '~typings/api' +import { Text, Button } from '@chakra-ui/react' +import { useTranslation } from 'next-i18next' +import Cell from '~components/Account/Booking/Cell' +import ConfirmButton from '~components/Account/Application/ConfirmButton' +import { client } from '~api/client-api' +import useToast from '~hooks/useToast' +import { useQueryClient } from 'react-query' + +interface Props { + application: Application +} + +const ApplicationPlaceListItem = ({ application }: Props) => { + const { errorToast, successToast } = useToast() + const { t } = useTranslation('application') + const queryClient = useQueryClient() + + const onDelete = async () => { + try { + await client.applications.applicationsDelete(application.id) + successToast(t('company.delete_success')) + queryClient.refetchQueries(['myApplications']) + } catch (e) { + errorToast(t('company.delete_error')) + } + } + + return ( + + + {application?.id} + + + {application?.place?.structureName} + + + {/* @ts-expect-error */} + {application?.disponibility?.espace?.name} + + + {`${format(application?.disponibility.start, 'dd/MM')} → ${format( + application?.disponibility.end, + 'dd/MM', + )}`} + + + {application?.creation_title} + + + + + + + + ) +} + +export default ApplicationPlaceListItem diff --git a/web/components/Account/Info/InfoCompanyApplications.tsx b/web/components/Account/Info/InfoCompanyApplications.tsx new file mode 100644 index 00000000..f22782c0 --- /dev/null +++ b/web/components/Account/Info/InfoCompanyApplications.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import Info from '~components/Account/Info/Info' +import { useTranslation } from 'next-i18next' +import { ROUTE_PLACES } from '~constants' +import { UsersPermissionsUser } from '~typings/api' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import { format } from '~utils/date' + +interface Props { + user: UsersPermissionsUser +} + +const InfoCompanyApplications = ({ user }: Props) => { + const { t } = useTranslation('account') + const { currentCampaign } = useCampaignContext() + if (!currentCampaign) return null + + return ( + + {currentCampaign?.mode === 'applications' + ? t(`applications.info.text.no_applications`, { + ...currentCampaign, + application_end: format( + currentCampaign?.application_end, + 'dd/MM/yyyy', + ), + }) + : t(`applications.info.text.next`, { + ...currentCampaign, + application_start: format( + currentCampaign?.application_start, + 'dd/MM', + ), + application_end: format(currentCampaign?.application_end, 'dd/MM'), + })} + + ) +} + +export default InfoCompanyApplications diff --git a/web/components/Account/Info/InfoPlaceApplications.tsx b/web/components/Account/Info/InfoPlaceApplications.tsx new file mode 100644 index 00000000..a81955ab --- /dev/null +++ b/web/components/Account/Info/InfoPlaceApplications.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import Info from '~components/Account/Info/Info' +import { useTranslation } from 'next-i18next' +import { ROUTE_ACCOUNT_PLACES, ROUTE_PLACES } from '~constants' +import { UsersPermissionsUser } from '~typings/api' +import useCampaignContext from '~components/Campaign/useCampaignContext' +import { format } from '~utils/date' + +interface Props { + user: UsersPermissionsUser +} + +const InfoPlaceApplications = ({ user }: Props) => { + const { t } = useTranslation('account') + const { currentCampaign } = useCampaignContext() + if (!currentCampaign) return null + + return ( + + {t(`applications.info.text.no_applications_company`, { + ...currentCampaign, + date: format(currentCampaign?.application_end, 'dd/MM'), + })} + + ) +} + +export default InfoPlaceApplications diff --git a/web/components/Campaign/Places/Application/ApplicationConfirmed.tsx b/web/components/Campaign/Places/Application/ApplicationConfirmed.tsx index b38241c0..546789a7 100644 --- a/web/components/Campaign/Places/Application/ApplicationConfirmed.tsx +++ b/web/components/Campaign/Places/Application/ApplicationConfirmed.tsx @@ -9,7 +9,7 @@ import { } from '@chakra-ui/react' import Link from '~components/Link' import { useTranslation } from 'next-i18next' -import { ROUTE_ACCOUNT_APPLICATIONS } from '~constants' +import { ROUTE_ACCOUNT_MY_APPLICATIONS } from '~constants' const ApplicationConfirmed = ({ structureName }) => { const { t } = useTranslation('place') @@ -41,7 +41,7 @@ const ApplicationConfirmed = ({ structureName }) => {