From 3208a1ea7fbef402eb139d2fe38bc67a1fc736bd Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Thu, 19 Feb 2026 20:44:21 +0530 Subject: [PATCH] progress bar started inlining with the stage status and action buttons visibility enhanced --- src/api/API.ts | 10 + .../applications/ApplicationDetails.tsx | 1320 ++++++++++------- .../applications/ApplicationsPage.tsx | 9 +- src/components/applications/MasterPage.tsx | 540 +++++-- src/services/master.service.ts | 26 + src/services/onboarding.service.ts | 29 +- 6 files changed, 1263 insertions(+), 671 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index c6ee256..8a9869c 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -53,6 +53,8 @@ export const API = { submitKTMatrix: (data: any) => client.post('/assessment/kt-matrix', data), submitLevel2Feedback: (data: any) => client.post('/assessment/level2-feedback', data), getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`), + updateRecommendation: (data: any) => client.post('/assessment/recommendation', data), + updateInterviewDecision: (data: any) => client.post('/assessment/decision', data), // Collaboration & Participants getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }), @@ -67,6 +69,14 @@ export const API = { updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data), deleteUser: (id: string) => client.delete(`/admin/users/${id}`), + // Email Templates + getEmailTemplates: () => client.get('/admin/email-templates'), + getEmailTemplate: (id: string) => client.get(`/admin/email-templates/${id}`), + createEmailTemplate: (data: any) => client.post('/admin/email-templates', data), + updateEmailTemplate: (id: string, data: any) => client.put(`/admin/email-templates/${id}`, data), + deleteEmailTemplate: (id: string) => client.delete(`/admin/email-templates/${id}`), + previewEmailTemplate: (data: any) => client.post('/admin/email-templates/preview', data), + // Prospective Login sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }), verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }), diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index f91cb40..8cbfeb3 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -1,11 +1,13 @@ import { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; -import { mockApplications, mockAuditLogs, mockDocuments, mockWorkNotes, mockQuestionnaireResponses, Application, ApplicationStatus } from '../../lib/mock-data'; +import { mockAuditLogs, mockDocuments, mockWorkNotes, Application, ApplicationStatus } from '../../lib/mock-data'; import { onboardingService } from '../../services/onboarding.service'; import { WorkNotesPage } from './WorkNotesPage'; -import QuestionnaireForm from '../dealer/QuestionnaireForm'; import QuestionnaireResponseView from './QuestionnaireResponseView'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store'; + import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; @@ -27,7 +29,6 @@ import { GraduationCap, Bike, Award, - AlertCircle, ClipboardList, ChevronDown, ChevronRight, @@ -236,6 +237,7 @@ const KT_MATRIX_CRITERIA = [ export function ApplicationDetails() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const { user: currentUser } = useSelector((state: RootState) => state.auth); const applicationId = id || ''; const onBack = () => navigate(-1); // const application = mockApplications.find(app => app.id === applicationId); @@ -347,8 +349,10 @@ export function ApplicationDetails() { const [location, setLocation] = useState(''); const [documents, setDocuments] = useState([]); const [showUploadModal, setShowUploadModal] = useState(false); + const [showUploadForm, setShowUploadForm] = useState(false); // Toggle for upload view const [uploadFile, setUploadFile] = useState(null); const [uploadDocType, setUploadDocType] = useState(''); + const [approvalFile, setApprovalFile] = useState(null); // State for approval modal file const [isUploading, setIsUploading] = useState(false); const [selectedInterviewerId, setSelectedInterviewerId] = useState(''); const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]); @@ -356,10 +360,11 @@ export function ApplicationDetails() { const [isScheduling, setIsScheduling] = useState(false); // KT Matrix State - const [selectedInterviewIdForFeedback, setSelectedInterviewIdForFeedback] = useState(''); const [ktMatrixScores, setKtMatrixScores] = useState>({}); const [ktMatrixRemarks, setKtMatrixRemarks] = useState(''); const [isSubmittingKT, setIsSubmittingKT] = useState(false); + const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); + const handleKTMatrixChange = (criterionName: string, score: number) => { setKtMatrixScores(prev => ({ @@ -385,7 +390,7 @@ export function ApplicationDetails() { } // Use the selected interview ID or fallback (though UI now forces selection) - const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id; + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id; if (!interviewId) { alert('No active interview found to link this KT Matrix to.'); @@ -415,6 +420,7 @@ export function ApplicationDetails() { // Reset form setKtMatrixScores({}); setKtMatrixRemarks(''); + await fetchInterviews(); // Silent refresh } catch (error) { toast.error('Failed to submit KT Matrix'); } finally { @@ -440,12 +446,12 @@ export function ApplicationDetails() { }; const handleSubmitLevel2Feedback = async () => { - if (!level2Feedback.recommendation || !level2Feedback.overallScore) { - alert('Please provide an overall score and recommendation.'); + if (!level2Feedback.overallScore) { + alert('Please provide an overall score.'); return; } - const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id; + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id; if (!interviewId) { alert('No active Level 2 interview found to link this feedback to.'); @@ -513,12 +519,12 @@ export function ApplicationDetails() { }; const handleSubmitLevel3Feedback = async () => { - if (!level3Feedback.recommendation || !level3Feedback.overallScore) { - alert('Please provide an overall score and recommendation.'); + if (!level3Feedback.overallScore) { + alert('Please provide an overall score.'); return; } - const interviewId = selectedInterviewIdForFeedback || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id; + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id; if (!interviewId) { alert('No active Level 3 interview found to link this feedback to.'); @@ -594,7 +600,8 @@ export function ApplicationDetails() { const handleAddInterviewer = () => { if (!selectedInterviewerId) return; - const userToAdd = users.find(u => u.id === selectedInterviewerId); + const usersList = Array.isArray(users) ? users : []; + const userToAdd = usersList.find(u => u.id === selectedInterviewerId); if (userToAdd && !scheduledInterviewParticipants.find(p => p.id === userToAdd.id)) { setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]); setSelectedInterviewerId(''); @@ -606,7 +613,7 @@ export function ApplicationDetails() { }; useEffect(() => { - if (activeTab === 'documents' && applicationId) { + if ((activeTab === 'documents' || activeTab === 'progress') && applicationId) { const fetchDocuments = async () => { try { const docs = await onboardingService.getDocuments(applicationId); @@ -621,16 +628,95 @@ export function ApplicationDetails() { useEffect(() => { const fetchUsers = async () => { + // Only fetch users if user has admin/DD roles to avoid 403s + if (!currentUser || !['DD Admin', 'Super Admin'].includes(currentUser.role)) { + return; + } try { const response = await onboardingService.getUsers(); - setUsers(response || []); + if (Array.isArray(response)) { + setUsers(response); + } else if (response && Array.isArray(response.data)) { + setUsers(response.data); + } else if (response && Array.isArray(response.users)) { + setUsers(response.users); + } else { + console.warn('Unexpected users response:', response); + setUsers([]); + } } catch (error) { console.error('Failed to fetch users', error); + setUsers([]); } }; fetchUsers(); }, []); + const handleScheduleInterview = async () => { + if (!interviewDate) { + alert('Please select date and time'); + return; + } + try { + setIsScheduling(true); + + const payload = { + applicationId: application?.id, + level: interviewType, + scheduledAt: interviewDate, + type: interviewMode, // 'virtual' | 'physical' + location: interviewMode === 'physical' ? location : meetingLink, + participants: scheduledInterviewParticipants.map(p => p.id) + }; + + await onboardingService.scheduleInterview(payload); + toast.success('Interview scheduled successfully'); + setShowScheduleModal(false); + // Refresh interviews + fetchInterviews(); + fetchApplication(); // Refresh application status + + } catch (error) { + toast.error('Failed to schedule interview'); + console.error(error); + } finally { + setIsScheduling(false); + } + }; + + const handleUpload = async () => { + if (!uploadFile || !uploadDocType) { + alert('Please select a file and document type'); + return; + } + + try { + setIsUploading(true); + const formData = new FormData(); + formData.append('file', uploadFile); + formData.append('documentType', uploadDocType); + if (selectedStage) { + formData.append('stage', selectedStage); + } + + await onboardingService.uploadDocument(applicationId!, formData); + + alert('Document uploaded successfully'); + setShowUploadModal(false); + setUploadFile(null); + setUploadDocType(''); + + // Refresh documents + const docs = await onboardingService.getDocuments(applicationId); + setDocuments(docs || []); + } catch (error) { + console.error('Upload failed', error); + alert('Failed to upload document'); + } finally { + setIsUploading(false); + } + }; + if (loading) { return
Loading application details...
; } @@ -890,123 +976,125 @@ export function ApplicationDetails() { const eorProgress = (eorChecklist.filter(item => item.completed).length / eorChecklist.length) * 100; - // Mock stage-specific documents - const stageDocuments: { [key: string]: typeof mockDocuments } = { - 'Submitted': [ - { id: 'd1', name: 'Application Form.pdf', type: 'PDF', uploadDate: '2025-10-01', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd2', name: 'Aadhaar Card.pdf', type: 'PDF', uploadDate: '2025-10-01', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd3', name: 'PAN Card.pdf', type: 'PDF', uploadDate: '2025-10-01', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Shortlist': [ - { id: 'd4', name: 'Business Plan.pdf', type: 'PDF', uploadDate: '2025-10-04', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd5', name: 'Financial Projections.xlsx', type: 'Excel', uploadDate: '2025-10-04', status: 'Verified', uploader: 'Amit Sharma' } - ], - '1st Level Interview': [ - { id: 'd6', name: 'Level 1 Evaluation Sheet.pdf', type: 'PDF', uploadDate: '2025-10-05', status: 'Verified', uploader: 'DD-ZM' } - ], - '2nd Level Interview': [ - { id: 'd7', name: 'Level 2 Evaluation Sheet.pdf', type: 'PDF', uploadDate: '2025-10-07', status: 'Verified', uploader: 'DD Lead' } - ], - '3rd Level Interview': [ - { id: 'd8', name: 'Level 3 Evaluation Sheet.pdf', type: 'PDF', uploadDate: '2025-10-09', status: 'Verified', uploader: 'NBH' }, - { id: 'd9', name: 'Final Interview Notes.pdf', type: 'PDF', uploadDate: '2025-10-09', status: 'Verified', uploader: 'DD-Head' } - ], - 'FDD': [ - { id: 'd10', name: 'Bank Statements.pdf', type: 'PDF', uploadDate: '2025-10-10', status: 'Verified', uploader: 'FDD Team' }, - { id: 'd11', name: 'Income Tax Returns.pdf', type: 'PDF', uploadDate: '2025-10-10', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd12', name: 'Credit Report.pdf', type: 'PDF', uploadDate: '2025-10-10', status: 'Verified', uploader: 'FDD Team' }, - { id: 'd13', name: 'Property Documents.pdf', type: 'PDF', uploadDate: '2025-10-10', status: 'Pending', uploader: 'Amit Sharma' }, - { id: 'd14', name: 'Business Valuation.pdf', type: 'PDF', uploadDate: '2025-10-10', status: 'Verified', uploader: 'FDD Team' } - ], - 'LOI Approval': [ - { id: 'd15', name: 'LOI Approval Document.pdf', type: 'PDF', uploadDate: '2025-10-11', status: 'Verified', uploader: 'DD Admin' } - ], - 'Security Details': [ - { id: 'd16', name: 'Police Verification.pdf', type: 'PDF', uploadDate: '2025-10-12', status: 'Verified', uploader: 'Security Team' }, - { id: 'd17', name: 'Background Check Report.pdf', type: 'PDF', uploadDate: '2025-10-12', status: 'Verified', uploader: 'Security Team' }, - { id: 'd18', name: 'Character Certificate.pdf', type: 'PDF', uploadDate: '2025-10-12', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'LOI Issue': [ - { id: 'd19', name: 'Letter of Intent.pdf', type: 'PDF', uploadDate: '2025-10-13', status: 'Verified', uploader: 'DD Admin' } - ], - 'Architectural Document Upload': [ - { id: 'd20', name: 'Site Plan.dwg', type: 'CAD', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd21', name: 'Floor Plan.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd22', name: 'Elevation Design.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd23', name: '3D Renders.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd24', name: 'Structural Design.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd25', name: 'Electrical Layout.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd26', name: 'Plumbing Layout.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd27', name: 'HVAC Design.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Architecture Team' } - ], - 'Architecture Team Completion': [ - { id: 'd28', name: 'Final Approval Certificate.pdf', type: 'PDF', uploadDate: '2025-10-15', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd29', name: 'As-Built Drawings.pdf', type: 'PDF', uploadDate: '2025-10-15', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd30', name: 'Compliance Certificate.pdf', type: 'PDF', uploadDate: '2025-10-15', status: 'Verified', uploader: 'Architecture Team' }, - { id: 'd31', name: 'Quality Assurance Report.pdf', type: 'PDF', uploadDate: '2025-10-15', status: 'Verified', uploader: 'Architecture Team' } - ], - 'GST': [ - { id: 'd32', name: 'GST Certificate.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'PAN': [ - { id: 'd33', name: 'PAN Card.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Nodal Agreement': [ - { id: 'd34', name: 'Nodal Agreement.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Cancelled Check': [ - { id: 'd35', name: 'Cancelled Cheque.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Partnership Deed/LLP/MOA/AOA/COI': [ - { id: 'd36', name: 'Partnership Deed.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd37', name: 'MOA.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Firm Registration Certificate': [ - { id: 'd38', name: 'Firm Registration.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Rental agreement/ Lease agreement / Own/ Land agreement': [ - { id: 'd39', name: 'Lease Agreement.pdf', type: 'PDF', uploadDate: '2025-10-14', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'LOI Acknowledgement Copy': [ - { id: 'd40', name: 'LOI Acknowledgement.pdf', type: 'PDF', uploadDate: '2025-10-15', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'LOA': [ - { id: 'd41', name: 'Letter of Authorization.pdf', type: 'PDF', uploadDate: '2025-10-16', status: 'Verified', uploader: 'DD Admin' } - ], - 'EOR Complete': [ - { id: 'd42', name: 'EOR Completion Certificate.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'DD Admin' }, - { id: 'd43', name: 'Training Completion Certificates.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'Training Team' }, - { id: 'd44', name: 'Infrastructure Photos.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd45', name: 'Trade License.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd46', name: 'Insurance Policy.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'Amit Sharma' }, - { id: 'd47', name: 'Vendor Agreements.pdf', type: 'PDF', uploadDate: '2025-10-17', status: 'Verified', uploader: 'Amit Sharma' } - ], - 'Inauguration': [ - { id: 'd48', name: 'Inauguration Invitation.pdf', type: 'PDF', uploadDate: '2025-10-18', status: 'Verified', uploader: 'DD Admin' }, - { id: 'd49', name: 'Event Photos.pdf', type: 'PDF', uploadDate: '2025-10-18', status: 'Verified', uploader: 'DD Admin' } - ] - }; + const getDocumentsForStage = (stageName: string) => { - return stageDocuments[stageName] || []; + return documents.filter(doc => + doc.stage === stageName || + (!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0])) + ); }; - const handleApprove = () => { + const handleApprove = async () => { + // Check if user has an active interview to approve + const activeInterview = interviews.find(i => + i.status !== 'Completed' && i.status !== 'Cancelled' && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + + // Handle File Upload if exists + if (approvalFile && applicationId) { + try { + const formData = new FormData(); + formData.append('file', approvalFile); + formData.append('documentType', 'Approval Attachment'); + + // Determine stage based on active interview + let stageName = null; + if (activeInterview) { + if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview'; + else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview'; + else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview'; + } + + // Fallback for document stage if it's a general approval + if (!stageName) { + if (application.status === 'Shortlisted' || application.status === 'Level 1 Pending') stageName = '1st Level Interview'; + else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Pending') stageName = '2nd Level Interview'; + else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Pending') stageName = '3rd Level Interview'; + } + + if (stageName) { + formData.append('stage', stageName); + } + + await onboardingService.uploadDocument(applicationId, formData); + toast.success('Document uploaded with approval'); + } catch (error) { + console.error('Failed to upload approval document', error); + toast.error('Failed to upload document'); + } + } + + if (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, + decision: 'Approved', + remarks: approvalRemark + }); + toast.success('Interview approved successfully'); + setShowApproveModal(false); + setApprovalRemark(''); + setApprovalFile(null); // Reset file + fetchInterviews(); + // Refresh application to check if status updated + if (id) { + const appData = await onboardingService.getApplicationById(id); + setApplication(appData); + } + return; + } catch (error) { + toast.error('Failed to approve interview'); + return; + } + } + if (!approvalRemark.trim()) { alert('Please enter a remark'); return; } - alert(`Application ${application.registrationNumber} approved!\nRemark: ${approvalRemark}`); + // Application level approval (mock for now) + alert(`Application ${application?.registrationNumber} approved!\nRemark: ${approvalRemark}`); setShowApproveModal(false); setApprovalRemark(''); + setApprovalFile(null); // Reset file }; - const handleReject = () => { + const handleReject = async () => { + // Check if user has an active interview to reject + const activeInterview = interviews.find(i => + i.status !== 'Completed' && i.status !== 'Cancelled' && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + + if (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, + decision: 'Rejected', + remarks: rejectionReason + }); + toast.success('Interview rejected'); + setShowRejectModal(false); + setRejectionReason(''); + fetchInterviews(); + if (id) { + const appData = await onboardingService.getApplicationById(id); + setApplication(appData); + } + return; + } catch (error) { + toast.error('Failed to reject interview'); + return; + } + } + if (!rejectionReason.trim()) { alert('Please enter a reason for rejection'); return; } - alert(`Application ${application.registrationNumber} rejected!\nReason: ${rejectionReason}`); + alert(`Application ${application?.registrationNumber} rejected!\nReason: ${rejectionReason}`); setShowRejectModal(false); setRejectionReason(''); }; @@ -1044,65 +1132,45 @@ export function ApplicationDetails() { } }; - const handleScheduleInterview = async () => { - if (!interviewDate) { - alert('Please select date and time'); - return; - } - try { - setIsScheduling(true); - await onboardingService.scheduleInterview({ - applicationId, - level: interviewType, - scheduledAt: interviewDate, - type: interviewMode, - location: interviewMode === 'physical' ? location : meetingLink, - participants: scheduledInterviewParticipants.map(u => u.id) - }); - toast.success('Interview scheduled successfully'); - setShowScheduleModal(false); - setScheduledInterviewParticipants([]); // Reset - setInterviewDate(''); - setMeetingLink(''); - setLocation(''); - fetchInterviews(); // Refresh list - fetchApplication(); // Refresh application status - } catch (error) { - alert('Failed to schedule interview'); - } finally { - setIsScheduling(false); - } - }; + if (loading) { + return
Loading...
; + } - const handleUpload = async () => { - if (!uploadFile || !uploadDocType) { - alert('Please select a file and document type'); - return; - } + if (!application) { + return
Application not found
; + } - try { - setIsUploading(true); - const formData = new FormData(); - formData.append('file', uploadFile); - formData.append('documentType', uploadDocType); + // Determine if current user has an active interview and if they have submitted feedback + const interviewsList = Array.isArray(interviews) ? interviews : []; - await onboardingService.uploadDocument(applicationId, formData); + // For action buttons, we only care about pending interviews + const activeInterviewForUser = interviewsList.find(i => + ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); - alert('Document uploaded successfully'); - setShowUploadModal(false); - setUploadFile(null); - setUploadDocType(''); + // For checking if a decision was ALREADY made, we look at ANY interview the user participated in for the current level + const lastInterviewForUser = [...interviewsList].reverse().find(i => + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); - // Refresh documents - const docs = await onboardingService.getDocuments(applicationId); - setDocuments(docs || []); - } catch (error) { - console.error('Upload failed', error); - alert('Failed to upload document'); - } finally { - setIsUploading(false); - } - }; + const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( + (e: any) => e.evaluatorId === currentUser?.id + ); + + // Robust checks for feedback and decision + // 1. If there's an active interview, feedback is required before Approve/Reject + // 2. hasMadeDecision should check if the evaluation has a recommendation + const hasSubmittedFeedback = !!currentUserEvaluation; + + // Specific to the current active interview context + const hasSubmittedFeedbackForActive = activeInterviewForUser && hasSubmittedFeedback; + const hasMadeDecisionForUser = currentUserEvaluation?.decision === 'Approved' || + currentUserEvaluation?.decision === 'Rejected' || + currentUserEvaluation?.decision === 'Selected'; // Maintain compatibility if needed + + // Final visibility flags + const shouldShowApproveReject = !hasMadeDecisionForUser && hasSubmittedFeedbackForActive; // If Work Notes page is open, show that instead if (showWorkNotesPage) { @@ -1118,21 +1186,28 @@ export function ApplicationDetails() { ); } + + return (
{/* Header */} -
+
- -
-

{application.name}

-

{application.registrationNumber}

+
+

{application.name}

+

{application.registrationNumber}

+
+ {/* Actions can be added here in the future */} +
+ +
{/* Main Content */}
@@ -1141,8 +1216,8 @@ export function ApplicationDetails() { Applicant Information - -
+ +
@@ -1284,16 +1359,16 @@ export function ApplicationDetails() { {application.isShortlisted !== false && ( - -
- - Questionnaire Response - Progress - Documents - Interviews - EOR Checklist - Payments - Audit Trail + +
+ + Questionnaire + Progress + Documents + Interviews + EOR Checklist + Payments + Audit Trail
@@ -1356,17 +1431,29 @@ export function ApplicationDetails() { Evaluators: {stage.evaluators.join(' + ')}

)} - {stage.documentsUploaded !== undefined && stage.documentsUploaded > 0 && ( -

{ - setSelectedStage(stage.name); - setShowDocumentsModal(true); - }} - > - Documents uploaded = {stage.documentsUploaded} -

- )} + {/* Stage Docs Link */} + {(() => { + const stageDocsCount = documents.filter(doc => + doc.stage === stage.name || + (!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0])) + ).length; + + return ( +
+ +
+ ); + })()} +

{stage.status === 'completed' && stage.date && `Completed: ${new Date(stage.date).toLocaleDateString()}`} {stage.status === 'active' && 'In Progress'} @@ -1418,7 +1505,7 @@ export function ApplicationDetails() { {/* Branch Content - Expandable */} {isExpanded && (

- {branch.stages.map((branchStage, stageIndex) => ( + {branch.stages.map((branchStage) => (
@@ -1444,17 +1531,29 @@ export function ApplicationDetails() { {branchStage.description && (

{branchStage.description}

)} - {branchStage.documentsUploaded !== undefined && branchStage.documentsUploaded > 0 && ( -

{ - setSelectedStage(branchStage.name); - setShowDocumentsModal(true); - }} - > - Documents uploaded = {branchStage.documentsUploaded} -

- )} + + {/* Branch Stage Docs Link */} + {(() => { + const branchDocsCount = documents.filter(doc => + doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) || + doc.stage === branchStage.name + ).length; + + return ( +
+ +
+ ); + })()}

{branchStage.status === 'completed' && branchStage.date && `Completed: ${new Date(branchStage.date).toLocaleDateString()}`} {branchStage.status === 'active' && 'In Progress'} @@ -1483,52 +1582,57 @@ export function ApplicationDetails() {

Uploaded Documents

-
- - - - File Name - Type - Upload Date - Uploader - Actions - - - - {documents.length === 0 ? ( +
+
+ - - No documents uploaded yet - + File Name + Type + Upload Date + Uploader + Actions - ) : ( - documents.map((doc) => ( - - - - {doc.fileName} - - {doc.documentType} - {new Date(doc.createdAt).toLocaleDateString()} - - {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} - - -
- -
+
+ + {documents.length === 0 ? ( + + + No documents uploaded yet - )))} - -
+ ) : ( + documents.map((doc) => ( + + + + {doc.fileName} + + {doc.documentType} + {new Date(doc.createdAt).toLocaleDateString()} + + {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} + + +
+ +
+
+
+ )))} + + +
{/* Interviews Tab */} @@ -1536,58 +1640,60 @@ export function ApplicationDetails() {

Scheduled Interviews

- - - - Level - Date & Time - Type - Location/Link - Status - Scheduled By - - - - {interviews.length === 0 ? ( +
+
+ - - No interviews scheduled yet - + Level + Date & Time + Type + Location/Link + Status + Scheduled By - ) : ( - interviews.map((interview) => ( - - Level {interview.level} - {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} - {interview.interviewType} - - {interview.interviewType === 'virtual' ? ( - - Join Meeting - - ) : ( - interview.linkOrLocation - )} + + + {(!interviews || interviews.length === 0) ? ( + + + No interviews scheduled yet - - - {interview.status} - - - {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} - )) - )} - -
+ ) : ( + (Array.isArray(interviews) ? interviews : []).map((interview) => ( + + Level {interview.level} + {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} + {interview.interviewType} + + {interview.interviewType === 'virtual' ? ( + + Join Meeting + + ) : ( + interview.linkOrLocation + )} + + + + {interview.status} + + + {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} + + )) + )} + + +

Interview Feedback

- {interviews.length === 0 ? ( + {(!interviews || interviews.length === 0) ? (

No interviews scheduled.

) : ( - interviews.map((interview) => ( + (Array.isArray(interviews) ? interviews : []).map((interview) => (

Level {interview.level} Interview @@ -1624,8 +1730,24 @@ export function ApplicationDetails() { ) : 'N/A'} - - {evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? ( + + {evalItem.remarks ? ( +
+ {evalItem.remarks} + {evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 && ( + + )} +
+ ) : evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? ( + {/* Show Approve/Reject block */} + {shouldShowApproveReject && ( + <> + - + + + )} + + {(hasMadeDecisionForUser) && ( +
+ You have {currentUserEvaluation?.recommendation === 'Approved' ? 'Approved' : 'Rejected'} +
+ )} @@ -1821,43 +1954,42 @@ export function ApplicationDetails() { Work Note - + {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && ( + + )} - - - - - - {interviews.length === 0 ? ( - No interviews scheduled - ) : ( - interviews.map(interview => ( - { - setSelectedInterviewIdForFeedback(interview.id); // Storing interview ID here - if (interview.level === 1) setShowKTMatrixModal(true); - else if (interview.level === 2) setShowLevel2FeedbackModal(true); - else setShowLevel3FeedbackModal(true); - }} - > - Level {interview.level} - {interview.interviewType} - - )) - )} - - + {/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */} + {activeInterviewForUser && !hasSubmittedFeedback && ( + + + + + + { + setSelectedInterviewForFeedback(activeInterviewForUser); + if (activeInterviewForUser.level === 1) setShowKTMatrixModal(true); + else if (activeInterviewForUser.level === 2) setShowLevel2FeedbackModal(true); + else setShowLevel3FeedbackModal(true); + }} + > + Level {activeInterviewForUser.level} - {activeInterviewForUser.interviewType} + + + + )} {application.status === 'Questionnaire Pending' && ( <> @@ -1873,55 +2005,57 @@ export function ApplicationDetails() { )} - - - - - - - Assign User to Application - - Select a user and their role for this application. - - -
-
- - -
-
- - -
- -
-
-
+ + + + Assign User to Application + + Select a user and their role for this application. + + +
+
+ + +
+
+ + +
+ +
+
+ + )} )} @@ -1929,38 +2063,40 @@ export function ApplicationDetails() { {/* Work Notes Chat */} {/* Only show Work Notes card for shortlisted applications (opportunity requests and regular dealership requests) */} {/* Hide Work Notes for unopportunity requests (lead generation) - no workflow tracking needed */} - {application.isShortlisted !== false && ( - - - Work Notes - - - -
- {mockWorkNotes.map((note) => ( -
-
- {note.user.charAt(0)} -
-
-
-

{note.user}

- {note.timestamp} + { + application.isShortlisted !== false && ( + + + Work Notes + + + +
+ {mockWorkNotes.map((note) => ( +
+
+ {note.user.charAt(0)} +
+
+
+

{note.user}

+ {note.timestamp} +
+

{note.message}

-

{note.message}

-
- ))} -
- - - - )} -
-
+ ))} +
+
+
+
+ ) + } +

+
{/* Approve Modal */} - + < Dialog open={showApproveModal} onOpenChange={setShowApproveModal} > Approve Application @@ -1981,7 +2117,11 @@ export function ApplicationDetails() {
- + setApprovalFile(e.target.files ? e.target.files[0] : null)} + />
- + {/* Reject Modal */} - + < Dialog open={showRejectModal} onOpenChange={setShowRejectModal} > Reject Application @@ -2040,10 +2180,10 @@ export function ApplicationDetails() {
- + {/* Work Note Modal */} - + < Dialog open={showWorkNoteModal} onOpenChange={setShowWorkNoteModal} > Add Work Note @@ -2083,10 +2223,10 @@ export function ApplicationDetails() {
- + {/* Schedule Interview Modal */} - + < Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal} > Schedule Interview @@ -2212,66 +2352,12 @@ export function ApplicationDetails() {
- + + - {/* Upload Document Modal */} - - - - Upload Document - - Select the document type and upload the file. - - -
-
- - -
-
- - setUploadFile(e.target.files ? e.target.files[0] : null)} - /> -
-
- - -
-
-
-
{/* KT Matrix Modal */} - + < Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal} > Fill KT Matrix @@ -2351,10 +2437,10 @@ export function ApplicationDetails() {
- + {/* Level 2 Feedback Modal */} - + < Dialog open={showLevel2FeedbackModal} onOpenChange={setShowLevel2FeedbackModal} > Level 2 Interview Feedback @@ -2449,22 +2535,7 @@ export function ApplicationDetails() { />
-
- - -
+
@@ -2495,10 +2566,10 @@ export function ApplicationDetails() {
- + {/* Feedback Details Modal */} - + < Dialog open={showFeedbackDetailsModal} onOpenChange={setShowFeedbackDetailsModal} > Interview Feedback Details @@ -2555,10 +2626,10 @@ export function ApplicationDetails() {
)} - + {/* Level 3 Feedback Modal */} - + < Dialog open={showLevel3FeedbackModal} onOpenChange={setShowLevel3FeedbackModal} > Level 3 Interview Feedback @@ -2710,85 +2781,186 @@ export function ApplicationDetails() { - + {/* Documents Modal */} - - - - Documents - {selectedStage} - + { + setShowDocumentsModal(open); + if (!open) setShowUploadForm(false); + }} + > + + + + + Documents - {selectedStage || 'General'} + + View and manage documents uploaded for this stage. -
- {selectedStage && getDocumentsForStage(selectedStage).length > 0 ? ( - - - - Document Name - Type - Upload Date - Uploaded By - Actions - - - - {getDocumentsForStage(selectedStage).map((doc) => ( - - -
- - {doc.name} -
-
- - {doc.type} - - {new Date(doc.uploadDate).toLocaleDateString()} - {doc.uploader} - - - -
- ))} -
-
- ) : ( -
- -

No Documents

-

No documents have been uploaded for this stage yet.

+ + {!showUploadForm ? ( +
+ {getDocumentsForStage(selectedStage || '').length > 0 ? ( +
+ + + + Document Name + Type + Upload Date + Uploaded By + Actions + + + + {getDocumentsForStage(selectedStage || '').map((doc) => ( + + +
+ + {doc.fileName} +
+
+ + + {doc.documentType?.toLowerCase() || 'Other'} + + + + {new Date(doc.createdAt).toLocaleDateString('en-GB')} + + + {doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')} + + + + +
+ ))} +
+
+
+ ) : ( +
+
+ +
+

No Documents Found

+

No documents have been uploaded for this stage yet.

+
+ )} + +
+ +
- )} - - - -
- -
-
+ ) : ( +
+
+
+
+ + +
+
+ + +
+
+
+ + setUploadFile(e.target.files ? e.target.files[0] : null)} + /> +
+
+ +
+ + +
+
+ )} -
+
); } diff --git a/src/components/applications/ApplicationsPage.tsx b/src/components/applications/ApplicationsPage.tsx index 9812c19..8848b14 100644 --- a/src/components/applications/ApplicationsPage.tsx +++ b/src/components/applications/ApplicationsPage.tsx @@ -84,7 +84,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP 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 + isShortlisted: app.ddLeadShortlisted || app.isShortlisted || false, // Use actual backend flags // Add other fields to match interface companyName: app.companyName, source: app.source, @@ -119,13 +119,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter; const matchesStatus = statusFilter === 'all' || app.status === statusFilter; - const isShortlisted = app.isShortlisted === true; // Only show shortlisted applications - const notExcluded = !excludedApplicationIds.includes(app.id); // Exclude APP-005, 006, 007, 008 + const isShortlisted = app.isShortlisted === true; + const isNotQuestionnaireStage = !['Questionnaire Pending', 'Questionnaire Completed', 'Submitted'].includes(app.status); + const notExcluded = !excludedApplicationIds.includes(app.id); // New Filter: My Assignments const matchesAssignment = !showMyAssignments || ((app as any).assignedTo === currentUser?.id); - return matchesSearch && matchesLocation && matchesStatus && isShortlisted && notExcluded && matchesAssignment; + return matchesSearch && matchesLocation && matchesStatus && isShortlisted && isNotQuestionnaireStage && notExcluded && matchesAssignment; }) .sort((a, b) => { if (sortBy === 'date') { diff --git a/src/components/applications/MasterPage.tsx b/src/components/applications/MasterPage.tsx index 76b3858..d4f925a 100644 --- a/src/components/applications/MasterPage.tsx +++ b/src/components/applications/MasterPage.tsx @@ -30,7 +30,11 @@ import { UserCog, Bell, AlertTriangle, - X + X, + Loader2, + Play, + Copy, + Check } from 'lucide-react'; import { toast } from 'sonner'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog'; @@ -140,19 +144,137 @@ interface SLAConfig { interface EmailTemplate { id: string; name: string; + templateCode?: string; subject: string; + body?: string; trigger: string; - lastModified: string; + isActive?: boolean; + createdAt?: string; + updatedAt?: string; } +// Template Scenarios Configuration +const TEMPLATE_SCENARIOS = [ + { + category: "Authentication", + scenarios: [ + { + name: "OTP Verification", + code: "AUTH_OTP", + description: "Sent when a user attempts to login or register.", + variables: ["{{otp}}", "{{expiry_minutes}}"], + sampleSubject: "Your Verification Code", + sampleBody: "Your OTP is {{otp}}. It expires in {{expiry_minutes}} minutes." + }, + { + name: "Welcome Email", + code: "USER_WELCOME", + description: "Sent to new users upon account creation.", + variables: ["{{name}}", "{{email}}", "{{role}}", "{{login_link}}"], + sampleSubject: "Welcome to Dealer Portal", + sampleBody: "Hello {{name}},
Your account has been created as a {{role}}.
Login here: {{login_link}}" + } + ] + }, + { + category: "Dealer Application", + scenarios: [ + { + name: "Application Received", + code: "APPLICATION_RECEIVED", + description: "Sent to applicant after submitting the form.", + variables: ["{{applicant_name}}", "{{application_id}}"], + sampleSubject: "Application Received - {{application_id}}", + sampleBody: "Dear {{applicant_name}},
We have received your application (ID: {{application_id}})." + }, + { + name: "Clarification Requested", + code: "CLARIFICATION_REQUESTED", + description: "Sent when admin requests more info.", + variables: ["{{applicant_name}}", "{{application_id}}", "{{clarification_details}}", "{{link}}"], + sampleSubject: "Action Required: Application Clarification", + sampleBody: "Dear {{applicant_name}},
Please provide the following information: {{clarification_details}}
Link: {{link}}" + }, + { + name: "Application Approved", + code: "APPLICATION_APPROVED", + description: "Sent when application is approved.", + variables: ["{{applicant_name}}", "{{application_id}}"], + sampleSubject: "Congratulations! Application Approved", + sampleBody: "Dear {{applicant_name}},
Your application {{application_id}} is approved." + }, + { + name: "Application Rejected", + code: "APPLICATION_REJECTED", + description: "Sent when application is rejected.", + variables: ["{{applicant_name}}", "{{application_id}}", "{{rejection_reason}}"], + sampleSubject: "Update on your Application", + sampleBody: "Dear {{applicant_name}},
Your application {{application_id}} was not successful due to: {{rejection_reason}}." + } + ] + }, + { + category: "Interview Process", + scenarios: [ + { + name: "Interview Scheduled", + code: "INTERVIEW_SCHEDULED", + description: "Sent when an interview is scheduled.", + variables: ["{{applicant_name}}", "{{round_name}}", "{{interview_date}}", "{{interview_time}}", "{{meeting_link}}", "{{interviewer_name}}"], + sampleSubject: "Interview Scheduled: {{round_name}}", + sampleBody: "Dear {{applicant_name}},
Your {{round_name}} interview is scheduled on {{interview_date}} at {{interview_time}}.
Link: {{meeting_link}}" + }, + { + name: "Interview Feedback", + code: "INTERVIEW_FEEDBACK", + description: "Sent to admin/relevant parties after feedback.", + variables: ["{{applicant_name}}", "{{round_name}}", "{{status}}", "{{score}}"], + sampleSubject: "Interview Feedback Submitted", + sampleBody: "Feedback for {{applicant_name}} ({{round_name}}): Status {{status}}, Score {{score}}." + } + ] + }, + { + category: "Onboarding & Documents", + scenarios: [ + { + name: "LOI Issued", + code: "LOI_ISSUED", + description: "Sent when Letter of Intent is issued.", + variables: ["{{applicant_name}}", "{{loi_amount}}", "{{valid_until}}", "{{payment_link}}"], + sampleSubject: "Letter of Intent Issued", + sampleBody: "Dear {{applicant_name}},
Your LOI is ready. Amount: {{loi_amount}}. Valid until: {{valid_until}}.
Pay here: {{payment_link}}" + }, + { + name: "LOA Issued", + code: "LOA_ISSUED", + description: "Sent when Letter of Acceptance is issued.", + variables: ["{{applicant_name}}", "{{dealer_code}}"], + sampleSubject: "Welcome Aboard! LOA Issued", + sampleBody: "Dear {{applicant_name}},
Welcome! Your Dealer Code is {{dealer_code}}." + } + ] + } +]; + interface Location { id: string; - state: string; - city: string; - district: string; - activeFrom: string; - activeTo: string; - status: 'Active' | 'Inactive'; + state?: string | { stateName: string; id: string }; + city?: string; + areaName: string; + district?: string | { districtName: string; state?: { stateName: string; id: string }; id: string }; + stateId?: string; // Sometimes flattened + districtId?: string; // Sometimes flattened + pincode: string; + activeFrom?: string; + activeTo?: string; + isActive: boolean; + manager?: { + id: string; + fullName: string; + email?: string; + }; + status?: 'Active' | 'Inactive'; // Keep for backward compatibility if needed } interface ZonalManager { @@ -241,6 +363,7 @@ interface UserAssignment { employeeId?: string; status: 'Active' | 'Inactive'; permissions?: string[]; + asmCode?: string; // Added for ASM role tracking } export function MasterPage() { @@ -363,7 +486,7 @@ export function MasterPage() { setLoading(true); try { // Fetch critical data first - const [rolesRes, zonesRes, permsRes, regionsRes, usersRes, statesRes, asmRes] = await Promise.all([ + const [rolesRes, zonesRes, permsRes, regionsRes, usersRes, statesRes, asmRes, emailTemplatesRes] = await Promise.all([ masterService.getRoles().catch(e => ({ success: false, error: e })), masterService.getZones().catch(e => ({ success: false, error: e })), masterService.getPermissions().catch(e => ({ success: false, error: e })), @@ -371,7 +494,8 @@ export function MasterPage() { masterService.getUsers().catch(e => ({ success: false, error: e })), masterService.getStates().catch(e => ({ success: false, error: e })), // Explicitly fetch Area Managers from the new dedicated endpoint - masterService.getAreaManagers().catch(e => ({ success: false, error: e })) + masterService.getAreaManagers().catch(e => ({ success: false, error: e })), + masterService.getEmailTemplates().catch(e => ({ success: false, error: e })) ]); // Fetch extensive data independently so it doesn't block critical UI @@ -523,6 +647,10 @@ export function MasterPage() { }))); } + if (emailTemplatesRes && emailTemplatesRes.success) { + setEmailTemplates(emailTemplatesRes.data); + } + } catch (error) { console.error('Error fetching master data:', error); toast.error('Failed to load configuration data'); @@ -644,16 +772,14 @@ export function MasterPage() { }, ]); - // Mock data for email templates - const [emailTemplates] = useState([ - { id: '1', name: 'Application Received', subject: 'Your Royal Enfield Dealership Application', trigger: 'On submission', lastModified: 'Dec 20, 2024' }, - { id: '2', name: 'Interview Scheduled', subject: 'Interview Scheduled - Royal Enfield Dealership', trigger: 'When interview scheduled', lastModified: 'Dec 18, 2024' }, - { id: '3', name: 'Application Approved', subject: 'Congratulations! Your Application is Approved', trigger: 'On approval', lastModified: 'Dec 15, 2024' }, - { id: '4', name: 'Application Rejected', subject: 'Update on Your Dealership Application', trigger: 'On rejection', lastModified: 'Dec 15, 2024' }, - { id: '5', name: 'Document Required', subject: 'Additional Documents Required', trigger: 'When documents requested', lastModified: 'Dec 10, 2024' }, - { id: '6', name: 'Payment Reminder', subject: 'Payment Reminder - Royal Enfield Dealership', trigger: 'Payment pending', lastModified: 'Dec 12, 2024' }, - { id: '7', name: 'SLA Breach Warning', subject: 'Action Required - Application Pending', trigger: 'Before SLA breach', lastModified: 'Dec 08, 2024' }, - ]); + // Email Template State + const [emailTemplates, setEmailTemplates] = useState([]); + const [testDataInput, setTestDataInput] = useState('{}'); + const [previewContent, setPreviewContent] = useState<{ subject: string, html: string } | null>(null); + const [previewLoading, setPreviewLoading] = useState(false); + const [editingTemplate, setEditingTemplate] = useState(null); + + // ... (keep existing handleSaveRole) @@ -726,9 +852,103 @@ export function MasterPage() { setSlaEscalations([]); }; - const handleSaveTemplate = () => { - toast.success('Email template saved successfully!'); - setShowTemplateDialog(false); + // Email Template Handlers + const handlePreviewTemplate = async () => { + try { + setPreviewLoading(true); + setPreviewContent(null); + + let parsedData = {}; + try { + parsedData = JSON.parse(testDataInput); + } catch (e) { + toast.error("Please enter valid JSON for test data"); + setPreviewLoading(false); + return; + } + + const response = await masterService.previewEmailTemplate({ + subject: editingTemplate?.subject, + body: editingTemplate?.body, + data: parsedData + }) as any; + + if (response.success && response.data) { + setPreviewContent(response.data); + } else { + toast.error(response.message || "Could not generate preview"); + } + } catch (error) { + console.error('Preview error:', error); + toast.error("Failed to generate preview"); + } finally { + setPreviewLoading(false); + } + }; + + const handleSaveTemplate = async () => { + try { + if (!editingTemplate?.name || !editingTemplate?.templateCode || !editingTemplate?.subject || !editingTemplate?.body) { + toast.error("Please fill in all required fields (Name, Code, Subject, Body)"); + return; + } + + const templateData = { + ...editingTemplate, + isActive: editingTemplate.isActive ?? true // Default to true if undefined + }; + + if (editingTemplate.id) { + // Update + const response = await masterService.updateEmailTemplate(editingTemplate.id, templateData) as any; + if (response.success) { + toast.success("Template updated successfully"); + setShowTemplateDialog(false); + // Refresh list + const listRes = await masterService.getEmailTemplates() as any; + if (listRes.success) setEmailTemplates(listRes.data); + } else { + toast.error(response.message || "Failed to update template"); + } + } else { + // Create + const response = await masterService.createEmailTemplate(templateData) as any; + if (response.success) { + toast.success("Template created successfully"); + setShowTemplateDialog(false); + const listRes = await masterService.getEmailTemplates() as any; + if (listRes.success) setEmailTemplates(listRes.data); + } else { + toast.error(response.message || "Failed to create template"); + } + } + } catch (error) { + console.error('Save template error:', error); + toast.error("Failed to save template"); + } + }; + + const handleEditTemplate = (template: EmailTemplate) => { + setEditingTemplate(template); + setShowTemplateDialog(true); + setPreviewContent(null); + setTestDataInput('{}'); + }; + + const handleDeleteTemplate = async (id: string) => { + if (!confirm('Are you sure you want to delete this template?')) return; + try { + const response = await masterService.deleteEmailTemplate(id); + if (response.success) { + toast.success("Template deleted"); + const listRes = await masterService.getEmailTemplates(); + if (listRes.success) setEmailTemplates(listRes.data); + } else { + toast.error(response.message || "Failed to delete template"); + } + } catch (error) { + toast.error("Failed to delete template"); + } }; const handleSaveLocation = async () => { @@ -1942,56 +2162,100 @@ export function MasterPage() {
- {template.name} + {template.name || template.templateCode}
- {template.subject} + {template.subject} - {template.trigger} + {template.templateCode || '-'} + + + {template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : '-'} - {template.lastModified}
- -
))} + {emailTemplates.length === 0 && ( + + + No templates found. Create one to get started. + + + )} - {/* Template Variables Card */} + {/* Template Scenarios Reference */} - Available Variables - Use these variables in your templates + Template Reference Guide + + Standard codes and available variables for system scenarios. + Use these codes to ensure the system sends the correct email. + -
- {[ - '{{applicant_name}}', - '{{application_id}}', - '{{location}}', - '{{interview_date}}', - '{{interview_time}}', - '{{reviewer_name}}', - '{{status}}', - '{{reason}}', - '{{payment_amount}}', - '{{due_date}}', - '{{company_name}}', - '{{support_email}}' - ].map((variable) => ( - - {variable} - +
+ {TEMPLATE_SCENARIOS.map((category) => ( +
+

+ {category.category} +

+
+ {category.scenarios.map((scenario) => ( +
+
+
+

{scenario.name}

+
+ + {scenario.code} + + +
+
+
+

{scenario.description}

+
+ {scenario.variables.map((v) => ( + { + navigator.clipboard.writeText(v); + toast.success("Variable copied!"); + }} + > + {v} + + ))} +
+
+ ))} +
+
))}
@@ -2456,59 +2720,151 @@ export function MasterPage() { - {/* Add/Edit Template Dialog */} - + - Email Template - Configure automated email template + {editingTemplate?.id ? 'Edit Email Template' : 'Add Email Template'} + Configure automated email template with dynamic content -
-
- - -
-
- - -
-
- - { + const scenario = TEMPLATE_SCENARIOS.flatMap(c => c.scenarios).find(s => s.code === code); + if (scenario) { + setEditingTemplate({ + ...editingTemplate!, + name: scenario.name, + templateCode: scenario.code, + subject: scenario.sampleSubject, + body: scenario.sampleBody + }); + // Also set sample test data + const sampleJson = scenario.variables.reduce((acc, v) => { + const key = v.replace(/{{|}}/g, ''); + acc[key] = `Test ${key}`; + return acc; + }, {} as any); + setTestDataInput(JSON.stringify(sampleJson, null, 2)); + toast.success(`Loaded template for ${scenario.name}`); + } + }}> + + - On Application Submission - On Approval - On Rejection - Interview Scheduled - Document Request - Payment Required + {TEMPLATE_SCENARIOS.map((category) => ( +
+ + {category.category} + + {category.scenarios.map((scenario) => ( + + {scenario.name} ({scenario.code}) + + ))} +
+ ))}
-
- -