From 90f29c11bdd328d1040b40a67d43451056c01696 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 12 Nov 2025 11:15:39 +0530 Subject: [PATCH] dashboard enhanced and pagination added --- ADMIN_FEATURES_GUIDE.md | 537 ---- DEPLOYMENT_CONFIGURATION.md | 216 -- src/pages/ClosedRequests/ClosedRequests.tsx | 125 +- src/pages/Dashboard/Dashboard.tsx | 854 +++++-- src/pages/MyRequests/MyRequests.tsx | 198 +- src/pages/OpenRequests/OpenRequests.tsx | 358 +-- src/pages/RequestDetail/RequestDetail.tsx | 2475 ------------------- src/services/dashboard.service.ts | 121 +- src/services/workflowApi.ts | 18 +- 9 files changed, 1282 insertions(+), 3620 deletions(-) delete mode 100644 ADMIN_FEATURES_GUIDE.md delete mode 100644 DEPLOYMENT_CONFIGURATION.md delete mode 100644 src/pages/RequestDetail/RequestDetail.tsx diff --git a/ADMIN_FEATURES_GUIDE.md b/ADMIN_FEATURES_GUIDE.md deleted file mode 100644 index b1b91f8..0000000 --- a/ADMIN_FEATURES_GUIDE.md +++ /dev/null @@ -1,537 +0,0 @@ -# πŸŽ›οΈ Admin Features - Frontend Implementation Guide - -## βœ… What's Been Implemented - -I've successfully integrated the **Admin Configuration & Holiday Management** system into your Settings page! - ---- - -## πŸ“Š **Features Overview** - -### **1. System Configuration Manager** βš™οΈ -- View and edit all system configurations -- Organized by category (TAT Settings, Document Policy, AI Config, etc.) -- Different input types: text, number, sliders, toggles -- Validation rules applied -- Save/Reset to default functionality -- Real-time feedback - -### **2. Holiday Calendar Manager** πŸ“… -- View holidays by year -- Add/edit/delete holidays -- Holiday types: National, Regional, Organizational, Optional -- Recurring holiday support -- Month-wise organized view -- Visual calendar interface - -### **3. Access Control** πŸ”’ -- Admin-only tabs (System Configuration, Holiday Calendar) -- Non-admin users see only User Settings -- Graceful degradation for non-admin users - ---- - -## 🎨 **UI Components Created** - -### **New Files:** -1. `src/services/adminApi.ts` - API service layer -2. `src/components/admin/ConfigurationManager.tsx` - Configuration UI -3. `src/components/admin/HolidayManager.tsx` - Holiday management UI -4. `src/components/admin/index.ts` - Barrel export - -### **Updated Files:** -1. `src/pages/Settings/Settings.tsx` - Integrated admin features - ---- - -## πŸš€ **How to Use (As Admin)** - -### **Access Settings:** -``` -Navigate to: Settings (sidebar menu) -``` - -### **Tabs Available (Admin Users):** -1. **User Settings** - Personal preferences (notifications, appearance, etc.) -2. **System Configuration** - Admin-only system settings -3. **Holiday Calendar** - Admin-only holiday management - -### **Tabs Available (Non-Admin Users):** -1. User Settings only - ---- - -## βš™οΈ **System Configuration Tab** - -### **Categories:** - -**TAT Settings:** -- Default TAT for Express Priority (hours) -- Default TAT for Standard Priority (hours) -- First Reminder Threshold (%) - Slider -- Second Reminder Threshold (%) - Slider -- Working Day Start Hour -- Working Day End Hour - -**Document Policy:** -- Maximum File Upload Size (MB) -- Allowed File Types -- Document Retention Period (Days) - -**AI Configuration:** -- Enable AI Remark Generation - Toggle -- AI Remark Maximum Characters - -### **How to Edit:** -1. Navigate to **System Configuration** tab -2. Select a category -3. Modify the value -4. Click **Save** -5. Click **Reset to Default** to restore original value - -### **Visual Indicators:** -- 🟑 **Modified Badge** - Value has been changed -- 🟠 **Requires Restart Badge** - Server restart needed after save -- βœ… **Success Message** - Configuration saved -- ❌ **Error Message** - Validation failed or save error - ---- - -## πŸ“… **Holiday Calendar Tab** - -### **Features:** - -**Year Selector:** -- View holidays for any year (current year Β±2) -- Dropdown selection - -**Add Holiday:** -1. Click **+ Add Holiday** button -2. Fill in form: - - **Date** (required) - - **Holiday Name** (required) - - **Description** (optional) - - **Holiday Type**: National/Regional/Organizational/Optional - - **Recurring** checkbox (for annual holidays) -3. Click **Add Holiday** - -**Edit Holiday:** -1. Find holiday in list -2. Click **Edit** button -3. Modify fields -4. Click **Update Holiday** - -**Delete Holiday:** -1. Find holiday in list -2. Click **Delete** button -3. Confirm deletion - -**View:** -- Holidays grouped by month -- Badges show holiday type -- "Recurring" badge for annual holidays -- Description shown below holiday name - ---- - -## 🎯 **Configuration Input Types** - -### **Text Input:** -```tsx - -``` -- Used for: File types, text values - -### **Number Input:** -```tsx - -``` -- Used for: TAT hours, file sizes, retention days -- Validation: min/max enforced - -### **Slider:** -```tsx - -``` -- Used for: Percentage thresholds -- Visual feedback with current value display - -### **Toggle Switch:** -```tsx - -``` -- Used for: Boolean settings (enable/disable features) - ---- - -## πŸ“Š **Backend Integration** - -### **API Endpoints Used:** - -**Configuration:** -- `GET /api/admin/configurations` - Fetch all configs -- `GET /api/admin/configurations?category=TAT_SETTINGS` - Filter by category -- `PUT /api/admin/configurations/:configKey` - Update value -- `POST /api/admin/configurations/:configKey/reset` - Reset to default - -**Holidays:** -- `GET /api/admin/holidays?year=2025` - Get holidays for year -- `POST /api/admin/holidays` - Create holiday -- `PUT /api/admin/holidays/:holidayId` - Update holiday -- `DELETE /api/admin/holidays/:holidayId` - Delete holiday -- `POST /api/admin/holidays/bulk-import` - Import multiple holidays - ---- - -## πŸ”’ **Security** - -### **Frontend:** -- Admin tabs only visible if `user.isAdmin === true` -- Uses `useAuth()` context to check admin status - -### **Backend:** -- All admin endpoints protected with `authenticateToken` -- Additional `requireAdmin` middleware -- Non-admin users get 403 Forbidden - ---- - -## 🎨 **UI/UX Features** - -### **Success/Error Messages:** -- Green alert for successful operations -- Red alert for errors -- Auto-dismiss after 3 seconds - -### **Loading States:** -- Spinner while fetching data -- Disabled buttons during save -- "Saving..." button text - -### **Validation:** -- Required field checks -- Min/max validation for numbers -- Visual feedback for invalid input - -### **Responsive Design:** -- Grid layout for large screens -- Stack layout for mobile -- Scrollable content areas - ---- - -## πŸ“± **Mobile Responsiveness** - -### **Configuration Manager:** -- Tabs stack on small screens -- Full-width inputs -- Touch-friendly buttons - -### **Holiday Manager:** -- Year selector and Add button stack vertically -- Holiday cards full-width on mobile -- Edit/Delete buttons accessible - ---- - -## πŸ§ͺ **Testing Guide** - -### **Test Configuration Management:** - -1. **Login as Admin:** - - Navigate to Settings - - Verify 3 tabs visible - -2. **Edit TAT Setting:** - - Go to System Configuration β†’ TAT Settings - - Change "Default TAT for Express Priority" to 36 - - Click Save - - Verify success message - - Check backend: value should be updated in DB - -3. **Use Slider:** - - Go to "First Reminder Threshold" - - Drag slider to 60% - - Click Save - - Verify success message - -4. **Toggle AI Feature:** - - Go to AI Configuration - - Toggle "Enable AI Remark Generation" - - Click Save - - Verify success message - -5. **Reset to Default:** - - Edit any configuration - - Click "Reset to Default" - - Confirm - - Verify value restored - -### **Test Holiday Management:** - -1. **Add Holiday:** - - Go to Holiday Calendar tab - - Click "+ Add Holiday" - - Fill form: - - Date: 2025-12-31 - - Name: New Year's Eve - - Type: Organizational - - Click "Add Holiday" - - Verify appears in December section - -2. **Edit Holiday:** - - Find holiday in list - - Click "Edit" - - Change description - - Click "Update Holiday" - - Verify changes saved - -3. **Delete Holiday:** - - Find holiday - - Click "Delete" - - Confirm - - Verify removed from list - -4. **Change Year:** - - Select different year from dropdown - - Verify holidays load for that year - -### **Test as Non-Admin:** -1. Login as regular user -2. Navigate to Settings -3. Verify only User Settings visible -4. Verify blue info card: "Admin features not accessible" - ---- - -## πŸŽ“ **Configuration Categories** - -### **TAT_SETTINGS:** -- Default TAT hours -- Reminder thresholds -- Working hours -- **Impact:** Affects all new workflow requests - -### **DOCUMENT_POLICY:** -- Max file size -- Allowed file types -- Retention period -- **Impact:** Affects file uploads system-wide - -### **AI_CONFIGURATION:** -- Enable/disable AI -- Max characters -- **Impact:** Affects conclusion remark generation - -### **NOTIFICATION_RULES:** (Future) -- Email/SMS preferences -- Notification frequency -- **Impact:** Affects all notifications - -### **WORKFLOW_SHARING:** (Future) -- Spectator permissions -- Share link settings -- **Impact:** Affects collaboration features - ---- - -## πŸ”„ **Data Flow** - -``` -Settings Page (Admin User) - ↓ -ConfigurationManager Component - ↓ -adminApi.getAllConfigurations() - ↓ -Backend: GET /api/admin/configurations - ↓ -Fetch from admin_configurations table - ↓ -Display by category with appropriate UI components - ↓ -User edits value - ↓ -adminApi.updateConfiguration(key, value) - ↓ -Backend: PUT /api/admin/configurations/:key - ↓ -Update database - ↓ -Success message + refresh -``` - ---- - -## 🎨 **Styling Reference** - -### **Color Scheme:** -- **TAT Settings:** Blue (`bg-blue-100`) -- **Document Policy:** Purple (`bg-purple-100`) -- **Notification Rules:** Amber (`bg-amber-100`) -- **AI Configuration:** Pink (`bg-pink-100`) -- **Workflow Sharing:** Emerald (`bg-emerald-100`) - -### **Holiday Types:** -- **NATIONAL:** Red (`bg-red-100`) -- **REGIONAL:** Blue (`bg-blue-100`) -- **ORGANIZATIONAL:** Purple (`bg-purple-100`) -- **OPTIONAL:** Gray (`bg-gray-100`) - ---- - -## πŸ“‹ **Future Enhancements** - -### **Configuration Manager:** -1. ✨ Bulk edit mode -2. ✨ Search/filter configurations -3. ✨ Configuration history (audit trail) -4. ✨ Import/export configurations -5. ✨ Configuration templates - -### **Holiday Manager:** -1. ✨ Visual calendar view (month grid) -2. ✨ Drag-and-drop dates -3. ✨ Import from Google Calendar -4. ✨ Export to CSV/iCal -5. ✨ Holiday templates by country -6. ✨ Multi-select delete -7. ✨ Holiday conflict detection - ---- - -## πŸ› **Troubleshooting** - -### **Admin Tabs Not Showing?** - -**Check:** -1. Is user logged in? -2. Is `user.isAdmin` true in database? -3. Check console for authentication errors - -**Solution:** -```sql --- Make user admin -UPDATE users SET is_admin = true WHERE email = 'your-email@example.com'; -``` - -### **Configurations Not Loading?** - -**Check:** -1. Backend running? -2. Admin auth token valid? -3. Check network tab for 403/401 errors - -**Solution:** -- Verify JWT token is valid -- Check `requireAdmin` middleware is working - -### **Holidays Not Saving?** - -**Check:** -1. Date format correct? (YYYY-MM-DD) -2. Holiday name filled? -3. Check console for validation errors - ---- - -## πŸ“š **Component API** - -### **ConfigurationManager Props:** -```typescript -interface ConfigurationManagerProps { - onConfigUpdate?: () => void; // Callback after config updated -} -``` - -### **HolidayManager Props:** -```typescript -// No props required - fully self-contained -``` - ---- - -## ✨ **Sample Screenshots** (Describe UI) - -### **Admin View:** -``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ Settings β”‚ -β”‚ Manage your account settings... β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ [User Settings] [System Config] [Holidays]β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -System Configuration Tab: -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ [TAT_SETTINGS] [DOCUMENT_POLICY] [AI] β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -TAT SETTINGS -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ ⏰ Default TAT for Express Priority β”‚ -β”‚ Default turnaround time in hours β”‚ -β”‚ Default: 24 β”‚ -β”‚ [24] ← input β”‚ -β”‚ [Save] [Reset to Default] β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ - -β”‚ ⏰ First TAT Reminder Threshold (%) β”‚ -β”‚ Send first reminder at... β”‚ -β”‚ 50% ━━●━━━━━━━━ Range: 0-100 β”‚ -β”‚ [Save] [Reset to Default] β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ -``` - ---- - -## 🎯 **Key Points** - -1. βœ… **Admin Only:** System Config & Holidays tabs require admin role -2. βœ… **Real-time Validation:** Min/max enforced on save -3. βœ… **Auto-refresh:** Changes reflect immediately -4. βœ… **Holiday TAT Impact:** Holidays automatically excluded from STANDARD priority -5. βœ… **Mobile Friendly:** Responsive design for all screen sizes - ---- - -## πŸš€ **Next Steps** - -### **Immediate:** -1. βœ… **Test as Admin** - Login and verify tabs visible -2. βœ… **Add Holidays** - Import Indian holidays or add manually -3. βœ… **Configure TAT** - Set organization-specific TAT defaults - -### **Future:** -1. πŸ“‹ Add visual calendar view for holidays -2. πŸ“‹ Add configuration audit trail -3. πŸ“‹ Add bulk configuration import/export -4. πŸ“‹ Add user role management UI -5. πŸ“‹ Add notification template editor - ---- - -## πŸ“ž **Support** - -**Common Issues:** -- Admin tabs not showing? β†’ Check `user.isAdmin` in database -- Configurations not loading? β†’ Check backend logs, verify admin token -- Holidays not affecting TAT? β†’ Verify priority is STANDARD, restart backend - -**Documentation:** -- Backend Guide: `Re_Backend/HOLIDAY_AND_ADMIN_CONFIG_COMPLETE.md` -- Setup Guide: `Re_Backend/SETUP_COMPLETE.md` -- API Docs: `Re_Backend/docs/HOLIDAY_CALENDAR_SYSTEM.md` - ---- - -**Status:** βœ… **COMPLETE & READY TO USE!** - ---- - -**Last Updated:** November 4, 2025 -**Version:** 1.0.0 -**Team:** Royal Enfield Workflow - diff --git a/DEPLOYMENT_CONFIGURATION.md b/DEPLOYMENT_CONFIGURATION.md deleted file mode 100644 index d2ab627..0000000 --- a/DEPLOYMENT_CONFIGURATION.md +++ /dev/null @@ -1,216 +0,0 @@ -# Deployment Configuration Guide - -## Issue: Token Exchange Failing in Production - -### Problem Description -The OAuth token exchange is working locally but failing in production. This happens because the frontend doesn't know where the backend is deployed. - -### Root Cause -In `src/services/authApi.ts`: -```typescript -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000/api/v1'; -``` - -When `VITE_API_BASE_URL` is not set in production, it defaults to `localhost`, causing the deployed frontend to try calling a non-existent local backend. - -### Solution: Configure Environment Variables - -## 1. Create Environment Files - -Create these files in the `Re_Figma_Code` directory: - -### `.env.example` (template) -```env -# API Configuration -VITE_API_BASE_URL=http://localhost:5000/api/v1 -VITE_BASE_URL=http://localhost:5000 -``` - -### `.env.local` (local development) -```env -VITE_API_BASE_URL=http://localhost:5000/api/v1 -VITE_BASE_URL=http://localhost:5000 -``` - -### `.env.production` (production) -```env -# Replace with your actual backend URL -VITE_API_BASE_URL=https://your-backend-domain.com/api/v1 -VITE_BASE_URL=https://your-backend-domain.com -``` - -## 2. Platform-Specific Configuration - -### If deploying on **Vercel**: -1. Go to your project settings -2. Navigate to "Environment Variables" -3. Add: - - `VITE_API_BASE_URL` = `https://your-backend-url.com/api/v1` - - `VITE_BASE_URL` = `https://your-backend-url.com` -4. Select "Production" environment -5. Redeploy - -### If deploying on **Netlify**: -1. Go to Site settings β†’ Environment variables -2. Add: - - `VITE_API_BASE_URL` = `https://your-backend-url.com/api/v1` - - `VITE_BASE_URL` = `https://your-backend-url.com` -3. Redeploy the site - -### If deploying with **Docker**: -Add to your `docker-compose.yml` or Dockerfile: -```yaml -environment: - - VITE_API_BASE_URL=https://your-backend-url.com/api/v1 - - VITE_BASE_URL=https://your-backend-url.com -``` - -### If using **CI/CD** (GitHub Actions, etc.): -Add to your build secrets/variables and pass them during build: -```bash -VITE_API_BASE_URL=https://your-backend-url.com/api/v1 npm run build -``` - -## 3. Update .gitignore - -Ensure `.env.local` and `.env.production` are in `.gitignore`: -``` -# Environment files -.env.local -.env.production -.env*.local -``` - -Keep `.env.example` committed for reference. - -## 4. OAuth Flow Validation - -After configuring, verify the flow works: - -### Local Flow: -1. Frontend: `http://localhost:3000` -2. Backend: `http://localhost:5000` -3. Okta redirects to: `http://localhost:3000/login/callback?code=...` -4. Frontend calls: `http://localhost:5000/api/v1/auth/token-exchange` -5. Backend exchanges code with Okta using `redirect_uri=http://localhost:3000/login/callback` - -### Production Flow: -1. Frontend: `https://your-frontend.com` -2. Backend: `https://your-backend.com` -3. Okta redirects to: `https://your-frontend.com/login/callback?code=...` -4. Frontend calls: `https://your-backend.com/api/v1/auth/token-exchange` -5. Backend exchanges code with Okta using `redirect_uri=https://your-frontend.com/login/callback` - -## 5. Update Okta Configuration - -Ensure your Okta app has the production callback URL registered: - -1. Log in to Okta Admin Console -2. Go to Applications β†’ Your App β†’ General Settings -3. Under "Sign-in redirect URIs", add: - - `http://localhost:3000/login/callback` (for local dev) - - `https://your-frontend-domain.com/login/callback` (for production) -4. Under "Sign-out redirect URIs", add: - - `http://localhost:3000` (for local dev) - - `https://your-frontend-domain.com` (for production) -5. Save changes - -## 6. CORS Configuration - -Ensure your backend allows requests from your production frontend: - -In `Re_Backend/src/app.ts` or `server.ts`, update CORS: -```typescript -app.use(cors({ - origin: [ - 'http://localhost:3000', - 'http://localhost:5173', - 'https://your-frontend-domain.com' // Add your production frontend URL - ], - credentials: true -})); -``` - -## 7. Testing Checklist - -- [ ] Environment variables are set in deployment platform -- [ ] Backend URL is reachable from frontend -- [ ] Okta callback URLs include production URLs -- [ ] CORS allows production frontend origin -- [ ] Backend is deployed and running -- [ ] Try login flow in production -- [ ] Check browser console for API call URLs -- [ ] Verify token exchange endpoint is being called correctly - -## 8. Debugging - -If still failing, check: - -### Frontend Console: -```javascript -// Should show your production backend URL -console.log('API_BASE_URL:', import.meta.env.VITE_API_BASE_URL); -``` - -### Network Tab: -- Look for the `/auth/token-exchange` request -- Verify it's calling your production backend, not localhost -- Check response status and error messages - -### Backend Logs: -- Check if token exchange request is reaching the backend -- Look for Okta API errors -- Verify redirect_uri matches what Okta expects - -## Quick Fix Commands - -### 1. Create environment files: -```bash -cd Re_Figma_Code - -# Create .env.local -echo "VITE_API_BASE_URL=http://localhost:5000/api/v1" > .env.local -echo "VITE_BASE_URL=http://localhost:5000" >> .env.local - -# Create .env.production (update URLs!) -echo "VITE_API_BASE_URL=https://your-backend-url.com/api/v1" > .env.production -echo "VITE_BASE_URL=https://your-backend-url.com" >> .env.production -``` - -### 2. Test locally: -```bash -npm run dev -``` - -### 3. Build for production: -```bash -npm run build -``` - -The build process will use `.env.production` values. - -## Common Issues - -### Issue 1: "Network Error" or "Failed to fetch" -**Cause**: CORS not configured or backend URL wrong -**Fix**: Check CORS settings and verify backend URL - -### Issue 2: "Invalid redirect_uri" -**Cause**: Okta doesn't have production callback URL -**Fix**: Add production URL to Okta app settings - -### Issue 3: Still calling localhost in production -**Cause**: Environment variable not loaded during build -**Fix**: Ensure variables are set BEFORE building, not at runtime - -### Issue 4: 404 on /api/v1/auth/token-exchange -**Cause**: Wrong backend URL or backend not deployed -**Fix**: Verify backend is running and URL is correct - -## Notes - -- Vite environment variables MUST start with `VITE_` to be exposed to client -- Environment variables are embedded at **build time**, not runtime -- Changing env vars requires rebuilding the frontend -- Never commit `.env.local` or `.env.production` with real URLs to git - diff --git a/src/pages/ClosedRequests/ClosedRequests.tsx b/src/pages/ClosedRequests/ClosedRequests.tsx index f616030..6eca2a6 100644 --- a/src/pages/ClosedRequests/ClosedRequests.tsx +++ b/src/pages/ClosedRequests/ClosedRequests.tsx @@ -101,26 +101,38 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); + + // Pagination states + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalRecords, setTotalRecords] = useState(0); + const [itemsPerPage] = useState(10); - const fetchRequests = async () => { + const fetchRequests = async (page: number = 1) => { try { - setLoading(true); - // Clear old data first - setItems([]); + if (page === 1) { + setLoading(true); + setItems([]); + } - const result = await workflowApi.listClosedByMe({ page: 1, limit: 50 }); + const result = await workflowApi.listClosedByMe({ page, limit: itemsPerPage }); console.log('[ClosedRequests] API Response:', result); // Debug log - const data = Array.isArray((result as any)?.data) - ? (result as any).data - : Array.isArray((result as any)?.data?.data) - ? (result as any).data.data - : Array.isArray(result as any) - ? (result as any) + // Extract data - workflowApi now returns { data: [], pagination: {} } + const data = Array.isArray((result as any)?.data) + ? (result as any).data : []; console.log('[ClosedRequests] Parsed data count:', data.length); // Debug log + // Set pagination data + const pagination = (result as any)?.pagination; + if (pagination) { + setCurrentPage(pagination.page || 1); + setTotalPages(pagination.totalPages || 1); + setTotalRecords(pagination.total || 0); + } + const mapped: Request[] = data .filter((r: any) => ['APPROVED', 'REJECTED', 'CLOSED'].includes((r.status || '').toString())) .map((r: any) => ({ @@ -151,11 +163,35 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { const handleRefresh = () => { setRefreshing(true); - fetchRequests(); + fetchRequests(currentPage); + }; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage); + fetchRequests(newPage); + } + }; + + const getPageNumbers = () => { + const pages = []; + const maxPagesToShow = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); + let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); + + if (endPage - startPage < maxPagesToShow - 1) { + startPage = Math.max(1, endPage - maxPagesToShow + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; }; useEffect(() => { - fetchRequests(); + fetchRequests(1); }, []); const filteredAndSortedRequests = useMemo(() => { @@ -233,7 +269,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
- {loading ? 'Loading…' : `${filteredAndSortedRequests.length} closed`} + {loading ? 'Loading…' : `${totalRecords || items.length} closed`} requests + + {currentPage > 3 && totalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers().map((pageNum) => ( + + ))} + + {currentPage < totalPages - 2 && totalPages > 5 && ( + <> + ... + + + )} + + +
+ + + + )} ); } diff --git a/src/pages/Dashboard/Dashboard.tsx b/src/pages/Dashboard/Dashboard.tsx index 90492fa..d383fe3 100644 --- a/src/pages/Dashboard/Dashboard.tsx +++ b/src/pages/Dashboard/Dashboard.tsx @@ -28,9 +28,10 @@ import { PieChart, Calendar } from 'lucide-react'; -import { dashboardService, type DashboardKPIs, type RecentActivity, type CriticalRequest, type DateRange } from '@/services/dashboard.service'; +import { dashboardService, type DashboardKPIs, type RecentActivity, type CriticalRequest, type DateRange, type AIRemarkUtilization, type ApproverPerformance } from '@/services/dashboard.service'; import { differenceInMinutes, differenceInHours, differenceInDays } from 'date-fns'; import { useAuth } from '@/contexts/AuthContext'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart as RechartsPieChart, Pie, Cell, BarChart, Bar } from 'recharts'; interface DashboardProps { onNavigate?: (page: string) => void; @@ -59,17 +60,88 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { const [departmentStats, setDepartmentStats] = useState([]); const [priorityDistribution, setPriorityDistribution] = useState([]); const [upcomingDeadlines, setUpcomingDeadlines] = useState([]); + const [aiRemarkUtilization, setAiRemarkUtilization] = useState(null); + const [approverPerformance, setApproverPerformance] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); // Filter states const [dateRange, setDateRange] = useState('month'); + + // Pagination states + const [activityPage, setActivityPage] = useState(1); + const [activityTotalPages, setActivityTotalPages] = useState(1); + const [activityTotalRecords, setActivityTotalRecords] = useState(0); + + const [criticalPage, setCriticalPage] = useState(1); + const [criticalTotalPages, setCriticalTotalPages] = useState(1); + const [criticalTotalRecords, setCriticalTotalRecords] = useState(0); + + const [deadlinesPage, setDeadlinesPage] = useState(1); + const [deadlinesTotalPages, setDeadlinesTotalPages] = useState(1); + const [deadlinesTotalRecords, setDeadlinesTotalRecords] = useState(0); + + const [approverPage, setApproverPage] = useState(1); + const [approverTotalPages, setApproverTotalPages] = useState(1); + const [approverTotalRecords, setApproverTotalRecords] = useState(0); // Determine user role const isAdmin = useMemo(() => { return (user as any)?.isAdmin || false; }, [user]); + // Fetch recent activities with pagination + const fetchRecentActivities = useCallback(async (page: number = 1) => { + try { + const result = await dashboardService.getRecentActivity(page, 10); + setRecentActivity(result.activities); + setActivityPage(result.pagination.currentPage); + setActivityTotalPages(result.pagination.totalPages); + setActivityTotalRecords(result.pagination.totalRecords); + } catch (error) { + console.error('Failed to fetch recent activities:', error); + } + }, []); + + // Fetch critical requests with pagination + const fetchCriticalRequests = useCallback(async (page: number = 1) => { + try { + const result = await dashboardService.getCriticalRequests(page, 10); + setCriticalRequests(result.criticalRequests); + setCriticalPage(result.pagination.currentPage); + setCriticalTotalPages(result.pagination.totalPages); + setCriticalTotalRecords(result.pagination.totalRecords); + } catch (error) { + console.error('Failed to fetch critical requests:', error); + } + }, []); + + // Fetch upcoming deadlines with pagination + const fetchUpcomingDeadlines = useCallback(async (page: number = 1) => { + try { + const result = await dashboardService.getUpcomingDeadlines(page, 10); + setUpcomingDeadlines(result.deadlines); + setDeadlinesPage(result.pagination.currentPage); + setDeadlinesTotalPages(result.pagination.totalPages); + setDeadlinesTotalRecords(result.pagination.totalRecords); + } catch (error) { + console.error('Failed to fetch upcoming deadlines:', error); + } + }, []); + + // Fetch approver performance with pagination + const fetchApproverPerformance = useCallback(async (selectedDateRange: DateRange = 'month', page: number = 1) => { + try { + const result = await dashboardService.getApproverPerformance(selectedDateRange, page, 10); + setApproverPerformance(result.performance); + setApproverPage(result.pagination.currentPage); + setApproverTotalPages(result.pagination.totalPages); + setApproverTotalRecords(result.pagination.totalRecords); + } catch (error) { + console.error('Failed to fetch approver performance:', error); + } + }, []); + // Fetch dashboard data const fetchDashboardData = useCallback(async (showRefreshing = false, selectedDateRange: DateRange = 'month') => { try { @@ -82,26 +154,49 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { // Fetch all data in parallel const [ kpisData, - activityData, - criticalData, + activityResult, + criticalResult, deptStats, priorityDist, - deadlines + deadlinesResult, + aiUtilization, + approverResult ] = await Promise.all([ dashboardService.getKPIs(selectedDateRange), - dashboardService.getRecentActivity(10), - dashboardService.getCriticalRequests(), + dashboardService.getRecentActivity(1, 10), + dashboardService.getCriticalRequests(1, 10), dashboardService.getDepartmentStats(selectedDateRange), dashboardService.getPriorityDistribution(selectedDateRange), - dashboardService.getUpcomingDeadlines(10) + dashboardService.getUpcomingDeadlines(1, 10), + dashboardService.getAIRemarkUtilization(selectedDateRange), + dashboardService.getApproverPerformance(selectedDateRange, 1, 10) ]); setKpis(kpisData); - setRecentActivity(activityData); - setCriticalRequests(criticalData); + setRecentActivity(activityResult.activities); + setActivityPage(activityResult.pagination.currentPage); + setActivityTotalPages(activityResult.pagination.totalPages); + setActivityTotalRecords(activityResult.pagination.totalRecords); + + setCriticalRequests(criticalResult.criticalRequests); + setCriticalPage(criticalResult.pagination.currentPage); + setCriticalTotalPages(criticalResult.pagination.totalPages); + setCriticalTotalRecords(criticalResult.pagination.totalRecords); + setDepartmentStats(deptStats); setPriorityDistribution(priorityDist); - setUpcomingDeadlines(deadlines); + + setUpcomingDeadlines(deadlinesResult.deadlines); + setDeadlinesPage(deadlinesResult.pagination.currentPage); + setDeadlinesTotalPages(deadlinesResult.pagination.totalPages); + setDeadlinesTotalRecords(deadlinesResult.pagination.totalRecords); + + setAiRemarkUtilization(aiUtilization); + + setApproverPerformance(approverResult.performance); + setApproverPage(approverResult.pagination.currentPage); + setApproverTotalPages(approverResult.pagination.totalPages); + setApproverTotalRecords(approverResult.pagination.totalRecords); } catch (error) { console.error('Failed to fetch dashboard data:', error); } finally { @@ -114,6 +209,52 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { fetchDashboardData(true, dateRange); }; + // Pagination handlers + const handleActivityPageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= activityTotalPages) { + setActivityPage(newPage); + fetchRecentActivities(newPage); + } + }; + + const handleCriticalPageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= criticalTotalPages) { + setCriticalPage(newPage); + fetchCriticalRequests(newPage); + } + }; + + const handleDeadlinesPageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= deadlinesTotalPages) { + setDeadlinesPage(newPage); + fetchUpcomingDeadlines(newPage); + } + }; + + const handleApproverPageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= approverTotalPages) { + setApproverPage(newPage); + fetchApproverPerformance(dateRange, newPage); + } + }; + + const getPageNumbers = (currentPage: number, totalPages: number) => { + const pages = []; + const maxPagesToShow = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); + let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); + + if (endPage - startPage < maxPagesToShow - 1) { + startPage = Math.max(1, endPage - maxPagesToShow + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; + useEffect(() => { fetchDashboardData(false, dateRange); }, [fetchDashboardData, dateRange]); @@ -142,10 +283,24 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { return `${diffDay} day${diffDay > 1 ? 's' : ''} ago`; }; - // Calculate progress for critical requests + // Calculate TAT progress for critical requests (how much time has been used) const calculateProgress = (request: CriticalRequest) => { - if (!request.currentLevel || !request.totalLevels) return 0; - return Math.round((request.currentLevel / request.totalLevels) * 100); + if (!request.originalTATHours || request.originalTATHours === 0) return 0; + + const originalTAT = request.originalTATHours; + const remainingTAT = request.totalTATHours; + + // If breached (negative remaining), show 100% + if (remainingTAT <= 0) return 100; + + // Calculate elapsed time + const elapsedTAT = originalTAT - remainingTAT; + + // Calculate percentage used + const percentageUsed = (elapsedTAT / originalTAT) * 100; + + // Ensure it's between 0 and 100 + return Math.min(100, Math.max(0, Math.round(percentageUsed))); }; // Format remaining time (can be negative if breached) @@ -552,10 +707,10 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { )} -
+
{/* High Priority Alerts */} - - + +
@@ -573,7 +728,8 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
- + +
{criticalRequests.length === 0 ? (
@@ -581,7 +737,8 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {

All requests are within TAT

) : ( - criticalRequests.slice(0, 3).map((request) => ( + <> + {criticalRequests.slice(0, 3).map((request) => (
onNavigate?.(`request/${request.requestNumber}`)}>
@@ -603,32 +760,34 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
- Progress + TAT Used {calculateProgress(request)}%
= 80 ? '[&>div]:bg-red-600' : calculateProgress(request) >= 50 ? '[&>div]:bg-orange-500' : '[&>div]:bg-green-600'}`} />
- )) + ))} + + )} - +
{/* Recent Activity */} - - + +
@@ -655,7 +814,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
- +
{recentActivity.length === 0 ? (
@@ -790,6 +949,80 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { ); }) )} + + {/* Pagination Controls */} + {activityTotalPages > 1 && recentActivity.length > 0 && ( +
+
+ Showing {((activityPage - 1) * 10) + 1} to {Math.min(activityPage * 10, activityTotalRecords)} of {activityTotalRecords} activities +
+ +
+ {/* Previous Button */} + + + {/* Page Numbers */} + {activityPage > 3 && activityTotalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers(activityPage, activityTotalPages).map((pageNum) => ( + + ))} + + {activityPage < activityTotalPages - 2 && activityTotalPages > 5 && ( + <> + ... + + + )} + + {/* Next Button */} + +
+
+ )}
@@ -997,10 +1230,12 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
-
- +
+
+ +
- Department-wise Summary + Department-wise Workflow Summary Workflow distribution across departments @@ -1013,36 +1248,37 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
-
- - - - - - - - - - - - - {departmentStats.map((dept, idx) => ( - - - - - - - - - ))} - -
DepartmentTotalApprovedRejectedPendingRate
{dept.department}{dept.totalRequests}{dept.approved}{dept.rejected}{dept.inProgress} - = 80 ? 'bg-green-50 text-green-700 border-green-200' : 'bg-yellow-50 text-yellow-700 border-yellow-200'}`}> - {dept.approvalRate}% - -
-
+ + + + + + + + + + + +
)} @@ -1050,70 +1286,69 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) { {/* Priority Distribution Report */} {isAdmin && priorityDistribution.length > 0 && ( - -
-
- -
- Priority Distribution - - Express vs Standard performance analysis - -
+ +
+
+ +
+
+ Priority Distribution Report + Express vs Standard workflow analysis
-
-
+ {/* Summary Cards */} +
{priorityDistribution.map((priority, idx) => { const avgCycleTime = Number(priority.avgCycleTimeHours) || 0; - const complianceRate = Number(priority.complianceRate) || 0; + const isExpress = priority.priority === 'express'; + const bgColor = isExpress ? 'bg-red-50' : 'bg-blue-50'; + const dotColor = isExpress ? 'bg-red-500' : 'bg-blue-500'; return ( -
-
-

- {priority.priority === 'express' && } - {priority.priority === 'standard' && } - {priority.priority} Priority -

- {priority.totalCount} requests +
+
+
+ {priority.priority}
-
-
-
- Avg Cycle Time - {avgCycleTime.toFixed(1)} hours -
- -
-
-
-

Approved

-

{priority.approvedCount || 0}

-
-
-

Breached

-

{priority.breachedCount || 0}

-
-
-
-
- TAT Compliance - = 80 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}> - {complianceRate}% - -
-
+
+ {priority.totalCount} +
+
+ Avg: {avgCycleTime.toFixed(1)}h cycle
); })}
+ + {/* Pie Chart */} +
+ + + ({ + name: p.priority.charAt(0).toUpperCase() + p.priority.slice(1), + value: p.totalCount, + percentage: Math.round((p.totalCount / priorityDistribution.reduce((sum, item) => sum + item.totalCount, 0)) * 100) + }))} + cx="50%" + cy="50%" + labelLine={false} + label={({ name, percentage }) => `${name}: ${percentage}%`} + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {priorityDistribution.map((priority, index) => ( + + ))} + + + + +
)} @@ -1123,84 +1358,121 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
-
- +
+
+ +
- TAT Breach Report + TAT Breach Report - Requests breached or approaching limits + Requests that breached defined turnaround time
- + + {criticalRequests.length} {criticalRequests.length === 1 ? 'Breach' : 'Breaches'} +
- - - - - - - - + + + + + + + {criticalRequests.map((req, idx) => ( - - - - + - - - - ))}
Request IDTitlePriorityLevelBreachesTAT StatusAction
Request IDTitlePriorityApproverBreach TimeLevel
+ onNavigate?.(`request/${req.requestNumber}`)}> {req.requestNumber} {req.title} - + {req.title} + {req.priority} {req.currentLevel}/{req.totalLevels} - {req.breachCount > 0 ? ( - {req.breachCount} - ) : ( - - - )} + + Level {req.currentLevel} - {req.breachCount > 0 ? ( -
- - {req.breachCount} Breached - - {req.totalTATHours > 0 && ( - - Current: {formatRemainingTime(req)} - - )} -
- ) : ( - - {formatRemainingTime(req)} - - )} +
+ + {formatRemainingTime(req)} + - + + {req.currentLevel}/{req.totalLevels}
+ + {/* Pagination Controls for TAT Breach Report */} + {criticalTotalPages > 1 && ( +
+
+ Showing {((criticalPage - 1) * 10) + 1} to {Math.min(criticalPage * 10, criticalTotalRecords)} of {criticalTotalRecords} critical requests +
+ +
+ + + {criticalPage > 3 && criticalTotalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers(criticalPage, criticalTotalPages).map((pageNum) => ( + + ))} + + {criticalPage < criticalTotalPages - 2 && criticalTotalPages > 5 && ( + <> + ... + + + )} + + +
+
+ )}
)} @@ -1267,7 +1539,257 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
); })} -
+
+ + {/* Pagination Controls for Upcoming Deadlines */} + {deadlinesTotalPages > 1 && ( +
+
+ Showing {((deadlinesPage - 1) * 10) + 1} to {Math.min(deadlinesPage * 10, deadlinesTotalRecords)} of {deadlinesTotalRecords} deadlines +
+ +
+ + + {deadlinesPage > 3 && deadlinesTotalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers(deadlinesPage, deadlinesTotalPages).map((pageNum) => ( + + ))} + + {deadlinesPage < deadlinesTotalPages - 2 && deadlinesTotalPages > 5 && ( + <> + ... + + + )} + + +
+
+ )} + + + )} + + {/* AI Remark Utilization Report (Admin Only) */} + {isAdmin && aiRemarkUtilization && ( + + +
+
+ +
+
+ AI Remark Utilization Report + AI-generated remarks usage and edits +
+
+
+ + {/* Summary Cards */} +
+
+
Total Usage
+
{aiRemarkUtilization.totalUsage}
+
+
+
Total Edits
+
{aiRemarkUtilization.totalEdits}
+
+
+
Edit Rate
+
{aiRemarkUtilization.editRate}%
+
+
+ + {/* Monthly Trends Line Graph */} + {aiRemarkUtilization.monthlyTrends && aiRemarkUtilization.monthlyTrends.length > 0 && ( + + + + + + + + + + + + )} +
+
+ )} + + {/* Approver Performance Report (Admin Only) */} + {isAdmin && approverPerformance.length > 0 && ( + + +
+
+ +
+
+ Approver Performance Report + Response time & TAT compliance tracking +
+
+
+ +
+ {approverPerformance.map((approver, idx) => { + const tatPercent = approver.tatCompliancePercent; + const getTatColorClass = (tat: number) => { + if (tat >= 95) return 'bg-green-100 text-green-700'; + if (tat >= 90) return 'bg-blue-100 text-blue-700'; + if (tat >= 85) return 'bg-orange-100 text-orange-700'; + return 'bg-red-100 text-red-700'; + }; + + return ( +
+
+
+
+ {idx + 1} +
+
+
{approver.approverName}
+
{approver.totalApproved} requests approved
+
+
+ + {tatPercent}% TAT + +
+
+
+
Avg Response
+
{approver.avgResponseHours.toFixed(1)}h
+
+
+
Pending
+
+ {approver.pendingCount} +
+
+
+
+ ); + })} +
+ + {/* Pagination Controls for Approver Performance */} + {approverTotalPages > 1 && ( +
+
+ Showing {((approverPage - 1) * 10) + 1} to {Math.min(approverPage * 10, approverTotalRecords)} of {approverTotalRecords} approvers +
+ +
+ + + {approverPage > 3 && approverTotalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers(approverPage, approverTotalPages).map((pageNum) => ( + + ))} + + {approverPage < approverTotalPages - 2 && approverTotalPages > 5 && ( + <> + ... + + + )} + + +
+
+ )}
)} diff --git a/src/pages/MyRequests/MyRequests.tsx b/src/pages/MyRequests/MyRequests.tsx index f2a610a..bae913c 100644 --- a/src/pages/MyRequests/MyRequests.tsx +++ b/src/pages/MyRequests/MyRequests.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { FileText, @@ -98,44 +99,81 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr const [apiRequests, setApiRequests] = useState([]); const [loading, setLoading] = useState(false); const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false); + + // Pagination states + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalRecords, setTotalRecords] = useState(0); + const [itemsPerPage] = useState(10); + + const fetchMyRequests = async (page: number = 1) => { + try { + if (page === 1) { + setLoading(true); + setApiRequests([]); + } + + const result = await workflowApi.listMyWorkflows({ page, limit: itemsPerPage }); + console.log('[MyRequests] API Response:', result); // Debug log + + // Extract data - workflowApi now returns { data: [], pagination: {} } + const items = Array.isArray((result as any)?.data) + ? (result as any).data + : []; + + console.log('[MyRequests] Parsed items:', items); // Debug log + + setApiRequests(items); + setHasFetchedFromApi(true); + + // Set pagination data + const pagination = (result as any)?.pagination; + if (pagination) { + setCurrentPage(pagination.page || 1); + setTotalPages(pagination.totalPages || 1); + setTotalRecords(pagination.total || 0); + } + } catch (error) { + console.error('[MyRequests] Error fetching requests:', error); + setApiRequests([]); + setHasFetchedFromApi(true); + } finally { + setLoading(false); + } + }; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage); + fetchMyRequests(newPage); + } + }; + + const getPageNumbers = () => { + const pages = []; + const maxPagesToShow = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); + let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); + + if (endPage - startPage < maxPagesToShow - 1) { + startPage = Math.max(1, endPage - maxPagesToShow + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; + }; useEffect(() => { - let mounted = true; - (async () => { - try { - setLoading(true); - // Clear old data first - setApiRequests([]); - - const result = await workflowApi.listMyWorkflows({ page: 1, limit: 20 }); - console.log('[MyRequests] API Response:', result); // Debug log - - // Handle nested data structure from API - const items = result?.data?.data ? result.data.data : - Array.isArray(result?.data) ? result.data : - Array.isArray(result) ? result : []; - - console.log('[MyRequests] Parsed items:', items); // Debug log - - if (!mounted) return; - setApiRequests(items); - setHasFetchedFromApi(true); // Mark that we've fetched from API - } catch (error) { - console.error('[MyRequests] Error fetching requests:', error); - if (!mounted) return; - setApiRequests([]); - setHasFetchedFromApi(true); // Still mark as fetched even on error - } finally { - if (mounted) setLoading(false); - } - })(); - return () => { mounted = false; }; + fetchMyRequests(1); }, []); // Convert API/dynamic requests to the format expected by this component // Once API has fetched (even if empty), always use API data, never fall back to props const sourceRequests = hasFetchedFromApi ? apiRequests : dynamicRequests; - const convertedDynamicRequests = sourceRequests.map((req: any) => { + const convertedDynamicRequests = Array.isArray(sourceRequests) ? sourceRequests.map((req: any) => { const createdAt = req.submittedAt || req.submitted_at || req.createdAt || req.created_at; const priority = (req.priority || '').toString().toLowerCase(); @@ -155,7 +193,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr templateType: req.templateType, templateName: req.templateName }; - }); + }) : []; // Use only API/dynamic requests const allRequests = convertedDynamicRequests; @@ -174,9 +212,9 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr return matchesSearch && matchesStatus && matchesPriority; }); - // Stats calculation + // Stats calculation - using total from pagination for total count const stats = { - total: allRequests.length, + total: totalRecords || allRequests.length, pending: allRequests.filter(r => r.status === 'pending').length, approved: allRequests.filter(r => r.status === 'approved').length, inReview: allRequests.filter(r => r.status === 'in-review').length, @@ -185,17 +223,26 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr }; return ( -
- {/* Header */} -
-
-

- - My Requests -

-

- Track and manage all your submitted requests -

+
+ {/* Enhanced Header */} +
+
+
+
+ +
+
+

My Requests

+

Track and manage all your submitted requests

+
+
+
+ +
+ + {loading ? 'Loading…' : `${totalRecords || allRequests.length} total`} + requests +
@@ -411,6 +458,67 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr )) )}
+ + {/* Pagination Controls */} + {totalPages > 1 && !loading && ( + + +
+
+ Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} requests +
+ +
+ + + {currentPage > 3 && totalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers().map((pageNum) => ( + + ))} + + {currentPage < totalPages - 2 && totalPages > 5 && ( + <> + ... + + + )} + + +
+
+
+
+ )}
); } \ No newline at end of file diff --git a/src/pages/OpenRequests/OpenRequests.tsx b/src/pages/OpenRequests/OpenRequests.tsx index fe2e97f..7dc3843 100644 --- a/src/pages/OpenRequests/OpenRequests.tsx +++ b/src/pages/OpenRequests/OpenRequests.tsx @@ -97,26 +97,38 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); + + // Pagination states + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalRecords, setTotalRecords] = useState(0); + const [itemsPerPage] = useState(10); - const fetchRequests = async () => { + const fetchRequests = async (page: number = 1) => { try { - setLoading(true); - // Clear old data first - setItems([]); + if (page === 1) { + setLoading(true); + setItems([]); + } - const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 }); + const result = await workflowApi.listOpenForMe({ page, limit: itemsPerPage }); console.log('[OpenRequests] API Response:', result); // Debug log - const data = Array.isArray((result as any)?.data) - ? (result as any).data - : Array.isArray((result as any)?.data?.data) - ? (result as any).data.data - : Array.isArray(result as any) - ? (result as any) + // 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) { + setCurrentPage(pagination.page || 1); + setTotalPages(pagination.totalPages || 1); + setTotalRecords(pagination.total || 0); + } + const mapped: Request[] = data.map((r: any) => { const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at; @@ -152,11 +164,35 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) { const handleRefresh = () => { setRefreshing(true); - fetchRequests(); + fetchRequests(currentPage); + }; + + const handlePageChange = (newPage: number) => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage); + fetchRequests(newPage); + } + }; + + const getPageNumbers = () => { + const pages = []; + const maxPagesToShow = 5; + let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2)); + let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1); + + if (endPage - startPage < maxPagesToShow - 1) { + startPage = Math.max(1, endPage - maxPagesToShow + 1); + } + + for (let i = startPage; i <= endPage; i++) { + pages.push(i); + } + + return pages; }; useEffect(() => { - fetchRequests(); + fetchRequests(1); }, []); const filteredAndSortedRequests = useMemo(() => { @@ -239,7 +275,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
- {loading ? 'Loading…' : `${filteredAndSortedRequests.length} open`} + {loading ? 'Loading…' : `${totalRecords || items.length} open`} requests + + {currentPage > 3 && totalPages > 5 && ( + <> + + ... + + )} + + {getPageNumbers().map((pageNum) => ( + + ))} + + {currentPage < totalPages - 2 && totalPages > 5 && ( + <> + ... + + + )} + + +
+
+ + + )}
); } diff --git a/src/pages/RequestDetail/RequestDetail.tsx b/src/pages/RequestDetail/RequestDetail.tsx deleted file mode 100644 index e15067a..0000000 --- a/src/pages/RequestDetail/RequestDetail.tsx +++ /dev/null @@ -1,2475 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useParams } 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'; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { Progress } from '@/components/ui/progress'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { Textarea } from '@/components/ui/textarea'; -import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; -import { FilePreview } from '@/components/common/FilePreview'; -import { CUSTOM_REQUEST_DATABASE } from '@/utils/customRequestDatabase'; -import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; -import workflowApi, { approveLevel, rejectLevel, addApproverAtLevel, skipApprover, addSpectator, downloadDocument, getDocumentPreviewUrl } from '@/services/workflowApi'; -import { uploadDocument } from '@/services/documentApi'; -import { ApprovalModal } from '@/components/approval/ApprovalModal/ApprovalModal'; -import { RejectionModal } from '@/components/approval/RejectionModal/RejectionModal'; -import { SkipApproverModal } from '@/components/approval/SkipApproverModal'; -import { AddApproverModal } from '@/components/participant/AddApproverModal'; -import { AddSpectatorModal } from '@/components/participant/AddSpectatorModal'; -import { ActionStatusModal } from '@/components/common/ActionStatusModal'; -import { WorkNoteChat } from '@/components/workNote/WorkNoteChat/WorkNoteChat'; -import { useAuth } from '@/contexts/AuthContext'; -import { getSocket, joinRequestRoom, leaveRequestRoom } from '@/utils/socket'; -import { getWorkNotes } from '@/services/workflowApi'; -import { Component, ErrorInfo, ReactNode } from 'react'; -import { - ArrowLeft, - Clock, - User, - FileText, - MessageSquare, - CheckCircle, - XCircle, - Download, - Eye, - TrendingUp, - RefreshCw, - Activity, - Mail, - Phone, - Upload, - UserPlus, - ClipboardList, - Paperclip, - AlertTriangle, - AlertCircle, - Loader2 -} from 'lucide-react'; - -// Simple Error Boundary for RequestDetail -class RequestDetailErrorBoundary extends Component< - { children: ReactNode }, - { hasError: boolean; error: Error | null } -> { - constructor(props: { children: ReactNode }) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: Error) { - return { hasError: true, error }; - } - - override componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('RequestDetail Error:', error, errorInfo); - } - - override render() { - if (this.state.hasError) { - return ( -
-
- -

Error Loading Request

-

- {this.state.error?.message || 'An unexpected error occurred'} -

- - -
-
- ); - } - return this.props.children; - } -} - -interface RequestDetailProps { - requestId: string; - onBack?: () => void; - dynamicRequests?: any[]; -} - -// Utility functions -const getPriorityConfig = (priority: string) => { - switch (priority) { - case 'express': - case 'urgent': - return { - color: 'bg-red-100 text-red-800 border-red-200', - label: 'urgent priority' - }; - case 'standard': - return { - color: 'bg-blue-100 text-blue-800 border-blue-200', - label: 'standard priority' - }; - default: - return { - color: 'bg-gray-100 text-gray-800 border-gray-200', - label: 'normal priority' - }; - } -}; - -const getStatusConfig = (status: string) => { - switch (status) { - case 'pending': - return { - color: 'bg-yellow-100 text-yellow-800 border-yellow-200', - label: 'pending' - }; - case 'in-review': - return { - color: 'bg-blue-100 text-blue-800 border-blue-200', - label: 'in-review' - }; - case 'approved': - return { - color: 'bg-green-100 text-green-800 border-green-200', - label: 'approved' - }; - case 'rejected': - return { - color: 'bg-red-100 text-red-800 border-red-200', - label: 'rejected' - }; - case 'closed': - return { - color: 'bg-gray-100 text-gray-800 border-gray-300', - label: 'closed' - }; - case 'skipped': - return { - color: 'bg-orange-100 text-orange-800 border-orange-200', - label: 'skipped' - }; - default: - return { - color: 'bg-gray-100 text-gray-800 border-gray-200', - label: status - }; - } -}; - -const getStepIcon = (status: string, isSkipped?: boolean) => { - if (isSkipped) { - return ; - } - - switch (status) { - case 'approved': - return ; - case 'rejected': - return ; - case 'pending': - case 'in-review': - return ; - case 'waiting': - return ; - default: - return ; - } -}; - -const getActionTypeIcon = (type: string) => { - switch (type) { - case 'approval': - case 'approved': - return ; - case 'rejection': - case 'rejected': - return ; - case 'comment': - return ; - case 'status_change': - case 'updated': - return ; - case 'assignment': - return ; - case 'created': - return ; - case 'reminder': - return ; - case 'document_added': - return ; - case 'sla_warning': - return ; - default: - return ; - } -}; - -function RequestDetailInner({ - requestId: propRequestId, - onBack, - dynamicRequests = [] -}: RequestDetailProps) { - const params = useParams<{ requestId: string }>(); - // Use requestNumber from URL params (which now contains requestNumber), fallback to prop - const requestIdentifier = params.requestId || propRequestId || ''; - - // Read tab from URL query parameter (e.g., ?tab=worknotes) - const urlParams = new URLSearchParams(window.location.search); - const initialTab = urlParams.get('tab') || 'overview'; - - const [activeTab, setActiveTab] = useState(initialTab); - const [apiRequest, setApiRequest] = useState(null); - const [isSpectator, setIsSpectator] = useState(false); - // approving/rejecting local states are managed inside modals now - const [currentApprovalLevel, setCurrentApprovalLevel] = useState(null); - const [showApproveModal, setShowApproveModal] = useState(false); - const [showRejectModal, setShowRejectModal] = useState(false); - const [showAddApproverModal, setShowAddApproverModal] = useState(false); - const [showAddSpectatorModal, setShowAddSpectatorModal] = useState(false); - const [showSkipApproverModal, setShowSkipApproverModal] = useState(false); - const [skipApproverData, setSkipApproverData] = useState<{ levelId: string; approverName: string; levelNumber: number } | null>(null); - const [previewDocument, setPreviewDocument] = useState<{ fileName: string; fileType: string; documentId: string; fileSize?: number } | null>(null); - const [uploadingDocument, setUploadingDocument] = useState(false); - const [unreadWorkNotes, setUnreadWorkNotes] = useState(0); - const [mergedMessages, setMergedMessages] = useState([]); - const [workNoteAttachments, setWorkNoteAttachments] = useState([]); - const [refreshing, setRefreshing] = useState(false); - const [showActionStatusModal, setShowActionStatusModal] = useState(false); - const [actionStatus, setActionStatus] = useState<{ success: boolean; title: string; message: string } | null>(null); - const [conclusionLoading, setConclusionLoading] = useState(false); - const [conclusionRemark, setConclusionRemark] = useState(''); - const [conclusionSubmitting, setConclusionSubmitting] = useState(false); - const [aiGenerated, setAiGenerated] = useState(false); - const { user } = useAuth(); - - // Auto-switch tab when URL query parameter changes (e.g., from notifications) - useEffect(() => { - 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]); // Re-run when navigating to different request - - // Shared refresh routine - const refreshDetails = async () => { - setRefreshing(true); - try { - const details = await workflowApi.getWorkflowDetails(requestIdentifier); - if (!details) { - console.warn('[RequestDetail] No details returned from API'); - return; - } - const wf = details.workflow || {}; - const approvals = Array.isArray(details.approvals) ? details.approvals : []; - const participants = Array.isArray(details.participants) ? details.participants : []; - const documents = Array.isArray(details.documents) ? details.documents : []; - const summary = details.summary || {}; - const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; - - // Debug: Log TAT alerts to console - if (tatAlerts.length > 0) { - console.log(`[RequestDetail] Found ${tatAlerts.length} TAT alerts for request:`, tatAlerts); - } else { - console.log('[RequestDetail] No TAT alerts found for this request'); - } - - const toInitials = (name?: string, email?: string) => { - const base = (name || email || 'NA').toString(); - return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase(); - }; - - const statusMap = (s: string) => { - const val = (s || '').toUpperCase(); - if (val === 'IN_PROGRESS') return 'in-review'; - if (val === 'PENDING') return 'pending'; - if (val === 'APPROVED') return 'approved'; - if (val === 'REJECTED') return 'rejected'; - if (val === 'CLOSED') return 'closed'; - if (val === 'SKIPPED') return 'skipped'; - return (s || '').toLowerCase(); - }; - - const currentLevel = summary?.currentLevel || wf.currentLevel || 1; - - const approvalFlow = approvals.map((a: any) => { - const levelNumber = a.levelNumber || 0; - const levelStatus = (a.status || '').toString().toUpperCase(); - const levelId = a.levelId || a.level_id; - - // Determine display status based on level and current status - let displayStatus = statusMap(a.status); - - // If level hasn't been reached yet and status is not completed/rejected - if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') { - displayStatus = 'waiting'; - } - // If it's the current level and status is PENDING - else if (levelNumber === currentLevel && levelStatus === 'PENDING') { - displayStatus = 'pending'; - } - - // Get TAT alerts for this level - const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId); - - // Debug log - if (levelAlerts.length > 0) { - console.log(`[RequestDetail] Level ${levelNumber} (${levelId}) has ${levelAlerts.length} TAT alerts:`, levelAlerts); - } - - return { - step: levelNumber, - levelId, - role: a.levelName || a.approverName || 'Approver', - status: displayStatus, - approver: a.approverName || a.approverEmail, - approverId: a.approverId || a.approver_id, - approverEmail: a.approverEmail, - tatHours: Number(a.tatHours || 0), - elapsedHours: Number(a.elapsedHours || 0), - remainingHours: Number(a.remainingHours || 0), - tatPercentageUsed: Number(a.tatPercentageUsed || 0), - actualHours: a.levelEndTime && a.levelStartTime ? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60)) : undefined, - comment: a.comments || undefined, - timestamp: a.actionDate || undefined, - levelStartTime: a.levelStartTime || a.tatStartTime, - tatAlerts: levelAlerts, - skipReason: a.skipReason || undefined, - isSkipped: levelStatus === 'SKIPPED' || a.isSkipped || false, - }; - }); - - // Map spectators - const spectators = participants - .filter((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR') - .map((p: any) => ({ - name: p.userName || p.user_name || p.userEmail || p.user_email, - role: 'Spectator', - email: p.userEmail || p.user_email, - avatar: toInitials(p.userName || p.user_name, p.userEmail || p.user_email), - })); - - const participantNameById = (uid?: string) => { - if (!uid) return undefined; - const p = participants.find((x: any) => x.userId === uid || x.user_id === uid); - if (p?.userName || p?.user_name) return p.userName || p.user_name; - if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email; - return uid; - }; - - const mappedDocuments = documents.map((d: any) => { - const sizeBytes = Number(d.fileSize || d.file_size || 0); - const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB'; - return { - documentId: d.documentId || d.document_id, - name: d.originalFileName || d.fileName || d.file_name, - fileType: d.fileType || d.file_type || '', - size: sizeMb, - sizeBytes: sizeBytes, - uploadedBy: participantNameById(d.uploadedBy || d.uploaded_by), - uploadedAt: d.uploadedAt || d.uploaded_at, - }; - }); - - // Filter out TAT breach activities as they're not important for activity timeline - const filteredActivities = Array.isArray(details.activities) - ? details.activities.filter((activity: any) => { - const activityType = (activity.type || '').toLowerCase(); - return activityType !== 'sla_warning'; // Exclude TAT breach/warning activities - }) - : []; - - const updatedRequest = { - ...wf, - id: wf.requestNumber || wf.requestId, - requestId: wf.requestId, - requestNumber: wf.requestNumber, - title: wf.title, - description: wf.description, - status: statusMap(wf.status), - priority: (wf.priority || '').toString().toLowerCase(), - approvalFlow, - approvals, - participants, - documents: mappedDocuments, - spectators, - summary, // ← Backend provides SLA in summary.sla - initiator: { - name: wf.initiator?.displayName || wf.initiator?.email, - role: wf.initiator?.designation || undefined, - department: wf.initiator?.department || undefined, - email: wf.initiator?.email || undefined, - phone: wf.initiator?.phone || undefined, - avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email), - }, - createdAt: wf.createdAt, - updatedAt: wf.updatedAt, - totalSteps: wf.totalLevels, - currentStep: summary?.currentLevel || wf.currentLevel, - auditTrail: filteredActivities, - conclusionRemark: wf.conclusionRemark || null, - closureDate: wf.closureDate || null, - }; - setApiRequest(updatedRequest); - - const userEmail = (user as any)?.email?.toLowerCase(); - const newCurrentLevel = approvals.find((a: any) => { - const st = (a.status || '').toString().toUpperCase(); - const approverEmail = (a.approverEmail || '').toLowerCase(); - return (st === 'PENDING' || st === 'IN_PROGRESS') && approverEmail === userEmail; - }); - setCurrentApprovalLevel(newCurrentLevel || null); - - // Update viewer role - const viewerId = (user as any)?.userId; - if (viewerId) { - const isSpec = participants.some((p: any) => (p.participantType || p.participant_type || '').toUpperCase() === 'SPECTATOR' && (p.userId || p.user_id) === viewerId); - setIsSpectator(isSpec); - } else { - setIsSpectator(false); - } - } catch (error) { - console.error('[RequestDetail] Error refreshing details:', error); - alert('Failed to refresh request details. Please try again.'); - } finally { - setRefreshing(false); - } - }; - - const handleRefresh = () => { - refreshDetails(); - }; - - const fetchExistingConclusion = async () => { - try { - const { getConclusion } = await import('@/services/conclusionApi'); - const result = await getConclusion(request.requestId || requestIdentifier); - if (result && result.aiGeneratedRemark) { - setConclusionRemark(result.finalRemark || result.aiGeneratedRemark); - setAiGenerated(!!result.aiGeneratedRemark); - } - } catch (err) { - // No conclusion yet - that's okay - console.log('[RequestDetail] No existing conclusion found'); - } - }; - - const handleGenerateConclusion = async () => { - try { - setConclusionLoading(true); - const { generateConclusion } = await import('@/services/conclusionApi'); - const result = await generateConclusion(request.requestId || requestIdentifier); - setConclusionRemark(result.aiGeneratedRemark); - setAiGenerated(true); - } catch (err) { - // Silently fail - user can write manually - setConclusionRemark(''); - setAiGenerated(false); - } finally { - setConclusionLoading(false); - } - }; - - const handleFinalizeConclusion = async () => { - if (!conclusionRemark.trim()) { - setActionStatus({ - success: false, - title: 'Validation Error', - message: 'Conclusion remark cannot be empty' - }); - setShowActionStatusModal(true); - return; - } - - try { - setConclusionSubmitting(true); - const { finalizeConclusion } = await import('@/services/conclusionApi'); - await finalizeConclusion(request.requestId || requestIdentifier, conclusionRemark); - - setActionStatus({ - success: true, - title: 'Request Closed with Successful Completion', - message: 'The request has been finalized and moved to Closed Requests.' - }); - setShowActionStatusModal(true); - - // Refresh to get updated status - await refreshDetails(); - - // Navigate to Closed Requests after a short delay (for user to see the success message) - setTimeout(() => { - // Use onBack if provided, otherwise navigate programmatically - if (onBack) { - onBack(); - // After going back, trigger navigation to closed requests - setTimeout(() => { - window.location.hash = '#/closed-requests'; - }, 100); - } else { - window.location.hash = '#/closed-requests'; - } - }, 2000); // 2 second delay to show success message - } catch (err: any) { - setActionStatus({ - success: false, - title: 'Error', - message: err.response?.data?.error || 'Failed to finalize conclusion' - }); - setShowActionStatusModal(true); - } finally { - setConclusionSubmitting(false); - } - }; - - // Work notes load - - // Approve modal onConfirm - async function handleApproveConfirm(description: string) { - const levelId = currentApprovalLevel?.levelId || currentApprovalLevel?.level_id; - if (!levelId) { alert('Approval level not found'); return; } - - await approveLevel(requestIdentifier, levelId, description || ''); - await refreshDetails(); - // Close modal + notify (assumes global handlers, replace as needed) - (window as any)?.closeModal?.(); - (window as any)?.toast?.('Approved successfully'); - } - - // Reject modal onConfirm (UI uses only comments/remarks; map it to both fields) - async function handleRejectConfirm(description: string) { - if (!description?.trim()) { alert('Comments & remarks are required'); return; } - - const levelId = currentApprovalLevel?.levelId || currentApprovalLevel?.level_id; - if (!levelId) { alert('Approval level not found'); return; } - - await rejectLevel(requestIdentifier, levelId, description.trim(), description.trim()); - await refreshDetails(); - // Close modal + notify (assumes global handlers, replace as needed) - (window as any)?.closeModal?.(); - (window as any)?.toast?.('Rejected successfully'); - } - - // Add approver modal handler (enhanced with level and TAT) - async function handleAddApprover(email: string, tatHours: number, level: number) { - try { - await addApproverAtLevel(requestIdentifier, email, tatHours, level); - await refreshDetails(); - setShowAddApproverModal(false); - setActionStatus({ - success: true, - title: 'Approver Added', - message: `Approver added successfully at Level ${level} with ${tatHours}h TAT` - }); - setShowActionStatusModal(true); - } catch (error: any) { - setActionStatus({ - success: false, - title: 'Failed to Add Approver', - message: error?.response?.data?.error || 'Failed to add approver. Please try again.' - }); - setShowActionStatusModal(true); - throw error; - } - } - - // Skip approver handler - async function handleSkipApprover(reason: string) { - if (!skipApproverData) return; - - try { - await skipApprover(requestIdentifier, skipApproverData.levelId, reason); - await refreshDetails(); - setShowSkipApproverModal(false); - setSkipApproverData(null); - setActionStatus({ - success: true, - title: 'Approver Skipped', - message: 'Approver skipped successfully. The workflow has moved to the next level.' - }); - setShowActionStatusModal(true); - } catch (error: any) { - setActionStatus({ - success: false, - title: 'Failed to Skip Approver', - message: error?.response?.data?.error || 'Failed to skip approver. Please try again.' - }); - setShowActionStatusModal(true); - throw error; - } - } - - // Add spectator modal handler - async function handleAddSpectator(email: string) { - try { - await addSpectator(requestIdentifier, email); - await refreshDetails(); - setShowAddSpectatorModal(false); - setActionStatus({ - success: true, - title: 'Spectator Added', - message: 'Spectator added successfully. They can now view this request.' - }); - setShowActionStatusModal(true); - } catch (error: any) { - setActionStatus({ - success: false, - title: 'Failed to Add Spectator', - message: error?.response?.data?.error || 'Failed to add spectator. Please try again.' - }); - setShowActionStatusModal(true); - throw error; - } - } - - // Document upload handler - const handleDocumentUpload = async (event: React.ChangeEvent) => { - const files = event.target.files; - if (!files || files.length === 0) return; - - setUploadingDocument(true); - try { - const file = files[0]; - if (!file) { - alert('No file selected'); - return; - } - - // Get UUID requestId (not request number) from current request - const requestId = apiRequest?.requestId; - if (!requestId) { - alert('Request ID not found'); - return; - } - - await uploadDocument(file, requestId, 'SUPPORTING'); - - // Refresh the details to show new document and activity - await refreshDetails(); - - alert('Document uploaded successfully'); - } catch (error: any) { - console.error('Upload error:', error); - alert(error?.response?.data?.error || 'Failed to upload document'); - } finally { - setUploadingDocument(false); - // Clear the input - if (event.target) { - event.target.value = ''; - } - } - }; - - // Trigger file input - const triggerFileInput = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif'; - input.onchange = handleDocumentUpload as any; - input.click(); - }; - - // Establish socket connection and join request room when component mounts - useEffect(() => { - if (!requestIdentifier) { - console.warn('[RequestDetail] No requestIdentifier, cannot join socket room'); - return; - } - - console.log('[RequestDetail] Initializing socket connection for:', requestIdentifier); - - let mounted = true; - let actualRequestId = requestIdentifier; - - (async () => { - try { - // Fetch UUID if we have request number - const details = await workflowApi.getWorkflowDetails(requestIdentifier); - if (details?.workflow?.requestId && mounted) { - actualRequestId = details.workflow.requestId; // Use UUID for room - console.log('[RequestDetail] Resolved UUID:', actualRequestId); - } - } catch (error) { - console.error('[RequestDetail] Failed to resolve UUID:', error); - } - - if (!mounted) return; - - // Get socket instance with base URL - const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; - const socket = getSocket(baseUrl); - - if (!socket) { - console.error('[RequestDetail] Socket not available after getSocket()'); - return; - } - - console.log('[RequestDetail] Socket instance obtained, connected:', socket.connected); - - const userId = (user as any)?.userId; - console.log('[RequestDetail] Current userId:', userId); - - // Wait for socket to connect before joining room - const handleConnect = () => { - console.log('[RequestDetail] Socket connected, joining room with UUID:', actualRequestId); - joinRequestRoom(socket, actualRequestId, userId); - console.log(`[RequestDetail] βœ… Joined request room: ${actualRequestId} - User is now ONLINE`); - }; - - // If already connected, join immediately - if (socket.connected) { - console.log('[RequestDetail] Socket already connected, joining immediately'); - handleConnect(); - } else { - console.log('[RequestDetail] Socket not connected yet, waiting for connect event'); - socket.on('connect', handleConnect); - } - - // Cleanup - only runs when component unmounts or requestIdentifier changes - return () => { - if (mounted) { - console.log('[RequestDetail] Cleaning up socket listeners and leaving room'); - socket.off('connect', handleConnect); - leaveRequestRoom(socket, actualRequestId); - console.log(`[RequestDetail] βœ… Left request room: ${actualRequestId} - User is now OFFLINE`); - } - }; - })(); - - return () => { - mounted = false; - }; - }, [requestIdentifier, user]); - - // Fetch and merge work notes with activities - useEffect(() => { - if (!requestIdentifier || !apiRequest) return; - - (async () => { - try { - const workNotes = await getWorkNotes(requestIdentifier); - const activities = apiRequest.auditTrail || []; - - // Merge work notes and activities - const merged = [...workNotes, ...activities]; - - // Sort by timestamp - merged.sort((a, b) => { - const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime(); - const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime(); - return timeA - timeB; - }); - - setMergedMessages(merged); - console.log(`[RequestDetail] Merged ${workNotes.length} work notes with ${activities.length} activities`); - } catch (error) { - console.error('[RequestDetail] Failed to fetch and merge messages:', error); - } - })(); - }, [requestIdentifier, apiRequest]); - - // Separate effect to listen for new work notes and update badge - useEffect(() => { - if (!requestIdentifier) return; - - const baseUrl = import.meta.env.VITE_API_BASE_URL?.replace('/api/v1', '') || 'http://localhost:5000'; - const socket = getSocket(baseUrl); - - if (!socket) return; - - // Listen for new work notes to update badge count when not on work notes tab - const handleNewWorkNote = (data: any) => { - console.log(`[RequestDetail] New work note received:`, data); - // Only increment badge if not currently viewing work notes tab - if (activeTab !== 'worknotes') { - setUnreadWorkNotes(prev => prev + 1); - } - - // Refresh merged messages when new work note arrives - (async () => { - try { - const workNotes = await getWorkNotes(requestIdentifier); - const activities = apiRequest?.auditTrail || []; - const merged = [...workNotes, ...activities].sort((a, b) => { - const timeA = new Date(a.createdAt || a.created_at || a.timestamp || 0).getTime(); - const timeB = new Date(b.createdAt || b.created_at || b.timestamp || 0).getTime(); - return timeA - timeB; - }); - setMergedMessages(merged); - } catch (error) { - console.error('[RequestDetail] Failed to refresh messages:', error); - } - })(); - }; - - socket.on('noteHandler', handleNewWorkNote); - socket.on('worknote:new', handleNewWorkNote); // Also listen to worknote:new - - // Listen for real-time TAT alerts - const handleTatAlert = (data: any) => { - console.log(`[RequestDetail] πŸ”” Real-time TAT alert received:`, data); - - // Show visual feedback (you can replace with a toast notification) - const alertEmoji = data.type === 'breach' ? '⏰' : data.type === 'threshold2' ? '⚠️' : '⏳'; - console.log(`%c${alertEmoji} TAT Alert: ${data.message}`, 'color: #ff6600; font-size: 14px; font-weight: bold;'); - - // Refresh the request to get updated TAT alerts - (async () => { - try { - const details = await workflowApi.getWorkflowDetails(requestIdentifier); - - if (details) { - setApiRequest(details); - - // Update approval steps with new TAT alerts - const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; - console.log(`[RequestDetail] Refreshed TAT alerts after real-time update:`, tatAlerts); - - // Optional: Show browser notification if user granted permission - if ('Notification' in window && Notification.permission === 'granted') { - new Notification(`${alertEmoji} TAT Alert`, { - body: data.message, - icon: '/favicon.ico', - tag: `tat-${data.requestId}-${data.type}`, - requireInteraction: false - }); - } - } - } catch (error) { - console.error('[RequestDetail] Failed to refresh after TAT alert:', error); - } - })(); - }; - - socket.on('tat:alert', handleTatAlert); - - // Cleanup - return () => { - socket.off('noteHandler', handleNewWorkNote); - socket.off('worknote:new', handleNewWorkNote); - socket.off('tat:alert', handleTatAlert); - }; - }, [requestIdentifier, activeTab, apiRequest]); - - // Clear unread count when switching to work notes tab - useEffect(() => { - if (activeTab === 'worknotes') { - setUnreadWorkNotes(0); - } - }, [activeTab]); - - useEffect(() => { - let mounted = true; - (async () => { - try { - - // Use requestIdentifier (which should now be requestNumber) for API call - const details = await workflowApi.getWorkflowDetails(requestIdentifier); - if (!mounted || !details) return; - const wf = details.workflow || {}; - const approvals = Array.isArray(details.approvals) ? details.approvals : []; - const participants = Array.isArray(details.participants) ? details.participants : []; - const documents = Array.isArray(details.documents) ? details.documents : []; - const summary = details.summary || {}; - const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; - - // Debug: Log TAT alerts to console - console.log('[RequestDetail] TAT Alerts received from API:', tatAlerts.length, tatAlerts); - - // Map to UI shape without changing UI - const toInitials = (name?: string, email?: string) => { - const base = (name || email || 'NA').toString(); - return base.split(' ').map(s => s[0]).filter(Boolean).join('').slice(0, 2).toUpperCase(); - }; - - const statusMap = (s: string) => { - const val = (s || '').toUpperCase(); - if (val === 'IN_PROGRESS') return 'in-review'; - if (val === 'PENDING') return 'pending'; - if (val === 'APPROVED') return 'approved'; - if (val === 'REJECTED') return 'rejected'; - if (val === 'CLOSED') return 'closed'; - return (s || '').toLowerCase(); - }; - - const priority = (wf.priority || '').toString().toLowerCase(); - const currentLevel = summary?.currentLevel || wf.currentLevel || 1; - - const approvalFlow = approvals.map((a: any) => { - const levelNumber = a.levelNumber || 0; - const levelStatus = (a.status || '').toString().toUpperCase(); - const levelId = a.levelId || a.level_id; - - // Determine display status based on level and current status - let displayStatus = statusMap(a.status); - - // If level hasn't been reached yet and status is not completed/rejected - if (levelNumber > currentLevel && levelStatus !== 'APPROVED' && levelStatus !== 'REJECTED') { - displayStatus = 'waiting'; - } - // If it's the current level and status is PENDING - else if (levelNumber === currentLevel && levelStatus === 'PENDING') { - displayStatus = 'pending'; - } - - // Get TAT alerts for this level - const levelAlerts = tatAlerts.filter((alert: any) => alert.levelId === levelId); - - // Debug log - if (levelAlerts.length > 0) { - console.log(`[RequestDetail useEffect] Level ${levelNumber} (${levelId}) has ${levelAlerts.length} TAT alerts:`, levelAlerts); - } - - return { - step: levelNumber, - levelId, - role: a.levelName || a.approverName || 'Approver', - status: displayStatus, - approver: a.approverName || a.approverEmail, - approverId: a.approverId || a.approver_id, - approverEmail: a.approverEmail, - tatHours: Number(a.tatHours || 0), - elapsedHours: Number(a.elapsedHours || 0), - remainingHours: Number(a.remainingHours || 0), - tatPercentageUsed: Number(a.tatPercentageUsed || 0), - actualHours: a.levelEndTime && a.levelStartTime ? Math.max(0, (new Date(a.levelEndTime).getTime() - new Date(a.levelStartTime).getTime()) / (1000 * 60 * 60)) : undefined, - comment: a.comments || undefined, - timestamp: a.actionDate || undefined, - levelStartTime: a.levelStartTime || a.tatStartTime, - tatAlerts: levelAlerts, - }; - }); - - // Find current approval level for logged-in user - const userEmail = (user as any)?.email?.toLowerCase(); - const userCurrentLevel = approvals.find((a: any) => { - const status = (a.status || '').toString().toUpperCase(); - const approverEmail = (a.approverEmail || '').toLowerCase(); - return (status === 'PENDING' || status === 'IN_PROGRESS') && - approverEmail === userEmail; - }); - setCurrentApprovalLevel(userCurrentLevel || null); - - const spectators = participants - .filter((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR') - .map((p: any) => ({ - name: p.userName || p.userEmail, - role: 'Spectator', - avatar: toInitials(p.userName, p.userEmail), - })); - - const participantNameById = (uid?: string) => { - if (!uid) return undefined; - const p = participants.find((x: any) => x.userId === uid); - if (p?.userName) return p.userName; - if (wf.initiatorId === uid) return wf.initiator?.displayName || wf.initiator?.email; - return uid; - }; - - const mappedDocuments = documents.map((d: any) => { - const sizeBytes = Number(d.fileSize || 0); - const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(2) + ' MB'; - return { - documentId: d.documentId || d.document_id, - name: d.originalFileName || d.fileName, - fileType: d.fileType || d.file_type || '', - size: sizeMb, - sizeBytes: sizeBytes, - uploadedBy: participantNameById(d.uploadedBy), - uploadedAt: d.uploadedAt, - }; - }); - - // Filter out TAT breach activities as they're not important for activity timeline - const filteredActivities = Array.isArray(details.activities) - ? details.activities.filter((activity: any) => { - const activityType = (activity.type || '').toLowerCase(); - return activityType !== 'sla_warning'; // Exclude TAT breach/warning activities - }) - : []; - - const mapped = { - id: wf.requestNumber || wf.requestId, - requestId: wf.requestId, // ← UUID for API calls - title: wf.title, - description: wf.description, - priority, - status: statusMap(wf.status), - summary, // ← Backend provides comprehensive SLA in summary.sla - initiator: { - name: wf.initiator?.displayName || wf.initiator?.email, - role: wf.initiator?.designation || undefined, - department: wf.initiator?.department || undefined, - email: wf.initiator?.email || undefined, - phone: wf.initiator?.phone || undefined, - avatar: toInitials(wf.initiator?.displayName, wf.initiator?.email), - }, - createdAt: wf.createdAt, - updatedAt: wf.updatedAt, - totalSteps: wf.totalLevels, - currentStep: summary?.currentLevel || wf.currentLevel, - approvalFlow, - approvals, // ← Added: Include raw approvals array with levelStartTime/tatStartTime - documents: mappedDocuments, - spectators, - auditTrail: filteredActivities, - conclusionRemark: wf.conclusionRemark || null, - closureDate: wf.closureDate || null, - }; - setApiRequest(mapped); - // Determine viewer role (spectator only means comment-only) - const viewerId = (user as any)?.userId; - if (viewerId) { - const isSpec = participants.some((p: any) => (p.participantType || '').toUpperCase() === 'SPECTATOR' && p.userId === viewerId); - setIsSpectator(isSpec); - } else { - setIsSpectator(false); - } - } catch (error) { - console.error('[RequestDetail] Error loading request details:', error); - if (mounted) { - // Set a minimal request object to prevent complete failure - setApiRequest(null); - } - } finally { - - } - })(); - return () => { mounted = false; }; - }, [requestIdentifier]); - - // Get request from any database or dynamic requests - const request = useMemo(() => { - if (apiRequest) return apiRequest; - // First check custom request database (by requestNumber or requestId) - const customRequest = CUSTOM_REQUEST_DATABASE[requestIdentifier]; - if (customRequest) return customRequest; - - // Then check claim management database - const claimRequest = CLAIM_MANAGEMENT_DATABASE[requestIdentifier]; - if (claimRequest) return claimRequest; - - // Then check dynamic requests (match by requestNumber or id) - const dynamicRequest = dynamicRequests.find((req: any) => - req.id === requestIdentifier || - req.requestNumber === requestIdentifier || - req.request_number === requestIdentifier - ); - if (dynamicRequest) return dynamicRequest; - - return null; - }, [requestIdentifier, dynamicRequests, apiRequest]); - - // Check if current user is the initiator - const isInitiator = useMemo(() => { - if (!request || !user) return false; - const userEmail = (user as any)?.email?.toLowerCase(); - const initiatorEmail = request.initiator?.email?.toLowerCase(); - return userEmail === initiatorEmail; - }, [request, user]); - - // Fetch existing conclusion when request is approved (generated by final approver) - useEffect(() => { - if (request?.status === 'approved' && isInitiator && !conclusionRemark) { - fetchExistingConclusion(); - } - }, [request?.status, isInitiator]); - - // Get all existing participants for validation - const existingParticipants = useMemo(() => { - if (!request) return []; - - const participants: Array<{ email: string; participantType: string; name?: string }> = []; - - // Add initiator - if (request.initiator?.email) { - participants.push({ - email: request.initiator.email.toLowerCase(), - participantType: 'INITIATOR', - name: request.initiator.name - }); - } - - // Add approvers from approval flow - if (request.approvalFlow && Array.isArray(request.approvalFlow)) { - request.approvalFlow.forEach((approval: any) => { - if (approval.approverEmail) { - participants.push({ - email: approval.approverEmail.toLowerCase(), - participantType: 'APPROVER', - name: approval.approver - }); - } - }); - } - - // Add spectators - if (request.spectators && Array.isArray(request.spectators)) { - request.spectators.forEach((spectator: any) => { - if (spectator.email) { - participants.push({ - email: spectator.email.toLowerCase(), - participantType: 'SPECTATOR', - name: spectator.name - }); - } - }); - } - - // Add from participants array if available - if (request.participants && Array.isArray(request.participants)) { - request.participants.forEach((p: any) => { - const email = (p.userEmail || p.email || '').toLowerCase(); - const participantType = (p.participantType || p.participant_type || '').toUpperCase(); - const name = p.userName || p.user_name || p.name; - - if (email && participantType && !participants.find(x => x.email === email)) { - participants.push({ email, participantType, name }); - } - }); - } - - return participants; - }, [request]); - - // Loading state - if (!request && !apiRequest) { - return ( -
-
-
-

Loading request details...

-
-
- ); - } - - if (!request) { - return ( -
-
-

Request Not Found

-

The request you're looking for doesn't exist.

- -
-
- ); - } - - const priorityConfig = getPriorityConfig(request.priority || 'standard'); - const statusConfig = getStatusConfig(request.status || 'pending'); - - // Check if request is approved and needs closure by initiator - const needsClosure = request.status === 'approved' && isInitiator; - - return ( - <> -
-
- {/* Header Section */} -
- {/* Top Header */} -
-
-
- - -
-
- -
- -
-

{request.id || 'N/A'}

-
- - {priorityConfig.label} - - - {statusConfig.label} - -
-
-
-
- - -
- -
-

{request.title}

-
-
- - {/* SLA Progress Section - Shows OVERALL request SLA from backend */} -
- {(() => { - const sla = request.summary?.sla || request.sla; - - if (!sla || request.status === 'approved' || request.status === 'rejected' || request.status === 'closed') { - return ( -
- - - {request.status === 'closed' ? 'πŸ”’ Request Closed' : - request.status === 'approved' ? 'βœ… Request Approved' : - request.status === 'rejected' ? '❌ Request Rejected' : 'SLA Not Available'} - -
- ); - } - - return ( -
-
-
- - SLA Progress -
- - {sla.percentageUsed || 0}% elapsed - -
- - div]:bg-red-600' : - sla.status === 'critical' ? '[&>div]:bg-orange-600' : - sla.status === 'approaching' ? '[&>div]:bg-yellow-600' : - '[&>div]:bg-green-600' - }`} - /> - -
- - {sla.elapsedText || `${sla.elapsedHours || 0}h`} elapsed - - - {sla.remainingText || `${sla.remainingHours || 0}h`} remaining - -
- - {sla.deadline && ( -

- Due: {new Date(sla.deadline).toLocaleString()} β€’ {sla.percentageUsed || 0}% elapsed -

- )} - - {sla.status === 'critical' && ( -

⚠️ Approaching Deadline

- )} - {sla.status === 'breached' && ( -

πŸ”΄ URGENT - Deadline Passed

- )} -
- ); - })()} -
-
- - {/* Tabs */} - - - - - Overview - - - - Workflow - - - - Docs - - - - Activity - - - - Work Notes - {unreadWorkNotes > 0 && ( - - {unreadWorkNotes > 9 ? '9+' : unreadWorkNotes} - - )} - - - - {/* Main Layout - Full width for Work Notes, Grid with sidebar for others */} -
- {/* Left Column - Tab Content (2/3 width for most tabs, full width for work notes) */} -
- - {/* Overview Tab */} - -
- {/* Request Initiator */} - - - - - Request Initiator - - - -
- - - {request.initiator?.avatar || 'U'} - - -
-

{request.initiator?.name || 'N/A'}

-

{request.initiator?.role || 'N/A'}

-

{request.initiator?.department || 'N/A'}

- -
-
- - {request.initiator?.email || 'N/A'} -
-
- - {request.initiator?.phone || 'N/A'} -
-
-
-
-
-
- - {/* Request Details */} - - - - - Request Details - - - -
- -
-

- {request.description} -

-
-
- - {/* Additional Details */} - {(request.category || request.subcategory) && ( -
- {request.category && ( -
- -

{request.category}

-
- )} - {request.subcategory && ( -
- -

{request.subcategory}

-
- )} -
- )} - - {request.amount && ( -
- -

{request.amount}

-
- )} - -
-
- -

{formatDateTime(request.createdAt)}

-
-
- -

{formatDateTime(request.updatedAt)}

-
-
-
-
- - {/* Claim Management Details - Show only for claim management requests */} - {request.claimDetails && ( - - - - - Claim Management Details - - - -
-
- -

{request.claimDetails.activityName || 'N/A'}

-
-
- -

{request.claimDetails.activityType || 'N/A'}

-
-
- -

{request.claimDetails.location || 'N/A'}

-
-
- -

{request.claimDetails.activityDate ? formatDateShort(request.claimDetails.activityDate) : 'N/A'}

-
-
- -

{request.claimDetails.dealerCode || 'N/A'}

-
-
- -

{request.claimDetails.dealerName || 'N/A'}

-
-
- - {request.claimDetails.requestDescription && ( -
- -

- {request.claimDetails.requestDescription} -

-
- )} -
-
- )} -
-
- - {/* Workflow Tab */} - - - -
-
- - - Approval Workflow - - - Track the approval progress through each step - -
- {request.totalSteps && (() => { - const completedCount = request.approvalFlow?.filter((s: any) => s.status === 'approved').length || 0; - return ( - - Step {request.currentStep} of {request.totalSteps} - {completedCount} completed - - ); - })()} -
-
- - {request.approvalFlow && request.approvalFlow.length > 0 ? ( -
- {request.approvalFlow.map((step: any, index: number) => { - const isActive = step.status === 'pending' || step.status === 'in-review'; - const isCompleted = step.status === 'approved'; - const isRejected = step.status === 'rejected'; - const isWaiting = step.status === 'waiting'; - - // Get approval details with backend-calculated SLA - const approval = request.approvals?.find((a: any) => a.levelId === step.levelId); - const tatHours = Number(step.tatHours || 0); - const actualHours = step.actualHours; - const savedHours = isCompleted && actualHours ? Math.max(0, tatHours - actualHours) : 0; - - return ( -
-
-
- {getStepIcon(step.status, step.isSkipped)} -
- -
- {/* Header with Approver Label and Status */} -
-
-
-

- Approver {index + 1} -

- - {step.isSkipped ? 'skipped' : step.status} - - {step.isSkipped && step.skipReason && ( - - - - - - -

⏭️ Skip Reason:

-

{step.skipReason}

-
-
-
- )} - {isCompleted && actualHours && ( - - {actualHours.toFixed(1)} hours - - )} -
- {(() => { - // Check if this approver is the current user - const currentUserEmail = (user as any)?.email?.toLowerCase(); - const approverEmail = step.approverEmail?.toLowerCase(); - const isCurrentUser = currentUserEmail && approverEmail && currentUserEmail === approverEmail; - - return ( - <> -

- {isCurrentUser ? ( - You - ) : ( - step.approver - )} -

-

{step.role}

- - ); - })()} -
-
-

Turnaround Time (TAT)

-

{tatHours} hours

-
-
- - {/* Completed Approver - Show Completion Details */} - {isCompleted && actualHours !== undefined && ( -
-
- Completed: - {step.timestamp ? formatDateTime(step.timestamp) : 'N/A'} -
-
- Completed in: - {actualHours.toFixed(1)} hours -
- - {/* Progress Bar for Completed */} -
- -
- Within TAT - {savedHours > 0 && ( - Saved {savedHours.toFixed(1)} hours - )} -
-
- - {/* Conclusion Remark */} - {step.comment && ( -
-

πŸ’¬ Conclusion Remark:

-

{step.comment}

-
- )} -
- )} - - {/* Active Approver - Show Real-time Progress from Backend */} - {isActive && approval?.sla && ( -
-
- Due by: - - {approval.sla.deadline ? formatDateTime(approval.sla.deadline) : 'Not set'} - -
- - {/* Current Approver - Time Tracking */} -
-

- - Current Approver - Time Tracking -

- -
-
- Time elapsed since assigned: - {approval.sla.elapsedText} -
-
- Time used: - {approval.sla.elapsedText} / {tatHours}h allocated -
-
- - {/* Progress Bar */} -
- div]:bg-red-600' : - approval.sla.status === 'critical' ? '[&>div]:bg-orange-600' : - '[&>div]:bg-yellow-600' - }`} - /> -
- - Progress: {approval.sla.percentageUsed}% of TAT used - - - {approval.sla.remainingText} remaining - -
- {approval.sla.status === 'breached' && ( -

- πŸ”΄ Deadline Breached -

- )} - {approval.sla.status === 'critical' && ( -

- ⚠️ Approaching Deadline -

- )} -
-
-
- )} - - {/* Waiting Approver - Show Assignment Info */} - {isWaiting && ( -
-
-

⏸️ Awaiting Previous Approval

-

Will be assigned after previous step

-

Allocated {tatHours} hours for approval

-
-
- )} - - {/* Rejected Status */} - {isRejected && step.comment && ( -
-

❌ Rejection Reason:

-

{step.comment}

-
- )} - - {/* Skipped Status */} - {step.isSkipped && step.skipReason && ( -
-

⏭️ Skip Reason:

-

{step.skipReason}

- {step.timestamp && ( -

Skipped on {formatDateTime(step.timestamp)}

- )} -
- )} - - {/* TAT Alerts/Reminders */} - {step.tatAlerts && step.tatAlerts.length > 0 && ( -
- {step.tatAlerts.map((alert: any, alertIndex: number) => ( -
-
-
- {(alert.thresholdPercentage || 0) === 50 && '⏳'} - {(alert.thresholdPercentage || 0) === 75 && '⚠️'} - {(alert.thresholdPercentage || 0) === 100 && '⏰'} -
-
-
-

- Reminder {alertIndex + 1} - {alert.thresholdPercentage || 0}% TAT -

- - {alert.isBreached ? 'BREACHED' : 'WARNING'} - -
- -

- {alert.thresholdPercentage || 0}% of SLA breach reminder have been sent -

- - {/* Time Tracking Details */} -
-
- Allocated: - - {Number(alert.tatHoursAllocated || 0).toFixed(2)}h - -
-
- Elapsed: - - {Number(alert.tatHoursElapsed || 0).toFixed(2)}h - {alert.metadata?.tatTestMode && ( - - ({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m) - - )} - -
-
- Remaining: - - {Number(alert.tatHoursRemaining || 0).toFixed(2)}h - {alert.metadata?.tatTestMode && ( - - ({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m) - - )} - -
-
- Due by: - - {alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'} - -
-
- -
-
-

- Reminder sent by system automatically -

- {(alert.metadata?.testMode || alert.metadata?.tatTestMode) && ( - - TEST MODE - - )} -
-

- Sent at: {alert.alertSentAt ? formatDateTime(alert.alertSentAt) : 'N/A'} -

- {(alert.metadata?.testMode || alert.metadata?.tatTestMode) && ( -

- Note: Test mode active (1 hour = 1 minute) -

- )} -
-
-
-
- ))} -
- )} - - {step.timestamp && ( -

- {isCompleted ? 'Approved' : isRejected ? 'Rejected' : 'Actioned'} on {formatDateTime(step.timestamp)} -

- )} - - {/* Skip Approver Button - Only show for initiator on pending/in-review levels */} - {isInitiator && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && ( -
- -

- Skip if approver is unavailable and move to next level -

-
- )} -
-
-
- ); - })} -
- ) : ( -

No workflow steps defined

- )} -
-
-
- - {/* Documents Tab */} - -
- {/* Section 1: Request Documents */} - - -
-
- - - Request Documents - - Documents attached while creating the request -
- -
-
- - {request.documents && request.documents.length > 0 ? ( -
- {request.documents.map((doc: any, index: number) => ( -
-
-
- -
-
-

{doc.name}

-

- {doc.size} β€’ Uploaded by {doc.uploadedBy} on {formatDateTime(doc.uploadedAt)} -

-
-
-
- {/* Preview button for images and PDFs */} - {doc.documentId && (() => { - const type = (doc.fileType || '').toLowerCase(); - return type.includes('image') || type.includes('pdf') || - type.includes('jpg') || type.includes('jpeg') || - type.includes('png') || type.includes('gif'); - })() && ( - - )} - - {/* Download button */} - -
-
- ))} -
- ) : ( -

No documents uploaded yet

- )} -
-
- - {/* Section 2: Work Note Attachments */} - - - - - Work Note Attachments - - Files shared in work notes discussions - - - {workNoteAttachments && workNoteAttachments.length > 0 ? ( -
- {workNoteAttachments.map((file: any, index: number) => { - return ( -
-
-
- -
-
-

{file.name}

-

- {file.size ? `${(file.size / 1024).toFixed(1)} KB` : 'Unknown size'} β€’ Shared by {file.uploadedBy} on {formatDateTime(file.uploadedAt)} -

-
-
-
- {/* Preview button */} - {file.attachmentId && (() => { - const type = (file.type || '').toLowerCase(); - return type.includes('image') || type.includes('pdf') || - type.includes('jpg') || type.includes('jpeg') || - type.includes('png') || type.includes('gif'); - })() && ( - - )} - - {/* Download button */} - -
-
- ); - })} -
- ) : ( -

No files shared in work notes yet

- )} -
-
-
-
- - {/* Activity Tab */} - - - - - - Activity Timeline - - - Complete audit trail of all request activities - - - -
- {request.auditTrail && request.auditTrail.length > 0 ? request.auditTrail.map((entry: any, index: number) => ( -
- {/* Icon */} -
-
- {getActionTypeIcon(entry.type)} -
-
- - {/* Activity Content */} -
-
- {/* Header with action title and timestamp */} -
-

{entry.action}

- {formatDateTime(entry.timestamp)} -
- - {/* Details (includes "by whom" at the end) */} -
-

{entry.details}

-
-
-
-
- )) : ( -
- -

No activity recorded yet

-

Actions and updates will appear here

-
- )} -
-
-
-
- - {/* Work Notes Tab - Full Width (Last Tab) */} - - -
- - {/* Right Column - Quick Actions Sidebar (1/3 width) - Hidden for Work Notes Tab */} - {activeTab !== 'worknotes' && ( -
- {/* Quick Actions */} - - - Quick Actions - - - {/* Only initiator can add approvers (not for closed requests) */} - {isInitiator && request.status !== 'closed' && ( - - )} - {/* Non-spectators can add spectators (not for closed requests) */} - {!isSpectator && request.status !== 'closed' && ( - - )} - -
- {!isSpectator && currentApprovalLevel && ( - <> - - - - )} -
-
-
- - {/* Spectators */} - {request.spectators && request.spectators.length > 0 && ( - - - Spectators - - - {request.spectators.map((spectator: any, index: number) => ( -
- - - {spectator.avatar} - - -
-

{spectator.name}

-

{spectator.role}

-
-
- ))} -
-
- )} -
- )} -
- - {/* Read-Only Conclusion Remark - Shows for closed requests */} - {request.status === 'closed' && request.conclusionRemark && activeTab !== 'worknotes' && ( -
- - - - - Conclusion Remark - - - Final summary of this closed request - - - -
-

- {request.conclusionRemark} -

-
- - {request.closureDate && ( -
- Request closed on {formatDateTime(request.closureDate)} - By {request.initiator?.name || 'Initiator'} -
- )} -
-
-
- )} - - {/* Conclusion Remark Section - Shows below tabs when request is approved */} - {needsClosure && activeTab !== 'worknotes' && ( -
- - -
-
- - - Conclusion Remark - Final Step - - - All approvals are complete. Please review and finalize the conclusion to close this request. - -
- -
-
- - {conclusionLoading ? ( -
-
- -

Preparing conclusion remark...

-
-
- ) : ( -
-
-
- - {aiGenerated && ( - βœ“ System-generated suggestion (editable) - )} -
- -