diff --git a/src/Mutations/invitationMutation.tsx b/src/Mutations/invitationMutation.tsx index 68864357a..88e264e31 100644 --- a/src/Mutations/invitationMutation.tsx +++ b/src/Mutations/invitationMutation.tsx @@ -36,3 +36,35 @@ export const DELETE_INVITATION = gql` } } `; + +export const UPDATE_INVITATION = gql` + mutation UpdateInvitation( + $invitationId: ID! + $orgToken: String! + $newEmail: String + $newRole: String + ) { + updateInvitation( + invitationId: $invitationId + orgToken: $orgToken + newEmail: $newEmail + newRole: $newRole + ) { + id + invitees { + email + role + } + inviterId + orgToken + } + } +`; +export const CANCEL_INVITATION = gql` + mutation CancelInvitation($id: ID!, $orgToken: String!) { + cancelInvitation(id: $id, orgToken: $orgToken) { + status + createdAt + } + } +`; diff --git a/src/Mutations/invitationStats.tsx b/src/Mutations/invitationStats.tsx index cf6cff6e5..0cf80f1a5 100644 --- a/src/Mutations/invitationStats.tsx +++ b/src/Mutations/invitationStats.tsx @@ -15,9 +15,11 @@ export const GET_INVITATIONS_STATISTICS_QUERY = gql` ) { totalInvitations pendingInvitationsCount + cancelledInvitationsCount getPendingInvitationsPercentsCount getAcceptedInvitationsPercentsCount + getCancelledInvitationsPercentsCount acceptedInvitationsCount } } -`; \ No newline at end of file +`; diff --git a/src/components/InvitationTable.tsx b/src/components/InvitationTable.tsx index 0475005a5..32491d45b 100644 --- a/src/components/InvitationTable.tsx +++ b/src/components/InvitationTable.tsx @@ -62,11 +62,8 @@ function DataTableStats({ data, columns, error, loading }: TableData) { }; return ( -
-
-
+
+
@@ -178,5 +175,4 @@ function DataTableStats({ data, columns, error, loading }: TableData) { ); } - export default DataTableStats; \ No newline at end of file diff --git a/src/pages/invitation.tsx b/src/pages/invitation.tsx index 63580c902..b2b62c6bf 100644 --- a/src/pages/invitation.tsx +++ b/src/pages/invitation.tsx @@ -15,9 +15,18 @@ import { GET_INVITATIONS_STATISTICS_QUERY } from '../Mutations/invitationStats'; import InvitationCardSkeleton from '../Skeletons/InvitationCardSkeleton'; import { useTranslation } from 'react-i18next'; import { Icon } from '@iconify/react'; -import { DELETE_INVITATION } from '../Mutations/invitationMutation'; +import { + CANCEL_INVITATION, + DELETE_INVITATION, + UPDATE_INVITATION, +} from '../Mutations/invitationMutation'; import Button from '../components/Buttons'; -import { GET_ALL_INVITATIONS, GET_INVITATIONS, GET_ROLES_AND_STATUSES } from '../queries/invitation.queries'; +import { + GET_ALL_INVITATIONS, + GET_INVITATIONS, + GET_ROLES_AND_STATUSES, +} from '../queries/invitation.queries'; +import { isValid } from 'date-fns'; interface Invitee { email: string; @@ -37,6 +46,8 @@ function Invitation() { const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [totalInvitations, setTotalInvitations] = useState(0); + const [cancelInviteeModel, setCancelInviteeModel] = useState(false); + const [cancelInvitation, setCancelInvitation] = useState(''); const [isModalOpen, setIsModalOpen] = useState(false); const [filterLoading, setFilterLoading] = useState(true); const [filterRange, setFilterRange] = useState('Last 7 days'); @@ -45,14 +56,30 @@ function Invitation() { endDate: '', }); const { t }: any = useTranslation(); + const removeInviteeMod = () => { + const newState = !removeInviteeModel; + setRemoveInviteeModel(newState); + }; + + const cancelInviteeMod = () => { + const newState = !cancelInviteeModel; + setCancelInviteeModel(newState); + }; + const [selectedRow, setSelectedRow] = useState(null); const [removeInviteeModel, setRemoveInviteeModel] = useState(false); const [deleteInvitation, setDeleteInvitation] = useState(''); + const [updateInviteeModel, setUpdateInviteeModel] = useState(false); const [buttonLoading, setButtonLoading] = useState(false); const [selectedRole, setSelectedRole] = useState(''); const [selectedStatus, setSelectedStatus] = useState(''); - const [filterVariables, setFilterVariables] = useState({ role: '', status: '' }); - + const [email, setEmail] = useState(''); + const [role, setRole] = useState(''); + const [selectedInvitationId, setSelectedInvitationId] = useState(''); + const [filterVariables, setFilterVariables] = useState({ + role: '', + status: '', + }); const organizationToken = localStorage.getItem('orgToken'); @@ -69,6 +96,7 @@ function Invitation() { } }; + // Fetch invitation statistics const { loading: isLoading, data: queryData, @@ -83,11 +111,39 @@ function Invitation() { }, skip: !organizationToken, fetchPolicy: 'network-only', - onError: (error) => { - toast.error('testtes111'); - }, + onError: (error) => toast.error('Error fetching data'), + }); + + // Fetch all invitations + const { + data, + loading: queryLoading, + error: queryError, + refetch, + } = useQuery(GET_ALL_INVITATIONS, { + fetchPolicy: 'network-only', }); + useEffect(() => { + if (invitationStats) { + setSelectedStatus(invitationStats); // Set the fetched status as the default value + } + }, [invitationStats]); + + // Set email and role when modal opens + useEffect(() => { + if (data && data.getAllInvitations) { + const invitation = data.getAllInvitations.invitations.find( + (inv: Invitationn) => inv.id === selectedInvitationId, + ); + + if (invitation && invitation.invitees.length > 0) { + setEmail(invitation.invitees[0].email); + setRole(invitation.invitees[0].role); + } + } + }, [data, selectedInvitationId]); + useEffect(() => { if (isLoading) { setFilterLoading(true); @@ -98,12 +154,14 @@ function Invitation() { refreshData(); setInvitationStats(queryData.getInvitationStatistics); } - }, [queryData, refreshData]); + }, [queryData, refreshData, isLoading]); + // Handle the range change const handleRangeChange = (e: React.ChangeEvent) => { setFilterRange(e.target.value); }; + // Handle custom date range const handleCustomRangeChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setCustomRange((prev) => ({ ...prev, [name]: value })); @@ -113,37 +171,26 @@ function Invitation() { return

Organization token not found. Please log in.

; } - const { - data, - loading: queryLoading, - error: queryError, - refetch, - } = useQuery(GET_ALL_INVITATIONS, { - fetchPolicy: 'network-only', - }); + const updateInviteeMod = () => { + const newState = !updateInviteeModel; + setUpdateInviteeModel(newState); + }; const [ fetchInvitations, { data: searchData, loading: searchLoading, error: searchError }, ] = useLazyQuery(GET_INVITATIONS, { - variables: { - query: searchQuery, - }, fetchPolicy: 'network-only', + variables: { query: searchQuery }, + fetchPolicy: 'network-only', }); - /* istanbul ignore next */ - const removeInviteeMod = () => { - const newState = !removeInviteeModel; - setRemoveInviteeModel(newState); - }; - const [ - filterInvitations, - { data: filterData, loading: filterLoad, error: filterError }, - ] = useLazyQuery(GET_ROLES_AND_STATUSES, { - variables: filterVariables, - fetchPolicy: 'network-only', - }); + filterInvitations, + { data: filterData, loading: filterLoad, error: filterError }, + ] = useLazyQuery(GET_ROLES_AND_STATUSES, { + variables: filterVariables, + fetchPolicy: 'network-only', + }); useEffect(() => { if (filterVariables.role || filterVariables.status) { @@ -151,6 +198,7 @@ function Invitation() { } }, [filterVariables, filterInvitations]); + // Consolidated effect to handle query and search data useEffect(() => { if (queryLoading || searchLoading || filterLoad) { setLoading(true); @@ -176,7 +224,17 @@ function Invitation() { setInvitations(data.getAllInvitations.invitations); setTotalInvitations(data.getAllInvitations.totalInvitations); } - }, [data, searchData, queryLoading, searchLoading, queryError, searchError, filterData, filterLoad, filterError]); + }, [ + data, + searchData, + queryLoading, + searchLoading, + queryError, + searchError, + filterData, + filterLoad, + filterError, + ]); const handleSearch = () => { if (!searchQuery.trim()) { @@ -188,17 +246,9 @@ function Invitation() { setError(null); setLoading(false); - fetchInvitations({ - variables: { - query: searchQuery, - }, - }); + fetchInvitations({ variables: { query: searchQuery } }); }; - const toggleOptions = (row: string) => { - setSelectedRow(selectedRow === row ? null : row); - } - const handleFilter = () => { if (!selectedRole && !selectedStatus) { toast.info('Please select role or status.'); @@ -209,20 +259,39 @@ function Invitation() { setError(null); setLoading(false); - setFilterVariables({ - role: selectedRole, - status: selectedStatus, - }); + setFilterVariables({ role: selectedRole, status: selectedStatus }); + }; + + const toggleOptions = (row: string) => { + setSelectedRow(selectedRow === row ? null : row); }; const disabledSearch = !searchQuery.trim(); + interface EmailInputProps { + email: string; + setEmail: (email: string) => void; + } + + const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Simple email validation regex + const isValidEmail = emailRegex.test(email); + setIsValid(isValidEmail); // Update validation state + return isValidEmail; + }; + + const handleChange = (e: React.ChangeEvent) => { + const newEmail = e.target.value; + setEmail(newEmail); + validateEmail(newEmail); // Validate on change + }; + // Defining invitation table let content; const capitalizeStrings = (str: string): string => { if (!str) return ''; - if(str === 'ttl'){ - return 'TTL' + if (str === 'ttl') { + return 'TTL'; } return str.charAt(0).toUpperCase() + str.slice(1); }; @@ -241,7 +310,7 @@ function Invitation() { (invitations?.length > 0 ? ' flex' : ' hidden') } > - {row.original.Status} + {row.original.Status} ); }, @@ -258,7 +327,7 @@ function Invitation() { width="25" cursor="pointer" color="#9e85f5" - onClick={() => toggleOptions(row.id)} + onClick={() => toggleOptions(row.id)} /> {selectedRow === row.id && (
@@ -267,6 +336,11 @@ function Invitation() {
{ + updateInviteeMod(); + setSelectedInvitationId(row.original.id); + toggleOptions(row.original.email); + }} >
+ +
+
{ + cancelInviteeMod(); + setCancelInvitation(row.original.id); + }} + > + +
+ Cancel + <> +
+ Cancel invitation + +
+
+
+
{row.original.Status === 'Pending' && ( -
-
- -
- Resend{' '} - <> -
- Resend invitation - -
-
-
+
+
+ +
+ Resend{' '} + <> +
+ Resend invitation + +
+
+
)} -
-
+
)} @@ -349,69 +446,116 @@ function Invitation() { invitation.invitees?.forEach((data: any) => { let entry: any = {}; entry.email = data.email; - entry.role = capitalizeStrings(data.role) - entry.Status = capitalizeStrings(invitation.status) - entry.id= invitation.id + entry.role = capitalizeStrings(data.role); + entry.Status = capitalizeStrings(invitation.status); + entry.id = invitation.id; datum.push(entry); }); }); } if (loading || searchLoading || filterLoad) { - content = 0 ? datum : []} - columns={columns} - loading={loading} - error={error} - />; + content = ( + 0 ? datum : []} + columns={columns} + loading={loading} + error={error} + /> + ); } else if (error || searchError || filterError) { - content = 0 ? datum : []} - columns={columns} - loading={loading} - error={error} - />; - } else { content = ( - <> 0 ? datum : []} columns={columns} loading={loading} error={error} /> + ); + } else { + content = ( + <> + 0 ? datum : []} + columns={columns} + loading={loading} + error={error} + /> ); } - const [DeleteInvitation] = useMutation( - DELETE_INVITATION, - { - variables: { - invitationId: deleteInvitation, - }, - onCompleted: (data) => { - setTimeout(() => { - setButtonLoading(false); - toast.success(data.deleteInvitation.message); - refetch() - removeInviteeMod(); - }, 1000); - }, - onError: (err) => { - setTimeout(() => { - setButtonLoading(false); - toast.error(err.message); - }, 500); - }, + const [DeleteInvitation] = useMutation(DELETE_INVITATION, { + variables: { + invitationId: deleteInvitation, }, - ); + onCompleted: (data) => { + setTimeout(() => { + setButtonLoading(false); + toast.success(data.deleteInvitation.message); + refetch(); + removeInviteeMod(); + }, 1000); + }, + onError: (err) => { + setTimeout(() => { + setButtonLoading(false); + toast.error(err.message); + }, 500); + }, + }); const handleOpenModal = () => setIsModalOpen(true); - const handleCloseModal = () => {setIsModalOpen(false); + const handleCloseModal = () => { + setIsModalOpen(false); refetch(); refreshData(); - } + }; + const [UpdateInvitation] = useMutation(UPDATE_INVITATION, { + variables: { + invitationId: selectedInvitationId, + orgToken: organizationToken, + newEmail: email || undefined, + newRole: role || undefined, + newStatus: selectedStatus || undefined, + }, + onCompleted: () => { + setTimeout(() => { + setButtonLoading(false); + toast.success('Invitation updated successfully!'); + refetch(); + updateInviteeMod(); + }, 1000); + }, + onError: (err) => { + setTimeout(() => { + setButtonLoading(false); + toast.error(`Failed to update invitation`); + }, 500); + }, + }); + const [CancelInvitation] = useMutation(CANCEL_INVITATION, { + variables: { + id: cancelInvitation, + orgToken: organizationToken, + }, + onCompleted: (data) => { + setTimeout(() => { + setButtonLoading(false); + toast.success( + data.cancelInvitation.message || 'Invitation canceled successfully.', + ); + refetch(); + cancelInviteeMod(); + }, 1000); + }, + onError: (err) => { + setTimeout(() => { + setButtonLoading(false); + toast.error(err.message); + }, 500); + }, + }); return (
{/* Header and Invite Button */} @@ -486,7 +630,7 @@ function Invitation() { } status="ACCEPTED" - time= {filterRange} + time={filterRange} staticNumber={invitationStats?.acceptedInvitationsCount || 0} percentage={ `${invitationStats?.getAcceptedInvitationsPercentsCount?.toFixed( @@ -507,7 +651,19 @@ function Invitation() { )}%` || '0' } /> - + + } + status="CANCELLED" + time={filterRange} + staticNumber={invitationStats?.cancelledInvitationsCount || 0} + percentage={ + `${invitationStats?.getCancelledInvitationsPercentsCount?.toFixed( + 1, + )}%` || '0' + } + />
- {/* Search Section */}

@@ -599,54 +754,59 @@ function Invitation() {

- -
-
-

Or filter by ROLE:

- - - - or STATUS: - - - - -
- {/* Table view */} - {content} +
+
+

+ Or filter by{' '} + + ROLE: + +

+ + + + + {' '} + or STATUS: + + + + + +
+ {/* Table view */} + {content}
- - {/* =========================== Start:: DeleteInvitee Model =============================== */} - -

- {t( - 'Are you sure you want to Delete this invitation?', - )} + {t('Are you sure you want to Delete this invitation?')}

@@ -700,8 +858,158 @@ function Invitation() {
+ {/* =========================== Start:: CancelInvitee Model =============================== */} +
+
+
+

+ {t('Cancel Invitation')} +

+
+
+
+
+
+

+ {t('Are you sure you want to Cancel this invitation?')} +

+
+ +
+ + +
+ +
+
+
+ {/* =========================== Start:: UpdateInvitee Model =============================== */}{' '} +
+
+
+

+ {t('Update Invitation')} +

+
+
+
+
+ {/* Email Input Field */} +
+ + setEmail(e.target.value)} + /> + {!isValid && ( +

+ Please enter a valid email address. +

+ )} +
+ + {/* Role Input Field */} +
+ + +
+ {/* Buttons */} +
+ + +
+ +
+
+
); } -export default Invitation; \ No newline at end of file +export default Invitation; + +function setIsValid(isValidEmail: boolean) { + throw new Error('Function not implemented.'); +}