Dealer_Onboard_Frontend/src/components/dealer/QuestionnaireForm.tsx

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;