Skip to content

Commit b1f4790

Browse files
authored
Merge pull request #171 from danielemery/112-support-entering-of-results-by-individual-question
Support entering of results by individual question
2 parents a1151d6 + adc3610 commit b1f4790

19 files changed

+772
-57
lines changed

package-lock.json

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@sentry/browser": "^9.6.0",
2424
"classnames": "^2.3.1",
2525
"date-fns": "^4.1.0",
26+
"dexie": "^4.0.11",
2627
"docker-react": "^0.0.3",
2728
"graphql": "^16.8.1",
2829
"joi": "^17.6.0",

src/EnterQuizResults.tsx

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Link, useParams } from 'react-router-dom';
1+
import { Link } from 'react-router-dom';
22

33
import { useMutation, useQuery } from '@apollo/client';
44
import { useState } from 'preact/hooks';
@@ -9,12 +9,15 @@ import Loader from './components/Loader';
99
import LoaderOverlay from './components/LoaderOverlay';
1010
import { UserSelector } from './components/UserSelector';
1111
import { formatDate, userIdentifier } from './helpers';
12+
import useAssertParams from './hooks/useAssertParams';
13+
import { useUnsubmittedAnswers } from './hooks/useUnsubmittedAnswers';
1214
import { COMPLETE_QUIZ, QUIZ, QUIZ_AND_AVAILABLE_USERS, QUIZZES } from './queries/quiz';
15+
import { UnsubmittedAnswer } from './services/db';
1316
import { Quiz } from './types/quiz';
1417
import { User } from './types/user';
1518

1619
export default function EnterQuizResults() {
17-
const { id } = useParams();
20+
const { id } = useAssertParams();
1821
const { loading, data } = useQuery<{
1922
quiz: Quiz;
2023
users: { edges: { node: User }[] };
@@ -32,18 +35,33 @@ export default function EnterQuizResults() {
3235

3336
const { user: authenticatedUser } = useQuizlord();
3437

35-
async function handleSubmit(score: number, participants: string[]) {
38+
const { unsubmittedAnswers, currentTotalScore, handleScoresDeleted } = useUnsubmittedAnswers(id);
39+
40+
async function handleSubmit(score: number, unsubmittedAnswers: UnsubmittedAnswer[], participants: string[]) {
3641
const participantsWithAuthenticatedUser = [authenticatedUser?.email, ...participants];
42+
const questionResults = unsubmittedAnswers.map((answer) => ({
43+
questionNum: answer.questionNumber,
44+
score: answer.score,
45+
}));
3746
await completeQuiz({
38-
variables: { quizId: id, completedBy: participantsWithAuthenticatedUser, score },
47+
variables: {
48+
quizId: id,
49+
completedBy: participantsWithAuthenticatedUser,
50+
score,
51+
questionResults,
52+
},
3953
});
54+
handleScoresDeleted();
4055
setComplete(true);
4156
}
4257

43-
const [score, setScore] = useState<number>(0);
58+
const [manuallyEnteredScore, setManuallyEnteredTotalScore] = useState<number>(0);
4459
const [participants, setParticipants] = useState<string[]>([]);
4560
const [complete, setComplete] = useState(false);
4661

62+
const missingAnswers =
63+
unsubmittedAnswers.length > 0 && unsubmittedAnswers.length < (data?.quiz?.questions?.length || 0);
64+
4765
if (!data || loading) {
4866
return <Loader message='Loading your quiz...' />;
4967
}
@@ -69,14 +87,31 @@ export default function EnterQuizResults() {
6987
Score
7088
</label>
7189
<div className='mt-1'>
72-
<input
73-
type='number'
74-
name='score'
75-
autoComplete='quiz-score'
76-
value={score}
77-
onChange={(e) => setScore(parseFloat((e.target as HTMLInputElement).value))}
78-
className='shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md'
79-
/>
90+
{unsubmittedAnswers.length > 0 ? (
91+
<div>
92+
<div>Calculated: {currentTotalScore}</div>
93+
<Link to={`/quiz/${id}/question/1`}>
94+
<Button>Edit</Button>
95+
</Link>
96+
<Button warning onClick={() => handleScoresDeleted()}>
97+
Delete & Override
98+
</Button>
99+
</div>
100+
) : (
101+
<div>
102+
<input
103+
type='number'
104+
name='score'
105+
autoComplete='quiz-score'
106+
value={manuallyEnteredScore}
107+
onChange={(e) => setManuallyEnteredTotalScore(parseFloat((e.target as HTMLInputElement).value))}
108+
className='shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border border-gray-300 rounded-md'
109+
/>
110+
<Link to={`/quiz/${id}/question/1`}>
111+
<Button>Individual Entry</Button>
112+
</Link>
113+
</div>
114+
)}
80115
</div>
81116
</div>
82117
<div>
@@ -119,9 +154,26 @@ export default function EnterQuizResults() {
119154
Cancel
120155
</Button>
121156
</Link>
122-
<Button onClick={() => handleSubmit(score, participants)} disabled={isCompletingQuiz}>
157+
<Button
158+
onClick={() =>
159+
handleSubmit(
160+
unsubmittedAnswers.length > 0 ? currentTotalScore : manuallyEnteredScore,
161+
unsubmittedAnswers,
162+
participants,
163+
)
164+
}
165+
disabled={isCompletingQuiz || missingAnswers}
166+
>
123167
Submit Results
124168
</Button>
169+
{missingAnswers && (
170+
<p className='mt-2 text-sm text-red-600 font-medium'>
171+
Questions results have been only partially entered.
172+
<br />
173+
Please either press &quot;Edit&quot; above and complete the remaining questions, or press &quot;Delete &
174+
Override&quot; above and manually enter your score.
175+
</p>
176+
)}
125177
</div>
126178
)}
127179
</div>

src/QuestionOverview.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Link } from 'react-router-dom';
2+
3+
import { QuestionCube } from './components/QuestionCube';
4+
import { LocalQuestion } from './hooks/useQuizQuestions';
5+
6+
interface QuestionOverviewProps {
7+
quizId: string;
8+
questions: LocalQuestion[];
9+
selectedNumber?: number;
10+
}
11+
12+
export default function QuestionOverview({ questions, quizId, selectedNumber }: QuestionOverviewProps) {
13+
return (
14+
<div className='grid grid-cols-5 gap-4'>
15+
{questions.map((q) => (
16+
<Link key={q.questionNum} to={`/quiz/${quizId}/question/${q.questionNum}`}>
17+
<QuestionCube
18+
questionNum={q.questionNum}
19+
score={q.unsubmittedScore}
20+
selected={selectedNumber === q.questionNum}
21+
/>
22+
</Link>
23+
))}
24+
</div>
25+
);
26+
}

src/QuizDetails.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useMutation, useQuery } from '@apollo/client';
44
import { Fragment } from 'preact';
55

66
import QuizImageComponent from './QuizImage';
7-
import QuizQuestions from './QuizQuestions';
7+
import QuizQuestions, { QuizQuestionWithResults } from './QuizQuestions';
88
import { useQuizlord } from './QuizlordProvider';
99
import Button from './components/Button';
1010
import Loader from './components/Loader';
@@ -18,7 +18,7 @@ import {
1818
} from './helpers';
1919
import { AI_PROCESS_QUIZ_IMAGES, MARK_INACCURATE_OCR, MARK_QUIZ_ILLEGIBLE, QUIZ, QUIZZES } from './queries/quiz';
2020
import { userCanPerformAction } from './services/authorization';
21-
import { Quiz as QuizType } from './types/quiz';
21+
import { QuizCompletion, QuizQuestion, Quiz as QuizType } from './types/quiz';
2222

2323
const imageTypeSortValues: {
2424
[imageType: string]: number;
@@ -81,7 +81,10 @@ export default function Quiz() {
8181
)}
8282
</dl>
8383
{data.quiz.questions.length > 0 ? (
84-
<QuizQuestions questions={data.quiz.questions} reportedInaccurateOCR={data.quiz.reportedInaccurateOCR} />
84+
<QuizQuestions
85+
questions={packAnswersIntoQuestions(data.quiz.questions, data.quiz.completions, user?.email)}
86+
reportedInaccurateOCR={data.quiz.reportedInaccurateOCR}
87+
/>
8588
) : null}
8689
{[...data.quiz.images]
8790
.sort((a, b) => {
@@ -166,3 +169,42 @@ export default function Quiz() {
166169
</div>
167170
);
168171
}
172+
173+
function packAnswersIntoQuestions(
174+
questions: QuizQuestion[],
175+
completions: QuizCompletion[],
176+
currentUserEmail?: string,
177+
): QuizQuestionWithResults[] {
178+
return questions.map((question) => {
179+
const resultsForQuestion = completions
180+
.filter((completion) => completion.questionResults?.length)
181+
.map((completion) => {
182+
const questionResult = completion.questionResults.find((result) => result.questionId === question.id);
183+
return {
184+
users: completion.completedBy,
185+
score: questionResult ? questionResult.score : 'INCORRECT',
186+
};
187+
});
188+
const myScore = currentUserEmail
189+
? resultsForQuestion.find((result) => result.users.some((user) => user.email === currentUserEmail))?.score
190+
: undefined;
191+
const averageScore =
192+
resultsForQuestion.length > 0
193+
? resultsForQuestion.reduce((acc, result) => {
194+
if (result.score === 'CORRECT') return acc + 1;
195+
if (result.score === 'HALF_CORRECT') return acc + 0.5;
196+
return acc;
197+
}, 0) / resultsForQuestion.length
198+
: undefined;
199+
const userResults = resultsForQuestion.map((result) => ({
200+
users: result.users.map((user) => userIdentifier(user)),
201+
score: result.score,
202+
}));
203+
return {
204+
...question,
205+
userResults,
206+
myScore,
207+
averageScore,
208+
};
209+
});
210+
}

0 commit comments

Comments
 (0)