From 9a12b5c25ffa0c8a3aebf1995877e0eca8d9ad97 Mon Sep 17 00:00:00 2001 From: "P.D.Kaumadi" <136589603+Kaumadi2k@users.noreply.github.com> Date: Wed, 4 Dec 2024 00:50:59 +0530 Subject: [PATCH] updated --- src/pages/examSetter/AddCandidate.tsx | 293 +++++++++++++++++++- src/pages/examSetter/AddProctors.tsx | 141 +++++++++- src/pages/examSetter/AdditionalFeatures.tsx | 177 +++++++++++- src/pages/examSetter/ExamSetterDashbord.tsx | 42 ++- src/pages/examSetter/MakeQuestions.tsx | 5 +- 5 files changed, 641 insertions(+), 17 deletions(-) diff --git a/src/pages/examSetter/AddCandidate.tsx b/src/pages/examSetter/AddCandidate.tsx index 12229b4..5541681 100644 --- a/src/pages/examSetter/AddCandidate.tsx +++ b/src/pages/examSetter/AddCandidate.tsx @@ -1,10 +1,297 @@ +import { useState, useEffect } from 'react'; +import { Select, List, Typography, message, Card, Row, Col, Divider, Button, Modal, notification } from 'antd'; +import { ExclamationCircleOutlined } from '@ant-design/icons'; // Import warning icon +import { + getCandidateGroupsByOrganizationForSearch, + getAllCandidatesForSearch, + updateExamCandidates, + getExamCandidates, // Fetch candidates for a specific exam + getCandidateConflictingExams, // New API call for getting conflicting exams +} from '../../api/services/ExamServices'; // Import API calls +import { CandidateEmailListRequest } from '../../api/examServiceTypes'; // Import type +const { Option } = Select; +const { Title } = Typography; const AddCandidate = () => { + interface Candidate { + id: number; + firstName: string; + lastName: string; + email: string; + } + + interface CandidateGroup { + id: number; + name: string; + candidates: Candidate[]; + } + + const [candidateGroups, setCandidateGroups] = useState([]); + const [individualCandidates, setIndividualCandidates] = useState([]); + const [filteredGroups, setFilteredGroups] = useState([]); + const [filteredCandidates, setFilteredCandidates] = useState([]); + const [selectedCandidates, setSelectedCandidates] = useState([]); + const [conflictingExams, setConflictingExams] = useState([]); // Store conflicting exams + const [hasConflicts, setHasConflicts] = useState(false); // Track if there are conflicts + + useEffect(() => { + const examId = sessionStorage.getItem('examId'); + if (examId) { + loadCandidateGroups(); + loadIndividualCandidates(); + loadExamCandidates(Number(examId)); // Fetch candidates for the given exam + checkForConflicts(Number(examId)); // Check for conflicts on page load + + } + + + }, []); + + const loadCandidateGroups = async () => { + try { + const organizationId = sessionStorage.getItem('orgId'); + const response = await getCandidateGroupsByOrganizationForSearch(Number(organizationId)); + setCandidateGroups(response.data || []); + setFilteredGroups(response.data || []); + } catch (error) { + message.error('Failed to load candidate groups'); + } + }; + + const loadIndividualCandidates = async () => { + try { + const response = await getAllCandidatesForSearch(); + setIndividualCandidates(response.data || []); + setFilteredCandidates(response.data || []); + } catch (error) { + message.error('Failed to load individual candidates'); + } + }; + + const loadExamCandidates = async (examId: number) => { + try { + const response = await getExamCandidates(examId); + setSelectedCandidates(response.data || []); + } catch (error) { + message.error('Failed to load candidates for the exam'); + } + }; + + const checkForConflicts = async (examId: number) => { + try { + const response = await getCandidateConflictingExams(examId); + setConflictingExams(response.data || []); + setHasConflicts(response.data.length > 0); // Set flag if conflicts exist + + // If conflicts are found, show a notification + if (response.data.length > 0) { + notification.warning({ + message: 'Conflicting Exams Detected', + description: 'There are conflicting exams for some of the selected candidates. Click "Show Conflicts" to view.', + icon: , + placement: 'topRight', + }); + } + } catch (error) { + message.error('Failed to check for conflicting exams'); + } + }; + + const handleGroupSearch = (searchTerm: string) => { + setFilteredGroups( + !searchTerm + ? candidateGroups + : candidateGroups.filter((group) => + group.name.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + }; + + const handleCandidateSearch = (searchTerm: string) => { + setFilteredCandidates( + !searchTerm + ? individualCandidates + : individualCandidates.filter( + (candidate) => + `${candidate.firstName} ${candidate.lastName}`.toLowerCase().includes(searchTerm.toLowerCase()) || + candidate.email.toLowerCase().includes(searchTerm.toLowerCase()) + ) + ); + }; + + const onGroupSelect = (groupId: number) => { + const selectedGroup = candidateGroups.find((group) => group.id === groupId); + if (selectedGroup) { + const newCandidates = selectedGroup.candidates.filter( + (candidate) => !selectedCandidates.some((c) => c.id === candidate.id) + ); + + if (newCandidates.length > 0) { + setSelectedCandidates([...selectedCandidates, ...newCandidates]); + newCandidates.forEach((candidate) => { + message.success(`${candidate.firstName} ${candidate.lastName} added to the list`); + }); + } else { + message.warning('All candidates in this group are already in the list'); + } + } + }; + + const onCandidateSelect = (candidateId: number) => { + const candidate = individualCandidates.find((c) => c.id === candidateId); + if (candidate) { + addCandidate(candidate); + } + }; + + const addCandidate = (candidate: Candidate) => { + if (!selectedCandidates.some((c) => c.id === candidate.id)) { + setSelectedCandidates([...selectedCandidates, candidate]); + message.success(`${candidate.firstName} ${candidate.lastName} added to the list`); + } else { + message.warning(`${candidate.firstName} ${candidate.lastName} is already in the list`); + } + }; + + const removeCandidate = (candidateId: number) => { + setSelectedCandidates(selectedCandidates.filter((c) => c.id !== candidateId)); + message.info('Candidate removed from the list'); + }; + + const saveCandidates = async () => { + const candidateEmails: CandidateEmailListRequest = { + emails: selectedCandidates.map((c) => c.email), + }; + const examId = sessionStorage.getItem('examId'); + + if (!examId) { + message.error('Exam ID is missing'); + return; + } + + try { + await updateExamCandidates(Number(examId), candidateEmails); + message.success('Candidates successfully updated for the exam'); + loadExamCandidates(Number(examId)); // Refresh the added candidates + + // After saving candidates, check for conflicts + checkForConflicts(Number(examId)); + } catch (error) { + message.error('Failed to update candidates for the exam'); + console.error(error); + } + }; + + const showConflictingExams = () => { + Modal.warning({ + title: 'Conflicting Exams', + content: ( + ( + + + + )} + /> + ), + okText: 'Understood', + }); + }; + return ( -
-

Add Candidate Component Placeholder

-
+ + Add Candidates for Exam + + {hasConflicts && ( + + + + + There are conflicting exams for some of the candidates. + + + + + )} + + + + + + + + + + + + + + + + Selected Candidates} + bordered + dataSource={selectedCandidates} + renderItem={(item) => ( + removeCandidate(item.id)}>Remove]}> + + + )} + /> + + + + + + + + + ); }; diff --git a/src/pages/examSetter/AddProctors.tsx b/src/pages/examSetter/AddProctors.tsx index 039c907..6736958 100644 --- a/src/pages/examSetter/AddProctors.tsx +++ b/src/pages/examSetter/AddProctors.tsx @@ -1,11 +1,146 @@ - +import React, { useState, useEffect } from 'react'; +import { Input, Button, List, Card, message, Tag, Row, Col } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { getExamSettersForSearch, getProctors, addOrUpdateProctors } from '../../api/services/ExamServices'; const AddProctors = () => { + const [examSetters, setExamSetters] = useState([]); + const [filteredProctors, setFilteredProctors] = useState([]); + const [selectedProctors, setSelectedProctors] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + + const organizationId = sessionStorage.getItem('orgId') ? Number(sessionStorage.getItem('orgId')) : null; + const examId = sessionStorage.getItem('examId') ? Number(sessionStorage.getItem('examId')) : null; + + useEffect(() => { + if (organizationId) { + getExamSettersForSearch(organizationId) + .then((response: any) => { + setExamSetters(response.data); + }) + .catch((error) => { + message.error('Failed to fetch exam setters'); + console.error(error); + }); + } + if (examId) { + fetchExistingProctors(examId); + } + }, [organizationId, examId]); + + const fetchExistingProctors = (examId: number) => { + getProctors(examId) + .then((response: any) => { + console.log('Existing proctors:', response.data); + setSelectedProctors(response.data); + }) + .catch((error) => { + message.error('Failed to fetch existing proctors'); + console.error(error); + }); + }; + + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + if (value.trim()) { + const filtered = examSetters.filter( + (proctor) => + `${proctor.firstName} ${proctor.lastName}`.toLowerCase().includes(value.toLowerCase()) || + proctor.email.toLowerCase().includes(value.toLowerCase()) + ); + setFilteredProctors(filtered); + } else { + setFilteredProctors([]); + } + }; + + const handleSelectProctor = (proctor: any) => { + if (selectedProctors.find((selected) => selected.email === proctor.email)) { + message.error('This proctor has already been added.'); + return; + } + setSelectedProctors([...selectedProctors, proctor]); + }; + + const handleRemoveProctor = (email: string) => { + setSelectedProctors(selectedProctors.filter((proctor) => proctor.email !== email)); + }; + + const handleSubmit = () => { + const emails = selectedProctors.map((proctor) => proctor.email); + console.log('Proctors to add:', emails); + addOrUpdateProctors(Number(examId), emails) + .then((response) => { + message.success(response.data.message || 'Proctors updated successfully.'); + fetchExistingProctors(Number(examId)); + }) + .catch((error) => { + message.error('Failed to update proctors'); + console.error(error); + }); + }; + return ( -
-

Add Proctors

+
+

Update Proctors

+ + } + value={searchTerm} + onChange={handleSearchChange} + style={{ marginBottom: '20px', width: '100%' }} + /> + + {filteredProctors.length > 0 && ( + + ( + handleSelectProctor(proctor)}> + Add + , + ]} + > + + + )} + /> + + )} + +

Selected Proctors

+ + {selectedProctors.map((proctor) => ( + + handleRemoveProctor(proctor.email)} + style={{ cursor: 'pointer' }} + > + {`${proctor.firstName} ${proctor.lastName}`} ({proctor.email}) + + + ))} + + +
); }; export default AddProctors; + diff --git a/src/pages/examSetter/AdditionalFeatures.tsx b/src/pages/examSetter/AdditionalFeatures.tsx index 4b48be0..0a127c9 100644 --- a/src/pages/examSetter/AdditionalFeatures.tsx +++ b/src/pages/examSetter/AdditionalFeatures.tsx @@ -1,11 +1,180 @@ +import React, { useEffect, useState } from 'react'; +import { Form, Input, Switch, Card, Button, message } from 'antd'; +import { + getRealTimeMonitoringStatus, + updateRealTimeMonitoring, + getBrowserLockdownStatus, + updateBrowserLockdown, + getHostedStatus, + updateHostedStatus, + getModerator, + setModerator, +} from './../../api/services/ExamServices'; +const ExamControlPanel = () => { + const examId = sessionStorage.getItem("examId"); + const [isRealTimeMonitoring, setRealTimeMonitoring] = useState(false); + const [realTimeMonitoringLink, setRealTimeMonitoringLink] = useState(''); + const [isBrowserLockdown, setBrowserLockdown] = useState(false); + const [isExamHosted, setExamHosted] = useState(false); + const [moderatorEmail, setModeratorEmail] = useState(''); + const [currentModerator, setCurrentModerator] = useState('No moderator assigned'); + const [isLoading, setIsLoading] = useState(false); + + const fetchInitialValues = async () => { + const examId = Number(sessionStorage.getItem("examId")); + try { + const realTimeResponse = await getRealTimeMonitoringStatus(examId); + setRealTimeMonitoring(realTimeResponse.data.realTimeMonitoring); + setRealTimeMonitoringLink(realTimeResponse.data.zoomLink || ''); + + const browserLockdownResponse = await getBrowserLockdownStatus(examId); + setBrowserLockdown(browserLockdownResponse.data.browserLockdown); + + const hostedResponse = await getHostedStatus(examId); + console.log(hostedResponse.data.hosted); + setExamHosted(hostedResponse.data.hosted); + + const moderatorResponse = await getModerator(examId); + if (moderatorResponse) { + setCurrentModerator( + `${moderatorResponse.data.firstName} ${moderatorResponse.data.lastName}` + ); + setModeratorEmail(moderatorResponse.data.email); + } + } catch (error) { + message.error('Failed to fetch initial data.'); + } + }; + + useEffect(() => { + fetchInitialValues(); + }, [examId]); + + const handleRealTimeMonitoringChange = async (enabled: boolean) => { + const examId = Number(sessionStorage.getItem("examId")); + if (enabled && !realTimeMonitoringLink.trim()) { + message.error('Please enter a valid Zoom meeting link before enabling real-time monitoring.'); + return; + } + + const zoomLinkValue = enabled ? realTimeMonitoringLink : null; + setRealTimeMonitoring(enabled); + + try { + await updateRealTimeMonitoring(examId, { + realTimeMonitoring: enabled, + zoomLink: zoomLinkValue || '', + }); + message.success('Real-time monitoring updated.'); + } catch (error) { + message.error('Failed to update real-time monitoring.'); + } + }; + + const handleBrowserLockdownChange = async (enabled: boolean) => { + setBrowserLockdown(enabled); + const examId = Number(sessionStorage.getItem("examId")); + try { + await updateBrowserLockdown(examId, enabled); + message.success('Browser lockdown updated.'); + } catch (error) { + message.error('Failed to update browser lockdown.'); + } + }; + + const handleHostExam = async () => { + const examId = Number(sessionStorage.getItem("examId")); + const newHostedStatus = !isExamHosted; + setExamHosted(newHostedStatus); + try { + await updateHostedStatus(examId, newHostedStatus); + message.success(newHostedStatus ? 'Exam is now hosted.' : 'Exam is no longer hosted.'); + } catch (error) { + message.error('Failed to update hosting status.'); + } + }; + + const handleSetModerator = async () => { + const examId = Number(sessionStorage.getItem("examId")); + try { + + await setModerator(examId, moderatorEmail); + message.success('Moderator set successfully.'); + setCurrentModerator(moderatorEmail); + } catch (error) { + message.error('Failed to set moderator.'); + } + }; -const AdditionalFeatures = () => { return ( -
-

Additional Features

+
+

Exam Control Panel

+ + {/* Real-Time Monitoring */} + +

Real-Time Monitoring

+ + setRealTimeMonitoringLink(e.target.value)} + /> + + + + +
+ + {/* Browser Lockdown */} + +

Browser Lockdown

+ + + +
+ + {/* Host the Exam */} + +

Host the Exam

+ +
+ + {/* Set Moderator */} + +

Set Moderator

+

Current Moderator: {currentModerator}

+ setModeratorEmail(e.target.value)} + style={{ marginBottom: '10px' }} + /> + +
); }; -export default AdditionalFeatures; +export default ExamControlPanel; diff --git a/src/pages/examSetter/ExamSetterDashbord.tsx b/src/pages/examSetter/ExamSetterDashbord.tsx index d30ac6a..95a7897 100644 --- a/src/pages/examSetter/ExamSetterDashbord.tsx +++ b/src/pages/examSetter/ExamSetterDashbord.tsx @@ -5,13 +5,16 @@ import { getLoggedInUser } from '../../utils/authUtils'; import { PageHeader } from '../../components'; import { HomeOutlined, PieChartOutlined } from '@ant-design/icons'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { ExamResponse, ModerateExamResponse } from '../../api/types'; +import { getExams } from '../../api/services/organization'; +import { getModeratingExams } from '../../api/services/ExamSetter'; // Mock Data const mockExams = [ { id: 1, - title: 'Exam 1', + title: 'General Knowledge', duration: 60, startDatetime: '2024-12-04T10:00:00Z', endDatetime: '2024-12-04T12:00:00Z', @@ -19,7 +22,7 @@ const mockExams = [ }, { id: 2, - title: 'Exam 2', + title: 'Basic Mathematics', duration: 45, startDatetime: '2024-12-05T09:00:00Z', endDatetime: '2024-12-05T09:45:00Z', @@ -27,7 +30,7 @@ const mockExams = [ }, { id: 3, - title: 'Exam 3', + title: 'Data Structures and Algorithms', duration: 90, startDatetime: '2024-12-06T11:00:00Z', endDatetime: '2024-12-06T12:30:00Z', @@ -47,10 +50,39 @@ export const ExamSetterDashBoardPage = () => { message.error('You must be logged in to perform this action.'); return null; } + + const organizationId = Number(sessionStorage.getItem('orgId')); + const [examsData, setExams] = useState([]); + const [proctoringExams, setProctoringExams] = useState([]); + const [moderatingExams, setModeratingExams] = useState([]); + + const fetchExams = async () => { + try { + const response = await getExams(organizationId); + const allExams = response.data; + + const createdById = loggedInUser.id; + const filteredExams = allExams.filter((exam) => exam.createdBy.id === createdById); + + console.log("Filtered Exams:", filteredExams); + setExams(filteredExams); + + const response1 = await getModeratingExams(createdById); + //setModeratingExams(response1.data) + } catch (error) { + message.error("Error fetching exams"); + console.log(error); + } + }; + + + useEffect(()=>{ + fetchExams(); + },[organizationId]); const renderRecentlyAddedExams = () => ( - {mockExams.map((exam) => ( + {examsData.map((exam) => ( { const [open, setOpen] = useState(false); const [contentModalOpen, setContentModalOpen] = useState(false); const [fileList, setFileList] = useState([]); - const { getOrganization } = useAuth(); + //const { getOrganization } = useAuth(); const [questions, setQuestions] = useState([]); const examId = sessionStorage.getItem('examId'); + const orgId = sessionStorage.getItem('orgId'); const [editModalOpen, setEditModalOpen] = useState(false); const [editingQuestion, setEditingQuestion] = useState(null); @@ -101,7 +102,7 @@ const MakeQuestions = () => { // Call loadQuestions on component mount useEffect(() => { loadQuestions(); - }, [examId, getOrganization]); + }, [examId, orgId]); const handleDelete = (questionId: number) => {