From 0ff5412d042d0ba0cf54e172e0881098d58e0f8a Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Tue, 17 Feb 2026 20:42:28 +0530 Subject: [PATCH] enhanced questionnarier and request detail screen --- src/api/API.ts | 12 ++ .../applications/ApplicationDetails.tsx | 153 ++++++++++++++++-- src/components/applications/WorkNotesPage.tsx | 120 +++++++------- src/components/dealer/QuestionnaireForm.tsx | 17 +- src/lib/mock-data.ts | 13 ++ src/services/onboarding.service.ts | 27 ++++ 6 files changed, 265 insertions(+), 77 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index 03f2cb5..0e67935 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -39,6 +39,18 @@ export const API = { getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth submitPublicResponse: (data: any) => axios.post('http://localhost:5000/api/questionnaire/public/submit', data), + // Assessment & Interviews + getAiSummary: (appId: string) => client.get(`/assessment/ai-summary/${appId}`), + scheduleInterview: (data: any) => client.post('/assessment/interviews', data), + updateInterview: (id: string, data: any) => client.put(`/assessment/interviews/${id}`, data), + submitEvaluation: (id: string, data: any) => client.post(`/assessment/interviews/${id}/evaluation`, data), + + // Collaboration & Participants + getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }), + addWorknote: (data: any) => client.post('/collaboration/worknotes', data), + addParticipant: (data: any) => client.post('/collaboration/participants', data), + removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`), + // User management routes getUsers: () => client.get('/admin/users'), createUser: (data: any) => client.post('/admin/users', data), diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index 5ebbeab..823da9f 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -37,7 +37,7 @@ import { Progress } from '../ui/progress'; import { Textarea } from '../ui/textarea'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog'; import { ScrollArea } from '../ui/scroll-area'; import { Table, @@ -152,6 +152,7 @@ export function ApplicationDetails() { loaDate: getStageDate('LOA'), eorCompleteDate: getStageDate('EOR Complete'), inaugurationDate: getStageDate('Inauguration'), + participants: data.participants || [], }; setApplication(mappedApp); } catch (error) { @@ -175,6 +176,7 @@ export function ApplicationDetails() { const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); const [showDocumentsModal, setShowDocumentsModal] = useState(false); + const [showAssignModal, setShowAssignModal] = useState(false); const [selectedStage, setSelectedStage] = useState(null); const [interviewMode, setInterviewMode] = useState('virtual'); const [approvalRemark, setApprovalRemark] = useState(''); @@ -184,6 +186,25 @@ export function ApplicationDetails() { 'architectural-work': true, 'statutory-documents': true }); + const [users, setUsers] = useState([]); + const [selectedUser, setSelectedUser] = useState(''); + const [participantType, setParticipantType] = useState('contributor'); + const [interviewDate, setInterviewDate] = useState(''); + const [interviewType, setInterviewType] = useState('level1'); + const [meetingLink, setMeetingLink] = useState(''); + const [location, setLocation] = useState(''); + + useEffect(() => { + const fetchUsers = async () => { + try { + const response = await onboardingService.getUsers(); + setUsers(response || []); + } catch (error) { + console.error('Failed to fetch users', error); + } + }; + fetchUsers(); + }, []); if (loading) { return
Loading application details...
; @@ -575,6 +596,50 @@ export function ApplicationDetails() { setWorkNote(''); }; + const handleAddParticipant = async () => { + if (!selectedUser) { + alert('Please select a user'); + return; + } + try { + await onboardingService.addParticipant({ + requestId: applicationId, + requestType: 'application', + userId: selectedUser, + participantType + }); + alert('User assigned successfully!'); + // Refresh application data + const data = await onboardingService.getApplicationById(applicationId); + setApplication({ ...application, participants: data.participants || [] }); + setSelectedUser(''); + setShowAssignModal(false); + } catch (error) { + alert('Failed to assign user'); + } + }; + + const handleScheduleInterview = async () => { + if (!interviewDate) { + alert('Please select date and time'); + return; + } + try { + await onboardingService.scheduleInterview({ + applicationId, + level: interviewType, + scheduledAt: interviewDate, + type: interviewMode, + location: interviewMode === 'virtual' ? meetingLink : location, + participants: [] // Optional: add multi-participant support if needed + }); + alert('Interview scheduled successfully!'); + setShowScheduleModal(false); + } catch (error) { + alert('Failed to schedule interview'); + } + }; + // If Work Notes page is open, show that instead if (showWorkNotesPage) { return ( @@ -584,6 +649,7 @@ export function ApplicationDetails() { registrationNumber={application.registrationNumber} onBack={() => setShowWorkNotesPage(false)} initialNotes={mockWorkNotes} + participants={application.participants} /> ); } @@ -1232,10 +1298,55 @@ export function ApplicationDetails() { )} - + + + + + + + Assign User to Application + + Select a user and their role for this application. + + +
+
+ + +
+
+ + +
+ +
+
+
)} @@ -1411,7 +1522,7 @@ export function ApplicationDetails() {
- @@ -1436,22 +1547,33 @@ export function ApplicationDetails() {
- -
-
- - + setInterviewDate(e.target.value)} + />
{interviewMode === 'virtual' && (
- + setMeetingLink(e.target.value)} + />
)} {interviewMode === 'physical' && (
- + setLocation(e.target.value)} + />
)}
@@ -1464,10 +1586,7 @@ export function ApplicationDetails() { diff --git a/src/components/applications/WorkNotesPage.tsx b/src/components/applications/WorkNotesPage.tsx index 8a08d2f..405294b 100644 --- a/src/components/applications/WorkNotesPage.tsx +++ b/src/components/applications/WorkNotesPage.tsx @@ -4,15 +4,15 @@ import { Input } from '../ui/input'; import { ScrollArea } from '../ui/scroll-area'; import { Avatar, AvatarFallback } from '../ui/avatar'; import { Badge } from '../ui/badge'; -import { - ArrowLeft, - Send, - Paperclip, +import { + ArrowLeft, + Send, + Paperclip, Smile, Image as ImageIcon, MessageSquare } from 'lucide-react'; -import { WorkNote } from '../../lib/mock-data'; +import { WorkNote, Participant } from '../../lib/mock-data'; interface WorkNotesPageProps { applicationId: string; @@ -20,20 +20,23 @@ interface WorkNotesPageProps { registrationNumber: string; onBack: () => void; initialNotes?: WorkNote[]; + participants?: Participant[]; } -interface Participant { +// This interface defines the structure for participants displayed in the UI +interface ParticipantUI { name: string; initials: string; color: string; } -export function WorkNotesPage({ - applicationId, +export function WorkNotesPage({ + applicationId, applicationName, registrationNumber, onBack, - initialNotes = [] + initialNotes = [], + participants: externalParticipants = [] }: WorkNotesPageProps) { const [notes, setNotes] = useState(initialNotes); const [message, setMessage] = useState(''); @@ -43,13 +46,40 @@ export function WorkNotesPage({ const inputRef = useRef(null); const scrollRef = useRef(null); - // Mock participants for @mentions - const participants: Participant[] = [ - { name: 'Sarah Chen', initials: 'SC', color: 'bg-green-600' }, - { name: 'Lisa Wong', initials: 'LW', color: 'bg-blue-600' }, - { name: 'Mark Johnson', initials: 'MJ', color: 'bg-purple-600' }, - { name: 'Anjali Sharma', initials: 'AS', color: 'bg-amber-600' }, - ]; + const getInitials = (name: string) => { + return name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .substring(0, 2); + }; + + const getAvatarColor = (name: string) => { + const colors = [ + 'bg-green-600', + 'bg-blue-600', + 'bg-purple-600', + 'bg-amber-600', + 'bg-pink-600', + 'bg-indigo-600', + 'bg-teal-600', + ]; + const index = name.length % colors.length; + return colors[index]; + }; + + // Map backend participants to the UI sub-format + const participantsList: ParticipantUI[] = externalParticipants.map(p => ({ + name: p.user?.name || 'Unknown User', + initials: getInitials(p.user?.name || 'U'), + color: getAvatarColor(p.user?.name || 'U') + })); + + // Fallback to current user if no participants + if (participantsList.length === 0) { + participantsList.push({ name: 'System User', initials: 'SU', color: 'bg-slate-600' }); + } // Scroll to bottom when new messages arrive useEffect(() => { @@ -61,14 +91,14 @@ export function WorkNotesPage({ const handleInputChange = (e: React.ChangeEvent) => { const value = e.target.value; const cursorPos = e.target.selectionStart || 0; - + setMessage(value); setCursorPosition(cursorPos); // Check if user is typing @ mention const textBeforeCursor = value.substring(0, cursorPos); const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); - + if (lastAtSymbol !== -1 && lastAtSymbol === textBeforeCursor.length - 1) { setShowMentionSuggestions(true); setMentionQuery(''); @@ -89,12 +119,12 @@ export function WorkNotesPage({ const textBeforeCursor = message.substring(0, cursorPosition); const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); const textAfterCursor = message.substring(cursorPosition); - - const newMessage = - message.substring(0, lastAtSymbol) + - `@${name} ` + + + const newMessage = + message.substring(0, lastAtSymbol) + + `@${name} ` + textAfterCursor; - + setMessage(newMessage); setShowMentionSuggestions(false); inputRef.current?.focus(); @@ -139,7 +169,7 @@ export function WorkNotesPage({ const renderMessageWithMentions = (text: string) => { const parts = text.split(/(@\w+\s*\w*)/g); - + return parts.map((part, index) => { if (part.startsWith('@')) { return ( @@ -152,30 +182,8 @@ export function WorkNotesPage({ }); }; - const getInitials = (name: string) => { - return name - .split(' ') - .map(n => n[0]) - .join('') - .toUpperCase() - .substring(0, 2); - }; - const getAvatarColor = (name: string) => { - const colors = [ - 'bg-green-600', - 'bg-blue-600', - 'bg-purple-600', - 'bg-amber-600', - 'bg-pink-600', - 'bg-indigo-600', - 'bg-teal-600', - ]; - const index = name.length % colors.length; - return colors[index]; - }; - - const filteredParticipants = participants.filter(p => + const filteredParticipants = participantsList.filter(p => p.name.toLowerCase().includes(mentionQuery.toLowerCase()) ); @@ -185,19 +193,19 @@ export function WorkNotesPage({
- - +
- +

Work Notes

@@ -210,8 +218,8 @@ export function WorkNotesPage({ {/* Participant Avatars */}
- {participants.slice(0, 3).map((participant, index) => ( - ( + @@ -220,9 +228,9 @@ export function WorkNotesPage({ ))} - {participants.length > 3 && ( + {participantsList.length > 3 && (
- +{participants.length - 3} + +{participantsList.length - 3}
)}
@@ -265,7 +273,7 @@ export function WorkNotesPage({
)} - +

{renderMessageWithMentions(note.message)} diff --git a/src/components/dealer/QuestionnaireForm.tsx b/src/components/dealer/QuestionnaireForm.tsx index 552e794..8468bad 100644 --- a/src/components/dealer/QuestionnaireForm.tsx +++ b/src/components/dealer/QuestionnaireForm.tsx @@ -6,7 +6,7 @@ interface Question { id: string; sectionName: string; questionText: string; - inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq'; + inputType: 'text' | 'yesno' | 'file' | 'number' | 'select' | 'mcq' | 'radio' | 'textarea' | 'email'; options?: any; questionOptions?: any[]; // From backend inclusion weight: number; @@ -158,9 +158,9 @@ const QuestionnaireForm: React.FC = ({ {q.questionText} {q.isMandatory && !readOnly && *} - {q.inputType === 'text' && ( + {(q.inputType === 'text' || q.inputType === 'email') && ( handleInputChange(q.id, e.target.value)} value={responses[q.id] || ''} @@ -168,6 +168,15 @@ const QuestionnaireForm: React.FC = ({ /> )} + {q.inputType === 'textarea' && ( +