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')} +

+
+
+
+
+
+

+ {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.'); +} 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