Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/user send enrollment request #194

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
88df2eb
Enrollment: fix requestAdmin file name and replace imports
NicoAntonelli Sep 15, 2024
fb8eee5
Entities: add sender & invitations count properties to related DTOs
NicoAntonelli Sep 15, 2024
2fb1872
Users: simplify userInList DTO extending base user
NicoAntonelli Sep 15, 2024
a6596f5
Project: add declined RequestState & manage enroll types
NicoAntonelli Sep 15, 2024
ec71e3a
Projects: unify request input & admin DTOs as in backend
NicoAntonelli Sep 15, 2024
5a161a0
Projects: add accept & decline service endpoints
NicoAntonelli Sep 15, 2024
6ba105b
Users: add CRUD invitation service endpoints
NicoAntonelli Sep 15, 2024
a6edd31
EnrollmentButton: erase import unnecessary casting
NicoAntonelli Sep 15, 2024
f6cda4b
Enrollment: fix enrollment request create name and props name
NicoAntonelli Sep 15, 2024
1fab926
Enrollment change role: translate placeholder to spanish
NicoAntonelli Sep 15, 2024
8b41b68
Filter: fix relative import and order
NicoAntonelli Sep 16, 2024
87d2eb5
Enrollment request rejected: fix spanish message
NicoAntonelli Sep 16, 2024
2a72f8f
Enrollment: rename update and cancel components and associated functions
NicoAntonelli Sep 16, 2024
b028ee2
Enrollment buton: rename to enrollment button request
NicoAntonelli Sep 16, 2024
e449816
Enrollment req admin: use custom type for action & rename component
NicoAntonelli Sep 16, 2024
473aec4
Enrollment req admin: fix aria-label for button
NicoAntonelli Sep 16, 2024
6711864
Enrollment button req: standardize props & fix props name
NicoAntonelli Sep 16, 2024
0268e04
Enrollment button req: add declined empty case
NicoAntonelli Sep 16, 2024
0080214
ProjectDetails: fix updated import
NicoAntonelli Sep 16, 2024
138546a
Enrollment invitation: create component
NicoAntonelli Sep 16, 2024
ef380c5
Enrollment invitation: update component
NicoAntonelli Sep 16, 2024
6b2dfb7
Enrollment invitation: cancel component
NicoAntonelli Sep 16, 2024
e454fab
Enrollment invitation: user CRUD component
NicoAntonelli Sep 16, 2024
7780b22
Enrollment invitation: inv button component (analog to req button)
NicoAntonelli Sep 16, 2024
c0b42df
Unenroll: fix component name
NicoAntonelli Sep 16, 2024
58abe4f
Enrollment: organize components in subfolders (invitation, request, l…
NicoAntonelli Sep 16, 2024
f8fb2a8
Enrollment button inv: fix imports and props name
NicoAntonelli Sep 16, 2024
c5e8c2b
Enrollment inv: set possibleProjects optional prop
NicoAntonelli Sep 16, 2024
cfacb7c
UserDetails: possible projects management
NicoAntonelli Sep 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions components/Common/Filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useQuery } from '@tanstack/react-query'
import { ActionIcon, Button, Drawer, Flex, Group, Text } from '@mantine/core'
import { useMediaQuery } from '@mantine/hooks'
import { IconFilter, IconXboxX } from '@tabler/icons-react'

import Theme from 'src/app/theme'
import { CurrentUserQueryOptions } from '../../../services/currentUser'
import { useQuery } from '@tanstack/react-query'
import { CurrentUserQueryOptions } from '@/services/currentUser'

interface FilterProps {
counter: number
Expand Down
218 changes: 218 additions & 0 deletions components/Enrollment/Invitation/EnrollmentButtonInvitation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import React from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { ActionIcon, Menu } from '@mantine/core'
import { modals } from '@mantine/modals'
import { notifications } from '@mantine/notifications'

import {
IconUserPlus,
IconSend,
IconTrash,
IconUserCheck,
IconUserOff,
IconPencil,
IconEye,
} from '@tabler/icons-react'
import { CurrentUserQueryOptions } from '@/services/currentUser'
import { ProjectsQueryKey } from '@/services/projects'
import { Users } from '@/services/users'
import sanitize from 'sanitize-html'

import { EnrollmentInvitationCancel } from './EnrollmentInvitationCancel'
import { EnrollmentInvitationCreate } from './EnrollmentInvitationCreate'
import { EnrollmentInvitationUpdate } from './EnrollmentInvitationUpdate'
import { NotLoggedError } from '@/components/Account/NotLoggedError'
import ProjectInList, { RequestState } from '@/entities/Project/ProjectInList'
import { verifyEmailNotification } from '@/components/Account/VerifyEmailNotification'

interface EnrollmentButtonInvitationProps {
userId: number
possibleProjects?: ProjectInList[]
projectId?: number
requestState?: RequestState | null
requesterMessage?: string
adminMessage?: string
}

const EnrollmentButtonInvitation: React.FC<EnrollmentButtonInvitationProps> = (
props: EnrollmentButtonInvitationProps
) => {
const queryClient = useQueryClient()

const { data: currentUser, error: errorCurrentUser } = useQuery(
CurrentUserQueryOptions.currentUser()
)

const cancelEnrollmentInvitationMutation = useMutation({
mutationFn: () => Users.cancelEnrollInvitation(props.userId, props.projectId as number),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] })
notifications.show({
title: 'Invitación de inscripción cancelada',
message: 'La invitación de inscripción ha sido cancelada',
})
},
})

const handleEnrollmentInvitationClick = (event: React.MouseEvent) => {
event.stopPropagation()

if (errorCurrentUser || !currentUser) {
notifications.show({
title: 'Debes iniciar sesión para realizar invitaciones de inscripción',
color: 'red',
message: <NotLoggedError action="realizar invitaciones de inscripción" />,
})

return
}

if (currentUser?.isEmailVerified === false) {
notifications.show(verifyEmailNotification('realizar invitaciones de inscripción'))

return
}

modals.open({
title: 'Invitación de inscripción',
centered: true,
children: (
<EnrollmentInvitationCreate
userId={props.userId}
possibleProjects={props.possibleProjects}
/>
),
})
}

const handleReviewRequestClick = (event: React.MouseEvent) => {
event.stopPropagation()

modals.open({
title: 'Invitación de inscripción',
centered: true,
children: (
<EnrollmentInvitationUpdate
content={props.requesterMessage}
userId={props.userId}
projectId={props.projectId as number}
/>
),
})
}

const handleReviewUserMessageClick = (event: React.MouseEvent) => {
event.stopPropagation()

modals.open({
title: 'Mensaje del usuario',
centered: true,
children: <div dangerouslySetInnerHTML={{ __html: sanitize(props.adminMessage ?? '') }} />,
})
}

const handleEnrollmentInvitationCancelClick = (event: React.MouseEvent) => {
event.stopPropagation()

cancelEnrollmentInvitationMutation.mutate()
}

const handleEnrollmentDeclinedClick = (event: React.MouseEvent) => {
event.stopPropagation()

modals.open({
title: 'Invitación declinada',
centered: true,
children: (
<EnrollmentInvitationCancel
userId={props.userId}
projectId={props.projectId as number}
adminMessage={props.adminMessage}
requesterMessage={props.requesterMessage}
/>
),
})
}

switch (props.requestState) {
case RequestState.Unenrolled:
case RequestState.Rejected:
case RequestState.Kicked:
case undefined:
case null:
return (
<ActionIcon
variant="transparent"
aria-label="Realizar invitación de inscripción"
onClick={handleEnrollmentInvitationClick}
size="lg"
color="gray">
<IconUserPlus />
</ActionIcon>
)
case RequestState.Declined:
return (
<ActionIcon
variant="transparent"
aria-label="La invitación ha sido declinada"
onClick={handleEnrollmentDeclinedClick}
size="lg"
color="red">
<IconUserOff />
</ActionIcon>
)
case RequestState.Pending:
return (
<Menu position="bottom" offset={0}>
<Menu.Target>
<ActionIcon
variant="transparent"
aria-label="Opciones de solicitud"
size="lg"
color="blue"
onClick={(event) => event.stopPropagation()}>
<IconSend />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{props.requesterMessage && (
<Menu.Item leftSection={<IconPencil size={14} />} onClick={handleReviewRequestClick}>
Revisar invitación
</Menu.Item>
)}
<Menu.Item
leftSection={<IconTrash color="red" size={14} />}
onClick={handleEnrollmentInvitationCancelClick}>
Cancelar solicitud
</Menu.Item>
</Menu.Dropdown>
</Menu>
)
case RequestState.Accepted:
return (
<Menu position="bottom" offset={0}>
<Menu.Target>
<ActionIcon
variant="transparent"
aria-label="Ver opciones de invitación"
size="lg"
color="blue"
onClick={(event) => event.stopPropagation()}>
<IconUserCheck />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
{props.adminMessage && (
<Menu.Item leftSection={<IconEye size={14} />} onClick={handleReviewUserMessageClick}>
Ver mensaje del usuario
</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
)
default:
return <></>
}
}

export default EnrollmentButtonInvitation
90 changes: 90 additions & 0 deletions components/Enrollment/Invitation/EnrollmentInvitationCancel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, { useMemo } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { Button, Group, LoadingOverlay, Text } from '@mantine/core'
import { modals } from '@mantine/modals'
import { notifications } from '@mantine/notifications'

import { ProjectsQueryKey } from '@/services/projects'
import { Users } from '@/services/users'
import sanitize from 'sanitize-html'

interface EnrollmentInvitationCancelProps {
userId: number
projectId: number
requesterMessage?: string
adminMessage?: string
}

export const EnrollmentInvitationCancel = (
props: EnrollmentInvitationCancelProps
): React.JSX.Element => {
const queryClient = useQueryClient()

const cancelEnrollmentInvitationMutation = useMutation({
mutationFn: () => Users.cancelEnrollInvitation(props.userId, props.projectId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [ProjectsQueryKey] })
notifications.show({
title: 'Invitación de inscripción cancelada',
message: 'La invitación de inscripción ha sido eliminada correctamente',
})
modals.closeAll()
},
onError: (error) => {
console.error('Error al descartar la invitación de inscripción', error)
notifications.show({
title: 'Error al descartar la invitación de inscripción',
message: 'Por favor, inténtalo de nuevo más tarde',
color: 'red',
})
},
})

const handleCancelInvitation = () => {
cancelEnrollmentInvitationMutation.mutate()
}

const requesterMessageSanitized = useMemo(
() => sanitize(props.requesterMessage || ''),
[props.requesterMessage]
)

const adminMessageSanitized = useMemo(
() => sanitize(props.adminMessage || ''),
[props.adminMessage]
)

return (
<>
<LoadingOverlay
visible={cancelEnrollmentInvitationMutation.isPending}
zIndex={1000}
overlayProps={{ radius: 'sm', blur: 2 }}
/>

{requesterMessageSanitized && (
<>
<Text size="lg" fw={700}>
Tu mensaje
</Text>
<div dangerouslySetInnerHTML={{ __html: requesterMessageSanitized }} />
</>
)}

{adminMessageSanitized && (
<>
<Text size="lg" fw={700}>
Mensaje del usuario
</Text>
<div dangerouslySetInnerHTML={{ __html: adminMessageSanitized }} />
</>
)}

<Group gap="xs" justify="flex-end">
<Button color="red" fullWidth onClick={handleCancelInvitation}>
Descartar
</Button>
</Group>
</>
)
}
Loading