From 99a59ac05b17eb6eb3a75ac1b92126a99b1569e3 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 21 Nov 2025 18:41:07 +0530 Subject: [PATCH] export enhanced code cleaning and with some new bug fixes --- README.md | 411 +++++++++++++++--- fix-imports.ps1 | 19 - migrate-files.ps1 | 53 --- src/App.tsx | 6 +- src/components/Auth/AuthDebugInfo.tsx | 9 +- .../admin/AnalyticsConfig/AnalyticsConfig.tsx | 1 - src/components/admin/ConfigurationManager.tsx | 6 +- .../admin/DashboardConfig/DashboardConfig.tsx | 1 - src/components/admin/HolidayManager.tsx | 16 +- .../NotificationConfig/NotificationConfig.tsx | 1 - .../admin/SharingConfig/SharingConfig.tsx | 1 - .../admin/UserRoleManager/UserRoleManager.tsx | 11 - .../common/Pagination/Pagination.tsx | 2 +- src/components/dashboard/KPICard/KPICard.tsx | 37 +- .../layout/PageLayout/PageLayout.tsx | 8 - .../AddApproverModal/AddApproverModal.tsx | 3 - .../AddSpectatorModal/AddSpectatorModal.tsx | 9 +- .../sla/SLAProgressBar/SLAProgressBar.tsx | 3 +- .../workNote/WorkNoteChat/WorkNoteChat.tsx | 176 +++----- .../WorkNoteChat/WorkNoteChatSimple.tsx | 5 - .../ApprovalWorkflow/ApprovalStepCard.tsx | 12 +- .../ClaimManagementDetail.tsx | 7 +- .../CreateRequest/ApprovalWorkflowStep.tsx | 4 +- .../workflow/CreateRequest/DocumentsStep.tsx | 2 +- .../CreateRequest/ParticipantsStep.tsx | 3 +- .../CreateRequest/ReviewSubmitStep.tsx | 1 - .../CreateRequest/TemplateSelectionStep.tsx | 2 +- src/contexts/AuthContext.tsx | 60 +-- src/hooks/useConclusionRemark.ts | 1 - src/hooks/useCreateRequestForm.ts | 10 +- src/hooks/useDocumentManagement.ts | 2 +- src/hooks/useDocumentUpload.ts | 4 +- src/hooks/useRequestDetails.ts | 12 +- src/hooks/useRequestSocket.ts | 6 +- src/main.tsx | 7 - .../ApproverPerformanceActionsStats.tsx | 11 +- .../types/approverPerformance.types.ts | 2 +- .../utils/statsCalculations.ts | 8 + src/pages/Auth/Auth.tsx | 15 - src/pages/Auth/AuthenticatedApp.tsx | 40 +- src/pages/ClosedRequests/ClosedRequests.tsx | 4 +- .../components/ClosedRequestCard.tsx | 6 +- .../components/ClosedRequestsFilters.tsx | 6 - .../components/ClosedRequestsPagination.tsx | 2 +- .../ClosedRequests/hooks/useClosedRequests.ts | 36 +- .../types/closedRequests.types.ts | 2 +- .../ClosedRequests/utils/configMappers.ts | 8 - .../utils/requestTransformers.ts | 2 +- .../hooks/useCreateRequestHandlers.ts | 2 +- src/pages/Dashboard/Dashboard.tsx | 6 +- .../sections/AdminAnalyticsSection.tsx | 3 +- .../components/sections/AdminKPICards.tsx | 66 +-- .../sections/CriticalAlertsSection.tsx | 3 +- .../sections/RecentActivitySection.tsx | 2 +- .../components/sections/TATBreachReport.tsx | 3 +- .../components/sections/UserKPICards.tsx | 56 ++- .../components/types/dashboard.types.ts | 1 + src/pages/Dashboard/hooks/useDashboardData.ts | 3 +- src/pages/Dashboard/types/dashboard.types.ts | 5 +- .../Dashboard/utils/dashboardCalculations.ts | 14 +- .../Dashboard/utils/dashboardNavigation.ts | 10 +- .../sections/RequestLifecycleReport.tsx | 2 +- src/pages/DetailedReports/utils/csvExports.ts | 209 +++++++-- .../MyRequests/components/RequestCard.tsx | 5 +- src/pages/Notifications/Notifications.tsx | 2 - src/pages/OpenRequests/OpenRequests.tsx | 101 ++--- src/pages/RequestDetail/RequestDetail.tsx | 36 +- .../components/QuickActionsSidebar.tsx | 116 ++--- .../components/tabs/OverviewTab.tsx | 2 +- .../components/tabs/WorkNotesTab.tsx | 3 + .../components/tabs/WorkflowTab.tsx | 45 +- src/pages/Requests/components/RequestCard.tsx | 5 +- src/services/adminApi.ts | 12 +- src/services/authApi.ts | 28 +- src/services/configService.ts | 2 - src/styles/globals.css | 10 + src/utils/dateFormatter.ts | 41 ++ src/utils/slaTracker.ts | 53 ++- src/utils/socket.ts | 9 +- src/utils/tokenManager.ts | 20 +- vite.config.ts | 3 + 81 files changed, 1177 insertions(+), 754 deletions(-) delete mode 100644 fix-imports.ps1 delete mode 100644 migrate-files.ps1 diff --git a/README.md b/README.md index 509aa7c..d6ca6c9 100644 --- a/README.md +++ b/README.md @@ -4,43 +4,92 @@ A modern, enterprise-grade approval and request management system built with Rea ## ๐Ÿ“‹ Table of Contents -- [Features](#features) -- [Tech Stack](#tech-stack) -- [Prerequisites](#prerequisites) -- [Installation](#installation) -- [Development](#development) -- [Project Structure](#project-structure) -- [Available Scripts](#available-scripts) -- [Configuration](#configuration) -- [Contributing](#contributing) +- [Features](#-features) +- [Tech Stack](#๏ธ-tech-stack) +- [Prerequisites](#-prerequisites) +- [Installation](#-installation) +- [Development](#-development) +- [Project Structure](#-project-structure) +- [Available Scripts](#-available-scripts) +- [Configuration](#๏ธ-configuration) +- [Key Features Deep Dive](#-key-features-deep-dive) +- [Troubleshooting](#-troubleshooting) +- [Contributing](#-contributing) ## โœจ Features -- **๐Ÿ”„ Dual Workflow System** - - Custom Request Workflow with user-defined approvers - - Claim Management Workflow (8-step predefined process) - -- **๐Ÿ“Š Comprehensive Dashboard** - - Real-time statistics and metrics - - High-priority alerts - - Recent activity tracking - -- **๐ŸŽฏ Request Management** - - Create, track, and manage approval requests - - Document upload and management - - Work notes and audit trails - - Spectator and stakeholder management - -- **๐ŸŽจ Modern UI/UX** - - Responsive design (mobile, tablet, desktop) - - Dark mode support - - Accessible components (WCAG compliant) - - Royal Enfield brand theming +### ๐Ÿ”„ Dual Workflow System +- **Custom Request Workflow** - User-defined approvers, spectators, and workflow steps +- **Claim Management Workflow** - 8-step predefined process for dealer claim management +- Flexible approval chains with multi-level approvers +- TAT (Turnaround Time) tracking at each approval level -- **๐Ÿ”” Notifications** - - Real-time toast notifications - - SLA tracking and reminders - - Approval status updates +### ๐Ÿ“Š Comprehensive Dashboard +- Real-time statistics and metrics +- High-priority alerts and critical request tracking +- Recent activity feed with pagination +- Upcoming deadlines and SLA breach warnings +- Department-wise performance metrics +- Customizable KPI widgets (Admin only) + +### ๐ŸŽฏ Request Management +- Create, track, and manage approval requests +- Document upload and management with file type validation +- Work notes and comprehensive audit trails +- Spectator and stakeholder management +- Request filtering, search, and export capabilities +- Detailed request lifecycle tracking + +### ๐Ÿ‘ฅ Admin Control Panel +- **User Management** - Search, assign roles (USER, MANAGEMENT, ADMIN), and manage user permissions +- **User Role Management** - Assign and manage user roles with Okta integration +- **System Configuration** - Comprehensive admin settings: + - **KPI Configuration** - Configure dashboard KPIs, visibility, and thresholds + - **Analytics Configuration** - Data retention, export formats, and analytics features + - **TAT Configuration** - Working hours, priority-based TAT, escalation settings + - **Notification Configuration** - Email templates, notification channels, and settings + - **Notification Preferences** - User-configurable notification settings + - **Document Configuration** - File type restrictions, size limits, upload policies + - **Dashboard Configuration** - Customize dashboard layout and widgets per role + - **AI Configuration** - AI provider settings, parameters, and features + - **Sharing Configuration** - Sharing policies and permissions +- **Holiday Management** - Configure business holidays for SLA calculations + +### ๐Ÿ“ˆ Approver Performance Analytics +- Detailed approver performance metrics and statistics +- Request approval history and trends +- Average approval time analysis +- Approval rate and efficiency metrics +- TAT compliance tracking per approver +- Performance comparison and benchmarking +- Export capabilities for performance reports + +### ๐Ÿ’ฌ Real-Time Live Chat (Work Notes) +- **WebSocket Integration** - Real-time bidirectional communication +- **Live Work Notes** - Instant messaging within request context +- **Presence Indicators** - See who's online/offline in real-time +- **Mention System** - @mention participants for notifications +- **File Attachments** - Share documents directly in chat +- **Message History** - Persistent chat history per request +- **Auto-reconnection** - Automatic reconnection on network issues +- **Room-based Communication** - Isolated chat rooms per request + +### ๐Ÿ”” Advanced Notifications +- **Web Push Notifications** - Browser push notifications using VAPID +- **Service Worker Integration** - Background notification delivery +- **Real-time Toast Notifications** - In-app notification system +- **SLA Tracking & Reminders** - Automated TAT breach alerts +- **Approval Status Updates** - Real-time status change notifications +- **Email Notifications** - Configurable email notification channels +- **Notification Preferences** - User-configurable notification settings + +### ๐ŸŽจ Modern UI/UX +- Responsive design (mobile, tablet, desktop) +- Dark mode support +- Accessible components (WCAG compliant) +- Royal Enfield brand theming +- Smooth animations and transitions +- Intuitive navigation and user flows ## ๐Ÿ› ๏ธ Tech Stack @@ -50,8 +99,11 @@ A modern, enterprise-grade approval and request management system built with Rea - **Styling:** Tailwind CSS 3.4+ - **UI Components:** shadcn/ui + Radix UI - **Icons:** Lucide React -- **Notifications:** Sonner -- **State Management:** React Hooks (useState, useMemo) +- **Notifications:** Sonner (Toast) + Web Push API (VAPID) +- **Real-Time Communication:** Socket.IO Client +- **State Management:** React Hooks (useState, useMemo, useContext) +- **Authentication:** Okta SSO Integration +- **HTTP Client:** Axios ## ๐Ÿ“ฆ Prerequisites @@ -74,7 +126,7 @@ A modern, enterprise-grade approval and request management system built with Rea \`\`\`bash git clone -cd Re_Figma_Code +cd Re_Frontend_Code \`\`\` ### 2. Install dependencies @@ -147,11 +199,20 @@ VITE_PUBLIC_VAPID_KEY=your-production-vapid-key | Variable | Description | Required | Default | |----------|-------------|----------|---------| | `VITE_API_BASE_URL` | Backend API base URL (with `/api/v1`) | Yes | `http://localhost:5000/api/v1` | -| `VITE_BASE_URL` | Base URL for direct file access (without `/api/v1`) | Yes | `http://localhost:5000` | +| `VITE_BASE_URL` | Base URL for WebSocket and direct file access (without `/api/v1`) | Yes | `http://localhost:5000` | | `VITE_OKTA_DOMAIN` | Okta domain for SSO authentication | Yes* | - | | `VITE_OKTA_CLIENT_ID` | Okta client ID for authentication | Yes* | - | | `VITE_PUBLIC_VAPID_KEY` | Public VAPID key for web push notifications | No | - | +**Notes:** +- `VITE_BASE_URL` is used for WebSocket connections and must point to the base backend URL (not `/api/v1`) +- `VITE_PUBLIC_VAPID_KEY` is required for web push notifications. Generate using: + \`\`\`bash + npm install -g web-push + web-push generate-vapid-keys + \`\`\` + Use the **public key** in the frontend `.env.local` file + \*Required if using Okta authentication ### 4. Verify setup @@ -213,28 +274,57 @@ Re_Figma_Code/ โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ components/ โ”‚ โ”‚ โ”œโ”€โ”€ ui/ # Reusable UI components (40+) +โ”‚ โ”‚ โ”œโ”€โ”€ admin/ # Admin components +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ AIConfig/ # AI configuration +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ AnalyticsConfig/ # Analytics settings +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DashboardConfig/ # Dashboard customization +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ DocumentConfig/ # Document policies +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ NotificationConfig/ # Notification settings +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ SharingConfig/ # Sharing policies +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ TATConfig/ # TAT configuration +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ UserManagement/ # User management +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ UserRoleManager/ # Role assignment +โ”‚ โ”‚ โ”œโ”€โ”€ approval/ # Approval workflow components +โ”‚ โ”‚ โ”œโ”€โ”€ common/ # Common reusable components +โ”‚ โ”‚ โ”œโ”€โ”€ dashboard/ # Dashboard widgets โ”‚ โ”‚ โ”œโ”€โ”€ modals/ # Modal components -โ”‚ โ”‚ โ”œโ”€โ”€ figma/ # Figma-specific components -โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ Layout.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ ClaimManagementWizard.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ NewRequestWizard.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ RequestDetail.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ ClaimManagementDetail.tsx -โ”‚ โ”‚ โ”œโ”€โ”€ MyRequests.tsx +โ”‚ โ”‚ โ”œโ”€โ”€ participant/ # Participant management +โ”‚ โ”‚ โ”œโ”€โ”€ workflow/ # Workflow components +โ”‚ โ”‚ โ””โ”€โ”€ workNote/ # Work notes/chat components +โ”‚ โ”œโ”€โ”€ pages/ +โ”‚ โ”‚ โ”œโ”€โ”€ Admin/ # Admin control panel +โ”‚ โ”‚ โ”œโ”€โ”€ ApproverPerformance/ # Approver analytics +โ”‚ โ”‚ โ”œโ”€โ”€ Auth/ # Authentication pages +โ”‚ โ”‚ โ”œโ”€โ”€ Dashboard/ # Main dashboard +โ”‚ โ”‚ โ”œโ”€โ”€ RequestDetail/ # Request detail view +โ”‚ โ”‚ โ”œโ”€โ”€ Requests/ # Request listing +โ”‚ โ”‚ โ””โ”€โ”€ ... +โ”‚ โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ”‚ โ”œโ”€โ”€ useRequestSocket.ts # WebSocket integration +โ”‚ โ”‚ โ”œโ”€โ”€ useDocumentUpload.ts # Document management +โ”‚ โ”‚ โ”œโ”€โ”€ useSLATracking.ts # SLA tracking +โ”‚ โ”‚ โ””โ”€โ”€ ... +โ”‚ โ”œโ”€โ”€ services/ # API services +โ”‚ โ”‚ โ”œโ”€โ”€ adminApi.ts # Admin API calls +โ”‚ โ”‚ โ”œโ”€โ”€ authApi.ts # Authentication API +โ”‚ โ”‚ โ”œโ”€โ”€ workflowApi.ts # Workflow API โ”‚ โ”‚ โ””โ”€โ”€ ... โ”‚ โ”œโ”€โ”€ utils/ -โ”‚ โ”‚ โ”œโ”€โ”€ customRequestDatabase.ts -โ”‚ โ”‚ โ”œโ”€โ”€ claimManagementDatabase.ts -โ”‚ โ”‚ โ””โ”€โ”€ dealerDatabase.ts +โ”‚ โ”‚ โ”œโ”€โ”€ socket.ts # Socket.IO utilities +โ”‚ โ”‚ โ”œโ”€โ”€ pushNotifications.ts # Web push notifications +โ”‚ โ”‚ โ”œโ”€โ”€ slaTracker.ts # SLA calculation utilities +โ”‚ โ”‚ โ””โ”€โ”€ ... +โ”‚ โ”œโ”€โ”€ contexts/ +โ”‚ โ”‚ โ””โ”€โ”€ AuthContext.tsx # Authentication context โ”‚ โ”œโ”€โ”€ styles/ โ”‚ โ”‚ โ””โ”€โ”€ globals.css โ”‚ โ”œโ”€โ”€ types/ -โ”‚ โ”‚ โ””โ”€โ”€ index.ts # TypeScript type definitions +โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ App.tsx โ”‚ โ””โ”€โ”€ main.tsx -โ”œโ”€โ”€ public/ # Static assets -โ”œโ”€โ”€ .vscode/ # VS Code settings +โ”œโ”€โ”€ public/ +โ”‚ โ””โ”€โ”€ service-worker.js # Service worker for push notifications +โ”œโ”€โ”€ .vscode/ โ”œโ”€โ”€ index.html โ”œโ”€โ”€ vite.config.ts โ”œโ”€โ”€ tsconfig.json @@ -267,7 +357,7 @@ The project uses path aliases for cleaner imports: \`\`\`typescript import { Button } from '@/components/ui/button'; -import { getDealerInfo } from '@/utils/dealerDatabase'; +import { getSocket } from '@/utils/socket'; \`\`\` Path aliases are configured in: @@ -311,6 +401,7 @@ To connect to the backend API: 1. **Update API base URL** in `.env.local`: \`\`\`env VITE_API_BASE_URL=http://localhost:5000/api/v1 + VITE_BASE_URL=http://localhost:5000 \`\`\` 2. **Configure CORS** in your backend to allow your frontend origin @@ -318,10 +409,24 @@ To connect to the backend API: 3. **Authentication:** - Configure Okta credentials in environment variables - Ensure backend validates JWT tokens from Okta + - Backend should handle token exchange and refresh -4. **API Services:** +4. **WebSocket Configuration:** + - Backend must support Socket.IO on the base URL + - Socket.IO path: `/socket.io` + - CORS must allow WebSocket connections + - Events: `worknote:new`, `presence:join`, `presence:leave`, `request:online-users` + +5. **Web Push Notifications:** + - Backend must support VAPID push notification delivery + - Service worker registration endpoint required + - Push subscription management API needed + - Generate VAPID keys using: `npm install -g web-push && web-push generate-vapid-keys` + +6. **API Services:** - API services are located in `src/services/` - All API calls use `axios` configured with base URL from environment + - WebSocket utilities in `src/utils/socket.ts` ### Development vs Production @@ -329,10 +434,181 @@ To connect to the backend API: - **Production:** Uses `.env.production` or environment variables set in deployment platform - **Never commit:** `.env.local` or `.env.production` (use `.env.example` as template) +## ๐Ÿš€ Key Features Deep Dive + +### ๐Ÿ‘ฅ Admin Control Panel + +The Admin Control Panel (`/admin`) provides comprehensive system management capabilities accessible only to ADMIN role users: + +#### User Management +- **User Search**: Search users via Okta integration with real-time search +- **Role Assignment**: Assign and manage user roles (USER, MANAGEMENT, ADMIN) +- **User Statistics**: View role distribution and user counts +- **Pagination**: Efficient pagination for large user lists +- **Filtering**: Filter users by role (ELEVATED, ADMIN, MANAGEMENT, USER, ALL) + +#### System Configuration Tabs + +1. **KPI Configuration** + - Configure dashboard KPIs with visibility settings per role + - Set alert thresholds and breach conditions + - Organize KPIs by categories (Request Volume, TAT Efficiency, Approver Load, etc.) + - Enable/disable KPIs dynamically + +2. **Analytics Configuration** + - Data retention policies + - Export format settings (CSV, Excel, PDF) + - Analytics feature toggles + - Data collection preferences + +3. **TAT Configuration** + - Working hours configuration (start/end time, days of week) + - Priority-based TAT settings (express, standard, urgent) + - Escalation rules and thresholds + - Holiday calendar integration + +4. **Notification Configuration** + - Email template customization + - Notification channel management (email, push, in-app) + - Notification frequency settings + - Delivery preferences + - **Notification Preferences** - User-configurable notification settings + +5. **Document Configuration** + - Allowed file types and extensions + - File size limits (per file and total) + - Upload validation rules + - Document policy enforcement + +6. **Dashboard Configuration** + - Customize dashboard layout per role + - Widget visibility settings + - Dashboard widget ordering + - Role-specific dashboard views + +7. **AI Configuration** + - AI provider settings (OpenAI, Anthropic, etc.) + - AI parameters and model selection + - AI feature toggles + - API key management + +8. **Sharing Configuration** + - Sharing policies and permissions + - External sharing settings + - Access control rules + +#### Holiday Management +- Configure business holidays for accurate SLA calculations +- Holiday calendar integration +- Regional holiday support + +### ๐Ÿ“ˆ Approver Performance Dashboard + +Comprehensive analytics dashboard (`/approver-performance`) for tracking and analyzing approver performance: + +#### Key Metrics +- **Approval Statistics**: Total approvals, approval rate, average approval time +- **TAT Compliance**: Percentage of approvals within TAT +- **Request History**: Complete list of requests handled by approver +- **Performance Trends**: Time-based performance analysis +- **SLA Metrics**: TAT breach analysis and compliance tracking + +#### Features +- **Advanced Filtering**: Filter by date range, status, priority, SLA compliance +- **Search Functionality**: Search requests by title, ID, or description +- **Export Capabilities**: Export performance data in CSV/Excel formats +- **Visual Analytics**: Charts and graphs for performance visualization +- **Comparison Tools**: Compare performance across different time periods +- **Detailed Request List**: View all requests with approval details + +### ๐Ÿ’ฌ Real-Time Live Chat (Work Notes) + +Powered by Socket.IO for instant, bidirectional communication within request context: + +#### Core Features +- **Real-Time Messaging**: Instant message delivery with Socket.IO +- **Presence Indicators**: See who's online/offline in real-time +- **@Mention System**: Mention participants using @username syntax +- **File Attachments**: Upload and share documents directly in chat +- **Message History**: Persistent chat history per request +- **Rich Text Support**: Format messages with mentions and links + +#### Technical Implementation +- **Socket.IO Client**: Real-time WebSocket communication +- **Room-Based Architecture**: Each request has isolated chat room +- **Auto-Reconnection**: Automatic reconnection on network issues +- **Presence Management**: Real-time user presence tracking +- **Message Persistence**: Messages stored in backend database + +#### Usage +- Access via Request Detail page > Work Notes tab +- Full-screen chat interface available +- Real-time updates for all participants +- Notification on new messages + +### ๐Ÿ”” Web Push Notifications + +Browser push notifications using VAPID (Voluntary Application Server Identification) protocol: + +#### Features +- **Service Worker Integration**: Background notification delivery +- **VAPID Protocol**: Secure, standards-based push notifications +- **Permission Management**: User-friendly permission requests +- **Notification Preferences**: User-configurable notification settings +- **Cross-Platform Support**: Works on desktop and mobile browsers +- **Offline Queue**: Notifications queued when browser is offline + +#### Configuration +1. Generate VAPID keys (public/private pair) +2. Set `VITE_PUBLIC_VAPID_KEY` in environment variables +3. Backend must support VAPID push notification delivery +4. Service worker automatically registers on app load + +#### Notification Types +- **SLA Alerts**: TAT breach and approaching deadline notifications +- **Approval Requests**: New requests assigned to approver +- **Status Updates**: Request status change notifications +- **Work Note Mentions**: Notifications when mentioned in chat +- **System Alerts**: Critical system notifications + +#### Browser Support +- Chrome/Edge: Full support +- Firefox: Full support +- Safari: Limited support (macOS/iOS) +- Requires HTTPS in production + ## ๐Ÿ”ง Troubleshooting ### Common Issues +#### WebSocket Connection Issues + +If real-time features (chat, presence) are not working: + +1. Verify backend Socket.IO server is running +2. Check `VITE_BASE_URL` in `.env.local` (should not include `/api/v1`) +3. Ensure CORS allows WebSocket connections +4. Check browser console for Socket.IO connection errors +5. Verify Socket.IO path is `/socket.io` (default) + +\`\`\`bash +# Test WebSocket connection +# Open browser console and check for Socket.IO connection logs +\`\`\` + +#### Web Push Notifications Not Working + +1. Ensure `VITE_PUBLIC_VAPID_KEY` is set in `.env.local` +2. Verify service worker is registered (check Application tab in DevTools) +3. Check browser notification permissions +4. Ensure HTTPS in production (required for push notifications) +5. Verify backend push notification endpoint is configured + +\`\`\`bash +# Check service worker registration +# Open DevTools > Application > Service Workers +\`\`\` + #### Port Already in Use If the default port (5173) is in use: @@ -381,6 +657,35 @@ npm run type-check - Verify all environment variables are set correctly - Ensure Node.js and npm versions meet requirements - Review backend logs for API-related issues +- Check Network tab for WebSocket connection status +- Verify service worker registration in DevTools > Application + +## ๐Ÿ” Role-Based Access Control + +The application supports three user roles with different access levels: + +### USER Role +- Create and manage own requests +- View assigned requests +- Approve/reject requests assigned to them +- Add work notes and comments +- Upload documents + +### MANAGEMENT Role +- All USER permissions +- View all requests across the organization +- Access to detailed reports and analytics +- Approver Performance dashboard access +- Export capabilities + +### ADMIN Role +- All MANAGEMENT permissions +- Access to Admin Control Panel (`/admin`) +- User management and role assignment +- System configuration (TAT, Notifications, Documents, etc.) +- KPI configuration and dashboard customization +- Holiday calendar management +- Full system administration capabilities ## ๐Ÿงช Testing (Future Enhancement) diff --git a/fix-imports.ps1 b/fix-imports.ps1 deleted file mode 100644 index 0280550..0000000 --- a/fix-imports.ps1 +++ /dev/null @@ -1,19 +0,0 @@ -# Fix all imports with version numbers -$files = Get-ChildItem -Path "src" -Filter "*.tsx" -Recurse - -foreach ($file in $files) { - $content = Get-Content $file.FullName -Raw - - # Remove version numbers from ALL package imports (universal pattern) - # Matches: package-name@version, @scope/package-name@version - $content = $content -replace '(from\s+[''"])([^''"]+)@[\d.]+([''"])', '$1$2$3' - $content = $content -replace '(import\s+[''"])([^''"]+)@[\d.]+([''"])', '$1$2$3' - - # Also fix motion/react to framer-motion - $content = $content -replace 'motion/react', 'framer-motion' - - Set-Content -Path $file.FullName -Value $content -NoNewline -} - -Write-Host "Fixed all imports!" -ForegroundColor Green - diff --git a/migrate-files.ps1 b/migrate-files.ps1 deleted file mode 100644 index 5d184e0..0000000 --- a/migrate-files.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -# PowerShell script to migrate files to src directory -# Run this script: .\migrate-files.ps1 - -Write-Host "Starting file migration to src directory..." -ForegroundColor Green - -# Check if src directory exists -if (-not (Test-Path "src")) { - Write-Host "Creating src directory..." -ForegroundColor Yellow - New-Item -ItemType Directory -Path "src" -Force | Out-Null -} - -# Function to move files with checks -function Move-WithCheck { - param($Source, $Destination) - - if (Test-Path $Source) { - Write-Host "Moving $Source to $Destination..." -ForegroundColor Cyan - Move-Item -Path $Source -Destination $Destination -Force - Write-Host "Moved $Source" -ForegroundColor Green - } else { - Write-Host "$Source not found, skipping..." -ForegroundColor Yellow - } -} - -# Move App.tsx -if (Test-Path "App.tsx") { - Move-WithCheck "App.tsx" "src\App.tsx" -} - -# Move components directory -if (Test-Path "components") { - Move-WithCheck "components" "src\components" -} - -# Move utils directory -if (Test-Path "utils") { - Move-WithCheck "utils" "src\utils" -} - -# Move styles directory -if (Test-Path "styles") { - Move-WithCheck "styles" "src\styles" -} - -Write-Host "" -Write-Host "Migration complete!" -ForegroundColor Green -Write-Host "" -Write-Host "Next steps:" -ForegroundColor Yellow -Write-Host "1. Update imports in src/App.tsx to use '@/' aliases" -ForegroundColor White -Write-Host "2. Fix the sonner import: import { toast } from 'sonner';" -ForegroundColor White -Write-Host "3. Run: npm run dev" -ForegroundColor White -Write-Host "" -Write-Host "See MIGRATION_GUIDE.md for detailed instructions" -ForegroundColor Cyan diff --git a/src/App.tsx b/src/App.tsx index 837de33..8d816b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -213,7 +213,6 @@ function AppRoutes({ onLogout }: AppProps) { // Add to dynamic requests setDynamicRequests([...dynamicRequests, newCustomRequest]); - console.log('New custom request created:', newCustomRequest); navigate('/my-requests'); toast.success('Request Submitted Successfully!', { description: `Your request "${requestData.title}" (${requestId}) has been created and sent for approval.`, @@ -221,7 +220,7 @@ function AppRoutes({ onLogout }: AppProps) { }); }; - const handleApprovalSubmit = (action: 'approve' | 'reject', comment: string) => { + const handleApprovalSubmit = (action: 'approve' | 'reject', _comment: string) => { return new Promise((resolve) => { setTimeout(() => { if (action === 'approve') { @@ -236,7 +235,6 @@ function AppRoutes({ onLogout }: AppProps) { }); } - console.log(`${action} action completed with comment:`, comment); setApprovalAction(null); resolve(true); }, 1000); @@ -658,8 +656,6 @@ interface MainAppProps { export default function App(props?: MainAppProps) { const { onLogout } = props || {}; - console.log('๐ŸŸข Main App component rendered'); - console.log('๐ŸŸข onLogout prop received:', !!onLogout); return ( diff --git a/src/components/Auth/AuthDebugInfo.tsx b/src/components/Auth/AuthDebugInfo.tsx index d754b98..69ce49d 100644 --- a/src/components/Auth/AuthDebugInfo.tsx +++ b/src/components/Auth/AuthDebugInfo.tsx @@ -14,14 +14,7 @@ export function AuthDebugInfo({ isOpen, onClose }: AuthDebugInfoProps) { const { user, isAuthenticated, isLoading, error } = useAuth0(); useEffect(() => { - console.log('AuthDebugInfo - Current Auth State:', { - isAuthenticated, - isLoading, - hasUser: !!user, - error: error?.message, - userData: user, - timestamp: new Date().toISOString() - }); + // Auth state debug info - removed console.log }, [user, isAuthenticated, isLoading, error]); if (!isOpen) return null; diff --git a/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx index 87bfcd4..7de8d2b 100644 --- a/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx +++ b/src/components/admin/AnalyticsConfig/AnalyticsConfig.tsx @@ -32,7 +32,6 @@ export function AnalyticsConfig() { const handleSave = () => { // TODO: Implement API call to save configuration - console.log('Saving analytics configuration:', config); toast.success('Analytics configuration saved successfully'); }; diff --git a/src/components/admin/ConfigurationManager.tsx b/src/components/admin/ConfigurationManager.tsx index c28159e..119b497 100644 --- a/src/components/admin/ConfigurationManager.tsx +++ b/src/components/admin/ConfigurationManager.tsx @@ -258,10 +258,12 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro } }; - // Filter out notification rules and dashboard layout categories + // Filter out notification rules, dashboard layout categories, and allow external sharing const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT']; + const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING']; const filteredConfigurations = configurations.filter( - config => !excludedCategories.includes(config.configCategory) + config => !excludedCategories.includes(config.configCategory) && + !excludedConfigKeys.includes(config.configKey) ); const groupedConfigs = filteredConfigurations.reduce((acc, config) => { diff --git a/src/components/admin/DashboardConfig/DashboardConfig.tsx b/src/components/admin/DashboardConfig/DashboardConfig.tsx index b030bd3..86547da 100644 --- a/src/components/admin/DashboardConfig/DashboardConfig.tsx +++ b/src/components/admin/DashboardConfig/DashboardConfig.tsx @@ -60,7 +60,6 @@ export function DashboardConfig() { const handleSave = () => { // TODO: Implement API call to save dashboard configuration - console.log('Saving dashboard configuration:', config); toast.success('Dashboard layout saved successfully'); }; diff --git a/src/components/admin/HolidayManager.tsx b/src/components/admin/HolidayManager.tsx index 66fc36c..354007f 100644 --- a/src/components/admin/HolidayManager.tsx +++ b/src/components/admin/HolidayManager.tsx @@ -88,6 +88,17 @@ export function HolidayManager() { setShowAddDialog(true); }; + // Calculate minimum date (tomorrow for new holidays, no restriction for editing) + const getMinDate = () => { + // Only enforce minimum date when adding new holidays, not when editing existing ones + if (editingHoliday) { + return undefined; // Allow editing past holidays + } + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + return tomorrow.toISOString().split('T')[0]; + }; + const handleSave = async () => { try { setError(null); @@ -357,9 +368,12 @@ export function HolidayManager() { type="date" value={formData.holidayDate} onChange={(e) => setFormData({ ...formData, holidayDate: e.target.value })} + min={getMinDate()} className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm" /> -

Select the holiday date

+

+ {editingHoliday ? 'Select the holiday date' : 'Select the holiday date (minimum: tomorrow)'} +

{/* Holiday Name Field */} diff --git a/src/components/admin/NotificationConfig/NotificationConfig.tsx b/src/components/admin/NotificationConfig/NotificationConfig.tsx index 9c3354c..b28f077 100644 --- a/src/components/admin/NotificationConfig/NotificationConfig.tsx +++ b/src/components/admin/NotificationConfig/NotificationConfig.tsx @@ -29,7 +29,6 @@ export function NotificationConfig() { const handleSave = () => { // TODO: Implement API call to save notification configuration - console.log('Saving notification configuration:', config); toast.success('Notification configuration saved successfully'); }; diff --git a/src/components/admin/SharingConfig/SharingConfig.tsx b/src/components/admin/SharingConfig/SharingConfig.tsx index abf6e44..e9383fc 100644 --- a/src/components/admin/SharingConfig/SharingConfig.tsx +++ b/src/components/admin/SharingConfig/SharingConfig.tsx @@ -24,7 +24,6 @@ export function SharingConfig() { const handleSave = () => { // TODO: Implement API call to save sharing configuration - console.log('Saving sharing configuration:', config); toast.success('Sharing policy saved successfully'); }; diff --git a/src/components/admin/UserRoleManager/UserRoleManager.tsx b/src/components/admin/UserRoleManager/UserRoleManager.tsx index 9811393..1c6ed40 100644 --- a/src/components/admin/UserRoleManager/UserRoleManager.tsx +++ b/src/components/admin/UserRoleManager/UserRoleManager.tsx @@ -107,13 +107,10 @@ export function UserRoleManager() { setSearching(true); try { const response = await userApi.searchUsers(query, 20); - console.log('Search response:', response); - console.log('Response.data:', response.data); // Backend returns { success: true, data: [...users], message, timestamp } // Axios response is in response.data, actual user array is in response.data.data const users = response.data?.data || []; - console.log('Parsed users:', users); setSearchResults(users); } catch (error: any) { @@ -218,17 +215,11 @@ export function UserRoleManager() { setLoadingUsers(true); try { const response = await userApi.getUsersByRole(roleFilter, page, limit); - - console.log('Users response:', response); // Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } } const usersData = response.data?.data?.users || []; const paginationData = response.data?.data?.pagination; const summaryData = response.data?.data?.summary; - - console.log('Parsed users:', usersData); - console.log('Pagination:', paginationData); - console.log('Summary:', summaryData); setUsers(usersData); @@ -257,11 +248,9 @@ export function UserRoleManager() { const fetchRoleStatistics = async () => { try { const response = await userApi.getRoleStatistics(); - console.log('Role statistics response:', response); // Handle different response formats const statsData = response.data?.data?.statistics || response.data?.statistics || []; - console.log('Statistics data:', statsData); setRoleStats({ admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'), diff --git a/src/components/common/Pagination/Pagination.tsx b/src/components/common/Pagination/Pagination.tsx index 09b99a1..d5fa867 100644 --- a/src/components/common/Pagination/Pagination.tsx +++ b/src/components/common/Pagination/Pagination.tsx @@ -121,7 +121,7 @@ export function Pagination({ variant={pageNum === currentPage ? "default" : "outline"} size="sm" onClick={() => onPageChange(pageNum)} - className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} + className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`} data-testid={`${testIdPrefix}-page-${pageNum}`} aria-current={pageNum === currentPage ? 'page' : undefined} > diff --git a/src/components/dashboard/KPICard/KPICard.tsx b/src/components/dashboard/KPICard/KPICard.tsx index c8b17e0..6ac4ec0 100644 --- a/src/components/dashboard/KPICard/KPICard.tsx +++ b/src/components/dashboard/KPICard/KPICard.tsx @@ -1,5 +1,5 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { LucideIcon } from 'lucide-react'; +import { LucideIcon, Info } from 'lucide-react'; import { ReactNode } from 'react'; interface KPICardProps { @@ -12,6 +12,8 @@ interface KPICardProps { children?: ReactNode; testId?: string; onClick?: () => void; + onJustifyClick?: () => void; + showJustifyButton?: boolean; } export function KPICard({ @@ -23,8 +25,15 @@ export function KPICard({ subtitle, children, testId = 'kpi-card', - onClick + onClick, + onJustifyClick, + showJustifyButton = false }: KPICardProps) { + const handleJustifyClick = (e: React.MouseEvent) => { + e.stopPropagation(); // Prevent card onClick from firing + onJustifyClick?.(); + }; + return ( {title} -
- +
+ {showJustifyButton && onJustifyClick && ( + + )} +
+ +
diff --git a/src/components/layout/PageLayout/PageLayout.tsx b/src/components/layout/PageLayout/PageLayout.tsx index 12f2a4d..4309408 100644 --- a/src/components/layout/PageLayout/PageLayout.tsx +++ b/src/components/layout/PageLayout/PageLayout.tsx @@ -112,7 +112,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on // Navigate to request detail page onNavigate(navigationUrl); - console.log('[PageLayout] Navigating to:', navigationUrl); } } @@ -148,8 +147,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on const notifs = result.data?.notifications || []; setNotifications(notifs); setUnreadCount(result.data?.unreadCount || 0); - - console.log('[PageLayout] Loaded', notifs.length, 'recent notifications,', result.data?.unreadCount, 'unread'); } catch (error) { console.error('[PageLayout] Failed to fetch notifications:', error); } @@ -166,7 +163,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on // Listen for new notifications const handleNewNotification = (data: { notification: Notification }) => { - console.log('[PageLayout] ๐Ÿ”” New notification received:', data); if (!mounted) return; setNotifications(prev => [data.notification, ...prev].slice(0, 4)); // Keep latest 4 for dropdown @@ -451,14 +447,10 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on { - console.log('๐Ÿ”ด Logout button clicked in PageLayout'); - console.log('๐Ÿ”ด onLogout function exists?', !!onLogout); setShowLogoutDialog(false); if (onLogout) { - console.log('๐Ÿ”ด Calling onLogout function...'); try { await onLogout(); - console.log('๐Ÿ”ด onLogout completed'); } catch (error) { console.error('๐Ÿ”ด Error calling onLogout:', error); } diff --git a/src/components/participant/AddApproverModal/AddApproverModal.tsx b/src/components/participant/AddApproverModal/AddApproverModal.tsx index fa53f82..410a363 100644 --- a/src/components/participant/AddApproverModal/AddApproverModal.tsx +++ b/src/components/participant/AddApproverModal/AddApproverModal.tsx @@ -221,8 +221,6 @@ export function AddApproverModal({ secondEmail: foundUser.secondEmail, location: foundUser.location }); - - console.log(`โœ… Validated approver: ${foundUser.displayName} (${foundUser.email})`); } catch (error) { console.error('Failed to validate approver:', error); setValidationModal({ @@ -358,7 +356,6 @@ export function AddApproverModal({ setSelectedUser(user); // Track that user was selected via @ search setSearchResults([]); setIsSearching(false); - console.log(`โœ… User selected and verified: ${user.displayName} (${user.email})`); } catch (error) { console.error('Failed to ensure user exists:', error); setValidationModal({ diff --git a/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx b/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx index eb89d5f..e9b938d 100644 --- a/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx +++ b/src/components/participant/AddSpectatorModal/AddSpectatorModal.tsx @@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Eye, X, AtSign, AlertCircle, Lightbulb } from 'lucide-react'; import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi'; -import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; interface AddSpectatorModalProps { @@ -70,8 +70,8 @@ export function AddSpectatorModal({ useEffect(() => { const loadSystemPolicy = async () => { try { - const workflowConfigs = await getAllConfigurations('WORKFLOW_SHARING'); - const tatConfigs = await getAllConfigurations('TAT_SETTINGS'); + const workflowConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const tatConfigs = await getPublicConfigurations('TAT_SETTINGS'); const allConfigs = [...workflowConfigs, ...tatConfigs]; const configMap: Record = {}; allConfigs.forEach((c: AdminConfiguration) => { @@ -250,8 +250,6 @@ export function AddSpectatorModal({ secondEmail: foundUser.secondEmail, location: foundUser.location }); - - console.log(`โœ… Validated spectator: ${foundUser.displayName} (${foundUser.email})`); } catch (error) { console.error('Failed to validate spectator:', error); setValidationModal({ @@ -373,7 +371,6 @@ export function AddSpectatorModal({ setSelectedUser(user); // Track that user was selected via @ search setSearchResults([]); setIsSearching(false); - console.log(`โœ… User selected and verified: ${user.displayName} (${user.email})`); } catch (error) { console.error('Failed to ensure user exists:', error); setValidationModal({ diff --git a/src/components/sla/SLAProgressBar/SLAProgressBar.tsx b/src/components/sla/SLAProgressBar/SLAProgressBar.tsx index 967fe12..0af90a5 100644 --- a/src/components/sla/SLAProgressBar/SLAProgressBar.tsx +++ b/src/components/sla/SLAProgressBar/SLAProgressBar.tsx @@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge'; import { Progress } from '@/components/ui/progress'; import { Clock, Lock, CheckCircle, XCircle, AlertTriangle, AlertOctagon } from 'lucide-react'; import { formatHoursMinutes } from '@/utils/slaTracker'; +import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; export interface SLAData { status: 'normal' | 'approaching' | 'critical' | 'breached'; @@ -90,7 +91,7 @@ export function SLAProgressBar({ {sla.deadline && (

- Due: {new Date(sla.deadline).toLocaleString()} โ€ข {sla.percentageUsed || 0}% elapsed + Due: {formatDateDDMMYYYY(sla.deadline, true)} โ€ข {sla.percentageUsed || 0}% elapsed

)} diff --git a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx index 74f7b63..b2cf007 100644 --- a/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx +++ b/src/components/workNote/WorkNoteChat/WorkNoteChat.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect, useMemo } from 'react'; import { getWorkNotes, createWorkNoteMultipart, getWorkflowDetails, downloadWorkNoteAttachment, getWorkNoteAttachmentPreviewUrl, addSpectator, addApproverAtLevel } from '@/services/workflowApi'; -import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; import { toast } from 'sonner'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket'; @@ -78,6 +78,7 @@ interface WorkNoteChatProps { requestTitle?: string; // Optional title for display onAttachmentsExtracted?: (attachments: any[]) => void; // Callback to pass attachments to parent isInitiator?: boolean; // Whether current user is the initiator + isSpectator?: boolean; // Whether current user is a spectator (view-only) currentLevels?: any[]; // Current approval levels for add approver modal onAddApprover?: (email: string, tatHours: number, level: number) => Promise; // Callback to add approver } @@ -140,7 +141,7 @@ const FileIcon = ({ type }: { type: string }) => { return ; }; -export function WorkNoteChat({ requestId, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted, isInitiator = false, currentLevels = [], onAddApprover }: WorkNoteChatProps) { +export function WorkNoteChat({ requestId, messages: externalMessages, onSend, skipSocketJoin = false, requestTitle, onAttachmentsExtracted, isInitiator = false, isSpectator = false, currentLevels = [], onAddApprover }: WorkNoteChatProps) { const routeParams = useParams<{ requestId: string }>(); const effectiveRequestId = requestId || routeParams.requestId || ''; const [message, setMessage] = useState(''); @@ -193,6 +194,24 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk msg.user.name.toLowerCase().includes(searchTerm.toLowerCase()) ); + // Determine if current user is a spectator (when not passed as prop) + const effectiveIsSpectator = useMemo(() => { + // If isSpectator is explicitly passed as prop, use it + if (isSpectator !== undefined) { + return isSpectator; + } + // Otherwise, determine from participants list + if (!currentUserId || participants.length === 0) { + return false; + } + return participants.some((p: any) => { + const pUserId = (p as any).userId || (p as any).user_id; + const pRole = (p.role || '').toString().toUpperCase(); + const pType = ((p as any).participantType || (p as any).participant_type || '').toString().toUpperCase(); + return pUserId === currentUserId && (pRole === 'SPECTATOR' || pType === 'SPECTATOR'); + }); + }, [isSpectator, currentUserId, participants]); + // Log when participants change - logging removed for performance useEffect(() => { // Participants state changed - logging removed @@ -230,7 +249,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk }; }) : []; setMessages(mapped as any); - console.log(`[WorkNoteChat] Loaded ${mapped.length} messages from backend`); } catch (error) { console.error('[WorkNoteChat] Failed to load messages:', error); } @@ -313,23 +331,19 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk useEffect(() => { // Skip if participants are already loaded (prevents resetting on tab switch) if (participantsLoadedRef.current) { - console.log('[WorkNoteChat] Participants already loaded, skipping reload'); return; } if (!effectiveRequestId) { - console.log('[WorkNoteChat] No requestId, skipping participants load'); return; } (async () => { try { - console.log('[WorkNoteChat] Fetching participants from backend...'); const details = await getWorkflowDetails(effectiveRequestId); const rows = Array.isArray(details?.participants) ? details.participants : []; if (rows.length === 0) { - console.log('[WorkNoteChat] No participants found in backend response'); return; } @@ -384,7 +398,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk return () => { // Don't reset on unmount, only on request change if (effectiveRequestId) { - console.log('[WorkNoteChat] Request changed, will reload participants on next mount'); participantsLoadedRef.current = false; } }; @@ -408,7 +421,7 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk useEffect(() => { const loadDocumentPolicy = async () => { try { - const configs = await getAllConfigurations('DOCUMENT_POLICY'); + const configs = await getPublicConfigurations('DOCUMENT_POLICY'); const configMap: Record = {}; configs.forEach((c: AdminConfiguration) => { configMap[c.configKey] = c.configValue; @@ -449,53 +462,37 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk // Only join room if not skipped (standalone mode) if (!skipSocketJoin) { - console.log('[WorkNoteChat] ๐Ÿšช About to join request room - requestId:', joinedId, 'userId:', currentUserId, 'socketId:', s.id); joinRequestRoom(s, joinedId, currentUserId); - console.log('[WorkNoteChat] โœ… Emitted join:request event (standalone mode)'); // Mark self as online immediately after joining room setParticipants(prev => { const updated = prev.map(p => (p as any).userId === currentUserId ? { ...p, status: 'online' as const } : p ); - const selfParticipant = prev.find(p => (p as any).userId === currentUserId); - if (selfParticipant) { - console.log('[WorkNoteChat] ๐ŸŸข Marked self as online:', selfParticipant.name); - } return updated; }); } else { - console.log('[WorkNoteChat] โญ๏ธ Skipping socket join - parent component handling connection'); - // Still mark self as online even when embedded (parent handles socket but we track presence) setParticipants(prev => { const updated = prev.map(p => (p as any).userId === currentUserId ? { ...p, status: 'online' as const } : p ); - const selfParticipant = prev.find(p => (p as any).userId === currentUserId); - if (selfParticipant) { - console.log('[WorkNoteChat] ๐ŸŸข Marked self as online (embedded mode):', selfParticipant.name); - } return updated; }); } // Handle new work notes const noteHandler = (payload: any) => { - console.log('[WorkNoteChat] ๐Ÿ“จ Received worknote:new event:', payload); const n = payload?.note || payload; if (!n) { - console.log('[WorkNoteChat] โš ๏ธ No note data in payload'); return; } const noteId = n.noteId || n.id; - console.log('[WorkNoteChat] Processing note:', noteId, 'from:', n.userName || n.user_name); // Prevent duplicates: check if message with same noteId already exists setMessages(prev => { if (prev.some(m => m.id === noteId)) { - console.log('[WorkNoteChat] โญ๏ธ Duplicate note, skipping:', noteId); return prev; // Already exists, don't add } @@ -525,66 +522,53 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk })) : undefined } as any; - console.log('[WorkNoteChat] โœ… Adding new message to state:', newMessage.id); return [...prev, newMessage]; }); }; // Handle presence: user joined const presenceJoinHandler = (data: { userId: string; requestId: string }) => { - console.log('[WorkNoteChat] ๐ŸŸข presence:join received - userId:', data.userId, 'requestId:', data.requestId); setParticipants(prev => { if (prev.length === 0) { - console.log('[WorkNoteChat] โš ๏ธ Cannot update presence:join - no participants loaded yet'); return prev; } const participant = prev.find(p => (p as any).userId === data.userId); if (!participant) { - console.log('[WorkNoteChat] โš ๏ธ User not found in participants list:', data.userId); return prev; } const updated = prev.map(p => (p as any).userId === data.userId ? { ...p, status: 'online' as const } : p ); - console.log('[WorkNoteChat] โœ… Marked user as online:', participant.name, '- Total online:', updated.filter(p => p.status === 'online').length); return updated; }); }; // Handle presence: user left const presenceLeaveHandler = (data: { userId: string; requestId: string }) => { - console.log('[WorkNoteChat] ๐Ÿ”ด presence:leave received - userId:', data.userId, 'requestId:', data.requestId); - // Never mark self as offline in own browser if (data.userId === currentUserId) { - console.log('[WorkNoteChat] โš ๏ธ Ignoring presence:leave for self - staying online in own view'); return; } setParticipants(prev => { if (prev.length === 0) { - console.log('[WorkNoteChat] โš ๏ธ Cannot update presence:leave - no participants loaded yet'); return prev; } const participant = prev.find(p => (p as any).userId === data.userId); if (!participant) { - console.log('[WorkNoteChat] โš ๏ธ User not found in participants list:', data.userId); return prev; } const updated = prev.map(p => (p as any).userId === data.userId ? { ...p, status: 'offline' as const } : p ); - console.log('[WorkNoteChat] โœ… Marked user as offline:', participant.name, '- Total online:', updated.filter(p => p.status === 'online').length); return updated; }); }; // Handle initial online users list const presenceOnlineHandler = (data: { requestId: string; userIds: string[] }) => { - // Presence update received - logging removed setParticipants(prev => { if (prev.length === 0) { - console.log('[WorkNoteChat] โš ๏ธ No participants loaded yet, cannot update online status. Response will be ignored.'); return prev; } @@ -608,36 +592,26 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk // Handle socket reconnection const connectHandler = () => { - console.log('[WorkNoteChat] ๐Ÿ”Œ Socket connected/reconnected'); - // Mark self as online on connection setParticipants(prev => { const updated = prev.map(p => (p as any).userId === currentUserId ? { ...p, status: 'online' as const } : p ); - const selfParticipant = prev.find(p => (p as any).userId === currentUserId); - if (selfParticipant) { - console.log('[WorkNoteChat] ๐ŸŸข Marked self as online on connect:', selfParticipant.name); - } return updated; }); // Rejoin room if needed if (!skipSocketJoin) { joinRequestRoom(s, joinedId, currentUserId); - console.log('[WorkNoteChat] ๐Ÿ”„ Rejoined request room on reconnection'); } // Request online users on connection with multiple retries if (participantsLoadedRef.current) { - console.log('[WorkNoteChat] ๐Ÿ“ก Requesting online users after connection...'); s.emit('request:online-users', { requestId: joinedId }); // Send additional requests with delay to ensure we get the response setTimeout(() => s.emit('request:online-users', { requestId: joinedId }), 300); setTimeout(() => s.emit('request:online-users', { requestId: joinedId }), 800); - } else { - console.log('[WorkNoteChat] โณ Participants not loaded yet, will request online users when they load'); } }; @@ -705,7 +679,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk // Only leave room if we joined it if (!skipSocketJoin) { leaveRequestRoom(s, joinedId); - console.log('[WorkNoteChat] ๐Ÿšช Emitting leave:request for room (standalone mode)'); } socketRef.current = null; // Socket cleanup completed - logging removed @@ -729,7 +702,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk const participant = participants.find(p => p.name.toLowerCase().includes(mentionedName.toLowerCase()) ); - console.log('[Mention Match] Looking for:', mentionedName, 'Found participant:', participant ? `${participant.name} (${(participant as any)?.userId})` : 'NOT FOUND'); return (participant as any)?.userId; }) .filter(Boolean); @@ -878,20 +850,13 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk (async () => { try { const rows = await getWorkNotes(effectiveRequestId); - console.log('[WorkNoteChat] Loaded work notes from backend:', rows); - - const mapped = Array.isArray(rows) ? rows.map((m: any) => { - const userName = m.userName || m.user_name || 'User'; - const userRole = m.userRole || m.user_role; // Get role directly from backend - const participantRole = getFormattedRole(userRole); - const noteUserId = m.userId || m.user_id; - - console.log('[WorkNoteChat] Mapping note:', { - rawNote: m, - extracted: { userName, userRole, participantRole } - }); - - return { + const mapped = Array.isArray(rows) ? rows.map((m: any) => { + const userName = m.userName || m.user_name || 'User'; + const userRole = m.userRole || m.user_role; // Get role directly from backend + const participantRole = getFormattedRole(userRole); + const noteUserId = m.userId || m.user_id; + + return { id: m.noteId || m.note_id || m.id || String(Math.random()), user: { name: userName, @@ -1036,7 +1001,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk // Request updated online users list from server to get correct status if (socketRef.current && socketRef.current.connected) { - console.log('[WorkNoteChat] ๐Ÿ“ก Requesting online users after adding spectator...'); socketRef.current.emit('request:online-users', { requestId: effectiveRequestId }); } } @@ -1165,7 +1129,6 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk } } } - console.log('[Extract Mentions] Found:', mentions, 'from text:', text); return mentions; }; @@ -1745,40 +1708,43 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk
-
-

Quick Actions

-
- {/* Only initiator can add approvers */} - {isInitiator && ( + {/* Quick Actions Section - Hide for spectators */} + {!effectiveIsSpectator && ( +
+

Quick Actions

+
+ {/* Only initiator can add approvers */} + {isInitiator && ( + + )} - )} - - {/* - */} + {/* + */} +
-
+ )}
@@ -1796,18 +1762,20 @@ export function WorkNoteChat({ requestId, messages: externalMessages, onSend, sk /> )} - {/* Add Spectator Modal */} - setShowAddSpectatorModal(false)} - onConfirm={handleAddSpectator} - requestIdDisplay={effectiveRequestId} - requestTitle={requestInfo.title} - existingParticipants={existingParticipants} - /> + {/* Add Spectator Modal - Hide for spectators */} + {!effectiveIsSpectator && ( + setShowAddSpectatorModal(false)} + onConfirm={handleAddSpectator} + requestIdDisplay={effectiveRequestId} + requestTitle={requestInfo.title} + existingParticipants={existingParticipants} + /> + )} - {/* Add Approver Modal */} - {isInitiator && ( + {/* Add Approver Modal - Hide for spectators */} + {!effectiveIsSpectator && isInitiator && ( setShowAddApproverModal(false)} diff --git a/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx b/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx index 013e582..d465edc 100644 --- a/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx +++ b/src/components/workNote/WorkNoteChat/WorkNoteChatSimple.tsx @@ -150,11 +150,8 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe const n = payload?.note || payload; if (!n) return; - console.log('[WorkNoteChat] Received note via socket:', n); - setMessages(prev => { if (prev.some(m => m.id === (n.noteId || n.note_id || n.id))) { - console.log('[WorkNoteChat] Duplicate detected, skipping'); return prev; } @@ -175,7 +172,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe isCurrentUser: noteUserId === currentUserId } as any; - console.log('[WorkNoteChat] Adding new message:', newMsg); return [...prev, newMsg]; }); }; @@ -265,7 +261,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe (async () => { try { const rows = await getWorkNotes(requestId); - console.log('[WorkNoteChat] Loaded work notes:', rows); const mapped = Array.isArray(rows) ? rows.map((m: any) => { const userName = m.userName || m.user_name || 'User'; diff --git a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx index 534d503..8c9f1b6 100644 --- a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx +++ b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx @@ -6,7 +6,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Textarea } from '@/components/ui/textarea'; import { CheckCircle, XCircle, Clock, AlertCircle, AlertTriangle, FastForward, MessageSquare, PauseCircle, Hourglass, AlertOctagon, FileEdit } from 'lucide-react'; -import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; +import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { formatHoursMinutes } from '@/utils/slaTracker'; import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; import { updateBreachReason as updateBreachReasonApi } from '@/services/workflowApi'; @@ -339,7 +339,7 @@ export function ApprovalStepCard({
Due by: - {approval.sla.deadline ? formatDateTime(approval.sla.deadline) : 'Not set'} + {approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'}
@@ -532,13 +532,13 @@ export function ApprovalStepCard({
Allocated: - {Number(alert.tatHoursAllocated || 0).toFixed(2)}h + {formatHoursMinutes(Number(alert.tatHoursAllocated || 0))}
Elapsed: - {Number(alert.tatHoursElapsed || 0).toFixed(2)}h + {formatHoursMinutes(Number(alert.tatHoursElapsed || 0))} {alert.metadata?.tatTestMode && ( ({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m) @@ -551,7 +551,7 @@ export function ApprovalStepCard({ - {Number(alert.tatHoursRemaining || 0).toFixed(2)}h + {formatHoursMinutes(Number(alert.tatHoursRemaining || 0))} {alert.metadata?.tatTestMode && ( ({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m) @@ -562,7 +562,7 @@ export function ApprovalStepCard({
Due by: - {alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'} + {alert.expectedCompletionTime ? formatDateDDMMYYYY(alert.expectedCompletionTime, true) : 'N/A'}
diff --git a/src/components/workflow/ClaimManagementDetail/ClaimManagementDetail.tsx b/src/components/workflow/ClaimManagementDetail/ClaimManagementDetail.tsx index 74d6f18..8a6d110 100644 --- a/src/components/workflow/ClaimManagementDetail/ClaimManagementDetail.tsx +++ b/src/components/workflow/ClaimManagementDetail/ClaimManagementDetail.tsx @@ -9,6 +9,7 @@ import { Progress } from '@/components/ui/progress'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; +import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { ArrowLeft, Clock, @@ -261,7 +262,7 @@ export function ClaimManagementDetail({

- Due: {claim.slaEndDate} โ€ข {claim.slaProgress}% elapsed + Due: {formatDateDDMMYYYY(claim.slaEndDate, true)} โ€ข {claim.slaProgress}% elapsed

@@ -762,8 +763,7 @@ export function ClaimManagementDetail({ setDealerDocModal(false)} - onSubmit={async (documents) => { - console.log('Dealer documents submitted:', documents); + onSubmit={async (_documents) => { toast.success('Documents Uploaded', { description: 'Your documents have been submitted for review.', }); @@ -779,7 +779,6 @@ export function ClaimManagementDetail({ isOpen={initiatorVerificationModal} onClose={() => setInitiatorVerificationModal(false)} onSubmit={async (data) => { - console.log('Verification data:', data); toast.success('Verification Complete', { description: `Amount set to ${data.approvedAmount}. E-invoice will be generated.`, }); diff --git a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx index 1a9b20d..b3f160e 100644 --- a/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx +++ b/src/components/workflow/CreateRequest/ApprovalWorkflowStep.tsx @@ -5,10 +5,9 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; -import { Users, Settings, Shield, User, CheckCircle, AlertCircle, Info, Clock, Minus, Plus } from 'lucide-react'; +import { Users, Settings, Shield, User, CheckCircle, Minus, Plus } from 'lucide-react'; import { FormData } from '@/hooks/useCreateRequestForm'; import { useMultiUserSearch } from '@/hooks/useUserSearch'; -import { useAuth } from '@/contexts/AuthContext'; import { ensureUserExists } from '@/services/userApi'; interface ApprovalWorkflowStepProps { @@ -35,7 +34,6 @@ export function ApprovalWorkflowStep({ updateFormData, onValidationError }: ApprovalWorkflowStepProps) { - const { user } = useAuth(); const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); const handleApproverEmailChange = (index: number, value: string) => { diff --git a/src/components/workflow/CreateRequest/DocumentsStep.tsx b/src/components/workflow/CreateRequest/DocumentsStep.tsx index aefe4f6..27bb122 100644 --- a/src/components/workflow/CreateRequest/DocumentsStep.tsx +++ b/src/components/workflow/CreateRequest/DocumentsStep.tsx @@ -41,7 +41,7 @@ export function DocumentsStep({ existingDocuments, documentsToDelete, onDocumentsChange, - onExistingDocumentsChange, + onExistingDocumentsChange: _onExistingDocumentsChange, onDocumentsToDeleteChange, onPreviewDocument, onDocumentErrors, diff --git a/src/components/workflow/CreateRequest/ParticipantsStep.tsx b/src/components/workflow/CreateRequest/ParticipantsStep.tsx index a80fb0a..25c38cb 100644 --- a/src/components/workflow/CreateRequest/ParticipantsStep.tsx +++ b/src/components/workflow/CreateRequest/ParticipantsStep.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import { motion } from 'framer-motion'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -8,7 +8,6 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Eye, Info, X } from 'lucide-react'; import { FormData } from '@/hooks/useCreateRequestForm'; import { useUserSearch } from '@/hooks/useUserSearch'; -import { ensureUserExists } from '@/services/userApi'; interface ParticipantsStepProps { formData: FormData; diff --git a/src/components/workflow/CreateRequest/ReviewSubmitStep.tsx b/src/components/workflow/CreateRequest/ReviewSubmitStep.tsx index 4a9a43f..01412d7 100644 --- a/src/components/workflow/CreateRequest/ReviewSubmitStep.tsx +++ b/src/components/workflow/CreateRequest/ReviewSubmitStep.tsx @@ -3,7 +3,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; import { CheckCircle, Rocket, FileText, Users, Eye, Upload, Flame, Target, TrendingUp, DollarSign } from 'lucide-react'; -import { format } from 'date-fns'; import { FormData, RequestTemplate } from '@/hooks/useCreateRequestForm'; interface ReviewSubmitStepProps { diff --git a/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx b/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx index 3d237cb..ad611d2 100644 --- a/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx +++ b/src/components/workflow/CreateRequest/TemplateSelectionStep.tsx @@ -1,5 +1,5 @@ import { motion, AnimatePresence } from 'framer-motion'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index a2d6e20..dc9a7ed 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -74,9 +74,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { const forceLogout = sessionStorage.getItem('__force_logout__'); if (logoutFlag === 'true' || forceLogout === 'true') { - console.log('๐Ÿ”ด Logout flag detected - PREVENTING auto-authentication'); - console.log('๐Ÿ”ด Clearing ALL authentication data and showing login screen'); - // Remove flags sessionStorage.removeItem('__logout_in_progress__'); sessionStorage.removeItem('__force_logout__'); @@ -98,17 +95,12 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { setIsLoading(false); setError(null); - console.log('๐Ÿ”ด Logout complete - user should see login screen'); return; } // PRIORITY 2: Check if URL has logout parameter (from redirect) const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('logout') || urlParams.has('okta_logged_out')) { - console.log('๐Ÿ”ด Logout parameter in URL - clearing everything', { - hasLogout: urlParams.has('logout'), - hasOktaLoggedOut: urlParams.has('okta_logged_out'), - }); TokenManager.clearAll(); localStorage.clear(); sessionStorage.clear(); @@ -133,7 +125,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { // If no auth data exists, we're likely after a logout - set unauthenticated state immediately if (!hasAuthData) { - console.log('๐Ÿ”ด No auth data found - setting unauthenticated state'); setIsAuthenticated(false); setUser(null); setIsLoading(false); @@ -144,7 +135,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { if (!isLoggingOut) { checkAuthStatus(); } else { - console.log('๐Ÿ”ด Skipping checkAuthStatus - logout in progress'); setIsLoading(false); } }, [isLoggingOut]); @@ -211,12 +201,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { // This is the frontend callback URL, NOT the backend URL // Backend will use this same URI when exchanging code with Okta const redirectUri = `${window.location.origin}/login/callback`; - console.log('๐Ÿ“ฅ Authorization Code Received:', { - code: code.substring(0, 10) + '...', - redirectUri, - fullUrl: window.location.href, - note: 'redirectUri is frontend URL (not backend) - must match Okta registration', - }); const result = await exchangeCodeForTokens(code, redirectUri); @@ -244,7 +228,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { const checkAuthStatus = async () => { // Don't check auth status if we're in the middle of logging out if (isLoggingOut) { - console.log('๐Ÿ”ด Skipping checkAuthStatus - logout in progress'); setIsLoading(false); return; } @@ -254,12 +237,9 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { const token = TokenManager.getAccessToken(); const storedUser = TokenManager.getUserData(); - - console.log('๐Ÿ” Checking auth status:', { hasToken: !!token, hasUser: !!storedUser, isLoggingOut }); // If no token at all, user is not authenticated if (!token) { - console.log('๐Ÿ” No token found - setting unauthenticated'); setIsAuthenticated(false); setUser(null); setIsLoading(false); @@ -361,7 +341,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { // This ensures Okta requires login even if a session still exists if (isAfterLogout) { authUrl += `&prompt=login`; - console.log('๐Ÿ” Adding prompt=login to force re-authentication after logout'); } window.location.href = authUrl; @@ -372,13 +351,11 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { }; const logout = async () => { - console.log('๐Ÿšช LOGOUT FUNCTION CALLED - Starting logout process'); - console.log('๐Ÿšช Current auth state:', { isAuthenticated, hasUser: !!user, isLoading }); - try { // CRITICAL: Get id_token from TokenManager before clearing anything // Okta logout endpoint works better with id_token_hint to properly end the session - const idToken = TokenManager.getIdToken(); + // Note: Currently not used but kept for future Okta integration + void TokenManager.getIdToken(); // Set logout flag to prevent auto-authentication after redirect // This must be set BEFORE clearing storage so it survives @@ -386,20 +363,16 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { sessionStorage.setItem('__force_logout__', 'true'); setIsLoggingOut(true); - console.log('๐Ÿšช Step 1: Resetting auth state...'); // Reset auth state FIRST to prevent any re-authentication setIsAuthenticated(false); setUser(null); setError(null); setIsLoading(true); // Set loading to prevent checkAuthStatus from running - console.log('๐Ÿšช Step 1: Auth state reset complete'); // Call backend logout API to clear server-side session and httpOnly cookies // IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies try { - console.log('๐Ÿšช Step 2: Calling backend logout API to clear httpOnly cookies...'); await logoutApi(); - console.log('๐Ÿšช Step 2: Backend logout API completed - httpOnly cookies should be cleared'); } catch (err) { console.error('๐Ÿšช Logout API error:', err); console.warn('๐Ÿšช Backend logout failed - httpOnly cookies may not be cleared'); @@ -407,16 +380,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { } // Clear all authentication data EXCEPT the logout flags and id_token (we need it for Okta logout) - console.log('========================================'); - console.log('LOGOUT - Clearing all authentication data'); - console.log('========================================'); - - // Store id_token temporarily if we have it - let tempIdToken: string | null = null; - if (idToken) { - tempIdToken = idToken; - console.log('๐Ÿšช Preserving id_token for Okta logout:', tempIdToken.substring(0, 20) + '...'); - } // Clear tokens but preserve logout flags const logoutInProgress = sessionStorage.getItem('__logout_in_progress__'); @@ -429,18 +392,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) { if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress); if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout); - console.log('๐Ÿšช Local storage cleared (logout flags preserved)'); - - // Final verification BEFORE redirect - console.log('๐Ÿšช Final verification - logout flags preserved:', { - logoutInProgress: sessionStorage.getItem('__logout_in_progress__'), - forceLogout: sessionStorage.getItem('__force_logout__'), - }); - - console.log('๐Ÿšช Clearing local session and redirecting to login...'); - console.log('๐Ÿšช Using prompt=login on next auth to force re-authentication'); - console.log('๐Ÿšช This will prevent auto-authentication even if Okta session exists'); - // Small delay to ensure sessionStorage is written before redirect await new Promise(resolve => setTimeout(resolve, 100)); @@ -523,11 +474,8 @@ export function _Auth0AuthProvider({ children }: { children: ReactNode }) { authorizationParams={{ redirect_uri: window.location.origin + '/login/callback', }} - onRedirectCallback={(appState) => { - console.log('Auth0 Redirect Callback:', { - appState, - returnTo: appState?.returnTo || window.location.pathname, - }); + onRedirectCallback={(_appState) => { + // Auth0 redirect callback handled }} > {children} diff --git a/src/hooks/useConclusionRemark.ts b/src/hooks/useConclusionRemark.ts index 9f465a4..543ce2d 100644 --- a/src/hooks/useConclusionRemark.ts +++ b/src/hooks/useConclusionRemark.ts @@ -71,7 +71,6 @@ export function useConclusionRemark( } } catch (err) { // No conclusion yet - this is expected for newly approved requests - console.log('[useConclusionRemark] No existing conclusion found'); } }; diff --git a/src/hooks/useCreateRequestForm.ts b/src/hooks/useCreateRequestForm.ts index 83736a1..23514c4 100644 --- a/src/hooks/useCreateRequestForm.ts +++ b/src/hooks/useCreateRequestForm.ts @@ -1,7 +1,6 @@ import { useState, useEffect } from 'react'; -import { useAuth } from '@/contexts/AuthContext'; import { getWorkflowDetails } from '@/services/workflowApi'; -import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; export interface RequestTemplate { id: string; @@ -127,7 +126,6 @@ export function useCreateRequestForm( editRequestId: string, templates: RequestTemplate[] ) { - const { user } = useAuth(); const [formData, setFormData] = useState(initialFormData); const [selectedTemplate, setSelectedTemplate] = useState(null); const [loadingDraft, setLoadingDraft] = useState(isEditing); @@ -148,7 +146,7 @@ export function useCreateRequestForm( const loadPolicies = async () => { try { // Load document policy - const docConfigs = await getAllConfigurations('DOCUMENT_POLICY'); + const docConfigs = await getPublicConfigurations('DOCUMENT_POLICY'); const docConfigMap: Record = {}; docConfigs.forEach((c: AdminConfiguration) => { docConfigMap[c.configKey] = c.configValue; @@ -164,8 +162,8 @@ export function useCreateRequestForm( }); // Load system policy - const workflowConfigs = await getAllConfigurations('WORKFLOW_SHARING'); - const tatConfigs = await getAllConfigurations('TAT_SETTINGS'); + const workflowConfigs = await getPublicConfigurations('WORKFLOW_SHARING'); + const tatConfigs = await getPublicConfigurations('TAT_SETTINGS'); const allConfigs = [...workflowConfigs, ...tatConfigs]; const configMap: Record = {}; allConfigs.forEach((c: AdminConfiguration) => { diff --git a/src/hooks/useDocumentManagement.ts b/src/hooks/useDocumentManagement.ts index d80dda8..4043467 100644 --- a/src/hooks/useDocumentManagement.ts +++ b/src/hooks/useDocumentManagement.ts @@ -29,7 +29,7 @@ export interface PreviewDocument { */ export function useDocumentManagement( documentPolicy: DocumentPolicy, - isEditing: boolean = false + _isEditing: boolean = false ) { const [documents, setDocuments] = useState([]); const [existingDocuments, setExistingDocuments] = useState([]); diff --git a/src/hooks/useDocumentUpload.ts b/src/hooks/useDocumentUpload.ts index d16ebba..e57ec80 100644 --- a/src/hooks/useDocumentUpload.ts +++ b/src/hooks/useDocumentUpload.ts @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { uploadDocument } from '@/services/documentApi'; -import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; +import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi'; import { toast } from 'sonner'; /** @@ -57,7 +57,7 @@ export function useDocumentUpload( useEffect(() => { const loadDocumentPolicy = async () => { try { - const configs = await getAllConfigurations('DOCUMENT_POLICY'); + const configs = await getPublicConfigurations('DOCUMENT_POLICY'); const configMap: Record = {}; configs.forEach((c: AdminConfiguration) => { configMap[c.configKey] = c.configValue; diff --git a/src/hooks/useRequestDetails.ts b/src/hooks/useRequestDetails.ts index e9ae530..ddd6a31 100644 --- a/src/hooks/useRequestDetails.ts +++ b/src/hooks/useRequestDetails.ts @@ -237,8 +237,10 @@ export function useRequestDetails( }, createdAt: wf.createdAt, updatedAt: wf.updatedAt, - totalSteps: wf.totalLevels, - currentStep: summary?.currentLevel || wf.currentLevel, + totalSteps: wf.totalLevels || 1, + // Store both raw and clamped values - raw for completion detection, clamped for display + currentStepRaw: summary?.currentLevel || wf.currentLevel || 1, + currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1), auditTrail: filteredActivities, conclusionRemark: wf.conclusionRemark || null, closureDate: wf.closureDate || null, @@ -407,8 +409,10 @@ export function useRequestDetails( }, createdAt: wf.createdAt, updatedAt: wf.updatedAt, - totalSteps: wf.totalLevels, - currentStep: summary?.currentLevel || wf.currentLevel, + totalSteps: wf.totalLevels || 1, + // Store both raw and clamped values - raw for completion detection, clamped for display + currentStepRaw: summary?.currentLevel || wf.currentLevel || 1, + currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1), approvalFlow, approvals, documents: mappedDocuments, diff --git a/src/hooks/useRequestSocket.ts b/src/hooks/useRequestSocket.ts index ac6a5a4..3f50e8b 100644 --- a/src/hooks/useRequestSocket.ts +++ b/src/hooks/useRequestSocket.ts @@ -208,9 +208,7 @@ export function useRequestSocket( * 3. Show browser notification if permission granted */ const handleTatAlert = (data: any) => { - // TAT alert received - single line log only const alertEmoji = data.type === 'breach' ? 'โฐ' : data.type === 'threshold2' ? 'โš ๏ธ' : 'โณ'; - console.log(`TAT Alert: ${data.message}`); // Refresh: Get updated TAT alerts from backend (async () => { @@ -218,8 +216,8 @@ export function useRequestSocket( const details = await workflowApi.getWorkflowDetails(requestIdentifier); if (details) { - const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; - console.log(`[useRequestSocket] Refreshed TAT alerts:`, tatAlerts); + // Extract TAT alerts for potential future use + void (Array.isArray(details.tatAlerts) ? details.tatAlerts : []); // Browser notification (if user granted permission) if ('Notification' in window && Notification.permission === 'granted') { diff --git a/src/main.tsx b/src/main.tsx index 1577405..9a10a3b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,13 +4,6 @@ import { AuthProvider } from './contexts/AuthContext'; import { AuthenticatedApp } from './pages/Auth'; import './styles/globals.css'; -console.log('Application Starting...'); -console.log('Environment:', { - hostname: window.location.hostname, - origin: window.location.origin, - isLocalhost: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1', -}); - ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/src/pages/ApproverPerformance/components/ApproverPerformanceActionsStats.tsx b/src/pages/ApproverPerformance/components/ApproverPerformanceActionsStats.tsx index 697fa95..baef557 100644 --- a/src/pages/ApproverPerformance/components/ApproverPerformanceActionsStats.tsx +++ b/src/pages/ApproverPerformance/components/ApproverPerformanceActionsStats.tsx @@ -2,7 +2,7 @@ * Approver's Actions Statistics Component */ -import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3 } from 'lucide-react'; +import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3, Archive } from 'lucide-react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import type { ApproverPerformance } from '@/services/dashboard.service'; import type { ApproverPerformanceStats } from '../types/approverPerformance.types'; @@ -33,7 +33,7 @@ export function ApproverPerformanceActionsStats({ Approver's Actions -
+
@@ -61,6 +61,13 @@ export function ApproverPerformanceActionsStats({
{calculatedStats.pendingByApprover}
Pending Actions
+
+
+ +
+
{calculatedStats.closedByApprover}
+
Closed Requests
+
diff --git a/src/pages/ApproverPerformance/types/approverPerformance.types.ts b/src/pages/ApproverPerformance/types/approverPerformance.types.ts index d826963..d920b77 100644 --- a/src/pages/ApproverPerformance/types/approverPerformance.types.ts +++ b/src/pages/ApproverPerformance/types/approverPerformance.types.ts @@ -2,13 +2,13 @@ * Type definitions for Approver Performance page */ -import type { ApproverPerformance } from '@/services/dashboard.service'; export interface ApproverPerformanceStats { total: number; approvedByApprover: number; rejectedByApprover: number; pendingByApprover: number; + closedByApprover: number; breached: number; compliant: number; approvalRate: number; diff --git a/src/pages/ApproverPerformance/utils/statsCalculations.ts b/src/pages/ApproverPerformance/utils/statsCalculations.ts index 9661832..dd081a7 100644 --- a/src/pages/ApproverPerformance/utils/statsCalculations.ts +++ b/src/pages/ApproverPerformance/utils/statsCalculations.ts @@ -25,6 +25,13 @@ export function calculateApproverStats( return ['pending', 'in_progress', 'in-progress'].includes(status); }).length; + // Count closed requests - check overall request status (wf.status = 'CLOSED') + // Closed requests are those that have been finalized with a conclusion remark + const closedByApprover = filtered.filter(r => { + const requestStatus = (r.status || '').toLowerCase(); + return requestStatus === 'closed'; + }).length; + // TAT compliance stats - check isBreached flag OR slaStatus === 'breached' // This includes pending requests that have breached their TAT const breached = filtered.filter(r => { @@ -72,6 +79,7 @@ export function calculateApproverStats( approvedByApprover, rejectedByApprover, pendingByApprover, + closedByApprover, // TAT stats breached, compliant, diff --git a/src/pages/Auth/Auth.tsx b/src/pages/Auth/Auth.tsx index 07ddfb1..63a50fe 100644 --- a/src/pages/Auth/Auth.tsx +++ b/src/pages/Auth/Auth.tsx @@ -6,28 +6,13 @@ import { LogIn, Shield } from 'lucide-react'; export function Auth() { const { login, isLoading, error } = useAuth(); - console.log('Auth Component Render - Auth0 State:', { - isLoading, - error: error?.message, - timestamp: new Date().toISOString() - }); - const handleSSOLogin = async () => { - console.log('========================================'); - console.log('SSO LOGIN - Button Clicked'); - console.log('Timestamp:', new Date().toISOString()); - console.log('Current URL:', window.location.href); - console.log('========================================'); - // Clear any existing session data - console.log('Clearing local storage and session storage...'); localStorage.clear(); sessionStorage.clear(); - console.log('Storage cleared'); try { await login(); - console.log('Login redirect initiated successfully'); } catch (loginError) { console.error('========================================'); console.error('LOGIN ERROR'); diff --git a/src/pages/Auth/AuthenticatedApp.tsx b/src/pages/Auth/AuthenticatedApp.tsx index 0825975..a10d684 100644 --- a/src/pages/Auth/AuthenticatedApp.tsx +++ b/src/pages/Auth/AuthenticatedApp.tsx @@ -13,27 +13,18 @@ export function AuthenticatedApp() { const isCallbackRoute = typeof window !== 'undefined' && window.location.pathname === '/login/callback'; const handleLogout = async () => { - console.log('๐Ÿ”ต ========================================'); - console.log('๐Ÿ”ต AuthenticatedApp.handleLogout - CALLED'); - console.log('๐Ÿ”ต Timestamp:', new Date().toISOString()); - console.log('๐Ÿ”ต logout function exists?', !!logout); - console.log('๐Ÿ”ต ========================================'); - try { if (!logout) { console.error('๐Ÿ”ต ERROR: logout function is undefined!'); return; } - console.log('๐Ÿ”ต Calling logout from auth context...'); // Call logout from auth context (handles all cleanup and redirect) await logout(); - console.log('๐Ÿ”ต Logout successful - redirecting to login...'); } catch (logoutError) { console.error('๐Ÿ”ต Logout error in handleLogout:', logoutError); // Even if logout fails, clear local data and redirect try { - console.log('๐Ÿ”ต Attempting emergency cleanup...'); localStorage.clear(); sessionStorage.clear(); window.location.href = '/'; @@ -44,33 +35,7 @@ export function AuthenticatedApp() { }; useEffect(() => { - console.log('AuthenticatedApp - Auth State Changed:', { - isAuthenticated, - isLoading, - error: error?.message, - hasUser: !!user, - timestamp: new Date().toISOString() - }); - - if (user) { - console.log('========================================'); - console.log('USER AUTHENTICATED - Full Details'); - console.log('========================================'); - console.log('User ID:', user.userId || user.sub); - console.log('Employee ID:', user.employeeId); - console.log('Email:', user.email); - console.log('Name:', user.displayName || user.name); - console.log('Display Name:', user.displayName); - console.log('First Name:', user.firstName); - console.log('Last Name:', user.lastName); - console.log('Department:', user.department); - console.log('Designation:', user.designation); - console.log('Role:', user.role); - console.log('========================================'); - console.log('ALL USER DATA:'); - console.log(JSON.stringify(user, null, 2)); - console.log('========================================'); - } + // Auth state changed }, [isAuthenticated, isLoading, error, user]); // Always show callback loader when on callback route (after all hooks) @@ -80,7 +45,6 @@ export function AuthenticatedApp() { // Show loading state while checking authentication if (isLoading) { - console.log('Auth0 is still loading...'); return (
@@ -112,12 +76,10 @@ export function AuthenticatedApp() { // Show login screen if not authenticated if (!isAuthenticated) { - console.log('User not authenticated, showing login screen'); return ; } // User is authenticated, show the app - console.log('User authenticated successfully, showing main application'); return ( <> diff --git a/src/pages/ClosedRequests/ClosedRequests.tsx b/src/pages/ClosedRequests/ClosedRequests.tsx index 5967794..efc9f51 100644 --- a/src/pages/ClosedRequests/ClosedRequests.tsx +++ b/src/pages/ClosedRequests/ClosedRequests.tsx @@ -12,7 +12,7 @@ import { useClosedRequests } from './hooks/useClosedRequests'; import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters'; // Types -import type { ClosedRequestsProps } from './types/closedRequests.types'; +import type { ClosedRequestsProps, ClosedRequestsFilters } from './types/closedRequests.types'; export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { // Data fetching hook @@ -24,7 +24,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { const filters = useClosedRequestsFilters({ onFiltersChange: useCallback( - (filters) => { + (filters: ClosedRequestsFilters) => { // Reset to page 1 when filters change fetchRef.current(1, { search: filters.search || undefined, diff --git a/src/pages/ClosedRequests/components/ClosedRequestCard.tsx b/src/pages/ClosedRequests/components/ClosedRequestCard.tsx index 767bb82..06d51f0 100644 --- a/src/pages/ClosedRequests/components/ClosedRequestCard.tsx +++ b/src/pages/ClosedRequests/components/ClosedRequestCard.tsx @@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { ArrowRight, Calendar, CheckCircle } from 'lucide-react'; -import { formatDateTime } from '@/utils/dateFormatter'; +import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; import { ClosedRequest } from '../types/closedRequests.types'; import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; @@ -88,13 +88,13 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
- Created: {request.createdAt !== 'โ€”' ? formatDateTime(request.createdAt) : 'โ€”'} + Created: {request.createdAt !== 'โ€”' ? formatDateDDMMYYYY(request.createdAt, true) : 'โ€”'}
{request.dueDate && (
- Closed: {formatDateTime(request.dueDate)} + Closed: {formatDateDDMMYYYY(request.dueDate, true)}
)}
diff --git a/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx b/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx index 0c9ad5f..7a118dc 100644 --- a/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx +++ b/src/pages/ClosedRequests/components/ClosedRequestsFilters.tsx @@ -110,12 +110,6 @@ export function ClosedRequestsFilters({ All Statuses - -
- - Approved -
-
diff --git a/src/pages/ClosedRequests/components/ClosedRequestsPagination.tsx b/src/pages/ClosedRequests/components/ClosedRequestsPagination.tsx index 67ce902..83eef99 100644 --- a/src/pages/ClosedRequests/components/ClosedRequestsPagination.tsx +++ b/src/pages/ClosedRequests/components/ClosedRequestsPagination.tsx @@ -66,7 +66,7 @@ export function ClosedRequestsPagination({ variant={pageNum === currentPage ? "default" : "outline"} size="sm" onClick={() => onPageChange(pageNum)} - className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} + className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`} data-testid={`closed-requests-pagination-page-${pageNum}`} > {pageNum} diff --git a/src/pages/ClosedRequests/hooks/useClosedRequests.ts b/src/pages/ClosedRequests/hooks/useClosedRequests.ts index 58c8eed..44e3b60 100644 --- a/src/pages/ClosedRequests/hooks/useClosedRequests.ts +++ b/src/pages/ClosedRequests/hooks/useClosedRequests.ts @@ -41,11 +41,28 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos // Always use user-scoped endpoint (not organization-wide) // Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user) // For organization-wide requests, use the "All Requests" screen (/requests) + // Only fetch rejected and closed requests - exclude approved + let statusFilter = filters?.status; + + // If user somehow selects approved (shouldn't be possible now), don't fetch + if (statusFilter === 'approved') { + setRequests([]); + setPagination({ + currentPage: 1, + totalPages: 1, + totalRecords: 0, + itemsPerPage, + }); + setLoading(false); + setRefreshing(false); + return; + } + const result = await workflowApi.listClosedByMe({ page, limit: itemsPerPage, search: filters?.search, - status: filters?.status, + status: statusFilter && statusFilter !== 'all' ? statusFilter : undefined, priority: filters?.priority, sortBy: filters?.sortBy, sortOrder: filters?.sortOrder @@ -56,9 +73,23 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos ? (result as any).data : []; + const mapped = transformClosedRequests(data); + // Filter out approved requests - only show rejected and closed + const filtered = mapped.filter(request => + request.status === 'rejected' || request.status === 'closed' + ); + setRequests(filtered); + // Set pagination data + // Note: Since we're filtering out approved requests client-side, + // the pagination count from backend may include approved requests. + // We'll use the filtered count for this page, but total records + // from backend is approximate. const paginationData = (result as any)?.pagination; if (paginationData) { + // If we filtered out some requests on this page, we need to adjust pagination + // For simplicity, we'll keep backend's total but note it may be slightly off + // A better solution would be to filter on backend, but for now this works setPagination({ currentPage: paginationData.page || 1, totalPages: paginationData.totalPages || 1, @@ -66,9 +97,6 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos itemsPerPage, }); } - - const mapped = transformClosedRequests(data); - setRequests(mapped); } catch (error) { console.error('[ClosedRequests] Error fetching requests:', error); setRequests([]); diff --git a/src/pages/ClosedRequests/types/closedRequests.types.ts b/src/pages/ClosedRequests/types/closedRequests.types.ts index 293d585..d00c280 100644 --- a/src/pages/ClosedRequests/types/closedRequests.types.ts +++ b/src/pages/ClosedRequests/types/closedRequests.types.ts @@ -8,7 +8,7 @@ export interface ClosedRequest { displayId?: string; title: string; description: string; - status: 'approved' | 'rejected' | 'closed'; + status: 'rejected' | 'closed'; priority: 'express' | 'standard'; initiator: { name: string; avatar: string }; createdAt: string; diff --git a/src/pages/ClosedRequests/utils/configMappers.ts b/src/pages/ClosedRequests/utils/configMappers.ts index cbed1ff..a7435d6 100644 --- a/src/pages/ClosedRequests/utils/configMappers.ts +++ b/src/pages/ClosedRequests/utils/configMappers.ts @@ -30,14 +30,6 @@ export function getPriorityConfig(priority: string): PriorityConfig { export function getStatusConfig(status: string): StatusConfig { switch (status) { - case 'approved': - return { - color: 'bg-emerald-100 text-emerald-800 border-emerald-300', - icon: CheckCircle, - iconColor: 'text-emerald-600', - label: 'Needs Closure', - description: 'Fully approved, awaiting initiator to finalize' - }; case 'closed': return { color: 'bg-slate-100 text-slate-800 border-slate-300', diff --git a/src/pages/ClosedRequests/utils/requestTransformers.ts b/src/pages/ClosedRequests/utils/requestTransformers.ts index f8ee67e..a596c12 100644 --- a/src/pages/ClosedRequests/utils/requestTransformers.ts +++ b/src/pages/ClosedRequests/utils/requestTransformers.ts @@ -11,7 +11,7 @@ export function transformClosedRequest(r: any): ClosedRequest { displayId: r.requestNumber || r.request_number || r.requestId, title: r.title, description: r.description, - status: (r.status || '').toString().toLowerCase() as 'approved' | 'rejected' | 'closed', + status: (r.status || '').toString().toLowerCase() as 'rejected' | 'closed', priority: (r.priority || '').toString().toLowerCase() as 'express' | 'standard', initiator: { name: r.initiator?.displayName || r.initiator?.email || 'โ€”', diff --git a/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts b/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts index 7381ab7..16df1c9 100644 --- a/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts +++ b/src/pages/CreateRequest/hooks/useCreateRequestHandlers.ts @@ -32,7 +32,7 @@ interface UseHandlersOptions { } export function useCreateRequestHandlers({ - selectedTemplate, + selectedTemplate: _selectedTemplate, setSelectedTemplate, updateFormData, formData, diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx index 234053a..4f1cf0e 100644 --- a/src/pages/Dashboard/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -157,6 +157,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { dateRange?: DateRange; startDate?: Date; endDate?: Date; + targetPage?: 'requests' | 'open-requests' | 'my-requests'; }) => { const url = buildFilterUrl(filters); onNavigate?.(url); @@ -211,11 +212,14 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { customStartDate={customStartDate} customEndDate={customEndDate} onKPIClick={handleKPIClick} + onNavigate={onNavigate} + userId={(user as any)?.userId} + userDisplayName={(user as any)?.displayName || (user as any)?.email} /> )} {/* Alerts and Activity Section */} -
+
onKPIClick(getFilterParams())} > -
- { - const express = priorityDistribution.find((p) => p.priority === 'express'); - const hours = express ? Number(express.avgCycleTimeHours) : 0; - return hours > 0 ? formatHoursMinutes(hours) : 'N/A'; - })()} - bgColor="bg-orange-50" - textColor="text-orange-600" - testId="stat-express-time" - onClick={(e) => { - e.stopPropagation(); - onKPIClick({ ...getFilterParams(), priority: 'express' }); - }} - /> - { - const standard = priorityDistribution.find((p) => p.priority === 'standard'); - const hours = standard ? Number(standard.avgCycleTimeHours) : 0; - return hours > 0 ? formatHoursMinutes(hours) : 'N/A'; - })()} - bgColor="bg-blue-50" - textColor="text-blue-600" - testId="stat-standard-time" - onClick={(e) => { - e.stopPropagation(); - onKPIClick({ ...getFilterParams(), priority: 'standard' }); - }} - /> +
+
+ { + const express = priorityDistribution.find((p) => p.priority === 'express'); + const hours = express ? Number(express.avgCycleTimeHours) : 0; + return hours > 0 ? formatHoursMinutes(hours) : 'N/A'; + })()} + bgColor="bg-orange-50" + textColor="text-orange-600" + testId="stat-express-time" + onClick={(e) => { + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), priority: 'express' }); + }} + /> + { + const standard = priorityDistribution.find((p) => p.priority === 'standard'); + const hours = standard ? Number(standard.avgCycleTimeHours) : 0; + return hours > 0 ? formatHoursMinutes(hours) : 'N/A'; + })()} + bgColor="bg-blue-50" + textColor="text-blue-600" + testId="stat-standard-time" + onClick={(e) => { + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), priority: 'standard' }); + }} + /> +
diff --git a/src/pages/Dashboard/components/sections/CriticalAlertsSection.tsx b/src/pages/Dashboard/components/sections/CriticalAlertsSection.tsx index d344831..2497e32 100644 --- a/src/pages/Dashboard/components/sections/CriticalAlertsSection.tsx +++ b/src/pages/Dashboard/components/sections/CriticalAlertsSection.tsx @@ -8,7 +8,8 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Flame, CheckCircle, ArrowRight } from 'lucide-react'; import { CriticalAlertCard } from '@/components/dashboard/CriticalAlertCard'; -import { CriticalRequest, CriticalAlertData } from '@/services/dashboard.service'; +import { CriticalRequest } from '@/services/dashboard.service'; +import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard'; import { getPageNumbers } from '../../utils/dashboardCalculations'; interface CriticalAlertsSectionProps { diff --git a/src/pages/Dashboard/components/sections/RecentActivitySection.tsx b/src/pages/Dashboard/components/sections/RecentActivitySection.tsx index 3ceb9af..3532884 100644 --- a/src/pages/Dashboard/components/sections/RecentActivitySection.tsx +++ b/src/pages/Dashboard/components/sections/RecentActivitySection.tsx @@ -124,7 +124,7 @@ export function RecentActivitySection({ variant={pageNum === pagination.page ? 'default' : 'outline'} size="sm" onClick={() => onPageChange(pageNum)} - className={`h-7 w-7 p-0 text-xs ${pageNum === pagination.page ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} + className={`h-7 w-7 p-0 text-xs ${pageNum === pagination.page ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`} data-testid={`activity-pagination-page-${pageNum}`} > {pageNum} diff --git a/src/pages/Dashboard/components/sections/TATBreachReport.tsx b/src/pages/Dashboard/components/sections/TATBreachReport.tsx index aed2770..c7eff44 100644 --- a/src/pages/Dashboard/components/sections/TATBreachReport.tsx +++ b/src/pages/Dashboard/components/sections/TATBreachReport.tsx @@ -6,7 +6,8 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { AlertTriangle } from 'lucide-react'; -import { CriticalRequest, CriticalAlertData, DateRange } from '@/services/dashboard.service'; +import { CriticalRequest, DateRange } from '@/services/dashboard.service'; +import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard'; import { Pagination } from '@/components/common/Pagination'; import { formatBreachTime } from '../../utils/dashboardCalculations'; import { KPIClickFilters } from '../../components/types/dashboard.types'; diff --git a/src/pages/Dashboard/components/sections/UserKPICards.tsx b/src/pages/Dashboard/components/sections/UserKPICards.tsx index 09e7992..3cb56c9 100644 --- a/src/pages/Dashboard/components/sections/UserKPICards.tsx +++ b/src/pages/Dashboard/components/sections/UserKPICards.tsx @@ -8,7 +8,8 @@ import { KPICard } from '@/components/dashboard/KPICard'; import { StatCard } from '@/components/dashboard/StatCard'; import { Progress } from '@/components/ui/progress'; import { DashboardKPIs, DateRange } from '@/services/dashboard.service'; -import { CriticalRequest, CriticalAlertData } from '@/services/dashboard.service'; +import { CriticalRequest } from '@/services/dashboard.service'; +import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard'; import { KPIClickFilters } from '../../components/types/dashboard.types'; interface UserKPICardsProps { @@ -18,6 +19,9 @@ interface UserKPICardsProps { customStartDate?: Date; customEndDate?: Date; onKPIClick: (filters: KPIClickFilters) => void; + onNavigate?: (page: string) => void; + userId?: string; + userDisplayName?: string; } export function UserKPICards({ @@ -27,6 +31,9 @@ export function UserKPICards({ customStartDate, customEndDate, onKPIClick, + onNavigate, + userId, + userDisplayName, }: UserKPICardsProps) { const getFilterParams = () => ({ dateRange, @@ -34,6 +41,19 @@ export function UserKPICards({ endDate: customEndDate, }); + const handleNavigateToApproverPerformance = () => { + if (!userId) return; + const params = new URLSearchParams(); + params.set('approverId', userId); + params.set('approverName', userDisplayName || 'My Performance'); + params.set('dateRange', dateRange); + if (dateRange === 'custom' && customStartDate && customEndDate) { + params.set('startDate', customStartDate.toISOString()); + params.set('endDate', customEndDate.toISOString()); + } + onNavigate?.(`/approver-performance?${params.toString()}`); + }; + const successRate = kpis && kpis.requestVolume.totalRequests > 0 ? ((kpis.requestVolume.approvedRequests / kpis.requestVolume.totalRequests) * 100) : 0; @@ -74,14 +94,14 @@ export function UserKPICards({ }} /> { e.stopPropagation(); - onKPIClick({ ...getFilterParams(), status: 'draft' }); + onKPIClick({ ...getFilterParams(), status: 'rejected' }); }} /> onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' })} + onJustifyClick={handleNavigateToApproverPerformance} + showJustifyButton={!!userId} >
{ + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' }); + }} /> { + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' }); + }} />
@@ -134,6 +165,9 @@ export function UserKPICards({ iconBgColor="bg-red-50" iconColor="text-red-600" testId="kpi-user-critical" + onClick={() => onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' })} + onJustifyClick={handleNavigateToApproverPerformance} + showJustifyButton={!!userId} >
{ + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' }); + }} /> { + e.stopPropagation(); + onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' }); + }} />
diff --git a/src/pages/Dashboard/components/types/dashboard.types.ts b/src/pages/Dashboard/components/types/dashboard.types.ts index c8055bc..f45f3b3 100644 --- a/src/pages/Dashboard/components/types/dashboard.types.ts +++ b/src/pages/Dashboard/components/types/dashboard.types.ts @@ -12,5 +12,6 @@ export interface KPIClickFilters { dateRange?: DateRange; startDate?: Date; endDate?: Date; + targetPage?: 'requests' | 'open-requests' | 'my-requests'; } diff --git a/src/pages/Dashboard/hooks/useDashboardData.ts b/src/pages/Dashboard/hooks/useDashboardData.ts index 649fc08..a165e38 100644 --- a/src/pages/Dashboard/hooks/useDashboardData.ts +++ b/src/pages/Dashboard/hooks/useDashboardData.ts @@ -4,7 +4,8 @@ import { useState, useCallback, useRef } from 'react'; import { dashboardService, type DashboardKPIs, type DateRange, type AIRemarkUtilization, type ApproverPerformance, type DepartmentStats, type PriorityDistribution, type UpcomingDeadline, type RecentActivity, type CriticalRequest } from '@/services/dashboard.service'; -import { ActivityData, CriticalAlertData } from '@/components/dashboard/ActivityFeedItem'; +import { ActivityData } from '@/components/dashboard/ActivityFeedItem'; +import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard'; interface UseDashboardDataOptions { isAdmin: boolean; diff --git a/src/pages/Dashboard/types/dashboard.types.ts b/src/pages/Dashboard/types/dashboard.types.ts index d9e4f52..36d21e2 100644 --- a/src/pages/Dashboard/types/dashboard.types.ts +++ b/src/pages/Dashboard/types/dashboard.types.ts @@ -2,8 +2,9 @@ * Dashboard-specific TypeScript types and interfaces */ -import { DateRange, DashboardKPIs, CriticalRequest, UpcomingDeadline, RecentActivity, DepartmentStats, PriorityDistribution, AIRemarkUtilization, ApproverPerformance } from '@/services/dashboard.service'; -import { ActivityData, CriticalAlertData } from '@/components/dashboard/ActivityFeedItem'; +import { DateRange, DashboardKPIs, CriticalRequest, UpcomingDeadline, DepartmentStats, PriorityDistribution, AIRemarkUtilization, ApproverPerformance } from '@/services/dashboard.service'; +import { ActivityData } from '@/components/dashboard/ActivityFeedItem'; +import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard'; export interface DashboardFilters { dateRange: DateRange; diff --git a/src/pages/Dashboard/utils/dashboardCalculations.ts b/src/pages/Dashboard/utils/dashboardCalculations.ts index 80db3dc..93e5dc3 100644 --- a/src/pages/Dashboard/utils/dashboardCalculations.ts +++ b/src/pages/Dashboard/utils/dashboardCalculations.ts @@ -41,17 +41,25 @@ export function getUpcomingDeadlinesNotBreached( /** * Format breach time for display + * Backend returns working hours, so we divide by 8 (working hours per day) not 24 */ export function formatBreachTime(hours: number): string { if (hours <= 0) return 'Just breached'; if (hours < 1) return `${Math.round(hours * 60)} min`; - if (hours < 24) { + + const WORKING_HOURS_PER_DAY = 8; + + // If less than one working day, show hours and minutes + if (hours < WORKING_HOURS_PER_DAY) { const h = Math.floor(hours); const m = Math.round((hours - h) * 60); return m > 0 ? `${h}h ${m}m` : `${h}h`; } - const days = Math.floor(hours / 24); - const remainingHours = hours % 24; + + // Convert working hours to working days + const days = Math.floor(hours / WORKING_HOURS_PER_DAY); + const remainingHours = hours % WORKING_HOURS_PER_DAY; + if (remainingHours > 0) { const h = Math.floor(remainingHours); const m = Math.round((remainingHours - h) * 60); diff --git a/src/pages/Dashboard/utils/dashboardNavigation.ts b/src/pages/Dashboard/utils/dashboardNavigation.ts index f9662d7..af10c2a 100644 --- a/src/pages/Dashboard/utils/dashboardNavigation.ts +++ b/src/pages/Dashboard/utils/dashboardNavigation.ts @@ -16,6 +16,7 @@ export function buildFilterUrl(filters: { dateRange?: DateRange; startDate?: Date; endDate?: Date; + targetPage?: 'requests' | 'open-requests' | 'my-requests'; }): string { const params = new URLSearchParams(); if (filters.status) params.set('status', filters.status); @@ -26,7 +27,14 @@ export function buildFilterUrl(filters: { if (filters.startDate) params.set('startDate', filters.startDate.toISOString()); if (filters.endDate) params.set('endDate', filters.endDate.toISOString()); const queryString = params.toString(); - return queryString ? `/requests?${queryString}` : '/requests'; + + // Determine target page + const targetPage = filters.targetPage || 'requests'; + const basePath = targetPage === 'open-requests' ? '/open-requests' + : targetPage === 'my-requests' ? '/my-requests' + : '/requests'; + + return queryString ? `${basePath}?${queryString}` : basePath; } export interface QuickAction { diff --git a/src/pages/DetailedReports/components/sections/RequestLifecycleReport.tsx b/src/pages/DetailedReports/components/sections/RequestLifecycleReport.tsx index 51c4022..6d26c0d 100644 --- a/src/pages/DetailedReports/components/sections/RequestLifecycleReport.tsx +++ b/src/pages/DetailedReports/components/sections/RequestLifecycleReport.tsx @@ -68,7 +68,7 @@ export function RequestLifecycleReport({
Request Lifecycle Report - End-to-end status with timeline and TAT compliance + End-to-end workflow status including all approval levels, approvers, dates, and TAT compliance
@@ -109,7 +110,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
- Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} + Submitted: {formatDateDDMMYYYY(request.submittedDate)}
diff --git a/src/pages/Notifications/Notifications.tsx b/src/pages/Notifications/Notifications.tsx index 6b69f3f..8c95ad2 100644 --- a/src/pages/Notifications/Notifications.tsx +++ b/src/pages/Notifications/Notifications.tsx @@ -47,8 +47,6 @@ export function Notifications({ onNavigate }: NotificationsProps) { setNotifications(notifs); setTotalCount(total); setTotalPages(Math.ceil(total / ITEMS_PER_PAGE)); - - console.log(`[Notifications] Loaded page ${page}, ${notifs.length} notifications, ${total} total`); } catch (error) { console.error('[Notifications] Failed to fetch:', error); } finally { diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx index 5295590..ccdb86f 100644 --- a/src/pages/OpenRequests/OpenRequests.tsx +++ b/src/pages/OpenRequests/OpenRequests.tsx @@ -1,4 +1,5 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -6,9 +7,9 @@ import { Input } from '@/components/ui/input'; import { Progress } from '@/components/ui/progress'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, Settings2, X, CheckCircle, XCircle } from 'lucide-react'; +import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle } from 'lucide-react'; import workflowApi from '@/services/workflowApi'; -import { formatDateShort } from '@/utils/dateFormatter'; +import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; interface Request { id: string; title: string; @@ -98,12 +99,18 @@ const getStatusConfig = (status: string) => { // getSLAUrgency removed - now using SLATracker component for real-time SLA display export function OpenRequests({ onViewRequest }: OpenRequestsProps) { - const [searchTerm, setSearchTerm] = useState(''); - const [priorityFilter, setPriorityFilter] = useState('all'); - const [statusFilter, setStatusFilter] = useState('all'); - const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>('created'); - const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); - const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); + const [searchParams] = useSearchParams(); + + // Initialize filters from URL params + const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || ''); + const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all'); + const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all'); + const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>( + (searchParams.get('sortBy') as 'created' | 'due' | 'priority' | 'sla') || 'created' + ); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>( + (searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc' + ); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); @@ -140,15 +147,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { sortBy: filters?.sortBy, sortOrder: filters?.sortOrder }); - console.log('[OpenRequests] API Response:', result); // Debug log // Extract data - workflowApi now returns { data: [], pagination: {} } const data = Array.isArray((result as any)?.data) ? (result as any).data : []; - console.log('[OpenRequests] Parsed data count:', data.length); // Debug log - // Set pagination data const pagination = (result as any)?.pagination; if (pagination) { @@ -231,31 +235,39 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { return pages; }; - // Initial fetch on mount + // Track if this is the initial mount + const isInitialMount = useRef(true); + + // Initial fetch on mount with URL params useEffect(() => { - fetchRequests(1, { - search: searchTerm || undefined, - status: statusFilter !== 'all' ? statusFilter : undefined, - priority: priorityFilter !== 'all' ? priorityFilter : undefined, - sortBy, - sortOrder - }); - }, [fetchRequests]); + if (isInitialMount.current) { + isInitialMount.current = false; + fetchRequests(1, { + search: searchTerm || undefined, + status: statusFilter !== 'all' ? statusFilter : undefined, + priority: priorityFilter !== 'all' ? priorityFilter : undefined, + sortBy, + sortOrder + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); // Only run on mount to use URL params // Fetch when filters or sorting change (with debouncing for search) useEffect(() => { + // Skip initial mount to avoid double fetch + if (isInitialMount.current) return; + // Debounce search: wait 500ms after user stops typing const timeoutId = setTimeout(() => { - if (items.length > 0 || loading) { // Only refetch if we've already loaded data once - setCurrentPage(1); // Reset to page 1 when filters change - fetchRequests(1, { - search: searchTerm || undefined, - status: statusFilter !== 'all' ? statusFilter : undefined, - priority: priorityFilter !== 'all' ? priorityFilter : undefined, - sortBy, - sortOrder - }); - } + setCurrentPage(1); // Reset to page 1 when filters change + fetchRequests(1, { + search: searchTerm || undefined, + status: statusFilter !== 'all' ? statusFilter : undefined, + priority: priorityFilter !== 'all' ? priorityFilter : undefined, + sortBy, + sortOrder + }); }, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns return () => clearTimeout(timeoutId); @@ -330,28 +342,17 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
-
- {activeFiltersCount > 0 && ( - - )} + {activeFiltersCount > 0 && ( -
+ )}
@@ -545,7 +546,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- Created: {request.createdAt !== 'โ€”' ? formatDateShort(request.createdAt) : 'โ€”'} + Created: {request.createdAt !== 'โ€”' ? formatDateDDMMYYYY(request.createdAt) : 'โ€”'}
@@ -621,7 +622,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { variant={pageNum === currentPage ? "default" : "outline"} size="sm" onClick={() => handlePageChange(pageNum)} - className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} + className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`} > {pageNum} diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx index adc6e4b..798d754 100644 --- a/src/pages/RequestDetail/RequestDetail.tsx +++ b/src/pages/RequestDetail/RequestDetail.tsx @@ -165,7 +165,6 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); if (tabParam) { - console.log('[RequestDetail] Auto-switching to tab:', tabParam); setActiveTab(tabParam); } }, [requestIdentifier]); @@ -227,47 +226,47 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests {/* Tabs */} -
- +
+ - - Overview + + Overview - - Workflow + + Workflow - - Docs + + Docs - - Activity + + Activity - - Work Notes + + Work Notes {unreadWorkNotes > 0 && ( diff --git a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx index 1c80390..8e01479 100644 --- a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx +++ b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx @@ -30,64 +30,66 @@ export function QuickActionsSidebar({ }: QuickActionsSidebarProps) { return (
- {/* Quick Actions Card */} - - - Quick Actions - - - {/* Add Approver */} - {isInitiator && request.status !== 'closed' && ( - - )} - - {/* Add Spectator */} - {!isSpectator && request.status !== 'closed' && ( - - )} - - {/* Approve/Reject Buttons */} -
- {!isSpectator && currentApprovalLevel && ( - <> - - - + {/* Quick Actions Card - Hide entire card for spectators */} + {!isSpectator && ( + + + Quick Actions + + + {/* Add Approver */} + {isInitiator && request.status !== 'closed' && ( + )} -
-
-
+ + {/* Add Spectator */} + {request.status !== 'closed' && ( + + )} + + {/* Approve/Reject Buttons */} +
+ {currentApprovalLevel && ( + <> + + + + )} +
+ + + )} {/* Spectators Card */} {request.spectators && request.spectators.length > 0 && ( diff --git a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx index 6c10006..6bf4545 100644 --- a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx +++ b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx @@ -24,7 +24,7 @@ interface OverviewTabProps { export function OverviewTab({ request, - isInitiator, + isInitiator: _isInitiator, needsClosure, conclusionRemark, setConclusionRemark, diff --git a/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx b/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx index 3efc5ec..da7b537 100644 --- a/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx +++ b/src/pages/RequestDetail/components/tabs/WorkNotesTab.tsx @@ -10,6 +10,7 @@ interface WorkNotesTabProps { mergedMessages: any[]; setWorkNoteAttachments: (attachments: any[]) => void; isInitiator: boolean; + isSpectator: boolean; currentLevels: any[]; onAddApprover: (email: string, tatHours: number, level: number) => Promise; } @@ -20,6 +21,7 @@ export function WorkNotesTab({ mergedMessages, setWorkNoteAttachments, isInitiator, + isSpectator, currentLevels, onAddApprover, }: WorkNotesTabProps) { @@ -32,6 +34,7 @@ export function WorkNotesTab({ messages={mergedMessages} onAttachmentsExtracted={setWorkNoteAttachments} isInitiator={isInitiator} + isSpectator={isSpectator} currentLevels={currentLevels} onAddApprover={onAddApprover} /> diff --git a/src/pages/RequestDetail/components/tabs/WorkflowTab.tsx b/src/pages/RequestDetail/components/tabs/WorkflowTab.tsx index 7d158e9..ae44a3c 100644 --- a/src/pages/RequestDetail/components/tabs/WorkflowTab.tsx +++ b/src/pages/RequestDetail/components/tabs/WorkflowTab.tsx @@ -30,10 +30,51 @@ export function WorkflowTab({ request, user, isInitiator, onSkipApprover, onRefr
{request.totalSteps && (() => { - const completedCount = request.approvalFlow?.filter((s: any) => s.status === 'approved').length || 0; + const totalSteps = request.totalSteps || 1; + // Use raw value if available (for completion detection), otherwise use clamped currentStep + // Backend sets currentLevel = levelNumber + 1 when final approver approves (closure step) + const rawCurrentStep = request.currentStepRaw !== undefined ? request.currentStepRaw : (request.currentStep || 1); + + // Count completed steps (approved or rejected) + const completedCount = request.approvalFlow?.filter((s: any) => { + const status = (s.status || '').toLowerCase(); + return status === 'approved' || status === 'rejected'; + }).length || 0; + + const allStepsCompleted = completedCount >= totalSteps; + const requestStatus = (request.status || '').toLowerCase(); + const isRequestCompleted = requestStatus === 'approved' || requestStatus === 'rejected' || requestStatus === 'closed'; + + // Backend sets currentLevel = levelNumber + 1 when final approver approves (closure step) + // So if currentStep > totalSteps, it means the workflow is in closure phase + const isInClosureStep = rawCurrentStep > totalSteps; + + // Ensure currentStep is valid: between 1 and totalSteps (for display purposes) + const currentStep = Math.min(Math.max(1, rawCurrentStep), totalSteps); + + // Show "Closure Step" when currentStep > totalSteps (workflow is in closure phase) + if (isInClosureStep) { + return ( + + Closure Step - {completedCount} of {totalSteps} steps completed + + ); + } + + // Show "Completed" when: + // 1. Request status is completed (approved/rejected/closed) + // 2. All steps are approved/rejected (completedCount >= totalSteps) + if (isRequestCompleted || allStepsCompleted) { + return ( + + Completed - {completedCount} of {totalSteps} steps + + ); + } + return ( - Step {request.currentStep} of {request.totalSteps} - {completedCount} completed + Step {currentStep} of {totalSteps} - {completedCount} completed ); })()} diff --git a/src/pages/Requests/components/RequestCard.tsx b/src/pages/Requests/components/RequestCard.tsx index 3544090..43bebca 100644 --- a/src/pages/Requests/components/RequestCard.tsx +++ b/src/pages/Requests/components/RequestCard.tsx @@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { User, ArrowRight, TrendingUp, Clock } from 'lucide-react'; import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import type { ConvertedRequest } from '../types/requests.types'; +import { formatDateDDMMYYYY } from '@/utils/dateFormatter'; interface RequestCardProps { request: ConvertedRequest; @@ -81,7 +82,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps) ID: {request.displayId || request.id} - Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} + Submitted: {formatDateDDMMYYYY(request.submittedDate)}
@@ -107,7 +108,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
- Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} + Submitted: {formatDateDDMMYYYY(request.submittedDate)}
diff --git a/src/services/adminApi.ts b/src/services/adminApi.ts index 868d19d..2cce581 100644 --- a/src/services/adminApi.ts +++ b/src/services/adminApi.ts @@ -39,7 +39,17 @@ export interface Holiday { } /** - * Get all admin configurations + * Get public configurations (accessible to all authenticated users) + * Returns non-sensitive configurations like DOCUMENT_POLICY, WORKFLOW_SHARING, TAT_SETTINGS + */ +export const getPublicConfigurations = async (category?: string): Promise => { + const params = category ? { category } : {}; + const response = await apiClient.get('/users/configurations', { params }); + return response.data.data; +}; + +/** + * Get all admin configurations (admin only) */ export const getAllConfigurations = async (category?: string): Promise => { const params = category ? { category } : {}; diff --git a/src/services/authApi.ts b/src/services/authApi.ts index 1c8270e..3e13da8 100644 --- a/src/services/authApi.ts +++ b/src/services/authApi.ts @@ -100,12 +100,6 @@ export async function exchangeCodeForTokens( code: string, redirectUri: string ): Promise { - console.log('๐Ÿ”„ Exchange Code for Tokens:', { - code: code ? `${code.substring(0, 10)}...` : 'MISSING', - redirectUri, - endpoint: `${API_BASE_URL}/auth/token-exchange`, - }); - try { const response = await apiClient.post( '/auth/token-exchange', @@ -122,21 +116,6 @@ export async function exchangeCodeForTokens( } ); - console.log('โœ… Token exchange successful', { - status: response.status, - statusText: response.statusText, - headers: response.headers, - contentType: response.headers['content-type'], - hasData: !!response.data, - dataType: typeof response.data, - dataIsArray: Array.isArray(response.data), - dataPreview: Array.isArray(response.data) - ? `Array[${response.data.length}]` - : typeof response.data === 'object' - ? JSON.stringify(response.data).substring(0, 100) - : String(response.data).substring(0, 100), - }); - // Check if response is an array (buffer issue) if (Array.isArray(response.data)) { console.error('โŒ Response is an array (buffer issue):', { @@ -158,9 +137,7 @@ export async function exchangeCodeForTokens( // Store id_token if available (needed for proper Okta logout) if (result.idToken) { TokenManager.setIdToken(result.idToken); - console.log('โœ… ID token stored for logout'); } - console.log('โœ… Tokens stored successfully'); } else { console.warn('โš ๏ธ Tokens missing in response', { result }); } @@ -219,13 +196,10 @@ export async function getCurrentUser() { */ export async function logout(): Promise { try { - console.log('๐Ÿ“ก Calling backend logout endpoint to clear httpOnly cookies...'); // Use withCredentials to ensure cookies are sent - const response = await apiClient.post('/auth/logout', {}, { + await apiClient.post('/auth/logout', {}, { withCredentials: true, // Ensure cookies are sent with request }); - console.log('๐Ÿ“ก Backend logout response:', response.status, response.statusText); - console.log('๐Ÿ“ก Response headers (check Set-Cookie):', response.headers); } catch (error: any) { console.error('๐Ÿ“ก Logout API error:', error); console.error('๐Ÿ“ก Error details:', { diff --git a/src/services/configService.ts b/src/services/configService.ts index b98c4e0..70ab96b 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -141,8 +141,6 @@ class ConfigService { const response = await apiClient.get('/config'); const serverConfig = response.data?.data || response.data; - console.log('[ConfigService] โœ… Loaded system configuration from server:', serverConfig); - // Merge with defaults (in case server doesn't return all fields) return { ...DEFAULT_CONFIG, diff --git a/src/styles/globals.css b/src/styles/globals.css index 2fc0c44..482a8f4 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -693,6 +693,16 @@ scrollbar-width: thin; scrollbar-color: rgb(203 213 225) transparent; } + + /* Hide scrollbar utility */ + .scrollbar-none { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + .scrollbar-none::-webkit-scrollbar { + display: none; /* Chrome, Safari and Opera */ + } } html { diff --git a/src/utils/dateFormatter.ts b/src/utils/dateFormatter.ts index ba3270c..6aa02d8 100644 --- a/src/utils/dateFormatter.ts +++ b/src/utils/dateFormatter.ts @@ -102,3 +102,44 @@ export function formatTimeOnly(dateString: string | Date | null | undefined): st } } +/** + * Format date with day first, then month (mmm), then year (e.g., "15 Nov 2025, 3:47:28 PM") + * Format: dd mmm yyyy (day first, then month name, then year) + * @param dateString - ISO date string or Date object + * @param includeTime - Whether to include time in the format + * @returns Formatted date string with day first + */ +export function formatDateDDMMYYYY(dateString: string | Date | null | undefined, includeTime: boolean = false): string { + if (!dateString) return 'N/A'; + + try { + const date = typeof dateString === 'string' ? new Date(dateString) : dateString; + + if (isNaN(date.getTime())) { + return 'Invalid Date'; + } + + // Always put day first, then month (mmm format), then year + const day = String(date.getDate()).padStart(2, '0'); + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const month = monthNames[date.getMonth()]; // Use month name (mmm) + const year = date.getFullYear(); + + if (includeTime) { + const hours24 = date.getHours(); + const hours12 = hours24 % 12 || 12; + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const ampm = hours24 >= 12 ? 'PM' : 'AM'; + // Format: dd mmm yyyy, hh:mm:ss AM/PM (day first, then month, then year) + return `${day} ${month} ${year}, ${hours12}:${minutes}:${seconds} ${ampm}`; + } + + // Format: dd mmm yyyy (day first, then month, then year) + return `${day} ${month} ${year}`; + } catch (error) { + console.error('Error formatting date:', error); + return String(dateString); + } +} + diff --git a/src/utils/slaTracker.ts b/src/utils/slaTracker.ts index 48bea19..d8bd862 100644 --- a/src/utils/slaTracker.ts +++ b/src/utils/slaTracker.ts @@ -24,7 +24,6 @@ async function ensureConfigLoaded() { WORK_START_DAY = config.workingHours.START_DAY; WORK_END_DAY = config.workingHours.END_DAY; configLoaded = true; - console.log('[SLA Tracker] โœ… Loaded working hours from backend:', { WORK_START_HOUR, WORK_END_HOUR }); } catch (error) { console.warn('[SLA Tracker] โš ๏ธ Using default working hours (9 AM - 6 PM)'); } @@ -223,23 +222,51 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date, } /** - * Format decimal hours to hours and minutes format (e.g., 2.6 -> "2h 23m") - * Simple format without considering working days + * Format decimal hours to hours and minutes format + * Considers 8 working hours = 1 day (consistent with TAT calculations and backend formatTime) + * Uses "hour" (singular) for 1 hour, "hours" (plural) for multiple hours + * Matches backend formatTime format from tatTimeUtils.ts for consistency + * Examples: 1 -> "1 hour", 8 -> "1 day", 9 -> "1 day 1 hour", 16 -> "2 days" */ export function formatHoursMinutes(hours: number | null | undefined): string { - if (hours === null || hours === undefined || hours < 0) return '0h'; - if (hours === 0) return '0h'; + if (hours === null || hours === undefined || hours < 0) return '0 hours'; + if (hours === 0) return '0 hours'; - const totalMinutes = Math.round(hours * 60); - const h = Math.floor(totalMinutes / 60); - const m = totalMinutes % 60; + const WORKING_HOURS_PER_DAY = 8; - if (h > 0 && m > 0) { - return `${h}h ${m}m`; - } else if (h > 0) { - return `${h}h`; + // If less than 1 hour, show minutes only + if (hours < 1) { + const m = Math.round(hours * 60); + return m > 0 ? `${m}m` : '0 hours'; + } + + // Calculate days and remaining hours (8 hours = 1 day) + // Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts + const days = Math.floor(hours / WORKING_HOURS_PER_DAY); + const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY); + const minutes = Math.round((hours % 1) * 60); + + // If we have days, format with days (matching backend format) + if (days > 0) { + const dayLabel = days === 1 ? 'day' : 'days'; + const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; + const minuteLabel = minutes === 1 ? 'min' : 'm'; + + if (minutes > 0) { + return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; + } else { + return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`; + } + } + + // No days, just hours and minutes + const hourLabel = remainingHrs === 1 ? 'hour' : 'hours'; + const minuteLabel = minutes === 1 ? 'min' : 'm'; + + if (minutes > 0) { + return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`; } else { - return `${m}m`; + return `${remainingHrs} ${hourLabel}`; } } diff --git a/src/utils/socket.ts b/src/utils/socket.ts index 097d012..643d468 100644 --- a/src/utils/socket.ts +++ b/src/utils/socket.ts @@ -29,8 +29,6 @@ export function getSocket(baseUrl?: string): Socket { const url = baseUrl || getSocketBaseUrl(); if (socket) return socket; - console.log('[Socket] Connecting to:', url); - socket = io(url, { withCredentials: true, transports: ['websocket', 'polling'], @@ -41,15 +39,15 @@ export function getSocket(baseUrl?: string): Socket { }); socket.on('connect', () => { - console.log('[Socket] Connected successfully:', socket?.id); + // Socket connected }); socket.on('connect_error', (error) => { console.error('[Socket] Connection error:', error.message); }); - socket.on('disconnect', (reason) => { - console.log('[Socket] Disconnected:', reason); + socket.on('disconnect', (_reason) => { + // Socket disconnected }); return socket; @@ -69,7 +67,6 @@ export function leaveRequestRoom(socket: Socket, requestId: string) { export function joinUserRoom(socket: Socket, userId: string) { socket.emit('join:user', { userId }); - console.log('[Socket] Joined personal notification room for user:', userId); } diff --git a/src/utils/tokenManager.ts b/src/utils/tokenManager.ts index 00c1e82..d8b8ed9 100644 --- a/src/utils/tokenManager.ts +++ b/src/utils/tokenManager.ts @@ -172,8 +172,6 @@ export class TokenManager { * IMPORTANT: This also sets a flag to prevent auto-authentication */ static clearAll(): void { - console.log('TokenManager.clearAll() - Starting cleanup...'); - // CRITICAL: Set logout flag in sessionStorage FIRST (before clearing) // This flag survives the redirect and prevents auto-authentication try { @@ -212,7 +210,6 @@ export class TokenManager { try { localStorage.removeItem(key); sessionStorage.removeItem(key); - console.log(`Removed ${key} from storage`); } catch (e) { console.warn(`Error removing ${key}:`, e); } @@ -233,7 +230,6 @@ export class TokenManager { allLocalStorageKeys.forEach(key => { try { localStorage.removeItem(key); - console.log(`Removed localStorage key: ${key}`); } catch (e) { console.warn(`Error removing localStorage key ${key}:`, e); } @@ -241,7 +237,6 @@ export class TokenManager { // Final clear as backup localStorage.clear(); - console.log('localStorage.clear() called'); } catch (e) { console.error('Error clearing localStorage:', e); } @@ -261,7 +256,6 @@ export class TokenManager { allSessionStorageKeys.forEach(key => { try { sessionStorage.removeItem(key); - console.log(`Removed sessionStorage key: ${key}`); } catch (e) { console.warn(`Error removing sessionStorage key ${key}:`, e); } @@ -269,7 +263,6 @@ export class TokenManager { // Final clear as backup sessionStorage.clear(); - console.log('sessionStorage.clear() called'); } catch (e) { console.error('Error clearing sessionStorage:', e); } @@ -328,23 +321,12 @@ export class TokenManager { }); }); - // Log remaining cookies (httpOnly cookies will still show here as they can't be cleared from JS) - console.log('โš ๏ธ Remaining cookies (httpOnly cookies cannot be cleared from JavaScript):', document.cookie); - console.log('โš ๏ธ httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this'); + // Note: httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this // Step 6: Verify cleanup - console.log('TokenManager.clearAll() - Verification:'); - console.log(`localStorage length: ${localStorage.length}`); - console.log(`sessionStorage length: ${sessionStorage.length}`); - console.log(`Cookies: ${document.cookie}`); - if (localStorage.length > 0 || sessionStorage.length > 0) { console.warn('WARNING: Storage not fully cleared!'); - console.log('Remaining localStorage keys:', Object.keys(localStorage)); - console.log('Remaining sessionStorage keys:', Object.keys(sessionStorage)); } - - console.log('TokenManager.clearAll() - Cleanup complete'); } /** diff --git a/vite.config.ts b/vite.config.ts index 4044b31..8e1ec1e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -193,5 +193,8 @@ export default defineConfig({ preserveSymlinks: false, }, }, + preview: { + port: 3000, + }, });