201 lines
7.8 KiB
TypeScript
201 lines
7.8 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';
|
|
options?: any;
|
|
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 = await API.getLatestQuestionnaire();
|
|
if (res.data && res.data.data && res.data.data.questions) {
|
|
setQuestions(res.data.data.questions);
|
|
}
|
|
} 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 handleSubmit = async () => {
|
|
const missing = questions.filter(q => q.isMandatory && !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">
|
|
<h3 className="text-xl font-semibold">Dealership Assessment Questionnaire</h3>
|
|
|
|
{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' && (
|
|
<input
|
|
type="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 === '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' && (
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="radio"
|
|
name={`q-${q.id}`}
|
|
value="yes"
|
|
checked={responses[q.id] === 'yes'}
|
|
onChange={() => handleInputChange(q.id, 'yes')}
|
|
disabled={readOnly}
|
|
/> Yes
|
|
</label>
|
|
<label className="flex items-center gap-2">
|
|
<input
|
|
type="radio"
|
|
name={`q-${q.id}`}
|
|
value="no"
|
|
checked={responses[q.id] === 'no'}
|
|
onChange={() => handleInputChange(q.id, 'no')}
|
|
disabled={readOnly}
|
|
/> No
|
|
</label>
|
|
</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;
|