275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { API } from '../../api/API';
|
|
import { toast } from 'sonner';
|
|
|
|
interface Question {
|
|
id: string;
|
|
sectionName: string;
|
|
questionText: string;
|
|
inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq' | 'radio' | 'textarea' | 'email';
|
|
options?: any;
|
|
questionOptions?: any[]; // From backend inclusion
|
|
weight: number;
|
|
order: number;
|
|
isMandatory: boolean;
|
|
}
|
|
|
|
interface QuestionnaireFormProps {
|
|
applicationId: string;
|
|
onComplete?: () => void;
|
|
readOnly?: boolean;
|
|
existingResponses?: any[];
|
|
publicMode?: boolean; // New prop
|
|
initialQuestions?: Question[]; // New prop to avoid re-fetching
|
|
}
|
|
|
|
const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
|
applicationId,
|
|
onComplete,
|
|
readOnly = false,
|
|
existingResponses,
|
|
publicMode = false,
|
|
initialQuestions
|
|
}) => {
|
|
const [questions, setQuestions] = useState<Question[]>(initialQuestions || []);
|
|
const [responses, setResponses] = useState<Record<string, any>>({});
|
|
const [loading, setLoading] = useState(!initialQuestions);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!initialQuestions) {
|
|
fetchQuestionnaire();
|
|
}
|
|
if (existingResponses) {
|
|
const initialResponses: any = {};
|
|
existingResponses.forEach(r => {
|
|
initialResponses[r.questionId] = r.responseValue;
|
|
});
|
|
setResponses(initialResponses);
|
|
}
|
|
}, [existingResponses, initialQuestions]);
|
|
|
|
const fetchQuestionnaire = async () => {
|
|
try {
|
|
// In public mode, we shouldn't fetch "latest" as it requires auth.
|
|
// Public page should provide initialQuestions.
|
|
// But if we ever needed to fetch, we'd need a public endpoint for just questions or rely on the parent.
|
|
if (publicMode) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const res: any = await API.getLatestQuestionnaire();
|
|
|
|
if (res?.data?.data?.questions) {
|
|
// Normalize questions
|
|
const normalized = res.data.data.questions.map((q: any) => ({
|
|
...q,
|
|
inputType: (q.inputType === 'mcq') ? 'select' : q.inputType
|
|
}));
|
|
setQuestions(normalized);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error('Failed to load questionnaire');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (questionId: string, value: any) => {
|
|
if (readOnly) return;
|
|
setResponses(prev => ({ ...prev, [questionId]: value }));
|
|
};
|
|
|
|
const handleFileChange = (questionId: string, e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (readOnly) return;
|
|
if (e.target.files && e.target.files[0]) {
|
|
const file = e.target.files[0];
|
|
if (file.size > 5 * 1024 * 1024) {
|
|
toast.error("File size must be less than 5MB");
|
|
return;
|
|
}
|
|
const reader = new FileReader();
|
|
reader.onloadend = () => {
|
|
handleInputChange(questionId, reader.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
// Skip mandatory check for 'file' type as per user request to bypass
|
|
const missing = questions.filter(q => q.isMandatory && q.inputType !== 'file' && !responses[q.id]);
|
|
if (missing.length > 0) {
|
|
toast.error(`Please answer all mandatory questions. Missing: ${missing.length}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
const payload = Object.entries(responses).map(([qId, val]) => ({
|
|
questionId: qId,
|
|
value: val
|
|
}));
|
|
|
|
if (publicMode) {
|
|
await API.submitPublicResponse({
|
|
applicationId,
|
|
responses: payload
|
|
});
|
|
} else {
|
|
await API.submitQuestionnaireResponse({
|
|
applicationId,
|
|
responses: payload
|
|
});
|
|
}
|
|
|
|
toast.success('Responses submitted successfully');
|
|
if (onComplete) onComplete();
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error('Failed to submit responses');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) return <div>Loading questionnaire...</div>;
|
|
if (questions.length === 0) return <div>No active questionnaire found.</div>;
|
|
|
|
const sections = questions.reduce((acc, q) => {
|
|
if (!acc[q.sectionName]) acc[q.sectionName] = [];
|
|
acc[q.sectionName].push(q);
|
|
return acc;
|
|
}, {} as Record<string, Question[]>);
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="rounded-lg overflow-hidden border border-slate-200 shadow-sm">
|
|
<div className="bg-black px-5 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-8 w-auto" />
|
|
<div>
|
|
<p className="text-white text-lg font-bold tracking-wide leading-tight">ROYAL ENFIELD</p>
|
|
<p className="text-slate-300 text-sm leading-tight">Dealership Partner Application</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="bg-white px-5 py-3 border-t border-slate-200">
|
|
<h3 className="text-xl font-semibold">Dealership Assessment Questionnaire</h3>
|
|
</div>
|
|
</div>
|
|
|
|
{Object.entries(sections).map(([sectionName, sectionQuestions]) => (
|
|
<div key={sectionName} className="border p-4 rounded bg-white shadow-sm">
|
|
<h4 className="font-medium text-lg mb-4 border-b pb-2">{sectionName}</h4>
|
|
<div className="space-y-4">
|
|
{sectionQuestions.map(q => (
|
|
<div key={q.id}>
|
|
<label className="block text-sm font-medium mb-1">
|
|
{q.questionText} {q.isMandatory && !readOnly && <span className="text-red-500">*</span>}
|
|
</label>
|
|
|
|
{(q.inputType === 'text' || q.inputType === 'email') && (
|
|
<input
|
|
type={q.inputType === 'email' ? 'email' : 'text'}
|
|
className="w-full border p-2 rounded disabled:bg-gray-100"
|
|
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
|
value={responses[q.id] || ''}
|
|
disabled={readOnly}
|
|
/>
|
|
)}
|
|
|
|
{q.inputType === 'textarea' && (
|
|
<textarea
|
|
className="w-full border p-2 rounded disabled:bg-gray-100 min-h-[100px]"
|
|
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
|
value={responses[q.id] || ''}
|
|
disabled={readOnly}
|
|
/>
|
|
)}
|
|
|
|
{q.inputType === 'file' && (
|
|
<div className="space-y-2">
|
|
<input
|
|
type="file"
|
|
className="w-full text-sm text-slate-500
|
|
file:mr-4 file:py-2 file:px-4
|
|
file:rounded-full file:border-0
|
|
file:text-sm file:font-semibold
|
|
file:bg-red-50 file:text-re-red
|
|
hover:file:bg-red-100"
|
|
onChange={(e) => handleFileChange(q.id, e)}
|
|
disabled={readOnly}
|
|
/>
|
|
{responses[q.id] && (
|
|
<div className="text-xs text-green-600 font-medium flex items-center gap-1">
|
|
✓ File Attached
|
|
</div>
|
|
)}
|
|
<div className="text-xs text-slate-400">
|
|
(Optional for now)
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{q.inputType === 'number' && (
|
|
<input
|
|
type="number"
|
|
className="w-full border p-2 rounded disabled:bg-gray-100"
|
|
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
|
value={responses[q.id] || ''}
|
|
disabled={readOnly}
|
|
/>
|
|
)}
|
|
|
|
{(q.inputType === 'yesno' || q.inputType === 'select' || q.inputType === 'mcq' || q.inputType === 'radio') && (
|
|
<div className="space-y-2">
|
|
{/* Use backend options if available, or fallbacks for legacy yesno */}
|
|
{(q.questionOptions && q.questionOptions.length > 0 ? q.questionOptions : (
|
|
q.inputType === 'yesno' ? [{ optionText: 'Yes' }, { optionText: 'No' }] : []
|
|
)).map((opt: any, idx: number) => {
|
|
const val = opt.optionText || opt.text;
|
|
return (
|
|
<label key={idx} className="flex items-center gap-2 cursor-pointer p-1 hover:bg-slate-50 rounded">
|
|
<input
|
|
type="radio"
|
|
name={`q-${q.id}`}
|
|
value={val}
|
|
checked={responses[q.id] === val}
|
|
onChange={() => handleInputChange(q.id, val)}
|
|
disabled={readOnly}
|
|
className="text-re-red focus:ring-re-red w-4 h-4"
|
|
/>
|
|
<span className="text-gray-700">{val}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
{/* Fallback if select but no options (shouldn't happen ideally) */}
|
|
{(!q.questionOptions || q.questionOptions.length === 0) && q.inputType !== 'yesno' && (
|
|
<div className="text-sm text-red-400 italic">No options defined.</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{!readOnly && (
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={submitting}
|
|
className="bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700 w-full md:w-auto"
|
|
>
|
|
{submitting ? 'Submitting...' : 'Submit Assessment'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default QuestionnaireForm;
|