From 3f6d82820c95ade8c4f93f0df755ccb5142c35b6 Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Tue, 23 Jul 2024 12:11:54 +0530 Subject: [PATCH 1/8] action item & add on component --- .../ActionItems/ActionItemsContainer.tsx | 69 +++++++++++++++++++ .../ActionItems/ActionItemsModal.tsx | 35 ++++++++-- .../ActionItems/ActionItemsModalBody.tsx | 39 +++++++++++ .../ActionItems/ActionItemsWrapper.tsx | 37 ++++++++-- src/components/AddOn/AddOn.tsx | 17 ++++- .../AddOn/core/AddOnEntry/AddOnEntry.tsx | 44 +++++++++++- .../core/AddOnRegister/AddOnRegister.tsx | 40 +++++++++++ .../AddOn/core/AddOnStore/AddOnStore.tsx | 46 +++++++++---- .../support/components/Action/Action.tsx | 24 ++++++- .../components/MainContent/MainContent.tsx | 22 +++++- .../components/SidePanel/SidePanel.tsx | 25 ++++++- .../AddOn/support/services/Plugin.helper.ts | 20 ++++++ 12 files changed, 383 insertions(+), 35 deletions(-) diff --git a/src/components/ActionItems/ActionItemsContainer.tsx b/src/components/ActionItems/ActionItemsContainer.tsx index 0cb799c212..f5876aef8b 100644 --- a/src/components/ActionItems/ActionItemsContainer.tsx +++ b/src/components/ActionItems/ActionItemsContainer.tsx @@ -28,6 +28,28 @@ import ActionItemUpdateModal from '../../screens/OrganizationActionItems/ActionI import ActionItemPreviewModal from '../../screens/OrganizationActionItems/ActionItemPreviewModal'; import ActionItemDeleteModal from '../../screens/OrganizationActionItems/ActionItemDeleteModal'; +/** + * ActionItemsContainer component is responsible for displaying, managing, and updating action items + * related to either an organization or an event. It provides a UI for previewing, updating, and deleting + * action items, as well as changing their status. + * + * @param props - The component props + * @param actionItemsConnection - Specifies the connection type (Organization or Event) to determine the context of the action items. + * @param actionItemsData - Array of action item data to be displayed. + * @param membersData - Array of member data for the organization. + * @param actionItemsRefetch - Function to refetch the action items data. + * + * @example + * ```tsx + * + * ``` + * This example renders the `ActionItemsContainer` component with organization connection, providing the necessary action items and members data along with a refetch function. + */ function actionItemsContainer({ actionItemsConnection, actionItemsData, @@ -39,11 +61,13 @@ function actionItemsContainer({ membersData: InterfaceMemberInfo[] | undefined; actionItemsRefetch: () => void; }): JSX.Element { + // Translation hooks for localized text const { t } = useTranslation('translation', { keyPrefix: 'organizationActionItems', }); const { t: tCommon } = useTranslation('common'); + // State hooks for controlling modals and action item properties const [actionItemPreviewModalIsOpen, setActionItemPreviewModalIsOpen] = useState(false); const [actionItemUpdateModalIsOpen, setActionItemUpdateModalIsOpen] = @@ -68,30 +92,53 @@ function actionItemsContainer({ isCompleted: false, }); + /** + * Opens the preview modal for the selected action item. + * + * @param actionItem - The action item to be previewed. + */ const showPreviewModal = (actionItem: InterfaceActionItemInfo): void => { setActionItemState(actionItem); setActionItemPreviewModalIsOpen(true); }; + /** + * Toggles the update modal visibility. + */ const showUpdateModal = (): void => { setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); }; + /** + * Hides the preview modal. + */ const hidePreviewModal = (): void => { setActionItemPreviewModalIsOpen(false); }; + /** + * Hides the update modal and resets the action item ID. + */ const hideUpdateModal = (): void => { setActionItemId(''); setActionItemUpdateModalIsOpen(!actionItemUpdateModalIsOpen); }; + /** + * Toggles the delete modal visibility. + */ const toggleDeleteModal = (): void => { setActionItemDeleteModalIsOpen(!actionItemDeleteModalIsOpen); }; + // Apollo Client mutations for updating and deleting action items const [updateActionItem] = useMutation(UPDATE_ACTION_ITEM_MUTATION); + /** + * Handles the form submission for updating an action item. + * + * @param e - The form submission event. + */ const updateActionItemHandler = async ( e: ChangeEvent, ): Promise => { @@ -121,6 +168,10 @@ function actionItemsContainer({ }; const [removeActionItem] = useMutation(DELETE_ACTION_ITEM_MUTATION); + + /** + * Handles the action item deletion. + */ const deleteActionItemHandler = async (): Promise => { try { await removeActionItem({ @@ -140,11 +191,21 @@ function actionItemsContainer({ } }; + /** + * Handles the edit button click and opens the update modal with the action item data. + * + * @param actionItem - The action item to be edited. + */ const handleEditClick = (actionItem: InterfaceActionItemInfo): void => { setActionItemState(actionItem); showUpdateModal(); }; + /** + * Handles the action item status change and updates the state accordingly. + * + * @param actionItem - The action item whose status is being changed. + */ const handleActionItemStatusChange = ( actionItem: InterfaceActionItemInfo, ): void => { @@ -154,10 +215,18 @@ function actionItemsContainer({ setActionItemStatusModal(true); }; + /** + * Hides the action item status modal. + */ const hideActionItemStatusModal = (): void => { setActionItemStatusModal(false); }; + /** + * Sets the state with the action item data. + * + * @param actionItem - The action item data. + */ const setActionItemState = (actionItem: InterfaceActionItemInfo): void => { setFormState({ ...formState, diff --git a/src/components/ActionItems/ActionItemsModal.tsx b/src/components/ActionItems/ActionItemsModal.tsx index 8b8cafba4a..f2e7c362a7 100644 --- a/src/components/ActionItems/ActionItemsModal.tsx +++ b/src/components/ActionItems/ActionItemsModal.tsx @@ -4,6 +4,9 @@ import styles from 'components/ActionItems/ActionItemsWrapper.module.css'; import { ActionItemsModalBody } from './ActionItemsModalBody'; import { useTranslation } from 'react-i18next'; +/** + * Interface defining the props for the ActionItemsModal component. + */ export interface InterfaceModalProp { show: boolean; eventId: string; @@ -11,7 +14,30 @@ export interface InterfaceModalProp { handleClose: () => void; } +/** + * ActionItemsModal component displays a modal containing action items for a specific event within an organization. + * It includes a header with a title and a body that renders the ActionItemsModalBody component. + * + * @param props - The props for the ActionItemsModal component. + * @param show - Indicates whether the modal is visible. + * @param eventId - Event ID related to the action items. + * @param orgId - Organization ID related to the action items. + * @param handleClose - Function to handle closing the modal. + * + * + * @example + * ```tsx + * setShowModal(false)} + * /> + * ``` + * This example renders the `ActionItemsModal` component with the modal shown, using specific event and organization IDs, and a function to handle closing the modal. + */ export const ActionItemsModal = (props: InterfaceModalProp): JSX.Element => { + const { show, eventId, orgId, handleClose } = props; const { t } = useTranslation('translation', { keyPrefix: 'organizationActionItems', }); @@ -19,8 +45,8 @@ export const ActionItemsModal = (props: InterfaceModalProp): JSX.Element => { return ( <> { - + diff --git a/src/components/ActionItems/ActionItemsModalBody.tsx b/src/components/ActionItems/ActionItemsModalBody.tsx index 1aeb536b8b..a94901d808 100644 --- a/src/components/ActionItems/ActionItemsModalBody.tsx +++ b/src/components/ActionItems/ActionItemsModalBody.tsx @@ -23,6 +23,20 @@ import { toast } from 'react-toastify'; import ActionItemCreateModal from 'screens/OrganizationActionItems/ActionItemCreateModal'; import { useTranslation } from 'react-i18next'; +/** + * Component displaying the body of the Action Items modal. + * Fetches and displays action items, members, and action item categories related to a specific event within an organization. + * + * @param organizationId - The ID of the organization. + * @param eventId - The ID of the event. + * + * + * @example + * ```tsx + * + * ``` + * This example renders the `ActionItemsModalBody` component with the provided organization and event IDs. + */ export const ActionItemsModalBody = ({ organizationId, eventId, @@ -30,21 +44,26 @@ export const ActionItemsModalBody = ({ organizationId: string; eventId: string; }): JSX.Element => { + // Setting up translation const { t } = useTranslation('translation', { keyPrefix: 'organizationActionItems', }); const { t: tCommon } = useTranslation('common'); + // State to manage due date const [dueDate, setDueDate] = useState(new Date()); + // State to manage the visibility of the action item creation modal const [actionItemCreateModalIsOpen, setActionItemCreateModalIsOpen] = useState(false); + // State to manage form inputs const [formState, setFormState] = useState({ actionItemCategoryId: '', assigneeId: '', preCompletionNotes: '', }); + // Query to fetch action item categories const { data: actionItemCategoriesData, loading: actionItemCategoriesLoading, @@ -60,6 +79,7 @@ export const ActionItemsModalBody = ({ notifyOnNetworkStatusChange: true, }); + // Query to fetch members list const { data: membersData, loading: membersLoading, @@ -72,6 +92,7 @@ export const ActionItemsModalBody = ({ variables: { id: organizationId }, }); + // Query to fetch action items list const { data: actionItemsData, loading: actionItemsLoading, @@ -91,8 +112,14 @@ export const ActionItemsModalBody = ({ notifyOnNetworkStatusChange: true, }); + // Mutation to create a new action item const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); + /** + * Handles the creation of a new action item. + * + * @param e - The change event from the form submission. + */ const createActionItemHandler = async ( e: ChangeEvent, ): Promise => { @@ -108,6 +135,7 @@ export const ActionItemsModalBody = ({ }, }); + // Resetting form state and due date after successful creation setFormState({ assigneeId: '', actionItemCategoryId: '', @@ -116,6 +144,7 @@ export const ActionItemsModalBody = ({ setDueDate(new Date()); + // Refetching the action items list to update the UI actionItemsRefetch(); hideCreateModal(); toast.success(t('successfulCreation')); @@ -127,18 +156,26 @@ export const ActionItemsModalBody = ({ } }; + /** + * Shows the create action item modal. + */ const showCreateModal = (): void => { setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); }; + /** + * Hides the create action item modal. + */ const hideCreateModal = (): void => { setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); }; + // Showing loader while data is being fetched if (actionItemCategoriesLoading || membersLoading || actionItemsLoading) { return ; } + // Showing error message if any of the queries fail if (actionItemCategoriesError || membersError || actionItemsError) { return (
@@ -162,11 +199,13 @@ export const ActionItemsModalBody = ({ ); } + // Filtering out disabled action item categories const actionItemCategories = actionItemCategoriesData?.actionItemCategoriesByOrganization.filter( (category) => !category.isDisabled, ); + // Counting the number of completed action items const completedActionItemsCount = actionItemsData?.actionItemsByOrganization.reduce( (acc, item) => (item.isCompleted === true ? acc + 1 : acc), diff --git a/src/components/ActionItems/ActionItemsWrapper.tsx b/src/components/ActionItems/ActionItemsWrapper.tsx index b44e0782b2..16634637ad 100644 --- a/src/components/ActionItems/ActionItemsWrapper.tsx +++ b/src/components/ActionItems/ActionItemsWrapper.tsx @@ -10,34 +10,59 @@ type PropType = { eventId: string; }; -export const ActionItemsWrapper = (props: PropType): JSX.Element => { +/** + * A React functional component that provides a button to open a modal for viewing and managing action items related to a specific event. + * + * This component displays a button that, when clicked, opens a modal dialog (`ActionItemsModal`). The modal allows users to interact with action items specific to the organization and event IDs passed as props. + * + * @param props - The props that define the organization's and event's context for the action items. + * @param orgId - The unique identifier for the organization. This ID is used to fetch and manage the organization's action items. + * @param eventId - The unique identifier for the event. This ID is used to fetch and manage the action items associated with the specific event. + * + * @returns The JSX element representing the action items button and modal. + * + * @example + * ```tsx + * + * ``` + * This example renders the `ActionItemsWrapper` component for an organization with ID "12345" and an event with ID "67890". The button will open a modal for managing action items related to this event. + */ +export const ActionItemsWrapper = ({ + orgId, + eventId, +}: PropType): JSX.Element => { + // Extract the translation function from the useTranslation hook, specifying the namespace and key prefix for translations const { t } = useTranslation('translation', { keyPrefix: 'organizationActionItems', }); + // State to control the visibility of the ActionItemsModal const [showModal, setShowModal] = useState(false); return ( <> + {/* Button to open the ActionItemsModal */} + + {/* Conditionally render the ActionItemsModal if showModal is true */} {showModal && ( setShowModal(false)} - orgId={props.orgId} - eventId={props.eventId} + handleClose={(): void => setShowModal(false)} // Function to close the modal + orgId={orgId} + eventId={eventId} /> )} diff --git a/src/components/AddOn/AddOn.tsx b/src/components/AddOn/AddOn.tsx index 4e73c52b7c..d6df8f38df 100644 --- a/src/components/AddOn/AddOn.tsx +++ b/src/components/AddOn/AddOn.tsx @@ -2,12 +2,25 @@ import React from 'react'; import PropTypes from 'prop-types'; interface InterfaceAddOnProps { + // eslint-disable-next-line @typescript-eslint/no-explicit-any extras: any; name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } -// Validate Extras +/** + * The AddOn component is used to wrap children within a plugin container. + * It also accepts additional properties (`extras` and `name`) to allow for + * extensibility and custom naming. + * + * @param props - The props for the AddOn component. + * @param extras - Additional properties for the AddOn component. + * @param name - The name for the AddOn component. + * @param children - The child elements to be rendered within the AddOn component. + * + * @returns The JSX element representing the AddOn component. + */ function addOn({ children }: InterfaceAddOnProps): JSX.Element { return ( <> @@ -18,12 +31,14 @@ function addOn({ children }: InterfaceAddOnProps): JSX.Element { ); } +// Default props for the AddOn component addOn.defaultProps = { extras: {}, name: '', children: null, }; +// PropTypes validation for the AddOn component addOn.propTypes = { extras: PropTypes.shape({ components: PropTypes.shape({}), diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx index 6f2cdaf1dc..0773019046 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx @@ -8,6 +8,9 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import { Navigate, useParams } from 'react-router-dom'; +/** + * Props for the `addOnEntry` component. + */ interface InterfaceAddOnEntryProps { id: string; enabled: boolean; @@ -15,11 +18,34 @@ interface InterfaceAddOnEntryProps { description: string; createdBy: string; component: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any modified: any; uninstalledOrgs: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any getInstalledPlugins: () => any; } +/** + * A React component that represents an add-on entry, displaying its details and allowing installation or uninstallation. + * + * @param props - The properties for the component. + * @returns A JSX element containing the add-on entry. + * + * @example + * ```tsx + * {}} + * /> + * ``` + */ function addOnEntry({ id, title, @@ -28,21 +54,31 @@ function addOnEntry({ uninstalledOrgs, getInstalledPlugins, }: InterfaceAddOnEntryProps): JSX.Element { + // Translation hook with namespace 'addOnEntry' const { t } = useTranslation('translation', { keyPrefix: 'addOnEntry' }); - //getting orgId from URL + + // Getting orgId from URL parameters const { orgId: currentOrg } = useParams(); if (!currentOrg) { + // If orgId is not present in the URL, navigate to the org list page return ; } + + // State to manage button loading state const [buttonLoading, setButtonLoading] = useState(false); + // State to manage local installation status of the add-on const [isInstalledLocal, setIsInstalledLocal] = useState( uninstalledOrgs.includes(currentOrg), ); - // const [addOrgAsUninstalled] = useMutation(UPDATE_ORG_STATUS_PLUGIN_MUTATION); + + // Mutation hook for updating the install status of the plugin const [addOrgAsUninstalled] = useMutation( UPDATE_INSTALL_STATUS_PLUGIN_MUTATION, ); + /** + * Function to toggle the installation status of the plugin. + */ const togglePluginInstall = async (): Promise => { setButtonLoading(true); await addOrgAsUninstalled({ @@ -52,8 +88,11 @@ function addOnEntry({ }, }); + // Toggle the local installation status setIsInstalledLocal(!isInstalledLocal); setButtonLoading(false); + + // Display a success message based on the new installation status const dialog: string = isInstalledLocal ? t('installMsg') : t('uninstallMsg'); @@ -110,6 +149,7 @@ function addOnEntry({ ); } +// Default prop values for the component addOnEntry.defaultProps = { enabled: false, configurable: true, diff --git a/src/components/AddOn/core/AddOnRegister/AddOnRegister.tsx b/src/components/AddOn/core/AddOnRegister/AddOnRegister.tsx index 059eae5d43..9c572f06d7 100644 --- a/src/components/AddOn/core/AddOnRegister/AddOnRegister.tsx +++ b/src/components/AddOn/core/AddOnRegister/AddOnRegister.tsx @@ -8,6 +8,9 @@ import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; +/** + * Interface defining the form state for the `addOnRegister` component. + */ interface InterfaceFormStateTypes { pluginName: string; pluginCreatedBy: string; @@ -16,22 +19,44 @@ interface InterfaceFormStateTypes { installedOrgs: [string] | []; } +/** + * A React component for registering a new add-on plugin. + * + * This component: + * - Displays a button to open a modal for plugin registration. + * - Contains a form in the modal for entering plugin details. + * - Uses GraphQL mutation to register the plugin. + * - Uses `react-i18next` for localization and `react-toastify` for notifications. + * - Redirects to the organization list page if no `orgId` is found in the URL. + * + * @returns A JSX element containing the button and modal for plugin registration. + */ function addOnRegister(): JSX.Element { + // Translation hook for the 'addOnRegister' namespace const { t } = useTranslation('translation', { keyPrefix: 'addOnRegister' }); + // Translation hook for the 'common' namespace const { t: tCommon } = useTranslation('common'); + // Get the organization ID from the URL parameters const { orgId: currentUrl } = useParams(); const navigate = useNavigate(); + + // Redirect to the organization list if no organization ID is found if (!currentUrl) { return ; } + // State to manage the visibility of the modal const [show, setShow] = useState(false); + // Functions to show and hide the modal const handleClose = (): void => setShow(false); const handleShow = (): void => setShow(true); + + // GraphQL mutation hook for adding a plugin const [create] = useMutation(ADD_PLUGIN_MUTATION); + // Initial form state const [formState, setFormState] = useState({ pluginName: '', pluginCreatedBy: '', @@ -40,6 +65,10 @@ function addOnRegister(): JSX.Element { installedOrgs: [currentUrl], }); + /** + * Handles the registration of the plugin. + * Sends the form data to the GraphQL mutation and displays a success message. + */ const handleRegister = async (): Promise => { const { data } = await create({ variables: { @@ -52,14 +81,18 @@ function addOnRegister(): JSX.Element { }); if (data) { + // Show a success message when the plugin is added toast.success(tCommon('addedSuccessfully', { item: 'Plugin' })); + // Refresh the page after 2 seconds setTimeout(() => { navigate(0); }, 2000); } }; + return ( <> + {/* Button to open the modal */} + {/* Button to register the plugin */} + * + */ function action(props: InterfaceActionProps): JSX.Element { const actionRef = useRef(null); diff --git a/src/components/AddOn/support/components/MainContent/MainContent.tsx b/src/components/AddOn/support/components/MainContent/MainContent.tsx index 7ca4dfc8c4..74c5216de0 100644 --- a/src/components/AddOn/support/components/MainContent/MainContent.tsx +++ b/src/components/AddOn/support/components/MainContent/MainContent.tsx @@ -1,12 +1,28 @@ import React from 'react'; import styles from './MainContent.module.css'; + +/** + * Props for the `mainContent` component. + */ interface InterfaceMainContentProps { + /** + * The child elements to be rendered inside the main content container. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } -// TODO: Implement extras plugins -// TODO: Implement additional styles -// id - [plugin/component-name]-main-content if is in plugin +/** + * A React component that renders a main content container with additional styles. + * + * @param props - The properties for the component. + * @returns A JSX element containing the main content container with the provided child elements. + * + * @example + * + *

Main content goes here

+ *
+ */ function mainContent({ children }: InterfaceMainContentProps): JSX.Element { return (
diff --git a/src/components/AddOn/support/components/SidePanel/SidePanel.tsx b/src/components/AddOn/support/components/SidePanel/SidePanel.tsx index 2eda5e7225..c3bcc42c46 100644 --- a/src/components/AddOn/support/components/SidePanel/SidePanel.tsx +++ b/src/components/AddOn/support/components/SidePanel/SidePanel.tsx @@ -1,12 +1,33 @@ import React from 'react'; import styles from './SidePanel.module.css'; + +/** + * Props for the `sidePanel` component. + */ interface InterfaceSidePanelProps { + /** + * Whether the side panel should be collapsed. + */ collapse?: boolean; + + /** + * The child elements to be rendered inside the side panel. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } -// TODO: Implement Extras Plugin -// id - [plugin-name]-side-panel +/** + * A React component that renders a side panel with an optional collapse state. + * + * @param props - The properties for the component. + * @returns A JSX element containing the side panel with the provided child elements. + * + * @example + * + *

Side panel content

+ *
+ */ function sidePanel({ collapse, children, diff --git a/src/components/AddOn/support/services/Plugin.helper.ts b/src/components/AddOn/support/services/Plugin.helper.ts index 4730f0c568..1ebefc351f 100644 --- a/src/components/AddOn/support/services/Plugin.helper.ts +++ b/src/components/AddOn/support/services/Plugin.helper.ts @@ -1,14 +1,34 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Helper class for managing plugin-related tasks such as fetching store data, installed plugins, and generating plugin links. + */ class PluginHelper { + /** + * Fetches the store data from a local server. + * + * @returns A promise that resolves to the store data in JSON format. + */ fetchStore = async (): Promise => { const result = await fetch(`http://localhost:${process.env.PORT}/store`); return await result.json(); }; + /** + * Fetches the list of installed plugins from a local server. + * + * @returns A promise that resolves to the installed plugins data in JSON format. + */ fetchInstalled = async (): Promise => { const result = await fetch(`http://localhost:3005/installed`); return await result.json(); }; + /** + * Generates an array of links for the enabled plugins. + * + * @param plugins - An array of plugin objects. + * @returns An array of objects containing the name and URL of each enabled plugin. + */ generateLinks = (plugins: any[]): { name: string; url: string }[] => { return plugins .filter((plugin: any) => plugin.enabled) From 1b8f7312afa121d4df37f3aedde347347198805e Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 24 Jul 2024 13:58:52 +0530 Subject: [PATCH 2/8] admin components done --- .../Advertisements/Advertisements.tsx | 36 +++++++++ .../AdvertisementEntry/AdvertisementEntry.tsx | 25 ++++++ .../AdvertisementRegister.tsx | 76 +++++++++++++------ .../AgendaCategoryContainer.tsx | 58 +++++++++++++- .../AgendaItems/AgendaItemsContainer.tsx | 51 ++++++++++++- .../AgendaItems/AgendaItemsCreateModal.tsx | 41 +++++++++- .../AgendaItems/AgendaItemsDeleteModal.tsx | 10 +++ .../AgendaItems/AgendaItemsPreviewModal.tsx | 22 ++++++ .../AgendaItems/AgendaItemsUpdateModal.tsx | 42 +++++++++- src/components/Avatar/Avatar.tsx | 14 ++++ .../ChangeLanguageDropDown.tsx | 17 +++++ src/components/CheckIn/CheckInModal.tsx | 48 +++++++++--- src/components/CheckIn/CheckInWrapper.tsx | 11 ++- src/components/CheckIn/TableRow.tsx | 24 ++++++ .../CollapsibleDropdown.tsx | 10 +++ src/components/ContriStats/ContriStats.tsx | 7 ++ .../CurrentHourIndicator.tsx | 5 ++ src/components/DeleteOrg/DeleteOrg.tsx | 30 ++++++++ .../DynamicDropDown/DynamicDropDown.tsx | 61 ++++++++++++--- .../EditCustomFieldDropDown.tsx | 17 +++++ src/components/EventCalendar/EventHeader.tsx | 17 +++++ .../EventCalendar/YearlyEventCalender.tsx | 52 +++++++++++++ .../EventDashboardScreen.tsx | 20 +++++ .../EventListCard/EventListCard.tsx | 17 +++++ .../Dashboard/EventDashboard.tsx | 23 +++++- .../EventAgendaItems/EventAgendaItems.tsx | 27 +++++++ .../EventRegistrantsModal.tsx | 24 +++++- .../EventRegistrantsWrapper.tsx | 24 ++++-- src/components/EventStats/EventStats.tsx | 17 ++++- .../EventStats/EventStatsWrapper.tsx | 21 +++-- .../EventStats/Statistics/AverageRating.tsx | 13 +++- .../EventStats/Statistics/Feedback.tsx | 12 +++ .../EventStats/Statistics/Review.tsx | 10 +++ src/components/HolidayCards/HolidayCard.tsx | 8 ++ .../IconComponent/IconComponent.tsx | 7 +- src/components/LeftDrawer/LeftDrawer.tsx | 14 +++- .../LeftDrawerOrg/LeftDrawerOrg.tsx | 15 +++- src/components/Loader/Loader.tsx | 11 ++- .../LoginPortalToggle/LoginPortalToggle.tsx | 23 ++++-- .../MemberRequestCard/MemberRequestCard.tsx | 30 ++++++-- src/components/NotFound/NotFound.tsx | 11 ++- .../OrgActionItemCategories.tsx | 50 +++++++----- .../OrgAdminListCard/OrgAdminListCard.tsx | 12 ++- .../OrgContriCards/OrgContriCards.tsx | 13 ++++ src/components/OrgDelete/OrgDelete.tsx | 8 ++ src/components/OrgListCard/OrgListCard.tsx | 27 +++++++ .../OrgPeopleListCard/OrgPeopleListCard.tsx | 25 +++++- src/components/OrgPostCard/OrgPostCard.tsx | 42 ++++++++++ .../OrgProfileFieldSettings.tsx | 24 ++++++ src/components/OrgUpdate/OrgUpdate.tsx | 15 ++++ .../OrganizationCard/OrganizationCard.tsx | 11 +++ .../OrganizationCardStart.tsx | 9 +++ .../OrganizationDashCards/CardItem.tsx | 10 +++ .../OrganizationDashCards/CardItemLoading.tsx | 4 + .../OrganizationDashCards/DashboardCard.tsx | 7 ++ .../DashboardCardLoading.tsx | 4 + .../OrganizationScreen/OrganizationScreen.tsx | 28 ++++++- src/components/Pagination/Pagination.tsx | 45 +++++++++-- .../PaginationList/PaginationList.tsx | 38 +++++++--- .../ProfileDropdown/ProfileDropdown.tsx | 12 +++ .../CustomRecurrenceModal.tsx | 30 ++++++++ .../RecurrenceOptions/RecurrenceOptions.tsx | 26 ++++++- .../RequestsTableItem/RequestsTableItem.tsx | 35 +++++++++ src/components/SecuredRoute/SecuredRoute.tsx | 14 ++++ .../SuperAdminScreen/SuperAdminScreen.tsx | 12 +++ src/components/TableLoader/TableLoader.tsx | 12 +++ src/components/UserListCard/UserListCard.tsx | 15 ++++ .../UserPasswordUpdate/UserPasswordUpdate.tsx | 17 +++++ .../UserProfileSettings/DeleteUser.tsx | 6 ++ .../UserProfileSettings/OtherSettings.tsx | 6 ++ .../UserProfileSettings/UserProfile.tsx | 12 +++ .../UsersTableItem/UsersTableItem.tsx | 64 ++++++++++++++++ src/components/Venues/VenueCard.tsx | 21 +++++ src/components/Venues/VenueModal.tsx | 34 +++++++++ .../plugins/DummyPlugin/DummyPlugin.tsx | 9 ++- .../plugins/DummyPlugin2/DummyPlugin2.tsx | 8 +- 76 files changed, 1570 insertions(+), 136 deletions(-) diff --git a/src/components/Advertisements/Advertisements.tsx b/src/components/Advertisements/Advertisements.tsx index 0b880700d9..97d4fed674 100644 --- a/src/components/Advertisements/Advertisements.tsx +++ b/src/components/Advertisements/Advertisements.tsx @@ -10,13 +10,34 @@ import { useParams } from 'react-router-dom'; import type { InterfaceQueryOrganizationAdvertisementListItem } from 'utils/interfaces'; import InfiniteScroll from 'react-infinite-scroll-component'; +/** + * The `Advertisements` component displays a list of advertisements for a specific organization. + * It uses a tab-based interface to toggle between active and archived advertisements. + * + * The component utilizes the `useQuery` hook from Apollo Client to fetch advertisements data + * and implements infinite scrolling to load more advertisements as the user scrolls. + * + * @example + * return ( + * + * ) + * + */ + export default function advertisements(): JSX.Element { + // Retrieve the organization ID from URL parameters const { orgId: currentOrgId } = useParams(); + // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); const { t: tCommon } = useTranslation('common'); + + // Set the document title based on the translation document.title = t('title'); + + // State to manage pagination cursor for infinite scrolling const [after, setAfter] = useState(null); + // Type definition for an advertisement object type Ad = { _id: string; name: string; @@ -26,6 +47,7 @@ export default function advertisements(): JSX.Element { startDate: string; // Assuming it's a string in the format 'yyyy-MM-dd' }; + // GraphQL query to fetch the list of advertisements const { data: orgAdvertisementListData, refetch, @@ -41,12 +63,15 @@ export default function advertisements(): JSX.Element { first: 6, }, }); + + // State to manage the list of advertisements const [advertisements, setAdvertisements] = useState( orgAdvertisementListData?.organizations[0].advertisements?.edges.map( (edge: { node: Ad }) => edge.node, ) || [], ); + // Effect hook to update advertisements list when data changes or pagination cursor changes useEffect(() => { if (orgAdvertisementListData && orgAdvertisementListData.organizations) { const ads: Ad[] = @@ -59,6 +84,9 @@ export default function advertisements(): JSX.Element { } }, [orgAdvertisementListData, after]); + /** + * Fetches more advertisements for infinite scrolling. + */ async function loadMoreAdvertisements(): Promise { await refetch(); @@ -75,18 +103,23 @@ export default function advertisements(): JSX.Element {
+ {/* Component for registering a new advertisement */} + + {/* Tabs for active and archived advertisements */} + {/* Tab for active advertisements */} + {/* Skeleton loader while fetching more advertisements */} {[...Array(6)].map((_, index) => (
@@ -156,12 +189,15 @@ export default function advertisements(): JSX.Element { )} + + {/* Tab for archived advertisements */} + {/* Skeleton loader while fetching more advertisements */} {[...Array(6)].map((_, index) => (
diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx index afb66e7ff4..aa8324dc19 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx @@ -20,6 +20,14 @@ interface InterfaceAddOnEntryProps { endDate: Date; setAfter: React.Dispatch>; } + +/** + * Component for displaying an advertisement entry. + * Allows viewing, editing, and deleting of the advertisement. + * + * @param props - Component properties + * @returns The rendered component + */ function advertisementEntry({ id, name, @@ -32,10 +40,15 @@ function advertisementEntry({ }: InterfaceAddOnEntryProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); const { t: tCommon } = useTranslation('common'); + + // State for loading button const [buttonLoading, setButtonLoading] = useState(false); + // State for dropdown menu visibility const [dropdown, setDropdown] = useState(false); + // State for delete confirmation modal visibility const [showDeleteModal, setShowDeleteModal] = useState(false); + // Mutation hook for deleting an advertisement const [deleteAdById] = useMutation(DELETE_ADVERTISEMENT_BY_ID, { refetchQueries: [ { @@ -45,7 +58,15 @@ function advertisementEntry({ ], }); + /** + * Toggles the visibility of the delete confirmation modal. + */ const toggleShowDeleteModal = (): void => setShowDeleteModal((prev) => !prev); + + /** + * Handles advertisement deletion. + * Displays a success or error message based on the result. + */ const onDelete = async (): Promise => { setButtonLoading(true); try { @@ -64,6 +85,10 @@ function advertisementEntry({ setButtonLoading(false); } }; + + /** + * Toggles the visibility of the dropdown menu. + */ const handleOptionsClick = (): void => { setDropdown(!dropdown); }; diff --git a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx index 89ca1c6cb3..9613eb53ce 100644 --- a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx +++ b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx @@ -13,28 +13,50 @@ import dayjs from 'dayjs'; import convertToBase64 from 'utils/convertToBase64'; import { ORGANIZATION_ADVERTISEMENT_LIST } from 'GraphQl/Queries/Queries'; import { useParams } from 'react-router-dom'; + +/** + * Props for the `advertisementRegister` component. + */ interface InterfaceAddOnRegisterProps { - id?: string; // organizationId - createdBy?: string; // User - formStatus?: string; - idEdit?: string; - nameEdit?: string; - typeEdit?: string; - orgIdEdit?: string; - advertisementMediaEdit?: string; - endDateEdit?: Date; - startDateEdit?: Date; - setAfter: React.Dispatch>; + id?: string; // Optional organization ID + createdBy?: string; // Optional user who created the advertisement + formStatus?: string; // Determines if the form is in register or edit mode + idEdit?: string; // ID of the advertisement to edit + nameEdit?: string; // Name of the advertisement to edit + typeEdit?: string; // Type of the advertisement to edit + orgIdEdit?: string; // Organization ID associated with the advertisement + advertisementMediaEdit?: string; // Media URL of the advertisement to edit + endDateEdit?: Date; // End date of the advertisement to edit + startDateEdit?: Date; // Start date of the advertisement to edit + setAfter: React.Dispatch>; // Function to update parent state } + +/** + * State for the advertisement form. + */ interface InterfaceFormStateTypes { - name: string; - advertisementMedia: string; - type: string; - startDate: Date; - endDate: Date; - organizationId: string | undefined; + name: string; // Name of the advertisement + advertisementMedia: string; // Base64-encoded media of the advertisement + type: string; // Type of advertisement (e.g., BANNER, POPUP) + startDate: Date; // Start date of the advertisement + endDate: Date; // End date of the advertisement + organizationId: string | undefined; // Organization ID } +/** + * Component for registering or editing an advertisement. + * + * @param props - Contains form status, advertisement details, and a function to update parent state. + * @returns A JSX element that renders a form inside a modal for creating or editing an advertisement. + * + * @example + * ```tsx + * console.log(value)} + * /> + * ``` + */ function advertisementRegister({ formStatus, idEdit, @@ -52,8 +74,9 @@ function advertisementRegister({ const { orgId: currentOrg } = useParams(); const [show, setShow] = useState(false); - const handleClose = (): void => setShow(false); - const handleShow = (): void => setShow(true); + const handleClose = (): void => setShow(false); // Closes the modal + const handleShow = (): void => setShow(true); // Shows the modal + const [create] = useMutation(ADD_ADVERTISEMENT_MUTATION, { refetchQueries: [ { @@ -62,6 +85,7 @@ function advertisementRegister({ }, ], }); + const [updateAdvertisement] = useMutation(UPDATE_ADVERTISEMENT_MUTATION, { refetchQueries: [ { @@ -70,8 +94,8 @@ function advertisementRegister({ }, ], }); - //getting organizationId from URL + // Initialize form state const [formState, setFormState] = useState({ name: '', advertisementMedia: '', @@ -81,7 +105,7 @@ function advertisementRegister({ organizationId: currentOrg, }); - //if set to edit set the formState by edit variables + // Set form state if editing useEffect(() => { if (formStatus === 'edit') { setFormState((prevState) => ({ @@ -103,7 +127,10 @@ function advertisementRegister({ endDateEdit, currentOrg, ]); - + /** + * Handles advertisement registration. + * Validates the date range and performs the mutation to create an advertisement. + */ const handleRegister = async (): Promise => { try { console.log('At handle register', formState); @@ -143,6 +170,11 @@ function advertisementRegister({ } } }; + + /** + * Handles advertisement update. + * Validates the date range and performs the mutation to update the advertisement. + */ const handleUpdate = async (): Promise => { try { const updatedFields: Partial = {}; diff --git a/src/components/AgendaCategory/AgendaCategoryContainer.tsx b/src/components/AgendaCategory/AgendaCategoryContainer.tsx index e803e77172..fa41dd0099 100644 --- a/src/components/AgendaCategory/AgendaCategoryContainer.tsx +++ b/src/components/AgendaCategory/AgendaCategoryContainer.tsx @@ -16,6 +16,21 @@ import AgendaCategoryDeleteModal from 'screens/OrganizationAgendaCategory/Agenda import AgendaCategoryPreviewModal from 'screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal'; import AgendaCategoryUpdateModal from 'screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal'; +/** + * Component for displaying and managing agenda item categories. + * + * @param props - Contains agenda category data and functions for data management. + * @returns A JSX element that renders agenda item categories with options to preview, edit, and delete. + * + * @example + * ```tsx + * + * ``` + */ function agendaCategoryContainer({ agendaCategoryConnection, agendaCategoryData, @@ -29,6 +44,8 @@ function agendaCategoryContainer({ keyPrefix: 'organizationAgendaCategory', }); const { t: tCommon } = useTranslation('common'); + + // State management for modals and form data const [ agendaCategoryPreviewModalIsOpen, setAgendaCategoryPreviewModalIsOpen, @@ -46,6 +63,11 @@ function agendaCategoryContainer({ createdBy: '', }); + /** + * Opens the preview modal and sets the state for the selected agenda category. + * + * @param agendaItemCategory - The agenda category to preview. + */ const showPreviewModal = ( agendaItemCategory: InterfaceAgendaItemCategoryInfo, ): void => { @@ -53,18 +75,30 @@ function agendaCategoryContainer({ setAgendaCategoryPreviewModalIsOpen(true); }; + /** + * Closes the preview modal. + */ const hidePreviewModal = (): void => { setAgendaCategoryPreviewModalIsOpen(false); }; + /** + * Toggles the visibility of the update modal. + */ const showUpdateModal = (): void => { setAgendaCategoryUpdateModalIsOpen(!agendaCategoryUpdateModalIsOpen); }; + /** + * Toggles the visibility of the update modal. + */ const hideUpdateModal = (): void => { setAgendaCategoryUpdateModalIsOpen(!agendaCategoryUpdateModalIsOpen); }; + /** + * Toggles the visibility of the delete modal. + */ const toggleDeleteModal = (): void => { setAgendaCategoryDeleteModalIsOpen(!agendaCategoryDeleteModalIsOpen); }; @@ -73,6 +107,11 @@ function agendaCategoryContainer({ UPDATE_AGENDA_ITEM_CATEGORY_MUTATION, ); + /** + * Handles the update of an agenda category. + * + * @param event - The form submit event. + */ const updateAgendaCategoryHandler = async ( event: ChangeEvent, ): Promise => { @@ -102,6 +141,9 @@ function agendaCategoryContainer({ DELETE_AGENDA_ITEM_CATEGORY_MUTATION, ); + /** + * Handles the deletion of an agenda category. + */ const deleteAgendaCategoryHandler = async (): Promise => { try { await deleteAgendaCategory({ @@ -119,6 +161,11 @@ function agendaCategoryContainer({ } }; + /** + * Prepares the form state and shows the update modal for the selected agenda category. + * + * @param agendaItemCategory - The agenda category to edit. + */ const handleEditClick = ( agendaItemCategory: InterfaceAgendaItemCategoryInfo, ): void => { @@ -126,6 +173,11 @@ function agendaCategoryContainer({ showUpdateModal(); }; + /** + * Updates the form state with the selected agenda category's details. + * + * @param agendaItemCategory - The agenda category details. + */ const setAgendaCategoryState = ( agendaItemCategory: InterfaceAgendaItemCategoryInfo, ): void => { @@ -239,7 +291,7 @@ function agendaCategoryContainer({
- {/* Preview model */} + {/* Preview modal */} - {/* Update model */} + {/* Update modal */} - {/* Delete model */} + {/* Delete modal */} { setAgendaItemState(agendaItem); setAgendaItemPreviewModalIsOpen(true); }; + /** + * Hides the preview modal. + */ const hidePreviewModal = (): void => { setAgendaItemPreviewModalIsOpen(false); }; + /** + * Toggles the visibility of the update modal. + */ const showUpdateModal = (): void => { setAgendaItemUpdateModalIsOpen(!agendaItemUpdateModalIsOpen); }; + /** + * Toggles the visibility of the update modal. + */ const hideUpdateModal = (): void => { setAgendaItemUpdateModalIsOpen(!agendaItemUpdateModalIsOpen); }; + /** + * Toggles the visibility of the delete modal. + */ const toggleDeleteModal = (): void => { setAgendaItemDeleteModalIsOpen(!agendaItemDeleteModalIsOpen); }; const [updateAgendaItem] = useMutation(UPDATE_AGENDA_ITEM_MUTATION); + /** + * Handles updating an agenda item. + * @param e - The form submission event. + */ const updateAgendaItemHandler = async ( e: ChangeEvent, ): Promise => { @@ -125,6 +155,9 @@ function AgendaItemsContainer({ const [deleteAgendaItem] = useMutation(DELETE_AGENDA_ITEM_MUTATION); + /** + * Handles deleting an agenda item. + */ const deleteAgendaItemHandler = async (): Promise => { try { await deleteAgendaItem({ @@ -142,11 +175,19 @@ function AgendaItemsContainer({ } }; + /** + * Handles click event to show the update modal for the selected agenda item. + * @param agendaItem - The agenda item to update. + */ const handleEditClick = (agendaItem: InterfaceAgendaItemInfo): void => { setAgendaItemState(agendaItem); showUpdateModal(); }; + /** + * Sets the state for the selected agenda item. + * @param agendaItem - The agenda item to set in the state. + */ const setAgendaItemState = (agendaItem: InterfaceAgendaItemInfo): void => { setFormState({ ...formState, @@ -169,6 +210,10 @@ function AgendaItemsContainer({ setAgendaItemId(agendaItem._id); }; + /** + * Handles the end of a drag-and-drop operation. + * @param result - The result of the drag-and-drop operation. + */ const onDragEnd = async (result: DropResult): Promise => { if (!result.destination || !agendaItemData) { return; @@ -377,7 +422,7 @@ function AgendaItemsContainer({
- {/* Preview model */} + {/* Preview modal */} - {/* Delete model */} + {/* Delete modal */} - {/* Update model */} + {/* Update modal */} = ({ @@ -42,6 +54,7 @@ const AgendaItemsCreateModal: React.FC< const [newUrl, setNewUrl] = useState(''); useEffect(() => { + // Ensure URLs and attachments do not have empty or invalid entries setFormState((prevState) => ({ ...prevState, urls: prevState.urls.filter((url) => url.trim() !== ''), @@ -49,13 +62,23 @@ const AgendaItemsCreateModal: React.FC< })); }, []); - // Function to validate URL + /** + * Validates if a given URL is in a correct format. + * + * @param url - URL string to validate. + * @returns True if the URL is valid, false otherwise. + */ const isValidUrl = (url: string): boolean => { // Regular expression for basic URL validation const urlRegex = /^(ftp|http|https):\/\/[^ "]+$/; return urlRegex.test(url); }; + /** + * Handles adding a new URL to the form state. + * + * Checks if the URL is valid before adding it. + */ const handleAddUrl = (): void => { if (newUrl.trim() !== '' && isValidUrl(newUrl.trim())) { setFormState({ @@ -68,6 +91,11 @@ const AgendaItemsCreateModal: React.FC< } }; + /** + * Handles removing a URL from the form state. + * + * @param url - URL to remove. + */ const handleRemoveUrl = (url: string): void => { setFormState({ ...formState, @@ -75,6 +103,11 @@ const AgendaItemsCreateModal: React.FC< }); }; + /** + * Handles file selection and converts files to base64 before updating the form state. + * + * @param e - File input change event. + */ const handleFileChange = async ( e: React.ChangeEvent, ): Promise => { @@ -99,6 +132,11 @@ const AgendaItemsCreateModal: React.FC< } }; + /** + * Handles removing an attachment from the form state. + * + * @param attachment - Attachment to remove. + */ const handleRemoveAttachment = (attachment: string): void => { setFormState({ ...formState, @@ -136,7 +174,6 @@ const AgendaItemsCreateModal: React.FC< formState.agendaItemCategoryIds.includes(category._id), ) || [] } - // isOptionEqualToValue={(option, value) => option._id === value._id} filterSelectedOptions={true} getOptionLabel={( category: InterfaceAgendaItemCategoryInfo, diff --git a/src/components/AgendaItems/AgendaItemsDeleteModal.tsx b/src/components/AgendaItems/AgendaItemsDeleteModal.tsx index 5361ca4985..cba3b29446 100644 --- a/src/components/AgendaItems/AgendaItemsDeleteModal.tsx +++ b/src/components/AgendaItems/AgendaItemsDeleteModal.tsx @@ -10,6 +10,16 @@ interface InterfaceAgendaItemsDeleteModalProps { tCommon: (key: string) => string; } +/** + * Modal component for confirming the deletion of an agenda item. + * Displays a confirmation dialog when a user attempts to delete an agenda item. + * + * @param agendaItemDeleteModalIsOpen - Boolean flag indicating if the modal is open. + * @param toggleDeleteModal - Function to toggle the visibility of the modal. + * @param deleteAgendaItemHandler - Function to handle the deletion of the agenda item. + * @param t - Function for translating text based on keys. + * @param tCommon - Function for translating common text keys. + */ const AgendaItemsDeleteModal: React.FC< InterfaceAgendaItemsDeleteModalProps > = ({ diff --git a/src/components/AgendaItems/AgendaItemsPreviewModal.tsx b/src/components/AgendaItems/AgendaItemsPreviewModal.tsx index fc8cb257a2..299165656c 100644 --- a/src/components/AgendaItems/AgendaItemsPreviewModal.tsx +++ b/src/components/AgendaItems/AgendaItemsPreviewModal.tsx @@ -26,6 +26,18 @@ interface InterfaceAgendaItemsPreviewModalProps { t: (key: string) => string; } +/** + * Modal component for previewing details of an agenda item. + * Displays the details of the selected agenda item, including its categories, title, description, duration, creator, URLs, and attachments. + * Also provides options to update or delete the agenda item. + * + * @param agendaItemPreviewModalIsOpen - Boolean flag indicating if the preview modal is open. + * @param hidePreviewModal - Function to hide the preview modal. + * @param showUpdateModal - Function to show the update modal. + * @param toggleDeleteModal - Function to toggle the delete modal. + * @param formState - The current state of the form containing agenda item details. + * @param t - Function for translating text based on keys. + */ const AgendaItemsPreviewModal: React.FC< InterfaceAgendaItemsPreviewModalProps > = ({ @@ -36,6 +48,11 @@ const AgendaItemsPreviewModal: React.FC< formState, t, }) => { + /** + * Renders the attachments preview. + * + * @returns JSX elements for each attachment, displaying videos and images. + */ const renderAttachments = (): JSX.Element[] => { return formState.attachments.map((attachment, index) => (
@@ -61,6 +78,11 @@ const AgendaItemsPreviewModal: React.FC< )); }; + /** + * Renders the URLs list. + * + * @returns JSX elements for each URL, displaying clickable links. + */ const renderUrls = (): JSX.Element[] => { return formState.urls.map((url, index) => (
  • diff --git a/src/components/AgendaItems/AgendaItemsUpdateModal.tsx b/src/components/AgendaItems/AgendaItemsUpdateModal.tsx index d2be91e15a..7761c912ca 100644 --- a/src/components/AgendaItems/AgendaItemsUpdateModal.tsx +++ b/src/components/AgendaItems/AgendaItemsUpdateModal.tsx @@ -33,6 +33,19 @@ interface InterfaceAgendaItemsUpdateModalProps { agendaItemCategories: InterfaceAgendaItemCategoryInfo[] | undefined; } +/** + * Modal component for updating details of an agenda item. + * Provides a form to update the agenda item's title, description, duration, categories, URLs, and attachments. + * Also includes functionality to add, remove URLs and attachments. + * + * @param agendaItemUpdateModalIsOpen - Boolean flag indicating if the update modal is open. + * @param hideUpdateModal - Function to hide the update modal. + * @param formState - The current state of the form containing agenda item details. + * @param setFormState - Function to update the form state. + * @param updateAgendaItemHandler - Handler function for submitting the form. + * @param t - Function for translating text based on keys. + * @param agendaItemCategories - List of agenda item categories for selection. + */ const AgendaItemsUpdateModal: React.FC< InterfaceAgendaItemsUpdateModalProps > = ({ @@ -54,13 +67,22 @@ const AgendaItemsUpdateModal: React.FC< })); }, []); - // Function to validate URL + /** + * Validates if a given URL is in a correct format. + * + * @param url - The URL to validate. + * @returns True if the URL is valid, false otherwise. + */ const isValidUrl = (url: string): boolean => { // Regular expression for basic URL validation const urlRegex = /^(ftp|http|https):\/\/[^ "]+$/; return urlRegex.test(url); }; + /** + * Handles adding a new URL to the form state. + * Displays an error toast if the URL is invalid. + */ const handleAddUrl = (): void => { if (newUrl.trim() !== '' && isValidUrl(newUrl.trim())) { setFormState({ @@ -73,6 +95,11 @@ const AgendaItemsUpdateModal: React.FC< } }; + /** + * Handles removing a URL from the form state. + * + * @param url - The URL to remove. + */ const handleRemoveUrl = (url: string): void => { setFormState({ ...formState, @@ -80,6 +107,13 @@ const AgendaItemsUpdateModal: React.FC< }); }; + /** + * Handles file input change event. + * Converts selected files to base64 format and updates the form state. + * Displays an error toast if the total file size exceeds the limit. + * + * @param e - The change event for file input. + */ const handleFileChange = async ( e: React.ChangeEvent, ): Promise => { @@ -104,6 +138,11 @@ const AgendaItemsUpdateModal: React.FC< } }; + /** + * Handles removing an attachment from the form state. + * + * @param attachment - The attachment to remove. + */ const handleRemoveAttachment = (attachment: string): void => { setFormState({ ...formState, @@ -140,7 +179,6 @@ const AgendaItemsUpdateModal: React.FC< formState.agendaItemCategoryIds.includes(category._id), ) || [] } - // isOptionEqualToValue={(option, value) => option._id === value._id} filterSelectedOptions={true} getOptionLabel={( category: InterfaceAgendaItemCategoryInfo, diff --git a/src/components/Avatar/Avatar.tsx b/src/components/Avatar/Avatar.tsx index 6d4cab405b..f6397dd737 100644 --- a/src/components/Avatar/Avatar.tsx +++ b/src/components/Avatar/Avatar.tsx @@ -12,6 +12,19 @@ interface InterfaceAvatarProps { radius?: number; } +/** + * A component that generates and displays an avatar based on the provided name. + * The avatar is generated using the DiceBear library with the initials style. + * + * @param name - The name used to generate the avatar. + * @param alt - Alternative text for the avatar image. + * @param size - Size of the avatar image. + * @param avatarStyle - Custom CSS class for the avatar image. + * @param dataTestId - Data-testid attribute for testing purposes. + * @param radius - Radius of the avatar corners. + * + * @returns JSX.Element - The rendered avatar image component. + */ const Avatar = ({ name, alt = 'Dummy Avatar', @@ -20,6 +33,7 @@ const Avatar = ({ dataTestId, radius, }: InterfaceAvatarProps): JSX.Element => { + // Memoize the avatar creation to avoid unnecessary recalculations const avatar = useMemo(() => { return createAvatar(initials, { size: size || 128, diff --git a/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx b/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx index e4bda6fd7f..ac758be867 100644 --- a/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx +++ b/src/components/ChangeLanguageDropdown/ChangeLanguageDropDown.tsx @@ -6,6 +6,7 @@ import cookies from 'js-cookie'; import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; import { useMutation } from '@apollo/client'; import useLocalStorage from 'utils/useLocalstorage'; + const { getItem } = useLocalStorage(); interface InterfaceChangeLanguageDropDownProps { @@ -14,6 +15,17 @@ interface InterfaceChangeLanguageDropDownProps { btnTextStyle?: string; } +/** + * A dropdown component that allows users to change the application's language. + * It updates the user's language preference in the backend and stores the selection in cookies. + * + * @param props - The properties for customizing the dropdown component. + * @param parentContainerStyle - Custom style for the dropdown container. + * @param btnStyle - Custom style for the dropdown button. + * @param btnTextStyle - Custom style for the button text. + * + * @returns JSX.Element - The rendered dropdown component for changing languages. + */ const ChangeLanguageDropDown = ( props: InterfaceChangeLanguageDropDownProps, ): JSX.Element => { @@ -21,6 +33,11 @@ const ChangeLanguageDropDown = ( const userId = getItem('userId'); const [updateUser] = useMutation(UPDATE_USER_MUTATION); + /** + * Changes the application's language and updates the user's language preference. + * + * @param languageCode - The code of the language to switch to. + */ const changeLanguage = async (languageCode: string): Promise => { if (userId) { try { diff --git a/src/components/CheckIn/CheckInModal.tsx b/src/components/CheckIn/CheckInModal.tsx index 6b4e5425c0..8c75bab598 100644 --- a/src/components/CheckIn/CheckInModal.tsx +++ b/src/components/CheckIn/CheckInModal.tsx @@ -13,26 +13,48 @@ import type { GridColDef, GridRowHeightReturnValue } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; import TextField from '@mui/material/TextField'; -export const CheckInModal = (props: InterfaceModalProp): JSX.Element => { +/** + * Modal component for managing event check-ins. Displays a list of attendees + * and their check-in statuses, allowing for filtering by user name. + * + * @param show - Boolean indicating whether the modal is visible. + * @param eventId - ID of the event whose check-ins are to be displayed. + * @param handleClose - Function to call when the modal is closed. + * + * @returns JSX.Element - The rendered modal component. + */ +export const CheckInModal = ({ + show, + eventId, + handleClose, +}: InterfaceModalProp): JSX.Element => { + // State to hold the data for the table const [tableData, setTableData] = useState([]); + // State for search filter input const [userFilterQuery, setUserFilterQuery] = useState(''); + + // State for filter model used in DataGrid const [filterQueryModel, setFilterQueryModel] = useState({ items: [{ field: 'userName', operator: 'contains', value: '' }], }); + // Query to get check-in data from the server const { data: checkInData, loading: checkInLoading, refetch: checkInRefetch, } = useQuery(EVENT_CHECKINS, { - variables: { id: props.eventId }, + variables: { id: eventId }, }); + // Effect runs whenever checkInData, eventId, or checkInLoading changes useEffect(() => { - checkInRefetch(); - if (checkInLoading) setTableData([]); - else + checkInRefetch(); // Refetch data when component mounts or updates + if (checkInLoading) { + setTableData([]); // Clear table data while loading + } else { + // Map the check-in data to table rows setTableData( checkInData.event.attendeesCheckInStatus.map( (checkIn: InterfaceAttendeeCheckIn) => ({ @@ -43,26 +65,29 @@ export const CheckInModal = (props: InterfaceModalProp): JSX.Element => { name: `${checkIn.user.firstName} ${checkIn.user.lastName}`, userId: checkIn.user._id, checkIn: checkIn.checkIn, - eventId: props.eventId, + eventId, }, }), ), ); - }, [checkInData, props.eventId, checkInLoading]); + } + }, [checkInData, eventId, checkInLoading]); + // Define columns for the DataGrid const columns: GridColDef[] = [ - { field: 'userName', headerName: 'User', width: 300 }, + { field: 'userName', headerName: 'User', width: 300 }, // Column for user names { field: 'checkInData', headerName: 'Check In Status', width: 400, renderCell: (props) => ( + // Render a custom row component for check-in status ), }, ]; - // Render the loading screen + // Show a loading indicator while data is loading if (checkInLoading) { return ( <> @@ -70,11 +95,12 @@ export const CheckInModal = (props: InterfaceModalProp): JSX.Element => { ); } + return ( <> { +/** + * Wrapper component that displays a button to open the CheckInModal. + * + * @param eventId - The ID of the event for which check-in management is being handled. + * + * @returns JSX.Element - The rendered CheckInWrapper component. + */ +export const CheckInWrapper = ({ eventId }: PropType): JSX.Element => { const [showModal, setShowModal] = useState(false); return ( @@ -33,7 +40,7 @@ export const CheckInWrapper = (props: PropType): JSX.Element => { setShowModal(false)} - eventId={props.eventId} + eventId={eventId} /> )} diff --git a/src/components/CheckIn/TableRow.tsx b/src/components/CheckIn/TableRow.tsx index 8daa5ed222..c2c2def815 100644 --- a/src/components/CheckIn/TableRow.tsx +++ b/src/components/CheckIn/TableRow.tsx @@ -8,6 +8,15 @@ import { generate } from '@pdfme/generator'; import { tagTemplate } from './tagTemplate'; import { useTranslation } from 'react-i18next'; +/** + * Component that represents a single row in the check-in table. + * Allows users to mark themselves as checked in and download a tag if they are already checked in. + * + * @param data - The data for the current row, including user and event information. + * @param refetch - Function to refetch the check-in data after marking a check-in. + * + * @returns JSX.Element - The rendered TableRow component. + */ export const TableRow = ({ data, refetch, @@ -18,6 +27,10 @@ export const TableRow = ({ const [checkInMutation] = useMutation(MARK_CHECKIN); const { t } = useTranslation('translation', { keyPrefix: 'checkIn' }); + /** + * Marks the user as checked in for the event. + * Displays success or error messages based on the result of the mutation. + */ const markCheckIn = (): void => { // as we do not want to clutter the UI currently with the same (only provide the most basic of operations) checkInMutation({ @@ -36,12 +49,23 @@ export const TableRow = ({ }); }; + /** + * Triggers a notification while generating and downloading a PDF tag. + * + * @returns A promise that resolves when the PDF is generated and opened. + */ const notify = (): Promise => toast.promise(generateTag, { pending: 'Generating pdf...', success: 'PDF generated successfully!', error: 'Error generating pdf!', }); + + /** + * Generates a PDF tag based on the provided data and opens it in a new tab. + * + * @returns A promise that resolves when the PDF is successfully generated and opened. + */ const generateTag = async (): Promise => { const inputs = [{ name: data.name }]; const pdf = await generate({ template: tagTemplate, inputs }); diff --git a/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx b/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx index 4b85a1911b..2991c2ade5 100644 --- a/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx +++ b/src/components/CollapsibleDropdown/CollapsibleDropdown.tsx @@ -12,6 +12,15 @@ export interface InterfaceCollapsibleDropdown { setShowDropdown: React.Dispatch>; } +/** + * A collapsible dropdown component that toggles visibility of sub-targets. + * + * @param showDropdown - Boolean indicating whether the dropdown is visible or not. + * @param target - Object containing the target information, including the name and sub-targets. + * @param setShowDropdown - Function to toggle the visibility of the dropdown. + * + * @returns JSX.Element - The rendered CollapsibleDropdown component. + */ const collapsibleDropdown = ({ target, showDropdown, @@ -22,6 +31,7 @@ const collapsibleDropdown = ({ const navigate = useNavigate(); const location = useLocation(); useEffect(() => { + // Show dropdown if the current path includes 'orgstore', otherwise hide it. if (location.pathname.includes('orgstore')) { setShowDropdown(true); } else { diff --git a/src/components/ContriStats/ContriStats.tsx b/src/components/ContriStats/ContriStats.tsx index 7891878335..995a7ab93d 100644 --- a/src/components/ContriStats/ContriStats.tsx +++ b/src/components/ContriStats/ContriStats.tsx @@ -11,6 +11,13 @@ interface InterfaceContriStatsProps { totalAmount: string; } +/** + * A component that displays contribution statistics. + * + * @param props - The properties passed to the component, including `recentAmount`, `highestAmount`, and `totalAmount`. + * + * @returns JSX.Element - The rendered component displaying the contribution stats. + */ function contriStats(props: InterfaceContriStatsProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'contriStats', diff --git a/src/components/CurrentHourIndicator/CurrentHourIndicator.tsx b/src/components/CurrentHourIndicator/CurrentHourIndicator.tsx index 9e4a85454a..98e02bbf5f 100644 --- a/src/components/CurrentHourIndicator/CurrentHourIndicator.tsx +++ b/src/components/CurrentHourIndicator/CurrentHourIndicator.tsx @@ -1,6 +1,11 @@ import React from 'react'; import styles from './CurrentHourIndicator.module.css'; +/** + * A component that displays an indicator for the current hour. + * + * @returns JSX.Element - The rendered component showing the current hour indicator. + */ const CurrentHourIndicator = (): JSX.Element => { return (
    diff --git a/src/components/DeleteOrg/DeleteOrg.tsx b/src/components/DeleteOrg/DeleteOrg.tsx index ab2772785f..e9370cb1a6 100644 --- a/src/components/DeleteOrg/DeleteOrg.tsx +++ b/src/components/DeleteOrg/DeleteOrg.tsx @@ -13,31 +13,60 @@ import styles from './DeleteOrg.module.css'; import { useNavigate, useParams } from 'react-router-dom'; import useLocalStorage from 'utils/useLocalstorage'; +/** + * A component for deleting an organization. + * + * It displays a card with a delete button. When the delete button is clicked, + * a modal appears asking for confirmation. Depending on the type of organization + * (sample or regular), it performs the delete operation and shows appropriate + * success or error messages. + * + * @returns JSX.Element - The rendered component with delete functionality. + */ function deleteOrg(): JSX.Element { + // Translation hook for localization const { t } = useTranslation('translation', { keyPrefix: 'deleteOrg', }); const { t: tCommon } = useTranslation('common'); + + // Get the current organization ID from the URL const { orgId: currentUrl } = useParams(); + // Navigation hook for redirecting const navigate = useNavigate(); + // State to control the visibility of the delete confirmation modal const [showDeleteModal, setShowDeleteModal] = useState(false); + + // Hook for accessing local storage const { getItem } = useLocalStorage(); + // Check if the user has super admin privileges const canDelete = getItem('SuperAdmin'); + + /** + * Toggles the visibility of the delete confirmation modal. + */ const toggleDeleteModal = (): void => setShowDeleteModal(!showDeleteModal); + // GraphQL mutations for deleting organizations const [del] = useMutation(DELETE_ORGANIZATION_MUTATION); const [removeSampleOrganization] = useMutation( REMOVE_SAMPLE_ORGANIZATION_MUTATION, ); + // Query to check if the organization is a sample organization const { data } = useQuery(IS_SAMPLE_ORGANIZATION_QUERY, { variables: { isSampleOrganizationId: currentUrl, }, }); + /** + * Deletes the organization. It handles both sample and regular organizations. + * Displays success or error messages based on the operation result. + */ const deleteOrg = async (): Promise => { if (data && data.isSampleOrganization) { + // If it's a sample organization, use a specific mutation removeSampleOrganization() .then(() => { toast.success(t('successfullyDeletedSampleOrganization')); @@ -49,6 +78,7 @@ function deleteOrg(): JSX.Element { toast.error(error.message); }); } else { + // For regular organizations, use a different mutation try { await del({ variables: { diff --git a/src/components/DynamicDropDown/DynamicDropDown.tsx b/src/components/DynamicDropDown/DynamicDropDown.tsx index 170b6be29b..6bb5e3efcb 100644 --- a/src/components/DynamicDropDown/DynamicDropDown.tsx +++ b/src/components/DynamicDropDown/DynamicDropDown.tsx @@ -2,23 +2,60 @@ import React from 'react'; import { Dropdown } from 'react-bootstrap'; import styles from './DynamicDropDown.module.css'; +/** + * Props for the DynamicDropDown component. + */ interface InterfaceChangeDropDownProps { parentContainerStyle?: string; btnStyle?: string; btnTextStyle?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any setFormState: React.Dispatch>; + // eslint-disable-next-line @typescript-eslint/no-explicit-any formState: any; fieldOptions: { value: string; label: string }[]; // Field options for dropdown fieldName: string; // Field name for labeling } -const DynamicDropDown = (props: InterfaceChangeDropDownProps): JSX.Element => { +/** + * A dynamic dropdown component that allows users to select an option. + * + * This component renders a dropdown with a toggle button. Clicking the button + * opens a menu with options. When an option is selected, it updates the form state. + * + * @param parentContainerStyle - Optional CSS class for styling the container. + * @param btnStyle - Optional CSS class for styling the dropdown button. + * @param setFormState - Function to update the form state with the selected option. + * @param formState - Current state of the form, used to determine the selected value. + * @param fieldOptions - Options to display in the dropdown. Each option has a value and a label. + * @param fieldName - The name of the field, used for labeling and key identification. + * @returns JSX.Element - The rendered dropdown component. + */ +const DynamicDropDown = ({ + parentContainerStyle = '', + btnStyle = '', + setFormState, + formState, + fieldOptions, + fieldName, +}: InterfaceChangeDropDownProps): JSX.Element => { + /** + * Updates the form state when a dropdown option is selected. + * + * @param value - The value of the selected option. + */ const handleFieldChange = (value: string): void => { - props.setFormState({ ...props.formState, [props.fieldName]: value }); + setFormState({ ...formState, [fieldName]: value }); }; + /** + * Retrieves the label for a given value from the options. + * + * @param value - The value for which to get the label. + * @returns The label corresponding to the value, or 'None' if not found. + */ const getLabel = (value: string): string => { - const selectedOption = props.fieldOptions.find( + const selectedOption = fieldOptions.find( (option) => option.value === value, ); return selectedOption ? selectedOption.label : `None`; @@ -26,23 +63,23 @@ const DynamicDropDown = (props: InterfaceChangeDropDownProps): JSX.Element => { return ( - {getLabel(props.formState[props.fieldName])} + {getLabel(formState[fieldName])} - {props.fieldOptions.map((option, index: number) => ( + {fieldOptions.map((option, index: number) => ( handleFieldChange(option.value)} - data-testid={`change-${props.fieldName.toLowerCase()}-btn-${option.value}`} + data-testid={`change-${fieldName.toLowerCase()}-btn-${option.value}`} > {option.label} diff --git a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx index 7a81e78e27..4207e3f259 100644 --- a/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx +++ b/src/components/EditCustomFieldDropDown/EditCustomFieldDropDown.tsx @@ -5,6 +5,9 @@ import availableFieldTypes from 'utils/fieldTypes'; import type { InterfaceCustomFieldData } from 'components/OrgProfileFieldSettings/OrgProfileFieldSettings'; import { useTranslation } from 'react-i18next'; +/** + * Props for the EditOrgCustomFieldDropDown component. + */ interface InterfaceEditCustomFieldDropDownProps { customFieldData: InterfaceCustomFieldData; setCustomFieldData: Dispatch>; @@ -13,6 +16,20 @@ interface InterfaceEditCustomFieldDropDownProps { btnTextStyle?: string; } +/** + * A dropdown component for editing custom field types. + * + * This component displays a dropdown menu that allows users to select a custom field type. + * It shows the current type of the field and provides a list of available types to choose from. + * When a new type is selected, it updates the custom field data. + * + * @param customFieldData - The current data of the custom field being edited. + * @param setCustomFieldData - Function to update the custom field data with the new type. + * @param parentContainerStyle - Optional CSS class to style the container of the dropdown. + * @param btnStyle - Optional CSS class to style the dropdown button. + * @param btnTextStyle - Optional CSS class to style the text inside the button. + * @returns JSX.Element - The rendered dropdown component. + */ const EditOrgCustomFieldDropDown = ({ customFieldData, setCustomFieldData, diff --git a/src/components/EventCalendar/EventHeader.tsx b/src/components/EventCalendar/EventHeader.tsx index a4bc59596c..1e4653ebf4 100644 --- a/src/components/EventCalendar/EventHeader.tsx +++ b/src/components/EventCalendar/EventHeader.tsx @@ -5,12 +5,24 @@ import styles from './EventCalendar.module.css'; import { ViewType } from '../../screens/OrganizationEvents/OrganizationEvents'; import { useTranslation } from 'react-i18next'; +/** + * Props for the EventHeader component. + */ interface InterfaceEventHeaderProps { viewType: ViewType; handleChangeView: (item: string | null) => void; showInviteModal: () => void; } +/** + * EventHeader component displays the header for the event calendar. + * It includes a search field, view type dropdown, event type dropdown, and a button to create an event. + * + * @param viewType - The current view type of the calendar. + * @param handleChangeView - Function to handle changing the view type. + * @param showInviteModal - Function to show the invite modal for creating an event. + * @returns JSX.Element - The rendered EventHeader component. + */ function eventHeader({ viewType, handleChangeView, @@ -34,6 +46,11 @@ function eventHeader({ required className={styles.inputField} value={eventName} + /** + * Updates the event name state when the input value changes. + * + * @param e - The event object from the input change. + */ /*istanbul ignore next*/ onChange={(e) => setEventName(e.target.value)} /> diff --git a/src/components/EventCalendar/YearlyEventCalender.tsx b/src/components/EventCalendar/YearlyEventCalender.tsx index 7b7a4d4b03..63870ded3c 100644 --- a/src/components/EventCalendar/YearlyEventCalender.tsx +++ b/src/components/EventCalendar/YearlyEventCalender.tsx @@ -7,6 +7,9 @@ import type { ViewType } from 'screens/OrganizationEvents/OrganizationEvents'; import { ChevronLeft, ChevronRight } from '@mui/icons-material'; import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils'; +/** + * Interface for event data used in the calendar. + */ interface InterfaceEventListCardProps { userRole?: string; key?: string; @@ -49,12 +52,18 @@ enum Status { DELETED = 'DELETED', } +/** + * Enum for different user roles. + */ enum Role { USER = 'USER', SUPERADMIN = 'SUPERADMIN', ADMIN = 'ADMIN', } +/** + * Interface for event attendees. + */ interface InterfaceIEventAttendees { userId: string; user?: string; @@ -62,9 +71,27 @@ interface InterfaceIEventAttendees { createdAt?: Date; } +/** + * Interface for organization list. + */ interface InterfaceIOrgList { admins: { _id: string }[]; } + +/** + * Calendar component to display events for a selected year. + * + * This component renders a yearly calendar with navigation to view previous and next years. + * It displays events for each day, with functionality to expand and view details of events. + * + * @param eventData - Array of event data to display on the calendar. + * @param refetchEvents - Function to refresh the event data. + * @param orgData - Organization data to filter events. + * @param userRole - Role of the user for access control. + * @param userId - ID of the user for filtering events they are attending. + * @param viewType - Type of view for the calendar. + * @returns JSX.Element - The rendered calendar component. + */ const Calendar: React.FC = ({ eventData, refetchEvents, @@ -95,6 +122,15 @@ const Calendar: React.FC = ({ ); const [expandedY, setExpandedY] = useState(null); + /** + * Filters events based on user role, organization data, and user ID. + * + * @param eventData - Array of event data to filter. + * @param orgData - Organization data for filtering events. + * @param userRole - Role of the user for access control. + * @param userId - ID of the user for filtering events they are attending. + * @returns Filtered array of event data. + */ const filterData = ( eventData: InterfaceEventListCardProps[], orgData?: InterfaceIOrgList, @@ -137,16 +173,27 @@ const Calendar: React.FC = ({ setEvents(data); }, [eventData, orgData, userRole, userId]); + /** + * Navigates to the previous year. + */ const handlePrevYear = (): void => { /*istanbul ignore next*/ setCurrentYear(currentYear - 1); }; + /** + * Navigates to the next year. + */ const handleNextYear = (): void => { /*istanbul ignore next*/ setCurrentYear(currentYear + 1); }; + /** + * Renders the days of the month for the calendar. + * + * @returns Array of JSX elements representing the days of each month. + */ const renderMonthDays = (): JSX.Element[] => { const renderedMonths: JSX.Element[] = []; @@ -320,6 +367,11 @@ const Calendar: React.FC = ({ return renderedMonths; }; + /** + * Renders the yearly calendar with navigation buttons. + * + * @returns JSX.Element - The rendered yearly calendar component. + */ const renderYearlyCalendar = (): JSX.Element => { return (
    diff --git a/src/components/EventDashboardScreen/EventDashboardScreen.tsx b/src/components/EventDashboardScreen/EventDashboardScreen.tsx index 9b518f5e0b..07686a57a3 100644 --- a/src/components/EventDashboardScreen/EventDashboardScreen.tsx +++ b/src/components/EventDashboardScreen/EventDashboardScreen.tsx @@ -10,6 +10,12 @@ import styles from './EventDashboardScreen.module.css'; import ProfileDropdown from 'components/ProfileDropdown/ProfileDropdown'; import useLocalStorage from 'utils/useLocalstorage'; +/** + * The EventDashboardScreen component is the main dashboard view for event management. + * It includes navigation, a sidebar, and a profile dropdown. + * + * @returns JSX.Element - The rendered EventDashboardScreen component. + */ const EventDashboardScreen = (): JSX.Element => { const { getItem } = useLocalStorage(); const isLoggedIn = getItem('IsLoggedIn'); @@ -19,6 +25,8 @@ const EventDashboardScreen = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: titleKey }); const [hideDrawer, setHideDrawer] = useState(null); const { orgId } = useParams(); + + // Redirect to home if orgId is not present or if user is not logged in if (!orgId) { return ; } @@ -43,22 +51,31 @@ const EventDashboardScreen = (): JSX.Element => { ); } + // Access targets from Redux store const appRoutes: { targets: TargetsType[]; } = useSelector((state: RootState) => state.appRoutes); const { targets } = appRoutes; const dispatch = useDispatch(); + + // Update targets when orgId changes useEffect(() => { dispatch(updateTargets(orgId)); }, [orgId]); // Added orgId to the dependency array + /** + * Handles window resize events to toggle the visibility of the sidebar drawer. + */ const handleResize = (): void => { if (window.innerWidth <= 820 && !hideDrawer) { setHideDrawer(true); } }; + /** + * Toggles the visibility of the sidebar drawer. + */ const toggleDrawer = (): void => { setHideDrawer(!hideDrawer); }; @@ -119,6 +136,9 @@ const EventDashboardScreen = (): JSX.Element => { export default EventDashboardScreen; +/** + * A mapping of route keys to translation key prefixes. + */ interface InterfaceMapType { [key: string]: string; } diff --git a/src/components/EventListCard/EventListCard.tsx b/src/components/EventListCard/EventListCard.tsx index 9d89647ee1..ffa508ff7c 100644 --- a/src/components/EventListCard/EventListCard.tsx +++ b/src/components/EventListCard/EventListCard.tsx @@ -5,6 +5,9 @@ import { Navigate, useParams } from 'react-router-dom'; import EventListCardModals from './EventListCardModals'; import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils'; +/** + * Props for the EventListCard component. + */ export interface InterfaceEventListCardProps { refetchEvents?: () => void; userRole?: string; @@ -33,6 +36,12 @@ export interface InterfaceEventListCardProps { }; } +/** + * Component that displays an event card with a modal for event details. + * + * @param props - The props for the EventListCard component. + * @returns The rendered EventListCard component. + */ function eventListCard(props: InterfaceEventListCardProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'eventListCard', @@ -42,14 +51,22 @@ function eventListCard(props: InterfaceEventListCardProps): JSX.Element { const [eventModalIsOpen, setEventModalIsOpen] = useState(false); const { orgId } = useParams(); + + // Redirect to home if orgId is not present if (!orgId) { return ; } + /** + * Opens the event modal. + */ const showViewModal = (): void => { setEventModalIsOpen(true); }; + /** + * Closes the event modal. + */ const hideViewModal = (): void => { setEventModalIsOpen(false); }; diff --git a/src/components/EventManagement/Dashboard/EventDashboard.tsx b/src/components/EventManagement/Dashboard/EventDashboard.tsx index 84b0493198..b811bcd137 100644 --- a/src/components/EventManagement/Dashboard/EventDashboard.tsx +++ b/src/components/EventManagement/Dashboard/EventDashboard.tsx @@ -6,6 +6,13 @@ import { EVENT_DETAILS } from 'GraphQl/Queries/Queries'; import { useQuery } from '@apollo/client'; import Loader from 'components/Loader/Loader'; +/** + * Component that displays event details. + * + * @param props - The props for the EventDashboard component. + * @param eventId - The ID of the event to fetch and display. + * @returns The rendered EventDashboard component. + */ const EventDashboard = (props: { eventId: string }): JSX.Element => { const { eventId } = props; const { t } = useTranslation('translation', { @@ -19,15 +26,28 @@ const EventDashboard = (props: { eventId: string }): JSX.Element => { }, ); + // Display a loader while fetching event data if (eventInfoLoading) { return ; } + /** + * Formats a time string (HH:MM) to a more readable format. + * + * @param timeString - The time string to format. + * @returns - The formatted time string. + */ function formatTime(timeString: string): string { const [hours, minutes] = timeString.split(':').slice(0, 2); return `${hours}:${minutes}`; } + /** + * Formats a date string to a more readable format (e.g., 1st Jan 2024). + * + * @param dateString - The date string to format. + * @returns - The formatted date string. + */ function formatDate(dateString: string): string { const date = new Date(dateString); const day = date.getDate(); @@ -55,6 +75,8 @@ const EventDashboard = (props: { eventId: string }): JSX.Element => { const formattedDate = `${day}${suffix} ${monthNames[monthIndex]} ${year}`; return formattedDate; } + + // Render event details return (
    @@ -75,7 +97,6 @@ const EventDashboard = (props: { eventId: string }): JSX.Element => {

    {t('to')}

    - {' '} {eventData.event.endTime !== null ? `${formatTime(eventData.event.endTime)}` : ``} diff --git a/src/components/EventManagement/EventAgendaItems/EventAgendaItems.tsx b/src/components/EventManagement/EventAgendaItems/EventAgendaItems.tsx index 09cc560bf1..73a074ba9f 100644 --- a/src/components/EventManagement/EventAgendaItems/EventAgendaItems.tsx +++ b/src/components/EventManagement/EventAgendaItems/EventAgendaItems.tsx @@ -23,6 +23,13 @@ import AgendaItemsCreateModal from 'components/AgendaItems/AgendaItemsCreateModa import styles from './EventAgendaItems.module.css'; import Loader from 'components/Loader/Loader'; +/** + * Component to manage and display agenda items for a specific event. + * + * @param props - The component props. + * @param eventId - The ID of the event to manage agenda items for. + * @returns The rendered component. + */ function EventAgendaItems(props: { eventId: string }): JSX.Element { const { eventId } = props; @@ -30,13 +37,16 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { keyPrefix: 'agendaItems', }); + // Extract organization ID from URL const url: string = window.location.href; const startIdx: number = url.indexOf('/event/') + '/event/'.length; const orgId: string = url.slice(startIdx, url.indexOf('/', startIdx)); + // State to manage the create agenda item modal visibility const [agendaItemCreateModalIsOpen, setAgendaItemCreateModalIsOpen] = useState(false); + // State to manage form values const [formState, setFormState] = useState({ agendaItemCategoryIds: [''], title: '', @@ -46,6 +56,7 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { urls: [''], }); + // Query for agenda item categories const { data: agendaCategoryData, loading: agendaCategoryLoading, @@ -59,6 +70,7 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { notifyOnNetworkStatusChange: true, }); + // Query for agenda items by event const { data: agendaItemData, loading: agendaItemLoading, @@ -74,8 +86,14 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { notifyOnNetworkStatusChange: true, }); + // Mutation for creating an agenda item const [createAgendaItem] = useMutation(CREATE_AGENDA_ITEM_MUTATION); + /** + * Handler for creating a new agenda item. + * + * @param e - The form submit event. + */ const createAgendaItemHandler = async ( e: ChangeEvent, ): Promise => { @@ -97,6 +115,7 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { }, }); + // Reset form state and hide modal setFormState({ title: '', description: '', @@ -115,16 +134,24 @@ function EventAgendaItems(props: { eventId: string }): JSX.Element { } }; + /** + * Toggles the visibility of the create agenda item modal. + */ const showCreateModal = (): void => { setAgendaItemCreateModalIsOpen(!agendaItemCreateModalIsOpen); }; + /** + * Hides the create agenda item modal. + */ const hideCreateModal = (): void => { setAgendaItemCreateModalIsOpen(!agendaItemCreateModalIsOpen); }; + // Show loader while data is loading if (agendaItemLoading || agendaCategoryLoading) return ; + // Show error message if there is an error loading data if (agendaItemError || agendaCategoryError) { return (

    diff --git a/src/components/EventRegistrantsModal/EventRegistrantsModal.tsx b/src/components/EventRegistrantsModal/EventRegistrantsModal.tsx index 811dd44941..85fa1ca426 100644 --- a/src/components/EventRegistrantsModal/EventRegistrantsModal.tsx +++ b/src/components/EventRegistrantsModal/EventRegistrantsModal.tsx @@ -15,6 +15,7 @@ import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import { useTranslation } from 'react-i18next'; +// Props for the EventRegistrantsModal component type ModalPropType = { show: boolean; eventId: string; @@ -22,23 +23,38 @@ type ModalPropType = { handleClose: () => void; }; +// User information interface interface InterfaceUser { _id: string; firstName: string; lastName: string; } +/** + * Modal component for managing event registrants. + * Allows adding and removing attendees from an event. + * + * @param show - Whether the modal is visible or not. + * @param eventId - The ID of the event. + * @param orgId - The ID of the organization. + * @param handleClose - Function to close the modal. + * @returns JSX element representing the modal. + */ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { const { eventId, orgId, handleClose, show } = props; const [member, setMember] = useState(null); + // Hooks for mutation operations const [addRegistrantMutation] = useMutation(ADD_EVENT_ATTENDEE); const [removeRegistrantMutation] = useMutation(REMOVE_EVENT_ATTENDEE); + // Translation hooks const { t } = useTranslation('translation', { keyPrefix: 'eventRegistrantsModal', }); const { t: tCommon } = useTranslation('common'); + + // Query hooks to fetch event attendees and organization members const { data: attendeesData, loading: attendeesLoading, @@ -51,6 +67,7 @@ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { variables: { id: orgId }, }); + // Function to add a new registrant to the event const addRegistrant = (): void => { if (member == null) { toast.warning('Please choose an user to add first!'); @@ -65,7 +82,7 @@ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { }) .then(() => { toast.success(tCommon('addedSuccessfully', { item: 'Attendee' })); - attendeesRefetch(); + attendeesRefetch(); // Refresh the list of attendees }) .catch((err) => { toast.error(t('errorAddingAttendee')); @@ -73,6 +90,7 @@ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { }); }; + // Function to remove a registrant from the event const deleteRegistrant = (userId: string): void => { toast.warn('Removing the attendee...'); removeRegistrantMutation({ @@ -83,7 +101,7 @@ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { }) .then(() => { toast.success(tCommon('removedSuccessfully', { item: 'Attendee' })); - attendeesRefetch(); + attendeesRefetch(); // Refresh the list of attendees }) .catch((err) => { toast.error(t('errorRemovingAttendee')); @@ -91,7 +109,7 @@ export const EventRegistrantsModal = (props: ModalPropType): JSX.Element => { }); }; - // Render the loading screen + // Show a loading screen if data is still being fetched if (attendeesLoading || memberLoading) { return ( <> diff --git a/src/components/EventRegistrantsModal/EventRegistrantsWrapper.tsx b/src/components/EventRegistrantsModal/EventRegistrantsWrapper.tsx index b621f8673a..b198fcdd6d 100644 --- a/src/components/EventRegistrantsModal/EventRegistrantsWrapper.tsx +++ b/src/components/EventRegistrantsModal/EventRegistrantsWrapper.tsx @@ -4,22 +4,35 @@ import { Button } from 'react-bootstrap'; import IconComponent from 'components/IconComponent/IconComponent'; import styles from './EventRegistrantsWrapper.module.css'; +// Props for the EventRegistrantsWrapper component type PropType = { eventId: string; orgId: string; }; -export const EventRegistrantsWrapper = (props: PropType): JSX.Element => { +/** + * Wrapper component that displays a button to show the event registrants modal. + * + * @param eventId - The ID of the event. + * @param orgId - The ID of the organization. + * @returns JSX element representing the wrapper with a button to show the modal. + */ +export const EventRegistrantsWrapper = ({ + eventId, + orgId, +}: PropType): JSX.Element => { + // State to control the visibility of the modal const [showModal, setShowModal] = useState(false); return ( <> + {/* Button to open the event registrants modal */} + {/* Render the EventRegistrantsModal if showModal is true */} {showModal && ( { - setShowModal(false); + setShowModal(false); // Hide the modal when closed }} - eventId={props.eventId} - orgId={props.orgId} + eventId={eventId} + orgId={orgId} /> )} diff --git a/src/components/EventStats/EventStats.tsx b/src/components/EventStats/EventStats.tsx index c9f1a70e8d..ce1e55e4bc 100644 --- a/src/components/EventStats/EventStats.tsx +++ b/src/components/EventStats/EventStats.tsx @@ -8,22 +8,33 @@ import eventStatsStyles from './EventStats.module.css'; import { useQuery } from '@apollo/client'; import { EVENT_FEEDBACKS } from 'GraphQl/Queries/Queries'; +// Props for the EventStats component type ModalPropType = { show: boolean; eventId: string; handleClose: () => void; }; +/** + * Component that displays event statistics in a modal. + * Shows feedback, reviews, and average rating for the event. + * + * @param show - Whether the modal is visible or not. + * @param handleClose - Function to close the modal. + * @param eventId - The ID of the event. + * @returns JSX element representing the event statistics modal. + */ export const EventStats = ({ show, handleClose, eventId, }: ModalPropType): JSX.Element => { + // Query to fetch event feedback data const { data, loading } = useQuery(EVENT_FEEDBACKS, { variables: { id: eventId }, }); - // Render the loading screen + // Show a loading screen while data is being fetched if (loading) { return ( <> @@ -36,7 +47,7 @@ export const EventStats = ({ <> Event Statistics + {/* Render feedback statistics */}
    + {/* Render review statistics and average rating */}
    diff --git a/src/components/EventStats/EventStatsWrapper.tsx b/src/components/EventStats/EventStatsWrapper.tsx index b501e77430..fb701a00da 100644 --- a/src/components/EventStats/EventStatsWrapper.tsx +++ b/src/components/EventStats/EventStatsWrapper.tsx @@ -4,21 +4,30 @@ import { Button } from 'react-bootstrap'; import IconComponent from 'components/IconComponent/IconComponent'; import styles from './EventStatsWrapper.module.css'; +// Props for the EventStatsWrapper component type PropType = { eventId: string; }; -export const EventStatsWrapper = (props: PropType): JSX.Element => { +/** + * Wrapper component that displays a button to show event statistics. + * + * @param eventId - The ID of the event. + * @returns JSX element representing the wrapper with a button to view event statistics. + */ +export const EventStatsWrapper = ({ eventId }: PropType): JSX.Element => { + // State to control the visibility of the EventStats component const [showModal, setShowModal] = useState(false); return ( <> + {/* Button to open the event statistics view */} + + {/* Render the EventStats component if showModal is true */} setShowModal(false)} - key={props.eventId || 'eventStatsDetails'} - eventId={props.eventId} + handleClose={(): void => setShowModal(false)} // Hide the EventStats component when closed + key={eventId || 'eventStatsDetails'} // Use eventId as key for the component + eventId={eventId} /> ); diff --git a/src/components/EventStats/Statistics/AverageRating.tsx b/src/components/EventStats/Statistics/AverageRating.tsx index 6dcfa03b3f..9f1a157e01 100644 --- a/src/components/EventStats/Statistics/AverageRating.tsx +++ b/src/components/EventStats/Statistics/AverageRating.tsx @@ -5,6 +5,7 @@ import FavoriteIcon from '@mui/icons-material/Favorite'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder'; import Typography from '@mui/material/Typography'; +// Props for the AverageRating component type ModalPropType = { data: { event: { @@ -15,12 +16,20 @@ type ModalPropType = { }; }; +// Type representing individual feedback type FeedbackType = { _id: string; rating: number; review: string | null; }; +/** + * Component that displays the average rating for an event. + * Shows a rating value and a star rating icon. + * + * @param data - Data containing the average feedback score to be displayed. + * @returns JSX element representing the average rating card with a star rating. + */ export const AverageRating = ({ data }: ModalPropType): JSX.Element => { return ( <> @@ -43,10 +52,10 @@ export const AverageRating = ({ data }: ModalPropType): JSX.Element => { emptyIcon={} sx={{ '& .MuiRating-iconFilled': { - color: '#ff6d75', + color: '#ff6d75', // Color for filled stars }, '& .MuiRating-iconHover': { - color: '#ff3d47', + color: '#ff3d47', // Color for star on hover }, }} /> diff --git a/src/components/EventStats/Statistics/Feedback.tsx b/src/components/EventStats/Statistics/Feedback.tsx index a4a9e41430..a6a255828f 100644 --- a/src/components/EventStats/Statistics/Feedback.tsx +++ b/src/components/EventStats/Statistics/Feedback.tsx @@ -6,6 +6,7 @@ import { } from '@mui/x-charts/PieChart'; import Card from 'react-bootstrap/Card'; +// Props for the FeedbackStats component type ModalPropType = { data: { event: { @@ -16,13 +17,22 @@ type ModalPropType = { }; }; +// Type representing individual feedback type FeedbackType = { _id: string; rating: number; review: string | null; }; +/** + * Component that displays a pie chart of feedback ratings for an event. + * Shows how many people gave each rating. + * + * @param data - Data containing event feedback to be displayed in the chart. + * @returns JSX element representing the feedback analysis card with a pie chart. + */ export const FeedbackStats = ({ data }: ModalPropType): JSX.Element => { + // Colors for the pie chart slices, from green (high ratings) to red (low ratings) const ratingColors = [ '#57bb8a', // Green '#94bd77', @@ -32,6 +42,7 @@ export const FeedbackStats = ({ data }: ModalPropType): JSX.Element => { '#dd776e', // Red ]; + // Count the number of feedbacks for each rating const count: Record = {}; data.event.feedback.forEach((feedback: FeedbackType) => { @@ -39,6 +50,7 @@ export const FeedbackStats = ({ data }: ModalPropType): JSX.Element => { else count[feedback.rating] = 1; }); + // Prepare data for the pie chart const chartData = []; for (let rating = 0; rating <= 5; rating++) { if (rating in count) diff --git a/src/components/EventStats/Statistics/Review.tsx b/src/components/EventStats/Statistics/Review.tsx index 0afc687023..343a8107c3 100644 --- a/src/components/EventStats/Statistics/Review.tsx +++ b/src/components/EventStats/Statistics/Review.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Card from 'react-bootstrap/Card'; import Rating from '@mui/material/Rating'; +// Props for the ReviewStats component type ModalPropType = { data: { event: { @@ -12,13 +13,22 @@ type ModalPropType = { }; }; +// Type representing individual feedback type FeedbackType = { _id: string; rating: number; review: string | null; }; +/** + * Component that displays reviews for an event. + * Shows a list of reviews with ratings and text. + * + * @param data - Data containing event feedback to be displayed. + * @returns JSX element representing the reviews card. + */ export const ReviewStats = ({ data }: ModalPropType): JSX.Element => { + // Filter out feedback that has a review const reviews = data.event.feedback.filter( (feedback: FeedbackType) => feedback.review != null, ); diff --git a/src/components/HolidayCards/HolidayCard.tsx b/src/components/HolidayCards/HolidayCard.tsx index 0235659ee2..56ac86405d 100644 --- a/src/components/HolidayCards/HolidayCard.tsx +++ b/src/components/HolidayCards/HolidayCard.tsx @@ -1,9 +1,17 @@ import React from 'react'; import styles from './HolidayCard.module.css'; +// Props for the HolidayCard component interface InterfaceHolidayList { holidayName: string; } + +/** + * Component that displays a card with the name of a holiday. + * + * @param props - Contains the holidayName to be displayed on the card. + * @returns JSX element representing a card with the holiday name. + */ const HolidayCard = (props: InterfaceHolidayList): JSX.Element => { /*istanbul ignore next*/ return
    {props?.holidayName}
    ; diff --git a/src/components/IconComponent/IconComponent.tsx b/src/components/IconComponent/IconComponent.tsx index 17f9af8b32..08181e0f5b 100644 --- a/src/components/IconComponent/IconComponent.tsx +++ b/src/components/IconComponent/IconComponent.tsx @@ -26,7 +26,12 @@ export interface InterfaceIconComponent { height?: string; width?: string; } - +/** + * Renders an icon based on the provided name. + * + * @param props - Contains the name of the icon and optional styles (fill, height, width). + * @returns JSX element representing the icon. + */ const iconComponent = (props: InterfaceIconComponent): JSX.Element => { switch (props.name) { case 'My Organizations': diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx index dc501d6185..91e679b210 100644 --- a/src/components/LeftDrawer/LeftDrawer.tsx +++ b/src/components/LeftDrawer/LeftDrawer.tsx @@ -10,10 +10,17 @@ import styles from './LeftDrawer.module.css'; import useLocalStorage from 'utils/useLocalstorage'; export interface InterfaceLeftDrawerProps { - hideDrawer: boolean | null; - setHideDrawer: React.Dispatch>; + hideDrawer: boolean | null; // Controls the visibility of the drawer + setHideDrawer: React.Dispatch>; // Function to set the visibility state } +/** + * LeftDrawer component for displaying navigation options. + * + * @param hideDrawer - Determines if the drawer should be hidden or shown. + * @param setHideDrawer - Function to update the visibility state of the drawer. + * @returns JSX element for the left navigation drawer. + */ const leftDrawer = ({ hideDrawer, setHideDrawer, @@ -24,6 +31,9 @@ const leftDrawer = ({ const { getItem } = useLocalStorage(); const superAdmin = getItem('SuperAdmin'); + /** + * Handles link click to hide the drawer on smaller screens. + */ const handleLinkClick = (): void => { if (window.innerWidth <= 820) { setHideDrawer(true); diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx index 6cc501db45..7305abbaf5 100644 --- a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx +++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx @@ -21,6 +21,15 @@ export interface InterfaceLeftDrawerProps { setHideDrawer: React.Dispatch>; } +/** + * LeftDrawerOrg component for displaying organization details and navigation options. + * + * @param orgId - ID of the current organization. + * @param targets - List of navigation targets. + * @param hideDrawer - Determines if the drawer should be hidden or shown. + * @param setHideDrawer - Function to update the visibility state of the drawer. + * @returns JSX element for the left navigation drawer with organization details. + */ const leftDrawerOrg = ({ targets, orgId, @@ -43,7 +52,8 @@ const leftDrawerOrg = ({ } = useQuery(ORGANIZATIONS_LIST, { variables: { id: orgId }, }); - // Set organization data + + // Set organization data when query data is available useEffect(() => { let isMounted = true; if (data && isMounted) { @@ -55,6 +65,9 @@ const leftDrawerOrg = ({ }; }, [data]); + /** + * Handles link click to hide the drawer on smaller screens. + */ const handleLinkClick = (): void => { if (window.innerWidth <= 820) { setHideDrawer(true); diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index fe5ddef793..b59c735175 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -3,10 +3,17 @@ import styles from './Loader.module.css'; import { Spinner } from 'react-bootstrap'; interface InterfaceLoaderProps { - styles?: StyleSheet | string; - size?: 'sm' | 'lg' | 'xl'; + styles?: StyleSheet | string; // Custom styles for the spinner wrapper + size?: 'sm' | 'lg' | 'xl'; // Size of the spinner } +/** + * Loader component for displaying a loading spinner. + * + * @param styles - Optional custom styles for the spinner wrapper. + * @param size - Size of the spinner. Can be 'sm', 'lg', or 'xl'. + * @returns JSX element for a loading spinner. + */ const Loader = (props: InterfaceLoaderProps): JSX.Element => { return ( <> diff --git a/src/components/LoginPortalToggle/LoginPortalToggle.tsx b/src/components/LoginPortalToggle/LoginPortalToggle.tsx index c1feb9f4da..76305b02d3 100644 --- a/src/components/LoginPortalToggle/LoginPortalToggle.tsx +++ b/src/components/LoginPortalToggle/LoginPortalToggle.tsx @@ -6,25 +6,36 @@ import Row from 'react-bootstrap/Row'; import { NavLink } from 'react-router-dom'; interface InterfaceLoginPortalToggleProps { - onToggle: (role: 'admin' | 'user') => void; + onToggle: (role: 'admin' | 'user') => void; // Callback function for role change } +/** + * Component for toggling between admin and user login portals. + * + * @param onToggle - Callback function to handle role changes ('admin' or 'user'). + * @returns JSX element for login portal toggle. + */ function loginPortalToggle({ onToggle, }: InterfaceLoginPortalToggleProps): JSX.Element { const { t: tCommon } = useTranslation('common'); - const [activeRole, setActiveRole] = useState<'admin' | 'user'>('admin'); + const [activeRole, setActiveRole] = useState<'admin' | 'user'>('admin'); // Default role is 'admin' + /** + * Handles navigation link click and updates the active role. + * + * @param role - The role to be activated ('admin' or 'user'). + */ const handleNavLinkClick = (role: 'admin' | 'user'): void => { - onToggle(role); - setActiveRole(role); + onToggle(role); // Invoke the callback with the new role + setActiveRole(role); // Update the active role }; return ( handleNavLinkClick('admin')} > @@ -33,7 +44,7 @@ function loginPortalToggle({ handleNavLinkClick('user')} > diff --git a/src/components/MemberRequestCard/MemberRequestCard.tsx b/src/components/MemberRequestCard/MemberRequestCard.tsx index 462e78d2ee..2c0550b724 100644 --- a/src/components/MemberRequestCard/MemberRequestCard.tsx +++ b/src/components/MemberRequestCard/MemberRequestCard.tsx @@ -14,15 +14,21 @@ import defaultImg from 'assets/images/blank.png'; import { errorHandler } from 'utils/errorHandler'; interface InterfaceMemberRequestCardProps { - key: string; - id: string; - memberName: string; - memberLocation: string; - joinDate: string; - memberImage: string; - email: string; + key: string; // Unique key for the component (could be omitted in most cases) + id: string; // Unique identifier for the member + memberName: string; // Name of the member + memberLocation: string; // Location of the member + joinDate: string; // Date when the member joined + memberImage: string; // URL for the member's image + email: string; // Email of the member } +/** + * Component for displaying and managing member requests. + * + * @param props - Properties for the member request card. + * @returns JSX element for member request card. + */ function memberRequestCard( props: InterfaceMemberRequestCardProps, ): JSX.Element { @@ -34,6 +40,10 @@ function memberRequestCard( }); const { t: tCommon } = useTranslation('common'); + /** + * Handles accepting a member request. + * Displays a success message and reloads the page. + */ const addMember = async (): Promise => { try { await acceptMutation({ @@ -54,6 +64,10 @@ function memberRequestCard( } }; + /** + * Handles rejecting a member request. + * Confirms rejection and reloads the page if confirmed. + */ const rejectMember = async (): Promise => { const sure = window.confirm('Are you sure you want to Reject Request ?'); if (sure) { @@ -118,7 +132,7 @@ function memberRequestCard(
    -
    +
    ); } diff --git a/src/components/NotFound/NotFound.tsx b/src/components/NotFound/NotFound.tsx index 6e2853cf82..6b5332bbcf 100644 --- a/src/components/NotFound/NotFound.tsx +++ b/src/components/NotFound/NotFound.tsx @@ -4,10 +4,17 @@ import { useTranslation } from 'react-i18next'; import styles from './NotFound.module.css'; interface InterfaceNotFoundProps { - title: string; - keyPrefix: string; + title: string; // Title of the page or resource not found + keyPrefix: string; // Translation key prefix } +/** + * Component to display a "Not Found" message. + * + * @param title - Title of the page or resource that was not found. + * @param keyPrefix - Prefix for translation keys. + * @returns JSX element for the "Not Found" page. + */ function notFound(props: InterfaceNotFoundProps): JSX.Element { const key = props.keyPrefix.toString(); const { t } = useTranslation('translation', { diff --git a/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx b/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx index f893319e38..72b1fdd23c 100644 --- a/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx +++ b/src/components/OrgActionItemCategories/OrgActionItemCategories.tsx @@ -18,21 +18,27 @@ import { useParams } from 'react-router-dom'; type ModalType = 'Create' | 'Update'; +/** + * Represents the component for managing organization action item categories. + * This component allows creating, updating, enabling, and disabling action item categories. + */ const OrgActionItemCategories = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'orgActionItemCategories', }); const { t: tCommon } = useTranslation('common'); - const [modalIsOpen, setModalIsOpen] = useState(false); - const [modalType, setModalType] = useState('Create'); - const [categoryId, setCategoryId] = useState(''); - - const [name, setName] = useState(''); - const [currName, setCurrName] = useState(''); + // State variables + const [modalIsOpen, setModalIsOpen] = useState(false); // Controls modal visibility + const [modalType, setModalType] = useState('Create'); // Type of modal (Create or Update) + const [categoryId, setCategoryId] = useState(''); // Current category ID for updating + const [name, setName] = useState(''); // Category name for creation or update + const [currName, setCurrName] = useState(''); // Current category name (used for comparison) + // Fetch organization ID from URL params const { orgId: currentUrl } = useParams(); + // Query to fetch action item categories const { data, loading, @@ -50,6 +56,7 @@ const OrgActionItemCategories = (): JSX.Element => { notifyOnNetworkStatusChange: true, }); + // Mutations for creating and updating categories const [createActionItemCategory] = useMutation( CREATE_ACTION_ITEM_CATEGORY_MUTATION, ); @@ -58,6 +65,7 @@ const OrgActionItemCategories = (): JSX.Element => { UPDATE_ACTION_ITEM_CATEGORY_MUTATION, ); + // Handles category creation const handleCreate = async ( e: ChangeEvent, ): Promise => { @@ -84,10 +92,11 @@ const OrgActionItemCategories = (): JSX.Element => { } }; + // Handles category update const handleEdit = async (e: ChangeEvent): Promise => { e.preventDefault(); if (name === currName) { - toast.error(t('sameNameConflict')); + toast.error(t('sameNameConflict')); // Show error if the name is the same } else { try { await updateActionItemCategory({ @@ -97,22 +106,22 @@ const OrgActionItemCategories = (): JSX.Element => { }, }); - setName(''); - setCategoryId(''); - refetch(); - - setModalIsOpen(false); + setName(''); // Clear the name input + setCategoryId(''); // Clear the category ID + refetch(); // Refetch the list of categories + setModalIsOpen(false); // Close the modal - toast.success(t('successfulUpdation')); + toast.success(t('successfulUpdation')); // Show success toast } catch (error: unknown) { if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); + toast.error(error.message); // Show error toast + console.log(error.message); // Log the error } } } }; + // Handles enabling or disabling a category const handleStatusChange = async ( id: string, disabledStatus: boolean, @@ -125,11 +134,11 @@ const OrgActionItemCategories = (): JSX.Element => { }, }); - refetch(); + refetch(); // Refetch the list of categories toast.success( disabledStatus ? t('categoryEnabled') : t('categoryDisabled'), - ); + ); // Show success toast } catch (error: unknown) { if (error instanceof Error) { toast.error(error.message); @@ -138,11 +147,13 @@ const OrgActionItemCategories = (): JSX.Element => { } }; + // Shows the modal for creating a new category const showCreateModal = (): void => { setModalType('Create'); setModalIsOpen(true); }; + // Shows the modal for updating an existing category const showUpdateModal = (name: string, id: string): void => { setCurrName(name); setName(name); @@ -151,16 +162,19 @@ const OrgActionItemCategories = (): JSX.Element => { setModalIsOpen(true); }; + // Hides the modal and clears input fields const hideModal = (): void => { setName(''); setCategoryId(''); setModalIsOpen(false); }; + // Show loader while data is being fetched if (loading) { return ; } + // Show error message if there's an error if (error) { return (
    @@ -174,6 +188,7 @@ const OrgActionItemCategories = (): JSX.Element => { ); } + // Render the list of action item categories const actionItemCategories = data?.actionItemCategoriesByOrganization; return ( @@ -234,6 +249,7 @@ const OrgActionItemCategories = (): JSX.Element => { })}
    + {/* Modal for creating or updating categories */} void; } - +/** + * Component to confirm and handle the removal of an admin. + * + * @param id - ID of the admin to be removed. + * @param toggleRemoveModal - Function to toggle the visibility of the modal. + * @returns JSX element for the removal confirmation modal. + */ function orgAdminListCard(props: InterfaceOrgPeopleListCardProps): JSX.Element { if (!props.id) { return ; @@ -25,6 +31,10 @@ function orgAdminListCard(props: InterfaceOrgPeopleListCardProps): JSX.Element { }); const { t: tCommon } = useTranslation('common'); + /** + * Function to remove the admin from the organization + * and display a success message. + */ const removeAdmin = async (): Promise => { try { const { data } = await remove({ diff --git a/src/components/OrgContriCards/OrgContriCards.tsx b/src/components/OrgContriCards/OrgContriCards.tsx index 69a9e03d58..6635be09b8 100644 --- a/src/components/OrgContriCards/OrgContriCards.tsx +++ b/src/components/OrgContriCards/OrgContriCards.tsx @@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next'; import styles from './OrgContriCards.module.css'; +/** + * Props for the OrgContriCards component + */ interface InterfaceOrgContriCardsProps { key: string; id: string; @@ -14,6 +17,16 @@ interface InterfaceOrgContriCardsProps { contriTransactionId: string; userEmail: string; } + +/** + * Component to display organization contribution cards + * + * This component shows the contribution details of a user in a card format. It includes + * the user's name, email, contribution date, transaction ID, and the contribution amount. + * + * @param props - The properties passed to the component + * @returns JSX.Element representing a contribution card + */ function orgContriCards(props: InterfaceOrgContriCardsProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgContriCards', diff --git a/src/components/OrgDelete/OrgDelete.tsx b/src/components/OrgDelete/OrgDelete.tsx index 0f1335dae4..e596445e5c 100644 --- a/src/components/OrgDelete/OrgDelete.tsx +++ b/src/components/OrgDelete/OrgDelete.tsx @@ -1,6 +1,13 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +/** + * Component for displaying organization deletion message + * + * This component renders a message related to deleting an organization. + * + * @returns JSX.Element representing the organization deletion message + */ function orgDelete(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgDelete', @@ -8,6 +15,7 @@ function orgDelete(): JSX.Element { return ( <> + {/* Container for the organization deletion message */}
    {t('deleteOrg')}
    diff --git a/src/components/OrgListCard/OrgListCard.tsx b/src/components/OrgListCard/OrgListCard.tsx index bdba9107b0..d1b06bafe0 100644 --- a/src/components/OrgListCard/OrgListCard.tsx +++ b/src/components/OrgListCard/OrgListCard.tsx @@ -16,20 +16,38 @@ import { useQuery } from '@apollo/client'; import { Tooltip } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; +/** + * Props for the OrgListCard component + */ export interface InterfaceOrgListCardProps { data: InterfaceOrgConnectionInfoType; } +/** + * Component for displaying a list card for an organization + * + * This component renders a card that displays information about an organization, + * including its name, address, members, and admins. It also provides a button + * to manage the organization, navigating to the organization's dashboard. + * + * @param props - The properties passed to the component + * @returns JSX.Element representing an organization list card + */ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element { + // Destructure data from props const { _id, admins, image, address, members, name } = props.data; + // Query to check if the organization is a sample organization const { data } = useQuery(IS_SAMPLE_ORGANIZATION_QUERY, { variables: { isSampleOrganizationId: _id, }, }); + // Use navigate hook from react-router-dom to navigate to the organization dashboard const navigate = useNavigate(); + + // Query to get the organization list const { data: userData, }: { @@ -40,6 +58,7 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element { variables: { id: _id }, }); + // Handle click event to navigate to the organization dashboard function handleClick(): void { const url = '/orgdash/' + _id; @@ -54,8 +73,10 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element { return ( <> + {/* Container for the organization card */}
    + {/* Container for the organization image */}
    {image ? ( {`${name} @@ -68,12 +89,15 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element { )}
    + {/* Tooltip for the organization name */}

    {name}

    + {/* Description of the organization */}
    {userData?.organizations[0].description}
    + {/* Display the organization address if available */} {address && address.city && (
    @@ -83,17 +107,20 @@ function orgListCard(props: InterfaceOrgListCardProps): JSX.Element {
    )} + {/* Display the number of admins and members */}
    {tCommon('admins')}: {admins.length}       {tCommon('members')}: {members.length}
    + {/* Button to manage the organization */} {t('removeMemberMsg')} + {/* Button to cancel the removal action */} + {/* Button to confirm the removal action */}
    + {/* Delete button */}
    + {/* Dropdown for filtering members */} + {/* Dropdown for sorting by name */}
    + {/* Render action item categories component if orgId is available */} {orgId && }
    diff --git a/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx b/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx index 88701077a6..00284468f1 100644 --- a/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx +++ b/src/screens/OrganizationActionItems/ActionItemCreateModal.tsx @@ -11,6 +11,9 @@ import type { InterfaceMemberInfo, } from 'utils/interfaces'; +/** + * Interface for the form state used in the `ActionItemCreateModal` component. + */ interface InterfaceFormStateType { actionItemCategoryId: string; assigneeId: string; @@ -18,6 +21,9 @@ interface InterfaceFormStateType { preCompletionNotes: string; } +/** + * Props for the `ActionItemCreateModal` component. + */ interface InterfaceActionItemCreateModalProps { actionItemCreateModalIsOpen: boolean; hideCreateModal: () => void; @@ -31,6 +37,12 @@ interface InterfaceActionItemCreateModalProps { setDueDate: (state: React.SetStateAction) => void; } +/** + * A modal component for creating action items. + * + * @param props - The properties passed to the component. + * @returns The `ActionItemCreateModal` component. + */ const ActionItemCreateModal: React.FC = ({ actionItemCreateModalIsOpen, hideCreateModal, diff --git a/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx b/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx index 53b93ba9e5..22725b5f56 100644 --- a/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx +++ b/src/screens/OrganizationActionItems/ActionItemDeleteModal.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { Modal, Button } from 'react-bootstrap'; import styles from './OrganizationActionItems.module.css'; +/** + * Props for the `ActionItemPreviewModal` component. + */ interface InterfaceActionItemCreateModalProps { actionItemDeleteModalIsOpen: boolean; deleteActionItemHandler: () => Promise; @@ -10,6 +13,12 @@ interface InterfaceActionItemCreateModalProps { tCommon: (key: string) => string; } +/** + * A modal component for confirming the deletion of an action item. + * + * @param props - The properties passed to the component. + * @returns The `ActionItemPreviewModal` component. + */ const ActionItemPreviewModal: React.FC = ({ actionItemDeleteModalIsOpen, deleteActionItemHandler, diff --git a/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx b/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx index 8f2d0b8f1c..ae2043bf88 100644 --- a/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx +++ b/src/screens/OrganizationActionItems/ActionItemPreviewModal.tsx @@ -3,7 +3,9 @@ import { Modal, Form, Button } from 'react-bootstrap'; import styles from './OrganizationActionItems.module.css'; import dayjs from 'dayjs'; - +/** + * State object for form details related to an action item. + */ interface InterfaceFormStateType { assigneeId: string; assignee: string; @@ -12,7 +14,9 @@ interface InterfaceFormStateType { preCompletionNotes: string; postCompletionNotes: string; } - +/** + * Props for the `ActionItemPreviewModal` component. + */ interface InterfaceActionItemCreateModalProps { actionItemPreviewModalIsOpen: boolean; hidePreviewModal: () => void; @@ -25,6 +29,12 @@ interface InterfaceActionItemCreateModalProps { assignmentDate: Date | null; } +/** + * A modal component for previewing the details of an action item. + * + * @param props - The properties passed to the component. + * @returns The `ActionItemPreviewModal` component. + */ const ActionItemPreviewModal: React.FC = ({ actionItemPreviewModalIsOpen, hidePreviewModal, diff --git a/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx b/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx index 901101e58b..c97fff9372 100644 --- a/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx +++ b/src/screens/OrganizationActionItems/ActionItemUpdateModal.tsx @@ -8,6 +8,9 @@ import type { Dayjs } from 'dayjs'; import styles from './OrganizationActionItems.module.css'; import type { InterfaceMemberInfo } from 'utils/interfaces'; +/** + * InterfaceFormStateType is an object containing the form state + */ interface InterfaceFormStateType { assigneeId: string; assignee: string; @@ -17,6 +20,21 @@ interface InterfaceFormStateType { postCompletionNotes: string; } +/** + * ActionItemUpdateModal component is used to update the action item details like assignee, preCompletionNotes, dueDate, completionDate + * @param actionItemUpdateModalIsOpen - boolean value to check if the modal is open or not + * @param hideUpdateModal - function to hide the modal + * @param formState - object containing the form state + * @param setFormState - function to set the form state + * @param updateActionItemHandler - function to update the action item + * @param t - i18n function to translate the text + * @param membersData - array of members data + * @param dueDate - due date of the action item + * @param setDueDate - function to set the due date + * @param completionDate - completion date of the action item + * @param setCompletionDate - function to set the completion date + * @returns returns the ActionItemUpdateModal component + */ interface InterfaceActionItemCreateModalProps { actionItemUpdateModalIsOpen: boolean; hideUpdateModal: () => void; diff --git a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx index 42c760206d..da16a69a3b 100644 --- a/src/screens/OrganizationActionItems/OrganizationActionItems.tsx +++ b/src/screens/OrganizationActionItems/OrganizationActionItems.tsx @@ -27,14 +27,23 @@ import ActionItemCreateModal from './ActionItemCreateModal'; import styles from './OrganizationActionItems.module.css'; import Loader from 'components/Loader/Loader'; +/** + * Component for managing and displaying action items within an organization. + * + * This component allows users to view, filter, sort, and create action items. It also handles fetching and displaying related data such as action item categories and members. + * + * @returns The rendered component. + */ function organizationActionItems(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'organizationActionItems', }); const { t: tCommon } = useTranslation('common'); + // Get the organization ID from URL parameters const { orgId: currentUrl } = useParams(); + // State for managing modal visibility and form data const [actionItemCreateModalIsOpen, setActionItemCreateModalIsOpen] = useState(false); const [dueDate, setDueDate] = useState(new Date()); @@ -49,6 +58,9 @@ function organizationActionItems(): JSX.Element { preCompletionNotes: '', }); + /** + * Query to fetch action item categories for the organization. + */ const { data: actionItemCategoriesData, loading: actionItemCategoriesLoading, @@ -64,6 +76,9 @@ function organizationActionItems(): JSX.Element { notifyOnNetworkStatusChange: true, }); + /** + * Query to fetch members of the organization. + */ const { data: membersData, loading: membersLoading, @@ -76,6 +91,9 @@ function organizationActionItems(): JSX.Element { variables: { id: currentUrl }, }); + /** + * Query to fetch action items for the organization based on filters and sorting. + */ const { data: actionItemsData, loading: actionItemsLoading, @@ -97,8 +115,17 @@ function organizationActionItems(): JSX.Element { notifyOnNetworkStatusChange: true, }); + /** + * Mutation to create a new action item. + */ const [createActionItem] = useMutation(CREATE_ACTION_ITEM_MUTATION); + /** + * Handler function to create a new action item. + * + * @param e - The form submit event. + * @returns A promise that resolves when the action item is created. + */ const createActionItemHandler = async ( e: ChangeEvent, ): Promise => { @@ -113,6 +140,7 @@ function organizationActionItems(): JSX.Element { }, }); + // Reset form and date after successful creation setFormState({ assigneeId: '', actionItemCategoryId: '', @@ -132,14 +160,25 @@ function organizationActionItems(): JSX.Element { } }; + /** + * Toggles the visibility of the create action item modal. + */ const showCreateModal = (): void => { setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); }; + /** + * Hides the create action item modal. + */ const hideCreateModal = (): void => { setActionItemCreateModalIsOpen(!actionItemCreateModalIsOpen); }; + /** + * Handles sorting action items by date. + * + * @param sort - The sorting order ('Latest' or 'Earliest'). + */ const handleSorting = (sort: string): void => { if (sort === 'Latest') { setOrderBy('Latest'); @@ -148,6 +187,11 @@ function organizationActionItems(): JSX.Element { } }; + /** + * Filters action items by status. + * + * @param status - The status to filter by ('Active' or 'Completed'). + */ const handleStatusFilter = (status: string): void => { if (status === 'Active') { setActionItemStatus('Active'); @@ -156,6 +200,9 @@ function organizationActionItems(): JSX.Element { } }; + /** + * Clears all filters applied to the action items. + */ const handleClearFilters = (): void => { setActionItemCategoryId(''); setActionItemCategoryName(''); diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx index 9b260d877f..57a3057b3f 100644 --- a/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx +++ b/src/screens/OrganizationAgendaCategory/AgendaCategoryCreateModal.tsx @@ -3,12 +3,18 @@ import { Modal, Form, Button } from 'react-bootstrap'; import type { ChangeEvent } from 'react'; import styles from './OrganizationAgendaCategory.module.css'; +/** + * InterfaceFormStateType is an object containing the form state + */ interface InterfaceFormStateType { name: string; description: string; createdBy: string; } +/** + * InterfaceAgendaCategoryCreateModalProps is an object containing the props for AgendaCategoryCreateModal component + */ interface InterfaceAgendaCategoryCreateModalProps { agendaCategoryCreateModalIsOpen: boolean; hideCreateModal: () => void; @@ -20,6 +26,16 @@ interface InterfaceAgendaCategoryCreateModalProps { t: (key: string) => string; } +/** + * AgendaCategoryCreateModal component is used to create the agenda category details like name, description + * @param agendaCategoryCreateModalIsOpen - boolean value to check if the modal is open or not + * @param hideCreateModal - function to hide the modal + * @param formState - object containing the form state + * @param setFormState - function to set the form state + * @param createAgendaCategoryHandler - function to create the agenda category + * @param t - i18n function to translate the text + * @returns returns the AgendaCategoryCreateModal component + */ const AgendaCategoryCreateModal: React.FC< InterfaceAgendaCategoryCreateModalProps > = ({ diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx index f2edb8fc62..a16186bac7 100644 --- a/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx +++ b/src/screens/OrganizationAgendaCategory/AgendaCategoryDeleteModal.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { Modal, Button } from 'react-bootstrap'; import styles from './OrganizationAgendaCategory.module.css'; +/** + * InterfaceAgendaCategoryDeleteModalProps is an object containing the props for AgendaCategoryDeleteModal component + */ interface InterfaceAgendaCategoryDeleteModalProps { agendaCategoryDeleteModalIsOpen: boolean; toggleDeleteModal: () => void; @@ -10,6 +13,15 @@ interface InterfaceAgendaCategoryDeleteModalProps { tCommon: (key: string) => string; } +/** + * AgendaCategoryDeleteModal component is used to delete the agenda category + * @param agendaCategoryDeleteModalIsOpen - boolean value to check if the modal is open or not + * @param toggleDeleteModal - function to toggle the modal + * @param deleteAgendaCategoryHandler - function to delete the agenda category + * @param t - i18n function to translate the text + * @param tCommon - i18n function to translate the text + * @returns returns the AgendaCategoryDeleteModal component + */ const AgendaCategoryDeleteModal: React.FC< InterfaceAgendaCategoryDeleteModalProps > = ({ diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx index c9dc17e56f..794db27ad3 100644 --- a/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx +++ b/src/screens/OrganizationAgendaCategory/AgendaCategoryPreviewModal.tsx @@ -3,12 +3,18 @@ import { Modal, Form, Button } from 'react-bootstrap'; import styles from './OrganizationAgendaCategory.module.css'; +/** + * InterfaceFormStateType is an object containing the form state + */ interface InterfaceFormStateType { name: string; description: string; createdBy: string; } +/** + * InterfaceAgendaCategoryPreviewModalProps is an object containing the props for AgendaCategoryPreviewModal component + */ interface InterfaceAgendaCategoryPreviewModalProps { agendaCategoryPreviewModalIsOpen: boolean; hidePreviewModal: () => void; @@ -19,6 +25,16 @@ interface InterfaceAgendaCategoryPreviewModalProps { t: (key: string) => string; } +/** + * AgendaCategoryPreviewModal component is used to preview the agenda category details like name, description, createdBy + * @param agendaCategoryPreviewModalIsOpen - boolean value to check if the modal is open or not + * @param hidePreviewModal - function to hide the modal + * @param showUpdateModal - function to show the update modal + * @param toggleDeleteModal - function to toggle the delete modal + * @param formState - object containing the form state + * @param t - i18n function to translate the text + * @returns returns the AgendaCategoryPreviewModal component + */ const AgendaCategoryPreviewModal: React.FC< InterfaceAgendaCategoryPreviewModalProps > = ({ diff --git a/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx b/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx index 868205c339..508f1db69b 100644 --- a/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx +++ b/src/screens/OrganizationAgendaCategory/AgendaCategoryUpdateModal.tsx @@ -4,12 +4,18 @@ import type { ChangeEvent } from 'react'; import styles from './OrganizationAgendaCategory.module.css'; +/** + * InterfaceFormStateType is an object containing the form state + */ interface InterfaceFormStateType { name: string; description: string; createdBy: string; } +/** + * InterfaceAgendaCategoryUpdateModalProps is an object containing the props for AgendaCategoryUpdateModal component + */ interface InterfaceAgendaCategoryUpdateModalProps { agendaCategoryUpdateModalIsOpen: boolean; hideUpdateModal: () => void; @@ -21,6 +27,16 @@ interface InterfaceAgendaCategoryUpdateModalProps { t: (key: string) => string; } +/** + * AgendaCategoryUpdateModal component is used to update the agenda category details like name, description + * @param agendaCategoryUpdateModalIsOpen - boolean value to check if the modal is open or not + * @param hideUpdateModal - function to hide the modal + * @param formState - object containing the form state + * @param setFormState - function to set the form state + * @param updateAgendaCategoryHandler - function to update the agenda category + * @param t - i18n function to translate the text + * @returns returns the AgendaCategoryUpdateModal component + */ const AgendaCategoryUpdateModal: React.FC< InterfaceAgendaCategoryUpdateModalProps > = ({ diff --git a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx index 4216d309a2..9fcb3f0a40 100644 --- a/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx +++ b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategory.tsx @@ -17,13 +17,22 @@ import AgendaCategoryCreateModal from './AgendaCategoryCreateModal'; import styles from './OrganizationAgendaCategory.module.css'; import Loader from 'components/Loader/Loader'; +/** + * Component for managing and displaying agenda item categories within an organization. + * + * This component allows users to view, create, and manage agenda item categories. It includes functionality for displaying categories, handling creation, and managing modal visibility. + * + * @returns The rendered component. + */ function organizationAgendaCategory(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'organizationAgendaCategory', }); + // Get the organization ID from URL parameters const { orgId: currentUrl } = useParams(); + // State for managing modal visibility and form data const [agendaCategoryCreateModalIsOpen, setAgendaCategoryCreateModalIsOpen] = useState(false); @@ -33,6 +42,9 @@ function organizationAgendaCategory(): JSX.Element { createdBy: '', }); + /** + * Query to fetch agenda item categories for the organization. + */ const { data: agendaCategoryData, loading: agendaCategoryLoading, @@ -48,10 +60,19 @@ function organizationAgendaCategory(): JSX.Element { notifyOnNetworkStatusChange: true, }); + /** + * Mutation to create a new agenda item category. + */ const [createAgendaCategory] = useMutation( CREATE_AGENDA_ITEM_CATEGORY_MUTATION, ); + /** + * Handler function to create a new agenda item category. + * + * @param e - The form submit event. + * @returns A promise that resolves when the agenda item category is created. + */ const createAgendaCategoryHandler = async ( e: ChangeEvent, ): Promise => { @@ -77,10 +98,16 @@ function organizationAgendaCategory(): JSX.Element { } }; + /** + * Toggles the visibility of the create agenda item category modal. + */ const showCreateModal = (): void => { setAgendaCategoryCreateModalIsOpen(!agendaCategoryCreateModalIsOpen); }; + /** + * Hides the create agenda item category modal. + */ const hideCreateModal = (): void => { setAgendaCategoryCreateModalIsOpen(!agendaCategoryCreateModalIsOpen); }; diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx index 5c32d292a3..c19cce6880 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx @@ -29,6 +29,13 @@ import type { } from 'utils/interfaces'; import styles from './OrganizationDashboard.module.css'; +/** + * Component for displaying the organization dashboard. + * + * This component provides an overview of various statistics and information related to an organization, including members, admins, posts, events, blocked users, and membership requests. It also displays upcoming events and latest posts. + * + * @returns The rendered component. + */ function organizationDashboard(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'dashboard' }); const { t: tCommon } = useTranslation('common'); @@ -45,6 +52,9 @@ function organizationDashboard(): JSX.Element { InterfaceQueryOrganizationEventListItem[] >([]); + /** + * Query to fetch organization data. + */ const { data, loading: loadingOrgData, @@ -59,6 +69,9 @@ function organizationDashboard(): JSX.Element { variables: { id: currentUrl }, }); + /** + * Query to fetch posts for the organization. + */ const { data: postData, loading: loadingPost, @@ -73,6 +86,9 @@ function organizationDashboard(): JSX.Element { variables: { id: currentUrl, first: 10 }, }); + /** + * Query to fetch events for the organization. + */ const { data: eventData, loading: loadingEvent, @@ -83,7 +99,9 @@ function organizationDashboard(): JSX.Element { }, }); - // UseEffect to update upcomingEvents array + /** + * UseEffect to update the list of upcoming events. + */ useEffect(() => { if (eventData && eventData?.eventsByOrganizationConnection.length > 0) { const tempUpcomingEvents: InterfaceQueryOrganizationEventListItem[] = []; @@ -100,6 +118,9 @@ function organizationDashboard(): JSX.Element { } }, [eventData?.eventsByOrganizationConnection]); + /** + * UseEffect to handle errors and navigate if necessary. + */ useEffect(() => { if (errorOrg || errorPost || errorEvent) { console.log('error', errorPost?.message); diff --git a/src/screens/OrganizationEvents/OrganizationEvents.tsx b/src/screens/OrganizationEvents/OrganizationEvents.tsx index aaa6f911a6..2998e89c39 100644 --- a/src/screens/OrganizationEvents/OrganizationEvents.tsx +++ b/src/screens/OrganizationEvents/OrganizationEvents.tsx @@ -40,6 +40,19 @@ export enum ViewType { YEAR = 'Year View', } +/** + * Organization Events Page Component to display the events of an organization + * and create new events for the organization by the admin or superadmin user. + * The component uses the EventCalendar component to display the events and EventHeader component + * to display the view type and create event button. + * The component uses the RecurrenceOptions component to display the recurrence options for the event. + * The component uses the CREATE_EVENT_MUTATION mutation to create a new event for the organization. + * The component uses the ORGANIZATION_EVENT_CONNECTION_LIST and ORGANIZATIONS_LIST queries to fetch the events + * and organization details. + * The component uses the useLocalStorage hook to get the user details from the local storage. + * + * @returns JSX.Element to display the Organization Events Page + */ function organizationEvents(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'organizationEvents', diff --git a/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx b/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx index a466e9326e..492f1866d9 100644 --- a/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx @@ -8,6 +8,9 @@ import { currencyOptions } from 'utils/currency'; import type { InterfaceCreateCampaign } from 'utils/interfaces'; import styles from './OrganizationFundCampaign.module.css'; +/** + * InterfaceCampaignCreateModal is an object containing the props for CampaignCreateModal component + */ interface InterfaceCampaignCreateModal { campaignCreateModalIsOpen: boolean; hideCreateCampaignModal: () => void; @@ -17,6 +20,16 @@ interface InterfaceCampaignCreateModal { t: (key: string) => string; } +/** + * CampaignCreateModal component is used to create the campaign + * @param campaignCreateModalIsOpen - boolean value to check if the modal is open or not + * @param hideCreateCampaignModal - function to hide the modal + * @param formState - object containing the form state + * @param setFormState - function to set the form state + * @param createCampaignHandler - function to create the campaign + * @param t - i18n function to translate the text + * @returns returns the CampaignCreateModal component + */ const CampaignCreateModal: React.FC = ({ campaignCreateModalIsOpen, hideCreateCampaignModal, diff --git a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx index 58351e1d1d..a4df9d1a80 100644 --- a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { Button, Modal } from 'react-bootstrap'; import styles from './OrganizationFundCampaign.module.css'; +/** + * InterfaceDeleteCampaignModal is an object containing the props for CampaignDeleteModal component + */ interface InterfaceDeleteCampaignModal { campaignDeleteModalIsOpen: boolean; hideDeleteCampaignModal: () => void; @@ -9,6 +12,16 @@ interface InterfaceDeleteCampaignModal { t: (key: string) => string; tCommon: (key: string) => string; } + +/** + * CampaignDeleteModal component is used to delete the campaign + * @param campaignDeleteModalIsOpen - boolean value to check if the modal is open or not + * @param hideDeleteCampaignModal - function to hide the modal + * @param deleteCampaignHandler - function to delete the campaign + * @param t - i18n function to translate the text + * @param tCommon - i18n function to translate the common text + * @returns returns the CampaignDeleteModal component + */ const CampaignDeleteModal: React.FC = ({ campaignDeleteModalIsOpen, hideDeleteCampaignModal, diff --git a/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx b/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx index 561c7c9252..16076df76a 100644 --- a/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx @@ -8,6 +8,9 @@ import { currencyOptions } from 'utils/currency'; import type { InterfaceCreateCampaign } from 'utils/interfaces'; import styles from './OrganizationFundCampaign.module.css'; +/** + * InterfaceCampaignUpdateModal is an object containing the props for CampaignUpdateModal component + */ interface InterfaceCampaignUpdateModal { campaignUpdateModalIsOpen: boolean; hideUpdateCampaignModal: () => void; @@ -18,6 +21,17 @@ interface InterfaceCampaignUpdateModal { showDeleteCampaignModal: () => void; } +/** + * CampaignUpdateModal component is used to update the campaign + * @param campaignUpdateModalIsOpen - boolean value to check if the modal is open or not + * @param hideUpdateCampaignModal - function to hide the modal + * @param formState - object containing the form state + * @param setFormState - function to set the form state + * @param updateCampaignHandler - function to update the campaign + * @param t - i18n function to translate the text + * @param showDeleteCampaignModal - function to show the delete campaign modal + * @returns returns the CampaignUpdateModal component + */ const CampaignUpdateModal: React.FC = ({ campaignUpdateModalIsOpen, hideUpdateCampaignModal, diff --git a/src/screens/OrganizationFunds/FundCreateModal.tsx b/src/screens/OrganizationFunds/FundCreateModal.tsx index 4dcf32c265..e89152453f 100644 --- a/src/screens/OrganizationFunds/FundCreateModal.tsx +++ b/src/screens/OrganizationFunds/FundCreateModal.tsx @@ -4,6 +4,9 @@ import { Button, Form, Modal } from 'react-bootstrap'; import type { InterfaceCreateFund } from 'utils/interfaces'; import styles from './OrganizationFunds.module.css'; +/** + * This interface defines the props for the FundCreateModal component. + */ interface InterfaceFundCreateModal { fundCreateModalIsOpen: boolean; hideCreateModal: () => void; @@ -17,6 +20,21 @@ interface InterfaceFundCreateModal { t: (key: string) => string; } +/** + * FundCreateModal component allows users to create a new fund. + * It displays a modal with a form to enter the fund details. + * + * @param fundCreateModalIsOpen - Controls whether the modal is open or closed. + * @param hideCreateModal - Function to hide the modal. + * @param formState - The current state of the form used to create a fund. + * @param setFormState - Function to update the state of the form. + * @param createFundHandler - Handler function for form submission. + * @param taxDeductible - Indicates whether the fund is tax deductible. + * @param setTaxDeductible - Function to set the tax deductible state. + * @param isDefault - Indicates whether the fund is the default fund. + * @param setIsDefault - Function to set the default state. + * @param t - Translation function to get the translated text. + */ const FundCreateModal: React.FC = ({ fundCreateModalIsOpen, hideCreateModal, diff --git a/src/screens/OrganizationFunds/FundUpdateModal.tsx b/src/screens/OrganizationFunds/FundUpdateModal.tsx index 2a351a1c8c..9e3942e562 100644 --- a/src/screens/OrganizationFunds/FundUpdateModal.tsx +++ b/src/screens/OrganizationFunds/FundUpdateModal.tsx @@ -4,6 +4,9 @@ import { Button, Form, Modal } from 'react-bootstrap'; import type { InterfaceCreateFund } from 'utils/interfaces'; import styles from './OrganizationFunds.module.css'; +/** + * This interface defines the props for the FundUpdateModal component. + */ interface InterfaceFundUpdateModal { fundUpdateModalIsOpen: boolean; hideUpdateModal: () => void; @@ -19,7 +22,24 @@ interface InterfaceFundUpdateModal { setIsDefault: (state: React.SetStateAction) => void; t: (key: string) => string; } - +/** + * FundUpdateModal component allows users to update an existing fund. + * It displays a modal with a form to modify the fund details. + * + * @param fundUpdateModalIsOpen - Controls whether the modal is open or closed. + * @param hideUpdateModal - Function to hide the modal. + * @param formState - The current state of the form used to update a fund. + * @param setFormState - Function to update the state of the form. + * @param updateFundHandler - Handler function for form submission. + * @param taxDeductible - Indicates whether the fund is tax deductible. + * @param setTaxDeductible - Function to set the tax deductible state. + * @param isArchived - Indicates whether the fund is archived. + * @param deleteFundHandler - Handler function to delete the fund. + * @param setIsArchived - Function to set the archived state. + * @param isDefault - Indicates whether the fund is the default fund. + * @param setIsDefault - Function to set the default state. + * @param t - Translation function to get the translated text. + */ const FundUpdateModal: React.FC = ({ fundUpdateModalIsOpen, hideUpdateModal, diff --git a/src/screens/OrganizationFunds/OrganizationFunds.tsx b/src/screens/OrganizationFunds/OrganizationFunds.tsx index b0846c8c0a..2232e04721 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.tsx @@ -51,6 +51,12 @@ const StyledTableRow = styled(TableRow)(() => ({ }, })); +/** + * This component handles displaying and managing organization funds. + * It allows users to create, update, delete, and filter funds. + * + * @returns A JSX.Element that renders the organization funds page. + */ const organizationFunds = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'funds', @@ -73,6 +79,9 @@ const organizationFunds = (): JSX.Element => { fundRef: '', }); + /** + * Fetches the list of funds by organization. + */ const { data: fundData, loading: fundLoading, @@ -92,6 +101,10 @@ const organizationFunds = (): JSX.Element => { }); const [fullName, setFullName] = useState(''); + + /** + * Handles the search functionality by refetching funds based on the filter. + */ const handleSearch = (): void => { refetchFunds({ organizationId: currentUrl, filter: fullName }); }; @@ -100,21 +113,46 @@ const organizationFunds = (): JSX.Element => { const [updateFund] = useMutation(UPDATE_FUND_MUTATION); const [deleteFund] = useMutation(REMOVE_FUND_MUTATION); + /** + * Toggles the visibility of the create fund modal. + */ const showCreateModal = (): void => { setFundCreateModalIsOpen(!fundCreateModalIsOpen); }; + + /** + * Hides the create fund modal. + */ const hideCreateModal = (): void => { setFundCreateModalIsOpen(!fundCreateModalIsOpen); }; + + /** + * Toggles the visibility of the update fund modal. + */ const showUpdateModal = (): void => { setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); }; + + /** + * Hides the update fund modal. + */ const hideUpdateModal = (): void => { setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); }; + + /** + * Toggles the visibility of the delete fund modal. + */ const toggleDeleteModal = (): void => { setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); }; + + /** + * Handles the edit action by setting the form state and showing the update modal. + * + * @param fund - The fund information to be edited. + */ const handleEditClick = (fund: InterfaceFundInfo): void => { setFormState({ fundName: fund.name, @@ -126,6 +164,12 @@ const organizationFunds = (): JSX.Element => { setFund(fund); showUpdateModal(); }; + + /** + * Handles the creation of a new fund. + * + * @param e - The form submission event. + */ const createFundHandler = async ( e: ChangeEvent, ): Promise => { @@ -156,6 +200,12 @@ const organizationFunds = (): JSX.Element => { } } }; + + /** + * Handles the updating of an existing fund. + * + * @param e - The form submission event. + */ const updateFundHandler = async ( e: ChangeEvent, ): Promise => { @@ -198,6 +248,10 @@ const organizationFunds = (): JSX.Element => { } } }; + + /** + * Handles the deletion of a fund. + */ const deleteFundHandler = async (): Promise => { try { await deleteFund({ @@ -216,6 +270,11 @@ const organizationFunds = (): JSX.Element => { } }; + /** + * Handles clicking on a fund to navigate to its campaign page. + * + * @param fundId - The ID of the fund to navigate to. + */ const handleClick = (fundId: String) => { navigate(`/orgfundcampaign/${currentUrl}/${fundId}`); }; diff --git a/src/screens/OrganizationPeople/AddMember.tsx b/src/screens/OrganizationPeople/AddMember.tsx index 5b7ddc2a21..3199293ef8 100644 --- a/src/screens/OrganizationPeople/AddMember.tsx +++ b/src/screens/OrganizationPeople/AddMember.tsx @@ -48,6 +48,15 @@ const StyledTableRow = styled(TableRow)(() => ({ }, })); +/** + * AddMember component is used to add new members to the organization by selecting from + * the existing users or creating a new user. + * It uses the following queries and mutations: + * ORGANIZATIONS_LIST, + * ORGANIZATIONS_MEMBER_CONNECTION_LIST, + * USERS_CONNECTION_LIST, + * ADD_MEMBER_MUTATION,SIGNUP_MUTATION. + */ function AddMember(): JSX.Element { const { t: translateOrgPeople } = useTranslation('translation', { keyPrefix: 'organizationPeople', diff --git a/src/screens/OrganizationPeople/OrganizationPeople.tsx b/src/screens/OrganizationPeople/OrganizationPeople.tsx index 6a2626fd95..d12890f942 100644 --- a/src/screens/OrganizationPeople/OrganizationPeople.tsx +++ b/src/screens/OrganizationPeople/OrganizationPeople.tsx @@ -22,6 +22,12 @@ import type { GridColDef, GridCellParams } from '@mui/x-data-grid'; import { Stack } from '@mui/material'; import Avatar from 'components/Avatar/Avatar'; +/** + * OrganizationPeople component is used to display the list of members, admins and users of the organization. + * It also provides the functionality to search the members, admins and users by their full name. + * It also provides the functionality to remove the members and admins from the organization. + * @returns JSX.Element which contains the list of members, admins and users of the organization. + */ function organizationPeople(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'organizationPeople', diff --git a/src/screens/OrganizationVenues/OrganizationVenues.tsx b/src/screens/OrganizationVenues/OrganizationVenues.tsx index 6c08733e17..c2c3afe7e4 100644 --- a/src/screens/OrganizationVenues/OrganizationVenues.tsx +++ b/src/screens/OrganizationVenues/OrganizationVenues.tsx @@ -15,13 +15,21 @@ import { DELETE_VENUE_MUTATION } from 'GraphQl/Mutations/VenueMutations'; import type { InterfaceQueryVenueListItem } from 'utils/interfaces'; import VenueCard from 'components/Venues/VenueCard'; +/** + * Component to manage and display the list of organization venues. + * Handles searching, sorting, and CRUD operations for venues. + */ function organizationVenues(): JSX.Element { + // Translation hooks for i18n support const { t } = useTranslation('translation', { keyPrefix: 'organizationVenues', }); const { t: tCommon } = useTranslation('common'); + // Setting the document title using the translation hook document.title = t('title'); + + // State hooks for managing component state const [venueModal, setVenueModal] = useState(false); const [venueModalMode, setVenueModalMode] = useState<'edit' | 'create'>( 'create', @@ -33,11 +41,13 @@ function organizationVenues(): JSX.Element { useState(null); const [venues, setVenues] = useState([]); + // Getting the organization ID from the URL parameters const { orgId } = useParams(); if (!orgId) { return ; } + // GraphQL query for fetching venue data const { data: venueData, loading: venueLoading, @@ -54,8 +64,13 @@ function organizationVenues(): JSX.Element { }, }); + // GraphQL mutation for deleting a venue const [deleteVenue] = useMutation(DELETE_VENUE_MUTATION); + /** + * Handles the deletion of a venue by ID. + * @param venueId - The ID of the venue to delete. + */ const handleDelete = async (venueId: string): Promise => { try { await deleteVenue({ @@ -68,36 +83,57 @@ function organizationVenues(): JSX.Element { } }; + /** + * Updates the search term state when the user types in the search input. + * @param event - The input change event. + */ const handleSearchChange = ( event: React.ChangeEvent, ): void => { setSearchTerm(event.target.value); }; + /** + * Updates the sort order state when the user selects a sort option. + * @param order - The order to sort venues by (highest or lowest capacity). + */ const handleSortChange = (order: 'highest' | 'lowest'): void => { setSortOrder(order); }; + /** + * Toggles the visibility of the venue modal. + */ const toggleVenueModal = (): void => { setVenueModal(!venueModal); }; + /** + * Shows the edit venue modal with the selected venue data. + * @param venueItem - The venue data to edit. + */ const showEditVenueModal = (venueItem: InterfaceQueryVenueListItem): void => { setVenueModalMode('edit'); setEditVenueData(venueItem); toggleVenueModal(); }; + /** + * Shows the create venue modal. + */ const showCreateVenueModal = (): void => { setVenueModalMode('create'); setEditVenueData(null); toggleVenueModal(); }; + + // Error handling for venue data fetch /* istanbul ignore next */ if (venueError) { errorHandler(t, venueError); } + // Updating venues state when venue data changes useEffect(() => { if (venueData && venueData.getVenueByOrgId) { setVenues(venueData.getVenueByOrgId); diff --git a/src/screens/PageNotFound/PageNotFound.tsx b/src/screens/PageNotFound/PageNotFound.tsx index 92f4a98f7e..58fc859b4f 100644 --- a/src/screens/PageNotFound/PageNotFound.tsx +++ b/src/screens/PageNotFound/PageNotFound.tsx @@ -6,15 +6,24 @@ import useLocalStorage from 'utils/useLocalstorage'; import styles from './PageNotFound.module.css'; import Logo from 'assets/images/talawa-logo-200x200.png'; +/** + * The `PageNotFound` component displays a 404 error page when a user navigates to a non-existent route. + * It shows a message indicating that the page was not found and provides a link to redirect users back + * to the appropriate home page based on their admin status. + * + */ const PageNotFound = (): JSX.Element => { + // Translation hooks for internationalization const { t } = useTranslation('translation', { keyPrefix: 'pageNotFound', }); const { t: tCommon } = useTranslation('common'); const { t: tErrors } = useTranslation('errors'); + // Set the document title to the translated title for the 404 page document.title = t('title'); + // Get the admin status from local storage const { getItem } = useLocalStorage(); const adminFor = getItem('AdminFor'); @@ -23,6 +32,7 @@ const PageNotFound = (): JSX.Element => {
    Logo + {/* Display a message based on admin status */} {adminFor != undefined ? (

    {tCommon('talawaAdminPortal')} @@ -31,10 +41,13 @@ const PageNotFound = (): JSX.Element => {

    {t('talawaUser')}

    )}
    + {/* Display the 404 error code */}

    {t('404')}

    + {/* Display a not found message */}

    {tErrors('notFoundMsg')}

    + {/* Provide a link to redirect users based on admin status */} {adminFor != undefined ? ( {t('backToHome')} diff --git a/src/screens/Requests/Requests.tsx b/src/screens/Requests/Requests.tsx index a779bd414c..a8d9f40315 100644 --- a/src/screens/Requests/Requests.tsx +++ b/src/screens/Requests/Requests.tsx @@ -26,14 +26,23 @@ interface InterfaceRequestsListItem { }; } +/** + * The `Requests` component fetches and displays a paginated list of membership requests + * for an organization, with functionality for searching, filtering, and infinite scrolling. + * + */ const Requests = (): JSX.Element => { + // Translation hooks for internationalization const { t } = useTranslation('translation', { keyPrefix: 'requests' }); const { t: tCommon } = useTranslation('common'); + // Set the document title to the translated title for the requests page document.title = t('title'); + // Hook for managing local storage const { getItem } = useLocalStorage(); + // Define constants and state variables const perPageResult = 8; const [isLoading, setIsLoading] = useState(true); const [hasMore, setHasMore] = useState(true); @@ -47,6 +56,7 @@ const Requests = (): JSX.Element => { const { orgId = '' } = useParams(); const organizationId = orgId; + // Query to fetch membership requests const { data, loading, fetchMore, refetch } = useQuery(MEMBERSHIP_REQUEST, { variables: { id: organizationId, @@ -57,6 +67,7 @@ const Requests = (): JSX.Element => { notifyOnNetworkStatusChange: true, }); + // Query to fetch the list of organizations const { data: orgsData } = useQuery(ORGANIZATION_CONNECTION_LIST); const [displayedRequests, setDisplayedRequests] = useState( data?.organizations[0]?.membershipRequests || [], @@ -77,14 +88,14 @@ const Requests = (): JSX.Element => { setDisplayedRequests(membershipRequests); }, [data]); - // To clear the search when the component is unmounted + // Clear the search field when the component is unmounted useEffect(() => { return () => { setSearchByName(''); }; }, []); - // Warn if there is no organization + // Show a warning if there are no organizations useEffect(() => { if (!orgsData) { return; @@ -95,7 +106,7 @@ const Requests = (): JSX.Element => { } }, [orgsData]); - // Send to orgList page if user is not admin + // Redirect to orgList page if the user is not an admin useEffect(() => { if (userRole != 'ADMIN' && userRole != 'SUPERADMIN') { window.location.assign('/orglist'); @@ -111,6 +122,11 @@ const Requests = (): JSX.Element => { } }, [loading]); + /** + * Handles the search input change and refetches the data based on the search value. + * + * @param value - The search term entered by the user. + */ const handleSearch = (value: string): void => { setSearchByName(value); if (value === '') { @@ -124,6 +140,11 @@ const Requests = (): JSX.Element => { }); }; + /** + * Handles search input when the Enter key is pressed. + * + * @param e - The keyboard event. + */ const handleSearchByEnter = ( e: React.KeyboardEvent, ): void => { @@ -133,6 +154,9 @@ const Requests = (): JSX.Element => { } }; + /** + * Handles the search button click to trigger the search. + */ const handleSearchByBtnClick = (): void => { const inputElement = document.getElementById( 'searchRequests', @@ -141,6 +165,9 @@ const Requests = (): JSX.Element => { handleSearch(inputValue); }; + /** + * Resets search and refetches the data. + */ const resetAndRefetch = (): void => { refetch({ first: perPageResult, @@ -149,6 +176,10 @@ const Requests = (): JSX.Element => { }); setHasMore(true); }; + + /** + * Loads more requests when scrolling to the bottom of the page. + */ /* istanbul ignore next */ const loadMoreRequests = (): void => { setIsLoadingMore(true); @@ -188,6 +219,7 @@ const Requests = (): JSX.Element => { }); }; + // Header titles for the table const headerTitles: string[] = [ t('sl_no'), tCommon('name'), diff --git a/src/screens/UserPortal/Chat/Chat.tsx b/src/screens/UserPortal/Chat/Chat.tsx index ee6a6c04f1..91e22d7f9d 100644 --- a/src/screens/UserPortal/Chat/Chat.tsx +++ b/src/screens/UserPortal/Chat/Chat.tsx @@ -25,7 +25,32 @@ interface InterfaceContactCardProps { interface InterfaceChatRoomProps { selectedContact: string; } - +/** + * The `chat` component provides a user interface for interacting with contacts and chat rooms within an organization. + * It features a contact list with search functionality and displays the chat room for the selected contact. + * The component uses GraphQL to fetch and manage contact data, and React state to handle user interactions. + * + * ## Features: + * - **Search Contacts:** Allows users to search for contacts by their first name. + * - **Contact List:** Displays a list of contacts with their details and a profile image. + * - **Chat Room:** Shows the chat room for the selected contact. + * + * ## GraphQL Queries: + * - `ORGANIZATIONS_MEMBER_CONNECTION_LIST`: Fetches a list of members within an organization, with optional filtering based on the first name. + * + * ## Event Handlers: + * - `handleSearch`: Updates the filterName state and refetches the contact data based on the search query. + * - `handleSearchByEnter`: Handles search input when the Enter key is pressed. + * - `handleSearchByBtnClick`: Handles search input when the search button is clicked. + * + * ## Rendering: + * - Displays a search input field and a search button for filtering contacts. + * - Shows a list of contacts with their details and profile images. + * - Renders a chat room component for the selected contact. + * - Displays a loading indicator while contact data is being fetched. + * + * @returns The rendered `chat` component. + */ export default function chat(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'userChat', diff --git a/src/screens/UserPortal/Events/Events.tsx b/src/screens/UserPortal/Events/Events.tsx index bf2de7e456..627df83539 100644 --- a/src/screens/UserPortal/Events/Events.tsx +++ b/src/screens/UserPortal/Events/Events.tsx @@ -21,11 +21,25 @@ import { errorHandler } from 'utils/errorHandler'; import useLocalStorage from 'utils/useLocalstorage'; import styles from './Events.module.css'; +/** + * Converts a time string to a Dayjs object. + * + * @param time - The time string to convert, in HH:mm:ss format. + * @returns A Dayjs object representing the time. + */ const timeToDayJs = (time: string): Dayjs => { const dateTimeString = dayjs().format('YYYY-MM-DD') + ' ' + time; return dayjs(dateTimeString, { format: 'YYYY-MM-DD HH:mm:ss' }); }; +/** + * Component to manage and display events for an organization. + * + * This component allows users to view, create, and manage events within an organization. + * It includes a calendar view, a form to create new events, and various filters and settings. + * + * @returns The JSX element for the events management interface. + */ export default function events(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'userEvents', @@ -34,6 +48,7 @@ export default function events(): JSX.Element { const { getItem } = useLocalStorage(); + // State variables to manage event details and UI const [events, setEvents] = React.useState([]); const [eventTitle, setEventTitle] = React.useState(''); const [eventDescription, setEventDescription] = React.useState(''); @@ -50,6 +65,7 @@ export default function events(): JSX.Element { const [createEventModal, setCreateEventmodalisOpen] = React.useState(false); const { orgId: organizationId } = useParams(); + // Query to fetch events for the organization const { data, refetch } = useQuery(ORGANIZATION_EVENTS_CONNECTION, { variables: { organization_id: organizationId, @@ -57,12 +73,15 @@ export default function events(): JSX.Element { }, }); + // Query to fetch organization details const { data: orgData } = useQuery(ORGANIZATIONS_LIST, { variables: { id: organizationId }, }); + // Mutation to create a new event const [create] = useMutation(CREATE_EVENT_MUTATION); + // Get user details from local storage const userId = getItem('id') as string; const superAdmin = getItem('SuperAdmin'); @@ -73,6 +92,12 @@ export default function events(): JSX.Element { ? 'ADMIN' : 'USER'; + /** + * Handles the form submission for creating a new event. + * + * @param e - The form submit event. + * @returns A promise that resolves when the event is created. + */ const createEvent = async ( e: ChangeEvent, ): Promise => { @@ -114,28 +139,52 @@ export default function events(): JSX.Element { } }; + /** + * Toggles the visibility of the event creation modal. + * + * @returns Void. + */ /* istanbul ignore next */ const toggleCreateEventModal = (): void => setCreateEventmodalisOpen(!createEventModal); + /** + * Updates the event title state when the input changes. + * + * @param event - The input change event. + * @returns Void. + */ const handleEventTitleChange = ( event: React.ChangeEvent, ): void => { setEventTitle(event.target.value); }; + /** + * Updates the event location state when the input changes. + * + * @param event - The input change event. + * @returns Void. + */ const handleEventLocationChange = ( event: React.ChangeEvent, ): void => { setEventLocation(event.target.value); }; + /** + * Updates the event description state when the input changes. + * + * @param event - The input change event. + * @returns Void. + */ const handleEventDescriptionChange = ( event: React.ChangeEvent, ): void => { setEventDescription(event.target.value); }; + // Update the list of events when the data from the query changes /* istanbul ignore next */ React.useEffect(() => { if (data) { @@ -143,10 +192,22 @@ export default function events(): JSX.Element { } }, [data]); + /** + * Shows the modal for creating a new event. + * + * @returns Void. + */ /* istanbul ignore next */ const showInviteModal = (): void => { setCreateEventmodalisOpen(true); }; + + /** + * Updates the calendar view type. + * + * @param item - The view type to set, or null to reset. + * @returns Void. + */ /* istanbul ignore next */ const handleChangeView = (item: string | null): void => { /*istanbul ignore next*/ diff --git a/src/screens/UserPortal/Organizations/Organizations.tsx b/src/screens/UserPortal/Organizations/Organizations.tsx index 433416387d..bc6aa08725 100644 --- a/src/screens/UserPortal/Organizations/Organizations.tsx +++ b/src/screens/UserPortal/Organizations/Organizations.tsx @@ -42,6 +42,9 @@ interface InterfaceOrganizationCardProps { }[]; } +/** + * Interface defining the structure of organization properties. + */ interface InterfaceOrganization { _id: string; name: string; @@ -66,6 +69,10 @@ interface InterfaceOrganization { }[]; } +/** + * Component for displaying and managing user organizations. + * + */ export default function organizations(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'userOrganizations', @@ -73,6 +80,9 @@ export default function organizations(): JSX.Element { const [hideDrawer, setHideDrawer] = useState(null); + /** + * Handles window resize events to toggle drawer visibility. + */ const handleResize = (): void => { if (window.innerWidth <= 820) { setHideDrawer(!hideDrawer); @@ -123,6 +133,12 @@ export default function organizations(): JSX.Element { }, ); + /** + * Handles page change in pagination. + * + * @param _event - The event triggering the page change. + * @param newPage - The new page number. + */ /* istanbul ignore next */ const handleChangePage = ( _event: React.MouseEvent | null, @@ -131,6 +147,11 @@ export default function organizations(): JSX.Element { setPage(newPage); }; + /** + * Handles change in the number of rows per page. + * + * @param event - The event triggering the change. + */ /* istanbul ignore next */ const handleChangeRowsPerPage = ( event: React.ChangeEvent, @@ -141,6 +162,11 @@ export default function organizations(): JSX.Element { setPage(0); }; + /** + * Searches organizations based on the provided filter value. + * + * @param value - The search filter value. + */ const handleSearch = (value: string): void => { setFilterName(value); @@ -148,6 +174,12 @@ export default function organizations(): JSX.Element { filter: value, }); }; + + /** + * Handles search input submission by pressing the Enter key. + * + * @param e - The keyboard event. + */ const handleSearchByEnter = ( e: React.KeyboardEvent, ): void => { @@ -156,6 +188,10 @@ export default function organizations(): JSX.Element { handleSearch(value); } }; + + /** + * Handles search button click to search organizations. + */ const handleSearchByBtnClick = (): void => { const value = (document.getElementById('searchUserOrgs') as HTMLInputElement)?.value || @@ -163,6 +199,9 @@ export default function organizations(): JSX.Element { handleSearch(value); }; + /** + * Updates the list of organizations based on query results and selected mode. + */ /* istanbul ignore next */ React.useEffect(() => { if (data) { @@ -189,6 +228,9 @@ export default function organizations(): JSX.Element { } }, [data]); + /** + * Updates the list of organizations based on the selected mode and query results. + */ /* istanbul ignore next */ React.useEffect(() => { if (mode === 0) { diff --git a/src/screens/UserPortal/People/People.tsx b/src/screens/UserPortal/People/People.tsx index c8f373f72d..f9475d40a7 100644 --- a/src/screens/UserPortal/People/People.tsx +++ b/src/screens/UserPortal/People/People.tsx @@ -31,21 +31,35 @@ interface InterfaceMember { userType: string; } +/** + * `People` component displays a list of people associated with an organization. + * It allows users to filter between all members and admins, search for members by their first name, + * and paginate through the list. + */ export default function people(): JSX.Element { + // i18n translation hook for user organization related translations const { t } = useTranslation('translation', { keyPrefix: 'userOrganizations', }); + + // i18n translation hook for common translations const { t: tCommon } = useTranslation('common'); + // State for managing current page in pagination const [page, setPage] = React.useState(0); + + // State for managing the number of rows per page in pagination const [rowsPerPage, setRowsPerPage] = React.useState(5); const [members, setMembers] = React.useState([]); const [mode, setMode] = React.useState(0); + // Extracting organization ID from URL parameters const { orgId: organizationId } = useParams(); + // Filter modes for dropdown selection const modes = ['All Members', 'Admins']; + // Query to fetch list of members of the organization const { data, loading, refetch } = useQuery( ORGANIZATIONS_MEMBER_CONNECTION_LIST, { @@ -56,10 +70,15 @@ export default function people(): JSX.Element { }, ); + // Query to fetch list of admins of the organization const { data: data2 } = useQuery(ORGANIZATION_ADMINS_LIST, { variables: { id: organizationId }, }); + /** + * Handles page change in pagination. + * + */ /* istanbul ignore next */ const handleChangePage = ( _event: React.MouseEvent | null, @@ -68,6 +87,10 @@ export default function people(): JSX.Element { setPage(newPage); }; + /** + * Handles change in the number of rows per page. + * + */ /* istanbul ignore next */ const handleChangeRowsPerPage = ( event: React.ChangeEvent, @@ -78,12 +101,20 @@ export default function people(): JSX.Element { setPage(0); }; + /** + * Searches for members based on the filter value. + * + */ const handleSearch = (newFilter: string): void => { refetch({ firstName_contains: newFilter, }); }; + /** + * Handles search operation triggered by pressing the Enter key. + * + */ const handleSearchByEnter = ( e: React.KeyboardEvent, ): void => { @@ -93,6 +124,9 @@ export default function people(): JSX.Element { } }; + /** + * Handles search operation triggered by clicking the search button. + */ const handleSearchByBtnClick = (): void => { const inputValue = (document.getElementById('searchPeople') as HTMLInputElement)?.value || @@ -107,6 +141,9 @@ export default function people(): JSX.Element { } }, [data]); + /** + * Updates the list of members based on the selected filter mode. + */ /* istanbul ignore next */ React.useEffect(() => { if (mode == 0) { @@ -178,7 +215,6 @@ export default function people(): JSX.Element { S.No Avatar - {/* Avatar */} Name Email Role diff --git a/src/screens/UserPortal/Settings/Settings.tsx b/src/screens/UserPortal/Settings/Settings.tsx index e4f89bd0a0..d96a559705 100644 --- a/src/screens/UserPortal/Settings/Settings.tsx +++ b/src/screens/UserPortal/Settings/Settings.tsx @@ -22,6 +22,12 @@ import OtherSettings from 'components/UserProfileSettings/OtherSettings'; import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; import ProfileDropdown from 'components/ProfileDropdown/ProfileDropdown'; +/** + * The Settings component allows users to view and update their profile settings. + * It includes functionality to handle image uploads, reset changes, and save updated user details. + * + * @returns The Settings component. + */ export default function settings(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'settings', @@ -30,6 +36,10 @@ export default function settings(): JSX.Element { const [hideDrawer, setHideDrawer] = useState(null); + /** + * Handler to adjust sidebar visibility based on window width. + * This function is invoked on window resize and when the component mounts. + */ const handleResize = (): void => { if (window.innerWidth <= 820) { setHideDrawer(!hideDrawer); @@ -65,8 +75,21 @@ export default function settings(): JSX.Element { image: '', }); + /** + * Ref to store the original image URL for comparison during updates. + */ const originalImageState = React.useRef(''); + + /** + * Ref to access the file input element for image uploads. + */ const fileInputRef = React.useRef(null); + + /** + * Handles the update of user details. + * This function sends a mutation request to update the user details + * and reloads the page on success. + */ const handleUpdateUserDetails = async (): Promise => { try { let updatedUserDetails = { ...userDetails }; @@ -91,6 +114,12 @@ export default function settings(): JSX.Element { } }; + /** + * Handles the change of a specific field in the user details state. + * + * @param fieldName - The name of the field to be updated. + * @param value - The new value for the field. + */ const handleFieldChange = (fieldName: string, value: string): void => { setUserDetails((prevState) => ({ ...prevState, @@ -98,12 +127,18 @@ export default function settings(): JSX.Element { })); }; + /** + * Triggers the file input click event to open the file picker dialog. + */ const handleImageUpload = (): void => { if (fileInputRef.current) { (fileInputRef.current as HTMLInputElement).click(); } }; + /** + * Resets the user details to the values fetched from the server. + */ const handleResetChanges = (): void => { /* istanbul ignore next */ if (data) { diff --git a/src/screens/UserPortal/UserScreen/UserScreen.tsx b/src/screens/UserPortal/UserScreen/UserScreen.tsx index a1605fa59d..7c7e3c1d05 100644 --- a/src/screens/UserPortal/UserScreen/UserScreen.tsx +++ b/src/screens/UserPortal/UserScreen/UserScreen.tsx @@ -9,11 +9,28 @@ import { Button } from 'react-bootstrap'; import UserSidebarOrg from 'components/UserPortal/UserSidebarOrg/UserSidebarOrg'; import ProfileDropdown from 'components/ProfileDropdown/ProfileDropdown'; +/** + * The UserScreen component serves as a container for user-specific pages + * within an organization context. It provides layout and sidebar navigation + * functionality based on the current organization ID and user roles. + * + * @returns The UserScreen component. + */ const UserScreen = (): JSX.Element => { + // Get the current location path for debugging or conditional rendering const location = useLocation(); + + /** + * State to manage the visibility of the sidebar (drawer). + */ const [hideDrawer, setHideDrawer] = useState(null); + + /** + * Retrieves the organization ID from the URL parameters. + */ const { orgId } = useParams(); + // Redirect to home if orgId is not present if (!orgId) { return ; } @@ -23,19 +40,31 @@ const UserScreen = (): JSX.Element => { } = useSelector((state: RootState) => state.userRoutes); const { targets } = appRoutes; + // Log the current path for debugging purposes console.log(location.pathname.split('/')); + // Initialize Redux dispatch const dispatch = useDispatch(); + + /** + * Effect hook to update targets based on the organization ID. + * This hook is triggered when the orgId changes. + */ useEffect(() => { dispatch(updateTargets(orgId)); }, [orgId]); // Added orgId to the dependency array + /** + * Handles window resize events to toggle the sidebar visibility + * based on the screen width. + */ const handleResize = (): void => { if (window.innerWidth <= 820) { setHideDrawer(!hideDrawer); } }; + // Set up event listener for window resize and clean up on unmount useEffect(() => { handleResize(); window.addEventListener('resize', handleResize); diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 3dae6ec17e..9999a631d6 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -19,7 +19,48 @@ import type { InterfaceQueryUserListItem } from 'utils/interfaces'; import styles from './Users.module.css'; import useLocalStorage from 'utils/useLocalstorage'; import type { ApolloError } from '@apollo/client'; - +/** + * The `Users` component is responsible for displaying a list of users in a paginated and sortable format. + * It supports search functionality, filtering, and sorting of users. The component integrates with GraphQL + * for fetching and managing user data and displays results with infinite scrolling. + * + * ## Features: + * - **Search:** Allows users to search for users by their first name. + * - **Sorting:** Provides options to sort users by creation date (newest or oldest). + * - **Filtering:** Enables filtering users based on their roles (admin, superadmin, user, etc.). + * - **Pagination:** Utilizes infinite scrolling to load more users as the user scrolls down. + * + * ## GraphQL Queries: + * - `USER_LIST`: Fetches a list of users with specified search, sorting, and pagination parameters. + * - `ORGANIZATION_CONNECTION_LIST`: Fetches a list of organizations to verify organization existence. + * + * + * ## Component State: + * - `isLoading`: Indicates whether the component is currently loading data. + * - `hasMore`: Indicates if there are more users to load. + * - `isLoadingMore`: Indicates if more users are currently being loaded. + * - `searchByName`: The current search query for user names. + * - `sortingOption`: The current sorting option (newest or oldest). + * - `filteringOption`: The current filtering option (admin, superadmin, user, cancel). + * - `displayedUsers`: The list of users currently displayed, filtered and sorted. + * + * ## Event Handlers: + * - `handleSearch`: Handles searching users by name and refetches the user list. + * - `handleSearchByEnter`: Handles search input when the Enter key is pressed. + * - `handleSearchByBtnClick`: Handles search input when the search button is clicked. + * - `resetAndRefetch`: Resets search and refetches the user list with default parameters. + * - `loadMoreUsers`: Loads more users when scrolling reaches the end of the list. + * - `handleSorting`: Updates sorting option and refetches the user list. + * - `handleFiltering`: Updates filtering option and refetches the user list. + * + * ## Rendering: + * - Displays a search input and button for searching users. + * - Provides dropdowns for sorting and filtering users. + * - Renders a table of users with infinite scrolling support. + * - Shows appropriate messages when no users are found or when search yields no results. + * + * @returns The rendered `Users` component. + */ const Users = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); const { t: tCommon } = useTranslation('common'); @@ -50,7 +91,9 @@ const Users = (): JSX.Element => { }: { data?: { users: InterfaceQueryUserListItem[] }; loading: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any fetchMore: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any refetch: any; error?: ApolloError; } = useQuery(USER_LIST, { @@ -130,6 +173,7 @@ const Users = (): JSX.Element => { setHasMore(true); }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleSearchByEnter = (e: any): void => { if (e.key === 'Enter') { const { value } = e.target; From 344583a8240ed1face3441ef3a2952779d47ea7f Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 24 Jul 2024 23:02:33 +0530 Subject: [PATCH 4/8] merge conflict solved --- public/locales/en/common.json | 1 + public/locales/en/translation.json | 47 +- public/locales/fr/common.json | 1 + public/locales/fr/translation.json | 58 +- public/locales/hi/common.json | 1 + public/locales/hi/translation.json | 86 +- public/locales/sp/common.json | 1 + public/locales/sp/translation.json | 86 +- public/locales/zh/common.json | 3 +- public/locales/zh/translation.json | 68 +- src/GraphQl/Queries/fundQueries.ts | 18 +- .../FundCampaignPledge.module.css | 136 +++- .../FundCampaignPledge.test.tsx | 113 ++- .../FundCampaignPledge/FundCampaignPledge.tsx | 347 ++++++-- .../PledgeDeleteModal.test.tsx | 9 +- .../FundCampaignPledge/PledgeDeleteModal.tsx | 20 - .../FundCampaignPledge/PledgeModal.test.tsx | 2 +- .../FundCampaignPledge/PledgeModal.tsx | 54 +- .../FundCampaignPledge/PledgesMocks.ts | 69 ++ .../CampaignCreateModal.tsx | 171 ---- .../CampaignDeleteModal.test.tsx | 95 +++ .../CampaignDeleteModal.tsx | 69 +- .../CampaignModal.test.tsx | 266 +++++++ .../CampaignModal.tsx | 291 +++++++ .../CampaignUpdateModal.tsx | 184 ----- .../OrganizationFundCampagins.tsx | 674 ++++++++-------- .../OrganizationFundCampaign.module.css | 35 +- .../OrganizationFundCampaign.test.tsx | 745 +++++------------- ...s.tsx => OrganizationFundCampaignMocks.ts} | 262 +++--- .../OrganizationFunds/FundCreateModal.tsx | 136 ---- .../FundDeleteModal.test.tsx | 100 +++ .../OrganizationFunds/FundDeleteModal.tsx | 79 ++ .../OrganizationFunds/FundModal.test.tsx | 260 ++++++ src/screens/OrganizationFunds/FundModal.tsx | 258 ++++++ .../OrganizationFunds/FundUpdateModal.tsx | 175 ---- .../OrganizationFunds.module.css | 99 +-- .../OrganizationFunds.test.tsx | 507 +++++------- .../OrganizationFunds/OrganizationFunds.tsx | 740 ++++++++--------- .../OrganizationFundsMocks.ts | 322 +++----- src/utils/interfaces.ts | 37 +- 40 files changed, 3533 insertions(+), 3092 deletions(-) delete mode 100644 src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx create mode 100644 src/screens/OrganizationFundCampaign/CampaignDeleteModal.test.tsx create mode 100644 src/screens/OrganizationFundCampaign/CampaignModal.test.tsx create mode 100644 src/screens/OrganizationFundCampaign/CampaignModal.tsx delete mode 100644 src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx rename src/screens/OrganizationFundCampaign/{OrganizationFundCampaignMocks.tsx => OrganizationFundCampaignMocks.ts} (59%) delete mode 100644 src/screens/OrganizationFunds/FundCreateModal.tsx create mode 100644 src/screens/OrganizationFunds/FundDeleteModal.test.tsx create mode 100644 src/screens/OrganizationFunds/FundDeleteModal.tsx create mode 100644 src/screens/OrganizationFunds/FundModal.test.tsx create mode 100644 src/screens/OrganizationFunds/FundModal.tsx delete mode 100644 src/screens/OrganizationFunds/FundUpdateModal.tsx diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 74e4c38a23..d3f108c550 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -77,6 +77,7 @@ "manage": "Manage", "searchResultsFor": "Search results for {{text}}", "none": "None", + "sort": "Sort", "Donate": "Donate", "addedSuccessfully": "{{item}} added Successfully", "updatedSuccessfully": "{{item}} updated Successfully", diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index a2d2ea718d..892c54088c 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -361,33 +361,24 @@ "title": "Funds", "createFund": "Create Fund", "fundName": "Fund Name", - "fundId": "Fund ID", - "fundOptions": "Opitons", - "noFunds": "No Funds Found", - "fundDetails": "Fund Details", + "fundId": "Fund (Reference) ID", "taxDeductible": "Tax Deductible", - "enterfundName": "Enter Fund Name", - "enterfundId": "Enter Fund ID", - "default": "Default Fund", + "default": "Default", "archived": "Archived", - "nonArchive": "Non-Archived", "fundCreate": "Create Fund", "fundUpdate": "Update Fund", "fundDelete": "Delete Fund", - "manageFund": "Manage Fund", - "searchFullName": "Search By Name", "noFundsFound": "No Funds Found", "createdBy": "Created By", "createdOn": "Created On", "status": "Status", - "archiveFund": "Archive Fund", - "archiveFundMsg": "On Archiving this fund will remove it from the fund listing.Thisaction can be undone", "fundCreated": "Fund created successfully", "fundUpdated": "Fund updated successfully", "fundDeleted": "Fund deleted successfully", - "fundArchived": "Fund archived successfully", - "fundUnarchived": "Fund unarchived successfully", - "deleteFundMsg": "Do you want to remove this fund?" + "deleteFundMsg": "Are you sure you want to delete this fund?", + "createdLatest": "Created Latest", + "createdEarliest": "Created Earliest", + "viewCampaigns": "View Campaigns" }, "fundCampaign": { "title": "Fundraising Campaigns", @@ -400,17 +391,21 @@ "deletedCampaign": "Campaign deleted successfully", "deleteCampaignMsg": "Are you sure you want to delete this campaign?", "noCampaigns": "No Campaigns Found", - "createCampaign": "Create Fund Campaign", - "updateCampaign": "Update Fund Campaign", - "manageCampaign": "Manage Fund Campaign", - "deleteCampaign": "Delete Fund Campaign", + "createCampaign": "Create Campaign", + "updateCampaign": "Update Campaign", + "deleteCampaign": "Delete Campaign", "currency": "Currency", "selectCurrency": "Select Currency", - "searchFullName": "Search By Name" + "searchFullName": "Search By Name", + "viewPledges": "View Pledges", + "noCampaignsFound": "No Campaigns Found", + "latestEndDate": "Latest End Date", + "earliestEndDate": "Earliest End Date", + "lowestGoal": "Lowest Goal", + "highestGoal": "Highest Goal" }, "pledges": { "title": "Fund Campaign Pledges", - "volunteers": "Volunteers", "pledgeAmount": "Pledge Amount", "pledgeOptions": "Options", "pledgeCreated": "Pledge created successfully", @@ -426,12 +421,16 @@ "editPledge": "Edit Pledge", "deletePledgeMsg": "Are you sure you want to delete this pledge?", "noPledges": "No Pledges Found", - "sort": "Sort", - "searchVolunteer": "Search By Volunteer", + "searchPledger": "Search By Pledgers", "highestAmount": "Highest Amount", "lowestAmount": "Lowest Amount", "latestEndDate": "Latest End Date", - "earliestEndDate": "Earliest End Date" + "earliestEndDate": "Earliest End Date", + "campaigns": "Campaigns", + "pledges": "Pledges", + "endsOn": "Ends on", + "raisedAmount": "Raised amount ", + "pledgedAmount": "Pledged amount" }, "orgPost": { "title": "Posts", diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json index f338a3f661..250584da70 100644 --- a/public/locales/fr/common.json +++ b/public/locales/fr/common.json @@ -77,6 +77,7 @@ "manage": "Gérer", "searchResultsFor": "Résultats de recherche pour {{text}}", "none": "Aucun", + "sort": "Trier", "Donate": "Faire un don", "addedSuccessfully": "{{item}} ajouté avec succès", "updatedSuccessfully": "{{item}} mis à jour avec succès", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index d1683af4c8..942f1382c9 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -366,56 +366,52 @@ "title": "Fonds", "createFund": "Créer un fonds", "fundName": "Nom du fonds", - "fundId": "ID du fonds", - "fundOptions": "Options", - "noFunds": "Aucun fonds trouvé", - "fundDetails": "Détails du fonds", - "taxDeductible": "Déductible d'impôts", - "enterfundName": "Entrez le nom du fonds", - "enterfundId": "Entrez l'ID du fonds", - "default": "Fonds de défaut", + "fundId": "ID de référence du fonds", + "taxDeductible": "Déductible d'impôt", + "default": "Par défaut", "archived": "Archivé", - "nonArchive": "Non archivé", "fundCreate": "Créer un fonds", "fundUpdate": "Mettre à jour le fonds", "fundDelete": "Supprimer le fonds", - "manageFund": "Gérer le fonds", - "searchFullName": "Rechercher par nom", + "searchByName": "Rechercher par nom", "noFundsFound": "Aucun fonds trouvé", "createdBy": "Créé par", - "createdOn": "Créé sur", + "createdOn": "Créé le", "status": "Statut", - "archiveFund": "Fonds d'archives", - "archiveFundMsg": "Lors de l'archivage, ce fonds le supprimera de la liste des fonds. Cette action peut être annulée.", "fundCreated": "Fonds créé avec succès", "fundUpdated": "Fonds mis à jour avec succès", "fundDeleted": "Fonds supprimé avec succès", - "fundArchived": "Fonds archivé avec succès", - "fundUnarchived": "Fonds désarchivé avec succès", - "deleteFundMsg": "Voulez-vous supprimer ce fonds ?" + "deleteFundMsg": "Êtes-vous sûr de vouloir supprimer ce fonds ?", + "createdLatest": "Créé le plus récemment", + "createdEarliest": "Créé le plus tôt", + "viewCampaigns": "Voir les campagnes" }, "fundCampaign": { "title": "Campagnes de collecte de fonds", "campaignName": "Nom de la campagne", - "campaignOptions": "Possibilités", + "campaignOptions": "Options", "fundingGoal": "Objectif de financement", "addCampaign": "Ajouter une campagne", "createdCampaign": "Campagne créée avec succès", "updatedCampaign": "Campagne mise à jour avec succès", "deletedCampaign": "Campagne supprimée avec succès", - "deleteCampaignMsg": "Êtes-vous sûr de vouloir supprimer cette campagne ?", + "deleteCampaignMsg": "Êtes-vous sûr de vouloir supprimer cette campagne ?", "noCampaigns": "Aucune campagne trouvée", - "createCampaign": "Créer une campagne de financement", - "updateCampaign": "Mettre à jour la campagne de financement", - "manageCampaign": "Gérer la campagne de financement", - "deleteCampaign": "Supprimer la campagne de financement", + "createCampaign": "Créer une campagne", + "updateCampaign": "Mettre à jour la campagne", + "deleteCampaign": "Supprimer la campagne", "currency": "Devise", - "selectCurrency": "Sélectionnez la devise", - "searchFullName": "Rechercher par nom" + "selectCurrency": "Sélectionner la devise", + "searchFullName": "Rechercher par nom", + "viewPledges": "Voir les promesses de dons", + "noCampaignsFound": "Aucune campagne trouvée", + "latestEndDate": "Dernière date de fin", + "earliestEndDate": "Date de fin la plus ancienne", + "lowestGoal": "Objectif le plus bas", + "highestGoal": "Objectif le plus élevé" }, "pledges": { "title": "Engagements de campagne de financement", - "volunteers": "Bénévoles", "pledgeAmount": "Montant de la promesse de don", "pledgeOptions": "Possibilités", "pledgeCreated": "Engagement créé avec succès", @@ -431,12 +427,16 @@ "editPledge": "Modifier l'engagement", "deletePledgeMsg": "Etes-vous sûr de vouloir supprimer cet engagement ?", "noPledges": "Aucun engagement trouvé", - "sort": "Trier", - "searchVolunteer": "Rechercher par bénévole", + "searchPledger": "Rechercher par les bailleurs de fonds", "highestAmount": "Montant le plus élevé", "lowestAmount": "Montant le plus bas", "latestEndDate": "Date de fin la plus récente", - "earliestEndDate": "Date de fin la plus proche" + "earliestEndDate": "Date de fin la plus proche", + "campaigns": "Campagnes", + "pledges": "Promesses de dons", + "endsOn": "Se termine le", + "raisedAmount": "Montant collecté", + "pledgedAmount": "Montant promis" }, "orgPost": { "title": "Des postes", diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json index c6faba1094..3889f73e59 100644 --- a/public/locales/hi/common.json +++ b/public/locales/hi/common.json @@ -77,6 +77,7 @@ "manage": "प्रबंधित करें", "searchResultsFor": "{{text}} के लिए खोज परिणाम", "none": "कोई नहीं", + "sort": "क्रम से लगाना", "Donate": "दान करें", "addedSuccessfully": "{{item}} सफलतापूर्वक जोड़ा गया", "updatedSuccessfully": "{{item}} सफलतापूर्वक अपडेट किया गया", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 36e1dbefca..fa4d611b49 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -364,58 +364,54 @@ }, "funds": { "title": "फंड", - "createFund": "फंड बनाएं", + "createFund": "फंड बनाएँ", "fundName": "फंड का नाम", - "fundId": "फंड आईडी", - "fundOptions": "Opitons", - "noFunds": "कोई फंड नहीं मिला", - "fundDetails": "फंड विवरण", - "taxDeductible": "कर छूट", - "enterfundName": "फंड का नाम दर्ज करें", - "enterfundId": "फंड आईडी दर्ज करें", - "default": "डिफ़ॉल्ट फंड", - "archived": "संग्रहीत", - "nonArchive": "गैर-अभिलेखित", - "fundCreate": "फंड बनाएं", - "fundUpdate": "अद्यतन निधि", - "fundDelete": "फ़ंड हटाएँ", - "manageFund": "फंड का प्रबंधन करें", - "searchFullName": "नाम से खोजें", + "fundId": "फंड (संदर्भ) आईडी", + "taxDeductible": "कर ड्यूटी कटाई", + "default": "डिफ़ॉल्ट", + "archived": "आर्काइव", + "fundCreate": "फंड बनाएँ", + "fundUpdate": "फंड अपडेट करें", + "fundDelete": "फंड को हटाएँ", + "searchByName": "नाम से खोजें", "noFundsFound": "कोई फंड नहीं मिला", - "createdBy": "के द्वारा बनाई गई", - "createdOn": "पर बनाया", + "createdBy": "द्वारा बनाया गया", + "createdOn": "पर बनाया गया", "status": "स्थिति", - "archiveFund": "पुरालेख कोष", - "archiveFundMsg": "इस फंड को संग्रहित करने पर इसे फंड सूची से हटा दिया जाएगा। इस कार्रवाई को पूर्ववत किया जा सकता है", "fundCreated": "फंड सफलतापूर्वक बनाया गया", "fundUpdated": "फंड सफलतापूर्वक अपडेट किया गया", - "fundDeleted": "फंड सफलतापूर्वक हटा दिया गया", - "fundArchived": "निधि सफलतापूर्वक संग्रहित की गई", - "fundUnarchived": "फंड सफलतापूर्वक संग्रहित किया गया", - "deleteFundMsg": "क्या आप इस फंड को हटाना चाहते हैं?" + "fundDeleted": "फंड सफलतापूर्वक हटाया गया", + "deleteFundMsg": "क्या आप वाकई इस फंड को हटाना चाहते हैं?", + "createdLatest": "सबसे पहले बनाया", + "createdEarliest": "सबसे जल्दी बनाया", + "viewCampaigns": "कैम्पेंस देखें" }, "fundCampaign": { - "title": "धन उगाही अभियान", - "campaignName": "अभियान का नाम", + "title": "फंडरेजिंग कैंपेन", + "campaignName": "कैंपेन का नाम", "campaignOptions": "विकल्प", - "fundingGoal": "वित्त पोषण लक्ष्य", - "addCampaign": "अभियान जोड़ें", - "createdCampaign": "अभियान सफलतापूर्वक बनाया गया", - "updatedCampaign": "अभियान सफलतापूर्वक अपडेट किया गया", - "deletedCampaign": "अभियान सफलतापूर्वक हटा दिया गया", - "deleteCampaignMsg": "क्या आप वाकई इस अभियान को हटाना चाहते हैं?", - "noCampaigns": "कोई अभियान नहीं मिला", - "createCampaign": "फंड अभियान बनाएं", - "updateCampaign": "अद्यतन निधि अभियान", - "manageCampaign": "फंड अभियान प्रबंधित करें", - "deleteCampaign": "फ़ंड अभियान हटाएँ", + "fundingGoal": "फंडिंग उद्देश्य", + "addCampaign": "कैंपेन जोड़ें", + "createdCampaign": "कैंपेन सफलतापूर्वक बनाई गई", + "updatedCampaign": "कैंपेन सफलतापूर्वक अपडेट की गई", + "deletedCampaign": "कैंपेन सफलतापूर्वक हटा दी गई", + "deleteCampaignMsg": "क्या आप वाकई इस कैंपेन को हटाना चाहते हैं?", + "noCampaigns": "कोई कैंपेन नहीं मिली", + "createCampaign": "कैंपेन बनाएँ", + "updateCampaign": "कैंपेन अपडेट करें", + "deleteCampaign": "कैंपेन को हटाएं", "currency": "मुद्रा", - "selectCurrency": "मुद्रा चुनें", - "searchFullName": "नाम से खोजें" + "selectCurrency": "मुद्रा का चयन करें", + "searchFullName": "नाम से खोजें", + "viewPledges": "प्लेज देखें", + "noCampaignsFound": "कोई कैंपेन नहीं मिली", + "latestEndDate": "अंतिम समाप्ति तिथि", + "earliestEndDate": "सबसे पहली समाप्ति तिथि", + "lowestGoal": "सबसे कम उद्देश्य", + "highestGoal": "सबसे ऊंचा उद्देश्य" }, "pledges": { "title": "निधि अभियान प्रतिज्ञाएँ", - "volunteers": "स्वयंसेवकों", "pledgeAmount": "प्रतिज्ञा राशि", "pledgeOptions": "विकल्प", "pledgeCreated": "प्रतिज्ञा सफलतापूर्वक बनाई गई", @@ -431,12 +427,16 @@ "editPledge": "प्रतिज्ञा संपादित करें", "deletePledgeMsg": "क्या आप वाकई इस प्रतिज्ञा को हटाना चाहते हैं?", "noPledges": "कोई प्रतिज्ञा नहीं मिली", - "sort": "क्रमबद्ध करें", - "searchVolunteer": "स्वयंसेवक द्वारा खोजें", + "searchPledger": "प्लेजर्स के द्वारा खोजें", "highestAmount": "सबसे अधिक राशि", "lowestAmount": "सबसे कम राशि", "latestEndDate": "नवीनतम समाप्ति तिथि", - "earliestEndDate": "सबसे प्रारंभिक समाप्ति तिथि" + "earliestEndDate": "सबसे प्रारंभिक समाप्ति तिथि", + "campaigns": "अभियान", + "pledges": "प्रतिज्ञाएँ", + "endsOn": "पर समाप्त होता है", + "raisedAmount": "उठाया गया राशि", + "pledgedAmount": "प्रतिबद्ध राशि" }, "orgPost": { "title": "पदों", diff --git a/public/locales/sp/common.json b/public/locales/sp/common.json index 55d3e609fe..d784652da5 100644 --- a/public/locales/sp/common.json +++ b/public/locales/sp/common.json @@ -77,6 +77,7 @@ "manage": "Administrar", "searchResultsFor": "Resultados de búsqueda para {{text}}", "none": "Ninguno", + "sort": "Ordenar", "Donate": "Donar", "addedSuccessfully": "{{item}} agregado con éxito", "updatedSuccessfully": "{{item}} actualizado con éxito", diff --git a/public/locales/sp/translation.json b/public/locales/sp/translation.json index 2016ed2e4c..be9d518c0a 100644 --- a/public/locales/sp/translation.json +++ b/public/locales/sp/translation.json @@ -490,65 +490,53 @@ "funds": { "title": "Fondos", "createFund": "Crear fondo", - "fundName": "Nombre del Fondo", - "fundId": "ID del Fondo", - "fundOptions": "Opciones", - "noFunds": "No se encontraron fondos", - "fundDetails": "Detalles del Fondo", + "fundName": "Nombre del fondo", + "fundId": "ID de referencia del fondo", "taxDeductible": "Deducible de impuestos", - "enterfundName": "Ingrese el nombre del fondo", - "enterfundId": "Ingrese el ID del fondo", - "default": "Fondo predeterminado", + "default": "Predeterminado", "archived": "Archivado", - "nonArchive": "No archivado", - "fundCreate": "Crear Fondo", - "fundUpdate": "Actualizar Fondo", - "fundDelete": "Eliminar Fondo", - "no": "No", - "yes": "Sí", - "manageFund": "Administrar fondo", - "searchFullName": "Buscar por nombre", - "filter": "Filtrar", + "fundCreate": "Crear fondo", + "fundUpdate": "Actualizar fondo", + "fundDelete": "Eliminar fondo", + "searchByName": "Buscar por nombre", "noFundsFound": "No se encontraron fondos", "createdBy": "Creado por", - "createdOn": "Creado el", + "createdOn": "Creado en", "status": "Estado", - "archiveFund": "Archivar Fondo", - "archiveFundMsg": "¿Desea archivar este fondo?", - "fundCreated": "Fondo creado exitosamente", - "fundUpdated": "Fondo actualizado exitosamente", - "fundDeleted": "Fondo eliminado exitosamente", - "fundArchived": "Fondo archivado exitosamente", - "fundUnarchived": "Fondo no archivado exitosamente", - "deleteFundMsg": "¿Desea eliminar este fondo?" + "fundCreated": "Fondo creado correctamente", + "fundUpdated": "Fondo actualizado correctamente", + "fundDeleted": "Fondo eliminado correctamente", + "deleteFundMsg": "¿Está seguro de que desea eliminar este fondo?", + "createdLatest": "Creado más reciente", + "createdEarliest": "Creado más temprano", + "viewCampaigns": "Ver campañas" }, "fundCampaign": { "title": "Campañas de recaudación de fondos", "campaignName": "Nombre de la campaña", - "startDate": "Fecha de inicio", - "endDate": "Fecha de finalización", - "campaignOptions": "Opciones de la campaña", - "fundingGoal": "Meta de financiamiento", + "campaignOptions": "Opciones", + "fundingGoal": "Meta de financiación", "addCampaign": "Agregar campaña", - "createdCampaign": "Campaña creada exitosamente", - "updatedCampaign": "Campaña actualizada exitosamente", - "deletedCampaign": "Campaña eliminada exitosamente", - "deleteCampaignMsg": "¿Desea eliminar esta campaña?", + "createdCampaign": "Campaña creada correctamente", + "updatedCampaign": "Campaña actualizada correctamente", + "deletedCampaign": "Campaña eliminada correctamente", + "deleteCampaignMsg": "¿Está seguro de que desea eliminar esta campaña?", "noCampaigns": "No se encontraron campañas", - "createCampaign": "Crear campaña de recaudación de fondos", - "updateCampaign": "Actualizar campaña de recaudación de fondos", - "manageCampaign": "Administrar campaña de recaudación de fondos", - "deleteCampaign": "Eliminar campaña de recaudación de fondos", - "no": "No", - "yes": "Sí", + "createCampaign": "Crear campaña", + "updateCampaign": "Actualizar campaña", + "deleteCampaign": "Eliminar campaña", "currency": "Moneda", "selectCurrency": "Seleccionar moneda", - "filter": "Filtrar", - "searchFullName": "Buscar por nombre" + "searchFullName": "Buscar por nombre", + "viewPledges": "Ver compromisos", + "noCampaignsFound": "No se encontraron campañas", + "latestEndDate": "Fecha de finalización más reciente", + "earliestEndDate": "Fecha de finalización más temprana", + "lowestGoal": "Meta más baja", + "highestGoal": "Meta más alta" }, "pledges": { "title": "Compromisos de Campaña de Financiamiento", - "volunteers": "Voluntarios", "startDate": "Fecha de Inicio", "endDate": "Fecha de Finalización", "pledgeAmount": "Monto del Compromiso", @@ -565,15 +553,17 @@ "amount": "Monto", "editPledge": "Editar Compromiso", "deletePledgeMsg": "¿Estás seguro de que quieres eliminar este compromiso?", - "no": "No", - "yes": "Sí", "noPledges": "No se encontraron compromisos", - "sort": "Ordenar", - "searchVolunteer": "Buscar por voluntario", + "searchPledger": "Buscar por compromisos", "highestAmount": "Cantidad más alta", "lowestAmount": "Cantidad más baja", "latestEndDate": "Fecha de finalización más reciente", - "earliestEndDate": "Fecha de finalización más cercana" + "earliestEndDate": "Fecha de finalización más cercana", + "campaigns": "Campañas", + "pledges": "Compromisos", + "endsOn": "Finaliza el", + "raisedAmount": "Monto recaudado", + "pledgedAmount": "Monto comprometido" }, "orgPost": { diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 9e4596f272..71b697b354 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -75,8 +75,9 @@ "profile": "轮廓", "noFiltersApplied": "未应用筛选器", "manage": "管理", - "searchResultsFor": "搜索结果:{{text}}", + "searchResultsFor": "搜索结果", "none": "没有", + "sort": "种类", "Donate": "捐赠", "addedSuccessfully": "{{item}} 添加成功", "updatedSuccessfully": "{{item}} 更新成功", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 3f44aeae9c..8cd40d13f8 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -363,59 +363,55 @@ "occurences": "事件" }, "funds": { - "title": "资金", + "title": "基金", "createFund": "创建基金", "fundName": "基金名称", - "fundId": "基金编号", - "fundOptions": "奥皮通斯", - "noFunds": "未找到资金", - "fundDetails": "基金详情", - "taxDeductible": "免税额", - "enterfundName": "输入基金名称", - "enterfundId": "输入基金ID", - "default": "违约基金", + "fundId": "基金(参考)ID", + "taxDeductible": "税前扣除", + "default": "默认", "archived": "已归档", - "nonArchive": "非存档", "fundCreate": "创建基金", "fundUpdate": "更新基金", "fundDelete": "删除基金", - "manageFund": "管理基金", - "searchFullName": "按名称搜索", - "noFundsFound": "未找到资金", - "createdBy": "由...制作", + "searchByName": "按名称搜索", + "noFundsFound": "未找到基金", + "createdBy": "由...创建", "createdOn": "创建于", - "status": "地位", - "archiveFund": "档案基金", - "archiveFundMsg": "存档后,该基金将从基金列表中删除。此操作可以撤消", + "status": "状态", "fundCreated": "基金创建成功", "fundUpdated": "基金更新成功", - "fundDeleted": "资金删除成功", - "fundArchived": "资金归档成功", - "fundUnarchived": "基金已成功解档", - "deleteFundMsg": "您想删除该基金吗?" + "fundDeleted": "基金删除成功", + "deleteFundMsg": "您确定要删除此基金吗?", + "createdLatest": "最近创建", + "createdEarliest": "最早创建", + "viewCampaigns": "查看活动" }, "fundCampaign": { - "title": "筹款活动", + "title": "募捐活动", "campaignName": "活动名称", "campaignOptions": "选项", - "fundingGoal": "资助目标", + "fundingGoal": "资金目标", "addCampaign": "添加活动", "createdCampaign": "活动创建成功", "updatedCampaign": "活动更新成功", - "deletedCampaign": "营销活动删除成功", - "deleteCampaignMsg": "您确定要删除此广告活动吗?", + "deletedCampaign": "活动删除成功", + "deleteCampaignMsg": "您确定要删除此活动吗?", "noCampaigns": "未找到活动", - "createCampaign": "创建基金活动", - "updateCampaign": "更新基金活动", - "manageCampaign": "管理基金活动", - "deleteCampaign": "删除基金活动", + "createCampaign": "创建活动", + "updateCampaign": "更新活动", + "deleteCampaign": "删除活动", "currency": "货币", "selectCurrency": "选择货币", - "searchFullName": "按名称搜索" + "searchFullName": "按名称搜索", + "viewPledges": "查看承诺", + "noCampaignsFound": "未找到活动", + "latestEndDate": "最新结束日期", + "earliestEndDate": "最早结束日期", + "lowestGoal": "最低目标", + "highestGoal": "最高目标" }, "pledges": { "title": "基金活动承诺", - "volunteers": "志愿者", "pledgeAmount": "质押金额", "pledgeOptions": "选项", "pledgeCreated": "质押创建成功", @@ -431,12 +427,16 @@ "editPledge": "编辑承诺", "deletePledgeMsg": "您确定要删除此承诺吗?", "noPledges": "未找到承诺", - "sort": "排序", - "searchVolunteer": "按志愿者搜索", + "searchPledger": "按承诺搜索", "highestAmount": "最高金额", "lowestAmount": "最低金额", "latestEndDate": "最新结束日期", - "earliestEndDate": "最早结束日期" + "earliestEndDate": "最早结束日期", + "campaigns": "活动", + "pledges": "承诺", + "endsOn": "结束于", + "raisedAmount": "募集金額", + "pledgedAmount": "承诺金額" }, "orgPost": { "title": "帖子", diff --git a/src/GraphQl/Queries/fundQueries.ts b/src/GraphQl/Queries/fundQueries.ts index 3648060d2e..daec99a8c9 100644 --- a/src/GraphQl/Queries/fundQueries.ts +++ b/src/GraphQl/Queries/fundQueries.ts @@ -9,10 +9,15 @@ import gql from 'graphql-tag'; * @returns The list of members associated with the organization. */ export const FUND_LIST = gql` - query FundsByOrganization($organizationId: ID!, $filter: String) { + query FundsByOrganization( + $organizationId: ID! + $filter: String + $orderBy: FundOrderByInput + ) { fundsByOrganization( organizationId: $organizationId where: { name_contains: $filter } + orderBy: $orderBy ) { _id name @@ -32,8 +37,12 @@ export const FUND_LIST = gql` `; export const FUND_CAMPAIGN = gql` - query GetFundById($id: ID!) { - getFundById(id: $id) { + query GetFundById( + $id: ID! + $where: CampaignWhereInput + $orderBy: CampaignOrderByInput + ) { + getFundById(id: $id, where: $where, orderBy: $orderBy) { campaigns { _id endDate @@ -49,6 +58,9 @@ export const FUND_CAMPAIGN = gql` export const FUND_CAMPAIGN_PLEDGE = gql` query GetFundraisingCampaignById($id: ID!, $orderBy: PledgeOrderByInput) { getFundraisingCampaignById(id: $id, orderBy: $orderBy) { + name + fundingGoal + currency startDate endDate pledges { diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.module.css b/src/screens/FundCampaignPledge/FundCampaignPledge.module.css index 0e9f3187b7..c46adfada3 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.module.css +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.module.css @@ -58,7 +58,8 @@ .btnsContainer { display: flex; - margin: 2.5rem 0 2.5rem 0; + gap: 0.8rem; + margin: 2.2rem 0 0.8rem 0; } .btnsContainer .input { @@ -111,7 +112,7 @@ height: 26px; } -.volunteerContainer { +.pledgerContainer { display: flex; align-items: center; justify-content: center; @@ -125,3 +126,134 @@ .noOutline input { outline: none; } + +.overviewContainer { + display: flex; + gap: 7rem; + width: 100%; + justify-content: space-between; + margin: 1.5rem 0 0 0; + padding: 1.25rem 2rem; + background-color: rgba(255, 255, 255, 0.591); + + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; + border-radius: 0.5rem; +} + +.titleContainer { + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.titleContainer h3 { + font-size: 1.75rem; + font-weight: 750; + color: #5e5e5e; + margin-top: 0.2rem; +} + +.titleContainer span { + font-size: 0.9rem; + margin-left: 0.5rem; + font-weight: lighter; + color: #707070; +} + +.raisedAmount { + display: flex; + justify-content: center; + align-items: center; + font-size: 1.25rem; + font-weight: 750; + color: #5e5e5e; +} + +.progressContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; + flex-grow: 1; +} + +.progress { + margin-top: 0.2rem; + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.endpoints { + display: flex; + position: relative; + font-size: 0.85rem; +} + +.start { + position: absolute; + top: 0px; +} + +.end { + position: absolute; + top: 0px; + right: 0px; +} + +.moreContainer { + display: flex; + align-items: center; +} + +.moreContainer:hover { + text-decoration: underline; + cursor: pointer; +} + +.popup { + z-index: 50; + border-radius: 0.5rem; + font-family: sans-serif; + font-weight: 500; + font-size: 0.875rem; + margin-top: 0.5rem; + padding: 0.75rem; + border: 1px solid #e2e8f0; + background-color: white; + color: #1e293b; + box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 0.15); + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.popupExtra { + max-height: 15rem; + overflow-y: auto; +} + +.toggleGroup { + width: 50%; + min-width: 27.75rem; + margin: 0.5rem 0rem; +} + +.toggleBtn { + padding: 0rem; + height: 30px; + display: flex; + justify-content: center; + align-items: center; +} + +.toggleBtn:hover { + color: #31bb6b !important; +} + +input[type='radio']:checked + label { + background-color: #31bb6a50 !important; +} + +input[type='radio']:checked + label:hover { + color: black !important; +} diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx index 7c61a1b48a..25e049e19d 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.test.tsx @@ -92,7 +92,7 @@ describe('Testing Campaign Pledge Screen', () => { it('should render the Campaign Pledge screen', async () => { renderFundCampaignPledge(link1); await waitFor(() => { - expect(screen.getByTestId('searchVolunteer')).toBeInTheDocument(); + expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); }); }); @@ -172,8 +172,8 @@ describe('Testing Campaign Pledge Screen', () => { it('Search the Pledges list by Users', async () => { renderFundCampaignPledge(link1); - const searchVolunteer = await screen.findByTestId('searchVolunteer'); - fireEvent.change(searchVolunteer, { + const searchPledger = await screen.findByTestId('searchPledger'); + fireEvent.change(searchPledger, { target: { value: 'John' }, }); @@ -197,13 +197,84 @@ describe('Testing Campaign Pledge Screen', () => { ); }); + it('check if user image renders', async () => { + renderFundCampaignPledge(link1); + await waitFor(() => { + expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); + }); + + const image = await screen.findByTestId('image1'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'img-url'); + }); + + it('should render extraUserDetails in Popup', async () => { + renderFundCampaignPledge(link1); + await waitFor(() => { + expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); + }); + + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('John Doe2')).toBeInTheDocument(); + expect(screen.queryByText('John Doe3')).toBeNull(); + expect(screen.queryByText('John Doe4')).toBeNull(); + + const moreContainer = await screen.findAllByTestId('moreContainer'); + userEvent.click(moreContainer[0]); + + await waitFor(() => { + expect(screen.getByText('John Doe3')).toBeInTheDocument(); + expect(screen.getByText('John Doe4')).toBeInTheDocument(); + expect(screen.getByTestId('extra1')).toBeInTheDocument(); + expect(screen.getByTestId('extra2')).toBeInTheDocument(); + expect(screen.getByTestId('extraAvatar8')).toBeInTheDocument(); + const image = screen.getByTestId('extraImage1'); + expect(image).toBeInTheDocument(); + expect(image).toHaveAttribute('src', 'img-url3'); + }); + + userEvent.click(moreContainer[0]); + await waitFor(() => { + expect(screen.queryByText('John Doe3')).toBeNull(); + expect(screen.queryByText('John Doe4')).toBeNull(); + }); + }); + + it('should render Progress Bar with Raised amount (CONSTANT) & Pledged Amount', async () => { + renderFundCampaignPledge(link1); + await waitFor(() => { + expect(screen.getByTestId('searchPledger')).toBeInTheDocument(); + }); + const raised = screen.getByText('Raised amount'); + const pledged = screen.getByText('Pledged amount'); + expect(pledged).toBeInTheDocument(); + expect(raised).toBeInTheDocument(); + + userEvent.click(raised); + + await waitFor(() => { + expect(screen.getByTestId('progressBar')).toBeInTheDocument(); + expect(screen.getByTestId('progressBar')).toHaveTextContent('$0'); + }); + + userEvent.click(pledged); + + await waitFor(() => { + expect(screen.getByTestId('progressBar')).toBeInTheDocument(); + expect(screen.getByTestId('progressBar')).toHaveTextContent('$300'); + }); + }); + it('Sort the Pledges list by Lowest Amount', async () => { renderFundCampaignPledge(link1); - const searchVolunteer = await screen.findByTestId('searchVolunteer'); - expect(searchVolunteer).toBeInTheDocument(); + const searchPledger = await screen.findByTestId('searchPledger'); + expect(searchPledger).toBeInTheDocument(); fireEvent.click(screen.getByTestId('filter')); + await waitFor(() => { + expect(screen.getByTestId('amount_ASC')).toBeInTheDocument(); + }); fireEvent.click(screen.getByTestId('amount_ASC')); await waitFor(() => { @@ -219,10 +290,13 @@ describe('Testing Campaign Pledge Screen', () => { it('Sort the Pledges list by Highest Amount', async () => { renderFundCampaignPledge(link1); - const searchVolunteer = await screen.findByTestId('searchVolunteer'); - expect(searchVolunteer).toBeInTheDocument(); + const searchPledger = await screen.findByTestId('searchPledger'); + expect(searchPledger).toBeInTheDocument(); fireEvent.click(screen.getByTestId('filter')); + await waitFor(() => { + expect(screen.getByTestId('amount_DESC')).toBeInTheDocument(); + }); fireEvent.click(screen.getByTestId('amount_DESC')); await waitFor(() => { @@ -238,10 +312,13 @@ describe('Testing Campaign Pledge Screen', () => { it('Sort the Pledges list by latest endDate', async () => { renderFundCampaignPledge(link1); - const searchVolunteer = await screen.findByTestId('searchVolunteer'); - expect(searchVolunteer).toBeInTheDocument(); + const searchPledger = await screen.findByTestId('searchPledger'); + expect(searchPledger).toBeInTheDocument(); fireEvent.click(screen.getByTestId('filter')); + await waitFor(() => { + expect(screen.getByTestId('endDate_DESC')).toBeInTheDocument(); + }); fireEvent.click(screen.getByTestId('endDate_DESC')); await waitFor(() => { @@ -257,10 +334,13 @@ describe('Testing Campaign Pledge Screen', () => { it('Sort the Pledges list by earliest endDate', async () => { renderFundCampaignPledge(link1); - const searchVolunteer = await screen.findByTestId('searchVolunteer'); - expect(searchVolunteer).toBeInTheDocument(); + const searchPledger = await screen.findByTestId('searchPledger'); + expect(searchPledger).toBeInTheDocument(); fireEvent.click(screen.getByTestId('filter')); + await waitFor(() => { + expect(screen.getByTestId('endDate_ASC')).toBeInTheDocument(); + }); fireEvent.click(screen.getByTestId('endDate_ASC')); await waitFor(() => { @@ -272,15 +352,4 @@ describe('Testing Campaign Pledge Screen', () => { expect(screen.getAllByTestId('amountCell')[0]).toHaveTextContent('200'); }); }); - - it('check if user image renders', async () => { - renderFundCampaignPledge(link1); - await waitFor(() => { - expect(screen.getByTestId('searchVolunteer')).toBeInTheDocument(); - }); - - const image = await screen.findByAltText('volunteer'); - expect(image).toBeInTheDocument(); - expect(image).toHaveAttribute('src', 'img-url'); - }); }); diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx index 376476f0ca..a6d3cf2f31 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx @@ -2,6 +2,7 @@ import { useQuery, type ApolloQueryResult } from '@apollo/client'; import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; import { FUND_CAMPAIGN_PLEDGE } from 'GraphQl/Queries/fundQueries'; import Loader from 'components/Loader/Loader'; +import { Unstable_Popup as BasePopup } from '@mui/base/Unstable_Popup'; import dayjs from 'dayjs'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Button, Dropdown, Form } from 'react-bootstrap'; @@ -11,32 +12,51 @@ import { currencySymbols } from 'utils/currency'; import styles from './FundCampaignPledge.module.css'; import PledgeDeleteModal from './PledgeDeleteModal'; import PledgeModal from './PledgeModal'; -import { Stack } from '@mui/material'; +import { Breadcrumbs, Link, Stack, Typography } from '@mui/material'; import { DataGrid } from '@mui/x-data-grid'; import Avatar from 'components/Avatar/Avatar'; import type { GridCellParams, GridColDef } from '@mui/x-data-grid'; import type { InterfacePledgeInfo, - InterfacePledgeVolunteer, + InterfacePledger, InterfaceQueryFundCampaignsPledges, } from 'utils/interfaces'; +import ProgressBar from 'react-bootstrap/ProgressBar'; -/** - * Enum representing the different modals used in the component. - */ -enum Modal { +interface InterfaceCampaignInfo { + name: string; + goal: number; + startDate: Date; + endDate: Date; + currency: string; +} + +enum ModalState { SAME = 'same', DELETE = 'delete', } -/** - * Component to manage and display pledges for a specific fundraising campaign. - * - * Handles pledge display, sorting, searching, and modals for editing and deleting pledges. - * Fetches data from GraphQL and provides user interactions for managing pledges. - * - * @returns JSX.Element - The rendered component. - */ +const dataGridStyle = { + '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { + outline: 'none !important', + }, + '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', + }, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-root': { + borderRadius: '0.5rem', + }, + '& .MuiDataGrid-main': { + borderRadius: '0.5rem', + }, +}; + const fundCampaignPledge = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'pledges', @@ -48,11 +68,28 @@ const fundCampaignPledge = (): JSX.Element => { return ; } - const [modalState, setModalState] = useState<{ [key in Modal]: boolean }>({ - [Modal.SAME]: false, - [Modal.DELETE]: false, + const [campaignInfo, setCampaignInfo] = useState({ + name: '', + goal: 0, + startDate: new Date(), + endDate: new Date(), + currency: '', }); + const [modalState, setModalState] = useState<{ + [key in ModalState]: boolean; + }>({ + [ModalState.SAME]: false, + [ModalState.DELETE]: false, + }); + + const [anchor, setAnchor] = useState(null); + const [extraUsers, setExtraUsers] = useState([]); + const [progressIndicator, setProgressIndicator] = useState< + 'raised' | 'pledged' + >('pledged'); + const open = Boolean(anchor); + const id = open ? 'simple-popup' : undefined; const [pledgeModalMode, setPledgeModalMode] = useState<'edit' | 'create'>( 'create', ); @@ -91,68 +128,69 @@ const fundCampaignPledge = (): JSX.Element => { 'YYYY-MM-DD', ).toDate(); - const pledges = useMemo(() => { - return ( + const { pledges, totalPledged } = useMemo(() => { + let totalPledged = 0; + const pledges = pledgeData?.getFundraisingCampaignById.pledges.filter((pledge) => { + totalPledged += pledge.amount; const search = searchTerm.toLowerCase(); return pledge.users.some((user) => { const fullName = `${user.firstName} ${user.lastName}`; return fullName.toLowerCase().includes(search); }); - }) ?? [] - ); + }) ?? []; + return { pledges, totalPledged }; }, [pledgeData, searchTerm]); + useEffect(() => { + if (pledgeData) { + setCampaignInfo({ + name: pledgeData.getFundraisingCampaignById.name, + goal: pledgeData.getFundraisingCampaignById.fundingGoal, + startDate: pledgeData.getFundraisingCampaignById.startDate, + endDate: pledgeData.getFundraisingCampaignById.endDate, + currency: pledgeData.getFundraisingCampaignById.currency, + }); + } + }, [pledgeData]); + useEffect(() => { refetchPledge(); }, [sortBy, refetchPledge]); - /** - * Opens the specified modal. - * - * @param modal - The modal to open. - */ - const openModal = (modal: Modal): void => { + const openModal = (modal: ModalState): void => { setModalState((prevState) => ({ ...prevState, [modal]: true })); }; - /** - * Closes the specified modal. - * - * @param modal - The modal to close. - */ - const closeModal = (modal: Modal): void => { + const closeModal = (modal: ModalState): void => { setModalState((prevState) => ({ ...prevState, [modal]: false })); }; - /** - * Handles opening the pledge modal for editing or creating a pledge. - * - * @param pledge - The pledge to edit, or null if creating a new pledge. - * @param mode - The mode of the modal ('edit' or 'create'). - */ const handleOpenModal = useCallback( (pledge: InterfacePledgeInfo | null, mode: 'edit' | 'create'): void => { setPledge(pledge); setPledgeModalMode(mode); - openModal(Modal.SAME); + openModal(ModalState.SAME); }, [openModal], ); - /** - * Handles opening the delete modal for a specific pledge. - * - * @param pledge - The pledge to delete. - */ const handleDeleteClick = useCallback( (pledge: InterfacePledgeInfo): void => { setPledge(pledge); - openModal(Modal.DELETE); + openModal(ModalState.DELETE); }, [openModal], ); + const handleClick = ( + event: React.MouseEvent, + users: InterfacePledger[], + ): void => { + setExtraUsers(users); + setAnchor(anchor ? null : event.currentTarget); + }; + if (pledgeLoading) return ; if (pledgeError) { return ( @@ -171,9 +209,9 @@ const fundCampaignPledge = (): JSX.Element => { const columns: GridColDef[] = [ { - field: 'volunteers', - headerName: 'Volunteers', - flex: 2, + field: 'pledgers', + headerName: 'Pledgers', + flex: 3, minWidth: 50, align: 'left', headerAlign: 'center', @@ -182,13 +220,15 @@ const fundCampaignPledge = (): JSX.Element => { renderCell: (params: GridCellParams) => { return (
    - {params.row.users.map( - (user: InterfacePledgeVolunteer, index: number) => ( -
    + {params.row.users + .slice(0, 2) + .map((user: InterfacePledger, index: number) => ( +
    {user.image ? ( volunteer ) : ( @@ -205,7 +245,16 @@ const fundCampaignPledge = (): JSX.Element => { {user.firstName + ' ' + user.lastName}
    - ), + ))} + {params.row.users.length > 2 && ( +
    handleClick(e, params.row.users.slice(2))} + > + +{params.row.users.length - 2} more... +
    )}
    ); @@ -239,7 +288,7 @@ const fundCampaignPledge = (): JSX.Element => { }, { field: 'amount', - headerName: 'Pledge Amount', + headerName: 'Pledged', flex: 1, minWidth: 100, align: 'center', @@ -262,6 +311,31 @@ const fundCampaignPledge = (): JSX.Element => { ); }, }, + { + field: 'donated', + headerName: 'Donated', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( +
    + { + currencySymbols[ + params.row.currency as keyof typeof currencySymbols + ] + } + 0 +
    + ); + }, + }, { field: 'action', headerName: 'Action', @@ -277,7 +351,7 @@ const fundCampaignPledge = (): JSX.Element => {
    - row._id} components={{ NoRowsOverlay: () => ( @@ -392,20 +551,7 @@ const fundCampaignPledge = (): JSX.Element => { ), }} - sx={{ - '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { - outline: 'none !important', - }, - '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { - outline: 'none', - }, - '& .MuiDataGrid-row:hover': { - backgroundColor: 'transparent', - }, - '& .MuiDataGrid-row.Mui-hovered': { - backgroundColor: 'transparent', - }, - }} + sx={dataGridStyle} getRowClassName={() => `${styles.rowBackground}`} autoHeight rowHeight={65} @@ -420,11 +566,10 @@ const fundCampaignPledge = (): JSX.Element => { columns={columns} isRowSelectable={() => false} /> - - {/* Update Pledge Modal */} + {/* Update Pledge ModalState */} closeModal(Modal.SAME)} + isOpen={modalState[ModalState.SAME]} + hide={() => closeModal(ModalState.SAME)} campaignId={fundCampaignId} orgId={orgId} pledge={pledge} @@ -432,14 +577,50 @@ const fundCampaignPledge = (): JSX.Element => { endDate={pledgeData?.getFundraisingCampaignById.endDate as Date} mode={pledgeModalMode} /> - - {/* Delete Pledge Modal */} + {/* Delete Pledge ModalState */} closeModal(Modal.DELETE)} + isOpen={modalState[ModalState.DELETE]} + hide={() => closeModal(ModalState.DELETE)} pledge={pledge} refetchPledge={refetchPledge} /> + 4 ? styles.popupExtra : ''}`} + > + {extraUsers.map((user: InterfacePledger, index: number) => ( +
    + {user.image ? ( + pledger + ) : ( +
    + +
    + )} + + {user.firstName + ' ' + user.lastName} + +
    + ))} +
    ); }; diff --git a/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx b/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx index c9d5c5f50e..dbaae76504 100644 --- a/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx +++ b/src/screens/FundCampaignPledge/PledgeDeleteModal.test.tsx @@ -23,14 +23,6 @@ jest.mock('react-toastify', () => ({ }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { - return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, - }; -}); - const link = new StaticMockLink(MOCKS); const link2 = new StaticMockLink(MOCKS_DELETE_PLEDGE_ERROR); const translations = JSON.parse( @@ -57,6 +49,7 @@ const pledgeProps: InterfaceDeletePledgeModal = { }, refetchPledge: jest.fn(), }; + const renderPledgeDeleteModal = ( link: ApolloLink, props: InterfaceDeletePledgeModal, diff --git a/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx b/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx index d85faa714e..a16d0df905 100644 --- a/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx +++ b/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx @@ -7,25 +7,12 @@ import { DELETE_PLEDGE } from 'GraphQl/Mutations/PledgeMutation'; import type { InterfacePledgeInfo } from 'utils/interfaces'; import { toast } from 'react-toastify'; -/** - * Props for the PledgeDeleteModal component. - */ export interface InterfaceDeletePledgeModal { isOpen: boolean; hide: () => void; pledge: InterfacePledgeInfo | null; refetchPledge: () => void; } - -/** - * Modal component for confirming the deletion of a pledge. - * - * Allows users to confirm or cancel the deletion of a pledge. - * Triggers a mutation to delete the pledge and refetches the pledge data upon success. - * - * @param props - The props for the component. - * @returns The rendered component. - */ const PledgeDeleteModal: React.FC = ({ isOpen, hide, @@ -39,12 +26,6 @@ const PledgeDeleteModal: React.FC = ({ const [deletePledge] = useMutation(DELETE_PLEDGE); - /** - * Handler for deleting the pledge. - * - * Executes the delete mutation and refetches the pledge data. - * Shows a success or error toast based on the result of the mutation. - */ const deleteHandler = async (): Promise => { try { await deletePledge({ @@ -93,5 +74,4 @@ const PledgeDeleteModal: React.FC = ({ ); }; - export default PledgeDeleteModal; diff --git a/src/screens/FundCampaignPledge/PledgeModal.test.tsx b/src/screens/FundCampaignPledge/PledgeModal.test.tsx index cd1e417f0a..49c0447992 100644 --- a/src/screens/FundCampaignPledge/PledgeModal.test.tsx +++ b/src/screens/FundCampaignPledge/PledgeModal.test.tsx @@ -131,7 +131,7 @@ describe('PledgeModal', () => { await waitFor(() => expect(screen.getByText(translations.editPledge)).toBeInTheDocument(), ); - expect(screen.getByTestId('volunteerSelect')).toHaveTextContent('John Doe'); + expect(screen.getByTestId('pledgerSelect')).toHaveTextContent('John Doe'); expect(screen.getByLabelText('Start Date')).toHaveValue('01/01/2024'); expect(screen.getByLabelText('End Date')).toHaveValue('10/01/2024'); expect(screen.getByLabelText('Currency')).toHaveTextContent('USD ($)'); diff --git a/src/screens/FundCampaignPledge/PledgeModal.tsx b/src/screens/FundCampaignPledge/PledgeModal.tsx index 46afdf64e6..3e22d604a0 100644 --- a/src/screens/FundCampaignPledge/PledgeModal.tsx +++ b/src/screens/FundCampaignPledge/PledgeModal.tsx @@ -6,7 +6,7 @@ import { currencyOptions, currencySymbols } from 'utils/currency'; import type { InterfaceCreatePledge, InterfacePledgeInfo, - InterfacePledgeVolunteer, + InterfacePledger, } from 'utils/interfaces'; import styles from './FundCampaignPledge.module.css'; import React, { useCallback, useEffect, useState } from 'react'; @@ -25,9 +25,6 @@ import { import { MEMBERS_LIST } from 'GraphQl/Queries/Queries'; -/** - * Props for the PledgeModal component. - */ export interface InterfacePledgeModal { isOpen: boolean; hide: () => void; @@ -38,16 +35,6 @@ export interface InterfacePledgeModal { endDate: Date; mode: 'create' | 'edit'; } - -/** - * Modal component for creating or editing a pledge. - * - * This component provides a form for creating a new pledge or editing an existing one. - * It includes fields for pledge amount, currency, start and end dates, and selected volunteers. - * - * @param props - The props for the component. - * @returns The rendered component. - */ const PledgeModal: React.FC = ({ isOpen, hide, @@ -70,8 +57,7 @@ const PledgeModal: React.FC = ({ pledgeEndDate: new Date(pledge?.endDate ?? new Date()), pledgeStartDate: new Date(pledge?.startDate ?? new Date()), }); - - const [volunteers, setVolunteers] = useState([]); + const [pledgers, setPledgers] = useState([]); const [updatePledge] = useMutation(UPDATE_PLEDGE); const [createPledge] = useMutation(CREATE_PlEDGE); @@ -92,7 +78,7 @@ const PledgeModal: React.FC = ({ useEffect(() => { if (memberData) { /*istanbul ignore next*/ - setVolunteers(memberData.organizations[0].members); + setPledgers(memberData.organizations[0].members); } }, [memberData]); @@ -104,14 +90,6 @@ const PledgeModal: React.FC = ({ pledgeEndDate, } = formState; - /** - * Handler for updating an existing pledge. - * - * Executes the update mutation if any changes are detected in the form fields. - * Shows a success or error toast based on the result of the mutation. - * - * @param e - The form submission event. - */ /*istanbul ignore next*/ const updatePledgeHandler = useCallback( async (e: ChangeEvent): Promise => { @@ -122,7 +100,7 @@ const PledgeModal: React.FC = ({ const updatedFields: { [key: string]: number | string | string[] | undefined; } = {}; - // Checks if there are changes to the pledge and adds them to the updatedFields object + // checks if there are changes to the pledge and adds them to the updatedFields object if (pledgeAmount !== pledge?.amount) { updatedFields.amount = pledgeAmount; } @@ -155,15 +133,7 @@ const PledgeModal: React.FC = ({ [formState, pledge], ); - /** - * Handler for creating a new pledge. - * - * Executes the create mutation with the form data. - * Shows a success or error toast based on the result of the mutation. - * Resets the form state upon success. - * - * @param e - The form submission event. - */ + // Function to create a new pledge const createPledgeHandler = useCallback( async (e: ChangeEvent): Promise => { try { @@ -220,31 +190,31 @@ const PledgeModal: React.FC = ({ } className="p-3" > - {/* A Multi-select dropdown enables admin to select more than one volunteer for participating in a pledge */} + {/* A Multi-select dropdown enables admin to select more than one pledger for participating in a pledge */} option._id === value._id} filterSelectedOptions={true} - getOptionLabel={(member: InterfacePledgeVolunteer): string => + getOptionLabel={(member: InterfacePledger): string => `${member.firstName} ${member.lastName}` } onChange={ /*istanbul ignore next*/ - (_, newVolunteers): void => { + (_, newPledgers): void => { setFormState({ ...formState, - pledgeUsers: newVolunteers, + pledgeUsers: newPledgers, }); } } renderInput={(params) => ( - + )} /> diff --git a/src/screens/FundCampaignPledge/PledgesMocks.ts b/src/screens/FundCampaignPledge/PledgesMocks.ts index cf07d1cf28..71d74b1805 100644 --- a/src/screens/FundCampaignPledge/PledgesMocks.ts +++ b/src/screens/FundCampaignPledge/PledgesMocks.ts @@ -18,6 +18,9 @@ export const MOCKS = [ result: { data: { getFundraisingCampaignById: { + name: 'Campaign Name', + fundingGoal: 1000, + currency: 'USD', startDate: '2024-01-01', endDate: '2024-08-08', pledges: [ @@ -34,6 +37,60 @@ export const MOCKS = [ lastName: 'Doe', image: 'img-url', }, + { + _id: '2', + firstName: 'John', + lastName: 'Doe2', + image: 'img-url2', + }, + { + _id: '3', + firstName: 'John', + lastName: 'Doe3', + image: 'img-url3', + }, + { + _id: '4', + firstName: 'John', + lastName: 'Doe4', + image: 'img-url4', + }, + { + _id: '5', + firstName: 'John', + lastName: 'Doe5', + image: 'img-url5', + }, + { + _id: '6', + firstName: 'John', + lastName: 'Doe6', + image: 'img-url6', + }, + { + _id: '7', + firstName: 'John', + lastName: 'Doe7', + image: 'img-url7', + }, + { + _id: '8', + firstName: 'John', + lastName: 'Doe8', + image: 'img-url8', + }, + { + _id: '9', + firstName: 'John', + lastName: 'Doe9', + image: 'img-url9', + }, + { + _id: '10', + firstName: 'John', + lastName: 'Doe10', + image: null, + }, ], }, { @@ -67,6 +124,9 @@ export const MOCKS = [ result: { data: { getFundraisingCampaignById: { + name: 'Campaign Name', + fundingGoal: 1000, + currency: 'USD', startDate: '2024-01-01', endDate: '2024-08-08', pledges: [ @@ -116,6 +176,9 @@ export const MOCKS = [ result: { data: { getFundraisingCampaignById: { + name: 'Campaign Name', + fundingGoal: 1000, + currency: 'USD', startDate: '2024-01-01', endDate: '2024-08-08', pledges: [ @@ -165,6 +228,9 @@ export const MOCKS = [ result: { data: { getFundraisingCampaignById: { + name: 'Campaign Name', + fundingGoal: 1000, + currency: 'USD', startDate: '2024-01-01', endDate: '2024-08-08', pledges: [ @@ -422,6 +488,9 @@ export const EMPTY_MOCKS = [ result: { data: { getFundraisingCampaignById: { + name: 'Campaign Name', + fundingGoal: 1000, + currency: 'USD', startDate: '2024-01-01', endDate: '2024-01-01', pledges: [], diff --git a/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx b/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx deleted file mode 100644 index 492f1866d9..0000000000 --- a/src/screens/OrganizationFundCampaign/CampaignCreateModal.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { DatePicker } from '@mui/x-date-pickers'; -import type { Dayjs } from 'dayjs'; -import dayjs from 'dayjs'; -import type { ChangeEvent } from 'react'; -import React from 'react'; -import { Button, Col, Form, Modal } from 'react-bootstrap'; -import { currencyOptions } from 'utils/currency'; -import type { InterfaceCreateCampaign } from 'utils/interfaces'; -import styles from './OrganizationFundCampaign.module.css'; - -/** - * InterfaceCampaignCreateModal is an object containing the props for CampaignCreateModal component - */ -interface InterfaceCampaignCreateModal { - campaignCreateModalIsOpen: boolean; - hideCreateCampaignModal: () => void; - formState: InterfaceCreateCampaign; - setFormState: (state: React.SetStateAction) => void; - createCampaignHandler: (e: ChangeEvent) => Promise; - t: (key: string) => string; -} - -/** - * CampaignCreateModal component is used to create the campaign - * @param campaignCreateModalIsOpen - boolean value to check if the modal is open or not - * @param hideCreateCampaignModal - function to hide the modal - * @param formState - object containing the form state - * @param setFormState - function to set the form state - * @param createCampaignHandler - function to create the campaign - * @param t - i18n function to translate the text - * @returns returns the CampaignCreateModal component - */ -const CampaignCreateModal: React.FC = ({ - campaignCreateModalIsOpen, - hideCreateCampaignModal, - formState, - setFormState, - createCampaignHandler, - t, -}) => { - return ( - <> - - -

    {t('createCampaign')}

    - -
    - - - - {t('campaignName')} - - setFormState({ - ...formState, - campaignName: e.target.value, - }) - } - /> - - -
    - { - if (date) { - setFormState({ - ...formState, - campaignStartDate: date.toDate(), - campaignEndDate: - formState.campaignEndDate && - (formState.campaignEndDate < date?.toDate() - ? date.toDate() - : formState.campaignEndDate), - }); - } - }} - minDate={dayjs(new Date())} - /> -
    -
    - { - if (date) { - setFormState({ - ...formState, - campaignEndDate: date.toDate(), - }); - } - }} - minDate={dayjs(formState.campaignStartDate)} - /> -
    -
    - - - {t('currency')} - - { - setFormState({ - ...formState, - campaignCurrency: e.target.value, - }); - }} - > - - {currencyOptions.map((currency) => ( - - ))} - - - - - {t('fundingGoal')} - { - if (parseInt(e.target.value) > 0) { - setFormState({ - ...formState, - campaignGoal: parseInt(e.target.value), - }); - } - }} - /> - - - - -
    -
    - - ); -}; -export default CampaignCreateModal; diff --git a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.test.tsx b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.test.tsx new file mode 100644 index 0000000000..7baf9bd933 --- /dev/null +++ b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import { MOCKS, MOCK_ERROR } from './OrganizationFundCampaignMocks'; +import CampaignDeleteModal, { + type InterfaceDeleteCampaignModal, +} from './CampaignDeleteModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCK_ERROR); +const translations = JSON.parse( + JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.fundCampaign), +); + +const campaignProps: InterfaceDeleteCampaignModal = { + isOpen: true, + hide: jest.fn(), + campaign: { + _id: 'campaignId1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: new Date('2021-01-01'), + endDate: new Date('2024-01-01'), + currency: 'USD', + createdAt: '2021-01-01', + }, + refetchCampaign: jest.fn(), +}; +const renderFundDeleteModal = ( + link: ApolloLink, + props: InterfaceDeleteCampaignModal, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('CampaignDeleteModal', () => { + it('should render CampaignDeleteModal', () => { + renderFundDeleteModal(link1, campaignProps); + expect(screen.getByTestId('deleteCampaignCloseBtn')).toBeInTheDocument(); + }); + + it('should successfully Delete Campaign', async () => { + renderFundDeleteModal(link1, campaignProps); + expect(screen.getByTestId('deleteCampaignCloseBtn')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(campaignProps.refetchCampaign).toHaveBeenCalled(); + expect(campaignProps.hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(translations.deletedCampaign); + }); + }); + + it('should fail to Delete Campaign', async () => { + renderFundDeleteModal(link2, campaignProps); + expect(screen.getByTestId('deleteCampaignCloseBtn')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); +}); diff --git a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx index a4df9d1a80..840c4869bb 100644 --- a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx @@ -1,42 +1,53 @@ import React from 'react'; import { Button, Modal } from 'react-bootstrap'; import styles from './OrganizationFundCampaign.module.css'; +import { useMutation } from '@apollo/client'; +import { DELETE_CAMPAIGN_MUTATION } from 'GraphQl/Mutations/CampaignMutation'; +import type { InterfaceCampaignInfo } from 'utils/interfaces'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; -/** - * InterfaceDeleteCampaignModal is an object containing the props for CampaignDeleteModal component - */ -interface InterfaceDeleteCampaignModal { - campaignDeleteModalIsOpen: boolean; - hideDeleteCampaignModal: () => void; - deleteCampaignHandler: () => Promise; - t: (key: string) => string; - tCommon: (key: string) => string; +export interface InterfaceDeleteCampaignModal { + isOpen: boolean; + hide: () => void; + campaign: InterfaceCampaignInfo | null; + refetchCampaign: () => void; } - -/** - * CampaignDeleteModal component is used to delete the campaign - * @param campaignDeleteModalIsOpen - boolean value to check if the modal is open or not - * @param hideDeleteCampaignModal - function to hide the modal - * @param deleteCampaignHandler - function to delete the campaign - * @param t - i18n function to translate the text - * @param tCommon - i18n function to translate the common text - * @returns returns the CampaignDeleteModal component - */ const CampaignDeleteModal: React.FC = ({ - campaignDeleteModalIsOpen, - hideDeleteCampaignModal, - deleteCampaignHandler, - t, - tCommon, + isOpen, + hide, + campaign, + refetchCampaign, }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'fundCampaign', + }); + const { t: tCommon } = useTranslation('common'); + + const [deleteCampaign] = useMutation(DELETE_CAMPAIGN_MUTATION); + + const deleteCampaignHandler = async (): Promise => { + try { + await deleteCampaign({ + variables: { + id: campaign?._id, + }, + }); + toast.success(t('deletedCampaign')); + refetchCampaign(); + hide(); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; return ( <> - +

    {t('deleteCampaign')}

    - diff --git a/src/screens/OrganizationFundCampaign/CampaignModal.test.tsx b/src/screens/OrganizationFundCampaign/CampaignModal.test.tsx new file mode 100644 index 0000000000..cdf631f87c --- /dev/null +++ b/src/screens/OrganizationFundCampaign/CampaignModal.test.tsx @@ -0,0 +1,266 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import { MOCKS, MOCK_ERROR } from './OrganizationFundCampaignMocks'; +import type { InterfaceCampaignModal } from './CampaignModal'; +import CampaignModal from './CampaignModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('@mui/x-date-pickers/DateTimePicker', () => { + return { + DateTimePicker: jest.requireActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ).DesktopDateTimePicker, + }; +}); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCK_ERROR); +const translations = JSON.parse( + JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.fundCampaign), +); + +const campaignProps: InterfaceCampaignModal[] = [ + { + isOpen: true, + hide: jest.fn(), + fundId: 'fundId', + campaign: { + _id: 'campaignId1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: new Date('2021-01-01'), + endDate: new Date('2024-01-01'), + currency: 'USD', + createdAt: '2021-01-01', + }, + refetchCampaign: jest.fn(), + mode: 'create', + }, + { + isOpen: true, + hide: jest.fn(), + fundId: 'fundId', + campaign: { + _id: 'campaignId1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: new Date('2021-01-01'), + endDate: new Date('2024-01-01'), + currency: 'USD', + createdAt: '2021-01-01', + }, + refetchCampaign: jest.fn(), + mode: 'edit', + }, +]; +const renderCampaignModal = ( + link: ApolloLink, + props: InterfaceCampaignModal, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('CampaignModal', () => { + afterEach(() => { + jest.clearAllMocks(); + cleanup(); + }); + + it('should populate form fields with correct values in edit mode', async () => { + renderCampaignModal(link1, campaignProps[1]); + await waitFor(() => + expect(screen.getAllByText(translations.updateCampaign)).toHaveLength(2), + ); + + expect(screen.getByLabelText(translations.campaignName)).toHaveValue( + 'Campaign 1', + ); + expect(screen.getByLabelText('Start Date')).toHaveValue('01/01/2021'); + expect(screen.getByLabelText('End Date')).toHaveValue('01/01/2024'); + expect(screen.getByLabelText(translations.currency)).toHaveTextContent( + 'USD ($)', + ); + expect(screen.getByLabelText(translations.fundingGoal)).toHaveValue('100'); + }); + + it('should update fundingGoal when input value changes', async () => { + renderCampaignModal(link1, campaignProps[1]); + const goalInput = screen.getByLabelText(translations.fundingGoal); + expect(goalInput).toHaveValue('100'); + fireEvent.change(goalInput, { target: { value: '200' } }); + expect(goalInput).toHaveValue('200'); + }); + + it('should not update fundingGoal when input value is less than or equal to 0', async () => { + renderCampaignModal(link1, campaignProps[1]); + const goalInput = screen.getByLabelText(translations.fundingGoal); + expect(goalInput).toHaveValue('100'); + fireEvent.change(goalInput, { target: { value: '-10' } }); + expect(goalInput).toHaveValue('100'); + }); + + it('should update Start Date when a new date is selected', async () => { + renderCampaignModal(link1, campaignProps[1]); + const startDateInput = screen.getByLabelText('Start Date'); + fireEvent.change(startDateInput, { target: { value: '02/01/2024' } }); + expect(startDateInput).toHaveValue('02/01/2024'); + }); + + it('Start Date onChange when its null', async () => { + renderCampaignModal(link1, campaignProps[1]); + const startDateInput = screen.getByLabelText('Start Date'); + expect(startDateInput).toHaveValue('01/01/2021'); + fireEvent.change(startDateInput, { target: { value: null } }); + expect(startDateInput).toHaveValue(''); + }); + + it('should update End Date when a new date is selected', async () => { + renderCampaignModal(link1, campaignProps[1]); + const endDateInput = screen.getByLabelText('End Date'); + fireEvent.change(endDateInput, { target: { value: '02/01/2024' } }); + expect(endDateInput).toHaveValue('02/01/2024'); + }); + + it('End Date onChange when its null', async () => { + renderCampaignModal(link1, campaignProps[1]); + const endDateInput = screen.getByLabelText('End Date'); + fireEvent.change(endDateInput, { target: { value: null } }); + expect(endDateInput).toHaveValue(''); + }); + + it('should create campaign', async () => { + renderCampaignModal(link1, campaignProps[0]); + + const campaignName = screen.getByLabelText(translations.campaignName); + fireEvent.change(campaignName, { target: { value: 'Campaign 2' } }); + + const startDate = screen.getByLabelText('Start Date'); + fireEvent.change(startDate, { target: { value: '02/01/2024' } }); + + const endDate = screen.getByLabelText('End Date'); + fireEvent.change(endDate, { target: { value: '02/02/2024' } }); + + const fundingGoal = screen.getByLabelText(translations.fundingGoal); + fireEvent.change(fundingGoal, { target: { value: '200' } }); + + const submitBtn = screen.getByTestId('submitCampaignBtn'); + expect(submitBtn).toBeInTheDocument(); + fireEvent.click(submitBtn); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(translations.createdCampaign); + expect(campaignProps[0].refetchCampaign).toHaveBeenCalled(); + expect(campaignProps[0].hide).toHaveBeenCalled(); + }); + }); + + it('should update campaign', async () => { + renderCampaignModal(link1, campaignProps[1]); + + const campaignName = screen.getByLabelText(translations.campaignName); + fireEvent.change(campaignName, { target: { value: 'Campaign 4' } }); + + const startDate = screen.getByLabelText('Start Date'); + fireEvent.change(startDate, { target: { value: '02/01/2023' } }); + + const endDate = screen.getByLabelText('End Date'); + fireEvent.change(endDate, { target: { value: '02/02/2023' } }); + + const fundingGoal = screen.getByLabelText(translations.fundingGoal); + fireEvent.change(fundingGoal, { target: { value: '400' } }); + + const submitBtn = screen.getByTestId('submitCampaignBtn'); + expect(submitBtn).toBeInTheDocument(); + + fireEvent.click(submitBtn); + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(translations.updatedCampaign); + expect(campaignProps[1].refetchCampaign).toHaveBeenCalled(); + expect(campaignProps[1].hide).toHaveBeenCalled(); + }); + }); + + it('Error: should create campaign', async () => { + renderCampaignModal(link2, campaignProps[0]); + + const campaignName = screen.getByLabelText(translations.campaignName); + fireEvent.change(campaignName, { target: { value: 'Campaign 2' } }); + + const startDate = screen.getByLabelText('Start Date'); + fireEvent.change(startDate, { target: { value: '02/01/2024' } }); + + const endDate = screen.getByLabelText('End Date'); + fireEvent.change(endDate, { target: { value: '02/02/2024' } }); + + const fundingGoal = screen.getByLabelText(translations.fundingGoal); + fireEvent.change(fundingGoal, { target: { value: '200' } }); + + const submitBtn = screen.getByTestId('submitCampaignBtn'); + expect(submitBtn).toBeInTheDocument(); + fireEvent.click(submitBtn); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); + + it('Error: should update campaign', async () => { + renderCampaignModal(link2, campaignProps[1]); + + const campaignName = screen.getByLabelText(translations.campaignName); + fireEvent.change(campaignName, { target: { value: 'Campaign 4' } }); + + const startDate = screen.getByLabelText('Start Date'); + fireEvent.change(startDate, { target: { value: '02/01/2023' } }); + + const endDate = screen.getByLabelText('End Date'); + fireEvent.change(endDate, { target: { value: '02/02/2023' } }); + + const fundingGoal = screen.getByLabelText(translations.fundingGoal); + fireEvent.change(fundingGoal, { target: { value: '400' } }); + + const submitBtn = screen.getByTestId('submitCampaignBtn'); + expect(submitBtn).toBeInTheDocument(); + + fireEvent.click(submitBtn); + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); +}); diff --git a/src/screens/OrganizationFundCampaign/CampaignModal.tsx b/src/screens/OrganizationFundCampaign/CampaignModal.tsx new file mode 100644 index 0000000000..dfe0738c07 --- /dev/null +++ b/src/screens/OrganizationFundCampaign/CampaignModal.tsx @@ -0,0 +1,291 @@ +import { DatePicker } from '@mui/x-date-pickers'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import type { ChangeEvent } from 'react'; +import React, { useEffect, useState } from 'react'; +import { Button, Col, Form, Modal } from 'react-bootstrap'; +import { currencyOptions, currencySymbols } from 'utils/currency'; +import styles from './OrganizationFundCampaign.module.css'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from '@apollo/client'; +import { + CREATE_CAMPAIGN_MUTATION, + UPDATE_CAMPAIGN_MUTATION, +} from 'GraphQl/Mutations/CampaignMutation'; +import { toast } from 'react-toastify'; +import { + FormControl, + InputLabel, + MenuItem, + Select, + TextField, +} from '@mui/material'; +import type { InterfaceCampaignInfo } from 'utils/interfaces'; + +export interface InterfaceCampaignModal { + isOpen: boolean; + hide: () => void; + fundId: string; + campaign: InterfaceCampaignInfo | null; + refetchCampaign: () => void; + mode: 'create' | 'edit'; +} + +const CampaignModal: React.FC = ({ + isOpen, + hide, + fundId, + refetchCampaign, + mode, + campaign, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'fundCampaign', + }); + const { t: tCommon } = useTranslation('common'); + + const [formState, setFormState] = useState({ + campaignName: campaign?.name ?? '', + campaignCurrency: campaign?.currency ?? 'USD', + campaignGoal: campaign?.fundingGoal ?? 0, + campaignStartDate: campaign?.startDate ?? new Date(), + campaignEndDate: campaign?.endDate ?? new Date(), + }); + + useEffect(() => { + setFormState({ + campaignCurrency: campaign?.currency ?? 'USD', + campaignEndDate: campaign?.endDate ?? new Date(), + campaignGoal: campaign?.fundingGoal ?? 0, + campaignName: campaign?.name ?? '', + campaignStartDate: campaign?.startDate ?? new Date(), + }); + }, [campaign]); + + const { + campaignName, + campaignCurrency, + campaignEndDate, + campaignGoal, + campaignStartDate, + } = formState; + + const [createCampaign] = useMutation(CREATE_CAMPAIGN_MUTATION); + const [updateCampaign] = useMutation(UPDATE_CAMPAIGN_MUTATION); + + const createCampaignHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + try { + await createCampaign({ + variables: { + name: formState.campaignName, + currency: formState.campaignCurrency, + fundingGoal: formState.campaignGoal, + startDate: dayjs(formState.campaignStartDate).format('YYYY-MM-DD'), + endDate: dayjs(formState.campaignEndDate).format('YYYY-MM-DD'), + fundId, + }, + }); + toast.success(t('createdCampaign')); + setFormState({ + campaignName: '', + campaignCurrency: 'USD', + campaignGoal: 0, + campaignStartDate: new Date(), + campaignEndDate: new Date(), + }); + refetchCampaign(); + hide(); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + /*istanbul ignore next*/ + const updateCampaignHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + try { + const updatedFields: { [key: string]: string | number | undefined } = {}; + if (campaign?.name !== campaignName) { + updatedFields.name = campaignName; + } + if (campaign?.currency !== campaignCurrency) { + updatedFields.currency = campaignCurrency; + } + if (campaign?.fundingGoal !== campaignGoal) { + updatedFields.fundingGoal = campaignGoal; + } + if (campaign?.startDate !== campaignStartDate) { + updatedFields.startDate = dayjs(campaignStartDate).format('YYYY-MM-DD'); + } + if (campaign?.endDate !== formState.campaignEndDate) { + updatedFields.endDate = dayjs(formState.campaignEndDate).format( + 'YYYY-MM-DD', + ); + } + await updateCampaign({ + variables: { + id: campaign?._id, + ...updatedFields, + }, + }); + setFormState({ + campaignName: '', + campaignCurrency: 'USD', + campaignGoal: 0, + campaignStartDate: new Date(), + campaignEndDate: new Date(), + }); + refetchCampaign(); + hide(); + toast.success(t('updatedCampaign')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + return ( + <> + + +

    + {t(mode === 'edit' ? 'updateCampaign' : 'createCampaign')} +

    + +
    + +
    + + + + setFormState({ + ...formState, + campaignName: e.target.value, + }) + } + /> + + + + + {/* Date Calendar Component to select start date of campaign*/} + { + if (date) { + setFormState({ + ...formState, + campaignStartDate: date.toDate(), + campaignEndDate: + campaignEndDate && + (campaignEndDate < date?.toDate() + ? date.toDate() + : campaignEndDate), + }); + } + }} + minDate={dayjs(new Date())} + /> + {/* Date Calendar Component to select end Date of campaign */} + { + if (date) { + setFormState({ + ...formState, + campaignEndDate: date.toDate(), + }); + } + }} + minDate={dayjs(campaignStartDate)} + /> + + + + {/* Dropdown to select the currency for funding goal of the campaign*/} + + + {t('currency')} + + + + {/* Input field to enter funding goal for the campaign */} + + { + if (parseInt(e.target.value) > 0) { + setFormState({ + ...formState, + campaignGoal: parseInt(e.target.value), + }); + } + }} + /> + + + {/* Button to create the campaign */} + +
    +
    +
    + + ); +}; +export default CampaignModal; diff --git a/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx b/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx deleted file mode 100644 index 16076df76a..0000000000 --- a/src/screens/OrganizationFundCampaign/CampaignUpdateModal.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { DatePicker } from '@mui/x-date-pickers'; -import type { Dayjs } from 'dayjs'; -import dayjs from 'dayjs'; -import type { ChangeEvent } from 'react'; -import React from 'react'; -import { Button, Col, Form, Modal } from 'react-bootstrap'; -import { currencyOptions } from 'utils/currency'; -import type { InterfaceCreateCampaign } from 'utils/interfaces'; -import styles from './OrganizationFundCampaign.module.css'; - -/** - * InterfaceCampaignUpdateModal is an object containing the props for CampaignUpdateModal component - */ -interface InterfaceCampaignUpdateModal { - campaignUpdateModalIsOpen: boolean; - hideUpdateCampaignModal: () => void; - formState: InterfaceCreateCampaign; - setFormState: (state: React.SetStateAction) => void; - updateCampaignHandler: (e: ChangeEvent) => Promise; - t: (key: string) => string; - showDeleteCampaignModal: () => void; -} - -/** - * CampaignUpdateModal component is used to update the campaign - * @param campaignUpdateModalIsOpen - boolean value to check if the modal is open or not - * @param hideUpdateCampaignModal - function to hide the modal - * @param formState - object containing the form state - * @param setFormState - function to set the form state - * @param updateCampaignHandler - function to update the campaign - * @param t - i18n function to translate the text - * @param showDeleteCampaignModal - function to show the delete campaign modal - * @returns returns the CampaignUpdateModal component - */ -const CampaignUpdateModal: React.FC = ({ - campaignUpdateModalIsOpen, - hideUpdateCampaignModal, - formState, - setFormState, - updateCampaignHandler, - t, - showDeleteCampaignModal, -}) => { - return ( - <> - - -

    {t('manageCampaign')}

    - -
    - -
    - - {t('campaignName')} - - setFormState({ - ...formState, - campaignName: e.target.value, - }) - } - /> - - -
    - { - if (date) { - setFormState({ - ...formState, - campaignStartDate: date.toDate(), - campaignEndDate: - formState.campaignEndDate && - (formState.campaignEndDate < date?.toDate() - ? date.toDate() - : formState.campaignEndDate), - }); - } - }} - minDate={dayjs(new Date())} - /> -
    -
    - { - if (date) { - setFormState({ - ...formState, - campaignEndDate: date.toDate(), - }); - } - }} - minDate={dayjs(formState.campaignStartDate)} - /> -
    -
    - - - {t('currency')} - - { - setFormState({ - ...formState, - campaignCurrency: e.target.value, - }); - }} - > - - {currencyOptions.map((currency) => ( - - ))} - - - - - {t('fundingGoal')} - { - if (parseInt(e.target.value) > 0) { - setFormState({ - ...formState, - campaignGoal: parseInt(e.target.value), - }); - } - }} - /> - - -
    - - -
    -
    -
    -
    - - ); -}; -export default CampaignUpdateModal; diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx b/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx index 24542d7e19..5b615392c4 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx @@ -1,57 +1,52 @@ -/*eslint-disable*/ -import { useMutation, useQuery } from '@apollo/client'; -import { Search, WarningAmberRounded } from '@mui/icons-material'; +import { useQuery } from '@apollo/client'; +import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Stack, Typography, Breadcrumbs, Link } from '@mui/material'; import { - CREATE_CAMPAIGN_MUTATION, - DELETE_CAMPAIGN_MUTATION, - UPDATE_CAMPAIGN_MUTATION, -} from 'GraphQl/Mutations/CampaignMutation'; -import { FUND_CAMPAIGN } from 'GraphQl/Queries/fundQueries'; -import Loader from 'components/Loader/Loader'; -import dayjs from 'dayjs'; -import { useState, type ChangeEvent } from 'react'; -import { Button, Col, Row, Dropdown, Form } from 'react-bootstrap'; + DataGrid, + type GridCellParams, + type GridColDef, +} from '@mui/x-data-grid'; +import { Button, Dropdown, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import React, { useCallback, useMemo, useState } from 'react'; +import dayjs from 'dayjs'; +import Loader from 'components/Loader/Loader'; +import CampaignModal from './CampaignModal'; +import CampaignDeleteModal from './CampaignDeleteModal'; +import { FUND_CAMPAIGN } from 'GraphQl/Queries/fundQueries'; +import styles from './OrganizationFundCampaign.module.css'; import { currencySymbols } from 'utils/currency'; -import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined'; import type { InterfaceCampaignInfo, - InterfaceCreateCampaign, InterfaceQueryOrganizationFundCampaigns, } from 'utils/interfaces'; -import CampaignCreateModal from './CampaignCreateModal'; -import CampaignDeleteModal from './CampaignDeleteModal'; -import CampaignUpdateModal from './CampaignUpdateModal'; -import styles from './OrganizationFundCampaign.module.css'; -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - styled, - tableCellClasses, -} from '@mui/material'; -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: ['#31bb6b', '!important'], - color: theme.palette.common.white, +const dataGridStyle = { + '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { + outline: 'none !important', }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14, + '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', }, -})); - -const StyledTableRow = styled(TableRow)(() => ({ - '&:last-child td, &:last-child th': { - border: 0, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-root': { + borderRadius: '0.5rem', }, -})); + '& .MuiDataGrid-main': { + borderRadius: '0.5rem', + }, +}; + +enum ModalState { + SAME = 'same', + DELETE = 'delete', +} const orgFundCampaign = (): JSX.Element => { const { t } = useTranslation('translation', { @@ -60,197 +55,84 @@ const orgFundCampaign = (): JSX.Element => { const { t: tCommon } = useTranslation('common'); const navigate = useNavigate(); - const { fundId: currentUrl, orgId: orgId } = useParams(); - const [campaignCreateModalIsOpen, setcampaignCreateModalIsOpen] = - useState(false); - const [campaignUpdateModalIsOpen, setcampaignUpdateModalIsOpen] = - useState(false); - const [campaignDeleteModalIsOpen, setcampaignDeleteModalIsOpen] = - useState(false); + const { fundId, orgId } = useParams(); + + if (!fundId || !orgId) { + return ; + } const [campaign, setCampaign] = useState(null); - const [formState, setFormState] = useState({ - campaignName: '', - campaignCurrency: 'USD', - campaignGoal: 0, - campaignStartDate: new Date(), - campaignEndDate: new Date(), + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState(null); + + const [modalState, setModalState] = useState<{ + [key in ModalState]: boolean; + }>({ + [ModalState.SAME]: false, + [ModalState.DELETE]: false, }); + const [campaignModalMode, setCampaignModalMode] = useState<'edit' | 'create'>( + 'create', + ); + const openModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: true })); + + const closeModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: false })); + + const handleOpenModal = useCallback( + (campaign: InterfaceCampaignInfo | null, mode: 'edit' | 'create'): void => { + setCampaign(campaign); + setCampaignModalMode(mode); + openModal(ModalState.SAME); + }, + [openModal], + ); + + const handleDeleteClick = useCallback( + (campaign: InterfaceCampaignInfo): void => { + setCampaign(campaign); + openModal(ModalState.DELETE); + }, + [openModal], + ); + const { - data: fundCampaignData, - loading: fundCampaignLoading, - error: fundCampaignError, - refetch: refetchFundCampaign, + data: campaignData, + loading: campaignLoading, + error: campaignError, + refetch: refetchCampaign, }: { data?: { getFundById: InterfaceQueryOrganizationFundCampaigns; }; loading: boolean; error?: Error | undefined; - refetch: any; + refetch: () => void; } = useQuery(FUND_CAMPAIGN, { variables: { - id: currentUrl, + id: fundId, + orderBy: sortBy, + where: { + name_contains: searchTerm, + }, }, }); - const [createCampaign] = useMutation(CREATE_CAMPAIGN_MUTATION); - const [updateCampaign] = useMutation(UPDATE_CAMPAIGN_MUTATION); - const [deleteCampaign] = useMutation(DELETE_CAMPAIGN_MUTATION); - - const showCreateCampaignModal = (): void => { - setcampaignCreateModalIsOpen(!campaignCreateModalIsOpen); - }; - const hideCreateCampaignModal = (): void => { - setcampaignCreateModalIsOpen(!campaignCreateModalIsOpen); - }; - const showUpdateCampaignModal = (): void => { - setcampaignUpdateModalIsOpen(!campaignUpdateModalIsOpen); - }; - const hideUpdateCampaignModal = (): void => { - setcampaignUpdateModalIsOpen(!campaignUpdateModalIsOpen); - setFormState({ - campaignName: '', - campaignCurrency: 'USD', - campaignGoal: 0, - campaignStartDate: new Date(), - campaignEndDate: new Date(), - }); - }; - const showDeleteCampaignModal = (): void => { - setcampaignDeleteModalIsOpen(!campaignDeleteModalIsOpen); - }; - const hideDeleteCampaignModal = (): void => { - setcampaignDeleteModalIsOpen(!campaignDeleteModalIsOpen); - }; - - const handleEditClick = (campaign: InterfaceCampaignInfo): void => { - setFormState({ - campaignName: campaign.name, - campaignCurrency: campaign.currency, - campaignGoal: campaign.fundingGoal, - campaignStartDate: new Date(campaign.startDate), - campaignEndDate: new Date(campaign.endDate), - }); - setCampaign(campaign); - showUpdateCampaignModal(); - }; - - const createCampaignHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createCampaign({ - variables: { - name: formState.campaignName, - currency: formState.campaignCurrency, - fundingGoal: formState.campaignGoal, - startDate: dayjs(formState.campaignStartDate).format('YYYY-MM-DD'), - endDate: dayjs(formState.campaignEndDate).format('YYYY-MM-DD'), - fundId: currentUrl, - }, - }); - toast.success(t('createdCampaign')); - setFormState({ - campaignName: '', - campaignCurrency: 'USD', - campaignGoal: 0, - campaignStartDate: new Date(), - campaignEndDate: new Date(), - }); - refetchFundCampaign(); - hideCreateCampaignModal(); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - const updateCampaignHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - const updatedFields: { [key: string]: any } = {}; - if (campaign?.name !== formState.campaignName) { - updatedFields.name = formState.campaignName; - } - if (campaign?.currency !== formState.campaignCurrency) { - updatedFields.currency = formState.campaignCurrency; - } - if (campaign?.fundingGoal !== formState.campaignGoal) { - updatedFields.fundingGoal = formState.campaignGoal; - } - if (campaign?.startDate !== formState.campaignStartDate) { - updatedFields.startDate = dayjs(formState.campaignStartDate).format( - 'YYYY-MM-DD', - ); - } - if (campaign?.endDate !== formState.campaignEndDate) { - updatedFields.endDate = dayjs(formState.campaignEndDate).format( - 'YYYY-MM-DD', - ); - } - await updateCampaign({ - variables: { - id: campaign?._id, - ...updatedFields, - }, - }); - setFormState({ - campaignName: '', - campaignCurrency: 'USD', - campaignGoal: 0, - campaignStartDate: new Date(), - campaignEndDate: new Date(), - }); - refetchFundCampaign(); - hideUpdateCampaignModal(); - toast.success(t('updatedCampaign')); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - const deleteCampaignHandler = async (): Promise => { - try { - await deleteCampaign({ - variables: { - id: campaign?._id, - }, - }); - toast.success(t('deletedCampaign')); - refetchFundCampaign(); - hideDeleteCampaignModal(); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - const handleClick = (campaignId: String) => { + const handleClick = (campaignId: string): void => { navigate(`/fundCampaignPledge/${orgId}/${campaignId}`); }; - const [searchQuery, setSearchQuery] = useState(''); - const [searchText, setSearchText] = useState(''); - const filteredCampaigns = fundCampaignData?.getFundById.campaigns.filter( - (campaign) => - campaign.name.toLowerCase().includes(searchQuery.toLowerCase()), - ); + const campaigns = useMemo(() => { + if (campaignData?.getFundById?.campaigns) + return campaignData.getFundById.campaigns; + return []; + }, [campaignData]); - if (fundCampaignLoading) { + if (campaignLoading) { return ; } - if (fundCampaignError) { + if (campaignError) { return (
    @@ -258,32 +140,199 @@ const orgFundCampaign = (): JSX.Element => {
    Error occured while loading Campaigns
    - {fundCampaignError.message} + {campaignError.message}
    ); } + const columns: GridColDef[] = [ + { + field: 'id', + headerName: 'Sr. No.', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return
    {params.row.id}
    ; + }, + }, + { + field: 'campaignName', + headerName: 'Campaign Name', + flex: 2, + minWidth: 150, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( +
    handleClick(params.row.campaign._id as string)} + > + {params.row.campaign.name} +
    + ); + }, + }, + { + field: 'startDate', + headerName: 'Start Date', + flex: 1, + minWidth: 150, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return dayjs(params.row.campaign.startDate).format('DD/MM/YYYY'); + }, + }, + { + field: 'endDate', + headerName: 'End Date', + minWidth: 150, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + flex: 1, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( +
    + {dayjs(params.row.campaign.endDate).format('DD/MM/YYYY')}{' '} +
    + ); + }, + }, + { + field: 'fundingGoal', + headerName: 'Funding Goal', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( +
    + { + currencySymbols[ + params.row.campaign.currency as keyof typeof currencySymbols + ] + } + {params.row.campaign.fundingGoal} +
    + ); + }, + }, + { + field: 'action', + headerName: 'Action', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( + <> + + + + ); + }, + }, + { + field: 'assocPledge', + headerName: 'Associated Pledges', + flex: 2, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return ( + + ); + }, + }, + ]; + return (
    + + navigate(`/orgfunds/${orgId}`)} + > + {tCommon('Funds')} + + FundRaising Campaign + +
    { - setSearchText(e.target.value); - }} + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} data-testid="searchFullName" />
    -
    - {filteredCampaigns && filteredCampaigns.length > 0 ? ( -
    - - - - - # - - {t('campaignName')} - - - {tCommon('startDate')} - - - {tCommon('endDate')} - - - {t('fundingGoal')} - - - {t('campaignOptions')} - - - - - {filteredCampaigns.map((campaign, index) => ( - - - {index + 1} - - handleClick(campaign._id)} - > - - {campaign.name} - - - - {dayjs(campaign.startDate).format('DD/MM/YYYY')} - - - {dayjs(campaign.endDate).format('DD/MM/YYYY')} - - - - {`${currencySymbols[campaign.currency as keyof typeof currencySymbols]}${campaign.fundingGoal}`} - - - - - - - ))} - -
    -
    -
    - ) : ( -
    -
    {t('noCampaigns')}
    -
    - )} -
    - - {/* Create Campaign Modal */} - row.campaign._id} + components={{ + NoRowsOverlay: () => ( + + {t('noCampaignsFound')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={campaigns.map((campaign, index) => ({ + id: index + 1, + campaign, + }))} + columns={columns} + isRowSelectable={() => false} /> - {/* Update Campaign Modal */} - closeModal(ModalState.SAME)} + refetchCampaign={refetchCampaign} + fundId={fundId} + campaign={campaign} + mode={campaignModalMode} /> - {/* Delete Campaign Modal */} closeModal(ModalState.DELETE)} + campaign={campaign} + refetchCampaign={refetchCampaign} />
    ); diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.module.css b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.module.css index 3ed22aa094..55202baef9 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.module.css +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.module.css @@ -1,5 +1,5 @@ .organizationFundCampaignContainer { - margin: 0.6rem 0; + margin: 0.5rem 0; } .goalButton { border: 1px solid rgba(49, 187, 107, 1) !important; @@ -11,6 +11,10 @@ margin: auto; box-shadow: 5px 5px 4px 0px rgba(49, 187, 107, 0.12); } +.rowBackground { + background-color: var(--bs-white); + max-height: 120px; +} .container { min-height: 100vh; } @@ -20,13 +24,22 @@ margin-left: 13vw; } .titlemodal { - color: var(--bs-gray-600); + color: #707070; font-weight: 600; - font-size: 20px; - margin-bottom: 20px; - padding-bottom: 5px; - border-bottom: 3px solid var(--bs-primary); + font-size: 32px; width: 65%; + margin-bottom: 0px; +} +.noOutline input { + outline: none; +} +.modalCloseBtn { + width: 40px; + height: 40px; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; } .greenregbtn { margin: 1rem 0 0; @@ -99,11 +112,13 @@ .btnsContainer { display: flex; - margin: 2.5rem 0 2.5rem 0; + margin: 2rem 0 2rem 0; + gap: 0.8rem; } .btnsContainer .btnsBlock { display: flex; + gap: 0.8rem; } .btnsContainer .btnsBlock div button { @@ -134,6 +149,12 @@ margin-bottom: 20px; } +.tableHeader { + background-color: var(--bs-primary); + color: var(--bs-white); + font-size: 1rem; +} + @media (max-width: 1020px) { .btnsContainer { flex-direction: column; diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx index 16f069444f..74f646f51a 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampaign.test.tsx @@ -1,19 +1,19 @@ +import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import type { RenderResult } from '@testing-library/react'; import { + cleanup, fireEvent, render, screen, waitFor, - waitForElementToBeRemoved, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { act } from 'react-dom/test-utils'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { toast } from 'react-toastify'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { store } from '../../state/store'; import { StaticMockLink } from '../../utils/StaticMockLink'; import i18nForTest from '../../utils/i18nForTest'; @@ -21,24 +21,17 @@ import OrganizaitionFundCampiagn from './OrganizationFundCampagins'; import { EMPTY_MOCKS, MOCKS, - MOCKS_ERROR_CREATE_CAMPAIGN, - MOCKS_ERROR_DELETE_CAMPAIGN, - MOCKS_ERROR_UPDATE_CAMPAIGN, - MOCK_FUND_CAMPAIGN_ERROR, + MOCK_ERROR, } from './OrganizationFundCampaignMocks'; -import React from 'react'; +import type { ApolloLink } from '@apollo/client'; -const mockNavigate = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, -})); jest.mock('react-toastify', () => ({ toast: { success: jest.fn(), error: jest.fn(), }, })); + jest.mock('@mui/x-date-pickers/DateTimePicker', () => { return { DateTimePicker: jest.requireActual( @@ -47,617 +40,297 @@ jest.mock('@mui/x-date-pickers/DateTimePicker', () => { }; }); -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} const link1 = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink(MOCK_FUND_CAMPAIGN_ERROR, true); -const link3 = new StaticMockLink(MOCKS_ERROR_CREATE_CAMPAIGN, true); -const link4 = new StaticMockLink(MOCKS_ERROR_UPDATE_CAMPAIGN, true); -const link5 = new StaticMockLink(MOCKS_ERROR_DELETE_CAMPAIGN, true); -const link6 = new StaticMockLink(EMPTY_MOCKS, true); +const link2 = new StaticMockLink(MOCK_ERROR, true); +const link3 = new StaticMockLink(EMPTY_MOCKS, true); const translations = JSON.parse( JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.fundCampaign), ); -describe('Testing FundCampaigns Screen', () => { - const formData = { - campaignName: 'Campaign 1', - campaignCurrency: 'USD', - campaignGoal: 100, - campaignStartDate: '03/10/2024', - campaignEndDate: '03/10/2024', - }; - - it('loads the Fund Campaigns screen', async () => { - const { getByText } = render( - +const renderFundCampaign = (link: ApolloLink): RenderResult => { + return render( + + - + - {} + + } + /> +
    } + /> +
  • } + /> +
    } + /> + - + - , - ); - await wait(); - await waitFor(() => { - expect(getByText(translations.addCampaign)).toBeInTheDocument(); - }); + + , + ); +}; + +describe('FundCampaigns Screen', () => { + beforeEach(() => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ orgId: 'orgId', fundId: 'fundId' }), + })); }); - it('renders the campaign screen with error', async () => { - const { queryByText } = render( - - - - - {} - - - - , - ); - await wait(); - await waitFor(() => { - expect(queryByText(translations.addCampaign)).not.toBeInTheDocument(); - }); + + afterEach(() => { + cleanup(); }); - it('renders the Error Component', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - await waitFor(() => { - expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); - }); + + afterAll(() => { + jest.clearAllMocks(); }); - it("opens and closes the 'Create Campaign' modal", async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getByTestId('addCampaignBtn')).toBeInTheDocument(), - ); - userEvent.click(screen.getByTestId('addCampaignBtn')); + + it('should render the Campaign Pledge screen', async () => { + renderFundCampaign(link1); await waitFor(() => { - return expect( - screen.findByTestId('createCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByTestId('searchFullName')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('createCampaignCloseBtn')); - await waitForElementToBeRemoved(() => - screen.queryByTestId('createCampaignCloseBtn'), - ); + + expect(screen.getByText('Campaign 1')).toBeInTheDocument(); + expect(screen.getByText('Campaign 2')).toBeInTheDocument(); }); - it('creates a new Campaign', async () => { + + it('should redirect to fallback URL if URL params are undefined', async () => { render( - - + + - {} + + } + /> +
    } + /> + - - + + , ); - await wait(); - await waitFor(() => - expect(screen.getByTestId('addCampaignBtn')).toBeInTheDocument(), - ); - userEvent.click(screen.getByTestId('addCampaignBtn')); await waitFor(() => { - return expect( - screen.findByTestId('createCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.type( - screen.getByPlaceholderText('Enter Campaign Name'), - formData.campaignName, - ); - userEvent.type( - screen.getByPlaceholderText('Enter Funding Goal'), - formData.campaignGoal.toString(), - ); - const currency = screen.getByTestId('currencySelect'); - fireEvent.change(currency, { - target: { value: formData.campaignCurrency }, - }); - const startDate = screen.getByLabelText('Start Date'); - const endDate = screen.getByLabelText('End Date'); - fireEvent.change(startDate, { - target: { value: formData.campaignStartDate }, - }); - fireEvent.change(endDate, { - target: { value: formData.campaignEndDate }, + expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); + }); - userEvent.click(screen.getByTestId('createCampaignBtn')); + it('open and close Create Campaign modal', async () => { + renderFundCampaign(link1); + + const addCampaignBtn = await screen.findByTestId('addCampaignBtn'); + expect(addCampaignBtn).toBeInTheDocument(); + userEvent.click(addCampaignBtn); - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith(translations.createdCampaign); - }); - }); - it('toast an error on unsuccessful campaign creation', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); await waitFor(() => - expect(screen.getByTestId('addCampaignBtn')).toBeInTheDocument(), + expect(screen.getAllByText(translations.createCampaign)).toHaveLength(2), ); - userEvent.click(screen.getByTestId('addCampaignBtn')); - await waitFor(() => { - return expect( - screen.findByTestId('createCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.type( - screen.getByPlaceholderText('Enter Campaign Name'), - formData.campaignName, - ); - userEvent.type( - screen.getByPlaceholderText('Enter Funding Goal'), - formData.campaignGoal.toString(), + userEvent.click(screen.getByTestId('campaignCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('campaignCloseBtn')).toBeNull(), ); - const endDateDatePicker = screen.getByLabelText('End Date'); - const startDateDatePicker = screen.getByLabelText('Start Date'); - - fireEvent.change(endDateDatePicker, { - target: { value: formData.campaignEndDate }, - }); - fireEvent.change(startDateDatePicker, { - target: { value: formData.campaignStartDate }, - }); + }); - userEvent.click(screen.getByTestId('createCampaignBtn')); + it('open and close update campaign modal', async () => { + renderFundCampaign(link1); await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getByTestId('searchFullName')).toBeInTheDocument(); }); - }); - it('opens and closes the Edit Campaign modal', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); + + const editCampaignBtn = await screen.findAllByTestId('editCampaignBtn'); + await waitFor(() => expect(editCampaignBtn[0]).toBeInTheDocument()); + userEvent.click(editCampaignBtn[0]); + await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), + expect( + screen.getAllByText(translations.updateCampaign)[0], + ).toBeInTheDocument(), ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('editCampaignCloseBtn')); - await waitForElementToBeRemoved(() => - screen.queryByTestId('editCampaignCloseBtn'), + userEvent.click(screen.getByTestId('campaignCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('campaignCloseBtn')).toBeNull(), ); }); - it("updates the Campaign's details", async () => { - render( - - - - - - {} - - - - - , + it('open and closes delete campaign modal', async () => { + renderFundCampaign(link1); + + const deleteCampaignBtn = await screen.findAllByTestId('deleteCampaignBtn'); + await waitFor(() => expect(deleteCampaignBtn[0]).toBeInTheDocument()); + userEvent.click(deleteCampaignBtn[0]); + + await waitFor(() => + expect(screen.getByText(translations.deleteCampaign)).toBeInTheDocument(), ); - await wait(); + userEvent.click(screen.getByTestId('deleteCampaignCloseBtn')); await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), + expect(screen.queryByTestId('deleteCampaignCloseBtn')).toBeNull(), ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - const campaignName = screen.getByPlaceholderText('Enter Campaign Name'); - fireEvent.change(campaignName, { - target: { value: 'Campaign 4' }, - }); - const fundingGoal = screen.getByPlaceholderText('Enter Funding Goal'); - fireEvent.change(fundingGoal, { - target: { value: 1000 }, - }); - const currency = screen.getByTestId('currencySelect'); - fireEvent.change(currency, { - target: { value: 'INR' }, - }); - const endDateDatePicker = screen.getByLabelText('End Date'); - const startDateDatePicker = screen.getByLabelText('Start Date'); - - const endDate = - formData.campaignEndDate < formData.campaignStartDate - ? formData.campaignStartDate - : formData.campaignEndDate; - fireEvent.change(endDateDatePicker, { - target: { value: endDate }, - }); + }); - fireEvent.change(startDateDatePicker, { - target: { value: formData.campaignStartDate }, + it('Search the Campaigns list by Name', async () => { + renderFundCampaign(link1); + const searchField = await screen.findByTestId('searchFullName'); + fireEvent.change(searchField, { + target: { value: '2' }, }); - userEvent.click(screen.getByTestId('editCampaignSubmitBtn')); - await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith(translations.updatedCampaign); + expect(screen.getByText('Campaign 2')).toBeInTheDocument(); + expect(screen.queryByText('Campaign 1')).toBeNull(); }); }); - it("updates the Campaign's details when date is null", async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); + it('should render the Campaign screen with error', async () => { + renderFundCampaign(link2); await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - const endDateDatePicker = screen.getByLabelText('End Date'); - const startDateDatePicker = screen.getByLabelText('Start Date'); - - fireEvent.change(endDateDatePicker, { - target: { value: null }, - }); - - fireEvent.change(startDateDatePicker, { - target: { value: null }, + expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); }); - - expect(startDateDatePicker.getAttribute('value')).toBe(''); - expect(endDateDatePicker.getAttribute('value')).toBe(''); }); - it("updates the Campaign's details when endDate is less than date", async () => { - const formData = { - campaignName: 'Campaign 1', - campaignCurrency: 'USD', - campaignGoal: 100, - campaignStartDate: '03/10/2024', - campaignEndDate: '03/10/2023', - }; - render( - - - - - - {} - - - - - , - ); - await wait(); + it('renders the empty campaign component', async () => { + renderFundCampaign(link3); await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), + expect( + screen.getByText(translations.noCampaignsFound), + ).toBeInTheDocument(), ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); + }); - const endDateDatePicker = screen.getByLabelText('End Date'); - const startDateDatePicker = screen.getByLabelText('Start Date'); + it('Sort the Campaigns list by Latest end Date', async () => { + renderFundCampaign(link1); - fireEvent.change(endDateDatePicker, { - target: { value: formData.campaignEndDate }, - }); + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); - fireEvent.change(startDateDatePicker, { - target: { value: formData.campaignStartDate }, - }); - }); + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('endDate_DESC')); - it("doesn't update when fund field has value less than or equal to 0", async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Campaign 1')).toBeInTheDocument(); + expect(screen.queryByText('Campaign 2')).toBeInTheDocument(); }); - const fundingGoal = screen.getByPlaceholderText( - 'Enter Funding Goal', - ) as HTMLInputElement; - - const initialValue = fundingGoal.value; //Vakue before updating - fireEvent.change(fundingGoal, { - target: { value: 0 }, + await waitFor(() => { + expect(screen.getAllByTestId('endDateCell')[0]).toHaveTextContent( + '01/01/2024', + ); }); - - expect(fundingGoal.value).toBe(initialValue); //Retains previous value }); - it('toast an error on unsuccessful campaign update', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await waitFor(() => { - return expect( - screen.findByTestId('editCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); - }); - userEvent.type( - screen.getByPlaceholderText('Enter Campaign Name'), - formData.campaignName, - ); + it('Sort the Campaigns list by Earliest end Date', async () => { + renderFundCampaign(link1); - userEvent.click(screen.getByTestId('editCampaignSubmitBtn')); + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); + + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('endDate_ASC')); await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getByText('Campaign 1')).toBeInTheDocument(); + expect(screen.queryByText('Campaign 2')).toBeInTheDocument(); }); - }); - it("opens and closes the 'Delete Campaign' modal", async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('deleteCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('deleteCampaignBtn')[0]); + await waitFor(() => { - return expect( - screen.findByTestId('deleteCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getAllByTestId('endDateCell')[0]).toHaveTextContent( + '01/01/2021', + ); }); - userEvent.click(screen.getByTestId('deleteCampaignCloseBtn')); - await waitForElementToBeRemoved(() => - screen.queryByTestId('deleteCampaignCloseBtn'), - ); }); - it('deletes a Campaign', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('deleteCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('deleteCampaignBtn')[0]); + + it('Sort the Campaigns list by lowest goal', async () => { + renderFundCampaign(link1); + + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); + + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('fundingGoal_ASC')); + await waitFor(() => { - return expect( - screen.findByTestId('deleteCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Campaign 1')).toBeInTheDocument(); + expect(screen.queryByText('Campaign 2')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('deleteyesbtn')); + await waitFor(() => { - expect(toast.success).toHaveBeenCalledWith(translations.deletedCampaign); + expect(screen.getAllByTestId('goalCell')[0]).toHaveTextContent('100'); }); }); - it('toast an error on unsuccessful campaign deletion', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('editCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('editCampaignBtn')[0]); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('deleteCampaignBtn')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('deleteCampaignBtn')[0]); + + it('Sort the Campaigns list by highest goal', async () => { + renderFundCampaign(link1); + + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); + + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('fundingGoal_DESC')); + await waitFor(() => { - return expect( - screen.findByTestId('deleteCampaignCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Campaign 1')).toBeInTheDocument(); + expect(screen.queryByText('Campaign 2')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('deleteyesbtn')); + await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getAllByTestId('goalCell')[0]).toHaveTextContent('200'); }); }); - it('renders the Empty Campaigns Component', async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); + + it('Click on Campaign Name', async () => { + renderFundCampaign(link1); + + const campaignName = await screen.findAllByTestId('campaignName'); + expect(campaignName[0]).toBeInTheDocument(); + fireEvent.click(campaignName[0]); + await waitFor(() => { - expect(screen.getByText(translations.noCampaigns)).toBeInTheDocument(); + expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument(); }); }); - it("redirects to 'FundCampaignPledge' screen", async () => { - render( - - - - - - {} - - - - - , - ); - await wait(); - await waitFor(() => - expect(screen.getAllByTestId('campaignName')[0]).toBeInTheDocument(), - ); - userEvent.click(screen.getAllByTestId('campaignName')[0]); + + it('Click on View Pledge', async () => { + renderFundCampaign(link1); + + const viewBtn = await screen.findAllByTestId('viewBtn'); + expect(viewBtn[0]).toBeInTheDocument(); + fireEvent.click(viewBtn[0]); + await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith( - '/fundCampaignPledge/undefined/1', - ); + expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument(); }); }); - it('search funds by name', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - userEvent.type(screen.getByTestId('searchFullName'), 'Funndds'); - await wait(); - userEvent.click(screen.getByTestId('searchBtn')); + it('should render the Fund screen on fund breadcrumb click', async () => { + renderFundCampaign(link1); + + const fundBreadcrumb = await screen.findByTestId('fundsLink'); + expect(fundBreadcrumb).toBeInTheDocument(); + fireEvent.click(fundBreadcrumb); + + await waitFor(() => { + expect(screen.getByTestId('fundScreen')).toBeInTheDocument(); + }); }); }); diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.tsx b/src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.ts similarity index 59% rename from src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.tsx rename to src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.ts index 09fbb9f4b2..160dfad5fb 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.tsx +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampaignMocks.ts @@ -10,7 +10,9 @@ export const MOCKS = [ request: { query: FUND_CAMPAIGN, variables: { - id: undefined, + id: 'fundId', + orderBy: null, + where: { name_contains: '' }, }, }, result: { @@ -18,7 +20,7 @@ export const MOCKS = [ getFundById: { campaigns: [ { - _id: '1', + _id: 'campaignId1', name: 'Campaign 1', fundingGoal: 100, startDate: '2024-01-01', @@ -40,92 +42,111 @@ export const MOCKS = [ }, { request: { - query: CREATE_CAMPAIGN_MUTATION, + query: FUND_CAMPAIGN, variables: { - fundId: undefined, - name: 'Campaign 1', - fundingGoal: 100, - startDate: '2024-03-10', - endDate: '2024-03-10', - currency: 'USD', + id: 'fundId', + orderBy: null, + where: { name_contains: '2' }, }, }, result: { data: { - createFundraisingCampaign: { - _id: '3', + getFundById: { + campaigns: [ + { + _id: '2', + name: 'Campaign 2', + fundingGoal: 200, + startDate: '2021-01-01', + endDate: '2021-01-01', + currency: 'USD', + }, + ], }, }, }, }, { request: { - query: UPDATE_CAMPAIGN_MUTATION, + query: FUND_CAMPAIGN, variables: { - id: '1', - name: 'Campaign 4', - startDate: '2024-03-10', - endDate: '2024-03-10', - fundingGoal: 1000, - currency: 'INR', + id: 'fundId', + orderBy: 'endDate_DESC', + where: { name_contains: '' }, }, }, result: { data: { - updateFundraisingCampaign: { - _id: '1', + getFundById: { + campaigns: [ + { + _id: '1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: '2024-01-01', + endDate: '2024-01-01', + currency: 'USD', + }, + { + _id: '2', + name: 'Campaign 2', + fundingGoal: 200, + startDate: '2021-01-01', + endDate: '2021-01-01', + currency: 'USD', + }, + ], }, }, }, }, { request: { - query: DELETE_CAMPAIGN_MUTATION, + query: FUND_CAMPAIGN, variables: { - id: '1', + id: 'fundId', + orderBy: 'endDate_ASC', + where: { name_contains: '' }, }, }, result: { data: { - removeFundraisingCampaign: { - _id: '1', + getFundById: { + campaigns: [ + { + _id: '2', + name: 'Campaign 2', + fundingGoal: 200, + startDate: '2021-01-01', + endDate: '2021-01-01', + currency: 'USD', + }, + { + _id: '1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: '2024-01-01', + endDate: '2024-01-01', + currency: 'USD', + }, + ], }, }, }, }, -]; - -export const MOCK_FUND_CAMPAIGN_ERROR = [ - { - request: { - query: FUND_CAMPAIGN, - variables: { - id: undefined, - }, - }, - error: new Error('An error occurred'), - }, -]; -export const MOCKS_ERROR_CREATE_CAMPAIGN = [ { request: { query: FUND_CAMPAIGN, variables: { - id: undefined, + id: 'fundId', + orderBy: 'fundingGoal_DESC', + where: { name_contains: '' }, }, }, result: { data: { getFundById: { campaigns: [ - { - _id: '1', - name: 'Campaign 1', - fundingGoal: 100, - startDate: '2021-01-01', - endDate: '2021-01-01', - currency: 'USD', - }, { _id: '2', name: 'Campaign 2', @@ -134,33 +155,26 @@ export const MOCKS_ERROR_CREATE_CAMPAIGN = [ endDate: '2021-01-01', currency: 'USD', }, + { + _id: '1', + name: 'Campaign 1', + fundingGoal: 100, + startDate: '2024-01-01', + endDate: '2024-01-01', + currency: 'USD', + }, ], }, }, }, }, - - { - request: { - query: CREATE_CAMPAIGN_MUTATION, - variables: { - fundId: undefined, - name: 'Campaign 1', - fundingGoal: 100, - startDate: '2024-03-10', - endDate: '2024-03-10', - currency: 'USD', - }, - }, - error: new Error('An error occurred'), - }, -]; -export const MOCKS_ERROR_UPDATE_CAMPAIGN = [ { request: { query: FUND_CAMPAIGN, variables: { - id: undefined, + id: 'fundId', + orderBy: 'fundingGoal_ASC', + where: { name_contains: '' }, }, }, result: { @@ -171,8 +185,8 @@ export const MOCKS_ERROR_UPDATE_CAMPAIGN = [ _id: '1', name: 'Campaign 1', fundingGoal: 100, - startDate: '2021-01-01', - endDate: '2021-01-01', + startDate: '2024-01-01', + endDate: '2024-01-01', currency: 'USD', }, { @@ -190,48 +204,39 @@ export const MOCKS_ERROR_UPDATE_CAMPAIGN = [ }, { request: { - query: UPDATE_CAMPAIGN_MUTATION, + query: CREATE_CAMPAIGN_MUTATION, variables: { - id: undefined, - name: 'Campaign 1', - fundingGoal: 100, - startDate: '2024-03-10', - endDate: '2024-03-10', + fundId: 'fundId', + name: 'Campaign 2', + fundingGoal: 200, + startDate: '2024-01-02', + endDate: '2024-02-02', currency: 'USD', }, }, - error: new Error('An error occurred'), + result: { + data: { + createFundraisingCampaign: { + _id: 'fundId', + }, + }, + }, }, -]; -export const MOCKS_ERROR_DELETE_CAMPAIGN = [ { request: { - query: FUND_CAMPAIGN, + query: UPDATE_CAMPAIGN_MUTATION, variables: { - id: undefined, + id: 'campaignId1', + name: 'Campaign 4', + fundingGoal: 400, + startDate: '2023-01-02', + endDate: '2023-02-02', }, }, result: { data: { - getFundById: { - campaigns: [ - { - _id: '1', - name: 'Campaign 1', - fundingGoal: 100, - startDate: '2021-01-01', - endDate: '2021-01-01', - currency: 'USD', - }, - { - _id: '2', - name: 'Campaign 2', - fundingGoal: 200, - startDate: '2021-01-01', - endDate: '2021-01-01', - currency: 'USD', - }, - ], + updateFundraisingCampaign: { + _id: 'campaignId1', }, }, }, @@ -240,18 +245,77 @@ export const MOCKS_ERROR_DELETE_CAMPAIGN = [ request: { query: DELETE_CAMPAIGN_MUTATION, variables: { - id: '1', + id: 'campaignId1', + }, + }, + result: { + data: { + removeFundraisingCampaign: { + _id: 'campaignId1', + }, + }, + }, + }, +]; + +export const MOCK_ERROR = [ + { + request: { + query: FUND_CAMPAIGN, + variables: { + id: 'fundId', + orderBy: null, + where: { name_contains: '2' }, }, }, error: new Error('An error occurred'), }, + { + request: { + query: CREATE_CAMPAIGN_MUTATION, + variables: { + fundId: 'fundId', + name: 'Campaign 2', + fundingGoal: 200, + startDate: '2024-01-02', + endDate: '2024-02-02', + currency: 'USD', + }, + }, + error: new Error('Mock graphql error'), + }, + { + request: { + query: UPDATE_CAMPAIGN_MUTATION, + variables: { + id: 'campaignId1', + name: 'Campaign 4', + fundingGoal: 400, + startDate: '2023-01-02', + endDate: '2023-02-02', + }, + }, + error: new Error('Mock graphql error'), + }, + { + request: { + query: DELETE_CAMPAIGN_MUTATION, + variables: { + id: 'campaignId1', + }, + }, + error: new Error('Mock graphql error'), + }, ]; + export const EMPTY_MOCKS = [ { request: { query: FUND_CAMPAIGN, variables: { - id: undefined, + id: 'fundId', + orderBy: null, + where: { name_contains: '' }, }, }, result: { diff --git a/src/screens/OrganizationFunds/FundCreateModal.tsx b/src/screens/OrganizationFunds/FundCreateModal.tsx deleted file mode 100644 index e89152453f..0000000000 --- a/src/screens/OrganizationFunds/FundCreateModal.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import type { ChangeEvent } from 'react'; -import { Button, Form, Modal } from 'react-bootstrap'; -import type { InterfaceCreateFund } from 'utils/interfaces'; -import styles from './OrganizationFunds.module.css'; - -/** - * This interface defines the props for the FundCreateModal component. - */ -interface InterfaceFundCreateModal { - fundCreateModalIsOpen: boolean; - hideCreateModal: () => void; - formState: InterfaceCreateFund; - setFormState: (state: React.SetStateAction) => void; - createFundHandler: (e: ChangeEvent) => Promise; - taxDeductible: boolean; - setTaxDeductible: (state: React.SetStateAction) => void; - isDefault: boolean; - setIsDefault: (state: React.SetStateAction) => void; - t: (key: string) => string; -} - -/** - * FundCreateModal component allows users to create a new fund. - * It displays a modal with a form to enter the fund details. - * - * @param fundCreateModalIsOpen - Controls whether the modal is open or closed. - * @param hideCreateModal - Function to hide the modal. - * @param formState - The current state of the form used to create a fund. - * @param setFormState - Function to update the state of the form. - * @param createFundHandler - Handler function for form submission. - * @param taxDeductible - Indicates whether the fund is tax deductible. - * @param setTaxDeductible - Function to set the tax deductible state. - * @param isDefault - Indicates whether the fund is the default fund. - * @param setIsDefault - Function to set the default state. - * @param t - Translation function to get the translated text. - */ -const FundCreateModal: React.FC = ({ - fundCreateModalIsOpen, - hideCreateModal, - formState, - setFormState, - createFundHandler, - taxDeductible, - setTaxDeductible, - isDefault, - setIsDefault, - t, -}) => { - return ( - <> - - -

    {t('fundCreate')}

    - -
    - -
    - - {t('fundName')} - - setFormState({ - ...formState, - fundName: e.target.value, - }) - } - /> - - - {t('fundId')} - - setFormState({ - ...formState, - fundRef: e.target.value, - }) - } - /> - -
    - -
    - - setTaxDeductible(!taxDeductible)} - /> -
    -
    - -
    - - setIsDefault(!isDefault)} - /> -
    -
    -
    - -
    -
    -
    - - ); -}; -export default FundCreateModal; diff --git a/src/screens/OrganizationFunds/FundDeleteModal.test.tsx b/src/screens/OrganizationFunds/FundDeleteModal.test.tsx new file mode 100644 index 0000000000..41fcd3ec5e --- /dev/null +++ b/src/screens/OrganizationFunds/FundDeleteModal.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import type { InterfaceDeleteFundModal } from './FundDeleteModal'; +import FundDeleteModal from './FundDeleteModal'; +import { MOCKS, MOCKS_ERROR } from './OrganizationFundsMocks'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const translations = JSON.parse( + JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.funds), +); + +const fundProps: InterfaceDeleteFundModal = { + isOpen: true, + hide: jest.fn(), + fund: { + _id: 'fundId', + name: 'Fund 1', + refrenceNumber: '1111', + taxDeductible: true, + isArchived: false, + isDefault: false, + createdAt: '2024-06-22', + organizationId: 'orgId', + creator: { + _id: 'creatorId1', + firstName: 'John', + lastName: 'Doe', + }, + }, + refetchFunds: jest.fn(), +}; +const renderFundDeleteModal = ( + link: ApolloLink, + props: InterfaceDeleteFundModal, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('FundDeleteModal', () => { + it('should render FundDeleteModal', () => { + renderFundDeleteModal(link1, fundProps); + expect(screen.getByTestId('deleteFundCloseBtn')).toBeInTheDocument(); + }); + + it('should successfully Delete Fund', async () => { + renderFundDeleteModal(link1, fundProps); + expect(screen.getByTestId('deleteFundCloseBtn')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(fundProps.refetchFunds).toHaveBeenCalled(); + expect(fundProps.hide).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith(translations.fundDeleted); + }); + }); + + it('should fail to Delete Fund', async () => { + renderFundDeleteModal(link2, fundProps); + expect(screen.getByTestId('deleteFundCloseBtn')).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('deleteyesbtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); +}); diff --git a/src/screens/OrganizationFunds/FundDeleteModal.tsx b/src/screens/OrganizationFunds/FundDeleteModal.tsx new file mode 100644 index 0000000000..fb13206f1a --- /dev/null +++ b/src/screens/OrganizationFunds/FundDeleteModal.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import type { InterfaceFundInfo } from 'utils/interfaces'; +import styles from './OrganizationFunds.module.css'; +import { Button, Modal } from 'react-bootstrap'; +import { REMOVE_FUND_MUTATION } from 'GraphQl/Mutations/FundMutation'; +import { useMutation } from '@apollo/client'; +import { toast } from 'react-toastify'; + +export interface InterfaceDeleteFundModal { + isOpen: boolean; + hide: () => void; + fund: InterfaceFundInfo | null; + refetchFunds: () => void; +} + +const FundDeleteModal: React.FC = ({ + isOpen, + hide, + fund, + refetchFunds, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'funds', + }); + const { t: tCommon } = useTranslation('common'); + + const [deleteFund] = useMutation(REMOVE_FUND_MUTATION); + + const deleteFundHandler = async (): Promise => { + try { + await deleteFund({ + variables: { + id: fund?._id, + }, + }); + refetchFunds(); + hide(); + toast.success(t('fundDeleted')); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + return ( + <> + + +

    {t('fundDelete')}

    + +
    + +

    {t('deleteFundMsg')}

    +
    + + + + +
    + + ); +}; + +export default FundDeleteModal; diff --git a/src/screens/OrganizationFunds/FundModal.test.tsx b/src/screens/OrganizationFunds/FundModal.test.tsx new file mode 100644 index 0000000000..c74b0434c3 --- /dev/null +++ b/src/screens/OrganizationFunds/FundModal.test.tsx @@ -0,0 +1,260 @@ +import React from 'react'; +import type { ApolloLink } from '@apollo/client'; +import { MockedProvider } from '@apollo/react-testing'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import type { RenderResult } from '@testing-library/react'; +import { + cleanup, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import i18nForTest from '../../utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { toast } from 'react-toastify'; +import { MOCKS, MOCKS_ERROR } from './OrganizationFundsMocks'; +import type { InterfaceFundModal } from './FundModal'; +import FundModal from './FundModal'; + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +const link1 = new StaticMockLink(MOCKS); +const link2 = new StaticMockLink(MOCKS_ERROR); +const translations = JSON.parse( + JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.funds), +); + +const fundProps: InterfaceFundModal[] = [ + { + isOpen: true, + hide: jest.fn(), + fund: { + _id: 'fundId', + name: 'Fund 1', + refrenceNumber: '1111', + taxDeductible: true, + isArchived: false, + isDefault: false, + createdAt: '2024-06-22', + organizationId: 'orgId', + creator: { + _id: 'creatorId1', + firstName: 'John', + lastName: 'Doe', + }, + }, + refetchFunds: jest.fn(), + orgId: 'orgId', + mode: 'create', + }, + { + isOpen: true, + hide: jest.fn(), + fund: { + _id: 'fundId', + name: 'Fund 1', + refrenceNumber: '1111', + taxDeductible: true, + isArchived: false, + isDefault: false, + createdAt: '2024-06-22', + organizationId: 'orgId', + creator: { + _id: 'creatorId1', + firstName: 'John', + lastName: 'Doe', + }, + }, + refetchFunds: jest.fn(), + orgId: 'orgId', + mode: 'edit', + }, +]; + +const renderFundModal = ( + link: ApolloLink, + props: InterfaceFundModal, +): RenderResult => { + return render( + + + + + + + + + + + , + ); +}; + +describe('PledgeModal', () => { + afterEach(() => { + cleanup(); + }); + + it('should populate form fields with correct values in edit mode', async () => { + renderFundModal(link1, fundProps[1]); + await waitFor(() => + expect( + screen.getAllByText(translations.fundUpdate)[0], + ).toBeInTheDocument(), + ); + expect(screen.getByLabelText(translations.fundName)).toHaveValue('Fund 1'); + expect(screen.getByLabelText(translations.fundId)).toHaveValue('1111'); + expect(screen.getByTestId('setTaxDeductibleSwitch')).toBeChecked(); + expect(screen.getByTestId('setDefaultSwitch')).not.toBeChecked(); + expect(screen.getByTestId('archivedSwitch')).not.toBeChecked(); + }); + + it('should update Fund Name when input value changes', async () => { + renderFundModal(link1, fundProps[1]); + const fundNameInput = screen.getByLabelText(translations.fundName); + expect(fundNameInput).toHaveValue('Fund 1'); + fireEvent.change(fundNameInput, { target: { value: 'Fund 2' } }); + expect(fundNameInput).toHaveValue('Fund 2'); + }); + + it('should update Fund Reference ID when input value changes', async () => { + renderFundModal(link1, fundProps[1]); + const fundIdInput = screen.getByLabelText(translations.fundId); + expect(fundIdInput).toHaveValue('1111'); + fireEvent.change(fundIdInput, { target: { value: '2222' } }); + expect(fundIdInput).toHaveValue('2222'); + }); + + it('should update Tax Deductible Switch when input value changes', async () => { + renderFundModal(link1, fundProps[1]); + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + expect(taxDeductibleSwitch).toBeChecked(); + fireEvent.click(taxDeductibleSwitch); + expect(taxDeductibleSwitch).not.toBeChecked(); + }); + + it('should update Tax Default switch when input value changes', async () => { + renderFundModal(link1, fundProps[1]); + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + expect(defaultSwitch).not.toBeChecked(); + fireEvent.click(defaultSwitch); + expect(defaultSwitch).toBeChecked(); + }); + + it('should update Tax isArchived switch when input value changes', async () => { + renderFundModal(link1, fundProps[1]); + const archivedSwitch = screen.getByTestId('archivedSwitch'); + expect(archivedSwitch).not.toBeChecked(); + fireEvent.click(archivedSwitch); + expect(archivedSwitch).toBeChecked(); + }); + + it('should create fund', async () => { + renderFundModal(link1, fundProps[0]); + + const fundNameInput = screen.getByLabelText(translations.fundName); + fireEvent.change(fundNameInput, { target: { value: 'Fund 2' } }); + + const fundIdInput = screen.getByLabelText(translations.fundId); + fireEvent.change(fundIdInput, { target: { value: '2222' } }); + + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + fireEvent.click(taxDeductibleSwitch); + + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + fireEvent.click(defaultSwitch); + + fireEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(translations.fundCreated); + expect(fundProps[0].refetchFunds).toHaveBeenCalled(); + expect(fundProps[0].hide).toHaveBeenCalled(); + }); + }); + + it('should update fund', async () => { + renderFundModal(link1, fundProps[1]); + + const fundNameInput = screen.getByLabelText(translations.fundName); + fireEvent.change(fundNameInput, { target: { value: 'Fund 2' } }); + + const fundIdInput = screen.getByLabelText(translations.fundId); + fireEvent.change(fundIdInput, { target: { value: '2222' } }); + + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + fireEvent.click(taxDeductibleSwitch); + + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + fireEvent.click(defaultSwitch); + + const archivedSwitch = screen.getByTestId('archivedSwitch'); + fireEvent.click(archivedSwitch); + + fireEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith(translations.fundUpdated); + expect(fundProps[1].refetchFunds).toHaveBeenCalled(); + expect(fundProps[1].hide).toHaveBeenCalled(); + }); + }); + + it('Error: should create fund', async () => { + renderFundModal(link2, fundProps[0]); + + const fundNameInput = screen.getByLabelText(translations.fundName); + fireEvent.change(fundNameInput, { target: { value: 'Fund 2' } }); + + const fundIdInput = screen.getByLabelText(translations.fundId); + fireEvent.change(fundIdInput, { target: { value: '2222' } }); + + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + fireEvent.click(taxDeductibleSwitch); + + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + fireEvent.click(defaultSwitch); + + fireEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); + + it('Error: should update fund', async () => { + renderFundModal(link2, fundProps[1]); + + const fundNameInput = screen.getByLabelText(translations.fundName); + fireEvent.change(fundNameInput, { target: { value: 'Fund 2' } }); + + const fundIdInput = screen.getByLabelText(translations.fundId); + fireEvent.change(fundIdInput, { target: { value: '2222' } }); + + const taxDeductibleSwitch = screen.getByTestId('setTaxDeductibleSwitch'); + fireEvent.click(taxDeductibleSwitch); + + const defaultSwitch = screen.getByTestId('setDefaultSwitch'); + fireEvent.click(defaultSwitch); + + const archivedSwitch = screen.getByTestId('archivedSwitch'); + fireEvent.click(archivedSwitch); + + fireEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Mock graphql error'); + }); + }); +}); diff --git a/src/screens/OrganizationFunds/FundModal.tsx b/src/screens/OrganizationFunds/FundModal.tsx new file mode 100644 index 0000000000..f4c6350a1c --- /dev/null +++ b/src/screens/OrganizationFunds/FundModal.tsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState } from 'react'; +import type { ChangeEvent } from 'react'; +import { Button, Form, Modal } from 'react-bootstrap'; +import type { InterfaceCreateFund, InterfaceFundInfo } from 'utils/interfaces'; +import styles from './OrganizationFunds.module.css'; +import { useTranslation } from 'react-i18next'; +import { useMutation } from '@apollo/client'; +import { + CREATE_FUND_MUTATION, + UPDATE_FUND_MUTATION, +} from 'GraphQl/Mutations/FundMutation'; +import { toast } from 'react-toastify'; +import { FormControl, TextField } from '@mui/material'; + +export interface InterfaceFundModal { + isOpen: boolean; + hide: () => void; + refetchFunds: () => void; + fund: InterfaceFundInfo | null; + orgId: string; + mode: 'create' | 'edit'; +} + +const FundModal: React.FC = ({ + isOpen, + hide, + refetchFunds, + fund, + orgId, + mode, +}) => { + const { t } = useTranslation('translation', { + keyPrefix: 'funds', + }); + + const [formState, setFormState] = useState({ + fundName: fund?.name ?? '', + fundRef: fund?.refrenceNumber ?? '', + isDefault: fund?.isDefault ?? false, + taxDeductible: fund?.taxDeductible ?? false, + isArchived: fund?.isArchived ?? false, + }); + + useEffect(() => { + setFormState({ + fundName: fund?.name ?? '', + fundRef: fund?.refrenceNumber ?? '', + isDefault: fund?.isDefault ?? false, + taxDeductible: fund?.taxDeductible ?? false, + isArchived: fund?.isArchived ?? false, + }); + }, [fund]); + + const [createFund] = useMutation(CREATE_FUND_MUTATION); + const [updateFund] = useMutation(UPDATE_FUND_MUTATION); + + const createFundHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + const { fundName, fundRef, isDefault, taxDeductible, isArchived } = + formState; + try { + await createFund({ + variables: { + name: fundName, + refrenceNumber: fundRef, + organizationId: orgId, + taxDeductible, + isArchived, + isDefault, + }, + }); + + setFormState({ + fundName: '', + fundRef: '', + isDefault: false, + taxDeductible: false, + isArchived: false, + }); + toast.success(t('fundCreated')); + refetchFunds(); + hide(); + } catch (error: unknown) { + toast.error((error as Error).message); + } + }; + + /*istanbul ignore next*/ + const updateFundHandler = async ( + e: ChangeEvent, + ): Promise => { + e.preventDefault(); + const { fundName, fundRef, taxDeductible, isArchived, isDefault } = + formState; + try { + const updatedFields: { [key: string]: string | boolean } = {}; + if (fundName != fund?.name) { + updatedFields.name = fundName; + } + if (fundRef != fund?.refrenceNumber) { + updatedFields.refrenceNumber = fundRef; + } + if (taxDeductible != fund?.taxDeductible) { + updatedFields.taxDeductible = taxDeductible; + } + if (isArchived != fund?.isArchived) { + updatedFields.isArchived = isArchived; + } + if (isDefault != fund?.isDefault) { + updatedFields.isDefault = isDefault; + } + + await updateFund({ + variables: { + id: fund?._id, + ...updatedFields, + }, + }); + setFormState({ + fundName: '', + fundRef: '', + isDefault: false, + taxDeductible: false, + isArchived: false, + }); + refetchFunds(); + hide(); + toast.success(t('fundUpdated')); + } catch (error: unknown) { + if (error instanceof Error) { + toast.error(error.message); + } + } + }; + + return ( + <> + + +

    + {t(mode === 'create' ? 'fundCreate' : 'fundUpdate')} +

    + +
    + +
    + + + + setFormState({ + ...formState, + fundName: e.target.value, + }) + } + /> + + + + + + setFormState({ + ...formState, + fundRef: e.target.value, + }) + } + /> + + + +
    + + + + setFormState({ + ...formState, + taxDeductible: !formState.taxDeductible, + }) + } + /> + + + + + setFormState({ + ...formState, + isDefault: !formState.isDefault, + }) + } + /> + + {mode === 'edit' && ( + + + + setFormState({ + ...formState, + isArchived: !formState.isArchived, + }) + } + /> + + )} +
    + +
    +
    +
    + + ); +}; +export default FundModal; diff --git a/src/screens/OrganizationFunds/FundUpdateModal.tsx b/src/screens/OrganizationFunds/FundUpdateModal.tsx deleted file mode 100644 index 9e3942e562..0000000000 --- a/src/screens/OrganizationFunds/FundUpdateModal.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import type { ChangeEvent } from 'react'; -import React from 'react'; -import { Button, Form, Modal } from 'react-bootstrap'; -import type { InterfaceCreateFund } from 'utils/interfaces'; -import styles from './OrganizationFunds.module.css'; - -/** - * This interface defines the props for the FundUpdateModal component. - */ -interface InterfaceFundUpdateModal { - fundUpdateModalIsOpen: boolean; - hideUpdateModal: () => void; - formState: InterfaceCreateFund; - setFormState: (state: React.SetStateAction) => void; - updateFundHandler: (e: ChangeEvent) => Promise; - taxDeductible: boolean; - setTaxDeductible: (state: React.SetStateAction) => void; - isArchived: boolean; - deleteFundHandler: () => Promise; - setIsArchived: (state: React.SetStateAction) => void; - isDefault: boolean; - setIsDefault: (state: React.SetStateAction) => void; - t: (key: string) => string; -} -/** - * FundUpdateModal component allows users to update an existing fund. - * It displays a modal with a form to modify the fund details. - * - * @param fundUpdateModalIsOpen - Controls whether the modal is open or closed. - * @param hideUpdateModal - Function to hide the modal. - * @param formState - The current state of the form used to update a fund. - * @param setFormState - Function to update the state of the form. - * @param updateFundHandler - Handler function for form submission. - * @param taxDeductible - Indicates whether the fund is tax deductible. - * @param setTaxDeductible - Function to set the tax deductible state. - * @param isArchived - Indicates whether the fund is archived. - * @param deleteFundHandler - Handler function to delete the fund. - * @param setIsArchived - Function to set the archived state. - * @param isDefault - Indicates whether the fund is the default fund. - * @param setIsDefault - Function to set the default state. - * @param t - Translation function to get the translated text. - */ -const FundUpdateModal: React.FC = ({ - fundUpdateModalIsOpen, - hideUpdateModal, - formState, - setFormState, - updateFundHandler, - taxDeductible, - setTaxDeductible, - isArchived, - deleteFundHandler, - setIsArchived, - isDefault, - setIsDefault, - t, -}) => { - return ( - <> - - -

    {t('manageFund')}

    - -
    - -
    - - {t('fundName')} - - setFormState({ - ...formState, - fundName: e.target.value, - }) - } - /> - - - {t('fundId')} - - setFormState({ - ...formState, - fundRef: e.target.value, - }) - } - /> - - -
    -
    - -
    - - setTaxDeductible(!taxDeductible)} - /> -
    -
    - -
    - - setIsDefault(!isDefault)} - /> -
    -
    -
    -
    - -
    - - setIsArchived(!isArchived)} - /> -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - ); -}; -export default FundUpdateModal; diff --git a/src/screens/OrganizationFunds/OrganizationFunds.module.css b/src/screens/OrganizationFunds/OrganizationFunds.module.css index 07e16ad0a8..aa9d89dfb1 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.module.css +++ b/src/screens/OrganizationFunds/OrganizationFunds.module.css @@ -21,24 +21,13 @@ border: 1px solid #31bb6b; position: relative; display: inline-block; - margin-top: 10px; - margin-bottom: 10px; color: #31bb6b; } -.createFundBtn { - margin-top: 10px; -} - .fundName { font-weight: 600; cursor: pointer; } -.fundModal { - max-width: 80vw; - margin-top: 2vh; - margin-left: 13vw; -} .modalHeader { border: none; @@ -49,12 +38,33 @@ color: var(--bs-emphasis-color); } +.fundModal { + max-width: 80vw; + margin-top: 2vh; + margin-left: 13vw; +} + .titlemodal { + color: #707070; font-weight: 600; - font-size: 25px; - margin-top: 1rem; + font-size: 32px; width: 65%; + margin-bottom: 0px; } + +.noOutline input { + outline: none; +} + +.modalCloseBtn { + width: 40px; + height: 40px; + padding: 1rem; + display: flex; + justify-content: center; + align-items: center; +} + .greenregbtn { margin: 1rem 0 0; margin-top: 15px; @@ -95,22 +105,12 @@ .btnsContainer { display: flex; - margin: 2.5rem 0 2.5rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; -} - -.btnsContainer .btnsBlock div button { - display: flex; - margin-left: 1rem; - justify-content: center; - align-items: center; + margin: 2rem 0 2.5rem 0; } .btnsContainer .input { flex: 1; + min-width: 18rem; position: relative; } @@ -130,48 +130,13 @@ margin-bottom: 20px; } -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock div button { - margin: 0; - } - - .createFundBtn { - margin-top: 0; - } -} - -@media screen and (max-width: 575.5px) { - .mainpageright { - width: 98%; - } +.rowBackground { + background-color: var(--bs-white); + max-height: 120px; } -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } +.tableHeader { + background-color: var(--bs-primary); + color: var(--bs-white); + font-size: 1rem; } diff --git a/src/screens/OrganizationFunds/OrganizationFunds.test.tsx b/src/screens/OrganizationFunds/OrganizationFunds.test.tsx index 93be2c281c..a1b49e68eb 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.test.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.test.tsx @@ -1,407 +1,260 @@ import React from 'react'; import { MockedProvider } from '@apollo/client/testing'; +import type { RenderResult } from '@testing-library/react'; import { - act, + cleanup, fireEvent, render, screen, waitFor, - waitForElementToBeRemoved, } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; -import { toast } from 'react-toastify'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; import OrganizationFunds from './OrganizationFunds'; -import { - MOCKS, - MOCKS_ERROR_CREATE_FUND, - MOCKS_ERROR_ORGANIZATIONS_FUNDS, - MOCKS_ERROR_REMOVE_FUND, - MOCKS_ERROR_UPDATE_FUND, - NO_FUNDS, -} from './OrganizationFundsMocks'; -const mockNavigate = jest.fn(); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockNavigate, -})); +import { MOCKS, MOCKS_ERROR, NO_FUNDS } from './OrganizationFundsMocks'; +import type { ApolloLink } from '@apollo/client'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; + jest.mock('react-toastify', () => ({ toast: { success: jest.fn(), error: jest.fn(), }, })); -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} + const link1 = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink(MOCKS_ERROR_ORGANIZATIONS_FUNDS, true); -const link3 = new StaticMockLink(MOCKS_ERROR_CREATE_FUND, true); -const link4 = new StaticMockLink(MOCKS_ERROR_UPDATE_FUND, true); -const link5 = new StaticMockLink(MOCKS_ERROR_REMOVE_FUND, true); -const link6 = new StaticMockLink(NO_FUNDS, true); +const link2 = new StaticMockLink(MOCKS_ERROR, true); +const link3 = new StaticMockLink(NO_FUNDS, true); const translations = JSON.parse( JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.funds), ); -describe('Testing OrganizationFunds screen', () => { - const formData = { - fundName: 'Test Fund', - fundRef: '1', - }; - it("loads the OrganizationFunds screen and it's components", async () => { - const { getByText } = render( - + +const renderOrganizationFunds = (link: ApolloLink): RenderResult => { + return render( + + - + - {} + + } + /> +
    } + /> +
    } + /> + - + - , - ); - await wait(); - await waitFor(() => { - expect(getByText(translations.createFund)).toBeInTheDocument(); - }); + + , + ); +}; + +describe('OrganizationFunds Screen =>', () => { + beforeEach(() => { + jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ orgId: 'orgId' }), + })); }); - it("renders the OrganizationFunds screen and it's components with error", async () => { - const { queryByText } = render( - - - - - {} - - - - , - ); - await wait(); - await waitFor(() => { - expect(queryByText(translations.createFund)).not.toBeInTheDocument(); - }); + + afterEach(() => { + jest.clearAllMocks(); }); - it('renders the Error component', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - await waitFor(() => { - expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); - }); + + afterEach(() => { + cleanup(); }); - it('renders the funds component based on fund type', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - await waitFor(() => { - expect(screen.getAllByTestId('fundtype')[0]).toBeInTheDocument(); - }); + + it('should render the Campaign Pledge screen', async () => { + renderOrganizationFunds(link1); await waitFor(() => { - expect(screen.getAllByTestId('fundtype')[0]).toHaveTextContent( - translations.nonArchive, - ); + expect(screen.getByTestId('searchByName')).toBeInTheDocument(); }); - expect(screen.getAllByTestId('fundtype')[1]).toHaveTextContent( - translations.archived, - ); }); - it("opens and closes the 'Create Fund' modal", async () => { + + it('should redirect to fallback URL if URL params are undefined', async () => { render( - - + + - {} + + } /> + } + /> + - - + + , ); - await wait(); - await waitFor(() => { - expect(screen.getByTestId('createFundBtn')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createFundBtn')); await waitFor(() => { - return expect( - screen.findByTestId('createFundModalCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByTestId('paramsError')).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('setTaxDeductibleSwitch')); - userEvent.click(screen.getByTestId('setDefaultSwitch')); - userEvent.click(screen.getByTestId('createFundModalCloseBtn')); - await waitForElementToBeRemoved(() => - screen.queryByTestId('createFundModalCloseBtn'), - ); }); - it('noFunds to be in the document', async () => { - render( - - - - - {} - - - - , + + it('open and close Create Fund modal', async () => { + renderOrganizationFunds(link1); + + const createFundBtn = await screen.findByTestId('createFundBtn'); + expect(createFundBtn).toBeInTheDocument(); + userEvent.click(createFundBtn); + + await waitFor(() => + expect(screen.getAllByText(translations.fundCreate)).toHaveLength(3), ); - await wait(); - await waitFor(() => { - expect(screen.getByText(translations.noFundsFound)).toBeInTheDocument(); - }); - }); - it('creates a new fund', async () => { - render( - - - - - {} - - - - , + userEvent.click(screen.getByTestId('fundModalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('fundModalCloseBtn')).toBeNull(), ); - await wait(); - await waitFor(() => { - expect(screen.getByTestId('createFundBtn')).toBeInTheDocument(); - }); - userEvent.click(screen.getByTestId('createFundBtn')); + }); + + it('open and close update fund modal', async () => { + renderOrganizationFunds(link1); + await waitFor(() => { - return expect( - screen.findByTestId('createFundModalCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByTestId('searchByName')).toBeInTheDocument(); }); - userEvent.type( - screen.getByPlaceholderText(translations.enterfundName), - formData.fundName, + + const editFundBtn = await screen.findAllByTestId('editFundBtn'); + await waitFor(() => expect(editFundBtn[0]).toBeInTheDocument()); + userEvent.click(editFundBtn[0]); + + await waitFor(() => + expect( + screen.getAllByText(translations.fundUpdate)[0], + ).toBeInTheDocument(), ); - userEvent.type( - screen.getByPlaceholderText(translations.enterfundId), - formData.fundRef, + userEvent.click(screen.getByTestId('fundModalCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('fundModalCloseBtn')).toBeNull(), ); - userEvent.click(screen.getByTestId('setTaxDeductibleSwitch')); - userEvent.click(screen.getByTestId('setDefaultSwitch')); - userEvent.click(screen.getByTestId('setTaxDeductibleSwitch')); - userEvent.click(screen.getByTestId('setDefaultSwitch')); - await wait(); - userEvent.click(screen.getByTestId('createFundFormSubmitBtn')); - - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.fundCreated); - }); }); - it('updates fund successfully', async () => { - render( - - - - - {} - - - - , + + it('open and closes delete fund modal', async () => { + renderOrganizationFunds(link1); + + const deleteFundBtn = await screen.findAllByTestId('deleteFundBtn'); + await waitFor(() => expect(deleteFundBtn[0]).toBeInTheDocument()); + userEvent.click(deleteFundBtn[0]); + + await waitFor(() => + expect(screen.getByText(translations.fundDelete)).toBeInTheDocument(), ); - await wait(); - userEvent.click(screen.getAllByTestId('editFundBtn')[0]); - await wait(); - userEvent.clear(screen.getByTestId('fundNameInput')); - userEvent.clear(screen.getByTestId('fundIdInput')); - userEvent.type(screen.getByTestId('fundNameInput'), 'Test Fund'); - userEvent.type(screen.getByTestId('fundIdInput'), '1'); - expect(screen.getByTestId('taxDeductibleSwitch')).toBeInTheDocument(); - expect(screen.getByTestId('defaultSwitch')).toBeInTheDocument(); - expect(screen.getByTestId('archivedSwitch')).toBeInTheDocument(); - expect(screen.getByTestId('updateFormBtn')).toBeInTheDocument(); - userEvent.click(screen.getByTestId('taxDeductibleSwitch')); - userEvent.click(screen.getByTestId('defaultSwitch')); - userEvent.click(screen.getByTestId('archivedSwitch')); - await wait(); - userEvent.click(screen.getByTestId('updateFormBtn')); - await wait(); - await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.fundUpdated); - }); - }); - it('toast error on unsuccessful fund creation', async () => { - render( - - - - - {} - - - - , + userEvent.click(screen.getByTestId('deleteFundCloseBtn')); + await waitFor(() => + expect(screen.queryByTestId('deleteFundCloseBtn')).toBeNull(), ); - await wait(); - await waitFor(() => { - expect(screen.getByTestId('createFundBtn')).toBeInTheDocument(); + }); + + it('Search the Funds list by name', async () => { + renderOrganizationFunds(link1); + const searchField = await screen.findByTestId('searchByName'); + fireEvent.change(searchField, { + target: { value: '2' }, }); - userEvent.click(screen.getByTestId('createFundBtn')); + await waitFor(() => { - return expect( - screen.findByTestId('createFundModalCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Fund 2')).toBeInTheDocument(); + expect(screen.queryByText('Fund 1')).toBeNull(); }); - userEvent.type( - screen.getByPlaceholderText(translations.enterfundName), - formData.fundName, - ); - userEvent.type( - screen.getByPlaceholderText(translations.enterfundId), - formData.fundRef, - ); - userEvent.click(screen.getByTestId('createFundFormSubmitBtn')); + }); + it('should render the Fund screen with error', async () => { + renderOrganizationFunds(link2); await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getByTestId('errorMsg')).toBeInTheDocument(); }); }); - it('toast error on unsuccessful fund update', async () => { - render( - - - - - {} - - - - , + + it('renders the empty fund component', async () => { + renderOrganizationFunds(link3); + await waitFor(() => + expect(screen.getByText(translations.noFundsFound)).toBeInTheDocument(), ); - await wait(); - await waitFor(() => { - expect(screen.getAllByTestId('editFundBtn')[0]).toBeInTheDocument(); - }); - userEvent.click(screen.getAllByTestId('editFundBtn')[0]); + }); + + it('Sort the Pledges list by Latest created Date', async () => { + renderOrganizationFunds(link1); + + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); + + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('createdAt_DESC')); + await waitFor(() => { - return expect( - screen.findByTestId('editFundModalCloseBtn'), - ).resolves.toBeInTheDocument(); + expect(screen.getByText('Fund 1')).toBeInTheDocument(); + expect(screen.queryByText('Fund 2')).toBeInTheDocument(); }); - userEvent.type( - screen.getByPlaceholderText(translations.enterfundName), - 'Test Fund Updated', - ); - userEvent.click(screen.getByTestId('updateFormBtn')); await waitFor(() => { - expect(toast.error).toHaveBeenCalled(); + expect(screen.getAllByTestId('createdOn')[0]).toHaveTextContent( + '22/06/2024', + ); }); }); - it('redirects to campaign screen when clicked on fund name', async () => { - render( - - - - - {} - - - - , - ); - await wait(); + + it('Sort the Pledges list by Earliest created Date', async () => { + renderOrganizationFunds(link1); + + const sortBtn = await screen.findByTestId('filter'); + expect(sortBtn).toBeInTheDocument(); + + fireEvent.click(sortBtn); + fireEvent.click(screen.getByTestId('createdAt_ASC')); + await waitFor(() => { - expect(screen.getAllByTestId('fundName')[0]).toBeInTheDocument(); + expect(screen.getByText('Fund 1')).toBeInTheDocument(); + expect(screen.queryByText('Fund 2')).toBeInTheDocument(); }); - userEvent.click(screen.getAllByTestId('fundName')[0]); + await waitFor(() => { - expect(mockNavigate).toBeCalledWith('/orgfundcampaign/undefined/1'); + expect(screen.getAllByTestId('createdOn')[0]).toHaveTextContent( + '21/06/2024', + ); }); }); - it('delete fund succesfully', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - userEvent.click(screen.getAllByTestId('editFundBtn')[0]); - await wait(); - userEvent.click(screen.getByTestId('fundDeleteModalDeleteBtn')); + + it('Click on Fund Name', async () => { + renderOrganizationFunds(link1); + + const fundName = await screen.findAllByTestId('fundName'); + expect(fundName[0]).toBeInTheDocument(); + fireEvent.click(fundName[0]); + await waitFor(() => { - expect(toast.success).toBeCalledWith(translations.fundDeleted); + expect(screen.getByTestId('campaignScreen')).toBeInTheDocument(); }); }); - it('throws error on unsuccessful fund deletion', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - userEvent.click(screen.getAllByTestId('editFundBtn')[0]); - await wait(); - userEvent.click(screen.getByTestId('fundDeleteModalDeleteBtn')); + + it('Click on View Campaign', async () => { + renderOrganizationFunds(link1); + + const viewBtn = await screen.findAllByTestId('viewBtn'); + expect(viewBtn[0]).toBeInTheDocument(); + fireEvent.click(viewBtn[0]); + await waitFor(() => { - expect(toast.error).toBeCalled(); + expect(screen.getByTestId('campaignScreen')).toBeInTheDocument(); }); }); - it('search funds by name', async () => { - render( - - - - - {} - - - - , - ); - await wait(); - userEvent.click(screen.getAllByTestId('editFundBtn')[0]); - await wait(); - userEvent.click(screen.getByTestId('editFundModalCloseBtn')); - await wait(); - userEvent.type(screen.getByTestId('searchFullName'), 'Funndds'); - await wait(); - userEvent.click(screen.getByTestId('searchBtn')); - }); }); diff --git a/src/screens/OrganizationFunds/OrganizationFunds.tsx b/src/screens/OrganizationFunds/OrganizationFunds.tsx index 2232e04721..f85f6ed7b9 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.tsx @@ -1,87 +1,93 @@ -/*eslint-disable*/ -import { useMutation, useQuery } from '@apollo/client'; -import { Search, WarningAmberRounded } from '@mui/icons-material'; +import { useQuery } from '@apollo/client'; +import { Search, Sort, WarningAmberRounded } from '@mui/icons-material'; +import { Stack } from '@mui/material'; import { - CREATE_FUND_MUTATION, - REMOVE_FUND_MUTATION, - UPDATE_FUND_MUTATION, -} from 'GraphQl/Mutations/FundMutation'; -import Loader from 'components/Loader/Loader'; -import { useState, type ChangeEvent } from 'react'; -import { Button, Col, Dropdown, Form, Row } from 'react-bootstrap'; + DataGrid, + type GridCellParams, + type GridColDef, +} from '@mui/x-data-grid'; +import { Button, Dropdown, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import { useNavigate, useParams } from 'react-router-dom'; -import { toast } from 'react-toastify'; -import type { - InterfaceCreateFund, - InterfaceFundInfo, - InterfaceQueryOrganizationFunds, -} from 'utils/interfaces'; -import FundCreateModal from './FundCreateModal'; -import FundUpdateModal from './FundUpdateModal'; -import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined'; -import styles from './OrganizationFunds.module.css'; -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - styled, - tableCellClasses, -} from '@mui/material'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import React, { useCallback, useMemo, useState } from 'react'; import dayjs from 'dayjs'; +import Loader from 'components/Loader/Loader'; +import FundModal from './FundModal'; +import FundDeleteModal from './FundDeleteModal'; import { FUND_LIST } from 'GraphQl/Queries/fundQueries'; +import styles from './OrganizationFunds.module.css'; +import type { InterfaceFundInfo } from 'utils/interfaces'; -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - backgroundColor: ['#31bb6b', '!important'], - color: theme.palette.common.white, +const dataGridStyle = { + '&.MuiDataGrid-root .MuiDataGrid-cell:focus-within': { + outline: 'none !important', }, - [`&.${tableCellClasses.body}`]: { - fontSize: 14, + '&.MuiDataGrid-root .MuiDataGrid-columnHeader:focus-within': { + outline: 'none', }, -})); - -const StyledTableRow = styled(TableRow)(() => ({ - '&:last-child td, &:last-child th': { - border: 0, + '& .MuiDataGrid-row:hover': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor: 'transparent', + }, + '& .MuiDataGrid-root': { + borderRadius: '0.5rem', }, -})); + '& .MuiDataGrid-main': { + borderRadius: '0.5rem', + }, +}; + +enum ModalState { + SAME = 'same', + DELETE = 'delete', +} -/** - * This component handles displaying and managing organization funds. - * It allows users to create, update, delete, and filter funds. - * - * @returns A JSX.Element that renders the organization funds page. - */ const organizationFunds = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'funds', }); const { t: tCommon } = useTranslation('common'); - const { orgId: currentUrl } = useParams(); + const { orgId } = useParams(); const navigate = useNavigate(); - const [fundCreateModalIsOpen, setFundCreateModalIsOpen] = - useState(false); - const [fundUpdateModalIsOpen, setFundUpdateModalIsOpen] = - useState(false); - const [taxDeductible, setTaxDeductible] = useState(true); - const [isArchived, setIsArchived] = useState(false); - const [isDefault, setIsDefault] = useState(false); + if (!orgId) { + return ; + } + const [fund, setFund] = useState(null); - const [formState, setFormState] = useState({ - fundName: '', - fundRef: '', + const [searchTerm, setSearchTerm] = useState(''); + const [sortBy, setSortBy] = useState<'createdAt_ASC' | 'createdAt_DESC'>( + 'createdAt_DESC', + ); + + const [modalState, setModalState] = useState<{ + [key in ModalState]: boolean; + }>({ + [ModalState.SAME]: false, + [ModalState.DELETE]: false, }); + const [fundModalMode, setFundModalMode] = useState<'edit' | 'create'>( + 'create', + ); + + const openModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: true })); + + const closeModal = (modal: ModalState): void => + setModalState((prevState) => ({ ...prevState, [modal]: false })); + + const handleOpenModal = useCallback( + (fund: InterfaceFundInfo | null, mode: 'edit' | 'create'): void => { + setFund(fund); + setFundModalMode(mode); + openModal(ModalState.SAME); + }, + [openModal], + ); - /** - * Fetches the list of funds by organization. - */ const { data: fundData, loading: fundLoading, @@ -89,194 +95,31 @@ const organizationFunds = (): JSX.Element => { refetch: refetchFunds, }: { data?: { - fundsByOrganization: InterfaceQueryOrganizationFunds[]; + fundsByOrganization: InterfaceFundInfo[]; }; loading: boolean; error?: Error | undefined; - refetch: any; + refetch: () => void; } = useQuery(FUND_LIST, { variables: { - organizationId: currentUrl, + organizationId: orgId, + filter: searchTerm, + orderBy: sortBy, }, }); - const [fullName, setFullName] = useState(''); - - /** - * Handles the search functionality by refetching funds based on the filter. - */ - const handleSearch = (): void => { - refetchFunds({ organizationId: currentUrl, filter: fullName }); - }; - - const [createFund] = useMutation(CREATE_FUND_MUTATION); - const [updateFund] = useMutation(UPDATE_FUND_MUTATION); - const [deleteFund] = useMutation(REMOVE_FUND_MUTATION); - - /** - * Toggles the visibility of the create fund modal. - */ - const showCreateModal = (): void => { - setFundCreateModalIsOpen(!fundCreateModalIsOpen); - }; - - /** - * Hides the create fund modal. - */ - const hideCreateModal = (): void => { - setFundCreateModalIsOpen(!fundCreateModalIsOpen); - }; - - /** - * Toggles the visibility of the update fund modal. - */ - const showUpdateModal = (): void => { - setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); - }; - - /** - * Hides the update fund modal. - */ - const hideUpdateModal = (): void => { - setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); - }; - - /** - * Toggles the visibility of the delete fund modal. - */ - const toggleDeleteModal = (): void => { - setFundUpdateModalIsOpen(!fundUpdateModalIsOpen); - }; - - /** - * Handles the edit action by setting the form state and showing the update modal. - * - * @param fund - The fund information to be edited. - */ - const handleEditClick = (fund: InterfaceFundInfo): void => { - setFormState({ - fundName: fund.name, - fundRef: fund.refrenceNumber, - }); - setTaxDeductible(fund.taxDeductible); - setIsArchived(fund.isArchived); - setIsDefault(fund.isDefault); - setFund(fund); - showUpdateModal(); - }; - - /** - * Handles the creation of a new fund. - * - * @param e - The form submission event. - */ - const createFundHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - await createFund({ - variables: { - name: formState.fundName, - refrenceNumber: formState.fundRef, - organizationId: currentUrl, - taxDeductible: taxDeductible, - isArchived: isArchived, - isDefault: isDefault, - }, - }); - - setFormState({ - fundName: '', - fundRef: '', - }); - toast.success(t('fundCreated')); - refetchFunds(); - hideCreateModal(); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; - - /** - * Handles the updating of an existing fund. - * - * @param e - The form submission event. - */ - const updateFundHandler = async ( - e: ChangeEvent, - ): Promise => { - e.preventDefault(); - try { - const updatedFields: { [key: string]: any } = {}; - if (formState.fundName != fund?.name) { - updatedFields.name = formState.fundName; - } - if (formState.fundRef != fund?.refrenceNumber) { - updatedFields.refrenceNumber = formState.fundRef; - } - if (taxDeductible != fund?.taxDeductible) { - updatedFields.taxDeductible = taxDeductible; - } - if (isArchived != fund?.isArchived) { - updatedFields.isArchived = isArchived; - } - if (isDefault != fund?.isDefault) { - updatedFields.isDefault = isDefault; - } - - await updateFund({ - variables: { - id: fund?._id, - ...updatedFields, - }, - }); - setFormState({ - fundName: '', - fundRef: '', - }); - refetchFunds(); - hideUpdateModal(); - toast.success(t('fundUpdated')); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; + const handleDeleteClick = useCallback( + (fund: InterfaceFundInfo): void => { + setFund(fund); + openModal(ModalState.DELETE); + }, + [openModal], + ); - /** - * Handles the deletion of a fund. - */ - const deleteFundHandler = async (): Promise => { - try { - await deleteFund({ - variables: { - id: fund?._id, - }, - }); - refetchFunds(); - toggleDeleteModal(); - toast.success(t('fundDeleted')); - } catch (error: unknown) { - if (error instanceof Error) { - toast.error(error.message); - console.log(error.message); - } - } - }; + const funds = useMemo(() => fundData?.fundsByOrganization ?? [], [fundData]); - /** - * Handles clicking on a fund to navigate to its campaign page. - * - * @param fundId - The ID of the fund to navigate to. - */ - const handleClick = (fundId: String) => { - navigate(`/orgfundcampaign/${currentUrl}/${fundId}`); + const handleClick = (fundId: string): void => { + navigate(`/orgfundcampaign/${orgId}/${fundId}`); }; if (fundLoading) { @@ -297,187 +140,250 @@ const organizationFunds = (): JSX.Element => { ); } + const columns: GridColDef[] = [ + { + field: 'id', + headerName: 'Sr. No.', + flex: 1, + minWidth: 100, + align: 'center', + headerAlign: 'center', + headerClassName: `${styles.tableHeader}`, + sortable: false, + renderCell: (params: GridCellParams) => { + return
    {params.row.id}
    ; + }, + }, + { + field: 'fundName', + headerName: 'Fund Name', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( +
    handleClick(params.row._id as string)} + > + {params.row.name} +
    + ); + }, + }, + { + field: 'createdBy', + headerName: 'Created By', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return params.row.creator.firstName + ' ' + params.row.creator.lastName; + }, + }, + { + field: 'createdOn', + headerName: 'Created On', + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + flex: 2, + renderCell: (params: GridCellParams) => { + return ( +
    + {dayjs(params.row.createdAt).format('DD/MM/YYYY')} +
    + ); + }, + }, + { + field: 'status', + headerName: 'Status', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return params.row.isArchived ? 'Archived' : 'Active'; + }, + }, + { + field: 'action', + headerName: 'Action', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + <> + + + + ); + }, + }, + { + field: 'assocCampaigns', + headerName: 'Associated Campaigns', + flex: 2, + align: 'center', + minWidth: 100, + headerAlign: 'center', + sortable: false, + headerClassName: `${styles.tableHeader}`, + renderCell: (params: GridCellParams) => { + return ( + + ); + }, + }, + ]; + return ( - <> - -
    -
    -
    - ) => { - setFullName(e.target.value); - }} - data-testid="searchFullName" - /> - +
    +
    +
    + + - - -
    -
    -
    - - - - {tCommon('filter')} - - -
    -
    - -
    -
    + {t('createdEarliest')} + + + +
    +
    +
    - -
    -
    - {fundData?.fundsByOrganization && - fundData.fundsByOrganization.length > 0 ? ( -
    - - - - - # - - {t('fundName')} - - - {t('createdBy')} - - - {t('createdOn')} - - - {t('status')} - - - {t('manageFund')} - - - - - {fundData.fundsByOrganization.map( - (fund: any, index: number) => ( - - - {index + 1} - - handleClick(fund._id)} - > - - {fund.name} - - - - {fund.creator.firstName} {fund.creator.lastName} - - - {dayjs(fund.createdAt).format('DD/MM/YYYY')} - - - - - - - - - ), - )} - -
    -
    -
    - ) : ( -
    -
    {t('noFundsFound')}
    -
    - )} -
    - {/* - - {/*
    - + + row._id} + components={{ + NoRowsOverlay: () => ( + + {t('noFundsFound')} + + ), + }} + sx={dataGridStyle} + getRowClassName={() => `${styles.rowBackground}`} + autoHeight + rowHeight={65} + rows={funds.map((fund, index) => ({ + id: index + 1, + ...fund, + }))} + columns={columns} + isRowSelectable={() => false} + /> + closeModal(ModalState.SAME)} + refetchFunds={refetchFunds} + fund={fund} + orgId={orgId} + mode={fundModalMode} + /> + + closeModal(ModalState.DELETE)} + fund={fund} + refetchFunds={refetchFunds} + /> +
    ); }; diff --git a/src/screens/OrganizationFunds/OrganizationFundsMocks.ts b/src/screens/OrganizationFunds/OrganizationFundsMocks.ts index cb1e07d78f..cd03dc7f31 100644 --- a/src/screens/OrganizationFunds/OrganizationFundsMocks.ts +++ b/src/screens/OrganizationFunds/OrganizationFundsMocks.ts @@ -10,21 +10,23 @@ export const MOCKS = [ request: { query: FUND_LIST, variables: { - id: undefined, + organizationId: 'orgId', + orderBy: 'createdAt_DESC', + filter: '', }, }, result: { data: { fundsByOrganization: [ { - _id: '1', + _id: 'fundId', name: 'Fund 1', - refrenceNumber: '123', + refrenceNumber: '1111', taxDeductible: true, isArchived: false, isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId1', + createdAt: '2024-06-22', + organizationId: 'orgId', creator: { _id: 'creatorId1', firstName: 'John', @@ -32,14 +34,14 @@ export const MOCKS = [ }, }, { - _id: '99', - name: 'Funndds', - refrenceNumber: '1234', + _id: 'fundId2', + name: 'Fund 2', + refrenceNumber: '2222', taxDeductible: true, isArchived: true, isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId1', + createdAt: '2024-06-21', + organizationId: 'orgId', creator: { _id: 'creatorId1', firstName: 'John', @@ -50,108 +52,27 @@ export const MOCKS = [ }, }, }, - { - request: { - query: CREATE_FUND_MUTATION, - variables: { - name: 'Test Fund', - refrenceNumber: '1', - taxDeductible: true, - isArchived: false, - isDefault: false, - organizationId: undefined, - }, - }, - result: { - data: { - createFund: { - _id: '3', - }, - }, - }, - }, - { - request: { - query: UPDATE_FUND_MUTATION, - variables: { - id: '1', - name: 'Test Fund', - refrenceNumber: '1', - taxDeductible: false, - isArchived: true, - isDefault: true, - }, - }, - result: { - data: { - updateFund: { - _id: '1', - }, - }, - }, - }, - { - request: { - query: REMOVE_FUND_MUTATION, - variables: { - id: '1', - }, - }, - result: { - data: { - removeFund: { - _id: '1', - }, - }, - }, - }, -]; -export const NO_FUNDS = [ - { - request: { - query: FUND_LIST, - variables: { - id: undefined, - }, - }, - result: { - data: { - fundsByOrganization: [], - }, - }, - }, -]; -export const MOCKS_ERROR_ORGANIZATIONS_FUNDS = [ - { - request: { - query: FUND_LIST, - variables: { - organizationId: '1', - }, - }, - error: new Error('Mock graphql error'), - }, -]; -export const MOCKS_ERROR_CREATE_FUND = [ { request: { query: FUND_LIST, variables: { - id: undefined, + organizationId: 'orgId', + orderBy: 'createdAt_ASC', + filter: '', }, }, result: { data: { fundsByOrganization: [ { - _id: '1', - name: 'Fund 1', - refrenceNumber: '123', + _id: 'fundId', + name: 'Fund 2', + refrenceNumber: '2222', taxDeductible: true, - isArchived: false, + isArchived: true, isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId1', + createdAt: '2024-06-21', + organizationId: 'orgId', creator: { _id: 'creatorId1', firstName: 'John', @@ -159,17 +80,17 @@ export const MOCKS_ERROR_CREATE_FUND = [ }, }, { - _id: '2', - name: 'Fund 2', - refrenceNumber: '456', - taxDeductible: false, + _id: 'fundId2', + name: 'Fund 1', + refrenceNumber: '1111', + taxDeductible: true, isArchived: false, isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId2', + createdAt: '2024-06-22', + organizationId: 'orgId', creator: { - _id: 'creatorId2', - firstName: 'Jane', + _id: 'creatorId1', + firstName: 'John', lastName: 'Doe', }, }, @@ -177,62 +98,33 @@ export const MOCKS_ERROR_CREATE_FUND = [ }, }, }, - { - request: { - query: CREATE_FUND_MUTATION, - variables: { - name: 'Fund 3', - refrenceNumber: '789', - taxDeductible: true, - isArchived: false, - isDefault: false, - organizationId: undefined, - }, - }, - error: new Error('Mock graphql error'), - }, -]; -export const MOCKS_ERROR_UPDATE_FUND = [ { request: { query: FUND_LIST, variables: { - id: undefined, + organizationId: 'orgId', + orderBy: 'createdAt_DESC', + filter: '2', }, }, result: { data: { fundsByOrganization: [ { - _id: '1', - name: 'Fund 1', - refrenceNumber: '123', + _id: 'fundId', + name: 'Fund 2', + refrenceNumber: '2222', taxDeductible: true, - isArchived: false, + isArchived: true, isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId1', + createdAt: '2024-06-21', + organizationId: 'orgId', creator: { _id: 'creatorId1', firstName: 'John', lastName: 'Doe', }, }, - { - _id: '2', - name: 'Fund 2', - refrenceNumber: '456', - taxDeductible: false, - isArchived: false, - isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId2', - creator: { - _id: 'creatorId2', - firstName: 'Jane', - lastName: 'Doe', - }, - }, ], }, }, @@ -241,18 +133,18 @@ export const MOCKS_ERROR_UPDATE_FUND = [ request: { query: CREATE_FUND_MUTATION, variables: { - name: 'Fund 3', - refrenceNumber: '789', - taxDeductible: true, + name: 'Fund 2', + refrenceNumber: '2222', + taxDeductible: false, isArchived: false, - isDefault: false, - organizationId: undefined, + isDefault: true, + organizationId: 'orgId', }, }, result: { data: { createFund: { - _id: '3', + _id: '2222', }, }, }, @@ -261,106 +153,102 @@ export const MOCKS_ERROR_UPDATE_FUND = [ request: { query: UPDATE_FUND_MUTATION, variables: { - id: undefined, - name: 'Fund 1', - refrenceNumber: '789', - taxDeductible: true, - isArchived: false, - isDefault: false, + id: 'fundId', + name: 'Fund 2', + refrenceNumber: '2222', + taxDeductible: false, + isArchived: true, + isDefault: true, + }, + }, + result: { + data: { + updateFund: { + _id: 'fundId', + }, }, }, - error: new Error('Mock graphql error'), }, -]; -export const MOCKS_ERROR_REMOVE_FUND = [ { request: { - query: FUND_LIST, + query: REMOVE_FUND_MUTATION, variables: { - id: undefined, + id: 'fundId', }, }, result: { data: { - fundsByOrganization: [ - { - _id: '3', - name: 'Fund 1', - refrenceNumber: '123', - taxDeductible: true, - isArchived: false, - isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId1', - creator: { - _id: 'creatorId1', - firstName: 'John', - lastName: 'Doe', - }, - }, - { - _id: '2', - name: 'Fund 2', - refrenceNumber: '456', - taxDeductible: false, - isArchived: false, - isDefault: false, - createdAt: '2021-07-01T00:00:00.000Z', - organizationId: 'organizationId2', - creator: { - _id: 'creatorId2', - firstName: 'Jane', - lastName: 'Doe', - }, - }, - ], + removeFund: { + _id: 'fundId', + }, }, }, }, +]; + +export const NO_FUNDS = [ { request: { - query: CREATE_FUND_MUTATION, + query: FUND_LIST, variables: { - name: 'Fund 3', - refrenceNumber: '789', - taxDeductible: true, - isArchived: false, - isDefault: false, - organizationId: undefined, + organizationId: 'orgId', + orderBy: 'createdAt_DESC', + filter: '', }, }, result: { data: { - createFund: { - _id: '3', - }, + fundsByOrganization: [], }, }, }, +]; + +export const MOCKS_ERROR = [ { request: { - query: UPDATE_FUND_MUTATION, + query: FUND_LIST, variables: { - id: undefined, - name: 'Fund 1', - taxDeductible: true, - isArchived: false, - isDefault: false, + organizationId: 'orgId', + orderBy: 'createdAt_DESC', + filter: '', }, }, - result: { - data: { - updateFund: { - _id: '1', - }, + error: new Error('Mock graphql error'), + }, + { + request: { + query: CREATE_FUND_MUTATION, + variables: { + name: 'Fund 2', + refrenceNumber: '2222', + taxDeductible: false, + isArchived: false, + isDefault: true, + organizationId: 'orgId', }, }, + error: new Error('Mock graphql error'), }, { request: { query: REMOVE_FUND_MUTATION, variables: { - id: undefined, + id: 'fundId', + }, + }, + error: new Error('Mock graphql error'), + }, + { + request: { + query: UPDATE_FUND_MUTATION, + variables: { + id: 'fundId', + name: 'Fund 2', + refrenceNumber: '2222', + taxDeductible: false, + isArchived: true, + isDefault: true, }, }, error: new Error('Mock graphql error'), diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts index d312002072..c4e15d0718 100644 --- a/src/utils/interfaces.ts +++ b/src/utils/interfaces.ts @@ -226,19 +226,7 @@ export interface InterfaceQueryOrganizationAdvertisementListItem { totalCount: number; }; } -export interface InterfaceQueryOrganizationFunds { - fundsByOrganization: { - _id: string; - name: string; - refrenceNumber: string; - taxDeductible: boolean; - isArchived: boolean; - isDefault: boolean; - createdAt: string; - organizationId: string; - creator: { _id: string; firstName: string; lastName: string }; - }[]; -} + export interface InterfaceQueryOrganizationFundCampaigns { campaigns: { _id: string; @@ -251,6 +239,9 @@ export interface InterfaceQueryOrganizationFundCampaigns { }[]; } export interface InterfaceQueryFundCampaignsPledges { + name: string; + fundingGoal: number; + currency: string; startDate: Date; endDate: Date; pledges: { @@ -259,7 +250,7 @@ export interface InterfaceQueryFundCampaignsPledges { currency: string; endDate: string; startDate: string; - users: InterfacePledgeVolunteer[]; + users: InterfacePledger[]; }[]; } export interface InterfaceFundInfo { @@ -270,6 +261,8 @@ export interface InterfaceFundInfo { isArchived: boolean; isDefault: boolean; createdAt: string; + organizationId: string; + creator: { _id: string; firstName: string; lastName: string }; } export interface InterfaceCampaignInfo { _id: string; @@ -286,7 +279,7 @@ export interface InterfacePledgeInfo { currency: string; endDate: string; startDate: string; - users: InterfacePledgeVolunteer[]; + users: InterfacePledger[]; } export interface InterfaceQueryOrganizationEventListItem { _id: string; @@ -383,6 +376,9 @@ export interface InterfaceAddress { export interface InterfaceCreateFund { fundName: string; fundRef: string; + isDefault: boolean; + isArchived: boolean; + taxDeductible: boolean; } export interface InterfacePostCard { @@ -421,16 +417,9 @@ export interface InterfacePostCard { }[]; fetchPosts: () => void; } -export interface InterfaceCreateCampaign { - campaignName: string; - campaignCurrency: string; - campaignGoal: number; - campaignStartDate: Date; - campaignEndDate: Date; -} export interface InterfaceCreatePledge { - pledgeUsers: InterfacePledgeVolunteer[]; + pledgeUsers: InterfacePledger[]; pledgeAmount: number; pledgeCurrency: string; pledgeStartDate: Date; @@ -452,7 +441,7 @@ export interface InterfaceQueryMembershipRequestsListItem { }[]; } -export interface InterfacePledgeVolunteer { +export interface InterfacePledger { _id: string; firstName: string; lastName: string; From e1ed349677340fc63799e465d8ffaf1d6ba5a4ca Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 24 Jul 2024 23:11:56 +0530 Subject: [PATCH 5/8] lines reduced --- src/components/OrgPostCard/OrgPostCard.tsx | 42 ------------ .../UsersTableItem/UsersTableItem.tsx | 64 ------------------- 2 files changed, 106 deletions(-) diff --git a/src/components/OrgPostCard/OrgPostCard.tsx b/src/components/OrgPostCard/OrgPostCard.tsx index 2fb752f444..be71622c7c 100644 --- a/src/components/OrgPostCard/OrgPostCard.tsx +++ b/src/components/OrgPostCard/OrgPostCard.tsx @@ -15,7 +15,6 @@ import convertToBase64 from 'utils/convertToBase64'; import { errorHandler } from 'utils/errorHandler'; import type { InterfacePostForm } from 'utils/interfaces'; import styles from './OrgPostCard.module.css'; - interface InterfaceOrgPostCardProps { key: string; id: string; @@ -26,39 +25,6 @@ interface InterfaceOrgPostCardProps { postVideo: string | null; pinned: boolean; } - -/** - * The `orgPostCard` component represents a card that displays information about a post - * in an organization. It shows the post's title, information, author, and optionally, - * a photo or video. Users can interact with the card to view more details, edit the post, - * delete the post, or pin/unpin the post. The component uses Apollo Client for GraphQL - * mutations, React Bootstrap for styling, and React hooks for state management. - * - * @param props - The properties passed to the component. - * @param key - A unique key for the component. - * @param id - The ID of the post. - * @param postTitle - The title of the post. - * @param postInfo - The information or description of the post. - * @param postAuthor - The author of the post. - * @param postPhoto - The URL of the post's photo (if any). - * @param postVideo - The URL of the post's video (if any). - * @param pinned - A boolean indicating whether the post is pinned. - * @returns A JSX element representing the post card. - * - * @example - * ```tsx - * - * ``` - */ export default function orgPostCard( props: InterfaceOrgPostCardProps, ): JSX.Element { @@ -79,14 +45,6 @@ export default function orgPostCard( const [playing, setPlaying] = useState(false); const videoRef = useRef(null); const [toggle] = useMutation(TOGGLE_PINNED_POST); - - /** - * Toggles the pinned status of a post. - * - * @param id - The ID of the post. - * @param pinned - The current pinned status of the post. - * @returns A promise that resolves when the pin status is toggled. - */ const togglePostPin = async (id: string, pinned: boolean): Promise => { try { const { data } = await toggle({ diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx index f7348f14b2..9d6ec65187 100644 --- a/src/components/UsersTableItem/UsersTableItem.tsx +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -14,24 +14,12 @@ import { toast } from 'react-toastify'; import { errorHandler } from 'utils/errorHandler'; import type { InterfaceQueryUserListItem } from 'utils/interfaces'; import styles from './UsersTableItem.module.css'; - -/** - * Props for the `UsersTableItem` component. - */ type Props = { user: InterfaceQueryUserListItem; index: number; loggedInUserId: string; resetAndRefetch: () => void; }; - -/** - * A component that displays a table row for a user with their joined and blocked organizations. - * Allows searching, viewing, and managing user roles within organizations. - * - * @param props - The props for the component. - * @returns A JSX element displaying user details and modals for managing organizations. - */ const UsersTableItem = (props: Props): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); const { t: tCommon } = useTranslation('common'); @@ -59,11 +47,6 @@ const UsersTableItem = (props: Props): JSX.Element => { const [removeUser] = useMutation(REMOVE_MEMBER_MUTATION); const [updateUserInOrgType] = useMutation(UPDATE_USER_ROLE_IN_ORG_MUTATION); const navigate = useNavigate(); - - /** - * Handles user removal from an organization. - * Confirms the removal via a modal and performs the mutation if confirmed. - */ const confirmRemoveUser = async (): Promise => { try { const { data } = await removeUser({ @@ -80,11 +63,6 @@ const UsersTableItem = (props: Props): JSX.Element => { errorHandler(t, error); } }; - - /** - * Handles the change of user role in an organization. - * @param e - The event object from the form select change. - */ const changeRoleInOrg = async ( e: React.ChangeEvent, ): Promise => { @@ -106,28 +84,14 @@ const UsersTableItem = (props: Props): JSX.Element => { errorHandler(t, error); } }; - - /** - * Navigates to the organization dashboard. - * @param _id - The ID of the organization to navigate to. - */ function goToOrg(_id: string): void { const url = '/orgdash/' + _id; window.location.replace(url); navigate(url); } - - /** - * Displays a toast message indicating that the profile page is coming soon. - */ function handleCreator(): void { toast.success('Profile Page Coming Soon !'); } - - /** - * Searches for joined organizations by name. - * @param value - The search input value. - */ const searchJoinedOrgs = (value: string): void => { setSearchByNameJoinedOrgs(value); if (value == '') { @@ -139,11 +103,6 @@ const UsersTableItem = (props: Props): JSX.Element => { setJoinedOrgs(filteredOrgs); } }; - - /** - * Searches for organizations that have blocked the user by name. - * @param value - The search input value. - */ const searchOrgsBlockedBy = (value: string): void => { setSearchByNameOrgsBlockedBy(value); if (value == '') { @@ -155,11 +114,6 @@ const UsersTableItem = (props: Props): JSX.Element => { setOrgsBlockedBy(filteredOrgs); } }; - - /** - * Handles the Enter key press event for searching joined organizations. - * @param e - The keyboard event object. - */ const handleSearchJoinedOrgs = ( e: React.KeyboardEvent, ): void => { @@ -168,11 +122,6 @@ const UsersTableItem = (props: Props): JSX.Element => { searchJoinedOrgs(value); } }; - - /** - * Handles the Enter key press event for searching organizations that blocked the user. - * @param e - The keyboard event object. - */ const handleSearchByOrgsBlockedBy = ( e: React.KeyboardEvent, ): void => { @@ -181,31 +130,18 @@ const UsersTableItem = (props: Props): JSX.Element => { searchOrgsBlockedBy(value); } }; - - /** - * Handles the search button click event for joined organizations. - */ const handleSearchButtonClickJoinedOrgs = (): void => { const inputValue = (document.getElementById('orgname-joined-orgs') as HTMLInputElement) ?.value || ''; searchJoinedOrgs(inputValue); }; - - /** - * Handles the search button click event for organizations that blocked the user. - */ const handleSearchButtonClickOrgsBlockedBy = (): void => { const inputValue = (document.getElementById('orgname-blocked-by') as HTMLInputElement) ?.value || ''; searchOrgsBlockedBy(inputValue); }; - - /** - * Handles the hiding of the remove user modal. - * Restores the visibility of the joined or blocked organizations modal based on user action. - */ function onHideRemoveUserModal(): void { setShowRemoveUserModal(false); if (removeUserProps.setShowOnCancel == 'JOINED') { From 60d971d8b978240b91b4b65928690a48a61ce8d7 Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 31 Jul 2024 13:34:08 +0530 Subject: [PATCH 6/8] some linting error fixed --- .../OrganizationCard/OrganizationCard.tsx | 2 - .../OrganizationCardStart.tsx | 2 - .../RequestsTableItem/RequestsTableItem.tsx | 6 +-- src/components/UserListCard/UserListCard.tsx | 3 +- .../UserPortal/ChatRoom/ChatRoom.tsx | 7 ---- .../UserPortal/CommentCard/CommentCard.tsx | 6 +-- .../UserPortal/ContactCard/ContactCard.tsx | 2 - .../OrganizationSidebar.tsx | 42 ------------------- 8 files changed, 5 insertions(+), 65 deletions(-) diff --git a/src/components/OrganizationCard/OrganizationCard.tsx b/src/components/OrganizationCard/OrganizationCard.tsx index 883b8fcc48..05f8712ec5 100644 --- a/src/components/OrganizationCard/OrganizationCard.tsx +++ b/src/components/OrganizationCard/OrganizationCard.tsx @@ -2,8 +2,6 @@ import React from 'react'; import styles from './OrganizationCard.module.css'; interface InterfaceOrganizationCardProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - key: any; image: string; id: string; name: string; diff --git a/src/components/OrganizationCardStart/OrganizationCardStart.tsx b/src/components/OrganizationCardStart/OrganizationCardStart.tsx index 96e49f5566..298fb13db9 100644 --- a/src/components/OrganizationCardStart/OrganizationCardStart.tsx +++ b/src/components/OrganizationCardStart/OrganizationCardStart.tsx @@ -2,8 +2,6 @@ import React from 'react'; import styles from './OrganizationCardStart.module.css'; interface InterfaceOrganizationCardStartProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - key: any; image: string; id: string; name: string; diff --git a/src/components/RequestsTableItem/RequestsTableItem.tsx b/src/components/RequestsTableItem/RequestsTableItem.tsx index 769eb03744..2b647a9ce2 100644 --- a/src/components/RequestsTableItem/RequestsTableItem.tsx +++ b/src/components/RequestsTableItem/RequestsTableItem.tsx @@ -70,8 +70,7 @@ const RequestsTableItem = (props: Props): JSX.Element => { toast.success(t('acceptedSuccessfully')); resetAndRefetch(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { /* istanbul ignore next */ errorHandler(t, error); } @@ -99,8 +98,7 @@ const RequestsTableItem = (props: Props): JSX.Element => { toast.success(t('rejectedSuccessfully')); resetAndRefetch(); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { /* istanbul ignore next */ errorHandler(t, error); } diff --git a/src/components/UserListCard/UserListCard.tsx b/src/components/UserListCard/UserListCard.tsx index 63706c8d22..193569424f 100644 --- a/src/components/UserListCard/UserListCard.tsx +++ b/src/components/UserListCard/UserListCard.tsx @@ -52,8 +52,7 @@ function userListCard(props: InterfaceUserListCardProps): JSX.Element { window.location.reload(); }, 2000); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { /* istanbul ignore next */ errorHandler(t, error); } diff --git a/src/components/UserPortal/ChatRoom/ChatRoom.tsx b/src/components/UserPortal/ChatRoom/ChatRoom.tsx index 4ce7960e75..dd36a93d8f 100644 --- a/src/components/UserPortal/ChatRoom/ChatRoom.tsx +++ b/src/components/UserPortal/ChatRoom/ChatRoom.tsx @@ -24,7 +24,6 @@ interface InterfaceChatRoomProps { selectedChatType: string; } - /** * A chat room component that displays messages and a message input field. * @@ -80,7 +79,6 @@ type Chat = { }[]; }; - export default function chatRoom(props: InterfaceChatRoomProps): JSX.Element { // Translation hook for text in different languages const { t } = useTranslation('translation', { @@ -88,10 +86,6 @@ export default function chatRoom(props: InterfaceChatRoomProps): JSX.Element { }); const isMountedRef = useRef(true); - - // State to manage the new message input - const [newMessage, setNewMessage] = React.useState(''); - useEffect(() => { return () => { isMountedRef.current = false; @@ -106,7 +100,6 @@ export default function chatRoom(props: InterfaceChatRoomProps): JSX.Element { const [directChat, setDirectChat] = useState(); const [groupChat, setGroupChat] = useState(); - /** * Handles changes to the new message input field. * diff --git a/src/components/UserPortal/CommentCard/CommentCard.tsx b/src/components/UserPortal/CommentCard/CommentCard.tsx index bf2afbe45b..026c2f1af3 100644 --- a/src/components/UserPortal/CommentCard/CommentCard.tsx +++ b/src/components/UserPortal/CommentCard/CommentCard.tsx @@ -90,8 +90,7 @@ function commentCard(props: InterfaceCommentCardProps): JSX.Element { setIsLikedByUser(false); props.handleDislikeComment(props.id); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { /* istanbul ignore next */ toast.error(error); } @@ -108,8 +107,7 @@ function commentCard(props: InterfaceCommentCardProps): JSX.Element { setIsLikedByUser(true); props.handleLikeComment(props.id); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (error: any) { + } catch (error: unknown) { /* istanbul ignore next */ toast.error(error); } diff --git a/src/components/UserPortal/ContactCard/ContactCard.tsx b/src/components/UserPortal/ContactCard/ContactCard.tsx index f1fcd23c0b..cd135a9e01 100644 --- a/src/components/UserPortal/ContactCard/ContactCard.tsx +++ b/src/components/UserPortal/ContactCard/ContactCard.tsx @@ -33,9 +33,7 @@ interface InterfaceContactCardProps { * @returns The rendered contact card component. */ function contactCard(props: InterfaceContactCardProps): JSX.Element { - // Full name of the contact - const contactName = `${props.firstName} ${props.lastName}`; /** * Updates the selected contact and its name when the card is clicked. diff --git a/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx b/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx index eba7f93103..796d30d568 100644 --- a/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx +++ b/src/components/UserPortal/OrganizationSidebar/OrganizationSidebar.tsx @@ -17,7 +17,6 @@ import type { InterfaceQueryOrganizationEventListItem, InterfaceMemberInfo, } from 'utils/interfaces'; -import { skip } from 'node:test'; /** * OrganizationSidebar displays the sidebar for an organization, showing a list of members and events. @@ -43,16 +42,6 @@ export default function organizationSidebar(): JSX.Element { // Extract the organization ID from the URL parameters const { orgId: organizationId } = useParams(); - - - // State variables for storing members and events data - const [members, setMembers] = React.useState([]); - const [events, setEvents] = React.useState([]); - - // Define the links to view all members and events - const eventsLink = `/user/events/id=${organizationId}`; - const peopleLink = `/user/people/id=${organizationId}`; - const [members, setMembers] = React.useState< InterfaceMemberInfo[] | undefined >(undefined); @@ -62,7 +51,6 @@ export default function organizationSidebar(): JSX.Element { const eventsLink = `/user/events/${organizationId}`; const peopleLink = `/user/people/${organizationId}`; - // Query to fetch members of the organization const { data: memberData, loading: memberLoading } = useQuery( ORGANIZATIONS_MEMBER_CONNECTION_LIST, @@ -123,35 +111,6 @@ export default function organizationSidebar(): JSX.Element { ) : ( - - {members.length ? ( - members.map( - ( - member: InterfaceMemberInfo, - index: React.Key | null | undefined, - ) => { - // Construct member's full name - const memberName = `${member.firstName} ${member.lastName}`; - return ( - -
    - -
    {memberName}
    -
    -
    - ); - }, - ) - {members && members.length ? ( members.map((member: InterfaceMemberInfo) => { const memberName = `${member.firstName} ${member.lastName}`; @@ -173,7 +132,6 @@ export default function organizationSidebar(): JSX.Element { ); }) - ) : (
    {t('noMembers')}
    )} From eb37eb46004404e920250123ac9520ede8d105fc Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 31 Jul 2024 14:35:31 +0530 Subject: [PATCH 7/8] completed --- src/App.tsx | 17 +++++++ src/components/AddOn/AddOn.tsx | 2 - .../AddOn/core/AddOnEntry/AddOnEntry.tsx | 2 - .../support/components/Action/Action.tsx | 1 - .../components/MainContent/MainContent.tsx | 1 - .../components/SidePanel/SidePanel.tsx | 1 - .../DynamicDropDown/DynamicDropDown.tsx | 2 - .../EventCalendar/EventCalendar.tsx | 36 +++++++++++++ .../EventListCard/EventListCardModals.tsx | 19 +++++++ .../useEventActionColumnConfig.tsx | 1 + src/components/OrgPostCard/OrgPostCard.tsx | 51 +++++++++++++++++++ .../OrganizationDashCards/CardItem.tsx | 1 - .../UserPortal/ChatRoom/ChatRoom.test.tsx | 4 +- .../CreateDirectChat.test.tsx | 4 +- .../CreateDirectChat/CreateDirectChat.tsx | 33 ++++++++++++ .../CreateGroupChat/CreateGroupChat.test.tsx | 6 +-- .../CreateGroupChat/CreateGroupChat.tsx | 7 +++ .../UsersTableItem/UsersTableItem.tsx | 25 +++++++++ .../FundCampaignPledge/FundCampaignPledge.tsx | 39 +++++++++++++- .../FundCampaignPledge/PledgeDeleteModal.tsx | 26 ++++++++++ .../FundCampaignPledge/PledgeModal.tsx | 34 +++++++++++++ .../CampaignDeleteModal.tsx | 15 ++++++ .../CampaignModal.tsx | 23 ++++++++- .../OrganizationFundCampagins.tsx | 36 ++++++++++++- .../OrganizationFunds/FundDeleteModal.tsx | 23 ++++++++- src/screens/OrganizationFunds/FundModal.tsx | 30 ++++++++++- .../OrganizationFunds/OrganizationFunds.tsx | 38 +++++++++++++- src/screens/UserPortal/Chat/Chat.test.tsx | 4 +- src/screens/UserPortal/Donate/Donate.tsx | 25 ++++++++- src/screens/UserPortal/Posts/Posts.tsx | 29 +++++++++++ src/screens/Users/Users.tsx | 3 -- src/utils/errorHandler.tsx | 2 +- 32 files changed, 510 insertions(+), 30 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 7ee6ba72cb..3df6aef390 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,23 @@ import EventDashboardScreen from 'components/EventDashboardScreen/EventDashboard const { setItem } = useLocalStorage(); +/** + * This is the main function for our application. It sets up all the routes and components, + * defining how the user can navigate through the app. The function uses React Router's `Routes` + * and `Route` components to map different URL paths to corresponding screens and components. + * + * ## Important Details + * - **UseEffect Hook**: This hook checks user authentication status using the `CHECK_AUTH` GraphQL query. + * - **Plugins**: It dynamically loads additional routes for any installed plugins. + * - **Routes**: + * - The root route ("/") takes the user to the `LoginPage`. + * - Protected routes are wrapped with the `SecuredRoute` component to ensure they are only accessible to authenticated users. + * - Admin and Super Admin routes allow access to organization and user management screens. + * - User portal routes allow end-users to interact with organizations, settings, chat, events, etc. + * + * @returns The rendered routes and components of the application. + */ + function app(): JSX.Element { /*const { updatePluginLinks, updateInstalled } = bindActionCreators( actionCreators, diff --git a/src/components/AddOn/AddOn.tsx b/src/components/AddOn/AddOn.tsx index d6df8f38df..1d617535b1 100644 --- a/src/components/AddOn/AddOn.tsx +++ b/src/components/AddOn/AddOn.tsx @@ -2,10 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; interface InterfaceAddOnProps { - // eslint-disable-next-line @typescript-eslint/no-explicit-any extras: any; name: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx index 0773019046..cc6feafa6b 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx @@ -18,10 +18,8 @@ interface InterfaceAddOnEntryProps { description: string; createdBy: string; component: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any modified: any; uninstalledOrgs: string[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any getInstalledPlugins: () => any; } diff --git a/src/components/AddOn/support/components/Action/Action.tsx b/src/components/AddOn/support/components/Action/Action.tsx index 1db35ecca6..f29d8492d3 100644 --- a/src/components/AddOn/support/components/Action/Action.tsx +++ b/src/components/AddOn/support/components/Action/Action.tsx @@ -7,7 +7,6 @@ interface InterfaceActionProps { /** * The child elements to be rendered inside the action component. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; /** diff --git a/src/components/AddOn/support/components/MainContent/MainContent.tsx b/src/components/AddOn/support/components/MainContent/MainContent.tsx index 74c5216de0..eddc1b993d 100644 --- a/src/components/AddOn/support/components/MainContent/MainContent.tsx +++ b/src/components/AddOn/support/components/MainContent/MainContent.tsx @@ -8,7 +8,6 @@ interface InterfaceMainContentProps { /** * The child elements to be rendered inside the main content container. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } diff --git a/src/components/AddOn/support/components/SidePanel/SidePanel.tsx b/src/components/AddOn/support/components/SidePanel/SidePanel.tsx index c3bcc42c46..d4c0d65966 100644 --- a/src/components/AddOn/support/components/SidePanel/SidePanel.tsx +++ b/src/components/AddOn/support/components/SidePanel/SidePanel.tsx @@ -13,7 +13,6 @@ interface InterfaceSidePanelProps { /** * The child elements to be rendered inside the side panel. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any children: any; } diff --git a/src/components/DynamicDropDown/DynamicDropDown.tsx b/src/components/DynamicDropDown/DynamicDropDown.tsx index 6bb5e3efcb..fac8b8fd85 100644 --- a/src/components/DynamicDropDown/DynamicDropDown.tsx +++ b/src/components/DynamicDropDown/DynamicDropDown.tsx @@ -9,9 +9,7 @@ interface InterfaceChangeDropDownProps { parentContainerStyle?: string; btnStyle?: string; btnTextStyle?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any setFormState: React.Dispatch>; - // eslint-disable-next-line @typescript-eslint/no-explicit-any formState: any; fieldOptions: { value: string; label: string }[]; // Field options for dropdown fieldName: string; // Field name for labeling diff --git a/src/components/EventCalendar/EventCalendar.tsx b/src/components/EventCalendar/EventCalendar.tsx index 566e66d62a..dcef49b61e 100644 --- a/src/components/EventCalendar/EventCalendar.tsx +++ b/src/components/EventCalendar/EventCalendar.tsx @@ -56,6 +56,20 @@ enum Role { interface InterfaceIOrgList { admins: { _id: string }[]; } + +/** + * The `Calendar` component displays a calendar with events. + * It supports different views such as day, month, and year. + * + * @param eventData - Array of event data to be displayed. + * @param refetchEvents - Optional function to refetch events. + * @param orgData - Optional organization data, which includes a list of admins. + * @param userRole - Optional role of the user, which can affect the visibility of events. + * @param userId - Optional ID of the current user, used to determine if the user is attending an event. + * @param viewType - Optional type of view for the calendar (day, month, year). + * + * @returns A calendar component displaying events based on the selected view type. + */ const Calendar: React.FC = ({ eventData, refetchEvents, @@ -83,6 +97,16 @@ const Calendar: React.FC = ({ return () => window.removeEventListener('resize', handleResize); }, []); + /** + * Filters the event data based on the user's role and organization data. + * + * @param eventData - Array of event data to be filtered. + * @param orgData - Optional organization data, including a list of admins. + * @param userRole - Optional role of the user. + * @param userId - Optional ID of the current user. + * + * @returns A filtered array of events based on the user's role and organization data. + */ const filterData = ( eventData: InterfaceEventListCardProps[], orgData?: InterfaceIOrgList, @@ -125,6 +149,9 @@ const Calendar: React.FC = ({ setEvents(data); }, [eventData, orgData, userRole, userId]); + /** + * Moves the calendar view to the previous month. + */ const handlePrevMonth = (): void => { /*istanbul ignore next*/ if (currentMonth === 0) { @@ -135,6 +162,9 @@ const Calendar: React.FC = ({ } }; + /** + * Moves the calendar view to the next month. + */ const handleNextMonth = (): void => { /*istanbul ignore next*/ if (currentMonth === 11) { @@ -145,6 +175,9 @@ const Calendar: React.FC = ({ } }; + /** + * Moves the calendar view to the previous date. + */ const handlePrevDate = (): void => { /*istanbul ignore next*/ if (currentDate > 1) { @@ -188,6 +221,9 @@ const Calendar: React.FC = ({ } }; + /** + * Moves the calendar view to today's date. + */ const handleTodayButton = (): void => { /*istanbul ignore next*/ setCurrentYear(today.getFullYear()); diff --git a/src/components/EventListCard/EventListCardModals.tsx b/src/components/EventListCard/EventListCardModals.tsx index 51612f45e5..878d8796a4 100644 --- a/src/components/EventListCard/EventListCardModals.tsx +++ b/src/components/EventListCard/EventListCardModals.tsx @@ -37,11 +37,24 @@ enum Role { ADMIN = 'ADMIN', } +/** + * Converts a time string to a Dayjs object representing the current date with the specified time. + * @param time - A string representing the time in 'HH:mm:ss' format. + * @returns A Dayjs object with the current date and specified time. + */ const timeToDayJs = (time: string): Dayjs => { const dateTimeString = dayjs().format('YYYY-MM-DD') + ' ' + time; return dayjs(dateTimeString, { format: 'YYYY-MM-DD HH:mm:ss' }); }; +/** + * Properties for the `EventListCardModals` component. + * eventListCardProps - The properties of the event list card. + * eventModalIsOpen - Boolean indicating if the event modal is open. + * hideViewModal - Function to hide the event modal. + * t - Function for translation of text. + * tCommon - Function for translation of common text. + */ interface InterfaceEventListCardModalProps { eventListCardProps: InterfaceEventListCardProps; eventModalIsOpen: boolean; @@ -50,6 +63,12 @@ interface InterfaceEventListCardModalProps { tCommon: (key: string) => string; } +/** + * The `EventListCardModals` component displays the modals related to events, such as viewing, + * updating, and deleting events. + * @param props - The properties for the component. + * @returns A JSX element containing the event modals. + */ function EventListCardModals({ eventListCardProps, eventModalIsOpen, diff --git a/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx b/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx index 0db74323f6..b1ec584f3b 100644 --- a/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx +++ b/src/components/EventManagement/EventActionItems/useEventActionColumnConfig.tsx @@ -14,6 +14,7 @@ export type Props = { }; type ColumnConfig = { + /** Configuration for the columns of the data grid. */ columns: GridColDef[]; }; diff --git a/src/components/OrgPostCard/OrgPostCard.tsx b/src/components/OrgPostCard/OrgPostCard.tsx index be71622c7c..76ae616277 100644 --- a/src/components/OrgPostCard/OrgPostCard.tsx +++ b/src/components/OrgPostCard/OrgPostCard.tsx @@ -25,6 +25,57 @@ interface InterfaceOrgPostCardProps { postVideo: string | null; pinned: boolean; } + +/** + * `OrgPostCard` component for displaying and managing organizational posts. + * + * This component displays a post card with options to view details, edit, delete, pin, or unpin the post. It supports video and image content, and includes modals for editing and deleting posts. The component uses GraphQL mutations for post management and integrates with a toast notification system for user feedback. + * + * ### Props + * - `key` (string): Unique key for the component. + * - `id` (string): Unique identifier of the post. + * - `postTitle` (string): Title of the post. + * - `postInfo` (string): Description or information about the post. + * - `postAuthor` (string): Author of the post. + * - `postPhoto` (string | null): URL of the post's image. + * - `postVideo` (string | null): URL of the post's video. + * - `pinned` (boolean): Indicates whether the post is pinned. + * + * ### State + * - `postformState`: State object for managing form data. + * - `postPhotoUpdated`: Boolean flag indicating if the post photo has been updated. + * - `postVideoUpdated`: Boolean flag indicating if the post video has been updated. + * - `togglePost`: String indicating the current toggle state ('Read more' or 'hide'). + * - `showEditModal`: Boolean flag indicating whether the edit modal is visible. + * - `showDeleteModal`: Boolean flag indicating whether the delete modal is visible. + * - `modalVisible`: Boolean flag indicating whether the modal for viewing post details is visible. + * - `menuVisible`: Boolean flag indicating whether the options menu is visible. + * - `playing`: Boolean flag indicating whether the video is playing. + * + * ### Refs + * - `videoRef`: Ref for the video element. + * + * ### Methods + * - `togglePostPin(id: string, pinned: boolean)`: Toggles the pinned state of the post. + * - `toggleShowEditModal()`: Toggles visibility of the edit modal. + * - `toggleShowDeleteModal()`: Toggles visibility of the delete modal. + * - `handleVideoPlay()`: Starts playing the video. + * - `handleVideoPause()`: Pauses the video. + * - `handleCardClick()`: Displays the modal with post details. + * - `handleMoreOptionsClick()`: Displays the options menu. + * - `clearImageInput()`: Clears the selected image input. + * - `clearVideoInput()`: Clears the selected video input. + * - `handletoggleClick()`: Toggles the visibility of the post information. + * - `handleEditModal()`: Shows the edit modal. + * - `handleDeleteModal()`: Shows the delete modal. + * - `handleInputEvent(e: ChangeEvent)`: Updates form state based on input changes. + * - `updatePostHandler(e: ChangeEvent)`: Handles form submission to update the post. + * - `deletePost()`: Deletes the post and provides feedback. + * + * ### Effects + * - Synchronizes form state with props on initial render. + */ + export default function orgPostCard( props: InterfaceOrgPostCardProps, ): JSX.Element { diff --git a/src/components/OrganizationDashCards/CardItem.tsx b/src/components/OrganizationDashCards/CardItem.tsx index 4123963d35..8ad99194cf 100644 --- a/src/components/OrganizationDashCards/CardItem.tsx +++ b/src/components/OrganizationDashCards/CardItem.tsx @@ -17,7 +17,6 @@ export interface InterfaceCardItem { time?: string; startdate?: string; enddate?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any creator?: any; location?: string; } diff --git a/src/components/UserPortal/ChatRoom/ChatRoom.test.tsx b/src/components/UserPortal/ChatRoom/ChatRoom.test.tsx index 7d72837767..c4fd4eadaf 100644 --- a/src/components/UserPortal/ChatRoom/ChatRoom.test.tsx +++ b/src/components/UserPortal/ChatRoom/ChatRoom.test.tsx @@ -801,7 +801,7 @@ describe('Testing Chatroom Component [User Portal]', () => { await wait(); }); - test('Test send message direct chat', async () => { + test('send message direct chat', async () => { setItem('userId', '2'); const mocks = [ SEND_MESSAGE_TO_DIRECT_CHAT_MOCK, @@ -864,7 +864,7 @@ describe('Testing Chatroom Component [User Portal]', () => { await wait(); }); - test('Test send message group chat', async () => { + test('send message group chat', async () => { const mocks = [ SEND_MESSAGE_TO_GROUP_CHAT_MOCK, ...DIRECT_CHAT_BY_ID_QUERY_MOCK, diff --git a/src/components/UserPortal/CreateDirectChat/CreateDirectChat.test.tsx b/src/components/UserPortal/CreateDirectChat/CreateDirectChat.test.tsx index b8c1dd86fb..338b890a2a 100644 --- a/src/components/UserPortal/CreateDirectChat/CreateDirectChat.test.tsx +++ b/src/components/UserPortal/CreateDirectChat/CreateDirectChat.test.tsx @@ -2107,7 +2107,7 @@ describe('Testing Create Direct Chat Modal [User Portal]', () => { })), }); - test('Test open and close create new direct chat modal', async () => { + test('open and close create new direct chat modal', async () => { const mock = [ ...GROUP_CHAT_BY_ID_QUERY_MOCK, ...DIRECT_CHAT_BY_ID_QUERY_MOCK, @@ -2157,7 +2157,7 @@ describe('Testing Create Direct Chat Modal [User Portal]', () => { fireEvent.click(closeButton); }); - test('Test create new direct chat', async () => { + test('create new direct chat', async () => { setItem('userId', '1'); const mock = [ CREATE_DIRECT_CHAT_MOCK, diff --git a/src/components/UserPortal/CreateDirectChat/CreateDirectChat.tsx b/src/components/UserPortal/CreateDirectChat/CreateDirectChat.tsx index 565dfd422f..2f16dd4b0f 100644 --- a/src/components/UserPortal/CreateDirectChat/CreateDirectChat.tsx +++ b/src/components/UserPortal/CreateDirectChat/CreateDirectChat.tsx @@ -19,9 +19,18 @@ import { Search } from '@mui/icons-material'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +/** + * Props for the CreateDirectChat component. + */ interface InterfaceCreateDirectChatProps { toggleCreateDirectChatModal: () => void; createDirectChatModalisOpen: boolean; + /** + * Function to refetch the contact list. + * + * @param variables - Optional variables to filter the contact list. + * @returns Promise with ApolloQueryResult. + */ contactRefetch: ( variables?: | Partial<{ @@ -31,6 +40,9 @@ interface InterfaceCreateDirectChatProps { ) => Promise>; } +/** + * Styled table cell with custom styles. + */ const StyledTableCell = styled(TableCell)(({ theme }) => ({ [`&.${tableCellClasses.head}`]: { backgroundColor: ['#31bb6b', '!important'], @@ -41,6 +53,9 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ }, })); +/** + * Styled table row with custom styles. + */ const StyledTableRow = styled(TableRow)(() => ({ '&:last-child td, &:last-child th': { border: 0, @@ -49,6 +64,12 @@ const StyledTableRow = styled(TableRow)(() => ({ const { getItem } = useLocalStorage(); +/** + * Component for creating a direct chat with a selected user. + * + * @param props - The props for the CreateDirectChat component. + * @returns JSX.Element + */ export default function groupChat({ toggleCreateDirectChatModal, createDirectChatModalisOpen, @@ -66,6 +87,12 @@ export default function groupChat({ const [createDirectChat] = useMutation(CREATE_DIRECT_CHAT); + /** + * Handles the creation of a direct chat with a selected user. + * + * @param id - The ID of the user to start a direct chat with. + * @returns Promise + */ const handleCreateDirectChat = async (id: string): Promise => { console.log(organizationId); await createDirectChat({ @@ -89,6 +116,12 @@ export default function groupChat({ }, }); + /** + * Handles changes in the user search input and refetches the user list. + * + * @param e - The form event. + * @returns void + */ const handleUserModalSearchChange = (e: React.FormEvent): void => { e.preventDefault(); /* istanbul ignore next */ diff --git a/src/components/UserPortal/CreateGroupChat/CreateGroupChat.test.tsx b/src/components/UserPortal/CreateGroupChat/CreateGroupChat.test.tsx index 3898653f47..77447dc29c 100644 --- a/src/components/UserPortal/CreateGroupChat/CreateGroupChat.test.tsx +++ b/src/components/UserPortal/CreateGroupChat/CreateGroupChat.test.tsx @@ -2543,7 +2543,7 @@ describe('Testing Create Direct Chat Modal [User Portal]', () => { })), }); - test('Test open and close create new direct chat modal', async () => { + test('open and close create new direct chat modal', async () => { const mock = [ ...USER_JOINED_ORG_MOCK, ...GROUP_CHAT_BY_ID_QUERY_MOCK, @@ -2580,7 +2580,7 @@ describe('Testing Create Direct Chat Modal [User Portal]', () => { fireEvent.click(closeButton); }); - test('Test create new group chat', async () => { + test('create new group chat', async () => { const mock = [ ...CREATE_GROUP_CHAT_MOCK, ...USER_JOINED_ORG_MOCK, @@ -2657,7 +2657,7 @@ describe('Testing Create Direct Chat Modal [User Portal]', () => { }); }, 3000); - test('Test add and remove user ', async () => { + test('add and remove user', async () => { setItem('userId', '1'); const mock = [ ...USER_JOINED_ORG_MOCK, diff --git a/src/components/UserPortal/CreateGroupChat/CreateGroupChat.tsx b/src/components/UserPortal/CreateGroupChat/CreateGroupChat.tsx index 29884fc2bd..3c0af847fb 100644 --- a/src/components/UserPortal/CreateGroupChat/CreateGroupChat.tsx +++ b/src/components/UserPortal/CreateGroupChat/CreateGroupChat.tsx @@ -87,6 +87,13 @@ const StyledTableRow = styled(TableRow)(() => ({ const { getItem } = useLocalStorage(); +/** + * + * @param toggleCreateGroupChatModal - function to toggle the create group chat modal + * @param createGroupChatModalisOpen - boolean to check if the create group chat modal is open + * @param groupChatListRefetch - function to refetch the group chat list + * @returns - returns the create group chat modal + */ export default function CreateGroupChat({ toggleCreateGroupChatModal, createGroupChatModalisOpen, diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx index 9d6ec65187..70371e0833 100644 --- a/src/components/UsersTableItem/UsersTableItem.tsx +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -20,6 +20,31 @@ type Props = { loggedInUserId: string; resetAndRefetch: () => void; }; +/** + * `UsersTableItem` is a React component that represents a row in a table displaying user information. + * This component handles displaying and interacting with user details, including: + * + * - Viewing organizations the user has joined or blocked. + * - Searching through these organizations. + * - Changing the user's role in various organizations. + * - Removing the user from an organization. + * + * It also manages multiple modals: + * + * - **Joined Organizations Modal**: Displays a list of organizations the user has joined, with search functionality. + * - **Blocked Organizations Modal**: Displays a list of organizations that have blocked the user, with search functionality. + * - **Remove User Modal**: Confirms the removal of the user from a specific organization. + * + * + * @param props - The props for the `UsersTableItem` component. + * @param user - The user data to be displayed. + * @param index - The index of the user in the table. + * @param loggedInUserId - The ID of the currently logged-in user. + * @param resetAndRefetch - A function to reset and refetch the data. + * + * @returns The rendered component. + */ + const UsersTableItem = (props: Props): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); const { t: tCommon } = useTranslation('common'); diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx index 9798336412..9c5adfaa2a 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx @@ -56,7 +56,44 @@ const dataGridStyle = { borderRadius: '0.5rem', }, }; - +/** + * `FundCampaignPledge` component displays and manages pledges for a fundraising campaign. + * + * This component fetches pledge data for a specific fundraising campaign using a GraphQL query. + * It supports searching, sorting, and managing pledges through modals for creating, editing, and deleting pledges. + * + * ## Functional Components: + * - **DataGrid**: Displays pledge information in a tabular format. + * - **PledgeModal**: Modal for creating or editing a pledge. + * - **PledgeDeleteModal**: Modal for confirming pledge deletion. + * - **BasePopup**: Popup to display additional users associated with a pledge. + * + * ## State Management: + * - `campaignInfo`: Stores information about the campaign. + * - `modalState`: Tracks the visibility state of modals. + * - `anchor`: Manages anchor for the popup display. + * - `extraUsers`: Holds additional users to be displayed in the popup. + * - `progressIndicator`: Indicates whether to show pledged or raised amount in the progress bar. + * - `pledgeModalMode`: Determines whether the modal is in edit or create mode. + * - `pledge`: Contains details of the currently selected pledge. + * - `searchTerm`: The term used for searching pledgers. + * - `sortBy`: Determines the sorting order of pledges. + * + * ## Event Handlers: + * - `openModal`: Opens the specified modal. + * - `closeModal`: Closes the specified modal. + * - `handleOpenModal`: Prepares and opens the modal for editing or creating a pledge. + * - `handleDeleteClick`: Prepares and opens the modal for deleting a pledge. + * - `handleClick`: Opens the popup to display additional users. + * + * ## Render: + * - Renders breadcrumbs for navigation. + * - Displays campaign details, including progress bar for pledged or raised amount. + * - Provides search and filter options for pledges. + * - Renders the `DataGrid` component with pledge details. + * - Displays modals and popup for user interactions. + * + */ const fundCampaignPledge = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'pledges', diff --git a/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx b/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx index a16d0df905..2ecf6c7c23 100644 --- a/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx +++ b/src/screens/FundCampaignPledge/PledgeDeleteModal.tsx @@ -13,6 +13,32 @@ export interface InterfaceDeletePledgeModal { pledge: InterfacePledgeInfo | null; refetchPledge: () => void; } + +/** + * A modal dialog for confirming the deletion of a pledge. + * + * @param isOpen - Indicates whether the modal is open. + * @param hide - Function to close the modal. + * @param pledge - The pledge object to be deleted. + * @param refetchPledge - Function to refetch the pledges after deletion. + * + * @returns The rendered modal component. + * + * + * The `PledgeDeleteModal` component displays a confirmation dialog when a user attempts to delete a pledge. + * It allows the user to either confirm or cancel the deletion. + * On confirmation, the `deletePledge` mutation is called to remove the pledge from the database, + * and the `refetchPledge` function is invoked to update the list of pledges. + * A success or error toast notification is shown based on the result of the deletion operation. + * + * The modal includes: + * - A header with a title and a close button. + * - A body with a message asking for confirmation. + * - A footer with "Yes" and "No" buttons to confirm or cancel the deletion. + * + * The `deletePledge` mutation is used to perform the deletion operation. + */ + const PledgeDeleteModal: React.FC = ({ isOpen, hide, diff --git a/src/screens/FundCampaignPledge/PledgeModal.tsx b/src/screens/FundCampaignPledge/PledgeModal.tsx index 3e22d604a0..ab126101de 100644 --- a/src/screens/FundCampaignPledge/PledgeModal.tsx +++ b/src/screens/FundCampaignPledge/PledgeModal.tsx @@ -35,6 +35,40 @@ export interface InterfacePledgeModal { endDate: Date; mode: 'create' | 'edit'; } + +/** + * A modal dialog for creating or editing a pledge. + * + * @param isOpen - Indicates whether the modal is open. + * @param hide - Function to close the modal. + * @param campaignId - The ID of the campaign associated with the pledge. + * @param orgId - The ID of the organization associated with the pledge. + * @param pledge - The pledge object to be edited, or `null` if creating a new pledge. + * @param refetchPledge - Function to refetch the list of pledges after creation or update. + * @param endDate - The end date of the campaign to ensure pledge dates are within this range. + * @param mode - The mode indicating whether the modal is for creating a new pledge or editing an existing one. + * + * @returns The rendered modal component. + * + * The `PledgeModal` component displays a form within a modal dialog for creating or editing a pledge. + * It includes fields for selecting users, entering an amount, choosing a currency, and setting start and end dates for the pledge. + * + * The modal includes: + * - A header with a title indicating the current mode (create or edit) and a close button. + * - A form with: + * - A multi-select dropdown for selecting users to participate in the pledge. + * - Date pickers for selecting the start and end dates of the pledge. + * - A dropdown for selecting the currency of the pledge amount. + * - An input field for entering the pledge amount. + * - A submit button to create or update the pledge. + * + * On form submission, the component either: + * - Calls `updatePledge` mutation to update an existing pledge, or + * - Calls `createPledge` mutation to create a new pledge. + * + * Success or error messages are displayed using toast notifications based on the result of the mutation. + */ + const PledgeModal: React.FC = ({ isOpen, hide, diff --git a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx index 840c4869bb..c6c0b5a35e 100644 --- a/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignDeleteModal.tsx @@ -7,12 +7,22 @@ import type { InterfaceCampaignInfo } from 'utils/interfaces'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; +/** + * Props for the CampaignDeleteModal component. + */ export interface InterfaceDeleteCampaignModal { isOpen: boolean; hide: () => void; campaign: InterfaceCampaignInfo | null; refetchCampaign: () => void; } + +/** + * Modal component for confirming the deletion of a campaign. + * + * @param props - The props for the CampaignDeleteModal component. + * @returns JSX.Element + */ const CampaignDeleteModal: React.FC = ({ isOpen, hide, @@ -26,6 +36,11 @@ const CampaignDeleteModal: React.FC = ({ const [deleteCampaign] = useMutation(DELETE_CAMPAIGN_MUTATION); + /** + * Handles the campaign deletion. + * + * @returns Promise + */ const deleteCampaignHandler = async (): Promise => { try { await deleteCampaign({ diff --git a/src/screens/OrganizationFundCampaign/CampaignModal.tsx b/src/screens/OrganizationFundCampaign/CampaignModal.tsx index dfe0738c07..87b2130e65 100644 --- a/src/screens/OrganizationFundCampaign/CampaignModal.tsx +++ b/src/screens/OrganizationFundCampaign/CampaignModal.tsx @@ -3,7 +3,7 @@ import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; -import { Button, Col, Form, Modal } from 'react-bootstrap'; +import { Button, Form, Modal } from 'react-bootstrap'; import { currencyOptions, currencySymbols } from 'utils/currency'; import styles from './OrganizationFundCampaign.module.css'; import { useTranslation } from 'react-i18next'; @@ -22,6 +22,9 @@ import { } from '@mui/material'; import type { InterfaceCampaignInfo } from 'utils/interfaces'; +/** + * Props for the CampaignModal component. + */ export interface InterfaceCampaignModal { isOpen: boolean; hide: () => void; @@ -31,6 +34,12 @@ export interface InterfaceCampaignModal { mode: 'create' | 'edit'; } +/** + * Modal component for creating or editing a campaign. + * + * @param props - The props for the CampaignModal component. + * @returns JSX.Element + */ const CampaignModal: React.FC = ({ isOpen, hide, @@ -73,6 +82,12 @@ const CampaignModal: React.FC = ({ const [createCampaign] = useMutation(CREATE_CAMPAIGN_MUTATION); const [updateCampaign] = useMutation(UPDATE_CAMPAIGN_MUTATION); + /** + * Handles form submission to create a new campaign. + * + * @param e - The form event. + * @returns Promise + */ const createCampaignHandler = async ( e: ChangeEvent, ): Promise => { @@ -103,6 +118,12 @@ const CampaignModal: React.FC = ({ } }; + /** + * Handles form submission to update an existing campaign. + * + * @param e - The form event. + * @returns Promise + */ /*istanbul ignore next*/ const updateCampaignHandler = async ( e: ChangeEvent, diff --git a/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx b/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx index 6cc671c5b1..95526f7d46 100644 --- a/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx +++ b/src/screens/OrganizationFundCampaign/OrganizationFundCampagins.tsx @@ -47,7 +47,41 @@ enum ModalState { SAME = 'same', DELETE = 'delete', } - +/** + * `orgFundCampaign` component displays a list of fundraising campaigns for a specific fund within an organization. + * It allows users to search, sort, view, edit, and delete campaigns. + * + * ### Functionality + * - Displays a data grid with campaigns information, including their names, start and end dates, funding goals, and actions. + * - Provides search functionality to filter campaigns by name. + * - Offers sorting options based on funding goal and end date. + * - Opens modals for creating, editing, or deleting campaigns. + * + * + * ### State + * - `campaign`: The current campaign being edited or deleted. + * - `searchTerm`: The term used for searching campaigns by name. + * - `sortBy`: The current sorting criteria for campaigns. + * - `modalState`: An object indicating the visibility of different modals (`same` for create/edit and `delete` for deletion). + * - `campaignModalMode`: Determines if the modal is in 'edit' or 'create' mode. + * + * ### Methods + * - `openModal(modal: ModalState)`: Opens the specified modal. + * - `closeModal(modal: ModalState)`: Closes the specified modal. + * - `handleOpenModal(campaign: InterfaceCampaignInfo | null, mode: 'edit' | 'create')`: Opens the modal for creating or editing a campaign. + * - `handleDeleteClick(campaign: InterfaceCampaignInfo)`: Opens the delete confirmation modal. + * - `handleClick(campaignId: string)`: Navigates to the pledge details page for a specific campaign. + * + * ### GraphQL Queries + * - Uses `FUND_CAMPAIGN` query to fetch the list of campaigns based on the provided fund ID, search term, and sorting criteria. + * + * ### Rendering + * - Renders a `DataGrid` component with campaigns information. + * - Displays modals for creating, editing, and deleting campaigns. + * - Shows error and loading states using `Loader` and error message components. + * + * @returns The rendered component including breadcrumbs, search and filter controls, data grid, and modals. + */ const orgFundCampaign = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'fundCampaign', diff --git a/src/screens/OrganizationFunds/FundDeleteModal.tsx b/src/screens/OrganizationFunds/FundDeleteModal.tsx index fb13206f1a..bca92614ca 100644 --- a/src/screens/OrganizationFunds/FundDeleteModal.tsx +++ b/src/screens/OrganizationFunds/FundDeleteModal.tsx @@ -13,7 +13,28 @@ export interface InterfaceDeleteFundModal { fund: InterfaceFundInfo | null; refetchFunds: () => void; } - +/** + * `FundDeleteModal` component provides a modal dialog for confirming the deletion of a fund. + * It prompts the user to confirm or cancel the deletion of a specific fund. + * + * ### Props + * - `isOpen`: A boolean indicating whether the modal is open or closed. + * - `hide`: A function to close the modal. + * - `fund`: The fund object to be deleted or `null` if no fund is selected. + * - `refetchFunds`: A function to refetch the list of funds after a successful deletion. + * + * ### Methods + * - `deleteFundHandler()`: Asynchronously handles the deletion of the fund using the `REMOVE_FUND_MUTATION` mutation. + * - `onClose()`: Closes the modal without deleting the fund. + * + * ### Behavior + * - Displays a confirmation modal when `isOpen` is `true`. + * - On confirmation, it triggers the `deleteFundHandler` to perform the deletion. + * - On successful deletion, it calls `refetchFunds`, hides the modal, and shows a success toast notification. + * - On failure, it shows an error toast notification. + * + * @returns The rendered modal dialog. + */ const FundDeleteModal: React.FC = ({ isOpen, hide, diff --git a/src/screens/OrganizationFunds/FundModal.tsx b/src/screens/OrganizationFunds/FundModal.tsx index f4c6350a1c..d347d02111 100644 --- a/src/screens/OrganizationFunds/FundModal.tsx +++ b/src/screens/OrganizationFunds/FundModal.tsx @@ -20,7 +20,35 @@ export interface InterfaceFundModal { orgId: string; mode: 'create' | 'edit'; } - +/** + * `FundModal` component provides a modal dialog for creating or editing a fund. + * It allows users to input fund details and submit them to the server. + * + * This component handles both the creation of new funds and the editing of existing funds, + * based on the `mode` prop. It displays a form with fields for the fund's name, description, + * and other relevant details. Upon submission, it interacts with the GraphQL API to save + * or update the fund details and triggers a refetch of the fund data. + * + * ### Props + * - `isOpen`: A boolean indicating whether the modal is open or closed. + * - `hide`: A function to close the modal. + * - `refetchFunds`: A function to refetch the fund list after a successful operation. + * - `fund`: The current fund object being edited or `null` if creating a new fund. + * - `orgId`: The ID of the organization to which the fund belongs. + * - `mode`: The mode of the modal, either 'edit' or 'create'. + * + * ### State + * - `name`: The name of the fund. + * - `description`: The description of the fund. + * - `amount`: The amount associated with the fund. + * - `status`: The status of the fund (e.g., active, archived). + * + * ### Methods + * - `handleSubmit()`: Handles form submission, creates or updates the fund, and triggers a refetch of the fund list. + * - `handleChange(event: React.ChangeEvent)`: Updates the state based on user input. + * + * @returns The rendered modal dialog. + */ const FundModal: React.FC = ({ isOpen, hide, diff --git a/src/screens/OrganizationFunds/OrganizationFunds.tsx b/src/screens/OrganizationFunds/OrganizationFunds.tsx index 4357877105..56ed17b377 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.tsx @@ -43,7 +43,43 @@ enum ModalState { SAME = 'same', DELETE = 'delete', } - +/** + * `organizationFunds` component displays a list of funds for a specific organization, + * allowing users to search, sort, view, edit, and delete funds. + * + * This component utilizes the `DataGrid` from Material-UI to present the list of funds in a tabular format, + * and includes functionality for filtering and sorting. It also handles the opening and closing of modals + * for creating, editing, and deleting funds. + * + * It includes: + * - A search input field to filter funds by name. + * - A dropdown menu to sort funds by creation date. + * - A button to create a new fund. + * - A table to display the list of funds with columns for fund details and actions. + * - Modals for creating, editing, and deleting funds. + * + * ### GraphQL Queries + * - `FUND_LIST`: Fetches a list of funds for the given organization, filtered and sorted based on the provided parameters. + * + * ### Props + * - `orgId`: The ID of the organization whose funds are being managed. + * + * ### State + * - `fund`: The currently selected fund for editing or deletion. + * - `searchTerm`: The current search term used for filtering funds. + * - `sortBy`: The current sorting order for funds. + * - `modalState`: The state of the modals (edit/create or delete). + * - `fundModalMode`: The mode of the fund modal (edit or create). + * + * ### Methods + * - `openModal(modal: ModalState)`: Opens the specified modal. + * - `closeModal(modal: ModalState)`: Closes the specified modal. + * - `handleOpenModal(fund: InterfaceFundInfo | null, mode: 'edit' | 'create')`: Opens the fund modal with the given fund and mode. + * - `handleDeleteClick(fund: InterfaceFundInfo)`: Opens the delete modal for the specified fund. + * - `handleClick(fundId: string)`: Navigates to the campaign page for the specified fund. + * + * @returns The rendered component. + */ const organizationFunds = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'funds', diff --git a/src/screens/UserPortal/Chat/Chat.test.tsx b/src/screens/UserPortal/Chat/Chat.test.tsx index bf41dcbe9b..66e6115ce7 100644 --- a/src/screens/UserPortal/Chat/Chat.test.tsx +++ b/src/screens/UserPortal/Chat/Chat.test.tsx @@ -2159,7 +2159,7 @@ describe('Testing Chat Screen [User Portal]', () => { }); }); - test('Test create new direct chat', async () => { + test('create new direct chat', async () => { const mock = [ ...USER_JOINED_ORG_MOCK, ...GROUP_CHAT_BY_ID_QUERY_MOCK, @@ -2199,7 +2199,7 @@ describe('Testing Chat Screen [User Portal]', () => { fireEvent.click(closeButton); }); - test('Test create new group chat', async () => { + test('create new group chat', async () => { const mock = [ ...USER_JOINED_ORG_MOCK, ...GROUP_CHAT_BY_ID_QUERY_MOCK, diff --git a/src/screens/UserPortal/Donate/Donate.tsx b/src/screens/UserPortal/Donate/Donate.tsx index 4e39c4e1be..73c5b09e75 100644 --- a/src/screens/UserPortal/Donate/Donate.tsx +++ b/src/screens/UserPortal/Donate/Donate.tsx @@ -28,7 +28,30 @@ export interface InterfaceDonationCardProps { payPalId: string; updatedAt: string; } - +/** + * `donate` component allows users to make donations to an organization and view their previous donations. + * + * This component fetches donation-related data using GraphQL queries and allows users to make donations + * using a mutation. It supports currency selection, donation amount input, and displays a paginated list + * of previous donations. + * + * It includes: + * - An input field for searching donations. + * - A dropdown to select currency. + * - An input field for entering donation amount. + * - A button to submit the donation. + * - A list of previous donations displayed in a paginated format. + * - An organization sidebar for navigation. + * + * ### GraphQL Queries + * - `ORGANIZATION_DONATION_CONNECTION_LIST`: Fetches the list of donations for the organization. + * - `USER_ORGANIZATION_CONNECTION`: Fetches organization details. + * + * ### GraphQL Mutations + * - `DONATE_TO_ORGANIZATION`: Performs the donation action. + * + * @returns The rendered component. + */ export default function donate(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'donate', diff --git a/src/screens/UserPortal/Posts/Posts.tsx b/src/screens/UserPortal/Posts/Posts.tsx index e3d3d8556c..256bb5f52f 100644 --- a/src/screens/UserPortal/Posts/Posts.tsx +++ b/src/screens/UserPortal/Posts/Posts.tsx @@ -119,21 +119,35 @@ type InterfacePostNode = { likes: InterfacePostLikes; }; +/** + * `home` component displays the main feed for a user, including posts, promoted content, and options to create a new post. + * + * It utilizes Apollo Client for fetching and managing data through GraphQL queries. The component fetches and displays posts from an organization, promoted advertisements, and handles user interactions for creating new posts. It also manages state for displaying modal dialogs and handling file uploads for new posts. + * + * @returns JSX.Element - The rendered `home` component. + */ export default function home(): JSX.Element { + // Translation hook for localized text const { t } = useTranslation('translation', { keyPrefix: 'home' }); const { t: tCommon } = useTranslation('common'); + + // Custom hook for accessing local storage const { getItem } = useLocalStorage(); const [posts, setPosts] = useState([]); const [pinnedPosts, setPinnedPosts] = useState([]); const [showModal, setShowModal] = useState(false); const [postImg, setPostImg] = useState(''); + + // Fetching the organization ID from URL parameters const { orgId } = useParams(); + // Redirect to user page if organization ID is not available if (!orgId) { return ; } + // Query hooks for fetching posts, advertisements, and user details const { data: promotedPostsData, }: { @@ -147,6 +161,7 @@ export default function home(): JSX.Element { first: 6, }, }); + const { data, refetch, @@ -164,12 +179,14 @@ export default function home(): JSX.Element { const user: InterfaceQueryUserListItem | undefined = userData?.user; + // Effect hook to update posts state when data changes useEffect(() => { if (data) { setPosts(data.organizations[0].posts.edges); } }, [data]); + // Effect hook to update advertisements state when data changes useEffect(() => { if (promotedPostsData && promotedPostsData.organizations) { const ads: Ad[] = @@ -189,6 +206,12 @@ export default function home(): JSX.Element { ); }, [posts]); + /** + * Converts a post node into props for the `PostCard` component. + * + * @param node - The post node to convert. + * @returns The props for the `PostCard` component. + */ const getCardProps = (node: InterfacePostNode): InterfacePostCard => { const { creator, @@ -270,10 +293,16 @@ export default function home(): JSX.Element { return cardProps; }; + /** + * Opens the post creation modal. + */ const handlePostButtonClick = (): void => { setShowModal(true); }; + /** + * Closes the post creation modal. + */ const handleModalClose = (): void => { setShowModal(false); }; diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 9999a631d6..0e958e7bab 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -91,9 +91,7 @@ const Users = (): JSX.Element => { }: { data?: { users: InterfaceQueryUserListItem[] }; loading: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any fetchMore: any; - // eslint-disable-next-line @typescript-eslint/no-explicit-any refetch: any; error?: ApolloError; } = useQuery(USER_LIST, { @@ -173,7 +171,6 @@ const Users = (): JSX.Element => { setHasMore(true); }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleSearchByEnter = (e: any): void => { if (e.key === 'Enter') { const { value } = e.target; diff --git a/src/utils/errorHandler.tsx b/src/utils/errorHandler.tsx index aa7c6dc9dc..bb5d7ab05e 100644 --- a/src/utils/errorHandler.tsx +++ b/src/utils/errorHandler.tsx @@ -1,7 +1,7 @@ import type { TFunction } from 'react-i18next'; import { toast } from 'react-toastify'; import i18n from './i18n'; -/* +/** This function is used to handle api errors in the application. It takes in the error object and displays the error message to the user. If the error is due to the Talawa API being unavailable, it displays a custom message. From b9cea01daf00f80b61151e13172e9f553fc6ae6c Mon Sep 17 00:00:00 2001 From: gautam-divyanshu Date: Wed, 31 Jul 2024 14:41:40 +0530 Subject: [PATCH 8/8] less than 600 lines --- .../EventCalendar/EventCalendar.tsx | 23 --------- src/components/OrgPostCard/OrgPostCard.tsx | 51 ------------------- .../UsersTableItem/UsersTableItem.tsx | 25 --------- .../FundCampaignPledge/FundCampaignPledge.tsx | 38 -------------- 4 files changed, 137 deletions(-) diff --git a/src/components/EventCalendar/EventCalendar.tsx b/src/components/EventCalendar/EventCalendar.tsx index dcef49b61e..fb76eb597c 100644 --- a/src/components/EventCalendar/EventCalendar.tsx +++ b/src/components/EventCalendar/EventCalendar.tsx @@ -57,19 +57,6 @@ interface InterfaceIOrgList { admins: { _id: string }[]; } -/** - * The `Calendar` component displays a calendar with events. - * It supports different views such as day, month, and year. - * - * @param eventData - Array of event data to be displayed. - * @param refetchEvents - Optional function to refetch events. - * @param orgData - Optional organization data, which includes a list of admins. - * @param userRole - Optional role of the user, which can affect the visibility of events. - * @param userId - Optional ID of the current user, used to determine if the user is attending an event. - * @param viewType - Optional type of view for the calendar (day, month, year). - * - * @returns A calendar component displaying events based on the selected view type. - */ const Calendar: React.FC = ({ eventData, refetchEvents, @@ -97,16 +84,6 @@ const Calendar: React.FC = ({ return () => window.removeEventListener('resize', handleResize); }, []); - /** - * Filters the event data based on the user's role and organization data. - * - * @param eventData - Array of event data to be filtered. - * @param orgData - Optional organization data, including a list of admins. - * @param userRole - Optional role of the user. - * @param userId - Optional ID of the current user. - * - * @returns A filtered array of events based on the user's role and organization data. - */ const filterData = ( eventData: InterfaceEventListCardProps[], orgData?: InterfaceIOrgList, diff --git a/src/components/OrgPostCard/OrgPostCard.tsx b/src/components/OrgPostCard/OrgPostCard.tsx index 76ae616277..be71622c7c 100644 --- a/src/components/OrgPostCard/OrgPostCard.tsx +++ b/src/components/OrgPostCard/OrgPostCard.tsx @@ -25,57 +25,6 @@ interface InterfaceOrgPostCardProps { postVideo: string | null; pinned: boolean; } - -/** - * `OrgPostCard` component for displaying and managing organizational posts. - * - * This component displays a post card with options to view details, edit, delete, pin, or unpin the post. It supports video and image content, and includes modals for editing and deleting posts. The component uses GraphQL mutations for post management and integrates with a toast notification system for user feedback. - * - * ### Props - * - `key` (string): Unique key for the component. - * - `id` (string): Unique identifier of the post. - * - `postTitle` (string): Title of the post. - * - `postInfo` (string): Description or information about the post. - * - `postAuthor` (string): Author of the post. - * - `postPhoto` (string | null): URL of the post's image. - * - `postVideo` (string | null): URL of the post's video. - * - `pinned` (boolean): Indicates whether the post is pinned. - * - * ### State - * - `postformState`: State object for managing form data. - * - `postPhotoUpdated`: Boolean flag indicating if the post photo has been updated. - * - `postVideoUpdated`: Boolean flag indicating if the post video has been updated. - * - `togglePost`: String indicating the current toggle state ('Read more' or 'hide'). - * - `showEditModal`: Boolean flag indicating whether the edit modal is visible. - * - `showDeleteModal`: Boolean flag indicating whether the delete modal is visible. - * - `modalVisible`: Boolean flag indicating whether the modal for viewing post details is visible. - * - `menuVisible`: Boolean flag indicating whether the options menu is visible. - * - `playing`: Boolean flag indicating whether the video is playing. - * - * ### Refs - * - `videoRef`: Ref for the video element. - * - * ### Methods - * - `togglePostPin(id: string, pinned: boolean)`: Toggles the pinned state of the post. - * - `toggleShowEditModal()`: Toggles visibility of the edit modal. - * - `toggleShowDeleteModal()`: Toggles visibility of the delete modal. - * - `handleVideoPlay()`: Starts playing the video. - * - `handleVideoPause()`: Pauses the video. - * - `handleCardClick()`: Displays the modal with post details. - * - `handleMoreOptionsClick()`: Displays the options menu. - * - `clearImageInput()`: Clears the selected image input. - * - `clearVideoInput()`: Clears the selected video input. - * - `handletoggleClick()`: Toggles the visibility of the post information. - * - `handleEditModal()`: Shows the edit modal. - * - `handleDeleteModal()`: Shows the delete modal. - * - `handleInputEvent(e: ChangeEvent)`: Updates form state based on input changes. - * - `updatePostHandler(e: ChangeEvent)`: Handles form submission to update the post. - * - `deletePost()`: Deletes the post and provides feedback. - * - * ### Effects - * - Synchronizes form state with props on initial render. - */ - export default function orgPostCard( props: InterfaceOrgPostCardProps, ): JSX.Element { diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx index 70371e0833..9d6ec65187 100644 --- a/src/components/UsersTableItem/UsersTableItem.tsx +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -20,31 +20,6 @@ type Props = { loggedInUserId: string; resetAndRefetch: () => void; }; -/** - * `UsersTableItem` is a React component that represents a row in a table displaying user information. - * This component handles displaying and interacting with user details, including: - * - * - Viewing organizations the user has joined or blocked. - * - Searching through these organizations. - * - Changing the user's role in various organizations. - * - Removing the user from an organization. - * - * It also manages multiple modals: - * - * - **Joined Organizations Modal**: Displays a list of organizations the user has joined, with search functionality. - * - **Blocked Organizations Modal**: Displays a list of organizations that have blocked the user, with search functionality. - * - **Remove User Modal**: Confirms the removal of the user from a specific organization. - * - * - * @param props - The props for the `UsersTableItem` component. - * @param user - The user data to be displayed. - * @param index - The index of the user in the table. - * @param loggedInUserId - The ID of the currently logged-in user. - * @param resetAndRefetch - A function to reset and refetch the data. - * - * @returns The rendered component. - */ - const UsersTableItem = (props: Props): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); const { t: tCommon } = useTranslation('common'); diff --git a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx index 9c5adfaa2a..71199db5e4 100644 --- a/src/screens/FundCampaignPledge/FundCampaignPledge.tsx +++ b/src/screens/FundCampaignPledge/FundCampaignPledge.tsx @@ -56,44 +56,6 @@ const dataGridStyle = { borderRadius: '0.5rem', }, }; -/** - * `FundCampaignPledge` component displays and manages pledges for a fundraising campaign. - * - * This component fetches pledge data for a specific fundraising campaign using a GraphQL query. - * It supports searching, sorting, and managing pledges through modals for creating, editing, and deleting pledges. - * - * ## Functional Components: - * - **DataGrid**: Displays pledge information in a tabular format. - * - **PledgeModal**: Modal for creating or editing a pledge. - * - **PledgeDeleteModal**: Modal for confirming pledge deletion. - * - **BasePopup**: Popup to display additional users associated with a pledge. - * - * ## State Management: - * - `campaignInfo`: Stores information about the campaign. - * - `modalState`: Tracks the visibility state of modals. - * - `anchor`: Manages anchor for the popup display. - * - `extraUsers`: Holds additional users to be displayed in the popup. - * - `progressIndicator`: Indicates whether to show pledged or raised amount in the progress bar. - * - `pledgeModalMode`: Determines whether the modal is in edit or create mode. - * - `pledge`: Contains details of the currently selected pledge. - * - `searchTerm`: The term used for searching pledgers. - * - `sortBy`: Determines the sorting order of pledges. - * - * ## Event Handlers: - * - `openModal`: Opens the specified modal. - * - `closeModal`: Closes the specified modal. - * - `handleOpenModal`: Prepares and opens the modal for editing or creating a pledge. - * - `handleDeleteClick`: Prepares and opens the modal for deleting a pledge. - * - `handleClick`: Opens the popup to display additional users. - * - * ## Render: - * - Renders breadcrumbs for navigation. - * - Displays campaign details, including progress bar for pledged or raised amount. - * - Provides search and filter options for pledges. - * - Renders the `DataGrid` component with pledge details. - * - Displays modals and popup for user interactions. - * - */ const fundCampaignPledge = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'pledges',