dashboard enhanced and pagination added

This commit is contained in:
laxmanhalaki 2025-11-12 11:15:39 +05:30
parent d60757ae72
commit 90f29c11bd
9 changed files with 1282 additions and 3620 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
setLoading(true); const [itemsPerPage] = useState(10);
// Clear old data first
setApiRequests([]);
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 console.log('[MyRequests] API Response:', result); // Debug log
// Handle nested data structure from API // Extract data - workflowApi now returns { data: [], pagination: {} }
const items = result?.data?.data ? result.data.data : const items = Array.isArray((result as any)?.data)
Array.isArray(result?.data) ? result.data : ? (result as any).data
Array.isArray(result) ? result : []; : [];
console.log('[MyRequests] Parsed items:', items); // Debug log console.log('[MyRequests] Parsed items:', items); // Debug log
if (!mounted) return;
setApiRequests(items); 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) { } catch (error) {
console.error('[MyRequests] Error fetching requests:', error); console.error('[MyRequests] Error fetching requests:', error);
if (!mounted) return;
setApiRequests([]); setApiRequests([]);
setHasFetchedFromApi(true); // Still mark as fetched even on error setHasFetchedFromApi(true);
} finally { } 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 // 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 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> <div>
<h1 className="text-xl sm:text-2xl md:text-3xl font-semibold text-gray-900 flex items-center gap-2"> <h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900">My Requests</h1>
<User className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-[--re-green]" /> <p className="text-sm sm:text-base text-gray-600">Track and manage all your submitted requests</p>
My Requests </div>
</h1> </div>
<p className="text-sm sm:text-base text-gray-600 mt-1"> </div>
Track and manage all your submitted requests
</p> <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>
); );
} }

View File

@ -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,168 +414,125 @@ 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>
</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 <Badge
variant="outline" 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} {request.priority}
</Badge> </Badge>
</div> </div>
{/* Main Content */} {/* Title */}
<div className="flex-1 min-w-0 space-y-3 sm:space-y-4 w-full"> <h4 className="text-sm font-semibold text-gray-800 line-clamp-1 leading-relaxed">
{/* 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">
{request.title} {request.title}
</h4> </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"> {/* SLA Display - Compact Version */}
<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 */}
{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-2"> <div className="flex items-center justify-between mb-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-1.5">
<Clock className="w-4 h-4 text-gray-600" /> <Clock className="w-3.5 h-3.5 text-gray-600" />
<span className="text-sm font-medium text-gray-900">SLA Progress</span> <span className="text-xs font-medium text-gray-900">TAT: {request.currentLevelSLA.percentageUsed}%</span>
</div> </div>
<Badge className={`text-xs ${ <div className="flex items-center gap-2 text-xs">
request.currentLevelSLA.status === 'breached' ? 'bg-red-600 text-white' : <span className="text-gray-600">{request.currentLevelSLA.elapsedText}</span>
request.currentLevelSLA.status === 'critical' ? 'bg-orange-600 text-white' : <span className={`font-semibold ${
request.currentLevelSLA.status === 'approaching' ? 'bg-yellow-600 text-white' : request.currentLevelSLA.status === 'breached' ? 'text-red-600' :
'bg-green-600 text-white' request.currentLevelSLA.status === 'critical' ? 'text-orange-600' :
'text-gray-700'
}`}> }`}>
{request.currentLevelSLA.percentageUsed}% {request.currentLevelSLA.remainingText} left
</Badge> </span>
</div>
</div> </div>
<Progress <Progress
value={request.currentLevelSLA.percentageUsed} value={request.currentLevelSLA.percentageUsed}
className={`h-2 mb-2 ${ className={`h-1.5 ${
request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' : request.currentLevelSLA.status === 'breached' ? '[&>div]:bg-red-600' :
request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' : request.currentLevelSLA.status === 'critical' ? '[&>div]:bg-orange-600' :
request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' : request.currentLevelSLA.status === 'approaching' ? '[&>div]:bg-yellow-600' :
'[&>div]:bg-green-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> </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}
</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">
{request.initiator.avatar} {request.initiator.avatar}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="min-w-0"> <span className="font-medium text-gray-900">{request.initiator.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.currentApprover && (
<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"> <Avatar className="h-6 w-6 ring-2 ring-yellow-200 shadow-sm">
<AvatarFallback className="bg-yellow-500 text-white text-xs sm:text-sm font-semibold"> <AvatarFallback className="bg-yellow-500 text-white text-[10px] font-bold">
{request.currentApprover.avatar} {request.currentApprover.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.currentApprover.name}</p>
<p className="text-xs text-gray-500">Current Approver</p>
</div>
</div> </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>
<div className="text-left sm:text-right"> {/* Right: Arrow */}
<div className="flex flex-col gap-1 text-xs text-gray-500"> <div className="flex-shrink-0 flex items-center pt-2">
<span className="flex items-center gap-1"> <ArrowRight className="w-5 h-5 text-gray-400 group-hover:text-blue-600 group-hover:translate-x-1 transition-all" />
<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>
</div> </div>
</div> </div>
</CardContent> </CardContent>
@ -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

View File

@ -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();

View File

@ -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) {