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 [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchRequests = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Clear old data first
|
||||
setItems([]);
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
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
|
||||
|
||||
// Extract data - workflowApi now returns { data: [], pagination: {} }
|
||||
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)
|
||||
: [];
|
||||
|
||||
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) {
|
||||
|
||||
<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…' : `${filteredAndSortedRequests.length} closed`}
|
||||
{loading ? 'Loading…' : `${totalRecords || items.length} closed`}
|
||||
<span className="hidden sm:inline ml-1">requests</span>
|
||||
</Badge>
|
||||
<Button
|
||||
@ -500,6 +536,67 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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 { 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,
|
||||
@ -99,43 +100,80 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasFetchedFromApi, setHasFetchedFromApi] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Clear old data first
|
||||
setApiRequests([]);
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
const [itemsPerPage] = useState(10);
|
||||
|
||||
const result = await workflowApi.listMyWorkflows({ page: 1, limit: 20 });
|
||||
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
|
||||
|
||||
// Handle nested data structure from API
|
||||
const items = result?.data?.data ? result.data.data :
|
||||
Array.isArray(result?.data) ? result.data :
|
||||
Array.isArray(result) ? result : [];
|
||||
// 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
|
||||
|
||||
if (!mounted) return;
|
||||
setApiRequests(items);
|
||||
setHasFetchedFromApi(true); // Mark that we've fetched from API
|
||||
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);
|
||||
if (!mounted) return;
|
||||
setApiRequests([]);
|
||||
setHasFetchedFromApi(true); // Still mark as fetched even on error
|
||||
setHasFetchedFromApi(true);
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => { mounted = 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
|
||||
// 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 (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-4 sm:space-y-6 max-w-7xl mx-auto">
|
||||
{/* Enhanced Header */}
|
||||
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-3 sm:gap-4 md:gap-6">
|
||||
<div className="space-y-1 sm:space-y-2">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<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">
|
||||
<FileText className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-semibold text-gray-900 flex items-center gap-2">
|
||||
<User className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[--re-green]" />
|
||||
My Requests
|
||||
</h1>
|
||||
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
||||
Track and manage all your submitted requests
|
||||
</p>
|
||||
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900">My Requests</h1>
|
||||
<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>
|
||||
|
||||
@ -411,6 +458,67 @@ export function MyRequests({ onViewRequest, dynamicRequests = [] }: MyRequestsPr
|
||||
))
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@ -98,25 +98,37 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const fetchRequests = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// Clear old data first
|
||||
setItems([]);
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalRecords, setTotalRecords] = useState(0);
|
||||
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
|
||||
|
||||
// Extract data - workflowApi now returns { data: [], pagination: {} }
|
||||
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)
|
||||
: [];
|
||||
|
||||
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) {
|
||||
|
||||
<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…' : `${filteredAndSortedRequests.length} open`}
|
||||
{loading ? 'Loading…' : `${totalRecords || items.length} open`}
|
||||
<span className="hidden sm:inline ml-1">requests</span>
|
||||
</Badge>
|
||||
<Button
|
||||
@ -370,7 +406,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
||||
</Card>
|
||||
|
||||
{/* Requests List */}
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{filteredAndSortedRequests.map((request) => {
|
||||
const priorityConfig = getPriorityConfig(request.priority);
|
||||
const statusConfig = getStatusConfig(request.status);
|
||||
@ -378,168 +414,125 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
||||
return (
|
||||
<Card
|
||||
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)}
|
||||
>
|
||||
<CardContent className="p-3 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row items-start gap-3 sm:gap-6">
|
||||
{/* Priority Indicator */}
|
||||
<div className="flex sm:flex-col items-center gap-2 pt-1 w-full sm:w-auto">
|
||||
<div className={`p-2 sm:p-3 rounded-xl ${priorityConfig.color} border flex-shrink-0`}>
|
||||
<priorityConfig.icon className={`w-4 h-4 sm:w-5 sm:h-5 ${priorityConfig.iconColor}`} />
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Left: Priority Icon */}
|
||||
<div className="flex-shrink-0 pt-1">
|
||||
<div className={`p-2.5 rounded-lg ${priorityConfig.color} border shadow-sm`}>
|
||||
<priorityConfig.icon className={`w-5 h-5 ${priorityConfig.iconColor}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center: Main Content */}
|
||||
<div className="flex-1 min-w-0 space-y-2.5">
|
||||
{/* Header Row */}
|
||||
<div className="flex items-center gap-2.5 flex-wrap">
|
||||
<h3 className="text-base font-bold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{(request as any).displayId || request.id}
|
||||
</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`text-xs font-medium ${priorityConfig.color} capitalize flex-shrink-0`}
|
||||
className={`${statusConfig.color} text-xs px-2.5 py-0.5 font-semibold shrink-0`}
|
||||
>
|
||||
<statusConfig.icon className="w-3.5 h-3.5 mr-1" />
|
||||
{(statusConfig as any).label || request.status}
|
||||
</Badge>
|
||||
{request.department && (
|
||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700 text-xs px-2.5 py-0.5 hidden sm:inline-flex">
|
||||
{request.department}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${priorityConfig.color} text-xs px-2.5 py-0.5 capitalize hidden md:inline-flex`}
|
||||
>
|
||||
{request.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-w-0 space-y-3 sm:space-y-4 w-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between gap-2 sm:gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-1.5 sm:gap-3 mb-2">
|
||||
<h3 className="text-sm sm:text-lg font-semibold text-gray-900 group-hover:text-blue-600 transition-colors">
|
||||
{(request as any).displayId || request.id}
|
||||
</h3>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={`${statusConfig.color} border font-medium text-xs shrink-0`}
|
||||
>
|
||||
<statusConfig.icon className="w-3 h-3 mr-1" />
|
||||
<span className="capitalize">{(statusConfig as any).label || request.status}</span>
|
||||
</Badge>
|
||||
{request.department && (
|
||||
<Badge variant="secondary" className="bg-gray-100 text-gray-700 text-xs hidden sm:inline-flex shrink-0">
|
||||
{request.department}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-base sm:text-xl font-bold text-gray-900 mb-2 line-clamp-2">
|
||||
{/* Title */}
|
||||
<h4 className="text-sm font-semibold text-gray-800 line-clamp-1 leading-relaxed">
|
||||
{request.title}
|
||||
</h4>
|
||||
<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>
|
||||
|
||||
{/* SLA Display - Shows backend-calculated SLA */}
|
||||
{/* SLA Display - Compact Version */}
|
||||
{request.currentLevelSLA && (
|
||||
<div className="pt-3 border-t border-gray-100">
|
||||
<div className={`p-3 rounded-lg ${
|
||||
<div className={`p-2 rounded-md ${
|
||||
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 === 'approaching' ? 'bg-yellow-50 border border-yellow-200' :
|
||||
'bg-green-50 border border-green-200'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="w-4 h-4 text-gray-600" />
|
||||
<span className="text-sm font-medium text-gray-900">SLA Progress</span>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock className="w-3.5 h-3.5 text-gray-600" />
|
||||
<span className="text-xs font-medium text-gray-900">TAT: {request.currentLevelSLA.percentageUsed}%</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'
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
|
||||
<span className={`font-semibold ${
|
||||
request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
|
||||
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
|
||||
'text-gray-700'
|
||||
}`}>
|
||||
{request.currentLevelSLA.percentageUsed}%
|
||||
</Badge>
|
||||
{request.currentLevelSLA.remainingText} left
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Progress
|
||||
value={request.currentLevelSLA.percentageUsed}
|
||||
className={`h-2 mb-2 ${
|
||||
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 className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-600">
|
||||
{request.currentLevelSLA.elapsedText} elapsed
|
||||
</span>
|
||||
<span className={`font-semibold ${
|
||||
request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
|
||||
request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
|
||||
'text-gray-700'
|
||||
}`}>
|
||||
{request.currentLevelSLA.remainingText} remaining
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{request.currentLevelSLA.deadline && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Due: {new Date(request.currentLevelSLA.deadline).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Info */}
|
||||
<div className="flex items-center gap-2 sm:gap-4 p-3 sm:p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AlertCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4 text-blue-500 flex-shrink-0" />
|
||||
<span className="text-xs sm:text-sm text-gray-700 font-medium truncate">
|
||||
{request.approvalStep}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Participants & Metadata */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 min-w-0">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Avatar className="h-7 w-7 sm:h-8 sm:w-8 ring-2 ring-white shadow-sm flex-shrink-0">
|
||||
<AvatarFallback className="bg-slate-700 text-white text-xs sm:text-sm font-semibold">
|
||||
{/* Metadata Row */}
|
||||
<div className="flex flex-wrap items-center gap-4 text-xs text-gray-600">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Avatar className="h-6 w-6 ring-2 ring-white shadow-sm">
|
||||
<AvatarFallback className="bg-gradient-to-br from-slate-700 to-slate-900 text-white text-[10px] font-bold">
|
||||
{request.initiator.avatar}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<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>
|
||||
<span className="font-medium text-gray-900">{request.initiator.name}</span>
|
||||
</div>
|
||||
|
||||
{request.currentApprover && (
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Avatar className="h-7 w-7 sm:h-8 sm:w-8 ring-2 ring-yellow-200 shadow-sm flex-shrink-0">
|
||||
<AvatarFallback className="bg-yellow-500 text-white text-xs sm:text-sm font-semibold">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Avatar className="h-6 w-6 ring-2 ring-yellow-200 shadow-sm">
|
||||
<AvatarFallback className="bg-yellow-500 text-white text-[10px] font-bold">
|
||||
{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>
|
||||
<span className="font-medium text-gray-900">{request.currentApprover.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{request.approvalStep && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<AlertCircle className="w-3.5 h-3.5 text-blue-500" />
|
||||
<span className="font-medium">{request.approvalStep}</span>
|
||||
</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 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>
|
||||
{/* 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>
|
||||
</CardContent>
|
||||
@ -574,6 +567,67 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
||||
</CardContent>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -95,10 +95,31 @@ export interface CriticalRequest {
|
||||
totalLevels: number;
|
||||
submissionDate: string;
|
||||
totalTATHours: number;
|
||||
originalTATHours: number;
|
||||
breachCount: number;
|
||||
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 {
|
||||
levelId: 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 {
|
||||
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) {
|
||||
console.error('Failed to fetch recent activity:', 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 {
|
||||
const response = await apiClient.get('/dashboard/requests/critical');
|
||||
return response.data.data;
|
||||
const response = await apiClient.get('/dashboard/requests/critical', {
|
||||
params: { page, limit }
|
||||
});
|
||||
return {
|
||||
criticalRequests: response.data.data,
|
||||
pagination: response.data.pagination
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch critical requests:', 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 {
|
||||
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) {
|
||||
console.error('Failed to fetch upcoming deadlines:', error);
|
||||
throw error;
|
||||
@ -298,6 +354,47 @@ class DashboardService {
|
||||
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();
|
||||
|
||||
@ -162,19 +162,31 @@ export async function listWorkflows(params: { page?: number; limit?: number } =
|
||||
export async function listMyWorkflows(params: { page?: number; limit?: number } = {}) {
|
||||
const { page = 1, limit = 20 } = params;
|
||||
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 } = {}) {
|
||||
const { page = 1, limit = 20 } = params;
|
||||
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 } = {}) {
|
||||
const { page = 1, limit = 20 } = params;
|
||||
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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user