export enhanced code cleaning and with some new bug fixes

This commit is contained in:
laxmanhalaki 2025-11-21 18:41:07 +05:30
parent 00f0b786f6
commit 99a59ac05b
81 changed files with 1177 additions and 754 deletions

393
README.md
View File

@ -4,43 +4,92 @@ A modern, enterprise-grade approval and request management system built with Rea
## 📋 Table of Contents ## 📋 Table of Contents
- [Features](#features) - [Features](#-features)
- [Tech Stack](#tech-stack) - [Tech Stack](#-tech-stack)
- [Prerequisites](#prerequisites) - [Prerequisites](#-prerequisites)
- [Installation](#installation) - [Installation](#-installation)
- [Development](#development) - [Development](#-development)
- [Project Structure](#project-structure) - [Project Structure](#-project-structure)
- [Available Scripts](#available-scripts) - [Available Scripts](#-available-scripts)
- [Configuration](#configuration) - [Configuration](#-configuration)
- [Contributing](#contributing) - [Key Features Deep Dive](#-key-features-deep-dive)
- [Troubleshooting](#-troubleshooting)
- [Contributing](#-contributing)
## ✨ Features ## ✨ Features
- **🔄 Dual Workflow System** ### 🔄 Dual Workflow System
- Custom Request Workflow with user-defined approvers - **Custom Request Workflow** - User-defined approvers, spectators, and workflow steps
- Claim Management Workflow (8-step predefined process) - **Claim Management Workflow** - 8-step predefined process for dealer claim management
- Flexible approval chains with multi-level approvers
- TAT (Turnaround Time) tracking at each approval level
- **📊 Comprehensive Dashboard** ### 📊 Comprehensive Dashboard
- Real-time statistics and metrics - Real-time statistics and metrics
- High-priority alerts - High-priority alerts and critical request tracking
- Recent activity tracking - Recent activity feed with pagination
- Upcoming deadlines and SLA breach warnings
- Department-wise performance metrics
- Customizable KPI widgets (Admin only)
- **🎯 Request Management** ### 🎯 Request Management
- Create, track, and manage approval requests - Create, track, and manage approval requests
- Document upload and management - Document upload and management with file type validation
- Work notes and audit trails - Work notes and comprehensive audit trails
- Spectator and stakeholder management - Spectator and stakeholder management
- Request filtering, search, and export capabilities
- Detailed request lifecycle tracking
- **🎨 Modern UI/UX** ### 👥 Admin Control Panel
- **User Management** - Search, assign roles (USER, MANAGEMENT, ADMIN), and manage user permissions
- **User Role Management** - Assign and manage user roles with Okta integration
- **System Configuration** - Comprehensive admin settings:
- **KPI Configuration** - Configure dashboard KPIs, visibility, and thresholds
- **Analytics Configuration** - Data retention, export formats, and analytics features
- **TAT Configuration** - Working hours, priority-based TAT, escalation settings
- **Notification Configuration** - Email templates, notification channels, and settings
- **Notification Preferences** - User-configurable notification settings
- **Document Configuration** - File type restrictions, size limits, upload policies
- **Dashboard Configuration** - Customize dashboard layout and widgets per role
- **AI Configuration** - AI provider settings, parameters, and features
- **Sharing Configuration** - Sharing policies and permissions
- **Holiday Management** - Configure business holidays for SLA calculations
### 📈 Approver Performance Analytics
- Detailed approver performance metrics and statistics
- Request approval history and trends
- Average approval time analysis
- Approval rate and efficiency metrics
- TAT compliance tracking per approver
- Performance comparison and benchmarking
- Export capabilities for performance reports
### 💬 Real-Time Live Chat (Work Notes)
- **WebSocket Integration** - Real-time bidirectional communication
- **Live Work Notes** - Instant messaging within request context
- **Presence Indicators** - See who's online/offline in real-time
- **Mention System** - @mention participants for notifications
- **File Attachments** - Share documents directly in chat
- **Message History** - Persistent chat history per request
- **Auto-reconnection** - Automatic reconnection on network issues
- **Room-based Communication** - Isolated chat rooms per request
### 🔔 Advanced Notifications
- **Web Push Notifications** - Browser push notifications using VAPID
- **Service Worker Integration** - Background notification delivery
- **Real-time Toast Notifications** - In-app notification system
- **SLA Tracking & Reminders** - Automated TAT breach alerts
- **Approval Status Updates** - Real-time status change notifications
- **Email Notifications** - Configurable email notification channels
- **Notification Preferences** - User-configurable notification settings
### 🎨 Modern UI/UX
- Responsive design (mobile, tablet, desktop) - Responsive design (mobile, tablet, desktop)
- Dark mode support - Dark mode support
- Accessible components (WCAG compliant) - Accessible components (WCAG compliant)
- Royal Enfield brand theming - Royal Enfield brand theming
- Smooth animations and transitions
- **🔔 Notifications** - Intuitive navigation and user flows
- Real-time toast notifications
- SLA tracking and reminders
- Approval status updates
## 🛠️ Tech Stack ## 🛠️ Tech Stack
@ -50,8 +99,11 @@ A modern, enterprise-grade approval and request management system built with Rea
- **Styling:** Tailwind CSS 3.4+ - **Styling:** Tailwind CSS 3.4+
- **UI Components:** shadcn/ui + Radix UI - **UI Components:** shadcn/ui + Radix UI
- **Icons:** Lucide React - **Icons:** Lucide React
- **Notifications:** Sonner - **Notifications:** Sonner (Toast) + Web Push API (VAPID)
- **State Management:** React Hooks (useState, useMemo) - **Real-Time Communication:** Socket.IO Client
- **State Management:** React Hooks (useState, useMemo, useContext)
- **Authentication:** Okta SSO Integration
- **HTTP Client:** Axios
## 📦 Prerequisites ## 📦 Prerequisites
@ -74,7 +126,7 @@ A modern, enterprise-grade approval and request management system built with Rea
\`\`\`bash \`\`\`bash
git clone <repository-url> git clone <repository-url>
cd Re_Figma_Code cd Re_Frontend_Code
\`\`\` \`\`\`
### 2. Install dependencies ### 2. Install dependencies
@ -147,11 +199,20 @@ VITE_PUBLIC_VAPID_KEY=your-production-vapid-key
| Variable | Description | Required | Default | | Variable | Description | Required | Default |
|----------|-------------|----------|---------| |----------|-------------|----------|---------|
| `VITE_API_BASE_URL` | Backend API base URL (with `/api/v1`) | Yes | `http://localhost:5000/api/v1` | | `VITE_API_BASE_URL` | Backend API base URL (with `/api/v1`) | Yes | `http://localhost:5000/api/v1` |
| `VITE_BASE_URL` | Base URL for direct file access (without `/api/v1`) | Yes | `http://localhost:5000` | | `VITE_BASE_URL` | Base URL for WebSocket and direct file access (without `/api/v1`) | Yes | `http://localhost:5000` |
| `VITE_OKTA_DOMAIN` | Okta domain for SSO authentication | Yes* | - | | `VITE_OKTA_DOMAIN` | Okta domain for SSO authentication | Yes* | - |
| `VITE_OKTA_CLIENT_ID` | Okta client ID for authentication | Yes* | - | | `VITE_OKTA_CLIENT_ID` | Okta client ID for authentication | Yes* | - |
| `VITE_PUBLIC_VAPID_KEY` | Public VAPID key for web push notifications | No | - | | `VITE_PUBLIC_VAPID_KEY` | Public VAPID key for web push notifications | No | - |
**Notes:**
- `VITE_BASE_URL` is used for WebSocket connections and must point to the base backend URL (not `/api/v1`)
- `VITE_PUBLIC_VAPID_KEY` is required for web push notifications. Generate using:
\`\`\`bash
npm install -g web-push
web-push generate-vapid-keys
\`\`\`
Use the **public key** in the frontend `.env.local` file
\*Required if using Okta authentication \*Required if using Okta authentication
### 4. Verify setup ### 4. Verify setup
@ -213,28 +274,57 @@ Re_Figma_Code/
├── src/ ├── src/
│ ├── components/ │ ├── components/
│ │ ├── ui/ # Reusable UI components (40+) │ │ ├── ui/ # Reusable UI components (40+)
│ │ ├── admin/ # Admin components
│ │ │ ├── AIConfig/ # AI configuration
│ │ │ ├── AnalyticsConfig/ # Analytics settings
│ │ │ ├── DashboardConfig/ # Dashboard customization
│ │ │ ├── DocumentConfig/ # Document policies
│ │ │ ├── NotificationConfig/ # Notification settings
│ │ │ ├── SharingConfig/ # Sharing policies
│ │ │ ├── TATConfig/ # TAT configuration
│ │ │ ├── UserManagement/ # User management
│ │ │ └── UserRoleManager/ # Role assignment
│ │ ├── approval/ # Approval workflow components
│ │ ├── common/ # Common reusable components
│ │ ├── dashboard/ # Dashboard widgets
│ │ ├── modals/ # Modal components │ │ ├── modals/ # Modal components
│ │ ├── figma/ # Figma-specific components │ │ ├── participant/ # Participant management
│ │ ├── Dashboard.tsx │ │ ├── workflow/ # Workflow components
│ │ ├── Layout.tsx │ │ └── workNote/ # Work notes/chat components
│ │ ├── ClaimManagementWizard.tsx │ ├── pages/
│ │ ├── NewRequestWizard.tsx │ │ ├── Admin/ # Admin control panel
│ │ ├── RequestDetail.tsx │ │ ├── ApproverPerformance/ # Approver analytics
│ │ ├── ClaimManagementDetail.tsx │ │ ├── Auth/ # Authentication pages
│ │ ├── MyRequests.tsx │ │ ├── Dashboard/ # Main dashboard
│ │ ├── RequestDetail/ # Request detail view
│ │ ├── Requests/ # Request listing
│ │ └── ...
│ ├── hooks/ # Custom React hooks
│ │ ├── useRequestSocket.ts # WebSocket integration
│ │ ├── useDocumentUpload.ts # Document management
│ │ ├── useSLATracking.ts # SLA tracking
│ │ └── ...
│ ├── services/ # API services
│ │ ├── adminApi.ts # Admin API calls
│ │ ├── authApi.ts # Authentication API
│ │ ├── workflowApi.ts # Workflow API
│ │ └── ... │ │ └── ...
│ ├── utils/ │ ├── utils/
│ │ ├── customRequestDatabase.ts │ │ ├── socket.ts # Socket.IO utilities
│ │ ├── claimManagementDatabase.ts │ │ ├── pushNotifications.ts # Web push notifications
│ │ └── dealerDatabase.ts │ │ ├── slaTracker.ts # SLA calculation utilities
│ │ └── ...
│ ├── contexts/
│ │ └── AuthContext.tsx # Authentication context
│ ├── styles/ │ ├── styles/
│ │ └── globals.css │ │ └── globals.css
│ ├── types/ │ ├── types/
│ │ └── index.ts # TypeScript type definitions │ │ └── index.ts
│ ├── App.tsx │ ├── App.tsx
│ └── main.tsx │ └── main.tsx
├── public/ # Static assets ├── public/
├── .vscode/ # VS Code settings │ └── service-worker.js # Service worker for push notifications
├── .vscode/
├── index.html ├── index.html
├── vite.config.ts ├── vite.config.ts
├── tsconfig.json ├── tsconfig.json
@ -267,7 +357,7 @@ The project uses path aliases for cleaner imports:
\`\`\`typescript \`\`\`typescript
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { getDealerInfo } from '@/utils/dealerDatabase'; import { getSocket } from '@/utils/socket';
\`\`\` \`\`\`
Path aliases are configured in: Path aliases are configured in:
@ -311,6 +401,7 @@ To connect to the backend API:
1. **Update API base URL** in `.env.local`: 1. **Update API base URL** in `.env.local`:
\`\`\`env \`\`\`env
VITE_API_BASE_URL=http://localhost:5000/api/v1 VITE_API_BASE_URL=http://localhost:5000/api/v1
VITE_BASE_URL=http://localhost:5000
\`\`\` \`\`\`
2. **Configure CORS** in your backend to allow your frontend origin 2. **Configure CORS** in your backend to allow your frontend origin
@ -318,10 +409,24 @@ To connect to the backend API:
3. **Authentication:** 3. **Authentication:**
- Configure Okta credentials in environment variables - Configure Okta credentials in environment variables
- Ensure backend validates JWT tokens from Okta - Ensure backend validates JWT tokens from Okta
- Backend should handle token exchange and refresh
4. **API Services:** 4. **WebSocket Configuration:**
- Backend must support Socket.IO on the base URL
- Socket.IO path: `/socket.io`
- CORS must allow WebSocket connections
- Events: `worknote:new`, `presence:join`, `presence:leave`, `request:online-users`
5. **Web Push Notifications:**
- Backend must support VAPID push notification delivery
- Service worker registration endpoint required
- Push subscription management API needed
- Generate VAPID keys using: `npm install -g web-push && web-push generate-vapid-keys`
6. **API Services:**
- API services are located in `src/services/` - API services are located in `src/services/`
- All API calls use `axios` configured with base URL from environment - All API calls use `axios` configured with base URL from environment
- WebSocket utilities in `src/utils/socket.ts`
### Development vs Production ### Development vs Production
@ -329,10 +434,181 @@ To connect to the backend API:
- **Production:** Uses `.env.production` or environment variables set in deployment platform - **Production:** Uses `.env.production` or environment variables set in deployment platform
- **Never commit:** `.env.local` or `.env.production` (use `.env.example` as template) - **Never commit:** `.env.local` or `.env.production` (use `.env.example` as template)
## 🚀 Key Features Deep Dive
### 👥 Admin Control Panel
The Admin Control Panel (`/admin`) provides comprehensive system management capabilities accessible only to ADMIN role users:
#### User Management
- **User Search**: Search users via Okta integration with real-time search
- **Role Assignment**: Assign and manage user roles (USER, MANAGEMENT, ADMIN)
- **User Statistics**: View role distribution and user counts
- **Pagination**: Efficient pagination for large user lists
- **Filtering**: Filter users by role (ELEVATED, ADMIN, MANAGEMENT, USER, ALL)
#### System Configuration Tabs
1. **KPI Configuration**
- Configure dashboard KPIs with visibility settings per role
- Set alert thresholds and breach conditions
- Organize KPIs by categories (Request Volume, TAT Efficiency, Approver Load, etc.)
- Enable/disable KPIs dynamically
2. **Analytics Configuration**
- Data retention policies
- Export format settings (CSV, Excel, PDF)
- Analytics feature toggles
- Data collection preferences
3. **TAT Configuration**
- Working hours configuration (start/end time, days of week)
- Priority-based TAT settings (express, standard, urgent)
- Escalation rules and thresholds
- Holiday calendar integration
4. **Notification Configuration**
- Email template customization
- Notification channel management (email, push, in-app)
- Notification frequency settings
- Delivery preferences
- **Notification Preferences** - User-configurable notification settings
5. **Document Configuration**
- Allowed file types and extensions
- File size limits (per file and total)
- Upload validation rules
- Document policy enforcement
6. **Dashboard Configuration**
- Customize dashboard layout per role
- Widget visibility settings
- Dashboard widget ordering
- Role-specific dashboard views
7. **AI Configuration**
- AI provider settings (OpenAI, Anthropic, etc.)
- AI parameters and model selection
- AI feature toggles
- API key management
8. **Sharing Configuration**
- Sharing policies and permissions
- External sharing settings
- Access control rules
#### Holiday Management
- Configure business holidays for accurate SLA calculations
- Holiday calendar integration
- Regional holiday support
### 📈 Approver Performance Dashboard
Comprehensive analytics dashboard (`/approver-performance`) for tracking and analyzing approver performance:
#### Key Metrics
- **Approval Statistics**: Total approvals, approval rate, average approval time
- **TAT Compliance**: Percentage of approvals within TAT
- **Request History**: Complete list of requests handled by approver
- **Performance Trends**: Time-based performance analysis
- **SLA Metrics**: TAT breach analysis and compliance tracking
#### Features
- **Advanced Filtering**: Filter by date range, status, priority, SLA compliance
- **Search Functionality**: Search requests by title, ID, or description
- **Export Capabilities**: Export performance data in CSV/Excel formats
- **Visual Analytics**: Charts and graphs for performance visualization
- **Comparison Tools**: Compare performance across different time periods
- **Detailed Request List**: View all requests with approval details
### 💬 Real-Time Live Chat (Work Notes)
Powered by Socket.IO for instant, bidirectional communication within request context:
#### Core Features
- **Real-Time Messaging**: Instant message delivery with Socket.IO
- **Presence Indicators**: See who's online/offline in real-time
- **@Mention System**: Mention participants using @username syntax
- **File Attachments**: Upload and share documents directly in chat
- **Message History**: Persistent chat history per request
- **Rich Text Support**: Format messages with mentions and links
#### Technical Implementation
- **Socket.IO Client**: Real-time WebSocket communication
- **Room-Based Architecture**: Each request has isolated chat room
- **Auto-Reconnection**: Automatic reconnection on network issues
- **Presence Management**: Real-time user presence tracking
- **Message Persistence**: Messages stored in backend database
#### Usage
- Access via Request Detail page > Work Notes tab
- Full-screen chat interface available
- Real-time updates for all participants
- Notification on new messages
### 🔔 Web Push Notifications
Browser push notifications using VAPID (Voluntary Application Server Identification) protocol:
#### Features
- **Service Worker Integration**: Background notification delivery
- **VAPID Protocol**: Secure, standards-based push notifications
- **Permission Management**: User-friendly permission requests
- **Notification Preferences**: User-configurable notification settings
- **Cross-Platform Support**: Works on desktop and mobile browsers
- **Offline Queue**: Notifications queued when browser is offline
#### Configuration
1. Generate VAPID keys (public/private pair)
2. Set `VITE_PUBLIC_VAPID_KEY` in environment variables
3. Backend must support VAPID push notification delivery
4. Service worker automatically registers on app load
#### Notification Types
- **SLA Alerts**: TAT breach and approaching deadline notifications
- **Approval Requests**: New requests assigned to approver
- **Status Updates**: Request status change notifications
- **Work Note Mentions**: Notifications when mentioned in chat
- **System Alerts**: Critical system notifications
#### Browser Support
- Chrome/Edge: Full support
- Firefox: Full support
- Safari: Limited support (macOS/iOS)
- Requires HTTPS in production
## 🔧 Troubleshooting ## 🔧 Troubleshooting
### Common Issues ### Common Issues
#### WebSocket Connection Issues
If real-time features (chat, presence) are not working:
1. Verify backend Socket.IO server is running
2. Check `VITE_BASE_URL` in `.env.local` (should not include `/api/v1`)
3. Ensure CORS allows WebSocket connections
4. Check browser console for Socket.IO connection errors
5. Verify Socket.IO path is `/socket.io` (default)
\`\`\`bash
# Test WebSocket connection
# Open browser console and check for Socket.IO connection logs
\`\`\`
#### Web Push Notifications Not Working
1. Ensure `VITE_PUBLIC_VAPID_KEY` is set in `.env.local`
2. Verify service worker is registered (check Application tab in DevTools)
3. Check browser notification permissions
4. Ensure HTTPS in production (required for push notifications)
5. Verify backend push notification endpoint is configured
\`\`\`bash
# Check service worker registration
# Open DevTools > Application > Service Workers
\`\`\`
#### Port Already in Use #### Port Already in Use
If the default port (5173) is in use: If the default port (5173) is in use:
@ -381,6 +657,35 @@ npm run type-check
- Verify all environment variables are set correctly - Verify all environment variables are set correctly
- Ensure Node.js and npm versions meet requirements - Ensure Node.js and npm versions meet requirements
- Review backend logs for API-related issues - Review backend logs for API-related issues
- Check Network tab for WebSocket connection status
- Verify service worker registration in DevTools > Application
## 🔐 Role-Based Access Control
The application supports three user roles with different access levels:
### USER Role
- Create and manage own requests
- View assigned requests
- Approve/reject requests assigned to them
- Add work notes and comments
- Upload documents
### MANAGEMENT Role
- All USER permissions
- View all requests across the organization
- Access to detailed reports and analytics
- Approver Performance dashboard access
- Export capabilities
### ADMIN Role
- All MANAGEMENT permissions
- Access to Admin Control Panel (`/admin`)
- User management and role assignment
- System configuration (TAT, Notifications, Documents, etc.)
- KPI configuration and dashboard customization
- Holiday calendar management
- Full system administration capabilities
## 🧪 Testing (Future Enhancement) ## 🧪 Testing (Future Enhancement)

View File

@ -1,19 +0,0 @@
# Fix all imports with version numbers
$files = Get-ChildItem -Path "src" -Filter "*.tsx" -Recurse
foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw
# Remove version numbers from ALL package imports (universal pattern)
# Matches: package-name@version, @scope/package-name@version
$content = $content -replace '(from\s+[''"])([^''"]+)@[\d.]+([''"])', '$1$2$3'
$content = $content -replace '(import\s+[''"])([^''"]+)@[\d.]+([''"])', '$1$2$3'
# Also fix motion/react to framer-motion
$content = $content -replace 'motion/react', 'framer-motion'
Set-Content -Path $file.FullName -Value $content -NoNewline
}
Write-Host "Fixed all imports!" -ForegroundColor Green

View File

@ -1,53 +0,0 @@
# PowerShell script to migrate files to src directory
# Run this script: .\migrate-files.ps1
Write-Host "Starting file migration to src directory..." -ForegroundColor Green
# Check if src directory exists
if (-not (Test-Path "src")) {
Write-Host "Creating src directory..." -ForegroundColor Yellow
New-Item -ItemType Directory -Path "src" -Force | Out-Null
}
# Function to move files with checks
function Move-WithCheck {
param($Source, $Destination)
if (Test-Path $Source) {
Write-Host "Moving $Source to $Destination..." -ForegroundColor Cyan
Move-Item -Path $Source -Destination $Destination -Force
Write-Host "Moved $Source" -ForegroundColor Green
} else {
Write-Host "$Source not found, skipping..." -ForegroundColor Yellow
}
}
# Move App.tsx
if (Test-Path "App.tsx") {
Move-WithCheck "App.tsx" "src\App.tsx"
}
# Move components directory
if (Test-Path "components") {
Move-WithCheck "components" "src\components"
}
# Move utils directory
if (Test-Path "utils") {
Move-WithCheck "utils" "src\utils"
}
# Move styles directory
if (Test-Path "styles") {
Move-WithCheck "styles" "src\styles"
}
Write-Host ""
Write-Host "Migration complete!" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Yellow
Write-Host "1. Update imports in src/App.tsx to use '@/' aliases" -ForegroundColor White
Write-Host "2. Fix the sonner import: import { toast } from 'sonner';" -ForegroundColor White
Write-Host "3. Run: npm run dev" -ForegroundColor White
Write-Host ""
Write-Host "See MIGRATION_GUIDE.md for detailed instructions" -ForegroundColor Cyan

View File

@ -213,7 +213,6 @@ function AppRoutes({ onLogout }: AppProps) {
// Add to dynamic requests // Add to dynamic requests
setDynamicRequests([...dynamicRequests, newCustomRequest]); setDynamicRequests([...dynamicRequests, newCustomRequest]);
console.log('New custom request created:', newCustomRequest);
navigate('/my-requests'); navigate('/my-requests');
toast.success('Request Submitted Successfully!', { toast.success('Request Submitted Successfully!', {
description: `Your request "${requestData.title}" (${requestId}) has been created and sent for approval.`, description: `Your request "${requestData.title}" (${requestId}) has been created and sent for approval.`,
@ -221,7 +220,7 @@ function AppRoutes({ onLogout }: AppProps) {
}); });
}; };
const handleApprovalSubmit = (action: 'approve' | 'reject', comment: string) => { const handleApprovalSubmit = (action: 'approve' | 'reject', _comment: string) => {
return new Promise((resolve) => { return new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
if (action === 'approve') { if (action === 'approve') {
@ -236,7 +235,6 @@ function AppRoutes({ onLogout }: AppProps) {
}); });
} }
console.log(`${action} action completed with comment:`, comment);
setApprovalAction(null); setApprovalAction(null);
resolve(true); resolve(true);
}, 1000); }, 1000);
@ -658,8 +656,6 @@ interface MainAppProps {
export default function App(props?: MainAppProps) { export default function App(props?: MainAppProps) {
const { onLogout } = props || {}; const { onLogout } = props || {};
console.log('🟢 Main App component rendered');
console.log('🟢 onLogout prop received:', !!onLogout);
return ( return (
<BrowserRouter> <BrowserRouter>

View File

@ -14,14 +14,7 @@ export function AuthDebugInfo({ isOpen, onClose }: AuthDebugInfoProps) {
const { user, isAuthenticated, isLoading, error } = useAuth0(); const { user, isAuthenticated, isLoading, error } = useAuth0();
useEffect(() => { useEffect(() => {
console.log('AuthDebugInfo - Current Auth State:', { // Auth state debug info - removed console.log
isAuthenticated,
isLoading,
hasUser: !!user,
error: error?.message,
userData: user,
timestamp: new Date().toISOString()
});
}, [user, isAuthenticated, isLoading, error]); }, [user, isAuthenticated, isLoading, error]);
if (!isOpen) return null; if (!isOpen) return null;

View File

@ -32,7 +32,6 @@ export function AnalyticsConfig() {
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save configuration // TODO: Implement API call to save configuration
console.log('Saving analytics configuration:', config);
toast.success('Analytics configuration saved successfully'); toast.success('Analytics configuration saved successfully');
}; };

View File

@ -258,10 +258,12 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro
} }
}; };
// Filter out notification rules and dashboard layout categories // Filter out notification rules, dashboard layout categories, and allow external sharing
const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT']; const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT'];
const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING'];
const filteredConfigurations = configurations.filter( const filteredConfigurations = configurations.filter(
config => !excludedCategories.includes(config.configCategory) config => !excludedCategories.includes(config.configCategory) &&
!excludedConfigKeys.includes(config.configKey)
); );
const groupedConfigs = filteredConfigurations.reduce((acc, config) => { const groupedConfigs = filteredConfigurations.reduce((acc, config) => {

View File

@ -60,7 +60,6 @@ export function DashboardConfig() {
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save dashboard configuration // TODO: Implement API call to save dashboard configuration
console.log('Saving dashboard configuration:', config);
toast.success('Dashboard layout saved successfully'); toast.success('Dashboard layout saved successfully');
}; };

View File

@ -88,6 +88,17 @@ export function HolidayManager() {
setShowAddDialog(true); setShowAddDialog(true);
}; };
// Calculate minimum date (tomorrow for new holidays, no restriction for editing)
const getMinDate = () => {
// Only enforce minimum date when adding new holidays, not when editing existing ones
if (editingHoliday) {
return undefined; // Allow editing past holidays
}
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
return tomorrow.toISOString().split('T')[0];
};
const handleSave = async () => { const handleSave = async () => {
try { try {
setError(null); setError(null);
@ -357,9 +368,12 @@ export function HolidayManager() {
type="date" type="date"
value={formData.holidayDate} value={formData.holidayDate}
onChange={(e) => setFormData({ ...formData, holidayDate: e.target.value })} onChange={(e) => setFormData({ ...formData, holidayDate: e.target.value })}
min={getMinDate()}
className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm" className="h-11 border-slate-300 focus:border-re-green focus:ring-2 focus:ring-re-green/20 rounded-lg transition-all shadow-sm"
/> />
<p className="text-xs text-slate-500">Select the holiday date</p> <p className="text-xs text-slate-500">
{editingHoliday ? 'Select the holiday date' : 'Select the holiday date (minimum: tomorrow)'}
</p>
</div> </div>
{/* Holiday Name Field */} {/* Holiday Name Field */}

View File

@ -29,7 +29,6 @@ export function NotificationConfig() {
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save notification configuration // TODO: Implement API call to save notification configuration
console.log('Saving notification configuration:', config);
toast.success('Notification configuration saved successfully'); toast.success('Notification configuration saved successfully');
}; };

View File

@ -24,7 +24,6 @@ export function SharingConfig() {
const handleSave = () => { const handleSave = () => {
// TODO: Implement API call to save sharing configuration // TODO: Implement API call to save sharing configuration
console.log('Saving sharing configuration:', config);
toast.success('Sharing policy saved successfully'); toast.success('Sharing policy saved successfully');
}; };

View File

@ -107,13 +107,10 @@ export function UserRoleManager() {
setSearching(true); setSearching(true);
try { try {
const response = await userApi.searchUsers(query, 20); const response = await userApi.searchUsers(query, 20);
console.log('Search response:', response);
console.log('Response.data:', response.data);
// Backend returns { success: true, data: [...users], message, timestamp } // Backend returns { success: true, data: [...users], message, timestamp }
// Axios response is in response.data, actual user array is in response.data.data // Axios response is in response.data, actual user array is in response.data.data
const users = response.data?.data || []; const users = response.data?.data || [];
console.log('Parsed users:', users);
setSearchResults(users); setSearchResults(users);
} catch (error: any) { } catch (error: any) {
@ -219,17 +216,11 @@ export function UserRoleManager() {
try { try {
const response = await userApi.getUsersByRole(roleFilter, page, limit); const response = await userApi.getUsersByRole(roleFilter, page, limit);
console.log('Users response:', response);
// Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } } // Backend returns { success: true, data: { users: [...], pagination: {...}, summary: {...} } }
const usersData = response.data?.data?.users || []; const usersData = response.data?.data?.users || [];
const paginationData = response.data?.data?.pagination; const paginationData = response.data?.data?.pagination;
const summaryData = response.data?.data?.summary; const summaryData = response.data?.data?.summary;
console.log('Parsed users:', usersData);
console.log('Pagination:', paginationData);
console.log('Summary:', summaryData);
setUsers(usersData); setUsers(usersData);
if (paginationData) { if (paginationData) {
@ -257,11 +248,9 @@ export function UserRoleManager() {
const fetchRoleStatistics = async () => { const fetchRoleStatistics = async () => {
try { try {
const response = await userApi.getRoleStatistics(); const response = await userApi.getRoleStatistics();
console.log('Role statistics response:', response);
// Handle different response formats // Handle different response formats
const statsData = response.data?.data?.statistics || response.data?.statistics || []; const statsData = response.data?.data?.statistics || response.data?.statistics || [];
console.log('Statistics data:', statsData);
setRoleStats({ setRoleStats({
admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'), admins: parseInt(statsData.find((s: any) => s.role === 'ADMIN')?.count || '0'),

View File

@ -121,7 +121,7 @@ export function Pagination({
variant={pageNum === currentPage ? "default" : "outline"} variant={pageNum === currentPage ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onPageChange(pageNum)} onClick={() => onPageChange(pageNum)}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
data-testid={`${testIdPrefix}-page-${pageNum}`} data-testid={`${testIdPrefix}-page-${pageNum}`}
aria-current={pageNum === currentPage ? 'page' : undefined} aria-current={pageNum === currentPage ? 'page' : undefined}
> >

View File

@ -1,5 +1,5 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { LucideIcon } from 'lucide-react'; import { LucideIcon, Info } from 'lucide-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
interface KPICardProps { interface KPICardProps {
@ -12,6 +12,8 @@ interface KPICardProps {
children?: ReactNode; children?: ReactNode;
testId?: string; testId?: string;
onClick?: () => void; onClick?: () => void;
onJustifyClick?: () => void;
showJustifyButton?: boolean;
} }
export function KPICard({ export function KPICard({
@ -23,8 +25,15 @@ export function KPICard({
subtitle, subtitle,
children, children,
testId = 'kpi-card', testId = 'kpi-card',
onClick onClick,
onJustifyClick,
showJustifyButton = false
}: KPICardProps) { }: KPICardProps) {
const handleJustifyClick = (e: React.MouseEvent) => {
e.stopPropagation(); // Prevent card onClick from firing
onJustifyClick?.();
};
return ( return (
<Card <Card
className="hover:shadow-lg transition-all duration-200 shadow-md cursor-pointer h-full flex flex-col" className="hover:shadow-lg transition-all duration-200 shadow-md cursor-pointer h-full flex flex-col"
@ -38,12 +47,26 @@ export function KPICard({
> >
{title} {title}
</CardTitle> </CardTitle>
<div className="flex items-center gap-2">
{showJustifyButton && onJustifyClick && (
<button
type="button"
onClick={handleJustifyClick}
className="p-1.5 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 transition-colors"
data-testid={`${testId}-justify-button`}
title="View detailed breakdown of numbers"
aria-label="View detailed breakdown"
>
<Info className="h-4 w-4" />
</button>
)}
<div className={`p-1.5 sm:p-2 rounded-lg ${iconBgColor}`} data-testid={`${testId}-icon-wrapper`}> <div className={`p-1.5 sm:p-2 rounded-lg ${iconBgColor}`} data-testid={`${testId}-icon-wrapper`}>
<Icon <Icon
className={`h-4 w-4 sm:h-4 sm:w-4 ${iconColor}`} className={`h-4 w-4 sm:h-4 sm:w-4 ${iconColor}`}
data-testid={`${testId}-icon`} data-testid={`${testId}-icon`}
/> />
</div> </div>
</div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col flex-1 py-3"> <CardContent className="flex flex-col flex-1 py-3">
<div <div

View File

@ -112,7 +112,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
// Navigate to request detail page // Navigate to request detail page
onNavigate(navigationUrl); onNavigate(navigationUrl);
console.log('[PageLayout] Navigating to:', navigationUrl);
} }
} }
@ -148,8 +147,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
const notifs = result.data?.notifications || []; const notifs = result.data?.notifications || [];
setNotifications(notifs); setNotifications(notifs);
setUnreadCount(result.data?.unreadCount || 0); setUnreadCount(result.data?.unreadCount || 0);
console.log('[PageLayout] Loaded', notifs.length, 'recent notifications,', result.data?.unreadCount, 'unread');
} catch (error) { } catch (error) {
console.error('[PageLayout] Failed to fetch notifications:', error); console.error('[PageLayout] Failed to fetch notifications:', error);
} }
@ -166,7 +163,6 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
// Listen for new notifications // Listen for new notifications
const handleNewNotification = (data: { notification: Notification }) => { const handleNewNotification = (data: { notification: Notification }) => {
console.log('[PageLayout] 🔔 New notification received:', data);
if (!mounted) return; if (!mounted) return;
setNotifications(prev => [data.notification, ...prev].slice(0, 4)); // Keep latest 4 for dropdown setNotifications(prev => [data.notification, ...prev].slice(0, 4)); // Keep latest 4 for dropdown
@ -451,14 +447,10 @@ export function PageLayout({ children, currentPage = 'dashboard', onNavigate, on
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
onClick={async () => { onClick={async () => {
console.log('🔴 Logout button clicked in PageLayout');
console.log('🔴 onLogout function exists?', !!onLogout);
setShowLogoutDialog(false); setShowLogoutDialog(false);
if (onLogout) { if (onLogout) {
console.log('🔴 Calling onLogout function...');
try { try {
await onLogout(); await onLogout();
console.log('🔴 onLogout completed');
} catch (error) { } catch (error) {
console.error('🔴 Error calling onLogout:', error); console.error('🔴 Error calling onLogout:', error);
} }

View File

@ -221,8 +221,6 @@ export function AddApproverModal({
secondEmail: foundUser.secondEmail, secondEmail: foundUser.secondEmail,
location: foundUser.location location: foundUser.location
}); });
console.log(`✅ Validated approver: ${foundUser.displayName} (${foundUser.email})`);
} catch (error) { } catch (error) {
console.error('Failed to validate approver:', error); console.error('Failed to validate approver:', error);
setValidationModal({ setValidationModal({
@ -358,7 +356,6 @@ export function AddApproverModal({
setSelectedUser(user); // Track that user was selected via @ search setSelectedUser(user); // Track that user was selected via @ search
setSearchResults([]); setSearchResults([]);
setIsSearching(false); setIsSearching(false);
console.log(`✅ User selected and verified: ${user.displayName} (${user.email})`);
} catch (error) { } catch (error) {
console.error('Failed to ensure user exists:', error); console.error('Failed to ensure user exists:', error);
setValidationModal({ setValidationModal({

View File

@ -5,7 +5,7 @@ import { Input } from '@/components/ui/input';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Eye, X, AtSign, AlertCircle, Lightbulb } from 'lucide-react'; import { Eye, X, AtSign, AlertCircle, Lightbulb } from 'lucide-react';
import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi'; import { searchUsers, ensureUserExists, type UserSummary } from '@/services/userApi';
import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal'; import { PolicyViolationModal } from '@/components/modals/PolicyViolationModal';
interface AddSpectatorModalProps { interface AddSpectatorModalProps {
@ -70,8 +70,8 @@ export function AddSpectatorModal({
useEffect(() => { useEffect(() => {
const loadSystemPolicy = async () => { const loadSystemPolicy = async () => {
try { try {
const workflowConfigs = await getAllConfigurations('WORKFLOW_SHARING'); const workflowConfigs = await getPublicConfigurations('WORKFLOW_SHARING');
const tatConfigs = await getAllConfigurations('TAT_SETTINGS'); const tatConfigs = await getPublicConfigurations('TAT_SETTINGS');
const allConfigs = [...workflowConfigs, ...tatConfigs]; const allConfigs = [...workflowConfigs, ...tatConfigs];
const configMap: Record<string, string> = {}; const configMap: Record<string, string> = {};
allConfigs.forEach((c: AdminConfiguration) => { allConfigs.forEach((c: AdminConfiguration) => {
@ -250,8 +250,6 @@ export function AddSpectatorModal({
secondEmail: foundUser.secondEmail, secondEmail: foundUser.secondEmail,
location: foundUser.location location: foundUser.location
}); });
console.log(`✅ Validated spectator: ${foundUser.displayName} (${foundUser.email})`);
} catch (error) { } catch (error) {
console.error('Failed to validate spectator:', error); console.error('Failed to validate spectator:', error);
setValidationModal({ setValidationModal({
@ -373,7 +371,6 @@ export function AddSpectatorModal({
setSelectedUser(user); // Track that user was selected via @ search setSelectedUser(user); // Track that user was selected via @ search
setSearchResults([]); setSearchResults([]);
setIsSearching(false); setIsSearching(false);
console.log(`✅ User selected and verified: ${user.displayName} (${user.email})`);
} catch (error) { } catch (error) {
console.error('Failed to ensure user exists:', error); console.error('Failed to ensure user exists:', error);
setValidationModal({ setValidationModal({

View File

@ -2,6 +2,7 @@ import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Clock, Lock, CheckCircle, XCircle, AlertTriangle, AlertOctagon } from 'lucide-react'; import { Clock, Lock, CheckCircle, XCircle, AlertTriangle, AlertOctagon } from 'lucide-react';
import { formatHoursMinutes } from '@/utils/slaTracker'; import { formatHoursMinutes } from '@/utils/slaTracker';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
export interface SLAData { export interface SLAData {
status: 'normal' | 'approaching' | 'critical' | 'breached'; status: 'normal' | 'approaching' | 'critical' | 'breached';
@ -90,7 +91,7 @@ export function SLAProgressBar({
{sla.deadline && ( {sla.deadline && (
<p className="text-xs text-gray-500" data-testid={`${testId}-deadline`}> <p className="text-xs text-gray-500" data-testid={`${testId}-deadline`}>
Due: {new Date(sla.deadline).toLocaleString()} {sla.percentageUsed || 0}% elapsed Due: {formatDateDDMMYYYY(sla.deadline, true)} {sla.percentageUsed || 0}% elapsed
</p> </p>
)} )}

View File

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

View File

@ -150,11 +150,8 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
const n = payload?.note || payload; const n = payload?.note || payload;
if (!n) return; if (!n) return;
console.log('[WorkNoteChat] Received note via socket:', n);
setMessages(prev => { setMessages(prev => {
if (prev.some(m => m.id === (n.noteId || n.note_id || n.id))) { if (prev.some(m => m.id === (n.noteId || n.note_id || n.id))) {
console.log('[WorkNoteChat] Duplicate detected, skipping');
return prev; return prev;
} }
@ -175,7 +172,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
isCurrentUser: noteUserId === currentUserId isCurrentUser: noteUserId === currentUserId
} as any; } as any;
console.log('[WorkNoteChat] Adding new message:', newMsg);
return [...prev, newMsg]; return [...prev, newMsg];
}); });
}; };
@ -265,7 +261,6 @@ export function WorkNoteChatSimple({ requestId, messages: externalMessages, onSe
(async () => { (async () => {
try { try {
const rows = await getWorkNotes(requestId); const rows = await getWorkNotes(requestId);
console.log('[WorkNoteChat] Loaded work notes:', rows);
const mapped = Array.isArray(rows) ? rows.map((m: any) => { const mapped = Array.isArray(rows) ? rows.map((m: any) => {
const userName = m.userName || m.user_name || 'User'; const userName = m.userName || m.user_name || 'User';

View File

@ -6,7 +6,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { CheckCircle, XCircle, Clock, AlertCircle, AlertTriangle, FastForward, MessageSquare, PauseCircle, Hourglass, AlertOctagon, FileEdit } from 'lucide-react'; import { CheckCircle, XCircle, Clock, AlertCircle, AlertTriangle, FastForward, MessageSquare, PauseCircle, Hourglass, AlertOctagon, FileEdit } from 'lucide-react';
import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; import { formatDateTime, formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { formatHoursMinutes } from '@/utils/slaTracker'; import { formatHoursMinutes } from '@/utils/slaTracker';
import { useAuth, hasManagementAccess } from '@/contexts/AuthContext'; import { useAuth, hasManagementAccess } from '@/contexts/AuthContext';
import { updateBreachReason as updateBreachReasonApi } from '@/services/workflowApi'; import { updateBreachReason as updateBreachReasonApi } from '@/services/workflowApi';
@ -339,7 +339,7 @@ export function ApprovalStepCard({
<div className="flex items-center justify-between text-xs"> <div className="flex items-center justify-between text-xs">
<span className="text-gray-600">Due by:</span> <span className="text-gray-600">Due by:</span>
<span className="font-medium text-gray-900"> <span className="font-medium text-gray-900">
{approval.sla.deadline ? formatDateTime(approval.sla.deadline) : 'Not set'} {approval.sla.deadline ? formatDateDDMMYYYY(approval.sla.deadline, true) : 'Not set'}
</span> </span>
</div> </div>
@ -532,13 +532,13 @@ export function ApprovalStepCard({
<div className="bg-white/50 rounded px-2 py-1"> <div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Allocated:</span> <span className="text-gray-500">Allocated:</span>
<span className="ml-1 font-medium text-gray-900"> <span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursAllocated || 0).toFixed(2)}h {formatHoursMinutes(Number(alert.tatHoursAllocated || 0))}
</span> </span>
</div> </div>
<div className="bg-white/50 rounded px-2 py-1"> <div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Elapsed:</span> <span className="text-gray-500">Elapsed:</span>
<span className="ml-1 font-medium text-gray-900"> <span className="ml-1 font-medium text-gray-900">
{Number(alert.tatHoursElapsed || 0).toFixed(2)}h {formatHoursMinutes(Number(alert.tatHoursElapsed || 0))}
{alert.metadata?.tatTestMode && ( {alert.metadata?.tatTestMode && (
<span className="text-purple-600 ml-1"> <span className="text-purple-600 ml-1">
({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m) ({(Number(alert.tatHoursElapsed || 0) * 60).toFixed(0)}m)
@ -551,7 +551,7 @@ export function ApprovalStepCard({
<span className={`ml-1 font-medium ${ <span className={`ml-1 font-medium ${
(alert.tatHoursRemaining || 0) < 2 ? 'text-red-600' : 'text-gray-900' (alert.tatHoursRemaining || 0) < 2 ? 'text-red-600' : 'text-gray-900'
}`}> }`}>
{Number(alert.tatHoursRemaining || 0).toFixed(2)}h {formatHoursMinutes(Number(alert.tatHoursRemaining || 0))}
{alert.metadata?.tatTestMode && ( {alert.metadata?.tatTestMode && (
<span className="text-purple-600 ml-1"> <span className="text-purple-600 ml-1">
({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m) ({(Number(alert.tatHoursRemaining || 0) * 60).toFixed(0)}m)
@ -562,7 +562,7 @@ export function ApprovalStepCard({
<div className="bg-white/50 rounded px-2 py-1"> <div className="bg-white/50 rounded px-2 py-1">
<span className="text-gray-500">Due by:</span> <span className="text-gray-500">Due by:</span>
<span className="ml-1 font-medium text-gray-900"> <span className="ml-1 font-medium text-gray-900">
{alert.expectedCompletionTime ? formatDateShort(alert.expectedCompletionTime) : 'N/A'} {alert.expectedCompletionTime ? formatDateDDMMYYYY(alert.expectedCompletionTime, true) : 'N/A'}
</span> </span>
</div> </div>
</div> </div>

View File

@ -9,6 +9,7 @@ import { Progress } from '@/components/ui/progress';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase'; import { CLAIM_MANAGEMENT_DATABASE } from '@/utils/claimManagementDatabase';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { import {
ArrowLeft, ArrowLeft,
Clock, Clock,
@ -261,7 +262,7 @@ export function ClaimManagementDetail({
</div> </div>
<Progress value={claim.slaProgress} className="h-2 mb-2" /> <Progress value={claim.slaProgress} className="h-2 mb-2" />
<p className="text-xs text-gray-600"> <p className="text-xs text-gray-600">
Due: {claim.slaEndDate} {claim.slaProgress}% elapsed Due: {formatDateDDMMYYYY(claim.slaEndDate, true)} {claim.slaProgress}% elapsed
</p> </p>
</div> </div>
</div> </div>
@ -762,8 +763,7 @@ export function ClaimManagementDetail({
<DealerDocumentModal <DealerDocumentModal
isOpen={dealerDocModal} isOpen={dealerDocModal}
onClose={() => setDealerDocModal(false)} onClose={() => setDealerDocModal(false)}
onSubmit={async (documents) => { onSubmit={async (_documents) => {
console.log('Dealer documents submitted:', documents);
toast.success('Documents Uploaded', { toast.success('Documents Uploaded', {
description: 'Your documents have been submitted for review.', description: 'Your documents have been submitted for review.',
}); });
@ -779,7 +779,6 @@ export function ClaimManagementDetail({
isOpen={initiatorVerificationModal} isOpen={initiatorVerificationModal}
onClose={() => setInitiatorVerificationModal(false)} onClose={() => setInitiatorVerificationModal(false)}
onSubmit={async (data) => { onSubmit={async (data) => {
console.log('Verification data:', data);
toast.success('Verification Complete', { toast.success('Verification Complete', {
description: `Amount set to ${data.approvedAmount}. E-invoice will be generated.`, description: `Amount set to ${data.approvedAmount}. E-invoice will be generated.`,
}); });

View File

@ -5,10 +5,9 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Users, Settings, Shield, User, CheckCircle, AlertCircle, Info, Clock, Minus, Plus } from 'lucide-react'; import { Users, Settings, Shield, User, CheckCircle, Minus, Plus } from 'lucide-react';
import { FormData } from '@/hooks/useCreateRequestForm'; import { FormData } from '@/hooks/useCreateRequestForm';
import { useMultiUserSearch } from '@/hooks/useUserSearch'; import { useMultiUserSearch } from '@/hooks/useUserSearch';
import { useAuth } from '@/contexts/AuthContext';
import { ensureUserExists } from '@/services/userApi'; import { ensureUserExists } from '@/services/userApi';
interface ApprovalWorkflowStepProps { interface ApprovalWorkflowStepProps {
@ -35,7 +34,6 @@ export function ApprovalWorkflowStep({
updateFormData, updateFormData,
onValidationError onValidationError
}: ApprovalWorkflowStepProps) { }: ApprovalWorkflowStepProps) {
const { user } = useAuth();
const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch(); const { userSearchResults, userSearchLoading, searchUsersForIndex, clearSearchForIndex } = useMultiUserSearch();
const handleApproverEmailChange = (index: number, value: string) => { const handleApproverEmailChange = (index: number, value: string) => {

View File

@ -41,7 +41,7 @@ export function DocumentsStep({
existingDocuments, existingDocuments,
documentsToDelete, documentsToDelete,
onDocumentsChange, onDocumentsChange,
onExistingDocumentsChange, onExistingDocumentsChange: _onExistingDocumentsChange,
onDocumentsToDeleteChange, onDocumentsToDeleteChange,
onPreviewDocument, onPreviewDocument,
onDocumentErrors, onDocumentErrors,

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -8,7 +8,6 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Eye, Info, X } from 'lucide-react'; import { Eye, Info, X } from 'lucide-react';
import { FormData } from '@/hooks/useCreateRequestForm'; import { FormData } from '@/hooks/useCreateRequestForm';
import { useUserSearch } from '@/hooks/useUserSearch'; import { useUserSearch } from '@/hooks/useUserSearch';
import { ensureUserExists } from '@/services/userApi';
interface ParticipantsStepProps { interface ParticipantsStepProps {
formData: FormData; formData: FormData;

View File

@ -3,7 +3,6 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { CheckCircle, Rocket, FileText, Users, Eye, Upload, Flame, Target, TrendingUp, DollarSign } from 'lucide-react'; import { CheckCircle, Rocket, FileText, Users, Eye, Upload, Flame, Target, TrendingUp, DollarSign } from 'lucide-react';
import { format } from 'date-fns';
import { FormData, RequestTemplate } from '@/hooks/useCreateRequestForm'; import { FormData, RequestTemplate } from '@/hooks/useCreateRequestForm';
interface ReviewSubmitStepProps { interface ReviewSubmitStepProps {

View File

@ -1,5 +1,5 @@
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';

View File

@ -74,9 +74,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const forceLogout = sessionStorage.getItem('__force_logout__'); const forceLogout = sessionStorage.getItem('__force_logout__');
if (logoutFlag === 'true' || forceLogout === 'true') { if (logoutFlag === 'true' || forceLogout === 'true') {
console.log('🔴 Logout flag detected - PREVENTING auto-authentication');
console.log('🔴 Clearing ALL authentication data and showing login screen');
// Remove flags // Remove flags
sessionStorage.removeItem('__logout_in_progress__'); sessionStorage.removeItem('__logout_in_progress__');
sessionStorage.removeItem('__force_logout__'); sessionStorage.removeItem('__force_logout__');
@ -98,17 +95,12 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
setIsLoading(false); setIsLoading(false);
setError(null); setError(null);
console.log('🔴 Logout complete - user should see login screen');
return; return;
} }
// PRIORITY 2: Check if URL has logout parameter (from redirect) // PRIORITY 2: Check if URL has logout parameter (from redirect)
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('logout') || urlParams.has('okta_logged_out')) { if (urlParams.has('logout') || urlParams.has('okta_logged_out')) {
console.log('🔴 Logout parameter in URL - clearing everything', {
hasLogout: urlParams.has('logout'),
hasOktaLoggedOut: urlParams.has('okta_logged_out'),
});
TokenManager.clearAll(); TokenManager.clearAll();
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
@ -133,7 +125,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
// If no auth data exists, we're likely after a logout - set unauthenticated state immediately // If no auth data exists, we're likely after a logout - set unauthenticated state immediately
if (!hasAuthData) { if (!hasAuthData) {
console.log('🔴 No auth data found - setting unauthenticated state');
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
setIsLoading(false); setIsLoading(false);
@ -144,7 +135,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
if (!isLoggingOut) { if (!isLoggingOut) {
checkAuthStatus(); checkAuthStatus();
} else { } else {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false); setIsLoading(false);
} }
}, [isLoggingOut]); }, [isLoggingOut]);
@ -211,12 +201,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
// This is the frontend callback URL, NOT the backend URL // This is the frontend callback URL, NOT the backend URL
// Backend will use this same URI when exchanging code with Okta // Backend will use this same URI when exchanging code with Okta
const redirectUri = `${window.location.origin}/login/callback`; const redirectUri = `${window.location.origin}/login/callback`;
console.log('📥 Authorization Code Received:', {
code: code.substring(0, 10) + '...',
redirectUri,
fullUrl: window.location.href,
note: 'redirectUri is frontend URL (not backend) - must match Okta registration',
});
const result = await exchangeCodeForTokens(code, redirectUri); const result = await exchangeCodeForTokens(code, redirectUri);
@ -244,7 +228,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const checkAuthStatus = async () => { const checkAuthStatus = async () => {
// Don't check auth status if we're in the middle of logging out // Don't check auth status if we're in the middle of logging out
if (isLoggingOut) { if (isLoggingOut) {
console.log('🔴 Skipping checkAuthStatus - logout in progress');
setIsLoading(false); setIsLoading(false);
return; return;
} }
@ -255,11 +238,8 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
const token = TokenManager.getAccessToken(); const token = TokenManager.getAccessToken();
const storedUser = TokenManager.getUserData(); const storedUser = TokenManager.getUserData();
console.log('🔍 Checking auth status:', { hasToken: !!token, hasUser: !!storedUser, isLoggingOut });
// If no token at all, user is not authenticated // If no token at all, user is not authenticated
if (!token) { if (!token) {
console.log('🔍 No token found - setting unauthenticated');
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
setIsLoading(false); setIsLoading(false);
@ -361,7 +341,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
// This ensures Okta requires login even if a session still exists // This ensures Okta requires login even if a session still exists
if (isAfterLogout) { if (isAfterLogout) {
authUrl += `&prompt=login`; authUrl += `&prompt=login`;
console.log('🔐 Adding prompt=login to force re-authentication after logout');
} }
window.location.href = authUrl; window.location.href = authUrl;
@ -372,13 +351,11 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
}; };
const logout = async () => { const logout = async () => {
console.log('🚪 LOGOUT FUNCTION CALLED - Starting logout process');
console.log('🚪 Current auth state:', { isAuthenticated, hasUser: !!user, isLoading });
try { try {
// CRITICAL: Get id_token from TokenManager before clearing anything // CRITICAL: Get id_token from TokenManager before clearing anything
// Okta logout endpoint works better with id_token_hint to properly end the session // Okta logout endpoint works better with id_token_hint to properly end the session
const idToken = TokenManager.getIdToken(); // Note: Currently not used but kept for future Okta integration
void TokenManager.getIdToken();
// Set logout flag to prevent auto-authentication after redirect // Set logout flag to prevent auto-authentication after redirect
// This must be set BEFORE clearing storage so it survives // This must be set BEFORE clearing storage so it survives
@ -386,20 +363,16 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
sessionStorage.setItem('__force_logout__', 'true'); sessionStorage.setItem('__force_logout__', 'true');
setIsLoggingOut(true); setIsLoggingOut(true);
console.log('🚪 Step 1: Resetting auth state...');
// Reset auth state FIRST to prevent any re-authentication // Reset auth state FIRST to prevent any re-authentication
setIsAuthenticated(false); setIsAuthenticated(false);
setUser(null); setUser(null);
setError(null); setError(null);
setIsLoading(true); // Set loading to prevent checkAuthStatus from running setIsLoading(true); // Set loading to prevent checkAuthStatus from running
console.log('🚪 Step 1: Auth state reset complete');
// Call backend logout API to clear server-side session and httpOnly cookies // Call backend logout API to clear server-side session and httpOnly cookies
// IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies // IMPORTANT: This MUST be called before clearing local storage to clear httpOnly cookies
try { try {
console.log('🚪 Step 2: Calling backend logout API to clear httpOnly cookies...');
await logoutApi(); await logoutApi();
console.log('🚪 Step 2: Backend logout API completed - httpOnly cookies should be cleared');
} catch (err) { } catch (err) {
console.error('🚪 Logout API error:', err); console.error('🚪 Logout API error:', err);
console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared'); console.warn('🚪 Backend logout failed - httpOnly cookies may not be cleared');
@ -407,16 +380,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
} }
// Clear all authentication data EXCEPT the logout flags and id_token (we need it for Okta logout) // Clear all authentication data EXCEPT the logout flags and id_token (we need it for Okta logout)
console.log('========================================');
console.log('LOGOUT - Clearing all authentication data');
console.log('========================================');
// Store id_token temporarily if we have it
let tempIdToken: string | null = null;
if (idToken) {
tempIdToken = idToken;
console.log('🚪 Preserving id_token for Okta logout:', tempIdToken.substring(0, 20) + '...');
}
// Clear tokens but preserve logout flags // Clear tokens but preserve logout flags
const logoutInProgress = sessionStorage.getItem('__logout_in_progress__'); const logoutInProgress = sessionStorage.getItem('__logout_in_progress__');
@ -429,18 +392,6 @@ function BackendAuthProvider({ children }: { children: ReactNode }) {
if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress); if (logoutInProgress) sessionStorage.setItem('__logout_in_progress__', logoutInProgress);
if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout); if (forceLogout) sessionStorage.setItem('__force_logout__', forceLogout);
console.log('🚪 Local storage cleared (logout flags preserved)');
// Final verification BEFORE redirect
console.log('🚪 Final verification - logout flags preserved:', {
logoutInProgress: sessionStorage.getItem('__logout_in_progress__'),
forceLogout: sessionStorage.getItem('__force_logout__'),
});
console.log('🚪 Clearing local session and redirecting to login...');
console.log('🚪 Using prompt=login on next auth to force re-authentication');
console.log('🚪 This will prevent auto-authentication even if Okta session exists');
// Small delay to ensure sessionStorage is written before redirect // Small delay to ensure sessionStorage is written before redirect
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
@ -523,11 +474,8 @@ export function _Auth0AuthProvider({ children }: { children: ReactNode }) {
authorizationParams={{ authorizationParams={{
redirect_uri: window.location.origin + '/login/callback', redirect_uri: window.location.origin + '/login/callback',
}} }}
onRedirectCallback={(appState) => { onRedirectCallback={(_appState) => {
console.log('Auth0 Redirect Callback:', { // Auth0 redirect callback handled
appState,
returnTo: appState?.returnTo || window.location.pathname,
});
}} }}
> >
<Auth0ContextWrapper>{children}</Auth0ContextWrapper> <Auth0ContextWrapper>{children}</Auth0ContextWrapper>

View File

@ -71,7 +71,6 @@ export function useConclusionRemark(
} }
} catch (err) { } catch (err) {
// No conclusion yet - this is expected for newly approved requests // No conclusion yet - this is expected for newly approved requests
console.log('[useConclusionRemark] No existing conclusion found');
} }
}; };

View File

@ -1,7 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { getWorkflowDetails } from '@/services/workflowApi'; import { getWorkflowDetails } from '@/services/workflowApi';
import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
export interface RequestTemplate { export interface RequestTemplate {
id: string; id: string;
@ -127,7 +126,6 @@ export function useCreateRequestForm(
editRequestId: string, editRequestId: string,
templates: RequestTemplate[] templates: RequestTemplate[]
) { ) {
const { user } = useAuth();
const [formData, setFormData] = useState<FormData>(initialFormData); const [formData, setFormData] = useState<FormData>(initialFormData);
const [selectedTemplate, setSelectedTemplate] = useState<RequestTemplate | null>(null); const [selectedTemplate, setSelectedTemplate] = useState<RequestTemplate | null>(null);
const [loadingDraft, setLoadingDraft] = useState(isEditing); const [loadingDraft, setLoadingDraft] = useState(isEditing);
@ -148,7 +146,7 @@ export function useCreateRequestForm(
const loadPolicies = async () => { const loadPolicies = async () => {
try { try {
// Load document policy // Load document policy
const docConfigs = await getAllConfigurations('DOCUMENT_POLICY'); const docConfigs = await getPublicConfigurations('DOCUMENT_POLICY');
const docConfigMap: Record<string, string> = {}; const docConfigMap: Record<string, string> = {};
docConfigs.forEach((c: AdminConfiguration) => { docConfigs.forEach((c: AdminConfiguration) => {
docConfigMap[c.configKey] = c.configValue; docConfigMap[c.configKey] = c.configValue;
@ -164,8 +162,8 @@ export function useCreateRequestForm(
}); });
// Load system policy // Load system policy
const workflowConfigs = await getAllConfigurations('WORKFLOW_SHARING'); const workflowConfigs = await getPublicConfigurations('WORKFLOW_SHARING');
const tatConfigs = await getAllConfigurations('TAT_SETTINGS'); const tatConfigs = await getPublicConfigurations('TAT_SETTINGS');
const allConfigs = [...workflowConfigs, ...tatConfigs]; const allConfigs = [...workflowConfigs, ...tatConfigs];
const configMap: Record<string, string> = {}; const configMap: Record<string, string> = {};
allConfigs.forEach((c: AdminConfiguration) => { allConfigs.forEach((c: AdminConfiguration) => {

View File

@ -29,7 +29,7 @@ export interface PreviewDocument {
*/ */
export function useDocumentManagement( export function useDocumentManagement(
documentPolicy: DocumentPolicy, documentPolicy: DocumentPolicy,
isEditing: boolean = false _isEditing: boolean = false
) { ) {
const [documents, setDocuments] = useState<File[]>([]); const [documents, setDocuments] = useState<File[]>([]);
const [existingDocuments, setExistingDocuments] = useState<any[]>([]); const [existingDocuments, setExistingDocuments] = useState<any[]>([]);

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { uploadDocument } from '@/services/documentApi'; import { uploadDocument } from '@/services/documentApi';
import { getAllConfigurations, AdminConfiguration } from '@/services/adminApi'; import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
import { toast } from 'sonner'; import { toast } from 'sonner';
/** /**
@ -57,7 +57,7 @@ export function useDocumentUpload(
useEffect(() => { useEffect(() => {
const loadDocumentPolicy = async () => { const loadDocumentPolicy = async () => {
try { try {
const configs = await getAllConfigurations('DOCUMENT_POLICY'); const configs = await getPublicConfigurations('DOCUMENT_POLICY');
const configMap: Record<string, string> = {}; const configMap: Record<string, string> = {};
configs.forEach((c: AdminConfiguration) => { configs.forEach((c: AdminConfiguration) => {
configMap[c.configKey] = c.configValue; configMap[c.configKey] = c.configValue;

View File

@ -237,8 +237,10 @@ export function useRequestDetails(
}, },
createdAt: wf.createdAt, createdAt: wf.createdAt,
updatedAt: wf.updatedAt, updatedAt: wf.updatedAt,
totalSteps: wf.totalLevels, totalSteps: wf.totalLevels || 1,
currentStep: summary?.currentLevel || wf.currentLevel, // Store both raw and clamped values - raw for completion detection, clamped for display
currentStepRaw: summary?.currentLevel || wf.currentLevel || 1,
currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1),
auditTrail: filteredActivities, auditTrail: filteredActivities,
conclusionRemark: wf.conclusionRemark || null, conclusionRemark: wf.conclusionRemark || null,
closureDate: wf.closureDate || null, closureDate: wf.closureDate || null,
@ -407,8 +409,10 @@ export function useRequestDetails(
}, },
createdAt: wf.createdAt, createdAt: wf.createdAt,
updatedAt: wf.updatedAt, updatedAt: wf.updatedAt,
totalSteps: wf.totalLevels, totalSteps: wf.totalLevels || 1,
currentStep: summary?.currentLevel || wf.currentLevel, // Store both raw and clamped values - raw for completion detection, clamped for display
currentStepRaw: summary?.currentLevel || wf.currentLevel || 1,
currentStep: Math.min(Math.max(1, summary?.currentLevel || wf.currentLevel || 1), wf.totalLevels || 1),
approvalFlow, approvalFlow,
approvals, approvals,
documents: mappedDocuments, documents: mappedDocuments,

View File

@ -208,9 +208,7 @@ export function useRequestSocket(
* 3. Show browser notification if permission granted * 3. Show browser notification if permission granted
*/ */
const handleTatAlert = (data: any) => { const handleTatAlert = (data: any) => {
// TAT alert received - single line log only
const alertEmoji = data.type === 'breach' ? '⏰' : data.type === 'threshold2' ? '⚠️' : '⏳'; const alertEmoji = data.type === 'breach' ? '⏰' : data.type === 'threshold2' ? '⚠️' : '⏳';
console.log(`TAT Alert: ${data.message}`);
// Refresh: Get updated TAT alerts from backend // Refresh: Get updated TAT alerts from backend
(async () => { (async () => {
@ -218,8 +216,8 @@ export function useRequestSocket(
const details = await workflowApi.getWorkflowDetails(requestIdentifier); const details = await workflowApi.getWorkflowDetails(requestIdentifier);
if (details) { if (details) {
const tatAlerts = Array.isArray(details.tatAlerts) ? details.tatAlerts : []; // Extract TAT alerts for potential future use
console.log(`[useRequestSocket] Refreshed TAT alerts:`, tatAlerts); void (Array.isArray(details.tatAlerts) ? details.tatAlerts : []);
// Browser notification (if user granted permission) // Browser notification (if user granted permission)
if ('Notification' in window && Notification.permission === 'granted') { if ('Notification' in window && Notification.permission === 'granted') {

View File

@ -4,13 +4,6 @@ import { AuthProvider } from './contexts/AuthContext';
import { AuthenticatedApp } from './pages/Auth'; import { AuthenticatedApp } from './pages/Auth';
import './styles/globals.css'; import './styles/globals.css';
console.log('Application Starting...');
console.log('Environment:', {
hostname: window.location.hostname,
origin: window.location.origin,
isLocalhost: window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1',
});
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<AuthProvider> <AuthProvider>

View File

@ -2,7 +2,7 @@
* Approver's Actions Statistics Component * Approver's Actions Statistics Component
*/ */
import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3 } from 'lucide-react'; import { CheckCircle, XCircle, Clock, FileText, Users, Target, Award, AlertCircle, BarChart3, Archive } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import type { ApproverPerformance } from '@/services/dashboard.service'; import type { ApproverPerformance } from '@/services/dashboard.service';
import type { ApproverPerformanceStats } from '../types/approverPerformance.types'; import type { ApproverPerformanceStats } from '../types/approverPerformance.types';
@ -33,7 +33,7 @@ export function ApproverPerformanceActionsStats({
<Users className="w-4 h-4" /> <Users className="w-4 h-4" />
Approver's Actions Approver's Actions
</h4> </h4>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
<div className="p-4 bg-green-50 rounded-lg border border-green-200"> <div className="p-4 bg-green-50 rounded-lg border border-green-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<CheckCircle className="w-5 h-5 text-green-600" /> <CheckCircle className="w-5 h-5 text-green-600" />
@ -61,6 +61,13 @@ export function ApproverPerformanceActionsStats({
<div className="text-2xl font-bold text-yellow-700">{calculatedStats.pendingByApprover}</div> <div className="text-2xl font-bold text-yellow-700">{calculatedStats.pendingByApprover}</div>
<div className="text-xs text-gray-600 mt-1">Pending Actions</div> <div className="text-xs text-gray-600 mt-1">Pending Actions</div>
</div> </div>
<div className="p-4 bg-gray-50 rounded-lg border border-gray-200">
<div className="flex items-center justify-between mb-2">
<Archive className="w-5 h-5 text-gray-600" />
</div>
<div className="text-2xl font-bold text-gray-700">{calculatedStats.closedByApprover}</div>
<div className="text-xs text-gray-600 mt-1">Closed Requests</div>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200"> <div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<FileText className="w-5 h-5 text-blue-600" /> <FileText className="w-5 h-5 text-blue-600" />

View File

@ -2,13 +2,13 @@
* Type definitions for Approver Performance page * Type definitions for Approver Performance page
*/ */
import type { ApproverPerformance } from '@/services/dashboard.service';
export interface ApproverPerformanceStats { export interface ApproverPerformanceStats {
total: number; total: number;
approvedByApprover: number; approvedByApprover: number;
rejectedByApprover: number; rejectedByApprover: number;
pendingByApprover: number; pendingByApprover: number;
closedByApprover: number;
breached: number; breached: number;
compliant: number; compliant: number;
approvalRate: number; approvalRate: number;

View File

@ -25,6 +25,13 @@ export function calculateApproverStats(
return ['pending', 'in_progress', 'in-progress'].includes(status); return ['pending', 'in_progress', 'in-progress'].includes(status);
}).length; }).length;
// Count closed requests - check overall request status (wf.status = 'CLOSED')
// Closed requests are those that have been finalized with a conclusion remark
const closedByApprover = filtered.filter(r => {
const requestStatus = (r.status || '').toLowerCase();
return requestStatus === 'closed';
}).length;
// TAT compliance stats - check isBreached flag OR slaStatus === 'breached' // TAT compliance stats - check isBreached flag OR slaStatus === 'breached'
// This includes pending requests that have breached their TAT // This includes pending requests that have breached their TAT
const breached = filtered.filter(r => { const breached = filtered.filter(r => {
@ -72,6 +79,7 @@ export function calculateApproverStats(
approvedByApprover, approvedByApprover,
rejectedByApprover, rejectedByApprover,
pendingByApprover, pendingByApprover,
closedByApprover,
// TAT stats // TAT stats
breached, breached,
compliant, compliant,

View File

@ -6,28 +6,13 @@ import { LogIn, Shield } from 'lucide-react';
export function Auth() { export function Auth() {
const { login, isLoading, error } = useAuth(); const { login, isLoading, error } = useAuth();
console.log('Auth Component Render - Auth0 State:', {
isLoading,
error: error?.message,
timestamp: new Date().toISOString()
});
const handleSSOLogin = async () => { const handleSSOLogin = async () => {
console.log('========================================');
console.log('SSO LOGIN - Button Clicked');
console.log('Timestamp:', new Date().toISOString());
console.log('Current URL:', window.location.href);
console.log('========================================');
// Clear any existing session data // Clear any existing session data
console.log('Clearing local storage and session storage...');
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
console.log('Storage cleared');
try { try {
await login(); await login();
console.log('Login redirect initiated successfully');
} catch (loginError) { } catch (loginError) {
console.error('========================================'); console.error('========================================');
console.error('LOGIN ERROR'); console.error('LOGIN ERROR');

View File

@ -13,27 +13,18 @@ export function AuthenticatedApp() {
const isCallbackRoute = typeof window !== 'undefined' && window.location.pathname === '/login/callback'; const isCallbackRoute = typeof window !== 'undefined' && window.location.pathname === '/login/callback';
const handleLogout = async () => { const handleLogout = async () => {
console.log('🔵 ========================================');
console.log('🔵 AuthenticatedApp.handleLogout - CALLED');
console.log('🔵 Timestamp:', new Date().toISOString());
console.log('🔵 logout function exists?', !!logout);
console.log('🔵 ========================================');
try { try {
if (!logout) { if (!logout) {
console.error('🔵 ERROR: logout function is undefined!'); console.error('🔵 ERROR: logout function is undefined!');
return; return;
} }
console.log('🔵 Calling logout from auth context...');
// Call logout from auth context (handles all cleanup and redirect) // Call logout from auth context (handles all cleanup and redirect)
await logout(); await logout();
console.log('🔵 Logout successful - redirecting to login...');
} catch (logoutError) { } catch (logoutError) {
console.error('🔵 Logout error in handleLogout:', logoutError); console.error('🔵 Logout error in handleLogout:', logoutError);
// Even if logout fails, clear local data and redirect // Even if logout fails, clear local data and redirect
try { try {
console.log('🔵 Attempting emergency cleanup...');
localStorage.clear(); localStorage.clear();
sessionStorage.clear(); sessionStorage.clear();
window.location.href = '/'; window.location.href = '/';
@ -44,33 +35,7 @@ export function AuthenticatedApp() {
}; };
useEffect(() => { useEffect(() => {
console.log('AuthenticatedApp - Auth State Changed:', { // Auth state changed
isAuthenticated,
isLoading,
error: error?.message,
hasUser: !!user,
timestamp: new Date().toISOString()
});
if (user) {
console.log('========================================');
console.log('USER AUTHENTICATED - Full Details');
console.log('========================================');
console.log('User ID:', user.userId || user.sub);
console.log('Employee ID:', user.employeeId);
console.log('Email:', user.email);
console.log('Name:', user.displayName || user.name);
console.log('Display Name:', user.displayName);
console.log('First Name:', user.firstName);
console.log('Last Name:', user.lastName);
console.log('Department:', user.department);
console.log('Designation:', user.designation);
console.log('Role:', user.role);
console.log('========================================');
console.log('ALL USER DATA:');
console.log(JSON.stringify(user, null, 2));
console.log('========================================');
}
}, [isAuthenticated, isLoading, error, user]); }, [isAuthenticated, isLoading, error, user]);
// Always show callback loader when on callback route (after all hooks) // Always show callback loader when on callback route (after all hooks)
@ -80,7 +45,6 @@ export function AuthenticatedApp() {
// Show loading state while checking authentication // Show loading state while checking authentication
if (isLoading) { if (isLoading) {
console.log('Auth0 is still loading...');
return ( return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100"> <div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="text-center"> <div className="text-center">
@ -112,12 +76,10 @@ export function AuthenticatedApp() {
// Show login screen if not authenticated // Show login screen if not authenticated
if (!isAuthenticated) { if (!isAuthenticated) {
console.log('User not authenticated, showing login screen');
return <Auth />; return <Auth />;
} }
// User is authenticated, show the app // User is authenticated, show the app
console.log('User authenticated successfully, showing main application');
return ( return (
<> <>
<App onLogout={handleLogout} /> <App onLogout={handleLogout} />

View File

@ -12,7 +12,7 @@ import { useClosedRequests } from './hooks/useClosedRequests';
import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters'; import { useClosedRequestsFilters } from './hooks/useClosedRequestsFilters';
// Types // Types
import type { ClosedRequestsProps } from './types/closedRequests.types'; import type { ClosedRequestsProps, ClosedRequestsFilters } from './types/closedRequests.types';
export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) { export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
// Data fetching hook // Data fetching hook
@ -24,7 +24,7 @@ export function ClosedRequests({ onViewRequest }: ClosedRequestsProps) {
const filters = useClosedRequestsFilters({ const filters = useClosedRequestsFilters({
onFiltersChange: useCallback( onFiltersChange: useCallback(
(filters) => { (filters: ClosedRequestsFilters) => {
// Reset to page 1 when filters change // Reset to page 1 when filters change
fetchRef.current(1, { fetchRef.current(1, {
search: filters.search || undefined, search: filters.search || undefined,

View File

@ -6,7 +6,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { ArrowRight, Calendar, CheckCircle } from 'lucide-react'; import { ArrowRight, Calendar, CheckCircle } from 'lucide-react';
import { formatDateTime } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
import { ClosedRequest } from '../types/closedRequests.types'; import { ClosedRequest } from '../types/closedRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
@ -88,13 +88,13 @@ export function ClosedRequestCard({ request, onViewRequest }: ClosedRequestCardP
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" /> <Calendar className="w-3.5 h-3.5" />
<span>Created: {request.createdAt !== '—' ? formatDateTime(request.createdAt) : '—'}</span> <span>Created: {request.createdAt !== '—' ? formatDateDDMMYYYY(request.createdAt, true) : '—'}</span>
</div> </div>
{request.dueDate && ( {request.dueDate && (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<CheckCircle className="w-3.5 h-3.5 text-slate-600" /> <CheckCircle className="w-3.5 h-3.5 text-slate-600" />
<span className="font-medium">Closed: {formatDateTime(request.dueDate)}</span> <span className="font-medium">Closed: {formatDateDDMMYYYY(request.dueDate, true)}</span>
</div> </div>
)} )}
</div> </div>

View File

@ -110,12 +110,6 @@ export function ClosedRequestsFilters({
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All Statuses</SelectItem> <SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="approved">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>Approved</span>
</div>
</SelectItem>
<SelectItem value="closed"> <SelectItem value="closed">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-gray-600" /> <CheckCircle className="w-4 h-4 text-gray-600" />

View File

@ -66,7 +66,7 @@ export function ClosedRequestsPagination({
variant={pageNum === currentPage ? "default" : "outline"} variant={pageNum === currentPage ? "default" : "outline"}
size="sm" size="sm"
onClick={() => onPageChange(pageNum)} onClick={() => onPageChange(pageNum)}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
data-testid={`closed-requests-pagination-page-${pageNum}`} data-testid={`closed-requests-pagination-page-${pageNum}`}
> >
{pageNum} {pageNum}

View File

@ -41,11 +41,28 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
// Always use user-scoped endpoint (not organization-wide) // Always use user-scoped endpoint (not organization-wide)
// Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user) // Backend filters by userId regardless of user role (ADMIN/MANAGEMENT/regular user)
// For organization-wide requests, use the "All Requests" screen (/requests) // For organization-wide requests, use the "All Requests" screen (/requests)
// Only fetch rejected and closed requests - exclude approved
let statusFilter = filters?.status;
// If user somehow selects approved (shouldn't be possible now), don't fetch
if (statusFilter === 'approved') {
setRequests([]);
setPagination({
currentPage: 1,
totalPages: 1,
totalRecords: 0,
itemsPerPage,
});
setLoading(false);
setRefreshing(false);
return;
}
const result = await workflowApi.listClosedByMe({ const result = await workflowApi.listClosedByMe({
page, page,
limit: itemsPerPage, limit: itemsPerPage,
search: filters?.search, search: filters?.search,
status: filters?.status, status: statusFilter && statusFilter !== 'all' ? statusFilter : undefined,
priority: filters?.priority, priority: filters?.priority,
sortBy: filters?.sortBy, sortBy: filters?.sortBy,
sortOrder: filters?.sortOrder sortOrder: filters?.sortOrder
@ -56,9 +73,23 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
? (result as any).data ? (result as any).data
: []; : [];
const mapped = transformClosedRequests(data);
// Filter out approved requests - only show rejected and closed
const filtered = mapped.filter(request =>
request.status === 'rejected' || request.status === 'closed'
);
setRequests(filtered);
// Set pagination data // Set pagination data
// Note: Since we're filtering out approved requests client-side,
// the pagination count from backend may include approved requests.
// We'll use the filtered count for this page, but total records
// from backend is approximate.
const paginationData = (result as any)?.pagination; const paginationData = (result as any)?.pagination;
if (paginationData) { if (paginationData) {
// If we filtered out some requests on this page, we need to adjust pagination
// For simplicity, we'll keep backend's total but note it may be slightly off
// A better solution would be to filter on backend, but for now this works
setPagination({ setPagination({
currentPage: paginationData.page || 1, currentPage: paginationData.page || 1,
totalPages: paginationData.totalPages || 1, totalPages: paginationData.totalPages || 1,
@ -66,9 +97,6 @@ export function useClosedRequests({ itemsPerPage = 10, initialFilters }: UseClos
itemsPerPage, itemsPerPage,
}); });
} }
const mapped = transformClosedRequests(data);
setRequests(mapped);
} catch (error) { } catch (error) {
console.error('[ClosedRequests] Error fetching requests:', error); console.error('[ClosedRequests] Error fetching requests:', error);
setRequests([]); setRequests([]);

View File

@ -8,7 +8,7 @@ export interface ClosedRequest {
displayId?: string; displayId?: string;
title: string; title: string;
description: string; description: string;
status: 'approved' | 'rejected' | 'closed'; status: 'rejected' | 'closed';
priority: 'express' | 'standard'; priority: 'express' | 'standard';
initiator: { name: string; avatar: string }; initiator: { name: string; avatar: string };
createdAt: string; createdAt: string;

View File

@ -30,14 +30,6 @@ export function getPriorityConfig(priority: string): PriorityConfig {
export function getStatusConfig(status: string): StatusConfig { export function getStatusConfig(status: string): StatusConfig {
switch (status) { switch (status) {
case 'approved':
return {
color: 'bg-emerald-100 text-emerald-800 border-emerald-300',
icon: CheckCircle,
iconColor: 'text-emerald-600',
label: 'Needs Closure',
description: 'Fully approved, awaiting initiator to finalize'
};
case 'closed': case 'closed':
return { return {
color: 'bg-slate-100 text-slate-800 border-slate-300', color: 'bg-slate-100 text-slate-800 border-slate-300',

View File

@ -11,7 +11,7 @@ export function transformClosedRequest(r: any): ClosedRequest {
displayId: r.requestNumber || r.request_number || r.requestId, displayId: r.requestNumber || r.request_number || r.requestId,
title: r.title, title: r.title,
description: r.description, description: r.description,
status: (r.status || '').toString().toLowerCase() as 'approved' | 'rejected' | 'closed', status: (r.status || '').toString().toLowerCase() as 'rejected' | 'closed',
priority: (r.priority || '').toString().toLowerCase() as 'express' | 'standard', priority: (r.priority || '').toString().toLowerCase() as 'express' | 'standard',
initiator: { initiator: {
name: r.initiator?.displayName || r.initiator?.email || '—', name: r.initiator?.displayName || r.initiator?.email || '—',

View File

@ -32,7 +32,7 @@ interface UseHandlersOptions {
} }
export function useCreateRequestHandlers({ export function useCreateRequestHandlers({
selectedTemplate, selectedTemplate: _selectedTemplate,
setSelectedTemplate, setSelectedTemplate,
updateFormData, updateFormData,
formData, formData,

View File

@ -157,6 +157,7 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
dateRange?: DateRange; dateRange?: DateRange;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
targetPage?: 'requests' | 'open-requests' | 'my-requests';
}) => { }) => {
const url = buildFilterUrl(filters); const url = buildFilterUrl(filters);
onNavigate?.(url); onNavigate?.(url);
@ -211,11 +212,14 @@ export function Dashboard({ onNavigate, onNewRequest }: DashboardProps) {
customStartDate={customStartDate} customStartDate={customStartDate}
customEndDate={customEndDate} customEndDate={customEndDate}
onKPIClick={handleKPIClick} onKPIClick={handleKPIClick}
onNavigate={onNavigate}
userId={(user as any)?.userId}
userDisplayName={(user as any)?.displayName || (user as any)?.email}
/> />
)} )}
{/* Alerts and Activity Section */} {/* Alerts and Activity Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6" style={{ height: '60vh', minHeight: '480px' }} data-testid="dashboard-alerts-activity"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6 h-[90vh] min-h-[720px] lg:h-[60vh] lg:min-h-[480px]" data-testid="dashboard-alerts-activity">
<CriticalAlertsSection <CriticalAlertsSection
isAdmin={isAdmin} isAdmin={isAdmin}
breachedRequests={breachedRequests} breachedRequests={breachedRequests}

View File

@ -8,7 +8,8 @@ import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Activity, MessageSquare, PieChart, Download } from 'lucide-react'; import { Activity, MessageSquare, PieChart, Download } from 'lucide-react';
import { DashboardKPIs, DepartmentStats, UpcomingDeadline, DateRange } from '@/services/dashboard.service'; import { DashboardKPIs, DepartmentStats, UpcomingDeadline, DateRange } from '@/services/dashboard.service';
import { CriticalRequest, CriticalAlertData } from '@/services/dashboard.service'; import { CriticalRequest } from '@/services/dashboard.service';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';
import { KPIClickFilters } from '../../components/types/dashboard.types'; import { KPIClickFilters } from '../../components/types/dashboard.types';

View File

@ -144,7 +144,7 @@ export function AdminKPICards({
{/* Avg Cycle Time */} {/* Avg Cycle Time */}
<KPICard <KPICard
title="Avg Cycle Time" title="Avg Cycle Time"
value={kpis?.tatEfficiency.avgCycleTimeHours ? formatHoursMinutes(kpis.tatEfficiency.avgCycleTimeHours) : '0h'} value={kpis?.tatEfficiency.avgCycleTimeHours ? formatHoursMinutes(kpis.tatEfficiency.avgCycleTimeHours) : '0 hours'}
icon={Clock} icon={Clock}
iconBgColor="bg-purple-50" iconBgColor="bg-purple-50"
iconColor="text-purple-600" iconColor="text-purple-600"
@ -152,6 +152,7 @@ export function AdminKPICards({
testId="kpi-avg-cycle-time" testId="kpi-avg-cycle-time"
onClick={() => onKPIClick(getFilterParams())} onClick={() => onKPIClick(getFilterParams())}
> >
<div className="flex flex-col flex-1">
<div className="grid grid-cols-2 gap-2 mt-auto"> <div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard <StatCard
label="Express" label="Express"
@ -184,6 +185,7 @@ export function AdminKPICards({
}} }}
/> />
</div> </div>
</div>
</KPICard> </KPICard>
</div> </div>
); );

View File

@ -8,7 +8,8 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Flame, CheckCircle, ArrowRight } from 'lucide-react'; import { Flame, CheckCircle, ArrowRight } from 'lucide-react';
import { CriticalAlertCard } from '@/components/dashboard/CriticalAlertCard'; import { CriticalAlertCard } from '@/components/dashboard/CriticalAlertCard';
import { CriticalRequest, CriticalAlertData } from '@/services/dashboard.service'; import { CriticalRequest } from '@/services/dashboard.service';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
import { getPageNumbers } from '../../utils/dashboardCalculations'; import { getPageNumbers } from '../../utils/dashboardCalculations';
interface CriticalAlertsSectionProps { interface CriticalAlertsSectionProps {

View File

@ -124,7 +124,7 @@ export function RecentActivitySection({
variant={pageNum === pagination.page ? 'default' : 'outline'} variant={pageNum === pagination.page ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => onPageChange(pageNum)} onClick={() => onPageChange(pageNum)}
className={`h-7 w-7 p-0 text-xs ${pageNum === pagination.page ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} className={`h-7 w-7 p-0 text-xs ${pageNum === pagination.page ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
data-testid={`activity-pagination-page-${pageNum}`} data-testid={`activity-pagination-page-${pageNum}`}
> >
{pageNum} {pageNum}

View File

@ -6,7 +6,8 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { CriticalRequest, CriticalAlertData, DateRange } from '@/services/dashboard.service'; import { CriticalRequest, DateRange } from '@/services/dashboard.service';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
import { Pagination } from '@/components/common/Pagination'; import { Pagination } from '@/components/common/Pagination';
import { formatBreachTime } from '../../utils/dashboardCalculations'; import { formatBreachTime } from '../../utils/dashboardCalculations';
import { KPIClickFilters } from '../../components/types/dashboard.types'; import { KPIClickFilters } from '../../components/types/dashboard.types';

View File

@ -8,7 +8,8 @@ import { KPICard } from '@/components/dashboard/KPICard';
import { StatCard } from '@/components/dashboard/StatCard'; import { StatCard } from '@/components/dashboard/StatCard';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { DashboardKPIs, DateRange } from '@/services/dashboard.service'; import { DashboardKPIs, DateRange } from '@/services/dashboard.service';
import { CriticalRequest, CriticalAlertData } from '@/services/dashboard.service'; import { CriticalRequest } from '@/services/dashboard.service';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
import { KPIClickFilters } from '../../components/types/dashboard.types'; import { KPIClickFilters } from '../../components/types/dashboard.types';
interface UserKPICardsProps { interface UserKPICardsProps {
@ -18,6 +19,9 @@ interface UserKPICardsProps {
customStartDate?: Date; customStartDate?: Date;
customEndDate?: Date; customEndDate?: Date;
onKPIClick: (filters: KPIClickFilters) => void; onKPIClick: (filters: KPIClickFilters) => void;
onNavigate?: (page: string) => void;
userId?: string;
userDisplayName?: string;
} }
export function UserKPICards({ export function UserKPICards({
@ -27,6 +31,9 @@ export function UserKPICards({
customStartDate, customStartDate,
customEndDate, customEndDate,
onKPIClick, onKPIClick,
onNavigate,
userId,
userDisplayName,
}: UserKPICardsProps) { }: UserKPICardsProps) {
const getFilterParams = () => ({ const getFilterParams = () => ({
dateRange, dateRange,
@ -34,6 +41,19 @@ export function UserKPICards({
endDate: customEndDate, endDate: customEndDate,
}); });
const handleNavigateToApproverPerformance = () => {
if (!userId) return;
const params = new URLSearchParams();
params.set('approverId', userId);
params.set('approverName', userDisplayName || 'My Performance');
params.set('dateRange', dateRange);
if (dateRange === 'custom' && customStartDate && customEndDate) {
params.set('startDate', customStartDate.toISOString());
params.set('endDate', customEndDate.toISOString());
}
onNavigate?.(`/approver-performance?${params.toString()}`);
};
const successRate = kpis && kpis.requestVolume.totalRequests > 0 const successRate = kpis && kpis.requestVolume.totalRequests > 0
? ((kpis.requestVolume.approvedRequests / kpis.requestVolume.totalRequests) * 100) ? ((kpis.requestVolume.approvedRequests / kpis.requestVolume.totalRequests) * 100)
: 0; : 0;
@ -74,14 +94,14 @@ export function UserKPICards({
}} }}
/> />
<StatCard <StatCard
label="Draft" label="Rejected"
value={kpis?.requestVolume.draftRequests || 0} value={kpis?.requestVolume.rejectedRequests || 0}
bgColor="bg-gray-50" bgColor="bg-red-50"
textColor="text-gray-600" textColor="text-red-600"
testId="stat-user-draft" testId="stat-user-rejected"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onKPIClick({ ...getFilterParams(), status: 'draft' }); onKPIClick({ ...getFilterParams(), status: 'rejected' });
}} }}
/> />
<StatCard <StatCard
@ -107,6 +127,9 @@ export function UserKPICards({
iconColor="text-orange-600" iconColor="text-orange-600"
subtitle="at current level" subtitle="at current level"
testId="kpi-pending-actions" testId="kpi-pending-actions"
onClick={() => onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' })}
onJustifyClick={handleNavigateToApproverPerformance}
showJustifyButton={!!userId}
> >
<div className="grid grid-cols-2 gap-2 mt-auto"> <div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard <StatCard
@ -115,6 +138,10 @@ export function UserKPICards({
bgColor="bg-blue-50" bgColor="bg-blue-50"
textColor="text-blue-600" textColor="text-blue-600"
testId="stat-today" testId="stat-today"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' });
}}
/> />
<StatCard <StatCard
label="This Week" label="This Week"
@ -122,6 +149,10 @@ export function UserKPICards({
bgColor="bg-green-50" bgColor="bg-green-50"
textColor="text-green-600" textColor="text-green-600"
testId="stat-week" testId="stat-week"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests', status: 'pending' });
}}
/> />
</div> </div>
</KPICard> </KPICard>
@ -134,6 +165,9 @@ export function UserKPICards({
iconBgColor="bg-red-50" iconBgColor="bg-red-50"
iconColor="text-red-600" iconColor="text-red-600"
testId="kpi-user-critical" testId="kpi-user-critical"
onClick={() => onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' })}
onJustifyClick={handleNavigateToApproverPerformance}
showJustifyButton={!!userId}
> >
<div className="grid grid-cols-2 gap-2 mt-auto"> <div className="grid grid-cols-2 gap-2 mt-auto">
<StatCard <StatCard
@ -142,6 +176,10 @@ export function UserKPICards({
bgColor="bg-orange-50" bgColor="bg-orange-50"
textColor="text-red-600" textColor="text-red-600"
testId="stat-user-breached" testId="stat-user-breached"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' });
}}
/> />
<StatCard <StatCard
label="Warning" label="Warning"
@ -149,6 +187,10 @@ export function UserKPICards({
bgColor="bg-yellow-50" bgColor="bg-yellow-50"
textColor="text-orange-600" textColor="text-orange-600"
testId="stat-user-warning" testId="stat-user-warning"
onClick={(e) => {
e.stopPropagation();
onKPIClick({ ...getFilterParams(), targetPage: 'open-requests' });
}}
/> />
</div> </div>
</KPICard> </KPICard>

View File

@ -12,5 +12,6 @@ export interface KPIClickFilters {
dateRange?: DateRange; dateRange?: DateRange;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
targetPage?: 'requests' | 'open-requests' | 'my-requests';
} }

View File

@ -4,7 +4,8 @@
import { useState, useCallback, useRef } from 'react'; import { useState, useCallback, useRef } from 'react';
import { dashboardService, type DashboardKPIs, type DateRange, type AIRemarkUtilization, type ApproverPerformance, type DepartmentStats, type PriorityDistribution, type UpcomingDeadline, type RecentActivity, type CriticalRequest } from '@/services/dashboard.service'; import { dashboardService, type DashboardKPIs, type DateRange, type AIRemarkUtilization, type ApproverPerformance, type DepartmentStats, type PriorityDistribution, type UpcomingDeadline, type RecentActivity, type CriticalRequest } from '@/services/dashboard.service';
import { ActivityData, CriticalAlertData } from '@/components/dashboard/ActivityFeedItem'; import { ActivityData } from '@/components/dashboard/ActivityFeedItem';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
interface UseDashboardDataOptions { interface UseDashboardDataOptions {
isAdmin: boolean; isAdmin: boolean;

View File

@ -2,8 +2,9 @@
* Dashboard-specific TypeScript types and interfaces * Dashboard-specific TypeScript types and interfaces
*/ */
import { DateRange, DashboardKPIs, CriticalRequest, UpcomingDeadline, RecentActivity, DepartmentStats, PriorityDistribution, AIRemarkUtilization, ApproverPerformance } from '@/services/dashboard.service'; import { DateRange, DashboardKPIs, CriticalRequest, UpcomingDeadline, DepartmentStats, PriorityDistribution, AIRemarkUtilization, ApproverPerformance } from '@/services/dashboard.service';
import { ActivityData, CriticalAlertData } from '@/components/dashboard/ActivityFeedItem'; import { ActivityData } from '@/components/dashboard/ActivityFeedItem';
import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard';
export interface DashboardFilters { export interface DashboardFilters {
dateRange: DateRange; dateRange: DateRange;

View File

@ -41,17 +41,25 @@ export function getUpcomingDeadlinesNotBreached(
/** /**
* Format breach time for display * Format breach time for display
* Backend returns working hours, so we divide by 8 (working hours per day) not 24
*/ */
export function formatBreachTime(hours: number): string { export function formatBreachTime(hours: number): string {
if (hours <= 0) return 'Just breached'; if (hours <= 0) return 'Just breached';
if (hours < 1) return `${Math.round(hours * 60)} min`; if (hours < 1) return `${Math.round(hours * 60)} min`;
if (hours < 24) {
const WORKING_HOURS_PER_DAY = 8;
// If less than one working day, show hours and minutes
if (hours < WORKING_HOURS_PER_DAY) {
const h = Math.floor(hours); const h = Math.floor(hours);
const m = Math.round((hours - h) * 60); const m = Math.round((hours - h) * 60);
return m > 0 ? `${h}h ${m}m` : `${h}h`; return m > 0 ? `${h}h ${m}m` : `${h}h`;
} }
const days = Math.floor(hours / 24);
const remainingHours = hours % 24; // Convert working hours to working days
const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
const remainingHours = hours % WORKING_HOURS_PER_DAY;
if (remainingHours > 0) { if (remainingHours > 0) {
const h = Math.floor(remainingHours); const h = Math.floor(remainingHours);
const m = Math.round((remainingHours - h) * 60); const m = Math.round((remainingHours - h) * 60);

View File

@ -16,6 +16,7 @@ export function buildFilterUrl(filters: {
dateRange?: DateRange; dateRange?: DateRange;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
targetPage?: 'requests' | 'open-requests' | 'my-requests';
}): string { }): string {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (filters.status) params.set('status', filters.status); if (filters.status) params.set('status', filters.status);
@ -26,7 +27,14 @@ export function buildFilterUrl(filters: {
if (filters.startDate) params.set('startDate', filters.startDate.toISOString()); if (filters.startDate) params.set('startDate', filters.startDate.toISOString());
if (filters.endDate) params.set('endDate', filters.endDate.toISOString()); if (filters.endDate) params.set('endDate', filters.endDate.toISOString());
const queryString = params.toString(); const queryString = params.toString();
return queryString ? `/requests?${queryString}` : '/requests';
// Determine target page
const targetPage = filters.targetPage || 'requests';
const basePath = targetPage === 'open-requests' ? '/open-requests'
: targetPage === 'my-requests' ? '/my-requests'
: '/requests';
return queryString ? `${basePath}?${queryString}` : basePath;
} }
export interface QuickAction { export interface QuickAction {

View File

@ -68,7 +68,7 @@ export function RequestLifecycleReport({
</div> </div>
<div> <div>
<CardTitle className="text-lg text-gray-900">Request Lifecycle Report</CardTitle> <CardTitle className="text-lg text-gray-900">Request Lifecycle Report</CardTitle>
<CardDescription className="text-gray-600">End-to-end status with timeline and TAT compliance</CardDescription> <CardDescription className="text-gray-600">End-to-end workflow status including all approval levels, approvers, dates, and TAT compliance</CardDescription>
</div> </div>
</div> </div>
<Button variant="outline" size="sm" className="gap-2" onClick={onExport} disabled={exporting} data-testid="export-lifecycle-button"> <Button variant="outline" size="sm" className="gap-2" onClick={onExport} disabled={exporting} data-testid="export-lifecycle-button">

View File

@ -3,11 +3,12 @@
*/ */
import { dashboardService, type DateRange } from '@/services/dashboard.service'; import { dashboardService, type DateRange } from '@/services/dashboard.service';
import { formatTAT, formatDate, formatDateForCSV, mapActivityType } from './formatting'; import { getWorkflowDetails } from '@/services/workflowApi';
import { LifecycleRequest, ActivityLogEntry, AgingWorkflow } from '../types/detailedReports.types'; import { formatDateForCSV, mapActivityType } from './formatting';
/** /**
* Export Lifecycle Report to CSV * Export Lifecycle Report to CSV with End-to-End Workflow Details
* Includes all approval levels, approvers, dates, and TAT compliance
*/ */
export async function exportLifecycleToCSV( export async function exportLifecycleToCSV(
dateRange: DateRange, dateRange: DateRange,
@ -17,25 +18,57 @@ export async function exportLifecycleToCSV(
// Fetch all data with a very large limit // Fetch all data with a very large limit
const result = await dashboardService.getLifecycleReport(1, 10000, dateRange, customStartDate, customEndDate); const result = await dashboardService.getLifecycleReport(1, 10000, dateRange, customStartDate, customEndDate);
// CSV Headers for comprehensive end-to-end workflow status
const csvRows = [ const csvRows = [
[ [
'Request Number', 'Request Number',
'Title', 'Title',
'Priority', 'Priority',
'Status', 'Overall Status',
'Initiator', 'Initiator',
'Submission Date', 'Submission Date',
'Current Stage', 'Closure Date',
'Overall TAT',
'Current Level',
'Total Levels', 'Total Levels',
'Current Level',
'Overall TAT (Hours)',
'Overall TAT (Days)',
'Breach Count', 'Breach Count',
'Level Number',
'Level Name',
'Approver Name',
'Approver Email',
'Level Status',
'Level Start Date',
'Level Completion Date',
'Level TAT (Hours)',
'Level TAT (Days)',
'Level TAT Compliance',
'Level Elapsed Hours',
'Level Remaining Hours',
'Level TAT % Used',
].join(','), ].join(','),
]; ];
result.lifecycleData.forEach((req: any) => { // Process each request and fetch detailed approval level information
const overallTAT = formatTAT(req.overallTATHours); for (const req of result.lifecycleData) {
try {
// Fetch detailed workflow information including all approval levels
const workflowDetails = await getWorkflowDetails(req.requestId || req.requestNumber);
const submissionDateCSV = req.submissionDate ? formatDateForCSV(req.submissionDate) : 'N/A'; const submissionDateCSV = req.submissionDate ? formatDateForCSV(req.submissionDate) : 'N/A';
const closureDateCSV = req.closureDate ? formatDateForCSV(req.closureDate) : 'N/A';
const overallTATHours = req.overallTATHours || 0;
const overallTATDays = overallTATHours > 0 ? (overallTATHours / 8).toFixed(2) : '0';
// Fix: Ensure currentLevel doesn't exceed totalLevels (data inconsistency from backend)
const totalLevels = req.totalLevels || 1;
const currentLevel = Math.min(Math.max(1, req.currentLevel || 1), totalLevels);
// Get approval levels from workflow details
const approvalLevels = workflowDetails?.approvals || [];
if (approvalLevels.length === 0) {
// If no approval levels found, still export request-level data
const row = [ const row = [
req.requestNumber || '', req.requestNumber || '',
`"${(req.title || '').replace(/"/g, '""')}"`, `"${(req.title || '').replace(/"/g, '""')}"`,
@ -43,16 +76,140 @@ export async function exportLifecycleToCSV(
req.status || '', req.status || '',
`"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`, `"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`,
submissionDateCSV, submissionDateCSV,
`"${(req.currentStageName || `Level ${req.currentLevel}`).replace(/"/g, '""')}"`, closureDateCSV,
overallTAT, totalLevels.toString(),
(req.currentLevel || '').toString(), currentLevel.toString(),
(req.totalLevels || '').toString(), overallTATHours.toString(),
overallTATDays,
(req.breachCount || 0).toString(), (req.breachCount || 0).toString(),
'N/A', // Level Number
'N/A', // Level Name
'N/A', // Approver Name
'N/A', // Approver Email
'N/A', // Level Status
'N/A', // Level Start Date
'N/A', // Level Completion Date
'N/A', // Level TAT Hours
'N/A', // Level TAT Days
'N/A', // Level TAT Compliance
'N/A', // Level Elapsed Hours
'N/A', // Level Remaining Hours
'N/A', // Level TAT % Used
];
csvRows.push(row.join(','));
} else {
// Export one row per approval level for comprehensive end-to-end view
approvalLevels.forEach((level: any) => {
const levelStartDateCSV = level.levelStartTime ? formatDateForCSV(level.levelStartTime) : 'N/A';
const levelCompletionDateCSV = level.levelEndTime || level.completedAt
? formatDateForCSV(level.levelEndTime || level.completedAt)
: 'N/A';
const levelTATHours = level.tatHours || 0;
const levelTATDays = level.tatDays || (levelTATHours > 0 ? (levelTATHours / 8).toFixed(2) : '0');
// Calculate TAT compliance status
let tatCompliance = 'N/A';
let elapsedHours = 'N/A';
let remainingHours = 'N/A';
let tatPercentageUsed = 'N/A';
if (level.levelStartTime) {
const startTime = new Date(level.levelStartTime);
const endTime = level.levelEndTime || level.completedAt
? new Date(level.levelEndTime || level.completedAt)
: new Date();
const elapsedMs = endTime.getTime() - startTime.getTime();
elapsedHours = (elapsedMs / (1000 * 60 * 60)).toFixed(2);
if (levelTATHours > 0) {
const elapsed = parseFloat(elapsedHours);
remainingHours = Math.max(0, levelTATHours - elapsed).toFixed(2);
tatPercentageUsed = ((elapsed / levelTATHours) * 100).toFixed(2);
if (level.status === 'APPROVED' || level.status === 'REJECTED') {
tatCompliance = elapsed <= levelTATHours ? 'Compliant' : 'Breached';
} else if (level.status === 'IN_PROGRESS' || level.status === 'PENDING') {
tatCompliance = elapsed <= levelTATHours ? 'On Track' : 'At Risk';
} else {
tatCompliance = 'N/A';
}
}
}
const row = [
req.requestNumber || '',
`"${(req.title || '').replace(/"/g, '""')}"`,
req.priority || 'medium',
req.status || '',
`"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`,
submissionDateCSV,
closureDateCSV,
(req.totalLevels || 0).toString(),
(req.currentLevel || 0).toString(),
overallTATHours.toString(),
overallTATDays,
(req.breachCount || 0).toString(),
(level.levelNumber || '').toString(),
`"${(level.levelName || `Level ${level.levelNumber}`).replace(/"/g, '""')}"`,
`"${(level.approverName || 'N/A').replace(/"/g, '""')}"`,
`"${(level.approverEmail || 'N/A').replace(/"/g, '""')}"`,
level.status || 'PENDING',
levelStartDateCSV,
levelCompletionDateCSV,
levelTATHours.toString(),
levelTATDays.toString(),
tatCompliance,
elapsedHours,
remainingHours,
tatPercentageUsed,
]; ];
csvRows.push(row.join(',')); csvRows.push(row.join(','));
}); });
}
} catch (error) {
console.error(`Failed to fetch details for request ${req.requestNumber}:`, error);
// Still export basic request info even if details fetch fails
const submissionDateCSV = req.submissionDate ? formatDateForCSV(req.submissionDate) : 'N/A';
const overallTATHours = req.overallTATHours || 0;
const overallTATDays = overallTATHours > 0 ? (overallTATHours / 8).toFixed(2) : '0';
downloadCSV(csvRows, `lifecycle-report-${new Date().toISOString().split('T')[0]}.csv`); // Fix: Ensure currentLevel doesn't exceed totalLevels
const totalLevels = req.totalLevels || 1;
const currentLevel = Math.min(Math.max(1, req.currentLevel || 1), totalLevels);
const row = [
req.requestNumber || '',
`"${(req.title || '').replace(/"/g, '""')}"`,
req.priority || 'medium',
req.status || '',
`"${(req.initiatorName || 'Unknown').replace(/"/g, '""')}"`,
submissionDateCSV,
'N/A',
totalLevels.toString(),
currentLevel.toString(),
overallTATHours.toString(),
overallTATDays,
(req.breachCount || 0).toString(),
'Error', // Level Number
'Error fetching details', // Level Name
'N/A', // Approver Name
'N/A', // Approver Email
'N/A', // Level Status
'N/A', // Level Start Date
'N/A', // Level Completion Date
'N/A', // Level TAT Hours
'N/A', // Level TAT Days
'N/A', // Level TAT Compliance
'N/A', // Level Elapsed Hours
'N/A', // Level Remaining Hours
'N/A', // Level TAT % Used
];
csvRows.push(row.join(','));
}
}
downloadCSV(csvRows, `lifecycle-report-end-to-end-${new Date().toISOString().split('T')[0]}.csv`);
} }
/** /**

View File

@ -9,6 +9,7 @@ import { ArrowRight, User, TrendingUp, Clock, FileText } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { MyRequest } from '../types/myRequests.types'; import { MyRequest } from '../types/myRequests.types';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface RequestCardProps { interface RequestCardProps {
request: MyRequest; request: MyRequest;
@ -81,7 +82,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
</span> </span>
<span className="truncate" data-testid="submitted-date"> <span className="truncate" data-testid="submitted-date">
<span className="font-medium">Submitted:</span>{' '} <span className="font-medium">Submitted:</span>{' '}
{request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} {formatDateDDMMYYYY(request.submittedDate)}
</span> </span>
</div> </div>
</div> </div>
@ -109,7 +110,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<div className="flex items-center gap-1.5 text-xs text-gray-500"> <div className="flex items-center gap-1.5 text-xs text-gray-500">
<Clock className="w-3.5 h-3.5 flex-shrink-0" /> <Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span data-testid="submitted-timestamp"> <span data-testid="submitted-timestamp">
Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} Submitted: {formatDateDDMMYYYY(request.submittedDate)}
</span> </span>
</div> </div>
</div> </div>

View File

@ -47,8 +47,6 @@ export function Notifications({ onNavigate }: NotificationsProps) {
setNotifications(notifs); setNotifications(notifs);
setTotalCount(total); setTotalCount(total);
setTotalPages(Math.ceil(total / ITEMS_PER_PAGE)); setTotalPages(Math.ceil(total / ITEMS_PER_PAGE));
console.log(`[Notifications] Loaded page ${page}, ${notifs.length} notifications, ${total} total`);
} catch (error) { } catch (error) {
console.error('[Notifications] Failed to fetch:', error); console.error('[Notifications] Failed to fetch:', error);
} finally { } finally {

View File

@ -1,4 +1,5 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback, useRef } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -6,9 +7,9 @@ import { Input } from '@/components/ui/input';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, Settings2, X, CheckCircle, XCircle } from 'lucide-react'; import { Calendar, Clock, Filter, Search, FileText, AlertCircle, ArrowRight, SortAsc, SortDesc, Flame, Target, RefreshCw, X, CheckCircle, XCircle } from 'lucide-react';
import workflowApi from '@/services/workflowApi'; import workflowApi from '@/services/workflowApi';
import { formatDateShort } from '@/utils/dateFormatter'; import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface Request { interface Request {
id: string; id: string;
title: string; title: string;
@ -98,12 +99,18 @@ const getStatusConfig = (status: string) => {
// getSLAUrgency removed - now using SLATracker component for real-time SLA display // getSLAUrgency removed - now using SLATracker component for real-time SLA display
export function OpenRequests({ onViewRequest }: OpenRequestsProps) { export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
const [searchTerm, setSearchTerm] = useState(''); const [searchParams] = useSearchParams();
const [priorityFilter, setPriorityFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('all'); // Initialize filters from URL params
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>('created'); const [searchTerm, setSearchTerm] = useState(searchParams.get('search') || '');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all');
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false); const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'sla'>(
(searchParams.get('sortBy') as 'created' | 'due' | 'priority' | 'sla') || 'created'
);
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>(
(searchParams.get('sortOrder') as 'asc' | 'desc') || 'desc'
);
const [items, setItems] = useState<Request[]>([]); const [items, setItems] = useState<Request[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
@ -140,15 +147,12 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
sortBy: filters?.sortBy, sortBy: filters?.sortBy,
sortOrder: filters?.sortOrder sortOrder: filters?.sortOrder
}); });
console.log('[OpenRequests] API Response:', result); // Debug log
// Extract data - workflowApi now returns { data: [], pagination: {} } // 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
: []; : [];
console.log('[OpenRequests] Parsed data count:', data.length); // Debug log
// Set pagination data // Set pagination data
const pagination = (result as any)?.pagination; const pagination = (result as any)?.pagination;
if (pagination) { if (pagination) {
@ -231,23 +235,13 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
return pages; return pages;
}; };
// Initial fetch on mount // Track if this is the initial mount
useEffect(() => { const isInitialMount = useRef(true);
fetchRequests(1, {
search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined,
sortBy,
sortOrder
});
}, [fetchRequests]);
// Fetch when filters or sorting change (with debouncing for search) // Initial fetch on mount with URL params
useEffect(() => { useEffect(() => {
// Debounce search: wait 500ms after user stops typing if (isInitialMount.current) {
const timeoutId = setTimeout(() => { isInitialMount.current = false;
if (items.length > 0 || loading) { // Only refetch if we've already loaded data once
setCurrentPage(1); // Reset to page 1 when filters change
fetchRequests(1, { fetchRequests(1, {
search: searchTerm || undefined, search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined, status: statusFilter !== 'all' ? statusFilter : undefined,
@ -256,6 +250,24 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
sortOrder sortOrder
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run on mount to use URL params
// Fetch when filters or sorting change (with debouncing for search)
useEffect(() => {
// Skip initial mount to avoid double fetch
if (isInitialMount.current) return;
// Debounce search: wait 500ms after user stops typing
const timeoutId = setTimeout(() => {
setCurrentPage(1); // Reset to page 1 when filters change
fetchRequests(1, {
search: searchTerm || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
priority: priorityFilter !== 'all' ? priorityFilter : undefined,
sortBy,
sortOrder
});
}, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns }, searchTerm ? 500 : 0); // Debounce only for search, instant for dropdowns
return () => clearTimeout(timeoutId); return () => clearTimeout(timeoutId);
@ -330,7 +342,6 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<div className="flex items-center gap-1 sm:gap-2">
{activeFiltersCount > 0 && ( {activeFiltersCount > 0 && (
<Button <Button
variant="ghost" variant="ghost"
@ -342,16 +353,6 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<span className="text-xs sm:text-sm">Clear</span> <span className="text-xs sm:text-sm">Clear</span>
</Button> </Button>
)} )}
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="gap-1 sm:gap-2 h-8 sm:h-9 px-2 sm:px-3"
>
<Settings2 className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="text-xs sm:text-sm hidden md:inline">{showAdvancedFilters ? 'Basic' : 'Advanced'}</span>
</Button>
</div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6"> <CardContent className="space-y-3 sm:space-y-4 px-3 sm:px-6">
@ -545,7 +546,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" /> <Calendar className="w-3.5 h-3.5" />
<span>Created: {request.createdAt !== '—' ? formatDateShort(request.createdAt) : '—'}</span> <span>Created: {request.createdAt !== '—' ? formatDateDDMMYYYY(request.createdAt) : '—'}</span>
</div> </div>
</div> </div>
</div> </div>
@ -621,7 +622,7 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
variant={pageNum === currentPage ? "default" : "outline"} variant={pageNum === currentPage ? "default" : "outline"}
size="sm" size="sm"
onClick={() => handlePageChange(pageNum)} onClick={() => handlePageChange(pageNum)}
className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-blue-600 text-white hover:bg-blue-700' : ''}`} className={`h-8 w-8 p-0 ${pageNum === currentPage ? 'bg-re-green text-white hover:bg-re-green/90' : ''}`}
> >
{pageNum} {pageNum}
</Button> </Button>

View File

@ -165,7 +165,6 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab'); const tabParam = urlParams.get('tab');
if (tabParam) { if (tabParam) {
console.log('[RequestDetail] Auto-switching to tab:', tabParam);
setActiveTab(tabParam); setActiveTab(tabParam);
} }
}, [requestIdentifier]); }, [requestIdentifier]);
@ -227,47 +226,47 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
{/* Tabs */} {/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="request-detail-tabs"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full" data-testid="request-detail-tabs">
<div className="overflow-x-auto -mx-4 px-4 sm:mx-0 sm:px-0 mb-4 sm:mb-6"> <div className="mb-4 sm:mb-6">
<TabsList className="inline-flex h-10 sm:h-11 items-center justify-start rounded-lg bg-gray-100 p-1 text-gray-500 min-w-max sm:w-full"> <TabsList className="grid grid-cols-3 sm:grid-cols-5 lg:flex lg:flex-row h-auto bg-gray-100 p-1.5 sm:p-1 rounded-lg gap-1.5 sm:gap-1">
<TabsTrigger <TabsTrigger
value="overview" value="overview"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm gap-1 sm:gap-1.5 shrink-0" className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-overview" data-testid="tab-overview"
> >
<ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <ClipboardList className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span>Overview</span> <span className="truncate">Overview</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="workflow" value="workflow"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm gap-1 sm:gap-1.5 shrink-0" className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-workflow" data-testid="tab-workflow"
> >
<TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <TrendingUp className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span>Workflow</span> <span className="truncate">Workflow</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="documents" value="documents"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm gap-1 sm:gap-1.5 shrink-0" className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900"
data-testid="tab-documents" data-testid="tab-documents"
> >
<FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <FileText className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span>Docs</span> <span className="truncate">Docs</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="activity" value="activity"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm gap-1 sm:gap-1.5 shrink-0" className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 col-span-1 sm:col-span-1"
data-testid="tab-activity" data-testid="tab-activity"
> >
<Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <Activity className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span>Activity</span> <span className="truncate">Activity</span>
</TabsTrigger> </TabsTrigger>
<TabsTrigger <TabsTrigger
value="worknotes" value="worknotes"
className="inline-flex items-center justify-center whitespace-nowrap rounded-md px-2 sm:px-3 py-1.5 text-xs sm:text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm gap-1 sm:gap-1.5 relative shrink-0" className="flex items-center justify-center gap-1 sm:gap-1.5 rounded-md px-2 sm:px-3 py-2.5 sm:py-1.5 text-xs sm:text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm text-gray-600 data-[state=active]:text-gray-900 relative col-span-2 sm:col-span-1"
data-testid="tab-worknotes" data-testid="tab-worknotes"
> >
<MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4" /> <MessageSquare className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span>Work Notes</span> <span className="truncate">Work Notes</span>
{unreadWorkNotes > 0 && ( {unreadWorkNotes > 0 && (
<Badge <Badge
className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0" className="absolute -top-1 -right-1 h-5 w-5 rounded-full bg-red-500 text-white text-[10px] flex items-center justify-center p-0"
@ -339,6 +338,7 @@ function RequestDetailInner({ requestId: propRequestId, onBack, dynamicRequests
mergedMessages={mergedMessages} mergedMessages={mergedMessages}
setWorkNoteAttachments={setWorkNoteAttachments} setWorkNoteAttachments={setWorkNoteAttachments}
isInitiator={isInitiator} isInitiator={isInitiator}
isSpectator={isSpectator}
currentLevels={currentLevels} currentLevels={currentLevels}
onAddApprover={handleAddApprover} onAddApprover={handleAddApprover}
/> />

View File

@ -30,7 +30,8 @@ export function QuickActionsSidebar({
}: QuickActionsSidebarProps) { }: QuickActionsSidebarProps) {
return ( return (
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
{/* Quick Actions Card */} {/* Quick Actions Card - Hide entire card for spectators */}
{!isSpectator && (
<Card data-testid="quick-actions-card"> <Card data-testid="quick-actions-card">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm sm:text-base">Quick Actions</CardTitle> <CardTitle className="text-sm sm:text-base">Quick Actions</CardTitle>
@ -50,7 +51,7 @@ export function QuickActionsSidebar({
)} )}
{/* Add Spectator */} {/* Add Spectator */}
{!isSpectator && request.status !== 'closed' && ( {request.status !== 'closed' && (
<Button <Button
variant="outline" variant="outline"
className="w-full justify-start gap-2 bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:text-gray-900 h-9 sm:h-10 text-xs sm:text-sm" className="w-full justify-start gap-2 bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:text-gray-900 h-9 sm:h-10 text-xs sm:text-sm"
@ -64,7 +65,7 @@ export function QuickActionsSidebar({
{/* Approve/Reject Buttons */} {/* Approve/Reject Buttons */}
<div className="pt-3 sm:pt-4 space-y-2"> <div className="pt-3 sm:pt-4 space-y-2">
{!isSpectator && currentApprovalLevel && ( {currentApprovalLevel && (
<> <>
<Button <Button
className="w-full bg-green-600 hover:bg-green-700 text-white h-9 sm:h-10 text-xs sm:text-sm" className="w-full bg-green-600 hover:bg-green-700 text-white h-9 sm:h-10 text-xs sm:text-sm"
@ -88,6 +89,7 @@ export function QuickActionsSidebar({
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
)}
{/* Spectators Card */} {/* Spectators Card */}
{request.spectators && request.spectators.length > 0 && ( {request.spectators && request.spectators.length > 0 && (

View File

@ -24,7 +24,7 @@ interface OverviewTabProps {
export function OverviewTab({ export function OverviewTab({
request, request,
isInitiator, isInitiator: _isInitiator,
needsClosure, needsClosure,
conclusionRemark, conclusionRemark,
setConclusionRemark, setConclusionRemark,

View File

@ -10,6 +10,7 @@ interface WorkNotesTabProps {
mergedMessages: any[]; mergedMessages: any[];
setWorkNoteAttachments: (attachments: any[]) => void; setWorkNoteAttachments: (attachments: any[]) => void;
isInitiator: boolean; isInitiator: boolean;
isSpectator: boolean;
currentLevels: any[]; currentLevels: any[];
onAddApprover: (email: string, tatHours: number, level: number) => Promise<void>; onAddApprover: (email: string, tatHours: number, level: number) => Promise<void>;
} }
@ -20,6 +21,7 @@ export function WorkNotesTab({
mergedMessages, mergedMessages,
setWorkNoteAttachments, setWorkNoteAttachments,
isInitiator, isInitiator,
isSpectator,
currentLevels, currentLevels,
onAddApprover, onAddApprover,
}: WorkNotesTabProps) { }: WorkNotesTabProps) {
@ -32,6 +34,7 @@ export function WorkNotesTab({
messages={mergedMessages} messages={mergedMessages}
onAttachmentsExtracted={setWorkNoteAttachments} onAttachmentsExtracted={setWorkNoteAttachments}
isInitiator={isInitiator} isInitiator={isInitiator}
isSpectator={isSpectator}
currentLevels={currentLevels} currentLevels={currentLevels}
onAddApprover={onAddApprover} onAddApprover={onAddApprover}
/> />

View File

@ -30,10 +30,51 @@ export function WorkflowTab({ request, user, isInitiator, onSkipApprover, onRefr
</CardDescription> </CardDescription>
</div> </div>
{request.totalSteps && (() => { {request.totalSteps && (() => {
const completedCount = request.approvalFlow?.filter((s: any) => s.status === 'approved').length || 0; const totalSteps = request.totalSteps || 1;
// Use raw value if available (for completion detection), otherwise use clamped currentStep
// Backend sets currentLevel = levelNumber + 1 when final approver approves (closure step)
const rawCurrentStep = request.currentStepRaw !== undefined ? request.currentStepRaw : (request.currentStep || 1);
// Count completed steps (approved or rejected)
const completedCount = request.approvalFlow?.filter((s: any) => {
const status = (s.status || '').toLowerCase();
return status === 'approved' || status === 'rejected';
}).length || 0;
const allStepsCompleted = completedCount >= totalSteps;
const requestStatus = (request.status || '').toLowerCase();
const isRequestCompleted = requestStatus === 'approved' || requestStatus === 'rejected' || requestStatus === 'closed';
// Backend sets currentLevel = levelNumber + 1 when final approver approves (closure step)
// So if currentStep > totalSteps, it means the workflow is in closure phase
const isInClosureStep = rawCurrentStep > totalSteps;
// Ensure currentStep is valid: between 1 and totalSteps (for display purposes)
const currentStep = Math.min(Math.max(1, rawCurrentStep), totalSteps);
// Show "Closure Step" when currentStep > totalSteps (workflow is in closure phase)
if (isInClosureStep) {
return (
<Badge variant="outline" className="font-medium text-xs sm:text-sm shrink-0 bg-blue-50 text-blue-700 border-blue-200">
Closure Step - {completedCount} of {totalSteps} steps completed
</Badge>
);
}
// Show "Completed" when:
// 1. Request status is completed (approved/rejected/closed)
// 2. All steps are approved/rejected (completedCount >= totalSteps)
if (isRequestCompleted || allStepsCompleted) {
return (
<Badge variant="outline" className="font-medium text-xs sm:text-sm shrink-0 bg-green-50 text-green-700 border-green-200">
Completed - {completedCount} of {totalSteps} steps
</Badge>
);
}
return ( return (
<Badge variant="outline" className="font-medium text-xs sm:text-sm shrink-0"> <Badge variant="outline" className="font-medium text-xs sm:text-sm shrink-0">
Step {request.currentStep} of {request.totalSteps} - {completedCount} completed Step {currentStep} of {totalSteps} - {completedCount} completed
</Badge> </Badge>
); );
})()} })()}

View File

@ -8,6 +8,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { User, ArrowRight, TrendingUp, Clock } from 'lucide-react'; import { User, ArrowRight, TrendingUp, Clock } from 'lucide-react';
import { getPriorityConfig, getStatusConfig } from '../utils/configMappers'; import { getPriorityConfig, getStatusConfig } from '../utils/configMappers';
import type { ConvertedRequest } from '../types/requests.types'; import type { ConvertedRequest } from '../types/requests.types';
import { formatDateDDMMYYYY } from '@/utils/dateFormatter';
interface RequestCardProps { interface RequestCardProps {
request: ConvertedRequest; request: ConvertedRequest;
@ -81,7 +82,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<span className="font-medium">ID:</span> {request.displayId || request.id} <span className="font-medium">ID:</span> {request.displayId || request.id}
</span> </span>
<span className="truncate" data-testid="submitted-date"> <span className="truncate" data-testid="submitted-date">
<span className="font-medium">Submitted:</span> {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} <span className="font-medium">Submitted:</span> {formatDateDDMMYYYY(request.submittedDate)}
</span> </span>
</div> </div>
</div> </div>
@ -107,7 +108,7 @@ export function RequestCard({ request, index, onViewRequest }: RequestCardProps)
<div className="flex items-center gap-1.5 text-xs text-gray-500"> <div className="flex items-center gap-1.5 text-xs text-gray-500">
<Clock className="w-3.5 h-3.5 flex-shrink-0" /> <Clock className="w-3.5 h-3.5 flex-shrink-0" />
<span data-testid="submitted-timestamp"> <span data-testid="submitted-timestamp">
Submitted: {request.submittedDate ? new Date(request.submittedDate).toLocaleDateString() : 'N/A'} Submitted: {formatDateDDMMYYYY(request.submittedDate)}
</span> </span>
</div> </div>
</div> </div>

View File

@ -39,7 +39,17 @@ export interface Holiday {
} }
/** /**
* Get all admin configurations * Get public configurations (accessible to all authenticated users)
* Returns non-sensitive configurations like DOCUMENT_POLICY, WORKFLOW_SHARING, TAT_SETTINGS
*/
export const getPublicConfigurations = async (category?: string): Promise<AdminConfiguration[]> => {
const params = category ? { category } : {};
const response = await apiClient.get('/users/configurations', { params });
return response.data.data;
};
/**
* Get all admin configurations (admin only)
*/ */
export const getAllConfigurations = async (category?: string): Promise<AdminConfiguration[]> => { export const getAllConfigurations = async (category?: string): Promise<AdminConfiguration[]> => {
const params = category ? { category } : {}; const params = category ? { category } : {};

View File

@ -100,12 +100,6 @@ export async function exchangeCodeForTokens(
code: string, code: string,
redirectUri: string redirectUri: string
): Promise<TokenExchangeResponse> { ): Promise<TokenExchangeResponse> {
console.log('🔄 Exchange Code for Tokens:', {
code: code ? `${code.substring(0, 10)}...` : 'MISSING',
redirectUri,
endpoint: `${API_BASE_URL}/auth/token-exchange`,
});
try { try {
const response = await apiClient.post<TokenExchangeResponse>( const response = await apiClient.post<TokenExchangeResponse>(
'/auth/token-exchange', '/auth/token-exchange',
@ -122,21 +116,6 @@ export async function exchangeCodeForTokens(
} }
); );
console.log('✅ Token exchange successful', {
status: response.status,
statusText: response.statusText,
headers: response.headers,
contentType: response.headers['content-type'],
hasData: !!response.data,
dataType: typeof response.data,
dataIsArray: Array.isArray(response.data),
dataPreview: Array.isArray(response.data)
? `Array[${response.data.length}]`
: typeof response.data === 'object'
? JSON.stringify(response.data).substring(0, 100)
: String(response.data).substring(0, 100),
});
// Check if response is an array (buffer issue) // Check if response is an array (buffer issue)
if (Array.isArray(response.data)) { if (Array.isArray(response.data)) {
console.error('❌ Response is an array (buffer issue):', { console.error('❌ Response is an array (buffer issue):', {
@ -158,9 +137,7 @@ export async function exchangeCodeForTokens(
// Store id_token if available (needed for proper Okta logout) // Store id_token if available (needed for proper Okta logout)
if (result.idToken) { if (result.idToken) {
TokenManager.setIdToken(result.idToken); TokenManager.setIdToken(result.idToken);
console.log('✅ ID token stored for logout');
} }
console.log('✅ Tokens stored successfully');
} else { } else {
console.warn('⚠️ Tokens missing in response', { result }); console.warn('⚠️ Tokens missing in response', { result });
} }
@ -219,13 +196,10 @@ export async function getCurrentUser() {
*/ */
export async function logout(): Promise<void> { export async function logout(): Promise<void> {
try { try {
console.log('📡 Calling backend logout endpoint to clear httpOnly cookies...');
// Use withCredentials to ensure cookies are sent // Use withCredentials to ensure cookies are sent
const response = await apiClient.post('/auth/logout', {}, { await apiClient.post('/auth/logout', {}, {
withCredentials: true, // Ensure cookies are sent with request withCredentials: true, // Ensure cookies are sent with request
}); });
console.log('📡 Backend logout response:', response.status, response.statusText);
console.log('📡 Response headers (check Set-Cookie):', response.headers);
} catch (error: any) { } catch (error: any) {
console.error('📡 Logout API error:', error); console.error('📡 Logout API error:', error);
console.error('📡 Error details:', { console.error('📡 Error details:', {

View File

@ -141,8 +141,6 @@ class ConfigService {
const response = await apiClient.get('/config'); const response = await apiClient.get('/config');
const serverConfig = response.data?.data || response.data; const serverConfig = response.data?.data || response.data;
console.log('[ConfigService] ✅ Loaded system configuration from server:', serverConfig);
// Merge with defaults (in case server doesn't return all fields) // Merge with defaults (in case server doesn't return all fields)
return { return {
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,

View File

@ -693,6 +693,16 @@
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgb(203 213 225) transparent; scrollbar-color: rgb(203 213 225) transparent;
} }
/* Hide scrollbar utility */
.scrollbar-none {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-none::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
} }
html { html {

View File

@ -102,3 +102,44 @@ export function formatTimeOnly(dateString: string | Date | null | undefined): st
} }
} }
/**
* Format date with day first, then month (mmm), then year (e.g., "15 Nov 2025, 3:47:28 PM")
* Format: dd mmm yyyy (day first, then month name, then year)
* @param dateString - ISO date string or Date object
* @param includeTime - Whether to include time in the format
* @returns Formatted date string with day first
*/
export function formatDateDDMMYYYY(dateString: string | Date | null | undefined, includeTime: boolean = false): string {
if (!dateString) return 'N/A';
try {
const date = typeof dateString === 'string' ? new Date(dateString) : dateString;
if (isNaN(date.getTime())) {
return 'Invalid Date';
}
// Always put day first, then month (mmm format), then year
const day = String(date.getDate()).padStart(2, '0');
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const month = monthNames[date.getMonth()]; // Use month name (mmm)
const year = date.getFullYear();
if (includeTime) {
const hours24 = date.getHours();
const hours12 = hours24 % 12 || 12;
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const ampm = hours24 >= 12 ? 'PM' : 'AM';
// Format: dd mmm yyyy, hh:mm:ss AM/PM (day first, then month, then year)
return `${day} ${month} ${year}, ${hours12}:${minutes}:${seconds} ${ampm}`;
}
// Format: dd mmm yyyy (day first, then month, then year)
return `${day} ${month} ${year}`;
} catch (error) {
console.error('Error formatting date:', error);
return String(dateString);
}
}

View File

@ -24,7 +24,6 @@ async function ensureConfigLoaded() {
WORK_START_DAY = config.workingHours.START_DAY; WORK_START_DAY = config.workingHours.START_DAY;
WORK_END_DAY = config.workingHours.END_DAY; WORK_END_DAY = config.workingHours.END_DAY;
configLoaded = true; configLoaded = true;
console.log('[SLA Tracker] ✅ Loaded working hours from backend:', { WORK_START_HOUR, WORK_END_HOUR });
} catch (error) { } catch (error) {
console.warn('[SLA Tracker] ⚠️ Using default working hours (9 AM - 6 PM)'); console.warn('[SLA Tracker] ⚠️ Using default working hours (9 AM - 6 PM)');
} }
@ -223,23 +222,51 @@ export function getSLAStatus(startDate: string | Date, deadline: string | Date,
} }
/** /**
* Format decimal hours to hours and minutes format (e.g., 2.6 -> "2h 23m") * Format decimal hours to hours and minutes format
* Simple format without considering working days * Considers 8 working hours = 1 day (consistent with TAT calculations and backend formatTime)
* Uses "hour" (singular) for 1 hour, "hours" (plural) for multiple hours
* Matches backend formatTime format from tatTimeUtils.ts for consistency
* Examples: 1 -> "1 hour", 8 -> "1 day", 9 -> "1 day 1 hour", 16 -> "2 days"
*/ */
export function formatHoursMinutes(hours: number | null | undefined): string { export function formatHoursMinutes(hours: number | null | undefined): string {
if (hours === null || hours === undefined || hours < 0) return '0h'; if (hours === null || hours === undefined || hours < 0) return '0 hours';
if (hours === 0) return '0h'; if (hours === 0) return '0 hours';
const totalMinutes = Math.round(hours * 60); const WORKING_HOURS_PER_DAY = 8;
const h = Math.floor(totalMinutes / 60);
const m = totalMinutes % 60;
if (h > 0 && m > 0) { // If less than 1 hour, show minutes only
return `${h}h ${m}m`; if (hours < 1) {
} else if (h > 0) { const m = Math.round(hours * 60);
return `${h}h`; return m > 0 ? `${m}m` : '0 hours';
}
// Calculate days and remaining hours (8 hours = 1 day)
// Match backend formatTime logic from Re_Backend/src/utils/tatTimeUtils.ts
const days = Math.floor(hours / WORKING_HOURS_PER_DAY);
const remainingHrs = Math.floor(hours % WORKING_HOURS_PER_DAY);
const minutes = Math.round((hours % 1) * 60);
// If we have days, format with days (matching backend format)
if (days > 0) {
const dayLabel = days === 1 ? 'day' : 'days';
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
const minuteLabel = minutes === 1 ? 'min' : 'm';
if (minutes > 0) {
return `${days} ${dayLabel} ${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
} else { } else {
return `${m}m`; return `${days} ${dayLabel} ${remainingHrs} ${hourLabel}`;
}
}
// No days, just hours and minutes
const hourLabel = remainingHrs === 1 ? 'hour' : 'hours';
const minuteLabel = minutes === 1 ? 'min' : 'm';
if (minutes > 0) {
return `${remainingHrs} ${hourLabel} ${minutes}${minuteLabel}`;
} else {
return `${remainingHrs} ${hourLabel}`;
} }
} }

View File

@ -29,8 +29,6 @@ export function getSocket(baseUrl?: string): Socket {
const url = baseUrl || getSocketBaseUrl(); const url = baseUrl || getSocketBaseUrl();
if (socket) return socket; if (socket) return socket;
console.log('[Socket] Connecting to:', url);
socket = io(url, { socket = io(url, {
withCredentials: true, withCredentials: true,
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
@ -41,15 +39,15 @@ export function getSocket(baseUrl?: string): Socket {
}); });
socket.on('connect', () => { socket.on('connect', () => {
console.log('[Socket] Connected successfully:', socket?.id); // Socket connected
}); });
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
console.error('[Socket] Connection error:', error.message); console.error('[Socket] Connection error:', error.message);
}); });
socket.on('disconnect', (reason) => { socket.on('disconnect', (_reason) => {
console.log('[Socket] Disconnected:', reason); // Socket disconnected
}); });
return socket; return socket;
@ -69,7 +67,6 @@ export function leaveRequestRoom(socket: Socket, requestId: string) {
export function joinUserRoom(socket: Socket, userId: string) { export function joinUserRoom(socket: Socket, userId: string) {
socket.emit('join:user', { userId }); socket.emit('join:user', { userId });
console.log('[Socket] Joined personal notification room for user:', userId);
} }

View File

@ -172,8 +172,6 @@ export class TokenManager {
* IMPORTANT: This also sets a flag to prevent auto-authentication * IMPORTANT: This also sets a flag to prevent auto-authentication
*/ */
static clearAll(): void { static clearAll(): void {
console.log('TokenManager.clearAll() - Starting cleanup...');
// CRITICAL: Set logout flag in sessionStorage FIRST (before clearing) // CRITICAL: Set logout flag in sessionStorage FIRST (before clearing)
// This flag survives the redirect and prevents auto-authentication // This flag survives the redirect and prevents auto-authentication
try { try {
@ -212,7 +210,6 @@ export class TokenManager {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
sessionStorage.removeItem(key); sessionStorage.removeItem(key);
console.log(`Removed ${key} from storage`);
} catch (e) { } catch (e) {
console.warn(`Error removing ${key}:`, e); console.warn(`Error removing ${key}:`, e);
} }
@ -233,7 +230,6 @@ export class TokenManager {
allLocalStorageKeys.forEach(key => { allLocalStorageKeys.forEach(key => {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
console.log(`Removed localStorage key: ${key}`);
} catch (e) { } catch (e) {
console.warn(`Error removing localStorage key ${key}:`, e); console.warn(`Error removing localStorage key ${key}:`, e);
} }
@ -241,7 +237,6 @@ export class TokenManager {
// Final clear as backup // Final clear as backup
localStorage.clear(); localStorage.clear();
console.log('localStorage.clear() called');
} catch (e) { } catch (e) {
console.error('Error clearing localStorage:', e); console.error('Error clearing localStorage:', e);
} }
@ -261,7 +256,6 @@ export class TokenManager {
allSessionStorageKeys.forEach(key => { allSessionStorageKeys.forEach(key => {
try { try {
sessionStorage.removeItem(key); sessionStorage.removeItem(key);
console.log(`Removed sessionStorage key: ${key}`);
} catch (e) { } catch (e) {
console.warn(`Error removing sessionStorage key ${key}:`, e); console.warn(`Error removing sessionStorage key ${key}:`, e);
} }
@ -269,7 +263,6 @@ export class TokenManager {
// Final clear as backup // Final clear as backup
sessionStorage.clear(); sessionStorage.clear();
console.log('sessionStorage.clear() called');
} catch (e) { } catch (e) {
console.error('Error clearing sessionStorage:', e); console.error('Error clearing sessionStorage:', e);
} }
@ -328,23 +321,12 @@ export class TokenManager {
}); });
}); });
// Log remaining cookies (httpOnly cookies will still show here as they can't be cleared from JS) // Note: httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this
console.log('⚠️ Remaining cookies (httpOnly cookies cannot be cleared from JavaScript):', document.cookie);
console.log('⚠️ httpOnly cookies can only be cleared by backend - backend logout endpoint should handle this');
// Step 6: Verify cleanup // Step 6: Verify cleanup
console.log('TokenManager.clearAll() - Verification:');
console.log(`localStorage length: ${localStorage.length}`);
console.log(`sessionStorage length: ${sessionStorage.length}`);
console.log(`Cookies: ${document.cookie}`);
if (localStorage.length > 0 || sessionStorage.length > 0) { if (localStorage.length > 0 || sessionStorage.length > 0) {
console.warn('WARNING: Storage not fully cleared!'); console.warn('WARNING: Storage not fully cleared!');
console.log('Remaining localStorage keys:', Object.keys(localStorage));
console.log('Remaining sessionStorage keys:', Object.keys(sessionStorage));
} }
console.log('TokenManager.clearAll() - Cleanup complete');
} }
/** /**

View File

@ -193,5 +193,8 @@ export default defineConfig({
preserveSymlinks: false, preserveSymlinks: false,
}, },
}, },
preview: {
port: 3000,
},
}); });