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 000000000..5e251d22f Binary files /dev/null and b/src/assets/no-event.png differ diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 00238a8fc..d77ea4b3b 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -13,7 +13,7 @@ import { useLazyQuery, useMutation } from '@apollo/client'; import { ADD_EVENT, EDIT_EVENT, CANCEL_EVENT } from '../Mutations/event'; import { GET_EVENTS } from '../queries/event.queries'; import moment from 'moment'; -import CalendarSkeleton from '../Skeletons/Calender.skeleton' +import CalendarSkeleton from '../Skeletons/Calender.skeleton'; import { toast } from 'react-toastify'; import EventGuestList from './EventGuestList'; /* istanbul ignore next */ @@ -48,12 +48,12 @@ const Calendar = () => { 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: + +