From d919e925c8b1339c8f3c587592a60013c9be06d4 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Mon, 23 Feb 2026 19:09:47 +0530 Subject: [PATCH] enhanced the worknote section implemented websocket and in-app notifucation also aaudit tril api integrated --- package-lock.json | 88 ++- package.json | 1 + src/App.tsx | 205 ++--- src/api/API.ts | 6 + src/components/admin/QuestionnaireBuilder.tsx | 40 +- .../applications/ApplicationDetails.tsx | 93 ++- src/components/applications/WorkNotesPage.tsx | 707 ++++++++++++++---- src/components/layout/Header.tsx | 145 ++-- src/context/SocketContext.tsx | 70 ++ src/services/audit.service.ts | 35 + src/services/notification.service.ts | 38 + src/services/worknote.service.ts | 36 + src/styles/globals.css | 22 + 13 files changed, 1165 insertions(+), 321 deletions(-) create mode 100644 src/context/SocketContext.tsx create mode 100644 src/services/audit.service.ts create mode 100644 src/services/notification.service.ts create mode 100644 src/services/worknote.service.ts diff --git a/package-lock.json b/package-lock.json index 150ad9a..1ec1279 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "react-resizable-panels": "^2.0.12", "react-router-dom": "^6.22.3", "recharts": "^2.12.2", + "socket.io-client": "^4.8.3", "sonner": "^1.4.3", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", @@ -2986,6 +2987,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -4227,7 +4234,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4338,6 +4344,28 @@ "embla-carousel": "8.6.0" } }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -5575,7 +5603,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -6316,6 +6343,34 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -6815,6 +6870,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 20752ea..e94754f 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-resizable-panels": "^2.0.12", "react-router-dom": "^6.22.3", "recharts": "^2.12.2", + "socket.io-client": "^4.8.3", "sonner": "^1.4.3", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", diff --git a/src/App.tsx b/src/App.tsx index b17098b..afe1d00 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,16 +35,17 @@ import { ConstitutionalChangePage } from './components/applications/Constitution import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails'; import { RelocationRequestPage } from './components/applications/RelocationRequestPage'; import { RelocationRequestDetails } from './components/applications/RelocationRequestDetails'; -import { WorknotePage } from './components/applications/WorknotePage'; import { DealerResignationPage } from './components/dealer/DealerResignationPage'; import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage'; import { DealerRelocationPage } from './components/dealer/DealerRelocationPage'; import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder'; import QuestionnaireList from './components/admin/QuestionnaireList'; +import { WorkNotesPage } from './components/applications/WorkNotesPage'; import { Toaster } from './components/ui/sonner'; import { User } from './lib/mock-data'; import { toast } from 'sonner'; import { API } from './api/API'; +import { SocketProvider } from './context/SocketContext'; // Layout Component const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => { @@ -174,119 +175,129 @@ export default function App() { // Protected Routes return ( - - {/* Prospective Dealer Route - STRICTLY ISOLATED */} - - + + + {/* Prospective Dealer Route - STRICTLY ISOLATED */} + + + + } + /> + + {/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */} + + - } - /> + }> + } /> - {/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */} - - - - }> - } /> + {/* Dashboards */} + navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : + currentUser?.role === 'Dealer' ? + navigate(`/${path}`)} /> : + navigate(`/${path}`)} /> + } /> - {/* Dashboards */} - navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : - currentUser?.role === 'Dealer' ? - navigate(`/${path}`)} /> : - navigate(`/${path}`)} /> - } /> + {/* Applications */} + navigate(`/applications/${id}`)} initialFilter="all" />} /> + } /> + window.history.back()} + /> + } /> - {/* Applications */} - navigate(`/applications/${id}`)} initialFilter="all" />} /> - navigate('/applications')} />} /> + navigate(`/applications/${id}`)} initialFilter="all" /> : + } /> - navigate(`/applications/${id}`)} initialFilter="all" /> : - } /> + {/* Admin/Lead Routes */} + navigate(`/applications/${id}`)} />} /> + navigate(`/applications/${id}`)} />} /> - {/* Admin/Lead Routes */} - navigate(`/applications/${id}`)} />} /> - navigate(`/applications/${id}`)} />} /> + {/* Other Modules */} + } /> + } /> + } /> + } /> + } /> + } /> - {/* Other Modules */} - } /> - } /> - } /> - } /> - } /> - } /> + {/* HR/Finance Modules (Simplified for brevity, following pattern) */} + navigate(`/resignation/${id}`)} />} /> + navigate('/resignation')} currentUser={currentUser} />} /> - {/* HR/Finance Modules (Simplified for brevity, following pattern) */} - navigate(`/resignation/${id}`)} />} /> - navigate('/resignation')} currentUser={currentUser} />} /> + navigate(`/termination/${id}`)} />} /> + navigate('/termination')} currentUser={currentUser} />} /> - navigate(`/termination/${id}`)} />} /> - navigate('/termination')} currentUser={currentUser} />} /> + navigate(`/fnf/${id}`)} />} /> + navigate('/fnf')} currentUser={currentUser} />} /> - navigate(`/fnf/${id}`)} />} /> - navigate('/fnf')} currentUser={currentUser} />} /> + navigate(`/finance-onboarding/${id}`)} />} /> + navigate('/finance-onboarding')} />} /> - navigate(`/finance-onboarding/${id}`)} />} /> - navigate('/finance-onboarding')} />} /> + navigate(`/finance-fnf/${id}`)} />} /> + navigate('/finance-fnf')} />} /> - navigate(`/finance-fnf/${id}`)} />} /> - navigate('/finance-fnf')} />} /> + navigate(`/constitutional-change/${id}`)} />} /> + navigate('/constitutional-change')} currentUser={currentUser} onOpenWorknote={() => { }} />} /> - navigate(`/constitutional-change/${id}`)} />} /> - navigate('/constitutional-change')} currentUser={currentUser} onOpenWorknote={() => { }} />} /> + navigate(`/relocation-requests/${id}`)} />} /> + navigate('/relocation-requests')} currentUser={currentUser} onOpenWorknote={() => { }} />} /> - navigate(`/relocation-requests/${id}`)} />} /> - navigate('/relocation-requests')} currentUser={currentUser} onOpenWorknote={() => { }} />} /> + {/* Dealer Routes */} + navigate(`/resignation/${id}`)} />} /> + navigate(`/constitutional-change/${id}`)} />} /> + navigate(`/relocation-requests/${id}`)} />} /> - {/* Dealer Routes */} - navigate(`/resignation/${id}`)} />} /> - navigate(`/constitutional-change/${id}`)} />} /> - navigate(`/relocation-requests/${id}`)} />} /> - - {/* Placeholder Routes */} - -

My Tasks

-

Task management interface would be displayed here

-

Shows applications assigned to the current user

- - } /> - -

Reports & Analytics

-

Advanced reporting and analytics dashboard

-

Charts, export capabilities, and custom filters

- - } /> - -

Settings

-
-
-

Profile Settings

-

Update your profile information and preferences

-
-
-

Notification Preferences

-

Configure email and system notifications

-
-
-

Security

-

Change password and manage security settings

+ {/* Placeholder Routes */} + +

My Tasks

+

Task management interface would be displayed here

+

Shows applications assigned to the current user

+
+ } /> + +

Reports & Analytics

+

Advanced reporting and analytics dashboard

+

Charts, export capabilities, and custom filters

+
+ } /> + +

Settings

+
+
+

Profile Settings

+

Update your profile information and preferences

+
+
+

Notification Preferences

+

Configure email and system notifications

+
+
+

Security

+

Change password and manage security settings

+
- - } /> + } /> - {/* Fallback */} - } /> - -
+ {/* Fallback */} + } /> + +
+ ); } diff --git a/src/api/API.ts b/src/api/API.ts index 8a9869c..087ea57 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -77,6 +77,12 @@ export const API = { deleteEmailTemplate: (id: string) => client.delete(`/admin/email-templates/${id}`), previewEmailTemplate: (data: any) => client.post('/admin/email-templates/preview', data), + // Audit Trail + getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) => + client.get('/audit/logs', { entityType, entityId, page, limit }), + getAuditSummary: (entityType: string, entityId: string) => + client.get('/audit/summary', { entityType, entityId }), + // 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/admin/QuestionnaireBuilder.tsx b/src/components/admin/QuestionnaireBuilder.tsx index ef21d0a..ee174cd 100644 --- a/src/components/admin/QuestionnaireBuilder.tsx +++ b/src/components/admin/QuestionnaireBuilder.tsx @@ -104,10 +104,22 @@ const QuestionnaireBuilder: React.FC = () => { const updateQuestion = (index: number, field: keyof Question, value: any) => { const newQuestions = [...questions]; + // Weight validation and option score capping + if (field === 'weight') { + const newWeight = parseFloat(value) || 0; + // Cap existing option scores if weight is decreased + if (newQuestions[index].options) { + newQuestions[index].options = newQuestions[index].options!.map(opt => ({ + ...opt, + score: Math.min(opt.score, newWeight) + })); + } + } + // 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: 'Yes', score: newQuestions[index].weight || 5 }, { text: 'No', score: 0 } ]; } else if (field === 'inputType' && value === 'select' && (!newQuestions[index].options)) { @@ -128,9 +140,15 @@ const QuestionnaireBuilder: React.FC = () => { const updateOption = (questionIndex: number, optionIndex: number, field: 'text' | 'score', value: any) => { const newQuestions = [...questions]; if (newQuestions[questionIndex].options) { + let finalValue = value; + if (field === 'score') { + const maxWeight = newQuestions[questionIndex].weight || 0; + finalValue = Math.min(parseFloat(value) || 0, maxWeight); + } + newQuestions[questionIndex].options![optionIndex] = { ...newQuestions[questionIndex].options![optionIndex], - [field]: value + [field]: finalValue }; setQuestions(newQuestions); } @@ -150,6 +168,18 @@ const QuestionnaireBuilder: React.FC = () => { return; } + // Validate option scores vs weight + for (let i = 0; i < questions.length; i++) { + const q = questions[i]; + if (q.inputType === 'select' || q.inputType === 'yesno') { + const invalidOption = q.options?.find(opt => opt.score > q.weight); + if (invalidOption) { + toast.error(`Question ${i + 1}: Option "${invalidOption.text}" score (${invalidOption.score}) exceeds question weightage (${q.weight})`); + return; + } + } + } + if (totalWeight !== 100) { toast.error(`Total weightage must be exactly 100. Current total: ${totalWeight}`); return; @@ -320,8 +350,10 @@ const QuestionnaireBuilder: React.FC = () => { 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" + max={q.weight} + min={0} + onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)} + className={`w-20 border ${opt.score > q.weight ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none`} /> + ) : ( + + + {file.fileName} + + )} + + ); + })} + + )} + ); })} - {notes.length === 0 && ( + {notes.length === 0 && !isLoading && (

No messages yet

Start the conversation by sending a message below

)} + + {isLoading && ( +
+ Loading notes... +
+ )} {/* Input Area */}
-
- {/* Mention Suggestions */} - {showMentionSuggestions && filteredParticipants.length > 0 && ( -
- {filteredParticipants.map((participant) => ( - - ))} +
+ + {/* Attachment Previews */} + {attachedFiles.length > 0 && ( +
+ {attachedFiles.map(file => { + const isPreviewable = file.mimeType.startsWith('image/') || file.mimeType === 'application/pdf'; + return ( +
+
+ {getFileIcon(file.mimeType, file.filePath)} +
+
+

isPreviewable && setPreviewFile(file)} + > + {file.fileName} +

+ +
+
+ ); + })}
)} - {/* Input Field */} -
- - - - - +
+ {/* Mention Suggestions moved inside relative container */} + {showMentionSuggestions && filteredParticipants.length > 0 && ( +
+ {filteredParticipants.map((participant) => ( + + ))} +
+ )} +
+ + + +
+
+ {COMMON_EMOJIS.map(emoji => ( + + ))} +
+
+ )} + + +
-

- Press Enter to send โ€ข Use @ to mention someone +

+ Press Enter to send โ€ข Use @ to mention someone โ€ข {isUploading ? 'Uploading files...' : 'Files attached appear above'}

+ + {/* Preview Modal */} + !open && setPreviewFile(null)}> + + + + {previewFile && getFileIcon(previewFile.mimeType)} + {previewFile?.fileName} + + +
+ {previewFile?.mimeType.startsWith('image/') ? ( + Preview + ) : previewFile?.mimeType === 'application/pdf' ? ( +