272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
import InputError from '@/components/input-error';
|
|
import LoadingButton from '@/components/loading-button';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { onHandleChange } from '@/lib/inertia';
|
|
import { useForm } from '@inertiajs/react';
|
|
import { useEffect, useState } from 'react';
|
|
import { Editor } from 'richtor';
|
|
import 'richtor/styles';
|
|
import FillBlankForm from './question-types/fill-blank-form';
|
|
import ListeningForm from './question-types/listening-form';
|
|
import MatchingForm from './question-types/matching-form';
|
|
import MultipleChoiceForm from './question-types/multiple-choice-form';
|
|
import OrderingForm from './question-types/ordering-form';
|
|
import ShortAnswerForm from './question-types/short-answer-form';
|
|
|
|
interface Props {
|
|
exam: Exam;
|
|
question?: ExamQuestion;
|
|
handler: React.ReactNode;
|
|
}
|
|
|
|
type QuestionFormData = {
|
|
exam_id: number | string;
|
|
title: string;
|
|
description: string;
|
|
marks: number;
|
|
options: {
|
|
answers?: string[];
|
|
matches?: Array<{ id: number; question: string; answer: string }>;
|
|
items?: string[];
|
|
correct_order?: number[];
|
|
sample_answer?: string;
|
|
audio_url?: string;
|
|
audio_file?: File;
|
|
audio_source?: 'url' | 'upload';
|
|
instructions?: string;
|
|
[key: string]: any;
|
|
};
|
|
question_options: Array<{
|
|
id?: number;
|
|
option_text: string;
|
|
is_correct: boolean;
|
|
sort: number;
|
|
}>;
|
|
question_type: ExamQuestionType;
|
|
exam_question_id: number | null;
|
|
};
|
|
|
|
const questionTypes: { value: ExamQuestionType; label: string }[] = [
|
|
{ value: 'multiple_choice', label: 'Multiple Choice' },
|
|
{ value: 'multiple_select', label: 'Multiple Select' },
|
|
{ value: 'matching', label: 'Matching' },
|
|
{ value: 'fill_blank', label: 'Fill in the Blank' },
|
|
{ value: 'ordering', label: 'Ordering' },
|
|
{ value: 'short_answer', label: 'Short Answer' },
|
|
{ value: 'listening', label: 'Listening' },
|
|
];
|
|
|
|
const QuestionDialog = ({ exam, question, handler }: Props) => {
|
|
const [open, setOpen] = useState(false);
|
|
const [isSubmit, setIsSubmit] = useState(false);
|
|
const [isFileSelected, setIsFileSelected] = useState(false);
|
|
const [isFileUploaded, setIsFileUploaded] = useState(false);
|
|
|
|
const initialFormData: QuestionFormData = {
|
|
exam_id: exam.id,
|
|
title: question?.title || '',
|
|
description: question?.description || '',
|
|
marks: question?.marks || 1,
|
|
options: question?.options || {},
|
|
question_options: (question?.question_options || []).map((opt) => ({
|
|
id: opt.id ? Number(opt.id) : undefined,
|
|
option_text: opt.option_text,
|
|
is_correct: opt.is_correct,
|
|
sort: opt.sort,
|
|
})),
|
|
question_type: question?.question_type || 'multiple_choice',
|
|
exam_question_id: question?.id ? Number(question.id) : null,
|
|
};
|
|
|
|
const { data, setData, post, put, errors, processing, reset, clearErrors } = useForm(initialFormData);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
// For listening questions with file upload
|
|
if (data.question_type === 'listening' && data.options?.audio_source === 'upload') {
|
|
// Only wait for upload if it's a NEW question or if a new file was selected
|
|
if (!question || isFileSelected) {
|
|
if (!isFileUploaded) {
|
|
setIsSubmit(true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Otherwise submit the form immediately
|
|
submitForm();
|
|
};
|
|
|
|
const submitForm = () => {
|
|
clearErrors();
|
|
|
|
if (question) {
|
|
put(route('exam-questions.update', question.id), {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
setOpen(false);
|
|
reset();
|
|
setIsSubmit(false);
|
|
setIsFileUploaded(false);
|
|
},
|
|
});
|
|
} else {
|
|
post(route('exam-questions.store'), {
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
setOpen(false);
|
|
reset();
|
|
setIsSubmit(false);
|
|
setIsFileUploaded(false);
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// Auto-submit after file upload completes
|
|
useEffect(() => {
|
|
if (isFileUploaded && isSubmit) {
|
|
submitForm();
|
|
}
|
|
}, [isFileUploaded, isSubmit]);
|
|
|
|
// Reset upload states when dialog opens/closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setIsSubmit(false);
|
|
setIsFileSelected(false);
|
|
setIsFileUploaded(false);
|
|
}
|
|
}, [open]);
|
|
|
|
const renderQuestionTypeForm = () => {
|
|
const props = {
|
|
data,
|
|
setData,
|
|
errors,
|
|
};
|
|
|
|
switch (data.question_type) {
|
|
case 'multiple_choice':
|
|
case 'multiple_select':
|
|
return <MultipleChoiceForm {...props} isMultipleSelect={data.question_type === 'multiple_select'} />;
|
|
case 'matching':
|
|
return <MatchingForm {...props} />;
|
|
case 'fill_blank':
|
|
return <FillBlankForm {...props} />;
|
|
case 'ordering':
|
|
return <OrderingForm {...props} />;
|
|
case 'short_answer':
|
|
return <ShortAnswerForm {...props} />;
|
|
case 'listening':
|
|
return (
|
|
<ListeningForm
|
|
{...props}
|
|
isSubmit={isSubmit}
|
|
setIsSubmit={setIsSubmit}
|
|
setIsFileSelected={setIsFileSelected}
|
|
setIsFileUploaded={setIsFileUploaded}
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger>{handler}</DialogTrigger>
|
|
<DialogContent className="max-h-[90vh] max-w-4xl overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>{question ? 'Edit Question' : 'Create Question'}</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<div>
|
|
<Label>Question Type *</Label>
|
|
<Select
|
|
value={data.question_type}
|
|
onValueChange={(value: ExamQuestionType) => setData('question_type', value)}
|
|
disabled={question ? true : false}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select question type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{questionTypes.map((type) => (
|
|
<SelectItem key={type.value} value={type.value}>
|
|
{type.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
<InputError message={errors.question_type} />
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Marks *</Label>
|
|
<Input
|
|
type="number"
|
|
step="0.5"
|
|
min="0.5"
|
|
name="marks"
|
|
value={data.marks.toString()}
|
|
onChange={(e) => setData('marks', parseFloat(e.target.value) || 0)}
|
|
placeholder="Enter marks"
|
|
/>
|
|
<InputError message={errors.marks} />
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Question Title *</Label>
|
|
<Input name="title" value={data.title} onChange={(e) => onHandleChange(e, setData)} placeholder="Enter question title" />
|
|
<InputError message={errors.title} />
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Description (Optional)</Label>
|
|
<Editor
|
|
ssr={true}
|
|
output="html"
|
|
placeholder={{
|
|
paragraph: 'Add additional context or instructions...',
|
|
imageCaption: 'Add additional context or instructions...',
|
|
}}
|
|
contentMinHeight={150}
|
|
contentMaxHeight={300}
|
|
initialContent={data.description}
|
|
onContentChange={(value) =>
|
|
setData((prev) => ({
|
|
...prev,
|
|
description: value as string,
|
|
}))
|
|
}
|
|
/>
|
|
<InputError message={errors.description} />
|
|
</div>
|
|
|
|
{renderQuestionTypeForm()}
|
|
|
|
<div className="flex justify-end gap-3 border-t pt-4">
|
|
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={processing || isSubmit}>
|
|
Cancel
|
|
</Button>
|
|
<LoadingButton loading={processing || isSubmit} disabled={processing || isSubmit}>
|
|
{question ? 'Update Question' : 'Create Question'}
|
|
</LoadingButton>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default QuestionDialog;
|