383 lines
22 KiB
TypeScript
383 lines
22 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { API } from '../../api/API';
|
|
import { toast } from 'sonner';
|
|
import { useSelector } from 'react-redux';
|
|
import { RootState } from '../../store';
|
|
import {
|
|
User, RefreshCw, HelpCircle, ArrowLeft,
|
|
Users, FileText, ChevronRight,
|
|
CheckCircle, Info
|
|
} from 'lucide-react';
|
|
|
|
type SubmittedView = 'none' | 'success' | 'already';
|
|
|
|
const PublicQuestionnairePage: React.FC = () => {
|
|
const { applicationId } = useParams<{ applicationId: string }>();
|
|
const navigate = useNavigate();
|
|
const { user } = useSelector((state: RootState) => state.auth);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [questions, setQuestions] = useState<any[]>([]);
|
|
const [sections, setSections] = useState<string[]>([]);
|
|
const [activeSection, setActiveSection] = useState<string>('');
|
|
const [responses, setResponses] = useState<Record<string, any>>({});
|
|
const [submitting, setSubmitting] = useState(false);
|
|
/** End-of-flow screen: success = just submitted; already = reopened link / second visit */
|
|
const [submittedView, setSubmittedView] = useState<SubmittedView>('none');
|
|
|
|
useEffect(() => {
|
|
const fetchQuestionnaire = async () => {
|
|
if (!applicationId) return;
|
|
try {
|
|
// Determine if we need public or private fetch based on auth
|
|
// For prospective users, we might want to use the public endpoint or a specific one
|
|
const res = await API.getPublicQuestionnaire(applicationId);
|
|
|
|
if (res.data.success) {
|
|
const fetchedQuestions = res.data.data.questions || [];
|
|
setQuestions(fetchedQuestions);
|
|
|
|
// Extract unique sections
|
|
const uniqueSections = Array.from(new Set(fetchedQuestions.map((q: any) => q.sectionName)));
|
|
setSections(uniqueSections as string[]);
|
|
if (uniqueSections.length > 0) {
|
|
setActiveSection(uniqueSections[0] as string);
|
|
}
|
|
|
|
// Check if already submitted (optional, backend might handle)
|
|
}
|
|
} catch (error: any) {
|
|
console.error("Error fetching questionnaire:", error);
|
|
if (error.response?.data?.code === 'ALREADY_SUBMITTED') {
|
|
setSubmittedView('already');
|
|
} else {
|
|
toast.error("Failed to load questionnaire");
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchQuestionnaire();
|
|
}, [applicationId]);
|
|
|
|
const handleInputChange = (questionId: string, value: any) => {
|
|
setResponses(prev => ({ ...prev, [questionId]: value }));
|
|
};
|
|
|
|
const handleNextSection = () => {
|
|
const currentIndex = sections.indexOf(activeSection);
|
|
if (currentIndex < sections.length - 1) {
|
|
setActiveSection(sections[currentIndex + 1]);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
};
|
|
|
|
const handlePrevSection = () => {
|
|
const currentIndex = sections.indexOf(activeSection);
|
|
if (currentIndex > 0) {
|
|
setActiveSection(sections[currentIndex - 1]);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async () => {
|
|
// Validate all mandatory questions
|
|
const missing = questions.filter(q => q.isMandatory && !responses[q.id]);
|
|
if (missing.length > 0) {
|
|
toast.error(`Please answer all mandatory questions. Missing: ${missing.length}`);
|
|
// Provide visual feedback or navigation to missing section?
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setSubmitting(true);
|
|
const payload = Object.entries(responses).map(([qId, val]) => ({
|
|
questionId: qId,
|
|
value: val
|
|
}));
|
|
|
|
await API.submitPublicResponse({
|
|
applicationId: applicationId!,
|
|
responses: payload
|
|
});
|
|
|
|
toast.success('Responses submitted successfully');
|
|
setSubmittedView('success');
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error('Failed to submit responses');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
if (loading) return (
|
|
<div className="flex items-center justify-center h-screen bg-slate-50">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div>
|
|
</div>
|
|
);
|
|
|
|
if (submittedView !== 'none') {
|
|
const isAlready = submittedView === 'already';
|
|
return (
|
|
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-6">
|
|
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full text-center border-t-4 border-re-red">
|
|
<div
|
|
className={`w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 ${
|
|
isAlready ? 'bg-slate-100 text-slate-600' : 'bg-green-100 text-green-600'
|
|
}`}
|
|
>
|
|
{isAlready ? <Info className="w-8 h-8" /> : <CheckCircle className="w-8 h-8" />}
|
|
</div>
|
|
<h2 className="text-2xl font-bold mb-2 text-slate-900">
|
|
{isAlready ? 'Already submitted' : 'Thank you'}
|
|
</h2>
|
|
<p className="text-slate-600 leading-relaxed">
|
|
{isAlready ? (
|
|
<>
|
|
This questionnaire has already been submitted for your application. You do not need
|
|
to complete it again. If you think this is a mistake, contact support using the same
|
|
email you used to apply. You may close this page when you are done.
|
|
</>
|
|
) : (
|
|
<>
|
|
Your assessment has been submitted successfully. We have received your responses and
|
|
will review them as part of your application. You may close this page when you are
|
|
done.
|
|
</>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const activeQuestions = questions.filter(q => q.sectionName === activeSection);
|
|
const currentSectionIndex = sections.indexOf(activeSection);
|
|
|
|
return (
|
|
<div className="flex-1 flex flex-col overflow-hidden h-screen bg-slate-50">
|
|
{/* Header */}
|
|
<header className="bg-white border-b border-slate-200 px-6 py-4 flex-shrink-0">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-slate-900 font-bold text-xl">Dealer Questionnaire Form</h1>
|
|
<p className="text-slate-600 text-sm max-w-2xl leading-snug">
|
|
Answer each section accurately. Your responses are part of the dealership application assessment and may be verified.
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{user && (
|
|
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
|
<div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center">
|
|
<User className="w-4 h-4 text-white" />
|
|
</div>
|
|
<div className="text-left">
|
|
<p className="text-slate-900 text-sm font-medium">{user.name || 'Applicant'}</p>
|
|
<p className="text-slate-600 text-xs">{user.role || 'Prospective Dealer'}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<button className="p-2 text-slate-500 hover:bg-slate-100 rounded-lg" title="Refresh" onClick={() => window.location.reload()}>
|
|
<RefreshCw className="w-4 h-4" />
|
|
</button>
|
|
<button className="p-2 text-slate-500 hover:bg-slate-100 rounded-lg" title="Help">
|
|
<HelpCircle className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 overflow-y-auto">
|
|
<div className="bg-white border-b border-slate-200 sticky top-0 z-20 shadow-sm">
|
|
<div className="max-w-5xl mx-auto px-8 py-3">
|
|
<button
|
|
onClick={() => navigate('/prospective-dashboard')}
|
|
className="inline-flex items-center gap-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors px-3 py-2 rounded-lg hover:bg-slate-100"
|
|
>
|
|
<ArrowLeft className="w-4 h-4" />
|
|
Back to Applicant Portal
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-5xl mx-auto py-8 px-6">
|
|
{/* Hero Section */}
|
|
<div className="bg-re-black rounded-t-lg overflow-hidden shadow-xl">
|
|
<div className="relative px-8 py-12">
|
|
<div className="relative z-10 text-center">
|
|
<div className="flex items-center justify-center mb-7">
|
|
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-12 w-auto" />
|
|
</div>
|
|
<div className="h-1 w-24 bg-amber-600 mx-auto mb-4"></div>
|
|
<h2 className="text-amber-400 text-xl mb-4 font-light">Dealership Partner Application</h2>
|
|
<p className="text-slate-300 max-w-2xl mx-auto leading-relaxed text-sm">
|
|
Thank you for your interest in becoming a Royal Enfield business partner.
|
|
Please complete this questionnaire to help us understand your profile and aspirations.
|
|
</p>
|
|
|
|
<div className="flex items-center justify-center gap-8 mt-8 border-t border-slate-700/50 pt-6 inline-flex mx-auto">
|
|
<div className="text-center px-4">
|
|
<div className="text-amber-400 text-2xl font-bold">{questions.length}</div>
|
|
<div className="text-slate-400 text-xs uppercase tracking-wider">Questions</div>
|
|
</div>
|
|
<div className="h-10 w-px bg-slate-700"></div>
|
|
<div className="text-center px-4">
|
|
<div className="text-amber-400 text-2xl font-bold">{sections.length}</div>
|
|
<div className="text-slate-400 text-xs uppercase tracking-wider">Sections</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Section Tabs */}
|
|
<div className="bg-slate-800/50 backdrop-blur-sm border-t border-slate-700">
|
|
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide px-8 py-4 no-scrollbar">
|
|
{sections.map((section) => (
|
|
<button
|
|
key={section}
|
|
onClick={() => setActiveSection(section)}
|
|
className={`flex items-center gap-2 px-4 py-2 rounded-lg whitespace-nowrap transition-all text-sm font-medium
|
|
${activeSection === section
|
|
? 'bg-amber-600 text-white shadow-lg'
|
|
: 'bg-slate-700/50 text-slate-300 hover:bg-slate-700 hover:text-white'
|
|
}`}
|
|
>
|
|
<FileText className="w-4 h-4" />
|
|
<span>{section}</span>
|
|
{activeSection === section && (
|
|
<span className="ml-2 bg-white/20 px-2 py-0.5 rounded text-xs">
|
|
{questions.filter(q => q.sectionName === section).length}
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Question Content */}
|
|
<div className="bg-white rounded-b-lg shadow-xl border border-slate-200 border-t-0 min-h-[400px]">
|
|
<div className="p-8">
|
|
<div className="flex items-start gap-4 pb-6 border-b-2 border-amber-100 mb-8">
|
|
<div className="w-12 h-12 bg-amber-50 rounded-lg flex items-center justify-center flex-shrink-0 text-amber-600">
|
|
<Users className="w-6 h-6" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<h3 className="text-slate-900 text-xl font-bold mb-1">{activeSection}</h3>
|
|
<p className="text-slate-500 text-sm">
|
|
Section {currentSectionIndex + 1} of {sections.length} • {activeQuestions.length} questions
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-10">
|
|
{activeQuestions.map((q, idx) => (
|
|
<div key={q.id} className="group animate-in fade-in duration-500" style={{ animationDelay: `${idx * 100}ms` }}>
|
|
<div className="flex items-start gap-5">
|
|
<div className="w-8 h-8 bg-slate-100 rounded-full flex items-center justify-center flex-shrink-0 group-hover:bg-amber-100 transition-colors text-slate-600 group-hover:text-amber-700 font-semibold text-sm">
|
|
{idx + 1}
|
|
</div>
|
|
<div className="flex-1 space-y-3">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<label className="text-sm font-medium text-slate-900 leading-relaxed block">
|
|
{q.questionText}
|
|
{q.isMandatory && <span className="text-red-500 ml-1">*</span>}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="max-w-xl">
|
|
{(q.inputType === 'text' || q.inputType === 'email' || q.inputType === 'number') && (
|
|
<input
|
|
type={q.inputType}
|
|
className="w-full h-10 px-3 rounded-lg border border-slate-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition-all placeholder:text-slate-400"
|
|
placeholder="Type your answer here..."
|
|
value={responses[q.id] || ''}
|
|
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
|
/>
|
|
)}
|
|
|
|
{q.inputType === 'textarea' && (
|
|
<textarea
|
|
className="w-full h-32 p-3 rounded-lg border border-slate-300 focus:border-amber-500 focus:ring-2 focus:ring-amber-200 outline-none transition-all placeholder:text-slate-400"
|
|
placeholder="Type your answer here..."
|
|
value={responses[q.id] || ''}
|
|
onChange={(e) => handleInputChange(q.id, e.target.value)}
|
|
/>
|
|
)}
|
|
|
|
{(q.inputType === 'select' || q.inputType === 'yesno' || q.inputType === 'radio' || q.inputType === 'mcq') && (
|
|
<div className="space-y-2">
|
|
{(q.questionOptions || (q.inputType === 'yesno' ? [{ optionText: 'Yes' }, { optionText: 'No' }] : [])).map((opt: any, i: number) => {
|
|
const val = opt.optionText || opt.text;
|
|
return (
|
|
<label key={i} className="flex items-center gap-3 cursor-pointer group/opt">
|
|
<input
|
|
type="radio"
|
|
name={`q-${q.id}`}
|
|
className="w-4 h-4 text-amber-600 focus:ring-amber-500 border-slate-300"
|
|
checked={responses[q.id] === val}
|
|
onChange={() => handleInputChange(q.id, val)}
|
|
/>
|
|
<span className="text-slate-700 group-hover/opt:text-slate-900 transition-colors">{val}</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{activeQuestions.length === 0 && (
|
|
<div className="text-center py-10 text-slate-500 italic">
|
|
No questions in this section.
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-10 mt-10 border-t border-slate-100">
|
|
<button
|
|
onClick={handlePrevSection}
|
|
disabled={currentSectionIndex === 0}
|
|
className="px-6 py-2.5 rounded-lg text-sm font-medium border border-slate-300 text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Previous Section
|
|
</button>
|
|
|
|
{currentSectionIndex < sections.length - 1 ? (
|
|
<button
|
|
onClick={handleNextSection}
|
|
className="px-6 py-2.5 rounded-lg text-sm font-medium bg-amber-600 text-white hover:bg-amber-700 flex items-center gap-2 transition-colors shadow-md hover:shadow-lg"
|
|
>
|
|
Next Section
|
|
<ChevronRight className="w-4 h-4" />
|
|
</button>
|
|
) : (
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={submitting}
|
|
className="px-8 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 flex items-center gap-2 transition-colors shadow-md hover:shadow-lg disabled:bg-slate-400"
|
|
>
|
|
{submitting ? 'Submitting...' : 'Submit Application'}
|
|
{!submitting && <CheckCircle className="w-4 h-4" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-8 text-center text-slate-500 text-sm">
|
|
© 2026 Royal Enfield. All rights reserved.
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PublicQuestionnairePage;
|