From 8ef092f723ec8230d476a8ba184595f1d173d230 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 30 Jan 2026 19:53:32 +0530 Subject: [PATCH] public questionnnaire as per google form and opportunity & non-opportunity . admin shortlist initiated we need to work on participant concept --- src/api/API.ts | 1 + src/components/admin/QuestionnaireBuilder.tsx | 205 ++++++++---- .../applications/ApplicationDetails.tsx | 16 +- .../applications/ApplicationsPage.tsx | 22 +- .../applications/OpportunityRequestsPage.tsx | 309 ++++++++++++++---- .../QuestionnaireResponseView.tsx | 116 +++++++ .../UnopportunityRequestsPage.tsx | 91 +++++- src/components/auth/LoginPage.tsx | 13 +- src/components/dealer/QuestionnaireForm.tsx | 102 ++++-- src/components/layout/Sidebar.tsx | 2 +- src/lib/mock-data.ts | 8 + src/pages/public/PublicQuestionnairePage.tsx | 28 +- src/services/onboarding.service.ts | 9 + 13 files changed, 738 insertions(+), 184 deletions(-) create mode 100644 src/components/applications/QuestionnaireResponseView.tsx diff --git a/src/api/API.ts b/src/api/API.ts index 49a9ea7..03f2cb5 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -27,6 +27,7 @@ export const API = { // Onboarding submitApplication: (data: any) => client.post('/onboarding/apply', data), getApplications: () => client.get('/onboarding/applications'), + shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data), getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`), getLatestQuestionnaire: () => client.get('/questionnaire/latest'), createQuestionnaireVersion: (data: any) => client.post('/questionnaire/version', data), diff --git a/src/components/admin/QuestionnaireBuilder.tsx b/src/components/admin/QuestionnaireBuilder.tsx index ff67dd2..ef21d0a 100644 --- a/src/components/admin/QuestionnaireBuilder.tsx +++ b/src/components/admin/QuestionnaireBuilder.tsx @@ -8,8 +8,8 @@ interface Question { id?: string; sectionName: string; questionText: string; - inputType: 'text' | 'yesno' | 'file' | 'number'; - options?: any; + inputType: 'text' | 'yesno' | 'file' | 'number' | 'select'; + options?: { text: string; score: number }[]; weight: number; order: number; isMandatory: boolean; @@ -53,10 +53,21 @@ const QuestionnaireBuilder: React.FC = () => { const data = response.data.data; setVersion(`${data.version} (Copy)`); // Default to making a copy if (data.questions && data.questions.length > 0) { - setQuestions(data.questions.map((q: any) => ({ - ...q, - weight: parseFloat(q.weight) // Ensure weight is number - }))); + setQuestions(data.questions.map((q: any) => { + let normalizedType = q.inputType?.toLowerCase().trim(); + if (normalizedType === 'mcq') normalizedType = 'select'; + + // Fallback validity check + const validTypes = ['text', 'number', 'file', 'yesno', 'select']; + if (!validTypes.includes(normalizedType)) normalizedType = 'text'; + + return { + ...q, + inputType: normalizedType, + weight: parseFloat(q.weight), // Ensure weight is number + options: q.questionOptions?.map((opt: any) => ({ text: opt.optionText, score: opt.score })) || [] + }; + })); } } else { toast.error('Failed to load questionnaire'); @@ -92,10 +103,47 @@ const QuestionnaireBuilder: React.FC = () => { const updateQuestion = (index: number, field: keyof Question, value: any) => { const newQuestions = [...questions]; + + // Auto-populate Yes/No options if switching to yesno + if (field === 'inputType' && value === 'yesno' && (!newQuestions[index].options || newQuestions[index].options?.length === 0)) { + newQuestions[index].options = [ + { text: 'Yes', score: 5 }, + { text: 'No', score: 0 } + ]; + } else if (field === 'inputType' && value === 'select' && (!newQuestions[index].options)) { + newQuestions[index].options = []; + } + newQuestions[index] = { ...newQuestions[index], [field]: value }; setQuestions(newQuestions); }; + const addOption = (questionIndex: number) => { + const newQuestions = [...questions]; + if (!newQuestions[questionIndex].options) newQuestions[questionIndex].options = []; + newQuestions[questionIndex].options!.push({ text: '', score: 0 }); + setQuestions(newQuestions); + }; + + const updateOption = (questionIndex: number, optionIndex: number, field: 'text' | 'score', value: any) => { + const newQuestions = [...questions]; + if (newQuestions[questionIndex].options) { + newQuestions[questionIndex].options![optionIndex] = { + ...newQuestions[questionIndex].options![optionIndex], + [field]: value + }; + setQuestions(newQuestions); + } + }; + + const removeOption = (questionIndex: number, optionIndex: number) => { + const newQuestions = [...questions]; + if (newQuestions[questionIndex].options) { + newQuestions[questionIndex].options = newQuestions[questionIndex].options!.filter((_, i) => i !== optionIndex); + setQuestions(newQuestions); + } + }; + const handleSave = async () => { if (questions.some(q => !q.questionText)) { toast.error('All questions must have text'); @@ -185,59 +233,61 @@ const QuestionnaireBuilder: React.FC = () => { -
-
- - updateQuestion(index, 'questionText', e.target.value)} - className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none transition-shadow" - placeholder="Enter your question here..." - /> -
+
+
+
+ + updateQuestion(index, 'questionText', e.target.value)} + className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none transition-shadow" + placeholder="Enter your question here..." + /> +
-
- - -
+
+ + +
-
- - -
+
+ + +
-
-
-
-
- updateQuestion(index, 'weight', parseFloat(e.target.value))} - className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8" - title="Weightage" - /> - % +
+
+
+
+ updateQuestion(index, 'weight', parseFloat(e.target.value))} + className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8" + title="Weightage" + /> + % +
-
- {/*
updateQuestion(index, 'isMandatory', !q.isMandatory)} >
@@ -245,8 +295,53 @@ const QuestionnaireBuilder: React.FC = () => {
Req.
*/} +
+ + {/* Options Editor for Select/YesNo */} + {(q.inputType === 'select' || q.inputType === 'yesno') && ( +
+ +
+ {q.options?.map((opt, optIndex) => ( +
+ updateOption(index, optIndex, 'text', e.target.value)} + className="flex-1 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none" + placeholder={`Option ${optIndex + 1}`} + /> +
+ Score: + updateOption(index, optIndex, 'score', parseFloat(e.target.value))} + className="w-20 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none" + /> +
+ +
+ ))} +
+ +
+ )}
-
+
); }; diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index af2db87..5ebbeab 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -4,6 +4,7 @@ import { mockApplications, mockAuditLogs, mockDocuments, mockWorkNotes, mockLeve import { onboardingService } from '../../services/onboarding.service'; import { WorkNotesPage } from './WorkNotesPage'; import QuestionnaireForm from '../dealer/QuestionnaireForm'; +import QuestionnaireResponseView from './QuestionnaireResponseView'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; @@ -118,7 +119,8 @@ export function ApplicationDetails() { ownsBike: data.ownRoyalEnfield === 'yes', pastExperience: data.experienceYears ? `${data.experienceYears} years` : (data.description || ''), status: data.overallStatus as ApplicationStatus, - questionnaireMarks: 0, + questionnaireMarks: data.score || data.questionnaireMarks || 0, // Read from score or correct field + questionnaireResponses: data.questionnaireResponses || [], // Map responses rank: 0, totalApplicantsAtLocation: 0, submissionDate: data.createdAt, @@ -769,17 +771,7 @@ export function ApplicationDetails() { {/* Questionnaire Response Tab */} -
-
- -

Questionnaire Responses

-
- {application.questionnaireMarks !== undefined && ( - Score: {application.questionnaireMarks}/100 - )} -
- - +
{/* Progress Tab */} diff --git a/src/components/applications/ApplicationsPage.tsx b/src/components/applications/ApplicationsPage.tsx index c9b0ab5..9812c19 100644 --- a/src/components/applications/ApplicationsPage.tsx +++ b/src/components/applications/ApplicationsPage.tsx @@ -30,6 +30,8 @@ import { import { Progress } from '../ui/progress'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; import { Label } from '../ui/label'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store'; interface ApplicationsPageProps { onViewDetails: (id: string) => void; @@ -37,6 +39,7 @@ interface ApplicationsPageProps { } export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsPageProps) { + const { user: currentUser } = useSelector((state: RootState) => state.auth); const [searchQuery, setSearchQuery] = useState(''); const [locationFilter, setLocationFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState(initialFilter || 'all'); @@ -44,6 +47,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP const [selectedIds, setSelectedIds] = useState([]); const [sortBy, setSortBy] = useState<'date'>('date'); const [showNewApplicationModal, setShowNewApplicationModal] = useState(false); + const [showMyAssignments, setShowMyAssignments] = useState(false); // Real Data Integration const [applications, setApplications] = useState([]); @@ -77,7 +81,8 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP rank: 0, totalApplicantsAtLocation: 0, submissionDate: app.createdAt, - assignedUsers: [], + assignedUsers: [], // Keeping this for UI compatibility if needed + assignedTo: app.assignedTo, // Add this field for filtering progress: app.progressPercentage || 0, isShortlisted: true, // Show all for admin view // Add other fields to match interface @@ -117,7 +122,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP const isShortlisted = app.isShortlisted === true; // Only show shortlisted applications const notExcluded = !excludedApplicationIds.includes(app.id); // Exclude APP-005, 006, 007, 008 - return matchesSearch && matchesLocation && matchesStatus && isShortlisted && notExcluded; + // New Filter: My Assignments + const matchesAssignment = !showMyAssignments || ((app as any).assignedTo === currentUser?.id); + + return matchesSearch && matchesLocation && matchesStatus && isShortlisted && notExcluded && matchesAssignment; }) .sort((a, b) => { if (sortBy === 'date') { @@ -240,6 +248,16 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP + {/* My Assignments Filter */} +
+ setShowMyAssignments(checked as boolean)} + /> + +
+ {/* Sort By */} setAssigneeEmail(e.target.value)} - className="mt-2" - /> -

The selected applications will be assigned to this user for processing

+
+ + + {/* Selected Users Badges */} +
+ {(!selectedAssignees || selectedAssignees.length === 0) && No users selected} + {selectedAssignees?.map(user => ( + user ? ( + + {user.fullName || user.email || 'Unknown User'} + + + ) : null + ))} +
+ + {/* User Search Combobox */} + + + + + + + + + No users found. + + {availableUsers + ?.filter(user => user && !selectedAssignees.some(selected => selected.id === user.id)) + .map((user) => ( + { + setSelectedAssignees([...selectedAssignees, user]); + setOpenUserSelect(false); + }} + className="cursor-pointer" + > + +
+ {user.fullName || 'Unknown Name'} + + {user.email || 'No Email'} • { + (typeof user.role === 'object' && user.role !== null) + ? (user.role as any).roleName + : (user.role || 'No Role') + } + +
+
+ ))} +
+
+
+
+
+

Use the search to find and add multiple interviewers/assignees.

+