From 0f56ecd500681f685f03a953ef9a30a5694529f1 Mon Sep 17 00:00:00 2001 From: IRAGENA Aime Divin Date: Fri, 15 Nov 2024 12:06:31 +0200 Subject: [PATCH] feat(super-admin-dashboard): implements a super admin dashboard (#625) --- package-lock.json | 29 +- package.json | 4 +- src/assets/dash-event-icon.svg | 3 + src/assets/multiple-users.svg | 3 + src/assets/no-event.png | Bin 0 -> 26455 bytes src/components/Calendar.tsx | 212 +++-- src/components/OrgStatusSymbol.tsx | 33 + src/components/Organizations.tsx | 41 +- src/components/icons/MultipleLogins.tsx | 20 + src/containers/DashRoutes.tsx | 4 +- src/pages/Dashboard.tsx | 4 +- src/pages/Organization/AdminLogin.tsx | 2 +- src/pages/SupAdDashboard.tsx | 30 - src/pages/SuperAdminDashboard.tsx | 797 ++++++++++++++++++ src/queries/organization.queries.tsx | 60 ++ tailwind.config.js | 43 +- tests/components/Calendar.test.tsx | 6 +- .../AdminTraineeDashboard.test.tsx.snap | 2 +- .../__snapshots__/Cohorts.test.tsx.snap | 38 +- tests/pages/SupAdDashboard.test.tsx | 11 - tests/pages/SuperAdminDashboard.test.tsx | 364 ++++++++ .../AdminTraineeDashboard.test.tsx.snap | 2 +- .../__snapshots__/Admindash.test.tsx.snap | 38 +- .../__snapshots__/GradingSystem.test.tsx.snap | 40 +- .../SupAdDashboard.test.tsx.snap | 43 - .../SuperAdminDashboard.test.tsx.snap | 538 ++++++++++++ .../TraineeRatingDashboard.test.tsx.snap | 40 +- .../UpdateTraineeRating.test.tsx.snap | 40 +- .../__snapshots__/userRegister.test.tsx.snap | 2 +- 29 files changed, 2131 insertions(+), 318 deletions(-) create mode 100644 src/assets/dash-event-icon.svg create mode 100644 src/assets/multiple-users.svg create mode 100644 src/assets/no-event.png create mode 100644 src/components/OrgStatusSymbol.tsx create mode 100644 src/components/icons/MultipleLogins.tsx delete mode 100644 src/pages/SupAdDashboard.tsx create mode 100644 src/pages/SuperAdminDashboard.tsx create mode 100644 src/queries/organization.queries.tsx delete mode 100644 tests/pages/SupAdDashboard.test.tsx create mode 100644 tests/pages/SuperAdminDashboard.test.tsx delete mode 100644 tests/pages/__snapshots__/SupAdDashboard.test.tsx.snap create mode 100644 tests/pages/__snapshots__/SuperAdminDashboard.test.tsx.snap diff --git a/package-lock.json b/package-lock.json index 172e4d9a0..f859dc9b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "apollo-upload-client": "^17.0.0", "autoprefixer": "^10.4.14", "axios": "^1.6.1", - "chart.js": "^4.3.2", + "chart.js": "^4.4.6", "cleave.js": "^1.6.0", "cloudinary": "^1.39.0", "cloudinary-react": "^1.8.1", @@ -85,7 +85,7 @@ "react-tooltip": "^4.5.1", "react-widgets": "^5.8.4", "reactjs-popup": "^2.0.5", - "recharts": "^2.7.2", + "recharts": "^2.13.3", "sheetjs-style": "^0.15.8", "sinon": "^14.0.2", "subscriptions-transport-ws": "^0.11.0", @@ -4043,7 +4043,8 @@ "node_modules/@kurkle/color": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.11", @@ -7197,9 +7198,10 @@ } }, "node_modules/chart.js": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", - "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -19698,6 +19700,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" @@ -20276,14 +20279,15 @@ } }, "node_modules/recharts": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", - "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.3.tgz", + "integrity": "sha512-YDZ9dOfK9t3ycwxgKbrnDlRC4BHdjlY73fet3a0C1+qGMjXVZe6+VXmpOIIhzkje5MMEL8AN4hLIe4AMskBzlA==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", - "react-is": "^16.10.2", + "react-is": "^18.3.1", "react-smooth": "^4.0.0", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", @@ -20310,11 +20314,6 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, - "node_modules/recharts/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/package.json b/package.json index a053d4731..ca823aa29 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "apollo-upload-client": "^17.0.0", "autoprefixer": "^10.4.14", "axios": "^1.6.1", - "chart.js": "^4.3.2", + "chart.js": "^4.4.6", "cleave.js": "^1.6.0", "cloudinary": "^1.39.0", "cloudinary-react": "^1.8.1", @@ -107,7 +107,7 @@ "react-tooltip": "^4.5.1", "react-widgets": "^5.8.4", "reactjs-popup": "^2.0.5", - "recharts": "^2.7.2", + "recharts": "^2.13.3", "sheetjs-style": "^0.15.8", "sinon": "^14.0.2", "subscriptions-transport-ws": "^0.11.0", diff --git a/src/assets/dash-event-icon.svg b/src/assets/dash-event-icon.svg new file mode 100644 index 000000000..a7d30c74d --- /dev/null +++ b/src/assets/dash-event-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/multiple-users.svg b/src/assets/multiple-users.svg new file mode 100644 index 000000000..8b94610db --- /dev/null +++ b/src/assets/multiple-users.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/no-event.png b/src/assets/no-event.png new file mode 100644 index 0000000000000000000000000000000000000000..5e251d22fb29b4ac33c3ade5665c47ca5b179fbc GIT binary patch literal 26455 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 m?;7Yi%=~Vjo^|uA2L2a5l+wJlk3gaT0000 { fetchPolicy: 'network-only', }); } catch (error: any) { - toast.error(error.message) + toast.error(error.message); } }; useEffect(() => { - fetchData() + fetchData(); }, []); const renderEvent = (e: EventContentArg) => ( @@ -91,10 +91,10 @@ const Calendar = () => { authToken: localStorage.getItem('auth_token'), orgToken: localStorage.getItem('orgToken'), invitees: selectedGuests, - } + }, }) .then(() => { - fetchData() + fetchData(); toast.success('Event has been added!'); // {{ edit_1 }} setNewEvent({ title: '', @@ -104,11 +104,10 @@ const Calendar = () => { timeToStart: '', timeToEnd: '', }); - setSelectedGuests([]) + setSelectedGuests([]); setTimeout(() => { setAddEventModel(false); }, 1000); - }) .catch((error) => { toast.error(error.message); // Handle error if needed @@ -124,8 +123,8 @@ const Calendar = () => { }; //edit section - const [editEvent] = useMutation(EDIT_EVENT) - const [editEventModel, setEditEventModel] = useState(false) + const [editEvent] = useMutation(EDIT_EVENT); + const [editEventModel, setEditEventModel] = useState(false); const [editedEvent, setEditedEvent] = useState({ id: '', title: '', @@ -137,9 +136,12 @@ const Calendar = () => { }); const handleEditEventModel = async (e: EventInput) => { - const event = data?.getEvents.find((event: any)=> event.id === e.event?.id) + const event = data?.getEvents.find( + (event: any) => event.id === e.event?.id, + ); if (event) { - if(event.user !== JSON.parse(localStorage.getItem('auth')!).userId) return + if (event.user !== JSON.parse(localStorage.getItem('auth')!).userId) + return; setEditedEvent((prev) => { return { ...prev, @@ -150,16 +152,16 @@ const Calendar = () => { hostName: event.hostName, timeToStart: event.timeToStart, timeToEnd: event.timeToEnd, - } - }) - setSelectedGuests(event.invitees.map((invitee: any) => invitee.email)) + }; + }); + setSelectedGuests(event.invitees.map((invitee: any) => invitee.email)); setEditEventModel(true); } }; const handleEditEvent = async (e: any) => { - e.preventDefault() - const { id, ...rest } = editedEvent + e.preventDefault(); + const { id, ...rest } = editedEvent; editEvent({ variables: { eventId: id, @@ -170,7 +172,7 @@ const Calendar = () => { }, }) .then(() => { - fetchData() + fetchData(); toast.success('Event has been updated!'); setEditedEvent({ id: '', @@ -181,7 +183,7 @@ const Calendar = () => { timeToStart: '', timeToEnd: '', }); - setSelectedGuests([]) + setSelectedGuests([]); setTimeout(() => { setEditEventModel(false); }, 1000); @@ -189,34 +191,34 @@ const Calendar = () => { .catch((error) => { toast.error(error.message); // Handle error if needed }); - } + }; const removeEditModel = (e: any) => { - e.preventDefault() - setSelectedGuests([]) - setEditEventModel(!editEventModel) - } + e.preventDefault(); + setSelectedGuests([]); + setEditEventModel(!editEventModel); + }; // delete section - const [showDeleteModal, setShowDeleteModal] = useState(false) - const [cancelEvent] = useMutation(CANCEL_EVENT) + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [cancelEvent] = useMutation(CANCEL_EVENT); const handleDeleteConfirmation = (e: any) => { - e.preventDefault() - setShowDeleteModal(prev => !prev) - } + e.preventDefault(); + setShowDeleteModal((prev) => !prev); + }; const handleDelete = async (e: any) => { - e.preventDefault() + e.preventDefault(); cancelEvent({ variables: { eventId: editedEvent.id, - authToken: localStorage.getItem('auth_token') + authToken: localStorage.getItem('auth_token'), }, }) .then(() => { - fetchData() - toast.success('Event cancelled successfully') + fetchData(); + toast.success('Event cancelled successfully'); setEditedEvent({ id: '', title: '', @@ -226,23 +228,24 @@ const Calendar = () => { timeToStart: '', timeToEnd: '', }); - setSelectedGuests([]) + setSelectedGuests([]); setTimeout(() => { - setShowDeleteModal(false) + setShowDeleteModal(false); setEditEventModel(false); }, 1000); - } - ) - .catch(err => { - toast.error(err.message) }) - } + .catch((err) => { + toast.error(err.message); + }); + }; return ( <>
@@ -366,11 +369,14 @@ const Calendar = () => { {showTraineeDropdown ? '-' : '+'}
- {showTraineeDropdown ? + {showTraineeDropdown ? ( : ''} + /> + ) : ( + '' + )}
@@ -381,7 +387,10 @@ const Calendar = () => { > {t('cancel')} -
@@ -391,8 +400,10 @@ const Calendar = () => {
@@ -448,7 +459,11 @@ const Calendar = () => { className=" dark:bg-dark-tertiary dark:text-white border border-primary rounded outline-none px-5 font-sans text-xs py-2 w-full" placeholderText={t('Start Date')} style={{ marginRight: '10px' }} - selected={editedEvent.start ? new Date(editedEvent.start) : new Date()} + selected={ + editedEvent.start + ? new Date(editedEvent.start) + : new Date() + } onChange={(start: any) => setEditedEvent({ ...editedEvent, @@ -465,8 +480,12 @@ const Calendar = () => { className="dark:bg-dark-tertiary dark:text-white border border-primary rounded outline-none px-5 font-sans text-xs py-2 w-full" placeholderText={t('End Date')} style={{ marginRight: '10px' }} - selected={editedEvent.end ? new Date(editedEvent.end) : new Date()} - onChange={(end: any) => setEditedEvent({ ...editedEvent, end })} + selected={ + editedEvent.end ? new Date(editedEvent.end) : new Date() + } + onChange={(end: any) => + setEditedEvent({ ...editedEvent, end }) + } />
@@ -519,11 +538,14 @@ const Calendar = () => { {showTraineeDropdown ? '-' : '+'}
- {showTraineeDropdown ? + {showTraineeDropdown ? ( : ''} + /> + ) : ( + '' + )}
@@ -534,11 +556,19 @@ const Calendar = () => { > {t('cancel')} -
- -
@@ -549,8 +579,10 @@ const Calendar = () => {
@@ -559,16 +591,26 @@ const Calendar = () => {
-

Please confirm the cancellation of event {editedEvent.title}.

+

+ Please confirm the cancellation of event{' '} + {editedEvent.title}. +

-
@@ -579,34 +621,36 @@ const Calendar = () => {

{t('Calendar')}

- {JSON.parse(localStorage.getItem('auth')!).role !== "trainee" ? - - :''} + {JSON.parse(localStorage.getItem('auth')!).role !== 'trainee' ? ( + + ) : ( + '' + )} {loading ? ( ) : ( - ({ - id: event.id, - end: moment(event.end).add({days:1}).format('YYYY-MM-DD'), - start: moment(event.start).format('YYYY-MM-DD'), - hostName: event.hostName, - timeToStart: event.timeToStart, - title: event.title, - timeToEnd: event.timeToEnd, - allDay: true, - }))} - plugins={[dayGridPlugin, interactionPlugin]} - initialView="dayGridMonth" - eventClick={handleEditEventModel} - /> + ({ + id: event.id, + end: moment(event.end).add({ days: 1 }).format('YYYY-MM-DD'), + start: moment(event.start).format('YYYY-MM-DD'), + hostName: event.hostName, + timeToStart: event.timeToStart, + title: event.title, + timeToEnd: event.timeToEnd, + allDay: true, + }))} + plugins={[dayGridPlugin, interactionPlugin]} + initialView="dayGridMonth" + eventClick={handleEditEventModel} + /> )}
diff --git a/src/components/OrgStatusSymbol.tsx b/src/components/OrgStatusSymbol.tsx new file mode 100644 index 000000000..8e3072712 --- /dev/null +++ b/src/components/OrgStatusSymbol.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +interface PropsInterface { + type: 'active' | 'pending' | 'rejected'; + label?: boolean; +} + +function OrgStatusSymbol({ type, label }: PropsInterface) { + const colorClasses = { + active: 'border-[#11AF0E] bg-[#11AF0E]', + pending: 'border-[#FFA500] bg-[#FFA500]', + rejected: 'border-[#C30909] bg-[#C30909]', + }; + + const selectedColor = colorClasses[type]; + + return ( +
+
+
+
+ {label && {type}} +
+ ); +} + +export default OrgStatusSymbol; diff --git a/src/components/Organizations.tsx b/src/components/Organizations.tsx index 227ea77b4..63fdd67e6 100644 --- a/src/components/Organizations.tsx +++ b/src/components/Organizations.tsx @@ -14,11 +14,16 @@ import OrgSkeleton from '../Skeletons/Organization.skeleton'; import { DeleteOrganization } from '../Mutations/OrganisationMutations'; import { RegisterNewOrganization } from '../Mutations/OrganisationMutations'; import { AddOrganization } from '../Mutations/OrganisationMutations'; +import { GET_ORGANIZATIONS } from '../queries/organization.queries'; import jwtDecode from 'jwt-decode'; import { useSearchParams,useNavigate } from 'react-router-dom'; export interface Admin { id: string; + profile: { + name: string; + phoneNumber: string + }; email: string; } export interface Organization { @@ -26,24 +31,10 @@ export interface Organization { name: string; description: string; admin: Admin; + status: 'active' | 'rejected' | 'pending'; [x: string]: any; } -export const getOrganizations = gql` - query GetOrganizations { - getOrganizations { - id - name - description - admin { - id - email - } - status - } - } -`; - function ActionButtons({ getData, setData, @@ -145,7 +136,7 @@ const Organizations = () => { loading: boolean; error?: any; refetch: Function; - } = useQuery(getOrganizations); + } = useQuery(GET_ORGANIZATIONS); const ApproveNewOrganization= async (token:string)=>{ try { @@ -583,15 +574,15 @@ useEffect(() => {
- {getLoading ? ( - - ) : ( - - )} + {getLoading ? ( + + ) : ( + + )}
diff --git a/src/components/icons/MultipleLogins.tsx b/src/components/icons/MultipleLogins.tsx new file mode 100644 index 000000000..52b0b4c0d --- /dev/null +++ b/src/components/icons/MultipleLogins.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +function MultipleLogins({ color }: { color: string }) { + return ( + + + + ); +} + +export default MultipleLogins; diff --git a/src/containers/DashRoutes.tsx b/src/containers/DashRoutes.tsx index d522e7b02..c057430b4 100644 --- a/src/containers/DashRoutes.tsx +++ b/src/containers/DashRoutes.tsx @@ -50,7 +50,7 @@ const AdminRatings = React.lazy(() => import('../pages/AdminRatings')); const UpdatedRatingDashboard = React.lazy( () => import('../pages/UpdatedRatingDashboard'), ); -const SupAdDashboard = React.lazy(() => import('../pages/SupAdDashboard')); +const SuperAdminDashboard = React.lazy(() => import('../pages/SuperAdminDashboard')); const Calendar = React.lazy(() => import('../components/Calendar')); const CoordinatorsPage = React.lazy( () => import('../containers/admin-dashBoard/CoordinatorModal'), @@ -143,7 +143,7 @@ function DashRoutes() { } /> } /> {/* } /> */} - } /> + } /> } /> } /> } /> diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 97e231bec..2d21af987 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import CheckRole from '../utils/CheckRoles'; -import SupAdDashboard from './SupAdDashboard'; +import SuperAdminDashboard from './SuperAdminDashboard'; import AdminDashboard from './AdminDashboard'; import TraineeDashboard from './TraineeDashboard'; import ManagerCard from '../components/ManagerCard'; @@ -10,7 +10,7 @@ export function Dashboard() { return ( <> - + diff --git a/src/pages/Organization/AdminLogin.tsx b/src/pages/Organization/AdminLogin.tsx index dfd8dcabc..f0c25fbef 100644 --- a/src/pages/Organization/AdminLogin.tsx +++ b/src/pages/Organization/AdminLogin.tsx @@ -91,7 +91,7 @@ function AdminLogin() { redirect ? navigate(`${redirect}`) : data.loginUser.user.role === 'superAdmin' - ? navigate(`/organizations${redirectParams}`) + ? navigate(`/dashboard`) : data.loginUser.user.role === 'admin' ? navigate(`/trainees`) : data.loginUser.user.role === 'coordinator' diff --git a/src/pages/SupAdDashboard.tsx b/src/pages/SupAdDashboard.tsx deleted file mode 100644 index 593eb3585..000000000 --- a/src/pages/SupAdDashboard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import Chart from '../components/Chart'; -import Card from '../components/Card'; -import useDocumentTitle from '../hook/useDocumentTitle'; -import Comingsoon from './Comingsoon'; - -function SupAdDashboard() { - useDocumentTitle('Dashboard'); - const { t } = useTranslation(); - return ( -
-
-
- {/*
- - - - -
- */} - - -
-
-
- ); -} - -export default SupAdDashboard; diff --git a/src/pages/SuperAdminDashboard.tsx b/src/pages/SuperAdminDashboard.tsx new file mode 100644 index 000000000..5a62f4536 --- /dev/null +++ b/src/pages/SuperAdminDashboard.tsx @@ -0,0 +1,797 @@ +/* eslint-disable react/no-array-index-key */ +import React, { useEffect, useState, useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import { GlobeAltIcon, HomeIcon, UsersIcon } from '@heroicons/react/solid'; +import { BiCalendarStar } from 'react-icons/bi'; +import { AtSign, MapPin } from 'lucide-react'; +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import Skeleton from 'react-loading-skeleton'; +import 'react-loading-skeleton/dist/skeleton.css'; +import { GoOrganization } from 'react-icons/go'; +import { GrGroup } from 'react-icons/gr'; +import { RiAdminLine } from 'react-icons/ri'; +import { useLazyQuery, useQuery } from '@apollo/client'; +import { format } from 'date-fns'; +import { Link, useNavigate } from 'react-router-dom'; +import { MdOutlineMail } from 'react-icons/md'; +import { ThemeContext } from '../hook/ThemeProvider'; +import useDocumentTitle from '../hook/useDocumentTitle'; +import OrgStatusSymbol from '../components/OrgStatusSymbol'; +import MultipleLogins from '../components/icons/MultipleLogins'; +import { Organization } from '../components/Organizations'; +import { + GET_ORGANIZATIONS, + GET_ALL_ORG_USERS, + GET_REGISTRATION_STATS, +} from '../queries/organization.queries'; +import { GET_EVENTS } from '../queries/event.queries'; +import { UserInterface } from './TraineeAttendanceTracker'; +import NoEvents from '../assets/no-event.png'; + +interface EventsInterface { + id: string; + title: string; + start: string; + end: string; + timeToStart: string; + timeToEnd: string; + hostName: string; +} + +interface AllOrgUsersInterface { + totalUsers: number; + organizations: { + organization: Organization; + members: UserInterface[]; + loginsCount: number; + monthPercentage: number; + recentLocation: string | null; + }[]; +} + +interface RegistrationDataStatsInterface { + month: + | 'jan' + | 'feb' + | 'mar' + | 'apr' + | 'may' + | 'jun' + | 'jul' + | 'aug' + | 'sep' + | 'oct' + | 'nov' + | 'dec' + | null; + users: number | null; + organizations: number | null; +} +interface RegistrationDataInterface { + year: number; + stats: RegistrationDataStatsInterface[]; +} + +function SuperAdminDashboard() { + useDocumentTitle('Dashboard'); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { colorTheme } = useContext(ThemeContext); + const [allOrganizationData, setAllOrganizationData] = useState< + Organization[] + >([]); + const [upcomingEvents, setUpcomingEvents] = useState([]); + const [allOrgsUsers, setAllOrgsUsers] = useState({ + totalUsers: 0, + organizations: [], + }); + + const [registrationData, setRegistrationData] = + useState(); + + const [selectedRegistrationData, setSelectedRegistrationData] = + useState(); + + const [selectedYear, setSelectedYear] = useState(); + const [registrationYears, setRegistrationYears] = useState(); + + const { + data: organizationsData, + loading: getOrganizationsDataLoading, + error: getOrganizationsDataError, + refetch: getOrganizationsDataRefetch, + }: { + data?: { + getOrganizations: Organization[]; + }; + loading: boolean; + error?: any; + refetch: Function; + } = useQuery(GET_ORGANIZATIONS); + + const [getEvents, { loading: getEventsDataLoading }] = + useLazyQuery(GET_EVENTS); + + const [getAllOrgUsers, { loading: getAllOrgUsersLoading }] = + useLazyQuery(GET_ALL_ORG_USERS); + + const [getRegistrationStats, { loading: getRegistrationStatsLoading }] = + useLazyQuery(GET_REGISTRATION_STATS); + + useEffect(() => { + if (organizationsData) { + setAllOrganizationData(organizationsData.getOrganizations); + } + }, [organizationsData]); + + useEffect(() => { + getEvents({ + variables: { + authToken: localStorage.getItem('auth_token'), + }, + fetchPolicy: 'network-only', + onCompleted: (data) => { + setUpcomingEvents((prevData) => { + const events: EventsInterface[] = data.getEvents; + return events + .filter((event) => new Date(event.start).getTime() >= Date.now()) + .sort( + (a, b) => + new Date(a.start).getTime() - new Date(b.start).getTime(), + ) + .slice(0, 3); + }); + }, + }); + }, []); + useEffect(() => { + getAllOrgUsers({ + fetchPolicy: 'network-only', + onCompleted: (data) => { + setAllOrgsUsers(data.getAllOrgUsers); + }, + }); + }, []); + useEffect(() => { + getRegistrationStats({ + fetchPolicy: 'network-only', + onCompleted: (data) => { + setRegistrationData(data.getRegistrationStats); + }, + }); + }, []); + + useEffect(() => { + const years = [new Date().getFullYear()]; + if (registrationData) { + years.push(...registrationData.map((data) => data.year)); + const sanitizedYears = [...new Set(years)].sort((a, b) => b - a); + setRegistrationYears(sanitizedYears); + setSelectedYear(sanitizedYears[0]); + return; + } + + const sanitizedYears = [...new Set(years)].sort((a, b) => b - a); + + setRegistrationYears(years); + }, [registrationData]); + + useEffect(() => { + const months = [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + ]; + let data: RegistrationDataStatsInterface[] = [ + { + month: null, + users: 0, + organizations: 0, + }, + ...months.map((month) => ({ + month: month as RegistrationDataStatsInterface['month'], + users: null, + organizations: null, + })), + ]; + if (registrationData) { + const tempData = registrationData.find( + (data) => data.year === selectedYear, + ); + if (tempData && tempData.stats.length) data = tempData.stats; + } + setSelectedRegistrationData(data); + }, [selectedYear]); + + const statsSkeleton = ( +
+ + + +
+ ); + + return ( +
+
+
+ {!getOrganizationsDataLoading && !getAllOrgUsersLoading && ( + <> +
+ + + Organizations + +
+
+

+ {allOrganizationData.length.toString().padStart(2, '0') || + '00'} +

+
+
+
+ +
+ + {allOrganizationData + .filter((org) => org.status.toLowerCase() === 'active') + .length.toString() + .padStart(2, '0') || '00'} + +
+
+
+ +
+ + {allOrganizationData + .filter((org) => org.status.toLowerCase() === 'pending') + .length.toString() + .padStart(2, '0') || '00'} + +
+
+
+ +
+ + {allOrganizationData + .filter( + (org) => org.status.toLowerCase() === 'rejected', + ) + .length.toString() + .padStart(2, '0') || '00'} + +
+
+
+ + )} + {(getOrganizationsDataLoading || getAllOrgUsersLoading) && + statsSkeleton} +
+
+ {!getOrganizationsDataLoading && !getAllOrgUsersLoading && ( + <> +
+ +

+ USERS +

+
+
+ + {allOrgsUsers.totalUsers.toString().padStart(2, '0')} + +
+ + )} + {(getOrganizationsDataLoading || getAllOrgUsersLoading) && + statsSkeleton} +
+
+ {!getOrganizationsDataLoading && !getAllOrgUsersLoading && ( + <> +
+ + + Domains + +
+
+ + 00 + +
+ + )} + {(getOrganizationsDataLoading || getAllOrgUsersLoading) && + statsSkeleton} +
+
+
+ {!getRegistrationStatsLoading && ( + <> +
+

Monthly Registrations

+
+ +
+
+
+ 700 + ? 350 + : window.innerWidth > 500 + ? 300 + : 250 + } + > + + + + + + + + + + +
+ {registrationYears?.map((year) => ( + + ))} +
+
+ + )} + {getRegistrationStatsLoading && ( +
+ + +
+ )} +
+
+
+ {!getOrganizationsDataLoading && ( + <> +
+

Recent Organization Requests

+
+
+
+ + + + + + + + + + {allOrganizationData + .slice( + allOrganizationData.length - 5, + allOrganizationData.length, + ) + .map((org) => ( + navigate('/organizations')} + > + + + + + ))} + +
+ Name + + Admin-Email + + Status +
+ {org.name} + + { + // eslint-disable-next-line no-nested-ternary + org.admin + ? // eslint-disable-next-line no-nested-ternary + org.admin.email.length > 20 + ? window.innerWidth > 530 + ? `${org.admin.email.slice(0, 22)}..` + : `${org.admin.email.slice(0, 16)}..` + : org.admin.email + : '-' + } + + +
+
+
+ + + +
+
+ + )} + {getOrganizationsDataLoading && ( +
+ + + {[...Array(5)].map((_, index) => ( + + ))} +
+ )} +
+
+ {!getEventsDataLoading && ( + <> +
+

Upcoming Events

+
+
+ {upcomingEvents.length ? ( + upcomingEvents.map((event) => ( + +
+ +
+
+ {event.title} - +

+ + {event.hostName} +

+
+
+

+ {event.timeToStart} + {event.timeToEnd && ` - ${event.timeToEnd}`} +

+

+ + {format(new Date(event.start), 'dd, MMM yyyy')} + +  -  + + {format(new Date(event.end), 'dd, MMM yyyy')} + +

+
+
+
+ + )) + ) : ( +
+ NoEventsImage +

+ Oops! No upcoming events scheduled +

+
+ )} +
+ + )} + {getEventsDataLoading && ( +
+ + +
+ )} +
+
+
+
+ {!getAllOrgUsersLoading && ( + <> +
+

+ Organization Updates scheduled +

+
+
+ {allOrgsUsers.organizations + .slice() + .sort((a, b) => b.members.length - a.members.length) + .slice(0, 3) + .map((org) => ( +
+
+
+ + + {org.organization.name} + +
+ + {format(new Date(), 'MMM, yyyy')} + +
+
+

+ + + {(org.organization.admin && + org.organization.admin.profile && + org.organization.admin.profile.name) || + '-'} + +

+

+ + {org.organization.admin && + org.organization.admin.email ? ( + + {org.organization.admin.email} + + ) : ( + - + )} +

+

+ + {org.members.length} +

+ {`${org.monthPercentage.toPrecision(2)}%`} +

+

+
+
+ ))} +
+ + )} + {getAllOrgUsersLoading && ( +
+ + +
+ )} +
+
+ {!getAllOrgUsersLoading && ( + <> +
+

Today's Login Overview

+
+
+ {allOrgsUsers.organizations + .slice() + .sort((a, b) => b.loginsCount - a.loginsCount) + .slice(0, 4) + .map((org) => ( +
+
+
+ +
+

{org.organization.name}

+

{org.loginsCount}

+
+
+
+
+
+ + + {org.recentLocation || 'unavailable'} + + {org.recentLocation && ( +

+ (Recent login location) +

+ )} +
+
+
+ ))} +
+ + )} + {getAllOrgUsersLoading && ( +
+ + +
+ )} +
+
+
+ ); +} + +export default SuperAdminDashboard; diff --git a/src/queries/organization.queries.tsx b/src/queries/organization.queries.tsx new file mode 100644 index 000000000..bc2335428 --- /dev/null +++ b/src/queries/organization.queries.tsx @@ -0,0 +1,60 @@ +import { gql } from '@apollo/client'; + +export const GET_ORGANIZATIONS = gql` + query GetOrganizations { + getOrganizations { + id + name + description + admin { + id + email + } + status + } + } +`; +export const GET_ALL_ORG_USERS = gql` + query GetAllOrgUsers { + getAllOrgUsers { + totalUsers + organizations { + organization { + id + name + description + admin { + id + email + profile { + name + phoneNumber + } + } + status + } + members { + email + profile { + name + } + } + monthPercentage + loginsCount + recentLocation + } + } + } +`; +export const GET_REGISTRATION_STATS = gql` + query GetRegistrationStats { + getRegistrationStats { + year + stats { + month + users + organizations + } + } + } +`; diff --git a/tailwind.config.js b/tailwind.config.js index 9ac45761f..7c55c1500 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -4,8 +4,9 @@ module.exports = { theme: { extend: { screens: { - xm:'360px', + xm: '360px', sm: '375px', + xsm: '500px', xmd: '600px', md: '768px', lg: '976px', @@ -40,27 +41,31 @@ module.exports = { // sans: ['PT Serif', 'serif'], serif: ['Inter', 'serif'], }, - extend: { - borderRadius: { - '4xl': '2rem', - }, - keyframes: { - wave: { - '0%': { transform: 'rotate(0.0deg)' }, - '10%': { transform: 'rotate(14deg)' }, - '20%': { transform: 'rotate(-8deg)' }, - '30%': { transform: 'rotate(14deg)' }, - '40%': { transform: 'rotate(-4deg)' }, - '50%': { transform: 'rotate(10.0deg)' }, - '60%': { transform: 'rotate(0.0deg)' }, - '100%': { transform: 'rotate(0.0deg)' }, - }, + borderRadius: { + '4xl': '2rem', + }, + keyframes: { + wave: { + '0%': { transform: 'rotate(0.0deg)' }, + '10%': { transform: 'rotate(14deg)' }, + '20%': { transform: 'rotate(-8deg)' }, + '30%': { transform: 'rotate(14deg)' }, + '40%': { transform: 'rotate(-4deg)' }, + '50%': { transform: 'rotate(10.0deg)' }, + '60%': { transform: 'rotate(0.0deg)' }, + '100%': { transform: 'rotate(0.0deg)' }, }, - animation: { - 'waving-hand': 'wave 10s linear infinite', + 'ping-live': { + '0%': { transform: 'scale(0.8)', opacity: '1' }, + '50%': { transform: 'scale(1)', opacity: '0.7' }, + '100%': { transform: 'scale(0.8)', opacity: '1' }, }, }, + animation: { + 'waving-hand': 'wave 10s linear infinite', + 'ping-live': 'ping-live 1.5s ease-in-out infinite', + }, }, - plugins: [], }, + plugins: [], }; diff --git a/tests/components/Calendar.test.tsx b/tests/components/Calendar.test.tsx index 57304bfa8..dbd478e6a 100644 --- a/tests/components/Calendar.test.tsx +++ b/tests/components/Calendar.test.tsx @@ -138,7 +138,7 @@ afterEach(()=>{ }) describe('Calendar Tests', () => { - it.skip('should display Calendar events', async () => { + it('should display Calendar events', async () => { render( @@ -189,7 +189,7 @@ describe('Calendar Tests', () => { }) }); - it.skip('should edit event when editEventForm is submitted', async () => { + it('should edit event when editEventForm is submitted', async () => { render( @@ -208,7 +208,7 @@ describe('Calendar Tests', () => { }) }); - it.skip('should delete event when delete button is clicked', async () => { + it('should delete event when delete button is clicked', async () => { render( diff --git a/tests/components/__snapshots__/AdminTraineeDashboard.test.tsx.snap b/tests/components/__snapshots__/AdminTraineeDashboard.test.tsx.snap index 74fc132bf..6af0d7afa 100644 --- a/tests/components/__snapshots__/AdminTraineeDashboard.test.tsx.snap +++ b/tests/components/__snapshots__/AdminTraineeDashboard.test.tsx.snap @@ -463,7 +463,7 @@ Array [ name="date" readOnly={true} type="text" - value="2024-11-02" + value="2024-11-14" />
- - Page - - - 1 - of - 1 - - - + + Page + + + 1 + of + 1 + + + +
- - | Go to page: - + + | Go to page: + +
{ - it('Should render SupAdDashboard', () => { - const elem = renderer.create().toJSON(); - expect(elem).toMatchSnapshot(); - }); -}); diff --git a/tests/pages/SuperAdminDashboard.test.tsx b/tests/pages/SuperAdminDashboard.test.tsx new file mode 100644 index 000000000..d2908f0fb --- /dev/null +++ b/tests/pages/SuperAdminDashboard.test.tsx @@ -0,0 +1,364 @@ +/* eslint-disable class-methods-use-this */ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { MemoryRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { cleanup, fireEvent, render, screen } from '@testing-library/react'; +import SuperAdminDashboard from '../../src/pages/SuperAdminDashboard'; +import { + GET_ALL_ORG_USERS, + GET_ORGANIZATIONS, + GET_REGISTRATION_STATS, +} from '../../src/queries/organization.queries'; +import { GET_EVENTS } from '../../src/queries/event.queries'; + +global.ResizeObserver = class { + observe() {} + + unobserve() {} + + disconnect() {} +}; + +const mocks = [ + { + request: { + query: GET_ALL_ORG_USERS, + }, + result: { + data: { + getAllOrgUsers: { + totalUsers: 2, + organizations: [ + { + organization: { + id: 'test-org-id', + name: 'test-org', + description: 'test-org', + admin: { + id: 'test-org-admin', + email: 'test-org-admin@test.com', + profile: { + name: 'test-org', + }, + }, + status: 'active', + }, + members: [ + { + email: 'test-org-admin@test.com', + profile: { + name: 'test-org', + }, + }, + ], + monthPercentage: 5.88235294117647, + loginsCount: 2, + recentLocation: null, + }, + { + organization: { + id: 'test-org2-id', + name: 'test-org2', + description: 'test-org2', + admin: { + id: 'test-org2-admin', + email: 'test-org2-admin@test.com', + profile: { + name: 'test-org2', + }, + }, + status: 'active', + }, + members: [ + { + email: 'test-org-user@test.com', + profile: { + name: 'test-org-user', + }, + }, + ], + monthPercentage: 0, + loginsCount: 0, + recentLocation: null, + }, + ], + }, + }, + }, + }, + { + request: { + query: GET_EVENTS, + variables: { + authToken: 'mocked-org-token', + }, + }, + result: { + data: { + getEvents: [ + { + id: 'test-event-id', + user: 'test-event-user', + end: '2024-12-10T14:43:49.000Z', + hostName: 'John Doe', + start: '2024-12-01T14:43:49.000Z', + timeToEnd: '14:01', + timeToStart: '08:45', + title: 'test-event-title', + invitees: [ + { + email: 'test-event-user@test.com', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: GET_REGISTRATION_STATS, + }, + result: { + data: { + getRegistrationStats: [ + { + year: 2024, + stats: [ + { + month: 'jan', + users: 0, + organizations: 0, + }, + { + month: 'feb', + users: 0, + organizations: 0, + }, + { + month: 'mar', + users: 0, + organizations: 0, + }, + { + month: 'apr', + users: 0, + organizations: 0, + }, + { + month: 'may', + users: 0, + organizations: 0, + }, + { + month: 'jun', + users: 0, + organizations: 0, + }, + { + month: 'jul', + users: 0, + organizations: 0, + }, + { + month: 'aug', + users: 0, + organizations: 0, + }, + { + month: 'sep', + users: 1, + organizations: 2, + }, + { + month: 'oct', + users: 1, + organizations: 1, + }, + { + month: 'nov', + users: 3, + organizations: 2, + }, + { + month: 'dec', + users: null, + organizations: null, + }, + ], + }, + { + year: 2023, + stats: [ + { + month: 'jan', + users: 0, + organizations: 0, + }, + { + month: 'feb', + users: 0, + organizations: 0, + }, + { + month: 'mar', + users: 0, + organizations: 0, + }, + { + month: 'apr', + users: 0, + organizations: 0, + }, + { + month: 'may', + users: 0, + organizations: 1, + }, + { + month: 'jun', + users: 0, + organizations: 0, + }, + { + month: 'jul', + users: 0, + organizations: 0, + }, + { + month: 'aug', + users: 0, + organizations: 0, + }, + { + month: 'sep', + users: 0, + organizations: 2, + }, + { + month: 'oct', + users: 1, + organizations: 0, + }, + { + month: 'nov', + users: 2, + organizations: 1, + }, + { + month: 'dec', + users: null, + organizations: null, + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: GET_ORGANIZATIONS, + }, + result: { + data: { + getOrganizations: [ + { + id: 'test-org-id', + name: 'test-org', + description: 'test-org-description', + admin: { + id: 'test-org-admin-id', + email: 'test-org-admin@test.com', + }, + status: 'active', + }, + { + id: 'test-org2-id', + name: 'test-org2', + description: 'test-org2-description', + admin: { + id: 'test-org2-admin-id', + email: 'test-org2-admin@test.com', + }, + status: 'pending', + }, + { + id: 'test-org3-id', + name: 'test-org3', + description: 'test-org3-description', + admin: { + id: 'test-org3-admin-id', + email: 'test-org3-admin@test.com', + }, + status: 'rejected', + }, + ], + }, + }, + }, +]; +jest.mock('recharts', () => { + const OriginalRecharts = jest.requireActual('recharts'); + return { + ...OriginalRecharts, + ResponsiveContainer: ({ children }: any) =>
{children}
, + }; +}); + +describe('Super Admin Dashboard test ', () => { + beforeEach(() => { + localStorage.setItem('auth_token', 'mocked-org-token'); + }); + afterEach(async () => { + localStorage.clear(); + jest.restoreAllMocks(); + await cleanup(); + }); + it('Should render SuperAdminDashboard', () => { + const elem = renderer + .create( + + + + + , + ) + .toJSON(); + expect(elem).toMatchSnapshot(); + }); + it('Should display platform stats', async () => { + await cleanup(); + render( + + + + + , + ); + + const orgCardTitle = await screen.findByText('Organizations'); + expect(orgCardTitle).toBeInTheDocument(); + const orgCount = await screen.findByText('03'); + expect(orgCount).toBeInTheDocument(); + }); + it('Should display on graph data from another year', async () => { + render( + + + + + , + ); + expect( + await screen.findByTestId('registrationStatsLoading'), + ).toBeInTheDocument(); + const year = await screen.findAllByText('2024'); + expect(year).toHaveLength(2); + const year2 = await screen.findAllByText('2023'); + expect(year2).toHaveLength(2); + fireEvent.click(year2[1]); + fireEvent.click(year[0]); + }); +}); diff --git a/tests/pages/__snapshots__/AdminTraineeDashboard.test.tsx.snap b/tests/pages/__snapshots__/AdminTraineeDashboard.test.tsx.snap index 46f0c7011..d167b0c8e 100644 --- a/tests/pages/__snapshots__/AdminTraineeDashboard.test.tsx.snap +++ b/tests/pages/__snapshots__/AdminTraineeDashboard.test.tsx.snap @@ -463,7 +463,7 @@ Array [ name="date" readOnly={true} type="text" - value="2024-11-02" + value="2024-11-14" />
- - Page - - - 1 - of - 1 - - - + + Page + + + 1 + of + 1 + + + +
- - | Go to page: - + + | Go to page: + +
- - Page - - - 1 - of - 0 - - - + + Page + + + 1 + of + 0 + + + +
- - | Go to page: - + + | Go to page: + +
-
-
-
-
-
-

- Dashboard -

-

- comingsoon -

-

- Somethingnewiscoming -

-
-
-
-
-
-
-`; diff --git a/tests/pages/__snapshots__/SuperAdminDashboard.test.tsx.snap b/tests/pages/__snapshots__/SuperAdminDashboard.test.tsx.snap new file mode 100644 index 000000000..261bcc9e3 --- /dev/null +++ b/tests/pages/__snapshots__/SuperAdminDashboard.test.tsx.snap @@ -0,0 +1,538 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Super Admin Dashboard test Should render SuperAdminDashboard 1`] = ` +
+
+
+
+ + + ‌ + + + + + ‌ + + + + + ‌ + +
+
+
+
+
+
+ + + ‌ + + + + + ‌ + + + + + ‌ + +
+
+
+
+
+
+ + + ‌ + + + + + ‌ + + + + + ‌ + +
+
+
+
+
+
+
+

+ Monthly Registrations +

+
+
- - Page - - - 1 - of - 0 - - - + + Page + + + 1 + of + 0 + + + +
- - | Go to page: - + + | Go to page: + +