From b30ca2ef8fba53b9842ae4d8e2c2c87eb2b18131 Mon Sep 17 00:00:00 2001 From: deepali chavhan Date: Wed, 5 Feb 2025 14:59:23 +0530 Subject: [PATCH] Task #234734 fix: Added school admin components --- src/App.jsx | 17 +- src/pages/student-admin/SchoolAdmin.tsx | 134 +++++++++ .../student-admin/SchoolAdminDetails.tsx | 104 +++++++ .../student-admin/SchoolAdminStudent.tsx | 254 ++++++++++++++++++ src/routes/schoolAdminAuth.tsx | 24 ++ src/utils/theme.tsx | 1 + 6 files changed, 531 insertions(+), 3 deletions(-) create mode 100644 src/pages/student-admin/SchoolAdmin.tsx create mode 100644 src/pages/student-admin/SchoolAdminDetails.tsx create mode 100644 src/pages/student-admin/SchoolAdminStudent.tsx create mode 100644 src/routes/schoolAdminAuth.tsx diff --git a/src/App.jsx b/src/App.jsx index 614073a..a980bed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,6 +14,7 @@ import customTheme from "./utils/theme"; import FingerprintJS from "@fingerprintjs/fingerprintjs"; import { checkUserDetails } from "./services/auth/auth"; import teacherAuthRoutes from "./routes/teacherAuth"; +import schoolAdminAuth from "./routes/schoolAdminAuth"; import { jwtDecode } from "jwt-decode"; const theme = extendTheme(customTheme); @@ -26,9 +27,19 @@ function AppRouter() { const navigate = useNavigate(); useEffect(() => { if (token && token !== "not-logged-in") { - const tokenDecoded = jwtDecode(token); - const roles = tokenDecoded?.resource_access?.["hasura-app"]?.roles; - if (authUser && Array.isArray(roles) && roles.includes("teacher")) { + if ( + authUser && + Array.isArray(authUser?.Teachers) && + authUser?.Teachers.some( + (teacher) => teacher.currentRole === "School_Admin" + ) + ) { + setRoutes(schoolAdminAuth); + } else if ( + authUser && + Array.isArray(authUser?.Teachers) && + authUser?.Teachers.some((teacher) => teacher.currentRole === "Teacher") + ) { setRoutes(teacherAuthRoutes); } else if (authRoutes) { setRoutes(authRoutes); diff --git a/src/pages/student-admin/SchoolAdmin.tsx b/src/pages/student-admin/SchoolAdmin.tsx new file mode 100644 index 0000000..50814f3 --- /dev/null +++ b/src/pages/student-admin/SchoolAdmin.tsx @@ -0,0 +1,134 @@ +import { Center, VStack } from "@chakra-ui/react"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { impression } from "../../services/telemetry"; +import CustomHeading from "../../components/common/typography/Heading"; +import Layout from "../../components/common/layout/layout"; +import ClassCard from "../../components/common/cards/ClassCard"; +import { getTeacherData } from "../../services/home"; +import { checkUserDetails } from "../../services/auth/auth"; + +export default function SchoolAdminHomepage(props: any) { + const { t } = useTranslation(); + const [error, setError] = useState(null); + const navigate = useNavigate(); + const { authUser } = props; + const [classes, setClasses] = useState([]); + const [loading, setLoading] = useState(true); + const fetchTeacherDataForClasses = React.useCallback(async () => { + try { + const result = await checkUserDetails(); + if (!result) { + setError("No class details found."); + return; + } + + const groupMemberships = result?.data?.GroupMemberships || []; + + const classDataArray = await Promise.all( + groupMemberships.map(async (classDetail: any) => { + const payload = { + groupId: classDetail?.Group?.groupId, + schoolUdise: classDetail?.School?.udiseCode, + grade: String(classDetail?.Group?.grade), + medium: classDetail?.Group?.medium, + board: classDetail?.Group?.board, + }; + + const data = await getTeacherData(payload); + + // Create class object + const classObj = { + ...data, + subjects: data?.subjectResults || [], + title: `Class ${classDetail?.Group?.grade}`, + groupId: classDetail?.Group?.groupId, + schoolUdise: classDetail?.School?.udiseCode, + grade: String(classDetail?.Group?.grade), + medium: classDetail?.Group?.medium, + board: classDetail?.Group?.board, + }; + + return classObj; + }) + ); + setClasses(classDataArray); + } catch (error: any) { + setError(`Error fetching teacher data for classes:${error?.message}`); + } finally { + setLoading(false); + } + }, [authUser]); + + useEffect(() => { + fetchTeacherDataForClasses(); + }, [authUser]); + + useEffect(() => { + impression({ + edata: { + type: "SchoolAdminHomepage", + pageid: "SchoolAdminHomepage", + uri: "/schoolAdmin", + query: Object.fromEntries( + new URLSearchParams(location.search).entries() + ), + visits: [], + }, + }); + }, []); + + const handleCardClick = (group: any, subject?: any) => { + navigate( + subject + ? `/schoolAdmin/${group.board}/${group.schoolUdise}/${group.grade}/${group.medium}/${group.groupId}/${subject}` + : `/schoolAdmin/${group.board}/${group.schoolUdise}/${group.grade}/${group.medium}/${group.groupId}`, + {} + ); + }; + + return ( + + {error ? ( +
+ + {error} + +
+ ) : ( + + + + {classes?.map((group: any) => ( + handleCardClick(group)} + subjectClick={(sub) => handleCardClick(group, sub)} + /> + ))} + + )} +
+ ); +} diff --git a/src/pages/student-admin/SchoolAdminDetails.tsx b/src/pages/student-admin/SchoolAdminDetails.tsx new file mode 100644 index 0000000..37cdcce --- /dev/null +++ b/src/pages/student-admin/SchoolAdminDetails.tsx @@ -0,0 +1,104 @@ +import { Box, Text, VStack } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import ClassCard from "../../components/common/cards/ClassCard"; +import Layout from "../../components/common/layout/layout"; +import { getTeacherData } from "../../services/home"; +import { impression } from "../../services/telemetry"; +import Students from "./SchoolAdminStudent"; + +export default function SchoolAdminDetails(props: any) { + const { t } = useTranslation(); + const [error, setError] = useState(null); + const { authUser } = props; + const { board, schoolUdise, grade, medium, groupId, subject } = useParams(); + const [classDetails, setClassDetails] = useState({}); + const navigate = useNavigate(); + const [selectedSubject, setSelectedSubject] = useState(subject); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchProgramId = async () => { + try { + const payload = { + groupId: groupId, + schoolUdise: schoolUdise, + grade: String(grade), + medium: medium, + board: board, + }; + let data = await getTeacherData(payload); + const classObj = { + ...data, + subjects: data?.subjectResults || [], + }; + setClassDetails(classObj); + } catch (error) { + console.error("Error fetching program data:", error); + setError(t("An unexpected error occurred. Please try again later.")); + } finally { + setLoading(false); + } + }; + + fetchProgramId(); + }, [authUser]); + + useEffect(() => { + impression({ + edata: { + type: "SchoolAdminDetails", + pageid: "SchoolAdminDetails", + uri: "/schoolAdmin", + query: Object.fromEntries( + new URLSearchParams(location.search).entries() + ), + visits: [], + }, + }); + }, []); + + return ( + navigate("/schoolAdmin"), + }} + > + + {error ? ( + + {error} + + ) : ( + + setSelectedSubject(e)} + selectedSubject={selectedSubject || "all"} + /> + {/* Table Section */} + + + )} + + + ); +} diff --git a/src/pages/student-admin/SchoolAdminStudent.tsx b/src/pages/student-admin/SchoolAdminStudent.tsx new file mode 100644 index 0000000..a4eaffb --- /dev/null +++ b/src/pages/student-admin/SchoolAdminStudent.tsx @@ -0,0 +1,254 @@ +import { SearchIcon } from "@chakra-ui/icons"; +import { + Button, + Flex, + HStack, + Input, + InputGroup, + InputRightElement, + Radio, + RadioGroup, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr, + VStack, +} from "@chakra-ui/react"; +import React, { memo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import CustomHeading from "../../components/common/typography/Heading"; +import ActionSheet from "../../components/common/ActionSheet"; +import { getTeacherData } from "../../services/home"; +import IconByName from "../../components/common/icons/Icon"; + +interface payload { + groupId: string; + schoolUdise: string; + grade: string; + medium: string; + board: string; + subject?: string; +} + +const subjectSort = [ + { label: "TEACHER_A_Z", value: "a-z" }, + { label: "TEACHER_Z_A", value: "z-a" }, + { label: "TEACHER_COMPLETIOIN_RATE_HIGH", value: "highToLow" }, + { label: "TEACHER_COMPLETIOIN_RATE_LOW", value: "lowToHigh" }, +]; + +const Students: React.FC = (payload) => { + const { t } = useTranslation(); + + const [students, setstudents] = useState | null>(null); + + const [activeCollapse, setActiveCollapse] = useState( + undefined + ); + const [radioSelection, setRadioSelection] = useState( + undefined + ); + const [selectedView, setSelectedView] = useState( + undefined + ); + const [searchText, setSearchText] = useState(""); + + const handleCollapseToggle = (collapse: string) => { + setActiveCollapse(collapse === activeCollapse ? undefined : collapse); + }; + + const handleViewChange = (view: string) => { + setSelectedView(view); + setActiveCollapse(undefined); + }; + + const filterSubjects = (students: Array) => { + const filteredStudents = students.filter((student) => + student.username.toLowerCase().includes(searchText.toLowerCase()) + ); + if (selectedView === "highToLow") { + filteredStudents.sort( + (a, b) => + Number(b.completionPercentage) - Number(a.completionPercentage) + ); + } else if (selectedView === "lowToHigh") { + filteredStudents.sort( + (a, b) => + Number(a.completionPercentage) - Number(b.completionPercentage) + ); + } else if (selectedView === "a-z") { + filteredStudents.sort((a, b) => a.username.localeCompare(b.username)); + } else if (selectedView === "z-a") { + filteredStudents.sort((a, b) => b.username.localeCompare(a.username)); + } + return filteredStudents; + }; + + React.useEffect(() => { + const init = async () => { + let students = await getTeacherData({ + ...payload, + studentProgress: "true", + }); + setstudents( + students?.userCompletionPercentages || students?.classResults || [] + ); + }; + init(); + }, [payload]); + + return ( + + handleCollapseToggle("none")} + headerComponent={ + + + {t("TEACHER_SORT")} + + + } + > + + setRadioSelection(value)} + w="100%" + > + + {subjectSort.map(({ label, value }) => ( + + + {t(label)} + + + ))} + + + + + + {/* Header Section */} + + + + + + + + setSearchText(e.target.value)} + value={searchText} + /> + + + + + + + + + + + + + + {filterSubjects(students || []).map((student, index) => ( + + + + + ))} + +
+ + {t("LEADERBOARD_NAME")} + + + + {t("TEACHER_COMPLETION_RATE")} + +
{student.username} + {student.completionPercentage} % +
+
+ ); +}; + +export default memo(Students); diff --git a/src/routes/schoolAdminAuth.tsx b/src/routes/schoolAdminAuth.tsx new file mode 100644 index 0000000..fe225eb --- /dev/null +++ b/src/routes/schoolAdminAuth.tsx @@ -0,0 +1,24 @@ +import { lazy } from "react"; + +const SchoolAdminHomepage = lazy( + () => import("../pages/student-admin/SchoolAdmin") +); + +const SchoolAdminDetails = lazy( + () => import("../pages/student-admin/SchoolAdminDetails") +); + +export default [ + { + path: "/schoolAdmin/:board/:schoolUdise/:grade/:medium/:groupId", + component: SchoolAdminDetails, + }, + { + path: "/schoolAdmin/:board/:schoolUdise/:grade/:medium/:groupId/:subject", + component: SchoolAdminDetails, + }, + { + path: "*", + component: SchoolAdminHomepage, + }, +]; diff --git a/src/utils/theme.tsx b/src/utils/theme.tsx index 3846927..ea62ae8 100644 --- a/src/utils/theme.tsx +++ b/src/utils/theme.tsx @@ -103,6 +103,7 @@ const customTheme = extendTheme({ tsSeaBlue20: "#03627C33", tsSeaBlue40: "#023B4A", green40: "#03570E", + blue40: "#343A8F", }, components: { Table: {