From 0dc6c7b521377f38426ec39049d2eed80d9b803c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 17 Oct 2025 17:16:58 +0300 Subject: [PATCH] Exam ve Pdf Test Route URL --- .../Seeds/HostData.json | 6 +- ...entExamInterface.tsx => ExamInterface.tsx} | 233 +++++++++------ .../ExamInterface/PDFTestInterface.tsx | 282 +++++++++--------- 3 files changed, 283 insertions(+), 238 deletions(-) rename ui/src/views/coordinator/ExamInterface/{StudentExamInterface.tsx => ExamInterface.tsx} (54%) diff --git a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json index bbfae841..f4e70d7f 100644 --- a/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json +++ b/api/src/Kurs.Platform.DbMigrator/Seeds/HostData.json @@ -12644,7 +12644,7 @@ { "key": "admin.coordinator.examDetail", "path": "/admin/coordinator/exam/:id", - "componentPath": "@/views/coordinator/Exams", + "componentPath": "@/views/coordinator/ExamInterface/ExamInterface", "routeType": "protected", "authority": ["App.Coordinator.Exams"] }, @@ -12658,7 +12658,7 @@ { "key": "admin.coordinator.assignmentDetail", "path": "/admin/coordinator/assignment/:id", - "componentPath": "@/views/coordinator/Assignments", + "componentPath": "@/views/coordinator/ExamInterface/ExamInterface", "routeType": "protected", "authority": ["App.Coordinator.Assignments"] }, @@ -12672,7 +12672,7 @@ { "key": "admin.coordinator.testDetail", "path": "/admin/coordinator/test/:id", - "componentPath": "@/views/coordinator/Tests", + "componentPath": "@/views/coordinator/ExamInterface/PDFTestInterface", "routeType": "protected", "authority": ["App.Coordinator.Tests"] }, diff --git a/ui/src/views/coordinator/ExamInterface/StudentExamInterface.tsx b/ui/src/views/coordinator/ExamInterface/ExamInterface.tsx similarity index 54% rename from ui/src/views/coordinator/ExamInterface/StudentExamInterface.tsx rename to ui/src/views/coordinator/ExamInterface/ExamInterface.tsx index c9722831..2229e7dc 100644 --- a/ui/src/views/coordinator/ExamInterface/StudentExamInterface.tsx +++ b/ui/src/views/coordinator/ExamInterface/ExamInterface.tsx @@ -1,40 +1,54 @@ -import React, { useState, useEffect } from 'react'; -import { QuestionRenderer } from './QuestionRenderer'; -import { ExamNavigation } from './ExamNavigation'; -import { ExamTimer } from './ExamTimer'; -import { SecurityWarning } from './SecurityWarning'; -import { Exam, ExamSession, StudentAnswer } from '@/types/coordinator'; -import { useExamSecurity } from '@/utils/hooks/useExamSecurity'; +import React, { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { QuestionRenderer } from './QuestionRenderer' +import { ExamNavigation } from './ExamNavigation' +import { ExamTimer } from './ExamTimer' +import { SecurityWarning } from './SecurityWarning' +import { ExamSession, StudentAnswer } from '@/types/coordinator' +import { useExamSecurity } from '@/utils/hooks/useExamSecurity' +import { useStoreState } from '@/store/store' +import { generateMockExam } from '@/mocks/mockExams' -interface StudentExamInterfaceProps { - exam: Exam; - studentId: string; - onExamComplete: (session: ExamSession) => void; - onExamSave?: (session: ExamSession) => void; -} - -export const StudentExamInterface: React.FC = ({ - exam, - studentId, - onExamComplete, - onExamSave -}) => { +const ExamInterface: React.FC = () => { + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const userId = useStoreState((state) => state.auth.user.id) + + // TODO: Replace with actual API call to fetch exam by id + const exam = generateMockExam().find((e) => e.id === id) + + if (!exam) { + return ( +
+
+

Sınav Bulunamadı

+

İstenen sınav bulunamadı veya erişim yetkiniz yok.

+ +
+
+ ) + } const [session, setSession] = useState({ id: `session-${Date.now()}`, examId: exam.id, - studentId, + studentId: userId, startTime: new Date(), answers: [], status: 'in-progress', - timeRemaining: exam.timeLimit * 60 - }); + timeRemaining: exam.timeLimit * 60, + }) - const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); + const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0) const [securityWarning, setSecurityWarning] = useState<{ - show: boolean; - message: string; - type: 'warning' | 'error' | 'info'; - }>({ show: false, message: '', type: 'warning' }); + show: boolean + message: string + type: 'warning' | 'error' | 'info' + }>({ show: false, message: '', type: 'warning' }) // Security configuration const securityConfig = { @@ -42,121 +56,150 @@ export const StudentExamInterface: React.FC = ({ disableCopyPaste: true, disableDevTools: true, fullScreenMode: true, - preventTabSwitch: true - }; + preventTabSwitch: true, + } - useExamSecurity(securityConfig, session.status === 'in-progress'); + useExamSecurity(securityConfig, session.status === 'in-progress') + + // Save exam session to backend/localStorage + const saveExamSession = (sessionData: ExamSession) => { + // TODO: Replace with actual API call + console.log('Saving exam session:', sessionData) + localStorage.setItem(`exam-session-${sessionData.examId}`, JSON.stringify(sessionData)) + } + + // Complete exam and navigate + const handleExamComplete = (sessionData: ExamSession) => { + // TODO: Replace with actual API call + console.log('Exam completed:', sessionData) + saveExamSession(sessionData) + // Navigate to results or dashboard + // navigate('/admin/coordinator/exams') + } // Auto-save mechanism useEffect(() => { const interval = setInterval(() => { - if (session.status === 'in-progress' && onExamSave) { - onExamSave(session); + if (session.status === 'in-progress') { + saveExamSession(session) } - }, 30000); // Save every 30 seconds + }, 30000) // Save every 30 seconds - return () => clearInterval(interval); - }, [session, onExamSave]); + return () => clearInterval(interval) + }, [session]) const handleAnswerChange = (questionId: string, answer: string | string[]) => { - setSession(prev => { - const existingAnswerIndex = prev.answers.findIndex(a => a.questionId === questionId); + setSession((prev) => { + const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === questionId) const newAnswer: StudentAnswer = { questionId, answer, - timeSpent: 0 // In a real app, track time spent per question - }; + timeSpent: 0, // In a real app, track time spent per question + } - const newAnswers = existingAnswerIndex >= 0 - ? prev.answers.map((a, i) => i === existingAnswerIndex ? newAnswer : a) - : [...prev.answers, newAnswer]; + const newAnswers = + existingAnswerIndex >= 0 + ? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a)) + : [...prev.answers, newAnswer] return { ...prev, - answers: newAnswers - }; - }); - }; + answers: newAnswers, + } + }) + } const handleTimeUp = () => { setSecurityWarning({ show: true, message: 'Süre doldu! Sınav otomatik olarak teslim ediliyor...', - type: 'error' - }); - + type: 'error', + }) + setTimeout(() => { - completeExam(); - }, 3000); - }; + completeExam() + }, 3000) + } const completeExam = () => { const completedSession: ExamSession = { ...session, endTime: new Date(), - status: 'completed' - }; - - setSession(completedSession); - onExamComplete(completedSession); - }; + status: 'completed', + } + + setSession(completedSession) + handleExamComplete(completedSession) + } const handleSubmitExam = () => { - const unanswered = exam.questions.filter(q => - !session.answers.find(a => a.questionId === q.id && a.answer) - ); + const unanswered = exam.questions.filter( + (q) => !session.answers.find((a) => a.questionId === q.id && a.answer), + ) if (unanswered.length > 0) { const confirmSubmit = window.confirm( - `${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?` - ); - - if (!confirmSubmit) return; + `${unanswered.length} soru cevaplanmamış. Yine de sınavı teslim etmek istiyor musunuz?`, + ) + + if (!confirmSubmit) return } - completeExam(); - }; + completeExam() + } - const currentQuestion = exam.questions[currentQuestionIndex]; - const currentAnswer = session.answers.find(a => a.questionId === currentQuestion?.id); + const currentQuestion = exam.questions[currentQuestionIndex] + const currentAnswer = session.answers.find((a) => a.questionId === currentQuestion?.id) const navigateQuestion = (direction: 'next' | 'prev' | number) => { if (typeof direction === 'number') { - setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction))); + setCurrentQuestionIndex(Math.max(0, Math.min(exam.questions.length - 1, direction))) } else if (direction === 'next') { - setCurrentQuestionIndex(prev => Math.min(exam.questions.length - 1, prev + 1)); + setCurrentQuestionIndex((prev) => Math.min(exam.questions.length - 1, prev + 1)) } else { - setCurrentQuestionIndex(prev => Math.max(0, prev - 1)); + setCurrentQuestionIndex((prev) => Math.max(0, prev - 1)) } - }; + } if (session.status === 'completed') { return (
- - + +

Sınav Tamamlandı!

- Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra bilgilendirileceksiniz. + Sınavınız başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra + bilgilendirileceksiniz.

- Başlama: {session.startTime.toLocaleString('tr-TR')}
+ Başlama: {session.startTime.toLocaleString('tr-TR')} +
Bitiş: {session.endTime?.toLocaleString('tr-TR')}
- ); + ) } return (
setSecurityWarning(prev => ({ ...prev, show: false }))} + onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))} message={securityWarning.message} type={securityWarning.type} /> @@ -170,14 +213,10 @@ export const StudentExamInterface: React.FC = ({ Soru {currentQuestionIndex + 1} / {exam.questions.length}

- +
- - + + @@ -228,7 +272,12 @@ export const StudentExamInterface: React.FC = ({ > Sonraki - +
@@ -246,5 +295,7 @@ export const StudentExamInterface: React.FC = ({ - ); -}; \ No newline at end of file + ) +} + +export default ExamInterface \ No newline at end of file diff --git a/ui/src/views/coordinator/ExamInterface/PDFTestInterface.tsx b/ui/src/views/coordinator/ExamInterface/PDFTestInterface.tsx index 24126fd5..db0a65ef 100644 --- a/ui/src/views/coordinator/ExamInterface/PDFTestInterface.tsx +++ b/ui/src/views/coordinator/ExamInterface/PDFTestInterface.tsx @@ -1,41 +1,55 @@ -import React, { useState, useEffect } from "react"; -import { ExamTimer } from "./ExamTimer"; -import { SecurityWarning } from "./SecurityWarning"; -import { FaFileAlt, FaImage, FaCheckCircle } from "react-icons/fa"; -import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from "@/types/coordinator"; -import { useExamSecurity } from "@/utils/hooks/useExamSecurity"; +import React, { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { ExamTimer } from './ExamTimer' +import { SecurityWarning } from './SecurityWarning' +import { FaFileAlt, FaImage, FaCheckCircle } from 'react-icons/fa' +import { Exam, ExamSession, StudentAnswer, AnswerKeyItem } from '@/types/coordinator' +import { useExamSecurity } from '@/utils/hooks/useExamSecurity' +import { useStoreState } from '@/store/store' +import { generateMockPDFTest } from '@/mocks/mockTests' -interface PDFTestInterfaceProps { - exam: Exam; - studentId: string; - onExamComplete: (session: ExamSession) => void; - onExamSave?: (session: ExamSession) => void; -} - -export const PDFTestInterface: React.FC = ({ - exam, - studentId, - onExamComplete, - onExamSave, -}) => { +const PDFTestInterface: React.FC = () => { + const { id } = useParams<{ id: string }>() + const navigate = useNavigate() + const userId = useStoreState((state) => state.auth.user.id) + + // TODO: Replace with actual API call to fetch test by id + const exam = generateMockPDFTest() + + if (!exam || exam.id !== id) { + return ( +
+
+

Test Bulunamadı

+

İstenen test bulunamadı veya erişim yetkiniz yok.

+ +
+
+ ) + } const [session, setSession] = useState({ id: `session-${Date.now()}`, examId: exam.id, - studentId, + studentId: userId, startTime: new Date(), answers: [], - status: "in-progress", + status: 'in-progress', timeRemaining: exam.timeLimit * 60, - }); + }) - const [answerKeyResponses, setAnswerKeyResponses] = useState< - Record - >({}); + const [answerKeyResponses, setAnswerKeyResponses] = useState>( + {}, + ) const [securityWarning, setSecurityWarning] = useState<{ - show: boolean; - message: string; - type: "warning" | "error" | "info"; - }>({ show: false, message: "", type: "warning" }); + show: boolean + message: string + type: 'warning' | 'error' | 'info' + }>({ show: false, message: '', type: 'warning' }) // Security configuration const securityConfig = { @@ -44,74 +58,86 @@ export const PDFTestInterface: React.FC = ({ disableDevTools: true, fullScreenMode: true, preventTabSwitch: true, - }; + } - useExamSecurity(securityConfig, session.status === "in-progress"); + useExamSecurity(securityConfig, session.status === 'in-progress') + + // Save test session to backend/localStorage + const saveTestSession = (sessionData: ExamSession) => { + // TODO: Replace with actual API call + console.log('Saving test session:', sessionData) + localStorage.setItem(`test-session-${sessionData.examId}`, JSON.stringify(sessionData)) + } + + // Complete test and navigate + const handleTestComplete = (sessionData: ExamSession) => { + // TODO: Replace with actual API call + console.log('Test completed:', sessionData) + saveTestSession(sessionData) + // Navigate to results or dashboard + // navigate('/admin/coordinator/tests') + } // Auto-save mechanism useEffect(() => { const interval = setInterval(() => { - if (session.status === "in-progress" && onExamSave) { - onExamSave(session); + if (session.status === 'in-progress') { + saveTestSession(session) } - }, 30000); + }, 30000) - return () => clearInterval(interval); - }, [session, onExamSave]); + return () => clearInterval(interval) + }, [session]) const handleAnswerChange = (itemId: string, answer: string | string[]) => { setAnswerKeyResponses((prev) => ({ ...prev, [itemId]: answer, - })); + })) // Update session answers setSession((prev) => { - const existingAnswerIndex = prev.answers.findIndex( - (a) => a.questionId === itemId - ); + const existingAnswerIndex = prev.answers.findIndex((a) => a.questionId === itemId) const newAnswer: StudentAnswer = { questionId: itemId, answer, timeSpent: 0, - }; + } const newAnswers = existingAnswerIndex >= 0 - ? prev.answers.map((a, i) => - i === existingAnswerIndex ? newAnswer : a - ) - : [...prev.answers, newAnswer]; + ? prev.answers.map((a, i) => (i === existingAnswerIndex ? newAnswer : a)) + : [...prev.answers, newAnswer] return { ...prev, answers: newAnswers, - }; - }); - }; + } + }) + } const handleTimeUp = () => { setSecurityWarning({ show: true, - message: "Süre doldu! Test otomatik olarak teslim ediliyor...", - type: "error", - }); + message: 'Süre doldu! Test otomatik olarak teslim ediliyor...', + type: 'error', + }) setTimeout(() => { - completeExam(); - }, 3000); - }; + completeExam() + }, 3000) + } const completeExam = () => { const completedSession: ExamSession = { ...session, endTime: new Date(), - status: "completed", - }; + status: 'completed', + } - setSession(completedSession); - onExamComplete(completedSession); - }; + setSession(completedSession) + handleTestComplete(completedSession) + } const handleSubmitExam = () => { const unanswered = @@ -120,45 +146,41 @@ export const PDFTestInterface: React.FC = ({ !answerKeyResponses[item.id] || (Array.isArray(answerKeyResponses[item.id]) && (answerKeyResponses[item.id] as string[]).length === 0) || - (typeof answerKeyResponses[item.id] === "string" && - !answerKeyResponses[item.id]) - ) || []; + (typeof answerKeyResponses[item.id] === 'string' && !answerKeyResponses[item.id]), + ) || [] if (unanswered.length > 0) { const confirmSubmit = window.confirm( - `${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?` - ); + `${unanswered.length} soru cevaplanmamış. Yine de testi teslim etmek istiyor musunuz?`, + ) - if (!confirmSubmit) return; + if (!confirmSubmit) return } - completeExam(); - }; + completeExam() + } const getAnsweredCount = () => { return ( exam.answerKeyTemplate?.filter((item) => { - const answer = answerKeyResponses[item.id]; + const answer = answerKeyResponses[item.id] if (Array.isArray(answer)) { - return answer.length > 0 && answer.some((a) => a.trim() !== ""); + return answer.length > 0 && answer.some((a) => a.trim() !== '') } - return answer && answer.toString().trim() !== ""; + return answer && answer.toString().trim() !== '' }).length || 0 - ); - }; + ) + } const renderAnswerKeyItem = (item: AnswerKeyItem) => { - const currentAnswer = answerKeyResponses[item.id]; + const currentAnswer = answerKeyResponses[item.id] switch (item.type) { - case "multiple-choice": + case 'multiple-choice': return (
{item.options?.map((option, index) => ( -
- ); + ) - case "fill-blank": + case 'fill-blank': return ( handleAnswerChange(item.id, e.target.value)} className="w-full text-sm border border-gray-300 rounded-lg px-2 py-1 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Cevabınızı yazın..." /> - ); + ) - case "true-false": + case 'true-false': return (
- ); + ) default: - return null; + return null } - }; + } - if (session.status === "completed") { + if (session.status === 'completed') { return (
-

- Test Tamamlandı! -

+

Test Tamamlandı!

- Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten - sonra bilgilendirileceksiniz. + Testiniz başarıyla teslim edildi. Sonuçlarınız değerlendirildikten sonra + bilgilendirileceksiniz.

- Başlama: {session.startTime.toLocaleString("tr-TR")} + Başlama: {session.startTime.toLocaleString('tr-TR')}
- Bitiş: {session.endTime?.toLocaleString("tr-TR")} + Bitiş: {session.endTime?.toLocaleString('tr-TR')}
- ); + ) } return (
- setSecurityWarning((prev) => ({ ...prev, show: false })) - } + onDismiss={() => setSecurityWarning((prev) => ({ ...prev, show: false }))} message={securityWarning.message} type={securityWarning.type} /> @@ -256,21 +274,14 @@ export const PDFTestInterface: React.FC = ({
-

- {exam.title} -

+

{exam.title}

- PDF Test - {getAnsweredCount()} /{" "} - {exam.answerKeyTemplate?.length || 0} cevaplandı + PDF Test - {getAnsweredCount()} / {exam.answerKeyTemplate?.length || 0} cevaplandı

- +