dashboard enhanced and pagination added
This commit is contained in:
parent
d60757ae72
commit
90f29c11bd
@ -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
|
|
||||||
<Input type="text" value={value} onChange={...} />
|
|
||||||
```
|
|
||||||
- Used for: File types, text values
|
|
||||||
|
|
||||||
### **Number Input:**
|
|
||||||
```tsx
|
|
||||||
<Input type="number" min={1} max={100} value={value} onChange={...} />
|
|
||||||
```
|
|
||||||
- Used for: TAT hours, file sizes, retention days
|
|
||||||
- Validation: min/max enforced
|
|
||||||
|
|
||||||
### **Slider:**
|
|
||||||
```tsx
|
|
||||||
<Slider value={[50]} min={0} max={100} step={1} onChange={...} />
|
|
||||||
```
|
|
||||||
- Used for: Percentage thresholds
|
|
||||||
- Visual feedback with current value display
|
|
||||||
|
|
||||||
### **Toggle Switch:**
|
|
||||||
```tsx
|
|
||||||
<Switch checked={true} onCheckedChange={...} />
|
|
||||||
```
|
|
||||||
- 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
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
|
|
||||||
@ -102,25 +102,37 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const fetchRequests = async () => {
|
// Pagination states
|
||||||
try {
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
setLoading(true);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
// Clear old data first
|
const [totalRecords, setTotalRecords] = useState(0);
|
||||||
setItems([]);
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
const result = await workflowApi.listClosedByMe({ page: 1, limit: 50 });
|
const fetchRequests = async (page: number = 1) => {
|
||||||
|
try {
|
||||||
|
if (page === 1) {
|
||||||
|
setLoading(true);
|
||||||
|
setItems([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await workflowApi.listClosedByMe({ page, limit: itemsPerPage });
|
||||||
console.log('[ClosedRequests] API Response:', result); // Debug log
|
console.log('[ClosedRequests] API Response:', result); // Debug log
|
||||||
|
|
||||||
|
// Extract data - workflowApi now returns { data: [], pagination: {} }
|
||||||
const data = Array.isArray((result as any)?.data)
|
const data = Array.isArray((result as any)?.data)
|
||||||
? (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)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log('[ClosedRequests] Parsed data count:', data.length); // Debug log
|
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
|
const mapped: Request[] = data
|
||||||
.filter((r: any) => ['APPROVED', 'REJECTED', 'CLOSED'].includes((r.status || '').toString()))
|
.filter((r: any) => ['APPROVED', 'REJECTED', 'CLOSED'].includes((r.status || '').toString()))
|
||||||
.map((r: any) => ({
|
.map((r: any) => ({
|
||||||
@ -151,11 +163,35 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
|
|||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setRefreshing(true);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchRequests();
|
fetchRequests(1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredAndSortedRequests = useMemo(() => {
|
const filteredAndSortedRequests = useMemo(() => {
|
||||||
@ -233,7 +269,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
|
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
|
||||||
{loading ? 'Loading…' : `${filteredAndSortedRequests.length} closed`}
|
{loading ? 'Loading…' : `${totalRecords || items.length} closed`}
|
||||||
<span className="hidden sm:inline ml-1">requests</span>
|
<span className="hidden sm:inline ml-1">requests</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
@ -500,6 +536,67 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{totalPages > 1 && !loading && (
|
||||||
|
<Card className="shadow-md">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
|
<div className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} closed requests
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4 rotate-180" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentPage > 3 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getPageNumbers().map((pageNum) => (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pageNum === currentPage ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentPage < totalPages - 2 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import {
|
import {
|
||||||
FileText,
|
FileText,
|
||||||
@ -99,43 +100,80 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false);
|
const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
// Pagination states
|
||||||
let mounted = true;
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
(async () => {
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
try {
|
const [totalRecords, setTotalRecords] = useState(0);
|
||||||
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
const fetchMyRequests = async (page: number = 1) => {
|
||||||
|
try {
|
||||||
|
if (page === 1) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// Clear old data first
|
|
||||||
setApiRequests([]);
|
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; };
|
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(() => {
|
||||||
|
fetchMyRequests(1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Convert API/dynamic requests to the format expected by this component
|
// 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
|
// Once API has fetched (even if empty), always use API data, never fall back to props
|
||||||
const sourceRequests = hasFetchedFromApi ? apiRequests : dynamicRequests;
|
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 createdAt = req.submittedAt || req.submitted_at || req.createdAt || req.created_at;
|
||||||
const priority = (req.priority || '').toString().toLowerCase();
|
const priority = (req.priority || '').toString().toLowerCase();
|
||||||
|
|
||||||
@ -155,7 +193,7 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
|||||||
templateType: req.templateType,
|
templateType: req.templateType,
|
||||||
templateName: req.templateName
|
templateName: req.templateName
|
||||||
};
|
};
|
||||||
});
|
}) : [];
|
||||||
|
|
||||||
// Use only API/dynamic requests
|
// Use only API/dynamic requests
|
||||||
const allRequests = convertedDynamicRequests;
|
const allRequests = convertedDynamicRequests;
|
||||||
@ -174,9 +212,9 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
|||||||
return matchesSearch && matchesStatus && matchesPriority;
|
return matchesSearch && matchesStatus && matchesPriority;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stats calculation
|
// Stats calculation - using total from pagination for total count
|
||||||
const stats = {
|
const stats = {
|
||||||
total: allRequests.length,
|
total: totalRecords || allRequests.length,
|
||||||
pending: allRequests.filter(r => r.status === 'pending').length,
|
pending: allRequests.filter(r => r.status === 'pending').length,
|
||||||
approved: allRequests.filter(r => r.status === 'approved').length,
|
approved: allRequests.filter(r => r.status === 'approved').length,
|
||||||
inReview: allRequests.filter(r => r.status === 'in-review').length,
|
inReview: allRequests.filter(r => r.status === 'in-review').length,
|
||||||
@ -185,17 +223,26 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Enhanced Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4 md:gap-6">
|
||||||
<div>
|
<div className="space-y-1 sm:space-y-2">
|
||||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-semibold text-gray-900 flex items-center gap-2">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<User className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[--re-green]" />
|
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl flex items-center justify-center shadow-lg">
|
||||||
My Requests
|
<FileText className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
|
||||||
</h1>
|
</div>
|
||||||
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
<div>
|
||||||
Track and manage all your submitted requests
|
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900">My Requests</h1>
|
||||||
</p>
|
<p className="text-sm sm:text-base text-gray-600">Track and manage all your submitted requests</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
|
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
|
||||||
|
{loading ? 'Loading…' : `${totalRecords || allRequests.length} total`}
|
||||||
|
<span className="hidden sm:inline ml-1">requests</span>
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -411,6 +458,67 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{totalPages > 1 && !loading && (
|
||||||
|
<Card className="shadow-md border-gray-200">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
|
<div className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} requests
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4 rotate-180" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentPage > 3 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getPageNumbers().map((pageNum) => (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pageNum === currentPage ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentPage < totalPages - 2 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -98,25 +98,37 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const fetchRequests = async () => {
|
// Pagination states
|
||||||
try {
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
setLoading(true);
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
// Clear old data first
|
const [totalRecords, setTotalRecords] = useState(0);
|
||||||
setItems([]);
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
const result = await workflowApi.listOpenForMe({ page: 1, limit: 50 });
|
const fetchRequests = async (page: number = 1) => {
|
||||||
|
try {
|
||||||
|
if (page === 1) {
|
||||||
|
setLoading(true);
|
||||||
|
setItems([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await workflowApi.listOpenForMe({ page, limit: itemsPerPage });
|
||||||
console.log('[OpenRequests] API Response:', result); // Debug log
|
console.log('[OpenRequests] API Response:', result); // Debug log
|
||||||
|
|
||||||
|
// Extract data - workflowApi now returns { data: [], pagination: {} }
|
||||||
const data = Array.isArray((result as any)?.data)
|
const data = Array.isArray((result as any)?.data)
|
||||||
? (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)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
console.log('[OpenRequests] Parsed data count:', data.length); // Debug log
|
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 mapped: Request[] = data.map((r: any) => {
|
||||||
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
|
const createdAt = r.submittedAt || r.submitted_at || r.createdAt || r.created_at;
|
||||||
|
|
||||||
@ -152,11 +164,35 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setRefreshing(true);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchRequests();
|
fetchRequests(1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredAndSortedRequests = useMemo(() => {
|
const filteredAndSortedRequests = useMemo(() => {
|
||||||
@ -239,7 +275,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
|
|
||||||
<div className="flex items-center gap-2 sm:gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
|
<Badge variant="secondary" className="text-xs sm:text-sm md:text-base px-2 sm:px-3 md:px-4 py-1 sm:py-1.5 md:py-2 bg-slate-100 text-slate-800 font-semibold">
|
||||||
{loading ? 'Loading…' : `${filteredAndSortedRequests.length} open`}
|
{loading ? 'Loading…' : `${totalRecords || items.length} open`}
|
||||||
<span className="hidden sm:inline ml-1">requests</span>
|
<span className="hidden sm:inline ml-1">requests</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button
|
<Button
|
||||||
@ -370,7 +406,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Requests List */}
|
{/* Requests List */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
{filteredAndSortedRequests.map((request) => {
|
{filteredAndSortedRequests.map((request) => {
|
||||||
const priorityConfig = getPriorityConfig(request.priority);
|
const priorityConfig = getPriorityConfig(request.priority);
|
||||||
const statusConfig = getStatusConfig(request.status);
|
const statusConfig = getStatusConfig(request.status);
|
||||||
@ -378,169 +414,126 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={request.id}
|
key={request.id}
|
||||||
className="group hover:shadow-xl transition-all duration-300 cursor-pointer border-0 shadow-md hover:scale-[1.01]"
|
className="group hover:shadow-lg transition-all duration-200 cursor-pointer border border-gray-200 hover:border-blue-400 hover:scale-[1.002]"
|
||||||
onClick={() => onViewRequest?.(request.id, request.title)}
|
onClick={() => onViewRequest?.(request.id, request.title)}
|
||||||
>
|
>
|
||||||
<CardContent className="p-3 sm:p-6">
|
<CardContent className="p-4">
|
||||||
<div className="flex flex-col sm:flex-row items-start gap-3 sm:gap-6">
|
<div className="flex items-start gap-4">
|
||||||
{/* Priority Indicator */}
|
{/* Left: Priority Icon */}
|
||||||
<div className="flex sm:flex-col items-center gap-2 pt-1 w-full sm:w-auto">
|
<div className="flex-shrink-0 pt-1">
|
||||||
<div className={`p-2 sm:p-3 rounded-xl ${priorityConfig.color} border flex-shrink-0`}>
|
<div className={`p-2.5 rounded-lg ${priorityConfig.color} border shadow-sm`}>
|
||||||
<priorityConfig.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${priorityConfig.iconColor}`} />
|
<priorityConfig.icon className={`w-5 h-5 ${priorityConfig.iconColor}`} />
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`text-xs font-medium ${priorityConfig.color} capitalize flex-shrink-0`}
|
|
||||||
>
|
|
||||||
{request.priority}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Center: Main Content */}
|
||||||
<div className="flex-1 min-w-0 space-y-3 sm:space-y-4 w-full">
|
<div className="flex-1 min-w-0 space-y-2.5">
|
||||||
{/* Header */}
|
{/* Header Row */}
|
||||||
<div className="flex items-start justify-between gap-2 sm:gap-4">
|
<div className="flex items-center gap-2.5 flex-wrap">
|
||||||
<div className="flex-1 min-w-0">
|
<h3 className="text-base font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||||
<div className="flex flex-wrap items-center gap-1.5 sm:gap-3 mb-2">
|
{(request as any).displayId || request.id}
|
||||||
<h3 className="text-sm sm:text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
</h3>
|
||||||
{(request as any).displayId || request.id}
|
<Badge
|
||||||
</h3>
|
variant="outline"
|
||||||
<Badge
|
className={`${statusConfig.color} text-xs px-2.5 py-0.5 font-semibold shrink-0`}
|
||||||
variant="outline"
|
>
|
||||||
className={`${statusConfig.color} border font-medium text-xs shrink-0`}
|
<statusConfig.icon className="w-3.5 h-3.5 mr-1" />
|
||||||
>
|
{(statusConfig as any).label || request.status}
|
||||||
<statusConfig.icon className="w-3 h-3 mr-1" />
|
</Badge>
|
||||||
<span className="capitalize">{(statusConfig as any).label || request.status}</span>
|
{request.department && (
|
||||||
</Badge>
|
<Badge variant="secondary" className="bg-blue-50 text-blue-700 text-xs px-2.5 py-0.5 hidden sm:inline-flex">
|
||||||
{request.department && (
|
{request.department}
|
||||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700 text-xs hidden sm:inline-flex shrink-0">
|
</Badge>
|
||||||
{request.department}
|
)}
|
||||||
</Badge>
|
<Badge
|
||||||
)}
|
variant="outline"
|
||||||
</div>
|
className={`${priorityConfig.color} text-xs px-2.5 py-0.5 capitalize hidden md:inline-flex`}
|
||||||
<h4 className="text-base sm:text-xl font-bold text-gray-900 mb-2 line-clamp-2">
|
>
|
||||||
{request.title}
|
{request.priority}
|
||||||
</h4>
|
</Badge>
|
||||||
<p className="text-xs sm:text-sm text-gray-600 line-clamp-2 leading-relaxed">
|
|
||||||
{request.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-2 flex-shrink-0">
|
|
||||||
<ArrowRight className="w-4 h-4 sm:w-5 sm:h-5 text-gray-400 group-hover:text-blue-600 transition-colors" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SLA Display - Shows backend-calculated SLA */}
|
{/* Title */}
|
||||||
|
<h4 className="text-sm font-semibold text-gray-800 line-clamp-1 leading-relaxed">
|
||||||
|
{request.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* SLA Display - Compact Version */}
|
||||||
{request.currentLevelSLA && (
|
{request.currentLevelSLA && (
|
||||||
<div className="pt-3 border-t border-gray-100">
|
<div className={`p-2 rounded-md ${
|
||||||
<div className={`p-3 rounded-lg ${
|
request.currentLevelSLA.status === 'breached' ? 'bg-red-50 border border-red-200' :
|
||||||
request.currentLevelSLA.status === 'breached' ? 'bg-red-50 border border-red-200' :
|
request.currentLevelSLA.status === 'critical' ? 'bg-orange-50 border border-orange-200' :
|
||||||
request.currentLevelSLA.status === 'critical' ? 'bg-orange-50 border border-orange-200' :
|
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-50 border border-yellow-200' :
|
||||||
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-50 border border-yellow-200' :
|
'bg-green-50 border border-green-200'
|
||||||
'bg-green-50 border border-green-200'
|
}`}>
|
||||||
}`}>
|
<div className="flex items-center justify-between mb-1.5">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<Clock className="w-3.5 h-3.5 text-gray-600" />
|
||||||
<Clock className="w-4 h-4 text-gray-600" />
|
<span className="text-xs font-medium text-gray-900">TAT: {request.currentLevelSLA.percentageUsed}%</span>
|
||||||
<span className="text-sm font-medium text-gray-900">SLA Progress</span>
|
|
||||||
</div>
|
|
||||||
<Badge className={`text-xs ${
|
|
||||||
request.currentLevelSLA.status === 'breached' ? 'bg-red-600 text-white' :
|
|
||||||
request.currentLevelSLA.status === 'critical' ? 'bg-orange-600 text-white' :
|
|
||||||
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-600 text-white' :
|
|
||||||
'bg-green-600 text-white'
|
|
||||||
}`}>
|
|
||||||
{request.currentLevelSLA.percentageUsed}%
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
<Progress
|
<span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
|
||||||
value={request.currentLevelSLA.percentageUsed}
|
|
||||||
className={`h-2 mb-2 ${
|
|
||||||
request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' :
|
|
||||||
request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' :
|
|
||||||
request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' :
|
|
||||||
'[&>div]:bg-green-600'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<span className="text-gray-600">
|
|
||||||
{request.currentLevelSLA.elapsedText} elapsed
|
|
||||||
</span>
|
|
||||||
<span className={`font-semibold ${
|
<span className={`font-semibold ${
|
||||||
request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
|
request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
|
||||||
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
|
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
|
||||||
'text-gray-700'
|
'text-gray-700'
|
||||||
}`}>
|
}`}>
|
||||||
{request.currentLevelSLA.remainingText} remaining
|
{request.currentLevelSLA.remainingText} left
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{request.currentLevelSLA.deadline && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
Due: {new Date(request.currentLevelSLA.deadline).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={request.currentLevelSLA.percentageUsed}
|
||||||
|
className={`h-1.5 ${
|
||||||
|
request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' :
|
||||||
|
request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' :
|
||||||
|
request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' :
|
||||||
|
'[&>div]:bg-green-600'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Status Info */}
|
{/* Metadata Row */}
|
||||||
<div className="flex items-center gap-2 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-600">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-1.5">
|
||||||
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-500 flex-shrink-0" />
|
<Avatar className="h-6 w-6 ring-2 ring-white shadow-sm">
|
||||||
<span className="text-xs sm:text-sm text-gray-700 font-medium truncate">
|
<AvatarFallback className="bg-gradient-to-br from-slate-700 to-slate-900 text-white text-[10px] font-bold">
|
||||||
{request.approvalStep}
|
{request.initiator.avatar}
|
||||||
</span>
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span className="font-medium text-gray-900">{request.initiator.name}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Participants & Metadata */}
|
{request.currentApprover && (
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
<div className="flex items-center gap-1.5">
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 min-w-0">
|
<Avatar className="h-6 w-6 ring-2 ring-yellow-200 shadow-sm">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<AvatarFallback className="bg-yellow-500 text-white text-[10px] font-bold">
|
||||||
<Avatar className="h-7 w-7 sm:h-8 sm:w-8 ring-2 ring-white shadow-sm flex-shrink-0">
|
{request.currentApprover.avatar}
|
||||||
<AvatarFallback className="bg-slate-700 text-white text-xs sm:text-sm font-semibold">
|
|
||||||
{request.initiator.avatar}
|
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="min-w-0">
|
<span className="font-medium text-gray-900">{request.currentApprover.name}</span>
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate">{request.initiator.name}</p>
|
|
||||||
<p className="text-xs text-gray-500">Initiator</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{request.currentApprover && (
|
{request.approvalStep && (
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-1.5">
|
||||||
<Avatar className="h-7 w-7 sm:h-8 sm:w-8 ring-2 ring-yellow-200 shadow-sm flex-shrink-0">
|
<AlertCircle className="w-3.5 h-3.5 text-blue-500" />
|
||||||
<AvatarFallback className="bg-yellow-500 text-white text-xs sm:text-sm font-semibold">
|
<span className="font-medium">{request.approvalStep}</span>
|
||||||
{request.currentApprover.avatar}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-xs sm:text-sm font-medium text-gray-900 truncate">{request.currentApprover.name}</p>
|
|
||||||
<p className="text-xs text-gray-500">Current Approver</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-left sm:text-right">
|
|
||||||
<div className="flex flex-col gap-1 text-xs text-gray-500">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="w-3 h-3 flex-shrink-0" />
|
|
||||||
<span className="truncate">Created: {request.createdAt !== '—' ? formatDateShort(request.createdAt) : '—'}</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Clock className="w-3 h-3 flex-shrink-0" />
|
|
||||||
<span className="truncate">Due: {request.currentLevelSLA?.deadline ? formatDateShort(request.currentLevelSLA.deadline) : 'Not set'}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
|
<span>Created: {request.createdAt !== '—' ? formatDateShort(request.createdAt) : '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Arrow */}
|
||||||
|
<div className="flex-shrink-0 flex items-center pt-2">
|
||||||
|
<ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -574,6 +567,67 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{totalPages > 1 && !loading && (
|
||||||
|
<Card className="shadow-md">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
|
||||||
|
<div className="text-xs sm:text-sm text-muted-foreground">
|
||||||
|
Showing {((currentPage - 1) * itemsPerPage) + 1} to {Math.min(currentPage * itemsPerPage, totalRecords)} of {totalRecords} open requests
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4 rotate-180" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{currentPage > 3 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(1)} className="h-8 w-8 p-0">1</Button>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getPageNumbers().map((pageNum) => (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pageNum === currentPage ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{currentPage < totalPages - 2 && totalPages > 5 && (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">...</span>
|
||||||
|
<Button variant="outline" size="sm" onClick={() => handlePageChange(totalPages)} className="h-8 w-8 p-0">{totalPages}</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -95,10 +95,31 @@ export interface CriticalRequest {
|
|||||||
totalLevels: number;
|
totalLevels: number;
|
||||||
submissionDate: string;
|
submissionDate: string;
|
||||||
totalTATHours: number;
|
totalTATHours: number;
|
||||||
|
originalTATHours: number;
|
||||||
breachCount: number;
|
breachCount: number;
|
||||||
isCritical: boolean;
|
isCritical: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AIRemarkUtilization {
|
||||||
|
totalUsage: number;
|
||||||
|
totalEdits: number;
|
||||||
|
editRate: number;
|
||||||
|
monthlyTrends: Array<{
|
||||||
|
month: string;
|
||||||
|
aiUsage: number;
|
||||||
|
manualEdits: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApproverPerformance {
|
||||||
|
approverId: string;
|
||||||
|
approverName: string;
|
||||||
|
totalApproved: number;
|
||||||
|
tatCompliancePercent: number;
|
||||||
|
avgResponseHours: number;
|
||||||
|
pendingCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpcomingDeadline {
|
export interface UpcomingDeadline {
|
||||||
levelId: string;
|
levelId: string;
|
||||||
requestId: string;
|
requestId: string;
|
||||||
@ -227,14 +248,25 @@ class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get recent activity feed
|
* Get recent activity feed with pagination
|
||||||
*/
|
*/
|
||||||
async getRecentActivity(limit: number = 10): Promise<RecentActivity[]> {
|
async getRecentActivity(page: number = 1, limit: number = 10): Promise<{
|
||||||
|
activities: RecentActivity[],
|
||||||
|
pagination: {
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
totalRecords: number,
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/dashboard/activity/recent', {
|
const response = await apiClient.get('/dashboard/activity/recent', {
|
||||||
params: { limit }
|
params: { page, limit }
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return {
|
||||||
|
activities: response.data.data,
|
||||||
|
pagination: response.data.pagination
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch recent activity:', error);
|
console.error('Failed to fetch recent activity:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -242,12 +274,25 @@ class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get critical requests
|
* Get critical requests with pagination
|
||||||
*/
|
*/
|
||||||
async getCriticalRequests(): Promise<CriticalRequest[]> {
|
async getCriticalRequests(page: number = 1, limit: number = 10): Promise<{
|
||||||
|
criticalRequests: CriticalRequest[],
|
||||||
|
pagination: {
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
totalRecords: number,
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/dashboard/requests/critical');
|
const response = await apiClient.get('/dashboard/requests/critical', {
|
||||||
return response.data.data;
|
params: { page, limit }
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
criticalRequests: response.data.data,
|
||||||
|
pagination: response.data.pagination
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch critical requests:', error);
|
console.error('Failed to fetch critical requests:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -255,14 +300,25 @@ class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get upcoming deadlines
|
* Get upcoming deadlines with pagination
|
||||||
*/
|
*/
|
||||||
async getUpcomingDeadlines(limit: number = 5): Promise<UpcomingDeadline[]> {
|
async getUpcomingDeadlines(page: number = 1, limit: number = 10): Promise<{
|
||||||
|
deadlines: UpcomingDeadline[],
|
||||||
|
pagination: {
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
totalRecords: number,
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
}> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get('/dashboard/deadlines/upcoming', {
|
const response = await apiClient.get('/dashboard/deadlines/upcoming', {
|
||||||
params: { limit }
|
params: { page, limit }
|
||||||
});
|
});
|
||||||
return response.data.data;
|
return {
|
||||||
|
deadlines: response.data.data,
|
||||||
|
pagination: response.data.pagination
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch upcoming deadlines:', error);
|
console.error('Failed to fetch upcoming deadlines:', error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -298,6 +354,47 @@ class DashboardService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get AI Remark Utilization with monthly trends
|
||||||
|
*/
|
||||||
|
async getAIRemarkUtilization(dateRange?: DateRange): Promise<AIRemarkUtilization> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/dashboard/stats/ai-remark-utilization', {
|
||||||
|
params: { dateRange }
|
||||||
|
});
|
||||||
|
return response.data.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch AI remark utilization:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Approver Performance metrics with pagination
|
||||||
|
*/
|
||||||
|
async getApproverPerformance(dateRange?: DateRange, page: number = 1, limit: number = 10): Promise<{
|
||||||
|
performance: ApproverPerformance[],
|
||||||
|
pagination: {
|
||||||
|
currentPage: number,
|
||||||
|
totalPages: number,
|
||||||
|
totalRecords: number,
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get('/dashboard/stats/approver-performance', {
|
||||||
|
params: { dateRange, page, limit }
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
performance: response.data.data,
|
||||||
|
pagination: response.data.pagination
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch approver performance:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dashboardService = new DashboardService();
|
export const dashboardService = new DashboardService();
|
||||||
|
|||||||
@ -162,19 +162,31 @@ export async function listWorkflows(params: { page?: number; limit?: number } =
|
|||||||
export async function listMyWorkflows(params: { page?: number; limit?: number } = {}) {
|
export async function listMyWorkflows(params: { page?: number; limit?: number } = {}) {
|
||||||
const { page = 1, limit = 20 } = params;
|
const { page = 1, limit = 20 } = params;
|
||||||
const res = await apiClient.get('/workflows/my', { params: { page, limit } });
|
const res = await apiClient.get('/workflows/my', { params: { page, limit } });
|
||||||
return res.data?.data || res.data;
|
// Response structure: { success, data: { data: [...], pagination: {...} } }
|
||||||
|
return {
|
||||||
|
data: res.data?.data?.data || res.data?.data || [],
|
||||||
|
pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listOpenForMe(params: { page?: number; limit?: number } = {}) {
|
export async function listOpenForMe(params: { page?: number; limit?: number } = {}) {
|
||||||
const { page = 1, limit = 20 } = params;
|
const { page = 1, limit = 20 } = params;
|
||||||
const res = await apiClient.get('/workflows/open-for-me', { params: { page, limit } });
|
const res = await apiClient.get('/workflows/open-for-me', { params: { page, limit } });
|
||||||
return res.data?.data || res.data;
|
// Response structure: { success, data: { data: [...], pagination: {...} } }
|
||||||
|
return {
|
||||||
|
data: res.data?.data?.data || res.data?.data || [],
|
||||||
|
pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listClosedByMe(params: { page?: number; limit?: number } = {}) {
|
export async function listClosedByMe(params: { page?: number; limit?: number } = {}) {
|
||||||
const { page = 1, limit = 20 } = params;
|
const { page = 1, limit = 20 } = params;
|
||||||
const res = await apiClient.get('/workflows/closed-by-me', { params: { page, limit } });
|
const res = await apiClient.get('/workflows/closed-by-me', { params: { page, limit } });
|
||||||
return res.data?.data || res.data;
|
// Response structure: { success, data: { data: [...], pagination: {...} } }
|
||||||
|
return {
|
||||||
|
data: res.data?.data?.data || res.data?.data || [],
|
||||||
|
pagination: res.data?.data?.pagination || { page, limit, total: 0, totalPages: 1 }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getWorkflowDetails(requestId: string) {
|
export async function getWorkflowDetails(requestId: string) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user