From aa3c3996834fa215be837ab7f8b2c82822dc32b8 Mon Sep 17 00:00:00 2001
From: shebelle <154588570+shebz2023@users.noreply.github.com>
Date: Sat, 14 Sep 2024 17:36:11 +0200
Subject: [PATCH] fx-rating-error (#492)
this commit refines the functions of validating
this commit utilizes zod library for validating
ttl rating trainees (#494)
Fetch invitation statistics (#463)
---
src/Mutations/invitationMutation.tsx | 41 +-
src/Mutations/invitationStats.tsx | 4 +-
src/components/InvitationTable.tsx | 5 +-
src/components/invitationModal.tsx | 11 +-
src/pages/invitation.tsx | 657 ++++++++++++++++++++-------
src/queries/invitation.queries.tsx | 8 +-
6 files changed, 535 insertions(+), 191 deletions(-)
diff --git a/src/Mutations/invitationMutation.tsx b/src/Mutations/invitationMutation.tsx
index 68864357a..40874aa79 100644
--- a/src/Mutations/invitationMutation.tsx
+++ b/src/Mutations/invitationMutation.tsx
@@ -1,13 +1,14 @@
import { gql } from '@apollo/client';
export const SEND_INVITATION = gql`
- mutation SendInvitation($invitees: [InviteeInput!]!, $orgToken: String!) {
- sendInvitation(invitees: $invitees, orgToken: $orgToken) {
+ mutation SendInvitation($invitees: [InviteeInput!]!,$orgName:String!,$orgToken: String!) {
+ sendInvitation(invitees: $invitees,orgName:$orgName, orgToken: $orgToken) {
status
invitees {
email
role
}
+ orgName
orgToken
createdAt
}
@@ -15,8 +16,8 @@ export const SEND_INVITATION = gql`
`;
export const UPLOAD_INVITATION_FILE = gql`
- mutation uploadInvitationFile($file: Upload!, $orgToken: String!) {
- uploadInvitationFile(file: $file, orgToken: $orgToken) {
+ mutation uploadInvitationFile($file: Upload!,$orgName:String!, $orgToken: String!) {
+ uploadInvitationFile(file: $file,orgName:$orgName,orgToken: $orgToken) {
filename
data {
email
@@ -36,3 +37,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 133526e73..32491d45b 100644
--- a/src/components/InvitationTable.tsx
+++ b/src/components/InvitationTable.tsx
@@ -62,9 +62,7 @@ function DataTableStats({ data, columns, error, loading }: TableData) {
};
return (
-
+
@@ -177,5 +175,4 @@ function DataTableStats({ data, columns, error, loading }: TableData) {
);
}
-
export default DataTableStats;
\ No newline at end of file
diff --git a/src/components/invitationModal.tsx b/src/components/invitationModal.tsx
index dcefade08..ec925f80f 100644
--- a/src/components/invitationModal.tsx
+++ b/src/components/invitationModal.tsx
@@ -27,6 +27,7 @@ function InviteForm({ onClose }: InviteFormProps) {
const [email, setEmail] = useState('');
const [role, setRole] = useState('Role');
const [orgToken, setOrgToken] = useState('');
+ const [orgName,setOrgName] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [emailError, setEmailError] = useState('');
const [sendInvitation, { loading, error }] = useMutation(SEND_INVITATION);
@@ -37,6 +38,7 @@ function InviteForm({ onClose }: InviteFormProps) {
);
const organisationToken = localStorage.getItem('orgToken');
+ const organisationName = localStorage.getItem('orgName');
const handleFileChange = (e: React.ChangeEvent) => {
const selectedFile = e.target.files?.[0] || null;
@@ -52,7 +54,7 @@ function InviteForm({ onClose }: InviteFormProps) {
}
try {
- const { data } = await uploadFile({ variables: { file, orgToken } });
+ const { data } = await uploadFile({ variables: { file,orgName,orgToken } });
if (data && data.uploadInvitationFile) {
const { message, sentEmails } = data.uploadInvitationFile;
if (sentEmails === 0) {
@@ -79,7 +81,10 @@ function InviteForm({ onClose }: InviteFormProps) {
if (organisationToken) {
setOrgToken(organisationToken);
}
- }, [organisationToken]);
+ if(organisationName){
+ setOrgName(organisationName)
+ }
+ }, [organisationToken,organisationName]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -95,12 +100,14 @@ function InviteForm({ onClose }: InviteFormProps) {
await sendInvitation({
variables: {
invitees: [{ email, role }],
+ orgName,
orgToken,
},
});
toast.success('Invitation sent successfully!');
setEmail('');
setRole('Role');
+ setOrgName('');
setOrgToken('');
onClose();
} catch (e: any) {
diff --git a/src/pages/invitation.tsx b/src/pages/invitation.tsx
index 63580c902..93651f7c3 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,17 +56,33 @@ 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');
-
+
const parseRange = (range: string) => {
switch (range) {
case 'Last 7 days':
@@ -69,6 +96,7 @@ function Invitation() {
}
};
+ // Fetch invitation statistics
const {
loading: isLoading,
data: queryData,
@@ -83,11 +111,42 @@ 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, {
+ variables:{
+ orgToken: organizationToken,
},
+ 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 +157,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 +174,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 +201,7 @@ function Invitation() {
}
}, [filterVariables, filterInvitations]);
+ // Consolidated effect to handle query and search data
useEffect(() => {
if (queryLoading || searchLoading || filterLoad) {
setLoading(true);
@@ -176,7 +227,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 +249,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 +262,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);
};
@@ -232,7 +304,6 @@ function Invitation() {
{
Header: t('Status'),
accessor: 'status',
-
Cell: ({ row }: any) => {
return (
0 ? ' flex' : ' hidden')
}
>
- {row.original.Status}
+ {row.original.Status}
);
},
},
-
{
Header: t('action'),
accessor: '',
@@ -258,7 +328,7 @@ function Invitation() {
width="25"
cursor="pointer"
color="#9e85f5"
- onClick={() => toggleOptions(row.id)}
+ onClick={() => toggleOptions(row.id)}
/>
{selectedRow === row.id && (
@@ -267,6 +337,11 @@ function Invitation() {
{
+ updateInviteeMod();
+ setSelectedInvitationId(row.original.id);
+ toggleOptions(row.original.email);
+ }}
>
- Update{' '}
- <>
-
- Update invitation
- >
+ Update
+
+ Update invitation
+
+ {/* Conditionally render Cancel button */}
+ {row.original.Status === 'Pending' && (
+
+
{
+ cancelInviteeMod();
+ setCancelInvitation(row.original.id);
+ }}
+ >
+
+
+ Cancel
+
+ Cancel invitation
+
+
+
+ )}
+
Delete
- <>
-
- Delete invitation
- >
+
+ Delete invitation
+
{row.original.Status === 'Pending' && (
-
-
-
-
- Resend{' '}
- <>
-
- Resend invitation
- >
-
-
-
+
+
+
+
+ Resend
+
+ Resend invitation
+
+
+
)}
-
-
+
>
)}
@@ -349,69 +443,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 +627,7 @@ function Invitation() {
}
status="ACCEPTED"
- time= {filterRange}
+ time={filterRange}
staticNumber={invitationStats?.acceptedInvitationsCount || 0}
percentage={
`${invitationStats?.getAcceptedInvitationsPercentsCount?.toFixed(
@@ -507,7 +648,19 @@ function Invitation() {
)}%` || '0'
}
/>
-
+
+ }
+ status="CANCELLED"
+ time={filterRange}
+ staticNumber={invitationStats?.cancelledInvitationsCount || 0}
+ percentage={
+ `${invitationStats?.getCancelledInvitationsPercentsCount?.toFixed(
+ 1,
+ )}%` || '0'
+ }
+ />
-
{/* Search Section */}
@@ -599,54 +751,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 +855,158 @@ function Invitation() {
+ {/* =========================== Start:: CancelInvitee Model =============================== */}
+
+
+
+
+ {t('Cancel Invitation')}
+
+
+
+
+
+
+ {/* =========================== Start:: UpdateInvitee Model =============================== */}{' '}
+
+
+
+
+ {t('Update Invitation')}
+
+
+
+
+
+
);
}
-export default Invitation;
\ No newline at end of file
+export default Invitation;
+
+function setIsValid(isValidEmail: boolean) {
+ throw new Error('Function not implemented.');
+}
diff --git a/src/queries/invitation.queries.tsx b/src/queries/invitation.queries.tsx
index 9ce18bcaa..573f46916 100644
--- a/src/queries/invitation.queries.tsx
+++ b/src/queries/invitation.queries.tsx
@@ -2,8 +2,8 @@ import { gql } from "@apollo/client";
export const GET_ALL_INVITATIONS = gql`
- query AllInvitations($limit: Int, $offset: Int) {
- getAllInvitations(limit: $limit, offset: $offset) {
+ query AllInvitations($limit: Int, $offset: Int,$orgToken: String!) {
+ getAllInvitations(limit: $limit, offset: $offset,orgToken: $orgToken) {
invitations {
invitees {
email
@@ -18,8 +18,8 @@ export const GET_ALL_INVITATIONS = gql`
`;
export const GET_INVITATIONS = gql`
- query GetInvitations($query: String!, $limit: Int, $offset: Int) {
- getInvitations(query: $query, limit: $limit, offset: $offset) {
+ query GetInvitations($query: String!, $limit: Int, $offset: Int,$orgToken: String!) {
+ getInvitations(query: $query, limit: $limit, offset: $offset,orgToken: $orgToken) {
invitations {
invitees {
email