Dealer_Onboard_Frontend/src/pages/public/PublicQuestionnairePage.tsx

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;