diff --git a/src/Chart/ProgressBar.tsx b/src/Chart/ProgressBar.tsx new file mode 100644 index 00000000..f82d5403 --- /dev/null +++ b/src/Chart/ProgressBar.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +interface ProgressBarProps { + passedPercentage: number; // Percentage of passed logins + failedPercentage: number; // Percentage of failed logins +} + +const ProgressBar: React.FC = ({ + passedPercentage, + failedPercentage, +}) => { + return ( +
+
+
{passedPercentage}%
+
{failedPercentage}%
+
+
+

+ Green: Passed + Logins +

+

+ Red: Failed + Logins +

+
+
+ ); +}; + +export default ProgressBar; diff --git a/src/Chart/TeamChart.tsx b/src/Chart/TeamChart.tsx new file mode 100644 index 00000000..aac17c59 --- /dev/null +++ b/src/Chart/TeamChart.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); + +interface TeamChartProps { + timeframe?: 'daily' | 'weekly' | 'monthly'; +} + +const TeamChart: React.FC = ({ timeframe = 'daily' }) => { + const chartData = { + daily: { + labels: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], + datasets: [ + { + label: 'Andela', + data: [1, 3, 0, 2, 1, 3, 2], + fill: false, + borderColor: '#4F46E5', + tension: 0.4, + } + ], + }, + weekly: { + labels: ['03', '06', '09', '12', '15', '18', '21', '24', '27', '30', '31', '34', '37', '40', '43', '46', '49', '54'], + datasets: [ + { + label: 'Andela', + data: [1, 3, 0, 2, 1, 3, 2, 0, 2, 1, 3, 0, 2, 1, 4, 1, 2, 4], + fill: false, + borderColor: '#4F46E5', + tension: 0.4, + }, + ], + }, + monthly: { + labels: Array.from({ length: 31 }, (_, i) => String(i + 1).padStart(2, '0')), + datasets: [ + { + label: 'Andela', + data: Array.from({ length: 31 }, () => Math.floor(Math.random() * 8)), + fill: false, + borderColor: '#4F46E5', + tension: 0.4, + }, + ], + }, + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom' as const, + }, + tooltip: { + mode: 'index' as const, + intersect: false, + }, + }, + scales: { + y: { + beginAtZero: true, + grid: { + color: '#D1D5DB', + }, + ticks: { + color: '#6B7280', + }, + }, + x: { + grid: { + display: false, + }, + ticks: { + color: '#6B7280', + }, + }, + }, + }; + + return ( +
+ +
+ ); +}; + +export default TeamChart; \ No newline at end of file diff --git a/src/Chart/UsersChart.tsx b/src/Chart/UsersChart.tsx index f43442a4..b148cae1 100644 --- a/src/Chart/UsersChart.tsx +++ b/src/Chart/UsersChart.tsx @@ -21,8 +21,6 @@ ChartJS.register( Legend, ); -// eslint-disable-next-line react/function-component-definition -// Rename the function component from usersChart to UsersChart const UsersChart: React.FC = () => { const data = { labels: [ @@ -50,6 +48,7 @@ const UsersChart: React.FC = () => { const options = { responsive: true, + maintainAspectRatio: false, plugins: { legend: { position: 'bottom' as const, @@ -71,8 +70,10 @@ const UsersChart: React.FC = () => { }; return ( -
- +
+
); }; diff --git a/src/components/AdminTeamDetails.tsx b/src/components/AdminTeamDetails.tsx index d42955da..0cdc2842 100644 --- a/src/components/AdminTeamDetails.tsx +++ b/src/components/AdminTeamDetails.tsx @@ -1,6 +1,8 @@ import React, { useState } from 'react'; import { FaAngleDown } from 'react-icons/fa6'; -import UsersChart from '../Chart/usersChart'; +import TeamChart from '../Chart/TeamChart'; +import ProgressBar from '../Chart/ProgressBar'; + interface TeamData { ttlName?: string; team?: string; @@ -19,12 +21,32 @@ interface TeamDetailsModalProps { teamData: TeamData | null; } +// Add this near the top of your TeamDetailsModal component +const loginStats = { + daily: { + passed: 60, + failed: 40, + total: "200k" + }, + weekly: { + passed: 75, + failed: 25, + total: "1.2M" + }, + monthly: { + passed: 85, + failed: 15, + total: "5M" + } +}; + function TeamDetailsModal({ isOpen, onClose, teamData, }: TeamDetailsModalProps) { const [activeTab, setActiveTab] = useState<'overview' | 'logins'>('overview'); + const [timeframe, setTimeframe] = useState<'daily' | 'weekly' | 'monthly'>('daily'); const [showAttendanceSummary, setShowAttendanceSummary] = useState(false); const handleAttendanceSummaryEnter = () => setShowAttendanceSummary(true); @@ -35,17 +57,16 @@ function TeamDetailsModal({ return (
- {/* Tabs for switching */}
+ + +
+
+
+

+ Logins Attempt Status +

+ +
+

Total Logins: {loginStats[timeframe].total}

+
+ +
+ )}
); } -export default TeamDetailsModal; +export default TeamDetailsModal; \ No newline at end of file diff --git a/src/components/DashboardCards.tsx b/src/components/DashboardCards.tsx new file mode 100644 index 00000000..8fb4a6d0 --- /dev/null +++ b/src/components/DashboardCards.tsx @@ -0,0 +1,291 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Doughnut } from 'react-chartjs-2'; +import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; +import { MdOutlineEventBusy , MdOutlineEventAvailable } from "react-icons/md"; + +import { RiTeamFill } from "react-icons/ri"; +import { Link, useNavigate } from 'react-router-dom'; +import { useLazyQuery, useQuery } from '@apollo/client'; +import { format } from 'date-fns'; +import Skeleton from 'react-loading-skeleton'; +import { get } from 'http'; +import { di } from '@fullcalendar/core/internal-common'; +import { GET_TEAMS_CARDS } from './CoordinatorCard'; +import { ThemeContext } from '../hook/ThemeProvider'; +import {GET_ALL_INVITATIONS} from '../queries/invitation.queries'; +import { GET_EVENTS } from '../queries/event.queries'; +import GET_TICKETS from '../queries/tickets.queries'; + +ChartJS.register(ArcElement, Tooltip, Legend); + +interface EventsInterface { + id: string; + title: string; + start: string; + end: string; + timeToStart: string; + timeToEnd: string; + hostName: string; +} + +function DashboardCards() { + const navigate = useNavigate(); + const { colorTheme } = useContext(ThemeContext); + const [upcomingEvents, setUpcomingEvents] = useState([]); + const [TeamsData, setTeamsData] = useState(null); + const [ticketsData, setTicketsData] = useState([]); + const [activeTicketsCount, setActiveTicketsCount] = useState(0); + const [closedTicketsCount, setClosedTicketsCount] = useState(0); + const [InvitationData, setInvitationData] = useState(null); + const [acceptedInvitationsCount, setAcceptedTicketsCount] = useState(0); + const [pendingInvitationsCount, setPendingTicketsCount] = useState(0); + const [declinedInvitationsCount, setDeclinedTicketsCount] = useState(0); + + const [getEvents, { loading: getEventsDataLoading }] = + useLazyQuery(GET_EVENTS); + + const [getAllTeams, { loading: getAllTeamsDataLoading }] = + useLazyQuery(GET_TEAMS_CARDS); + + const { loading: getTicketsDataLoading } = useQuery(GET_TICKETS, { + onCompleted: (data) => { + const tickets = data.getAllTickets || []; + setTicketsData(tickets); + // Count active and closed tickets + const activeCount = tickets.filter((ticket: { status: string; }) => ticket.status !== 'closed').length; + const closedCount = tickets.filter((ticket: { status: string; }) => ticket.status === 'closed').length; + setActiveTicketsCount(activeCount); + setClosedTicketsCount(closedCount); + }, + fetchPolicy: 'network-only', + }); + + const { loading: getInvitationsDataLoading } = useQuery(GET_ALL_INVITATIONS, { + onCompleted: (data) => { + const invitations = data.getAllInvitations || []; + setInvitationData(invitations); + // Count active and closed tickets + const acceptedInvitationsCount = invitations.filter((invite: { status: string; }) => invite.status === 'accepted').length; + const pendingInvitationsCount = invitations.filter((invite: { status: string; }) => invite.status === 'pending').length; + const declinedInvitationsCount = invitations.filter((invite: { status: string; }) => invite.status === 'cancelled').length; + setAcceptedTicketsCount(acceptedInvitationsCount); + setPendingTicketsCount(pendingInvitationsCount); + setDeclinedTicketsCount(declinedInvitationsCount); + }, + fetchPolicy: 'network-only', + }); + + 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); + }); + }, + }); + }, [getEvents]); + + useEffect(() => { + getAllTeams({ + variables: { + orgToken: localStorage.getItem('orgToken'), + }, + fetchPolicy: 'network-only', + onCompleted: (data: any) => { + setTeamsData(data.getAllTeams); + }, + }) + }, [getAllTeams]); + + const totalTeams = TeamsData ? TeamsData.length : 0; + + const statsSkeleton = ( +
+ + + +
+ ); + + const chartData = { + labels: ['Active Tickets', 'Closed Tickets'], + datasets: [ + { + label: 'Tickets', + data: [activeTicketsCount, closedTicketsCount], + backgroundColor: ['#7758b0', '#FF6384'], + hoverBackgroundColor: ['#7758b0', '#FF6384'], + hoveroffset: 3 + }, + ], + }; + + const InvitationChartData = { + labels: ['Accepted Invitations', 'Pending Invitations', 'Declined Invitations'], + datasets: [ + { + label: 'Invitations', + data: [acceptedInvitationsCount, pendingInvitationsCount, declinedInvitationsCount], + backgroundColor: ['#7758b0', '#FFCE56', '#FF6384'], + hoverBackgroundColor: ['#7758b0', '#FFCE56', '#FF6384'], + hoveroffset: 3 + }, + ], + }; + + return ( + +
+
+ {/* Tickets Overview */} +
+

Tickets Overview

+ {getTicketsDataLoading ? ( + + ) : ( +
+ +
+ )} +
+ + {/* Invitations Overview */} +
+

Invitations Overview

+ {getInvitationsDataLoading ? ( + + ) : ( +
+ +
+ )} +
+ + {/* Teams Card */} +
+ {!getAllTeamsDataLoading && ( + <> +
+ {/* Increased icon size */} +

+ TEAMS +

+
+
+ + {totalTeams} + +
+ + )} + {getAllTeamsDataLoading && statsSkeleton} +
+ + {/* Upcoming Events */} +
+

Upcoming Events

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

+ By {event.hostName} +

+
+
+

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

+

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

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

+ Oops! No upcoming events scheduled +

+
+ )} +
+ {getEventsDataLoading && ( +
+ + +
+ )} +
+
+
+ + ); +}; + +export default DashboardCards; \ No newline at end of file diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx index 1323d7a9..5e544c97 100644 --- a/src/pages/AdminDashboard.tsx +++ b/src/pages/AdminDashboard.tsx @@ -5,8 +5,10 @@ import { useMutation } from '@apollo/client'; import { toast } from 'react-toastify'; import { FaEye } from 'react-icons/fa'; import PieChart from '../Chart/PieChart'; +// import PieChart from '../Chart/PieChart'; +import DashboardCards from '../components/DashboardCards'; import BarChart from '../Chart/BarChart'; -import UsersChart from '../Chart/usersChart'; +import UsersChart from '../Chart/UsersChart'; // eslint-disable-next-line import/no-useless-path-segments import useDocumentTitle from '../hook/useDocumentTitle'; import Comingsoon from './Comingsoon'; @@ -125,11 +127,11 @@ function AdminDashboard() { +
+ +
-
- -
Users